- 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
54 lines
2.0 KiB
TypeScript
54 lines
2.0 KiB
TypeScript
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 };
|