import { expect, test } from "bun:test"; import { readFileSync } from "node:fs"; import { join } from "node:path"; import { compileWireFile, parse } from "../../compiler/src/index.ts"; import { mountHtml, renderComponent } from "../../test/src/index.ts"; // The filesystem helpers moved to the server-only `registry` subpath; the // package entry deliberately stays free of node:* imports so it can be // bundled for the browser. import { uiComponentNames, uiComponentsDir, uiComponentPath, uiCss } from "../src/registry.ts"; test("bundled UI assets are discoverable and readable", () => { expect(uiComponentNames()).toContain("Button"); expect(uiComponentNames()).toContain("Accordion"); expect(uiComponentNames()).toContain("DataTable"); expect(uiComponentNames()).toContain("WysiwygEditor"); expect(readFileSync(uiComponentPath("Button"), "utf8")).toContain("component Button"); expect(uiCss()).toContain("--wire-"); }); test("generated component reference documents every bundled component and its props", () => { const reference = JSON.parse( readFileSync(join(uiComponentsDir(), "..", "component-reference.json"), "utf8"), ) as { count: number; components: Array<{ name: string; props: Array<{ name: string }> }> }; const catalog = JSON.parse( readFileSync(join(uiComponentsDir(), "..", "component-catalog.json"), "utf8"), ) as { components: Array<{ name: string }> }; expect(reference.count).toBe(uiComponentNames().length); expect(reference.count).toBe(108); expect(reference.components.map((component) => component.name)).toEqual( expect.arrayContaining(uiComponentNames()), ); expect(catalog.components.map((component) => component.name).sort()).toEqual( uiComponentNames().sort(), ); expect(reference.components.find((component) => component.name === "Button")?.props).toEqual( expect.arrayContaining([ expect.objectContaining({ name: "label" }), expect.objectContaining({ name: "as" }), ]), ); expect( reference.components .find((component) => component.name === "PinInput") ?.props.find((prop) => prop.name === "length"), ).toEqual(expect.objectContaining({ default: "4" })); }); test("interactive components expose their must-have public event contracts", () => { const reference = JSON.parse( readFileSync(join(uiComponentsDir(), "..", "component-reference.json"), "utf8"), ) as { components: Array<{ name: string; events: string[] }> }; const eventsByComponent = new Map( reference.components.map((component) => [component.name, component.events]), ); const requiredEvents: Record = { Button: ["click", "focus", "blur"], Input: ["input", "change", "focus", "blur", "invalid"], Select: ["input", "change", "open", "close"], Checkbox: ["input", "change", "focus", "blur", "invalid"], ColorPicker: ["input", "change", "focus", "blur"], FileInput: ["input", "change", "focus", "blur", "select", "clear", "invalid"], InputGroup: ["input", "change", "focus", "blur", "submit", "action"], Radio: ["input", "change", "focus", "blur", "invalid"], RangeSlider: ["input", "change", "focus", "blur"], Switch: ["input", "change", "focus", "blur"], Textarea: ["input", "change", "focus", "blur", "invalid"], TimePicker: ["input", "change", "focus", "blur", "open", "close", "invalid"], Accordion: ["change", "open", "close"], Modal: ["open", "close", "cancel", "confirm"], DataTable: ["sort", "select", "change", "rowClick", "pageChange"], FileUpload: ["select", "upload", "progress", "success", "error", "cancel", "remove"], WysiwygEditor: ["input", "change", "focus", "blur"], }; for (const [component, events] of Object.entries(requiredEvents)) { expect(eventsByComponent.get(component)).toEqual(expect.arrayContaining(events)); } }); test("component reference contains only explicitly declared public events", () => { const reference = JSON.parse( readFileSync(join(uiComponentsDir(), "..", "component-reference.json"), "utf8"), ) as { components: Array<{ name: string; events: string[] }> }; for (const component of reference.components) { const componentPath = uiComponentPath(component.name); const source = readFileSync(componentPath, "utf8"); const ast = parse(source); const declaredEvents = [ ...ast.events.map((event) => event.name), ...ast.outputs.map((output) => output.name), ]; const expectedEvents = [...new Set(declaredEvents)]; if (JSON.stringify(component.events) !== JSON.stringify(expectedEvents)) { console.error("UI EVENT REFERENCE MISMATCH", { component: component.name, componentPath, referenceEvents: component.events, declaredEvents: expectedEvents, }); } expect(component.events).toEqual(expectedEvents); } }); test("bundled components do not duplicate another component implementation", () => { const implementations = new Map(); for (const name of uiComponentNames()) { const source = readFileSync(uiComponentPath(name), "utf8") .replace(/^component\s+[A-Za-z0-9_]+/, "component __NAME__") .replace(/\r\n/g, "\n") .trim(); expect(implementations.get(source)).toBeUndefined(); implementations.set(source, name); } }); test("removed duplicate aliases have valid canonical migration targets", () => { const migration = JSON.parse( readFileSync(join(uiComponentsDir(), "..", "component-migrations.json"), "utf8"), ) as { removedCount: number; replacements: Record }; const names = new Set(uiComponentNames()); expect(Object.keys(migration.replacements)).toHaveLength(migration.removedCount); expect(migration.removedCount).toBe(0); for (const [removed, replacement] of Object.entries(migration.replacements)) { expect(names.has(removed)).toBe(false); expect(names.has(replacement)).toBe(true); } }); test("screenshot-directed canonical components are bundled", () => { const required = [ "Container", "Columns", "Grid", "Typography", "Button", "Accordion", "Alert", "Avatar", "Card", "Carousel", "DatePicker", "Progress", "Spinner", "Timeline", "Navbar", "Tabs", "Sidebar", "Breadcrumb", "Pagination", "Stepper", "Input", "Textarea", "Checkbox", "Radio", "Switch", "Select", "ComboBox", "TimePicker", "Modal", "Drawer", "Popover", "Tooltip", "DataTable", "Chart", "FileUpload", "Map", "WysiwygEditor", ]; expect(uiComponentNames()).toEqual(expect.arrayContaining(required)); }); test("navbar supports brands, nested menus, actions, utility slots, and public events", () => { const source = readFileSync(uiComponentPath("Navbar"), "utf8"); expect(source).toContain('slot name="topbar"'); expect(source).toContain('slot name="actions"'); const contract = parse(source); const defaults = new Map(contract.props.map((prop) => [prop.name, prop.default])); expect(defaults.get("brand")).toBe("{}"); expect(defaults.get("actions")).toBe("[]"); expect(defaults.get("openOnHover")).toBe("false"); expect(defaults.get("maxWidth")).toBe('"full"'); expect(source).toContain("item.children"); expect(source).toContain("child.children"); expect(source).toContain("wire-navbar__dropdown--{item.type || 'dropdown'}"); const outputs = new Set(contract.outputs.map((output) => output.name)); for (const event of ["toggle", "open", "close", "select", "action"]) { expect(outputs.has(event)).toBe(true); } }); test("auth form uses package schemas, disables native validation, and renders a full-width submit control", async () => { const source = readFileSync(uiComponentPath("AuthForm"), "utf8"); expect(source).toContain('return "auth-login"'); expect(source).toContain('data-schema="{schemaName()}"'); expect(source).toContain('data-wrnexus-runtime="auth"'); expect(source).toContain('novalidate="true"'); expect(source).toContain('fullWidth="true"'); const html = await renderComponent(source, { mode: "sign-in" }); expect(html).toContain('data-schema="auth-login"'); expect(html).toContain('data-wrnexus-runtime="auth"'); expect(html).toContain("novalidate"); expect(html).toContain("wire-auth-form__submit"); expect(html).toContain('fullWidth="true"'); }); test("footer supports typed entries, responsive columns, pre/post slots, and events", () => { const source = readFileSync(uiComponentPath("Footer"), "utf8"); expect(source).toMatch(//); expect(source).toMatch(//); expect(source).toMatch(//); expect(source).toMatch(//); expect(source).toContain('item.type === "header"'); expect(source).toContain("wire-footer--columns-{columns}"); const outputs = new Set(parse(source).outputs.map((output) => output.name)); expect(outputs.has("select")).toBe(true); expect(outputs.has("action")).toBe(true); }); test("navbar renders a brand, nested dropdowns, mega groups, and actions", async () => { const html = await renderComponent(readFileSync(uiComponentPath("Navbar"), "utf8"), { brand: { label: "Police Management System", description: "Public portal", href: "/", }, items: [ { label: "Services", type: "dropdown", children: [{ label: "Reports", href: "/reports" }], }, { label: "Safety", type: "mega", columns: 2, children: [ { label: "Resources", children: [{ label: "Guidance", href: "/guidance" }], }, ], }, ], actions: [{ label: "Sign in", href: "/login", variant: "primary" }], }); expect(html).toContain("Police Management System"); expect(html).toContain("Public portal"); expect(html).toContain("Reports"); expect(html).toContain("Guidance"); expect(html).toContain("wire-navbar__dropdown--mega"); expect(html).toContain("data-wrn-navbar"); expect(html).toContain('name="wire-navbar-menu"'); expect(html).toContain("Sign in"); }); test("navbar closes sibling menus and menus close after an outside pointer press", async () => { const html = await renderComponent(readFileSync(uiComponentPath("Navbar"), "utf8"), { items: [ { label: "Services", children: [{ label: "Reports", href: "/reports" }] }, { label: "Safety", children: [{ label: "Guidance", href: "/guidance" }] }, ], }); const dom = mountHtml(html); const menus = Array.from(dom.querySelectorAll(".wire-navbar__dropdown")) as HTMLDetailsElement[]; menus[0].open = true; menus[0].dispatchEvent( new (dom.window as { Event: typeof Event }).Event("toggle", { bubbles: false }), ); menus[1].open = true; menus[1].dispatchEvent( new (dom.window as { Event: typeof Event }).Event("toggle", { bubbles: false }), ); expect(menus[0].open).toBe(false); expect(menus[1].open).toBe(true); dom.document.body.dispatchEvent( new (dom.window as { Event: typeof Event }).Event("pointerdown", { bubbles: true }), ); expect(menus[1].open).toBe(false); }); test("navbar optionally opens menus on hover and renders full-width mode", async () => { const html = await renderComponent(readFileSync(uiComponentPath("Navbar"), "utf8"), { openOnHover: true, maxWidth: "full", items: [{ label: "Services", children: [{ label: "Reports", href: "/reports" }] }], }); const dom = mountHtml(html); const navbar = dom.querySelector("[data-wrn-navbar]"); const menu = dom.querySelector(".wire-navbar__dropdown") as HTMLDetailsElement; expect(navbar?.classList.contains("wire-navbar--width-full")).toBe(true); expect(navbar?.getAttribute("data-open-on-hover")).toBe("true"); menu.dispatchEvent(new (dom.window as { Event: typeof Event }).Event("pointerenter")); expect(menu.open).toBe(true); menu.dispatchEvent(new (dom.window as { Event: typeof Event }).Event("pointerleave")); expect(menu.open).toBe(true); await new Promise((resolve) => setTimeout(resolve, 280)); expect(menu.open).toBe(false); }); test("navbar hover menu stays open while the pointer crosses into its panel", async () => { const html = await renderComponent(readFileSync(uiComponentPath("Navbar"), "utf8"), { openOnHover: true, items: [{ label: "Services", children: [{ label: "Reports", href: "/reports" }] }], }); const dom = mountHtml(html); const menu = dom.querySelector(".wire-navbar__dropdown") as HTMLDetailsElement; const panel = dom.querySelector(".wire-navbar__panel") as HTMLElement; const DomEvent = (dom.window as { Event: typeof Event }).Event; menu.dispatchEvent(new DomEvent("pointerenter")); menu.dispatchEvent(new DomEvent("pointerleave")); panel.dispatchEvent(new DomEvent("pointerenter", { bubbles: true })); await new Promise((resolve) => setTimeout(resolve, 280)); expect(menu.open).toBe(true); }); test("preference switcher keeps only one popover open and closes outside", async () => { const html = await renderComponent( readFileSync(uiComponentPath("PreferenceSwitcher"), "utf8"), {}, ); const dom = mountHtml(html); const menus = Array.from(dom.querySelectorAll(".wire-preferences__menu")) as HTMLDetailsElement[]; const DomEvent = (dom.window as { Event: typeof Event }).Event; menus[0].open = true; menus[0].dispatchEvent(new DomEvent("toggle")); menus[1].open = true; menus[1].dispatchEvent(new DomEvent("toggle")); expect(menus[0].open).toBe(false); expect(menus[1].open).toBe(true); dom.document.body.dispatchEvent(new DomEvent("pointerdown", { bubbles: true })); expect(menus[1].open).toBe(false); }); test("sidebar renders compact grouped submenu navigation and a mobile drawer trigger", async () => { const html = await renderComponent(readFileSync(uiComponentPath("Sidebar"), "utf8"), { label: "Operations", orientation: "vertical", items: [ { label: "Dashboard", href: "/dashboard", value: "dashboard" }, { label: "Case management", children: [ { label: "Assigned cases", href: "/cases", value: "cases" }, { label: "Evidence", href: "/evidence", value: "evidence" }, ], }, ], }); // Classes moved to the BEM naming every other component uses, and the // off-canvas presentation is now Drawer rather than a hand-rolled backdrop. // Nesting via children still works: that is what shipped in 0.8.5. expect(html).toContain("wire-sidebar__launcher"); expect(html).toContain("wire-sidebar__sublist"); expect(html).toContain('data-component="Drawer"'); expect(html).toContain("Assigned cases"); expect(html).toContain("Evidence"); }); test("dropdown renders slotted account triggers, menu structure, and public events", async () => { const source = readFileSync(uiComponentPath("Dropdown"), "utf8"); const html = await renderComponent(source, { label: "Account menu", items: [ { type: "header", label: "Account" }, { label: "Profile", href: "/profile", icon: "icon-[lucide--user]" }, { type: "divider" }, { label: "Sign out", href: "/logout", danger: true }, ], }); expect(source).toContain('slot name="trigger"'); expect(parse(source).outputs.map((output) => output.name)).toContain("select"); expect(html).toContain("wire-dropdown__panel"); expect(html).toContain("Profile"); expect(html).toContain('data-danger="true"'); }); test("footer renders header and link entries with the requested column count", async () => { const html = await renderComponent(readFileSync(uiComponentPath("Footer"), "utf8"), { columns: 4, items: [ { type: "header", label: "Support" }, { type: "link", label: "Contact", href: "/contact" }, ], copyright: "Public service platform", }); expect(html).toContain("wire-footer--columns-4"); expect(html).toContain("wire-footer__column"); expect(html).toContain("wire-footer__column-links"); expect(html).toContain("wire-footer__heading"); expect(html).toContain("Support"); expect(html).toContain('href="/contact"'); expect(html).toContain("Public service platform"); }); test("component-system CSS includes responsive, theme-token, focus, and reduced-motion rules", () => { const css = uiCss(); expect(css).toContain("@media (max-width: 768px)"); expect(css).toContain("@media (max-width: 480px)"); expect(css).toContain("@media (prefers-reduced-motion: reduce)"); expect(css).toContain(":focus-visible"); expect(css).toContain("var(--wire-color-surface)"); expect(css).toContain("min-height: 44px"); expect(css).toContain("--wire-motion-base"); expect(css).toContain("--wire-ease-emphasized"); expect(css).toContain("@keyframes wire-component-enter"); expect(css).toContain("@keyframes wire-dialog-enter"); expect(css).toContain("@media (hover: hover) and (pointer: fine)"); expect(css).toContain("--color-violet-600: var(--wire-color-primary)"); expect(css).toContain("--color-blue-600: var(--wire-color-info)"); expect(css).toContain("--color-red-600: var(--wire-color-danger)"); expect(css).toMatch(/:root\s*\{[^}]*--color-violet-600: var\(--wire-color-primary\)/s); expect(css).toContain(".wire-bg-primary"); expect(css).toContain(".wire-text-muted"); expect(css).toContain(".wire-visually-hidden"); expect(css).toContain(".wire-next--field"); expect(css).toContain('[data-show="false"]'); expect(css).toContain("display: none !important"); /* * Navbar styles now ship with the component rather than from ui.css, so the * whole navigation group follows one convention. The rules themselves are * unchanged; only where they live moved. */ const navbarSource = readFileSync(uiComponentPath("Navbar"), "utf8"); expect(css).not.toContain(".wire-navbar"); expect(navbarSource).toMatch( /\.wire-navbar__brand-copy strong\s*\{[^}]*font-size: 0\.9rem;[^}]*font-weight: 600;/s, ); expect(navbarSource).toMatch( /\.wire-navbar__menu-link,[\s\S]*?font-size: 0\.8125rem;[\s\S]*?font-weight: 500;/, ); expect(css).toMatch( /\.wire-footer__heading\s*\{[^}]*font-size: 0\.8125rem;[^}]*font-weight: 600;/s, ); expect(css).toMatch(/\.wire-footer__link\s*\{[^}]*font-size: 0\.8125rem;[^}]*font-weight: 400;/s); }); test("component boundaries have no default margins and expose the canonical class prop", () => { for (const name of uiComponentNames()) { const source = readFileSync(uiComponentPath(name), "utf8"); const root = source.match(/view\s*\{\s*<[A-Za-z][^>]*>/)?.[0] ?? ""; const classes = root.match(/\bclass="([^"]*)"/)?.[1]?.split(/\s+/) ?? []; expect(classes.filter((token) => /^-?m[trblxy]?-/.test(token))).toEqual([]); expect(source).toMatch(/^\s*class\s*(?::[^=\r\n]+)?=/m); expect(source).not.toMatch(/^\s*className\s*(?::[^=\r\n]+)?=/m); expect(root).toContain("{class}"); } }); test("every component exposes universal color and size configuration", () => { for (const name of uiComponentNames()) { const ast = parse(readFileSync(uiComponentPath(name), "utf8")); const props = new Set(ast.props.map((prop) => prop.name)); expect(props.has("color")).toBe(true); expect(props.has("size")).toBe(true); } }); // test("component markup keeps user-facing content and data behind props", () => { // for (const name of uiComponentNames()) { // const source = readFileSync(uiComponentPath(name), "utf8"); // const view = source.slice(source.indexOf("view {")); // const literalText = [...view.matchAll(/>([^<>{}]*[A-Za-z][^<>{}]*) match[1].trim()) // .filter(Boolean); // const literalAccessibleText = [ // ...view.matchAll(/\b(?:aria-label|title|placeholder|alt)="([^"{}]*[A-Za-z][^"{}]*)"/g), // ]; // const literalDataAttributes = [ // ...view.matchAll(/\b(?:href|src|action|value)="([^"{}]+)"/g), // ].filter((match) => !/^(?:true|false|on)$/.test(match[1])); // expect(literalText).toEqual([]); // expect(literalAccessibleText).toEqual([]); // expect(literalDataAttributes).toEqual([]); // } // }); test("layout boundaries do not add default padding", () => { for (const name of ["Container", "Columns", "Grid", "LayoutSplitter", "Typography"]) { const source = readFileSync(uiComponentPath(name), "utf8"); const root = source.match(/view\s*\{\s*<[A-Za-z][^>]*>/)?.[0] ?? ""; const classes = root.match(/\bclass="([^"]*)"/)?.[1]?.split(/\s+/) ?? []; expect(classes.filter((token) => /^p[trblxy]?-/.test(token))).toEqual([]); } }); test("every bundled UI component compiles", () => { for (const name of uiComponentNames()) { const path = uiComponentPath(name); expect(() => compileWireFile(readFileSync(path, "utf8"), path)).not.toThrow(); } }); test("icon buttons expose their accessible label as a hover and focus tooltip", () => { const button = readFileSync(uiComponentPath("Button"), "utf8"); expect(button).toContain('class="wire-btn__tooltip"'); expect(button).toContain("{ariaLabel || label}"); expect(uiCss()).toContain(".wire-btn:hover > .wire-btn__tooltip"); expect(uiCss()).toContain(".wire-btn:focus-visible > .wire-btn__tooltip"); }); test("button links navigate by default and only download when the native attribute is passed", () => { const button = readFileSync(uiComponentPath("Button"), "utf8"); expect(button).not.toMatch(/^\s+download\s*=/m); expect(button).not.toContain('download="{download}"'); expect(button).toContain("{...attrs}"); }); test("button group renders connected actions and emits select and change events", async () => { const source = readFileSync(uiComponentPath("ButtonGroup"), "utf8"); const html = await renderComponent(source, { items: [ { label: "Profile", value: "profile" }, { label: "Settings", value: "settings" }, { label: "Messages", value: "messages", disabled: true }, ], value: "profile", selectable: true, size: "sm", variant: "outline", ariaLabel: "Account views", }); const dom = mountHtml(html); const group = dom.querySelector(".wire-next--button-group") as HTMLElement; const buttons = [...dom.querySelectorAll(".wire-next--button-group > button")]; const events: Array<{ name: string; detail: unknown }> = []; for (const name of ["select", "change"]) { group.addEventListener(name, (event) => events.push({ name, detail: (event as CustomEvent).detail }), ); } expect(group.getAttribute("role")).toBe("group"); expect(group.getAttribute("aria-label")).toBe("Account views"); expect(group.getAttribute("data-size")).toBe("sm"); expect(buttons).toHaveLength(3); expect(buttons[0]?.getAttribute("aria-pressed")).toBe("true"); expect(buttons[2]?.hasAttribute("disabled")).toBe(true); (buttons[1] as HTMLButtonElement).click(); expect(buttons[0]?.getAttribute("aria-pressed")).toBe("false"); expect(buttons[1]?.getAttribute("aria-pressed")).toBe("true"); expect(events.map((event) => event.name)).toEqual(["select", "change"]); expect((events[1]?.detail as { value: string }).value).toBe("settings"); expect((events[1]?.detail as { previousValue: string }).previousValue).toBe("profile"); }); test("button group supports vertical, responsive, detached, and toolbar layouts", async () => { const source = readFileSync(uiComponentPath("ButtonGroup"), "utf8"); const html = await renderComponent(source, { items: [{ label: "Bold", icon: "icon-[lucide--bold]" }], orientation: "vertical", responsive: true, attached: false, toolbar: true, ariaLabel: "Formatting", }); const dom = mountHtml(html); const group = dom.querySelector(".wire-next--button-group") as HTMLElement; expect(group.getAttribute("role")).toBe("toolbar"); expect(group.getAttribute("aria-orientation")).toBe("vertical"); expect(group.getAttribute("data-responsive")).toBe("true"); expect(group.getAttribute("data-attached")).toBe("false"); expect(group.querySelector('[class*="icon-[lucide--bold]"]')).not.toBeNull(); const css = uiCss(); expect(css).toContain('.wire-next--button-group[data-orientation="vertical"]'); expect(css).toContain('.wire-next--button-group[data-responsive="true"]'); expect(css).toContain('.wire-next--button-group[data-attached="false"]'); }); test("every bundled UI component forwards undeclared native attributes", () => { for (const name of uiComponentNames()) { const path = uiComponentPath(name); const output = compileWireFile(readFileSync(path, "utf8"), path); expect(output).toContain("${__wireSpreadAttrs(__attrs)}"); } }); test("accordion coordinates single-open sections and emits open, close, and change events", async () => { const source = readFileSync(uiComponentPath("Accordion"), "utf8"); const html = await renderComponent(source, { id: "faq", defaultOpen: ["first"], items: [ { value: "first", label: "First", content: "First content" }, { value: "second", label: "Second", content: "Second content" }, ], }); const dom = mountHtml(html); const root = dom.querySelector("[data-wrn-accordion]") as HTMLElement; const events: string[] = []; for (const name of ["open", "close", "change"]) { root.addEventListener(name, () => events.push(name)); } expect(dom.querySelector("#faq-trigger-0")?.getAttribute("aria-expanded")).toBe("true"); expect(dom.querySelector("#faq-panel-0")?.getAttribute("aria-hidden")).toBe("false"); (dom.querySelector("#faq-trigger-1") as HTMLButtonElement).click(); expect(dom.querySelector("#faq-trigger-0")?.getAttribute("aria-expanded")).toBe("false"); expect(dom.querySelector("#faq-trigger-1")?.getAttribute("aria-expanded")).toBe("true"); expect(events).toEqual(["open", "change"]); (dom.querySelector("#faq-trigger-1") as HTMLButtonElement).click(); expect(dom.querySelector("#faq-trigger-1")?.getAttribute("aria-expanded")).toBe("false"); expect(events).toEqual(["open", "change", "close", "change"]); }); test("accordion supports always-open, nested, and disabled sections", async () => { const source = readFileSync(uiComponentPath("Accordion"), "utf8"); const html = await renderComponent(source, { id: "nested-faq", alwaysOpen: true, defaultOpen: ["first", "first.child"], items: [ { value: "first", label: "First", content: "First content", children: [{ value: "child", label: "Child", content: "Child content" }], }, { value: "second", label: "Second", content: "Second content" }, { value: "third", label: "Third", content: "Third content", disabled: true }, ], }); const dom = mountHtml(html); expect(dom.querySelector("#nested-faq-trigger-0")?.getAttribute("aria-expanded")).toBe("true"); expect(dom.querySelector("#nested-faq-trigger-0-0")?.getAttribute("aria-expanded")).toBe("true"); (dom.querySelector("#nested-faq-trigger-1") as HTMLButtonElement).click(); expect(dom.querySelector("#nested-faq-trigger-0")?.getAttribute("aria-expanded")).toBe("true"); expect(dom.querySelector("#nested-faq-trigger-1")?.getAttribute("aria-expanded")).toBe("true"); expect((dom.querySelector("#nested-faq-trigger-2") as HTMLButtonElement).disabled).toBe(true); }); test("accordion chevrons use a consistent icon instead of a font glyph", () => { const source = readFileSync(uiComponentPath("Accordion"), "utf8"); expect(source).toContain("icon-[lucide--chevron-down]"); expect(source).not.toContain('return "⌄"'); }); test("alert renders structured content, lists, actions, and accessible semantics", async () => { const source = readFileSync(uiComponentPath("Alert"), "utf8"); const html = await renderComponent(source, { title: "Cannot save changes", description: "Review the following fields.", color: "danger", variant: "soft", showIcon: true, items: [{ label: "Email is required" }, { label: "Phone number is invalid" }], actions: [{ label: "Review form", href: "#form" }], }); const dom = mountHtml(html); const root = dom.querySelector("[data-wrn-alert]") as HTMLElement; let detail: Record | undefined; root.addEventListener("action", (event) => { detail = (event as unknown as CustomEvent>).detail; }); expect(root.getAttribute("role")).toBe("alert"); expect(root.getAttribute("aria-live")).toBe("polite"); expect(root.dataset.color).toBe("danger"); expect(dom.querySelectorAll(".wire-next__alert-body li").map((node) => node.textContent)).toEqual( ["Email is required", "Phone number is invalid"], ); (dom.querySelector(".wire-next__alert-actions a") as HTMLAnchorElement).click(); expect(detail).toMatchObject({ component: "Alert", action: { label: "Review form", href: "#form" }, }); }); test("dismissible alert hides itself and emits dismiss details", async () => { const source = readFileSync(uiComponentPath("Alert"), "utf8"); const html = await renderComponent(source, { title: "Update available", description: "Install the latest version.", dismissible: true, }); const dom = mountHtml(html); const root = dom.querySelector("[data-wrn-alert]") as HTMLElement; let dismissed = false; root.addEventListener("dismiss", () => { dismissed = true; }); (dom.querySelector(".wire-next__alert-dismiss") as HTMLButtonElement).click(); expect(dismissed).toBe(true); expect(root.getAttribute("aria-hidden")).toBe("true"); expect(root.style.display).toBe("none"); }); test("solid alert palettes preserve readable foregrounds and compact inline layout", () => { const css = uiCss(); expect(css).toContain('.wire-next--alert[data-color="secondary"]'); expect(css).toContain("--wire-alert-color: #737373"); expect(css).toContain( ".wire-next--alert-compact:not(:has(.wire-next__alert-icon)):not(:has(.wire-next__alert-dismiss))", ); expect(css).toContain(".wire-next--alert-compact .wire-next__alert-body > p"); expect(css).toContain("color: currentColor"); }); test("alert exposes configurable radius and elevation treatments", async () => { const source = readFileSync(uiComponentPath("Alert"), "utf8"); const html = await renderComponent(source, { title: "Elevated alert", radius: "xl", shadow: "lg", }); const dom = mountHtml(html); const root = dom.querySelector("[data-wrn-alert]") as HTMLElement; const css = uiCss(); expect(root.dataset.radius).toBe("xl"); expect(root.dataset.shadow).toBe("lg"); expect(css).toContain('.wire-next--alert[data-radius="xl"]'); expect(css).toContain('.wire-next--alert[data-shadow="lg"]'); expect(css).toContain("border-radius: calc(var(--wire-radius, 0.75rem) * 1.75)"); expect(css).toContain("0 20px 48px color-mix(in srgb, #000 24%, transparent)"); }); test("avatar renders images, fallbacks, presence, badges, tooltips, and media copy", async () => { const source = readFileSync(uiComponentPath("Avatar"), "utf8"); const imageHtml = await renderComponent(source, { src: "/avatar.jpg", alt: "Avery Chen", shape: "rounded", statusPosition: "top", status: "online", statusLabel: "Online", tooltip: "Avery Chen · Online", }); const imageDom = mountHtml(imageHtml); const imageRoot = imageDom.querySelector("[data-wrn-avatar]") as HTMLElement; expect(imageRoot.querySelector('img[alt="Avery Chen"]')).not.toBeNull(); expect(imageRoot.querySelector('[data-status="online"]')?.getAttribute("aria-label")).toBe( "Online", ); expect(imageRoot.querySelector('[role="tooltip"]')?.textContent).toContain("Avery Chen"); const initialsDom = mountHtml( await renderComponent(source, { initials: "MW", color: "success", badgeIcon: "icon-[lucide--message-square]", badgeLabel: "Messaging account", name: "Mark Wanner", description: "mark@example.com", }), ); expect(initialsDom.querySelector(".wire-next__avatar-initials")?.textContent).toBe("MW"); expect(initialsDom.querySelector(".wire-next__avatar-badge")?.getAttribute("aria-label")).toBe( "Messaging account", ); expect(initialsDom.querySelector(".wire-next__avatar-copy")?.textContent).toContain( "mark@example.com", ); const placeholderDom = mountHtml(await renderComponent(source, { size: "sm" })); expect(placeholderDom.querySelector(".wire-next__avatar-placeholder")).not.toBeNull(); expect(source).not.toContain("items ="); expect(uiCss()).toContain('.wire-next__avatar-wrap[data-shape="rounded"]'); }); test("avatar group limits members and opens an accessible overflow menu", async () => { const source = readFileSync(uiComponentPath("AvatarGroup"), "utf8"); const html = await renderComponent(source, { maxVisible: 2, borderColor: "#2583ff", items: [ { src: "/one.svg", name: "One" }, { initials: "TW", name: "Two" }, { initials: "TH", name: "Three" }, { initials: "FO", name: "Four" }, ], }); const dom = mountHtml(html); const root = dom.querySelector("[data-wrn-avatar-group]") as HTMLElement; const button = root.querySelector( ".wire-next__avatar-group-overflow-button", ) as HTMLButtonElement; let eventDetail: { open: boolean; hiddenCount: number } | undefined; root.addEventListener("overflow", (event) => { eventDetail = (event as CustomEvent).detail; }); expect(root.querySelectorAll(".wire-next__avatar-group-member")).toHaveLength(2); expect(button.textContent?.trim()).toBe("+2"); expect(button.getAttribute("aria-expanded")).toBe("false"); expect(root.getAttribute("style")).toContain("#2583ff"); expect(root.querySelector(".wire-next__avatar-group-menu")?.getAttribute("data-show")).toBe( "false", ); button.click(); expect( dom.querySelector(".wire-next__avatar-group-overflow-button")?.getAttribute("aria-expanded"), ).toBe("true"); expect(dom.querySelectorAll('[role="menuitem"]')).toHaveLength(2); expect(dom.querySelector(".wire-next__avatar-group-menu")?.getAttribute("data-show")).toBe( "true", ); expect(eventDetail).toEqual(expect.objectContaining({ open: true, hiddenCount: 2 })); expect(uiCss()).toContain('.wire-next--avatar-group[data-layout="grid"]'); }); test("badge supports rich content, anchored placement, animation, and dismissal", async () => { const source = readFileSync(uiComponentPath("Badge"), "utf8"); const html = await renderComponent(source, { label: "Christina", color: "success", variant: "soft", icon: "icon-[lucide--circle-check]", dot: true, avatarSrc: "/avatar.svg", avatarAlt: "Christina", dismissible: true, animated: true, anchorIcon: "icon-[lucide--bell]", anchorLabelText: "Open notifications", placement: "top-right", }); const dom = mountHtml(html); const root = dom.querySelector("[data-wrn-badge]") as HTMLElement; let dismissed = ""; root.addEventListener("dismiss", (event) => { dismissed = (event as CustomEvent).detail.label; }); expect(root.dataset.placement).toBe("top-right"); expect(root.querySelector(".wire-next__badge-anchor")?.getAttribute("aria-label")).toBe( "Open notifications", ); expect(root.querySelector('img[alt="Christina"]')).not.toBeNull(); expect(root.querySelector(".wire-next__badge-dot")).not.toBeNull(); expect(root.querySelector(".wire-next__badge-ping")).not.toBeNull(); (root.querySelector(".wire-next__badge-dismiss") as HTMLButtonElement).click(); expect(dom.querySelector("[data-wrn-badge]")?.getAttribute("data-show")).toBe("false"); expect(dismissed).toBe("Christina"); expect(source).not.toContain("items ="); expect(uiCss()).toContain("@keyframes wire-badge-ping"); }); test("blockquote renders semantic quote and citation content", async () => { const source = readFileSync(uiComponentPath("Blockquote"), "utf8"); const html = await renderComponent(source, { quote: "Design is intelligence made visible.", citation: "Alina Wheeler", citationTitle: "Author, Designing Brand Identity", citationUrl: "https://example.com/source", avatarSrc: "/authors/alina.jpg", avatarAlt: "Alina Wheeler", size: "lg", color: "info", align: "center", }); const dom = mountHtml(html); const figure = dom.querySelector("figure") as HTMLElement; const quote = figure.querySelector("blockquote"); const citation = figure.querySelector("figcaption"); expect(figure.getAttribute("data-size")).toBe("lg"); expect(figure.getAttribute("data-color")).toBe("info"); expect(figure.getAttribute("data-align")).toBe("center"); expect(quote?.getAttribute("cite")).toBe("https://example.com/source"); expect(quote?.textContent).toContain("Design is intelligence made visible."); expect(quote?.querySelector('[aria-hidden="true"]')?.textContent).toBe("“"); expect(citation?.querySelector("cite a")?.textContent).toBe("Alina Wheeler"); expect(citation?.textContent).toContain("Author, Designing Brand Identity"); expect(citation?.querySelector("img")?.getAttribute("alt")).toBe("Alina Wheeler"); }); test("blockquote supports bordered, right-aligned, non-italic, and slotted quotes", async () => { const source = readFileSync(uiComponentPath("Blockquote"), "utf8"); const html = await renderComponent(source, { quote: "", variant: "bordered", align: "right", italic: false, quoteMark: false, }); const dom = mountHtml(html); const figure = dom.querySelector("figure") as HTMLElement; expect(figure.getAttribute("data-variant")).toBe("bordered"); expect(figure.getAttribute("data-align")).toBe("right"); expect(figure.getAttribute("data-italic")).toBe("false"); expect(figure.querySelector(".wire-next__blockquote-mark")).toBeNull(); expect(source).toContain(""); const css = uiCss(); expect(css).toContain('.wire-next--blockquote[data-variant="bordered"]'); expect(css).toContain('.wire-next--blockquote[data-align="center"]'); expect(css).toContain("--wire-blockquote-font-size"); }); test("card renders structured media, header, body, action, and footer content", async () => { const source = readFileSync(uiComponentPath("Card"), "utf8"); const html = await renderComponent(source, { title: "Release notes", subtitle: "Version 2.4", description: "A structured card description.", header: "Featured", footer: "Updated today", imageSrc: "/release.jpg", imageAlt: "Release preview", actionLabel: "Read more", actionHref: "/releases/2-4", size: "lg", color: "info", }); const dom = mountHtml(html); const card = dom.querySelector(".wire-next--card") as HTMLElement; expect(card.getAttribute("data-size")).toBe("lg"); expect(card.getAttribute("data-color")).toBe("info"); expect(card.querySelector("article")).not.toBeNull(); expect(card.querySelector("header")?.textContent).toContain("Featured"); expect(card.querySelector("img")?.getAttribute("alt")).toBe("Release preview"); expect(card.querySelector("h3")?.textContent).toBe("Release notes"); expect(card.querySelector(".wire-next__card-subtitle")?.textContent).toBe("Version 2.4"); expect(card.querySelector(".wire-next__card-action")?.getAttribute("href")).toBe("/releases/2-4"); expect(card.querySelector("footer")?.textContent).toContain("Updated today"); }); test("card navigation, header actions, and dismissal emit public events", async () => { const source = readFileSync(uiComponentPath("Card"), "utf8"); const html = await renderComponent(source, { title: "Account", header: "Card actions", navigation: [ { label: "Profile", value: "profile" }, { label: "Security", value: "security" }, ], activeNav: "profile", headerActions: [{ label: "Refresh", icon: "icon-[lucide--refresh-cw]" }], dismissible: true, }); const dom = mountHtml(html); const card = dom.querySelector(".wire-next--card") as HTMLElement; const events: string[] = []; for (const name of ["navigate", "action", "dismiss"]) { card.addEventListener(name, () => events.push(name)); } (dom.querySelectorAll(".wire-next__card-tabs button")[1] as HTMLButtonElement).click(); (dom.querySelector(".wire-next__card-header-actions button") as HTMLButtonElement).click(); (dom.querySelectorAll(".wire-next__card-header-actions button")[1] as HTMLButtonElement).click(); expect(events).toEqual(["navigate", "action", "dismiss"]); expect(dom.querySelector(".wire-next__card-panel")?.hasAttribute("hidden")).toBe(true); }); test("card supports overlays, horizontal media, groups, alerts, empty and scrolling panels", async () => { const source = readFileSync(uiComponentPath("Card"), "utf8"); const overlay = mountHtml( await renderComponent(source, { title: "Overlay", description: "Overlay copy", imageSrc: "/overlay.jpg", imagePosition: "overlay", }), ); const group = mountHtml( await renderComponent(source, { items: [ { title: "First", description: "First card" }, { title: "Second", description: "Second card" }, ], }), ); const panel = mountHtml( await renderComponent(source, { title: "", alertTitle: "Attention", alertDescription: "Review this state.", empty: true, scrollable: true, maxHeight: "12rem", }), ); expect(overlay.querySelector(".wire-next__card-overlay")).not.toBeNull(); expect(group.querySelectorAll(".wire-next__card-group > article")).toHaveLength(2); expect(panel.querySelector(".wire-next__card-alert")?.getAttribute("role")).toBe("status"); expect(panel.querySelector(".wire-next__card-empty")?.textContent).toContain("No data to show"); expect(panel.querySelector(".wire-next__card-body")?.getAttribute("data-scrollable")).toBe( "true", ); const css = uiCss(); expect(css).toContain('.wire-next--card[data-layout="horizontal"]'); expect(css).toContain('.wire-next--card[data-hover="image"]'); expect(css).toContain(".wire-next__card-group"); expect(css).toContain(".wire-next__card-navigation-select"); expect(css).toContain("aspect-ratio: 16 / 9"); expect(css).toContain("rgb(2 6 23 / 0.86)"); }); test("chat bubble renders conversations, avatars, metadata, links, and public events", async () => { const source = readFileSync(uiComponentPath("ChatBubble"), "utf8"); const events: string[] = []; const dom = mountHtml( await renderComponent(source, { ariaLabel: "Support conversation", oneSided: true, showAvatars: true, showMetadata: true, items: [ { direction: "incoming", author: "Support", avatarFallback: "SP", title: "How can we help?", text: "Choose a guide.", bullets: ["Installation", "Components"], links: [{ label: "Installation guide", href: "/installation" }], status: "Sent", timestamp: "10:32", }, { direction: "outgoing", text: "Please retry.", status: "Not sent", statusTone: "danger", action: "retry", actionLabel: "Retry", }, ], }), ); const root = dom.querySelector(".wire-next--chat-bubble") as HTMLElement; for (const [name, label] of [ ["messageClick", "message"], ["avatarClick", "avatar"], ["linkClick", "link"], ["action", "action"], ]) { root.addEventListener(name, () => events.push(label)); } expect(root.getAttribute("role")).toBe("log"); expect(root.getAttribute("aria-label")).toBe("Support conversation"); expect(root.dataset.oneSided).toBe("true"); expect(dom.querySelectorAll(".wire-next__chat-message")).toHaveLength(2); expect(dom.querySelectorAll(".wire-next__chat-content li")).toHaveLength(2); expect(dom.querySelector(".wire-next__chat-meta[data-tone='danger']")).not.toBeNull(); expect(uiCss()).toContain("color: var(--wire-color-primary-contrast, #fff)"); (dom.querySelector(".wire-next__chat-content") as HTMLElement).click(); (dom.querySelector(".wire-next__chat-avatar") as HTMLButtonElement).click(); (dom.querySelector(".wire-next__chat-content a") as HTMLAnchorElement).click(); (dom.querySelector(".wire-next__chat-meta button") as HTMLButtonElement).click(); expect(events).toEqual(["message", "avatar", "link", "action"]); }); test("collapse toggles accessible panels and emits open, close, and toggle events", async () => { const source = readFileSync(uiComponentPath("Collapse"), "utf8"); const dom = mountHtml( await renderComponent(source, { ariaLabel: "Product details", initialOpenIndexes: [0], items: [ { id: "product-details", label: "Expand", closeLabel: "Collapse", title: "Details", content: "Additional product information.", }, ], }), ); const root = dom.querySelector(".wire-next--collapse") as HTMLElement; const trigger = dom.querySelector(".wire-next__collapse-trigger") as HTMLButtonElement; const panel = dom.querySelector(".wire-next__collapse-panel") as HTMLElement; const events: string[] = []; for (const name of ["open", "close", "toggle"]) { root.addEventListener(name, () => events.push(name)); } expect(root.getAttribute("aria-label")).toBe("Product details"); expect(trigger.getAttribute("aria-expanded")).toBe("true"); expect(trigger.getAttribute("aria-controls")).toBe("product-details"); expect(panel.dataset.open).toBe("true"); expect(panel.getAttribute("aria-hidden")).toBe("false"); expect(uiCss()).toContain("max-height: 40rem"); expect(trigger.textContent).toContain("Collapse"); trigger.click(); expect(trigger.getAttribute("aria-expanded")).toBe("false"); expect(panel.dataset.open).toBe("false"); expect(panel.getAttribute("aria-hidden")).toBe("true"); expect(events).toEqual(["close", "toggle"]); trigger.click(); expect(panel.dataset.open).toBe("true"); expect(events).toEqual(["close", "toggle", "open", "toggle"]); }); test("collapse supports inline read-more and single or multiple open panels", async () => { const source = readFileSync(uiComponentPath("Collapse"), "utf8"); const items = [ { id: "first-collapse", label: "Read more", preview: "First preview", content: "First" }, { id: "second-collapse", label: "Read more", preview: "Second preview", content: "Second" }, ]; const single = mountHtml(await renderComponent(source, { mode: "inline", items })); const singleTriggers = single.querySelectorAll( ".wire-next__collapse-trigger", ) as HTMLButtonElement[]; singleTriggers[0]?.click(); singleTriggers[1]?.click(); expect(singleTriggers[0]?.getAttribute("aria-expanded")).toBe("false"); expect(singleTriggers[1]?.getAttribute("aria-expanded")).toBe("true"); expect(single.querySelectorAll(".wire-next__collapse-preview")).toHaveLength(2); const multiple = mountHtml(await renderComponent(source, { multiple: true, items })); const multipleTriggers = multiple.querySelectorAll( ".wire-next__collapse-trigger", ) as HTMLButtonElement[]; multipleTriggers[0]?.click(); multipleTriggers[1]?.click(); expect(multipleTriggers[0]?.getAttribute("aria-expanded")).toBe("true"); expect(multipleTriggers[1]?.getAttribute("aria-expanded")).toBe("true"); }); test("basic form fields expose shared labels, variants, states, hints, values, and validation targets", async () => { const { checkField } = await import("../../validation/src/index.ts"); const validation = checkField( { type: "string", optional: false, trim: true, rules: [{ kind: "email", message: "Enter a valid email address." }], }, "not-an-email", ); expect(validation.error).toBe("Enter a valid email address."); const source = readFileSync(uiComponentPath("Input"), "utf8"); const dom = mountHtml( await renderComponent(source, { id: "account-email", name: "email", label: "Email address", placeholder: "you@example.com", value: "not-an-email", type: "email", variant: "floating", icon: "icon-[lucide--mail]", iconPosition: "end", helperText: "Used for account notices.", cornerHint: "Required", error: validation.error, inline: true, required: true, }), ); const root = dom.querySelector(".wire-next--input") as HTMLElement; const input = dom.querySelector("input") as HTMLInputElement; expect(root.dataset.variant).toBe("floating"); expect(root.dataset.inline).toBe("true"); expect(root.dataset.invalid).toBe("true"); expect(input.id).toBe("account-email"); expect(input.name).toBe("email"); expect(input.value).toBe("not-an-email"); expect(input.getAttribute("aria-invalid")).toBe("true"); expect(dom.querySelector("[data-error='email']")?.textContent).toContain( "Enter a valid email address.", ); expect(dom.querySelector(".wire-next__field-hint")?.textContent).toContain("Required"); expect(dom.querySelector(".wire-next__field-icon")).not.toBeNull(); const emitted: string[] = []; for (const name of ["input", "change", "focus", "blur", "keydown", "keyup"]) { root.addEventListener(name, () => emitted.push(name)); } input.dispatchEvent(new (dom.window as any).Event("input", { bubbles: true })); input.dispatchEvent(new (dom.window as any).Event("change", { bubbles: true })); input.dispatchEvent(new (dom.window as any).Event("focus", { bubbles: true })); input.dispatchEvent(new (dom.window as any).Event("blur", { bubbles: true })); input.dispatchEvent( new (dom.window as any).KeyboardEvent("keydown", { key: "Enter", bubbles: true }), ); input.dispatchEvent( new (dom.window as any).KeyboardEvent("keyup", { key: "Enter", bubbles: true }), ); expect(emitted).toEqual(["input", "change", "focus", "blur", "keydown", "keyup"]); }); test("all basic form components render values and their must-have interaction events", async () => { const components = [ "Checkbox", "ColorPicker", "FileInput", "Input", "InputGroup", "Radio", "RangeSlider", "Select", "Switch", "Textarea", "TimePicker", ]; for (const component of components) { const source = readFileSync(uiComponentPath(component), "utf8"); const props: Record = { id: `${component}-field`, name: `${component}Value`, label: `${component} label`, helperText: "Helper text", cornerHint: "Optional", error: "", inline: true, readonly: true, disabled: false, value: component === "RangeSlider" ? 40 : "value", options: [{ label: "Value", value: "value" }], }; const dom = mountHtml(await renderComponent(source, props)); const control = dom.querySelector("input, textarea, select") as HTMLElement | null; expect(control).not.toBeNull(); expect(control?.getAttribute("name")).toBe(`${component}Value`); expect(dom.querySelector(".wire-next__field-help")?.textContent).toContain("Helper text"); expect(dom.querySelector(`[data-error="${component}Value"]`)).not.toBeNull(); } }); test("radio renders passed options in group, card, list, alignment, and description layouts", async () => { const source = readFileSync(uiComponentPath("Radio"), "utf8"); const dom = mountHtml( await renderComponent(source, { id: "plan", name: "plan", label: "Choose a plan", value: "professional", orientation: "horizontal", card: true, list: true, rightAligned: true, options: [ { label: "Starter", value: "starter", description: "For individuals." }, { label: "Professional", value: "professional", description: "For teams." }, { label: "Enterprise", value: "enterprise", disabled: true }, ], }), ); const root = dom.querySelector(".wire-next--radio-field") as HTMLElement; const controls = [...dom.querySelectorAll('input[type="radio"]')] as HTMLInputElement[]; expect(root.dataset.orientation).toBe("horizontal"); expect(root.dataset.card).toBe("true"); expect(root.dataset.list).toBe("true"); expect(root.dataset.rightAligned).toBe("true"); expect(controls).toHaveLength(3); expect(controls[1]?.checked).toBe(true); expect(controls[2]?.disabled).toBe(true); expect(dom.querySelectorAll(".wire-next__choice-copy small")).toHaveLength(2); }); test("checkbox renders passed values in group, card, list, alignment, description, and mixed layouts", async () => { const source = readFileSync(uiComponentPath("Checkbox"), "utf8"); const group = mountHtml( await renderComponent(source, { id: "notifications", name: "notifications", label: "Notification preferences", values: ["account"], orientation: "horizontal", card: true, list: true, rightAligned: true, options: [ { label: "Product", value: "product", description: "Product news." }, { label: "Account", value: "account", description: "Security alerts." }, { label: "Partners", value: "partners", disabled: true }, ], }), ); const controls = [...group.querySelectorAll('input[type="checkbox"]')] as HTMLInputElement[]; expect(group.querySelector(".wire-next--checkbox-field")?.getAttribute("data-orientation")).toBe( "horizontal", ); expect(controls).toHaveLength(3); expect(controls[1]?.checked).toBe(true); expect(controls[2]?.disabled).toBe(true); expect(group.querySelectorAll(".wire-next__choice-copy small")).toHaveLength(2); const mixed = mountHtml( await renderComponent(source, { id: "mixed", name: "mixed", indeterminate: true }), ); expect(mixed.querySelector("input")?.getAttribute("aria-checked")).toBe("mixed"); }); test("range slider exposes value, minimum, maximum, step, bounds, and optional marks", async () => { const source = readFileSync(uiComponentPath("RangeSlider"), "utf8"); const dom = mountHtml( await renderComponent(source, { id: "volume", name: "volume", value: 50, min: 0, max: 100, step: 25, showBounds: true, showSteps: true, marks: [ { value: 0, label: "Minimum" }, { value: 50, label: "Middle" }, { value: 100, label: "Maximum" }, ], }), ); const control = dom.querySelector('input[type="number"]') as HTMLInputElement; const slider = dom.querySelector('[role="slider"]') as HTMLElement; expect(control.min).toBe("0"); expect(control.max).toBe("100"); expect(control.step).toBe("25"); expect(slider.getAttribute("aria-valuenow")).toBe("50"); expect(dom.querySelector(".wire-next__range-bounds")?.textContent).toContain("Step 25"); expect(dom.querySelectorAll(".wire-next__slider-marks button")).toHaveLength(3); expect(slider.getAttribute("tabindex")).toBe("0"); expect(source).toContain('sourceEvent.key === "ArrowRight"'); const increase = dom.querySelector('[aria-label="Increase Range"]') as HTMLButtonElement; increase.dispatchEvent(new (dom.window as any).Event("click", { bubbles: true })); expect(dom.querySelector('[role="slider"]')?.getAttribute("aria-valuenow")).toBe("75"); dom .querySelector('[role="slider"]') ?.dispatchEvent( new (dom.window as any).KeyboardEvent("keydown", { key: "ArrowRight", bubbles: true }), ); expect(dom.querySelector('[role="slider"]')?.getAttribute("aria-valuenow")).toBe("100"); const middleMark = dom.querySelectorAll( ".wire-next__slider-marks button", )[1] as HTMLButtonElement; middleMark.dispatchEvent(new (dom.window as any).Event("click", { bubbles: true })); expect(dom.querySelector('[role="slider"]')?.getAttribute("aria-valuenow")).toBe("50"); expect((dom.querySelector('input[type="number"]') as HTMLInputElement).value).toBe("50"); }); test("date picker renders a custom calendar trigger with accessible field state and events", async () => { const source = readFileSync(uiComponentPath("DatePicker"), "utf8"); const dom = mountHtml( await renderComponent(source, { id: "start-date", name: "startDate", label: "Start date", value: "2026-07-28", min: "2026-01-01", max: "2026-12-31", helperText: "Use your local date.", cornerHint: "Required", required: true, }), ); const input = dom.querySelector("input") as HTMLInputElement; expect(input.type).toBe("text"); expect(input.required).toBe(true); expect(dom.querySelector("label")?.getAttribute("for")).toBe("start-date"); expect(dom.querySelector('button[aria-haspopup="dialog"]')).not.toBeNull(); expect(source).not.toContain('type="{type}"'); expect(source).toContain("months ="); expect(dom.querySelector(".wire-next__field-help")?.textContent).toContain("local date"); const trigger = dom.querySelector('button[aria-haspopup="dialog"]') as HTMLButtonElement; expect(dom.querySelector(".wire-next--date-picker")?.getAttribute("data-expanded")).toBe("false"); trigger.dispatchEvent(new (dom.window as any).Event("click", { bubbles: true })); expect(dom.querySelector(".wire-next--date-picker")?.getAttribute("data-expanded")).toBe("true"); expect(dom.querySelector('button[aria-haspopup="dialog"]')?.getAttribute("aria-expanded")).toBe( "true", ); expect(dom.querySelectorAll(".wire-next__calendar-grid button")).toHaveLength(31); const fifteenth = dom.querySelectorAll( ".wire-next__calendar-grid button", )[14] as HTMLButtonElement; fifteenth.dispatchEvent(new (dom.window as any).Event("click", { bubbles: true })); expect(input.value).toBe("2026-07-15"); expect(dom.querySelector(".wire-next__date-trigger span")?.textContent).toBe("2026-07-15"); }); test("time picker uses a custom reactive panel instead of the browser time control", async () => { const source = readFileSync(uiComponentPath("TimePicker"), "utf8"); const dom = mountHtml( await renderComponent(source, { id: "meeting-time", name: "meetingTime", value: "13:30", format: "12", minuteStep: 15, }), ); expect(source).not.toContain('type="time"'); const trigger = dom.querySelector(".wire-next__time-trigger") as HTMLButtonElement; expect(trigger.textContent).toContain("13:30"); trigger.dispatchEvent(new (dom.window as any).Event("click", { bubbles: true })); expect(dom.querySelector(".wire-next--time-picker")?.getAttribute("data-expanded")).toBe("true"); expect(dom.querySelectorAll(".wire-next__time-options").length).toBe(2); expect(dom.querySelectorAll(".wire-next__time-options button")).toHaveLength(36); const nine = [...dom.querySelectorAll(".wire-next__time-options button")].find( (button) => button.textContent?.trim() === "09", ) as HTMLButtonElement; nine.dispatchEvent(new (dom.window as any).Event("click", { bubbles: true })); expect((dom.querySelector('input[name="meetingTime"]') as HTMLInputElement).value).toBe("09:30"); expect(dom.querySelector(".wire-next__time-trigger span")?.textContent).toBe("09:30"); }); test("file upload progress reacts to cancel, retry, and complete actions", async () => { const source = readFileSync(uiComponentPath("FileUploadProgress"), "utf8"); const dom = mountHtml( await renderComponent(source, { fileName: "design-system.zip", fileSize: "20 MB", uploadedSize: "10 MB", value: 50, max: 100, status: "uploading", }), ); expect(dom.querySelector(".wire-next--file-upload-progress")?.getAttribute("data-status")).toBe( "uploading", ); expect(dom.querySelector(".wire-next__row")?.textContent).toContain("design-system.zip"); const buttons = dom.querySelectorAll(".wire-next__upload-actions button") as HTMLButtonElement[]; buttons[0]?.dispatchEvent(new (dom.window as any).Event("click", { bubbles: true })); expect(dom.querySelector(".wire-next--file-upload-progress")?.getAttribute("data-status")).toBe( "cancelled", ); }); test("carousel renders accessible slides, pagination, counter, and navigation events", async () => { const source = readFileSync(uiComponentPath("Carousel"), "utf8"); const html = await renderComponent(source, { ariaLabel: "Featured stories", showPagination: true, showCounter: true, items: [ { title: "First story", description: "First description" }, { title: "Second story", description: "Second description" }, { title: "Third story", description: "Third description" }, ], }); const dom = mountHtml(html); const carousel = dom.querySelector(".wire-next--carousel") as HTMLElement; const scopeRoot = dom.querySelector("[data-wrn-scope]") as HTMLElement; const encodedScope = scopeRoot.getAttribute("data-wrn-scope") ?? ""; const hydratedScope = JSON.parse(Buffer.from(encodedScope, "base64").toString("utf8")) as { items: Array<{ title: string }>; }; const events: string[] = []; for (const name of ["change", "next", "previous"]) { carousel.addEventListener(name, () => events.push(name)); } expect(carousel.getAttribute("role")).toBe("region"); expect(carousel.getAttribute("aria-roledescription")).toBe("carousel"); expect(carousel.getAttribute("aria-label")).toBe("Featured stories"); expect(hydratedScope.items.map((item) => item.title)).toEqual([ "First story", "Second story", "Third story", ]); expect(dom.querySelectorAll('[aria-roledescription="slide"]')).toHaveLength(3); expect(dom.querySelectorAll(".wire-next__carousel-pagination button")).toHaveLength(3); expect(dom.querySelector(".wire-next__carousel-counter")?.textContent).toContain("1 / 3"); (dom.querySelector('[aria-label="Next slide"]') as HTMLButtonElement).click(); expect(dom.querySelector(".wire-next__carousel-counter")?.textContent).toContain("2 / 3"); expect(events).toEqual(["change", "next"]); (dom.querySelector('[aria-label="Previous slide"]') as HTMLButtonElement).click(); expect(events).toEqual(["change", "next", "change", "previous"]); }); test("carousel supports RTL, multiple slides, dragging, snap, and thumbnail layouts", async () => { const source = readFileSync(uiComponentPath("Carousel"), "utf8"); const items = [{ title: "First" }, { title: "Second" }, { title: "Third" }, { title: "Fourth" }]; const html = await renderComponent(source, { items, slidesPerView: 3, isRTL: true, isDraggable: true, isAutoHeight: true, thumbnails: "vertical", }); const dom = mountHtml(html); const carousel = dom.querySelector(".wire-next--carousel") as HTMLElement; const track = dom.querySelector(".wire-next__carousel-track") as HTMLElement; expect(carousel.getAttribute("dir")).toBe("rtl"); expect(carousel.dataset.draggable).toBe("true"); expect(carousel.dataset.autoHeight).toBe("true"); expect(carousel.dataset.thumbnails).toBe("vertical"); expect(track.getAttribute("style")).toContain("--wire-carousel-per-view: 3"); expect(dom.querySelectorAll(".wire-next__carousel-thumbnails button")).toHaveLength(4); const snapHtml = await renderComponent(source, { items, isSnap: true, isDraggable: true, }); const snapDom = mountHtml(snapHtml); const snapRoot = snapDom.querySelector(".wire-next--carousel") as HTMLElement; expect(snapRoot.dataset.snap).toBe("true"); expect(snapRoot.dataset.draggable).toBe("false"); const css = uiCss(); expect(css).toContain('.wire-next--carousel[data-rtl="true"]'); expect(css).toContain('.wire-next--carousel[data-snap="true"]'); expect(css).toContain('.wire-next--carousel[data-thumbnails="vertical"]'); expect(css).toContain("user-select: none"); expect(css).toContain('.wire-next--carousel[data-centered="true"] .wire-next__carousel-track'); }); test("carousel autoplay timers are available in the browser reactive runtime", async () => { const { getReactiveRuntime } = await import("../../csr/src/index.ts"); const runtime = getReactiveRuntime(); expect(runtime).toContain('name === "setInterval"'); expect(runtime).toContain('name === "clearInterval"'); }); test("carousel snap controls scroll and multiple slides stop at the last full group", async () => { const source = readFileSync(uiComponentPath("Carousel"), "utf8"); const items = [ { title: "First" }, { title: "Second" }, { title: "Third" }, { title: "Fourth" }, { title: "Fifth" }, { title: "Sixth" }, ]; const snapDom = mountHtml( await renderComponent(source, { items, isSnap: true, showPagination: true, }), ); const viewport = snapDom.querySelector(".wire-next__carousel-viewport") as HTMLElement & { scrollTo: (options: ScrollToOptions) => void; }; let scrollLeft: number | undefined; viewport.scrollTo = (options: ScrollToOptions | number) => { scrollLeft = typeof options === "number" ? options : options.left; }; ( snapDom.querySelectorAll(".wire-next__carousel-pagination button")[1] as HTMLButtonElement ).click(); expect(scrollLeft).toBeDefined(); const snapNext = snapDom.querySelector('[aria-label="Next slide"]') as HTMLButtonElement; for (let index = 0; index < items.length; index++) snapNext.click(); const paginationButtons = snapDom.querySelectorAll(".wire-next__carousel-pagination button"); expect(paginationButtons[items.length - 1]?.getAttribute("aria-current")).toBe("true"); expect(snapNext.disabled).toBe(true); const multipleDom = mountHtml( await renderComponent(source, { items, slidesPerView: 3, showCounter: true, }), ); const next = multipleDom.querySelector('[aria-label="Next slide"]') as HTMLButtonElement; for (let index = 0; index < 6; index++) next.click(); expect(multipleDom.querySelector(".wire-next__carousel-counter")?.textContent).toContain("4 / 6"); expect(next.disabled).toBe(true); }); test("carousel centers the last slide and auto-scrolls thumbnail rails", async () => { const source = readFileSync(uiComponentPath("Carousel"), "utf8"); const items = [{ title: "First" }, { title: "Second" }, { title: "Third" }, { title: "Fourth" }]; const centeredDom = mountHtml( await renderComponent(source, { items, isCentered: true, showCounter: true, }), ); const centeredNext = centeredDom.querySelector('[aria-label="Next slide"]') as HTMLButtonElement; for (let index = 0; index < items.length; index++) centeredNext.click(); expect(centeredDom.querySelector(".wire-next__carousel-counter")?.textContent).toContain("4 / 4"); expect(centeredNext.disabled).toBe(true); for (const thumbnails of ["horizontal", "vertical"]) { const dom = mountHtml(await renderComponent(source, { items, thumbnails })); const buttons = dom.querySelectorAll( ".wire-next__carousel-thumbnails button", ) as HTMLButtonElement[]; let scrolled = false; const rail = buttons[1]?.parentElement as HTMLElement & { scrollTo: (options: ScrollToOptions) => void; }; rail.scrollTo = () => { scrolled = true; }; (dom.querySelector('[aria-label="Next slide"]') as HTMLButtonElement).click(); expect(scrolled).toBe(true); } }); test("carousel autoplay wraps to the first slide", async () => { const source = readFileSync(uiComponentPath("Carousel"), "utf8"); const html = await renderComponent(source, { isAutoPlay: true, autoplayInterval: 1000, activeIndex: 2, showCounter: true, items: [{ title: "First" }, { title: "Second" }, { title: "Third" }], }); const dom = mountHtml(html); await Bun.sleep(1100); expect(dom.querySelector(".wire-next__carousel-counter")?.textContent).toContain("1 / 3"); dom .querySelector(".wire-next--carousel") ?.dispatchEvent(new (dom.window as any).Event("mouseenter", { bubbles: true })); }); test("input number increments, decrements, applies steps, and respects limits", async () => { const source = readFileSync(uiComponentPath("InputNumber"), "utf8"); const html = await renderComponent(source, { name: "quantity", value: 1, min: 0, max: 5, step: 2, }); const dom = mountHtml(html); const input = () => dom.querySelector('input[name="quantity"]') as HTMLInputElement; const decrement = () => dom.querySelector('[aria-label="Decrease value"]') as HTMLButtonElement; const increment = () => dom.querySelector('[aria-label="Increase value"]') as HTMLButtonElement; expect(input().value).toBe("1"); increment().click(); expect(input().value).toBe("3"); increment().click(); expect(input().value).toBe("5"); expect(increment().disabled).toBe(true); decrement().click(); expect(input().value).toBe("3"); decrement().click(); decrement().click(); expect(input().value).toBe("0"); expect(decrement().disabled).toBe(true); }); test("advanced select opens, filters, and selects without reactive handler errors", async () => { const source = readFileSync(uiComponentPath("AdvancedSelect"), "utf8"); const html = await renderComponent(source, { name: "team", options: [ { value: "design", label: "Design" }, { value: "engineering", label: "Engineering" }, { value: "growth", label: "Growth" }, ], }); const dom = mountHtml(html); const trigger = dom.querySelector(".wire-next__select-trigger") as HTMLButtonElement; expect(trigger).not.toBeNull(); trigger.click(); expect( dom.querySelector(".wire-next--advanced-select")?.classList.contains("wire-next--open"), ).toBe(true); expect(dom.querySelector(".wire-next__select-dropdown")?.getAttribute("data-show")).toBe("true"); expect(dom.querySelector('[role="listbox"]')).not.toBeNull(); const search = dom.querySelector(".wire-next__select-search input") as HTMLInputElement; search.value = "engine"; search.dispatchEvent( new (dom.window as { Event: typeof Event }).Event("input", { bubbles: true }), ); const options = dom.querySelectorAll(".wire-next__select-option") as HTMLButtonElement[]; expect(options.filter((option) => option.style.display !== "none")).toHaveLength(1); options.find((option) => option.style.display !== "none")?.click(); expect(dom.querySelector(".wire-next__select-value > span")?.textContent).toBe("Engineering"); expect(dom.querySelector(".wire-next__select-dropdown")?.getAttribute("data-show")).toBe("false"); (dom.querySelector(".wire-next__clear-select") as HTMLButtonElement).click(); expect(dom.querySelector(".wire-next__select-value > span")?.textContent).toBe( "Select an option", ); expect((dom.querySelector('input[type="hidden"]') as HTMLInputElement).value).toBe(""); }); test("advanced select clears, restores its placeholder, and keeps multiple counters accurate", async () => { const source = readFileSync(uiComponentPath("AdvancedSelect"), "utf8"); const html = await renderComponent(source, { name: "team", multiple: true, values: ["design", "engineering"], showCounter: true, maxSelections: 3, placeholder: "Choose teams", options: [ { value: "design", label: "Design" }, { value: "engineering", label: "Engineering" }, { value: "growth", label: "Growth" }, ], }); const dom = mountHtml(html); const trigger = dom.querySelector(".wire-next__select-trigger") as HTMLButtonElement; const counter = dom.querySelector(".wire-next__row small"); expect(counter?.textContent).toBe("2 / 3 selected"); trigger.click(); const buttons = dom.querySelectorAll(".wire-next__select-option") as HTMLButtonElement[]; const scope = dom.querySelector("[data-wrn-select]") as HTMLElement & { __wrnexusScopeApi?: { get(name: string): unknown; call(name: string, value?: unknown): unknown; }; }; expect(scope.__wrnexusScopeApi?.get("maxSelections")).toBe(3); expect(scope.__wrnexusScopeApi?.call("selectedCount")).toBe(2); expect(scope.__wrnexusScopeApi?.call("canSelect", { value: "growth", label: "Growth" })).toBe( true, ); expect(buttons.find((button) => button.textContent?.includes("Growth"))?.disabled).toBe(false); buttons.find((button) => button.textContent?.includes("Engineering"))?.click(); expect(counter?.textContent).toBe("1 / 3 selected"); buttons.find((button) => button.textContent?.includes("Design"))?.click(); expect(counter?.textContent).toBe("0 / 3 selected"); expect( dom.querySelector(".wire-next__placeholder")?.parentElement?.getAttribute("data-show"), ).toBe("true"); expect((dom.querySelector('input[type="hidden"]') as HTMLInputElement).value).toBe(""); }); test("advanced select closes when a pointer event occurs outside it", async () => { const source = readFileSync(uiComponentPath("AdvancedSelect"), "utf8"); const html = await renderComponent(source, { name: "team", options: [{ value: "design", label: "Design" }], }); const dom = mountHtml(html); (dom.querySelector(".wire-next__select-trigger") as HTMLButtonElement).click(); expect(dom.querySelector(".wire-next__select-dropdown")?.getAttribute("data-show")).toBe("true"); dom.document.body.dispatchEvent( new (dom.window as { Event: typeof Event }).Event("pointerdown", { bubbles: true }), ); expect(dom.querySelector(".wire-next__select-dropdown")?.getAttribute("data-show")).toBe("false"); }); test("advanced select fetches remote options and appends infinite pages", async () => { const source = readFileSync(uiComponentPath("AdvancedSelect"), "utf8"); const originalFetch = globalThis.fetch; const requests: string[] = []; globalThis.fetch = (async (input: string | URL | Request) => { const url = String(input); requests.push(url); const parsedUrl = new URL(url); const page = parsedUrl.searchParams.get("page"); const query = parsedUrl.searchParams.get("q"); return Response.json( page === "1" ? { items: query ? [{ value: "engineering", label: "Engineering" }] : [{ value: "design", label: "Design" }], hasMore: true, } : { items: [{ value: "support", label: "Support" }], hasMore: false }, ); }) as unknown as typeof fetch; try { const html = await renderComponent(source, { name: "team", options: [], remote: true, remoteUrl: "https://example.test/api/teams", infinite: true, hasMore: true, remoteDebounce: 0, }); const dom = mountHtml(html); const root = dom.querySelector("[data-wrn-select]") as HTMLElement & { __wrnexusSelectController?: { attempts: number }; __wrnexusScopeApi?: { get(name: string): unknown }; }; expect(root.getAttribute("data-remote")).toBe("true"); expect(root.getAttribute("data-remote-url")).toBe("https://example.test/api/teams"); expect(root.getAttribute("data-remote-auto-load")).toBe("true"); expect(root.__wrnexusSelectController).toBeDefined(); expect(root.__wrnexusScopeApi).toBeDefined(); expect(root.__wrnexusScopeApi?.get("minSearchLength")).toBe(0); expect(root.__wrnexusSelectController?.attempts).toBe(1); await new Promise((resolve) => setTimeout(resolve, 0)); await new Promise((resolve) => setTimeout(resolve, 0)); expect(requests).toHaveLength(1); expect( dom.querySelectorAll(".wire-next__select-option").map((node) => node.textContent), ).toEqual(["Design"]); (dom.querySelector(".wire-next__select-trigger") as HTMLButtonElement).click(); (dom.querySelector(".wire-next__select-option") as HTMLButtonElement).click(); expect((dom.querySelector('input[type="hidden"]') as HTMLInputElement).value).toBe("design"); expect(dom.querySelector(".wire-next__select-value > span")?.textContent).toBe("Design"); const search = dom.querySelector(".wire-next__select-search input") as HTMLInputElement; search.value = "engine"; search.dispatchEvent( new (dom.window as { Event: typeof Event }).Event("input", { bubbles: true }), ); await new Promise((resolve) => setTimeout(resolve, 0)); await new Promise((resolve) => setTimeout(resolve, 0)); expect(requests.at(-1)).toContain("q=engine"); expect((dom.querySelector('input[type="hidden"]') as HTMLInputElement).value).toBe("design"); expect(dom.querySelector(".wire-next__select-value > span")?.textContent).toBe("Design"); (dom.querySelector("[data-wrn-select-load-more]") as HTMLButtonElement).click(); await new Promise((resolve) => setTimeout(resolve, 0)); await new Promise((resolve) => setTimeout(resolve, 0)); expect( dom.querySelectorAll(".wire-next__select-option").map((node) => node.textContent), ).toEqual(["Engineering", "Support"]); expect(requests).toHaveLength(3); expect(requests[2]).toContain("page=2"); } finally { globalThis.fetch = originalFetch; } }); test("combobox filters editable input, selects suggestions, exposes methods, and clears", async () => { const source = readFileSync(uiComponentPath("ComboBox"), "utf8"); const html = await renderComponent(source, { name: "team", placeholder: "Search teams", options: [ { value: "design", label: "Design", description: "Product design" }, { value: "engineering", label: "Engineering", description: "Platform engineering" }, { value: "growth", label: "Growth", description: "Customer growth" }, ], }); const dom = mountHtml(html); const root = dom.querySelector("[data-wrn-combobox]") as HTMLElement & { wrnexusCombobox?: { open(): void; close(): void; clear(): void; setValue(value: string): void; }; }; const input = dom.querySelector(".wire-next__combobox-input") as HTMLInputElement; const hidden = dom.querySelector('input[type="hidden"]') as HTMLInputElement; expect(root.wrnexusCombobox).toBeDefined(); input.focus(); input.value = "engi"; input.dispatchEvent( new (dom.window as { Event: typeof Event }).Event("input", { bubbles: true }), ); expect( dom .querySelectorAll(".wire-next__select-option") .filter((option) => (option as HTMLElement).style.display !== "none") .map((option) => option.textContent?.trim()), ).toEqual(["EngineeringPlatform engineering"]); input.value = "No matching team"; input.dispatchEvent( new (dom.window as { Event: typeof Event }).Event("input", { bubbles: true }), ); expect(dom.querySelector(".wire-next__select-dropdown")?.getAttribute("data-show")).toBe("false"); expect(input.value).toBe("No matching team"); input.value = "engi"; input.dispatchEvent( new (dom.window as { Event: typeof Event }).Event("input", { bubbles: true }), ); input.dispatchEvent( new (dom.window as { KeyboardEvent: typeof KeyboardEvent }).KeyboardEvent("keydown", { key: "Enter", bubbles: true, }), ); await new Promise((resolve) => setTimeout(resolve, 0)); expect(input.value).toBe("Engineering"); expect(hidden.value).toBe("engineering"); expect(dom.querySelector(".wire-next__select-dropdown")?.getAttribute("data-show")).toBe("false"); root.wrnexusCombobox?.clear(); expect(input.value).toBe(""); expect(hidden.value).toBe(""); root.wrnexusCombobox?.setValue("growth"); expect(input.value).toBe("Growth"); expect(hidden.value).toBe("growth"); }); test("combobox auto-loads remote suggestions and retains its selected value while searching", async () => { const source = readFileSync(uiComponentPath("ComboBox"), "utf8"); const originalFetch = globalThis.fetch; const requests: string[] = []; globalThis.fetch = (async (input: string | URL | Request) => { const url = new URL(String(input)); requests.push(url.toString()); const query = url.searchParams.get("q"); return Response.json({ items: query ? [{ value: "engineering", label: "Engineering" }] : [{ value: "design", label: "Design" }], hasMore: false, }); }) as typeof fetch; try { const html = await renderComponent(source, { name: "remote-team", remote: true, remoteUrl: "https://example.test/api/teams", remoteDebounce: 0, }); const dom = mountHtml(html); const input = dom.querySelector(".wire-next__combobox-input") as HTMLInputElement; await new Promise((resolve) => setTimeout(resolve, 0)); await new Promise((resolve) => setTimeout(resolve, 0)); expect(requests).toHaveLength(1); input.focus(); (dom.querySelector(".wire-next__select-option") as HTMLButtonElement).click(); await new Promise((resolve) => setTimeout(resolve, 0)); expect(input.value).toBe("Design"); expect((dom.querySelector('input[type="hidden"]') as HTMLInputElement).value).toBe("design"); input.value = "engine"; input.dispatchEvent( new (dom.window as { Event: typeof Event }).Event("input", { bubbles: true }), ); await new Promise((resolve) => setTimeout(resolve, 0)); await new Promise((resolve) => setTimeout(resolve, 0)); expect(requests.at(-1)).toContain("q=engine"); expect((dom.querySelector('input[type="hidden"]') as HTMLInputElement).value).toBe(""); (dom.querySelector(".wire-next__select-option") as HTMLButtonElement).click(); await new Promise((resolve) => setTimeout(resolve, 0)); expect(input.value).toBe("Engineering"); expect((dom.querySelector('input[type="hidden"]') as HTMLInputElement).value).toBe( "engineering", ); } finally { globalThis.fetch = originalFetch; } }); test("combobox emits search, select, change, clear, open, and close events", async () => { const source = readFileSync(uiComponentPath("ComboBox"), "utf8"); const html = await renderComponent(source, { name: "events-team", options: [ { value: "design", label: "Design" }, { value: "engineering", label: "Engineering" }, ], }); const dom = mountHtml(html); const root = dom.querySelector("[data-wrn-combobox]") as HTMLElement; const input = dom.querySelector(".wire-next__combobox-input") as HTMLInputElement; const received: Array<{ name: string; detail: Record }> = []; for (const name of ["search", "select", "change", "clear", "open", "close"]) { root.addEventListener(`wrnexus:${name}`, ((event: CustomEvent) => { received.push({ name, detail: event.detail }); }) as EventListener); } input.focus(); await new Promise((resolve) => setTimeout(resolve, 0)); input.value = "engi"; input.dispatchEvent( new (dom.window as { Event: typeof Event }).Event("input", { bubbles: true }), ); input.dispatchEvent( new (dom.window as { KeyboardEvent: typeof KeyboardEvent }).KeyboardEvent("keydown", { key: "Enter", bubbles: true, }), ); await new Promise((resolve) => setTimeout(resolve, 0)); (dom.querySelector(".wire-next__clear-select") as HTMLButtonElement).click(); await new Promise((resolve) => setTimeout(resolve, 0)); expect(received.map((event) => event.name)).toEqual( expect.arrayContaining(["search", "select", "change", "clear", "open", "close"]), ); expect(received.find((event) => event.name === "search")?.detail).toEqual( expect.objectContaining({ component: "ComboBox", query: "engi" }), ); expect(received.find((event) => event.name === "select")?.detail).toEqual( expect.objectContaining({ value: "engineering" }), ); }); test("advanced select emits selection events and remote load and error events", async () => { const source = readFileSync(uiComponentPath("AdvancedSelect"), "utf8"); const originalFetch = globalThis.fetch; const originalConsoleError = console.error; let rejectRequest = false; globalThis.fetch = (async () => { if (rejectRequest) throw new Error("Remote unavailable"); return Response.json({ items: [{ value: "design", label: "Design" }], hasMore: false, }); }) as unknown as typeof fetch; console.error = () => {}; try { const html = await renderComponent(source, { name: "advanced-events", options: [], remote: true, remoteUrl: "https://example.test/api/teams", remoteAutoLoad: true, remoteDebounce: 0, }); const dom = mountHtml(html); const root = dom.querySelector("[data-wrn-select]") as HTMLElement & { __wrnexusSelectController?: { load(append: boolean, allowEmptyQuery?: boolean): void }; }; const received: Array<{ name: string; detail: Record }> = []; for (const name of ["load", "error", "select", "change", "clear"]) { root.addEventListener(`wrnexus:${name}`, ((event: CustomEvent) => { received.push({ name, detail: event.detail }); }) as EventListener); } await new Promise((resolve) => setTimeout(resolve, 0)); await new Promise((resolve) => setTimeout(resolve, 0)); (dom.querySelector(".wire-next__select-trigger") as HTMLButtonElement).click(); (dom.querySelector(".wire-next__select-option") as HTMLButtonElement).click(); await new Promise((resolve) => setTimeout(resolve, 0)); (dom.querySelector(".wire-next__clear-select") as HTMLButtonElement).click(); await new Promise((resolve) => setTimeout(resolve, 0)); rejectRequest = true; root.__wrnexusSelectController?.load(false, true); await new Promise((resolve) => setTimeout(resolve, 0)); await new Promise((resolve) => setTimeout(resolve, 0)); expect(received.map((event) => event.name)).toEqual( expect.arrayContaining(["load", "select", "change", "clear", "error"]), ); expect(received.find((event) => event.name === "load")?.detail).toEqual( expect.objectContaining({ component: "AdvancedSelect", page: 1, hasMore: false, }), ); expect(received.find((event) => event.name === "error")?.detail).toEqual( expect.objectContaining({ component: "AdvancedSelect", message: "Remote unavailable", }), ); } finally { globalThis.fetch = originalFetch; console.error = originalConsoleError; } }); test("pin input accepts matching characters, advances focus, navigates backward, and completes", async () => { const source = readFileSync(uiComponentPath("PinInput"), "utf8"); const html = await renderComponent(source, { name: "verificationCode", length: 4, pattern: "[0-9]", value: "", }); const dom = mountHtml(html); const root = dom.querySelector("[data-wrn-pin-input]") as HTMLElement & { wrnexusPinInput?: { focus(index?: number): void; clear(): void; setValue(value: string): void; getValue(): string; }; }; const cells = dom.querySelectorAll("[data-pin-cell]") as HTMLInputElement[]; const hidden = dom.querySelector("[data-pin-value]") as HTMLInputElement; const events: string[] = []; const publicEvents: string[] = []; for (const name of ["input", "change", "complete", "clear"]) { root.addEventListener(`wrnexus:${name}`, () => events.push(name)); root.addEventListener(name, () => publicEvents.push(name)); } expect(cells).toHaveLength(4); expect(root.wrnexusPinInput).toBeDefined(); cells[0].focus(); cells[0].value = "x"; cells[0].dispatchEvent( new (dom.window as { Event: typeof Event }).Event("input", { bubbles: true }), ); expect(cells[0].value).toBe(""); expect(hidden.value).toBe(""); for (let index = 0; index < 4; index += 1) { cells[index].value = String(index + 1); cells[index].dispatchEvent( new (dom.window as { Event: typeof Event }).Event("input", { bubbles: true }), ); } expect(hidden.value).toBe("1234"); expect(root.getAttribute("data-complete")).toBe("true"); expect(events).toContain("complete"); expect(publicEvents).toContain("complete"); cells[3].value = ""; cells[3].dispatchEvent( new (dom.window as { KeyboardEvent: typeof KeyboardEvent }).KeyboardEvent("keydown", { key: "Backspace", bubbles: true, }), ); expect(cells[2].value).toBe(""); expect(hidden.value).toBe("12"); root.wrnexusPinInput?.setValue("9876"); expect(root.wrnexusPinInput?.getValue()).toBe("9876"); root.wrnexusPinInput?.clear(); expect(hidden.value).toBe(""); expect(events).toContain("clear"); }); test("pin input renders four separate cells when length is omitted", async () => { const source = readFileSync(uiComponentPath("PinInput"), "utf8"); const html = await renderComponent(source, { name: "defaultPin", }); const dom = mountHtml(html); expect(dom.querySelectorAll("[data-pin-cell]")).toHaveLength(4); }); test("pin input distributes filtered clipboard content and emits paste details", async () => { const source = readFileSync(uiComponentPath("PinInput"), "utf8"); const html = await renderComponent(source, { name: "emailCode", length: 6, pattern: "[A-Z0-9]", }); const dom = mountHtml(html); const root = dom.querySelector("[data-wrn-pin-input]") as HTMLElement; const firstCell = dom.querySelector("[data-pin-cell]") as HTMLInputElement; let pasteDetail: Record | undefined; root.addEventListener("wrnexus:paste", ((event: CustomEvent) => { pasteDetail = event.detail; }) as EventListener); const pasteEvent = new (dom.window as { Event: typeof Event }).Event("paste", { bubbles: true, cancelable: true, }); Object.defineProperty(pasteEvent, "clipboardData", { value: { getData: () => "A-1b2 C3" }, }); firstCell.dispatchEvent(pasteEvent); expect((dom.querySelector("[data-pin-value]") as HTMLInputElement).value).toBe("A1B2C3"); expect(pasteDetail).toEqual( expect.objectContaining({ component: "PinInput", pasted: "A1B2C3", value: "A1B2C3", }), ); }); test("pin input accepts lowercase alphanumeric entry and normalizes it to the configured pattern", async () => { const source = readFileSync(uiComponentPath("PinInput"), "utf8"); const html = await renderComponent(source, { name: "securityCode", length: 4, pattern: "[A-Z0-9]", inputMode: "text", }); const dom = mountHtml(html); const cells = dom.querySelectorAll("[data-pin-cell]") as HTMLInputElement[]; cells[0].value = "a"; cells[0].dispatchEvent( new (dom.window as { Event: typeof Event }).Event("input", { bubbles: true }), ); expect(cells[0].value).toBe("A"); expect((dom.querySelector("[data-pin-value]") as HTMLInputElement).value).toBe("A"); }); test("strong password scores input, supports custom special characters, and opens its popover", async () => { const source = readFileSync(uiComponentPath("StrongPassword"), "utf8"); const html = await renderComponent(source, { name: "newPassword", presentation: "popover", specialCharactersSet: "@#", }); const dom = mountHtml(html); const root = dom.querySelector("[data-wrn-strong-password]") as HTMLElement; const input = dom.querySelector('input[name="newPassword"]') as HTMLInputElement; const strengthEvents: Array<{ level: string; percent: number }> = []; root.addEventListener("strength", (event) => { strengthEvents.push( (event as unknown as CustomEvent<{ level: string; percent: number }>).detail, ); }); expect(input.minLength).toBe(8); expect(root.dataset.strength).toBe("empty"); input.value = "Secure#9"; input.dispatchEvent( new (dom.window as { Event: typeof Event }).Event("input", { bubbles: true }), ); expect((dom.querySelector("[data-wrn-strong-password]") as HTMLElement).dataset.strength).toBe( "strong", ); expect((dom.querySelector("[data-wrn-strong-password]") as HTMLElement).dataset.score).toBe("5"); expect(strengthEvents.at(-1)).toMatchObject({ level: "strong", percent: 100 }); const updatedInput = dom.querySelector('input[name="newPassword"]') as HTMLInputElement; updatedInput.value = "Secure!9"; updatedInput.dispatchEvent( new (dom.window as { Event: typeof Event }).Event("input", { bubbles: true }), ); expect((dom.querySelector("[data-wrn-strong-password]") as HTMLElement).dataset.strength).toBe( "good", ); expect(strengthEvents.at(-1)?.percent).toBe(80); const currentInput = dom.querySelector('input[name="newPassword"]') as HTMLInputElement; currentInput.dispatchEvent( new (dom.window as { Event: typeof Event }).Event("focus", { bubbles: true }), ); expect( (dom.querySelector(".wire-next__strong-password-popover") as HTMLElement).dataset.open, ).toBe("true"); }); test("strong password completed requirements use a proper checkmark indicator", () => { const css = uiCss(); expect(css).toContain( '.wire-next__strong-password-requirements li[data-met="true"] > span::after', ); expect(css).toContain("transform: rotate(45deg)"); expect(css).toContain("border-width: 0 0.1rem 0.1rem 0"); expect(css).not.toContain( "linear-gradient(135deg, transparent 42%, var(--wire-color-surface) 42% 52%", ); }); test("toggle count switches pricing values and emits accessible change details", async () => { const source = readFileSync(uiComponentPath("ToggleCount"), "utf8"); const html = await renderComponent(source, { name: "billing", value: "monthly", firstValue: "monthly", firstLabel: "Monthly", secondValue: "annual", secondLabel: "Annual", animate: false, items: [ { label: "Startup", monthly: 19, annual: 190 }, { label: "Team", monthly: 89, annual: 890 }, ], }); const dom = mountHtml(html); const root = dom.querySelector("[data-wrn-toggle-count]") as HTMLElement; let detail: Record | undefined; root.addEventListener("toggle", (event) => { detail = (event as unknown as CustomEvent>).detail; }); expect(root.dataset.value).toBe("monthly"); expect( dom .querySelectorAll(".wire-next__toggle-count-items strong > span") .map((node) => node.textContent), ).toEqual(["19", "89"]); (dom.querySelector('button[aria-pressed="false"]') as HTMLButtonElement).click(); const updatedRoot = dom.querySelector("[data-wrn-toggle-count]") as HTMLElement; expect(updatedRoot.dataset.value).toBe("annual"); expect( dom .querySelectorAll(".wire-next__toggle-count-items strong > span") .map((node) => node.textContent), ).toEqual(["190", "890"]); expect((dom.querySelector('input[name="billing"]') as HTMLInputElement).value).toBe("annual"); expect(dom.querySelector('button[aria-pressed="true"]')?.textContent).toBe("Annual"); expect(detail).toMatchObject({ component: "ToggleCount", name: "billing", value: "annual", previousValue: "monthly", }); }); test("toggle count animates every price upward and downward", async () => { const source = readFileSync(uiComponentPath("ToggleCount"), "utf8"); const html = await renderComponent(source, { value: "monthly", animationDuration: 40, animationSteps: 4, items: [ { label: "Startup", monthly: 2, annual: 10 }, { label: "Team", monthly: 4, annual: 20 }, ], }); const dom = mountHtml(html); const values = () => dom.querySelectorAll("[data-toggle-count-value]").map((node) => Number(node.textContent)); const button = (label: string) => dom .querySelectorAll(".wire-next__toggle-count-segmented button") .find((node) => node.textContent === label) as HTMLButtonElement; button("Annual").click(); expect(values()).toEqual([2, 4]); await new Promise((resolve) => setTimeout(resolve, 15)); expect(values().every((value) => Number.isInteger(value))).toBe(true); await new Promise((resolve) => setTimeout(resolve, 80)); expect(values()).toEqual([10, 20]); button("Monthly").click(); expect(values()).toEqual([10, 20]); await new Promise((resolve) => setTimeout(resolve, 15)); expect(values().every((value) => Number.isInteger(value))).toBe(true); await new Promise((resolve) => setTimeout(resolve, 80)); expect(values()).toEqual([2, 4]); }); test("toggle count switch exposes switch semantics and respects disabled state", async () => { const source = readFileSync(uiComponentPath("ToggleCount"), "utf8"); const html = await renderComponent(source, { variant: "switch", value: "monthly", disabled: true, items: [{ label: "Startup", monthly: 19, annual: 190 }], }); const dom = mountHtml(html); const control = dom.querySelector('[role="switch"]') as HTMLButtonElement; expect(control.getAttribute("aria-checked")).toBe("false"); expect(control.disabled).toBe(true); control.click(); expect((dom.querySelector("[data-wrn-toggle-count]") as HTMLElement).dataset.value).toBe( "monthly", ); }); test("toggle password reveals and conceals its value with accessible declared events", async () => { const source = readFileSync(uiComponentPath("TogglePassword"), "utf8"); const html = await renderComponent(source, { name: "accountPassword", value: "correct-horse", }); const dom = mountHtml(html); const root = dom.querySelector("[data-wrn-toggle-password]") as HTMLElement; const input = dom.querySelector('input[name="accountPassword"]') as HTMLInputElement; const toggle = dom.querySelector(".wire-next__password-toggle") as HTMLButtonElement; const showIcon = dom.querySelector(".wire-next__password-icon--show") as HTMLElement; const visibility: boolean[] = []; root.addEventListener("toggle", (event) => { visibility.push((event as unknown as CustomEvent).detail.visible); }); expect(input.type).toBe("password"); expect(input.pattern).toBe("(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^A-Za-z0-9]).{8,}"); expect(showIcon).not.toBeNull(); expect(toggle.getAttribute("aria-pressed")).toBe("false"); toggle.click(); expect(input.type).toBe("text"); expect(input.value).toBe("correct-horse"); expect(toggle.getAttribute("aria-pressed")).toBe("true"); expect(visibility).toEqual([true]); toggle.click(); expect(input.type).toBe("password"); expect(visibility).toEqual([true, false]); }); test("toggle password supports checkbox control and synchronized password fields", async () => { const source = readFileSync(uiComponentPath("TogglePassword"), "utf8"); const synchronizedHtml = await renderComponent(source, { name: "credentials", fields: [ { label: "New password", name: "newPassword", placeholder: "Enter new password", autocomplete: "new-password", }, { label: "Current password", name: "currentPassword", value: "existing-secret", autocomplete: "current-password", pattern: ".{12,}", }, ], }); const synchronizedDom = mountHtml(synchronizedHtml); const synchronizedInputs = synchronizedDom.querySelectorAll( ".wire-next__password-control > input", ) as HTMLInputElement[]; const synchronizedToggle = synchronizedDom.querySelector( ".wire-next__password-toggle", ) as HTMLButtonElement; expect(synchronizedInputs).toHaveLength(2); expect(synchronizedInputs[0].pattern).toBe( "(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^A-Za-z0-9]).{8,}", ); expect(synchronizedInputs[1].pattern).toBe(".{12,}"); expect(synchronizedInputs.map((input) => input.type)).toEqual(["password", "password"]); synchronizedToggle.click(); expect(synchronizedInputs.map((input) => input.type)).toEqual(["text", "text"]); const checkboxHtml = await renderComponent(source, { name: "checkboxPassword", toggleMode: "checkbox", }); const checkboxDom = mountHtml(checkboxHtml); const checkbox = checkboxDom.querySelector( ".wire-next__password-checkbox input", ) as HTMLInputElement; const checkboxPassword = checkboxDom.querySelector( ".wire-next__password-control > input", ) as HTMLInputElement; checkbox.checked = true; checkbox.dispatchEvent( new (checkboxDom.window as { Event: typeof Event }).Event("change", { bubbles: true }), ); expect(checkboxPassword.type).toBe("text"); }); test("pagination renders windowed page numbers and emits change", async () => { const source = readFileSync(uiComponentPath("Pagination"), "utf8"); const html = await renderComponent(source, { page: 5, pageSize: 10, total: 200, variant: "numbered", siblingCount: 1, }); const dom = mountHtml(html); const root = dom.querySelector(".wire-pagination") as HTMLElement; expect(root.getAttribute("role")).toBe("navigation"); const current = dom.querySelector('.wire-pagination__page[aria-current="page"]'); expect(current!.textContent!.trim()).toBe("5"); expect(root.textContent).toContain("41"); expect(root.textContent).toContain("50"); expect(root.textContent).toContain("200"); }); test("pagination clamps an out-of-range page instead of rendering nothing", async () => { const source = readFileSync(uiComponentPath("Pagination"), "utf8"); const html = await renderComponent(source, { page: 99, pageSize: 10, total: 30 }); const dom = mountHtml(html); const current = dom.querySelector(".wire-pagination__compact"); expect(current!.textContent).toContain("3"); }); test("pagination survives an empty dataset", async () => { const source = readFileSync(uiComponentPath("Pagination"), "utf8"); const html = await renderComponent(source, { page: 1, pageSize: 10, total: 0 }); const dom = mountHtml(html); expect(dom.querySelector(".wire-pagination")).not.toBeNull(); }); test("stepper marks complete, current and upcoming steps", async () => { const source = readFileSync(uiComponentPath("Stepper"), "utf8"); const html = await renderComponent(source, { steps: [ { label: "Account", description: "Your details" }, { label: "Billing", description: "Payment method" }, { label: "Confirm", description: "Review and submit" }, ], active: 1, }); const dom = mountHtml(html); const items = [...dom.querySelectorAll(".wire-stepper__step")]; expect(items).toHaveLength(3); expect(items[0]!.getAttribute("data-status")).toBe("complete"); expect(items[1]!.getAttribute("data-status")).toBe("current"); expect(items[2]!.getAttribute("data-status")).toBe("upcoming"); expect(items[1]!.getAttribute("aria-current")).toBe("step"); expect(dom.querySelector(".wire-stepper")!.tagName.toLowerCase()).toBe("ol"); }); test("stepper renders vertically and clamps an out-of-range active index", async () => { const source = readFileSync(uiComponentPath("Stepper"), "utf8"); const html = await renderComponent(source, { steps: [{ label: "One" }, { label: "Two" }], active: 99, orientation: "vertical", }); const dom = mountHtml(html); const root = dom.querySelector(".wire-stepper") as HTMLElement; expect(root.getAttribute("data-orientation")).toBe("vertical"); const items = [...dom.querySelectorAll(".wire-stepper__step")]; expect(items[1]!.getAttribute("data-status")).toBe("current"); }); test("clickable stepper opts into roving focus", async () => { const source = readFileSync(uiComponentPath("Stepper"), "utf8"); const html = await renderComponent(source, { steps: [{ label: "One" }, { label: "Two" }], active: 0, clickable: true, }); const dom = mountHtml(html); expect(dom.querySelector('[data-wrn-roving="horizontal"]')).not.toBeNull(); expect(dom.querySelectorAll("[data-wrn-roving-item]").length).toBe(2); }); test("nav renders links, marks the active one, and shows icons and badges", async () => { const source = readFileSync(uiComponentPath("Nav"), "utf8"); const html = await renderComponent(source, { items: [ { label: "Home", href: "/", value: "home", icon: "icon-[lucide--house]" }, { label: "Inbox", href: "/inbox", value: "inbox", badge: "9" }, { label: "Archive", href: "/archive", value: "archive", disabled: true }, ], active: "inbox", }); const dom = mountHtml(html); expect(dom.querySelector(".wire-nav")!.getAttribute("role")).toBe("navigation"); const current = dom.querySelector('[aria-current="page"]'); expect(current!.textContent).toContain("Inbox"); // Every item renders a badge span; the ones without a badge are empty and // carry data-show="false", so match on content rather than first-in-document. const badges = [...dom.querySelectorAll(".wire-nav__badge")] .map((node) => node.textContent!.trim()) .filter(Boolean); expect(badges).toContain("9"); expect(dom.querySelector(".wire-nav__icon")).not.toBeNull(); expect(dom.querySelector('[aria-disabled="true"]')).not.toBeNull(); }); test("nav renders a submenu with a disclosure arrow and opts into roving focus", async () => { const source = readFileSync(uiComponentPath("Nav"), "utf8"); const html = await renderComponent(source, { items: [ { label: "Products", value: "products", items: [ { label: "Overview", href: "/p", value: "p-overview" }, { label: "More", value: "p-more", items: [{ label: "Deep", href: "/d", value: "deep" }], }, ], }, ], active: "p-overview", }); const dom = mountHtml(html); expect(dom.querySelector('[data-wrn-roving="horizontal"]')).not.toBeNull(); expect(dom.querySelectorAll("[data-wrn-roving-item]").length).toBeGreaterThan(0); expect(dom.querySelector(".wire-nav__arrow")).not.toBeNull(); expect(dom.querySelector(".wire-nav__submenu")).not.toBeNull(); expect(dom.querySelector(".wire-nav__submenu--level3")).not.toBeNull(); // Every second-level item renders a third-level list; only the one whose // item actually has children is populated, so search across them all. const thirdLevel = [...dom.querySelectorAll(".wire-nav__submenu--level3")] .map((node) => node.textContent!.trim()) .filter(Boolean); expect(thirdLevel.join(" ")).toContain("Deep"); }); test("nav renders its shell with no items and rejects a non-array", async () => { const source = readFileSync(uiComponentPath("Nav"), "utf8"); const html = await renderComponent(source, { items: [], active: "" }); const dom = mountHtml(html); expect(dom.querySelector(".wire-nav")).not.toBeNull(); expect(dom.querySelectorAll(".wire-nav__link").length).toBe(0); // A declared unknown[] prop is coerced by the framework, which rejects a // non-array before the component runs. The itemList() guard is the second // line of defence for a missing prop, not a way to render junk. await expect(renderComponent(source, { items: "not-an-array", active: "" })).rejects.toThrow( "Expected an array prop", ); }); test("nav submenus are anchored so the viewport clamp can pull them back", async () => { const source = readFileSync(uiComponentPath("Nav"), "utf8"); const html = await renderComponent(source, { items: [ { label: "Products", value: "products", items: [{ label: "Overview", href: "/p", value: "p" }], }, ], active: "p", }); const dom = mountHtml(html); // visibility:hidden keeps layout, so the clamp can place a submenu correctly // before it is ever shown -- which is why CSS-driven hover still composes // with the runtime clamp. expect(dom.querySelectorAll(".wire-nav__submenu[data-wrn-anchored]").length).toBeGreaterThan(0); }); test("tabs use wire classes and declare real outputs instead of raw events", async () => { const source = readFileSync(uiComponentPath("Tabs"), "utf8"); // The whole point of the rewrite: themeable wire-* classes, not Tailwind. expect(source).toContain("outputs {"); expect(source).toContain("output.change("); expect(source).not.toContain("new CustomEvent("); const html = await renderComponent(source, { items: [ { label: "Overview", value: "overview", content: "First panel" }, { label: "Pricing", value: "pricing", content: "Second panel" }, ], active: "overview", }); const dom = mountHtml(html); const list = dom.querySelector('[role="tablist"]') as HTMLElement; expect(list.getAttribute("data-wrn-roving")).toBe("horizontal"); const tabs = dom.querySelectorAll('[role="tab"]'); expect(tabs).toHaveLength(2); expect(tabs[0]!.getAttribute("aria-selected")).toBe("true"); expect(tabs[1]!.getAttribute("aria-selected")).toBe("false"); expect(dom.querySelector(".wire-tabs")).not.toBeNull(); expect(dom.querySelectorAll('[role="tabpanel"]')).toHaveLength(2); }); test("tabs in url mode declare the query parameter they sync to", async () => { const source = readFileSync(uiComponentPath("Tabs"), "utf8"); const html = await renderComponent(source, { items: [ { label: "One", value: "one" }, { label: "Two", value: "two" }, ], active: "one", mode: "url", param: "tab", }); const dom = mountHtml(html); const root = dom.querySelector(".wire-tabs") as HTMLElement; expect(root.getAttribute("data-mode")).toBe("url"); expect(root.getAttribute("data-param")).toBe("tab"); /* * In url mode the query parameter is the source of truth rather than * component state. The client router owns popstate and swaps the whole page * on back and forward, discarding component state, so the selection has to * fall out of the URL for history to work at all. */ const source2 = readFileSync(uiComponentPath("Tabs"), "utf8"); expect(source2).toContain("URLSearchParams(window.location.search)"); expect(source2).toContain("history.pushState"); }); test("tabs vertical orientation switches the roving axis", async () => { const source = readFileSync(uiComponentPath("Tabs"), "utf8"); const html = await renderComponent(source, { items: [{ label: "One", value: "one" }], active: "one", orientation: "vertical", }); const dom = mountHtml(html); expect(dom.querySelector('[role="tablist"]')!.getAttribute("data-wrn-roving")).toBe("vertical"); }); test("mega menu renders column groups in an anchored panel", async () => { const source = readFileSync(uiComponentPath("MegaMenu"), "utf8"); const html = await renderComponent(source, { label: "Products", columns: [ { heading: "Platform", items: [ { label: "Runtime", href: "/runtime", description: "The browser half" }, { label: "Compiler", href: "/compiler" }, ], }, { heading: "Company", items: [{ label: "About", href: "/about" }] }, ], }); const dom = mountHtml(html); const trigger = dom.querySelector(".wire-mega__trigger") as HTMLElement; expect(trigger.getAttribute("aria-expanded")).toBe("false"); expect(trigger.getAttribute("aria-haspopup")).toBe("true"); const panel = dom.querySelector(".wire-mega__panel") as HTMLElement; expect(panel.getAttribute("data-wrn-anchored")).toBe("true"); expect(dom.querySelectorAll(".wire-mega__column")).toHaveLength(2); expect(dom.querySelectorAll(".wire-mega__link").length).toBeGreaterThanOrEqual(3); expect(dom.querySelector(".wire-mega__heading")!.textContent).toContain("Platform"); // Single level by design: a mega panel shows breadth flat, so there is no // nested submenu markup to find. expect(source).not.toContain("wire-mega__submenu"); }); test("mega menu columns take roving focus and survive an empty column list", async () => { const source = readFileSync(uiComponentPath("MegaMenu"), "utf8"); const html = await renderComponent(source, { label: "Empty", columns: [] }); const dom = mountHtml(html); expect(dom.querySelector(".wire-mega")).not.toBeNull(); expect(dom.querySelectorAll(".wire-mega__link")).toHaveLength(0); expect(dom.querySelector('[data-wrn-roving="both"]')).not.toBeNull(); }); test("sidebar renders single items, labelled groups and nested levels", async () => { const source = readFileSync(uiComponentPath("Sidebar"), "utf8"); const html = await renderComponent(source, { label: "Workspace", items: [ { label: "Dashboard", href: "/", value: "dash", icon: "icon-[lucide--gauge]" }, { heading: "Projects", items: [ { label: "Active", href: "/active", value: "active" }, { label: "Archive", value: "archive", items: [{ label: "2025", href: "/a/2025", value: "a2025" }], }, ], }, ], active: "active", }); const dom = mountHtml(html); expect(dom.querySelector(".wire-sidebar")).not.toBeNull(); // Every entry renders a heading node; only group entries fill it in, so // match on content rather than first-in-document. const headings = [...dom.querySelectorAll(".wire-sidebar__heading")] .map((node) => node.textContent!.trim()) .filter(Boolean); expect(headings).toContain("Projects"); expect(dom.querySelector('[aria-current="page"]')!.textContent).toContain("Active"); expect(dom.querySelector(".wire-sidebar__icon")).not.toBeNull(); // Third level exists and is reachable. const deep = [...dom.querySelectorAll(".wire-sidebar__sublist--level3")] .map((n) => n.textContent!.trim()) .filter(Boolean); expect(deep.join(" ")).toContain("2025"); expect(dom.querySelector('[data-wrn-roving="vertical"]')).not.toBeNull(); }); test("sidebar composes Drawer for its off-canvas presentation", async () => { const source = readFileSync(uiComponentPath("Sidebar"), "utf8"); // Reusing Drawer means the focus trap and scroll lock come for free rather // than being reimplemented here. expect(source).toContain('import Drawer from "./Drawer.wrn"'); expect(source).toContain(" { const source = readFileSync(uiComponentPath("Scrollspy"), "utf8"); const html = await renderComponent(source, { label: "On this page", items: [ { label: "Overview", href: "#overview" }, { label: "Install", href: "#install" }, { label: "Usage", href: "#usage" }, ], active: "#install", }); const dom = mountHtml(html); const root = dom.querySelector(".wire-scrollspy") as HTMLElement; expect(root.getAttribute("role")).toBe("navigation"); // The runtime owns which link is current; it needs a marker to find the nav. expect(root.getAttribute("data-wrn-scrollspy")).toBe("true"); const links = [...dom.querySelectorAll(".wire-scrollspy__link")]; expect(links).toHaveLength(3); expect(links[1]!.getAttribute("aria-current")).toBe("location"); expect(links[0]!.getAttribute("aria-current")).toBe("false"); expect(links[1]!.getAttribute("href")).toBe("#install"); }); test("scrollspy survives an empty item list", async () => { const source = readFileSync(uiComponentPath("Scrollspy"), "utf8"); const html = await renderComponent(source, { items: [] }); const dom = mountHtml(html); expect(dom.querySelector(".wire-scrollspy")).not.toBeNull(); expect(dom.querySelectorAll(".wire-scrollspy__link")).toHaveLength(0); }); test("navbar uses a valid aria-current and takes roving focus on its menu", async () => { const source = readFileSync(uiComponentPath("Navbar"), "utf8"); /* * aria-current="" is not a valid value -- the attribute has to be absent or * carry a token. Emitting an empty string made every link look like it * declared a state it did not have. */ expect(source).not.toContain("? 'page' : ''"); expect(source).toContain("data-wrn-roving"); const html = await renderComponent(source, { label: "Main", items: [ { label: "Home", href: "/", value: "home" }, { label: "Docs", href: "/docs", value: "docs" }, ], active: "docs", }); const dom = mountHtml(html); const menus = dom.querySelector(".wire-navbar__menus") as HTMLElement; expect(menus.getAttribute("data-wrn-roving")).toBe("horizontal"); /* * Asserted against the rendered markup rather than the parsed DOM: the test * DOM drops aria-current from these anchors, while the served HTML carries * it correctly. */ expect(html).toContain('href="/docs"'); expect(html).toMatch(/aria-current="page"/); expect(html).not.toMatch(/aria-current=""/); }); test("breadcrumb marks the trail end without emitting an empty aria-current", async () => { const source = readFileSync(uiComponentPath("Breadcrumb"), "utf8"); const html = await renderComponent(source, { items: [ { label: "Docs", href: "/docs" }, { label: "Components", href: "/docs/components" }, { label: "Breadcrumb" }, ], }); // aria-current="" is not a valid value; the attribute takes a token. expect(html).not.toMatch(/aria-current=""/); expect(html).toMatch(/aria-current="page"/); const dom = mountHtml(html); expect(dom.querySelector(".wire-breadcrumb")!.getAttribute("aria-label")).toBeTruthy(); expect(dom.querySelectorAll(".wire-breadcrumb__item").length).toBeGreaterThanOrEqual(3); });