tuffgal 0.2.0-alpha.2 → 0.2.0-alpha.4
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/CHANGELOG.md +41 -19
- package/README.md +6 -6
- package/dist/cli.d.ts +5 -5
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +12 -5
- package/dist/cli.js.map +1 -1
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +6 -6
- package/dist/commands/supervise.d.ts +3 -3
- package/dist/commands/supervise.js +8 -4
- package/dist/commands/supervise.js.map +1 -1
- package/dist/config.d.ts +29 -13
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +5 -2
- package/dist/config.js.map +1 -1
- package/dist/coverage/screens.d.ts +10 -5
- package/dist/coverage/screens.d.ts.map +1 -1
- package/dist/coverage/screens.js +18 -8
- package/dist/coverage/screens.js.map +1 -1
- package/dist/reporter/assets/report.css +11 -11
- package/dist/reporter/assets/report.js +81 -21
- package/dist/reporter/template.js +56 -45
- package/dist/reporter/template.js.map +1 -1
- package/dist/runner/approve.d.ts +5 -5
- package/dist/runner/approve.js +5 -5
- package/dist/runner/approveFrom.d.ts +8 -8
- package/dist/runner/approveFrom.js +32 -31
- package/dist/runner/approveFrom.js.map +1 -1
- package/dist/runner/breakpointPasses.d.ts +16 -9
- package/dist/runner/breakpointPasses.d.ts.map +1 -1
- package/dist/runner/breakpointPasses.js +17 -9
- package/dist/runner/breakpointPasses.js.map +1 -1
- package/dist/runner/bridges/devServers.d.ts +10 -1
- package/dist/runner/bridges/devServers.d.ts.map +1 -1
- package/dist/runner/bridges/devServers.js +16 -8
- package/dist/runner/bridges/devServers.js.map +1 -1
- package/dist/runner/coverage.d.ts +3 -3
- package/dist/runner/coverage.js +5 -5
- package/dist/runner/coverage.js.map +1 -1
- package/dist/runner/exitCode.d.ts +7 -7
- package/dist/runner/exitCode.js +7 -7
- package/dist/runner/interpolate.js +1 -1
- package/dist/runner/interpolate.js.map +1 -1
- package/dist/runner/manifest.d.ts +16 -16
- package/dist/runner/manifest.js +12 -12
- package/dist/runner/mode.d.ts +2 -2
- package/dist/runner/mode.js +4 -4
- package/dist/runner/orphanScan.d.ts +3 -3
- package/dist/runner/orphanScan.js +5 -5
- package/dist/runner/orphanScan.js.map +1 -1
- package/dist/runner/resolveLocator.d.ts +5 -5
- package/dist/runner/resolveLocator.js +6 -6
- package/dist/runner/resolveLocator.js.map +1 -1
- package/dist/runner/run.d.ts +55 -18
- package/dist/runner/run.d.ts.map +1 -1
- package/dist/runner/run.js +118 -67
- package/dist/runner/run.js.map +1 -1
- package/dist/runner/runAction.d.ts +4 -4
- package/dist/runner/runAction.d.ts.map +1 -1
- package/dist/runner/runAction.js +72 -34
- package/dist/runner/runAction.js.map +1 -1
- package/dist/runner/runStory.d.ts +29 -18
- package/dist/runner/runStory.d.ts.map +1 -1
- package/dist/runner/runStory.js +38 -42
- package/dist/runner/runStory.js.map +1 -1
- package/dist/runner/scheduler.d.ts +11 -0
- package/dist/runner/scheduler.d.ts.map +1 -1
- package/dist/runner/scheduler.js +1 -1
- package/dist/runner/scheduler.js.map +1 -1
- package/dist/runner/steps/intercept.d.ts +6 -4
- package/dist/runner/steps/intercept.d.ts.map +1 -1
- package/dist/runner/steps/intercept.js.map +1 -1
- package/dist/runner/steps/navigate.d.ts +5 -2
- package/dist/runner/steps/navigate.d.ts.map +1 -1
- package/dist/runner/steps/navigate.js +17 -3
- package/dist/runner/steps/navigate.js.map +1 -1
- package/dist/runner/steps/read.d.ts +1 -1
- package/dist/runner/steps/read.js +1 -1
- package/dist/runner/steps/scroll.d.ts +1 -1
- package/dist/runner/steps/scroll.d.ts.map +1 -1
- package/dist/runner/steps/scroll.js +3 -2
- package/dist/runner/steps/scroll.js.map +1 -1
- package/dist/schema/action.d.ts +2 -2
- package/dist/schema/action.d.ts.map +1 -1
- package/dist/schema/action.js +58 -13
- package/dist/schema/action.js.map +1 -1
- package/dist/schema/load.js +1 -1
- package/dist/schema/load.js.map +1 -1
- package/dist/schema/result.d.ts +22 -22
- package/dist/schema/result.d.ts.map +1 -1
- package/dist/schema/result.js +1 -1
- package/dist/schema/story.d.ts +1 -1
- package/dist/schema/story.js +5 -5
- package/dist/screenshots/baselineStore.d.ts +44 -21
- package/dist/screenshots/baselineStore.d.ts.map +1 -1
- package/dist/screenshots/baselineStore.js +59 -28
- package/dist/screenshots/baselineStore.js.map +1 -1
- package/dist/screenshots/capture.d.ts +35 -4
- package/dist/screenshots/capture.d.ts.map +1 -1
- package/dist/screenshots/capture.js +61 -9
- package/dist/screenshots/capture.js.map +1 -1
- package/dist/screenshots/diff.d.ts +57 -8
- package/dist/screenshots/diff.d.ts.map +1 -1
- package/dist/screenshots/diff.js +78 -28
- package/dist/screenshots/diff.js.map +1 -1
- package/dist/util.d.ts +23 -2
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +42 -2
- package/dist/util.js.map +1 -1
- package/package.json +4 -8
|
@@ -8,7 +8,7 @@ export interface BaselinePaths {
|
|
|
8
8
|
* Pre-breakpoint baseline location (`<action>/0.png`). Populated so a caller
|
|
9
9
|
* that finds no breakpoint-keyed baseline can fall back to a project's
|
|
10
10
|
* legacy committed baseline instead of declaring every action `new`. Read
|
|
11
|
-
* fallback only
|
|
11
|
+
* fallback only. The runner reads `baseline` first, then `legacyBaseline`;
|
|
12
12
|
* promotion always writes the breakpoint-keyed `baseline`, never this. See
|
|
13
13
|
* the read-order note below.
|
|
14
14
|
*/
|
|
@@ -19,7 +19,7 @@ export interface BaselinePaths {
|
|
|
19
19
|
* Proposed-new-baseline PNG for CI mode, under
|
|
20
20
|
* `<report>/candidates/<action>/<breakpoint>.png`. Mirrors the `baseline`
|
|
21
21
|
* layout exactly (action dir + breakpoint filename) so approving a candidate
|
|
22
|
-
* set is a plain tree copy into `paths.baselines`. Always report-rooted
|
|
22
|
+
* set is a plain tree copy into `paths.baselines`. Always report-rooted,
|
|
23
23
|
* never affected by the comparison root, which only moves the baseline side.
|
|
24
24
|
*/
|
|
25
25
|
candidate: string;
|
|
@@ -31,7 +31,7 @@ export interface StoreOptions {
|
|
|
31
31
|
/**
|
|
32
32
|
* Root the baseline-side paths (`baseline`, `a11yBaseline`, and the two
|
|
33
33
|
* `legacy*` fallbacks) resolve under. Defaults to `baselinesDir`, so an
|
|
34
|
-
* omitted root reproduces the committed-baselines layout byte-for-byte
|
|
34
|
+
* omitted root reproduces the committed-baselines layout byte-for-byte, the
|
|
35
35
|
* zero-behaviour-change default. Local mode passes the per-machine cache dir
|
|
36
36
|
* here to self-diff against it instead. Report-side artifacts (actual, diff,
|
|
37
37
|
* candidate, a11yActual, a11yCandidate) ignore this and stay report-rooted.
|
|
@@ -70,7 +70,7 @@ export declare const ACTION_NAME_PATTERN: RegExp;
|
|
|
70
70
|
*/
|
|
71
71
|
export declare const BREAKPOINT_SEGMENT_PATTERN: RegExp;
|
|
72
72
|
/**
|
|
73
|
-
* Computes deterministic paths for the baseline (the comparison target
|
|
73
|
+
* Computes deterministic paths for the baseline (the comparison target:
|
|
74
74
|
* committed baselines by default, or the local cache when `comparisonRoot` is
|
|
75
75
|
* supplied), actual (regenerated each run), diff (regenerated when a baseline
|
|
76
76
|
* existed and the diff was non-zero), and candidate (proposed-new-baseline for
|
|
@@ -110,31 +110,54 @@ export declare function writeText(path: string, content: string): Promise<void>;
|
|
|
110
110
|
* case) is swallowed and the original buffer is returned, so a recompress
|
|
111
111
|
* miss degrades to "write the bytes we were given" rather than a torn file.
|
|
112
112
|
*
|
|
113
|
-
* This is the
|
|
114
|
-
*
|
|
115
|
-
* writes
|
|
116
|
-
*
|
|
113
|
+
* This is the lossless-recompress seam for the tool's DURABLE PNG writes:
|
|
114
|
+
* {@link writeDurablePng} routes through it, so baseline, candidate, cache, and
|
|
115
|
+
* promoted-baseline writes inherit it without per-callsite wiring. Transient
|
|
116
|
+
* report artifacts (`actual`, `diff`) deliberately skip it via
|
|
117
|
+
* {@link writeTransientPng}. The max-effort encode is wasted on a file that is
|
|
118
|
+
* overwritten or deleted next run. Deterministic: pngjs's encoder is a pure
|
|
119
|
+
* function of the pixel data and the fixed options above.
|
|
117
120
|
*
|
|
118
121
|
* Deliberately NOT palette-quantised and never lossy: colour type and bit depth
|
|
119
122
|
* are preserved by the RGBA round-trip.
|
|
120
123
|
*/
|
|
121
124
|
export declare function recompressPng(png: Buffer): Buffer;
|
|
122
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Writes a DURABLE PNG (a baseline, a candidate, or a promoted baseline) that
|
|
127
|
+
* outlives the run and is re-read on every later comparison. These earn the
|
|
128
|
+
* max-effort lossless recompress (see {@link recompressPng}): the level-9
|
|
129
|
+
* deflate + adaptive per-scanline filtering is paid once at write, and every
|
|
130
|
+
* subsequent read of the smaller file benefits.
|
|
131
|
+
*
|
|
132
|
+
* Transient report artifacts (the run's `actual`, the `diff` overlay) are
|
|
133
|
+
* overwritten or deleted next run and must NOT pay this cost. They route
|
|
134
|
+
* through {@link writeTransientPng}, which skips recompress entirely.
|
|
135
|
+
*/
|
|
136
|
+
export declare function writeDurablePng(path: string, png: Buffer): Promise<void>;
|
|
137
|
+
/**
|
|
138
|
+
* Writes a TRANSIENT PNG (the run's `actual` capture or the `diff` overlay)
|
|
139
|
+
* verbatim, with NO recompress pass. These artifacts live only for the current
|
|
140
|
+
* report: an `actual` is overwritten on the next run and a `diff` is deleted the
|
|
141
|
+
* moment a comparison passes, so shrinking them with the expensive level-9
|
|
142
|
+
* encode is wasted work. The `diff` overlay in particular arrives already
|
|
143
|
+
* deflate-encoded by {@link renderDiffOverlay}'s `PNG.sync.write`, so a
|
|
144
|
+
* recompress round-trip would only decode and re-encode it back to
|
|
145
|
+
* near-identical bytes.
|
|
146
|
+
*
|
|
147
|
+
* Durable artifacts (baselines, candidates, promoted baselines) DO earn the
|
|
148
|
+
* recompress and use {@link writeDurablePng}.
|
|
149
|
+
*/
|
|
150
|
+
export declare function writeTransientPng(path: string, png: Buffer): Promise<void>;
|
|
123
151
|
export declare function deleteIfExists(path: string): Promise<void>;
|
|
124
152
|
/**
|
|
125
153
|
* Copies an already-written PNG (the run's `actual`, or a `candidate`) to a new
|
|
126
|
-
* destination verbatim. No recompress here on purpose: the source
|
|
127
|
-
* by
|
|
128
|
-
*
|
|
129
|
-
* recompressed for free. The only
|
|
130
|
-
* per-machine cache; `approve --from`
|
|
131
|
-
* once during validation and writes the
|
|
154
|
+
* destination verbatim. No recompress here on purpose: the source is a durable
|
|
155
|
+
* baseline produced by {@link writeDurablePng}, which already ran
|
|
156
|
+
* `recompressPng`, so the bytes on disk are the recompressed ones. Copying them
|
|
157
|
+
* forward keeps the destination losslessly recompressed for free. The only
|
|
158
|
+
* non-test caller is `approve` refreshing the per-machine cache; `approve --from`
|
|
159
|
+
* does NOT use this. It reads each source once during validation and writes the
|
|
160
|
+
* retained buffer through {@link writeDurablePng}.
|
|
132
161
|
*/
|
|
133
162
|
export declare function copyRecompressedPng(actualPath: string, destinationPath: string): Promise<void>;
|
|
134
|
-
/**
|
|
135
|
-
* Turns an absolute screenshot path into a path that is relative to the
|
|
136
|
-
* generated report directory so the HTML report can reference it with a
|
|
137
|
-
* portable `src` attribute.
|
|
138
|
-
*/
|
|
139
|
-
export declare function pathRelativeToReport(reportDir: string, absolute: string): string;
|
|
140
163
|
//# sourceMappingURL=baselineStore.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"baselineStore.d.ts","sourceRoot":"","sources":["../../src/screenshots/baselineStore.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;;OAOG;IACH,cAAc,EAAE,MAAM,CAAC;IACvB,iFAAiF;IACjF,kBAAkB,EAAE,MAAM,CAAC;IAC3B;;;;;;OAMG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,uFAAuF;IACvF,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,YAAY;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAC;CACpB;AAcD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,QAAiB,CAAC;AAElD;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,QAAkB,CAAC;AAE1D;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,YAAY,GAAG,aAAa,CA6C7D;AAiBD,wBAAgB,gBAAgB,CAAC,CAAC,EAChC,GAAG,EAAE,MAAM,EACX,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACnB,OAAO,CAAC,CAAC,CAAC,CAcZ;AAED,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAO5E;AAED,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAO7B;AAED,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG5E;AAED
|
|
1
|
+
{"version":3,"file":"baselineStore.d.ts","sourceRoot":"","sources":["../../src/screenshots/baselineStore.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;;OAOG;IACH,cAAc,EAAE,MAAM,CAAC;IACvB,iFAAiF;IACjF,kBAAkB,EAAE,MAAM,CAAC;IAC3B;;;;;;OAMG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,uFAAuF;IACvF,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,YAAY;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAC;CACpB;AAcD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,QAAiB,CAAC;AAElD;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,QAAkB,CAAC;AAE1D;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,YAAY,GAAG,aAAa,CA6C7D;AAiBD,wBAAgB,gBAAgB,CAAC,CAAC,EAChC,GAAG,EAAE,MAAM,EACX,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACnB,OAAO,CAAC,CAAC,CAAC,CAcZ;AAED,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAO5E;AAED,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAO7B;AAED,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG5E;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAWjD;AAYD;;;;;;;;;;GAUG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,IAAI,CAAC,CAEf;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,IAAI,CAAC,CAEf;AAED,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAMhE;AAED;;;;;;;;;GASG;AACH,wBAAsB,mBAAmB,CACvC,UAAU,EAAE,MAAM,EAClB,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,IAAI,CAAC,CAGf"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { access, copyFile, mkdir, readFile, rm, writeFile, } from 'node:fs/promises';
|
|
2
|
-
import { dirname, join
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
3
|
import { PNG } from 'pngjs';
|
|
4
4
|
/**
|
|
5
5
|
* Reduces an arbitrary breakpoint name to a filesystem-safe path segment.
|
|
@@ -37,7 +37,7 @@ export const ACTION_NAME_PATTERN = /^[a-z0-9-]+$/;
|
|
|
37
37
|
*/
|
|
38
38
|
export const BREAKPOINT_SEGMENT_PATTERN = /^[a-z0-9_-]+$/;
|
|
39
39
|
/**
|
|
40
|
-
* Computes deterministic paths for the baseline (the comparison target
|
|
40
|
+
* Computes deterministic paths for the baseline (the comparison target:
|
|
41
41
|
* committed baselines by default, or the local cache when `comparisonRoot` is
|
|
42
42
|
* supplied), actual (regenerated each run), diff (regenerated when a baseline
|
|
43
43
|
* existed and the diff was non-zero), and candidate (proposed-new-baseline for
|
|
@@ -80,15 +80,15 @@ export function pathsFor(options) {
|
|
|
80
80
|
}
|
|
81
81
|
/**
|
|
82
82
|
* Per-path serialization for baseline creation. A baseline is keyed on action
|
|
83
|
-
* name *and* breakpoint (see `pathsFor
|
|
83
|
+
* name *and* breakpoint (see `pathsFor`, the breakpoint is part of the
|
|
84
84
|
* filename), so when the same action+breakpoint runs in two stories
|
|
85
85
|
* concurrently (`workers > 1`, fresh run), both would otherwise read "no
|
|
86
|
-
* baseline" and race to write the same `<breakpoint>.png
|
|
86
|
+
* baseline" and race to write the same `<breakpoint>.png`, a torn or
|
|
87
87
|
* last-writer-wins file. Callers wrap their read-then-maybe-write critical
|
|
88
88
|
* section in this lock so exactly one writer per baseline path runs at a time;
|
|
89
89
|
* later callers see the baseline the first writer produced. The map is keyed by
|
|
90
|
-
* path, so distinct actions
|
|
91
|
-
* whose baseline paths differ
|
|
90
|
+
* path, so distinct actions (and now distinct breakpoints of the same action,
|
|
91
|
+
* whose baseline paths differ) never block each other; entries are bounded by
|
|
92
92
|
* action count times breakpoint count.
|
|
93
93
|
*/
|
|
94
94
|
const baselineLocks = new Map();
|
|
@@ -140,10 +140,13 @@ export async function writeText(path, content) {
|
|
|
140
140
|
* case) is swallowed and the original buffer is returned, so a recompress
|
|
141
141
|
* miss degrades to "write the bytes we were given" rather than a torn file.
|
|
142
142
|
*
|
|
143
|
-
* This is the
|
|
144
|
-
*
|
|
145
|
-
* writes
|
|
146
|
-
*
|
|
143
|
+
* This is the lossless-recompress seam for the tool's DURABLE PNG writes:
|
|
144
|
+
* {@link writeDurablePng} routes through it, so baseline, candidate, cache, and
|
|
145
|
+
* promoted-baseline writes inherit it without per-callsite wiring. Transient
|
|
146
|
+
* report artifacts (`actual`, `diff`) deliberately skip it via
|
|
147
|
+
* {@link writeTransientPng}. The max-effort encode is wasted on a file that is
|
|
148
|
+
* overwritten or deleted next run. Deterministic: pngjs's encoder is a pure
|
|
149
|
+
* function of the pixel data and the fixed options above.
|
|
147
150
|
*
|
|
148
151
|
* Deliberately NOT palette-quantised and never lossy: colour type and bit depth
|
|
149
152
|
* are preserved by the RGBA round-trip.
|
|
@@ -154,16 +157,51 @@ export function recompressPng(png) {
|
|
|
154
157
|
recompressed = PNG.sync.write(PNG.sync.read(png));
|
|
155
158
|
}
|
|
156
159
|
catch {
|
|
157
|
-
// Not a decodable PNG, or an encoder edge case
|
|
160
|
+
// Not a decodable PNG, or an encoder edge case. Write what we were given
|
|
158
161
|
// rather than risk emitting a corrupt or truncated file.
|
|
159
162
|
return png;
|
|
160
163
|
}
|
|
161
|
-
// A recompress that grows the file is a loss, not a win
|
|
164
|
+
// A recompress that grows the file is a loss, not a win. Keep the smaller.
|
|
162
165
|
return recompressed.length < png.length ? recompressed : png;
|
|
163
166
|
}
|
|
164
|
-
|
|
167
|
+
/**
|
|
168
|
+
* Shared write core: ensure the parent directory exists, then write the bytes
|
|
169
|
+
* verbatim. The durable/transient split lives one level up. This only handles
|
|
170
|
+
* the mkdir + write both share.
|
|
171
|
+
*/
|
|
172
|
+
async function writePngBytes(path, bytes) {
|
|
165
173
|
await mkdir(dirname(path), { recursive: true });
|
|
166
|
-
await writeFile(path,
|
|
174
|
+
await writeFile(path, bytes);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Writes a DURABLE PNG (a baseline, a candidate, or a promoted baseline) that
|
|
178
|
+
* outlives the run and is re-read on every later comparison. These earn the
|
|
179
|
+
* max-effort lossless recompress (see {@link recompressPng}): the level-9
|
|
180
|
+
* deflate + adaptive per-scanline filtering is paid once at write, and every
|
|
181
|
+
* subsequent read of the smaller file benefits.
|
|
182
|
+
*
|
|
183
|
+
* Transient report artifacts (the run's `actual`, the `diff` overlay) are
|
|
184
|
+
* overwritten or deleted next run and must NOT pay this cost. They route
|
|
185
|
+
* through {@link writeTransientPng}, which skips recompress entirely.
|
|
186
|
+
*/
|
|
187
|
+
export async function writeDurablePng(path, png) {
|
|
188
|
+
await writePngBytes(path, recompressPng(png));
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Writes a TRANSIENT PNG (the run's `actual` capture or the `diff` overlay)
|
|
192
|
+
* verbatim, with NO recompress pass. These artifacts live only for the current
|
|
193
|
+
* report: an `actual` is overwritten on the next run and a `diff` is deleted the
|
|
194
|
+
* moment a comparison passes, so shrinking them with the expensive level-9
|
|
195
|
+
* encode is wasted work. The `diff` overlay in particular arrives already
|
|
196
|
+
* deflate-encoded by {@link renderDiffOverlay}'s `PNG.sync.write`, so a
|
|
197
|
+
* recompress round-trip would only decode and re-encode it back to
|
|
198
|
+
* near-identical bytes.
|
|
199
|
+
*
|
|
200
|
+
* Durable artifacts (baselines, candidates, promoted baselines) DO earn the
|
|
201
|
+
* recompress and use {@link writeDurablePng}.
|
|
202
|
+
*/
|
|
203
|
+
export async function writeTransientPng(path, png) {
|
|
204
|
+
await writePngBytes(path, png);
|
|
167
205
|
}
|
|
168
206
|
export async function deleteIfExists(path) {
|
|
169
207
|
try {
|
|
@@ -175,23 +213,16 @@ export async function deleteIfExists(path) {
|
|
|
175
213
|
}
|
|
176
214
|
/**
|
|
177
215
|
* Copies an already-written PNG (the run's `actual`, or a `candidate`) to a new
|
|
178
|
-
* destination verbatim. No recompress here on purpose: the source
|
|
179
|
-
* by
|
|
180
|
-
*
|
|
181
|
-
* recompressed for free. The only
|
|
182
|
-
* per-machine cache; `approve --from`
|
|
183
|
-
* once during validation and writes the
|
|
216
|
+
* destination verbatim. No recompress here on purpose: the source is a durable
|
|
217
|
+
* baseline produced by {@link writeDurablePng}, which already ran
|
|
218
|
+
* `recompressPng`, so the bytes on disk are the recompressed ones. Copying them
|
|
219
|
+
* forward keeps the destination losslessly recompressed for free. The only
|
|
220
|
+
* non-test caller is `approve` refreshing the per-machine cache; `approve --from`
|
|
221
|
+
* does NOT use this. It reads each source once during validation and writes the
|
|
222
|
+
* retained buffer through {@link writeDurablePng}.
|
|
184
223
|
*/
|
|
185
224
|
export async function copyRecompressedPng(actualPath, destinationPath) {
|
|
186
225
|
await mkdir(dirname(destinationPath), { recursive: true });
|
|
187
226
|
await copyFile(actualPath, destinationPath);
|
|
188
227
|
}
|
|
189
|
-
/**
|
|
190
|
-
* Turns an absolute screenshot path into a path that is relative to the
|
|
191
|
-
* generated report directory so the HTML report can reference it with a
|
|
192
|
-
* portable `src` attribute.
|
|
193
|
-
*/
|
|
194
|
-
export function pathRelativeToReport(reportDir, absolute) {
|
|
195
|
-
return relative(reportDir, absolute);
|
|
196
|
-
}
|
|
197
228
|
//# sourceMappingURL=baselineStore.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"baselineStore.js","sourceRoot":"","sources":["../../src/screenshots/baselineStore.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,MAAM,EACN,QAAQ,EACR,KAAK,EACL,QAAQ,EACR,EAAE,EACF,SAAS,GACV,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,
|
|
1
|
+
{"version":3,"file":"baselineStore.js","sourceRoot":"","sources":["../../src/screenshots/baselineStore.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,MAAM,EACN,QAAQ,EACR,KAAK,EACL,QAAQ,EACR,EAAE,EACF,SAAS,GACV,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAE1C,OAAO,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAqD5B;;;;;;;GAOG;AACH,SAAS,iBAAiB,CAAC,UAAkB;IAC3C,OAAO,UAAU,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC;AAC/D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,SAAiB;IAC7C,OAAO,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;AACvC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,cAAc,CAAC;AAElD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,eAAe,CAAC;AAE1D;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,QAAQ,CAAC,OAAqB;IAC5C,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;IAC5D,MAAM,EAAE,GAAG,iBAAiB,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IACjD,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,IAAI,OAAO,CAAC,YAAY,CAAC;IACtE,OAAO;QACL,QAAQ,EAAE,IAAI,CAAC,cAAc,EAAE,OAAO,CAAC,UAAU,EAAE,GAAG,EAAE,MAAM,CAAC;QAC/D,MAAM,EAAE,IAAI,CACV,OAAO,CAAC,SAAS,EACjB,aAAa,EACb,SAAS,EACT,GAAG,OAAO,CAAC,UAAU,IAAI,EAAE,aAAa,CACzC;QACD,IAAI,EAAE,IAAI,CACR,OAAO,CAAC,SAAS,EACjB,aAAa,EACb,SAAS,EACT,GAAG,OAAO,CAAC,UAAU,IAAI,EAAE,WAAW,CACvC;QACD,YAAY,EAAE,IAAI,CAAC,cAAc,EAAE,OAAO,CAAC,UAAU,EAAE,GAAG,EAAE,YAAY,CAAC;QACzE,UAAU,EAAE,IAAI,CACd,OAAO,CAAC,SAAS,EACjB,aAAa,EACb,SAAS,EACT,GAAG,OAAO,CAAC,UAAU,IAAI,EAAE,YAAY,CACxC;QACD,sEAAsE;QACtE,6EAA6E;QAC7E,SAAS,EAAE,IAAI,CACb,aAAa,CAAC,OAAO,CAAC,SAAS,CAAC,EAChC,OAAO,CAAC,UAAU,EAClB,GAAG,EAAE,MAAM,CACZ;QACD,aAAa,EAAE,IAAI,CACjB,aAAa,CAAC,OAAO,CAAC,SAAS,CAAC,EAChC,OAAO,CAAC,UAAU,EAClB,GAAG,EAAE,YAAY,CAClB;QACD,wEAAwE;QACxE,0EAA0E;QAC1E,4EAA4E;QAC5E,yEAAyE;QACzE,4DAA4D;QAC5D,cAAc,EAAE,IAAI,CAAC,cAAc,EAAE,OAAO,CAAC,UAAU,EAAE,OAAO,CAAC;QACjE,kBAAkB,EAAE,IAAI,CAAC,cAAc,EAAE,OAAO,CAAC,UAAU,EAAE,WAAW,CAAC;KAC1E,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,aAAa,GAAG,IAAI,GAAG,EAAyB,CAAC;AAEvD,MAAM,UAAU,gBAAgB,CAC9B,GAAW,EACX,EAAoB;IAEpB,MAAM,QAAQ,GAAG,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;IAC7D,2EAA2E;IAC3E,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;IAClC,4EAA4E;IAC5E,+DAA+D;IAC/D,aAAa,CAAC,GAAG,CACf,GAAG,EACH,GAAG,CAAC,IAAI,CACN,GAAG,EAAE,CAAC,SAAS,EACf,GAAG,EAAE,CAAC,SAAS,CAChB,CACF,CAAC;IACF,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,IAAY;IAC7C,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,IAAI,CAAC,CAAC;IACrB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC;AACxB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,IAAY;IAEZ,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,IAAI,CAAC,CAAC;IACrB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,OAAO,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAChC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,IAAY,EAAE,OAAe;IAC3D,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAChD,MAAM,SAAS,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;AACzC,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,UAAU,aAAa,CAAC,GAAW;IACvC,IAAI,YAAoB,CAAC;IACzB,IAAI,CAAC;QACH,YAAY,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IACpD,CAAC;IAAC,MAAM,CAAC;QACP,yEAAyE;QACzE,yDAAyD;QACzD,OAAO,GAAG,CAAC;IACb,CAAC;IACD,2EAA2E;IAC3E,OAAO,YAAY,CAAC,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC;AAC/D,CAAC;AAED;;;;GAIG;AACH,KAAK,UAAU,aAAa,CAAC,IAAY,EAAE,KAAa;IACtD,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAChD,MAAM,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;AAC/B,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,IAAY,EACZ,GAAW;IAEX,MAAM,aAAa,CAAC,IAAI,EAAE,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC;AAChD,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,IAAY,EACZ,GAAW;IAEX,MAAM,aAAa,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;AACjC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,IAAY;IAC/C,IAAI,CAAC;QACH,MAAM,EAAE,CAAC,IAAI,CAAC,CAAC;IACjB,CAAC;IAAC,MAAM,CAAC;QACP,oCAAoC;IACtC,CAAC;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,UAAkB,EAClB,eAAuB;IAEvB,MAAM,KAAK,CAAC,OAAO,CAAC,eAAe,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3D,MAAM,QAAQ,CAAC,UAAU,EAAE,eAAe,CAAC,CAAC;AAC9C,CAAC"}
|
|
@@ -1,14 +1,45 @@
|
|
|
1
1
|
import type { Locator, Page } from 'playwright';
|
|
2
2
|
import type { CaptureMode } from '../config.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Safety bound for a `fullPage` capture. `maxPixels` caps the composited image's
|
|
5
|
+
* total area (width × height); `label` identifies the capture in the error the
|
|
6
|
+
* guard throws so the operator knows exactly which story/action to fix.
|
|
7
|
+
*/
|
|
8
|
+
export interface FullPageGuard {
|
|
9
|
+
maxPixels: number;
|
|
10
|
+
label: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Thrown when a `fullPage` capture's measured layout area exceeds its
|
|
14
|
+
* {@link FullPageGuard} cap. Carries the offending dimensions and the cap so
|
|
15
|
+
* callers (and the message) can report precisely. Failing here is deliberate:
|
|
16
|
+
* compositing and decoding a runaway-tall page to an RGBA array risks exhausting
|
|
17
|
+
* memory, so a loud, actionable error beats an OOM crash.
|
|
18
|
+
*/
|
|
19
|
+
export declare class FullPageTooLargeError extends Error {
|
|
20
|
+
readonly width: number;
|
|
21
|
+
readonly height: number;
|
|
22
|
+
readonly maxPixels: number;
|
|
23
|
+
constructor(label: string, dimensions: {
|
|
24
|
+
width: number;
|
|
25
|
+
height: number;
|
|
26
|
+
}, maxPixels: number);
|
|
27
|
+
}
|
|
3
28
|
/**
|
|
4
29
|
* Screenshot with animations disabled, caret hidden, and any masks applied so
|
|
5
30
|
* the same UI renders to bit-identical pixels across runs. Masks are Playwright
|
|
6
|
-
* locators whose bounding boxes are blacked out before the image is encoded
|
|
7
|
-
*
|
|
31
|
+
* locators whose bounding boxes are blacked out before the image is encoded.
|
|
32
|
+
* Use them to neutralise randomised or time-based regions.
|
|
8
33
|
*
|
|
9
34
|
* `mode` controls scope: `viewport` (default) captures only the page's current
|
|
10
|
-
* viewport box
|
|
35
|
+
* viewport box (what the user sees above the fold) while `fullPage`
|
|
11
36
|
* composites the entire scrollable document however tall.
|
|
37
|
+
*
|
|
38
|
+
* `fullPageGuard`, when supplied, bounds a `fullPage` capture's area: the page's
|
|
39
|
+
* measured layout dimensions are checked against the cap before the shutter
|
|
40
|
+
* fires, and an over-cap page throws {@link FullPageTooLargeError} rather than
|
|
41
|
+
* compositing an array large enough to risk an OOM. It is ignored for `viewport`
|
|
42
|
+
* captures (already bounded by the breakpoint) and when omitted.
|
|
12
43
|
*/
|
|
13
|
-
export declare function capturePage(page: Page, masks?: Locator[], mode?: CaptureMode): Promise<Buffer>;
|
|
44
|
+
export declare function capturePage(page: Page, masks?: Locator[], mode?: CaptureMode, fullPageGuard?: FullPageGuard): Promise<Buffer>;
|
|
14
45
|
//# sourceMappingURL=capture.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capture.d.ts","sourceRoot":"","sources":["../../src/screenshots/capture.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD
|
|
1
|
+
{"version":3,"file":"capture.d.ts","sourceRoot":"","sources":["../../src/screenshots/capture.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;GAMG;AACH,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;gBAEzB,KAAK,EAAE,MAAM,EACb,UAAU,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAC7C,SAAS,EAAE,MAAM;CAkBpB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,IAAI,EACV,KAAK,GAAE,OAAO,EAAO,EACrB,IAAI,GAAE,WAAwB,EAC9B,aAAa,CAAC,EAAE,aAAa,GAC5B,OAAO,CAAC,MAAM,CAAC,CAuDjB"}
|
|
@@ -1,18 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown when a `fullPage` capture's measured layout area exceeds its
|
|
3
|
+
* {@link FullPageGuard} cap. Carries the offending dimensions and the cap so
|
|
4
|
+
* callers (and the message) can report precisely. Failing here is deliberate:
|
|
5
|
+
* compositing and decoding a runaway-tall page to an RGBA array risks exhausting
|
|
6
|
+
* memory, so a loud, actionable error beats an OOM crash.
|
|
7
|
+
*/
|
|
8
|
+
export class FullPageTooLargeError extends Error {
|
|
9
|
+
width;
|
|
10
|
+
height;
|
|
11
|
+
maxPixels;
|
|
12
|
+
constructor(label, dimensions, maxPixels) {
|
|
13
|
+
const area = dimensions.width * dimensions.height;
|
|
14
|
+
super(`Full-page screenshot for ${label} would composite a ` +
|
|
15
|
+
`${dimensions.width}x${dimensions.height} image ` +
|
|
16
|
+
`(${area.toLocaleString('en-US')} pixels), exceeding the ` +
|
|
17
|
+
`${maxPixels.toLocaleString('en-US')}-pixel safety cap. A page this ` +
|
|
18
|
+
`tall decodes to a huge RGBA array and risks exhausting memory. Bound ` +
|
|
19
|
+
`the page height (e.g. cap infinite-scroll content), switch this story ` +
|
|
20
|
+
`to captureMode 'viewport', or raise maxFullPagePixels in your tuffgal ` +
|
|
21
|
+
`config.`);
|
|
22
|
+
this.name = 'FullPageTooLargeError';
|
|
23
|
+
this.width = dimensions.width;
|
|
24
|
+
this.height = dimensions.height;
|
|
25
|
+
this.maxPixels = maxPixels;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
1
28
|
/**
|
|
2
29
|
* Screenshot with animations disabled, caret hidden, and any masks applied so
|
|
3
30
|
* the same UI renders to bit-identical pixels across runs. Masks are Playwright
|
|
4
|
-
* locators whose bounding boxes are blacked out before the image is encoded
|
|
5
|
-
*
|
|
31
|
+
* locators whose bounding boxes are blacked out before the image is encoded.
|
|
32
|
+
* Use them to neutralise randomised or time-based regions.
|
|
6
33
|
*
|
|
7
34
|
* `mode` controls scope: `viewport` (default) captures only the page's current
|
|
8
|
-
* viewport box
|
|
35
|
+
* viewport box (what the user sees above the fold) while `fullPage`
|
|
9
36
|
* composites the entire scrollable document however tall.
|
|
37
|
+
*
|
|
38
|
+
* `fullPageGuard`, when supplied, bounds a `fullPage` capture's area: the page's
|
|
39
|
+
* measured layout dimensions are checked against the cap before the shutter
|
|
40
|
+
* fires, and an over-cap page throws {@link FullPageTooLargeError} rather than
|
|
41
|
+
* compositing an array large enough to risk an OOM. It is ignored for `viewport`
|
|
42
|
+
* captures (already bounded by the breakpoint) and when omitted.
|
|
10
43
|
*/
|
|
11
|
-
export async function capturePage(page, masks = [], mode = 'viewport') {
|
|
44
|
+
export async function capturePage(page, masks = [], mode = 'viewport', fullPageGuard) {
|
|
12
45
|
// `fullPage` composites the whole scrollable document into one image. When
|
|
13
46
|
// the document is captured at a non-zero offset, `position: sticky` / `fixed`
|
|
14
47
|
// chrome (sidebars, headers) resolves against that scroll position and lands
|
|
15
|
-
// shifted down by `scrollY` in the stitch
|
|
48
|
+
// shifted down by `scrollY` in the stitch. The same UI produces a different
|
|
16
49
|
// image purely because an earlier step left the viewport scrolled. Resetting
|
|
17
50
|
// to the origin first pins those elements to their static-baseline position
|
|
18
51
|
// so the capture is deterministic. `instant` defeats any `scroll-behavior:
|
|
@@ -20,15 +53,34 @@ export async function capturePage(page, masks = [], mode = 'viewport') {
|
|
|
20
53
|
// timing race.
|
|
21
54
|
//
|
|
22
55
|
// A `viewport` shot captures exactly the box the flow scrolled to, so it must
|
|
23
|
-
// NOT reset
|
|
56
|
+
// NOT reset. A story that scrolls to a below-the-fold section (a Settings
|
|
24
57
|
// panel, a success banner) would otherwise capture the top of the page
|
|
25
58
|
// instead of the region it navigated to. The flow lands at a deterministic
|
|
26
59
|
// offset, so the viewport shot is deterministic without any reset.
|
|
27
60
|
//
|
|
28
|
-
//
|
|
29
|
-
// the
|
|
61
|
+
// The same page-context call that resets scroll also returns the document's
|
|
62
|
+
// layout dimensions, so the guard measures the true (unclipped) full-page size
|
|
63
|
+
// (Chromium may clip the actual screenshot, but the layout height reflects the
|
|
64
|
+
// real pathology) in one round-trip, with no extra evaluate. String form so
|
|
65
|
+
// the snippet runs in the page (DOM) context without pulling the DOM lib into
|
|
66
|
+
// this Node-side module's type environment.
|
|
30
67
|
if (mode === 'fullPage') {
|
|
31
|
-
await page.evaluate(`
|
|
68
|
+
const dimensions = (await page.evaluate(`(() => {
|
|
69
|
+
window.scrollTo({ top: 0, left: 0, behavior: 'instant' });
|
|
70
|
+
const el = document.documentElement;
|
|
71
|
+
const body = document.body;
|
|
72
|
+
return {
|
|
73
|
+
width: Math.max(el.scrollWidth, el.clientWidth, body ? body.scrollWidth : 0),
|
|
74
|
+
height: Math.max(el.scrollHeight, el.clientHeight, body ? body.scrollHeight : 0),
|
|
75
|
+
};
|
|
76
|
+
})()`));
|
|
77
|
+
// Guard only when a cap is supplied. The measurement above always runs (it
|
|
78
|
+
// is also the scroll reset), but an unbounded caller (e.g. a test) simply
|
|
79
|
+
// ignores the returned dimensions.
|
|
80
|
+
if (fullPageGuard &&
|
|
81
|
+
dimensions.width * dimensions.height > fullPageGuard.maxPixels) {
|
|
82
|
+
throw new FullPageTooLargeError(fullPageGuard.label, dimensions, fullPageGuard.maxPixels);
|
|
83
|
+
}
|
|
32
84
|
}
|
|
33
85
|
return page.screenshot({
|
|
34
86
|
fullPage: mode === 'fullPage',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capture.js","sourceRoot":"","sources":["../../src/screenshots/capture.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"capture.js","sourceRoot":"","sources":["../../src/screenshots/capture.ts"],"names":[],"mappings":"AAaA;;;;;;GAMG;AACH,MAAM,OAAO,qBAAsB,SAAQ,KAAK;IACrC,KAAK,CAAS;IACd,MAAM,CAAS;IACf,SAAS,CAAS;IAC3B,YACE,KAAa,EACb,UAA6C,EAC7C,SAAiB;QAEjB,MAAM,IAAI,GAAG,UAAU,CAAC,KAAK,GAAG,UAAU,CAAC,MAAM,CAAC;QAClD,KAAK,CACH,4BAA4B,KAAK,qBAAqB;YACpD,GAAG,UAAU,CAAC,KAAK,IAAI,UAAU,CAAC,MAAM,SAAS;YACjD,IAAI,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,0BAA0B;YAC1D,GAAG,SAAS,CAAC,cAAc,CAAC,OAAO,CAAC,iCAAiC;YACrE,uEAAuE;YACvE,wEAAwE;YACxE,wEAAwE;YACxE,SAAS,CACZ,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,uBAAuB,CAAC;QACpC,IAAI,CAAC,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC;QAC9B,IAAI,CAAC,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC;QAChC,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC7B,CAAC;CACF;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,IAAU,EACV,QAAmB,EAAE,EACrB,OAAoB,UAAU,EAC9B,aAA6B;IAE7B,2EAA2E;IAC3E,8EAA8E;IAC9E,6EAA6E;IAC7E,4EAA4E;IAC5E,6EAA6E;IAC7E,4EAA4E;IAC5E,2EAA2E;IAC3E,yEAAyE;IACzE,eAAe;IACf,EAAE;IACF,8EAA8E;IAC9E,0EAA0E;IAC1E,uEAAuE;IACvE,2EAA2E;IAC3E,mEAAmE;IACnE,EAAE;IACF,4EAA4E;IAC5E,+EAA+E;IAC/E,+EAA+E;IAC/E,4EAA4E;IAC5E,8EAA8E;IAC9E,4CAA4C;IAC5C,IAAI,IAAI,KAAK,UAAU,EAAE,CAAC;QACxB,MAAM,UAAU,GAAG,CAAC,MAAM,IAAI,CAAC,QAAQ,CACrC;;;;;;;;WAQK,CACN,CAAsC,CAAC;QACxC,2EAA2E;QAC3E,0EAA0E;QAC1E,mCAAmC;QACnC,IACE,aAAa;YACb,UAAU,CAAC,KAAK,GAAG,UAAU,CAAC,MAAM,GAAG,aAAa,CAAC,SAAS,EAC9D,CAAC;YACD,MAAM,IAAI,qBAAqB,CAC7B,aAAa,CAAC,KAAK,EACnB,UAAU,EACV,aAAa,CAAC,SAAS,CACxB,CAAC;QACJ,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC,UAAU,CAAC;QACrB,QAAQ,EAAE,IAAI,KAAK,UAAU;QAC7B,UAAU,EAAE,UAAU;QACtB,KAAK,EAAE,MAAM;QACb,IAAI,EAAE,KAAK;KACZ,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { PNG } from 'pngjs';
|
|
2
|
+
export interface DiffScore {
|
|
3
3
|
diffPixels: number;
|
|
4
4
|
totalPixels: number;
|
|
5
5
|
diffRatio: number;
|
|
@@ -13,14 +13,63 @@ export interface DiffOutcome {
|
|
|
13
13
|
ssimScore: number;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
16
|
+
* The decoded PNG pair, carried out of {@link scoreDiff} so the changed branch
|
|
17
|
+
* can render its overlay from the SAME decode rather than reading the two
|
|
18
|
+
* buffers a second time. Both PNGs are validated (matching dimensions, packed
|
|
19
|
+
* RGBA) by {@link decodePair} before they reach here.
|
|
20
|
+
*/
|
|
21
|
+
export interface DecodedPair {
|
|
22
|
+
baselinePng: PNG;
|
|
23
|
+
actualPng: PNG;
|
|
24
|
+
}
|
|
25
|
+
/** {@link scoreDiff}'s result: the reported metrics plus the decode to reuse. */
|
|
26
|
+
export interface ScoredDiff {
|
|
27
|
+
score: DiffScore;
|
|
28
|
+
/**
|
|
29
|
+
* The decoded pair {@link scoreDiff} already produced. On the changed branch
|
|
30
|
+
* the caller threads this straight into {@link renderDiffOverlay}, so the
|
|
31
|
+
* failing comparison decodes the image pair exactly once instead of twice.
|
|
32
|
+
*/
|
|
33
|
+
decoded: DecodedPair;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Diffing is split into two phases so the common case stays cheap:
|
|
37
|
+
*
|
|
38
|
+
* 1. {@link scoreDiff}: the gate. Computes SSIM (the pass/changed
|
|
39
|
+
* decision) plus the pixel metrics reported on every outcome
|
|
40
|
+
* (`diffPixels`, `diffRatio`). It runs pixelmatch in count-only mode
|
|
41
|
+
* (no output buffer) and never encodes a diff image. It also returns the
|
|
42
|
+
* {@link DecodedPair} it read, so the changed branch can render its
|
|
43
|
+
* overlay without a second decode.
|
|
44
|
+
* 2. {@link renderDiffOverlay}: the visualisation. Takes that same decoded
|
|
45
|
+
* pair, runs pixelmatch with an output buffer, and deflate-encodes the
|
|
46
|
+
* red-highlight overlay PNG for the human-facing report.
|
|
47
|
+
*
|
|
48
|
+
* The overlay is only kept when a comparison FAILS; a passing comparison
|
|
49
|
+
* deletes it. Because passing is the common case in a healthy suite,
|
|
50
|
+
* encoding the overlay on every comparison was pure waste, so the caller
|
|
51
|
+
* runs {@link scoreDiff} unconditionally and reaches for
|
|
52
|
+
* {@link renderDiffOverlay} only on the changed branch, where the image is
|
|
53
|
+
* actually written. The decode is shared across both, so the changed branch
|
|
54
|
+
* pays for exactly one `PNG.sync.read` pair; the pass branch keeps its
|
|
55
|
+
* no-allocate, no-encode shape (the overlay is never filled on a pass).
|
|
56
|
+
*/
|
|
57
|
+
export declare function scoreDiff(baseline: Buffer, actual: Buffer, pixelThreshold: number): ScoredDiff;
|
|
58
|
+
/**
|
|
59
|
+
* Renders the red-highlight overlay PNG for the report from the already-decoded
|
|
60
|
+
* pair {@link scoreDiff} produced. This is the expensive half (pixelmatch
|
|
61
|
+
* fills an output buffer and `PNG.sync.write` deflate-encodes it) so it runs
|
|
62
|
+
* only on the changed/failed branch where the diff image is actually written to
|
|
63
|
+
* disk.
|
|
20
64
|
*
|
|
21
|
-
*
|
|
65
|
+
* Takes the {@link DecodedPair} rather than raw buffers so it reuses
|
|
66
|
+
* {@link scoreDiff}'s decode: the changed branch reads the image pair once, not
|
|
67
|
+
* twice. The dimensions/packing invariants were already enforced by
|
|
68
|
+
* {@link decodePair} when the pair was decoded, so no re-validation is needed
|
|
69
|
+
* here. pixelmatch's count is output-independent, so filling the overlay here
|
|
70
|
+
* cannot disturb the `diffPixels` {@link scoreDiff} already reported.
|
|
22
71
|
*/
|
|
23
|
-
export declare function
|
|
72
|
+
export declare function renderDiffOverlay(decoded: DecodedPair, pixelThreshold: number): Buffer;
|
|
24
73
|
export declare class ScreenshotSizeMismatchError extends Error {
|
|
25
74
|
readonly baseline: {
|
|
26
75
|
width: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../../src/screenshots/diff.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../../src/screenshots/diff.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAG5B,MAAM,WAAW,SAAS;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;;OAMG;IACH,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,GAAG,CAAC;IACjB,SAAS,EAAE,GAAG,CAAC;CAChB;AAED,iFAAiF;AACjF,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,SAAS,CAAC;IACjB;;;;OAIG;IACH,OAAO,EAAE,WAAW,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,SAAS,CACvB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM,GACrB,UAAU,CA6CZ;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,WAAW,EACpB,cAAc,EAAE,MAAM,GACrB,MAAM,CAeR;AAuCD,qBAAa,2BAA4B,SAAQ,KAAK;IACpD,QAAQ,CAAC,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACrD,QAAQ,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;gBAEjD,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAC3C,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE;CAS5C"}
|