@recursica/adapter-tester 5.1.2 → 5.1.3

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.
@@ -21,9 +21,10 @@ export interface SourceOfTruthGolden {
21
21
  * matching release tag (changesets tags every release as
22
22
  * `<packageName>@<version>`), caching what's downloaded under `cacheDir`.
23
23
  *
24
- * Returns `null` — degrading the divergence check to a skip, not a failure —
25
- * when no golden baseline exists yet for this version, or the registry/repo
26
- * is unreachable.
24
+ * Throws when no golden baseline exists yet for this version, or the
25
+ * registry/repo is unreachable — a divergence check that can't reach its
26
+ * baseline must fail loudly, not degrade to a silent skip that Playwright
27
+ * then reports as a pass.
27
28
  */
28
- export declare function resolveSourceOfTruthGolden(location: SourceOfTruthGoldenLocation): Promise<SourceOfTruthGolden | null>;
29
+ export declare function resolveSourceOfTruthGolden(location: SourceOfTruthGoldenLocation): Promise<SourceOfTruthGolden>;
29
30
  //# sourceMappingURL=resolveSourceOfTruthGolden.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"resolveSourceOfTruthGolden.d.ts","sourceRoot":"","sources":["../../src/golden/resolveSourceOfTruthGolden.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,cAAc,CAAC;AAChE,OAAO,EAIL,KAAK,cAAc,EACpB,MAAM,oBAAoB,CAAC;AAO5B,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,cAAc,CAAC;IACzB;kDAC8C;IAC9C,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CACpD;AA2BD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,0BAA0B,CAC9C,QAAQ,EAAE,2BAA2B,GACpC,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAiFrC"}
1
+ {"version":3,"file":"resolveSourceOfTruthGolden.d.ts","sourceRoot":"","sources":["../../src/golden/resolveSourceOfTruthGolden.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,cAAc,CAAC;AAChE,OAAO,EAIL,KAAK,cAAc,EACpB,MAAM,oBAAoB,CAAC;AAO5B,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,cAAc,CAAC;IACzB;kDAC8C;IAC9C,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CACpD;AA2BD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,0BAA0B,CAC9C,QAAQ,EAAE,2BAA2B,GACpC,OAAO,CAAC,mBAAmB,CAAC,CA6E9B"}
@@ -1 +1 @@
1
- {"version":3,"file":"runVisualRegression.d.ts","sourceRoot":"","sources":["../../src/testing/runVisualRegression.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE1D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAcxD,UAAU,cAAc;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAmED;;;;wEAIwE;AACxE,MAAM,WAAW,oBAAoB;IACnC,6EAA6E;IAC7E,aAAa,EAAE,MAAM,CAAC;IACtB,iEAAiE;IACjE,UAAU,EAAE,MAAM,CAAC;IACnB,2DAA2D;IAC3D,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B;;;+EAG2E;IAC3E,wBAAwB,EAAE,MAAM,EAAE,CAAC;IACnC;;;;uDAImD;IACnD,UAAU,EAAE,CACV,KAAK,EAAE,cAAc,EACrB,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,QAAQ,KACf,OAAO,CAAC,IAAI,CAAC,CAAC;CACpB;AAED;;;;wEAIwE;AACxE,MAAM,WAAW,oBAAoB;IACnC,6EAA6E;IAC7E,aAAa,EAAE,MAAM,CAAC;IACtB,iEAAiE;IACjE,UAAU,EAAE,MAAM,CAAC;IACnB,2DAA2D;IAC3D,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B;;;;uDAImD;IACnD,UAAU,EAAE,CACV,KAAK,EAAE,cAAc,EACrB,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,QAAQ,KACf,OAAO,CAAC,IAAI,CAAC,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,2BAA2B,CAC/C,MAAM,EAAE,mBAAmB,GAC1B,OAAO,CAAC,oBAAoB,CAAC,CA6R/B"}
1
+ {"version":3,"file":"runVisualRegression.d.ts","sourceRoot":"","sources":["../../src/testing/runVisualRegression.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE1D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAcxD,UAAU,cAAc;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAmED;;;;wEAIwE;AACxE,MAAM,WAAW,oBAAoB;IACnC,6EAA6E;IAC7E,aAAa,EAAE,MAAM,CAAC;IACtB,iEAAiE;IACjE,UAAU,EAAE,MAAM,CAAC;IACnB,2DAA2D;IAC3D,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B;;;+EAG2E;IAC3E,wBAAwB,EAAE,MAAM,EAAE,CAAC;IACnC;;;;uDAImD;IACnD,UAAU,EAAE,CACV,KAAK,EAAE,cAAc,EACrB,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,QAAQ,KACf,OAAO,CAAC,IAAI,CAAC,CAAC;CACpB;AAED;;;;wEAIwE;AACxE,MAAM,WAAW,oBAAoB;IACnC,6EAA6E;IAC7E,aAAa,EAAE,MAAM,CAAC;IACtB,iEAAiE;IACjE,UAAU,EAAE,MAAM,CAAC;IACnB,2DAA2D;IAC3D,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B;;;;uDAImD;IACnD,UAAU,EAAE,CACV,KAAK,EAAE,cAAc,EACrB,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,QAAQ,KACf,OAAO,CAAC,IAAI,CAAC,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,2BAA2B,CAC/C,MAAM,EAAE,mBAAmB,GAC1B,OAAO,CAAC,oBAAoB,CAAC,CA8R/B"}
package/dist/testing.cjs CHANGED
@@ -1,6 +1,6 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const L=require("@playwright/test"),i=require("node:fs"),X=require("pixelmatch"),P=require("pngjs"),E=require("node:path"),R=require("./index-C6uYPRmx.cjs");function B(e,t){const n=P.PNG.sync.read(e),r=P.PNG.sync.read(t);if(n.width!==r.width||n.height!==r.height)return{diffPixels:1/0,diffImage:null};const s=new P.PNG({width:n.width,height:n.height});return{diffPixels:X(n.data,r.data,s.data,n.width,n.height,{threshold:.1}),diffImage:P.PNG.sync.write(s)}}const z="http://json-schema.org/draft-07/schema#",H="https://github.com/borderux/recursica/tree/main/packages/adapter-tester/src/golden/manifest.schema.json",Y="test/golden/manifest.json",Q="Tracks golden (baseline) image metadata for @recursica/adapter-tester's golden-image visual regression checks. One manifest lives alongside its adapter's test/golden/<story-id>.png files, keyed by story id.",Z="object",ee={type:"object",additionalProperties:!1,required:["createdAt"],properties:{createdAt:{type:"string",format:"date-time",description:"When this story's own golden PNG was last captured, via --update-golden, --approve-divergence, or first-run auto-create."},sourceOfTruthCreatedAt:{type:"string",format:"date-time",description:"The source-of-truth adapter's (mantine) manifest `createdAt` for this story at the time this adapter's divergence from it was last reviewed via --approve-divergence. Omitted on the source-of-truth adapter's own manifest, and omitted here until the first approval. If mantine's current `createdAt` for this story is newer than this value, the divergence is flagged again for re-review."}}},te={$schema:z,$id:H,title:Y,description:Q,type:Z,additionalProperties:ee},re=R.index,D=new R.ajvExports.Ajv({allErrors:!0,strict:!0});re(D);const _=D.compile(te);function I(e,t){if(_(e))return;const n=(_.errors??[]).map(r=>{var a;const s=(a=r.params)!=null&&a.additionalProperty?` '${r.params.additionalProperty}'`:"";return` - ${r.instancePath||"root"} ${r.message}${s}`}).join(`
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const L=require("@playwright/test"),i=require("node:fs"),z=require("pixelmatch"),P=require("pngjs"),E=require("node:path"),D=require("./index-C6uYPRmx.cjs");function B(e,t){const n=P.PNG.sync.read(e),r=P.PNG.sync.read(t);if(n.width!==r.width||n.height!==r.height)return{diffPixels:1/0,diffImage:null};const s=new P.PNG({width:n.width,height:n.height});return{diffPixels:z(n.data,r.data,s.data,n.width,n.height,{threshold:.1}),diffImage:P.PNG.sync.write(s)}}const H="http://json-schema.org/draft-07/schema#",Y="https://github.com/borderux/recursica/tree/main/packages/adapter-tester/src/golden/manifest.schema.json",Q="test/golden/manifest.json",Z="Tracks golden (baseline) image metadata for @recursica/adapter-tester's golden-image visual regression checks. One manifest lives alongside its adapter's test/golden/<story-id>.png files, keyed by story id.",ee="object",te={type:"object",additionalProperties:!1,required:["createdAt"],properties:{createdAt:{type:"string",format:"date-time",description:"When this story's own golden PNG was last captured, via --update-golden, --approve-divergence, or first-run auto-create."},sourceOfTruthCreatedAt:{type:"string",format:"date-time",description:"The source-of-truth adapter's (mantine) manifest `createdAt` for this story at the time this adapter's divergence from it was last reviewed via --approve-divergence. Omitted on the source-of-truth adapter's own manifest, and omitted here until the first approval. If mantine's current `createdAt` for this story is newer than this value, the divergence is flagged again for re-review."}}},re={$schema:H,$id:Y,title:Q,description:Z,type:ee,additionalProperties:te},ne=D.index,U=new D.ajvExports.Ajv({allErrors:!0,strict:!0});ne(U);const _=U.compile(re);function C(e,t){if(_(e))return;const n=(_.errors??[]).map(r=>{var c;const s=(c=r.params)!=null&&c.additionalProperty?` '${r.params.additionalProperty}'`:"";return` - ${r.instancePath||"root"} ${r.message}${s}`}).join(`
2
2
  `);throw new Error(`Invalid ${t}:
3
- ${n}`)}function j(e){return E.join(e,"manifest.json")}function J(e){return E.join(e,"manifest.json.lock")}function x(e,t){return E.join(e,`${t}.png`)}function b(e){const t=j(e);if(!i.existsSync(t))return{};const n=JSON.parse(i.readFileSync(t,"utf8"));return I(n,t),n}function ne(e,t){const n=j(e);I(t,n);const r={};for(const a of Object.keys(t).sort())r[a]=t[a];i.mkdirSync(e,{recursive:!0});const s=`${n}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;i.writeFileSync(s,JSON.stringify(r,null,2)+`
4
- `),i.renameSync(s,n)}const oe=25,se=15e3;function ie(e){return new Promise(t=>setTimeout(t,e))}async function ae(e){i.mkdirSync(e,{recursive:!0});const t=J(e),n=Date.now()+se;for(;;)try{i.closeSync(i.openSync(t,"wx"));return}catch(r){if(r.code!=="EEXIST")throw r;if(Date.now()>=n)throw new Error(`Timed out waiting for the manifest lock at "${t}" — delete it if a previous run crashed while holding it.`);await ie(oe)}}function ce(e){i.rmSync(J(e),{force:!0})}async function U(e,t,n){await ae(e);try{const r=b(e),s=n(r[t]);return s===void 0?delete r[t]:r[t]=s,ne(e,r),s}finally{ce(e)}}function de(e,t,n){i.mkdirSync(e,{recursive:!0}),i.writeFileSync(x(e,t),n)}async function ue(e,t){const n=b(e),r=Object.keys(n).filter(s=>!t.has(s));for(const s of r){const a=x(e,s);i.existsSync(a)&&i.unlinkSync(a),await U(e,s,()=>{})}return r}const he="borderux/recursica-adapter-mantine-v8";async function le(e,t){var a,u;const n=await fetch(`https://registry.npmjs.org/${e}`);if(!n.ok)throw new Error(`Could not reach npm registry for ${e}: ${n.statusText}`);const r=await n.json(),s=((a=r["dist-tags"])==null?void 0:a[t])??((u=r.versions)!=null&&u[t]?t:void 0);if(!s)throw new Error(`${e} has no version or dist-tag "${t}" on the npm registry.`);return s}async function fe(e){if(e.type==="local")return i.existsSync(j(e.dir))?{manifest:b(e.dir),async readImage(h){const d=x(e.dir,h);return i.existsSync(d)?i.readFileSync(d):null}}:(console.warn(`No golden baseline found yet at ${e.dir} — source-of-truth divergence check skipped for this run.`),null);let t;try{t=await le(e.packageName,e.versionSpec)}catch(c){return console.warn(`Could not resolve ${e.packageName}@${e.versionSpec} — source-of-truth divergence check skipped for this run.`,c),null}const n=E.join(e.cacheDir,t),r=`${e.packageName}@${t}`,s=`https://raw.githubusercontent.com/${he}/${r}/test/golden`;let a;const u=j(n);if(i.existsSync(u))a=b(n);else{let c;try{c=await fetch(`${s}/manifest.json`)}catch(m){return console.warn(`Could not reach GitHub to fetch ${r}'s golden baseline — source-of-truth divergence check skipped for this run.`,m),null}if(!c.ok)return console.warn(`No golden baseline published for ${r} — source-of-truth divergence check skipped for this run.`),null;const h=await c.text(),d=JSON.parse(h);I(d,`${s}/manifest.json`),i.mkdirSync(n,{recursive:!0}),i.writeFileSync(u,h),a=d}return{manifest:a,async readImage(c){const h=x(n,c);if(i.existsSync(h))return i.readFileSync(h);const d=await fetch(`${s}/${c}.png`);if(!d.ok)return null;const m=Buffer.from(await d.arrayBuffer());return i.mkdirSync(n,{recursive:!0}),i.writeFileSync(h,m),m}}}const pe=["Theme","Tokens","Introduction"];function M(e,t){return e===t||e.startsWith(t)}async function ge(e,t){let n;try{const r=await fetch(`${e.url}/index.json`);if(!r.ok)throw new Error(`Failed to fetch Storybook index: ${r.statusText}`);const a=(await r.json()).entries||{};n=Object.values(a).filter(u=>u.type==="story"&&!t.some(c=>u.title===c||u.title.startsWith(`${c}/`))),n.sort((u,c)=>u.id.localeCompare(c.id))}catch(r){throw console.error("Failed to load Storybook index from",`${e.url}/index.json`,r),new Error(`Storybook target "${e.name}" is not responsive or index.json is missing. Please ensure its Storybook is running.`)}return n}function q(e,t,n){let r;for(const s of Object.keys(t))M(e,s)&&(!r||s.length>r.length)&&(r=s);return r!==void 0?t[r]:n}async function me(e){const t=e.isSourceOfTruthAdapter?e.targets[0]:e.targets.find(o=>!o.sourceOfTruth);if(!t)throw new Error("adapter-tester config has no non-sourceOfTruth target to run the golden check against.");const n=e.excludeTitlePrefixes??pe,r=e.stories??{},s=Object.keys(r).filter(o=>r[o].exclude),a=Object.fromEntries(Object.entries(r).filter(([,o])=>o.goldenThreshold!==void 0).map(([o,l])=>[o,l.goldenThreshold])),u=Object.fromEntries(Object.entries(r).filter(([,o])=>o.sourceOfTruthThreshold!==void 0).map(([o,l])=>[o,l.sourceOfTruthThreshold])),c=e.goldenDir,h=e.goldenMode,d=e.checkMode,m=await ge(t,n),G=m.filter(o=>!s.some(l=>M(o.id,l)));if(h==="update-golden"){const o=await ue(c,new Set(G.map(l=>l.id)));o.length>0&&console.warn(`Pruned ${o.length} orphaned golden(s) no longer in Storybook: ${o.join(", ")}`)}const w=d!=="divergence"||e.isSourceOfTruthAdapter||!e.sourceOfTruthGolden?null:await fe(e.sourceOfTruthGolden),K=new Set(m.map(o=>o.id)),T=w?Object.keys(w.manifest).filter(o=>!K.has(o)&&!s.some(l=>M(o,l))).sort():[];T.length>0&&console.error(`[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.`),console.log([`[adapter-tester] target: "${t.name}" (${t.url})`,`[adapter-tester] checkMode: "${d}" (${d==="divergence"?"live render vs source-of-truth's golden":"live render vs this project's own golden"})`,`[adapter-tester] goldenMode: "${h}"`,`[adapter-tester] goldenThresholdPixels: ${e.goldenThresholdPixels}`,`[adapter-tester] sourceOfTruthThresholdPixels: ${e.sourceOfTruthThresholdPixels}`,d==="divergence"?e.isSourceOfTruthAdapter?"[adapter-tester] sourceOfTruthGolden: skipped — this is the source-of-truth adapter, nothing to diverge from":e.sourceOfTruthGolden?w?`[adapter-tester] sourceOfTruthGolden: resolved, ${Object.keys(w.manifest).length} golden(s) available (config: ${JSON.stringify(e.sourceOfTruthGolden)})`:`[adapter-tester] sourceOfTruthGolden: unavailable — no baseline found or unreachable (config: ${JSON.stringify(e.sourceOfTruthGolden)}); divergence check will skip every story`:"[adapter-tester] sourceOfTruthGolden: skipped — no sourceOfTruthGolden configured":'[adapter-tester] sourceOfTruthGolden: not used in "own" checkMode',`[adapter-tester] stories: ${G.length} to check (excluded: ${s.length}, title prefixes excluded: ${n.join(", ")||"none"})`,`[adapter-tester] story parity with source of truth: ${T.length===0?"OK":`${T.length} missing (see error above)`}`].join(`
5
- `));const V=d==="divergence"?"Source-of-Truth Divergence Check":"Own-Drift Golden Image Check";return{ownTargetName:t.name,suiteLabel:V,stories:G,missingFromSourceOfTruth:T,checkStory:async(o,l,g)=>{const y=await l.newPage();await y.setViewportSize({width:800,height:600}),await y.goto(`${t.url}/iframe.html?id=${o.id}&viewMode=story`,{waitUntil:"networkidle"}),await y.waitForSelector("#storybook-root"),await y.addStyleTag({content:"* { -webkit-font-smoothing: antialiased !important; -moz-osx-font-smoothing: grayscale !important; }"}),await y.evaluate(()=>Promise.all(Array.from(document.querySelectorAll('link[rel="stylesheet"]')).map(f=>f.sheet?Promise.resolve():new Promise(p=>{f.addEventListener("load",()=>p(),{once:!0}),f.addEventListener("error",()=>p(),{once:!0})}))).then(()=>document.fonts.ready)),await y.waitForTimeout(300);const k=await y.screenshot(),N=x(c,o.id),v=b(c)[o.id],W=h!=="check"||!v||!i.existsSync(N);let $;if(W)de(c,o.id,k),$=v!=null&&v.sourceOfTruthCreatedAt?{createdAt:new Date().toISOString(),sourceOfTruthCreatedAt:v.sourceOfTruthCreatedAt}:{createdAt:new Date().toISOString()},v||g.annotations.push({type:"golden-created",description:`No golden existed yet for "${o.id}" — captured one from this run.`});else if($=v,d==="own"){const f=i.readFileSync(N),{diffPixels:p,diffImage:S}=B(k,f),O=q(o.id,a,e.goldenThresholdPixels);p>=O&&(await g.attach("expected",{body:f,contentType:"image/png"}),await g.attach("actual",{body:k,contentType:"image/png"}),S&&await g.attach("diff",{body:S,contentType:"image/png"})),L.expect.soft(p,`"${o.id}" has drifted from its own golden image (${p} mismatched pixels, threshold ${O})`).toBeLessThan(O)}if(w){const f=w.manifest[o.id],p=f?await w.readImage(o.id):null;if(f&&p)if(h==="approve-divergence")$={...$,sourceOfTruthCreatedAt:f.createdAt};else{const{diffPixels:S,diffImage:O}=B(k,p),A=q(o.id,u,e.sourceOfTruthThresholdPixels),F=$.sourceOfTruthCreatedAt,C=S<A||F!==void 0&&F>=f.createdAt;C||(await g.attach("expected",{body:p,contentType:"image/png"}),await g.attach("actual",{body:k,contentType:"image/png"}),O&&await g.attach("diff",{body:O,contentType:"image/png"}),g.annotations.push({type:"source-of-truth-divergence",description:`"${o.id}" differs from the source of truth's golden by ${S} mismatched pixels (threshold ${A}) and hasn't been reviewed — run with --approve-divergence if this is intentional, or fix the adapter styling.`})),L.expect.soft(C,`"${o.id}" differs from the source of truth's golden by ${S} mismatched pixels (threshold ${A}) and hasn't been reviewed — run with --approve-divergence if this is intentional, or fix the adapter styling.`).toBe(!0)}}await U(c,o.id,()=>$)}}}exports.resolveVisualRegressionPlan=me;
3
+ ${n}`)}function j(e){return E.join(e,"manifest.json")}function J(e){return E.join(e,"manifest.json.lock")}function k(e,t){return E.join(e,`${t}.png`)}function b(e){const t=j(e);if(!i.existsSync(t))return{};const n=JSON.parse(i.readFileSync(t,"utf8"));return C(n,t),n}function oe(e,t){const n=j(e);C(t,n);const r={};for(const c of Object.keys(t).sort())r[c]=t[c];i.mkdirSync(e,{recursive:!0});const s=`${n}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;i.writeFileSync(s,JSON.stringify(r,null,2)+`
4
+ `),i.renameSync(s,n)}const se=25,ie=15e3;function ae(e){return new Promise(t=>setTimeout(t,e))}async function ce(e){i.mkdirSync(e,{recursive:!0});const t=J(e),n=Date.now()+ie;for(;;)try{i.closeSync(i.openSync(t,"wx"));return}catch(r){if(r.code!=="EEXIST")throw r;if(Date.now()>=n)throw new Error(`Timed out waiting for the manifest lock at "${t}" — delete it if a previous run crashed while holding it.`);await ae(se)}}function de(e){i.rmSync(J(e),{force:!0})}async function K(e,t,n){await ce(e);try{const r=b(e),s=n(r[t]);return s===void 0?delete r[t]:r[t]=s,oe(e,r),s}finally{de(e)}}function he(e,t,n){i.mkdirSync(e,{recursive:!0}),i.writeFileSync(k(e,t),n)}async function ue(e,t){const n=b(e),r=Object.keys(n).filter(s=>!t.has(s));for(const s of r){const c=k(e,s);i.existsSync(c)&&i.unlinkSync(c),await K(e,s,()=>{})}return r}const q="borderux/recursica-adapter-mantine-v8";async function le(e,t){var c,h;const n=await fetch(`https://registry.npmjs.org/${e}`);if(!n.ok)throw new Error(`Could not reach npm registry for ${e}: ${n.statusText}`);const r=await n.json(),s=((c=r["dist-tags"])==null?void 0:c[t])??((h=r.versions)!=null&&h[t]?t:void 0);if(!s)throw new Error(`${e} has no version or dist-tag "${t}" on the npm registry.`);return s}async function fe(e){if(e.type==="local"){if(!i.existsSync(j(e.dir)))throw new Error(`No golden baseline found at ${e.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.`);return{manifest:b(e.dir),async readImage(u){const d=k(e.dir,u);return i.existsSync(d)?i.readFileSync(d):null}}}let t;try{t=await le(e.packageName,e.versionSpec)}catch(a){throw new Error(`Could not resolve ${e.packageName}@${e.versionSpec} on the npm registry — the source-of-truth divergence check has nothing to compare against.`,{cause:a})}const n=E.join(e.cacheDir,t),r=`${e.packageName}@${t}`,s=`https://raw.githubusercontent.com/${q}/${r}/test/golden`;let c;const h=j(n);if(i.existsSync(h))c=b(n);else{let a;try{a=await fetch(`${s}/manifest.json`)}catch(m){throw new Error(`Could not reach GitHub to fetch ${r}'s golden baseline (${s}/manifest.json) — the source-of-truth divergence check has nothing to compare against.`,{cause:m})}if(!a.ok)throw new Error(`No golden baseline published for tag "${r}" in ${q} (${s}/manifest.json returned ${a.status}) — the source-of-truth divergence check has nothing to compare against. Confirm that tag exists and its release actually published test/golden/.`);const u=await a.text(),d=JSON.parse(u);C(d,`${s}/manifest.json`),i.mkdirSync(n,{recursive:!0}),i.writeFileSync(h,u),c=d}return{manifest:c,async readImage(a){const u=k(n,a);if(i.existsSync(u))return i.readFileSync(u);const d=await fetch(`${s}/${a}.png`);if(!d.ok)return null;const m=Buffer.from(await d.arrayBuffer());return i.mkdirSync(n,{recursive:!0}),i.writeFileSync(u,m),m}}}const pe=["Theme","Tokens","Introduction"];function M(e,t){return e===t||e.startsWith(t)}async function ge(e,t){let n;try{const r=await fetch(`${e.url}/index.json`);if(!r.ok)throw new Error(`Failed to fetch Storybook index: ${r.statusText}`);const c=(await r.json()).entries||{};n=Object.values(c).filter(h=>h.type==="story"&&!t.some(a=>h.title===a||h.title.startsWith(`${a}/`))),n.sort((h,a)=>h.id.localeCompare(a.id))}catch(r){throw console.error("Failed to load Storybook index from",`${e.url}/index.json`,r),new Error(`Storybook target "${e.name}" is not responsive or index.json is missing. Please ensure its Storybook is running.`)}return n}function R(e,t,n){let r;for(const s of Object.keys(t))M(e,s)&&(!r||s.length>r.length)&&(r=s);return r!==void 0?t[r]:n}async function me(e){const t=e.isSourceOfTruthAdapter?e.targets[0]:e.targets.find(o=>!o.sourceOfTruth);if(!t)throw new Error("adapter-tester config has no non-sourceOfTruth target to run the golden check against.");const n=e.excludeTitlePrefixes??pe,r=e.stories??{},s=Object.keys(r).filter(o=>r[o].exclude),c=Object.fromEntries(Object.entries(r).filter(([,o])=>o.goldenThreshold!==void 0).map(([o,l])=>[o,l.goldenThreshold])),h=Object.fromEntries(Object.entries(r).filter(([,o])=>o.sourceOfTruthThreshold!==void 0).map(([o,l])=>[o,l.sourceOfTruthThreshold])),a=e.goldenDir,u=e.goldenMode,d=e.checkMode,m=await ge(t,n),A=m.filter(o=>!s.some(l=>M(o.id,l)));if(u==="update-golden"){const o=await ue(a,new Set(A.map(l=>l.id)));o.length>0&&console.warn(`Pruned ${o.length} orphaned golden(s) no longer in Storybook: ${o.join(", ")}`)}const v=d!=="divergence"||e.isSourceOfTruthAdapter||!e.sourceOfTruthGolden?null:await fe(e.sourceOfTruthGolden),V=new Set(m.map(o=>o.id)),T=v?Object.keys(v.manifest).filter(o=>!V.has(o)&&!s.some(l=>M(o,l))).sort():[];T.length>0&&console.error(`[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.`),console.log([`[adapter-tester] target: "${t.name}" (${t.url})`,`[adapter-tester] checkMode: "${d}" (${d==="divergence"?"live render vs source-of-truth's golden":"live render vs this project's own golden"})`,`[adapter-tester] goldenMode: "${u}"`,`[adapter-tester] goldenThresholdPixels: ${e.goldenThresholdPixels}`,`[adapter-tester] sourceOfTruthThresholdPixels: ${e.sourceOfTruthThresholdPixels}`,d==="divergence"?e.isSourceOfTruthAdapter?"[adapter-tester] sourceOfTruthGolden: skipped — this is the source-of-truth adapter, nothing to diverge from":e.sourceOfTruthGolden?`[adapter-tester] sourceOfTruthGolden: resolved, ${Object.keys(v.manifest).length} golden(s) available (config: ${JSON.stringify(e.sourceOfTruthGolden)})`:"[adapter-tester] sourceOfTruthGolden: skipped — no sourceOfTruthGolden configured":'[adapter-tester] sourceOfTruthGolden: not used in "own" checkMode',`[adapter-tester] stories: ${A.length} to check (excluded: ${s.length}, title prefixes excluded: ${n.join(", ")||"none"})`,`[adapter-tester] story parity with source of truth: ${T.length===0?"OK":`${T.length} missing (see error above)`}`].join(`
5
+ `));const W=d==="divergence"?"Source-of-Truth Divergence Check":"Own-Drift Golden Image Check";return{ownTargetName:t.name,suiteLabel:W,stories:A,missingFromSourceOfTruth:T,checkStory:async(o,l,g)=>{const w=await l.newPage();await w.setViewportSize({width:800,height:600}),await w.goto(`${t.url}/iframe.html?id=${o.id}&viewMode=story`,{waitUntil:"networkidle"}),await w.waitForSelector("#storybook-root"),await w.addStyleTag({content:"* { -webkit-font-smoothing: antialiased !important; -moz-osx-font-smoothing: grayscale !important; }"}),await w.evaluate(()=>Promise.all(Array.from(document.querySelectorAll('link[rel="stylesheet"]')).map(f=>f.sheet?Promise.resolve():new Promise(p=>{f.addEventListener("load",()=>p(),{once:!0}),f.addEventListener("error",()=>p(),{once:!0})}))).then(()=>document.fonts.ready)),await w.waitForTimeout(300);const O=await w.screenshot(),I=k(a,o.id),y=b(a)[o.id],X=u!=="check"||!y||!i.existsSync(I);let $;if(X)he(a,o.id,O),$=y!=null&&y.sourceOfTruthCreatedAt?{createdAt:new Date().toISOString(),sourceOfTruthCreatedAt:y.sourceOfTruthCreatedAt}:{createdAt:new Date().toISOString()},y||g.annotations.push({type:"golden-created",description:`No golden existed yet for "${o.id}" — captured one from this run.`});else if($=y,d==="own"){const f=i.readFileSync(I),{diffPixels:p,diffImage:S}=B(O,f),x=R(o.id,c,e.goldenThresholdPixels);p>=x&&(await g.attach("expected",{body:f,contentType:"image/png"}),await g.attach("actual",{body:O,contentType:"image/png"}),S&&await g.attach("diff",{body:S,contentType:"image/png"})),L.expect.soft(p,`"${o.id}" has drifted from its own golden image (${p} mismatched pixels, threshold ${x})`).toBeLessThan(x)}if(v){const f=v.manifest[o.id],p=f?await v.readImage(o.id):null;if(f&&p)if(u==="approve-divergence")$={...$,sourceOfTruthCreatedAt:f.createdAt};else{const{diffPixels:S,diffImage:x}=B(O,p),G=R(o.id,h,e.sourceOfTruthThresholdPixels),F=$.sourceOfTruthCreatedAt,N=S<G||F!==void 0&&F>=f.createdAt;N||(await g.attach("expected",{body:p,contentType:"image/png"}),await g.attach("actual",{body:O,contentType:"image/png"}),x&&await g.attach("diff",{body:x,contentType:"image/png"}),g.annotations.push({type:"source-of-truth-divergence",description:`"${o.id}" differs from the source of truth's golden by ${S} mismatched pixels (threshold ${G}) and hasn't been reviewed — run with --approve-divergence if this is intentional, or fix the adapter styling.`})),L.expect.soft(N,`"${o.id}" differs from the source of truth's golden by ${S} mismatched pixels (threshold ${G}) and hasn't been reviewed — run with --approve-divergence if this is intentional, or fix the adapter styling.`).toBe(!0)}}await K(a,o.id,()=>$)}}}exports.resolveVisualRegressionPlan=me;
6
6
  //# sourceMappingURL=testing.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"testing.cjs","sources":["../src/golden/diffPng.ts","../src/golden/validateManifest.ts","../src/golden/manifestStore.ts","../src/golden/resolveSourceOfTruthGolden.ts","../src/testing/runVisualRegression.ts"],"sourcesContent":["import pixelmatch from \"pixelmatch\";\nimport { PNG } from \"pngjs\";\n\nexport interface PngDiffResult {\n /** Mismatched-pixel count, or `Infinity` if the two images aren't even the\n * same dimensions — pixelmatch itself throws on a size mismatch, and a size\n * mismatch is itself a real difference, not something to swallow. */\n diffPixels: number;\n /** Visual highlight of the mismatched pixels, encoded as a PNG buffer.\n * `null` when `diffPixels` is `Infinity` — there's no pixel-aligned diff to\n * render across two different-sized images. */\n diffImage: Buffer | null;\n}\n\n/** Pixel-diffs two PNG buffers. */\nexport function diffPngBuffers(a: Buffer, b: Buffer): PngDiffResult {\n const imgA = PNG.sync.read(a);\n const imgB = PNG.sync.read(b);\n if (imgA.width !== imgB.width || imgA.height !== imgB.height) {\n return { diffPixels: Infinity, diffImage: null };\n }\n const diff = new PNG({ width: imgA.width, height: imgA.height });\n const diffPixels = pixelmatch(\n imgA.data,\n imgB.data,\n diff.data,\n imgA.width,\n imgA.height,\n { threshold: 0.1 },\n );\n return { diffPixels, diffImage: PNG.sync.write(diff) };\n}\n","import { Ajv } from \"ajv\";\nimport * as ajvFormatsModule from \"ajv-formats\";\nimport type { FormatsPlugin } from \"ajv-formats\";\nimport schema from \"./manifest.schema.json\" with { type: \"json\" };\n\n// See validateFileConfig.ts for why `.default` has to be unwrapped by hand.\nconst addFormats = (ajvFormatsModule as unknown as { default: FormatsPlugin })\n .default;\n\nconst ajv = new Ajv({ allErrors: true, strict: true });\naddFormats(ajv);\nconst validate = ajv.compile(schema);\n\n/**\n * Validates a parsed `test/golden/manifest.json` against `manifest.schema.json`.\n * Throws with every violation listed — callers must not silently coerce or\n * drop invalid entries.\n */\nexport function validateGoldenManifest(data: unknown, path: string): void {\n if (validate(data)) return;\n\n const errors = (validate.errors ?? [])\n .map((error) => {\n const extra = error.params?.additionalProperty\n ? ` '${error.params.additionalProperty}'`\n : \"\";\n return ` - ${error.instancePath || \"root\"} ${error.message}${extra}`;\n })\n .join(\"\\n\");\n throw new Error(`Invalid ${path}:\\n${errors}`);\n}\n","import {\n closeSync,\n existsSync,\n mkdirSync,\n openSync,\n readFileSync,\n renameSync,\n rmSync,\n unlinkSync,\n writeFileSync,\n} from \"node:fs\";\nimport { join } from \"node:path\";\nimport { validateGoldenManifest } from \"./validateManifest.js\";\n\nexport interface GoldenManifestEntry {\n createdAt: string;\n sourceOfTruthCreatedAt?: string;\n}\n\nexport type GoldenManifest = Record<string, GoldenManifestEntry>;\n\nexport function manifestPath(goldenDir: string): string {\n return join(goldenDir, \"manifest.json\");\n}\n\nfunction manifestLockPath(goldenDir: string): string {\n return join(goldenDir, \"manifest.json.lock\");\n}\n\nexport function goldenImagePath(goldenDir: string, storyId: string): string {\n return join(goldenDir, `${storyId}.png`);\n}\n\n/** Returns `{}` if no manifest exists yet — a fresh adapter with no goldens\n * captured is the normal starting state, not an error. */\nexport function loadManifest(goldenDir: string): GoldenManifest {\n const path = manifestPath(goldenDir);\n if (!existsSync(path)) return {};\n const data = JSON.parse(readFileSync(path, \"utf8\"));\n validateGoldenManifest(data, path);\n return data;\n}\n\n/** Validates before writing, and sorts keys so the diff on a reviewed PR is\n * stable regardless of the order stories happened to run in. Writes to a\n * temp file and renames over the real one — `rename` is atomic, so a\n * concurrent `loadManifest` (running in another Playwright worker) never\n * observes a half-written file. */\nexport function saveManifest(\n goldenDir: string,\n manifest: GoldenManifest,\n): void {\n const path = manifestPath(goldenDir);\n validateGoldenManifest(manifest, path);\n const sorted: GoldenManifest = {};\n for (const key of Object.keys(manifest).sort()) {\n sorted[key] = manifest[key]!;\n }\n mkdirSync(goldenDir, { recursive: true });\n const tmpPath = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;\n writeFileSync(tmpPath, JSON.stringify(sorted, null, 2) + \"\\n\");\n renameSync(tmpPath, path);\n}\n\nconst LOCK_RETRY_MS = 25;\nconst LOCK_TIMEOUT_MS = 15_000;\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/** Exclusive-create the lock file, spin-retrying until it's free. `wx` fails\n * atomically (`EEXIST`) if another worker process already holds it — that's\n * the only signal we need, no third-party lock library required for a\n * same-machine, same-run lock like this. */\nasync function acquireManifestLock(goldenDir: string): Promise<void> {\n mkdirSync(goldenDir, { recursive: true });\n const path = manifestLockPath(goldenDir);\n const deadline = Date.now() + LOCK_TIMEOUT_MS;\n for (;;) {\n try {\n closeSync(openSync(path, \"wx\"));\n return;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"EEXIST\") throw error;\n if (Date.now() >= deadline) {\n throw new Error(\n `Timed out waiting for the manifest lock at \"${path}\" — delete it if a previous run crashed while holding it.`,\n );\n }\n await sleep(LOCK_RETRY_MS);\n }\n }\n}\n\nfunction releaseManifestLock(goldenDir: string): void {\n rmSync(manifestLockPath(goldenDir), { force: true });\n}\n\n/** Runs `updater` against this story's manifest entry under an exclusive\n * lock on `manifest.json`: reloads the manifest fresh, applies `updater`,\n * and saves it back, all before releasing the lock. Concurrent Playwright\n * workers each own a different story, so this is the only section that\n * needs to serialize — everything else about a story (its screenshot, its\n * golden image file, its diff) is independent of every other story. */\nexport async function updateManifestEntry(\n goldenDir: string,\n storyId: string,\n updater: (\n entry: GoldenManifestEntry | undefined,\n ) => GoldenManifestEntry | undefined,\n): Promise<GoldenManifestEntry | undefined> {\n await acquireManifestLock(goldenDir);\n try {\n const manifest = loadManifest(goldenDir);\n const nextEntry = updater(manifest[storyId]);\n if (nextEntry === undefined) {\n delete manifest[storyId];\n } else {\n manifest[storyId] = nextEntry;\n }\n saveManifest(goldenDir, manifest);\n return nextEntry;\n } finally {\n releaseManifestLock(goldenDir);\n }\n}\n\nexport function saveGoldenImage(\n goldenDir: string,\n storyId: string,\n buffer: Buffer,\n): void {\n mkdirSync(goldenDir, { recursive: true });\n writeFileSync(goldenImagePath(goldenDir, storyId), buffer);\n}\n\n/** Removes the golden `.png` + manifest entry for every story id in the\n * manifest that isn't in `currentStoryIds` (e.g. a story renamed or deleted\n * from Storybook) — otherwise those never get cleaned up on their own,\n * since a run only ever adds/updates entries for stories it actually saw.\n * Returns the pruned ids, for the caller to report. Both the file removal\n * and the manifest delete are idempotent, so it's safe for this to run\n * redundantly from more than one Playwright worker. */\nexport async function pruneOrphanedGoldens(\n goldenDir: string,\n currentStoryIds: ReadonlySet<string>,\n): Promise<string[]> {\n const manifest = loadManifest(goldenDir);\n const orphanIds = Object.keys(manifest).filter(\n (id) => !currentStoryIds.has(id),\n );\n for (const id of orphanIds) {\n const imagePath = goldenImagePath(goldenDir, id);\n if (existsSync(imagePath)) unlinkSync(imagePath);\n await updateManifestEntry(goldenDir, id, () => undefined);\n }\n return orphanIds;\n}\n","import { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { SourceOfTruthGoldenLocation } from \"../config.js\";\nimport {\n goldenImagePath,\n loadManifest,\n manifestPath,\n type GoldenManifest,\n} from \"./manifestStore.js\";\nimport { validateGoldenManifest } from \"./validateManifest.js\";\n\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 * Returns `null` — degrading the divergence check to a skip, not a failure —\n * when no golden baseline exists yet for this version, or the registry/repo\n * is unreachable.\n */\nexport async function resolveSourceOfTruthGolden(\n location: SourceOfTruthGoldenLocation,\n): Promise<SourceOfTruthGolden | null> {\n if (location.type === \"local\") {\n if (!existsSync(manifestPath(location.dir))) {\n console.warn(\n `No golden baseline found yet at ${location.dir} — source-of-truth divergence check skipped for this run.`,\n );\n return null;\n }\n const manifest = loadManifest(location.dir);\n return {\n manifest,\n async readImage(storyId) {\n const path = goldenImagePath(location.dir, storyId);\n return existsSync(path) ? readFileSync(path) : null;\n },\n };\n }\n\n let version: string;\n try {\n version = await resolveNpmVersion(\n location.packageName,\n location.versionSpec,\n );\n } catch (error) {\n console.warn(\n `Could not resolve ${location.packageName}@${location.versionSpec} — source-of-truth divergence check skipped for this run.`,\n error,\n );\n return null;\n }\n\n const cacheDir = join(location.cacheDir, version);\n const tag = `${location.packageName}@${version}`;\n // @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 console.warn(\n `Could not reach GitHub to fetch ${tag}'s golden baseline — source-of-truth divergence check skipped for this run.`,\n error,\n );\n return null;\n }\n if (!response.ok) {\n console.warn(\n `No golden baseline published for ${tag} — source-of-truth divergence check skipped for this run.`,\n );\n return null;\n }\n const text = await response.text();\n const parsed = JSON.parse(text);\n validateGoldenManifest(parsed, `${rawBase}/manifest.json`);\n mkdirSync(cacheDir, { recursive: true });\n writeFileSync(cachedManifestPath, text);\n manifest = parsed;\n }\n\n return {\n manifest,\n async readImage(storyId) {\n const cachedImagePath = goldenImagePath(cacheDir, storyId);\n if (existsSync(cachedImagePath)) return readFileSync(cachedImagePath);\n const response = await fetch(`${rawBase}/${storyId}.png`);\n if (!response.ok) return null;\n const buffer = Buffer.from(await response.arrayBuffer());\n mkdirSync(cacheDir, { recursive: true });\n writeFileSync(cachedImagePath, buffer);\n return buffer;\n },\n };\n}\n","import { expect } from \"@playwright/test\";\nimport type { Browser, TestInfo } from \"@playwright/test\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport type { AdapterTesterConfig } from \"../config.js\";\nimport { diffPngBuffers } from \"../golden/diffPng.js\";\nimport {\n type GoldenManifestEntry,\n goldenImagePath,\n loadManifest,\n pruneOrphanedGoldens,\n saveGoldenImage,\n updateManifestEntry,\n} from \"../golden/manifestStore.js\";\nimport { resolveSourceOfTruthGolden } from \"../golden/resolveSourceOfTruthGolden.js\";\n\nconst DEFAULT_EXCLUDE_TITLE_PREFIXES = [\"Theme\", \"Tokens\", \"Introduction\"];\n\ninterface StorybookEntry {\n type: string;\n id: string;\n name: string;\n title: string;\n}\n\nfunction matchesPrefix(id: string, prefix: string): boolean {\n return id === prefix || id.startsWith(prefix);\n}\n\n/** Fetches every story `target`'s Storybook currently has, filtered only by\n * `excludeTitlePrefixes` — not `stories.<id>.exclude`, so callers can still\n * tell an excluded story apart from one that's genuinely missing. */\nasync function fetchStories(\n target: { name: string; url: string },\n excludeTitlePrefixes: string[],\n): Promise<StorybookEntry[]> {\n let stories: StorybookEntry[];\n try {\n const response = await fetch(`${target.url}/index.json`);\n if (!response.ok) {\n throw new Error(\n `Failed to fetch Storybook index: ${response.statusText}`,\n );\n }\n const data = (await response.json()) as any;\n const entries = data.entries || {};\n stories = Object.values(entries).filter(\n (entry: any) =>\n entry.type === \"story\" &&\n !excludeTitlePrefixes.some(\n (prefix) =>\n entry.title === prefix || entry.title.startsWith(`${prefix}/`),\n ),\n ) as StorybookEntry[];\n stories.sort((a, b) => a.id.localeCompare(b.id));\n } catch (error) {\n console.error(\n \"Failed to load Storybook index from\",\n `${target.url}/index.json`,\n error,\n );\n throw new Error(\n `Storybook target \"${target.name}\" is not responsive or index.json is missing. Please ensure its Storybook is running.`,\n );\n }\n return stories;\n}\n\n/** Resolves the diff threshold for `storyId`: the longest (most specific)\n * `storyThresholds` key matching by prefix, falling back to\n * `defaultThresholdPixels` when nothing matches. */\nfunction resolveThreshold(\n storyId: string,\n storyThresholds: Record<string, number>,\n defaultThresholdPixels: number,\n): number {\n let bestMatch: string | undefined;\n for (const prefix of Object.keys(storyThresholds)) {\n if (\n matchesPrefix(storyId, prefix) &&\n (!bestMatch || prefix.length > bestMatch.length)\n ) {\n bestMatch = prefix;\n }\n }\n return bestMatch !== undefined\n ? storyThresholds[bestMatch]!\n : defaultThresholdPixels;\n}\n\n/** Everything a generated Playwright spec needs to register the golden-image\n * suite itself. Split out from the actual `test.describe`/`test` calls so\n * those calls execute in the spec file that imports this, not in this\n * library file — otherwise Playwright's HTML report groups every story under\n * this file's own (sourcemapped) path instead of a stable spec name. */\nexport interface VisualRegressionPlan {\n /** `config`'s own (non-source-of-truth) target name, for the suite title. */\n ownTargetName: string;\n /** Suite title suffix describing which check mode is running. */\n suiteLabel: string;\n /** Stories to check, already filtered and sorted by id. */\n stories: StorybookEntry[];\n /** Story ids the source-of-truth adapter has a golden for but this\n * project's own Storybook doesn't — empty outside `checkMode: \"divergence\"`\n * or when `sourceOfTruthGolden` didn't resolve. Excludes ids covered by a\n * `stories.<id>.exclude` entry: an intentional gap, not a sync failure. */\n missingFromSourceOfTruth: string[];\n /** Golden-checks one story. Call this from inside a `test(story.id, ...)`\n * body — safe to run concurrently across Playwright workers, since each\n * call only ever reads/writes its own story's manifest entry (locked at\n * the point it writes it back, so concurrent workers never race each\n * other's entries — see `updateManifestEntry`). */\n checkStory: (\n story: StorybookEntry,\n browser: Browser,\n testInfo: TestInfo,\n ) => Promise<void>;\n}\n\n/** Everything a generated Playwright spec needs to register the golden-image\n * suite itself. Split out from the actual `test.describe`/`test` calls so\n * those calls execute in the spec file that imports this, not in this\n * library file — otherwise Playwright's HTML report groups every story under\n * this file's own (sourcemapped) path instead of a stable spec name. */\nexport interface VisualRegressionPlan {\n /** `config`'s own (non-source-of-truth) target name, for the suite title. */\n ownTargetName: string;\n /** Suite title suffix describing which check mode is running. */\n suiteLabel: string;\n /** Stories to check, already filtered and sorted by id. */\n stories: StorybookEntry[];\n /** Golden-checks one story. Call this from inside a `test(story.id, ...)`\n * body — safe to run concurrently across Playwright workers, since each\n * call only ever reads/writes its own story's manifest entry (locked at\n * the point it writes it back, so concurrent workers never race each\n * other's entries — see `updateManifestEntry`). */\n checkStory: (\n story: StorybookEntry,\n browser: Browser,\n testInfo: TestInfo,\n ) => Promise<void>;\n}\n\n/**\n * Resolves the golden-image plan for `config`'s own target (the one target\n * in `config.targets` not marked `sourceOfTruth`).\n *\n * Two independent checks per story, gated by `config.checkMode`. Neither\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 : sourceOfTruthGolden\n ? `[adapter-tester] sourceOfTruthGolden: resolved, ${Object.keys(sourceOfTruthGolden.manifest).length} golden(s) available (config: ${JSON.stringify(config.sourceOfTruthGolden)})`\n : `[adapter-tester] sourceOfTruthGolden: unavailable — no baseline found or unreachable (config: ${JSON.stringify(config.sourceOfTruthGolden)}); divergence check will skip every story`\n : `[adapter-tester] sourceOfTruthGolden: not used in \"own\" checkMode`,\n `[adapter-tester] stories: ${stories.length} to check (excluded: ${excludeStoryIds.length}, title prefixes excluded: ${excludeTitlePrefixes.join(\", \") || \"none\"})`,\n `[adapter-tester] story parity with source of truth: ${missingFromSourceOfTruth.length === 0 ? \"OK\" : `${missingFromSourceOfTruth.length} missing (see error above)`}`,\n ].join(\"\\n\"),\n );\n\n const suiteLabel =\n checkMode === \"divergence\"\n ? \"Source-of-Truth Divergence Check\"\n : \"Own-Drift Golden Image Check\";\n\n return {\n ownTargetName: ownTarget.name,\n suiteLabel,\n stories,\n missingFromSourceOfTruth,\n checkStory: async (story, browser, testInfo) => {\n const page = await browser.newPage();\n await page.setViewportSize({ width: 800, height: 600 });\n await page.goto(\n `${ownTarget.url}/iframe.html?id=${story.id}&viewMode=story`,\n { waitUntil: \"networkidle\" },\n );\n await page.waitForSelector(\"#storybook-root\");\n await page.addStyleTag({\n content: `* { -webkit-font-smoothing: antialiased !important; -moz-osx-font-smoothing: grayscale !important; }`,\n });\n // 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.cjs","sources":["../src/golden/diffPng.ts","../src/golden/validateManifest.ts","../src/golden/manifestStore.ts","../src/golden/resolveSourceOfTruthGolden.ts","../src/testing/runVisualRegression.ts"],"sourcesContent":["import pixelmatch from \"pixelmatch\";\nimport { PNG } from \"pngjs\";\n\nexport interface PngDiffResult {\n /** Mismatched-pixel count, or `Infinity` if the two images aren't even the\n * same dimensions — pixelmatch itself throws on a size mismatch, and a size\n * mismatch is itself a real difference, not something to swallow. */\n diffPixels: number;\n /** Visual highlight of the mismatched pixels, encoded as a PNG buffer.\n * `null` when `diffPixels` is `Infinity` — there's no pixel-aligned diff to\n * render across two different-sized images. */\n diffImage: Buffer | null;\n}\n\n/** Pixel-diffs two PNG buffers. */\nexport function diffPngBuffers(a: Buffer, b: Buffer): PngDiffResult {\n const imgA = PNG.sync.read(a);\n const imgB = PNG.sync.read(b);\n if (imgA.width !== imgB.width || imgA.height !== imgB.height) {\n return { diffPixels: Infinity, diffImage: null };\n }\n const diff = new PNG({ width: imgA.width, height: imgA.height });\n const diffPixels = pixelmatch(\n imgA.data,\n imgB.data,\n diff.data,\n imgA.width,\n imgA.height,\n { threshold: 0.1 },\n );\n return { diffPixels, diffImage: PNG.sync.write(diff) };\n}\n","import { Ajv } from \"ajv\";\nimport * as ajvFormatsModule from \"ajv-formats\";\nimport type { FormatsPlugin } from \"ajv-formats\";\nimport schema from \"./manifest.schema.json\" with { type: \"json\" };\n\n// See validateFileConfig.ts for why `.default` has to be unwrapped by hand.\nconst addFormats = (ajvFormatsModule as unknown as { default: FormatsPlugin })\n .default;\n\nconst ajv = new Ajv({ allErrors: true, strict: true });\naddFormats(ajv);\nconst validate = ajv.compile(schema);\n\n/**\n * Validates a parsed `test/golden/manifest.json` against `manifest.schema.json`.\n * Throws with every violation listed — callers must not silently coerce or\n * drop invalid entries.\n */\nexport function validateGoldenManifest(data: unknown, path: string): void {\n if (validate(data)) return;\n\n const errors = (validate.errors ?? [])\n .map((error) => {\n const extra = error.params?.additionalProperty\n ? ` '${error.params.additionalProperty}'`\n : \"\";\n return ` - ${error.instancePath || \"root\"} ${error.message}${extra}`;\n })\n .join(\"\\n\");\n throw new Error(`Invalid ${path}:\\n${errors}`);\n}\n","import {\n closeSync,\n existsSync,\n mkdirSync,\n openSync,\n readFileSync,\n renameSync,\n rmSync,\n unlinkSync,\n writeFileSync,\n} from \"node:fs\";\nimport { join } from \"node:path\";\nimport { validateGoldenManifest } from \"./validateManifest.js\";\n\nexport interface GoldenManifestEntry {\n createdAt: string;\n sourceOfTruthCreatedAt?: string;\n}\n\nexport type GoldenManifest = Record<string, GoldenManifestEntry>;\n\nexport function manifestPath(goldenDir: string): string {\n return join(goldenDir, \"manifest.json\");\n}\n\nfunction manifestLockPath(goldenDir: string): string {\n return join(goldenDir, \"manifest.json.lock\");\n}\n\nexport function goldenImagePath(goldenDir: string, storyId: string): string {\n return join(goldenDir, `${storyId}.png`);\n}\n\n/** Returns `{}` if no manifest exists yet — a fresh adapter with no goldens\n * captured is the normal starting state, not an error. */\nexport function loadManifest(goldenDir: string): GoldenManifest {\n const path = manifestPath(goldenDir);\n if (!existsSync(path)) return {};\n const data = JSON.parse(readFileSync(path, \"utf8\"));\n validateGoldenManifest(data, path);\n return data;\n}\n\n/** Validates before writing, and sorts keys so the diff on a reviewed PR is\n * stable regardless of the order stories happened to run in. Writes to a\n * temp file and renames over the real one — `rename` is atomic, so a\n * concurrent `loadManifest` (running in another Playwright worker) never\n * observes a half-written file. */\nexport function saveManifest(\n goldenDir: string,\n manifest: GoldenManifest,\n): void {\n const path = manifestPath(goldenDir);\n validateGoldenManifest(manifest, path);\n const sorted: GoldenManifest = {};\n for (const key of Object.keys(manifest).sort()) {\n sorted[key] = manifest[key]!;\n }\n mkdirSync(goldenDir, { recursive: true });\n const tmpPath = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;\n writeFileSync(tmpPath, JSON.stringify(sorted, null, 2) + \"\\n\");\n renameSync(tmpPath, path);\n}\n\nconst LOCK_RETRY_MS = 25;\nconst LOCK_TIMEOUT_MS = 15_000;\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/** Exclusive-create the lock file, spin-retrying until it's free. `wx` fails\n * atomically (`EEXIST`) if another worker process already holds it — that's\n * the only signal we need, no third-party lock library required for a\n * same-machine, same-run lock like this. */\nasync function acquireManifestLock(goldenDir: string): Promise<void> {\n mkdirSync(goldenDir, { recursive: true });\n const path = manifestLockPath(goldenDir);\n const deadline = Date.now() + LOCK_TIMEOUT_MS;\n for (;;) {\n try {\n closeSync(openSync(path, \"wx\"));\n return;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"EEXIST\") throw error;\n if (Date.now() >= deadline) {\n throw new Error(\n `Timed out waiting for the manifest lock at \"${path}\" — delete it if a previous run crashed while holding it.`,\n );\n }\n await sleep(LOCK_RETRY_MS);\n }\n }\n}\n\nfunction releaseManifestLock(goldenDir: string): void {\n rmSync(manifestLockPath(goldenDir), { force: true });\n}\n\n/** Runs `updater` against this story's manifest entry under an exclusive\n * lock on `manifest.json`: reloads the manifest fresh, applies `updater`,\n * and saves it back, all before releasing the lock. Concurrent Playwright\n * workers each own a different story, so this is the only section that\n * needs to serialize — everything else about a story (its screenshot, its\n * golden image file, its diff) is independent of every other story. */\nexport async function updateManifestEntry(\n goldenDir: string,\n storyId: string,\n updater: (\n entry: GoldenManifestEntry | undefined,\n ) => GoldenManifestEntry | undefined,\n): Promise<GoldenManifestEntry | undefined> {\n await acquireManifestLock(goldenDir);\n try {\n const manifest = loadManifest(goldenDir);\n const nextEntry = updater(manifest[storyId]);\n if (nextEntry === undefined) {\n delete manifest[storyId];\n } else {\n manifest[storyId] = nextEntry;\n }\n saveManifest(goldenDir, manifest);\n return nextEntry;\n } finally {\n releaseManifestLock(goldenDir);\n }\n}\n\nexport function saveGoldenImage(\n goldenDir: string,\n storyId: string,\n buffer: Buffer,\n): void {\n mkdirSync(goldenDir, { recursive: true });\n writeFileSync(goldenImagePath(goldenDir, storyId), buffer);\n}\n\n/** Removes the golden `.png` + manifest entry for every story id in the\n * manifest that isn't in `currentStoryIds` (e.g. a story renamed or deleted\n * from Storybook) — otherwise those never get cleaned up on their own,\n * since a run only ever adds/updates entries for stories it actually saw.\n * Returns the pruned ids, for the caller to report. Both the file removal\n * and the manifest delete are idempotent, so it's safe for this to run\n * redundantly from more than one Playwright worker. */\nexport async function pruneOrphanedGoldens(\n goldenDir: string,\n currentStoryIds: ReadonlySet<string>,\n): Promise<string[]> {\n const manifest = loadManifest(goldenDir);\n const orphanIds = Object.keys(manifest).filter(\n (id) => !currentStoryIds.has(id),\n );\n for (const id of orphanIds) {\n const imagePath = goldenImagePath(goldenDir, id);\n if (existsSync(imagePath)) unlinkSync(imagePath);\n await updateManifestEntry(goldenDir, id, () => undefined);\n }\n return orphanIds;\n}\n","import { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { SourceOfTruthGoldenLocation } from \"../config.js\";\nimport {\n goldenImagePath,\n loadManifest,\n manifestPath,\n type GoldenManifest,\n} from \"./manifestStore.js\";\nimport { validateGoldenManifest } from \"./validateManifest.js\";\n\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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"}
package/dist/testing.js CHANGED
@@ -1,15 +1,15 @@
1
1
  import { expect as D } from "@playwright/test";
2
- import { existsSync as v, unlinkSync as H, readFileSync as E, mkdirSync as b, writeFileSync as A, closeSync as Y, openSync as Q, renameSync as Z, rmSync as ee } from "node:fs";
3
- import te from "pixelmatch";
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";
3
+ import re from "pixelmatch";
4
4
  import { PNG as j } from "pngjs";
5
5
  import { join as M } from "node:path";
6
- import { a as re, i as oe } from "./index-DqQzFSAH.js";
7
- function J(e, t) {
6
+ import { a as oe, i as ne } from "./index-DqQzFSAH.js";
7
+ function R(e, t) {
8
8
  const o = j.sync.read(e), r = j.sync.read(t);
9
9
  if (o.width !== r.width || o.height !== r.height)
10
10
  return { diffPixels: 1 / 0, diffImage: null };
11
11
  const s = new j({ width: o.width, height: o.height });
12
- return { diffPixels: te(
12
+ return { diffPixels: re(
13
13
  o.data,
14
14
  r.data,
15
15
  s.data,
@@ -18,21 +18,21 @@ function J(e, t) {
18
18
  { threshold: 0.1 }
19
19
  ), diffImage: j.sync.write(s) };
20
20
  }
21
- const ne = "http://json-schema.org/draft-07/schema#", se = "https://github.com/borderux/recursica/tree/main/packages/adapter-tester/src/golden/manifest.schema.json", ie = "test/golden/manifest.json", ae = "Tracks golden (baseline) image metadata for @recursica/adapter-tester's golden-image visual regression checks. One manifest lives alongside its adapter's test/golden/<story-id>.png files, keyed by story id.", ce = "object", de = { type: "object", additionalProperties: !1, required: ["createdAt"], properties: { createdAt: { type: "string", format: "date-time", description: "When this story's own golden PNG was last captured, via --update-golden, --approve-divergence, or first-run auto-create." }, sourceOfTruthCreatedAt: { type: "string", format: "date-time", description: "The source-of-truth adapter's (mantine) manifest `createdAt` for this story at the time this adapter's divergence from it was last reviewed via --approve-divergence. Omitted on the source-of-truth adapter's own manifest, and omitted here until the first approval. If mantine's current `createdAt` for this story is newer than this value, the divergence is flagged again for re-review." } } }, he = {
22
- $schema: ne,
23
- $id: se,
24
- title: ie,
25
- description: ae,
26
- type: ce,
27
- additionalProperties: de
28
- }, ue = oe, K = new re.Ajv({ allErrors: !0, strict: !0 });
29
- ue(K);
30
- const R = K.compile(he);
21
+ const se = "http://json-schema.org/draft-07/schema#", ie = "https://github.com/borderux/recursica/tree/main/packages/adapter-tester/src/golden/manifest.schema.json", ae = "test/golden/manifest.json", ce = "Tracks golden (baseline) image metadata for @recursica/adapter-tester's golden-image visual regression checks. One manifest lives alongside its adapter's test/golden/<story-id>.png files, keyed by story id.", de = "object", he = { type: "object", additionalProperties: !1, required: ["createdAt"], properties: { createdAt: { type: "string", format: "date-time", description: "When this story's own golden PNG was last captured, via --update-golden, --approve-divergence, or first-run auto-create." }, sourceOfTruthCreatedAt: { type: "string", format: "date-time", description: "The source-of-truth adapter's (mantine) manifest `createdAt` for this story at the time this adapter's divergence from it was last reviewed via --approve-divergence. Omitted on the source-of-truth adapter's own manifest, and omitted here until the first approval. If mantine's current `createdAt` for this story is newer than this value, the divergence is flagged again for re-review." } } }, ue = {
22
+ $schema: se,
23
+ $id: ie,
24
+ title: ae,
25
+ description: ce,
26
+ type: de,
27
+ additionalProperties: he
28
+ }, fe = ne, q = new oe.Ajv({ allErrors: !0, strict: !0 });
29
+ fe(q);
30
+ const U = q.compile(ue);
31
31
  function L(e, t) {
32
- if (R(e)) return;
33
- const o = (R.errors ?? []).map((r) => {
34
- var i;
35
- const s = (i = r.params) != null && i.additionalProperty ? ` '${r.params.additionalProperty}'` : "";
32
+ if (U(e)) return;
33
+ const o = (U.errors ?? []).map((r) => {
34
+ var a;
35
+ const s = (a = r.params) != null && a.additionalProperty ? ` '${r.params.additionalProperty}'` : "";
36
36
  return ` - ${r.instancePath || "root"} ${r.message}${s}`;
37
37
  }).join(`
38
38
  `);
@@ -42,7 +42,7 @@ ${o}`);
42
42
  function G(e) {
43
43
  return M(e, "manifest.json");
44
44
  }
45
- function q(e) {
45
+ function V(e) {
46
46
  return M(e, "manifest.json.lock");
47
47
  }
48
48
  function S(e, t) {
@@ -50,7 +50,7 @@ function S(e, t) {
50
50
  }
51
51
  function P(e) {
52
52
  const t = G(e);
53
- if (!v(t)) return {};
53
+ if (!y(t)) return {};
54
54
  const o = JSON.parse(E(t, "utf8"));
55
55
  return L(o, t), o;
56
56
  }
@@ -58,23 +58,23 @@ function le(e, t) {
58
58
  const o = G(e);
59
59
  L(t, o);
60
60
  const r = {};
61
- for (const i of Object.keys(t).sort())
62
- r[i] = t[i];
61
+ for (const a of Object.keys(t).sort())
62
+ r[a] = t[a];
63
63
  b(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
- `), Z(s, o);
66
+ `), ee(s, o);
67
67
  }
68
- const fe = 25, pe = 15e3;
69
- function me(e) {
68
+ const pe = 25, me = 15e3;
69
+ function ge(e) {
70
70
  return new Promise((t) => setTimeout(t, e));
71
71
  }
72
- async function ge(e) {
72
+ async function we(e) {
73
73
  b(e, { recursive: !0 });
74
- const t = q(e), o = Date.now() + pe;
74
+ const t = V(e), o = Date.now() + me;
75
75
  for (; ; )
76
76
  try {
77
- Y(Q(t, "wx"));
77
+ Q(Z(t, "wx"));
78
78
  return;
79
79
  } catch (r) {
80
80
  if (r.code !== "EEXIST") throw r;
@@ -82,44 +82,44 @@ async function ge(e) {
82
82
  throw new Error(
83
83
  `Timed out waiting for the manifest lock at "${t}" — delete it if a previous run crashed while holding it.`
84
84
  );
85
- await me(fe);
85
+ await ge(pe);
86
86
  }
87
87
  }
88
- function we(e) {
89
- ee(q(e), { force: !0 });
88
+ function ye(e) {
89
+ te(V(e), { force: !0 });
90
90
  }
91
- async function V(e, t, o) {
92
- await ge(e);
91
+ async function W(e, t, o) {
92
+ await we(e);
93
93
  try {
94
94
  const r = P(e), s = o(r[t]);
95
95
  return s === void 0 ? delete r[t] : r[t] = s, le(e, r), s;
96
96
  } finally {
97
- we(e);
97
+ ye(e);
98
98
  }
99
99
  }
100
- function ye(e, t, o) {
100
+ function ve(e, t, o) {
101
101
  b(e, { recursive: !0 }), A(S(e, t), o);
102
102
  }
103
- async function ve(e, t) {
103
+ async function Te(e, t) {
104
104
  const o = P(e), r = Object.keys(o).filter(
105
105
  (s) => !t.has(s)
106
106
  );
107
107
  for (const s of r) {
108
- const i = S(e, s);
109
- v(i) && H(i), await V(e, s, () => {
108
+ const a = S(e, s);
109
+ y(a) && Y(a), await W(e, s, () => {
110
110
  });
111
111
  }
112
112
  return r;
113
113
  }
114
- const Te = "borderux/recursica-adapter-mantine-v8";
114
+ const J = "borderux/recursica-adapter-mantine-v8";
115
115
  async function $e(e, t) {
116
- var i, d;
116
+ var a, d;
117
117
  const o = await fetch(`https://registry.npmjs.org/${e}`);
118
118
  if (!o.ok)
119
119
  throw new Error(
120
120
  `Could not reach npm registry for ${e}: ${o.statusText}`
121
121
  );
122
- const r = await o.json(), s = ((i = r["dist-tags"]) == null ? void 0 : i[t]) ?? ((d = r.versions) != null && d[t] ? t : void 0);
122
+ const r = await o.json(), s = ((a = r["dist-tags"]) == null ? void 0 : a[t]) ?? ((d = r.versions) != null && d[t] ? t : void 0);
123
123
  if (!s)
124
124
  throw new Error(
125
125
  `${e} has no version or dist-tag "${t}" on the npm registry.`
@@ -127,67 +127,70 @@ async function $e(e, t) {
127
127
  return s;
128
128
  }
129
129
  async function Oe(e) {
130
- if (e.type === "local")
131
- return v(G(e.dir)) ? {
130
+ if (e.type === "local") {
131
+ if (!y(G(e.dir)))
132
+ throw new Error(
133
+ `No golden baseline found at ${e.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.`
134
+ );
135
+ return {
132
136
  manifest: P(e.dir),
133
137
  async readImage(h) {
134
138
  const c = S(e.dir, h);
135
- return v(c) ? E(c) : null;
139
+ return y(c) ? E(c) : null;
136
140
  }
137
- } : (console.warn(
138
- `No golden baseline found yet at ${e.dir} — source-of-truth divergence check skipped for this run.`
139
- ), null);
141
+ };
142
+ }
140
143
  let t;
141
144
  try {
142
145
  t = await $e(
143
146
  e.packageName,
144
147
  e.versionSpec
145
148
  );
146
- } catch (a) {
147
- return console.warn(
148
- `Could not resolve ${e.packageName}@${e.versionSpec} — source-of-truth divergence check skipped for this run.`,
149
- a
150
- ), null;
149
+ } catch (i) {
150
+ throw new Error(
151
+ `Could not resolve ${e.packageName}@${e.versionSpec} on the npm registry — the source-of-truth divergence check has nothing to compare against.`,
152
+ { cause: i }
153
+ );
151
154
  }
152
- const o = M(e.cacheDir, t), r = `${e.packageName}@${t}`, s = `https://raw.githubusercontent.com/${Te}/${r}/test/golden`;
153
- let i;
155
+ const o = M(e.cacheDir, t), r = `${e.packageName}@${t}`, s = `https://raw.githubusercontent.com/${J}/${r}/test/golden`;
156
+ let a;
154
157
  const d = G(o);
155
- if (v(d))
156
- i = P(o);
158
+ if (y(d))
159
+ a = P(o);
157
160
  else {
158
- let a;
161
+ let i;
159
162
  try {
160
- a = await fetch(`${s}/manifest.json`);
163
+ i = await fetch(`${s}/manifest.json`);
161
164
  } catch (m) {
162
- return console.warn(
163
- `Could not reach GitHub to fetch ${r}'s golden baseline — source-of-truth divergence check skipped for this run.`,
164
- m
165
- ), null;
165
+ throw new Error(
166
+ `Could not reach GitHub to fetch ${r}'s golden baseline (${s}/manifest.json) — the source-of-truth divergence check has nothing to compare against.`,
167
+ { cause: m }
168
+ );
166
169
  }
167
- if (!a.ok)
168
- return console.warn(
169
- `No golden baseline published for ${r} — source-of-truth divergence check skipped for this run.`
170
- ), null;
171
- const h = await a.text(), c = JSON.parse(h);
172
- L(c, `${s}/manifest.json`), b(o, { recursive: !0 }), A(d, h), i = c;
170
+ if (!i.ok)
171
+ throw new Error(
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
+ );
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;
173
176
  }
174
177
  return {
175
- manifest: i,
176
- async readImage(a) {
177
- const h = S(o, a);
178
- if (v(h)) return E(h);
179
- const c = await fetch(`${s}/${a}.png`);
178
+ manifest: a,
179
+ async readImage(i) {
180
+ const h = S(o, i);
181
+ if (y(h)) return E(h);
182
+ const c = await fetch(`${s}/${i}.png`);
180
183
  if (!c.ok) return null;
181
184
  const m = Buffer.from(await c.arrayBuffer());
182
185
  return b(o, { recursive: !0 }), A(h, m), m;
183
186
  }
184
187
  };
185
188
  }
186
- const ke = ["Theme", "Tokens", "Introduction"];
189
+ const xe = ["Theme", "Tokens", "Introduction"];
187
190
  function N(e, t) {
188
191
  return e === t || e.startsWith(t);
189
192
  }
190
- async function xe(e, t) {
193
+ async function ke(e, t) {
191
194
  let o;
192
195
  try {
193
196
  const r = await fetch(`${e.url}/index.json`);
@@ -195,12 +198,12 @@ async function xe(e, t) {
195
198
  throw new Error(
196
199
  `Failed to fetch Storybook index: ${r.statusText}`
197
200
  );
198
- const i = (await r.json()).entries || {};
199
- o = Object.values(i).filter(
201
+ const a = (await r.json()).entries || {};
202
+ o = Object.values(a).filter(
200
203
  (d) => d.type === "story" && !t.some(
201
- (a) => d.title === a || d.title.startsWith(`${a}/`)
204
+ (i) => d.title === i || d.title.startsWith(`${i}/`)
202
205
  )
203
- ), o.sort((d, a) => d.id.localeCompare(a.id));
206
+ ), o.sort((d, i) => d.id.localeCompare(i.id));
204
207
  } catch (r) {
205
208
  throw console.error(
206
209
  "Failed to load Storybook index from",
@@ -212,7 +215,7 @@ async function xe(e, t) {
212
215
  }
213
216
  return o;
214
217
  }
215
- function U(e, t, o) {
218
+ function K(e, t, o) {
216
219
  let r;
217
220
  for (const s of Object.keys(t))
218
221
  N(e, s) && (!r || s.length > r.length) && (r = s);
@@ -224,26 +227,26 @@ async function Ge(e) {
224
227
  throw new Error(
225
228
  "adapter-tester config has no non-sourceOfTruth target to run the golden check against."
226
229
  );
227
- const o = e.excludeTitlePrefixes ?? ke, r = e.stories ?? {}, s = Object.keys(r).filter(
230
+ const o = e.excludeTitlePrefixes ?? xe, r = e.stories ?? {}, s = Object.keys(r).filter(
228
231
  (n) => r[n].exclude
229
- ), i = Object.fromEntries(
232
+ ), a = Object.fromEntries(
230
233
  Object.entries(r).filter(([, n]) => n.goldenThreshold !== void 0).map(([n, u]) => [n, u.goldenThreshold])
231
234
  ), d = Object.fromEntries(
232
235
  Object.entries(r).filter(([, n]) => n.sourceOfTruthThreshold !== void 0).map(([n, u]) => [n, u.sourceOfTruthThreshold])
233
- ), a = e.goldenDir, h = e.goldenMode, c = e.checkMode, m = await xe(t, o), I = m.filter(
236
+ ), i = e.goldenDir, h = e.goldenMode, c = e.checkMode, m = await ke(t, o), C = m.filter(
234
237
  (n) => !s.some((u) => N(n.id, u))
235
238
  );
236
239
  if (h === "update-golden") {
237
- const n = await ve(
238
- a,
239
- new Set(I.map((u) => u.id))
240
+ const n = await Te(
241
+ i,
242
+ new Set(C.map((u) => u.id))
240
243
  );
241
244
  n.length > 0 && console.warn(
242
245
  `Pruned ${n.length} orphaned golden(s) no longer in Storybook: ${n.join(", ")}`
243
246
  );
244
247
  }
245
- const g = c !== "divergence" || e.isSourceOfTruthAdapter || !e.sourceOfTruthGolden ? null : await Oe(e.sourceOfTruthGolden), W = new Set(m.map((n) => n.id)), T = g ? Object.keys(g.manifest).filter(
246
- (n) => !W.has(n) && !s.some((u) => N(n, u))
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))
247
250
  ).sort() : [];
248
251
  T.length > 0 && console.error(
249
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.`
@@ -254,111 +257,116 @@ async function Ge(e) {
254
257
  `[adapter-tester] goldenMode: "${h}"`,
255
258
  `[adapter-tester] goldenThresholdPixels: ${e.goldenThresholdPixels}`,
256
259
  `[adapter-tester] sourceOfTruthThresholdPixels: ${e.sourceOfTruthThresholdPixels}`,
257
- c === "divergence" ? e.isSourceOfTruthAdapter ? "[adapter-tester] sourceOfTruthGolden: skipped — this is the source-of-truth adapter, nothing to diverge from" : e.sourceOfTruthGolden ? g ? `[adapter-tester] sourceOfTruthGolden: resolved, ${Object.keys(g.manifest).length} golden(s) available (config: ${JSON.stringify(e.sourceOfTruthGolden)})` : `[adapter-tester] sourceOfTruthGolden: unavailable — no baseline found or unreachable (config: ${JSON.stringify(e.sourceOfTruthGolden)}); divergence check will skip every story` : "[adapter-tester] sourceOfTruthGolden: skipped — no sourceOfTruthGolden configured" : '[adapter-tester] sourceOfTruthGolden: not used in "own" checkMode',
258
- `[adapter-tester] stories: ${I.length} to check (excluded: ${s.length}, title prefixes excluded: ${o.join(", ") || "none"})`,
260
+ c === "divergence" ? e.isSourceOfTruthAdapter ? "[adapter-tester] sourceOfTruthGolden: skipped — this is the source-of-truth adapter, nothing to diverge from" : e.sourceOfTruthGolden ? (
261
+ // Unresolvable-baseline case never reaches here — resolveSourceOfTruthGolden
262
+ // throws instead of returning null, failing the whole run before this
263
+ // logs, rather than letting the divergence check silently no-op.
264
+ `[adapter-tester] sourceOfTruthGolden: resolved, ${Object.keys(v.manifest).length} golden(s) available (config: ${JSON.stringify(e.sourceOfTruthGolden)})`
265
+ ) : "[adapter-tester] sourceOfTruthGolden: skipped — no sourceOfTruthGolden configured" : '[adapter-tester] sourceOfTruthGolden: not used in "own" checkMode',
266
+ `[adapter-tester] stories: ${C.length} to check (excluded: ${s.length}, title prefixes excluded: ${o.join(", ") || "none"})`,
259
267
  `[adapter-tester] story parity with source of truth: ${T.length === 0 ? "OK" : `${T.length} missing (see error above)`}`
260
268
  ].join(`
261
269
  `)
262
270
  );
263
- const X = c === "divergence" ? "Source-of-Truth Divergence Check" : "Own-Drift Golden Image Check";
271
+ const z = c === "divergence" ? "Source-of-Truth Divergence Check" : "Own-Drift Golden Image Check";
264
272
  return {
265
273
  ownTargetName: t.name,
266
- suiteLabel: X,
267
- stories: I,
274
+ suiteLabel: z,
275
+ stories: C,
268
276
  missingFromSourceOfTruth: T,
269
277
  checkStory: async (n, u, p) => {
270
- const w = await u.newPage();
271
- await w.setViewportSize({ width: 800, height: 600 }), await w.goto(
278
+ const g = await u.newPage();
279
+ await g.setViewportSize({ width: 800, height: 600 }), await g.goto(
272
280
  `${t.url}/iframe.html?id=${n.id}&viewMode=story`,
273
281
  { waitUntil: "networkidle" }
274
- ), await w.waitForSelector("#storybook-root"), await w.addStyleTag({
282
+ ), await g.waitForSelector("#storybook-root"), await g.addStyleTag({
275
283
  content: "* { -webkit-font-smoothing: antialiased !important; -moz-osx-font-smoothing: grayscale !important; }"
276
- }), await w.evaluate(
284
+ }), await g.evaluate(
277
285
  () => Promise.all(
278
286
  Array.from(
279
287
  document.querySelectorAll(
280
288
  'link[rel="stylesheet"]'
281
289
  )
282
290
  ).map(
283
- (l) => l.sheet ? Promise.resolve() : new Promise((f) => {
284
- l.addEventListener("load", () => f(), {
291
+ (f) => f.sheet ? Promise.resolve() : new Promise((l) => {
292
+ f.addEventListener("load", () => l(), {
285
293
  once: !0
286
- }), l.addEventListener("error", () => f(), {
294
+ }), f.addEventListener("error", () => l(), {
287
295
  once: !0
288
296
  });
289
297
  })
290
298
  )
291
299
  ).then(() => document.fonts.ready)
292
- ), await w.waitForTimeout(300);
293
- const x = await w.screenshot(), B = S(a, n.id), y = P(a)[n.id], z = h !== "check" || !y || !v(B);
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);
294
302
  let $;
295
- if (z)
296
- ye(a, n.id, x), $ = y != null && y.sourceOfTruthCreatedAt ? {
303
+ if (H)
304
+ ve(i, n.id, k), $ = w != null && w.sourceOfTruthCreatedAt ? {
297
305
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
298
- sourceOfTruthCreatedAt: y.sourceOfTruthCreatedAt
299
- } : { createdAt: (/* @__PURE__ */ new Date()).toISOString() }, y || p.annotations.push({
306
+ sourceOfTruthCreatedAt: w.sourceOfTruthCreatedAt
307
+ } : { createdAt: (/* @__PURE__ */ new Date()).toISOString() }, w || p.annotations.push({
300
308
  type: "golden-created",
301
309
  description: `No golden existed yet for "${n.id}" — captured one from this run.`
302
310
  });
303
- else if ($ = y, c === "own") {
304
- const l = E(B), { diffPixels: f, diffImage: O } = J(
305
- x,
306
- l
307
- ), k = U(
311
+ else if ($ = w, c === "own") {
312
+ const f = E(B), { diffPixels: l, diffImage: O } = R(
313
+ k,
314
+ f
315
+ ), x = K(
308
316
  n.id,
309
- i,
317
+ a,
310
318
  e.goldenThresholdPixels
311
319
  );
312
- f >= k && (await p.attach("expected", {
313
- body: l,
320
+ l >= x && (await p.attach("expected", {
321
+ body: f,
314
322
  contentType: "image/png"
315
323
  }), await p.attach("actual", {
316
- body: x,
324
+ body: k,
317
325
  contentType: "image/png"
318
326
  }), O && await p.attach("diff", {
319
327
  body: O,
320
328
  contentType: "image/png"
321
329
  })), D.soft(
322
- f,
323
- `"${n.id}" has drifted from its own golden image (${f} mismatched pixels, threshold ${k})`
324
- ).toBeLessThan(k);
330
+ l,
331
+ `"${n.id}" has drifted from its own golden image (${l} mismatched pixels, threshold ${x})`
332
+ ).toBeLessThan(x);
325
333
  }
326
- if (g) {
327
- const l = g.manifest[n.id], f = l ? await g.readImage(n.id) : null;
328
- if (l && f)
334
+ if (v) {
335
+ const f = v.manifest[n.id], l = f ? await v.readImage(n.id) : null;
336
+ if (f && l)
329
337
  if (h === "approve-divergence")
330
338
  $ = {
331
339
  ...$,
332
- sourceOfTruthCreatedAt: l.createdAt
340
+ sourceOfTruthCreatedAt: f.createdAt
333
341
  };
334
342
  else {
335
- const { diffPixels: O, diffImage: k } = J(
336
- x,
337
- f
338
- ), C = U(
343
+ const { diffPixels: O, diffImage: x } = R(
344
+ k,
345
+ l
346
+ ), I = K(
339
347
  n.id,
340
348
  d,
341
349
  e.sourceOfTruthThresholdPixels
342
- ), F = $.sourceOfTruthCreatedAt, _ = O < C || F !== void 0 && F >= l.createdAt;
350
+ ), F = $.sourceOfTruthCreatedAt, _ = O < I || F !== void 0 && F >= f.createdAt;
343
351
  _ || (await p.attach("expected", {
344
- body: f,
352
+ body: l,
345
353
  contentType: "image/png"
346
354
  }), await p.attach("actual", {
347
- body: x,
348
- contentType: "image/png"
349
- }), k && await p.attach("diff", {
350
355
  body: k,
351
356
  contentType: "image/png"
357
+ }), x && await p.attach("diff", {
358
+ body: x,
359
+ contentType: "image/png"
352
360
  }), p.annotations.push({
353
361
  type: "source-of-truth-divergence",
354
- description: `"${n.id}" differs from the source of truth's golden by ${O} mismatched pixels (threshold ${C}) and hasn't been reviewed — run with --approve-divergence if this is intentional, or fix the adapter styling.`
362
+ description: `"${n.id}" differs from the source of truth's golden by ${O} mismatched pixels (threshold ${I}) and hasn't been reviewed — run with --approve-divergence if this is intentional, or fix the adapter styling.`
355
363
  })), D.soft(
356
364
  _,
357
- `"${n.id}" differs from the source of truth's golden by ${O} mismatched pixels (threshold ${C}) and hasn't been reviewed — run with --approve-divergence if this is intentional, or fix the adapter styling.`
365
+ `"${n.id}" differs from the source of truth's golden by ${O} mismatched pixels (threshold ${I}) and hasn't been reviewed — run with --approve-divergence if this is intentional, or fix the adapter styling.`
358
366
  ).toBe(!0);
359
367
  }
360
368
  }
361
- await V(a, n.id, () => $);
369
+ await W(i, n.id, () => $);
362
370
  }
363
371
  };
364
372
  }
@@ -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 * Returns `null` — degrading the divergence check to a skip, not a failure —\n * when no golden baseline exists yet for this version, or the registry/repo\n * is unreachable.\n */\nexport async function resolveSourceOfTruthGolden(\n location: SourceOfTruthGoldenLocation,\n): Promise<SourceOfTruthGolden | null> {\n if (location.type === \"local\") {\n if (!existsSync(manifestPath(location.dir))) {\n console.warn(\n `No golden baseline found yet at ${location.dir} — source-of-truth divergence check skipped for this run.`,\n );\n return null;\n }\n const manifest = loadManifest(location.dir);\n return {\n manifest,\n async readImage(storyId) {\n const path = goldenImagePath(location.dir, storyId);\n return existsSync(path) ? readFileSync(path) : null;\n },\n };\n }\n\n let version: string;\n try {\n version = await resolveNpmVersion(\n location.packageName,\n location.versionSpec,\n );\n } catch (error) {\n console.warn(\n `Could not resolve ${location.packageName}@${location.versionSpec} — source-of-truth divergence check skipped for this run.`,\n error,\n );\n return null;\n }\n\n const cacheDir = join(location.cacheDir, version);\n const tag = `${location.packageName}@${version}`;\n // @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 console.warn(\n `Could not reach GitHub to fetch ${tag}'s golden baseline — source-of-truth divergence check skipped for this run.`,\n error,\n );\n return null;\n }\n if (!response.ok) {\n console.warn(\n `No golden baseline published for ${tag} — source-of-truth divergence check skipped for this run.`,\n );\n return null;\n }\n const text = await response.text();\n const parsed = JSON.parse(text);\n validateGoldenManifest(parsed, `${rawBase}/manifest.json`);\n mkdirSync(cacheDir, { recursive: true });\n writeFileSync(cachedManifestPath, text);\n manifest = parsed;\n }\n\n return {\n manifest,\n async readImage(storyId) {\n const cachedImagePath = goldenImagePath(cacheDir, storyId);\n if (existsSync(cachedImagePath)) return readFileSync(cachedImagePath);\n const response = await fetch(`${rawBase}/${storyId}.png`);\n if (!response.ok) return null;\n const buffer = Buffer.from(await response.arrayBuffer());\n mkdirSync(cacheDir, { recursive: true });\n writeFileSync(cachedImagePath, buffer);\n return buffer;\n },\n };\n}\n","import { expect } from \"@playwright/test\";\nimport type { Browser, TestInfo } from \"@playwright/test\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport type { AdapterTesterConfig } from \"../config.js\";\nimport { diffPngBuffers } from \"../golden/diffPng.js\";\nimport {\n type GoldenManifestEntry,\n goldenImagePath,\n loadManifest,\n pruneOrphanedGoldens,\n saveGoldenImage,\n updateManifestEntry,\n} from \"../golden/manifestStore.js\";\nimport { resolveSourceOfTruthGolden } from \"../golden/resolveSourceOfTruthGolden.js\";\n\nconst DEFAULT_EXCLUDE_TITLE_PREFIXES = [\"Theme\", \"Tokens\", \"Introduction\"];\n\ninterface StorybookEntry {\n type: string;\n id: string;\n name: string;\n title: string;\n}\n\nfunction matchesPrefix(id: string, prefix: string): boolean {\n return id === prefix || id.startsWith(prefix);\n}\n\n/** Fetches every story `target`'s Storybook currently has, filtered only by\n * `excludeTitlePrefixes` — not `stories.<id>.exclude`, so callers can still\n * tell an excluded story apart from one that's genuinely missing. */\nasync function fetchStories(\n target: { name: string; url: string },\n excludeTitlePrefixes: string[],\n): Promise<StorybookEntry[]> {\n let stories: StorybookEntry[];\n try {\n const response = await fetch(`${target.url}/index.json`);\n if (!response.ok) {\n throw new Error(\n `Failed to fetch Storybook index: ${response.statusText}`,\n );\n }\n const data = (await response.json()) as any;\n const entries = data.entries || {};\n stories = Object.values(entries).filter(\n (entry: any) =>\n entry.type === \"story\" &&\n !excludeTitlePrefixes.some(\n (prefix) =>\n entry.title === prefix || entry.title.startsWith(`${prefix}/`),\n ),\n ) as StorybookEntry[];\n stories.sort((a, b) => a.id.localeCompare(b.id));\n } catch (error) {\n console.error(\n \"Failed to load Storybook index from\",\n `${target.url}/index.json`,\n error,\n );\n throw new Error(\n `Storybook target \"${target.name}\" is not responsive or index.json is missing. Please ensure its Storybook is running.`,\n );\n }\n return stories;\n}\n\n/** Resolves the diff threshold for `storyId`: the longest (most specific)\n * `storyThresholds` key matching by prefix, falling back to\n * `defaultThresholdPixels` when nothing matches. */\nfunction resolveThreshold(\n storyId: string,\n storyThresholds: Record<string, number>,\n defaultThresholdPixels: number,\n): number {\n let bestMatch: string | undefined;\n for (const prefix of Object.keys(storyThresholds)) {\n if (\n matchesPrefix(storyId, prefix) &&\n (!bestMatch || prefix.length > bestMatch.length)\n ) {\n bestMatch = prefix;\n }\n }\n return bestMatch !== undefined\n ? storyThresholds[bestMatch]!\n : defaultThresholdPixels;\n}\n\n/** Everything a generated Playwright spec needs to register the golden-image\n * suite itself. Split out from the actual `test.describe`/`test` calls so\n * those calls execute in the spec file that imports this, not in this\n * library file — otherwise Playwright's HTML report groups every story under\n * this file's own (sourcemapped) path instead of a stable spec name. */\nexport interface VisualRegressionPlan {\n /** `config`'s own (non-source-of-truth) target name, for the suite title. */\n ownTargetName: string;\n /** Suite title suffix describing which check mode is running. */\n suiteLabel: string;\n /** Stories to check, already filtered and sorted by id. */\n stories: StorybookEntry[];\n /** Story ids the source-of-truth adapter has a golden for but this\n * project's own Storybook doesn't — empty outside `checkMode: \"divergence\"`\n * or when `sourceOfTruthGolden` didn't resolve. Excludes ids covered by a\n * `stories.<id>.exclude` entry: an intentional gap, not a sync failure. */\n missingFromSourceOfTruth: string[];\n /** Golden-checks one story. Call this from inside a `test(story.id, ...)`\n * body — safe to run concurrently across Playwright workers, since each\n * call only ever reads/writes its own story's manifest entry (locked at\n * the point it writes it back, so concurrent workers never race each\n * other's entries — see `updateManifestEntry`). */\n checkStory: (\n story: StorybookEntry,\n browser: Browser,\n testInfo: TestInfo,\n ) => Promise<void>;\n}\n\n/** Everything a generated Playwright spec needs to register the golden-image\n * suite itself. Split out from the actual `test.describe`/`test` calls so\n * those calls execute in the spec file that imports this, not in this\n * library file — otherwise Playwright's HTML report groups every story under\n * this file's own (sourcemapped) path instead of a stable spec name. */\nexport interface VisualRegressionPlan {\n /** `config`'s own (non-source-of-truth) target name, for the suite title. */\n ownTargetName: string;\n /** Suite title suffix describing which check mode is running. */\n suiteLabel: string;\n /** Stories to check, already filtered and sorted by id. */\n stories: StorybookEntry[];\n /** Golden-checks one story. Call this from inside a `test(story.id, ...)`\n * body — safe to run concurrently across Playwright workers, since each\n * call only ever reads/writes its own story's manifest entry (locked at\n * the point it writes it back, so concurrent workers never race each\n * other's entries — see `updateManifestEntry`). */\n checkStory: (\n story: StorybookEntry,\n browser: Browser,\n testInfo: TestInfo,\n ) => Promise<void>;\n}\n\n/**\n * Resolves the golden-image plan for `config`'s own target (the one target\n * in `config.targets` not marked `sourceOfTruth`).\n *\n * Two independent checks per story, gated by `config.checkMode`. Neither\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 : sourceOfTruthGolden\n ? `[adapter-tester] sourceOfTruthGolden: resolved, ${Object.keys(sourceOfTruthGolden.manifest).length} golden(s) available (config: ${JSON.stringify(config.sourceOfTruthGolden)})`\n : `[adapter-tester] sourceOfTruthGolden: unavailable — no baseline found or unreachable (config: ${JSON.stringify(config.sourceOfTruthGolden)}); divergence check will skip every story`\n : `[adapter-tester] sourceOfTruthGolden: not used in \"own\" checkMode`,\n `[adapter-tester] stories: ${stories.length} to check (excluded: ${excludeStoryIds.length}, title prefixes excluded: ${excludeTitlePrefixes.join(\", \") || \"none\"})`,\n `[adapter-tester] story parity with source of truth: ${missingFromSourceOfTruth.length === 0 ? \"OK\" : `${missingFromSourceOfTruth.length} missing (see error above)`}`,\n ].join(\"\\n\"),\n );\n\n const suiteLabel =\n checkMode === \"divergence\"\n ? \"Source-of-Truth Divergence Check\"\n : \"Own-Drift Golden Image Check\";\n\n return {\n ownTargetName: ownTarget.name,\n suiteLabel,\n stories,\n missingFromSourceOfTruth,\n checkStory: async (story, browser, testInfo) => {\n const page = await browser.newPage();\n await page.setViewportSize({ width: 800, height: 600 });\n await page.goto(\n `${ownTarget.url}/iframe.html?id=${story.id}&viewMode=story`,\n { waitUntil: \"networkidle\" },\n );\n await page.waitForSelector(\"#storybook-root\");\n await page.addStyleTag({\n content: `* { -webkit-font-smoothing: antialiased !important; -moz-osx-font-smoothing: grayscale !important; }`,\n });\n // 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":";;;;;;AAeO,SAASA,EAAeC,GAAWC,GAA0B;AAClE,QAAMC,IAAOC,EAAI,KAAK,KAAKH,CAAC,GACtBI,IAAOD,EAAI,KAAK,KAAKF,CAAC;AAC5B,MAAIC,EAAK,UAAUE,EAAK,SAASF,EAAK,WAAWE,EAAK;AACpD,WAAO,EAAE,YAAY,OAAU,WAAW,KAAA;AAE5C,QAAMC,IAAO,IAAIF,EAAI,EAAE,OAAOD,EAAK,OAAO,QAAQA,EAAK,QAAQ;AAS/D,SAAO,EAAE,YARUI;AAAA,IACjBJ,EAAK;AAAA,IACLE,EAAK;AAAA,IACLC,EAAK;AAAA,IACLH,EAAK;AAAA,IACLA,EAAK;AAAA,IACL,EAAE,WAAW,IAAA;AAAA,EAAI,GAEE,WAAWC,EAAI,KAAK,MAAME,CAAI,EAAA;AACrD;;;;;;;;GCzBME,KAAcC,IAGdC,IAAM,IAAIC,GAAAA,IAAI,EAAE,WAAW,IAAM,QAAQ,IAAM;AACrDH,GAAWE,CAAG;AACd,MAAME,IAAWF,EAAI,QAAQG,EAAM;AAO5B,SAASC,EAAuBC,GAAeC,GAAoB;AACxE,MAAIJ,EAASG,CAAI,EAAG;AAEpB,QAAME,KAAUL,EAAS,UAAU,CAAA,GAChC,IAAI,CAACM,MAAU;;AACd,UAAMC,KAAQC,IAAAF,EAAM,WAAN,QAAAE,EAAc,qBACxB,KAAKF,EAAM,OAAO,kBAAkB,MACpC;AACJ,WAAO,OAAOA,EAAM,gBAAgB,MAAM,IAAIA,EAAM,OAAO,GAAGC,CAAK;AAAA,EACrE,CAAC,EACA,KAAK;AAAA,CAAI;AACZ,QAAM,IAAI,MAAM,WAAWH,CAAI;AAAA,EAAMC,CAAM,EAAE;AAC/C;ACTO,SAASI,EAAaC,GAA2B;AACtD,SAAOC,EAAKD,GAAW,eAAe;AACxC;AAEA,SAASE,EAAiBF,GAA2B;AACnD,SAAOC,EAAKD,GAAW,oBAAoB;AAC7C;AAEO,SAASG,EAAgBH,GAAmBI,GAAyB;AAC1E,SAAOH,EAAKD,GAAW,GAAGI,CAAO,MAAM;AACzC;AAIO,SAASC,EAAaL,GAAmC;AAC9D,QAAMN,IAAOK,EAAaC,CAAS;AACnC,MAAI,CAACM,EAAWZ,CAAI,UAAU,CAAA;AAC9B,QAAMD,IAAO,KAAK,MAAMc,EAAab,GAAM,MAAM,CAAC;AAClD,SAAAF,EAAuBC,GAAMC,CAAI,GAC1BD;AACT;AAOO,SAASe,GACdR,GACAS,GACM;AACN,QAAMf,IAAOK,EAAaC,CAAS;AACnC,EAAAR,EAAuBiB,GAAUf,CAAI;AACrC,QAAMgB,IAAyB,CAAA;AAC/B,aAAWC,KAAO,OAAO,KAAKF,CAAQ,EAAE;AACtC,IAAAC,EAAOC,CAAG,IAAIF,EAASE,CAAG;AAE5B,EAAAC,EAAUZ,GAAW,EAAE,WAAW,GAAA,CAAM;AACxC,QAAMa,IAAU,GAAGnB,CAAI,QAAQ,QAAQ,GAAG,IAAI,KAAK,OAAA,EAAS,SAAS,EAAE,EAAE,MAAM,CAAC,CAAC;AACjF,EAAAoB,EAAcD,GAAS,KAAK,UAAUH,GAAQ,MAAM,CAAC,IAAI;AAAA,CAAI,GAC7DK,EAAWF,GAASnB,CAAI;AAC1B;AAEA,MAAMsB,KAAgB,IAChBC,KAAkB;AAExB,SAASC,GAAMC,GAA2B;AACxC,SAAO,IAAI,QAAQ,CAACC,MAAY,WAAWA,GAASD,CAAE,CAAC;AACzD;AAMA,eAAeE,GAAoBrB,GAAkC;AACnE,EAAAY,EAAUZ,GAAW,EAAE,WAAW,GAAA,CAAM;AACxC,QAAMN,IAAOQ,EAAiBF,CAAS,GACjCsB,IAAW,KAAK,IAAA,IAAQL;AAC9B;AACE,QAAI;AACF,MAAAM,EAAUC,EAAS9B,GAAM,IAAI,CAAC;AAC9B;AAAA,IACF,SAASE,GAAO;AACd,UAAKA,EAAgC,SAAS,SAAU,OAAMA;AAC9D,UAAI,KAAK,IAAA,KAAS0B;AAChB,cAAM,IAAI;AAAA,UACR,+CAA+C5B,CAAI;AAAA,QAAA;AAGvD,YAAMwB,GAAMF,EAAa;AAAA,IAC3B;AAEJ;AAEA,SAASS,GAAoBzB,GAAyB;AACpD,EAAA0B,GAAOxB,EAAiBF,CAAS,GAAG,EAAE,OAAO,IAAM;AACrD;AAQA,eAAsB2B,EACpB3B,GACAI,GACAwB,GAG0C;AAC1C,QAAMP,GAAoBrB,CAAS;AACnC,MAAI;AACF,UAAMS,IAAWJ,EAAaL,CAAS,GACjC6B,IAAYD,EAAQnB,EAASL,CAAO,CAAC;AAC3C,WAAIyB,MAAc,SAChB,OAAOpB,EAASL,CAAO,IAEvBK,EAASL,CAAO,IAAIyB,GAEtBrB,GAAaR,GAAWS,CAAQ,GACzBoB;AAAA,EACT,UAAA;AACE,IAAAJ,GAAoBzB,CAAS;AAAA,EAC/B;AACF;AAEO,SAAS8B,GACd9B,GACAI,GACA2B,GACM;AACN,EAAAnB,EAAUZ,GAAW,EAAE,WAAW,GAAA,CAAM,GACxCc,EAAcX,EAAgBH,GAAWI,CAAO,GAAG2B,CAAM;AAC3D;AASA,eAAsBC,GACpBhC,GACAiC,GACmB;AACnB,QAAMxB,IAAWJ,EAAaL,CAAS,GACjCkC,IAAY,OAAO,KAAKzB,CAAQ,EAAE;AAAA,IACtC,CAAC0B,MAAO,CAACF,EAAgB,IAAIE,CAAE;AAAA,EAAA;AAEjC,aAAWA,KAAMD,GAAW;AAC1B,UAAME,IAAYjC,EAAgBH,GAAWmC,CAAE;AAC/C,IAAI7B,EAAW8B,CAAS,KAAGC,EAAWD,CAAS,GAC/C,MAAMT,EAAoB3B,GAAWmC,GAAI,MAAA;AAAA,KAAe;AAAA,EAC1D;AACA,SAAOD;AACT;ACjJA,MAAMI,KAAc;AASpB,eAAeC,GACbC,GACAC,GACiB;;AACjB,QAAMC,IAAW,MAAM,MAAM,8BAA8BF,CAAW,EAAE;AACxE,MAAI,CAACE,EAAS;AACZ,UAAM,IAAI;AAAA,MACR,oCAAoCF,CAAW,KAAKE,EAAS,UAAU;AAAA,IAAA;AAG3E,QAAMjD,IAAQ,MAAMiD,EAAS,KAAA,GAIvBC,MACJ7C,IAAAL,EAAK,WAAW,MAAhB,gBAAAK,EAAoB2C,SACnBG,IAAAnD,EAAK,aAAL,QAAAmD,EAAgBH,KAAeA,IAAc;AAChD,MAAI,CAACE;AACH,UAAM,IAAI;AAAA,MACR,GAAGH,CAAW,gCAAgCC,CAAW;AAAA,IAAA;AAG7D,SAAOE;AACT;AAqBA,eAAsBE,GACpBC,GACqC;AACrC,MAAIA,EAAS,SAAS;AACpB,WAAKxC,EAAWP,EAAa+C,EAAS,GAAG,CAAC,IAOnC;AAAA,MACL,UAFezC,EAAayC,EAAS,GAAG;AAAA,MAGxC,MAAM,UAAU1C,GAAS;AACvB,cAAMV,IAAOS,EAAgB2C,EAAS,KAAK1C,CAAO;AAClD,eAAOE,EAAWZ,CAAI,IAAIa,EAAab,CAAI,IAAI;AAAA,MACjD;AAAA,IAAA,KAXA,QAAQ;AAAA,MACN,mCAAmCoD,EAAS,GAAG;AAAA,IAAA,GAE1C;AAYX,MAAIC;AACJ,MAAI;AACF,IAAAA,IAAU,MAAMR;AAAA,MACdO,EAAS;AAAA,MACTA,EAAS;AAAA,IAAA;AAAA,EAEb,SAASlD,GAAO;AACd,mBAAQ;AAAA,MACN,qBAAqBkD,EAAS,WAAW,IAAIA,EAAS,WAAW;AAAA,MACjElD;AAAA,IAAA,GAEK;AAAA,EACT;AAEA,QAAMoD,IAAW/C,EAAK6C,EAAS,UAAUC,CAAO,GAC1CE,IAAM,GAAGH,EAAS,WAAW,IAAIC,CAAO,IAIxCG,IAAU,qCAAqCZ,EAAW,IAAIW,CAAG;AAEvE,MAAIxC;AACJ,QAAM0C,IAAqBpD,EAAaiD,CAAQ;AAChD,MAAI1C,EAAW6C,CAAkB;AAC/B,IAAA1C,IAAWJ,EAAa2C,CAAQ;AAAA,OAC3B;AACL,QAAIN;AACJ,QAAI;AACF,MAAAA,IAAW,MAAM,MAAM,GAAGQ,CAAO,gBAAgB;AAAA,IACnD,SAAStD,GAAO;AACd,qBAAQ;AAAA,QACN,mCAAmCqD,CAAG;AAAA,QACtCrD;AAAA,MAAA,GAEK;AAAA,IACT;AACA,QAAI,CAAC8C,EAAS;AACZ,qBAAQ;AAAA,QACN,oCAAoCO,CAAG;AAAA,MAAA,GAElC;AAET,UAAMG,IAAO,MAAMV,EAAS,KAAA,GACtBW,IAAS,KAAK,MAAMD,CAAI;AAC9B,IAAA5D,EAAuB6D,GAAQ,GAAGH,CAAO,gBAAgB,GACzDtC,EAAUoC,GAAU,EAAE,WAAW,GAAA,CAAM,GACvClC,EAAcqC,GAAoBC,CAAI,GACtC3C,IAAW4C;AAAA,EACb;AAEA,SAAO;AAAA,IACL,UAAA5C;AAAA,IACA,MAAM,UAAUL,GAAS;AACvB,YAAMkD,IAAkBnD,EAAgB6C,GAAU5C,CAAO;AACzD,UAAIE,EAAWgD,CAAe,EAAG,QAAO/C,EAAa+C,CAAe;AACpE,YAAMZ,IAAW,MAAM,MAAM,GAAGQ,CAAO,IAAI9C,CAAO,MAAM;AACxD,UAAI,CAACsC,EAAS,GAAI,QAAO;AACzB,YAAMX,IAAS,OAAO,KAAK,MAAMW,EAAS,aAAa;AACvD,aAAA9B,EAAUoC,GAAU,EAAE,WAAW,GAAA,CAAM,GACvClC,EAAcwC,GAAiBvB,CAAM,GAC9BA;AAAA,IACT;AAAA,EAAA;AAEJ;ACtIA,MAAMwB,KAAiC,CAAC,SAAS,UAAU,cAAc;AASzE,SAASC,EAAcrB,GAAYsB,GAAyB;AAC1D,SAAOtB,MAAOsB,KAAUtB,EAAG,WAAWsB,CAAM;AAC9C;AAKA,eAAeC,GACbC,GACAC,GAC2B;AAC3B,MAAIC;AACJ,MAAI;AACF,UAAMnB,IAAW,MAAM,MAAM,GAAGiB,EAAO,GAAG,aAAa;AACvD,QAAI,CAACjB,EAAS;AACZ,YAAM,IAAI;AAAA,QACR,oCAAoCA,EAAS,UAAU;AAAA,MAAA;AAI3D,UAAMoB,KADQ,MAAMpB,EAAS,KAAA,GACR,WAAW,CAAA;AAChC,IAAAmB,IAAU,OAAO,OAAOC,CAAO,EAAE;AAAA,MAC/B,CAACC,MACCA,EAAM,SAAS,WACf,CAACH,EAAqB;AAAA,QACpB,CAACH,MACCM,EAAM,UAAUN,KAAUM,EAAM,MAAM,WAAW,GAAGN,CAAM,GAAG;AAAA,MAAA;AAAA,IACjE,GAEJI,EAAQ,KAAK,CAAClF,GAAGC,MAAMD,EAAE,GAAG,cAAcC,EAAE,EAAE,CAAC;AAAA,EACjD,SAASgB,GAAO;AACd,kBAAQ;AAAA,MACN;AAAA,MACA,GAAG+D,EAAO,GAAG;AAAA,MACb/D;AAAA,IAAA,GAEI,IAAI;AAAA,MACR,qBAAqB+D,EAAO,IAAI;AAAA,IAAA;AAAA,EAEpC;AACA,SAAOE;AACT;AAKA,SAASG,EACP5D,GACA6D,GACAC,GACQ;AACR,MAAIC;AACJ,aAAWV,KAAU,OAAO,KAAKQ,CAAe;AAC9C,IACET,EAAcpD,GAASqD,CAAM,MAC5B,CAACU,KAAaV,EAAO,SAASU,EAAU,YAEzCA,IAAYV;AAGhB,SAAOU,MAAc,SACjBF,EAAgBE,CAAS,IACzBD;AACN;AA6EA,eAAsBE,GACpBC,GAC+B;AAC/B,QAAMC,IAAYD,EAAO,yBACrBA,EAAO,QAAQ,CAAC,IAChBA,EAAO,QAAQ,KAAK,CAACV,MAAW,CAACA,EAAO,aAAa;AACzD,MAAI,CAACW;AACH,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAGJ,QAAMV,IACJS,EAAO,wBAAwBd,IAC3BgB,IAAiBF,EAAO,WAAW,CAAA,GACnCG,IAAkB,OAAO,KAAKD,CAAc,EAAE;AAAA,IAClD,CAACpC,MAAOoC,EAAepC,CAAE,EAAG;AAAA,EAAA,GAExBsC,IAAwB,OAAO;AAAA,IACnC,OAAO,QAAQF,CAAc,EAC1B,OAAO,CAAC,GAAGG,CAAQ,MAAMA,EAAS,oBAAoB,MAAS,EAC/D,IAAI,CAAC,CAACvC,GAAIuC,CAAQ,MAAM,CAACvC,GAAIuC,EAAS,eAAgB,CAAC;AAAA,EAAA,GAEtDC,IAA+B,OAAO;AAAA,IAC1C,OAAO,QAAQJ,CAAc,EAC1B,OAAO,CAAC,GAAGG,CAAQ,MAAMA,EAAS,2BAA2B,MAAS,EACtE,IAAI,CAAC,CAACvC,GAAIuC,CAAQ,MAAM,CAACvC,GAAIuC,EAAS,sBAAuB,CAAC;AAAA,EAAA,GAE7D1E,IAAYqE,EAAO,WACnBO,IAAaP,EAAO,YACpBQ,IAAYR,EAAO,WAKnBS,IAAa,MAAMpB,GAAaY,GAAWV,CAAoB,GAC/DC,IAAUiB,EAAW;AAAA,IACzB,CAACf,MACC,CAACS,EAAgB,KAAK,CAACf,MAAWD,EAAcO,EAAM,IAAIN,CAAM,CAAC;AAAA,EAAA;AASrE,MAAImB,MAAe,iBAAiB;AAClC,UAAMG,IAAiB,MAAM/C;AAAA,MAC3BhC;AAAA,MACA,IAAI,IAAI6D,EAAQ,IAAI,CAACmB,MAAUA,EAAM,EAAE,CAAC;AAAA,IAAA;AAE1C,IAAID,EAAe,SAAS,KAC1B,QAAQ;AAAA,MACN,UAAUA,EAAe,MAAM,+CAA+CA,EAAe,KAAK,IAAI,CAAC;AAAA,IAAA;AAAA,EAG7G;AAEA,QAAME,IACJJ,MAAc,gBACdR,EAAO,0BACP,CAACA,EAAO,sBACJ,OACA,MAAMxB,GAA2BwB,EAAO,mBAAmB,GAM3Da,IAAc,IAAI,IAAIJ,EAAW,IAAI,CAACf,MAAUA,EAAM,EAAE,CAAC,GACzDoB,IAA2BF,IAC7B,OAAO,KAAKA,EAAoB,QAAQ,EACrC;AAAA,IACC,CAAC9C,MACC,CAAC+C,EAAY,IAAI/C,CAAE,KACnB,CAACqC,EAAgB,KAAK,CAACf,MAAWD,EAAcrB,GAAIsB,CAAM,CAAC;AAAA,EAAA,EAE9D,KAAA,IACH,CAAA;AAEJ,EAAI0B,EAAyB,SAAS,KACpC,QAAQ;AAAA,IACN,oBAAoBA,EAAyB,MAAM,mEAAmEA,EAAyB,KAAK,IAAI,CAAC;AAAA,EAAA,GAI7J,QAAQ;AAAA,IACN;AAAA,MACE,6BAA6Bb,EAAU,IAAI,MAAMA,EAAU,GAAG;AAAA,MAC9D,gCAAgCO,CAAS,MAAMA,MAAc,eAAe,4CAA4C,0CAA0C;AAAA,MAClK,iCAAiCD,CAAU;AAAA,MAC3C,2CAA2CP,EAAO,qBAAqB;AAAA,MACvE,kDAAkDA,EAAO,4BAA4B;AAAA,MACrFQ,MAAc,eACVR,EAAO,yBACL,iHACCA,EAAO,sBAENY,IACE,mDAAmD,OAAO,KAAKA,EAAoB,QAAQ,EAAE,MAAM,iCAAiC,KAAK,UAAUZ,EAAO,mBAAmB,CAAC,MAC9K,iGAAiG,KAAK,UAAUA,EAAO,mBAAmB,CAAC,8CAH7I,sFAIJ;AAAA,MACJ,6BAA6BR,EAAQ,MAAM,wBAAwBW,EAAgB,MAAM,8BAA8BZ,EAAqB,KAAK,IAAI,KAAK,MAAM;AAAA,MAChK,uDAAuDuB,EAAyB,WAAW,IAAI,OAAO,GAAGA,EAAyB,MAAM,4BAA4B;AAAA,IAAA,EACpK,KAAK;AAAA,CAAI;AAAA,EAAA;AAGb,QAAMC,IACJP,MAAc,eACV,qCACA;AAEN,SAAO;AAAA,IACL,eAAeP,EAAU;AAAA,IACzB,YAAAc;AAAA,IACA,SAAAvB;AAAA,IACA,0BAAAsB;AAAA,IACA,YAAY,OAAOH,GAAOK,GAASC,MAAa;AAC9C,YAAMC,IAAO,MAAMF,EAAQ,QAAA;AAC3B,YAAME,EAAK,gBAAgB,EAAE,OAAO,KAAK,QAAQ,KAAK,GACtD,MAAMA,EAAK;AAAA,QACT,GAAGjB,EAAU,GAAG,mBAAmBU,EAAM,EAAE;AAAA,QAC3C,EAAE,WAAW,cAAA;AAAA,MAAc,GAE7B,MAAMO,EAAK,gBAAgB,iBAAiB,GAC5C,MAAMA,EAAK,YAAY;AAAA,QACrB,SAAS;AAAA,MAAA,CACV,GAUD,MAAMA,EAAK;AAAA,QAAS,MAClB,QAAQ;AAAA,UACN,MAAM;AAAA,YACJ,SAAS;AAAA,cACP;AAAA,YAAA;AAAA,UACF,EACA;AAAA,YAAI,CAACC,MACLA,EAAK,QACD,QAAQ,YACR,IAAI,QAAc,CAACpE,MAAY;AAC7B,cAAAoE,EAAK,iBAAiB,QAAQ,MAAMpE,EAAA,GAAW;AAAA,gBAC7C,MAAM;AAAA,cAAA,CACP,GACDoE,EAAK,iBAAiB,SAAS,MAAMpE,EAAA,GAAW;AAAA,gBAC9C,MAAM;AAAA,cAAA,CACP;AAAA,YACH,CAAC;AAAA,UAAA;AAAA,QACP,EACA,KAAK,MAAM,SAAS,MAAM,KAAK;AAAA,MAAA,GAEnC,MAAMmE,EAAK,eAAe,GAAG;AAC7B,YAAME,IAAa,MAAMF,EAAK,WAAA,GAExBnD,IAAYjC,EAAgBH,GAAWgF,EAAM,EAAE,GAI/CjB,IAAQ1D,EAAaL,CAAS,EAAEgF,EAAM,EAAE,GACxCU,IACJd,MAAe,WAAW,CAACb,KAAS,CAACzD,EAAW8B,CAAS;AAE3D,UAAIuD;AACJ,UAAID;AACF,QAAA5D,GAAgB9B,GAAWgF,EAAM,IAAIS,CAAU,GAC/CE,IAAe5B,KAAA,QAAAA,EAAO,yBAClB;AAAA,UACE,YAAW,oBAAI,KAAA,GAAO,YAAA;AAAA,UACtB,wBAAwBA,EAAM;AAAA,QAAA,IAEhC,EAAE,gCAAe,KAAA,GAAO,cAAY,GACnCA,KACHuB,EAAS,YAAY,KAAK;AAAA,UACxB,MAAM;AAAA,UACN,aAAa,8BAA8BN,EAAM,EAAE;AAAA,QAAA,CACpD;AAAA,eAGHW,IAAe5B,GACXc,MAAc,OAAO;AACvB,cAAMe,IAAerF,EAAa6B,CAAS,GACrC,EAAE,YAAAyD,GAAY,WAAAC,EAAA,IAAcpH;AAAA,UAChC+G;AAAA,UACAG;AAAA,QAAA,GAEIG,IAAY/B;AAAA,UAChBgB,EAAM;AAAA,UACNP;AAAA,UACAJ,EAAO;AAAA,QAAA;AAET,QAAIwB,KAAcE,MAChB,MAAMT,EAAS,OAAO,YAAY;AAAA,UAChC,MAAMM;AAAA,UACN,aAAa;AAAA,QAAA,CACd,GACD,MAAMN,EAAS,OAAO,UAAU;AAAA,UAC9B,MAAMG;AAAA,UACN,aAAa;AAAA,QAAA,CACd,GACGK,KACF,MAAMR,EAAS,OAAO,QAAQ;AAAA,UAC5B,MAAMQ;AAAA,UACN,aAAa;AAAA,QAAA,CACd,IAGLE,EACG;AAAA,UACCH;AAAA,UACA,IAAIb,EAAM,EAAE,4CAA4Ca,CAAU,iCAAiCE,CAAS;AAAA,QAAA,EAE7G,aAAaA,CAAS;AAAA,MAC3B;AAGF,UAAId,GAAqB;AACvB,cAAMgB,IAAqBhB,EAAoB,SAASD,EAAM,EAAE,GAC1DkB,IAAqBD,IACvB,MAAMhB,EAAoB,UAAUD,EAAM,EAAE,IAC5C;AAEJ,YAAIiB,KAAsBC;AACxB,cAAItB,MAAe;AACjB,YAAAe,IAAe;AAAA,cACb,GAAGA;AAAA,cACH,wBAAwBM,EAAmB;AAAA,YAAA;AAAA,eAExC;AAIL,kBAAM,EAAE,YAAAJ,GAAY,WAAAC,EAAA,IAAcpH;AAAA,cAChC+G;AAAA,cACAS;AAAA,YAAA,GAEIH,IAAY/B;AAAA,cAChBgB,EAAM;AAAA,cACNL;AAAA,cACAN,EAAO;AAAA,YAAA,GAEH8B,IAAaR,EAAa,wBAC1BS,IACJP,IAAaE,KACZI,MAAe,UACdA,KAAcF,EAAmB;AACrC,YAAKG,MACH,MAAMd,EAAS,OAAO,YAAY;AAAA,cAChC,MAAMY;AAAA,cACN,aAAa;AAAA,YAAA,CACd,GACD,MAAMZ,EAAS,OAAO,UAAU;AAAA,cAC9B,MAAMG;AAAA,cACN,aAAa;AAAA,YAAA,CACd,GACGK,KACF,MAAMR,EAAS,OAAO,QAAQ;AAAA,cAC5B,MAAMQ;AAAA,cACN,aAAa;AAAA,YAAA,CACd,GAEHR,EAAS,YAAY,KAAK;AAAA,cACxB,MAAM;AAAA,cACN,aAAa,IAAIN,EAAM,EAAE,kDAAkDa,CAAU,iCAAiCE,CAAS;AAAA,YAAA,CAChI,IAEHC,EACG;AAAA,cACCI;AAAA,cACA,IAAIpB,EAAM,EAAE,kDAAkDa,CAAU,iCAAiCE,CAAS;AAAA,YAAA,EAEnH,KAAK,EAAI;AAAA,UACd;AAAA,MAEJ;AAKA,YAAMpE,EAAoB3B,GAAWgF,EAAM,IAAI,MAAMW,CAAY;AAAA,IACnE;AAAA,EAAA;AAEJ;"}
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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"}
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.2",
16
+ "version": "5.1.3",
17
17
  "type": "module",
18
18
  "main": "./dist/index.cjs",
19
19
  "module": "./dist/index.js",