@recursica/adapter-tester 3.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/testing.js CHANGED
@@ -1,156 +1,209 @@
1
- import { test as I, expect as M } from "@playwright/test";
2
- import { existsSync as l, readFileSync as g, mkdirSync as w, writeFileSync as y } from "node:fs";
3
- import L from "pixelmatch";
4
- import { PNG as O } from "pngjs";
5
- import { join as P } from "node:path";
6
- import { a as R, i as U } from "./index-DqQzFSAH.js";
7
- function G(e, t) {
8
- const r = O.sync.read(e), s = O.sync.read(t);
9
- if (r.width !== s.width || r.height !== s.height)
10
- return 1 / 0;
11
- const o = new O({ width: r.width, height: r.height });
12
- return L(r.data, s.data, o.data, r.width, r.height, {
13
- threshold: 0.1
14
- });
15
- }
16
- const V = "http://json-schema.org/draft-07/schema#", _ = "https://github.com/borderux/recursica/tree/main/packages/adapter-tester/src/golden/manifest.schema.json", J = "test/golden/manifest.json", W = "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.", H = "object", K = { 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." } } }, X = {
17
- $schema: V,
18
- $id: _,
19
- title: J,
20
- description: W,
21
- type: H,
22
- additionalProperties: K
23
- }, q = U, C = new R.Ajv({ allErrors: !0, strict: !0 });
24
- q(C);
25
- const N = C.compile(X);
26
- function S(e, t) {
27
- if (N(e)) return;
28
- const r = (N.errors ?? []).map((s) => {
29
- var c;
30
- const o = (c = s.params) != null && c.additionalProperty ? ` '${s.params.additionalProperty}'` : "";
31
- return ` - ${s.instancePath || "root"} ${s.message}${o}`;
1
+ import { expect as K } from "@playwright/test";
2
+ import { existsSync as g, unlinkSync as J, readFileSync as $, mkdirSync as x, writeFileSync as P, closeSync as V, openSync as W, renameSync as X, rmSync as q } from "node:fs";
3
+ import H from "pixelmatch";
4
+ import { PNG as S } from "pngjs";
5
+ import { join as E } from "node:path";
6
+ import { a as z, i as Y } from "./index-DqQzFSAH.js";
7
+ function N(e, t) {
8
+ const n = S.sync.read(e), r = S.sync.read(t);
9
+ if (n.width !== r.width || n.height !== r.height)
10
+ return { diffPixels: 1 / 0, diffImage: null };
11
+ const o = new S({ width: n.width, height: n.height });
12
+ return { diffPixels: H(
13
+ n.data,
14
+ r.data,
15
+ o.data,
16
+ n.width,
17
+ n.height,
18
+ { threshold: 0.1 }
19
+ ), diffImage: S.sync.write(o) };
20
+ }
21
+ const Q = "http://json-schema.org/draft-07/schema#", Z = "https://github.com/borderux/recursica/tree/main/packages/adapter-tester/src/golden/manifest.schema.json", ee = "test/golden/manifest.json", te = "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.", re = "object", ne = { 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." } } }, oe = {
22
+ $schema: Q,
23
+ $id: Z,
24
+ title: ee,
25
+ description: te,
26
+ type: re,
27
+ additionalProperties: ne
28
+ }, se = Y, B = new z.Ajv({ allErrors: !0, strict: !0 });
29
+ se(B);
30
+ const L = B.compile(oe);
31
+ function M(e, t) {
32
+ if (L(e)) return;
33
+ const n = (L.errors ?? []).map((r) => {
34
+ var a;
35
+ const o = (a = r.params) != null && a.additionalProperty ? ` '${r.params.additionalProperty}'` : "";
36
+ return ` - ${r.instancePath || "root"} ${r.message}${o}`;
32
37
  }).join(`
33
38
  `);
34
39
  throw new Error(`Invalid ${t}:
35
- ${r}`);
40
+ ${n}`);
41
+ }
42
+ function j(e) {
43
+ return E(e, "manifest.json");
36
44
  }
37
- function v(e) {
38
- return P(e, "manifest.json");
45
+ function F(e) {
46
+ return E(e, "manifest.json.lock");
39
47
  }
40
- function $(e, t) {
41
- return P(e, `${t}.png`);
48
+ function O(e, t) {
49
+ return E(e, `${t}.png`);
42
50
  }
43
51
  function b(e) {
44
- const t = v(e);
45
- if (!l(t)) return {};
46
- const r = JSON.parse(g(t, "utf8"));
47
- return S(r, t), r;
48
- }
49
- function z(e, t) {
50
- const r = v(e);
51
- S(t, r);
52
- const s = {};
53
- for (const o of Object.keys(t).sort())
54
- s[o] = t[o];
55
- w(e, { recursive: !0 }), y(r, JSON.stringify(s, null, 2) + `
56
- `);
52
+ const t = j(e);
53
+ if (!g(t)) return {};
54
+ const n = JSON.parse($(t, "utf8"));
55
+ return M(n, t), n;
56
+ }
57
+ function ie(e, t) {
58
+ const n = j(e);
59
+ M(t, n);
60
+ const r = {};
61
+ for (const a of Object.keys(t).sort())
62
+ r[a] = t[a];
63
+ x(e, { recursive: !0 });
64
+ const o = `${n}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
65
+ P(o, JSON.stringify(r, null, 2) + `
66
+ `), X(o, n);
67
+ }
68
+ const ae = 25, ce = 15e3;
69
+ function de(e) {
70
+ return new Promise((t) => setTimeout(t, e));
71
+ }
72
+ async function ue(e) {
73
+ x(e, { recursive: !0 });
74
+ const t = F(e), n = Date.now() + ce;
75
+ for (; ; )
76
+ try {
77
+ V(W(t, "wx"));
78
+ return;
79
+ } catch (r) {
80
+ if (r.code !== "EEXIST") throw r;
81
+ if (Date.now() >= n)
82
+ throw new Error(
83
+ `Timed out waiting for the manifest lock at "${t}" — delete it if a previous run crashed while holding it.`
84
+ );
85
+ await de(ae);
86
+ }
87
+ }
88
+ function fe(e) {
89
+ q(F(e), { force: !0 });
90
+ }
91
+ async function _(e, t, n) {
92
+ await ue(e);
93
+ try {
94
+ const r = b(e), o = n(r[t]);
95
+ return o === void 0 ? delete r[t] : r[t] = o, ie(e, r), o;
96
+ } finally {
97
+ fe(e);
98
+ }
99
+ }
100
+ function he(e, t, n) {
101
+ x(e, { recursive: !0 }), P(O(e, t), n);
57
102
  }
58
- function Q(e, t, r) {
59
- w(e, { recursive: !0 }), y($(e, t), r);
103
+ async function le(e, t) {
104
+ const n = b(e), r = Object.keys(n).filter(
105
+ (o) => !t.has(o)
106
+ );
107
+ for (const o of r) {
108
+ const a = O(e, o);
109
+ g(a) && J(a), await _(e, o, () => {
110
+ });
111
+ }
112
+ return r;
60
113
  }
61
- const Y = "borderux/recursica";
62
- async function Z(e, t) {
63
- var c, u;
64
- const r = await fetch(`https://registry.npmjs.org/${e}`);
65
- if (!r.ok)
114
+ const pe = "borderux/recursica";
115
+ async function me(e, t) {
116
+ var a, d;
117
+ const n = await fetch(`https://registry.npmjs.org/${e}`);
118
+ if (!n.ok)
66
119
  throw new Error(
67
- `Could not reach npm registry for ${e}: ${r.statusText}`
120
+ `Could not reach npm registry for ${e}: ${n.statusText}`
68
121
  );
69
- const s = await r.json(), o = ((c = s["dist-tags"]) == null ? void 0 : c[t]) ?? ((u = s.versions) != null && u[t] ? t : void 0);
122
+ const r = await n.json(), o = ((a = r["dist-tags"]) == null ? void 0 : a[t]) ?? ((d = r.versions) != null && d[t] ? t : void 0);
70
123
  if (!o)
71
124
  throw new Error(
72
125
  `${e} has no version or dist-tag "${t}" on the npm registry.`
73
126
  );
74
127
  return o;
75
128
  }
76
- function ee(e) {
129
+ function ge(e) {
77
130
  return `packages/${e.split("/").pop()}`;
78
131
  }
79
- async function te(e) {
132
+ async function we(e) {
80
133
  if (e.type === "local")
81
- return l(v(e.dir)) ? {
134
+ return g(j(e.dir)) ? {
82
135
  manifest: b(e.dir),
83
- async readImage(i) {
84
- const d = $(e.dir, i);
85
- return l(d) ? g(d) : null;
136
+ async readImage(c) {
137
+ const u = O(e.dir, c);
138
+ return g(u) ? $(u) : null;
86
139
  }
87
140
  } : (console.warn(
88
141
  `No golden baseline found yet at ${e.dir} — source-of-truth divergence check skipped for this run.`
89
142
  ), null);
90
143
  let t;
91
144
  try {
92
- t = await Z(
145
+ t = await me(
93
146
  e.packageName,
94
147
  e.versionSpec
95
148
  );
96
- } catch (a) {
149
+ } catch (i) {
97
150
  return console.warn(
98
151
  `Could not resolve ${e.packageName}@${e.versionSpec} — source-of-truth divergence check skipped for this run.`,
99
- a
152
+ i
100
153
  ), null;
101
154
  }
102
- const r = P(e.cacheDir, t), s = `${e.packageName}@${t}`, o = `https://raw.githubusercontent.com/${Y}/${s}/${ee(e.packageName)}/test/golden`;
103
- let c;
104
- const u = v(r);
105
- if (l(u))
106
- c = b(r);
155
+ const n = E(e.cacheDir, t), r = `${e.packageName}@${t}`, o = `https://raw.githubusercontent.com/${pe}/${r}/${ge(e.packageName)}/test/golden`;
156
+ let a;
157
+ const d = j(n);
158
+ if (g(d))
159
+ a = b(n);
107
160
  else {
108
- let a;
161
+ let i;
109
162
  try {
110
- a = await fetch(`${o}/manifest.json`);
111
- } catch (n) {
163
+ i = await fetch(`${o}/manifest.json`);
164
+ } catch (f) {
112
165
  return console.warn(
113
- `Could not reach GitHub to fetch ${s}'s golden baseline — source-of-truth divergence check skipped for this run.`,
114
- n
166
+ `Could not reach GitHub to fetch ${r}'s golden baseline — source-of-truth divergence check skipped for this run.`,
167
+ f
115
168
  ), null;
116
169
  }
117
- if (!a.ok)
170
+ if (!i.ok)
118
171
  return console.warn(
119
- `No golden baseline published for ${s} — source-of-truth divergence check skipped for this run.`
172
+ `No golden baseline published for ${r} — source-of-truth divergence check skipped for this run.`
120
173
  ), null;
121
- const i = await a.text(), d = JSON.parse(i);
122
- S(d, `${o}/manifest.json`), w(r, { recursive: !0 }), y(u, i), c = d;
174
+ const c = await i.text(), u = JSON.parse(c);
175
+ M(u, `${o}/manifest.json`), x(n, { recursive: !0 }), P(d, c), a = u;
123
176
  }
124
177
  return {
125
- manifest: c,
126
- async readImage(a) {
127
- const i = $(r, a);
128
- if (l(i)) return g(i);
129
- const d = await fetch(`${o}/${a}.png`);
130
- if (!d.ok) return null;
131
- const n = Buffer.from(await d.arrayBuffer());
132
- return w(r, { recursive: !0 }), y(i, n), n;
178
+ manifest: a,
179
+ async readImage(i) {
180
+ const c = O(n, i);
181
+ if (g(c)) return $(c);
182
+ const u = await fetch(`${o}/${i}.png`);
183
+ if (!u.ok) return null;
184
+ const f = Buffer.from(await u.arrayBuffer());
185
+ return x(n, { recursive: !0 }), P(c, f), f;
133
186
  }
134
187
  };
135
188
  }
136
- const re = ["Theme", "Tokens", "Introduction"];
189
+ const ye = ["Theme", "Tokens", "Introduction"];
137
190
  function D(e, t) {
138
191
  return e === t || e.startsWith(t);
139
192
  }
140
- async function se(e, t, r) {
141
- let s;
193
+ async function ve(e, t, n) {
194
+ let r;
142
195
  try {
143
196
  const o = await fetch(`${e.url}/index.json`);
144
197
  if (!o.ok)
145
198
  throw new Error(
146
199
  `Failed to fetch Storybook index: ${o.statusText}`
147
200
  );
148
- const u = (await o.json()).entries || {};
149
- s = Object.values(u).filter(
150
- (a) => a.type === "story" && !t.some(
151
- (i) => a.title === i || a.title.startsWith(`${i}/`)
152
- ) && !r.some((i) => D(a.id, i))
153
- ), s.sort((a, i) => a.id.localeCompare(i.id));
201
+ const d = (await o.json()).entries || {};
202
+ r = Object.values(d).filter(
203
+ (i) => i.type === "story" && !t.some(
204
+ (c) => i.title === c || i.title.startsWith(`${c}/`)
205
+ ) && !n.some((c) => D(i.id, c))
206
+ ), r.sort((i, c) => i.id.localeCompare(c.id));
154
207
  } catch (o) {
155
208
  throw console.error(
156
209
  "Failed to load Storybook index from",
@@ -160,82 +213,115 @@ async function se(e, t, r) {
160
213
  `Storybook target "${e.name}" is not responsive or index.json is missing. Please ensure its Storybook is running.`
161
214
  );
162
215
  }
163
- return s;
216
+ return r;
164
217
  }
165
- function oe(e, t, r) {
166
- let s;
218
+ function ke(e, t, n) {
219
+ let r;
167
220
  for (const o of Object.keys(t))
168
- D(e, o) && (!s || o.length > s.length) && (s = o);
169
- return s !== void 0 ? t[s] : r;
221
+ D(e, o) && (!r || o.length > r.length) && (r = o);
222
+ return r !== void 0 ? t[r] : n;
170
223
  }
171
- async function le(e) {
172
- const t = e.isSourceOfTruthAdapter ? e.targets[0] : e.targets.find((n) => !n.sourceOfTruth);
224
+ async function je(e) {
225
+ const t = e.isSourceOfTruthAdapter ? e.targets[0] : e.targets.find((s) => !s.sourceOfTruth);
173
226
  if (!t)
174
227
  throw new Error(
175
228
  "adapter-tester config has no non-sourceOfTruth target to run the golden check against."
176
229
  );
177
- const r = e.excludeTitlePrefixes ?? re, s = e.excludeStoryIds ?? [], o = e.storyThresholds ?? {}, c = e.goldenDir, u = e.goldenMode, a = await se(
230
+ const n = e.excludeTitlePrefixes ?? ye, r = e.stories ?? {}, o = Object.keys(r).filter(
231
+ (s) => r[s].exclude
232
+ ), a = Object.fromEntries(
233
+ Object.entries(r).filter(([, s]) => s.threshold !== void 0).map(([s, y]) => [s, y.threshold])
234
+ ), d = e.goldenDir, i = e.goldenMode, c = e.checkMode, u = await ve(
178
235
  t,
179
- r,
180
- s
181
- ), i = b(c), d = e.isSourceOfTruthAdapter || !e.sourceOfTruthGolden ? null : await te(e.sourceOfTruthGolden);
182
- I.describe(`${t.name} — Golden Image Visual Regression`, () => {
183
- for (const n of a)
184
- I(`Golden regression for: ${n.title} - ${n.name} (${n.id})`, async ({
185
- browser: B
186
- }, T) => {
187
- const p = await B.newPage();
188
- await p.setViewportSize({ width: 800, height: 600 }), await p.goto(
189
- `${t.url}/iframe.html?id=${n.id}&viewMode=story`,
190
- { waitUntil: "networkidle" }
191
- ), await p.waitForSelector("#storybook-root"), await p.waitForTimeout(300);
192
- const j = await p.screenshot(), k = $(c, n.id), f = i[n.id];
193
- if (u !== "check" || !f || !l(k))
194
- Q(c, n.id, j), i[n.id] = f != null && f.sourceOfTruthCreatedAt ? {
195
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
196
- sourceOfTruthCreatedAt: f.sourceOfTruthCreatedAt
197
- } : { createdAt: (/* @__PURE__ */ new Date()).toISOString() }, f || T.annotations.push({
198
- type: "golden-created",
199
- description: `No golden existed yet for "${n.id}" — captured one from this run.`
200
- });
201
- else {
202
- const h = g(k), m = G(j, h);
203
- await T.attach("Live vs Golden Diff", {
204
- body: `${m} mismatched pixels`,
205
- contentType: "text/plain"
206
- });
207
- const x = oe(
208
- n.id,
209
- o,
210
- e.diffThresholdPixels
211
- );
212
- M.soft(
213
- m,
214
- `"${n.id}" has drifted from its own golden image`
215
- ).toBeLessThan(x);
216
- }
217
- const A = i[n.id];
218
- if (d) {
219
- const h = d.manifest[n.id], m = h ? await d.readImage(n.id) : null;
220
- if (h && m)
221
- if (u === "approve-divergence")
222
- i[n.id] = {
223
- ...A,
224
- sourceOfTruthCreatedAt: h.createdAt
225
- };
226
- else {
227
- const x = g(k), F = G(x, m), E = A.sourceOfTruthCreatedAt;
228
- F === 0 || E !== void 0 && E >= h.createdAt || T.annotations.push({
229
- type: "source-of-truth-divergence",
230
- description: `"${n.id}" differs from the source of truth's golden and hasn't been reviewed — run with --approve-divergence if this is intentional, or fix the adapter styling.`
231
- });
232
- }
233
- }
234
- z(c, i);
235
- });
236
- });
236
+ n,
237
+ o
238
+ );
239
+ if (i === "update-golden") {
240
+ const s = await le(
241
+ d,
242
+ new Set(u.map((y) => y.id))
243
+ );
244
+ s.length > 0 && console.warn(
245
+ `Pruned ${s.length} orphaned golden(s) no longer in Storybook: ${s.join(", ")}`
246
+ );
247
+ }
248
+ const f = c !== "divergence" || e.isSourceOfTruthAdapter || !e.sourceOfTruthGolden ? null : await we(e.sourceOfTruthGolden), R = c === "divergence" ? "Source-of-Truth Divergence Check" : "Own-Drift Golden Image Check";
249
+ return {
250
+ ownTargetName: t.name,
251
+ suiteLabel: R,
252
+ stories: u,
253
+ checkStory: async (s, y, h) => {
254
+ const v = await y.newPage();
255
+ await v.setViewportSize({ width: 800, height: 600 }), await v.goto(
256
+ `${t.url}/iframe.html?id=${s.id}&viewMode=story`,
257
+ { waitUntil: "networkidle" }
258
+ ), await v.waitForSelector("#storybook-root"), await v.waitForTimeout(300);
259
+ const A = await v.screenshot(), I = O(d, s.id), l = b(d)[s.id], U = i !== "check" || !l || !g(I);
260
+ let w;
261
+ if (U)
262
+ he(d, s.id, A), w = l != null && l.sourceOfTruthCreatedAt ? {
263
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
264
+ sourceOfTruthCreatedAt: l.sourceOfTruthCreatedAt
265
+ } : { createdAt: (/* @__PURE__ */ new Date()).toISOString() }, l || h.annotations.push({
266
+ type: "golden-created",
267
+ description: `No golden existed yet for "${s.id}" — captured one from this run.`
268
+ });
269
+ else if (w = l, c === "own") {
270
+ const p = $(I), { diffPixels: m, diffImage: k } = N(
271
+ A,
272
+ p
273
+ ), T = ke(
274
+ s.id,
275
+ a,
276
+ e.diffThresholdPixels
277
+ );
278
+ m >= T && (await h.attach("expected", {
279
+ body: p,
280
+ contentType: "image/png"
281
+ }), await h.attach("actual", {
282
+ body: A,
283
+ contentType: "image/png"
284
+ }), k && await h.attach("diff", {
285
+ body: k,
286
+ contentType: "image/png"
287
+ })), K.soft(
288
+ m,
289
+ `"${s.id}" has drifted from its own golden image (${m} mismatched pixels, threshold ${T})`
290
+ ).toBeLessThan(T);
291
+ }
292
+ if (f) {
293
+ const p = f.manifest[s.id], m = p ? await f.readImage(s.id) : null;
294
+ if (p && m)
295
+ if (i === "approve-divergence")
296
+ w = {
297
+ ...w,
298
+ sourceOfTruthCreatedAt: p.createdAt
299
+ };
300
+ else {
301
+ const k = $(I), { diffPixels: T, diffImage: C } = N(
302
+ k,
303
+ m
304
+ ), G = w.sourceOfTruthCreatedAt;
305
+ T === 0 || G !== void 0 && G >= p.createdAt || (await h.attach("expected", {
306
+ body: m,
307
+ contentType: "image/png"
308
+ }), await h.attach("actual", {
309
+ body: k,
310
+ contentType: "image/png"
311
+ }), C && await h.attach("diff", {
312
+ body: C,
313
+ contentType: "image/png"
314
+ }), h.annotations.push({
315
+ type: "source-of-truth-divergence",
316
+ description: `"${s.id}" differs from the source of truth's golden and hasn't been reviewed — run with --approve-divergence if this is intentional, or fix the adapter styling.`
317
+ }));
318
+ }
319
+ }
320
+ await _(d, s.id, () => w);
321
+ }
322
+ };
237
323
  }
238
324
  export {
239
- le as runVisualRegression
325
+ je as resolveVisualRegressionPlan
240
326
  };
241
327
  //# sourceMappingURL=testing.js.map