import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; import { join, resolve } from "node:path"; import { describe, expect, it } from "@suaveplan/testing/runner"; const PKG_ROOT = resolve(__dirname, ".."); const PKG_JSON = JSON.parse( readFileSync(join(PKG_ROOT, "package.json"), "utf8") ) as { exports: Record }; const REQUIRED_HOOKS = [ "use-engine-state", "use-audio-engine", "use-timeline", "use-transport", "use-transport-time", "use-track", "use-plugin-chain", "use-chain-plugin", "use-audio-fx-plugin", "use-parameter", "use-preset", "use-chain-snapshot", "use-bus", "use-midi-controller", "use-analyser", "use-playlist", ]; const REQUIRED_COMPONENTS = [ "plugin-slot", "plugin-chain-view", "plugin-parameter-knob", "plugin-parameter-slider", "preset-manager", "midi-controller-setup", "transport-controls", "track-list", "track-row", "master-meter", "mixer", "audio-visualizer", "audio-player", "basic-player", "premium-player", "microphone-controls", "glados-controls", "plugin-loader", ]; const REQUIRED_SURFACES = [ ".", "./provider", "./hooks", ...REQUIRED_HOOKS.map((h) => `./hooks/${h}`), "./components", ...REQUIRED_COMPONENTS.map((c) => `./components/${c}`), "./stores", "./a11y", "./error-codes", ]; describe("subpath exports", () => { it("every Phase-1 surface has a subpath export entry", { timeout: 5000, }, () => { for (const surface of REQUIRED_SURFACES) { expect(PKG_JSON.exports[surface]).toBeDefined(); } }); it("every component has a subpath export entry", { timeout: 5000, }, () => { for (const c of REQUIRED_COMPONENTS) { expect(PKG_JSON.exports[`./components/${c}`]).toBeDefined(); } expect(PKG_JSON.exports["./components"]).toBeDefined(); }); it("includes exactly 16 hook subpaths (13 foundation + 1 Phase 3 + 2 player-tier)", { timeout: 5000, }, () => { const hookKeys = Object.keys(PKG_JSON.exports).filter((k) => k.startsWith("./hooks/") ); expect(hookKeys.length).toBe(16); }); }); const STORY_NAME_REGEX = /^(Default|Interactive|Edge.*)$/; function readStoryNames(filePath: string): ReadonlyArray { if (!existsSync(filePath)) return []; const text = readFileSync(filePath, "utf8"); const names: string[] = []; const re = /export\s+const\s+(\w+)\s*:\s*Story\s*=/g; let m: RegExpExecArray | null = re.exec(text); while (m !== null) { names.push(m[1] as string); m = re.exec(text); } return names; } interface ExportedComponentFile { readonly path: string; readonly storyPath: string; } function listExportedComponentFiles(): ExportedComponentFile[] { const files: ExportedComponentFile[] = []; const componentsDir = join(PKG_ROOT, "src", "components"); for (const name of readdirSync(componentsDir)) { const dir = join(componentsDir, name); if (!statSync(dir).isDirectory()) continue; if (name === "internal") continue; files.push({ path: join(dir, `${name}.tsx`), storyPath: join(dir, `${name}.stories.tsx`), }); } files.push({ path: join(PKG_ROOT, "src", "provider", "audio-provider.tsx"), storyPath: join( PKG_ROOT, "src", "provider", "audio-provider.stories.tsx" ), }); return files; } describe("Storybook coverage gate", () => { it("every component has default, interactive, and edge-case Storybook stories", { timeout: 5000, }, () => { const componentFiles = listExportedComponentFiles(); const missing: string[] = []; for (const file of componentFiles) { if (!existsSync(file.storyPath)) { missing.push(`${file.path}: no sibling ${file.storyPath}`); continue; } const names = readStoryNames(file.storyPath); const matched = names.filter((n) => STORY_NAME_REGEX.test(n)); const hasDefault = matched.includes("Default"); const hasInteractive = matched.includes("Interactive"); const hasEdge = matched.some((n) => n.startsWith("Edge")); if (!hasDefault || !hasInteractive || !hasEdge) { missing.push( `${file.path}: stories=[${names.join( ", " )}]; require Default + Interactive + Edge*` ); } } expect(missing).toEqual([]); }); });