dimensionlab-website/packages/ui/src/storybook.test.ts

211 lines
6.3 KiB
TypeScript

import { existsSync, readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
const root = process.cwd();
const packageRoot = existsSync(join(root, "packages/ui/package.json"))
? join(root, "packages/ui")
: root;
const componentsDir = join(packageRoot, "src/components");
const storiesDir = join(packageRoot, "src/stories");
const componentDomains = {
foundation: [
"Badge",
"Button",
"IconButton",
"IconGlyph",
"ProgressMeter",
"Separator",
"StatusBadge",
"ThemeToggle",
],
frames: [
"CornerBracketFrame",
"DashboardFrame",
"DashboardHeader",
"DiagonalStripeField",
"FooterCell",
"FooterStatusCell",
"GridFrame",
"ModuleCard",
"Panel",
"ScanlineField",
],
operations: [
"ServiceGroupPanel",
"ServicePanel",
"ServiceRow",
"StatusStrip",
"SystemState",
"WeatherModule",
],
telemetry: [
"LineChart",
"SignalTrace",
"Sparkline",
"TelemetryCard",
"TelemetryGrid",
"TelemetryStrip",
],
} as const;
const componentFiles: ReadonlyMap<string, string> = new Map(
Object.entries(componentDomains).flatMap(([domain, components]) =>
components.map((component) => [
component,
join(componentsDir, domain, `${component}.tsx`),
]),
),
);
const requiredStoryFiles = [
"Badge.stories.tsx",
"Button.stories.tsx",
"DashboardFrame.stories.tsx",
"DashboardHeader.stories.tsx",
"DashboardOnePager.stories.tsx",
"CornerBracketFrame.stories.tsx",
"DiagonalStripeField.stories.tsx",
"FooterCell.stories.tsx",
"FooterStatusCell.stories.tsx",
"GridFrame.stories.tsx",
"IconButton.stories.tsx",
"IconGlyph.stories.tsx",
"LineChart.stories.tsx",
"ModuleCard.stories.tsx",
"Panel.stories.tsx",
"ProgressMeter.stories.tsx",
"Separator.stories.tsx",
"ServiceGroupPanel.stories.tsx",
"ServicePanel.stories.tsx",
"ServiceRow.stories.tsx",
"SignalTrace.stories.tsx",
"Sparkline.stories.tsx",
"StatusBadge.stories.tsx",
"StatusStrip.stories.tsx",
"SystemState.stories.tsx",
"ScanlineField.stories.tsx",
"TelemetryCard.stories.tsx",
"TelemetryGrid.stories.tsx",
"TelemetryStrip.stories.tsx",
"ThemeToggle.stories.tsx",
"WeatherModule.stories.tsx",
] as const;
const forbiddenStoryContent = [
"dimension lab",
"dimensionlab",
"vince",
"homepage",
] as const;
describe("Storybook inventory", () => {
test("exposes scripts for local and static Storybook review", () => {
const packageJson = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8")) as {
scripts?: Record<string, string>;
};
expect(packageJson.scripts?.storybook).toBeTypeOf("string");
expect(packageJson.scripts?.storybook).toContain("storybook dev");
expect(packageJson.scripts?.["build-storybook"]).toBe("storybook build");
});
test("has a story for every reusable dashboard UI component", () => {
for (const filename of requiredStoryFiles) {
expect(existsSync(join(storiesDir, filename)), `${filename} is missing`).toBe(true);
}
});
test("loads dashboard component styles through the global app stylesheet", () => {
const packageStyles = readFileSync(join(packageRoot, "src/styles.css"), "utf8");
const dashboardFrame = readFileSync(requiredComponentPath("DashboardFrame"), "utf8");
expect(packageStyles).toContain('./components/styles.css');
expect(dashboardFrame).not.toContain('./styles.css');
});
test("configures Storybook theme switching for reusable components", () => {
const previewSource = readFileSync(join(packageRoot, ".storybook/preview.ts"), "utf8");
expect(previewSource).toContain("globalTypes");
expect(previewSource).toContain("data-ui-theme");
expect(previewSource).toContain("../src/styles.css");
});
test("keeps component and story files paired as the UI inventory changes", () => {
const componentStoryFiles = [...componentFiles.keys()].map(
(component) => `${component}.stories.tsx`,
);
for (const filename of componentStoryFiles) {
expect(existsSync(join(storiesDir, filename)), `${filename} is missing`).toBe(true);
}
});
test("keeps Storybook fixtures generic and content-free", () => {
const storyText = readdirSync(storiesDir)
.filter((filename) => filename.endsWith(".tsx") || filename.endsWith(".ts"))
.map((filename) => readFileSync(join(storiesDir, filename), "utf8").toLowerCase())
.join("\n");
for (const forbidden of forbiddenStoryContent) {
expect(storyText).not.toContain(forbidden);
}
});
test("includes style-only decorative primitives in the reusable UI inventory", () => {
for (const component of [
"CornerBracketFrame",
"DiagonalStripeField",
"ScanlineField",
"SignalTrace",
]) {
expect(existsSync(requiredComponentPath(component)), `${component} is missing`).toBe(true);
expect(
existsSync(join(storiesDir, `${component}.stories.tsx`)),
`${component}.stories.tsx is missing`,
).toBe(true);
}
});
test("keeps decorative primitives hidden from assistive technology", () => {
for (const component of [
"CornerBracketFrame",
"DiagonalStripeField",
"ScanlineField",
"SignalTrace",
]) {
const source = readFileSync(requiredComponentPath(component), "utf8");
expect(source).toContain('aria-hidden="true"');
}
});
test("does not add deferred form/navigation primitives", () => {
for (const component of ["Input", "ToggleGroup", "ScrollArea"]) {
expect(componentFiles.has(component)).toBe(false);
expect(existsSync(join(componentsDir, `${component}.tsx`))).toBe(false);
expect(existsSync(join(storiesDir, `${component}.stories.tsx`))).toBe(false);
}
});
test("does not keep legacy stories in the React Storybook inventory", () => {
const legacyStoryExtension = [".stories", ".sve", "lte"].join("");
const legacyStories = readdirSync(storiesDir).filter((filename) =>
filename.endsWith(legacyStoryExtension),
);
expect(legacyStories).toEqual([]);
});
});
function requiredComponentPath(component: string): string {
const componentPath = componentFiles.get(component);
expect(componentPath, `${component} is missing from the UI component map`).toBeTypeOf(
"string",
);
return componentPath as string;
}