Render the folder tree on the server
The sidebar tree is part of the page (chain to the current page expanded, active row marked), so it no longer paints late and the sessionStorage snapshot is gone; only the rail's scroll position is kept. Folders are native <details> that lazy-load their children as HTML via htmx. The tree picker hosts the same server-rendered rows with radio inputs, so ?tree returns HTML and the JSON listing is removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -179,3 +179,94 @@ test("a failed move shows the server's reason and keeps the dialog", async ({ pa
|
||||
await expect(modal(page)).toBeVisible();
|
||||
await expect(page).toHaveURL(/\/Mover\/$/);
|
||||
});
|
||||
|
||||
function mkpage(rel, title) {
|
||||
mkdirSync(join(wiki, rel), { recursive: true });
|
||||
writeFileSync(join(wiki, rel, "index.md"), `# ${title}\n`);
|
||||
}
|
||||
|
||||
test("sidebar tree is in the page itself, no fetch on load", async ({ page }) => {
|
||||
const treeRequests = [];
|
||||
page.on("request", (r) => {
|
||||
if (r.url().includes("?tree")) treeRequests.push(r.url());
|
||||
});
|
||||
const res = await page.request.get("/Projects/");
|
||||
expect(await res.text()).toMatch(/class="tree-name tree-folder" href="\/Projects\/"/);
|
||||
await page.goto("/Projects/");
|
||||
await expect(sidebar(page).locator(".tree-row.is-active")).toContainText("Projects");
|
||||
expect(treeRequests).toEqual([]);
|
||||
});
|
||||
|
||||
test("sidebar folders expand lazily and collapse", async ({ page }) => {
|
||||
mkpage("Tree/One/Deep", "Deep");
|
||||
mkpage("Tree/Two", "Two");
|
||||
await page.goto("/");
|
||||
const treeRow = sidebar(page).locator(".tree-row", { hasText: "Tree" });
|
||||
const chevron = treeRow.locator(".tree-chevron");
|
||||
const glyph = () => chevron.evaluate((el) => getComputedStyle(el, "::before").content);
|
||||
await expect(sidebar(page).getByRole("link", { name: "One" })).toHaveCount(0);
|
||||
expect(await glyph()).toBe('"▸"');
|
||||
await chevron.click();
|
||||
await expect(sidebar(page).getByRole("link", { name: "One" })).toBeVisible();
|
||||
expect(await glyph()).toBe('"▾"');
|
||||
// One has a subfolder, Two has none: only One gets a chevron.
|
||||
await expect(sidebar(page).locator("summary.tree-row", { hasText: "One" })).toHaveCount(1);
|
||||
await expect(sidebar(page).locator(".tree-row", { hasText: "Two" }).locator(".tree-chevron")).toHaveClass(/is-leaf/);
|
||||
await treeRow.locator(".tree-chevron").click();
|
||||
await expect(sidebar(page).getByRole("link", { name: "One" })).toBeHidden();
|
||||
});
|
||||
|
||||
test("tree picker selects inside a lazily expanded folder", async ({ page }) => {
|
||||
mkpage("Tree/One/Deep", "Deep");
|
||||
mkpage("Mover2", "Mover2");
|
||||
await page.goto("/Mover2/");
|
||||
await pageAction(page, "Move page (M)");
|
||||
const picker = modal(page).locator(".tree-picker");
|
||||
await picker.locator(".tree-row", { hasText: "Tree" }).locator(".tree-chevron").click();
|
||||
await picker.locator(".tree-row", { hasText: "One" }).locator(".tree-chevron").click();
|
||||
await picker.getByText("Deep", { exact: true }).click();
|
||||
await expect(modal(page).locator(".tree-selected-path")).toHaveText("/Tree/One/Deep");
|
||||
await modal(page).getByRole("button", { name: "NEXT" }).click();
|
||||
await modal(page).getByRole("button", { name: "MOVE" }).click();
|
||||
await expect(page).toHaveURL(/\/Tree\/One\/Deep\/Mover2\/$/);
|
||||
});
|
||||
|
||||
test("tree picker can pick the root", async ({ page }) => {
|
||||
mkpage("Tree/Two", "Two");
|
||||
await page.goto("/Tree/Two/");
|
||||
await pageAction(page, "Move page (M)");
|
||||
// The current parent is preselected and its chain expanded.
|
||||
await expect(modal(page).locator(".tree-selected-path")).toHaveText("/Tree");
|
||||
await modal(page).locator(".tree-picker").getByText("/", { exact: true }).click();
|
||||
await modal(page).getByRole("button", { name: "NEXT" }).click();
|
||||
await modal(page).getByPlaceholder("Page name").fill("TwoTop");
|
||||
await modal(page).getByRole("button", { name: "MOVE" }).click();
|
||||
await expect(page).toHaveURL(/127\.0\.0\.1:\d+\/TwoTop\/$/);
|
||||
});
|
||||
|
||||
test("editor embed picker selects a file", async ({ page }) => {
|
||||
mkpage("Embeds", "Embeds");
|
||||
writeFileSync(join(wiki, "Embeds", "photo.jpg"), "x");
|
||||
await page.goto("/Embeds/?edit");
|
||||
await page.locator(".cm-content").click();
|
||||
await page.keyboard.press("Alt+Shift+G");
|
||||
const picker = modal(page).locator(".tree-picker");
|
||||
// Folder mode rows aren't selectable here; the file is.
|
||||
await picker.getByText("photo.jpg").click();
|
||||
await expect(modal(page).locator(".tree-selected-path")).toHaveText("/Embeds/photo.jpg");
|
||||
await modal(page).getByRole("button", { name: "NEXT" }).click();
|
||||
await modal(page).getByRole("button", { name: "INSERT" }).click();
|
||||
await expect(page.locator(".cm-content")).toContainText("photo.jpg");
|
||||
});
|
||||
|
||||
test("mobile: FOLDERS opens the tree in an overlay", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 400, height: 800 });
|
||||
await page.goto("/Projects/");
|
||||
await expect(sidebar(page)).toBeHidden();
|
||||
await page.getByRole("button", { name: "ACTIONS" }).click();
|
||||
await page.getByTitle("Folders").click();
|
||||
const overlay = page.locator(".overlay");
|
||||
await expect(overlay.getByRole("link", { name: "Projects" })).toBeVisible();
|
||||
await overlay.getByRole("link", { name: "Projects" }).click();
|
||||
await expect(overlay).toHaveCount(0);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user