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"; import { Window } from "happy-dom"; const source = (name: string) => readFileSync(uiComponentPath(name), "utf8"); describe("account shell UI regressions", () => { test("BackToTop is hidden in server HTML unless explicitly always visible", async () => { for (const alwaysVisible of [false, true]) { const html = await renderComponent(source("BackToTop"), { alwaysVisible }); const server = new Window(); try { server.document.body.innerHTML = html; expect(server.document.querySelector(".wrn-back-to-top")?.hasAttribute("hidden")).toBe( !alwaysVisible, ); } finally { await server.happyDOM.close(); } } }); test("Drawer has a self-contained close icon and a compact centered mobile header", async () => { const dom = mountHtml( await renderComponent(source("Drawer"), { title: "Account center", open: true, duration: 0 }), ); const close = dom.querySelector(".wrn-drawer__close") as HTMLButtonElement; expect(close.getAttribute("aria-label")).toBe("Close drawer"); expect(close.querySelector('svg[aria-hidden="true"] path')).not.toBeNull(); expect(close.querySelector('[class*="icon-"]')).toBeNull(); expect(source("Drawer")).toMatch(/\.wrn-drawer__header\s*\{[^}]*align-items: center;/); expect(source("Drawer")).toContain("padding-block: 0.75rem;"); let closed = false; dom.querySelector('[data-ui-component="Drawer"]')?.addEventListener("wrnexus:close", () => { closed = true; }); close.click(); expect(closed).toBe(true); }); test("Dropdown allows anchored mobile menus without changing existing sheet defaults", async () => { for (const mobilePanel of [undefined, "dropdown", "sheet"]) { const dom = mountHtml( await renderComponent(source("Dropdown"), { label: "User", placement: "bottom-end", ...(mobilePanel ? { mobilePanel } : {}), }), ); expect(dom.querySelector(".wrn-dropdown")?.getAttribute("data-mobile-panel")).toBe( mobilePanel ?? "sheet", ); (dom.querySelector(".wrn-dropdown__trigger") as HTMLButtonElement).click(); expect(dom.querySelector(".wrn-dropdown__panel")?.hasAttribute("hidden")).toBe(false); } expect(source("Dropdown")).not.toContain( ".wrn-dropdown[data-placement] .wrn-dropdown__panel {", ); }); test("Dropdown is natively hidden before CSS or hydration and still toggles afterward", async () => { for (const props of [{}, { open: true }, { defaultOpen: true }]) { const html = await renderComponent(source("Dropdown"), { label: "User", ...props }); const server = new Window(); try { server.document.body.innerHTML = html; const closed = !(props.open || props.defaultOpen); for (const selector of [".wrn-dropdown__panel", ".wrn-dropdown__dismiss-layer"]) { expect(server.document.querySelector(selector)?.hasAttribute("hidden")).toBe(closed); } } finally { await server.happyDOM.close(); } } const dom = mountHtml(await renderComponent(source("Dropdown"), { label: "User" })); const trigger = dom.querySelector(".wrn-dropdown__trigger") as HTMLButtonElement; const panel = dom.querySelector(".wrn-dropdown__panel")!; expect(panel.hasAttribute("hidden")).toBe(true); trigger.click(); expect(panel.hasAttribute("hidden")).toBe(false); expect(trigger.getAttribute("aria-expanded")).toBe("true"); trigger.click(); expect(panel.hasAttribute("hidden")).toBe(true); }); 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][data-mobile-panel="sheet"] .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;/); }); }); test('floating selection and preference panels opt into shared boundary handling', async () => { for (const name of ['PreferenceSwitcher', 'ComboBox', 'AdvancedSelect', 'TimePicker', 'DataTable', 'Navbar']) { expect(readFileSync(uiComponentPath(name), 'utf8')).toContain('data-wrn-anchored="true"'); } });