// Boosted navigation: outside the editor, page links swap the next page into // the open document instead of loading a new one. A marker on window survives // a boosted navigation and disappears with a full page load. import { test, expect } from "@playwright/test"; import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; const wiki = join(dirname(fileURLToPath(import.meta.url)), "..", ".tmp", "wiki"); test.beforeEach(({ page }) => { page.on("pageerror", (err) => { // Chrome aborts the cross-document view transition (style.css) when the // next document can't take part, e.g. a plain-text file, and reports it // as an unhandled rejection that no page script can catch. if (err.message.includes("ViewTransition opt-in disabled")) return; throw err; }); }); function randomWord(n) { return Array.from({ length: n }, () => String.fromCharCode(97 + Math.floor(Math.random() * 26))).join(""); } function sandbox(pages) { const root = "t" + randomWord(7); for (const [rel, body] of Object.entries(pages)) { const dir = join(wiki, root, rel); mkdirSync(dir, { recursive: true }); if (body !== null) writeFileSync(join(dir, "index.md"), body); } return root; } const sidebar = (page) => page.locator("aside.tree-sidebar"); const link = (scope, name) => scope.getByRole("link", { name, exact: true }); const mark = (page) => page.evaluate(() => (window.__marker = 1)); const marked = (page) => page.evaluate(() => window.__marker === 1); const long = (title) => `# ${title}\n\n` + Array.from({ length: 40 }, (_, i) => `## Part ${i}\n\ntext\n`).join("\n"); test("sidebar navigation swaps the page in without a reload", async ({ page }) => { const root = sandbox({ "": "# Parent\n", Child: "# Child\n\n## First\n\none\n\n## Second\n\n- [ ] boosted task\n", }); await page.goto(`/${root}/`); await mark(page); await link(sidebar(page), "Child").click(); await expect(page).toHaveURL(new RegExp(`/${root}/Child/$`)); await expect(page.locator("main h1")).toContainText("Child"); await expect(page).toHaveTitle("Child"); expect(await marked(page)).toBe(true); await expect(sidebar(page).locator(".tree-row.is-active")).toHaveText("Child"); // Page scripts ran for the new content: TOC and section edit links. await expect(page.locator("nav.toc")).toContainText("Second"); await expect(page.locator("main h2", { hasText: "First" }).getByRole("link", { name: "[edit]" })).toBeVisible(); // htmx attributes in the new content work: the task toggles. await page.locator("main input.task-checkbox").check(); await expect.poll(() => readFileSync(join(wiki, root, "Child", "index.md"), "utf8")).toContain("- [x] boosted task"); }); test("Back and Forward restore pages and scroll without a reload", async ({ page }) => { const root = sandbox({ "": long("Parent"), Child: long("Child") }); await page.goto(`/${root}/`); await mark(page); const center = page.locator(".center"); await center.evaluate((el) => (el.scrollTop = 600)); await link(sidebar(page), "Child").click(); await expect(page.locator("main h1")).toContainText("Child"); // A new page starts at the top. expect(await center.evaluate((el) => el.scrollTop)).toBe(0); await page.goBack(); await expect(page).toHaveURL(new RegExp(`/${root}/$`)); await expect(page.locator("main h1")).toContainText("Parent"); await expect(page).toHaveTitle("Parent"); await expect.poll(() => center.evaluate((el) => el.scrollTop)).toBeGreaterThan(500); await page.goForward(); await expect(page.locator("main h1")).toContainText("Child"); expect(await marked(page)).toBe(true); }); test("Back to the first, fully loaded page works", async ({ page }) => { const root = sandbox({ "": "# Parent\n", Child: "# Child\n" }); await page.goto(`/${root}/Child/`); await mark(page); await link(sidebar(page), root).click(); await expect(page.locator("main h1")).toContainText("Parent"); await page.goBack(); await expect(page).toHaveURL(new RegExp(`/${root}/Child/$`)); await expect(page.locator("main h1")).toContainText("Child"); expect(await marked(page)).toBe(true); }); test("files and the editor load as full pages", async ({ page }) => { const root = sandbox({ "": "# Parent\n" }); writeFileSync(join(wiki, root, "note.txt"), "plain file"); await page.goto(`/${root}/`); await mark(page); await page.locator("main").getByRole("link", { name: "note.txt" }).click(); await expect(page).toHaveURL(new RegExp(`/${root}/note.txt$`)); await expect(page.locator("body")).toContainText("plain file"); expect(await marked(page)).toBe(false); await page.goto(`/${root}/`); await mark(page); await page.locator("header").getByTitle("Edit page (E)").click(); await expect(page).toHaveURL(/\?edit/); await expect(page.locator(".cm-content")).toBeVisible(); expect(await marked(page)).toBe(false); }); test("header actions follow the page", async ({ page }) => { const root = sandbox({ "": "# Parent\n" }); await page.goto("/"); await mark(page); const move = page.locator("header").getByTitle("Move page (M)"); await expect(move).toHaveCount(0); await link(sidebar(page), root).click(); await expect(page.locator("main h1")).toContainText("Parent"); await expect(move).toBeVisible(); await page.goBack(); await expect(page.locator("main h1")).toContainText("Home"); await expect(move).toHaveCount(0); expect(await marked(page)).toBe(true); }); test("search results and the search box follow boosted navigation", async ({ page }) => { const root = sandbox({ "": "# Parent\n", Child: "# Child\n" }); expect((await page.request.post("/_reindex")).status()).toBe(204); await page.goto(`/${root}/`); await mark(page); const input = page.locator("header .search-input"); await input.fill(root); await input.press("Enter"); await expect(page).toHaveURL(new RegExp(`/\?q=${root}$`)); await expect(page).toHaveTitle(`Search: ${root}`); await expect(page.locator("main .search-card").first()).toBeVisible(); await link(sidebar(page), root).click(); await expect(page.locator("main h1")).toContainText("Parent"); await expect(input).toHaveValue(""); expect(await marked(page)).toBe(true); }); test("a mutation swaps in its result without a reload", async ({ page }) => { const root = sandbox({ "": "# Parent\n", Gone: "# Gone\n" }); await page.goto(`/${root}/Gone/`); await mark(page); await page.locator("header").getByTitle("Delete page").click(); await page.locator(".modal").getByRole("button", { name: "DELETE" }).click(); await expect(page).toHaveURL(new RegExp(`/${root}/$`)); await expect(page.locator("main h1")).toContainText("Parent"); await expect(page.locator(".modal")).toHaveCount(0); await expect(link(sidebar(page), "Gone")).toHaveCount(0); expect(await marked(page)).toBe(true); });