import { test, expect, type Page } from "@playwright/test"; import { LAST_PAGE_KEY } from "../lib/last-page"; // / is the dashboard now. It used to REPLACE itself with wherever you were // last; it OFFERS that URL as a chip instead, with the same storage, the same // guard and the same HEAD-probed walk-up -- so a stale URL degrades to its // parent, and a hostile one is not offered at all. // // The suite runs workers: 1 with NO state reset between tests, so a remembered // URL leaks forward unless each test says what storage holds. /** * Plant (or clear) the remembered URL, applied ON THE DASHBOARD DOCUMENT ONLY. * * The recorder also writes on `pagehide`, so navigating to / from another page * overwrites a value planted earlier. An init script runs at document start -- * after the old page's pagehide and before the chip's effect reads storage. */ async function remembered(page: Page, value: string | null) { await page.addInitScript( ([key, v]) => { if (location.pathname !== "/") return; if (v === null) localStorage.removeItem(key); else localStorage.setItem(key, v); }, [LAST_PAGE_KEY, value] as const, ); } const chip = (page: Page) => page.locator("[data-resume]"); test("/ is the dashboard, and with nothing remembered there is no chip", async ({ page }) => { await remembered(page, null); await page.goto("/"); await expect(page).toHaveURL(/\/$/); await expect(page.getByRole("link", { name: "home", exact: true })).toHaveAttribute("aria-current", "page"); await expect(page.locator("[data-panel=now]")).toBeVisible(); // Give the effect time to have run before asserting absence. await expect(page.locator("[data-panel=projects]")).toBeVisible(); await expect(chip(page)).toHaveCount(0); }); test("the chip offers the page you were last on, query string included", async ({ page }) => { // No planted value: this is the recorder's OWN write, which is what ships. await page.goto("/sort?limit=5"); await page.goto("/"); await expect(chip(page)).toHaveAttribute("data-resume", "/sort?limit=5"); await chip(page).click(); await expect(page).toHaveURL(/\/sort\?limit=5$/); }); test("a remembered URL that has gone stale walks UP instead of offering a 404", async ({ page }) => { await remembered(page, "/browse/__gone__/wide"); await page.goto("/"); await expect(chip(page)).toHaveAttribute("data-resume", "/browse"); // And the URL really is dead -- otherwise this passes for the wrong reason // the day that route quietly starts rendering something. expect((await page.request.get("/browse/__gone__/wide")).status()).toBe(404); }); for (const hostile of ["//evil.example", "https://evil.example", "/api/verdict", "/"]) { test(`a stored ${hostile} is not offered`, async ({ page }) => { // Storage is user-editable and outlives any deploy, so a value read back // is untrusted input. Two of these leave the machine, one is not a page, // and one is this page. await remembered(page, hostile); await page.goto("/"); await expect(page.locator("[data-panel=projects]")).toBeVisible(); await expect(chip(page)).toHaveCount(0); }); } test("the song piles are one nav entry, and every pile URL still works", async ({ page }) => { await page.goto("/"); await page.locator("[data-nav-group=song] button").click(); await page.getByRole("menuitem", { name: "sort" }).click(); await expect(page).toHaveURL(/\/sort$/); // On a pile page the group itself is lit, since the pile is under it. await expect(page.locator("[data-nav-group=song] button")).toHaveAttribute("aria-current", "page"); for (const p of ["/salvage", "/verify", "/keeps", "/browse/sources"]) { const res = await page.goto(p); expect(res?.status(), `${p} should still be 200`).toBe(200); } });