1// The application, walked. These specs assert arrangement, not pixels: the2// URL a doorway lands on, the island a page carries, the controls a topbar3// offers, the session the next page remembers. The registry underneath is4// dist/e2e/bundles — the committed corpus, packed by tests/e2e/prep — so5// every expectation below is a fact about bytes in this repository.6import { expect, test, type Page } from "@playwright/test";78// The corpus contract, spelled out rather than discovered, so that a volume9// quietly failing to serve is a red test and not a shorter loop.10const MARS = { slug: "mars", world: "global", title: "Mars", lenses: 2 };11const BEND = { slug: "bend-or", world: "2026-08-02", title: "Bend, Oregon", lenses: 1 };12// The included Earth volume: the committed bundle the desktop embeds,13// installed into the e2e registry by tests/e2e/prep beside the corpus. It is14// the one entry with real rasters.15const EARTH = { slug: "earth", world: "earth", title: "Earth", lenses: 1 };1617// island reads the page's own account of the arrangement: the inert JSON18// script node every explorer page carries.19async function island(page: Page) {20 const text = await page.locator("#atlas-session-island").textContent();21 expect(text, "the page carries no session island").toBeTruthy();22 return JSON.parse(text!) as {23 last: string;24 entry: null | { volume: string; world: string; lens: number };25 };26}2728test.describe("the doorways", () => {29 test("the root is a doorway to a real world URL", async ({ page }) => {30 await page.goto("/");31 await expect(page).toHaveURL(/\/v\/[a-z0-9-]+\/[a-z0-9-]+$/);32 });3334 test("open names a volume and lands on its world", async ({ page }) => {35 await page.goto(`/open?volume=${MARS.slug}&world=${MARS.world}`);36 await expect(page).toHaveURL(`/v/${MARS.slug}/${MARS.world}`);37 });3839 test("the catalog serves every corpus volume", async ({ request }) => {40 const response = await request.get("/data/catalog.json");41 expect(response.ok()).toBeTruthy();42 const catalog = (await response.json()) as {43 volumes: { slug: string; stamp: string; worlds: unknown[] }[];44 };45 const slugs = catalog.volumes.map((v) => v.slug).sort();46 expect(slugs).toEqual([BEND.slug, EARTH.slug, MARS.slug].sort());47 for (const volume of catalog.volumes) {48 expect(volume.stamp).toMatch(/^[0-9a-f]{64}$/);49 expect(volume.worlds.length).toBeGreaterThan(0);50 }51 });5253 test("a volume nobody installed is not found", async ({ page }) => {54 const response = await page.goto("/v/nowhere/nothing");55 expect(response?.status()).toBe(404);56 });5758 // The raster leg, end to end: the included Earth carries real tiles, so the59 // data plane can be asked for imagery and answer with an image — the one60 // assertion the tile-less corpus volumes can never carry.61 test("the included Earth serves a real raster tile", async ({ request }) => {62 const response = await request.get("/data/catalog.json");63 const catalog = (await response.json()) as {64 volumes: { slug: string; stamp: string }[];65 };66 const earth = catalog.volumes.find((v) => v.slug === EARTH.slug);67 expect(earth, "the catalog does not list earth").toBeTruthy();68 const tile = await request.get(69 `/data/v/${EARTH.slug}/${earth!.stamp.slice(0, 12)}/tiles/earth/0/0/0.jpg`,70 );71 expect(tile.ok()).toBeTruthy();72 expect(tile.headers()["content-type"]).toBe("image/jpeg");73 expect((await tile.body()).length).toBeGreaterThan(1000);74 });75});7677test.describe("the explorer page", () => {78 for (const volume of [MARS, BEND, EARTH]) {79 test(`${volume.slug} arrives arranged`, async ({ page }) => {80 await page.goto(`/v/${volume.slug}/${volume.world}`);8182 // The page's own account agrees with its address.83 const state = await island(page);84 expect(state.entry?.volume).toBe(volume.slug);85 expect(state.entry?.world).toBe(volume.world);8687 // The topbar's selects say where the reader is and what else there is.88 // A crumb with one option is a fact, not a choice: every installed89 // volume carries a single world, so the world crumb reads but does not90 // edit, while the volume crumb (three volumes installed) stays a real91 // select.92 await expect(page.locator("#volume-select")).toHaveValue(volume.slug);93 await expect(page.locator("#volume-select")).toBeEnabled();94 await expect(page.locator("#world-select")).toHaveValue(volume.world);95 await expect(page.locator("#world-select")).toBeDisabled();96 const lensField = page.locator("#lens-field");97 if (volume.lenses > 1) {98 await expect(lensField).toBeVisible();99 await expect(page.locator("#lens-select")).toBeEnabled();100 await expect(page.locator("#lens-select option")).toHaveCount(volume.lenses);101 } else {102 await expect(lensField).toBeHidden();103 }104105 // The viewport is the seam's: the custom elements are there, and the106 // chart has booted far enough to put a canvas on the page. No tile107 // assertions — the corpus commits no rasters, and surviving that is108 // the render suite's own proof.109 await expect(page.locator("atlas-viewport")).toHaveCount(1);110 await expect(page.locator("atlas-chart")).toHaveCount(1);111 await expect(page.locator("atlas-globe")).toHaveCount(1);112 await expect(page.locator("atlas-chart canvas, #map canvas").first()).toBeAttached({113 timeout: 15_000,114 });115116 // The keyboard contract rides every page.117 await expect(page.locator("#atlas-shortcuts")).toBeAttached();118 });119 }120121 test("a lens is addressed by name and remembered", async ({ page }) => {122 await page.goto(`/v/${MARS.slug}/${MARS.world}`);123 const options = page.locator("#lens-select option");124 await expect(options).toHaveCount(MARS.lenses);125 const second = await options.nth(1).getAttribute("value");126 expect(second).toBeTruthy();127128 await page.locator("#lens-select").selectOption(second!);129 // The session answer swaps the shell and republishes the island.130 await expect131 .poll(async () => (await island(page)).entry?.lens, {132 message: "the island never recorded the lens change",133 })134 .toBe(1);135 // And the keyboard goes back to the map: a crumb that kept focus would136 // spend the reader's next letter walking its own options.137 await expect138 .poll(() => page.evaluate(() => document.activeElement?.id ?? ""), {139 message: "the crumb kept the keyboard after it was answered",140 })141 .toBe("map");142143 // A fresh visit to the same world resumes the same lens.144 await page.goto(`/v/${MARS.slug}/${MARS.world}`);145 expect((await island(page)).entry?.lens).toBe(1);146 });147148 test("the last volume is where the root door leads", async ({ page }) => {149 await page.goto(`/v/${MARS.slug}/${MARS.world}`);150 await expect(page.locator("#atlas-shell")).toBeAttached();151 await page.goto("/");152 await expect(page).toHaveURL(`/v/${MARS.slug}/${MARS.world}`);153 });154});