Files
WRNexusJS/packages/ui/test/component-audit.test.ts
T
ClintchizandClaude Opus 5.5 1bed8e4b74 feat(ui): component redesign pass and fixes, release ui 0.8.87
Redesign
- Shared field foundation rules (duplicated across 15 form components and
  emitted globally) are now identical everywhere, so later copies no longer
  silently override earlier redesigns.
- Drawn checkbox and radio controls, white switch thumbs (the thumb was the
  text colour, a black blob in light mode), recessed field wells, keycap
  actions in Modal, Drawer, Popover, Toaster, Pagination, Stepper, DataTable.
- Menus (Dropdown, ContextMenu, Popover, Select dropdown, Navbar panels) share
  one flat raised panel with neutral hover rows and mono section labels.
- Alerts with an accent rail and icon chip; card headers without the grey
  band; accordion, collapse, list, list group, timeline, stats, metrics,
  chat, skeleton, avatar group, clipboard, splitter, section, sidebar,
  navbar and upload progress restyled.
- Chart rebuilt: nice y-axis ticks, gridlines, rounded bars, line and area
  modes, hover tooltips, hidden series leave the scale.

Fixes
- Checkbox and radio labels toggle their input again (empty for="" broke it).
- TimePicker kept every clicked time pressed; only the chosen hour and
  minute are pressed now, and nothing is pressed before a value exists.
- ColorPicker hex readout now follows the picked colour.
- Disabled FileInput no longer shows the native file-input text.
- Date and time panels float instead of pushing content.
- ContextMenu opened at the pointer closes on scroll.
- Drawer left/top/bottom placements slide from their own edge again.
- Image-only Card no longer renders an empty body strip.
- Timeline supports item.time, current/upcoming states, sizes and
  compact/alternate variants; showcase demos use distinct variants.
- ChatBubble: delivery receipts (sending/sent/delivered/read/failed/error)
  and meta aligned to its bubble.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:18:13 +05:30

136 lines
5.9 KiB
TypeScript

