Add Playwright E2E test suite (25 tests across 4 specs)
- playwright.config.ts: headless CI setup with JSON/HTML reporters - e2e/global-setup.ts: app reachability check before tests - e2e/auth.spec.ts: 6 auth tests (sign-in, session, protected routes) - e2e/compare.spec.ts: 8 API tests (validation, SSE flow, DB persistence) - e2e/comparisons.spec.ts: 6 CRUD tests (list, detail, 404, view counts) - e2e/user.spec.ts: 5 user tests (stats, pagination, session binding) - e2e/helpers.ts: shared SSE parsing, auth, URL resolution utilities - e2e/README.md: setup and run instructions - AGENTS.md: updated with LLM provider notes and testing docs - .gitignore: added playwright report/result directories - package.json: added @playwright/test and test:e2e scripts
This commit is contained in:
53
e2e/helpers.ts
Normal file
53
e2e/helpers.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { type Page, type Request, type Response, expect } from "@playwright/test";
|
||||
import { baseURL, targetHost, testUser } from "../playwright.config";
|
||||
|
||||
/** Resolve URL relative to the target host (bypasses localhost resolution). */
|
||||
export function resolveUrl(path: string): string {
|
||||
const base = baseURL.replace("localhost", targetHost);
|
||||
return `${base}${path.startsWith("/") ? path : "/" + path}`;
|
||||
}
|
||||
|
||||
/** Sign in as the test user via the API and return the session cookie. */
|
||||
export async function getSessionCookie(page: Page): Promise<string> {
|
||||
const res = await page.request.post(resolveUrl("/api/auth/sign-in/email"), {
|
||||
data: { email: testUser.email, password: testUser.password },
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
if (!res.ok()) {
|
||||
throw new Error(`Sign-in failed: ${res.status()} ${await res.text()}`);
|
||||
}
|
||||
const setCookie = res.headers()["set-cookie"] ?? "";
|
||||
const match = setCookie.match(/session_token=([^;]+)/);
|
||||
if (!match) throw new Error("No session_token cookie in sign-in response");
|
||||
return `session_token=${match[1]}`;
|
||||
}
|
||||
|
||||
/** Parse SSE stream from a fetch response body. */
|
||||
export async function parseSSEStream(body: string): Promise<Record<string, unknown>[]> {
|
||||
const events: Record<string, unknown>[] = [];
|
||||
for (const line of body.split("\n")) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith("event:")) continue;
|
||||
if (trimmed.startsWith("data:")) {
|
||||
const dataStr = trimmed.slice(5).trim();
|
||||
try {
|
||||
events.push(JSON.parse(dataStr));
|
||||
} catch {
|
||||
// skip malformed
|
||||
}
|
||||
}
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
/** Extract the last SSE event of a given type. */
|
||||
export function lastSSEOfType(
|
||||
events: Record<string, unknown>[],
|
||||
type: "progress" | "done"
|
||||
): Record<string, unknown> | undefined {
|
||||
return events.reverse().find((e) => e[type] !== undefined);
|
||||
}
|
||||
|
||||
// Re-export common helpers
|
||||
export { expect, resolveUrl as url, testUser };
|
||||
export type { Page, Request, Response };
|
||||
Reference in New Issue
Block a user