// Baseline end-to-end coverage of the page UI. Tests drive the app through // visible labels, not implementation details, so they keep passing when the // client-side mechanics change (e.g. the move to HTMX). import { test, expect } from "@playwright/test"; import { mkdirSync, readFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; const wiki = join(dirname(fileURLToPath(import.meta.url)), "..", ".tmp", "wiki"); // Fail any test that logs a JS error. test.beforeEach(({ page }) => { page.on("pageerror", (err) => { throw err; }); }); const sidebar = (page) => page.locator("aside.tree-sidebar"); const modal = (page) => page.locator(".modal"); // Page actions sit in a dropdown on narrow screens and inline (icon-only) on // wide ones; address them by title and open the dropdown only when present. async function pageAction(page, title) { const toggle = page.getByRole("button", { name: "ACTIONS" }); if (await toggle.isVisible()) await toggle.click(); await page.locator("header").getByTitle(title).click(); } test("renders a page and navigates via the sidebar", async ({ page }) => { await page.goto("/"); await expect(page.locator("main h1")).toContainText("Home"); await sidebar(page).getByRole("link", { name: "Projects" }).click(); await expect(page).toHaveURL(/\/Projects\/$/); await expect(page.locator("main h1")).toContainText("Projects"); // The sidebar expands the active folder's children. await expect(sidebar(page).getByRole("link", { name: "Alpha" })).toBeVisible(); }); test("task checkbox toggle persists", async ({ page }) => { await page.goto("/Projects/"); const box = page.locator("main input.task-checkbox").first(); await expect(box).not.toBeChecked(); await box.check(); await expect(box).toBeEnabled(); await expect.poll(() => readFileSync(join(wiki, "Projects", "index.md"), "utf8")).toContain("- [x] first task"); await page.reload(); await expect(page.locator("main input.task-checkbox").first()).toBeChecked(); }); test("view settings switch the listing to thumbnails", async ({ page }) => { await page.goto("/Projects/"); await expect(page.locator("main table.data-table")).toBeVisible(); await page.getByTitle("View & sorting").click(); await modal(page).getByRole("button", { name: "Thumbnail" }).click(); await modal(page).getByRole("button", { name: "SAVE" }).click(); await expect(page.locator("main .thumb-grid")).toBeVisible(); await expect(page).toHaveURL(/\/Projects\/$/); }); test("editor saves a page", async ({ page }) => { await page.goto("/Projects/Alpha/"); await pageAction(page, "Edit page (E)"); await expect(page).toHaveURL(/\?edit/); const editor = page.locator(".cm-content"); await editor.click(); await page.keyboard.press("Control+End"); await page.keyboard.type("\nAdded by e2e.\n"); await page.getByRole("button", { name: "SAVE" }).click(); await expect(page).toHaveURL(/\/Projects\/Alpha\/$/); await expect(page.locator("main")).toContainText("Added by e2e."); // Save replaces the editor's history entry: Back leaves the page entirely. await page.goBack(); await expect(page).not.toHaveURL(/\?edit/); }); test("move renames a page", async ({ page }) => { await page.goto("/Projects/Beta/"); await pageAction(page, "Move page (M)"); // Tree picker preselects the current parent; keep it. await modal(page).getByRole("button", { name: "NEXT" }).click(); await modal(page).locator("input").fill("Gamma"); await modal(page).getByRole("button", { name: "MOVE" }).click(); await expect(page).toHaveURL(/\/Projects\/Gamma\/$/); await expect(page.locator("main h1")).toContainText("Beta"); }); test("move onto an empty folder offers a merge", async ({ page }) => { mkdirSync(join(wiki, "Projects", "Empty"), { recursive: true }); await page.goto("/Projects/Alpha/"); await pageAction(page, "Move page (M)"); await modal(page).getByRole("button", { name: "NEXT" }).click(); await modal(page).locator("input").fill("Empty"); await modal(page).getByRole("button", { name: "MOVE" }).click(); await expect(modal(page)).toContainText("Merge folders?"); await modal(page).getByRole("button", { name: "MERGE" }).click(); await expect(page).toHaveURL(/\/Projects\/Empty\/$/); await expect(page.locator("main h1")).toContainText("Alpha"); }); test("delete removes a page and returns to the parent", async ({ page }) => { await page.goto("/Notes/"); await pageAction(page, "Delete page"); await expect(modal(page)).toContainText("Delete /Notes/"); await modal(page).getByRole("button", { name: "DELETE" }).click(); await expect(page).toHaveURL(/127\.0\.0\.1:\d+\/$/); await expect(sidebar(page).getByRole("link", { name: "Notes" })).toHaveCount(0); });