From fdbbd568372ec50c12b725fd4a5132e61213459d Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Fri, 17 Jul 2026 15:34:31 -0700 Subject: [PATCH 1/2] Fix Playwright frontend host resolution Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4933d548-7c5e-4451-84b1-0ea00dbe4322 --- frontend/playwright.config.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index 8edf8735e0..b22664df1b 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -4,6 +4,7 @@ const CI_SEEDED_MODE = !!process.env.CI && process.env.E2E_SEEDED_MODE === "true"; const E2E_BACKEND_PORT = process.env.PYRIT_E2E_BACKEND_PORT ?? "18000"; const E2E_BACKEND_URL = `http://127.0.0.1:${E2E_BACKEND_PORT}`; +const E2E_FRONTEND_URL = "http://127.0.0.1:3000"; export default defineConfig({ testDir: "./e2e", @@ -19,7 +20,7 @@ export default defineConfig({ timeout: 30000, use: { - baseURL: "http://localhost:3000", + baseURL: E2E_FRONTEND_URL, trace: "on-first-retry", screenshot: "only-on-failure", // Pre-set localStorage so the onboarding tour doesn't auto-start and @@ -28,7 +29,7 @@ export default defineConfig({ cookies: [], origins: [ { - origin: "http://localhost:3000", + origin: E2E_FRONTEND_URL, localStorage: [ { name: "pyrit-tour-completed", value: "true" }, ], @@ -77,7 +78,7 @@ export default defineConfig({ command: "npx vite --host 127.0.0.1 --port 3000 --strictPort", env: { PYRIT_BACKEND_URL: E2E_BACKEND_URL }, // Use 127.0.0.1 to avoid Node.js 17+ resolving localhost to IPv6 ::1 - url: "http://127.0.0.1:3000", + url: E2E_FRONTEND_URL, reuseExistingServer: false, timeout: 120_000, }, @@ -87,7 +88,7 @@ export default defineConfig({ command: process.env.CI ? "npx vite --port 3000" : "python dev.py", - url: "http://127.0.0.1:3000", + url: E2E_FRONTEND_URL, reuseExistingServer: !process.env.CI, timeout: 120_000, }, From e0f6118f52e23355a6562656968c5df4d3a77a7b Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Sun, 19 Jul 2026 07:08:09 -0700 Subject: [PATCH 2/2] TEST: Consolidate Playwright E2E stabilization Add bounded readiness waits, serialize credential-backed projects, and allow isolated frontend ports while preserving IPv4 host resolution. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4933d548-7c5e-4451-84b1-0ea00dbe4322 --- frontend/e2e/api.spec.ts | 12 ++++++------ frontend/e2e/flows.spec.ts | 7 +++++-- frontend/playwright.config.ts | 30 +++++++++++++++++++++++++----- 3 files changed, 36 insertions(+), 13 deletions(-) diff --git a/frontend/e2e/api.spec.ts b/frontend/e2e/api.spec.ts index c21b95cbb6..d9a1670ef2 100644 --- a/frontend/e2e/api.spec.ts +++ b/frontend/e2e/api.spec.ts @@ -2,10 +2,10 @@ import { test, expect } from "@playwright/test"; // API tests go through the Vite dev server proxy (/api -> configured backend) // rather than hitting the backend directly, so they work as soon as -// Playwright's webServer (port 3000) is ready. +// Playwright's webServer is ready. test.describe("API Health Check", () => { - // The backend may still be starting when Vite (port 3000) is already up. + // The backend may still be starting when Vite is already up. // Poll the health endpoint through the proxy until the backend is ready. test.beforeAll(async ({ request }) => { const maxWait = 30_000; @@ -13,7 +13,7 @@ test.describe("API Health Check", () => { const start = Date.now(); while (Date.now() - start < maxWait) { try { - const resp = await request.get("/api/health"); + const resp = await request.get("/api/health", { timeout: 2_000 }); if (resp.ok()) return; } catch { // Backend not ready yet @@ -24,7 +24,7 @@ test.describe("API Health Check", () => { }); test("should have healthy backend API @seeded", async ({ request }) => { - const response = await request.get("/api/health"); + const response = await request.get("/api/health", { timeout: 10_000 }); expect(response.ok()).toBe(true); const data = await response.json(); @@ -48,7 +48,7 @@ test.describe("Targets API", () => { const start = Date.now(); while (Date.now() - start < maxWait) { try { - const resp = await request.get("/api/health"); + const resp = await request.get("/api/health", { timeout: 2_000 }); if (resp.ok()) return; } catch { // Backend not ready yet @@ -108,7 +108,7 @@ test.describe("Attacks API", () => { const start = Date.now(); while (Date.now() - start < maxWait) { try { - const resp = await request.get("/api/health"); + const resp = await request.get("/api/health", { timeout: 2_000 }); if (resp.ok()) return; } catch { // Backend not ready yet diff --git a/frontend/e2e/flows.spec.ts b/frontend/e2e/flows.spec.ts index 69f3cacc23..249e9cd5cf 100644 --- a/frontend/e2e/flows.spec.ts +++ b/frontend/e2e/flows.spec.ts @@ -50,7 +50,7 @@ async function waitForBackend(request: APIRequestContext): Promise { const start = Date.now(); while (Date.now() - start < maxWait) { try { - const resp = await request.get("/api/health"); + const resp = await request.get("/api/health", { timeout: 2_000 }); if (resp.ok()) return; } catch { // Backend not ready yet @@ -528,6 +528,8 @@ async function assertLiveAssistant( exp: TargetVariant["expectAssistantLive"], ): Promise { const assistantBubble = page.getByTestId("message-bubble-1"); + await expect(assistantBubble).toBeVisible({ timeout: 90_000 }); + if (exp.hasText) { await expect(assistantBubble).toContainText(/\S/, { timeout: 90_000 }); } @@ -985,7 +987,8 @@ for (const variant of TARGET_VARIANTS) { let targetRegistryName: string; - test.beforeAll(async ({ request }) => { + test.beforeAll(async ({ request }, testInfo) => { + testInfo.setTimeout(120_000); if (!hasLiveConfiguration(variant)) return; await waitForBackend(request); targetRegistryName = await createTarget( diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index b22664df1b..c89819ab03 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -1,10 +1,24 @@ import { defineConfig, devices } from "@playwright/test"; +const E2E_FRONTEND_PORT = Number.parseInt( + process.env.E2E_FRONTEND_PORT ?? "3000", + 10, +); +if ( + !Number.isInteger(E2E_FRONTEND_PORT) || + E2E_FRONTEND_PORT < 1 || + E2E_FRONTEND_PORT > 65535 +) { + throw new Error("E2E_FRONTEND_PORT must be a valid TCP port."); +} + +const E2E_FRONTEND_URL = `http://127.0.0.1:${E2E_FRONTEND_PORT}`; +const USE_DEDICATED_VITE = + Boolean(process.env.CI) || process.env.E2E_FRONTEND_PORT !== undefined; const CI_SEEDED_MODE = !!process.env.CI && process.env.E2E_SEEDED_MODE === "true"; const E2E_BACKEND_PORT = process.env.PYRIT_E2E_BACKEND_PORT ?? "18000"; const E2E_BACKEND_URL = `http://127.0.0.1:${E2E_BACKEND_PORT}`; -const E2E_FRONTEND_URL = "http://127.0.0.1:3000"; export default defineConfig({ testDir: "./e2e", @@ -48,11 +62,15 @@ export default defineConfig({ name: "seeded", use: { ...devices["Desktop Chrome"] }, grep: /@seeded/, + fullyParallel: false, + workers: 1, }, { name: "live", use: { ...devices["Desktop Chrome"] }, grep: /@live/, + fullyParallel: false, + workers: 1, }, // Firefox can be enabled by installing: npx playwright install firefox // { @@ -75,7 +93,9 @@ export default defineConfig({ timeout: 120_000, }, { - command: "npx vite --host 127.0.0.1 --port 3000 --strictPort", + command: + `npx vite --host 127.0.0.1 --port ${E2E_FRONTEND_PORT} ` + + "--strictPort", env: { PYRIT_BACKEND_URL: E2E_BACKEND_URL }, // Use 127.0.0.1 to avoid Node.js 17+ resolving localhost to IPv6 ::1 url: E2E_FRONTEND_URL, @@ -85,11 +105,11 @@ export default defineConfig({ ] : { // Mock CI needs only Vite. Local seeded/live runs use dev.py. - command: process.env.CI - ? "npx vite --port 3000" + command: USE_DEDICATED_VITE + ? `npx vite --host 127.0.0.1 --port ${E2E_FRONTEND_PORT} --strictPort` : "python dev.py", url: E2E_FRONTEND_URL, - reuseExistingServer: !process.env.CI, + reuseExistingServer: !USE_DEDICATED_VITE, timeout: 120_000, }, });