release: WRNexusJS 0.6.0
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { readdirSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
function outputBlocks(source: string): string[] {
|
||||
const blocks: string[] = [];
|
||||
for (const match of source.matchAll(/\boutputs\s*\{/g)) {
|
||||
const brace = source.indexOf("{", match.index);
|
||||
let depth = 0;
|
||||
let quote = "";
|
||||
for (let index = brace; index < source.length; index++) {
|
||||
const char = source[index]!;
|
||||
if (quote) {
|
||||
if (char === "\\") index++;
|
||||
else if (char === quote) quote = "";
|
||||
continue;
|
||||
}
|
||||
if (char === '"' || char === "'" || char === "`") quote = char;
|
||||
else if (char === "{") depth++;
|
||||
else if (char === "}" && --depth === 0) {
|
||||
blocks.push(source.slice(brace + 1, index));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
test("all UI output payload declarations are concrete", () => {
|
||||
const root = join(import.meta.dir, "../components");
|
||||
const offenders: string[] = [];
|
||||
for (const file of readdirSync(root).filter((name) => name.endsWith(".wrn"))) {
|
||||
for (const block of outputBlocks(readFileSync(join(root, file), "utf8"))) {
|
||||
if (/\bunknown\b/.test(block)) offenders.push(file);
|
||||
}
|
||||
}
|
||||
expect(offenders).toEqual([]);
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
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");
|
||||
|
||||
@@ -166,7 +167,13 @@ describe("@wrnexus/ui redesign component pack", () => {
|
||||
|
||||
for (const [name, tokens] of Object.entries(expectations)) {
|
||||
const source = await readFile(join(componentsDir, `${name}.wrn`), "utf8");
|
||||
for (const token of tokens) expect(source).toContain(token);
|
||||
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]!);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+22
-15
@@ -1,7 +1,7 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { compileWireFile } from "../../compiler/src/index.ts";
|
||||
import { compileWireFile, parse } from "../../compiler/src/index.ts";
|
||||
import { mountHtml, renderComponent } from "../../test/src/index.ts";
|
||||
import { uiComponentNames, uiComponentsDir, uiComponentPath, uiCss } from "../src/index.ts";
|
||||
|
||||
@@ -83,9 +83,11 @@ test("component reference contains only explicitly declared public events", () =
|
||||
const componentPath = uiComponentPath(component.name);
|
||||
const source = readFileSync(componentPath, "utf8");
|
||||
|
||||
const ast = parse(source);
|
||||
const declaredEvents = [
|
||||
...source.matchAll(/@event\s+([A-Za-z][A-Za-z0-9_]*)\s*=\s*function/g),
|
||||
].map((match) => match[1]);
|
||||
...ast.events.map((event) => event.name),
|
||||
...ast.outputs.map((output) => output.name),
|
||||
];
|
||||
|
||||
const expectedEvents = [...new Set(declaredEvents)];
|
||||
|
||||
@@ -178,15 +180,18 @@ test("navbar supports brands, nested menus, actions, utility slots, and public e
|
||||
const source = readFileSync(uiComponentPath("Navbar"), "utf8");
|
||||
expect(source).toContain('slot name="topbar"');
|
||||
expect(source).toContain('slot name="actions"');
|
||||
expect(source).toContain("brand = {}");
|
||||
expect(source).toContain("actions = []");
|
||||
expect(source).toContain("openOnHover = false");
|
||||
expect(source).toContain('maxWidth = "full"');
|
||||
const contract = parse(source);
|
||||
const defaults = new Map(contract.props.map((prop) => [prop.name, prop.default]));
|
||||
expect(defaults.get("brand")).toBe("{}");
|
||||
expect(defaults.get("actions")).toBe("[]");
|
||||
expect(defaults.get("openOnHover")).toBe("false");
|
||||
expect(defaults.get("maxWidth")).toBe('"full"');
|
||||
expect(source).toContain("item.children");
|
||||
expect(source).toContain("child.children");
|
||||
expect(source).toContain("wire-navbar__dropdown--{item.type || 'dropdown'}");
|
||||
const outputs = new Set(contract.outputs.map((output) => output.name));
|
||||
for (const event of ["toggle", "open", "close", "select", "action"]) {
|
||||
expect(source).toContain(`@event ${event} = function`);
|
||||
expect(outputs.has(event)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -214,8 +219,9 @@ test("footer supports typed entries, responsive columns, pre/post slots, and eve
|
||||
expect(source).toMatch(/<slot\s+name="copyright-right"\s*>/);
|
||||
expect(source).toContain('item.type === "header"');
|
||||
expect(source).toContain("wire-footer--columns-{columns}");
|
||||
expect(source).toContain("@event select = function");
|
||||
expect(source).toContain("@event action = function");
|
||||
const outputs = new Set(parse(source).outputs.map((output) => output.name));
|
||||
expect(outputs.has("select")).toBe(true);
|
||||
expect(outputs.has("action")).toBe(true);
|
||||
});
|
||||
|
||||
test("navbar renders a brand, nested dropdowns, mega groups, and actions", async () => {
|
||||
@@ -377,10 +383,10 @@ test("dropdown renders slotted account triggers, menu structure, and public even
|
||||
});
|
||||
|
||||
expect(source).toContain('slot name="trigger"');
|
||||
expect(source).toContain("@event select = function");
|
||||
expect(parse(source).outputs.map((output) => output.name)).toContain("select");
|
||||
expect(html).toContain("wire-dropdown__panel");
|
||||
expect(html).toContain("Profile");
|
||||
expect(html).toContain("wire-dropdown__item--danger");
|
||||
expect(html).toContain('data-danger="true"');
|
||||
});
|
||||
|
||||
test("footer renders header and link entries with the requested column count", async () => {
|
||||
@@ -452,9 +458,10 @@ test("component boundaries have no default margins and expose the canonical clas
|
||||
|
||||
test("every component exposes universal color and size configuration", () => {
|
||||
for (const name of uiComponentNames()) {
|
||||
const source = readFileSync(uiComponentPath(name), "utf8");
|
||||
expect(source).toMatch(/^\s+color\s*=/m);
|
||||
expect(source).toMatch(/^\s+size\s*=/m);
|
||||
const ast = parse(readFileSync(uiComponentPath(name), "utf8"));
|
||||
const props = new Set(ast.props.map((prop) => prop.name));
|
||||
expect(props.has("color")).toBe(true);
|
||||
expect(props.has("size")).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user