Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions frontend/e2e/api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@ 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;
const interval = 1_000;
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
Expand All @@ -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();
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions frontend/e2e/flows.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ async function waitForBackend(request: APIRequestContext): Promise<void> {
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
Expand Down Expand Up @@ -528,6 +528,8 @@ async function assertLiveAssistant(
exp: TargetVariant["expectAssistantLive"],
): Promise<void> {
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 });
}
Expand Down Expand Up @@ -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(
Expand Down
37 changes: 29 additions & 8 deletions frontend/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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
Expand All @@ -28,7 +43,7 @@ export default defineConfig({
cookies: [],
origins: [
{
origin: "http://localhost:3000",
origin: E2E_FRONTEND_URL,
localStorage: [
{ name: "pyrit-tour-completed", value: "true" },
],
Expand All @@ -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
// {
Expand All @@ -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,
},
});