@recursica/adapter-tester 5.1.3 → 5.2.0

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/dist/testing.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { expect as D } from "@playwright/test";
2
- import { existsSync as y, unlinkSync as Y, readFileSync as E, mkdirSync as b, writeFileSync as A, closeSync as Q, openSync as Z, renameSync as ee, rmSync as te } from "node:fs";
2
+ import { existsSync as y, unlinkSync as Y, readFileSync as E, mkdirSync as k, writeFileSync as A, closeSync as Q, openSync as Z, renameSync as ee, rmSync as te } from "node:fs";
3
3
  import re from "pixelmatch";
4
4
  import { PNG as j } from "pngjs";
5
5
  import { join as M } from "node:path";
@@ -28,7 +28,7 @@ const se = "http://json-schema.org/draft-07/schema#", ie = "https://github.com/b
28
28
  }, fe = ne, q = new oe.Ajv({ allErrors: !0, strict: !0 });
29
29
  fe(q);
30
30
  const U = q.compile(ue);
31
- function L(e, t) {
31
+ function N(e, t) {
32
32
  if (U(e)) return;
33
33
  const o = (U.errors ?? []).map((r) => {
34
34
  var a;
@@ -52,15 +52,15 @@ function P(e) {
52
52
  const t = G(e);
53
53
  if (!y(t)) return {};
54
54
  const o = JSON.parse(E(t, "utf8"));
55
- return L(o, t), o;
55
+ return N(o, t), o;
56
56
  }
57
57
  function le(e, t) {
58
58
  const o = G(e);
59
- L(t, o);
59
+ N(t, o);
60
60
  const r = {};
61
61
  for (const a of Object.keys(t).sort())
62
62
  r[a] = t[a];
63
- b(e, { recursive: !0 });
63
+ k(e, { recursive: !0 });
64
64
  const s = `${o}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
65
65
  A(s, JSON.stringify(r, null, 2) + `
66
66
  `), ee(s, o);
@@ -70,7 +70,7 @@ function ge(e) {
70
70
  return new Promise((t) => setTimeout(t, e));
71
71
  }
72
72
  async function we(e) {
73
- b(e, { recursive: !0 });
73
+ k(e, { recursive: !0 });
74
74
  const t = V(e), o = Date.now() + me;
75
75
  for (; ; )
76
76
  try {
@@ -98,7 +98,7 @@ async function W(e, t, o) {
98
98
  }
99
99
  }
100
100
  function ve(e, t, o) {
101
- b(e, { recursive: !0 }), A(S(e, t), o);
101
+ k(e, { recursive: !0 }), A(S(e, t), o);
102
102
  }
103
103
  async function Te(e, t) {
104
104
  const o = P(e), r = Object.keys(o).filter(
@@ -152,7 +152,7 @@ async function Oe(e) {
152
152
  { cause: i }
153
153
  );
154
154
  }
155
- const o = M(e.cacheDir, t), r = `${e.packageName}@${t}`, s = `https://raw.githubusercontent.com/${J}/${r}/test/golden`;
155
+ const o = M(e.cacheDir, t), r = `v${t}`, s = `https://raw.githubusercontent.com/${J}/${r}/test/golden`;
156
156
  let a;
157
157
  const d = G(o);
158
158
  if (y(d))
@@ -172,7 +172,7 @@ async function Oe(e) {
172
172
  `No golden baseline published for tag "${r}" in ${J} (${s}/manifest.json returned ${i.status}) — the source-of-truth divergence check has nothing to compare against. Confirm that tag exists and its release actually published test/golden/.`
173
173
  );
174
174
  const h = await i.text(), c = JSON.parse(h);
175
- L(c, `${s}/manifest.json`), b(o, { recursive: !0 }), A(d, h), a = c;
175
+ N(c, `${s}/manifest.json`), k(o, { recursive: !0 }), A(d, h), a = c;
176
176
  }
177
177
  return {
178
178
  manifest: a,
@@ -182,15 +182,15 @@ async function Oe(e) {
182
182
  const c = await fetch(`${s}/${i}.png`);
183
183
  if (!c.ok) return null;
184
184
  const m = Buffer.from(await c.arrayBuffer());
185
- return b(o, { recursive: !0 }), A(h, m), m;
185
+ return k(o, { recursive: !0 }), A(h, m), m;
186
186
  }
187
187
  };
188
188
  }
189
189
  const xe = ["Theme", "Tokens", "Introduction"];
190
- function N(e, t) {
190
+ function L(e, t) {
191
191
  return e === t || e.startsWith(t);
192
192
  }
193
- async function ke(e, t) {
193
+ async function be(e, t) {
194
194
  let o;
195
195
  try {
196
196
  const r = await fetch(`${e.url}/index.json`);
@@ -218,7 +218,7 @@ async function ke(e, t) {
218
218
  function K(e, t, o) {
219
219
  let r;
220
220
  for (const s of Object.keys(t))
221
- N(e, s) && (!r || s.length > r.length) && (r = s);
221
+ L(e, s) && (!r || s.length > r.length) && (r = s);
222
222
  return r !== void 0 ? t[r] : o;
223
223
  }
224
224
  async function Ge(e) {
@@ -233,8 +233,8 @@ async function Ge(e) {
233
233
  Object.entries(r).filter(([, n]) => n.goldenThreshold !== void 0).map(([n, u]) => [n, u.goldenThreshold])
234
234
  ), d = Object.fromEntries(
235
235
  Object.entries(r).filter(([, n]) => n.sourceOfTruthThreshold !== void 0).map(([n, u]) => [n, u.sourceOfTruthThreshold])
236
- ), i = e.goldenDir, h = e.goldenMode, c = e.checkMode, m = await ke(t, o), C = m.filter(
237
- (n) => !s.some((u) => N(n.id, u))
236
+ ), i = e.goldenDir, h = e.goldenMode, c = e.checkMode, m = await be(t, o), C = m.filter(
237
+ (n) => !s.some((u) => L(n.id, u))
238
238
  );
239
239
  if (h === "update-golden") {
240
240
  const n = await Te(
@@ -246,7 +246,7 @@ async function Ge(e) {
246
246
  );
247
247
  }
248
248
  const v = c !== "divergence" || e.isSourceOfTruthAdapter || !e.sourceOfTruthGolden ? null : await Oe(e.sourceOfTruthGolden), X = new Set(m.map((n) => n.id)), T = v ? Object.keys(v.manifest).filter(
249
- (n) => !X.has(n) && !s.some((u) => N(n, u))
249
+ (n) => !X.has(n) && !s.some((u) => L(n, u))
250
250
  ).sort() : [];
251
251
  T.length > 0 && console.error(
252
252
  `[adapter-tester] ${T.length} stor(y/ies) exist in the source of truth but are missing here: ${T.join(", ")}. Add the missing story, or mark it \`exclude: true\` under \`stories\` in adapter-tester.config.json if intentional.`
@@ -298,10 +298,10 @@ async function Ge(e) {
298
298
  )
299
299
  ).then(() => document.fonts.ready)
300
300
  ), await g.waitForTimeout(300);
301
- const k = await g.screenshot(), B = S(i, n.id), w = P(i)[n.id], H = h !== "check" || !w || !y(B);
301
+ const b = await g.screenshot(), B = S(i, n.id), w = P(i)[n.id], H = h !== "check" || !w || !y(B);
302
302
  let $;
303
303
  if (H)
304
- ve(i, n.id, k), $ = w != null && w.sourceOfTruthCreatedAt ? {
304
+ ve(i, n.id, b), $ = w != null && w.sourceOfTruthCreatedAt ? {
305
305
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
306
306
  sourceOfTruthCreatedAt: w.sourceOfTruthCreatedAt
307
307
  } : { createdAt: (/* @__PURE__ */ new Date()).toISOString() }, w || p.annotations.push({
@@ -310,7 +310,7 @@ async function Ge(e) {
310
310
  });
311
311
  else if ($ = w, c === "own") {
312
312
  const f = E(B), { diffPixels: l, diffImage: O } = R(
313
- k,
313
+ b,
314
314
  f
315
315
  ), x = K(
316
316
  n.id,
@@ -321,7 +321,7 @@ async function Ge(e) {
321
321
  body: f,
322
322
  contentType: "image/png"
323
323
  }), await p.attach("actual", {
324
- body: k,
324
+ body: b,
325
325
  contentType: "image/png"
326
326
  }), O && await p.attach("diff", {
327
327
  body: O,
@@ -341,7 +341,7 @@ async function Ge(e) {
341
341
  };
342
342
  else {
343
343
  const { diffPixels: O, diffImage: x } = R(
344
- k,
344
+ b,
345
345
  l
346
346
  ), I = K(
347
347
  n.id,
@@ -352,7 +352,7 @@ async function Ge(e) {
352
352
  body: l,
353
353
  contentType: "image/png"
354
354
  }), await p.attach("actual", {
355
- body: k,
355
+ body: b,
356
356
  contentType: "image/png"
357
357
  }), x && await p.attach("diff", {
358
358
  body: x,
@@ -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\n// The source-of-truth adapter (Mantine) lives in its own standalone repo, not\n// this monorepo — its golden images are fetched from here, not from borderux/recursica.\nconst GITHUB_REPO = \"borderux/recursica-adapter-mantine-v8\";\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/**\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 * Throws when no golden baseline exists yet for this version, or the\n * registry/repo is unreachable — a divergence check that can't reach its\n * baseline must fail loudly, not degrade to a silent skip that Playwright\n * then reports as a pass.\n */\nexport async function resolveSourceOfTruthGolden(\n location: SourceOfTruthGoldenLocation,\n): Promise<SourceOfTruthGolden> {\n if (location.type === \"local\") {\n if (!existsSync(manifestPath(location.dir))) {\n throw new Error(\n `No golden baseline found at ${location.dir} — the source-of-truth divergence check has nothing to compare against. Capture one there first (--update-golden), or fix sourceOfTruth.dir if it's pointing at the wrong checkout.`,\n );\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 throw new Error(\n `Could not resolve ${location.packageName}@${location.versionSpec} on the npm registry — the source-of-truth divergence check has nothing to compare against.`,\n { cause: error },\n );\n }\n\n const cacheDir = join(location.cacheDir, version);\n const tag = `${location.packageName}@${version}`;\n // @recursica/adapter-mantine-v8 is a standalone single-package repo (unlike\n // the old @recursica/mantine-adapter, which lived at `packages/<name>`\n // inside this monorepo) — its `test/golden/` sits directly at the repo root.\n const rawBase = `https://raw.githubusercontent.com/${GITHUB_REPO}/${tag}/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 throw new Error(\n `Could not reach GitHub to fetch ${tag}'s golden baseline (${rawBase}/manifest.json) — the source-of-truth divergence check has nothing to compare against.`,\n { cause: error },\n );\n }\n if (!response.ok) {\n throw new Error(\n `No golden baseline published for tag \"${tag}\" in ${GITHUB_REPO} (${rawBase}/manifest.json returned ${response.status}) — the source-of-truth divergence check has nothing to compare against. Confirm that tag exists and its release actually published test/golden/.`,\n );\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\n * boots the source-of-truth adapter's own Storybook — its side of both\n * checks is always a stored golden file, never a live page:\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 run's live render vs the source-of-truth's stored golden (`config`'s\n * `sourceOfTruthGolden`) — always fresh, no `--update-golden` step needed\n * first. Skipped entirely when `config.isSourceOfTruthAdapter` is true —\n * the source-of-truth adapter has nothing above it to diverge from — and\n * skipped per-story when the source of truth has no baseline for it yet.\n * A 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\" ? \"live render 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 : // Unresolvable-baseline case never reaches here — resolveSourceOfTruthGolden\n // throws instead of returning null, failing the whole run before this\n // logs, rather than letting the divergence check silently no-op.\n `[adapter-tester] sourceOfTruthGolden: resolved, ${Object.keys(sourceOfTruthGolden!.manifest).length} golden(s) available (config: ${JSON.stringify(config.sourceOfTruthGolden)})`\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 // Web fonts (e.g. `withRecursicaFonts`'s Google Fonts `<link>`s, injected\n // from a React effect after this page's `networkidle` already fired) can\n // still be mid-download here. Wait for any `<link>` stylesheet that's\n // still loading to settle — so the browser has parsed its `@font-face`\n // rules and started the actual font-file fetches `document.fonts.ready`\n // below needs to know about — then for the fonts themselves. Without\n // this, the very first golden capture of a newly-introduced font family\n // is a coin flip between the real font and its fallback, depending on\n // how fast the network happens to be.\n await page.evaluate(() =>\n Promise.all(\n Array.from(\n document.querySelectorAll<HTMLLinkElement>(\n 'link[rel=\"stylesheet\"]',\n ),\n ).map((link) =>\n link.sheet\n ? Promise.resolve()\n : new Promise<void>((resolve) => {\n link.addEventListener(\"load\", () => resolve(), {\n once: true,\n });\n link.addEventListener(\"error\", () => resolve(), {\n once: true,\n });\n }),\n ),\n ).then(() => document.fonts.ready),\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 // Always this run's live render, never the stored own-drift\n // golden — divergence should reflect what's on screen right now,\n // not whatever was last captured via --update-golden.\n const { diffPixels, diffImage } = diffPngBuffers(\n liveBuffer,\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: liveBuffer,\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","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","link","liveBuffer","capturingNewGolden","currentEntry","goldenBuffer","diffPixels","diffImage","threshold","expect","sourceOfTruthEntry","sourceOfTruthImage","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\n// The source-of-truth adapter (Mantine) lives in its own standalone repo, not\n// this monorepo — its golden images are fetched from here, not from borderux/recursica.\nconst GITHUB_REPO = \"borderux/recursica-adapter-mantine-v8\";\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/**\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 * Throws when no golden baseline exists yet for this version, or the\n * registry/repo is unreachable — a divergence check that can't reach its\n * baseline must fail loudly, not degrade to a silent skip that Playwright\n * then reports as a pass.\n */\nexport async function resolveSourceOfTruthGolden(\n location: SourceOfTruthGoldenLocation,\n): Promise<SourceOfTruthGolden> {\n if (location.type === \"local\") {\n if (!existsSync(manifestPath(location.dir))) {\n throw new Error(\n `No golden baseline found at ${location.dir} — the source-of-truth divergence check has nothing to compare against. Capture one there first (--update-golden), or fix sourceOfTruth.dir if it's pointing at the wrong checkout.`,\n );\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 throw new Error(\n `Could not resolve ${location.packageName}@${location.versionSpec} on the npm registry — the source-of-truth divergence check has nothing to compare against.`,\n { cause: error },\n );\n }\n\n const cacheDir = join(location.cacheDir, version);\n // @recursica/adapter-mantine-v8 is a standalone single-package repo (unlike\n // the old @recursica/mantine-adapter, which lived at `packages/<name>`\n // inside this monorepo) — its `test/golden/` sits directly at the repo root.\n // changesets/action tags single-package repos as `v<version>`, not\n // `<packageName>@<version>` (that longer form is only its default for\n // multi-package/monorepo releases) — this repo's release.yml confirms\n // `v<version>` is what actually gets tagged and pushed on every release.\n const tag = `v${version}`;\n const rawBase = `https://raw.githubusercontent.com/${GITHUB_REPO}/${tag}/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 throw new Error(\n `Could not reach GitHub to fetch ${tag}'s golden baseline (${rawBase}/manifest.json) — the source-of-truth divergence check has nothing to compare against.`,\n { cause: error },\n );\n }\n if (!response.ok) {\n throw new Error(\n `No golden baseline published for tag \"${tag}\" in ${GITHUB_REPO} (${rawBase}/manifest.json returned ${response.status}) — the source-of-truth divergence check has nothing to compare against. Confirm that tag exists and its release actually published test/golden/.`,\n );\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\n * boots the source-of-truth adapter's own Storybook — its side of both\n * checks is always a stored golden file, never a live page:\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 run's live render vs the source-of-truth's stored golden (`config`'s\n * `sourceOfTruthGolden`) — always fresh, no `--update-golden` step needed\n * first. Skipped entirely when `config.isSourceOfTruthAdapter` is true —\n * the source-of-truth adapter has nothing above it to diverge from — and\n * skipped per-story when the source of truth has no baseline for it yet.\n * A 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\" ? \"live render 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 : // Unresolvable-baseline case never reaches here — resolveSourceOfTruthGolden\n // throws instead of returning null, failing the whole run before this\n // logs, rather than letting the divergence check silently no-op.\n `[adapter-tester] sourceOfTruthGolden: resolved, ${Object.keys(sourceOfTruthGolden!.manifest).length} golden(s) available (config: ${JSON.stringify(config.sourceOfTruthGolden)})`\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 // Web fonts (e.g. `withRecursicaFonts`'s Google Fonts `<link>`s, injected\n // from a React effect after this page's `networkidle` already fired) can\n // still be mid-download here. Wait for any `<link>` stylesheet that's\n // still loading to settle — so the browser has parsed its `@font-face`\n // rules and started the actual font-file fetches `document.fonts.ready`\n // below needs to know about — then for the fonts themselves. Without\n // this, the very first golden capture of a newly-introduced font family\n // is a coin flip between the real font and its fallback, depending on\n // how fast the network happens to be.\n await page.evaluate(() =>\n Promise.all(\n Array.from(\n document.querySelectorAll<HTMLLinkElement>(\n 'link[rel=\"stylesheet\"]',\n ),\n ).map((link) =>\n link.sheet\n ? Promise.resolve()\n : new Promise<void>((resolve) => {\n link.addEventListener(\"load\", () => resolve(), {\n once: true,\n });\n link.addEventListener(\"error\", () => resolve(), {\n once: true,\n });\n }),\n ),\n ).then(() => document.fonts.ready),\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 // Always this run's live render, never the stored own-drift\n // golden — divergence should reflect what's on screen right now,\n // not whatever was last captured via --update-golden.\n const { diffPixels, diffImage } = diffPngBuffers(\n liveBuffer,\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: liveBuffer,\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","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","link","liveBuffer","capturingNewGolden","currentEntry","goldenBuffer","diffPixels","diffImage","threshold","expect","sourceOfTruthEntry","sourceOfTruthImage","approvedAt","isKnownDivergence"],"mappings":"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"}
package/package.json CHANGED
@@ -13,7 +13,7 @@
13
13
  "url": "git+https://github.com/borderux/recursica.git",
14
14
  "directory": "packages/adapter-tester"
15
15
  },
16
- "version": "5.1.3",
16
+ "version": "5.2.0",
17
17
  "type": "module",
18
18
  "main": "./dist/index.cjs",
19
19
  "module": "./dist/index.js",
package/report-header.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # UI Component Discrepancy Report
2
2
 
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).*
3
+ *You are an expert AI frontend developer. Below is a report of visual discrepancies between the Source of Truth (`@recursica/adapter-mantine-v8` running in Storybook) and the target implementation (`@recursica/adapter-mui-v7` running in Storybook).*
4
4
 
5
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
 
@@ -57,6 +57,11 @@
57
57
  "storybookTemplateVersion": {
58
58
  "type": "string",
59
59
  "description": "npm version/tag of @recursica/storybook-template to install in the harness. Defaults to `latest`."
60
+ },
61
+ "tokensSource": {
62
+ "type": "string",
63
+ "enum": ["target", "mantine-package"],
64
+ "description": "Which Recursica token snapshot the harness's Mantine components render with. \"target\" (default): this project's own committed recursica_variables_scoped.css/recursica_tokens.json/recursica_brand.json/recursica_ui-kit.json — makes the divergence check mean exactly \"given the same tokens this project is using, does it render the same as Mantine's own components,\" isolating adapter-mapping correctness from any token-snapshot drift between repos. \"mantine-package\": @recursica/adapter-mantine-v8's own bundled token files instead (whatever it was last published with) — also flags, as a side effect, a project whose own committed tokens have gone stale relative to Mantine's. Never the deprecated @recursica/official-release package."
60
65
  }
61
66
  }
62
67
  },
@@ -91,7 +96,7 @@
91
96
  },
92
97
  "isSourceOfTruthAdapter": {
93
98
  "type": "boolean",
94
- "description": "True only for the source-of-truth adapter's own config (mantine-adapter). Skips `sourceOfTruth` entirely — there's nothing above it to diverge from — and runs the own-drift golden check standalone, against just this project's own Storybook. Defaults to false."
99
+ "description": "True only for the source-of-truth adapter's own config (@recursica/adapter-mantine-v8). Skips `sourceOfTruth` entirely — there's nothing above it to diverge from — and runs the own-drift golden check standalone, against just this project's own Storybook. Defaults to false."
95
100
  },
96
101
  "goldenThresholdPixels": {
97
102
  "type": "number",
@@ -1 +0,0 @@
1
- {"version":3,"file":"mantineSourceOfTruth-BDJ5mfpd.js","sources":["../src/harness/mantineSourceOfTruth.ts"],"sourcesContent":["import { mkdirSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\n/**\n * Generates a small, throwaway Storybook project that installs\n * `@recursica/adapter-mantine-v8` as a real npm dependency (not a workspace\n * link) and boots a real Storybook from its published `src/**\\/*.stories.tsx`\n * files, using `@recursica/storybook-template`'s exported factories.\n *\n * This lets any repo — including ones that never checked out the Recursica\n * monorepo — run adapter-tester's visual regression suite against Mantine\n * (Recursica's source-of-truth adapter) as one side of the comparison, while\n * the other side is that repo's own already-running local Storybook.\n *\n * See PROPOSAL-installed-package-harness.md for the verified prototype this\n * is built from, and the three upstream gaps it works around.\n */\n\nexport interface MantineSourceOfTruthHarnessOptions {\n /**\n * Directory the harness project is scaffolded into. Regenerated on every\n * call — add it to your .gitignore rather than committing it.\n */\n dir: string;\n /** First-guess port, shown for `--dry-run` visibility only. The harness's\n * Storybook is never pinned to this — see `HarnessWebServerConfig.port`. */\n port: number;\n /** npm version/range for @recursica/adapter-mantine-v8. Defaults to \"latest\". */\n mantineAdapterVersion?: string;\n /** npm version/range for @recursica/storybook-template. Defaults to \"latest\". */\n storybookTemplateVersion?: string;\n}\n\nexport interface HarnessWebServerConfig {\n command: string;\n /** First-guess port, shown for `--dry-run` visibility only — not\n * authoritative. The real port is whatever this Storybook's own startup\n * banner reports once it's actually running (see portDiscovery.ts), since\n * Storybook silently falls back to an OS-assigned port whenever this one\n * is taken. */\n port: number;\n cwd: string;\n reuseExistingServer: boolean;\n /** File the real, detected port is cached in between runs, so a later\n * `reuseExistingServer` run can find this instance again. */\n cacheFile: string;\n timeout: number;\n}\n\n// Peer/dev ranges pinned to what @recursica/mantine-adapter and\n// @recursica/storybook-template themselves require, so the harness can't\n// drift onto an incompatible Mantine or Storybook major version.\nconst MANTINE_CORE_RANGE = \"^8.0.0\";\nconst STORYBOOK_RANGE = \"^10.3.3\";\nconst REACT_RANGE = \"^19.0.0\";\n\n// storybook-template's createMainConfig() defaults its addons list to these\n// three but doesn't declare them as peerDependencies (proposal gap 2) — a\n// harness that skips installing any of them gets a silent \"could not\n// resolve addon\" warning at boot, then a hard runtime crash later when Vite\n// pre-bundles preview.tsx's dependency graph. Installed explicitly here.\nconst DEFAULT_ADDON_DEPENDENCIES = {\n \"@storybook/addon-docs\": STORYBOOK_RANGE,\n \"@storybook/addon-a11y\": STORYBOOK_RANGE,\n \"storybook-dark-mode\": \"^5.0.0\",\n};\n\n// adapter-mantine-v8's Introduction.stories.tsx (Version.tsx/OverStyling.tsx)\n// needs react-markdown, but it's a devDependency there — Storybook-only,\n// never bundled into dist — so an external `npm install` of the published\n// package won't pull it in. The harness boots a real Storybook against\n// src/, so it must provide this itself. Installed explicitly here.\nconst WORKAROUND_DEPENDENCIES = {\n \"react-markdown\": \"^10.1.0\",\n};\n\nfunction harnessPackageJson(options: {\n mantineAdapterVersion: string;\n storybookTemplateVersion: string;\n}) {\n return {\n name: \"adapter-tester-mantine-source-of-truth-harness\",\n private: true,\n type: \"module\",\n scripts: {\n // No `-p` pin — Storybook silently falls back to an OS-assigned port\n // whenever its default is taken, so the caller detects the real port\n // from this process's own output rather than trusting a fixed one.\n storybook: `storybook dev`,\n },\n dependencies: {\n \"@recursica/adapter-mantine-v8\": options.mantineAdapterVersion,\n \"@recursica/storybook-template\": options.storybookTemplateVersion,\n \"@recursica/official-release\": \"latest\",\n \"@recursica/adapter-common\": \"latest\",\n \"@mantine/core\": MANTINE_CORE_RANGE,\n \"@mantine/dates\": MANTINE_CORE_RANGE,\n react: REACT_RANGE,\n \"react-dom\": REACT_RANGE,\n storybook: STORYBOOK_RANGE,\n \"@storybook/react-vite\": STORYBOOK_RANGE,\n ...DEFAULT_ADDON_DEPENDENCIES,\n ...WORKAROUND_DEPENDENCIES,\n },\n };\n}\n\nconst MAIN_TS = `import { createMainConfig } from \"@recursica/storybook-template/main\";\n\nconst config = createMainConfig({\n stories: [\n \"../node_modules/@recursica/adapter-mantine-v8/src/**/*.stories.@(js|jsx|mjs|ts|tsx)\",\n ],\n enableCORS: true,\n});\n\n// react-docgen-typescript can't resolve a TS project for a config file living\n// in .storybook/ when the component source it's docgen'ing lives three\n// directories down inside node_modules — it throws \"Cannot read properties\n// of undefined (reading 'fileExists')\", which surfaces as a plain 404 on\n// preview.tsx. Docgen only powers Storybook's Controls/Docs tables, which\n// this harness never renders, so disabling it is a safe workaround (see\n// PROPOSAL-installed-package-harness.md, gap 3).\nconfig.typescript = { ...config.typescript, reactDocgen: false };\n\nexport default config;\n`;\n\nconst PREVIEW_TSX = `import type { Preview } from \"@storybook/react-vite\";\nimport { createPreviewConfig } from \"@recursica/storybook-template/preview\";\nimport { MantineProvider } from \"@mantine/core\";\nimport { Layer } from \"@recursica/adapter-common\";\nimport \"@mantine/core/styles.css\";\nimport \"@mantine/dates/styles.css\";\nimport \"@recursica/adapter-common/style.css\";\nimport \"@recursica/official-release/recursica_variables_scoped.css\";\nimport recursicaTokens from \"@recursica/official-release/recursica_tokens.json\";\nimport recursicaBrand from \"@recursica/official-release/recursica_brand.json\";\nimport recursicaUIKit from \"@recursica/official-release/recursica_ui-kit.json\";\n\nconst basePreview = createPreviewConfig({\n defaultTheme: \"light\",\n recursicaTokensJsonPath: recursicaTokens,\n recursicaBrandJsonPath: recursicaBrand,\n recursicaUIKitJsonPath: recursicaUIKit,\n});\n\n// Mirrors adapter-mantine-v8's own .storybook/preview.tsx decorator (every story defaults to\n// withLayer: true, layer: 0, wrapped with 48px padding) — every real adapter's own preview.tsx\n// applies this same wrapping, so a target adapter's story renders inside the same Layer\n// chrome/padding the source-of-truth side does. Without this, target screenshots come out\n// dramatically smaller/differently-positioned than the source of truth's (no Layer padding,\n// background, or border-radius at all), which alone can blow past the pixel-diff threshold\n// regardless of whether the actual Recursica tokens match — a false positive, not a real\n// component bug. ColorSchemeWrapper (mantine-adapter's dark-mode-toggle sync helper) is\n// intentionally not replicated — it only matters for the interactive dev-mode UI, not automated\n// screenshot diffing, which always runs in a single theme.\nconst preview: Preview = {\n ...basePreview,\n decorators: [\n (Story, context) => {\n const { withLayer = true, layer = 0 } = context.args;\n const content = <Story />;\n return (\n <MantineProvider>\n {withLayer ? (\n <Layer layer={layer as 0 | 1 | 2 | 3} style={{ padding: \"48px\" }}>\n {content}\n </Layer>\n ) : (\n content\n )}\n </MantineProvider>\n );\n },\n ...(basePreview.decorators || []),\n ],\n};\n\nexport default preview;\n`;\n\n/** Writes the harness project's files to `options.dir` without booting it. */\nexport function scaffoldMantineSourceOfTruthHarness(\n options: MantineSourceOfTruthHarnessOptions,\n): string {\n const {\n dir,\n mantineAdapterVersion = \"latest\",\n storybookTemplateVersion = \"latest\",\n } = options;\n\n mkdirSync(join(dir, \".storybook\"), { recursive: true });\n writeFileSync(\n join(dir, \"package.json\"),\n JSON.stringify(\n harnessPackageJson({\n mantineAdapterVersion,\n storybookTemplateVersion,\n }),\n null,\n 2,\n ) + \"\\n\",\n );\n writeFileSync(join(dir, \".storybook/main.ts\"), MAIN_TS);\n writeFileSync(join(dir, \".storybook/preview.tsx\"), PREVIEW_TSX);\n writeFileSync(join(dir, \".gitignore\"), \"node_modules\\n\");\n\n return dir;\n}\n\n/**\n * Scaffolds the harness and returns a Playwright `webServer` entry for it.\n * Spread the result directly into `playwright.config.ts`'s `webServer` array.\n */\nexport function mantineSourceOfTruthWebServer(\n options: MantineSourceOfTruthHarnessOptions,\n): HarnessWebServerConfig {\n const dir = scaffoldMantineSourceOfTruthHarness(options);\n const {\n mantineAdapterVersion = \"latest\",\n storybookTemplateVersion = \"latest\",\n } = options;\n\n // A bare `npm install` is satisfied by a package-lock.json already sitting\n // in `dir` from a prior run and skips re-resolving against the registry\n // entirely — no network call — so a run can silently keep testing against\n // a stale @recursica/adapter-mantine-v8/storybook-template even after a newer\n // version is published. Naming the two version-pinned packages as explicit\n // `pkg@specifier` CLI args instead forces npm to re-check just those two\n // against the registry every run, while the rest of node_modules stays\n // cached.\n const command = `npm install @recursica/adapter-mantine-v8@${mantineAdapterVersion} @recursica/storybook-template@${storybookTemplateVersion} --no-audit --no-fund && npm run storybook`;\n\n return {\n command,\n port: options.port,\n cwd: dir,\n reuseExistingServer: !process.env.CI,\n cacheFile: join(dir, \"last-port.json\"),\n timeout: 180 * 1000,\n };\n}\n"],"names":["MANTINE_CORE_RANGE","STORYBOOK_RANGE","REACT_RANGE","DEFAULT_ADDON_DEPENDENCIES","WORKAROUND_DEPENDENCIES","harnessPackageJson","options","MAIN_TS","PREVIEW_TSX","scaffoldMantineSourceOfTruthHarness","dir","mantineAdapterVersion","storybookTemplateVersion","mkdirSync","join","writeFileSync","mantineSourceOfTruthWebServer"],"mappings":";;AAoDA,MAAMA,IAAqB,UACrBC,IAAkB,WAClBC,IAAc,WAOdC,IAA6B;AAAA,EACjC,yBAAyBF;AAAA,EACzB,yBAAyBA;AAAA,EACzB,uBAAuB;AACzB,GAOMG,IAA0B;AAAA,EAC9B,kBAAkB;AACpB;AAEA,SAASC,EAAmBC,GAGzB;AACD,SAAO;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,IACT,MAAM;AAAA,IACN,SAAS;AAAA;AAAA;AAAA;AAAA,MAIP,WAAW;AAAA,IAAA;AAAA,IAEb,cAAc;AAAA,MACZ,iCAAiCA,EAAQ;AAAA,MACzC,iCAAiCA,EAAQ;AAAA,MACzC,+BAA+B;AAAA,MAC/B,6BAA6B;AAAA,MAC7B,iBAAiBN;AAAA,MACjB,kBAAkBA;AAAA,MAClB,OAAOE;AAAA,MACP,aAAaA;AAAA,MACb,WAAWD;AAAA,MACX,yBAAyBA;AAAA,MACzB,GAAGE;AAAA,MACH,GAAGC;AAAA,IAAA;AAAA,EACL;AAEJ;AAEA,MAAMG,IAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,GAqBVC,IAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAuDb,SAASC,EACdH,GACQ;AACR,QAAM;AAAA,IACJ,KAAAI;AAAA,IACA,uBAAAC,IAAwB;AAAA,IACxB,0BAAAC,IAA2B;AAAA,EAAA,IACzBN;AAEJ,SAAAO,EAAUC,EAAKJ,GAAK,YAAY,GAAG,EAAE,WAAW,IAAM,GACtDK;AAAA,IACED,EAAKJ,GAAK,cAAc;AAAA,IACxB,KAAK;AAAA,MACHL,EAAmB;AAAA,QACjB,uBAAAM;AAAA,QACA,0BAAAC;AAAA,MAAA,CACD;AAAA,MACD;AAAA,MACA;AAAA,IAAA,IACE;AAAA;AAAA,EAAA,GAENG,EAAcD,EAAKJ,GAAK,oBAAoB,GAAGH,CAAO,GACtDQ,EAAcD,EAAKJ,GAAK,wBAAwB,GAAGF,CAAW,GAC9DO,EAAcD,EAAKJ,GAAK,YAAY,GAAG;AAAA,CAAgB,GAEhDA;AACT;AAMO,SAASM,EACdV,GACwB;AACxB,QAAMI,IAAMD,EAAoCH,CAAO,GACjD;AAAA,IACJ,uBAAAK,IAAwB;AAAA,IACxB,0BAAAC,IAA2B;AAAA,EAAA,IACzBN;AAYJ,SAAO;AAAA,IACL,SAHc,6CAA6CK,CAAqB,kCAAkCC,CAAwB;AAAA,IAI1I,MAAMN,EAAQ;AAAA,IACd,KAAKI;AAAA,IACL,qBAAqB,CAAC,QAAQ,IAAI;AAAA,IAClC,WAAWI,EAAKJ,GAAK,gBAAgB;AAAA,IACrC,SAAS,MAAM;AAAA,EAAA;AAEnB;"}
@@ -1,75 +0,0 @@
1
- "use strict";const o=require("node:fs"),t=require("node:path"),n="^8.0.0",s="^10.3.3",c="^19.0.0",l={"@storybook/addon-docs":s,"@storybook/addon-a11y":s,"storybook-dark-mode":"^5.0.0"},p={"react-markdown":"^10.1.0"};function u(r){return{name:"adapter-tester-mantine-source-of-truth-harness",private:!0,type:"module",scripts:{storybook:"storybook dev"},dependencies:{"@recursica/adapter-mantine-v8":r.mantineAdapterVersion,"@recursica/storybook-template":r.storybookTemplateVersion,"@recursica/official-release":"latest","@recursica/adapter-common":"latest","@mantine/core":n,"@mantine/dates":n,react:c,"react-dom":c,storybook:s,"@storybook/react-vite":s,...l,...p}}}const m=`import { createMainConfig } from "@recursica/storybook-template/main";
2
-
3
- const config = createMainConfig({
4
- stories: [
5
- "../node_modules/@recursica/adapter-mantine-v8/src/**/*.stories.@(js|jsx|mjs|ts|tsx)",
6
- ],
7
- enableCORS: true,
8
- });
9
-
10
- // react-docgen-typescript can't resolve a TS project for a config file living
11
- // in .storybook/ when the component source it's docgen'ing lives three
12
- // directories down inside node_modules — it throws "Cannot read properties
13
- // of undefined (reading 'fileExists')", which surfaces as a plain 404 on
14
- // preview.tsx. Docgen only powers Storybook's Controls/Docs tables, which
15
- // this harness never renders, so disabling it is a safe workaround (see
16
- // PROPOSAL-installed-package-harness.md, gap 3).
17
- config.typescript = { ...config.typescript, reactDocgen: false };
18
-
19
- export default config;
20
- `,f=`import type { Preview } from "@storybook/react-vite";
21
- import { createPreviewConfig } from "@recursica/storybook-template/preview";
22
- import { MantineProvider } from "@mantine/core";
23
- import { Layer } from "@recursica/adapter-common";
24
- import "@mantine/core/styles.css";
25
- import "@mantine/dates/styles.css";
26
- import "@recursica/adapter-common/style.css";
27
- import "@recursica/official-release/recursica_variables_scoped.css";
28
- import recursicaTokens from "@recursica/official-release/recursica_tokens.json";
29
- import recursicaBrand from "@recursica/official-release/recursica_brand.json";
30
- import recursicaUIKit from "@recursica/official-release/recursica_ui-kit.json";
31
-
32
- const basePreview = createPreviewConfig({
33
- defaultTheme: "light",
34
- recursicaTokensJsonPath: recursicaTokens,
35
- recursicaBrandJsonPath: recursicaBrand,
36
- recursicaUIKitJsonPath: recursicaUIKit,
37
- });
38
-
39
- // Mirrors adapter-mantine-v8's own .storybook/preview.tsx decorator (every story defaults to
40
- // withLayer: true, layer: 0, wrapped with 48px padding) — every real adapter's own preview.tsx
41
- // applies this same wrapping, so a target adapter's story renders inside the same Layer
42
- // chrome/padding the source-of-truth side does. Without this, target screenshots come out
43
- // dramatically smaller/differently-positioned than the source of truth's (no Layer padding,
44
- // background, or border-radius at all), which alone can blow past the pixel-diff threshold
45
- // regardless of whether the actual Recursica tokens match — a false positive, not a real
46
- // component bug. ColorSchemeWrapper (mantine-adapter's dark-mode-toggle sync helper) is
47
- // intentionally not replicated — it only matters for the interactive dev-mode UI, not automated
48
- // screenshot diffing, which always runs in a single theme.
49
- const preview: Preview = {
50
- ...basePreview,
51
- decorators: [
52
- (Story, context) => {
53
- const { withLayer = true, layer = 0 } = context.args;
54
- const content = <Story />;
55
- return (
56
- <MantineProvider>
57
- {withLayer ? (
58
- <Layer layer={layer as 0 | 1 | 2 | 3} style={{ padding: "48px" }}>
59
- {content}
60
- </Layer>
61
- ) : (
62
- content
63
- )}
64
- </MantineProvider>
65
- );
66
- },
67
- ...(basePreview.decorators || []),
68
- ],
69
- };
70
-
71
- export default preview;
72
- `;function d(r){const{dir:e,mantineAdapterVersion:a="latest",storybookTemplateVersion:i="latest"}=r;return o.mkdirSync(t.join(e,".storybook"),{recursive:!0}),o.writeFileSync(t.join(e,"package.json"),JSON.stringify(u({mantineAdapterVersion:a,storybookTemplateVersion:i}),null,2)+`
73
- `),o.writeFileSync(t.join(e,".storybook/main.ts"),m),o.writeFileSync(t.join(e,".storybook/preview.tsx"),f),o.writeFileSync(t.join(e,".gitignore"),`node_modules
74
- `),e}function y(r){const e=d(r),{mantineAdapterVersion:a="latest",storybookTemplateVersion:i="latest"}=r;return{command:`npm install @recursica/adapter-mantine-v8@${a} @recursica/storybook-template@${i} --no-audit --no-fund && npm run storybook`,port:r.port,cwd:e,reuseExistingServer:!process.env.CI,cacheFile:t.join(e,"last-port.json"),timeout:180*1e3}}exports.mantineSourceOfTruthWebServer=y;exports.scaffoldMantineSourceOfTruthHarness=d;
75
- //# sourceMappingURL=mantineSourceOfTruth-CdRlVrRC.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"mantineSourceOfTruth-CdRlVrRC.cjs","sources":["../src/harness/mantineSourceOfTruth.ts"],"sourcesContent":["import { mkdirSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\n/**\n * Generates a small, throwaway Storybook project that installs\n * `@recursica/adapter-mantine-v8` as a real npm dependency (not a workspace\n * link) and boots a real Storybook from its published `src/**\\/*.stories.tsx`\n * files, using `@recursica/storybook-template`'s exported factories.\n *\n * This lets any repo — including ones that never checked out the Recursica\n * monorepo — run adapter-tester's visual regression suite against Mantine\n * (Recursica's source-of-truth adapter) as one side of the comparison, while\n * the other side is that repo's own already-running local Storybook.\n *\n * See PROPOSAL-installed-package-harness.md for the verified prototype this\n * is built from, and the three upstream gaps it works around.\n */\n\nexport interface MantineSourceOfTruthHarnessOptions {\n /**\n * Directory the harness project is scaffolded into. Regenerated on every\n * call — add it to your .gitignore rather than committing it.\n */\n dir: string;\n /** First-guess port, shown for `--dry-run` visibility only. The harness's\n * Storybook is never pinned to this — see `HarnessWebServerConfig.port`. */\n port: number;\n /** npm version/range for @recursica/adapter-mantine-v8. Defaults to \"latest\". */\n mantineAdapterVersion?: string;\n /** npm version/range for @recursica/storybook-template. Defaults to \"latest\". */\n storybookTemplateVersion?: string;\n}\n\nexport interface HarnessWebServerConfig {\n command: string;\n /** First-guess port, shown for `--dry-run` visibility only — not\n * authoritative. The real port is whatever this Storybook's own startup\n * banner reports once it's actually running (see portDiscovery.ts), since\n * Storybook silently falls back to an OS-assigned port whenever this one\n * is taken. */\n port: number;\n cwd: string;\n reuseExistingServer: boolean;\n /** File the real, detected port is cached in between runs, so a later\n * `reuseExistingServer` run can find this instance again. */\n cacheFile: string;\n timeout: number;\n}\n\n// Peer/dev ranges pinned to what @recursica/mantine-adapter and\n// @recursica/storybook-template themselves require, so the harness can't\n// drift onto an incompatible Mantine or Storybook major version.\nconst MANTINE_CORE_RANGE = \"^8.0.0\";\nconst STORYBOOK_RANGE = \"^10.3.3\";\nconst REACT_RANGE = \"^19.0.0\";\n\n// storybook-template's createMainConfig() defaults its addons list to these\n// three but doesn't declare them as peerDependencies (proposal gap 2) — a\n// harness that skips installing any of them gets a silent \"could not\n// resolve addon\" warning at boot, then a hard runtime crash later when Vite\n// pre-bundles preview.tsx's dependency graph. Installed explicitly here.\nconst DEFAULT_ADDON_DEPENDENCIES = {\n \"@storybook/addon-docs\": STORYBOOK_RANGE,\n \"@storybook/addon-a11y\": STORYBOOK_RANGE,\n \"storybook-dark-mode\": \"^5.0.0\",\n};\n\n// adapter-mantine-v8's Introduction.stories.tsx (Version.tsx/OverStyling.tsx)\n// needs react-markdown, but it's a devDependency there — Storybook-only,\n// never bundled into dist — so an external `npm install` of the published\n// package won't pull it in. The harness boots a real Storybook against\n// src/, so it must provide this itself. Installed explicitly here.\nconst WORKAROUND_DEPENDENCIES = {\n \"react-markdown\": \"^10.1.0\",\n};\n\nfunction harnessPackageJson(options: {\n mantineAdapterVersion: string;\n storybookTemplateVersion: string;\n}) {\n return {\n name: \"adapter-tester-mantine-source-of-truth-harness\",\n private: true,\n type: \"module\",\n scripts: {\n // No `-p` pin — Storybook silently falls back to an OS-assigned port\n // whenever its default is taken, so the caller detects the real port\n // from this process's own output rather than trusting a fixed one.\n storybook: `storybook dev`,\n },\n dependencies: {\n \"@recursica/adapter-mantine-v8\": options.mantineAdapterVersion,\n \"@recursica/storybook-template\": options.storybookTemplateVersion,\n \"@recursica/official-release\": \"latest\",\n \"@recursica/adapter-common\": \"latest\",\n \"@mantine/core\": MANTINE_CORE_RANGE,\n \"@mantine/dates\": MANTINE_CORE_RANGE,\n react: REACT_RANGE,\n \"react-dom\": REACT_RANGE,\n storybook: STORYBOOK_RANGE,\n \"@storybook/react-vite\": STORYBOOK_RANGE,\n ...DEFAULT_ADDON_DEPENDENCIES,\n ...WORKAROUND_DEPENDENCIES,\n },\n };\n}\n\nconst MAIN_TS = `import { createMainConfig } from \"@recursica/storybook-template/main\";\n\nconst config = createMainConfig({\n stories: [\n \"../node_modules/@recursica/adapter-mantine-v8/src/**/*.stories.@(js|jsx|mjs|ts|tsx)\",\n ],\n enableCORS: true,\n});\n\n// react-docgen-typescript can't resolve a TS project for a config file living\n// in .storybook/ when the component source it's docgen'ing lives three\n// directories down inside node_modules — it throws \"Cannot read properties\n// of undefined (reading 'fileExists')\", which surfaces as a plain 404 on\n// preview.tsx. Docgen only powers Storybook's Controls/Docs tables, which\n// this harness never renders, so disabling it is a safe workaround (see\n// PROPOSAL-installed-package-harness.md, gap 3).\nconfig.typescript = { ...config.typescript, reactDocgen: false };\n\nexport default config;\n`;\n\nconst PREVIEW_TSX = `import type { Preview } from \"@storybook/react-vite\";\nimport { createPreviewConfig } from \"@recursica/storybook-template/preview\";\nimport { MantineProvider } from \"@mantine/core\";\nimport { Layer } from \"@recursica/adapter-common\";\nimport \"@mantine/core/styles.css\";\nimport \"@mantine/dates/styles.css\";\nimport \"@recursica/adapter-common/style.css\";\nimport \"@recursica/official-release/recursica_variables_scoped.css\";\nimport recursicaTokens from \"@recursica/official-release/recursica_tokens.json\";\nimport recursicaBrand from \"@recursica/official-release/recursica_brand.json\";\nimport recursicaUIKit from \"@recursica/official-release/recursica_ui-kit.json\";\n\nconst basePreview = createPreviewConfig({\n defaultTheme: \"light\",\n recursicaTokensJsonPath: recursicaTokens,\n recursicaBrandJsonPath: recursicaBrand,\n recursicaUIKitJsonPath: recursicaUIKit,\n});\n\n// Mirrors adapter-mantine-v8's own .storybook/preview.tsx decorator (every story defaults to\n// withLayer: true, layer: 0, wrapped with 48px padding) — every real adapter's own preview.tsx\n// applies this same wrapping, so a target adapter's story renders inside the same Layer\n// chrome/padding the source-of-truth side does. Without this, target screenshots come out\n// dramatically smaller/differently-positioned than the source of truth's (no Layer padding,\n// background, or border-radius at all), which alone can blow past the pixel-diff threshold\n// regardless of whether the actual Recursica tokens match — a false positive, not a real\n// component bug. ColorSchemeWrapper (mantine-adapter's dark-mode-toggle sync helper) is\n// intentionally not replicated — it only matters for the interactive dev-mode UI, not automated\n// screenshot diffing, which always runs in a single theme.\nconst preview: Preview = {\n ...basePreview,\n decorators: [\n (Story, context) => {\n const { withLayer = true, layer = 0 } = context.args;\n const content = <Story />;\n return (\n <MantineProvider>\n {withLayer ? (\n <Layer layer={layer as 0 | 1 | 2 | 3} style={{ padding: \"48px\" }}>\n {content}\n </Layer>\n ) : (\n content\n )}\n </MantineProvider>\n );\n },\n ...(basePreview.decorators || []),\n ],\n};\n\nexport default preview;\n`;\n\n/** Writes the harness project's files to `options.dir` without booting it. */\nexport function scaffoldMantineSourceOfTruthHarness(\n options: MantineSourceOfTruthHarnessOptions,\n): string {\n const {\n dir,\n mantineAdapterVersion = \"latest\",\n storybookTemplateVersion = \"latest\",\n } = options;\n\n mkdirSync(join(dir, \".storybook\"), { recursive: true });\n writeFileSync(\n join(dir, \"package.json\"),\n JSON.stringify(\n harnessPackageJson({\n mantineAdapterVersion,\n storybookTemplateVersion,\n }),\n null,\n 2,\n ) + \"\\n\",\n );\n writeFileSync(join(dir, \".storybook/main.ts\"), MAIN_TS);\n writeFileSync(join(dir, \".storybook/preview.tsx\"), PREVIEW_TSX);\n writeFileSync(join(dir, \".gitignore\"), \"node_modules\\n\");\n\n return dir;\n}\n\n/**\n * Scaffolds the harness and returns a Playwright `webServer` entry for it.\n * Spread the result directly into `playwright.config.ts`'s `webServer` array.\n */\nexport function mantineSourceOfTruthWebServer(\n options: MantineSourceOfTruthHarnessOptions,\n): HarnessWebServerConfig {\n const dir = scaffoldMantineSourceOfTruthHarness(options);\n const {\n mantineAdapterVersion = \"latest\",\n storybookTemplateVersion = \"latest\",\n } = options;\n\n // A bare `npm install` is satisfied by a package-lock.json already sitting\n // in `dir` from a prior run and skips re-resolving against the registry\n // entirely — no network call — so a run can silently keep testing against\n // a stale @recursica/adapter-mantine-v8/storybook-template even after a newer\n // version is published. Naming the two version-pinned packages as explicit\n // `pkg@specifier` CLI args instead forces npm to re-check just those two\n // against the registry every run, while the rest of node_modules stays\n // cached.\n const command = `npm install @recursica/adapter-mantine-v8@${mantineAdapterVersion} @recursica/storybook-template@${storybookTemplateVersion} --no-audit --no-fund && npm run storybook`;\n\n return {\n command,\n port: options.port,\n cwd: dir,\n reuseExistingServer: !process.env.CI,\n cacheFile: join(dir, \"last-port.json\"),\n timeout: 180 * 1000,\n };\n}\n"],"names":["MANTINE_CORE_RANGE","STORYBOOK_RANGE","REACT_RANGE","DEFAULT_ADDON_DEPENDENCIES","WORKAROUND_DEPENDENCIES","harnessPackageJson","options","MAIN_TS","PREVIEW_TSX","scaffoldMantineSourceOfTruthHarness","dir","mantineAdapterVersion","storybookTemplateVersion","mkdirSync","join","writeFileSync","mantineSourceOfTruthWebServer"],"mappings":"+DAoDMA,EAAqB,SACrBC,EAAkB,UAClBC,EAAc,UAOdC,EAA6B,CACjC,wBAAyBF,EACzB,wBAAyBA,EACzB,sBAAuB,QACzB,EAOMG,EAA0B,CAC9B,iBAAkB,SACpB,EAEA,SAASC,EAAmBC,EAGzB,CACD,MAAO,CACL,KAAM,iDACN,QAAS,GACT,KAAM,SACN,QAAS,CAIP,UAAW,eAAA,EAEb,aAAc,CACZ,gCAAiCA,EAAQ,sBACzC,gCAAiCA,EAAQ,yBACzC,8BAA+B,SAC/B,4BAA6B,SAC7B,gBAAiBN,EACjB,iBAAkBA,EAClB,MAAOE,EACP,YAAaA,EACb,UAAWD,EACX,wBAAyBA,EACzB,GAAGE,EACH,GAAGC,CAAA,CACL,CAEJ,CAEA,MAAMG,EAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAqBVC,EAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAuDb,SAASC,EACdH,EACQ,CACR,KAAM,CACJ,IAAAI,EACA,sBAAAC,EAAwB,SACxB,yBAAAC,EAA2B,QAAA,EACzBN,EAEJO,OAAAA,EAAAA,UAAUC,EAAAA,KAAKJ,EAAK,YAAY,EAAG,CAAE,UAAW,GAAM,EACtDK,EAAAA,cACED,EAAAA,KAAKJ,EAAK,cAAc,EACxB,KAAK,UACHL,EAAmB,CACjB,sBAAAM,EACA,yBAAAC,CAAA,CACD,EACD,KACA,CAAA,EACE;AAAA,CAAA,EAENG,EAAAA,cAAcD,EAAAA,KAAKJ,EAAK,oBAAoB,EAAGH,CAAO,EACtDQ,EAAAA,cAAcD,EAAAA,KAAKJ,EAAK,wBAAwB,EAAGF,CAAW,EAC9DO,EAAAA,cAAcD,EAAAA,KAAKJ,EAAK,YAAY,EAAG;AAAA,CAAgB,EAEhDA,CACT,CAMO,SAASM,EACdV,EACwB,CACxB,MAAMI,EAAMD,EAAoCH,CAAO,EACjD,CACJ,sBAAAK,EAAwB,SACxB,yBAAAC,EAA2B,QAAA,EACzBN,EAYJ,MAAO,CACL,QAHc,6CAA6CK,CAAqB,kCAAkCC,CAAwB,6CAI1I,KAAMN,EAAQ,KACd,IAAKI,EACL,oBAAqB,CAAC,QAAQ,IAAI,GAClC,UAAWI,EAAAA,KAAKJ,EAAK,gBAAgB,EACrC,QAAS,IAAM,GAAA,CAEnB"}