Files
WRNexusJS/packages/ui/test/ui-redesign-contract.test.ts
T
Clintchiz 270040fb5f
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 9m54s
feat(ui): add in-flow floating navbar
2026-08-24 19:47:47 +05:30

212 lines
6.5 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { readdir, readFile } from "node:fs/promises";
import { join } from "node:path";
import { parse } from "../../compiler/src/index.ts";
const componentsDir = join(import.meta.dir, "..", "components");
const replacementComponents = [
"Container",
"Grid",
"Columns",
"Card",
"Link",
"Image",
"SearchBox",
"Map",
"List",
"Marquee",
"Tabs",
"Timeline",
"Footer",
"Typography",
"Divider",
] as const;
const newComponents = [
"Section",
"SectionHeader",
"MarketingSectionHeader",
"PageHeader",
"TextLink",
"FeatureGrid",
"FeatureCard",
"FeatureIconCard",
"Hero",
"SplitHero",
"HeroActions",
"AnnouncementBar",
"MetricCard",
"MetricGrid",
"StatsBar",
"CTASection",
"BackToTop",
"PublicPageShell",
] as const;
const allComponents = [...replacementComponents, ...newComponents];
function openingTags(source: string): string[] {
const tags: string[] = [];
let index = 0;
while (index < source.length) {
const start = source.indexOf("<", index);
if (start === -1) break;
let cursor = start + 1;
let quote: "'" | '"' | null = null;
while (cursor < source.length) {
const char = source[cursor];
if (quote) {
if (char === quote) quote = null;
} else if (char === "'" || char === '"') {
quote = char;
} else if (char === ">") {
tags.push(source.slice(start, cursor + 1));
cursor += 1;
break;
}
cursor += 1;
}
index = cursor;
}
return tags;
}
describe("@wrnexus/ui redesign component pack", () => {
test("contains every replacement and new component", async () => {
const files = new Set(await readdir(componentsDir));
for (const name of allComponents) {
expect(files.has(`${name}.wrn`)).toBe(true);
}
});
for (const name of allComponents) {
test(`${name} has a valid component shell`, async () => {
const source = await readFile(join(componentsDir, `${name}.wrn`), "utf8");
expect(source).toContain(`component ${name} {`);
expect(source).toContain("props {");
expect(source).toContain("view {");
expect(source).toContain(`data-ui-component="${name}"`);
expect(source).not.toContain("React");
expect(source).not.toContain("useState");
expect(source).not.toContain("<script");
});
}
test("opening tags do not contain duplicate class attributes", async () => {
for (const name of allComponents) {
const source = await readFile(join(componentsDir, `${name}.wrn`), "utf8");
for (const tag of openingTags(source)) {
const plainClassAttributes = tag.match(/(?<![:\\w-])class\\s*=/g) ?? [];
expect(plainClassAttributes.length).toBeLessThanOrEqual(1);
}
}
});
test("dynamic attribute expressions stay single quoted", async () => {
for (const name of allComponents) {
const source = await readFile(join(componentsDir, `${name}.wrn`), "utf8");
expect(source).not.toMatch(/=\\"\{[^\n]*\}\\"/);
}
});
test("Navbar only displays its utility bar when the topbar slot has rendered content", async () => {
const source = await readFile(join(componentsDir, "Navbar.wrn"), "utf8");
expect(source).toContain(".wrn-navbar__topbar:not(:has(> *))");
expect(source).toContain(".wrn-navbar__topbar:has(> *)");
expect(source).not.toContain(".wrn-navbar__topbar:not(:empty)");
});
test("Navbar supports in-flow floating and elevated menu panels", async () => {
const source = await readFile(join(componentsDir, "Navbar.wrn"), "utf8");
const ast = parse(source);
const defaults = new Map(ast.props.map((prop) => [prop.name, prop.default]));
expect(defaults.get("floating")).toBe("false");
expect(defaults.get("startFloating")).toBe('"0px"');
expect(source).toContain("wrn-navbar--floating");
expect(source).toContain("--wrn-navbar-floating-start");
expect(source).toContain("z-index: 1100");
});
test("CTASection exposes responsive section spacing", async () => {
const source = await readFile(join(componentsDir, "CTASection.wrn"), "utf8");
const ast = parse(source);
const defaults = new Map(ast.props.map((prop) => [prop.name, prop.default]));
expect(defaults.get("spacing")).toBe('"none"');
expect(source).toContain("data-spacing='{spacing}'");
expect(source).toContain('.wrn-cta-section[data-spacing="sm"]');
expect(source).toContain('.wrn-cta-section[data-spacing="md"]');
expect(source).toContain('.wrn-cta-section[data-spacing="lg"]');
});
test("existing public defaults remain release compatible", async () => {
const expectations: Record<string, string[]> = {
Container: [
'size = "default"',
'color = "primary"',
"columns = 2",
'gap = "md"',
'maxWidth = "xl"',
],
Grid: [
'size = "default"',
'color = "primary"',
"columns = 2",
'gap = "md"',
'maxWidth = "xl"',
],
Columns: [
'size = "default"',
'color = "primary"',
"columns = 2",
'gap = "md"',
'maxWidth = "xl"',
],
Link: ['label = "Link"', 'href = "#"', "external = false"],
Image: ['src = ""', 'alt = ""', 'loading = "lazy"', "rounded = false"],
SearchBox: [
'label = "Search Box"',
'type = "search"',
"disabled = false",
"required = false",
],
Map: ['title = "Map"', "items = []", 'variant = "default"'],
List: ['title = "List"', "items = []", 'variant = "default"'],
Marquee: ['title = "Marquee"', "items = []", 'variant = "default"'],
Tabs: ['label = "Tabs"', "items = []", 'active = ""', 'orientation = "horizontal"'],
Timeline: ['title = "Timeline"', "items = []", 'variant = "default"'],
Footer: ['label = "Footer navigation"', "items = []", "columns = 3", 'maxWidth = "compact"'],
Typography: [
'size = "default"',
'color = "primary"',
"columns = 2",
'gap = "md"',
'maxWidth = "xl"',
],
Divider: ['label = ""', 'orientation = "horizontal"'],
};
for (const [name, tokens] of Object.entries(expectations)) {
const source = await readFile(join(componentsDir, `${name}.wrn`), "utf8");
const ast = parse(source);
const defaults = new Map(ast.props.map((prop) => [prop.name, prop.default]));
for (const token of tokens) {
const match = /^([A-Za-z_$][\w$]*)\s*=\s*([\s\S]+)$/.exec(token);
expect(match).not.toBeNull();
expect(defaults.get(match![1]!)).toBe(match![2]!);
}
}
});
});