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 8edf8735e0..c89819ab03 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -1,5 +1,20 @@ 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"; @@ -19,7 +34,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 +43,7 @@ export default defineConfig({ cookies: [], origins: [ { - origin: "http://localhost:3000", + origin: E2E_FRONTEND_URL, localStorage: [ { name: "pyrit-tour-completed", value: "true" }, ], @@ -47,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 // { @@ -74,21 +93,23 @@ 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: "http://127.0.0.1:3000", + url: E2E_FRONTEND_URL, reuseExistingServer: false, timeout: 120_000, }, ] : { // 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: "http://127.0.0.1:3000", - reuseExistingServer: !process.env.CI, + url: E2E_FRONTEND_URL, + reuseExistingServer: !USE_DEDICATED_VITE, timeout: 120_000, }, });