import { expect, test } from "bun:test"; import { readFileSync } from "node:fs"; import { join } from "node:path"; import { compileWireFile } from "../../compiler/src/index.ts"; import { mountHtml, renderComponent } from "../../test/src/index.ts"; import { uiComponentNames, uiComponentsDir, uiCss } from "../src/index.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(join(uiComponentsDir(), "Button.wrn"), "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(85); 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("bundled components do not duplicate another component implementation", () => { const implementations = new Map(); for (const name of uiComponentNames()) { const source = readFileSync(join(uiComponentsDir(), `${name}.wrn`), "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", "Table", "DataTable", "Chart", "FileUpload", "Map", "WysiwygEditor", ]; expect(uiComponentNames()).toEqual(expect.arrayContaining(required)); }); 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(".wire-next--table"); expect(css).toContain('[data-show="false"]'); expect(css).toContain("display: none !important"); }); test("component boundaries have no default margins and expose the canonical class prop", () => { for (const name of uiComponentNames()) { const source = readFileSync(join(uiComponentsDir(), `${name}.wrn`), "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 source = readFileSync(join(uiComponentsDir(), `${name}.wrn`), "utf8"); expect(source).toMatch(/^\s+color\s*=/m); expect(source).toMatch(/^\s+size\s*=/m); } }); // test("component markup keeps user-facing content and data behind props", () => { // for (const name of uiComponentNames()) { // const source = readFileSync(join(uiComponentsDir(), `${name}.wrn`), "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(join(uiComponentsDir(), `${name}.wrn`), "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 = join(uiComponentsDir(), `${name}.wrn`); expect(() => compileWireFile(readFileSync(path, "utf8"), path)).not.toThrow(); } }); test("icon buttons expose their accessible label as a hover and focus tooltip", () => { const button = readFileSync(join(uiComponentsDir(), "Button.wrn"), "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(join(uiComponentsDir(), "Button.wrn"), "utf8"); expect(button).not.toMatch(/^\s+download\s*=/m); expect(button).not.toContain('download="{download}"'); expect(button).toContain("{...attrs}"); }); test("every bundled UI component forwards undeclared native attributes", () => { for (const name of uiComponentNames()) { const path = join(uiComponentsDir(), `${name}.wrn`); const output = compileWireFile(readFileSync(path, "utf8"), path); expect(output).toContain("${__wireSpreadAttrs(__attrs)}"); } }); test("advanced select opens, filters, and selects without reactive handler errors", async () => { const source = readFileSync(join(uiComponentsDir(), "AdvancedSelect.wrn"), "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(join(uiComponentsDir(), "AdvancedSelect.wrn"), "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(join(uiComponentsDir(), "AdvancedSelect.wrn"), "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(join(uiComponentsDir(), "AdvancedSelect.wrn"), "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(join(uiComponentsDir(), "ComboBox.wrn"), "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(join(uiComponentsDir(), "ComboBox.wrn"), "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(join(uiComponentsDir(), "ComboBox.wrn"), "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(join(uiComponentsDir(), "AdvancedSelect.wrn"), "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(join(uiComponentsDir(), "PinInput.wrn"), "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(join(uiComponentsDir(), "PinInput.wrn"), "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(join(uiComponentsDir(), "PinInput.wrn"), "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(join(uiComponentsDir(), "PinInput.wrn"), "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(join(uiComponentsDir(), "StrongPassword.wrn"), "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 popover = dom.querySelector(".wire-next__strong-password-popover") as HTMLElement; 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("toggle password reveals and conceals its value with accessible declared events", async () => { const source = readFileSync(join(uiComponentsDir(), "TogglePassword.wrn"), "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(join(uiComponentsDir(), "TogglePassword.wrn"), "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"); });