From 13528b07af5589e85610905db09dc7db92800ae4 Mon Sep 17 00:00:00 2001 From: Jeppe B <2jepp9350@gmail.com> Date: Tue, 21 Apr 2026 19:33:10 +0200 Subject: [PATCH 1/3] Update tests.yml --- .github/workflows/tests.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cfeacf05..04c4364e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,7 +16,8 @@ concurrency: jobs: format-tests: - runs-on: ubuntu-latest + # Match the labels exposed by the Coolify-managed GitHub runner. + runs-on: [self-hosted, Linux, X64, default] steps: - name: Checkout repository uses: actions/checkout@v5 @@ -38,7 +39,7 @@ jobs: build-and-unit: needs: format-tests - runs-on: ubuntu-latest + runs-on: [self-hosted, Linux, X64, default] steps: - name: Checkout repository uses: actions/checkout@v5 @@ -63,7 +64,7 @@ jobs: e2e-smoke: if: github.event_name != 'schedule' needs: build-and-unit - runs-on: ubuntu-latest + runs-on: [self-hosted, Linux, X64, default] strategy: fail-fast: false matrix: @@ -102,7 +103,7 @@ jobs: if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.ref_name == github.event.repository.default_branch needs: build-and-unit name: E2E-full-${{ matrix.role }}-${{ matrix.browser_label }}-${{ matrix.device }} - runs-on: ubuntu-latest + runs-on: [self-hosted, Linux, X64, default] strategy: fail-fast: false matrix: From 75b8dff0f8b402acb5524228bc34a99fe91ca5fc Mon Sep 17 00:00:00 2001 From: Jeppe Bundgaard Date: Tue, 21 Apr 2026 22:27:33 +0200 Subject: [PATCH 2/3] Fix Playwright dev server startup on runner --- playwright.config.ts | 3 +- playwright.global-setup.mjs | 149 ++++++++++++++++++++++++++++++------ playwright.prod.config.ts | 7 +- src/config.js | 21 ++--- 4 files changed, 142 insertions(+), 38 deletions(-) diff --git a/playwright.config.ts b/playwright.config.ts index 59523327..72b45707 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -2,7 +2,8 @@ import path from "node:path"; import { defineConfig, devices } from "@playwright/test"; const devPort = Number(process.env.PLAYWRIGHT_DEV_PORT || 5173); -const baseURL = process.env.PLAYWRIGHT_BASE_URL || `http://localhost:${devPort}`; +const devHost = process.env.PLAYWRIGHT_DEV_HOST || "127.0.0.1"; +const baseURL = process.env.PLAYWRIGHT_BASE_URL || `http://${devHost}:${devPort}`; const isCI = !!process.env.CI; const artifactNamespace = (process.env.PLAYWRIGHT_ARTIFACT_NAMESPACE || "").trim(); const artifactRoot = artifactNamespace diff --git a/playwright.global-setup.mjs b/playwright.global-setup.mjs index 614bee2c..d28995b8 100644 --- a/playwright.global-setup.mjs +++ b/playwright.global-setup.mjs @@ -1,17 +1,20 @@ 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 baseURL = `http://localhost:${devPort}`; +const devHost = process.env.PLAYWRIGHT_DEV_HOST || "127.0.0.1"; +const baseURL = process.env.PLAYWRIGHT_BASE_URL || `http://${devHost}:${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"], { @@ -52,8 +55,46 @@ async function getListeningProcessOnUnix(port) { CommandLine: commandMatch ? commandMatch[1] : "", }; } catch { - return null; + // Fall through to other tools that are commonly available on Linux runners. } + + try { + const { stdout } = await execFileAsync("ss", ["-ltnp", `sport = :${port}`], { + cwd: process.cwd(), + }); + const line = stdout + .split(/\r?\n/) + .find((entry) => entry.includes("LISTEN") && entry.includes(`:${port}`) && entry.includes("pid=")); + const pidMatch = line?.match(/pid=(\d+)/); + if (pidMatch) { + return { + Id: Number(pidMatch[1]), + CommandLine: line.match(/users:\(\("([^"]+)"/)?.[1] || "", + }; + } + } catch { + // Fall through to netstat. + } + + try { + const { stdout } = await execFileAsync("netstat", ["-ltnp"], { + cwd: process.cwd(), + }); + const line = stdout + .split(/\r?\n/) + .find((entry) => entry.includes("LISTEN") && entry.match(new RegExp(`:${port}\\s`))); + const pidMatch = line?.match(/\s(\d+)\/([^\s]+)\s*$/); + if (pidMatch) { + return { + Id: Number(pidMatch[1]), + CommandLine: pidMatch[2], + }; + } + } catch { + // No more fallbacks available. + } + + return null; } async function getListeningProcess(port) { @@ -85,23 +126,82 @@ async function killProcessTree(pid) { } } -async function waitForServerReady(url, timeoutMs = 120_000) { - const deadline = Date.now() + timeoutMs; - - while (Date.now() < deadline) { - try { - const response = await fetch(url, { redirect: "manual" }); - if (response.status < 500) { - return; - } - } catch { - // keep polling until ready +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; +} - await new Promise((resolve) => setTimeout(resolve, 1000)); +function formatProcessOutputSection(label, lines) { + if (lines.length === 0) { + return ""; } - throw new Error(`Timed out waiting for the local Playwright dev server at ${url}.`); + 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) { + 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 + ); + } + + 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)); + } + + 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); + } } async function buildWarmupTargets() { @@ -180,35 +280,40 @@ export default async function globalSetup() { const serverProcess = process.platform === "win32" - ? spawn("cmd.exe", ["/d", "/s", "/c", `npm.cmd run dev -- --host localhost --port ${devPort} --strictPort`], { + ? spawn("cmd.exe", ["/d", "/s", "/c", `npm.cmd run dev -- --host ${devHost} --port ${devPort} --strictPort`], { cwd: process.cwd(), detached: true, env: { ...process.env, PLAYWRIGHT: "1", }, - stdio: "ignore", + stdio: ["ignore", "pipe", "pipe"], windowsHide: true, }) - : spawn("npm", ["run", "dev", "--", "--host", "localhost", "--port", String(devPort), "--strictPort"], { + : spawn("npm", ["run", "dev", "--", "--host", devHost, "--port", String(devPort), "--strictPort"], { cwd: process.cwd(), detached: true, env: { ...process.env, PLAYWRIGHT: "1", }, - stdio: "ignore", + stdio: ["ignore", "pipe", "pipe"], }); - - serverProcess.unref(); + const stdoutLines = []; + const stderrLines = []; + const stdoutReader = captureProcessOutput(serverProcess.stdout, stdoutLines); + const stderrReader = captureProcessOutput(serverProcess.stderr, stderrLines); await fs.writeFile(pidFile, JSON.stringify({ pid: serverProcess.pid, port: devPort }), "utf8"); try { - await waitForServerReady(baseURL); + await waitForServerReady(baseURL, serverProcess, stdoutLines, stderrLines); await warmUpDevServer(baseURL); } catch (error) { await killProcessTree(serverProcess.pid); throw error; + } finally { + stdoutReader.close(); + stderrReader.close(); } } diff --git a/playwright.prod.config.ts b/playwright.prod.config.ts index 9c795325..cd6d136b 100644 --- a/playwright.prod.config.ts +++ b/playwright.prod.config.ts @@ -1,6 +1,6 @@ import { defineConfig, devices } from "@playwright/test"; -const baseURL = "http://localhost:4173"; +const baseURL = "http://127.0.0.1:4173"; const isCI = !!process.env.CI; process.env.PLAYWRIGHT_BASE_URL = baseURL; @@ -13,10 +13,7 @@ export default defineConfig({ forbidOnly: isCI, retries: isCI ? 2 : 0, workers: isCI ? 2 : 3, - reporter: [ - ["list"], - ["html", { open: "never", outputFolder: "output/playwright/prod/report" }], - ], + reporter: [["list"], ["html", { open: "never", outputFolder: "output/playwright/prod/report" }]], outputDir: "output/playwright/prod/test-results", use: { baseURL, diff --git a/src/config.js b/src/config.js index c2f85395..280a58de 100644 --- a/src/config.js +++ b/src/config.js @@ -2,19 +2,20 @@ export const IS_DEV = import.meta.env.VITE_IS_DEV; // Set to false in production // Development mode export const POS_STEP_1_VERSION = 2; //(IS_DEV ? 2 : 1); -export const API_URL = (IS_DEV ? 'https://api.truckwash.io:4433' : 'https://api.truckwash.io'); +export const API_URL = IS_DEV ? "https://api.truckwash.io:4433" : "https://api.truckwash.io"; // Allowed origins export const ALLOWED_ORIGINS = [ - 'https://truckwash.io', - 'https://www.truckwash.io', - 'http://localhost:5173', - 'http://localhost:4173', - 'http://localhost:4174', - 'http://127.0.0.1:4173', - 'http://127.0.0.1:4174', + "https://truckwash.io", + "https://www.truckwash.io", + "http://localhost:5173", + "http://localhost:4173", + "http://localhost:4174", + "http://127.0.0.1:5173", + "http://127.0.0.1:4173", + "http://127.0.0.1:4174", ]; -export const MIGRATION_ORIGIN = 'https://truckwash.io'; +export const MIGRATION_ORIGIN = "https://truckwash.io"; // Global request queue / retry / progress configuration export const REQUEST_QUEUE_CONFIG = Object.freeze({ @@ -43,7 +44,7 @@ export const REQUEST_QUEUE_CONFIG = Object.freeze({ jitterMs: 50, }), ping: Object.freeze({ - endpoint: '/ping', + endpoint: "/ping", intervalMs: 30000, timeoutMs: 5000, }), From 892471bd3aa4b6979c9ba3cc2ecdbc2e907ac66f Mon Sep 17 00:00:00 2001 From: Jeppe Bundgaard Date: Wed, 22 Apr 2026 09:39:37 +0200 Subject: [PATCH 3/3] Classify full E2E draft and superuser specs --- scripts/run-playwright-full-slice.mjs | 6 ++++++ 1 file changed, 6 insertions(+) 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",