Serve delete, move and merge dialogs as htmx fragments
The server renders the dialogs (?dialog=delete|move) and answers a move that needs confirmation with the merge dialog itself, so the client no longer builds action URLs or interprets X-Merge-Available. Successful mutations send HX-Redirect, which hx.js turns into a history replace. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+50
-3
@@ -2,7 +2,7 @@
|
||||
// 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 { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
@@ -78,7 +78,7 @@ test("move renames a page", async ({ page }) => {
|
||||
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).getByPlaceholder("Page name").fill("Gamma");
|
||||
await modal(page).getByRole("button", { name: "MOVE" }).click();
|
||||
await expect(page).toHaveURL(/\/Projects\/Gamma\/$/);
|
||||
await expect(page.locator("main h1")).toContainText("Beta");
|
||||
@@ -89,7 +89,7 @@ test("move onto an empty folder offers a merge", async ({ page }) => {
|
||||
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).getByPlaceholder("Page name").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();
|
||||
@@ -105,3 +105,50 @@ test("delete removes a page and returns to the parent", async ({ page }) => {
|
||||
await expect(page).toHaveURL(/127\.0\.0\.1:\d+\/$/);
|
||||
await expect(sidebar(page).getByRole("link", { name: "Notes" })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("delete replaces the history entry, so Back skips the deleted page", async ({ page }) => {
|
||||
mkdirSync(join(wiki, "Scratch", "Gone"), { recursive: true });
|
||||
writeFileSync(join(wiki, "Scratch", "Gone", "index.md"), "# Gone\n");
|
||||
await page.goto("/Scratch/");
|
||||
await page.goto("/Scratch/Gone/");
|
||||
await pageAction(page, "Delete page");
|
||||
await modal(page).getByRole("button", { name: "DELETE" }).click();
|
||||
await expect(page).toHaveURL(/\/Scratch\/$/);
|
||||
await page.goBack();
|
||||
await expect(page).toHaveURL(/\/Scratch\/$/);
|
||||
});
|
||||
|
||||
test("server dialogs close with Escape and CANCEL", async ({ page }) => {
|
||||
await page.goto("/Projects/");
|
||||
await pageAction(page, "Delete page");
|
||||
await expect(modal(page)).toBeVisible();
|
||||
// CANCEL is focused, so Enter must not delete.
|
||||
await expect(modal(page).getByRole("button", { name: "CANCEL" })).toBeFocused();
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(modal(page)).toHaveCount(0);
|
||||
await pageAction(page, "Delete page");
|
||||
await modal(page).getByRole("button", { name: "CANCEL" }).click();
|
||||
await expect(modal(page)).toHaveCount(0);
|
||||
await expect(page).toHaveURL(/\/Projects\/$/);
|
||||
});
|
||||
|
||||
test("a failed move shows the server's reason and keeps the dialog", async ({ page }) => {
|
||||
mkdirSync(join(wiki, "Taken"), { recursive: true });
|
||||
writeFileSync(join(wiki, "Taken", "index.md"), "# Taken\n");
|
||||
mkdirSync(join(wiki, "Mover"), { recursive: true });
|
||||
writeFileSync(join(wiki, "Mover", "index.md"), "# Mover\n");
|
||||
await page.goto("/Mover/");
|
||||
await pageAction(page, "Move page (M)");
|
||||
await modal(page).getByRole("button", { name: "NEXT" }).click();
|
||||
await modal(page).getByPlaceholder("Page name").fill("Taken");
|
||||
// Dismiss the alert from the handler: an open alert blocks the click.
|
||||
const messages = [];
|
||||
page.on("dialog", (d) => {
|
||||
messages.push(d.message());
|
||||
d.dismiss();
|
||||
});
|
||||
await modal(page).getByRole("button", { name: "MOVE" }).click();
|
||||
await expect.poll(() => messages).toEqual([expect.stringContaining("destination already exists")]);
|
||||
await expect(modal(page)).toBeVisible();
|
||||
await expect(page).toHaveURL(/\/Mover\/$/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user