diff --git a/playwright.global-setup.mjs b/playwright.global-setup.mjs index b4e14800..614bee2c 100644 --- a/playwright.global-setup.mjs +++ b/playwright.global-setup.mjs @@ -1,20 +1,17 @@ import { execFile, spawn } from "node:child_process"; import fs from "node:fs/promises"; import path from "node:path"; -import readline from "node:readline"; import { promisify } from "node:util"; const execFileAsync = promisify(execFile); const devPort = Number(process.env.PLAYWRIGHT_DEV_PORT || 5173); -const devHost = process.env.PLAYWRIGHT_DEV_HOST || "127.0.0.1"; -const baseURL = process.env.PLAYWRIGHT_BASE_URL || `http://${devHost}:${devPort}`; +const baseURL = `http://localhost:${devPort}`; const runtimeNamespace = String(process.env.PLAYWRIGHT_ARTIFACT_NAMESPACE || `port-${devPort}`) .trim() .replace(/[^a-zA-Z0-9._-]+/g, "-"); const pidFile = path.resolve(process.cwd(), "output/playwright", `dev-server-${runtimeNamespace}.json`); const posViewsDirectory = path.resolve(process.cwd(), "src/views/dashboards/departmentDashboard/modules/Pos"); const shopComponentsDirectory = path.resolve(process.cwd(), "src/components/shop"); -const serverOutputLimit = 80; async function getListeningProcessOnWindows(port) { const { stdout } = await execFileAsync("netstat", ["-ano", "-p", "tcp"], { @@ -38,59 +35,6 @@ async function getListeningProcessOnWindows(port) { }; } -async function readUnixProcessDetails(pid, fallbackCommandLine = "") { - const [rawCommandLine, currentWorkingDirectory] = await Promise.all([ - fs.readFile(`/proc/${pid}/cmdline`).catch(() => Buffer.alloc(0)), - fs.readlink(`/proc/${pid}/cwd`).catch(() => ""), - ]); - const commandLine = rawCommandLine.toString("utf8").split("\0").filter(Boolean).join(" ").trim(); - const commandContext = [commandLine || fallbackCommandLine, currentWorkingDirectory].filter(Boolean).join(" | "); - - return { - Id: pid, - CommandLine: commandContext || fallbackCommandLine, - }; -} - -function parseSsListeningProcess(stdout, port) { - const line = stdout - .split(/\r?\n/) - .find((entry) => entry.includes("LISTEN") && entry.includes(`:${port}`) && entry.includes("pid=")); - if (!line) { - return null; - } - - const pidMatch = line.match(/pid=(\d+)/); - if (!pidMatch) { - return null; - } - - const commandMatch = line.match(/users:\(\("([^"]+)"/); - return { - pid: Number(pidMatch[1]), - commandLine: commandMatch ? commandMatch[1] : "", - }; -} - -function parseNetstatListeningProcess(stdout, port) { - const line = stdout - .split(/\r?\n/) - .find((entry) => entry.includes("LISTEN") && entry.match(new RegExp(`:${port}\\s`))); - if (!line) { - return null; - } - - const pidMatch = line.match(/\s(\d+)\/([^\s]+)\s*$/); - if (!pidMatch) { - return null; - } - - return { - pid: Number(pidMatch[1]), - commandLine: pidMatch[2], - }; -} - async function getListeningProcessOnUnix(port) { try { const { stdout } = await execFileAsync("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-Fp", "-Fc"], { @@ -103,36 +47,13 @@ async function getListeningProcessOnUnix(port) { return null; } - return readUnixProcessDetails(Number(pidMatch[1]), commandMatch ? commandMatch[1] : ""); + return { + Id: Number(pidMatch[1]), + CommandLine: commandMatch ? commandMatch[1] : "", + }; } catch { - // Fall through to other tools that are commonly available on Linux runners. + return null; } - - try { - const { stdout } = await execFileAsync("ss", ["-ltnp", `sport = :${port}`], { - cwd: process.cwd(), - }); - const processMatch = parseSsListeningProcess(stdout, port); - if (processMatch) { - return readUnixProcessDetails(processMatch.pid, processMatch.commandLine); - } - } catch { - // Fall through to netstat. - } - - try { - const { stdout } = await execFileAsync("netstat", ["-ltnp"], { - cwd: process.cwd(), - }); - const processMatch = parseNetstatListeningProcess(stdout, port); - if (processMatch) { - return readUnixProcessDetails(processMatch.pid, processMatch.commandLine); - } - } catch { - // No more fallbacks available. - } - - return null; } async function getListeningProcess(port) { @@ -164,82 +85,23 @@ async function killProcessTree(pid) { } } -function captureProcessOutput(stream, lines) { - const reader = readline.createInterface({ input: stream }); - reader.on("line", (line) => { - lines.push(line); - if (lines.length > serverOutputLimit) { - lines.splice(0, lines.length - serverOutputLimit); - } - }); - return reader; -} - -function formatProcessOutputSection(label, lines) { - if (lines.length === 0) { - return ""; - } - - return `\n${label}:\n${lines.join("\n")}`; -} - -function formatServerExitError(url, exitInfo, stdoutLines, stderrLines) { - const exitLabel = exitInfo.signal ? `signal ${exitInfo.signal}` : `code ${exitInfo.code ?? 0}`; - return new Error( - `The local Playwright dev server exited before ${url} became ready (${exitLabel}).${formatProcessOutputSection( - "stdout", - stdoutLines - )}${formatProcessOutputSection("stderr", stderrLines)}` - ); -} - -async function waitForServerReady(url, serverProcess, stdoutLines, stderrLines, timeoutMs = 120_000) { +async function waitForServerReady(url, timeoutMs = 120_000) { const deadline = Date.now() + timeoutMs; - let exitInfo = null; - const handleExit = (code, signal) => { - exitInfo = { - code, - signal, - }; - }; - serverProcess.on("exit", handleExit); - - try { - while (Date.now() < deadline) { - if (exitInfo || serverProcess.exitCode !== null || serverProcess.signalCode !== null) { - throw formatServerExitError( - url, - exitInfo || { - code: serverProcess.exitCode, - signal: serverProcess.signalCode, - }, - stdoutLines, - stderrLines - ); + while (Date.now() < deadline) { + try { + const response = await fetch(url, { redirect: "manual" }); + if (response.status < 500) { + return; } - - try { - const response = await fetch(url, { redirect: "manual" }); - if (response.status < 500) { - return; - } - } catch { - // keep polling until ready - } - - await new Promise((resolve) => setTimeout(resolve, 1000)); + } catch { + // keep polling until ready } - throw new Error( - `Timed out waiting for the local Playwright dev server at ${url}.${formatProcessOutputSection( - "stdout", - stdoutLines - )}${formatProcessOutputSection("stderr", stderrLines)}` - ); - } finally { - serverProcess.off("exit", handleExit); + await new Promise((resolve) => setTimeout(resolve, 1000)); } + + throw new Error(`Timed out waiting for the local Playwright dev server at ${url}.`); } async function buildWarmupTargets() { @@ -318,40 +180,35 @@ export default async function globalSetup() { const serverProcess = process.platform === "win32" - ? spawn("cmd.exe", ["/d", "/s", "/c", `npm.cmd run dev -- --host ${devHost} --port ${devPort} --strictPort`], { + ? spawn("cmd.exe", ["/d", "/s", "/c", `npm.cmd run dev -- --host localhost --port ${devPort} --strictPort`], { cwd: process.cwd(), detached: true, env: { ...process.env, PLAYWRIGHT: "1", }, - stdio: ["ignore", "pipe", "pipe"], + stdio: "ignore", windowsHide: true, }) - : spawn("npm", ["run", "dev", "--", "--host", devHost, "--port", String(devPort), "--strictPort"], { + : spawn("npm", ["run", "dev", "--", "--host", "localhost", "--port", String(devPort), "--strictPort"], { cwd: process.cwd(), detached: true, env: { ...process.env, PLAYWRIGHT: "1", }, - stdio: ["ignore", "pipe", "pipe"], + stdio: "ignore", }); - const stdoutLines = []; - const stderrLines = []; - const stdoutReader = captureProcessOutput(serverProcess.stdout, stdoutLines); - const stderrReader = captureProcessOutput(serverProcess.stderr, stderrLines); + + serverProcess.unref(); await fs.writeFile(pidFile, JSON.stringify({ pid: serverProcess.pid, port: devPort }), "utf8"); try { - await waitForServerReady(baseURL, serverProcess, stdoutLines, stderrLines); + await waitForServerReady(baseURL); await warmUpDevServer(baseURL); } catch (error) { await killProcessTree(serverProcess.pid); throw error; - } finally { - stdoutReader.close(); - stderrReader.close(); } } diff --git a/scripts/run-playwright-full-slice.mjs b/scripts/run-playwright-full-slice.mjs index c0fd8489..485b4c4c 100644 --- a/scripts/run-playwright-full-slice.mjs +++ b/scripts/run-playwright-full-slice.mjs @@ -44,9 +44,13 @@ const ownedFilesByRole = { "admin-department-visibility.spec.ts", "admin-overview-mobile.spec.ts", "admin-overview-night-washes.spec.ts", + "admin-pos-drafts.spec.ts", "admin-pos-orders.spec.ts", "adminModuleGoals.spec.ts", "adminModulePosMobileOrderFlow.spec.ts", + "assign-draft-order-modal-create.spec.ts", + "assign-draft-order-modal-layout.spec.ts", + "change-customer.spec.ts", "change-invoice-collection.spec.ts", "economic-queue-workflow.spec.js", "pos-customer-rules.spec.js", @@ -63,8 +67,10 @@ const ownedFilesByRole = { "invoice-distribution.smoke.spec.js", "invoice-transfer-queue-history.spec.js", "invoicing-period.smoke.spec.js", + "superuser-bookings.spec.ts", "superuser-customer-complaints.spec.ts", "superuser-department-gates.spec.ts", + "superuser-drafts.spec.ts", "superuser-system-status.smoke.spec.js", "superuser-vehicles.smoke.spec.js", "workfeed-config.smoke.spec.js",