fix(ui): make Pagination usable server-rendered, surface output errors
Pagination reported the requested page only through its change output, so on a server-rendered page the controls did nothing: the parent had to own client state to react, and the emitted page never became a URL. An optional hrefTemplate now renders the steps and page numbers as anchors, which work before hydration and without JavaScript and give each page a crawlable URL. Buttons remain the default for client-owned lists. End steps are clamped and marked disabled rather than linking past the first or last page. Output handler errors are no longer swallowed. Nothing awaits invokeOutput, so a handler that threw became an unhandled rejection that never reached the console and presented as a control that silently does nothing. Handler errors are now reported as WRN-DEV-OUTPUT-HANDLER-ERROR. Regenerates the component reference and the UI visual contract. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2558,6 +2558,64 @@ test("pagination clamps an out-of-range page instead of rendering nothing", asyn
|
||||
expect(current!.textContent).toContain("3");
|
||||
});
|
||||
|
||||
test("pagination renders links when given an href template", async () => {
|
||||
const source = readFileSync(uiComponentPath("Pagination"), "utf8");
|
||||
const html = await renderComponent(source, {
|
||||
page: 2,
|
||||
pageSize: 10,
|
||||
total: 50,
|
||||
variant: "numbered",
|
||||
hrefTemplate: "/news?page={page}",
|
||||
});
|
||||
const dom = mountHtml(html);
|
||||
|
||||
// Server-rendered navigation must be anchors, so the controls work before
|
||||
// hydration and without JavaScript.
|
||||
expect(dom.querySelector(".wrn-pagination__step button")).toBeNull();
|
||||
const steps = [...dom.querySelectorAll("a.wrn-pagination__step")] as HTMLElement[];
|
||||
expect(steps.length).toBe(2);
|
||||
expect(steps[0]!.getAttribute("href")).toBe("/news?page=1");
|
||||
expect(steps[1]!.getAttribute("href")).toBe("/news?page=3");
|
||||
|
||||
const pages = [...dom.querySelectorAll("a.wrn-pagination__page")] as HTMLElement[];
|
||||
expect(pages.some((page) => page.getAttribute("href") === "/news?page=5")).toBe(true);
|
||||
});
|
||||
|
||||
test("pagination href steps are disabled and clamped at both ends", async () => {
|
||||
const source = readFileSync(uiComponentPath("Pagination"), "utf8");
|
||||
const first = mountHtml(
|
||||
await renderComponent(source, {
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 30,
|
||||
hrefTemplate: "/news?page={page}",
|
||||
}),
|
||||
);
|
||||
const previous = first.querySelector("a.wrn-pagination__step") as HTMLElement;
|
||||
expect(previous.getAttribute("data-disabled")).toBe("true");
|
||||
// Clamped rather than linking to page 0.
|
||||
expect(previous.getAttribute("href")).toBe("/news?page=1");
|
||||
|
||||
const last = mountHtml(
|
||||
await renderComponent(source, {
|
||||
page: 3,
|
||||
pageSize: 10,
|
||||
total: 30,
|
||||
hrefTemplate: "/news?page={page}",
|
||||
}),
|
||||
);
|
||||
const steps = [...last.querySelectorAll("a.wrn-pagination__step")] as HTMLElement[];
|
||||
expect(steps[1]!.getAttribute("data-disabled")).toBe("true");
|
||||
expect(steps[1]!.getAttribute("href")).toBe("/news?page=3");
|
||||
});
|
||||
|
||||
test("pagination still renders buttons without an href template", async () => {
|
||||
const source = readFileSync(uiComponentPath("Pagination"), "utf8");
|
||||
const dom = mountHtml(await renderComponent(source, { page: 2, pageSize: 10, total: 50 }));
|
||||
expect(dom.querySelectorAll("button.wrn-pagination__step").length).toBe(2);
|
||||
expect(dom.querySelector("a.wrn-pagination__step")).toBeNull();
|
||||
});
|
||||
|
||||
test("pagination survives an empty dataset", async () => {
|
||||
const source = readFileSync(uiComponentPath("Pagination"), "utf8");
|
||||
const html = await renderComponent(source, { page: 1, pageSize: 10, total: 0 });
|
||||
|
||||
Reference in New Issue
Block a user