Repository Public
1import { expect, test } from "@playwright/test";23/**4 * Public cinema and API contract. Runs against PW_BASE_URL (production or a local server).5 * Tests that need a published film skip themselves on an empty catalogue.6 */7let slug: string | null = null;8let master: string | null = null;910test.beforeAll(async ({ request }) => {11 const r = await request.get("/api/films?limit=1");12 expect(r.ok()).toBeTruthy();13 const d = await r.json();14 slug = d.films[0]?.slug ?? null;15 master = d.films[0]?.video ?? null;16});1718test("home shows a playable film or an honest empty state, with security headers", async ({ page }) => {19 const res = await page.goto("/");20 expect(res?.status()).toBe(200);21 const h = res!.headers();22 expect(h["content-security-policy"]).toContain("default-src 'self'");23 expect(h["x-frame-options"]).toBe("DENY");24 expect(h["x-content-type-options"]).toBe("nosniff");25 await expect(page.locator("html")).toHaveAttribute("lang", "en");26 await expect(page.locator("h1")).toBeVisible();27 if (slug) await expect(page.locator("video").first()).toBeVisible();28 else await expect(page.getByText("The projector is warming up")).toBeVisible();29 await expect(page.getByRole("link", { name: /Skip to content/ })).toHaveCount(1);30});3132test("French localisation is complete on the home page", async ({ page }) => {33 await page.goto("/fr");34 await expect(page.locator("html")).toHaveAttribute("lang", "fr");35 await expect(page.getByRole("link", { name: "Explorer" }).first()).toBeVisible();36 await expect(page.getByRole("heading", { name: "Séries" })).toBeVisible();37 await expect(page.getByText("Tous les films de CLIP sont générés par IA.")).toBeVisible();38});3940test("/en/* redirects to the canonical root URL", async ({ request }) => {41 const r = await request.get("/en/about", { maxRedirects: 0 });42 expect(r.status()).toBe(308);43 expect(r.headers().location).toMatch(/\/about$/);44});4546test("the feed plays the first film and answers the keyboard", async ({ page }) => {47 test.skip(!slug, "no published film");48 await page.goto("/feed");49 const video = page.locator("video").first();50 await expect(video).toBeVisible();51 await expect.poll(async () => video.evaluate((v: HTMLVideoElement) => v.readyState), { timeout: 20_000 }).toBeGreaterThan(1);52 await expect(page.getByRole("button", { name: /Unmute|Mute/ }).first()).toBeVisible();53 await expect(page.getByRole("slider", { name: "Playback position" }).first()).toBeVisible();54 await page.keyboard.press("c");55 await expect(page.getByRole("button", { name: /Captions:/ }).first()).toBeVisible();56});5758test("a film page carries title, disclosure, provenance and structured data", async ({ page }) => {59 test.skip(!slug, "no published film");60 const res = await page.goto(`/film/${slug}`);61 expect(res?.status()).toBe(200);62 await expect(page.locator("h1")).toBeVisible();63 await expect(page.getByText("AI-generated film").first()).toBeVisible();64 await expect(page.getByRole("heading", { name: "Provenance" })).toBeVisible();65 const ld = JSON.parse((await page.locator('script[type="application/ld+json"]').textContent()) ?? "{}");66 expect(ld["@type"]).toBe("VideoObject");67 expect(ld.contentUrl).toContain("/media/");68 await expect(page.locator('meta[property="og:video"]')).toHaveCount(1);69 // Private production data never reaches the page.70 const html = await page.content();71 expect(html).not.toMatch(/imagePrompt|videoPrompt|image_prompt|video_prompt|xai-[A-Za-z0-9]{20,}/);72});7374test("media is served with range support", async ({ request }) => {75 test.skip(!master, "no published film");76 const r = await request.get(master!, { headers: { Range: "bytes=0-1023" } });77 expect(r.status()).toBe(206);78 expect(r.headers()["content-range"]).toMatch(/^bytes 0-1023\/\d+$/);79 expect(r.headers()["content-type"]).toBe("video/mp4");80 expect((await r.body()).length).toBe(1024);81});8283test("private or unknown media is not served", async ({ request }) => {84 expect((await request.get(`/media/${"0".repeat(64)}.mp4`)).status()).toBe(404);85 expect((await request.get("/media/../../etc/passwd")).status()).toBe(404);86});8788test("public API: stable shapes, cursor pagination, no private fields", async ({ request }) => {89 const list = await (await request.get("/api/films?limit=1")).json();90 expect(Array.isArray(list.films)).toBeTruthy();91 expect(list).toHaveProperty("nextCursor");92 expect((await request.get("/api/films?cursor=not-a-cursor")).ok()).toBeTruthy();93 const series = await (await request.get("/api/series?locale=fr")).json();94 expect(series.series.length).toBe(8);95 expect(series.series.map((s: { title: string }) => s.title)).toContain("Mondes impossibles");96 if (slug) {97 const one = await (await request.get(`/api/films/${slug}`)).json();98 expect(one.film.slug).toBe(slug);99 expect(one.film.provenance.aiGenerated).toBe(true);100 expect(JSON.stringify(one)).not.toMatch(/prompt|cost_usd|request_id|embedding/i);101 }102 expect((await request.get("/api/films/does-not-exist")).status()).toBe(404);103 const api = await (await request.get("/api/openapi.json")).json();104 expect(api.openapi).toBe("3.1.0");105 expect(Object.keys(api.paths)).toContain("/api/studio/kill-switch");106});107108test("search explains why a film matched", async ({ page, request }) => {109 test.skip(!slug, "no published film");110 const film = (await (await request.get("/api/films?limit=1")).json()).films[0];111 const word = String(film.title).split(/\s+/).sort((a: string, b: string) => b.length - a.length)[0];112 await page.goto(`/search?q=${encodeURIComponent(word)}`);113 await expect(page.getByText(/Matched by:/).first()).toBeVisible();114});115116test("explore, series, about and report pages render", async ({ page }) => {117 for (const [path, heading] of [["/explore", "Explore"], ["/series", "Series"], ["/about", "How CLIP makes a film"], ["/report", "Report a film"], ["/fr/about", "Comment CLIP fabrique un film"]] as const) {118 const res = await page.goto(path);119 expect(res?.status(), path).toBe(200);120 await expect(page.getByRole("heading", { level: 1, name: heading })).toBeVisible();121 }122});123124test("unknown pages return a real 404", async ({ request }) => {125 expect((await request.get("/this-page-does-not-exist")).status()).toBe(404);126 expect((await request.get("/film/this-film-does-not-exist")).status()).toBe(404);127});128129test("robots, sitemap and health", async ({ request }) => {130 const robots = await (await request.get("/robots.txt")).text();131 expect(robots).toContain("Disallow: /studio");132 expect(robots).toContain("sitemap.xml");133 const sitemap = await (await request.get("/sitemap.xml")).text();134 expect(sitemap).toContain("<urlset");135 if (slug) expect(sitemap).toContain(`/film/${slug}`);136 const health = await (await request.get("/api/health")).json();137 expect(health.checks.database.ok).toBe(true);138 expect(health.checks.assets.ok).toBe(true);139});140141test("the page has no horizontal overflow and a visible focus order", async ({ page }) => {142 await page.goto("/");143 const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth);144 expect(overflow).toBeLessThanOrEqual(1);145 await page.keyboard.press("Tab");146 await expect(page.getByRole("link", { name: /Skip to content/ })).toBeFocused();147});148