@recursica/adapter-tester 5.0.0 → 5.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/adapter-tester.schema.json.d.ts +4 -0
- package/dist/cli.cjs +13 -11
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +36 -29
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/devServer.d.ts.map +1 -1
- package/dist/fileConfig.d.ts +3 -0
- package/dist/fileConfig.d.ts.map +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/testing/runVisualRegression.d.ts.map +1 -1
- package/dist/testing.cjs +4 -4
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.js +16 -14
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
- package/report-header.txt +1 -1
- package/src/adapter-tester.schema.json +4 -0
package/dist/testing.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.cjs","sources":["../src/golden/diffPng.ts","../src/golden/validateManifest.ts","../src/golden/manifestStore.ts","../src/golden/resolveSourceOfTruthGolden.ts","../src/testing/runVisualRegression.ts"],"sourcesContent":["import pixelmatch from \"pixelmatch\";\nimport { PNG } from \"pngjs\";\n\nexport interface PngDiffResult {\n /** Mismatched-pixel count, or `Infinity` if the two images aren't even the\n * same dimensions — pixelmatch itself throws on a size mismatch, and a size\n * mismatch is itself a real difference, not something to swallow. */\n diffPixels: number;\n /** Visual highlight of the mismatched pixels, encoded as a PNG buffer.\n * `null` when `diffPixels` is `Infinity` — there's no pixel-aligned diff to\n * render across two different-sized images. */\n diffImage: Buffer | null;\n}\n\n/** Pixel-diffs two PNG buffers. */\nexport function diffPngBuffers(a: Buffer, b: Buffer): PngDiffResult {\n const imgA = PNG.sync.read(a);\n const imgB = PNG.sync.read(b);\n if (imgA.width !== imgB.width || imgA.height !== imgB.height) {\n return { diffPixels: Infinity, diffImage: null };\n }\n const diff = new PNG({ width: imgA.width, height: imgA.height });\n const diffPixels = pixelmatch(\n imgA.data,\n imgB.data,\n diff.data,\n imgA.width,\n imgA.height,\n { threshold: 0.1 },\n );\n return { diffPixels, diffImage: PNG.sync.write(diff) };\n}\n","import { Ajv } from \"ajv\";\nimport * as ajvFormatsModule from \"ajv-formats\";\nimport type { FormatsPlugin } from \"ajv-formats\";\nimport schema from \"./manifest.schema.json\" with { type: \"json\" };\n\n// See validateFileConfig.ts for why `.default` has to be unwrapped by hand.\nconst addFormats = (ajvFormatsModule as unknown as { default: FormatsPlugin })\n .default;\n\nconst ajv = new Ajv({ allErrors: true, strict: true });\naddFormats(ajv);\nconst validate = ajv.compile(schema);\n\n/**\n * Validates a parsed `test/golden/manifest.json` against `manifest.schema.json`.\n * Throws with every violation listed — callers must not silently coerce or\n * drop invalid entries.\n */\nexport function validateGoldenManifest(data: unknown, path: string): void {\n if (validate(data)) return;\n\n const errors = (validate.errors ?? [])\n .map((error) => {\n const extra = error.params?.additionalProperty\n ? ` '${error.params.additionalProperty}'`\n : \"\";\n return ` - ${error.instancePath || \"root\"} ${error.message}${extra}`;\n })\n .join(\"\\n\");\n throw new Error(`Invalid ${path}:\\n${errors}`);\n}\n","import {\n closeSync,\n existsSync,\n mkdirSync,\n openSync,\n readFileSync,\n renameSync,\n rmSync,\n unlinkSync,\n writeFileSync,\n} from \"node:fs\";\nimport { join } from \"node:path\";\nimport { validateGoldenManifest } from \"./validateManifest.js\";\n\nexport interface GoldenManifestEntry {\n createdAt: string;\n sourceOfTruthCreatedAt?: string;\n}\n\nexport type GoldenManifest = Record<string, GoldenManifestEntry>;\n\nexport function manifestPath(goldenDir: string): string {\n return join(goldenDir, \"manifest.json\");\n}\n\nfunction manifestLockPath(goldenDir: string): string {\n return join(goldenDir, \"manifest.json.lock\");\n}\n\nexport function goldenImagePath(goldenDir: string, storyId: string): string {\n return join(goldenDir, `${storyId}.png`);\n}\n\n/** Returns `{}` if no manifest exists yet — a fresh adapter with no goldens\n * captured is the normal starting state, not an error. */\nexport function loadManifest(goldenDir: string): GoldenManifest {\n const path = manifestPath(goldenDir);\n if (!existsSync(path)) return {};\n const data = JSON.parse(readFileSync(path, \"utf8\"));\n validateGoldenManifest(data, path);\n return data;\n}\n\n/** Validates before writing, and sorts keys so the diff on a reviewed PR is\n * stable regardless of the order stories happened to run in. Writes to a\n * temp file and renames over the real one — `rename` is atomic, so a\n * concurrent `loadManifest` (running in another Playwright worker) never\n * observes a half-written file. */\nexport function saveManifest(\n goldenDir: string,\n manifest: GoldenManifest,\n): void {\n const path = manifestPath(goldenDir);\n validateGoldenManifest(manifest, path);\n const sorted: GoldenManifest = {};\n for (const key of Object.keys(manifest).sort()) {\n sorted[key] = manifest[key]!;\n }\n mkdirSync(goldenDir, { recursive: true });\n const tmpPath = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;\n writeFileSync(tmpPath, JSON.stringify(sorted, null, 2) + \"\\n\");\n renameSync(tmpPath, path);\n}\n\nconst LOCK_RETRY_MS = 25;\nconst LOCK_TIMEOUT_MS = 15_000;\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/** Exclusive-create the lock file, spin-retrying until it's free. `wx` fails\n * atomically (`EEXIST`) if another worker process already holds it — that's\n * the only signal we need, no third-party lock library required for a\n * same-machine, same-run lock like this. */\nasync function acquireManifestLock(goldenDir: string): Promise<void> {\n mkdirSync(goldenDir, { recursive: true });\n const path = manifestLockPath(goldenDir);\n const deadline = Date.now() + LOCK_TIMEOUT_MS;\n for (;;) {\n try {\n closeSync(openSync(path, \"wx\"));\n return;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"EEXIST\") throw error;\n if (Date.now() >= deadline) {\n throw new Error(\n `Timed out waiting for the manifest lock at \"${path}\" — delete it if a previous run crashed while holding it.`,\n );\n }\n await sleep(LOCK_RETRY_MS);\n }\n }\n}\n\nfunction releaseManifestLock(goldenDir: string): void {\n rmSync(manifestLockPath(goldenDir), { force: true });\n}\n\n/** Runs `updater` against this story's manifest entry under an exclusive\n * lock on `manifest.json`: reloads the manifest fresh, applies `updater`,\n * and saves it back, all before releasing the lock. Concurrent Playwright\n * workers each own a different story, so this is the only section that\n * needs to serialize — everything else about a story (its screenshot, its\n * golden image file, its diff) is independent of every other story. */\nexport async function updateManifestEntry(\n goldenDir: string,\n storyId: string,\n updater: (\n entry: GoldenManifestEntry | undefined,\n ) => GoldenManifestEntry | undefined,\n): Promise<GoldenManifestEntry | undefined> {\n await acquireManifestLock(goldenDir);\n try {\n const manifest = loadManifest(goldenDir);\n const nextEntry = updater(manifest[storyId]);\n if (nextEntry === undefined) {\n delete manifest[storyId];\n } else {\n manifest[storyId] = nextEntry;\n }\n saveManifest(goldenDir, manifest);\n return nextEntry;\n } finally {\n releaseManifestLock(goldenDir);\n }\n}\n\nexport function saveGoldenImage(\n goldenDir: string,\n storyId: string,\n buffer: Buffer,\n): void {\n mkdirSync(goldenDir, { recursive: true });\n writeFileSync(goldenImagePath(goldenDir, storyId), buffer);\n}\n\n/** Removes the golden `.png` + manifest entry for every story id in the\n * manifest that isn't in `currentStoryIds` (e.g. a story renamed or deleted\n * from Storybook) — otherwise those never get cleaned up on their own,\n * since a run only ever adds/updates entries for stories it actually saw.\n * Returns the pruned ids, for the caller to report. Both the file removal\n * and the manifest delete are idempotent, so it's safe for this to run\n * redundantly from more than one Playwright worker. */\nexport async function pruneOrphanedGoldens(\n goldenDir: string,\n currentStoryIds: ReadonlySet<string>,\n): Promise<string[]> {\n const manifest = loadManifest(goldenDir);\n const orphanIds = Object.keys(manifest).filter(\n (id) => !currentStoryIds.has(id),\n );\n for (const id of orphanIds) {\n const imagePath = goldenImagePath(goldenDir, id);\n if (existsSync(imagePath)) unlinkSync(imagePath);\n await updateManifestEntry(goldenDir, id, () => undefined);\n }\n return orphanIds;\n}\n","import { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { SourceOfTruthGoldenLocation } from \"../config.js\";\nimport {\n goldenImagePath,\n loadManifest,\n manifestPath,\n type GoldenManifest,\n} from \"./manifestStore.js\";\nimport { validateGoldenManifest } from \"./validateManifest.js\";\n\nconst GITHUB_REPO = \"borderux/recursica\";\n\nexport interface SourceOfTruthGolden {\n manifest: GoldenManifest;\n /** Returns the golden PNG bytes for a story, or `null` if the source of\n * truth has no golden captured for it yet. */\n readImage(storyId: string): Promise<Buffer | null>;\n}\n\nasync function resolveNpmVersion(\n packageName: string,\n versionSpec: string,\n): Promise<string> {\n const response = await fetch(`https://registry.npmjs.org/${packageName}`);\n if (!response.ok) {\n throw new Error(\n `Could not reach npm registry for ${packageName}: ${response.statusText}`,\n );\n }\n const data = (await response.json()) as {\n \"dist-tags\"?: Record<string, string>;\n versions?: Record<string, unknown>;\n };\n const resolved =\n data[\"dist-tags\"]?.[versionSpec] ??\n (data.versions?.[versionSpec] ? versionSpec : undefined);\n if (!resolved) {\n throw new Error(\n `${packageName} has no version or dist-tag \"${versionSpec}\" on the npm registry.`,\n );\n }\n return resolved;\n}\n\n// This monorepo's packages all live at `packages/<unscoped-name>` — mirrors\n// the same convention `mantineSourceOfTruthHarness` and every existing\n// `packages/*/package.json`'s `repository.directory` field already assume.\nfunction packageDirectory(packageName: string): string {\n return `packages/${packageName.split(\"/\").pop()}`;\n}\n\n/**\n * Resolves the source-of-truth adapter's golden images for the divergence\n * check. Never boots a Storybook — both location types resolve to plain\n * files, fetched once and cached, not re-diffed per pixel over the wire.\n *\n * `location.type === \"local\"`: a sibling package already checked out (this\n * monorepo's own `sourceOfTruth.type: \"url\"` mode) — read its\n * `test/golden/` directly, including any uncommitted local changes.\n *\n * `location.type === \"npm\"`: no local checkout (the default, standalone-repo\n * mode) — resolve the installed version against the npm registry, then fetch\n * that exact version's `test/golden/` from the public GitHub repo at the\n * matching release tag (changesets tags every release as\n * `<packageName>@<version>`), caching what's downloaded under `cacheDir`.\n *\n * Returns `null` — degrading the divergence check to a skip, not a failure —\n * when no golden baseline exists yet for this version, or the registry/repo\n * is unreachable.\n */\nexport async function resolveSourceOfTruthGolden(\n location: SourceOfTruthGoldenLocation,\n): Promise<SourceOfTruthGolden | null> {\n if (location.type === \"local\") {\n if (!existsSync(manifestPath(location.dir))) {\n console.warn(\n `No golden baseline found yet at ${location.dir} — source-of-truth divergence check skipped for this run.`,\n );\n return null;\n }\n const manifest = loadManifest(location.dir);\n return {\n manifest,\n async readImage(storyId) {\n const path = goldenImagePath(location.dir, storyId);\n return existsSync(path) ? readFileSync(path) : null;\n },\n };\n }\n\n let version: string;\n try {\n version = await resolveNpmVersion(\n location.packageName,\n location.versionSpec,\n );\n } catch (error) {\n console.warn(\n `Could not resolve ${location.packageName}@${location.versionSpec} — source-of-truth divergence check skipped for this run.`,\n error,\n );\n return null;\n }\n\n const cacheDir = join(location.cacheDir, version);\n const tag = `${location.packageName}@${version}`;\n const rawBase = `https://raw.githubusercontent.com/${GITHUB_REPO}/${tag}/${packageDirectory(location.packageName)}/test/golden`;\n\n let manifest: GoldenManifest;\n const cachedManifestPath = manifestPath(cacheDir);\n if (existsSync(cachedManifestPath)) {\n manifest = loadManifest(cacheDir);\n } else {\n let response: Response;\n try {\n response = await fetch(`${rawBase}/manifest.json`);\n } catch (error) {\n console.warn(\n `Could not reach GitHub to fetch ${tag}'s golden baseline — source-of-truth divergence check skipped for this run.`,\n error,\n );\n return null;\n }\n if (!response.ok) {\n console.warn(\n `No golden baseline published for ${tag} — source-of-truth divergence check skipped for this run.`,\n );\n return null;\n }\n const text = await response.text();\n const parsed = JSON.parse(text);\n validateGoldenManifest(parsed, `${rawBase}/manifest.json`);\n mkdirSync(cacheDir, { recursive: true });\n writeFileSync(cachedManifestPath, text);\n manifest = parsed;\n }\n\n return {\n manifest,\n async readImage(storyId) {\n const cachedImagePath = goldenImagePath(cacheDir, storyId);\n if (existsSync(cachedImagePath)) return readFileSync(cachedImagePath);\n const response = await fetch(`${rawBase}/${storyId}.png`);\n if (!response.ok) return null;\n const buffer = Buffer.from(await response.arrayBuffer());\n mkdirSync(cacheDir, { recursive: true });\n writeFileSync(cachedImagePath, buffer);\n return buffer;\n },\n };\n}\n","import { expect } from \"@playwright/test\";\nimport type { Browser, TestInfo } from \"@playwright/test\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport type { AdapterTesterConfig } from \"../config.js\";\nimport { diffPngBuffers } from \"../golden/diffPng.js\";\nimport {\n type GoldenManifestEntry,\n goldenImagePath,\n loadManifest,\n pruneOrphanedGoldens,\n saveGoldenImage,\n updateManifestEntry,\n} from \"../golden/manifestStore.js\";\nimport { resolveSourceOfTruthGolden } from \"../golden/resolveSourceOfTruthGolden.js\";\n\nconst DEFAULT_EXCLUDE_TITLE_PREFIXES = [\"Theme\", \"Tokens\", \"Introduction\"];\n\ninterface StorybookEntry {\n type: string;\n id: string;\n name: string;\n title: string;\n}\n\nfunction matchesPrefix(id: string, prefix: string): boolean {\n return id === prefix || id.startsWith(prefix);\n}\n\n/** Fetches every story `target`'s Storybook currently has, filtered only by\n * `excludeTitlePrefixes` — not `stories.<id>.exclude`, so callers can still\n * tell an excluded story apart from one that's genuinely missing. */\nasync function fetchStories(\n target: { name: string; url: string },\n excludeTitlePrefixes: string[],\n): Promise<StorybookEntry[]> {\n let stories: StorybookEntry[];\n try {\n const response = await fetch(`${target.url}/index.json`);\n if (!response.ok) {\n throw new Error(\n `Failed to fetch Storybook index: ${response.statusText}`,\n );\n }\n const data = (await response.json()) as any;\n const entries = data.entries || {};\n stories = Object.values(entries).filter(\n (entry: any) =>\n entry.type === \"story\" &&\n !excludeTitlePrefixes.some(\n (prefix) =>\n entry.title === prefix || entry.title.startsWith(`${prefix}/`),\n ),\n ) as StorybookEntry[];\n stories.sort((a, b) => a.id.localeCompare(b.id));\n } catch (error) {\n console.error(\n \"Failed to load Storybook index from\",\n `${target.url}/index.json`,\n error,\n );\n throw new Error(\n `Storybook target \"${target.name}\" is not responsive or index.json is missing. Please ensure its Storybook is running.`,\n );\n }\n return stories;\n}\n\n/** Resolves the diff threshold for `storyId`: the longest (most specific)\n * `storyThresholds` key matching by prefix, falling back to\n * `defaultThresholdPixels` when nothing matches. */\nfunction resolveThreshold(\n storyId: string,\n storyThresholds: Record<string, number>,\n defaultThresholdPixels: number,\n): number {\n let bestMatch: string | undefined;\n for (const prefix of Object.keys(storyThresholds)) {\n if (\n matchesPrefix(storyId, prefix) &&\n (!bestMatch || prefix.length > bestMatch.length)\n ) {\n bestMatch = prefix;\n }\n }\n return bestMatch !== undefined\n ? storyThresholds[bestMatch]!\n : defaultThresholdPixels;\n}\n\n/** Everything a generated Playwright spec needs to register the golden-image\n * suite itself. Split out from the actual `test.describe`/`test` calls so\n * those calls execute in the spec file that imports this, not in this\n * library file — otherwise Playwright's HTML report groups every story under\n * this file's own (sourcemapped) path instead of a stable spec name. */\nexport interface VisualRegressionPlan {\n /** `config`'s own (non-source-of-truth) target name, for the suite title. */\n ownTargetName: string;\n /** Suite title suffix describing which check mode is running. */\n suiteLabel: string;\n /** Stories to check, already filtered and sorted by id. */\n stories: StorybookEntry[];\n /** Story ids the source-of-truth adapter has a golden for but this\n * project's own Storybook doesn't — empty outside `checkMode: \"divergence\"`\n * or when `sourceOfTruthGolden` didn't resolve. Excludes ids covered by a\n * `stories.<id>.exclude` entry: an intentional gap, not a sync failure. */\n missingFromSourceOfTruth: string[];\n /** Golden-checks one story. Call this from inside a `test(story.id, ...)`\n * body — safe to run concurrently across Playwright workers, since each\n * call only ever reads/writes its own story's manifest entry (locked at\n * the point it writes it back, so concurrent workers never race each\n * other's entries — see `updateManifestEntry`). */\n checkStory: (\n story: StorybookEntry,\n browser: Browser,\n testInfo: TestInfo,\n ) => Promise<void>;\n}\n\n/** Everything a generated Playwright spec needs to register the golden-image\n * suite itself. Split out from the actual `test.describe`/`test` calls so\n * those calls execute in the spec file that imports this, not in this\n * library file — otherwise Playwright's HTML report groups every story under\n * this file's own (sourcemapped) path instead of a stable spec name. */\nexport interface VisualRegressionPlan {\n /** `config`'s own (non-source-of-truth) target name, for the suite title. */\n ownTargetName: string;\n /** Suite title suffix describing which check mode is running. */\n suiteLabel: string;\n /** Stories to check, already filtered and sorted by id. */\n stories: StorybookEntry[];\n /** Golden-checks one story. Call this from inside a `test(story.id, ...)`\n * body — safe to run concurrently across Playwright workers, since each\n * call only ever reads/writes its own story's manifest entry (locked at\n * the point it writes it back, so concurrent workers never race each\n * other's entries — see `updateManifestEntry`). */\n checkStory: (\n story: StorybookEntry,\n browser: Browser,\n testInfo: TestInfo,\n ) => Promise<void>;\n}\n\n/**\n * Resolves the golden-image plan for `config`'s own target (the one target\n * in `config.targets` not marked `sourceOfTruth`).\n *\n * Two independent checks per story, gated by `config.checkMode`, neither of\n * which boots the source-of-truth adapter's own Storybook — the divergence\n * check below compares stored golden files, not live pages:\n *\n * 1. **Own-drift (`checkMode: \"own\"`, the default; hard fail):** this run's\n * live render vs this project's own stored `test/golden/<story-id>.png`.\n * No golden yet for a story is not a failure — one is captured from this\n * run instead (same as `--update-golden`, scoped to just that story), in\n * either mode.\n * 2. **Source-of-truth divergence (`checkMode: \"divergence\"`; hard fail):**\n * this project's own golden vs the source-of-truth's golden (`config`'s\n * `sourceOfTruthGolden`). Skipped entirely when\n * `config.isSourceOfTruthAdapter` is true — the\n * source-of-truth adapter has nothing above it to diverge from — and\n * skipped per-story when neither side has a baseline yet. A\n * once-flagged divergence stays quiet after `--approve-divergence`,\n * until the source of truth's own golden changes again.\n */\nexport async function resolveVisualRegressionPlan(\n config: AdapterTesterConfig,\n): Promise<VisualRegressionPlan> {\n const ownTarget = config.isSourceOfTruthAdapter\n ? config.targets[0]\n : config.targets.find((target) => !target.sourceOfTruth);\n if (!ownTarget) {\n throw new Error(\n \"adapter-tester config has no non-sourceOfTruth target to run the golden check against.\",\n );\n }\n const excludeTitlePrefixes =\n config.excludeTitlePrefixes ?? DEFAULT_EXCLUDE_TITLE_PREFIXES;\n const storyOverrides = config.stories ?? {};\n const excludeStoryIds = Object.keys(storyOverrides).filter(\n (id) => storyOverrides[id]!.exclude,\n );\n const goldenStoryThresholds = Object.fromEntries(\n Object.entries(storyOverrides)\n .filter(([, override]) => override.goldenThreshold !== undefined)\n .map(([id, override]) => [id, override.goldenThreshold!]),\n );\n const sourceOfTruthStoryThresholds = Object.fromEntries(\n Object.entries(storyOverrides)\n .filter(([, override]) => override.sourceOfTruthThreshold !== undefined)\n .map(([id, override]) => [id, override.sourceOfTruthThreshold!]),\n );\n const goldenDir = config.goldenDir;\n const goldenMode = config.goldenMode;\n const checkMode = config.checkMode;\n\n // Fetched with only excludeTitlePrefixes applied — not excludeStoryIds —\n // so the source-of-truth story-parity check below can tell an excluded\n // story apart from one that's genuinely missing from this Storybook.\n const ownStories = await fetchStories(ownTarget, excludeTitlePrefixes);\n const stories = ownStories.filter(\n (entry) =>\n !excludeStoryIds.some((prefix) => matchesPrefix(entry.id, prefix)),\n );\n\n // `--update-golden` redefines this project's own baseline, so it's also\n // the point a renamed/removed story's now-orphaned golden gets cleaned up\n // — otherwise nothing ever prunes it, since a run only ever adds/updates\n // entries for stories it actually saw in this pass. Uses the full current\n // story list (not narrowed by any `--grep` Playwright itself applies), so\n // this catches every orphan regardless of how the run is scoped.\n if (goldenMode === \"update-golden\") {\n const prunedStoryIds = await pruneOrphanedGoldens(\n goldenDir,\n new Set(stories.map((story) => story.id)),\n );\n if (prunedStoryIds.length > 0) {\n console.warn(\n `Pruned ${prunedStoryIds.length} orphaned golden(s) no longer in Storybook: ${prunedStoryIds.join(\", \")}`,\n );\n }\n }\n\n const sourceOfTruthGolden =\n checkMode !== \"divergence\" ||\n config.isSourceOfTruthAdapter ||\n !config.sourceOfTruthGolden\n ? null\n : await resolveSourceOfTruthGolden(config.sourceOfTruthGolden);\n\n // Checked against `ownStories` (title-prefix-excluded only), not `stories`\n // — a story marked `exclude: true` still counts as \"present\", it's just\n // not diffed. Only a story the source of truth has that this adapter\n // never built at all, and hasn't acknowledged via `exclude`, is missing.\n const ownStoryIds = new Set(ownStories.map((entry) => entry.id));\n const missingFromSourceOfTruth = sourceOfTruthGolden\n ? Object.keys(sourceOfTruthGolden.manifest)\n .filter(\n (id) =>\n !ownStoryIds.has(id) &&\n !excludeStoryIds.some((prefix) => matchesPrefix(id, prefix)),\n )\n .sort()\n : [];\n\n if (missingFromSourceOfTruth.length > 0) {\n console.error(\n `[adapter-tester] ${missingFromSourceOfTruth.length} stor(y/ies) exist in the source of truth but are missing here: ${missingFromSourceOfTruth.join(\", \")}. Add the missing story, or mark it \\`exclude: true\\` under \\`stories\\` in adapter-tester.config.json if intentional.`,\n );\n }\n\n console.log(\n [\n `[adapter-tester] target: \"${ownTarget.name}\" (${ownTarget.url})`,\n `[adapter-tester] checkMode: \"${checkMode}\" (${checkMode === \"divergence\" ? \"this project's golden vs source-of-truth's golden\" : \"live render vs this project's own golden\"})`,\n `[adapter-tester] goldenMode: \"${goldenMode}\"`,\n `[adapter-tester] goldenThresholdPixels: ${config.goldenThresholdPixels}`,\n `[adapter-tester] sourceOfTruthThresholdPixels: ${config.sourceOfTruthThresholdPixels}`,\n checkMode === \"divergence\"\n ? config.isSourceOfTruthAdapter\n ? `[adapter-tester] sourceOfTruthGolden: skipped — this is the source-of-truth adapter, nothing to diverge from`\n : !config.sourceOfTruthGolden\n ? `[adapter-tester] sourceOfTruthGolden: skipped — no sourceOfTruthGolden configured`\n : sourceOfTruthGolden\n ? `[adapter-tester] sourceOfTruthGolden: resolved, ${Object.keys(sourceOfTruthGolden.manifest).length} golden(s) available (config: ${JSON.stringify(config.sourceOfTruthGolden)})`\n : `[adapter-tester] sourceOfTruthGolden: unavailable — no baseline found or unreachable (config: ${JSON.stringify(config.sourceOfTruthGolden)}); divergence check will skip every story`\n : `[adapter-tester] sourceOfTruthGolden: not used in \"own\" checkMode`,\n `[adapter-tester] stories: ${stories.length} to check (excluded: ${excludeStoryIds.length}, title prefixes excluded: ${excludeTitlePrefixes.join(\", \") || \"none\"})`,\n `[adapter-tester] story parity with source of truth: ${missingFromSourceOfTruth.length === 0 ? \"OK\" : `${missingFromSourceOfTruth.length} missing (see error above)`}`,\n ].join(\"\\n\"),\n );\n\n const suiteLabel =\n checkMode === \"divergence\"\n ? \"Source-of-Truth Divergence Check\"\n : \"Own-Drift Golden Image Check\";\n\n return {\n ownTargetName: ownTarget.name,\n suiteLabel,\n stories,\n missingFromSourceOfTruth,\n checkStory: async (story, browser, testInfo) => {\n const page = await browser.newPage();\n await page.setViewportSize({ width: 800, height: 600 });\n await page.goto(\n `${ownTarget.url}/iframe.html?id=${story.id}&viewMode=story`,\n { waitUntil: \"networkidle\" },\n );\n await page.waitForSelector(\"#storybook-root\");\n await page.waitForTimeout(300);\n const liveBuffer = await page.screenshot();\n\n const imagePath = goldenImagePath(goldenDir, story.id);\n // Only this worker ever touches this story's key, so reading it here\n // (outside the lock `updateManifestEntry` takes at the end) can't\n // race another worker — they're all reading/writing different keys.\n const entry = loadManifest(goldenDir)[story.id];\n const capturingNewGolden =\n goldenMode !== \"check\" || !entry || !existsSync(imagePath);\n\n let currentEntry: GoldenManifestEntry;\n if (capturingNewGolden) {\n saveGoldenImage(goldenDir, story.id, liveBuffer);\n currentEntry = entry?.sourceOfTruthCreatedAt\n ? {\n createdAt: new Date().toISOString(),\n sourceOfTruthCreatedAt: entry.sourceOfTruthCreatedAt,\n }\n : { createdAt: new Date().toISOString() };\n if (!entry) {\n testInfo.annotations.push({\n type: \"golden-created\",\n description: `No golden existed yet for \"${story.id}\" — captured one from this run.`,\n });\n }\n } else {\n currentEntry = entry;\n if (checkMode === \"own\") {\n const goldenBuffer = readFileSync(imagePath);\n const { diffPixels, diffImage } = diffPngBuffers(\n liveBuffer,\n goldenBuffer,\n );\n const threshold = resolveThreshold(\n story.id,\n goldenStoryThresholds,\n config.goldenThresholdPixels,\n );\n if (diffPixels >= threshold) {\n await testInfo.attach(\"expected\", {\n body: goldenBuffer,\n contentType: \"image/png\",\n });\n await testInfo.attach(\"actual\", {\n body: liveBuffer,\n contentType: \"image/png\",\n });\n if (diffImage) {\n await testInfo.attach(\"diff\", {\n body: diffImage,\n contentType: \"image/png\",\n });\n }\n }\n expect\n .soft(\n diffPixels,\n `\"${story.id}\" has drifted from its own golden image (${diffPixels} mismatched pixels, threshold ${threshold})`,\n )\n .toBeLessThan(threshold);\n }\n }\n\n if (sourceOfTruthGolden) {\n const sourceOfTruthEntry = sourceOfTruthGolden.manifest[story.id];\n const sourceOfTruthImage = sourceOfTruthEntry\n ? await sourceOfTruthGolden.readImage(story.id)\n : null;\n\n if (sourceOfTruthEntry && sourceOfTruthImage) {\n if (goldenMode === \"approve-divergence\") {\n currentEntry = {\n ...currentEntry,\n sourceOfTruthCreatedAt: sourceOfTruthEntry.createdAt,\n };\n } else {\n const ownImage = readFileSync(imagePath);\n const { diffPixels, diffImage } = diffPngBuffers(\n ownImage,\n sourceOfTruthImage,\n );\n const threshold = resolveThreshold(\n story.id,\n sourceOfTruthStoryThresholds,\n config.sourceOfTruthThresholdPixels,\n );\n const approvedAt = currentEntry.sourceOfTruthCreatedAt;\n const isKnownDivergence =\n diffPixels < threshold ||\n (approvedAt !== undefined &&\n approvedAt >= sourceOfTruthEntry.createdAt);\n if (!isKnownDivergence) {\n await testInfo.attach(\"expected\", {\n body: sourceOfTruthImage,\n contentType: \"image/png\",\n });\n await testInfo.attach(\"actual\", {\n body: ownImage,\n contentType: \"image/png\",\n });\n if (diffImage) {\n await testInfo.attach(\"diff\", {\n body: diffImage,\n contentType: \"image/png\",\n });\n }\n testInfo.annotations.push({\n type: \"source-of-truth-divergence\",\n description: `\"${story.id}\" differs from the source of truth's golden by ${diffPixels} mismatched pixels (threshold ${threshold}) and hasn't been reviewed — run with --approve-divergence if this is intentional, or fix the adapter styling.`,\n });\n }\n expect\n .soft(\n isKnownDivergence,\n `\"${story.id}\" differs from the source of truth's golden by ${diffPixels} mismatched pixels (threshold ${threshold}) and hasn't been reviewed — run with --approve-divergence if this is intentional, or fix the adapter styling.`,\n )\n .toBe(true);\n }\n }\n }\n\n // Locked read-modify-write of just this story's entry — see\n // `updateManifestEntry` for why that's enough to make this safe\n // across concurrent Playwright workers.\n await updateManifestEntry(goldenDir, story.id, () => currentEntry);\n },\n };\n}\n"],"names":["diffPngBuffers","a","b","imgA","PNG","imgB","diff","pixelmatch","addFormats","ajvFormatsModule.default","ajv","Ajv","validate","schema","validateGoldenManifest","data","path","errors","error","extra","_a","manifestPath","goldenDir","join","manifestLockPath","goldenImagePath","storyId","loadManifest","existsSync","readFileSync","saveManifest","manifest","sorted","key","mkdirSync","tmpPath","writeFileSync","renameSync","LOCK_RETRY_MS","LOCK_TIMEOUT_MS","sleep","ms","resolve","acquireManifestLock","deadline","closeSync","openSync","releaseManifestLock","rmSync","updateManifestEntry","updater","nextEntry","saveGoldenImage","buffer","pruneOrphanedGoldens","currentStoryIds","orphanIds","id","imagePath","unlinkSync","GITHUB_REPO","resolveNpmVersion","packageName","versionSpec","response","resolved","_b","packageDirectory","resolveSourceOfTruthGolden","location","version","cacheDir","tag","rawBase","cachedManifestPath","text","parsed","cachedImagePath","DEFAULT_EXCLUDE_TITLE_PREFIXES","matchesPrefix","prefix","fetchStories","target","excludeTitlePrefixes","stories","entries","entry","resolveThreshold","storyThresholds","defaultThresholdPixels","bestMatch","resolveVisualRegressionPlan","config","ownTarget","storyOverrides","excludeStoryIds","goldenStoryThresholds","override","sourceOfTruthStoryThresholds","goldenMode","checkMode","ownStories","prunedStoryIds","story","sourceOfTruthGolden","ownStoryIds","missingFromSourceOfTruth","suiteLabel","browser","testInfo","page","liveBuffer","capturingNewGolden","currentEntry","goldenBuffer","diffPixels","diffImage","threshold","expect","sourceOfTruthEntry","sourceOfTruthImage","ownImage","approvedAt","isKnownDivergence"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"testing.cjs","sources":["../src/golden/diffPng.ts","../src/golden/validateManifest.ts","../src/golden/manifestStore.ts","../src/golden/resolveSourceOfTruthGolden.ts","../src/testing/runVisualRegression.ts"],"sourcesContent":["import pixelmatch from \"pixelmatch\";\nimport { PNG } from \"pngjs\";\n\nexport interface PngDiffResult {\n /** Mismatched-pixel count, or `Infinity` if the two images aren't even the\n * same dimensions — pixelmatch itself throws on a size mismatch, and a size\n * mismatch is itself a real difference, not something to swallow. */\n diffPixels: number;\n /** Visual highlight of the mismatched pixels, encoded as a PNG buffer.\n * `null` when `diffPixels` is `Infinity` — there's no pixel-aligned diff to\n * render across two different-sized images. */\n diffImage: Buffer | null;\n}\n\n/** Pixel-diffs two PNG buffers. */\nexport function diffPngBuffers(a: Buffer, b: Buffer): PngDiffResult {\n const imgA = PNG.sync.read(a);\n const imgB = PNG.sync.read(b);\n if (imgA.width !== imgB.width || imgA.height !== imgB.height) {\n return { diffPixels: Infinity, diffImage: null };\n }\n const diff = new PNG({ width: imgA.width, height: imgA.height });\n const diffPixels = pixelmatch(\n imgA.data,\n imgB.data,\n diff.data,\n imgA.width,\n imgA.height,\n { threshold: 0.1 },\n );\n return { diffPixels, diffImage: PNG.sync.write(diff) };\n}\n","import { Ajv } from \"ajv\";\nimport * as ajvFormatsModule from \"ajv-formats\";\nimport type { FormatsPlugin } from \"ajv-formats\";\nimport schema from \"./manifest.schema.json\" with { type: \"json\" };\n\n// See validateFileConfig.ts for why `.default` has to be unwrapped by hand.\nconst addFormats = (ajvFormatsModule as unknown as { default: FormatsPlugin })\n .default;\n\nconst ajv = new Ajv({ allErrors: true, strict: true });\naddFormats(ajv);\nconst validate = ajv.compile(schema);\n\n/**\n * Validates a parsed `test/golden/manifest.json` against `manifest.schema.json`.\n * Throws with every violation listed — callers must not silently coerce or\n * drop invalid entries.\n */\nexport function validateGoldenManifest(data: unknown, path: string): void {\n if (validate(data)) return;\n\n const errors = (validate.errors ?? [])\n .map((error) => {\n const extra = error.params?.additionalProperty\n ? ` '${error.params.additionalProperty}'`\n : \"\";\n return ` - ${error.instancePath || \"root\"} ${error.message}${extra}`;\n })\n .join(\"\\n\");\n throw new Error(`Invalid ${path}:\\n${errors}`);\n}\n","import {\n closeSync,\n existsSync,\n mkdirSync,\n openSync,\n readFileSync,\n renameSync,\n rmSync,\n unlinkSync,\n writeFileSync,\n} from \"node:fs\";\nimport { join } from \"node:path\";\nimport { validateGoldenManifest } from \"./validateManifest.js\";\n\nexport interface GoldenManifestEntry {\n createdAt: string;\n sourceOfTruthCreatedAt?: string;\n}\n\nexport type GoldenManifest = Record<string, GoldenManifestEntry>;\n\nexport function manifestPath(goldenDir: string): string {\n return join(goldenDir, \"manifest.json\");\n}\n\nfunction manifestLockPath(goldenDir: string): string {\n return join(goldenDir, \"manifest.json.lock\");\n}\n\nexport function goldenImagePath(goldenDir: string, storyId: string): string {\n return join(goldenDir, `${storyId}.png`);\n}\n\n/** Returns `{}` if no manifest exists yet — a fresh adapter with no goldens\n * captured is the normal starting state, not an error. */\nexport function loadManifest(goldenDir: string): GoldenManifest {\n const path = manifestPath(goldenDir);\n if (!existsSync(path)) return {};\n const data = JSON.parse(readFileSync(path, \"utf8\"));\n validateGoldenManifest(data, path);\n return data;\n}\n\n/** Validates before writing, and sorts keys so the diff on a reviewed PR is\n * stable regardless of the order stories happened to run in. Writes to a\n * temp file and renames over the real one — `rename` is atomic, so a\n * concurrent `loadManifest` (running in another Playwright worker) never\n * observes a half-written file. */\nexport function saveManifest(\n goldenDir: string,\n manifest: GoldenManifest,\n): void {\n const path = manifestPath(goldenDir);\n validateGoldenManifest(manifest, path);\n const sorted: GoldenManifest = {};\n for (const key of Object.keys(manifest).sort()) {\n sorted[key] = manifest[key]!;\n }\n mkdirSync(goldenDir, { recursive: true });\n const tmpPath = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;\n writeFileSync(tmpPath, JSON.stringify(sorted, null, 2) + \"\\n\");\n renameSync(tmpPath, path);\n}\n\nconst LOCK_RETRY_MS = 25;\nconst LOCK_TIMEOUT_MS = 15_000;\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/** Exclusive-create the lock file, spin-retrying until it's free. `wx` fails\n * atomically (`EEXIST`) if another worker process already holds it — that's\n * the only signal we need, no third-party lock library required for a\n * same-machine, same-run lock like this. */\nasync function acquireManifestLock(goldenDir: string): Promise<void> {\n mkdirSync(goldenDir, { recursive: true });\n const path = manifestLockPath(goldenDir);\n const deadline = Date.now() + LOCK_TIMEOUT_MS;\n for (;;) {\n try {\n closeSync(openSync(path, \"wx\"));\n return;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"EEXIST\") throw error;\n if (Date.now() >= deadline) {\n throw new Error(\n `Timed out waiting for the manifest lock at \"${path}\" — delete it if a previous run crashed while holding it.`,\n );\n }\n await sleep(LOCK_RETRY_MS);\n }\n }\n}\n\nfunction releaseManifestLock(goldenDir: string): void {\n rmSync(manifestLockPath(goldenDir), { force: true });\n}\n\n/** Runs `updater` against this story's manifest entry under an exclusive\n * lock on `manifest.json`: reloads the manifest fresh, applies `updater`,\n * and saves it back, all before releasing the lock. Concurrent Playwright\n * workers each own a different story, so this is the only section that\n * needs to serialize — everything else about a story (its screenshot, its\n * golden image file, its diff) is independent of every other story. */\nexport async function updateManifestEntry(\n goldenDir: string,\n storyId: string,\n updater: (\n entry: GoldenManifestEntry | undefined,\n ) => GoldenManifestEntry | undefined,\n): Promise<GoldenManifestEntry | undefined> {\n await acquireManifestLock(goldenDir);\n try {\n const manifest = loadManifest(goldenDir);\n const nextEntry = updater(manifest[storyId]);\n if (nextEntry === undefined) {\n delete manifest[storyId];\n } else {\n manifest[storyId] = nextEntry;\n }\n saveManifest(goldenDir, manifest);\n return nextEntry;\n } finally {\n releaseManifestLock(goldenDir);\n }\n}\n\nexport function saveGoldenImage(\n goldenDir: string,\n storyId: string,\n buffer: Buffer,\n): void {\n mkdirSync(goldenDir, { recursive: true });\n writeFileSync(goldenImagePath(goldenDir, storyId), buffer);\n}\n\n/** Removes the golden `.png` + manifest entry for every story id in the\n * manifest that isn't in `currentStoryIds` (e.g. a story renamed or deleted\n * from Storybook) — otherwise those never get cleaned up on their own,\n * since a run only ever adds/updates entries for stories it actually saw.\n * Returns the pruned ids, for the caller to report. Both the file removal\n * and the manifest delete are idempotent, so it's safe for this to run\n * redundantly from more than one Playwright worker. */\nexport async function pruneOrphanedGoldens(\n goldenDir: string,\n currentStoryIds: ReadonlySet<string>,\n): Promise<string[]> {\n const manifest = loadManifest(goldenDir);\n const orphanIds = Object.keys(manifest).filter(\n (id) => !currentStoryIds.has(id),\n );\n for (const id of orphanIds) {\n const imagePath = goldenImagePath(goldenDir, id);\n if (existsSync(imagePath)) unlinkSync(imagePath);\n await updateManifestEntry(goldenDir, id, () => undefined);\n }\n return orphanIds;\n}\n","import { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { SourceOfTruthGoldenLocation } from \"../config.js\";\nimport {\n goldenImagePath,\n loadManifest,\n manifestPath,\n type GoldenManifest,\n} from \"./manifestStore.js\";\nimport { validateGoldenManifest } from \"./validateManifest.js\";\n\nconst GITHUB_REPO = \"borderux/recursica\";\n\nexport interface SourceOfTruthGolden {\n manifest: GoldenManifest;\n /** Returns the golden PNG bytes for a story, or `null` if the source of\n * truth has no golden captured for it yet. */\n readImage(storyId: string): Promise<Buffer | null>;\n}\n\nasync function resolveNpmVersion(\n packageName: string,\n versionSpec: string,\n): Promise<string> {\n const response = await fetch(`https://registry.npmjs.org/${packageName}`);\n if (!response.ok) {\n throw new Error(\n `Could not reach npm registry for ${packageName}: ${response.statusText}`,\n );\n }\n const data = (await response.json()) as {\n \"dist-tags\"?: Record<string, string>;\n versions?: Record<string, unknown>;\n };\n const resolved =\n data[\"dist-tags\"]?.[versionSpec] ??\n (data.versions?.[versionSpec] ? versionSpec : undefined);\n if (!resolved) {\n throw new Error(\n `${packageName} has no version or dist-tag \"${versionSpec}\" on the npm registry.`,\n );\n }\n return resolved;\n}\n\n// This monorepo's packages all live at `packages/<unscoped-name>` — mirrors\n// the same convention `mantineSourceOfTruthHarness` and every existing\n// `packages/*/package.json`'s `repository.directory` field already assume.\nfunction packageDirectory(packageName: string): string {\n return `packages/${packageName.split(\"/\").pop()}`;\n}\n\n/**\n * Resolves the source-of-truth adapter's golden images for the divergence\n * check. Never boots a Storybook — both location types resolve to plain\n * files, fetched once and cached, not re-diffed per pixel over the wire.\n *\n * `location.type === \"local\"`: a sibling package already checked out (this\n * monorepo's own `sourceOfTruth.type: \"url\"` mode) — read its\n * `test/golden/` directly, including any uncommitted local changes.\n *\n * `location.type === \"npm\"`: no local checkout (the default, standalone-repo\n * mode) — resolve the installed version against the npm registry, then fetch\n * that exact version's `test/golden/` from the public GitHub repo at the\n * matching release tag (changesets tags every release as\n * `<packageName>@<version>`), caching what's downloaded under `cacheDir`.\n *\n * Returns `null` — degrading the divergence check to a skip, not a failure —\n * when no golden baseline exists yet for this version, or the registry/repo\n * is unreachable.\n */\nexport async function resolveSourceOfTruthGolden(\n location: SourceOfTruthGoldenLocation,\n): Promise<SourceOfTruthGolden | null> {\n if (location.type === \"local\") {\n if (!existsSync(manifestPath(location.dir))) {\n console.warn(\n `No golden baseline found yet at ${location.dir} — source-of-truth divergence check skipped for this run.`,\n );\n return null;\n }\n const manifest = loadManifest(location.dir);\n return {\n manifest,\n async readImage(storyId) {\n const path = goldenImagePath(location.dir, storyId);\n return existsSync(path) ? readFileSync(path) : null;\n },\n };\n }\n\n let version: string;\n try {\n version = await resolveNpmVersion(\n location.packageName,\n location.versionSpec,\n );\n } catch (error) {\n console.warn(\n `Could not resolve ${location.packageName}@${location.versionSpec} — source-of-truth divergence check skipped for this run.`,\n error,\n );\n return null;\n }\n\n const cacheDir = join(location.cacheDir, version);\n const tag = `${location.packageName}@${version}`;\n const rawBase = `https://raw.githubusercontent.com/${GITHUB_REPO}/${tag}/${packageDirectory(location.packageName)}/test/golden`;\n\n let manifest: GoldenManifest;\n const cachedManifestPath = manifestPath(cacheDir);\n if (existsSync(cachedManifestPath)) {\n manifest = loadManifest(cacheDir);\n } else {\n let response: Response;\n try {\n response = await fetch(`${rawBase}/manifest.json`);\n } catch (error) {\n console.warn(\n `Could not reach GitHub to fetch ${tag}'s golden baseline — source-of-truth divergence check skipped for this run.`,\n error,\n );\n return null;\n }\n if (!response.ok) {\n console.warn(\n `No golden baseline published for ${tag} — source-of-truth divergence check skipped for this run.`,\n );\n return null;\n }\n const text = await response.text();\n const parsed = JSON.parse(text);\n validateGoldenManifest(parsed, `${rawBase}/manifest.json`);\n mkdirSync(cacheDir, { recursive: true });\n writeFileSync(cachedManifestPath, text);\n manifest = parsed;\n }\n\n return {\n manifest,\n async readImage(storyId) {\n const cachedImagePath = goldenImagePath(cacheDir, storyId);\n if (existsSync(cachedImagePath)) return readFileSync(cachedImagePath);\n const response = await fetch(`${rawBase}/${storyId}.png`);\n if (!response.ok) return null;\n const buffer = Buffer.from(await response.arrayBuffer());\n mkdirSync(cacheDir, { recursive: true });\n writeFileSync(cachedImagePath, buffer);\n return buffer;\n },\n };\n}\n","import { expect } from \"@playwright/test\";\nimport type { Browser, TestInfo } from \"@playwright/test\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport type { AdapterTesterConfig } from \"../config.js\";\nimport { diffPngBuffers } from \"../golden/diffPng.js\";\nimport {\n type GoldenManifestEntry,\n goldenImagePath,\n loadManifest,\n pruneOrphanedGoldens,\n saveGoldenImage,\n updateManifestEntry,\n} from \"../golden/manifestStore.js\";\nimport { resolveSourceOfTruthGolden } from \"../golden/resolveSourceOfTruthGolden.js\";\n\nconst DEFAULT_EXCLUDE_TITLE_PREFIXES = [\"Theme\", \"Tokens\", \"Introduction\"];\n\ninterface StorybookEntry {\n type: string;\n id: string;\n name: string;\n title: string;\n}\n\nfunction matchesPrefix(id: string, prefix: string): boolean {\n return id === prefix || id.startsWith(prefix);\n}\n\n/** Fetches every story `target`'s Storybook currently has, filtered only by\n * `excludeTitlePrefixes` — not `stories.<id>.exclude`, so callers can still\n * tell an excluded story apart from one that's genuinely missing. */\nasync function fetchStories(\n target: { name: string; url: string },\n excludeTitlePrefixes: string[],\n): Promise<StorybookEntry[]> {\n let stories: StorybookEntry[];\n try {\n const response = await fetch(`${target.url}/index.json`);\n if (!response.ok) {\n throw new Error(\n `Failed to fetch Storybook index: ${response.statusText}`,\n );\n }\n const data = (await response.json()) as any;\n const entries = data.entries || {};\n stories = Object.values(entries).filter(\n (entry: any) =>\n entry.type === \"story\" &&\n !excludeTitlePrefixes.some(\n (prefix) =>\n entry.title === prefix || entry.title.startsWith(`${prefix}/`),\n ),\n ) as StorybookEntry[];\n stories.sort((a, b) => a.id.localeCompare(b.id));\n } catch (error) {\n console.error(\n \"Failed to load Storybook index from\",\n `${target.url}/index.json`,\n error,\n );\n throw new Error(\n `Storybook target \"${target.name}\" is not responsive or index.json is missing. Please ensure its Storybook is running.`,\n );\n }\n return stories;\n}\n\n/** Resolves the diff threshold for `storyId`: the longest (most specific)\n * `storyThresholds` key matching by prefix, falling back to\n * `defaultThresholdPixels` when nothing matches. */\nfunction resolveThreshold(\n storyId: string,\n storyThresholds: Record<string, number>,\n defaultThresholdPixels: number,\n): number {\n let bestMatch: string | undefined;\n for (const prefix of Object.keys(storyThresholds)) {\n if (\n matchesPrefix(storyId, prefix) &&\n (!bestMatch || prefix.length > bestMatch.length)\n ) {\n bestMatch = prefix;\n }\n }\n return bestMatch !== undefined\n ? storyThresholds[bestMatch]!\n : defaultThresholdPixels;\n}\n\n/** Everything a generated Playwright spec needs to register the golden-image\n * suite itself. Split out from the actual `test.describe`/`test` calls so\n * those calls execute in the spec file that imports this, not in this\n * library file — otherwise Playwright's HTML report groups every story under\n * this file's own (sourcemapped) path instead of a stable spec name. */\nexport interface VisualRegressionPlan {\n /** `config`'s own (non-source-of-truth) target name, for the suite title. */\n ownTargetName: string;\n /** Suite title suffix describing which check mode is running. */\n suiteLabel: string;\n /** Stories to check, already filtered and sorted by id. */\n stories: StorybookEntry[];\n /** Story ids the source-of-truth adapter has a golden for but this\n * project's own Storybook doesn't — empty outside `checkMode: \"divergence\"`\n * or when `sourceOfTruthGolden` didn't resolve. Excludes ids covered by a\n * `stories.<id>.exclude` entry: an intentional gap, not a sync failure. */\n missingFromSourceOfTruth: string[];\n /** Golden-checks one story. Call this from inside a `test(story.id, ...)`\n * body — safe to run concurrently across Playwright workers, since each\n * call only ever reads/writes its own story's manifest entry (locked at\n * the point it writes it back, so concurrent workers never race each\n * other's entries — see `updateManifestEntry`). */\n checkStory: (\n story: StorybookEntry,\n browser: Browser,\n testInfo: TestInfo,\n ) => Promise<void>;\n}\n\n/** Everything a generated Playwright spec needs to register the golden-image\n * suite itself. Split out from the actual `test.describe`/`test` calls so\n * those calls execute in the spec file that imports this, not in this\n * library file — otherwise Playwright's HTML report groups every story under\n * this file's own (sourcemapped) path instead of a stable spec name. */\nexport interface VisualRegressionPlan {\n /** `config`'s own (non-source-of-truth) target name, for the suite title. */\n ownTargetName: string;\n /** Suite title suffix describing which check mode is running. */\n suiteLabel: string;\n /** Stories to check, already filtered and sorted by id. */\n stories: StorybookEntry[];\n /** Golden-checks one story. Call this from inside a `test(story.id, ...)`\n * body — safe to run concurrently across Playwright workers, since each\n * call only ever reads/writes its own story's manifest entry (locked at\n * the point it writes it back, so concurrent workers never race each\n * other's entries — see `updateManifestEntry`). */\n checkStory: (\n story: StorybookEntry,\n browser: Browser,\n testInfo: TestInfo,\n ) => Promise<void>;\n}\n\n/**\n * Resolves the golden-image plan for `config`'s own target (the one target\n * in `config.targets` not marked `sourceOfTruth`).\n *\n * Two independent checks per story, gated by `config.checkMode`, neither of\n * which boots the source-of-truth adapter's own Storybook — the divergence\n * check below compares stored golden files, not live pages:\n *\n * 1. **Own-drift (`checkMode: \"own\"`, the default; hard fail):** this run's\n * live render vs this project's own stored `test/golden/<story-id>.png`.\n * No golden yet for a story is not a failure — one is captured from this\n * run instead (same as `--update-golden`, scoped to just that story), in\n * either mode.\n * 2. **Source-of-truth divergence (`checkMode: \"divergence\"`; hard fail):**\n * this project's own golden vs the source-of-truth's golden (`config`'s\n * `sourceOfTruthGolden`). Skipped entirely when\n * `config.isSourceOfTruthAdapter` is true — the\n * source-of-truth adapter has nothing above it to diverge from — and\n * skipped per-story when neither side has a baseline yet. A\n * once-flagged divergence stays quiet after `--approve-divergence`,\n * until the source of truth's own golden changes again.\n */\nexport async function resolveVisualRegressionPlan(\n config: AdapterTesterConfig,\n): Promise<VisualRegressionPlan> {\n const ownTarget = config.isSourceOfTruthAdapter\n ? config.targets[0]\n : config.targets.find((target) => !target.sourceOfTruth);\n if (!ownTarget) {\n throw new Error(\n \"adapter-tester config has no non-sourceOfTruth target to run the golden check against.\",\n );\n }\n const excludeTitlePrefixes =\n config.excludeTitlePrefixes ?? DEFAULT_EXCLUDE_TITLE_PREFIXES;\n const storyOverrides = config.stories ?? {};\n const excludeStoryIds = Object.keys(storyOverrides).filter(\n (id) => storyOverrides[id]!.exclude,\n );\n const goldenStoryThresholds = Object.fromEntries(\n Object.entries(storyOverrides)\n .filter(([, override]) => override.goldenThreshold !== undefined)\n .map(([id, override]) => [id, override.goldenThreshold!]),\n );\n const sourceOfTruthStoryThresholds = Object.fromEntries(\n Object.entries(storyOverrides)\n .filter(([, override]) => override.sourceOfTruthThreshold !== undefined)\n .map(([id, override]) => [id, override.sourceOfTruthThreshold!]),\n );\n const goldenDir = config.goldenDir;\n const goldenMode = config.goldenMode;\n const checkMode = config.checkMode;\n\n // Fetched with only excludeTitlePrefixes applied — not excludeStoryIds —\n // so the source-of-truth story-parity check below can tell an excluded\n // story apart from one that's genuinely missing from this Storybook.\n const ownStories = await fetchStories(ownTarget, excludeTitlePrefixes);\n const stories = ownStories.filter(\n (entry) =>\n !excludeStoryIds.some((prefix) => matchesPrefix(entry.id, prefix)),\n );\n\n // `--update-golden` redefines this project's own baseline, so it's also\n // the point a renamed/removed story's now-orphaned golden gets cleaned up\n // — otherwise nothing ever prunes it, since a run only ever adds/updates\n // entries for stories it actually saw in this pass. Uses the full current\n // story list (not narrowed by any `--grep` Playwright itself applies), so\n // this catches every orphan regardless of how the run is scoped.\n if (goldenMode === \"update-golden\") {\n const prunedStoryIds = await pruneOrphanedGoldens(\n goldenDir,\n new Set(stories.map((story) => story.id)),\n );\n if (prunedStoryIds.length > 0) {\n console.warn(\n `Pruned ${prunedStoryIds.length} orphaned golden(s) no longer in Storybook: ${prunedStoryIds.join(\", \")}`,\n );\n }\n }\n\n const sourceOfTruthGolden =\n checkMode !== \"divergence\" ||\n config.isSourceOfTruthAdapter ||\n !config.sourceOfTruthGolden\n ? null\n : await resolveSourceOfTruthGolden(config.sourceOfTruthGolden);\n\n // Checked against `ownStories` (title-prefix-excluded only), not `stories`\n // — a story marked `exclude: true` still counts as \"present\", it's just\n // not diffed. Only a story the source of truth has that this adapter\n // never built at all, and hasn't acknowledged via `exclude`, is missing.\n const ownStoryIds = new Set(ownStories.map((entry) => entry.id));\n const missingFromSourceOfTruth = sourceOfTruthGolden\n ? Object.keys(sourceOfTruthGolden.manifest)\n .filter(\n (id) =>\n !ownStoryIds.has(id) &&\n !excludeStoryIds.some((prefix) => matchesPrefix(id, prefix)),\n )\n .sort()\n : [];\n\n if (missingFromSourceOfTruth.length > 0) {\n console.error(\n `[adapter-tester] ${missingFromSourceOfTruth.length} stor(y/ies) exist in the source of truth but are missing here: ${missingFromSourceOfTruth.join(\", \")}. Add the missing story, or mark it \\`exclude: true\\` under \\`stories\\` in adapter-tester.config.json if intentional.`,\n );\n }\n\n console.log(\n [\n `[adapter-tester] target: \"${ownTarget.name}\" (${ownTarget.url})`,\n `[adapter-tester] checkMode: \"${checkMode}\" (${checkMode === \"divergence\" ? \"this project's golden vs source-of-truth's golden\" : \"live render vs this project's own golden\"})`,\n `[adapter-tester] goldenMode: \"${goldenMode}\"`,\n `[adapter-tester] goldenThresholdPixels: ${config.goldenThresholdPixels}`,\n `[adapter-tester] sourceOfTruthThresholdPixels: ${config.sourceOfTruthThresholdPixels}`,\n checkMode === \"divergence\"\n ? config.isSourceOfTruthAdapter\n ? `[adapter-tester] sourceOfTruthGolden: skipped — this is the source-of-truth adapter, nothing to diverge from`\n : !config.sourceOfTruthGolden\n ? `[adapter-tester] sourceOfTruthGolden: skipped — no sourceOfTruthGolden configured`\n : sourceOfTruthGolden\n ? `[adapter-tester] sourceOfTruthGolden: resolved, ${Object.keys(sourceOfTruthGolden.manifest).length} golden(s) available (config: ${JSON.stringify(config.sourceOfTruthGolden)})`\n : `[adapter-tester] sourceOfTruthGolden: unavailable — no baseline found or unreachable (config: ${JSON.stringify(config.sourceOfTruthGolden)}); divergence check will skip every story`\n : `[adapter-tester] sourceOfTruthGolden: not used in \"own\" checkMode`,\n `[adapter-tester] stories: ${stories.length} to check (excluded: ${excludeStoryIds.length}, title prefixes excluded: ${excludeTitlePrefixes.join(\", \") || \"none\"})`,\n `[adapter-tester] story parity with source of truth: ${missingFromSourceOfTruth.length === 0 ? \"OK\" : `${missingFromSourceOfTruth.length} missing (see error above)`}`,\n ].join(\"\\n\"),\n );\n\n const suiteLabel =\n checkMode === \"divergence\"\n ? \"Source-of-Truth Divergence Check\"\n : \"Own-Drift Golden Image Check\";\n\n return {\n ownTargetName: ownTarget.name,\n suiteLabel,\n stories,\n missingFromSourceOfTruth,\n checkStory: async (story, browser, testInfo) => {\n const page = await browser.newPage();\n await page.setViewportSize({ width: 800, height: 600 });\n await page.goto(\n `${ownTarget.url}/iframe.html?id=${story.id}&viewMode=story`,\n { waitUntil: \"networkidle\" },\n );\n await page.waitForSelector(\"#storybook-root\");\n await page.addStyleTag({\n content: `* { -webkit-font-smoothing: antialiased !important; -moz-osx-font-smoothing: grayscale !important; }`,\n });\n await page.waitForTimeout(300);\n const liveBuffer = await page.screenshot();\n\n const imagePath = goldenImagePath(goldenDir, story.id);\n // Only this worker ever touches this story's key, so reading it here\n // (outside the lock `updateManifestEntry` takes at the end) can't\n // race another worker — they're all reading/writing different keys.\n const entry = loadManifest(goldenDir)[story.id];\n const capturingNewGolden =\n goldenMode !== \"check\" || !entry || !existsSync(imagePath);\n\n let currentEntry: GoldenManifestEntry;\n if (capturingNewGolden) {\n saveGoldenImage(goldenDir, story.id, liveBuffer);\n currentEntry = entry?.sourceOfTruthCreatedAt\n ? {\n createdAt: new Date().toISOString(),\n sourceOfTruthCreatedAt: entry.sourceOfTruthCreatedAt,\n }\n : { createdAt: new Date().toISOString() };\n if (!entry) {\n testInfo.annotations.push({\n type: \"golden-created\",\n description: `No golden existed yet for \"${story.id}\" — captured one from this run.`,\n });\n }\n } else {\n currentEntry = entry;\n if (checkMode === \"own\") {\n const goldenBuffer = readFileSync(imagePath);\n const { diffPixels, diffImage } = diffPngBuffers(\n liveBuffer,\n goldenBuffer,\n );\n const threshold = resolveThreshold(\n story.id,\n goldenStoryThresholds,\n config.goldenThresholdPixels,\n );\n if (diffPixels >= threshold) {\n await testInfo.attach(\"expected\", {\n body: goldenBuffer,\n contentType: \"image/png\",\n });\n await testInfo.attach(\"actual\", {\n body: liveBuffer,\n contentType: \"image/png\",\n });\n if (diffImage) {\n await testInfo.attach(\"diff\", {\n body: diffImage,\n contentType: \"image/png\",\n });\n }\n }\n expect\n .soft(\n diffPixels,\n `\"${story.id}\" has drifted from its own golden image (${diffPixels} mismatched pixels, threshold ${threshold})`,\n )\n .toBeLessThan(threshold);\n }\n }\n\n if (sourceOfTruthGolden) {\n const sourceOfTruthEntry = sourceOfTruthGolden.manifest[story.id];\n const sourceOfTruthImage = sourceOfTruthEntry\n ? await sourceOfTruthGolden.readImage(story.id)\n : null;\n\n if (sourceOfTruthEntry && sourceOfTruthImage) {\n if (goldenMode === \"approve-divergence\") {\n currentEntry = {\n ...currentEntry,\n sourceOfTruthCreatedAt: sourceOfTruthEntry.createdAt,\n };\n } else {\n const ownImage = readFileSync(imagePath);\n const { diffPixels, diffImage } = diffPngBuffers(\n ownImage,\n sourceOfTruthImage,\n );\n const threshold = resolveThreshold(\n story.id,\n sourceOfTruthStoryThresholds,\n config.sourceOfTruthThresholdPixels,\n );\n const approvedAt = currentEntry.sourceOfTruthCreatedAt;\n const isKnownDivergence =\n diffPixels < threshold ||\n (approvedAt !== undefined &&\n approvedAt >= sourceOfTruthEntry.createdAt);\n if (!isKnownDivergence) {\n await testInfo.attach(\"expected\", {\n body: sourceOfTruthImage,\n contentType: \"image/png\",\n });\n await testInfo.attach(\"actual\", {\n body: ownImage,\n contentType: \"image/png\",\n });\n if (diffImage) {\n await testInfo.attach(\"diff\", {\n body: diffImage,\n contentType: \"image/png\",\n });\n }\n testInfo.annotations.push({\n type: \"source-of-truth-divergence\",\n description: `\"${story.id}\" differs from the source of truth's golden by ${diffPixels} mismatched pixels (threshold ${threshold}) and hasn't been reviewed — run with --approve-divergence if this is intentional, or fix the adapter styling.`,\n });\n }\n expect\n .soft(\n isKnownDivergence,\n `\"${story.id}\" differs from the source of truth's golden by ${diffPixels} mismatched pixels (threshold ${threshold}) and hasn't been reviewed — run with --approve-divergence if this is intentional, or fix the adapter styling.`,\n )\n .toBe(true);\n }\n }\n }\n\n // Locked read-modify-write of just this story's entry — see\n // `updateManifestEntry` for why that's enough to make this safe\n // across concurrent Playwright workers.\n await updateManifestEntry(goldenDir, story.id, () => currentEntry);\n },\n };\n}\n"],"names":["diffPngBuffers","a","b","imgA","PNG","imgB","diff","pixelmatch","addFormats","ajvFormatsModule.default","ajv","Ajv","validate","schema","validateGoldenManifest","data","path","errors","error","extra","_a","manifestPath","goldenDir","join","manifestLockPath","goldenImagePath","storyId","loadManifest","existsSync","readFileSync","saveManifest","manifest","sorted","key","mkdirSync","tmpPath","writeFileSync","renameSync","LOCK_RETRY_MS","LOCK_TIMEOUT_MS","sleep","ms","resolve","acquireManifestLock","deadline","closeSync","openSync","releaseManifestLock","rmSync","updateManifestEntry","updater","nextEntry","saveGoldenImage","buffer","pruneOrphanedGoldens","currentStoryIds","orphanIds","id","imagePath","unlinkSync","GITHUB_REPO","resolveNpmVersion","packageName","versionSpec","response","resolved","_b","packageDirectory","resolveSourceOfTruthGolden","location","version","cacheDir","tag","rawBase","cachedManifestPath","text","parsed","cachedImagePath","DEFAULT_EXCLUDE_TITLE_PREFIXES","matchesPrefix","prefix","fetchStories","target","excludeTitlePrefixes","stories","entries","entry","resolveThreshold","storyThresholds","defaultThresholdPixels","bestMatch","resolveVisualRegressionPlan","config","ownTarget","storyOverrides","excludeStoryIds","goldenStoryThresholds","override","sourceOfTruthStoryThresholds","goldenMode","checkMode","ownStories","prunedStoryIds","story","sourceOfTruthGolden","ownStoryIds","missingFromSourceOfTruth","suiteLabel","browser","testInfo","page","liveBuffer","capturingNewGolden","currentEntry","goldenBuffer","diffPixels","diffImage","threshold","expect","sourceOfTruthEntry","sourceOfTruthImage","ownImage","approvedAt","isKnownDivergence"],"mappings":"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"}
|
package/dist/testing.js
CHANGED
|
@@ -263,29 +263,31 @@ async function Ie(e) {
|
|
|
263
263
|
].join(`
|
|
264
264
|
`)
|
|
265
265
|
);
|
|
266
|
-
const
|
|
266
|
+
const z = c === "divergence" ? "Source-of-Truth Divergence Check" : "Own-Drift Golden Image Check";
|
|
267
267
|
return {
|
|
268
268
|
ownTargetName: t.name,
|
|
269
|
-
suiteLabel:
|
|
269
|
+
suiteLabel: z,
|
|
270
270
|
stories: M,
|
|
271
271
|
missingFromSourceOfTruth: T,
|
|
272
272
|
checkStory: async (n, u, l) => {
|
|
273
|
-
const
|
|
274
|
-
await
|
|
273
|
+
const $ = await u.newPage();
|
|
274
|
+
await $.setViewportSize({ width: 800, height: 600 }), await $.goto(
|
|
275
275
|
`${t.url}/iframe.html?id=${n.id}&viewMode=story`,
|
|
276
276
|
{ waitUntil: "networkidle" }
|
|
277
|
-
), await
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
277
|
+
), await $.waitForSelector("#storybook-root"), await $.addStyleTag({
|
|
278
|
+
content: "* { -webkit-font-smoothing: antialiased !important; -moz-osx-font-smoothing: grayscale !important; }"
|
|
279
|
+
}), await $.waitForTimeout(300);
|
|
280
|
+
const I = await $.screenshot(), C = S(a, n.id), g = j(a)[n.id], H = h !== "check" || !g || !v(C);
|
|
281
|
+
let O;
|
|
282
|
+
if (H)
|
|
283
|
+
ve(a, n.id, I), O = g != null && g.sourceOfTruthCreatedAt ? {
|
|
282
284
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
283
285
|
sourceOfTruthCreatedAt: g.sourceOfTruthCreatedAt
|
|
284
286
|
} : { createdAt: (/* @__PURE__ */ new Date()).toISOString() }, g || l.annotations.push({
|
|
285
287
|
type: "golden-created",
|
|
286
288
|
description: `No golden existed yet for "${n.id}" — captured one from this run.`
|
|
287
289
|
});
|
|
288
|
-
else if (
|
|
290
|
+
else if (O = g, c === "own") {
|
|
289
291
|
const m = x(C), { diffPixels: w, diffImage: k } = R(
|
|
290
292
|
I,
|
|
291
293
|
m
|
|
@@ -312,8 +314,8 @@ async function Ie(e) {
|
|
|
312
314
|
const m = p.manifest[n.id], w = m ? await p.readImage(n.id) : null;
|
|
313
315
|
if (m && w)
|
|
314
316
|
if (h === "approve-divergence")
|
|
315
|
-
|
|
316
|
-
|
|
317
|
+
O = {
|
|
318
|
+
...O,
|
|
317
319
|
sourceOfTruthCreatedAt: m.createdAt
|
|
318
320
|
};
|
|
319
321
|
else {
|
|
@@ -324,7 +326,7 @@ async function Ie(e) {
|
|
|
324
326
|
n.id,
|
|
325
327
|
d,
|
|
326
328
|
e.sourceOfTruthThresholdPixels
|
|
327
|
-
), _ =
|
|
329
|
+
), _ = O.sourceOfTruthCreatedAt, D = y < N || _ !== void 0 && _ >= m.createdAt;
|
|
328
330
|
D || (await l.attach("expected", {
|
|
329
331
|
body: w,
|
|
330
332
|
contentType: "image/png"
|
|
@@ -343,7 +345,7 @@ async function Ie(e) {
|
|
|
343
345
|
).toBe(!0);
|
|
344
346
|
}
|
|
345
347
|
}
|
|
346
|
-
await X(a, n.id, () =>
|
|
348
|
+
await X(a, n.id, () => O);
|
|
347
349
|
}
|
|
348
350
|
};
|
|
349
351
|
}
|
package/dist/testing.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.js","sources":["../src/golden/diffPng.ts","../src/golden/validateManifest.ts","../src/golden/manifestStore.ts","../src/golden/resolveSourceOfTruthGolden.ts","../src/testing/runVisualRegression.ts"],"sourcesContent":["import pixelmatch from \"pixelmatch\";\nimport { PNG } from \"pngjs\";\n\nexport interface PngDiffResult {\n /** Mismatched-pixel count, or `Infinity` if the two images aren't even the\n * same dimensions — pixelmatch itself throws on a size mismatch, and a size\n * mismatch is itself a real difference, not something to swallow. */\n diffPixels: number;\n /** Visual highlight of the mismatched pixels, encoded as a PNG buffer.\n * `null` when `diffPixels` is `Infinity` — there's no pixel-aligned diff to\n * render across two different-sized images. */\n diffImage: Buffer | null;\n}\n\n/** Pixel-diffs two PNG buffers. */\nexport function diffPngBuffers(a: Buffer, b: Buffer): PngDiffResult {\n const imgA = PNG.sync.read(a);\n const imgB = PNG.sync.read(b);\n if (imgA.width !== imgB.width || imgA.height !== imgB.height) {\n return { diffPixels: Infinity, diffImage: null };\n }\n const diff = new PNG({ width: imgA.width, height: imgA.height });\n const diffPixels = pixelmatch(\n imgA.data,\n imgB.data,\n diff.data,\n imgA.width,\n imgA.height,\n { threshold: 0.1 },\n );\n return { diffPixels, diffImage: PNG.sync.write(diff) };\n}\n","import { Ajv } from \"ajv\";\nimport * as ajvFormatsModule from \"ajv-formats\";\nimport type { FormatsPlugin } from \"ajv-formats\";\nimport schema from \"./manifest.schema.json\" with { type: \"json\" };\n\n// See validateFileConfig.ts for why `.default` has to be unwrapped by hand.\nconst addFormats = (ajvFormatsModule as unknown as { default: FormatsPlugin })\n .default;\n\nconst ajv = new Ajv({ allErrors: true, strict: true });\naddFormats(ajv);\nconst validate = ajv.compile(schema);\n\n/**\n * Validates a parsed `test/golden/manifest.json` against `manifest.schema.json`.\n * Throws with every violation listed — callers must not silently coerce or\n * drop invalid entries.\n */\nexport function validateGoldenManifest(data: unknown, path: string): void {\n if (validate(data)) return;\n\n const errors = (validate.errors ?? [])\n .map((error) => {\n const extra = error.params?.additionalProperty\n ? ` '${error.params.additionalProperty}'`\n : \"\";\n return ` - ${error.instancePath || \"root\"} ${error.message}${extra}`;\n })\n .join(\"\\n\");\n throw new Error(`Invalid ${path}:\\n${errors}`);\n}\n","import {\n closeSync,\n existsSync,\n mkdirSync,\n openSync,\n readFileSync,\n renameSync,\n rmSync,\n unlinkSync,\n writeFileSync,\n} from \"node:fs\";\nimport { join } from \"node:path\";\nimport { validateGoldenManifest } from \"./validateManifest.js\";\n\nexport interface GoldenManifestEntry {\n createdAt: string;\n sourceOfTruthCreatedAt?: string;\n}\n\nexport type GoldenManifest = Record<string, GoldenManifestEntry>;\n\nexport function manifestPath(goldenDir: string): string {\n return join(goldenDir, \"manifest.json\");\n}\n\nfunction manifestLockPath(goldenDir: string): string {\n return join(goldenDir, \"manifest.json.lock\");\n}\n\nexport function goldenImagePath(goldenDir: string, storyId: string): string {\n return join(goldenDir, `${storyId}.png`);\n}\n\n/** Returns `{}` if no manifest exists yet — a fresh adapter with no goldens\n * captured is the normal starting state, not an error. */\nexport function loadManifest(goldenDir: string): GoldenManifest {\n const path = manifestPath(goldenDir);\n if (!existsSync(path)) return {};\n const data = JSON.parse(readFileSync(path, \"utf8\"));\n validateGoldenManifest(data, path);\n return data;\n}\n\n/** Validates before writing, and sorts keys so the diff on a reviewed PR is\n * stable regardless of the order stories happened to run in. Writes to a\n * temp file and renames over the real one — `rename` is atomic, so a\n * concurrent `loadManifest` (running in another Playwright worker) never\n * observes a half-written file. */\nexport function saveManifest(\n goldenDir: string,\n manifest: GoldenManifest,\n): void {\n const path = manifestPath(goldenDir);\n validateGoldenManifest(manifest, path);\n const sorted: GoldenManifest = {};\n for (const key of Object.keys(manifest).sort()) {\n sorted[key] = manifest[key]!;\n }\n mkdirSync(goldenDir, { recursive: true });\n const tmpPath = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;\n writeFileSync(tmpPath, JSON.stringify(sorted, null, 2) + \"\\n\");\n renameSync(tmpPath, path);\n}\n\nconst LOCK_RETRY_MS = 25;\nconst LOCK_TIMEOUT_MS = 15_000;\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/** Exclusive-create the lock file, spin-retrying until it's free. `wx` fails\n * atomically (`EEXIST`) if another worker process already holds it — that's\n * the only signal we need, no third-party lock library required for a\n * same-machine, same-run lock like this. */\nasync function acquireManifestLock(goldenDir: string): Promise<void> {\n mkdirSync(goldenDir, { recursive: true });\n const path = manifestLockPath(goldenDir);\n const deadline = Date.now() + LOCK_TIMEOUT_MS;\n for (;;) {\n try {\n closeSync(openSync(path, \"wx\"));\n return;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"EEXIST\") throw error;\n if (Date.now() >= deadline) {\n throw new Error(\n `Timed out waiting for the manifest lock at \"${path}\" — delete it if a previous run crashed while holding it.`,\n );\n }\n await sleep(LOCK_RETRY_MS);\n }\n }\n}\n\nfunction releaseManifestLock(goldenDir: string): void {\n rmSync(manifestLockPath(goldenDir), { force: true });\n}\n\n/** Runs `updater` against this story's manifest entry under an exclusive\n * lock on `manifest.json`: reloads the manifest fresh, applies `updater`,\n * and saves it back, all before releasing the lock. Concurrent Playwright\n * workers each own a different story, so this is the only section that\n * needs to serialize — everything else about a story (its screenshot, its\n * golden image file, its diff) is independent of every other story. */\nexport async function updateManifestEntry(\n goldenDir: string,\n storyId: string,\n updater: (\n entry: GoldenManifestEntry | undefined,\n ) => GoldenManifestEntry | undefined,\n): Promise<GoldenManifestEntry | undefined> {\n await acquireManifestLock(goldenDir);\n try {\n const manifest = loadManifest(goldenDir);\n const nextEntry = updater(manifest[storyId]);\n if (nextEntry === undefined) {\n delete manifest[storyId];\n } else {\n manifest[storyId] = nextEntry;\n }\n saveManifest(goldenDir, manifest);\n return nextEntry;\n } finally {\n releaseManifestLock(goldenDir);\n }\n}\n\nexport function saveGoldenImage(\n goldenDir: string,\n storyId: string,\n buffer: Buffer,\n): void {\n mkdirSync(goldenDir, { recursive: true });\n writeFileSync(goldenImagePath(goldenDir, storyId), buffer);\n}\n\n/** Removes the golden `.png` + manifest entry for every story id in the\n * manifest that isn't in `currentStoryIds` (e.g. a story renamed or deleted\n * from Storybook) — otherwise those never get cleaned up on their own,\n * since a run only ever adds/updates entries for stories it actually saw.\n * Returns the pruned ids, for the caller to report. Both the file removal\n * and the manifest delete are idempotent, so it's safe for this to run\n * redundantly from more than one Playwright worker. */\nexport async function pruneOrphanedGoldens(\n goldenDir: string,\n currentStoryIds: ReadonlySet<string>,\n): Promise<string[]> {\n const manifest = loadManifest(goldenDir);\n const orphanIds = Object.keys(manifest).filter(\n (id) => !currentStoryIds.has(id),\n );\n for (const id of orphanIds) {\n const imagePath = goldenImagePath(goldenDir, id);\n if (existsSync(imagePath)) unlinkSync(imagePath);\n await updateManifestEntry(goldenDir, id, () => undefined);\n }\n return orphanIds;\n}\n","import { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { SourceOfTruthGoldenLocation } from \"../config.js\";\nimport {\n goldenImagePath,\n loadManifest,\n manifestPath,\n type GoldenManifest,\n} from \"./manifestStore.js\";\nimport { validateGoldenManifest } from \"./validateManifest.js\";\n\nconst GITHUB_REPO = \"borderux/recursica\";\n\nexport interface SourceOfTruthGolden {\n manifest: GoldenManifest;\n /** Returns the golden PNG bytes for a story, or `null` if the source of\n * truth has no golden captured for it yet. */\n readImage(storyId: string): Promise<Buffer | null>;\n}\n\nasync function resolveNpmVersion(\n packageName: string,\n versionSpec: string,\n): Promise<string> {\n const response = await fetch(`https://registry.npmjs.org/${packageName}`);\n if (!response.ok) {\n throw new Error(\n `Could not reach npm registry for ${packageName}: ${response.statusText}`,\n );\n }\n const data = (await response.json()) as {\n \"dist-tags\"?: Record<string, string>;\n versions?: Record<string, unknown>;\n };\n const resolved =\n data[\"dist-tags\"]?.[versionSpec] ??\n (data.versions?.[versionSpec] ? versionSpec : undefined);\n if (!resolved) {\n throw new Error(\n `${packageName} has no version or dist-tag \"${versionSpec}\" on the npm registry.`,\n );\n }\n return resolved;\n}\n\n// This monorepo's packages all live at `packages/<unscoped-name>` — mirrors\n// the same convention `mantineSourceOfTruthHarness` and every existing\n// `packages/*/package.json`'s `repository.directory` field already assume.\nfunction packageDirectory(packageName: string): string {\n return `packages/${packageName.split(\"/\").pop()}`;\n}\n\n/**\n * Resolves the source-of-truth adapter's golden images for the divergence\n * check. Never boots a Storybook — both location types resolve to plain\n * files, fetched once and cached, not re-diffed per pixel over the wire.\n *\n * `location.type === \"local\"`: a sibling package already checked out (this\n * monorepo's own `sourceOfTruth.type: \"url\"` mode) — read its\n * `test/golden/` directly, including any uncommitted local changes.\n *\n * `location.type === \"npm\"`: no local checkout (the default, standalone-repo\n * mode) — resolve the installed version against the npm registry, then fetch\n * that exact version's `test/golden/` from the public GitHub repo at the\n * matching release tag (changesets tags every release as\n * `<packageName>@<version>`), caching what's downloaded under `cacheDir`.\n *\n * Returns `null` — degrading the divergence check to a skip, not a failure —\n * when no golden baseline exists yet for this version, or the registry/repo\n * is unreachable.\n */\nexport async function resolveSourceOfTruthGolden(\n location: SourceOfTruthGoldenLocation,\n): Promise<SourceOfTruthGolden | null> {\n if (location.type === \"local\") {\n if (!existsSync(manifestPath(location.dir))) {\n console.warn(\n `No golden baseline found yet at ${location.dir} — source-of-truth divergence check skipped for this run.`,\n );\n return null;\n }\n const manifest = loadManifest(location.dir);\n return {\n manifest,\n async readImage(storyId) {\n const path = goldenImagePath(location.dir, storyId);\n return existsSync(path) ? readFileSync(path) : null;\n },\n };\n }\n\n let version: string;\n try {\n version = await resolveNpmVersion(\n location.packageName,\n location.versionSpec,\n );\n } catch (error) {\n console.warn(\n `Could not resolve ${location.packageName}@${location.versionSpec} — source-of-truth divergence check skipped for this run.`,\n error,\n );\n return null;\n }\n\n const cacheDir = join(location.cacheDir, version);\n const tag = `${location.packageName}@${version}`;\n const rawBase = `https://raw.githubusercontent.com/${GITHUB_REPO}/${tag}/${packageDirectory(location.packageName)}/test/golden`;\n\n let manifest: GoldenManifest;\n const cachedManifestPath = manifestPath(cacheDir);\n if (existsSync(cachedManifestPath)) {\n manifest = loadManifest(cacheDir);\n } else {\n let response: Response;\n try {\n response = await fetch(`${rawBase}/manifest.json`);\n } catch (error) {\n console.warn(\n `Could not reach GitHub to fetch ${tag}'s golden baseline — source-of-truth divergence check skipped for this run.`,\n error,\n );\n return null;\n }\n if (!response.ok) {\n console.warn(\n `No golden baseline published for ${tag} — source-of-truth divergence check skipped for this run.`,\n );\n return null;\n }\n const text = await response.text();\n const parsed = JSON.parse(text);\n validateGoldenManifest(parsed, `${rawBase}/manifest.json`);\n mkdirSync(cacheDir, { recursive: true });\n writeFileSync(cachedManifestPath, text);\n manifest = parsed;\n }\n\n return {\n manifest,\n async readImage(storyId) {\n const cachedImagePath = goldenImagePath(cacheDir, storyId);\n if (existsSync(cachedImagePath)) return readFileSync(cachedImagePath);\n const response = await fetch(`${rawBase}/${storyId}.png`);\n if (!response.ok) return null;\n const buffer = Buffer.from(await response.arrayBuffer());\n mkdirSync(cacheDir, { recursive: true });\n writeFileSync(cachedImagePath, buffer);\n return buffer;\n },\n };\n}\n","import { expect } from \"@playwright/test\";\nimport type { Browser, TestInfo } from \"@playwright/test\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport type { AdapterTesterConfig } from \"../config.js\";\nimport { diffPngBuffers } from \"../golden/diffPng.js\";\nimport {\n type GoldenManifestEntry,\n goldenImagePath,\n loadManifest,\n pruneOrphanedGoldens,\n saveGoldenImage,\n updateManifestEntry,\n} from \"../golden/manifestStore.js\";\nimport { resolveSourceOfTruthGolden } from \"../golden/resolveSourceOfTruthGolden.js\";\n\nconst DEFAULT_EXCLUDE_TITLE_PREFIXES = [\"Theme\", \"Tokens\", \"Introduction\"];\n\ninterface StorybookEntry {\n type: string;\n id: string;\n name: string;\n title: string;\n}\n\nfunction matchesPrefix(id: string, prefix: string): boolean {\n return id === prefix || id.startsWith(prefix);\n}\n\n/** Fetches every story `target`'s Storybook currently has, filtered only by\n * `excludeTitlePrefixes` — not `stories.<id>.exclude`, so callers can still\n * tell an excluded story apart from one that's genuinely missing. */\nasync function fetchStories(\n target: { name: string; url: string },\n excludeTitlePrefixes: string[],\n): Promise<StorybookEntry[]> {\n let stories: StorybookEntry[];\n try {\n const response = await fetch(`${target.url}/index.json`);\n if (!response.ok) {\n throw new Error(\n `Failed to fetch Storybook index: ${response.statusText}`,\n );\n }\n const data = (await response.json()) as any;\n const entries = data.entries || {};\n stories = Object.values(entries).filter(\n (entry: any) =>\n entry.type === \"story\" &&\n !excludeTitlePrefixes.some(\n (prefix) =>\n entry.title === prefix || entry.title.startsWith(`${prefix}/`),\n ),\n ) as StorybookEntry[];\n stories.sort((a, b) => a.id.localeCompare(b.id));\n } catch (error) {\n console.error(\n \"Failed to load Storybook index from\",\n `${target.url}/index.json`,\n error,\n );\n throw new Error(\n `Storybook target \"${target.name}\" is not responsive or index.json is missing. Please ensure its Storybook is running.`,\n );\n }\n return stories;\n}\n\n/** Resolves the diff threshold for `storyId`: the longest (most specific)\n * `storyThresholds` key matching by prefix, falling back to\n * `defaultThresholdPixels` when nothing matches. */\nfunction resolveThreshold(\n storyId: string,\n storyThresholds: Record<string, number>,\n defaultThresholdPixels: number,\n): number {\n let bestMatch: string | undefined;\n for (const prefix of Object.keys(storyThresholds)) {\n if (\n matchesPrefix(storyId, prefix) &&\n (!bestMatch || prefix.length > bestMatch.length)\n ) {\n bestMatch = prefix;\n }\n }\n return bestMatch !== undefined\n ? storyThresholds[bestMatch]!\n : defaultThresholdPixels;\n}\n\n/** Everything a generated Playwright spec needs to register the golden-image\n * suite itself. Split out from the actual `test.describe`/`test` calls so\n * those calls execute in the spec file that imports this, not in this\n * library file — otherwise Playwright's HTML report groups every story under\n * this file's own (sourcemapped) path instead of a stable spec name. */\nexport interface VisualRegressionPlan {\n /** `config`'s own (non-source-of-truth) target name, for the suite title. */\n ownTargetName: string;\n /** Suite title suffix describing which check mode is running. */\n suiteLabel: string;\n /** Stories to check, already filtered and sorted by id. */\n stories: StorybookEntry[];\n /** Story ids the source-of-truth adapter has a golden for but this\n * project's own Storybook doesn't — empty outside `checkMode: \"divergence\"`\n * or when `sourceOfTruthGolden` didn't resolve. Excludes ids covered by a\n * `stories.<id>.exclude` entry: an intentional gap, not a sync failure. */\n missingFromSourceOfTruth: string[];\n /** Golden-checks one story. Call this from inside a `test(story.id, ...)`\n * body — safe to run concurrently across Playwright workers, since each\n * call only ever reads/writes its own story's manifest entry (locked at\n * the point it writes it back, so concurrent workers never race each\n * other's entries — see `updateManifestEntry`). */\n checkStory: (\n story: StorybookEntry,\n browser: Browser,\n testInfo: TestInfo,\n ) => Promise<void>;\n}\n\n/** Everything a generated Playwright spec needs to register the golden-image\n * suite itself. Split out from the actual `test.describe`/`test` calls so\n * those calls execute in the spec file that imports this, not in this\n * library file — otherwise Playwright's HTML report groups every story under\n * this file's own (sourcemapped) path instead of a stable spec name. */\nexport interface VisualRegressionPlan {\n /** `config`'s own (non-source-of-truth) target name, for the suite title. */\n ownTargetName: string;\n /** Suite title suffix describing which check mode is running. */\n suiteLabel: string;\n /** Stories to check, already filtered and sorted by id. */\n stories: StorybookEntry[];\n /** Golden-checks one story. Call this from inside a `test(story.id, ...)`\n * body — safe to run concurrently across Playwright workers, since each\n * call only ever reads/writes its own story's manifest entry (locked at\n * the point it writes it back, so concurrent workers never race each\n * other's entries — see `updateManifestEntry`). */\n checkStory: (\n story: StorybookEntry,\n browser: Browser,\n testInfo: TestInfo,\n ) => Promise<void>;\n}\n\n/**\n * Resolves the golden-image plan for `config`'s own target (the one target\n * in `config.targets` not marked `sourceOfTruth`).\n *\n * Two independent checks per story, gated by `config.checkMode`, neither of\n * which boots the source-of-truth adapter's own Storybook — the divergence\n * check below compares stored golden files, not live pages:\n *\n * 1. **Own-drift (`checkMode: \"own\"`, the default; hard fail):** this run's\n * live render vs this project's own stored `test/golden/<story-id>.png`.\n * No golden yet for a story is not a failure — one is captured from this\n * run instead (same as `--update-golden`, scoped to just that story), in\n * either mode.\n * 2. **Source-of-truth divergence (`checkMode: \"divergence\"`; hard fail):**\n * this project's own golden vs the source-of-truth's golden (`config`'s\n * `sourceOfTruthGolden`). Skipped entirely when\n * `config.isSourceOfTruthAdapter` is true — the\n * source-of-truth adapter has nothing above it to diverge from — and\n * skipped per-story when neither side has a baseline yet. A\n * once-flagged divergence stays quiet after `--approve-divergence`,\n * until the source of truth's own golden changes again.\n */\nexport async function resolveVisualRegressionPlan(\n config: AdapterTesterConfig,\n): Promise<VisualRegressionPlan> {\n const ownTarget = config.isSourceOfTruthAdapter\n ? config.targets[0]\n : config.targets.find((target) => !target.sourceOfTruth);\n if (!ownTarget) {\n throw new Error(\n \"adapter-tester config has no non-sourceOfTruth target to run the golden check against.\",\n );\n }\n const excludeTitlePrefixes =\n config.excludeTitlePrefixes ?? DEFAULT_EXCLUDE_TITLE_PREFIXES;\n const storyOverrides = config.stories ?? {};\n const excludeStoryIds = Object.keys(storyOverrides).filter(\n (id) => storyOverrides[id]!.exclude,\n );\n const goldenStoryThresholds = Object.fromEntries(\n Object.entries(storyOverrides)\n .filter(([, override]) => override.goldenThreshold !== undefined)\n .map(([id, override]) => [id, override.goldenThreshold!]),\n );\n const sourceOfTruthStoryThresholds = Object.fromEntries(\n Object.entries(storyOverrides)\n .filter(([, override]) => override.sourceOfTruthThreshold !== undefined)\n .map(([id, override]) => [id, override.sourceOfTruthThreshold!]),\n );\n const goldenDir = config.goldenDir;\n const goldenMode = config.goldenMode;\n const checkMode = config.checkMode;\n\n // Fetched with only excludeTitlePrefixes applied — not excludeStoryIds —\n // so the source-of-truth story-parity check below can tell an excluded\n // story apart from one that's genuinely missing from this Storybook.\n const ownStories = await fetchStories(ownTarget, excludeTitlePrefixes);\n const stories = ownStories.filter(\n (entry) =>\n !excludeStoryIds.some((prefix) => matchesPrefix(entry.id, prefix)),\n );\n\n // `--update-golden` redefines this project's own baseline, so it's also\n // the point a renamed/removed story's now-orphaned golden gets cleaned up\n // — otherwise nothing ever prunes it, since a run only ever adds/updates\n // entries for stories it actually saw in this pass. Uses the full current\n // story list (not narrowed by any `--grep` Playwright itself applies), so\n // this catches every orphan regardless of how the run is scoped.\n if (goldenMode === \"update-golden\") {\n const prunedStoryIds = await pruneOrphanedGoldens(\n goldenDir,\n new Set(stories.map((story) => story.id)),\n );\n if (prunedStoryIds.length > 0) {\n console.warn(\n `Pruned ${prunedStoryIds.length} orphaned golden(s) no longer in Storybook: ${prunedStoryIds.join(\", \")}`,\n );\n }\n }\n\n const sourceOfTruthGolden =\n checkMode !== \"divergence\" ||\n config.isSourceOfTruthAdapter ||\n !config.sourceOfTruthGolden\n ? null\n : await resolveSourceOfTruthGolden(config.sourceOfTruthGolden);\n\n // Checked against `ownStories` (title-prefix-excluded only), not `stories`\n // — a story marked `exclude: true` still counts as \"present\", it's just\n // not diffed. Only a story the source of truth has that this adapter\n // never built at all, and hasn't acknowledged via `exclude`, is missing.\n const ownStoryIds = new Set(ownStories.map((entry) => entry.id));\n const missingFromSourceOfTruth = sourceOfTruthGolden\n ? Object.keys(sourceOfTruthGolden.manifest)\n .filter(\n (id) =>\n !ownStoryIds.has(id) &&\n !excludeStoryIds.some((prefix) => matchesPrefix(id, prefix)),\n )\n .sort()\n : [];\n\n if (missingFromSourceOfTruth.length > 0) {\n console.error(\n `[adapter-tester] ${missingFromSourceOfTruth.length} stor(y/ies) exist in the source of truth but are missing here: ${missingFromSourceOfTruth.join(\", \")}. Add the missing story, or mark it \\`exclude: true\\` under \\`stories\\` in adapter-tester.config.json if intentional.`,\n );\n }\n\n console.log(\n [\n `[adapter-tester] target: \"${ownTarget.name}\" (${ownTarget.url})`,\n `[adapter-tester] checkMode: \"${checkMode}\" (${checkMode === \"divergence\" ? \"this project's golden vs source-of-truth's golden\" : \"live render vs this project's own golden\"})`,\n `[adapter-tester] goldenMode: \"${goldenMode}\"`,\n `[adapter-tester] goldenThresholdPixels: ${config.goldenThresholdPixels}`,\n `[adapter-tester] sourceOfTruthThresholdPixels: ${config.sourceOfTruthThresholdPixels}`,\n checkMode === \"divergence\"\n ? config.isSourceOfTruthAdapter\n ? `[adapter-tester] sourceOfTruthGolden: skipped — this is the source-of-truth adapter, nothing to diverge from`\n : !config.sourceOfTruthGolden\n ? `[adapter-tester] sourceOfTruthGolden: skipped — no sourceOfTruthGolden configured`\n : sourceOfTruthGolden\n ? `[adapter-tester] sourceOfTruthGolden: resolved, ${Object.keys(sourceOfTruthGolden.manifest).length} golden(s) available (config: ${JSON.stringify(config.sourceOfTruthGolden)})`\n : `[adapter-tester] sourceOfTruthGolden: unavailable — no baseline found or unreachable (config: ${JSON.stringify(config.sourceOfTruthGolden)}); divergence check will skip every story`\n : `[adapter-tester] sourceOfTruthGolden: not used in \"own\" checkMode`,\n `[adapter-tester] stories: ${stories.length} to check (excluded: ${excludeStoryIds.length}, title prefixes excluded: ${excludeTitlePrefixes.join(\", \") || \"none\"})`,\n `[adapter-tester] story parity with source of truth: ${missingFromSourceOfTruth.length === 0 ? \"OK\" : `${missingFromSourceOfTruth.length} missing (see error above)`}`,\n ].join(\"\\n\"),\n );\n\n const suiteLabel =\n checkMode === \"divergence\"\n ? \"Source-of-Truth Divergence Check\"\n : \"Own-Drift Golden Image Check\";\n\n return {\n ownTargetName: ownTarget.name,\n suiteLabel,\n stories,\n missingFromSourceOfTruth,\n checkStory: async (story, browser, testInfo) => {\n const page = await browser.newPage();\n await page.setViewportSize({ width: 800, height: 600 });\n await page.goto(\n `${ownTarget.url}/iframe.html?id=${story.id}&viewMode=story`,\n { waitUntil: \"networkidle\" },\n );\n await page.waitForSelector(\"#storybook-root\");\n await page.waitForTimeout(300);\n const liveBuffer = await page.screenshot();\n\n const imagePath = goldenImagePath(goldenDir, story.id);\n // Only this worker ever touches this story's key, so reading it here\n // (outside the lock `updateManifestEntry` takes at the end) can't\n // race another worker — they're all reading/writing different keys.\n const entry = loadManifest(goldenDir)[story.id];\n const capturingNewGolden =\n goldenMode !== \"check\" || !entry || !existsSync(imagePath);\n\n let currentEntry: GoldenManifestEntry;\n if (capturingNewGolden) {\n saveGoldenImage(goldenDir, story.id, liveBuffer);\n currentEntry = entry?.sourceOfTruthCreatedAt\n ? {\n createdAt: new Date().toISOString(),\n sourceOfTruthCreatedAt: entry.sourceOfTruthCreatedAt,\n }\n : { createdAt: new Date().toISOString() };\n if (!entry) {\n testInfo.annotations.push({\n type: \"golden-created\",\n description: `No golden existed yet for \"${story.id}\" — captured one from this run.`,\n });\n }\n } else {\n currentEntry = entry;\n if (checkMode === \"own\") {\n const goldenBuffer = readFileSync(imagePath);\n const { diffPixels, diffImage } = diffPngBuffers(\n liveBuffer,\n goldenBuffer,\n );\n const threshold = resolveThreshold(\n story.id,\n goldenStoryThresholds,\n config.goldenThresholdPixels,\n );\n if (diffPixels >= threshold) {\n await testInfo.attach(\"expected\", {\n body: goldenBuffer,\n contentType: \"image/png\",\n });\n await testInfo.attach(\"actual\", {\n body: liveBuffer,\n contentType: \"image/png\",\n });\n if (diffImage) {\n await testInfo.attach(\"diff\", {\n body: diffImage,\n contentType: \"image/png\",\n });\n }\n }\n expect\n .soft(\n diffPixels,\n `\"${story.id}\" has drifted from its own golden image (${diffPixels} mismatched pixels, threshold ${threshold})`,\n )\n .toBeLessThan(threshold);\n }\n }\n\n if (sourceOfTruthGolden) {\n const sourceOfTruthEntry = sourceOfTruthGolden.manifest[story.id];\n const sourceOfTruthImage = sourceOfTruthEntry\n ? await sourceOfTruthGolden.readImage(story.id)\n : null;\n\n if (sourceOfTruthEntry && sourceOfTruthImage) {\n if (goldenMode === \"approve-divergence\") {\n currentEntry = {\n ...currentEntry,\n sourceOfTruthCreatedAt: sourceOfTruthEntry.createdAt,\n };\n } else {\n const ownImage = readFileSync(imagePath);\n const { diffPixels, diffImage } = diffPngBuffers(\n ownImage,\n sourceOfTruthImage,\n );\n const threshold = resolveThreshold(\n story.id,\n sourceOfTruthStoryThresholds,\n config.sourceOfTruthThresholdPixels,\n );\n const approvedAt = currentEntry.sourceOfTruthCreatedAt;\n const isKnownDivergence =\n diffPixels < threshold ||\n (approvedAt !== undefined &&\n approvedAt >= sourceOfTruthEntry.createdAt);\n if (!isKnownDivergence) {\n await testInfo.attach(\"expected\", {\n body: sourceOfTruthImage,\n contentType: \"image/png\",\n });\n await testInfo.attach(\"actual\", {\n body: ownImage,\n contentType: \"image/png\",\n });\n if (diffImage) {\n await testInfo.attach(\"diff\", {\n body: diffImage,\n contentType: \"image/png\",\n });\n }\n testInfo.annotations.push({\n type: \"source-of-truth-divergence\",\n description: `\"${story.id}\" differs from the source of truth's golden by ${diffPixels} mismatched pixels (threshold ${threshold}) and hasn't been reviewed — run with --approve-divergence if this is intentional, or fix the adapter styling.`,\n });\n }\n expect\n .soft(\n isKnownDivergence,\n `\"${story.id}\" differs from the source of truth's golden by ${diffPixels} mismatched pixels (threshold ${threshold}) and hasn't been reviewed — run with --approve-divergence if this is intentional, or fix the adapter styling.`,\n )\n .toBe(true);\n }\n }\n }\n\n // Locked read-modify-write of just this story's entry — see\n // `updateManifestEntry` for why that's enough to make this safe\n // across concurrent Playwright workers.\n await updateManifestEntry(goldenDir, story.id, () => currentEntry);\n },\n };\n}\n"],"names":["diffPngBuffers","a","b","imgA","PNG","imgB","diff","pixelmatch","addFormats","ajvFormatsModule.default","ajv","Ajv","validate","schema","validateGoldenManifest","data","path","errors","error","extra","_a","manifestPath","goldenDir","join","manifestLockPath","goldenImagePath","storyId","loadManifest","existsSync","readFileSync","saveManifest","manifest","sorted","key","mkdirSync","tmpPath","writeFileSync","renameSync","LOCK_RETRY_MS","LOCK_TIMEOUT_MS","sleep","ms","resolve","acquireManifestLock","deadline","closeSync","openSync","releaseManifestLock","rmSync","updateManifestEntry","updater","nextEntry","saveGoldenImage","buffer","pruneOrphanedGoldens","currentStoryIds","orphanIds","id","imagePath","unlinkSync","GITHUB_REPO","resolveNpmVersion","packageName","versionSpec","response","resolved","_b","packageDirectory","resolveSourceOfTruthGolden","location","version","cacheDir","tag","rawBase","cachedManifestPath","text","parsed","cachedImagePath","DEFAULT_EXCLUDE_TITLE_PREFIXES","matchesPrefix","prefix","fetchStories","target","excludeTitlePrefixes","stories","entries","entry","resolveThreshold","storyThresholds","defaultThresholdPixels","bestMatch","resolveVisualRegressionPlan","config","ownTarget","storyOverrides","excludeStoryIds","goldenStoryThresholds","override","sourceOfTruthStoryThresholds","goldenMode","checkMode","ownStories","prunedStoryIds","story","sourceOfTruthGolden","ownStoryIds","missingFromSourceOfTruth","suiteLabel","browser","testInfo","page","liveBuffer","capturingNewGolden","currentEntry","goldenBuffer","diffPixels","diffImage","threshold","expect","sourceOfTruthEntry","sourceOfTruthImage","ownImage","approvedAt","isKnownDivergence"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"testing.js","sources":["../src/golden/diffPng.ts","../src/golden/validateManifest.ts","../src/golden/manifestStore.ts","../src/golden/resolveSourceOfTruthGolden.ts","../src/testing/runVisualRegression.ts"],"sourcesContent":["import pixelmatch from \"pixelmatch\";\nimport { PNG } from \"pngjs\";\n\nexport interface PngDiffResult {\n /** Mismatched-pixel count, or `Infinity` if the two images aren't even the\n * same dimensions — pixelmatch itself throws on a size mismatch, and a size\n * mismatch is itself a real difference, not something to swallow. */\n diffPixels: number;\n /** Visual highlight of the mismatched pixels, encoded as a PNG buffer.\n * `null` when `diffPixels` is `Infinity` — there's no pixel-aligned diff to\n * render across two different-sized images. */\n diffImage: Buffer | null;\n}\n\n/** Pixel-diffs two PNG buffers. */\nexport function diffPngBuffers(a: Buffer, b: Buffer): PngDiffResult {\n const imgA = PNG.sync.read(a);\n const imgB = PNG.sync.read(b);\n if (imgA.width !== imgB.width || imgA.height !== imgB.height) {\n return { diffPixels: Infinity, diffImage: null };\n }\n const diff = new PNG({ width: imgA.width, height: imgA.height });\n const diffPixels = pixelmatch(\n imgA.data,\n imgB.data,\n diff.data,\n imgA.width,\n imgA.height,\n { threshold: 0.1 },\n );\n return { diffPixels, diffImage: PNG.sync.write(diff) };\n}\n","import { Ajv } from \"ajv\";\nimport * as ajvFormatsModule from \"ajv-formats\";\nimport type { FormatsPlugin } from \"ajv-formats\";\nimport schema from \"./manifest.schema.json\" with { type: \"json\" };\n\n// See validateFileConfig.ts for why `.default` has to be unwrapped by hand.\nconst addFormats = (ajvFormatsModule as unknown as { default: FormatsPlugin })\n .default;\n\nconst ajv = new Ajv({ allErrors: true, strict: true });\naddFormats(ajv);\nconst validate = ajv.compile(schema);\n\n/**\n * Validates a parsed `test/golden/manifest.json` against `manifest.schema.json`.\n * Throws with every violation listed — callers must not silently coerce or\n * drop invalid entries.\n */\nexport function validateGoldenManifest(data: unknown, path: string): void {\n if (validate(data)) return;\n\n const errors = (validate.errors ?? [])\n .map((error) => {\n const extra = error.params?.additionalProperty\n ? ` '${error.params.additionalProperty}'`\n : \"\";\n return ` - ${error.instancePath || \"root\"} ${error.message}${extra}`;\n })\n .join(\"\\n\");\n throw new Error(`Invalid ${path}:\\n${errors}`);\n}\n","import {\n closeSync,\n existsSync,\n mkdirSync,\n openSync,\n readFileSync,\n renameSync,\n rmSync,\n unlinkSync,\n writeFileSync,\n} from \"node:fs\";\nimport { join } from \"node:path\";\nimport { validateGoldenManifest } from \"./validateManifest.js\";\n\nexport interface GoldenManifestEntry {\n createdAt: string;\n sourceOfTruthCreatedAt?: string;\n}\n\nexport type GoldenManifest = Record<string, GoldenManifestEntry>;\n\nexport function manifestPath(goldenDir: string): string {\n return join(goldenDir, \"manifest.json\");\n}\n\nfunction manifestLockPath(goldenDir: string): string {\n return join(goldenDir, \"manifest.json.lock\");\n}\n\nexport function goldenImagePath(goldenDir: string, storyId: string): string {\n return join(goldenDir, `${storyId}.png`);\n}\n\n/** Returns `{}` if no manifest exists yet — a fresh adapter with no goldens\n * captured is the normal starting state, not an error. */\nexport function loadManifest(goldenDir: string): GoldenManifest {\n const path = manifestPath(goldenDir);\n if (!existsSync(path)) return {};\n const data = JSON.parse(readFileSync(path, \"utf8\"));\n validateGoldenManifest(data, path);\n return data;\n}\n\n/** Validates before writing, and sorts keys so the diff on a reviewed PR is\n * stable regardless of the order stories happened to run in. Writes to a\n * temp file and renames over the real one — `rename` is atomic, so a\n * concurrent `loadManifest` (running in another Playwright worker) never\n * observes a half-written file. */\nexport function saveManifest(\n goldenDir: string,\n manifest: GoldenManifest,\n): void {\n const path = manifestPath(goldenDir);\n validateGoldenManifest(manifest, path);\n const sorted: GoldenManifest = {};\n for (const key of Object.keys(manifest).sort()) {\n sorted[key] = manifest[key]!;\n }\n mkdirSync(goldenDir, { recursive: true });\n const tmpPath = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;\n writeFileSync(tmpPath, JSON.stringify(sorted, null, 2) + \"\\n\");\n renameSync(tmpPath, path);\n}\n\nconst LOCK_RETRY_MS = 25;\nconst LOCK_TIMEOUT_MS = 15_000;\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/** Exclusive-create the lock file, spin-retrying until it's free. `wx` fails\n * atomically (`EEXIST`) if another worker process already holds it — that's\n * the only signal we need, no third-party lock library required for a\n * same-machine, same-run lock like this. */\nasync function acquireManifestLock(goldenDir: string): Promise<void> {\n mkdirSync(goldenDir, { recursive: true });\n const path = manifestLockPath(goldenDir);\n const deadline = Date.now() + LOCK_TIMEOUT_MS;\n for (;;) {\n try {\n closeSync(openSync(path, \"wx\"));\n return;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"EEXIST\") throw error;\n if (Date.now() >= deadline) {\n throw new Error(\n `Timed out waiting for the manifest lock at \"${path}\" — delete it if a previous run crashed while holding it.`,\n );\n }\n await sleep(LOCK_RETRY_MS);\n }\n }\n}\n\nfunction releaseManifestLock(goldenDir: string): void {\n rmSync(manifestLockPath(goldenDir), { force: true });\n}\n\n/** Runs `updater` against this story's manifest entry under an exclusive\n * lock on `manifest.json`: reloads the manifest fresh, applies `updater`,\n * and saves it back, all before releasing the lock. Concurrent Playwright\n * workers each own a different story, so this is the only section that\n * needs to serialize — everything else about a story (its screenshot, its\n * golden image file, its diff) is independent of every other story. */\nexport async function updateManifestEntry(\n goldenDir: string,\n storyId: string,\n updater: (\n entry: GoldenManifestEntry | undefined,\n ) => GoldenManifestEntry | undefined,\n): Promise<GoldenManifestEntry | undefined> {\n await acquireManifestLock(goldenDir);\n try {\n const manifest = loadManifest(goldenDir);\n const nextEntry = updater(manifest[storyId]);\n if (nextEntry === undefined) {\n delete manifest[storyId];\n } else {\n manifest[storyId] = nextEntry;\n }\n saveManifest(goldenDir, manifest);\n return nextEntry;\n } finally {\n releaseManifestLock(goldenDir);\n }\n}\n\nexport function saveGoldenImage(\n goldenDir: string,\n storyId: string,\n buffer: Buffer,\n): void {\n mkdirSync(goldenDir, { recursive: true });\n writeFileSync(goldenImagePath(goldenDir, storyId), buffer);\n}\n\n/** Removes the golden `.png` + manifest entry for every story id in the\n * manifest that isn't in `currentStoryIds` (e.g. a story renamed or deleted\n * from Storybook) — otherwise those never get cleaned up on their own,\n * since a run only ever adds/updates entries for stories it actually saw.\n * Returns the pruned ids, for the caller to report. Both the file removal\n * and the manifest delete are idempotent, so it's safe for this to run\n * redundantly from more than one Playwright worker. */\nexport async function pruneOrphanedGoldens(\n goldenDir: string,\n currentStoryIds: ReadonlySet<string>,\n): Promise<string[]> {\n const manifest = loadManifest(goldenDir);\n const orphanIds = Object.keys(manifest).filter(\n (id) => !currentStoryIds.has(id),\n );\n for (const id of orphanIds) {\n const imagePath = goldenImagePath(goldenDir, id);\n if (existsSync(imagePath)) unlinkSync(imagePath);\n await updateManifestEntry(goldenDir, id, () => undefined);\n }\n return orphanIds;\n}\n","import { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { SourceOfTruthGoldenLocation } from \"../config.js\";\nimport {\n goldenImagePath,\n loadManifest,\n manifestPath,\n type GoldenManifest,\n} from \"./manifestStore.js\";\nimport { validateGoldenManifest } from \"./validateManifest.js\";\n\nconst GITHUB_REPO = \"borderux/recursica\";\n\nexport interface SourceOfTruthGolden {\n manifest: GoldenManifest;\n /** Returns the golden PNG bytes for a story, or `null` if the source of\n * truth has no golden captured for it yet. */\n readImage(storyId: string): Promise<Buffer | null>;\n}\n\nasync function resolveNpmVersion(\n packageName: string,\n versionSpec: string,\n): Promise<string> {\n const response = await fetch(`https://registry.npmjs.org/${packageName}`);\n if (!response.ok) {\n throw new Error(\n `Could not reach npm registry for ${packageName}: ${response.statusText}`,\n );\n }\n const data = (await response.json()) as {\n \"dist-tags\"?: Record<string, string>;\n versions?: Record<string, unknown>;\n };\n const resolved =\n data[\"dist-tags\"]?.[versionSpec] ??\n (data.versions?.[versionSpec] ? versionSpec : undefined);\n if (!resolved) {\n throw new Error(\n `${packageName} has no version or dist-tag \"${versionSpec}\" on the npm registry.`,\n );\n }\n return resolved;\n}\n\n// This monorepo's packages all live at `packages/<unscoped-name>` — mirrors\n// the same convention `mantineSourceOfTruthHarness` and every existing\n// `packages/*/package.json`'s `repository.directory` field already assume.\nfunction packageDirectory(packageName: string): string {\n return `packages/${packageName.split(\"/\").pop()}`;\n}\n\n/**\n * Resolves the source-of-truth adapter's golden images for the divergence\n * check. Never boots a Storybook — both location types resolve to plain\n * files, fetched once and cached, not re-diffed per pixel over the wire.\n *\n * `location.type === \"local\"`: a sibling package already checked out (this\n * monorepo's own `sourceOfTruth.type: \"url\"` mode) — read its\n * `test/golden/` directly, including any uncommitted local changes.\n *\n * `location.type === \"npm\"`: no local checkout (the default, standalone-repo\n * mode) — resolve the installed version against the npm registry, then fetch\n * that exact version's `test/golden/` from the public GitHub repo at the\n * matching release tag (changesets tags every release as\n * `<packageName>@<version>`), caching what's downloaded under `cacheDir`.\n *\n * Returns `null` — degrading the divergence check to a skip, not a failure —\n * when no golden baseline exists yet for this version, or the registry/repo\n * is unreachable.\n */\nexport async function resolveSourceOfTruthGolden(\n location: SourceOfTruthGoldenLocation,\n): Promise<SourceOfTruthGolden | null> {\n if (location.type === \"local\") {\n if (!existsSync(manifestPath(location.dir))) {\n console.warn(\n `No golden baseline found yet at ${location.dir} — source-of-truth divergence check skipped for this run.`,\n );\n return null;\n }\n const manifest = loadManifest(location.dir);\n return {\n manifest,\n async readImage(storyId) {\n const path = goldenImagePath(location.dir, storyId);\n return existsSync(path) ? readFileSync(path) : null;\n },\n };\n }\n\n let version: string;\n try {\n version = await resolveNpmVersion(\n location.packageName,\n location.versionSpec,\n );\n } catch (error) {\n console.warn(\n `Could not resolve ${location.packageName}@${location.versionSpec} — source-of-truth divergence check skipped for this run.`,\n error,\n );\n return null;\n }\n\n const cacheDir = join(location.cacheDir, version);\n const tag = `${location.packageName}@${version}`;\n const rawBase = `https://raw.githubusercontent.com/${GITHUB_REPO}/${tag}/${packageDirectory(location.packageName)}/test/golden`;\n\n let manifest: GoldenManifest;\n const cachedManifestPath = manifestPath(cacheDir);\n if (existsSync(cachedManifestPath)) {\n manifest = loadManifest(cacheDir);\n } else {\n let response: Response;\n try {\n response = await fetch(`${rawBase}/manifest.json`);\n } catch (error) {\n console.warn(\n `Could not reach GitHub to fetch ${tag}'s golden baseline — source-of-truth divergence check skipped for this run.`,\n error,\n );\n return null;\n }\n if (!response.ok) {\n console.warn(\n `No golden baseline published for ${tag} — source-of-truth divergence check skipped for this run.`,\n );\n return null;\n }\n const text = await response.text();\n const parsed = JSON.parse(text);\n validateGoldenManifest(parsed, `${rawBase}/manifest.json`);\n mkdirSync(cacheDir, { recursive: true });\n writeFileSync(cachedManifestPath, text);\n manifest = parsed;\n }\n\n return {\n manifest,\n async readImage(storyId) {\n const cachedImagePath = goldenImagePath(cacheDir, storyId);\n if (existsSync(cachedImagePath)) return readFileSync(cachedImagePath);\n const response = await fetch(`${rawBase}/${storyId}.png`);\n if (!response.ok) return null;\n const buffer = Buffer.from(await response.arrayBuffer());\n mkdirSync(cacheDir, { recursive: true });\n writeFileSync(cachedImagePath, buffer);\n return buffer;\n },\n };\n}\n","import { expect } from \"@playwright/test\";\nimport type { Browser, TestInfo } from \"@playwright/test\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport type { AdapterTesterConfig } from \"../config.js\";\nimport { diffPngBuffers } from \"../golden/diffPng.js\";\nimport {\n type GoldenManifestEntry,\n goldenImagePath,\n loadManifest,\n pruneOrphanedGoldens,\n saveGoldenImage,\n updateManifestEntry,\n} from \"../golden/manifestStore.js\";\nimport { resolveSourceOfTruthGolden } from \"../golden/resolveSourceOfTruthGolden.js\";\n\nconst DEFAULT_EXCLUDE_TITLE_PREFIXES = [\"Theme\", \"Tokens\", \"Introduction\"];\n\ninterface StorybookEntry {\n type: string;\n id: string;\n name: string;\n title: string;\n}\n\nfunction matchesPrefix(id: string, prefix: string): boolean {\n return id === prefix || id.startsWith(prefix);\n}\n\n/** Fetches every story `target`'s Storybook currently has, filtered only by\n * `excludeTitlePrefixes` — not `stories.<id>.exclude`, so callers can still\n * tell an excluded story apart from one that's genuinely missing. */\nasync function fetchStories(\n target: { name: string; url: string },\n excludeTitlePrefixes: string[],\n): Promise<StorybookEntry[]> {\n let stories: StorybookEntry[];\n try {\n const response = await fetch(`${target.url}/index.json`);\n if (!response.ok) {\n throw new Error(\n `Failed to fetch Storybook index: ${response.statusText}`,\n );\n }\n const data = (await response.json()) as any;\n const entries = data.entries || {};\n stories = Object.values(entries).filter(\n (entry: any) =>\n entry.type === \"story\" &&\n !excludeTitlePrefixes.some(\n (prefix) =>\n entry.title === prefix || entry.title.startsWith(`${prefix}/`),\n ),\n ) as StorybookEntry[];\n stories.sort((a, b) => a.id.localeCompare(b.id));\n } catch (error) {\n console.error(\n \"Failed to load Storybook index from\",\n `${target.url}/index.json`,\n error,\n );\n throw new Error(\n `Storybook target \"${target.name}\" is not responsive or index.json is missing. Please ensure its Storybook is running.`,\n );\n }\n return stories;\n}\n\n/** Resolves the diff threshold for `storyId`: the longest (most specific)\n * `storyThresholds` key matching by prefix, falling back to\n * `defaultThresholdPixels` when nothing matches. */\nfunction resolveThreshold(\n storyId: string,\n storyThresholds: Record<string, number>,\n defaultThresholdPixels: number,\n): number {\n let bestMatch: string | undefined;\n for (const prefix of Object.keys(storyThresholds)) {\n if (\n matchesPrefix(storyId, prefix) &&\n (!bestMatch || prefix.length > bestMatch.length)\n ) {\n bestMatch = prefix;\n }\n }\n return bestMatch !== undefined\n ? storyThresholds[bestMatch]!\n : defaultThresholdPixels;\n}\n\n/** Everything a generated Playwright spec needs to register the golden-image\n * suite itself. Split out from the actual `test.describe`/`test` calls so\n * those calls execute in the spec file that imports this, not in this\n * library file — otherwise Playwright's HTML report groups every story under\n * this file's own (sourcemapped) path instead of a stable spec name. */\nexport interface VisualRegressionPlan {\n /** `config`'s own (non-source-of-truth) target name, for the suite title. */\n ownTargetName: string;\n /** Suite title suffix describing which check mode is running. */\n suiteLabel: string;\n /** Stories to check, already filtered and sorted by id. */\n stories: StorybookEntry[];\n /** Story ids the source-of-truth adapter has a golden for but this\n * project's own Storybook doesn't — empty outside `checkMode: \"divergence\"`\n * or when `sourceOfTruthGolden` didn't resolve. Excludes ids covered by a\n * `stories.<id>.exclude` entry: an intentional gap, not a sync failure. */\n missingFromSourceOfTruth: string[];\n /** Golden-checks one story. Call this from inside a `test(story.id, ...)`\n * body — safe to run concurrently across Playwright workers, since each\n * call only ever reads/writes its own story's manifest entry (locked at\n * the point it writes it back, so concurrent workers never race each\n * other's entries — see `updateManifestEntry`). */\n checkStory: (\n story: StorybookEntry,\n browser: Browser,\n testInfo: TestInfo,\n ) => Promise<void>;\n}\n\n/** Everything a generated Playwright spec needs to register the golden-image\n * suite itself. Split out from the actual `test.describe`/`test` calls so\n * those calls execute in the spec file that imports this, not in this\n * library file — otherwise Playwright's HTML report groups every story under\n * this file's own (sourcemapped) path instead of a stable spec name. */\nexport interface VisualRegressionPlan {\n /** `config`'s own (non-source-of-truth) target name, for the suite title. */\n ownTargetName: string;\n /** Suite title suffix describing which check mode is running. */\n suiteLabel: string;\n /** Stories to check, already filtered and sorted by id. */\n stories: StorybookEntry[];\n /** Golden-checks one story. Call this from inside a `test(story.id, ...)`\n * body — safe to run concurrently across Playwright workers, since each\n * call only ever reads/writes its own story's manifest entry (locked at\n * the point it writes it back, so concurrent workers never race each\n * other's entries — see `updateManifestEntry`). */\n checkStory: (\n story: StorybookEntry,\n browser: Browser,\n testInfo: TestInfo,\n ) => Promise<void>;\n}\n\n/**\n * Resolves the golden-image plan for `config`'s own target (the one target\n * in `config.targets` not marked `sourceOfTruth`).\n *\n * Two independent checks per story, gated by `config.checkMode`, neither of\n * which boots the source-of-truth adapter's own Storybook — the divergence\n * check below compares stored golden files, not live pages:\n *\n * 1. **Own-drift (`checkMode: \"own\"`, the default; hard fail):** this run's\n * live render vs this project's own stored `test/golden/<story-id>.png`.\n * No golden yet for a story is not a failure — one is captured from this\n * run instead (same as `--update-golden`, scoped to just that story), in\n * either mode.\n * 2. **Source-of-truth divergence (`checkMode: \"divergence\"`; hard fail):**\n * this project's own golden vs the source-of-truth's golden (`config`'s\n * `sourceOfTruthGolden`). Skipped entirely when\n * `config.isSourceOfTruthAdapter` is true — the\n * source-of-truth adapter has nothing above it to diverge from — and\n * skipped per-story when neither side has a baseline yet. A\n * once-flagged divergence stays quiet after `--approve-divergence`,\n * until the source of truth's own golden changes again.\n */\nexport async function resolveVisualRegressionPlan(\n config: AdapterTesterConfig,\n): Promise<VisualRegressionPlan> {\n const ownTarget = config.isSourceOfTruthAdapter\n ? config.targets[0]\n : config.targets.find((target) => !target.sourceOfTruth);\n if (!ownTarget) {\n throw new Error(\n \"adapter-tester config has no non-sourceOfTruth target to run the golden check against.\",\n );\n }\n const excludeTitlePrefixes =\n config.excludeTitlePrefixes ?? DEFAULT_EXCLUDE_TITLE_PREFIXES;\n const storyOverrides = config.stories ?? {};\n const excludeStoryIds = Object.keys(storyOverrides).filter(\n (id) => storyOverrides[id]!.exclude,\n );\n const goldenStoryThresholds = Object.fromEntries(\n Object.entries(storyOverrides)\n .filter(([, override]) => override.goldenThreshold !== undefined)\n .map(([id, override]) => [id, override.goldenThreshold!]),\n );\n const sourceOfTruthStoryThresholds = Object.fromEntries(\n Object.entries(storyOverrides)\n .filter(([, override]) => override.sourceOfTruthThreshold !== undefined)\n .map(([id, override]) => [id, override.sourceOfTruthThreshold!]),\n );\n const goldenDir = config.goldenDir;\n const goldenMode = config.goldenMode;\n const checkMode = config.checkMode;\n\n // Fetched with only excludeTitlePrefixes applied — not excludeStoryIds —\n // so the source-of-truth story-parity check below can tell an excluded\n // story apart from one that's genuinely missing from this Storybook.\n const ownStories = await fetchStories(ownTarget, excludeTitlePrefixes);\n const stories = ownStories.filter(\n (entry) =>\n !excludeStoryIds.some((prefix) => matchesPrefix(entry.id, prefix)),\n );\n\n // `--update-golden` redefines this project's own baseline, so it's also\n // the point a renamed/removed story's now-orphaned golden gets cleaned up\n // — otherwise nothing ever prunes it, since a run only ever adds/updates\n // entries for stories it actually saw in this pass. Uses the full current\n // story list (not narrowed by any `--grep` Playwright itself applies), so\n // this catches every orphan regardless of how the run is scoped.\n if (goldenMode === \"update-golden\") {\n const prunedStoryIds = await pruneOrphanedGoldens(\n goldenDir,\n new Set(stories.map((story) => story.id)),\n );\n if (prunedStoryIds.length > 0) {\n console.warn(\n `Pruned ${prunedStoryIds.length} orphaned golden(s) no longer in Storybook: ${prunedStoryIds.join(\", \")}`,\n );\n }\n }\n\n const sourceOfTruthGolden =\n checkMode !== \"divergence\" ||\n config.isSourceOfTruthAdapter ||\n !config.sourceOfTruthGolden\n ? null\n : await resolveSourceOfTruthGolden(config.sourceOfTruthGolden);\n\n // Checked against `ownStories` (title-prefix-excluded only), not `stories`\n // — a story marked `exclude: true` still counts as \"present\", it's just\n // not diffed. Only a story the source of truth has that this adapter\n // never built at all, and hasn't acknowledged via `exclude`, is missing.\n const ownStoryIds = new Set(ownStories.map((entry) => entry.id));\n const missingFromSourceOfTruth = sourceOfTruthGolden\n ? Object.keys(sourceOfTruthGolden.manifest)\n .filter(\n (id) =>\n !ownStoryIds.has(id) &&\n !excludeStoryIds.some((prefix) => matchesPrefix(id, prefix)),\n )\n .sort()\n : [];\n\n if (missingFromSourceOfTruth.length > 0) {\n console.error(\n `[adapter-tester] ${missingFromSourceOfTruth.length} stor(y/ies) exist in the source of truth but are missing here: ${missingFromSourceOfTruth.join(\", \")}. Add the missing story, or mark it \\`exclude: true\\` under \\`stories\\` in adapter-tester.config.json if intentional.`,\n );\n }\n\n console.log(\n [\n `[adapter-tester] target: \"${ownTarget.name}\" (${ownTarget.url})`,\n `[adapter-tester] checkMode: \"${checkMode}\" (${checkMode === \"divergence\" ? \"this project's golden vs source-of-truth's golden\" : \"live render vs this project's own golden\"})`,\n `[adapter-tester] goldenMode: \"${goldenMode}\"`,\n `[adapter-tester] goldenThresholdPixels: ${config.goldenThresholdPixels}`,\n `[adapter-tester] sourceOfTruthThresholdPixels: ${config.sourceOfTruthThresholdPixels}`,\n checkMode === \"divergence\"\n ? config.isSourceOfTruthAdapter\n ? `[adapter-tester] sourceOfTruthGolden: skipped — this is the source-of-truth adapter, nothing to diverge from`\n : !config.sourceOfTruthGolden\n ? `[adapter-tester] sourceOfTruthGolden: skipped — no sourceOfTruthGolden configured`\n : sourceOfTruthGolden\n ? `[adapter-tester] sourceOfTruthGolden: resolved, ${Object.keys(sourceOfTruthGolden.manifest).length} golden(s) available (config: ${JSON.stringify(config.sourceOfTruthGolden)})`\n : `[adapter-tester] sourceOfTruthGolden: unavailable — no baseline found or unreachable (config: ${JSON.stringify(config.sourceOfTruthGolden)}); divergence check will skip every story`\n : `[adapter-tester] sourceOfTruthGolden: not used in \"own\" checkMode`,\n `[adapter-tester] stories: ${stories.length} to check (excluded: ${excludeStoryIds.length}, title prefixes excluded: ${excludeTitlePrefixes.join(\", \") || \"none\"})`,\n `[adapter-tester] story parity with source of truth: ${missingFromSourceOfTruth.length === 0 ? \"OK\" : `${missingFromSourceOfTruth.length} missing (see error above)`}`,\n ].join(\"\\n\"),\n );\n\n const suiteLabel =\n checkMode === \"divergence\"\n ? \"Source-of-Truth Divergence Check\"\n : \"Own-Drift Golden Image Check\";\n\n return {\n ownTargetName: ownTarget.name,\n suiteLabel,\n stories,\n missingFromSourceOfTruth,\n checkStory: async (story, browser, testInfo) => {\n const page = await browser.newPage();\n await page.setViewportSize({ width: 800, height: 600 });\n await page.goto(\n `${ownTarget.url}/iframe.html?id=${story.id}&viewMode=story`,\n { waitUntil: \"networkidle\" },\n );\n await page.waitForSelector(\"#storybook-root\");\n await page.addStyleTag({\n content: `* { -webkit-font-smoothing: antialiased !important; -moz-osx-font-smoothing: grayscale !important; }`,\n });\n await page.waitForTimeout(300);\n const liveBuffer = await page.screenshot();\n\n const imagePath = goldenImagePath(goldenDir, story.id);\n // Only this worker ever touches this story's key, so reading it here\n // (outside the lock `updateManifestEntry` takes at the end) can't\n // race another worker — they're all reading/writing different keys.\n const entry = loadManifest(goldenDir)[story.id];\n const capturingNewGolden =\n goldenMode !== \"check\" || !entry || !existsSync(imagePath);\n\n let currentEntry: GoldenManifestEntry;\n if (capturingNewGolden) {\n saveGoldenImage(goldenDir, story.id, liveBuffer);\n currentEntry = entry?.sourceOfTruthCreatedAt\n ? {\n createdAt: new Date().toISOString(),\n sourceOfTruthCreatedAt: entry.sourceOfTruthCreatedAt,\n }\n : { createdAt: new Date().toISOString() };\n if (!entry) {\n testInfo.annotations.push({\n type: \"golden-created\",\n description: `No golden existed yet for \"${story.id}\" — captured one from this run.`,\n });\n }\n } else {\n currentEntry = entry;\n if (checkMode === \"own\") {\n const goldenBuffer = readFileSync(imagePath);\n const { diffPixels, diffImage } = diffPngBuffers(\n liveBuffer,\n goldenBuffer,\n );\n const threshold = resolveThreshold(\n story.id,\n goldenStoryThresholds,\n config.goldenThresholdPixels,\n );\n if (diffPixels >= threshold) {\n await testInfo.attach(\"expected\", {\n body: goldenBuffer,\n contentType: \"image/png\",\n });\n await testInfo.attach(\"actual\", {\n body: liveBuffer,\n contentType: \"image/png\",\n });\n if (diffImage) {\n await testInfo.attach(\"diff\", {\n body: diffImage,\n contentType: \"image/png\",\n });\n }\n }\n expect\n .soft(\n diffPixels,\n `\"${story.id}\" has drifted from its own golden image (${diffPixels} mismatched pixels, threshold ${threshold})`,\n )\n .toBeLessThan(threshold);\n }\n }\n\n if (sourceOfTruthGolden) {\n const sourceOfTruthEntry = sourceOfTruthGolden.manifest[story.id];\n const sourceOfTruthImage = sourceOfTruthEntry\n ? await sourceOfTruthGolden.readImage(story.id)\n : null;\n\n if (sourceOfTruthEntry && sourceOfTruthImage) {\n if (goldenMode === \"approve-divergence\") {\n currentEntry = {\n ...currentEntry,\n sourceOfTruthCreatedAt: sourceOfTruthEntry.createdAt,\n };\n } else {\n const ownImage = readFileSync(imagePath);\n const { diffPixels, diffImage } = diffPngBuffers(\n ownImage,\n sourceOfTruthImage,\n );\n const threshold = resolveThreshold(\n story.id,\n sourceOfTruthStoryThresholds,\n config.sourceOfTruthThresholdPixels,\n );\n const approvedAt = currentEntry.sourceOfTruthCreatedAt;\n const isKnownDivergence =\n diffPixels < threshold ||\n (approvedAt !== undefined &&\n approvedAt >= sourceOfTruthEntry.createdAt);\n if (!isKnownDivergence) {\n await testInfo.attach(\"expected\", {\n body: sourceOfTruthImage,\n contentType: \"image/png\",\n });\n await testInfo.attach(\"actual\", {\n body: ownImage,\n contentType: \"image/png\",\n });\n if (diffImage) {\n await testInfo.attach(\"diff\", {\n body: diffImage,\n contentType: \"image/png\",\n });\n }\n testInfo.annotations.push({\n type: \"source-of-truth-divergence\",\n description: `\"${story.id}\" differs from the source of truth's golden by ${diffPixels} mismatched pixels (threshold ${threshold}) and hasn't been reviewed — run with --approve-divergence if this is intentional, or fix the adapter styling.`,\n });\n }\n expect\n .soft(\n isKnownDivergence,\n `\"${story.id}\" differs from the source of truth's golden by ${diffPixels} mismatched pixels (threshold ${threshold}) and hasn't been reviewed — run with --approve-divergence if this is intentional, or fix the adapter styling.`,\n )\n .toBe(true);\n }\n }\n }\n\n // Locked read-modify-write of just this story's entry — see\n // `updateManifestEntry` for why that's enough to make this safe\n // across concurrent Playwright workers.\n await updateManifestEntry(goldenDir, story.id, () => currentEntry);\n },\n };\n}\n"],"names":["diffPngBuffers","a","b","imgA","PNG","imgB","diff","pixelmatch","addFormats","ajvFormatsModule.default","ajv","Ajv","validate","schema","validateGoldenManifest","data","path","errors","error","extra","_a","manifestPath","goldenDir","join","manifestLockPath","goldenImagePath","storyId","loadManifest","existsSync","readFileSync","saveManifest","manifest","sorted","key","mkdirSync","tmpPath","writeFileSync","renameSync","LOCK_RETRY_MS","LOCK_TIMEOUT_MS","sleep","ms","resolve","acquireManifestLock","deadline","closeSync","openSync","releaseManifestLock","rmSync","updateManifestEntry","updater","nextEntry","saveGoldenImage","buffer","pruneOrphanedGoldens","currentStoryIds","orphanIds","id","imagePath","unlinkSync","GITHUB_REPO","resolveNpmVersion","packageName","versionSpec","response","resolved","_b","packageDirectory","resolveSourceOfTruthGolden","location","version","cacheDir","tag","rawBase","cachedManifestPath","text","parsed","cachedImagePath","DEFAULT_EXCLUDE_TITLE_PREFIXES","matchesPrefix","prefix","fetchStories","target","excludeTitlePrefixes","stories","entries","entry","resolveThreshold","storyThresholds","defaultThresholdPixels","bestMatch","resolveVisualRegressionPlan","config","ownTarget","storyOverrides","excludeStoryIds","goldenStoryThresholds","override","sourceOfTruthStoryThresholds","goldenMode","checkMode","ownStories","prunedStoryIds","story","sourceOfTruthGolden","ownStoryIds","missingFromSourceOfTruth","suiteLabel","browser","testInfo","page","liveBuffer","capturingNewGolden","currentEntry","goldenBuffer","diffPixels","diffImage","threshold","expect","sourceOfTruthEntry","sourceOfTruthImage","ownImage","approvedAt","isKnownDivergence"],"mappings":"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"}
|
package/package.json
CHANGED
package/report-header.txt
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
*You are an expert AI frontend developer. Below is a report of visual discrepancies between the Source of Truth (`@recursica/mantine-adapter` running in Storybook) and the target implementation (`@recursica/mui-adapter` running in Storybook).*
|
|
4
4
|
|
|
5
|
-
*Each section below represents a specific component story and its active control arguments. The section header provides the human-readable component name along with the exact Storybook URL configuration string. Use this context to apply the necessary CSS/TypeScript fixes to the
|
|
5
|
+
*Each section below represents a specific component story and its active control arguments. The section header provides the human-readable component name along with the exact Storybook URL configuration string. Use this context to apply the necessary CSS/TypeScript fixes to the current adapter to perfectly match the Mantine Source of Truth.*
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -133,6 +133,10 @@
|
|
|
133
133
|
"type": "array",
|
|
134
134
|
"items": { "type": "string" },
|
|
135
135
|
"description": "Storybook title prefixes to exclude from comparison, in addition to the built-in denylist (`Theme/*`, `Tokens/*`, `Introduction`)."
|
|
136
|
+
},
|
|
137
|
+
"reportHeader": {
|
|
138
|
+
"type": "string",
|
|
139
|
+
"description": "Overrides the AI report header text shown in Dev Mode's \"Full Report\" export. Defaults to the contents of `report-header.txt`."
|
|
136
140
|
}
|
|
137
141
|
}
|
|
138
142
|
}
|