SPB Git

Repository Public

public.spec.ts 7.1 KB 148 lines typescript

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
Files main
Menu

Theme