@juxtapose/core 0.2.0 → 0.4.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.
@@ -0,0 +1,202 @@
1
+ // A single self-contained report.html.
2
+ //
3
+ // The constraint that shapes everything here: it must work opened from
4
+ // `file://`, because that is how someone downloads a CI artifact and looks at
5
+ // it. No external stylesheet, no script src, no sibling image directory —
6
+ // images inline as data URIs and the whole thing is one file you can email.
7
+ //
8
+ // This complements rather than duplicates the addon panel. The panel is for
9
+ // local review with a dev server running; this is for a reviewer looking at a
10
+ // pull request who has checked nothing out.
11
+ import { formatPercent } from './format.js';
12
+ export function buildHtmlReport({ report, images }) {
13
+ const needsReview = report.results.filter((r) => r.status === 'fail' || r.status === 'new' || r.status === 'stale');
14
+ const s = report.summary;
15
+ return `<!doctype html>
16
+ <meta charset="utf-8">
17
+ <meta name="viewport" content="width=device-width, initial-scale=1">
18
+ <title>juxtapose — ${escapeHtml(report.branch)} @ ${escapeHtml(report.commitSha.slice(0, 7))}</title>
19
+ <style>${STYLE}</style>
20
+
21
+ <header>
22
+ <h1>${needsReview.length === 0 ? 'Visual tests passed' : 'Visual changes need review'}</h1>
23
+ <dl>
24
+ ${stat('pass', s.pass)}
25
+ ${s.fail ? stat('changed', s.fail, 'bad') : ''}
26
+ ${s.new ? stat('new', s.new, 'warn') : ''}
27
+ ${s.stale ? stat('stale', s.stale, 'warn') : ''}
28
+ ${s.skip ? stat('skipped', s.skip) : ''}
29
+ </dl>
30
+ <p class="meta">
31
+ ${escapeHtml(report.branch)} ·
32
+ <code>${escapeHtml(report.commitSha.slice(0, 7))}</code> ·
33
+ ${report.capturedCount}/${report.totalCount} captured ·
34
+ ${(report.durationMs / 1000).toFixed(1)}s
35
+ </p>
36
+ </header>
37
+
38
+ ${needsReview.length === 0
39
+ ? `<p class="empty">Every captured story matches its approved baseline.</p>`
40
+ : needsReview.map((r) => storySection(r, images.get(r.storyId))).join('\n')}
41
+
42
+ <script>${SCRIPT}</script>
43
+ `;
44
+ }
45
+ function stat(label, value, tone = '') {
46
+ return `<div class="${tone}"><dt>${label}</dt><dd>${value}</dd></div>`;
47
+ }
48
+ function storySection(result, imgs) {
49
+ const baseline = dataUri(imgs?.baseline);
50
+ const current = dataUri(imgs?.current);
51
+ const diff = dataUri(imgs?.diff);
52
+ // A new story has no baseline, so there is nothing to compare — show the
53
+ // capture alone rather than an empty half. Same reasoning as the panel:
54
+ // controls that compare nothing read as broken.
55
+ const comparable = Boolean(baseline && current);
56
+ return `
57
+ <section>
58
+ <h2>
59
+ <span class="badge ${result.status}">${statusLabel(result.status)}</span>
60
+ ${escapeHtml(result.storyId)}
61
+ ${result.status === 'fail' ? `<span class="pct">${escapeHtml(formatPercent(result.mismatchPercent))} changed</span>` : ''}
62
+ </h2>
63
+ ${result.error ? `<p class="error">${escapeHtml(result.error)}</p>` : ''}
64
+ ${comparable
65
+ ? `
66
+ <div class="tabs">
67
+ <button data-mode="sxs" class="on">Side by side</button>
68
+ <button data-mode="slider">Slider</button>
69
+ ${diff ? `<button data-mode="diff">Diff</button>` : ''}
70
+ </div>
71
+
72
+ <div class="views">
73
+ <div class="view sxs on">
74
+ <figure><figcaption>Baseline</figcaption><img src="${baseline}" alt="baseline"></figure>
75
+ <figure><figcaption>Current</figcaption><img src="${current}" alt="current"></figure>
76
+ </div>
77
+
78
+ <div class="view slider">
79
+ <div class="wipe" style="--pos:50%">
80
+ <img src="${baseline}" alt="baseline">
81
+ <img class="over" src="${current}" alt="current">
82
+ <div class="handle"></div>
83
+ </div>
84
+ <input type="range" min="0" max="100" value="50" aria-label="Wipe between baseline and current">
85
+ </div>
86
+
87
+ ${diff ? `<div class="view diff"><img src="${diff}" alt="diff overlay"></div>` : ''}
88
+ </div>`
89
+ : current
90
+ ? `<div class="single"><img src="${current}" alt="capture"></div>`
91
+ : `<p class="error">No capture available for this story.</p>`}
92
+ </section>`;
93
+ }
94
+ /**
95
+ * Inline an image.
96
+ *
97
+ * Data URIs are what make the file portable — a sibling images/ directory
98
+ * breaks the moment someone moves or emails the html. The cost is roughly 33%
99
+ * inflation from base64, acceptable for a CI artifact.
100
+ */
101
+ function dataUri(bytes) {
102
+ return bytes ? `data:image/webp;base64,${bytes.toString('base64')}` : null;
103
+ }
104
+ function statusLabel(status) {
105
+ switch (status) {
106
+ case 'fail':
107
+ return 'changed';
108
+ case 'new':
109
+ return 'no baseline';
110
+ case 'stale':
111
+ return 'stale';
112
+ default:
113
+ return status;
114
+ }
115
+ }
116
+ /** Escape anything interpolated into markup. Story ids come from the platform
117
+ * and are not ours to trust. */
118
+ function escapeHtml(value) {
119
+ return value
120
+ .replace(/&/g, '&amp;')
121
+ .replace(/</g, '&lt;')
122
+ .replace(/>/g, '&gt;')
123
+ .replace(/"/g, '&quot;');
124
+ }
125
+ const STYLE = `
126
+ :root {
127
+ --fg: #0f172a; --muted: #64748b; --line: #e2e8f0; --bg: #fff;
128
+ --bad: #dc2626; --warn: #d97706;
129
+ font-family: system-ui, -apple-system, sans-serif;
130
+ }
131
+ @media (prefers-color-scheme: dark) {
132
+ :root { --fg: #e2e8f0; --muted: #94a3b8; --line: #334155; --bg: #0f172a; }
133
+ }
134
+ * { box-sizing: border-box; }
135
+ body { margin: 0 auto; padding: 24px; max-width: 1200px;
136
+ color: var(--fg); background: var(--bg); }
137
+ h1 { font-size: 20px; margin: 0 0 12px; }
138
+ dl { display: flex; gap: 20px; margin: 0 0 8px; padding: 0; }
139
+ dl div { display: flex; gap: 6px; align-items: baseline; }
140
+ dt { color: var(--muted); font-size: 13px; }
141
+ dd { margin: 0; font-weight: 600; font-variant-numeric: tabular-nums; }
142
+ dl .bad dd { color: var(--bad); } dl .warn dd { color: var(--warn); }
143
+ .meta { color: var(--muted); font-size: 13px; margin: 0 0 28px; }
144
+ .empty { color: var(--muted); }
145
+ section { border-top: 1px solid var(--line); padding: 20px 0; }
146
+ h2 { font-size: 15px; font-weight: 600; display: flex; align-items: center;
147
+ gap: 10px; margin: 0 0 12px; }
148
+ .badge { font-size: 11px; text-transform: uppercase; letter-spacing: .04em;
149
+ padding: 2px 8px; border-radius: 999px; color: #fff; }
150
+ .badge.fail { background: var(--bad); }
151
+ .badge.new, .badge.stale { background: var(--warn); }
152
+ .pct { color: var(--muted); font-weight: 400; font-size: 13px; }
153
+ .error { color: var(--bad); font-size: 13px; margin: 0 0 12px; }
154
+ .tabs { display: flex; gap: 4px; margin-bottom: 12px; }
155
+ .tabs button { font: inherit; font-size: 12px; padding: 4px 10px; cursor: pointer;
156
+ border: 1px solid var(--line); background: transparent;
157
+ color: var(--muted); border-radius: 4px; }
158
+ .tabs button.on { background: var(--fg); color: var(--bg); border-color: var(--fg); }
159
+ .view { display: none; }
160
+ .view.on { display: block; }
161
+ .view.sxs.on { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
162
+ figure { margin: 0; }
163
+ figcaption { font-size: 11px; text-transform: uppercase; letter-spacing: .04em;
164
+ color: var(--muted); margin-bottom: 6px; }
165
+ img { max-width: 100%; height: auto; display: block; border: 1px solid var(--line); }
166
+ .single img { max-width: 640px; }
167
+ .wipe { position: relative; display: inline-block; max-width: 100%; }
168
+ .wipe .over { position: absolute; inset: 0; clip-path: inset(0 0 0 var(--pos)); }
169
+ .wipe .handle { position: absolute; top: 0; bottom: 0; left: var(--pos);
170
+ width: 2px; background: var(--bad); pointer-events: none; }
171
+ input[type=range] { width: 100%; margin-top: 8px; }
172
+ `;
173
+ /**
174
+ * Tab switching and the wipe slider.
175
+ *
176
+ * Inline and dependency-free on purpose: a `file://` page cannot load anything
177
+ * external, and a report needing a build step to view defeats the point of
178
+ * being an artifact.
179
+ */
180
+ const SCRIPT = `
181
+ document.querySelectorAll('section').forEach(function (section) {
182
+ section.querySelectorAll('.tabs button').forEach(function (btn) {
183
+ btn.addEventListener('click', function () {
184
+ section.querySelectorAll('.tabs button').forEach(function (b) {
185
+ b.classList.toggle('on', b === btn);
186
+ });
187
+ section.querySelectorAll('.view').forEach(function (v) {
188
+ v.classList.toggle('on', v.classList.contains(btn.dataset.mode));
189
+ });
190
+ });
191
+ });
192
+
193
+ var range = section.querySelector('input[type=range]');
194
+ var wipe = section.querySelector('.wipe');
195
+ if (range && wipe) {
196
+ range.addEventListener('input', function () {
197
+ wipe.style.setProperty('--pos', range.value + '%');
198
+ });
199
+ }
200
+ });
201
+ `;
202
+ //# sourceMappingURL=html.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html.js","sourceRoot":"","sources":["../../src/report/html.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ export interface Phases {
2
+ /** Time a promise-returning step, recording it under `name`. */
3
+ time<T>(name: string, fn: () => Promise<T>): Promise<T>;
4
+ /** Add to a phase directly, for work not shaped like a single call. */
5
+ add(name: string, ms: number): void;
6
+ /** Count of things a phase handled, e.g. how many diffs actually ran. */
7
+ count(name: string, n?: number): void;
8
+ readonly timings: Record<string, number>;
9
+ readonly counts: Record<string, number>;
10
+ }
11
+ export declare function phases(): Phases;
12
+ /**
13
+ * `enumerate 12ms | capture 48.2s | diff(7) 810ms`
14
+ *
15
+ * Phases are printed in insertion order, which is execution order, so the line
16
+ * reads as the run happened. Sub-millisecond phases are dropped: they are noise
17
+ * next to a capture measured in seconds.
18
+ */
19
+ export declare function formatPhases(p: Pick<Phases, 'timings' | 'counts'>): string;
@@ -0,0 +1,50 @@
1
+ // Wall-time per stage of a run.
2
+ //
3
+ // A run reports one total today, which is enough to know it was slow and not
4
+ // enough to know why. The phases have very different cost profiles — capture
5
+ // dominates, diff is a cold path that only runs for stories that already
6
+ // failed, and enumerate/affected are fixed overhead — so the total alone
7
+ // cannot distinguish "700 stories" from "7 stories and a broken diff".
8
+ export function phases() {
9
+ const timings = {};
10
+ const counts = {};
11
+ const add = (name, ms) => {
12
+ timings[name] = (timings[name] ?? 0) + ms;
13
+ };
14
+ return {
15
+ timings,
16
+ counts,
17
+ add,
18
+ count(name, n = 1) {
19
+ counts[name] = (counts[name] ?? 0) + n;
20
+ },
21
+ async time(name, fn) {
22
+ const started = Date.now();
23
+ try {
24
+ return await fn();
25
+ }
26
+ finally {
27
+ add(name, Date.now() - started);
28
+ }
29
+ },
30
+ };
31
+ }
32
+ /**
33
+ * `enumerate 12ms | capture 48.2s | diff(7) 810ms`
34
+ *
35
+ * Phases are printed in insertion order, which is execution order, so the line
36
+ * reads as the run happened. Sub-millisecond phases are dropped: they are noise
37
+ * next to a capture measured in seconds.
38
+ */
39
+ export function formatPhases(p) {
40
+ const parts = [];
41
+ for (const [name, ms] of Object.entries(p.timings)) {
42
+ if (ms < 1)
43
+ continue;
44
+ const n = p.counts[name];
45
+ const label = n === undefined ? name : `${name}(${n})`;
46
+ parts.push(`${label} ${ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${ms}ms`}`);
47
+ }
48
+ return parts.join(' | ');
49
+ }
50
+ //# sourceMappingURL=phases.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"phases.js","sourceRoot":"","sources":["../../src/run/phases.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,EAAE;AACF,6EAA6E;AAC7E,6EAA6E;AAC7E,yEAAyE;AACzE,yEAAyE;AACzE,uEAAuE;AAavE,MAAM,UAAU,MAAM;IACpB,MAAM,OAAO,GAA2B,EAAE,CAAC;IAC3C,MAAM,MAAM,GAA2B,EAAE,CAAC;IAE1C,MAAM,GAAG,GAAG,CAAC,IAAY,EAAE,EAAU,EAAE,EAAE;QACvC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,CAAC;IAC5C,CAAC,CAAC;IAEF,OAAO;QACL,OAAO;QACP,MAAM;QACN,GAAG;QACH,KAAK,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC;YACf,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;QACzC,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE;YACjB,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YAC3B,IAAI,CAAC;gBACH,OAAO,MAAM,EAAE,EAAE,CAAC;YACpB,CAAC;oBAAS,CAAC;gBACT,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,CAAC;YAClC,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,CAAqC;IAChE,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;QACnD,IAAI,EAAE,GAAG,CAAC;YAAE,SAAS;QACrB,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACzB,MAAM,KAAK,GAAG,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC;QACvD,KAAK,CAAC,IAAI,CAAC,GAAG,KAAK,IAAI,EAAE,IAAI,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC;IAClF,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC3B,CAAC"}
package/dist/run/run.d.ts CHANGED
@@ -2,6 +2,7 @@ import type { Browser } from 'playwright';
2
2
  import type { Platform } from '../platform.js';
3
3
  import type { BaselineStore, DiffStatus, ProjectConfig, RunReport, StoryManifest, Viewport } from '../types.js';
4
4
  import { type AncestryCheck } from '../baseline/status.js';
5
+ import { type Phases } from './phases.js';
5
6
  export declare const DEFAULT_VIEWPORT: Viewport;
6
7
  export interface RunOptions {
7
8
  browser: Browser;
@@ -20,6 +21,9 @@ export interface RunOptions {
20
21
  onArtifact?: (artifact: RunArtifact) => Promise<void>;
21
22
  onProgress?: (done: number, total: number, storyId: string) => void;
22
23
  signal?: AbortSignal;
24
+ /** Collects per-phase wall time. Pass one to fold in phases the caller timed
25
+ * itself (enumerate, affected, launch); omit and one is made internally. */
26
+ phases?: Phases;
23
27
  }
24
28
  export interface RunArtifact {
25
29
  storyId: string;
package/dist/run/run.js CHANGED
@@ -4,15 +4,18 @@
4
4
  // so it runs for every story. Decoding and diffing are expensive and explain
5
5
  // nothing about a passing story, so they run only for stories that already
6
6
  // failed. On a green run this function never decodes a single image.
7
+ import { captureEnv } from '../capture/env.js';
7
8
  import { capturePool } from '../capture/pool.js';
8
9
  import { resolveStatus, toDiffStatus } from '../baseline/status.js';
9
10
  import { diffImages, resolveThreshold, resolveMinDiffPercent } from '../diff/diff.js';
11
+ import { phases } from './phases.js';
10
12
  export const DEFAULT_VIEWPORT = { width: 1280, height: 800 };
11
13
  export async function runCapture(opts) {
12
14
  const startedAt = new Date();
13
15
  const started = Date.now();
14
16
  const viewport = opts.config?.viewport ?? DEFAULT_VIEWPORT;
15
- const manifest = await opts.store.readManifest();
17
+ const p = opts.phases ?? phases();
18
+ const manifest = await p.time('baselines', () => opts.store.readManifest());
16
19
  const results = [];
17
20
  const pool = capturePool(opts.stories, {
18
21
  browser: opts.browser,
@@ -24,6 +27,10 @@ export async function runCapture(opts) {
24
27
  onProgress: opts.onProgress,
25
28
  signal: opts.signal,
26
29
  });
30
+ // Capture is measured as the whole streamed loop rather than per story: the
31
+ // pool runs `concurrency` pages at once, so summing per-story durations would
32
+ // report several times the wall-clock actually spent.
33
+ const captureStarted = Date.now();
27
34
  for await (const outcome of pool) {
28
35
  if (outcome.skipped) {
29
36
  results.push({
@@ -48,13 +55,19 @@ export async function runCapture(opts) {
48
55
  });
49
56
  continue;
50
57
  }
51
- results.push(await judge(outcome.snapshot, outcome.story, manifest, opts));
58
+ results.push(await judge(outcome.snapshot, outcome.story, manifest, opts, p));
52
59
  }
60
+ // Diffing happens inside the loop, so subtract it to leave capture alone.
61
+ p.add('capture', Date.now() - captureStarted - (p.timings.diff ?? 0));
53
62
  const summary = tally(results);
54
63
  return {
55
64
  runId: opts.runId,
56
65
  branch: opts.branch,
57
66
  commitSha: opts.commitSha,
67
+ // Carried on the report because `approve` has no browser of its own — it
68
+ // reads captured images off disk, so this is the only path by which the
69
+ // fingerprint reaches a baseline entry.
70
+ env: captureEnv(opts.browser),
58
71
  capturedCount: opts.stories.length,
59
72
  totalCount: opts.totalCount,
60
73
  results: results.sort((a, b) => a.storyId.localeCompare(b.storyId)),
@@ -64,7 +77,7 @@ export async function runCapture(opts) {
64
77
  };
65
78
  }
66
79
  /** Decide one story's fate. Cheap path first. */
67
- async function judge(snapshot, story, manifest, opts) {
80
+ async function judge(snapshot, story, manifest, opts, p) {
68
81
  const baselineStatus = resolveStatus(story.storyId, snapshot.image, manifest, opts.ancestry);
69
82
  const status = toDiffStatus(baselineStatus);
70
83
  const base = {
@@ -73,6 +86,10 @@ async function judge(snapshot, story, manifest, opts) {
73
86
  mismatchPixels: 0,
74
87
  mismatchPercent: 0,
75
88
  changedRegions: [],
89
+ // The effective viewport, including any per-story override. Recording it
90
+ // with the regions means nobody downstream has to re-enumerate stories to
91
+ // find out what the coordinates are relative to.
92
+ viewport: snapshot.viewport,
76
93
  durationMs: snapshot.durationMs,
77
94
  };
78
95
  // pass / new / stale need no pixel work: there is either nothing to explain
@@ -87,10 +104,13 @@ async function judge(snapshot, story, manifest, opts) {
87
104
  await opts.onArtifact?.({ storyId: story.storyId, status, current: snapshot.image });
88
105
  return { ...base, error: 'baseline missing from store' };
89
106
  }
90
- const diff = await diffImages(baseline, snapshot.image, {
107
+ // Counted as well as timed: under a byte-exact hash the number of diffs is
108
+ // how broken the run was, and it is the figure that explains a slow one.
109
+ p.count('diff');
110
+ const diff = await p.time('diff', () => diffImages(baseline, snapshot.image, {
91
111
  threshold: resolveThreshold(story.params, opts.config),
92
112
  minDiffPercent: resolveMinDiffPercent(story.params, opts.config),
93
- });
113
+ }));
94
114
  await opts.onArtifact?.({
95
115
  storyId: story.storyId,
96
116
  status,
@@ -1 +1 @@
1
- {"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/run/run.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/run/run.ts"],"names":[],"mappings":"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"}
package/dist/types.d.ts CHANGED
@@ -18,6 +18,14 @@ export interface StoryParams {
18
18
  waitForSelector?: string;
19
19
  /** Max ms to wait for render-ready before screenshotting anyway. Default 2000. */
20
20
  captureTimeout?: number;
21
+ /** Selectors to cover with a solid box before the screenshot. For a clock or
22
+ * a random avatar; prefer stubbing the input. Masks the element's bounding
23
+ * box, so give it a fixed size or the box resizes with the content. */
24
+ mask?: string[];
25
+ /** Exclude from watch-mode recapture while leaving normal runs alone. For
26
+ * stories whose play function or delay makes a keystroke-triggered capture
27
+ * cost more than it returns. Default true. */
28
+ liveCapture?: boolean;
21
29
  }
22
30
  export interface Viewport {
23
31
  width: number;
@@ -59,6 +67,13 @@ export interface DiffResult {
59
67
  mismatchPercent: number;
60
68
  /** Bounding boxes of changed clusters. Empty unless status is 'fail'. */
61
69
  changedRegions: ChangedRegion[];
70
+ /** The viewport the capture was taken at — what `changedRegions` coordinates
71
+ * are relative to. Absent only when the capture never produced a snapshot.
72
+ *
73
+ * Recording it here is what lets a consumer describe a region spatially
74
+ * without re-enumerating stories to recover a per-story viewport override,
75
+ * which is both slow and wrong when the override exists. */
76
+ viewport?: Viewport;
62
77
  /** Set when the capture or diff threw. status is 'fail' in that case. */
63
78
  error?: string;
64
79
  durationMs: number;
@@ -67,6 +82,9 @@ export interface RunReport {
67
82
  runId: string;
68
83
  branch: string;
69
84
  commitSha: string;
85
+ /** What captured these pixels. Optional so a report written by an older
86
+ * version still parses. */
87
+ env?: CaptureEnv;
70
88
  /** How many stories we captured vs how many exist. */
71
89
  capturedCount: number;
72
90
  totalCount: number;
@@ -75,10 +93,27 @@ export interface RunReport {
75
93
  durationMs: number;
76
94
  startedAt: string;
77
95
  }
96
+ /** What rendered the pixels. Baselines from different environments are not
97
+ * comparable byte-for-byte, so recording this is what lets an all-red suite
98
+ * after a toolchain bump be reported as a bump rather than as N regressions. */
99
+ export interface CaptureEnv {
100
+ /** Playwright's `browser.version()`, e.g. "151.0.7922.34". */
101
+ browser: string;
102
+ /** `${platform}-${arch}`, e.g. "darwin-arm64". */
103
+ os: string;
104
+ /** libvips, which encodes the WebP. Measured to move bytes far more readily
105
+ * than the browser does: a libvips bump changed all 17 example baseline
106
+ * hashes with zero changed pixels, while Chromium 149 -> 151 left 16 of 18
107
+ * byte-identical. */
108
+ encoder: string;
109
+ }
78
110
  export interface ApprovalMeta {
79
111
  commit: string;
80
112
  approvedBy: string;
81
113
  approvedAt: string;
114
+ /** Absent on entries approved before this was recorded. Absent must not read
115
+ * as "matches": an unknown origin is unknown, not current. */
116
+ env?: CaptureEnv;
82
117
  }
83
118
  export interface BaselineEntry extends ApprovalMeta {
84
119
  /** sha256 of the baseline image bytes. Detects drift between manifest
@@ -110,8 +145,6 @@ export interface ProjectConfig {
110
145
  viewport?: Viewport;
111
146
  threshold?: number;
112
147
  minDiffPercent?: number;
113
- /** Glob patterns of story ids to never capture. */
114
- ignore?: string[];
115
148
  storage?: StorageConfig;
116
149
  }
117
150
  export type StorageConfig = {
package/package.json CHANGED
@@ -1,8 +1,16 @@
1
1
  {
2
2
  "name": "@juxtapose/core",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "Platform-agnostic capture, diff, baseline and analysis core for juxtapose.",
6
+ "keywords": [
7
+ "visual-regression",
8
+ "screenshot-testing",
9
+ "playwright",
10
+ "visual-testing",
11
+ "pixelmatch"
12
+ ],
13
+ "homepage": "https://github.com/juxtapose-dev/juxtapose/tree/main/packages/core#readme",
6
14
  "publishConfig": {
7
15
  "access": "public"
8
16
  },
@@ -11,6 +19,7 @@
11
19
  "types": "./dist/index.d.ts",
12
20
  "exports": {
13
21
  ".": "./dist/index.js",
22
+ "./format": "./dist/report/format.js",
14
23
  "./package.json": "./package.json"
15
24
  },
16
25
  "files": [
@@ -18,7 +27,7 @@
18
27
  ],
19
28
  "dependencies": {
20
29
  "pixelmatch": "^6.0.0",
21
- "sharp": "^0.33.5"
30
+ "sharp": "^0.35.3"
22
31
  },
23
32
  "peerDependencies": {
24
33
  "playwright": "^1.48.0"