import { test, expect } from "bun:test";
import { readFileSync } from "node:fs";
import { mountHtml, renderComponent } from "../../test/src/index.ts";
import { uiComponentPath } from "../src/registry.ts";
async function mount(name: string, props: Record<string, unknown>) {
return mountHtml(await renderComponent(readFileSync(uiComponentPath(name), "utf8"), props));
}
test("grouped combobox renders suggestions and commits a selection", async () => {
const dom = await mount("ComboBox", {
name: "teams",
groups: [
{
label: "Product",
options: [
{ value: "design", label: "Design" },
{ value: "engineering", label: "Engineering" },
],
},
],
});
const input = dom.querySelector('input[role="combobox"]') as HTMLInputElement;
input.dispatchEvent(new (dom.window as Window & typeof globalThis).Event("focus"));
const option = dom.querySelector('[data-option-value="engineering"]') as HTMLButtonElement;
expect(option).not.toBeNull();
option.click();
expect(input.value).toBe("Engineering");
});
test("toggle count falls back to its first choice when value is empty or invalid", async () => {
for (const value of ["", "unknown"]) {
const dom = await mount("ToggleCount", {
value,
firstValue: "monthly",
items: [{ title: "Team", monthly: 29, annual: 290 }],
});
expect(dom.querySelector('[aria-pressed="true"]')?.textContent).toContain("Monthly");
expect(dom.document.body.textContent).toContain("29");
}
});
test("password leading icon is optional", async () => {
const plain = await mount("TogglePassword", {});
expect(plain.querySelector(".wrn-next__password-leading-icon")).toBeNull();
const decorated = await mount("TogglePassword", { icon: "icon-[lucide--lock]" });
expect(decorated.querySelector(".wrn-next__password-leading-icon")?.className).toContain(
"icon-[lucide--lock]",
);
});
test("button group changes the selected segment", async () => {
const dom = await mount("ButtonGroup", {
items: [
{ label: "Week", value: "week" },
{ label: "Month", value: "month" },
],
value: "week",
selectable: true,
});
(dom.querySelector('[data-value="month"]') as HTMLButtonElement).click();
expect(dom.querySelector('[aria-pressed="true"]')?.textContent).toContain("Month");
});
test("carousel exposes both unframed and framed surfaces", async () => {
for (const framed of [false, true]) {
const dom = await mount("Carousel", {
framed,
items: [{ title: "First" }, { title: "Second" }],
});
expect(dom.querySelector("[data-framed]")?.getAttribute("data-framed")).toBe(String(framed));
}
});
test("rating commits a star selection and readonly rating stays disabled", async () => {
const dom = await mount("Rating", {value: 2});
(dom.querySelector('[aria-label="Rate 4 of 5"]') as HTMLButtonElement).click();
expect(dom.querySelector('output')?.textContent).toContain("4 / 5");
expect(dom.querySelectorAll('[data-filled="true"]').length).toBe(4);
const readonly = await mount("Rating", {readonly:true});
expect((readonly.querySelector('button') as HTMLButtonElement).disabled).toBe(true);
});
test("fractional rating uses the requested step and commits precise values", async () => {
const dom = await mount("Rating", {value:3.5, step:0.2});
const slider = dom.querySelector('input[type="range"]') as HTMLInputElement;
expect(slider.step).toBe("0.2");
slider.value = "4.2";
slider.dispatchEvent(new (dom.window as Window & typeof globalThis).Event("input", {bubbles:true}));
expect(dom.querySelector('output')?.textContent).toContain("4.2 / 5");
});
test("card header and footer can be suppressed independently", async () => {
const dom = await mount("Card", {title:"Hidden title",footer:"Hidden footer",showHeader:false,showFooter:false,description:"Body remains"});
expect(dom.querySelector('.wrn-next__card-header')).toBeNull();
expect(dom.querySelector('.wrn-next__card-footer')).toBeNull();
expect(dom.document.body.textContent).toContain("Body remains");
});
test("map embeds a geographic provider and requires a Google key", async () => {
const osm = await mount("Map", {latitude:12.97,longitude:77.59});
expect(osm.querySelector('iframe')?.getAttribute('src')).toContain('openstreetmap.org/export/embed.html');
const google = await mount("Map", {provider:"google",apiKey:""});
expect(google.querySelector('iframe')).toBeNull();
expect(google.document.body.textContent).toContain('API key');
});
test("chart accepts data arrays and chooses line rendering", async () => {
const dom = await mount("Chart", {type:"line",data:[{label:"Mon",value:12},{label:"Tue",value:18}]});
expect(dom.querySelector('path.wrn-chart__line')).not.toBeNull();
expect(dom.querySelectorAll('.wrn-chart__hit').length).toBe(2);
});
test("calendar Done and outside dismiss both close the panel", async () => {
const dom = await mount("DatePicker", {value:"2026-09-20"});
const trigger = dom.querySelector('.wrn-next__date-trigger') as HTMLButtonElement;
trigger.click();
(dom.querySelector('.wrn-next__date-done') as HTMLButtonElement).click();
expect(trigger.getAttribute('aria-expanded')).toBe('false');
trigger.click();
(dom.querySelector('.wrn-picker-dismiss') as HTMLButtonElement).click();
expect(trigger.getAttribute('aria-expanded')).toBe('false');
});
test("fractional slider increments stay precise and disabled controls do not change", async () => {
const dom = await mount("RangeSlider", {min:0,max:5,value:2.4,step:0.2});
(dom.querySelector('[aria-label="Increase Range"]') as HTMLButtonElement).click();
expect(dom.querySelector('[role="slider"]')?.getAttribute('aria-valuenow')).toBe('2.6');
const locked = await mount("RangeSlider", {value:2,disabled:true});
(locked.querySelector('[role="slider"]') as HTMLElement).dispatchEvent(new (locked.window as Window & typeof globalThis).KeyboardEvent('keydown',{key:'ArrowRight',bubbles:true}));
expect(locked.querySelector('[role="slider"]')?.getAttribute('aria-valuenow')).toBe('2');
});