Files
WRNexusJS/packages/ui/test/account-shell-regressions.test.ts
T

171 lines
6.4 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import { renderComponent, mountHtml } from "../../test/src/index.ts";
import { uiComponentPath } from "../src/registry.ts";
import { parse } from "../../compiler/src/index.ts";
const source = (name: string) => readFileSync(uiComponentPath(name), "utf8");
describe("account shell UI regressions", () => {
test("Sidebar supplies navigation to its mobile Drawer without an app slot", async () => {
const dom = mountHtml(
await renderComponent(source("Sidebar"), {
items: [
{ heading: "Account", items: [{ label: "Profile", href: "/profile", value: "profile" }] },
],
active: "profile",
}),
);
const mobileNav = dom.querySelector(".wrn-sidebar__drawer-nav");
expect(mobileNav?.querySelector('a[href="/profile"]')?.getAttribute("aria-current")).toBe(
"page",
);
expect(dom.querySelector('.wrn-sidebar__rail a[href="/profile"]')).not.toBeNull();
});
test("Sidebar embedded in another drawer does not generate a second mobile drawer", async () => {
const dom = mountHtml(
await renderComponent(source("Sidebar"), {
mobile: false,
items: [{ label: "Profile", href: "/profile" }],
}),
);
expect(dom.querySelector('[data-component="Drawer"]')).toBeNull();
expect(dom.querySelector(".wrn-sidebar")?.getAttribute("data-mobile")).toBe("false");
});
test("Sidebar blocks disabled links at every nesting level", async () => {
const dom = mountHtml(
await renderComponent(source("Sidebar"), {
items: [
{
label: "Parent",
disabled: true,
href: "/parent",
items: [
{
label: "Child",
disabled: true,
href: "/child",
items: [{ label: "Leaf", disabled: true, href: "/leaf" }],
},
],
},
],
}),
);
for (const href of ["/parent", "/child", "/leaf"]) {
const link = dom.querySelector(`.wrn-sidebar__rail a[href="${href}"]`)!;
expect(link.getAttribute("aria-disabled")).toBe("true");
expect(link.getAttribute("tabindex")).toBe("-1");
const click = new (dom.window as { MouseEvent: typeof MouseEvent }).MouseEvent("click", {
bubbles: true,
cancelable: true,
});
link.dispatchEvent(click);
expect(click.defaultPrevented).toBe(true);
}
});
test("Dropdown keyboard navigation skips disabled links and ArrowUp starts at the last item", async () => {
const dom = mountHtml(
await renderComponent(source("Dropdown"), {
label: "Account",
items: [
{ label: "Profile", href: "/profile" },
{ label: "Disabled", href: "/disabled", disabled: true },
{ label: "Security", href: "/security" },
],
}),
);
const trigger = dom.querySelector(".wrn-dropdown__trigger") as HTMLButtonElement;
// Execute the component's real JS functions: the lightweight mount runtime
// intentionally does not interpret nested timer callbacks.
const functions = parse(source("Dropdown"))
.functions.join("\n")
.replace(/\b(client|shared) function/g, "function");
const handle = new Function(
"document",
"window",
"output",
`
let open = false, visible = false, disabled = false;
${functions}
return handleKeydown;
`,
)(dom.document, dom.window, {
open() {},
close() {},
toggle({ open }: { open: boolean }) {
trigger.setAttribute("aria-expanded", String(open));
},
});
const key = (_target: Element, value: string) =>
handle({
key: value,
currentTarget: dom.querySelector(".wrn-dropdown"),
preventDefault() {},
stopPropagation() {},
});
trigger.focus();
key(trigger, "ArrowUp");
await new Promise((resolve) => setTimeout(resolve, 10));
expect(dom.document.activeElement?.textContent).toContain("Security");
key(dom.document.activeElement!, "ArrowUp");
await new Promise((resolve) => setTimeout(resolve, 10));
expect(dom.document.activeElement?.textContent).toContain("Profile");
key(dom.document.activeElement!, "Escape");
expect(dom.document.activeElement).toBe(trigger);
expect(trigger.getAttribute("aria-expanded")).toBe("false");
});
test("Dropdown preserves falsy action values and has an explicit accessible trigger name", async () => {
const dom = mountHtml(
await renderComponent(source("Dropdown"), {
label: "",
ariaLabel: "Account menu",
items: [{ label: "Zero", value: 0, action: true }],
}),
);
let value: unknown = undefined;
dom.querySelector(".wrn-dropdown")!.addEventListener("action", (event) => {
value = (event as CustomEvent).detail.value;
});
const trigger = dom.querySelector(".wrn-dropdown__trigger") as HTMLButtonElement;
expect(trigger.getAttribute("aria-label")).toBe("Account menu");
trigger.click();
(dom.querySelector("[data-dropdown-item]") as HTMLButtonElement).click();
expect(value).toBe(0);
});
test("Dropdown closes when keyboard focus leaves the component", async () => {
const dom = mountHtml(
await renderComponent(source("Dropdown"), {
label: "Menu",
items: [{ label: "Profile", href: "/profile" }],
}),
);
const trigger = dom.querySelector(".wrn-dropdown__trigger") as HTMLButtonElement;
trigger.click();
const outside = dom.document.createElement("button");
dom.document.body.append(outside);
trigger.dispatchEvent(
new (dom.window as { FocusEvent: typeof FocusEvent }).FocusEvent("focusout", {
bubbles: true,
relatedTarget: outside,
}),
);
expect(trigger.getAttribute("aria-expanded")).toBe("false");
});
test("layout fixes belong to components, not consuming applications", () => {
expect(source("Sidebar")).toMatch(
/\.wrn-sidebar__icon\s*\{[^}]*width: 1.05rem;[^}]*height: 1.05rem;/s,
);
expect(source("Sidebar")).toContain("margin-inline-start");
expect(source("Dropdown")).toContain(".wrn-dropdown[data-placement] .wrn-dropdown__panel");
expect(source("Footer")).toMatch(/\.wrn-footer__copyright-left\s*\{\s*grid-column: 1;/);
expect(source("Footer")).toMatch(/\.wrn-footer__copyright-right\s*\{\s*grid-column: 3;/);
});
});