feat: sync showcase with WRNexusJS 0.8.7
This commit is contained in:
+290
-423
@@ -2,6 +2,7 @@ import { expect, test } from "bun:test";
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { compileWireFile } from "@wrnexus/compiler";
|
||||
import { parse as parseWireSyntax } from "@wrnexus/syntax";
|
||||
|
||||
const root = join(import.meta.dir, "..");
|
||||
const pagesDir = join(root, "app", "pages");
|
||||
@@ -10,9 +11,48 @@ const reference = JSON.parse(
|
||||
readFileSync(join(root, "node_modules", "@wrnexus", "ui", "component-reference.json"), "utf8"),
|
||||
) as {
|
||||
count: number;
|
||||
components: Array<{ name: string; mount: string; category: string; events: string[] }>;
|
||||
components: Array<{
|
||||
name: string;
|
||||
mount: string;
|
||||
category: string;
|
||||
props: Array<{ name: string }>;
|
||||
slots: string[];
|
||||
events: string[];
|
||||
}>;
|
||||
};
|
||||
const slugOf = (name: string) => name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
||||
const manifest = JSON.parse(readFileSync(join(root, "showcase-manifest.json"), "utf8")) as {
|
||||
componentCount: number;
|
||||
categoryCount: number;
|
||||
totalDemoCount: number;
|
||||
components: Array<{
|
||||
name: string;
|
||||
mount: string;
|
||||
slug: string;
|
||||
category: string;
|
||||
demoCount: number;
|
||||
propCount: number;
|
||||
slots: string[];
|
||||
events: string[];
|
||||
profiled: boolean;
|
||||
}>;
|
||||
};
|
||||
|
||||
const detailSource = (slug: string) => readFileSync(join(detailPagesDir, `${slug}.wrn`), "utf8");
|
||||
|
||||
const interactiveOverlayNames = [
|
||||
"ContextMenu",
|
||||
"Drawer",
|
||||
"Dropdown",
|
||||
"Modal",
|
||||
"Popover",
|
||||
"Tooltip",
|
||||
];
|
||||
|
||||
function mountCount(source: string, mount: string): number {
|
||||
const tag = new RegExp(`<${mount}(?:\\s|>|/)`, "g");
|
||||
const legacy = new RegExp(`<div\\s+data-component="${mount}"`, "g");
|
||||
return (source.match(tag)?.length ?? 0) + (source.match(legacy)?.length ?? 0);
|
||||
}
|
||||
|
||||
test("showcase configures local Lucide icon generation", () => {
|
||||
const packageJson = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as {
|
||||
@@ -24,10 +64,28 @@ test("showcase configures local Lucide icon generation", () => {
|
||||
expect(packageJson.devDependencies["@iconify-json/lucide"]).toBeTruthy();
|
||||
expect(styles).toContain('@plugin "@iconify/tailwind4";');
|
||||
expect(styles).toContain(".playground-preview-source > .wire-action:not(.w-full)");
|
||||
expect(styles).toContain("width: fit-content;");
|
||||
expect(styles).toContain('[data-ui-component="MetricGrid"]');
|
||||
expect(styles).toContain("width: 100%;");
|
||||
});
|
||||
|
||||
test("showcase uses a searchable three-column documentation shell", () => {
|
||||
test("showcase manifest matches the generated UI reference", () => {
|
||||
expect(manifest.componentCount).toBe(reference.count);
|
||||
expect(manifest.components).toHaveLength(reference.count);
|
||||
expect(manifest.totalDemoCount).toBeGreaterThanOrEqual(reference.count * 3);
|
||||
expect(new Set(manifest.components.map((component) => component.slug)).size).toBe(
|
||||
reference.count,
|
||||
);
|
||||
|
||||
for (const component of reference.components) {
|
||||
const documented = manifest.components.find((entry) => entry.name === component.name);
|
||||
expect(documented).toBeTruthy();
|
||||
expect(documented?.mount).toBe(component.mount);
|
||||
expect(documented?.events).toEqual(component.events);
|
||||
expect(documented?.slots).toEqual(component.slots);
|
||||
}
|
||||
});
|
||||
|
||||
test("showcase uses a searchable documentation shell", () => {
|
||||
const layout = readFileSync(join(root, "app", "layouts", "showcase.wrn"), "utf8");
|
||||
const styles = readFileSync(join(root, "app", "styles", "global.css"), "utf8");
|
||||
const runtime = readFileSync(join(root, "public", "playground.js"), "utf8");
|
||||
@@ -40,66 +98,6 @@ test("showcase uses a searchable three-column documentation shell", () => {
|
||||
expect(runtime).toContain('event.key.toLowerCase() === "k"');
|
||||
});
|
||||
|
||||
test("component sidebar shows delivery status and refreshes its active link after navigation", () => {
|
||||
const layout = readFileSync(join(root, "app", "layouts", "showcase.wrn"), "utf8");
|
||||
const styles = readFileSync(join(root, "app", "styles", "global.css"), "utf8");
|
||||
const runtime = readFileSync(join(root, "public", "playground.js"), "utf8");
|
||||
const completeComponents = [
|
||||
"Accordion",
|
||||
"Alert",
|
||||
"Avatar",
|
||||
"Avatar Group",
|
||||
"Badge",
|
||||
"Blockquote",
|
||||
"Button",
|
||||
"Button Group",
|
||||
"Card",
|
||||
"Carousel",
|
||||
"Chat Bubble",
|
||||
"Collapse",
|
||||
"Checkbox",
|
||||
"Date Picker",
|
||||
"Device Frame",
|
||||
"File Upload Progress",
|
||||
"Color Picker",
|
||||
"File Input",
|
||||
"Input",
|
||||
"Input Group",
|
||||
"Radio",
|
||||
"Range Slider",
|
||||
"Select",
|
||||
"Switch",
|
||||
"Textarea",
|
||||
"Time Picker",
|
||||
"Advanced Select",
|
||||
"Combo Box",
|
||||
"Input Number",
|
||||
"Pin Input",
|
||||
"Strong Password",
|
||||
"Toggle Count",
|
||||
"Toggle Password",
|
||||
];
|
||||
|
||||
expect(layout.match(/data-component-status="complete"/g)).toHaveLength(completeComponents.length);
|
||||
expect(layout.match(/data-component-status="pending"/g)).toHaveLength(
|
||||
reference.count - completeComponents.length,
|
||||
);
|
||||
for (const component of completeComponents) {
|
||||
expect(layout).toContain(
|
||||
`<span data-docs-component-name>${component}</span><small data-component-status="complete">Complete</small>`,
|
||||
);
|
||||
}
|
||||
expect(styles).toContain('[data-component-status="working"]');
|
||||
expect(runtime).toContain('window.addEventListener("wrnexus:navigated", updateActiveNavigation)');
|
||||
expect(runtime).toContain('link.removeAttribute("aria-current")');
|
||||
});
|
||||
|
||||
test("accordion examples do not include the generic composable slot placeholder", () => {
|
||||
const page = readFileSync(join(detailPagesDir, "accordion.wrn"), "utf8");
|
||||
|
||||
expect(page).not.toContain("Composable content area.");
|
||||
});
|
||||
|
||||
test("showcase SSR document layout maps persisted design cookies to html attributes", () => {
|
||||
const source = readFileSync(join(root, "app", "layouts", "document.wrn"), "utf8");
|
||||
expect(source).toContain("<html");
|
||||
@@ -111,380 +109,247 @@ test("showcase SSR document layout maps persisted design cookies to html attribu
|
||||
}
|
||||
});
|
||||
|
||||
test("advanced select demos use validation schemas and a real remote API", () => {
|
||||
const page = readFileSync(
|
||||
join(root, "app", "pages", "components", "advanced-select.wrn"),
|
||||
"utf8",
|
||||
test("category pages preview every UI component exactly once", () => {
|
||||
const categoryPages = readdirSync(pagesDir)
|
||||
.filter((file) => manifest.components.some((component) => `${component.category}.wrn` === file))
|
||||
.map((file) => readFileSync(join(pagesDir, file), "utf8"))
|
||||
.join("\n");
|
||||
|
||||
const invalidPreviews = reference.components.flatMap((component) => {
|
||||
const count = mountCount(categoryPages, component.mount);
|
||||
return count === 1
|
||||
? []
|
||||
: [{ name: component.name, mount: component.mount, category: component.category, count }];
|
||||
});
|
||||
|
||||
expect(invalidPreviews).toEqual([]);
|
||||
});
|
||||
|
||||
test("every component has a detail page, playground, and manifest-driven demos", () => {
|
||||
const files = readdirSync(detailPagesDir).filter((entry) => entry.endsWith(".wrn"));
|
||||
expect(files).toHaveLength(reference.count);
|
||||
|
||||
for (const component of manifest.components) {
|
||||
const source = detailSource(component.slug);
|
||||
expect(source).toContain("data-playground-preview");
|
||||
expect(source).toContain("data-playground-form");
|
||||
expect(source).toContain("data-playground-code");
|
||||
expect(source).toContain('href="#playground"');
|
||||
expect(source.match(/class="demo-workbench"/g)).toHaveLength(component.demoCount);
|
||||
expect(mountCount(source, component.mount)).toBeGreaterThanOrEqual(component.demoCount + 1);
|
||||
}
|
||||
}, 20_000);
|
||||
|
||||
test("playgrounds preserve typed values, safe DOM replacement, themes, and event output", () => {
|
||||
const generator = readFileSync(join(root, "scripts", "generate-showcase.mjs"), "utf8");
|
||||
const runtime = readFileSync(join(root, "public", "playground.js"), "utf8");
|
||||
|
||||
expect(generator).toContain("JSON.parse(");
|
||||
expect(generator).toContain("Number(");
|
||||
expect(generator).toContain("data-playground-json");
|
||||
expect(generator).toContain("prop.name}='{${playgroundStateName(prop)}}'");
|
||||
expect(generator).toContain('data-slot="${escapeAttribute(slot)}"');
|
||||
const generatorWithoutDataSlots = generator.replaceAll(
|
||||
'data-slot="${escapeAttribute(slot)}"',
|
||||
"",
|
||||
);
|
||||
expect(generatorWithoutDataSlots).not.toContain('slot="${escapeAttribute(slot)}"');
|
||||
|
||||
expect(runtime).toContain("fetch(url");
|
||||
expect(runtime).toContain("navigator.clipboard.writeText");
|
||||
expect(runtime).toContain("preview.replaceChildren");
|
||||
expect(runtime).not.toContain("preview.innerHTML");
|
||||
expect(runtime).toContain("__wrnexusHydrateScopes");
|
||||
expect(runtime).toContain("wireTheme?.bind?.(document)");
|
||||
expect(runtime).toContain("bindPlaygroundEvents");
|
||||
expect(runtime).toContain('attributes.push(`${name}="${field.checked}"`)');
|
||||
});
|
||||
|
||||
test("playground enum controls use public profiles instead of inferred implementation comparisons", () => {
|
||||
const generator = readFileSync(join(root, "scripts", "generate-showcase.mjs"), "utf8");
|
||||
expect(generator).toContain("profileFor(component.name)?.options?.[prop.name]");
|
||||
expect(generator).not.toContain("source.matchAll(pattern)");
|
||||
|
||||
const pageHeader = manifest.components.find((component) => component.name === "PageHeader");
|
||||
if (pageHeader) {
|
||||
const source = detailSource(pageHeader.slug);
|
||||
expect(source).toContain('name="pg_icon" type="text"');
|
||||
expect(source).toMatch(/<option value="default"(?: selected)?>default<\/option>/);
|
||||
expect(source).toMatch(/<option value="solid"(?: selected)?>solid<\/option>/);
|
||||
}
|
||||
});
|
||||
|
||||
test("every declared public event is documented and visible in the playground", () => {
|
||||
for (const component of manifest.components.filter((entry) => entry.events.length > 0)) {
|
||||
const source = detailSource(component.slug);
|
||||
expect(source).toContain('id="events"');
|
||||
expect(source).toContain('href="#events"');
|
||||
expect(source).toContain("data-playground-event-log");
|
||||
for (const event of component.events) {
|
||||
expect(source).toContain(`<code>@${event}</code>`);
|
||||
expect(source).toContain(`@${event}='console.log(payload)'`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("new and recently repaired components have production use-case profiles", () => {
|
||||
const expectedProfiles = [
|
||||
"PublicPageShell",
|
||||
"AnnouncementBar",
|
||||
"Breadcrumb",
|
||||
"PageHeader",
|
||||
"Hero",
|
||||
"HeroActions",
|
||||
"SplitHero",
|
||||
"Section",
|
||||
"SectionHeader",
|
||||
"MarketingSectionHeader",
|
||||
"TextLink",
|
||||
"FeatureGrid",
|
||||
"FeatureCard",
|
||||
"FeatureIconCard",
|
||||
"MetricGrid",
|
||||
"MetricCard",
|
||||
"StatsBar",
|
||||
"CTASection",
|
||||
"BackToTop",
|
||||
"Footer",
|
||||
"ContextMenu",
|
||||
"Drawer",
|
||||
"Dropdown",
|
||||
"Modal",
|
||||
"Popover",
|
||||
"Tooltip",
|
||||
];
|
||||
|
||||
for (const name of expectedProfiles) {
|
||||
const component = manifest.components.find((entry) => entry.name === name);
|
||||
if (!component) continue;
|
||||
expect(component.profiled).toBe(true);
|
||||
const source = detailSource(component.slug);
|
||||
expect(source.match(/class="demo-workbench"/g)?.length ?? 0).toBeGreaterThanOrEqual(3);
|
||||
}
|
||||
});
|
||||
|
||||
test("grid, hero, shell, and overlay detail pages use responsive showcase stages", () => {
|
||||
const styles = readFileSync(join(root, "app", "styles", "global.css"), "utf8");
|
||||
for (const name of [
|
||||
"MetricGrid",
|
||||
"FeatureGrid",
|
||||
"StatsBar",
|
||||
"Hero",
|
||||
"SplitHero",
|
||||
"AnnouncementBar",
|
||||
"CTASection",
|
||||
"Footer",
|
||||
]) {
|
||||
expect(styles).toContain(`[data-ui-component="${name}"]`);
|
||||
}
|
||||
|
||||
const metricGrid = manifest.components.find((component) => component.name === "MetricGrid");
|
||||
if (metricGrid) {
|
||||
const source = detailSource(metricGrid.slug);
|
||||
expect(source).toContain('columns="3"');
|
||||
const metricGridReference = reference.components.find(
|
||||
(component) => component.name === "MetricGrid",
|
||||
);
|
||||
if (metricGridReference?.props.some((prop) => prop.name === "maxWidth")) {
|
||||
expect(source).toContain('maxWidth="full"');
|
||||
}
|
||||
expect(source).toContain("items='[");
|
||||
}
|
||||
});
|
||||
|
||||
test("overlay previews start closed and expose usable click, hover, or context triggers", () => {
|
||||
const generator = readFileSync(join(root, "scripts", "generate-showcase.mjs"), "utf8");
|
||||
const profiles = readFileSync(join(root, "scripts", "showcase-profiles.mjs"), "utf8");
|
||||
const styles = readFileSync(join(root, "app", "styles", "global.css"), "utf8");
|
||||
|
||||
expect(generator).toContain("interactiveOverlayComponents");
|
||||
expect(generator).toContain("overlayVisibilityProps.has(name)");
|
||||
expect(generator).toContain(
|
||||
'data-interactive-preview="${isInteractiveOverlay(component) ? "true" : "false"}"',
|
||||
);
|
||||
expect(styles).toContain('.catalog-card[data-interactive-preview="true"]');
|
||||
expect(styles).toContain('[data-ui-component="Drawer"]');
|
||||
expect(styles).toContain('[data-ui-component="Modal"]');
|
||||
|
||||
const overlaysPage = readFileSync(join(pagesDir, "overlays.wrn"), "utf8");
|
||||
for (const name of interactiveOverlayNames) {
|
||||
const component = manifest.components.find((entry) => entry.name === name);
|
||||
if (!component) continue;
|
||||
|
||||
const detail = detailSource(component.slug);
|
||||
expect(detail).toContain("demo-interaction-hint");
|
||||
expect(detail).not.toMatch(new RegExp(`<${component.mount}[^>]*(?:open|defaultOpen)="true"`));
|
||||
expect(overlaysPage).not.toMatch(
|
||||
new RegExp(`<${component.mount}[^>]*(?:open|defaultOpen)="true"`),
|
||||
);
|
||||
}
|
||||
|
||||
expect(profiles).toContain('trigger: "contextmenu"');
|
||||
expect(profiles).toContain('trigger: "click"');
|
||||
expect(profiles).toContain('trigger: "hover"');
|
||||
expect(profiles).toContain('trigger: "both"');
|
||||
expect(profiles).toContain('triggerLabel: "Open drawer"');
|
||||
expect(profiles).toContain('triggerLabel: "Open modal"');
|
||||
expect(profiles).toContain('triggerLabel: "View details"');
|
||||
});
|
||||
|
||||
test("advanced select demos use validation schemas and a real remote API", () => {
|
||||
const component = manifest.components.find((entry) => entry.name === "AdvancedSelect");
|
||||
if (!component) return;
|
||||
const page = detailSource(component.slug);
|
||||
expect(page).toContain('data-schema="advanced-select-validation"');
|
||||
expect(page).toContain('data-schema="advanced-select-dynamic-validation"');
|
||||
expect(readFileSync(join(root, "app", "api", "teams.ts"), "utf8")).toContain("export const GET");
|
||||
});
|
||||
|
||||
test("category pages preview every UI component exactly once", () => {
|
||||
const categoryPages = readdirSync(pagesDir)
|
||||
.filter((file) => file !== "index.wrn" && file.endsWith(".wrn"))
|
||||
.map((file) => readFileSync(join(pagesDir, file), "utf8"))
|
||||
.join("\n");
|
||||
|
||||
const mounts = [...categoryPages.matchAll(/<div data-component="([^"]+)"/g)].map(
|
||||
(match) => match[1],
|
||||
);
|
||||
|
||||
expect(mounts).toHaveLength(reference.count);
|
||||
expect(new Set(mounts).size).toBe(reference.count);
|
||||
expect(mounts.sort()).toEqual(reference.components.map((component) => component.mount).sort());
|
||||
});
|
||||
|
||||
test("every component has a detail page with its expected live use cases", () => {
|
||||
const files = readdirSync(detailPagesDir).filter((entry) => entry.endsWith(".wrn"));
|
||||
|
||||
expect(files).toHaveLength(reference.count);
|
||||
|
||||
const detailMounts = files.flatMap((file) => {
|
||||
const source = readFileSync(join(detailPagesDir, file), "utf8");
|
||||
return [...source.matchAll(/<div data-component="([^"]+)"/g)].map((match) => match[1]);
|
||||
});
|
||||
|
||||
// Every detail page has one playground plus three standard demos. Interactive
|
||||
// components add specialized capability demos beyond that shared baseline.
|
||||
expect(detailMounts).toHaveLength(reference.count * 4 + 249);
|
||||
for (const component of reference.components) {
|
||||
const expectedMounts =
|
||||
component.mount === "Accordion"
|
||||
? 10
|
||||
: component.mount === "Alert"
|
||||
? 16
|
||||
: component.mount === "Avatar"
|
||||
? 18
|
||||
: component.mount === "AvatarGroup"
|
||||
? 6
|
||||
: component.mount === "Badge"
|
||||
? 16
|
||||
: component.mount === "Button"
|
||||
? 9
|
||||
: component.mount === "ButtonGroup"
|
||||
? 8
|
||||
: component.mount === "Card"
|
||||
? 24
|
||||
: component.mount === "Carousel"
|
||||
? 14
|
||||
: component.mount === "ChatBubble"
|
||||
? 5
|
||||
: component.mount === "Collapse"
|
||||
? 3
|
||||
: component.mount === "FileUploadProgress"
|
||||
? 7
|
||||
: [
|
||||
"Checkbox",
|
||||
"ColorPicker",
|
||||
"FileInput",
|
||||
"Input",
|
||||
"InputGroup",
|
||||
"Radio",
|
||||
"RangeSlider",
|
||||
"Select",
|
||||
"Switch",
|
||||
"Textarea",
|
||||
"TimePicker",
|
||||
].includes(component.mount)
|
||||
? component.mount === "Radio" || component.mount === "Checkbox"
|
||||
? 12
|
||||
: 10
|
||||
: component.mount === "AdvancedSelect"
|
||||
? 42
|
||||
: component.mount === "ComboBox"
|
||||
? 22
|
||||
: component.mount === "InputNumber"
|
||||
? 13
|
||||
: component.mount === "PinInput"
|
||||
? 19
|
||||
: component.mount === "StrongPassword"
|
||||
? 7
|
||||
: component.mount === "ToggleCount"
|
||||
? 5
|
||||
: component.mount === "TogglePassword"
|
||||
? 11
|
||||
: 4;
|
||||
expect(detailMounts.filter((mount) => mount === component.mount)).toHaveLength(expectedMounts);
|
||||
}
|
||||
|
||||
const uploadProgressPage = readFileSync(join(detailPagesDir, "file-upload-progress.wrn"), "utf8");
|
||||
for (const fileName of [
|
||||
"design-system.zip",
|
||||
"product-images.tar",
|
||||
"launch-video.mp4",
|
||||
"quarterly-report.pdf",
|
||||
"customer-export.csv",
|
||||
"brand-assets.fig",
|
||||
]) {
|
||||
expect(uploadProgressPage).toContain(fileName);
|
||||
}
|
||||
expect(uploadProgressPage).toContain('status="complete"');
|
||||
expect(uploadProgressPage).toContain('status="error"');
|
||||
expect(uploadProgressPage).toContain('status="cancelled"');
|
||||
}, 15_000);
|
||||
|
||||
test("every detail page has a type-aware realtime playground", () => {
|
||||
const files = readdirSync(detailPagesDir).filter((entry) => entry.endsWith(".wrn"));
|
||||
for (const file of files) {
|
||||
const source = readFileSync(join(detailPagesDir, file), "utf8");
|
||||
expect(source).toContain("data-playground-preview");
|
||||
expect(source).toContain("data-playground-form");
|
||||
expect(source).toContain("data-playground-code");
|
||||
expect(source).toContain("data-playground-copy");
|
||||
expect(source).toContain('href="#playground"');
|
||||
}
|
||||
|
||||
const runtime = readFileSync(join(root, "public", "playground.js"), "utf8");
|
||||
expect(runtime).toContain("fetch(url");
|
||||
expect(runtime).toContain("HTMLSelectElement");
|
||||
expect(runtime).toContain("navigator.clipboard.writeText");
|
||||
expect(runtime).toContain("updateCode(playground)");
|
||||
expect(runtime).toContain("__wrnexusHydrateScopes");
|
||||
expect(runtime).toContain("data-playground-json");
|
||||
expect(runtime).toContain('createPolicy("wrnexus-playground"');
|
||||
expect(runtime).toContain("preview.replaceChildren");
|
||||
expect(runtime).not.toContain("preview.innerHTML");
|
||||
});
|
||||
|
||||
test("detail pages show component syntax beside each live preview", () => {
|
||||
const buttonPage = readFileSync(join(detailPagesDir, "button.wrn"), "utf8");
|
||||
|
||||
expect(buttonPage).toContain('name="pg_label" type="text"');
|
||||
expect(buttonPage).toContain('name="pg_variant"');
|
||||
expect(buttonPage).toContain('<option value="destructive">destructive</option>');
|
||||
expect(buttonPage).not.toContain('<option value="success">success</option></select></label>');
|
||||
expect(buttonPage).toContain('name="pg_color"');
|
||||
expect(buttonPage).toContain('<option value="success">success</option>');
|
||||
expect(buttonPage).toContain('name="pg_size"');
|
||||
expect(buttonPage).toContain('<option value="icon-lg">icon-lg</option>');
|
||||
expect(buttonPage.match(/class="demo-workbench"/g)).toHaveLength(8);
|
||||
expect(buttonPage.match(/<Button/g)).toHaveLength(9);
|
||||
expect(buttonPage).not.toContain("<div data-component="Button"");
|
||||
expect(buttonPage).not.toContain('<details class="demo-code">');
|
||||
});
|
||||
|
||||
test("every detail page lists all public events immediately before props", () => {
|
||||
for (const component of reference.components) {
|
||||
const source = readFileSync(join(detailPagesDir, `${slugOf(component.name)}.wrn`), "utf8");
|
||||
const eventsIndex = source.indexOf('<section id="events"');
|
||||
const propsIndex = source.indexOf('<section id="api"');
|
||||
|
||||
expect(eventsIndex).toBeGreaterThan(-1);
|
||||
expect(propsIndex).toBeGreaterThan(eventsIndex);
|
||||
expect(source.slice(eventsIndex, propsIndex)).toContain("Component events");
|
||||
expect(source).toContain('<a href="#events">Events</a>');
|
||||
|
||||
if (component.events.length === 0) {
|
||||
expect(source.slice(eventsIndex, propsIndex)).toContain("No public component events.");
|
||||
} else {
|
||||
for (const event of component.events) {
|
||||
expect(source.slice(eventsIndex, propsIndex)).toContain(`<code>@${event}</code>`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("button documents click, focus, and blur events", () => {
|
||||
const source = readFileSync(join(detailPagesDir, "button.wrn"), "utf8");
|
||||
|
||||
for (const event of ["click", "focus", "blur"]) {
|
||||
expect(source).toContain(`<code>@${event}</code>`);
|
||||
}
|
||||
});
|
||||
|
||||
test("button group documents every requested layout and size", () => {
|
||||
const source = readFileSync(join(detailPagesDir, "button-group.wrn"), "utf8");
|
||||
|
||||
for (const heading of [
|
||||
"Example",
|
||||
"Small",
|
||||
"Medium",
|
||||
"Large",
|
||||
"Responsive stack",
|
||||
"Vertical stack",
|
||||
"Button toolbar",
|
||||
]) {
|
||||
expect(source).toContain(`<h2>${heading}</h2>`);
|
||||
}
|
||||
expect(source.match(/class="demo-workbench"/g)).toHaveLength(7);
|
||||
expect(source).toContain('<span class="detail-toc-group">Basic usage</span>');
|
||||
expect(source).toContain('<span class="detail-toc-group">Sizes</span>');
|
||||
expect(source).toContain('responsive="true"');
|
||||
expect(source).toContain('orientation="vertical"');
|
||||
expect(source).toContain('toolbar="true"');
|
||||
expect(source).toContain('ariaLabel="Text formatting"');
|
||||
for (const event of ["click", "select", "change"]) {
|
||||
expect(source).toContain(`<code>@${event}</code>`);
|
||||
}
|
||||
});
|
||||
|
||||
test("radio documents single, grouped, card, list, aligned, and validation layouts", () => {
|
||||
const source = readFileSync(join(detailPagesDir, "radio.wrn"), "utf8");
|
||||
for (const heading of [
|
||||
"Default radio",
|
||||
"Disabled",
|
||||
"Inline radio group",
|
||||
"Vertical radio group",
|
||||
"Radios with descriptions",
|
||||
"Radio cards",
|
||||
"Vertical radio cards",
|
||||
"Right-aligned radio",
|
||||
"Radio list group",
|
||||
"Horizontal radio list group",
|
||||
"Validation states",
|
||||
]) {
|
||||
expect(source).toContain(`<h2>${heading}</h2>`);
|
||||
}
|
||||
});
|
||||
|
||||
test("checkbox documents mixed, grouped, card, list, aligned, and validation layouts", () => {
|
||||
const source = readFileSync(join(detailPagesDir, "checkbox.wrn"), "utf8");
|
||||
for (const heading of [
|
||||
"Default checkbox",
|
||||
"Indeterminate",
|
||||
"Disabled",
|
||||
"Checkbox group options",
|
||||
"Checkboxes with descriptions",
|
||||
"Checkbox cards",
|
||||
"Vertical checkbox cards",
|
||||
"Right-aligned checkbox",
|
||||
"Checkbox list group",
|
||||
"Horizontal checkbox list group",
|
||||
"Validation states",
|
||||
]) {
|
||||
expect(source).toContain(`<h2>${heading}</h2>`);
|
||||
}
|
||||
});
|
||||
|
||||
test("card documents content, media, animation, layout, alert, and panel capabilities", () => {
|
||||
const source = readFileSync(join(detailPagesDir, "card.wrn"), "utf8");
|
||||
const headings = [
|
||||
"Default card",
|
||||
"Body",
|
||||
"Simple card",
|
||||
"Header and footer",
|
||||
"Footer only",
|
||||
"Small",
|
||||
"Default",
|
||||
"Large",
|
||||
"Navigation",
|
||||
"Navigation with select on mobile",
|
||||
"Top image",
|
||||
"Bottom image",
|
||||
"Image overlays",
|
||||
"Image scaling animation on hover",
|
||||
"Transition on hover",
|
||||
"Horizontal",
|
||||
"Card group",
|
||||
"Top bordered card",
|
||||
"Card actions",
|
||||
"Card with alert",
|
||||
"Centered body content",
|
||||
"Empty state",
|
||||
"Scrollable body",
|
||||
test("specialized form component examples remain complete", () => {
|
||||
const checks: Array<[string, string[]]> = [
|
||||
["PinInput", ["Different lengths", 'length="3"', 'length="5"', 'length="7"']],
|
||||
[
|
||||
"TogglePassword",
|
||||
[
|
||||
"Checkbox-controlled toggle",
|
||||
"Synchronized password fields",
|
||||
'data-schema="toggle-password-validation"',
|
||||
],
|
||||
],
|
||||
[
|
||||
"StrongPassword",
|
||||
["Live strength meter", "Requirements in a popover", 'specialCharactersSet="@#_-."'],
|
||||
],
|
||||
];
|
||||
|
||||
for (const heading of headings) {
|
||||
expect(source).toContain(`<h2>${heading}</h2>`);
|
||||
}
|
||||
expect(source.match(/class="demo-workbench"/g)).toHaveLength(23);
|
||||
for (const section of [
|
||||
"Basic usage",
|
||||
"Content",
|
||||
"Structure",
|
||||
"Sizes",
|
||||
"Navigation",
|
||||
"Images",
|
||||
"Animations",
|
||||
"Card layout",
|
||||
"Panels",
|
||||
]) {
|
||||
expect(source).toContain(`<span class="detail-toc-group">${section}</span>`);
|
||||
}
|
||||
expect(source).toContain('imagePosition="overlay"');
|
||||
expect(source).toContain('hover="image"');
|
||||
expect(source).toContain('hover="raise"');
|
||||
expect(source).toContain('layout="horizontal"');
|
||||
expect(source).toContain('mobileNavigation="true"');
|
||||
expect(source).toContain('scrollable="true"');
|
||||
for (const event of ["click", "action", "navigate", "dismiss", "load", "error"]) {
|
||||
expect(source).toContain(`<code>@${event}</code>`);
|
||||
for (const [name, needles] of checks) {
|
||||
const component = manifest.components.find((entry) => entry.name === name);
|
||||
if (!component) continue;
|
||||
const source = detailSource(component.slug);
|
||||
for (const needle of needles) expect(source).toContain(needle);
|
||||
}
|
||||
});
|
||||
|
||||
test("advanced select and combobox document their complete event contract", () => {
|
||||
for (const slug of ["advanced-select", "combo-box"]) {
|
||||
const source = readFileSync(join(detailPagesDir, `${slug}.wrn`), "utf8");
|
||||
for (const event of ["search", "select", "change", "clear", "open", "close", "load", "error"]) {
|
||||
expect(source).toContain(`<code>@${event}</code>`);
|
||||
}
|
||||
expect(source).toContain('id="events"');
|
||||
expect(source).toContain("event.detail");
|
||||
}
|
||||
});
|
||||
|
||||
test("pin input documents verification events", () => {
|
||||
const source = readFileSync(join(detailPagesDir, "pin-input.wrn"), "utf8");
|
||||
for (const event of ["input", "change", "complete", "paste", "clear", "error"]) {
|
||||
expect(source).toContain(`<code>@${event}</code>`);
|
||||
}
|
||||
expect(source).toContain('id="events"');
|
||||
expect(source).toContain("event.detail");
|
||||
expect(source).toContain("Different lengths");
|
||||
expect(source).toContain('length="3"');
|
||||
expect(source).toContain('length="5"');
|
||||
expect(source).toContain('length="7"');
|
||||
});
|
||||
|
||||
test("toggle password documents visibility states, validation, and declared events", () => {
|
||||
const source = readFileSync(join(detailPagesDir, "toggle-password.wrn"), "utf8");
|
||||
for (const event of ["input", "change", "toggle"]) {
|
||||
expect(source).toContain(`<code>@${event}</code>`);
|
||||
}
|
||||
expect(source).toContain("Checkbox-controlled toggle");
|
||||
expect(source).toContain("Synchronized password fields");
|
||||
expect(source).toContain("Without visibility toggle");
|
||||
expect(source).toContain("fields='[\n {");
|
||||
expect(source).toContain('"label": "New password"');
|
||||
expect(source).toContain('"name": "current-password"');
|
||||
expect(source).not.toContain("fields='[..]'");
|
||||
expect(source).toContain('data-schema="toggle-password-validation"');
|
||||
});
|
||||
|
||||
test("strong password documents live scoring, requirements, popover, and custom characters", () => {
|
||||
const source = readFileSync(join(detailPagesDir, "strong-password.wrn"), "utf8");
|
||||
expect(source).toContain("Live strength meter");
|
||||
expect(source).toContain("Requirements and hint text");
|
||||
expect(source).toContain("Requirements in a popover");
|
||||
expect(source).toContain("Custom special characters");
|
||||
expect(source).toContain('presentation="popover"');
|
||||
expect(source).toContain('specialCharactersSet="@#_-."');
|
||||
for (const event of ["input", "change", "strength"]) {
|
||||
expect(source).toContain(`<code>@${event}</code>`);
|
||||
}
|
||||
});
|
||||
|
||||
test("complex component props are shown as complete readable multiline values", () => {
|
||||
for (const slug of ["advanced-select", "combo-box", "select", "toggle-password"]) {
|
||||
const source = readFileSync(join(detailPagesDir, `${slug}.wrn`), "utf8");
|
||||
test("complex component props are complete readable multiline values", () => {
|
||||
for (const name of ["AdvancedSelect", "ComboBox", "Select", "TogglePassword"]) {
|
||||
const component = manifest.components.find((entry) => entry.name === name);
|
||||
if (!component) continue;
|
||||
const source = detailSource(component.slug);
|
||||
expect(source).not.toMatch(/\b(?:options|groups|items|fields)='\[\.\.\.\]'/);
|
||||
}
|
||||
|
||||
const advancedSelect = readFileSync(join(detailPagesDir, "advanced-select.wrn"), "utf8");
|
||||
expect(advancedSelect).toContain("options='[\n {");
|
||||
expect(advancedSelect).toContain('"value": "design"');
|
||||
});
|
||||
|
||||
test("code examples preserve literal object braces after client hydration", () => {
|
||||
const source = readFileSync(join(detailPagesDir, "toggle-password.wrn"), "utf8");
|
||||
expect(source).toContain("{");
|
||||
expect(source).toContain("}");
|
||||
expect(source).not.toContain('fields=\'[\n {\n "label"');
|
||||
test("generated playground boolean states avoid multiline-sensitive ternaries", () => {
|
||||
const generator = readFileSync(join(root, "scripts", "generate-showcase.mjs"), "utf8");
|
||||
expect(generator).toContain('?? "${initial}") === "true"');
|
||||
expect(generator).not.toContain("=== null ? ${initial} :");
|
||||
|
||||
for (const component of manifest.components) {
|
||||
const source = detailSource(component.slug);
|
||||
expect(source).not.toMatch(/state\s+playground_[A-Za-z0-9_$]+\s*=.*===\s*null\s*\?/);
|
||||
}
|
||||
});
|
||||
|
||||
test("every generated showcase page compiles", () => {
|
||||
test("every generated showcase page compiles and is accepted by the route syntax parser", () => {
|
||||
const paths = [
|
||||
...readdirSync(pagesDir)
|
||||
.filter((entry) => entry.endsWith(".wrn"))
|
||||
@@ -495,6 +360,8 @@ test("every generated showcase page compiles", () => {
|
||||
];
|
||||
|
||||
for (const path of paths) {
|
||||
expect(() => compileWireFile(readFileSync(path, "utf8"), path)).not.toThrow();
|
||||
const source = readFileSync(path, "utf8");
|
||||
expect(() => parseWireSyntax(source)).not.toThrow();
|
||||
expect(() => compileWireFile(source, path)).not.toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user