@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.
- package/dist/analysis/affected.d.ts +21 -0
- package/dist/analysis/affected.js +37 -0
- package/dist/analysis/affected.js.map +1 -1
- package/dist/capture/capture.js +8 -1
- package/dist/capture/capture.js.map +1 -1
- package/dist/capture/env.d.ts +8 -0
- package/dist/capture/env.js +38 -0
- package/dist/capture/env.js.map +1 -0
- package/dist/index.d.ts +11 -11
- package/dist/index.js +28 -11
- package/dist/index.js.map +1 -1
- package/dist/report/comment.d.ts +2 -1
- package/dist/report/comment.js +299 -57
- package/dist/report/comment.js.map +1 -1
- package/dist/report/format.d.ts +2 -0
- package/dist/report/format.js +12 -0
- package/dist/report/format.js.map +1 -0
- package/dist/report/github.d.ts +12 -0
- package/dist/report/github.js +21 -0
- package/dist/report/github.js.map +1 -1
- package/dist/report/html.d.ts +12 -0
- package/dist/report/html.js +202 -0
- package/dist/report/html.js.map +1 -0
- package/dist/run/phases.d.ts +19 -0
- package/dist/run/phases.js +50 -0
- package/dist/run/phases.js.map +1 -0
- package/dist/run/run.d.ts +4 -0
- package/dist/run/run.js +25 -5
- package/dist/run/run.js.map +1 -1
- package/dist/types.d.ts +35 -2
- package/package.json +11 -2
|
@@ -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, '&')
|
|
121
|
+
.replace(/</g, '<')
|
|
122
|
+
.replace(/>/g, '>')
|
|
123
|
+
.replace(/"/g, '"');
|
|
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
|
|
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
|
-
|
|
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,
|
package/dist/run/run.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/run/run.ts"],"names":[],"mappings":"AAAA,oEAAoE;AACpE,EAAE;AACF,gFAAgF;AAChF,6EAA6E;AAC7E,2EAA2E;AAC3E,qEAAqE;
|
|
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.
|
|
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.
|
|
30
|
+
"sharp": "^0.35.3"
|
|
22
31
|
},
|
|
23
32
|
"peerDependencies": {
|
|
24
33
|
"playwright": "^1.48.0"
|