import { test, expect } from "@playwright/test"; import { resolveUrl, getSessionCookie, parseSSEStream, lastSSEOfType } from "./helpers"; /** * E2E tests for the /api/compare endpoint. * Covers: request validation, full SSE flow, success/failure states, and DB persistence. */ test.describe("Compare API", () => { let cookie: string; test.beforeEach(async ({ page }) => { cookie = await getSessionCookie(page); }); // ─── Input Validation ──────────────────────────────────────────────────── test("1. Rejects fewer than 2 items", async ({ page }) => { const res = await page.request.post(resolveUrl("/api/compare"), { data: { items: ["React"] }, headers: { "Content-Type": "application/json", Cookie: cookie }, }); expect(res.status()).toBe(400); const body = await res.json(); expect(body.error).toMatch(/2 items|at least/i); }); test("2. Rejects more than 10 items", async ({ page }) => { const res = await page.request.post(resolveUrl("/api/compare"), { data: { items: Array.from({ length: 11 }, (_, i) => `Item${i}`) }, headers: { "Content-Type": "application/json", Cookie: cookie }, }); expect(res.status()).toBe(400); const body = await res.json(); expect(body.error).toMatch(/10|maximum/i); }); test("3. Rejects empty item names", async ({ page }) => { const res = await page.request.post(resolveUrl("/api/compare"), { data: { items: ["React", " ", "Vue"] }, headers: { "Content-Type": "application/json", Cookie: cookie }, }); expect(res.status()).toBe(400); const body = await res.json(); expect(body.error).toMatch(/empty/i); }); test("4. Rejects unauthenticated requests", async ({ page }) => { const res = await page.request.post(resolveUrl("/api/compare"), { data: { items: ["React", "Vue"] }, headers: { "Content-Type": "application/json" }, }); expect(res.status()).toBe(401); }); // ─── Happy Path ────────────────────────────────────────────────────────── test("5. Compare returns SSE stream with progress events", async ({ page }) => { const res = await page.request.post(resolveUrl("/api/compare"), { data: { query: "Python vs JavaScript", items: ["Python", "JavaScript"] }, headers: { "Content-Type": "application/json", Cookie: cookie, Accept: "text/event-stream", }, }); expect(res.status()).toBe(200); expect(res.headers()["content-type"]).toContain("text/event-stream"); const body = await res.body(); if (!body) { test.skip(); return; } const bodyStr = Buffer.from(body).toString("utf-8"); const events = await parseSSEStream(bodyStr); // Should have at least one progress event const progressEvents = events.filter((e) => e.status !== undefined); expect(progressEvents.length).toBeGreaterThan(0); }); test("6. Full compare flow completes successfully", async ({ page }) => { const res = await page.request.post(resolveUrl("/api/compare"), { data: { query: "React vs Vue", items: ["React", "Vue"] }, headers: { "Content-Type": "application/json", Cookie: cookie, Accept: "text/event-stream", }, timeout: 120_000, }); expect(res.status()).toBe(200); const body = Buffer.from((await res.body()) ?? []).toString("utf-8"); const events = await parseSSEStream(body); // Must have a final status const finalProgress = lastSSEOfType(events, "progress"); expect(finalProgress).toBeDefined(); expect(["completed", "failed"]).toContain(finalProgress!.status); if (finalProgress!.status === "completed") { const doneEvent = lastSSEOfType(events, "done") as Record | undefined; expect(doneEvent).toBeDefined(); const doneData = doneEvent!.done as Record; expect(doneData).toHaveProperty("id"); expect(doneData).toHaveProperty("slug"); expect((doneData.data as Record)).toHaveProperty("summary"); expect((doneData.data as Record)).toHaveProperty("items"); } }); test("7. Comparison is persisted to DB after completion", async ({ page }) => { // Run a comparison const res = await page.request.post(resolveUrl("/api/compare"), { data: { query: "Rust vs Go", items: ["Rust", "Go"] }, headers: { "Content-Type": "application/json", Cookie: cookie, Accept: "text/event-stream" }, timeout: 120_000, }); expect(res.status()).toBe(200); const body = Buffer.from((await res.body()) ?? []).toString("utf-8"); const events = await parseSSEStream(body); const doneEvent = lastSSEOfType(events, "done") as Record | undefined; if (!doneEvent) { test.skip(); return; } const { slug } = doneEvent.done as { slug: string }; expect(slug).toBeTruthy(); // Fetch it back const getRes = await page.request.get(resolveUrl(`/api/comparisons/${slug}`), { headers: { Cookie: cookie }, }); expect(getRes.status()).toBe(200); const comparison = await getRes.json(); expect(comparison.slug).toBe(slug); expect(comparison.status).toBe("completed"); }); test("8. Compare with custom dimensions", async ({ page }) => { const res = await page.request.post(resolveUrl("/api/compare"), { data: { query: "React vs Angular", items: ["React", "Angular"], dimensions: ["Performance", "Learning Curve", "Ecosystem"], }, headers: { "Content-Type": "application/json", Cookie: cookie, Accept: "text/event-stream", }, timeout: 120_000, }); expect(res.status()).toBe(200); const body = Buffer.from((await res.body()) ?? []).toString("utf-8"); const events = await parseSSEStream(body); const finalProgress = lastSSEOfType(events, "progress"); expect(finalProgress).toBeDefined(); expect(["completed", "failed"]).toContain(finalProgress!.status); }); });