142 lines
7.2 KiB
TypeScript
142 lines
7.2 KiB
TypeScript
import { readFile } from "node:fs/promises";
|
|
import { resolve } from "node:path";
|
|
|
|
import { describe, expect, test } from "vitest";
|
|
|
|
import {
|
|
customJsxAuthoringCatalog,
|
|
getCustomJsxPropType,
|
|
resolveCustomJsxPropDescriptor,
|
|
} from "../core/component-catalog";
|
|
import {
|
|
CUSTOM_JSX_AUTHORING_CATALOG_SCHEMA_VERSION,
|
|
CUSTOM_WIDGET_AUTHORING_VERSION,
|
|
} from "../core/component-catalog-types";
|
|
import { customJsxComponentRegistry } from "../core/component-registry";
|
|
|
|
const repositoryRoot = resolve(import.meta.dirname, "../../../..");
|
|
const catalogPath = resolve(repositoryRoot, "packages/custom-widgets/src/core/component-catalog.generated.json");
|
|
const publicCatalogPath = resolve(repositoryRoot, "apps/docs/static/custom-widgets/component-catalog-v1.json");
|
|
|
|
// Keep a reviewed ceiling close to the generated artifact so accidental metadata
|
|
// duplication must receive explicit review.
|
|
const MAX_CATALOG_BYTES = 800 * 1024;
|
|
|
|
describe("Custom JSX authoring catalog", () => {
|
|
test("is versioned against the installed authoring surface", async () => {
|
|
const mantinePackage = JSON.parse(
|
|
await readFile(resolve(repositoryRoot, "node_modules/@mantine/core/package.json"), "utf8"),
|
|
) as { version: string };
|
|
|
|
expect(customJsxAuthoringCatalog.schemaVersion).toBe(CUSTOM_JSX_AUTHORING_CATALOG_SCHEMA_VERSION);
|
|
expect(customJsxAuthoringCatalog.mantineVersion).toBe(mantinePackage.version);
|
|
expect(customJsxAuthoringCatalog.customWidgetVersion).toBe(CUSTOM_WIDGET_AUTHORING_VERSION);
|
|
});
|
|
|
|
test("publishes deterministic unique component and prop records", () => {
|
|
const componentNames = customJsxAuthoringCatalog.components.map(({ name }) => name);
|
|
const globalPropNames = customJsxAuthoringCatalog.globalProps.map(({ name }) => name);
|
|
const types = customJsxAuthoringCatalog.types;
|
|
expect(new Set(componentNames).size).toBe(componentNames.length);
|
|
expect(new Set(globalPropNames).size).toBe(globalPropNames.length);
|
|
expect(new Set(types).size).toBe(types.length);
|
|
expect(componentNames).toEqual([...componentNames].toSorted());
|
|
expect(globalPropNames).toEqual([...globalPropNames].toSorted());
|
|
expect(types).toEqual([...types].toSorted());
|
|
|
|
const globals = new Map(customJsxAuthoringCatalog.globalProps.map((prop) => [prop.name, prop]));
|
|
for (const component of customJsxAuthoringCatalog.components) {
|
|
const propNames = component.props.map(({ name }) => name);
|
|
expect(new Set(propNames).size, component.name).toBe(propNames.length);
|
|
expect(propNames, component.name).toEqual([...propNames].toSorted());
|
|
for (const prop of component.props) {
|
|
expect(prop.source, `${component.name}.${prop.name}`).toBe("component");
|
|
expect(prop.typeRef, `${component.name}.${prop.name}`).toBeGreaterThanOrEqual(0);
|
|
expect(prop.typeRef, `${component.name}.${prop.name}`).toBeLessThan(types.length);
|
|
expect(getCustomJsxPropType(prop), `${component.name}.${prop.name}`).not.toBe("");
|
|
const global = globals.get(prop.name);
|
|
if (global) {
|
|
expect(
|
|
prop.required !== global.required ||
|
|
getCustomJsxPropType(prop) !== getCustomJsxPropType(global) ||
|
|
JSON.stringify(prop.literalValues ?? []) !== JSON.stringify(global.literalValues ?? []),
|
|
`${component.name}.${prop.name} must be a meaningful global-prop override`,
|
|
).toBe(true);
|
|
}
|
|
}
|
|
}
|
|
for (const prop of customJsxAuthoringCatalog.globalProps) {
|
|
expect(prop.source, prop.name).toBe("global");
|
|
expect(prop.required, prop.name).toBe(false);
|
|
expect(prop.typeRef, prop.name).toBeGreaterThanOrEqual(0);
|
|
expect(prop.typeRef, prop.name).toBeLessThan(types.length);
|
|
expect(resolveCustomJsxPropDescriptor(prop).type, prop.name).not.toBe("");
|
|
}
|
|
});
|
|
|
|
test("covers the complete runtime component registry", () => {
|
|
const catalogNames = customJsxAuthoringCatalog.components.map(({ name }) => name);
|
|
const runtimeNames = customJsxComponentRegistry.map(({ name }) => name).toSorted();
|
|
expect(catalogNames).toEqual(runtimeNames);
|
|
});
|
|
|
|
test("includes prop APIs, bindings, accessibility, and denied reasons", () => {
|
|
const textInput = customJsxAuthoringCatalog.components.find(({ name }) => name === "TextInput");
|
|
const label = textInput?.props.find(({ name }) => name === "label");
|
|
expect(label && getCustomJsxPropType(label)).toContain("ReactNode");
|
|
expect(label?.required).toBe(false);
|
|
expect(label?.description).toBeTruthy();
|
|
expect(textInput?.bind).toEqual({ type: "string", initialProp: "defaultValue" });
|
|
|
|
const switchComponent = customJsxAuthoringCatalog.components.find(({ name }) => name === "Switch");
|
|
expect(switchComponent?.bind).toEqual({ type: "boolean", initialProp: "defaultChecked" });
|
|
|
|
expect(customJsxAuthoringCatalog.components.some(({ name }) => name === "RecursiveList")).toBe(false);
|
|
|
|
const portal = customJsxAuthoringCatalog.components.find(({ name }) => name === "Portal");
|
|
expect(portal?.safety).toBe("denied");
|
|
expect(portal?.deniedReason).toBeTruthy();
|
|
expect(portal?.props).toEqual([]);
|
|
|
|
const tableOfContents = customJsxAuthoringCatalog.components.find(({ name }) => name === "TableOfContents");
|
|
expect(tableOfContents?.safety).toBe("denied");
|
|
expect(tableOfContents?.deniedReason).toContain("outside the widget root");
|
|
|
|
const tooltip = customJsxAuthoringCatalog.components.find(({ name }) => name === "Tooltip");
|
|
expect(tooltip?.blockedProps).toContainEqual(
|
|
expect.objectContaining({ name: "target", reason: expect.stringContaining("outside the widget root") }),
|
|
);
|
|
expect(tooltip?.props.map(({ name }) => name)).not.toContain("target");
|
|
|
|
const datePickerInput = customJsxAuthoringCatalog.components.find(({ name }) => name === "DatePickerInput");
|
|
expect(datePickerInput?.blockedProps.map(({ name }) => name)).toEqual(["dropdownType", "modalProps"]);
|
|
});
|
|
|
|
test("documents blocked prop capabilities once and excludes them from APIs", () => {
|
|
const blockedNames = customJsxAuthoringCatalog.blockedCapabilities.map(({ name }) => name);
|
|
expect(new Set(blockedNames).size).toBe(blockedNames.length);
|
|
expect(customJsxAuthoringCatalog.blockedCapabilities).toContainEqual(
|
|
expect.objectContaining({ kind: "prop", name: "dangerouslySetInnerHTML" }),
|
|
);
|
|
expect(customJsxAuthoringCatalog.blockedCapabilities).toContainEqual(
|
|
expect.objectContaining({ kind: "prop-pattern", name: "on*" }),
|
|
);
|
|
const authoredProps = [
|
|
...customJsxAuthoringCatalog.globalProps,
|
|
...customJsxAuthoringCatalog.components.flatMap(({ props }) => props),
|
|
];
|
|
expect(authoredProps.some(({ name }) => /^on/iu.test(name))).toBe(false);
|
|
expect(authoredProps.some(({ name }) => name === "dangerouslySetInnerHTML")).toBe(false);
|
|
});
|
|
|
|
test("stays within the reviewed compact byte budget", async () => {
|
|
const catalog = await readFile(catalogPath);
|
|
expect(catalog.byteLength).toBeLessThanOrEqual(MAX_CATALOG_BYTES);
|
|
});
|
|
|
|
test("publishes a byte-identical static documentation copy", async () => {
|
|
const [catalog, publicCatalog] = await Promise.all([readFile(catalogPath), readFile(publicCatalogPath)]);
|
|
expect(publicCatalog.equals(catalog)).toBe(true);
|
|
});
|
|
});
|