Refresh UI components and showcase; release updated framework packages
This commit is contained in:
1 parent
d800228585
commit
c77f5e1cec
293 files changed
+22544
-35778
No files matched your search
@@ -0,0 +1,79 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { mountHtml, renderComponent } from "../../test/src/index.ts";
|
||||
import { uiComponentPath } from "../src/registry.ts";
|
||||
|
||||
const source = readFileSync(uiComponentPath("DatePicker"), "utf8");
|
||||
|
||||
test("calendar aligns leap February with Monday-first labels and disables out-of-range dates", async () => {
|
||||
const dom = mountHtml(
|
||||
await renderComponent(source, {
|
||||
value: "2024-02-15",
|
||||
firstDayOfWeek: 1,
|
||||
min: "2024-02-10",
|
||||
max: "2024-02-20",
|
||||
}),
|
||||
);
|
||||
const days = dom.querySelectorAll(".wrn-next__calendar-grid button") as HTMLButtonElement[];
|
||||
expect(days).toHaveLength(29);
|
||||
expect(days[0].style.gridColumn).toBe("4");
|
||||
expect(dom.querySelector(".wrn-next__calendar-weekdays")?.textContent).toContain("Mon");
|
||||
expect(days[8].disabled).toBe(true);
|
||||
expect(days[9].disabled).toBe(false);
|
||||
expect(days[20].disabled).toBe(true);
|
||||
expect(days[14].getAttribute("aria-pressed")).toBe("true");
|
||||
expect(days[14].getAttribute("aria-label")).toContain("February");
|
||||
});
|
||||
|
||||
test("calendar starts from an empty value, selects a date, and returns focus on Escape", async () => {
|
||||
const dom = mountHtml(await renderComponent(source, { name: "date", value: "" }));
|
||||
const trigger = dom.querySelector(".wrn-next__date-trigger") as HTMLButtonElement;
|
||||
trigger.click();
|
||||
const days = dom.querySelectorAll(".wrn-next__calendar-grid button") as HTMLButtonElement[];
|
||||
days[0].click();
|
||||
expect((dom.querySelector("input") as HTMLInputElement).value).toMatch(/^\d{4}-\d{2}-01$/);
|
||||
const selected = dom.querySelector('button[aria-pressed="true"]') as HTMLButtonElement;
|
||||
selected.focus();
|
||||
selected.dispatchEvent(
|
||||
new (dom.window as Window & typeof globalThis).KeyboardEvent("keydown", {
|
||||
key: "ArrowRight",
|
||||
bubbles: true,
|
||||
}),
|
||||
);
|
||||
expect((dom.document.activeElement as HTMLElement)?.textContent).toBe("2");
|
||||
dom.document.activeElement!.dispatchEvent(
|
||||
new (dom.window as Window & typeof globalThis).KeyboardEvent("keydown", {
|
||||
key: "Escape",
|
||||
bubbles: true,
|
||||
}),
|
||||
);
|
||||
expect(trigger.getAttribute("aria-expanded")).toBe("false");
|
||||
expect(dom.document.activeElement).toBe(trigger);
|
||||
});
|
||||
|
||||
test("calendar has 28 days in a non-leap February and cannot select while read-only", async () => {
|
||||
const dom = mountHtml(await renderComponent(source, { value: "2025-02-15", readonly: true }));
|
||||
const days = dom.querySelectorAll(".wrn-next__calendar-grid button") as HTMLButtonElement[];
|
||||
expect(days).toHaveLength(28);
|
||||
expect(Array.from(days).every((day) => day.disabled)).toBe(true);
|
||||
days[0].click();
|
||||
expect((dom.querySelector("input") as HTMLInputElement).value).toBe("2025-02-15");
|
||||
});
|
||||
|
||||
test("calendar keeps native month and year controls aligned after opening and changing months", async () => {
|
||||
const dom = mountHtml(await renderComponent(source, { value: "2026-09-20" }));
|
||||
(dom.querySelector(".wrn-next__date-trigger") as HTMLButtonElement).click();
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
let selectors = dom.querySelectorAll(".wrn-next__date-selectors select") as HTMLSelectElement[];
|
||||
expect(selectors[0].value).toBe("09");
|
||||
expect(selectors[1].value).toBe("2026");
|
||||
selectors[0].value = "02";
|
||||
selectors[0].dispatchEvent(
|
||||
new (dom.window as Window & typeof globalThis).Event("change", { bubbles: true }),
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
selectors = dom.querySelectorAll(".wrn-next__date-selectors select") as HTMLSelectElement[];
|
||||
expect(selectors[0].value).toBe("02");
|
||||
expect(selectors[1].value).toBe("2026");
|
||||
expect(dom.querySelectorAll(".wrn-next__calendar-grid button")).toHaveLength(28);
|
||||
});
|
||||
@@ -0,0 +1,135 @@
|
||||
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('polyline')).not.toBeNull();
|
||||
expect(dom.querySelectorAll('.wrn-chart__point-labels button').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');
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { readdirSync, readFileSync } from "node:fs";
|
||||
import { basename, dirname, resolve } from "node:path";
|
||||
|
||||
test("component imports resolve on case-sensitive production filesystems", () => {
|
||||
const root = resolve(import.meta.dir, "../components");
|
||||
for (const name of readdirSync(root).filter(name => name.endsWith(".wrn"))) {
|
||||
for (const match of readFileSync(resolve(root, name), "utf8").matchAll(/from\s+["'](\.[^"']+\.wrn)["']/g)) {
|
||||
const target = resolve(root, match[1]);
|
||||
expect(readdirSync(dirname(target)), `${name}: ${match[1]}`).toContain(basename(target));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -542,7 +542,7 @@ test("component-system CSS includes responsive, theme-token, focus, and reduced-
|
||||
expect(css).toContain("--wrn-ease-emphasized");
|
||||
expect(css).toContain("@keyframes wrn-component-enter");
|
||||
expect(css).toContain("@keyframes wrn-dialog-enter");
|
||||
expect(css).toContain("@media (hover: hover) and (pointer: fine)");
|
||||
expect(uiCss()).not.toMatch(/\[data-component\]\s*\{\s*animation:/);
|
||||
expect(css).toContain("--color-violet-600: var(--wrn-color-primary)");
|
||||
expect(css).toContain("--color-blue-600: var(--wrn-color-info)");
|
||||
expect(css).toContain("--color-red-600: var(--wrn-color-danger)");
|
||||
@@ -1672,7 +1672,7 @@ test("date picker renders a custom calendar trigger with accessible field state
|
||||
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.querySelectorAll(".wrn-next__date-selectors select")[0]?.querySelectorAll("option")).toHaveLength(12);
|
||||
expect(dom.querySelector(".wrn-next__field-help")?.textContent).toContain("local date");
|
||||
const trigger = dom.querySelector('button[aria-haspopup="dialog"]') as HTMLButtonElement;
|
||||
expect(dom.querySelector(".wrn-next--date-picker")?.getAttribute("data-expanded")).toBe("false");
|
||||
@@ -3185,6 +3185,24 @@ test("announcement full-width surface does not create viewport scrollbar overflo
|
||||
expect(source).not.toContain("calc(50% - 50vw)");
|
||||
});
|
||||
|
||||
test("announcement status uses a compatible element and preserves live announcements", async () => {
|
||||
const html = await renderComponent(readFileSync(uiComponentPath("AnnouncementBar"), "utf8"), { message: "Service update" });
|
||||
const dom = mountHtml(html);
|
||||
const status = dom.querySelector('[role="status"]')!;
|
||||
expect(status.tagName).toBe("DIV");
|
||||
expect(status.getAttribute("aria-live")).toBe("polite");
|
||||
expect(status.textContent).toContain("Service update");
|
||||
});
|
||||
|
||||
test("feature grid supports arbitrary slotted articles without imposing list semantics", async () => {
|
||||
const grid = await renderComponent(readFileSync(uiComponentPath("FeatureGrid"), "utf8"));
|
||||
const card = await renderComponent(readFileSync(uiComponentPath("FeatureCard"), "utf8"), { title: "Feature" });
|
||||
expect(mountHtml(grid).querySelector('[role="list"]')).toBeNull();
|
||||
const dom = mountHtml(card);
|
||||
expect(dom.querySelector('[role="listitem"]')).toBeNull();
|
||||
expect(dom.querySelector("article")?.textContent).toContain("Feature");
|
||||
});
|
||||
|
||||
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: [] });
|
||||
@@ -3194,6 +3212,16 @@ test("mega menu columns take roving focus and survive an empty column list", asy
|
||||
expect(dom.querySelector('[data-wrn-roving="both"]')).not.toBeNull();
|
||||
});
|
||||
|
||||
test("mega menu trigger keeps an accessible name for an empty or missing label", async () => {
|
||||
const source = readFileSync(uiComponentPath("MegaMenu"), "utf8");
|
||||
for (const label of ["", undefined, "Products"]) {
|
||||
const dom = mountHtml(await renderComponent(source, { label, columns: [] }));
|
||||
const trigger = dom.querySelector(".wrn-mega__trigger")!;
|
||||
expect(trigger.getAttribute("aria-label")).toBe(label || "Menu");
|
||||
expect(dom.querySelector(".wrn-mega__trigger-label")?.getAttribute("data-label")).toBe(label || "Menu");
|
||||
}
|
||||
});
|
||||
|
||||
test("catalog mega menu renders only the active rail category", async () => {
|
||||
const source = readFileSync(uiComponentPath("MegaMenu"), "utf8");
|
||||
expect(source).toContain(".wrn-mega__list > li");
|
||||
|
||||
Reference in new issue
Block a user