run-docker-playwright.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. #!/usr/bin/env node
  2. /**
  3. * Streamlined Docker orchestration for Playwright testing
  4. * Builds entire app outside Docker, installs only Playwright deps inside
  5. */
  6. import { spawn } from "child_process"
  7. import fs from "fs-extra"
  8. import path from "path"
  9. import { fileURLToPath } from "url"
  10. import pkg from "signale"
  11. const { Signale } = pkg
  12. // --- Configuration
  13. const WORKSPACE_ROOT = process.env.WORKSPACE_ROOT || path.resolve(__dirname, "../..")
  14. const IMAGE_NAME = "playwright-ci:latest"
  15. const log = new Signale({
  16. types: {
  17. status: { badge: "🔧", color: "blue", label: "status" },
  18. success: { badge: "✅", color: "green", label: "success" },
  19. error: { badge: "❌", color: "red", label: "error" },
  20. warning: { badge: "⚠️", color: "yellow", label: "warning" },
  21. },
  22. })
  23. // ---
  24. const __filename = fileURLToPath(import.meta.url)
  25. const __dirname = path.dirname(__filename)
  26. const activeProcesses = new Set()
  27. // Helper function to run commands
  28. function runCommand(command, args, options = {}) {
  29. return new Promise((resolve, reject) => {
  30. const child = spawn(command, args, {
  31. stdio: options.stdio || "inherit",
  32. cwd: options.cwd,
  33. env: { ...process.env, ...options.env },
  34. })
  35. activeProcesses.add(child)
  36. let stdout = ""
  37. let stderr = ""
  38. if (child.stdout) {
  39. child.stdout.on("data", (data) => {
  40. stdout += data.toString()
  41. })
  42. }
  43. if (child.stderr) {
  44. child.stderr.on("data", (data) => {
  45. stderr += data.toString()
  46. })
  47. }
  48. child.on("close", (code) => {
  49. activeProcesses.delete(child)
  50. if (code === 0) {
  51. resolve({ stdout: stdout.trim(), stderr: stderr.trim() })
  52. } else {
  53. reject(new Error(`Command failed with exit code ${code}: ${stderr || stdout}`))
  54. }
  55. })
  56. child.on("error", (error) => {
  57. activeProcesses.delete(child)
  58. reject(error)
  59. })
  60. })
  61. }
  62. // Function to kill all active child processes
  63. function killAllChildProcesses() {
  64. if (activeProcesses.size > 0) {
  65. console.log(`\n🛑 Terminating ${activeProcesses.size} active child process(es)...`)
  66. for (const child of activeProcesses) {
  67. try {
  68. child.kill("SIGTERM")
  69. setTimeout(() => {
  70. if (!child.killed) {
  71. child.kill("SIGKILL")
  72. }
  73. }, 5000)
  74. } catch (_error) {}
  75. }
  76. activeProcesses.clear()
  77. }
  78. }
  79. // Help text
  80. function showHelp() {
  81. console.log(`
  82. 🚀 Streamlined Docker Playwright Runner
  83. Usage:
  84. node run-docker-playwright.js [playwright-args...]
  85. Examples:
  86. node run-docker-playwright.js # Run all tests
  87. node run-docker-playwright.js tests/sanity.test.ts # Run specific test
  88. node run-docker-playwright.js --grep "login" # Run tests matching pattern
  89. `)
  90. }
  91. // Check for help flag
  92. if (process.argv.includes("--help") || process.argv.includes("-h")) {
  93. showHelp()
  94. process.exit(0)
  95. }
  96. async function validateEnvironment() {
  97. log.status("Validating environment...")
  98. if (!process.env.OPENROUTER_API_KEY) {
  99. log.error("OPENROUTER_API_KEY environment variable is not set")
  100. console.log('Please set it with: export OPENROUTER_API_KEY="your-api-key-here"')
  101. process.exit(1)
  102. }
  103. try {
  104. await runCommand("docker", ["--version"], { stdio: "pipe" })
  105. } catch {
  106. log.error("Docker is not available. Please install Docker first.")
  107. process.exit(1)
  108. }
  109. log.success("Environment validation passed")
  110. }
  111. async function buildHostArtifacts() {
  112. log.status("Building host artifacts...")
  113. process.chdir(WORKSPACE_ROOT)
  114. log.status(process.cwd())
  115. log.status("Installing dependencies...")
  116. await runCommand("pnpm", ["install", "--frozen-lockfile"], { cwd: WORKSPACE_ROOT })
  117. log.status("Building everything...")
  118. await runCommand("pnpm", ["-w", "run", "build"], { cwd: WORKSPACE_ROOT })
  119. log.success("Host artifacts built successfully")
  120. }
  121. async function buildDockerImage() {
  122. log.status("Building Docker image...")
  123. const buildArgs = [
  124. "buildx",
  125. "build",
  126. "-f",
  127. path.join(__dirname, "Dockerfile.playwright-ci"),
  128. "-t",
  129. IMAGE_NAME,
  130. "--load", // Load the image into Docker daemon
  131. ]
  132. // Add cache arguments if running in CI
  133. if (process.env.CI) {
  134. buildArgs.push(
  135. "--cache-from",
  136. "type=local,src=/tmp/.buildx-cache",
  137. "--cache-to",
  138. "type=local,dest=/tmp/.buildx-cache,mode=max",
  139. )
  140. }
  141. buildArgs.push(WORKSPACE_ROOT)
  142. await runCommand("docker", buildArgs)
  143. log.success("Docker image built successfully")
  144. }
  145. async function runPlaywrightTests() {
  146. log.status("Running Playwright tests in Docker...")
  147. // Ensure and clean output directories
  148. const testResultsDir = path.join(__dirname, "test-results")
  149. const reportDir = path.join(__dirname, "playwright-report")
  150. await fs.ensureDir(testResultsDir)
  151. await fs.ensureDir(reportDir)
  152. await fs.emptyDir(testResultsDir)
  153. await fs.emptyDir(reportDir)
  154. // Ensure Docker cache directory exists
  155. const dockerCacheDir = path.join(WORKSPACE_ROOT, ".docker-cache")
  156. await fs.ensureDir(dockerCacheDir)
  157. // Docker run arguments
  158. const dockerArgs = [
  159. "run",
  160. "--rm",
  161. "--cap-add=IPC_LOCK", // Required for keyring memory operations
  162. "-v",
  163. `${WORKSPACE_ROOT}:/workspace`,
  164. "-v",
  165. `${WORKSPACE_ROOT}/node_modules:/workspace/node_modules:ro`,
  166. "-v",
  167. `${WORKSPACE_ROOT}/apps/playwright-e2e/node_modules:/workspace/apps/playwright-e2e/node_modules:ro`,
  168. "-v",
  169. `${dockerCacheDir}:/workspace/.docker-cache`,
  170. "-e",
  171. "OPENROUTER_API_KEY",
  172. "-e",
  173. "CI=true",
  174. "-e",
  175. "GNOME_KEYRING_CONTROL=1", // Enable keyring support
  176. IMAGE_NAME,
  177. ]
  178. // Add test arguments - pass through all arguments as Playwright test args
  179. const testArgs = process.argv.slice(2).filter((arg) => !["--help", "-h"].includes(arg))
  180. dockerArgs.push(...testArgs)
  181. await runCommand("docker", dockerArgs)
  182. log.success("Playwright tests completed successfully!")
  183. console.log("\n📊 Test Results:")
  184. console.log(` • Test results: ${testResultsDir}`)
  185. console.log(` • HTML report: ${reportDir}`)
  186. }
  187. // Main execution
  188. async function main() {
  189. console.log(`📁 Workspace: ${WORKSPACE_ROOT}\n`)
  190. try {
  191. await validateEnvironment()
  192. await buildHostArtifacts()
  193. await buildDockerImage()
  194. await runPlaywrightTests()
  195. console.log("\n🎉 All done!")
  196. } catch (error) {
  197. log.error(`Failed: ${error.message}`)
  198. process.exit(1)
  199. }
  200. }
  201. // Graceful shutdown
  202. process.on("SIGTERM", () => {
  203. log.warning("Received SIGTERM, shutting down gracefully")
  204. killAllChildProcesses()
  205. process.exit(0)
  206. })
  207. process.on("SIGINT", () => {
  208. log.warning("Received SIGINT (Ctrl+C), shutting down gracefully")
  209. killAllChildProcesses()
  210. process.exit(0)
  211. })
  212. // Handle uncaught exceptions to ensure cleanup
  213. process.on("uncaughtException", (error) => {
  214. log.error(`Uncaught exception: ${error.message}`)
  215. killAllChildProcesses()
  216. process.exit(1)
  217. })
  218. process.on("unhandledRejection", (reason, promise) => {
  219. log.error(`Unhandled rejection at: ${promise}, reason: ${reason}`)
  220. killAllChildProcesses()
  221. process.exit(1)
  222. })
  223. main()