playwright-base-test.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. // kilocode_change - new file
  2. import { test as base, type Page, _electron } from "@playwright/test"
  3. export { expect } from "@playwright/test"
  4. import * as path from "path"
  5. import * as os from "os"
  6. import * as fs from "fs"
  7. import { fileURLToPath } from "url"
  8. import { camelCase } from "change-case"
  9. import { setupConsoleLogging, cleanLogMessage } from "../helpers/console-logging"
  10. import { waitForAllExtensionActivation, closeAllTabs } from "../helpers"
  11. // ES module equivalent of __dirname
  12. const __filename = fileURLToPath(import.meta.url)
  13. const __dirname = path.dirname(__filename)
  14. export type TestOptions = {
  15. vscodeVersion: string
  16. }
  17. export type TestFixtures = TestOptions & {
  18. workbox: Page
  19. createProject: () => Promise<string>
  20. createTempDir: () => Promise<string>
  21. takeScreenshot: (name?: string) => Promise<void>
  22. }
  23. export const test = base.extend<TestFixtures>({
  24. vscodeVersion: ["stable", { option: true }],
  25. workbox: async ({ createProject, createTempDir }, use) => {
  26. // Fail early if OPENROUTER_API_KEY is not set
  27. if (!process.env.OPENROUTER_API_KEY) {
  28. throw new Error("OPENROUTER_API_KEY environment variable is required for Playwright tests")
  29. }
  30. const defaultCachePath = await createTempDir()
  31. const userDataDir = path.join(defaultCachePath, "user-data")
  32. seedUserSettings(userDataDir)
  33. // Use the pre-downloaded VS Code from global setup
  34. const vscodePath = process.env.VSCODE_EXECUTABLE_PATH
  35. if (!vscodePath) {
  36. throw new Error("VSCODE_EXECUTABLE_PATH not found. Make sure global setup ran successfully.")
  37. }
  38. const electronApp = await _electron.launch({
  39. executablePath: vscodePath,
  40. env: {
  41. ...process.env,
  42. VSCODE_SKIP_GETTING_STARTED: "1",
  43. VSCODE_DISABLE_WORKSPACE_TRUST: "1",
  44. ELECTRON_DISABLE_SECURITY_WARNINGS: "1",
  45. },
  46. args: [
  47. "--no-sandbox",
  48. "--disable-gpu-sandbox",
  49. "--disable-gpu",
  50. "--disable-dev-shm-usage",
  51. "--disable-setuid-sandbox",
  52. "--disable-renderer-backgrounding",
  53. "--disable-ipc-flooding-protection",
  54. "--disable-web-security",
  55. "--disable-updates",
  56. "--skip-welcome",
  57. "--skip-release-notes",
  58. "--skip-getting-started",
  59. "--disable-workspace-trust",
  60. "--disable-telemetry",
  61. "--disable-crash-reporter",
  62. "--enable-logging",
  63. "--log-level=0",
  64. "--disable-extensions-except=kilocode.kilo-code",
  65. "--disable-extension-recommendations",
  66. "--disable-extension-update-check",
  67. "--disable-default-apps",
  68. "--disable-background-timer-throttling",
  69. "--disable-renderer-backgrounding",
  70. "--disable-component-extensions-with-background-pages",
  71. `--extensionDevelopmentPath=${path.resolve(__dirname, "..", "..", "..", "src")}`,
  72. `--extensions-dir=${path.join(defaultCachePath, "extensions")}`,
  73. `--user-data-dir=${userDataDir}`,
  74. "--enable-proposed-api=kilocode.kilo-code",
  75. await createProject(),
  76. ],
  77. })
  78. const workbox = await electronApp.firstWindow()
  79. // Setup pass-through logs for the core process and the webview
  80. if (process.env.PLAYWRIGHT_VERBOSE_LOGS === "true") {
  81. electronApp.process().stdout?.on("data", (data) => {
  82. const output = data.toString().trim()
  83. const cleaned = cleanLogMessage(output)
  84. if (cleaned) {
  85. console.log(`📋 [VSCode] ${cleaned}`)
  86. }
  87. })
  88. electronApp.process().stderr?.on("data", (data) => {
  89. const output = data.toString().trim()
  90. const cleaned = cleanLogMessage(output)
  91. if (cleaned) {
  92. // Determine severity based on content
  93. const isError = output.toLowerCase().includes("error") || output.toLowerCase().includes("failed")
  94. const icon = isError ? "❌" : "⚠️"
  95. console.log(`${icon} [VSCode] ${cleaned}`)
  96. }
  97. })
  98. // Set up comprehensive console logging for the main workbox window
  99. setupConsoleLogging(workbox, "WORKBOX")
  100. // Set up logging for any new windows/webviews that get created
  101. electronApp.on("window", (newWindow) => {
  102. console.log(`🪟 [VSCode] New window created: ${newWindow.url()}`)
  103. setupConsoleLogging(newWindow, "WEBVIEW")
  104. })
  105. }
  106. await workbox.waitForLoadState("domcontentloaded")
  107. await workbox.waitForSelector(".monaco-workbench")
  108. console.log("✅ VS Code workbox ready for testing")
  109. await use(workbox)
  110. await electronApp.close()
  111. try {
  112. const logPath = path.join(defaultCachePath, "user-data")
  113. const logOutputPath = test.info().outputPath("vscode-logs")
  114. await fs.promises.cp(logPath, logOutputPath, { recursive: true })
  115. } catch (error) {
  116. console.warn(`Failed to copy VSCode logs: ${error.message}`)
  117. }
  118. },
  119. createProject: async ({ createTempDir }, use) => {
  120. await use(async () => {
  121. const projectPath = await createTempDir()
  122. if (fs.existsSync(projectPath)) await fs.promises.rm(projectPath, { recursive: true })
  123. console.log(`Creating test project in ${projectPath}`)
  124. await fs.promises.mkdir(projectPath)
  125. const packageJson = {
  126. name: "test-project",
  127. version: "1.0.0",
  128. }
  129. await fs.promises.writeFile(path.join(projectPath, "package.json"), JSON.stringify(packageJson, null, 2))
  130. return projectPath
  131. })
  132. },
  133. // eslint-disable-next-line no-empty-pattern
  134. createTempDir: async ({}, use) => {
  135. const tempDirs: string[] = []
  136. let counter = 0
  137. await use(async () => {
  138. const testInfo = test.info()
  139. const fileName = testInfo.file.split("/").pop()?.replace(".test.ts", "") || "unknown"
  140. const sanitizedTestName = camelCase(testInfo.title)
  141. const dirName = `e2e-${fileName}-${sanitizedTestName}-${counter++}`
  142. const tempDirPath = path.join(os.tmpdir(), dirName)
  143. // Clean up any existing directory first
  144. try {
  145. await fs.promises.rm(tempDirPath, { recursive: true })
  146. } catch (_error) {
  147. // Directory might not exist, which is fine
  148. }
  149. // Create the directory
  150. await fs.promises.mkdir(tempDirPath, { recursive: true })
  151. // Get the real path after directory exists
  152. const tempDir = await fs.promises.realpath(tempDirPath)
  153. tempDirs.push(tempDir)
  154. return tempDir
  155. })
  156. for (const tempDir of tempDirs) {
  157. try {
  158. await fs.promises.rm(tempDir, { recursive: true })
  159. } catch (error) {
  160. console.warn(`Failed to cleanup temp dir ${tempDir}:`, error)
  161. }
  162. }
  163. },
  164. takeScreenshot: async ({ workbox: page }, use) => {
  165. await use(async (name?: string) => {
  166. await waitForAllExtensionActivation(page)
  167. await closeAllTabs(page)
  168. // Extract test suite from the test file name or use a default
  169. const testInfo = test.info()
  170. const fileName = testInfo.file.split("/").pop()?.replace(".test.ts", "") || "unknown"
  171. const testName = testInfo.title || "Unknown Test"
  172. const testSuite = camelCase(fileName)
  173. // Create a hierarchical name: TestSuite__TestName__ScreenshotName
  174. const screenshotName = name || `screenshot-${Date.now()}`
  175. const hierarchicalName = `${testSuite}__${testName}__${screenshotName}`
  176. .replace(/[^a-zA-Z0-9_-]/g, "-") // Replace special chars with dashes, keep underscores
  177. .replace(/-+/g, "-") // Replace multiple dashes with single dash
  178. .replace(/^-|-$/g, "") // Remove leading/trailing dashes
  179. const screenshotPath = test.info().outputPath(`${hierarchicalName}.png`)
  180. await page.screenshot({ path: screenshotPath, fullPage: true })
  181. console.log(`📸 Screenshot captured: ${hierarchicalName}`)
  182. })
  183. },
  184. })
  185. function seedUserSettings(userDataDir: string) {
  186. const userDir = path.join(userDataDir, "User")
  187. const settingsPath = path.join(userDir, "settings.json")
  188. fs.mkdirSync(userDir, { recursive: true })
  189. const settings = {
  190. "workbench.startupEditor": "none", // hides 'Get Started'
  191. "workbench.tips.enabled": false,
  192. "update.showReleaseNotes": false,
  193. "extensions.ignoreRecommendations": true,
  194. "telemetry.telemetryLevel": "off",
  195. }
  196. fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2))
  197. }