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:
2026-08-13 15:46:00 +05:30
co-authored by Claude Opus 5
parent 2e42be7b31
commit 5afb2d1875
6 changed files with 210 additions and 35 deletions
+58
View File
@@ -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 });