tuffgal 0.1.0-alpha.9 → 0.2.0-alpha.2
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 +149 -2
- package/README.md +78 -3
- package/dist/cli.d.ts +50 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +204 -18
- package/dist/cli.js.map +1 -1
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +11 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/config.d.ts +38 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +20 -1
- package/dist/config.js.map +1 -1
- package/dist/coverage/screens.d.ts +11 -6
- package/dist/coverage/screens.d.ts.map +1 -1
- package/dist/coverage/screens.js +12 -7
- package/dist/coverage/screens.js.map +1 -1
- package/dist/reporter/assets/report.css +415 -51
- package/dist/reporter/assets/report.js +502 -68
- package/dist/reporter/template.d.ts +8 -1
- package/dist/reporter/template.d.ts.map +1 -1
- package/dist/reporter/template.js +517 -85
- package/dist/reporter/template.js.map +1 -1
- package/dist/reporter/writeReport.d.ts +2 -2
- package/dist/reporter/writeReport.d.ts.map +1 -1
- package/dist/reporter/writeReport.js +3 -3
- package/dist/reporter/writeReport.js.map +1 -1
- package/dist/runner/approve.d.ts +34 -9
- package/dist/runner/approve.d.ts.map +1 -1
- package/dist/runner/approve.js +54 -10
- package/dist/runner/approve.js.map +1 -1
- package/dist/runner/approveFrom.d.ts +58 -0
- package/dist/runner/approveFrom.d.ts.map +1 -0
- package/dist/runner/approveFrom.js +384 -0
- package/dist/runner/approveFrom.js.map +1 -0
- package/dist/runner/breakpointPasses.d.ts +39 -0
- package/dist/runner/breakpointPasses.d.ts.map +1 -0
- package/dist/runner/breakpointPasses.js +107 -0
- package/dist/runner/breakpointPasses.js.map +1 -0
- package/dist/runner/bridges/database.d.ts +4 -3
- package/dist/runner/bridges/database.d.ts.map +1 -1
- package/dist/runner/bridges/database.js +4 -3
- package/dist/runner/bridges/database.js.map +1 -1
- package/dist/runner/exitCode.d.ts +34 -0
- package/dist/runner/exitCode.d.ts.map +1 -0
- package/dist/runner/exitCode.js +41 -0
- package/dist/runner/exitCode.js.map +1 -0
- package/dist/runner/manifest.d.ts +149 -0
- package/dist/runner/manifest.d.ts.map +1 -0
- package/dist/runner/manifest.js +229 -0
- package/dist/runner/manifest.js.map +1 -0
- package/dist/runner/mode.d.ts +41 -0
- package/dist/runner/mode.d.ts.map +1 -0
- package/dist/runner/mode.js +43 -0
- package/dist/runner/mode.js.map +1 -0
- package/dist/runner/orphanScan.d.ts +68 -0
- package/dist/runner/orphanScan.d.ts.map +1 -0
- package/dist/runner/orphanScan.js +150 -0
- package/dist/runner/orphanScan.js.map +1 -0
- package/dist/runner/run.d.ts +85 -1
- package/dist/runner/run.d.ts.map +1 -1
- package/dist/runner/run.js +267 -41
- package/dist/runner/run.js.map +1 -1
- package/dist/runner/runAction.d.ts +16 -0
- package/dist/runner/runAction.d.ts.map +1 -1
- package/dist/runner/runAction.js +112 -13
- package/dist/runner/runAction.js.map +1 -1
- package/dist/runner/runStory.d.ts +15 -4
- package/dist/runner/runStory.d.ts.map +1 -1
- package/dist/runner/runStory.js +41 -19
- package/dist/runner/runStory.js.map +1 -1
- package/dist/runner/storyFilter.d.ts +8 -0
- package/dist/runner/storyFilter.d.ts.map +1 -1
- package/dist/runner/storyFilter.js +11 -0
- package/dist/runner/storyFilter.js.map +1 -1
- package/dist/schema/action.d.ts +11 -11
- package/dist/schema/result.d.ts +125 -6
- package/dist/schema/result.d.ts.map +1 -1
- package/dist/schema/result.js.map +1 -1
- package/dist/screenshots/baselineStore.d.ts +92 -10
- package/dist/screenshots/baselineStore.d.ts.map +1 -1
- package/dist/screenshots/baselineStore.js +102 -18
- package/dist/screenshots/baselineStore.js.map +1 -1
- package/dist/screenshots/capture.d.ts +10 -5
- package/dist/screenshots/capture.d.ts.map +1 -1
- package/dist/screenshots/capture.js +31 -18
- package/dist/screenshots/capture.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The process exit code the CLI reports for a completed run. A small, pure
|
|
3
|
+
* derivation kept apart from the run driver so its precedence table is
|
|
4
|
+
* unit-testable in isolation.
|
|
5
|
+
*
|
|
6
|
+
* Precedence, highest wins:
|
|
7
|
+
* - `1` — one or more stories failed (a step threw / the harness broke).
|
|
8
|
+
* Always outranks every other signal: a broken run's failure is the
|
|
9
|
+
* headline, not the visual diff it never got to compute cleanly.
|
|
10
|
+
* - `3` — CI mode only: the committed environment manifest is present but its
|
|
11
|
+
* pixel-affecting keys diverge from this run's capture environment (a
|
|
12
|
+
* platform/browser/capture-mode/… drift). The comparison still ran — this
|
|
13
|
+
* is not an abort — but the diffs are computed against baselines rendered in
|
|
14
|
+
* a different environment, so the signal is "expect a full re-approve",
|
|
15
|
+
* distinct from ordinary pending changes. Outranks `2`: an environment
|
|
16
|
+
* mismatch subsumes and explains the `new`/`changed` churn it produces.
|
|
17
|
+
* - `2` — CI mode only: pending baseline changes a human must approve —
|
|
18
|
+
* `new` or `changed` stories, or `deleted` (orphaned committed baselines an
|
|
19
|
+
* approve/prune should retire). This is the PR-gate signal — the run itself
|
|
20
|
+
* succeeded, but committed baselines don't yet reflect the current UI. A
|
|
21
|
+
* deleted-only run still gates: the baseline set has drifted from the story
|
|
22
|
+
* set even though nothing rendered differently.
|
|
23
|
+
* - `0` — clean: every story passed, the environment matched (or no manifest
|
|
24
|
+
* to check), and no baselines were orphaned.
|
|
25
|
+
*
|
|
26
|
+
* Local mode never emits `2` or `3`: local runs are advisory and never read
|
|
27
|
+
* `paths.baselines` at all (no committed manifest to check, no committed set to
|
|
28
|
+
* gate against), so `new`/`changed`/`deleted`/mismatch there are informational.
|
|
29
|
+
* It keeps the "fail only on failed" behaviour (`1` or `0`).
|
|
30
|
+
*/
|
|
31
|
+
export function deriveExitCode(mode, totals, mismatch) {
|
|
32
|
+
if (totals.failed > 0)
|
|
33
|
+
return 1;
|
|
34
|
+
if (mode === 'ci' && mismatch)
|
|
35
|
+
return 3;
|
|
36
|
+
if (mode === 'ci' && totals.changed + totals.new + totals.deleted > 0) {
|
|
37
|
+
return 2;
|
|
38
|
+
}
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=exitCode.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exitCode.js","sourceRoot":"","sources":["../../src/runner/exitCode.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,UAAU,cAAc,CAC5B,IAAa,EACb,MAA2B,EAC3B,QAAiB;IAEjB,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,CAAC,CAAC;IAChC,IAAI,IAAI,KAAK,IAAI,IAAI,QAAQ;QAAE,OAAO,CAAC,CAAC;IACxC,IAAI,IAAI,KAAK,IAAI,IAAI,MAAM,CAAC,OAAO,GAAG,MAAM,CAAC,GAAG,GAAG,MAAM,CAAC,OAAO,GAAG,CAAC,EAAE,CAAC;QACtE,OAAO,CAAC,CAAC;IACX,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC"}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { ResolvedConfig } from '../config.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Bump when the capture pipeline changes behaviour in a way that shifts pixels
|
|
4
|
+
* for the same input UI — a new default screenshot option, a change to how the
|
|
5
|
+
* clock is frozen, a mask-application tweak, etc. A committed baseline set was
|
|
6
|
+
* rendered under one `CAPTURE_SCHEMA`; a run under a higher value is a pixel-
|
|
7
|
+
* affecting mismatch (see {@link PIXEL_AFFECTING_KEYS}) that prompts re-approve.
|
|
8
|
+
*
|
|
9
|
+
* This is deliberately separate from `schemaVersion` (the manifest *file*
|
|
10
|
+
* format): the file shape can stay stable while capture behaviour evolves, and
|
|
11
|
+
* vice versa.
|
|
12
|
+
*/
|
|
13
|
+
export declare const CAPTURE_SCHEMA = 1;
|
|
14
|
+
/** Manifest file-format version. Bumped only when this JSON shape changes. */
|
|
15
|
+
export declare const SCHEMA_VERSION = 1;
|
|
16
|
+
/** One breakpoint's identity as recorded in the manifest. */
|
|
17
|
+
export interface ManifestBreakpoint {
|
|
18
|
+
name: string;
|
|
19
|
+
width: number;
|
|
20
|
+
height: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The environment manifest committed beside the baselines (`<baselines>/
|
|
24
|
+
* manifest.json`). Written only by `approve --from` (a later wave) from the
|
|
25
|
+
* environment block of the candidate run's `results.json`; read by `run --ci`
|
|
26
|
+
* to detect when the current capture environment has drifted from the one the
|
|
27
|
+
* committed baselines were rendered under.
|
|
28
|
+
*
|
|
29
|
+
* `tuffgalVersion` and `playwrightVersion` are informational only — they are
|
|
30
|
+
* recorded for debugging but never contribute to a mismatch (a tuffgal patch
|
|
31
|
+
* release or a Playwright bump that does not move the bundled browser leaves
|
|
32
|
+
* pixels untouched). Every other field is pixel-affecting; see
|
|
33
|
+
* {@link PIXEL_AFFECTING_KEYS}.
|
|
34
|
+
*/
|
|
35
|
+
export interface EnvironmentManifest {
|
|
36
|
+
schemaVersion: number;
|
|
37
|
+
captureSchema: number;
|
|
38
|
+
/** Informational: the tuffgal release that captured the baselines. */
|
|
39
|
+
tuffgalVersion: string;
|
|
40
|
+
/** Informational: the Playwright release used. */
|
|
41
|
+
playwrightVersion: string;
|
|
42
|
+
browser: string;
|
|
43
|
+
browserVersion: string;
|
|
44
|
+
platform: string;
|
|
45
|
+
captureMode: string;
|
|
46
|
+
breakpoints: ManifestBreakpoint[];
|
|
47
|
+
deviceScaleFactor: number;
|
|
48
|
+
frozenTime: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The manifest keys whose divergence between the committed manifest and the
|
|
52
|
+
* current run shifts rendered pixels, so a difference must gate the run (exit
|
|
53
|
+
* 3) and prompt a re-approve. `schemaVersion` is excluded (it versions the file
|
|
54
|
+
* shape, not the pixels); `tuffgalVersion`/`playwrightVersion` are excluded as
|
|
55
|
+
* informational. `browser` is excluded because a browser-*name* change is
|
|
56
|
+
* already implied by `browserVersion` (which carries the vendor+version string)
|
|
57
|
+
* and the runner only launches chromium today — comparing the name would add a
|
|
58
|
+
* redundant key with no independent signal.
|
|
59
|
+
*/
|
|
60
|
+
export declare const PIXEL_AFFECTING_KEYS: readonly ["captureSchema", "browserVersion", "platform", "captureMode", "breakpoints", "deviceScaleFactor", "frozenTime"];
|
|
61
|
+
export type PixelAffectingKey = (typeof PIXEL_AFFECTING_KEYS)[number];
|
|
62
|
+
/**
|
|
63
|
+
* Result of reading `<baselines>/manifest.json`. Three outcomes the caller must
|
|
64
|
+
* distinguish, because they gate differently:
|
|
65
|
+
* - `ok` — a well-formed manifest was read; compare it against the run.
|
|
66
|
+
* - `missing` — no file on disk. The bootstrap case: the first `approve
|
|
67
|
+
* --from` has not written a manifest yet, so `run --ci` treats it as "no
|
|
68
|
+
* expectation to violate" — `environment.expected: null`, no mismatch, no
|
|
69
|
+
* exit 3. Never gate a project that has simply not been approved once.
|
|
70
|
+
* - `malformed` — a file exists but is not valid JSON or fails shape checks.
|
|
71
|
+
* A committed manifest that cannot be parsed is a real problem the run
|
|
72
|
+
* should surface, but crashing the whole comparison is too blunt. Treated
|
|
73
|
+
* as a mismatch-with-note: `expected: null`, `mismatch: true`, and
|
|
74
|
+
* `mismatchKeys: ['manifest']` so the report/comment can say "the committed
|
|
75
|
+
* manifest is unreadable — re-approve to rewrite it" without aborting.
|
|
76
|
+
*/
|
|
77
|
+
export type ManifestReadResult = {
|
|
78
|
+
status: 'ok';
|
|
79
|
+
manifest: EnvironmentManifest;
|
|
80
|
+
} | {
|
|
81
|
+
status: 'missing';
|
|
82
|
+
} | {
|
|
83
|
+
status: 'malformed';
|
|
84
|
+
reason: string;
|
|
85
|
+
};
|
|
86
|
+
/** The sentinel `mismatchKeys` entry for an unreadable committed manifest. */
|
|
87
|
+
export declare const MALFORMED_MANIFEST_KEY = "manifest";
|
|
88
|
+
/** The filename of the environment manifest inside `paths.baselines`. */
|
|
89
|
+
export declare const MANIFEST_FILENAME = "manifest.json";
|
|
90
|
+
/**
|
|
91
|
+
* Reads and shape-checks `<baselinesDir>/manifest.json`. Tolerant by design
|
|
92
|
+
* (see {@link ManifestReadResult}): a missing file is the bootstrap case, a
|
|
93
|
+
* malformed one is surfaced as a note rather than a crash. Shape validation is
|
|
94
|
+
* intentionally shallow — enough to trust the field reads in {@link
|
|
95
|
+
* compareEnvironment}, not a full structural mirror.
|
|
96
|
+
*/
|
|
97
|
+
export declare function readManifest(baselinesDir: string): Promise<ManifestReadResult>;
|
|
98
|
+
/**
|
|
99
|
+
* Returns a human-readable reason string when `value` is not a well-formed
|
|
100
|
+
* {@link EnvironmentManifest}, or `undefined` when it passes. Checks every key
|
|
101
|
+
* the comparison reads so a truncated or wrong-typed manifest is caught here
|
|
102
|
+
* (surfaced as `malformed`) rather than producing a garbage per-key delta.
|
|
103
|
+
*
|
|
104
|
+
* Exported so `approve --from` can run the same shape check against a candidate
|
|
105
|
+
* artifact's `environment.actual` before promoting it into the committed
|
|
106
|
+
* manifest — a well-formed-but-wrong-shaped environment block must fail closed,
|
|
107
|
+
* not slip into `manifest.json` and silently defeat the drift gate.
|
|
108
|
+
*/
|
|
109
|
+
export declare function validateManifestShape(value: unknown): string | undefined;
|
|
110
|
+
/** Outcome of comparing the committed manifest against the current run's env. */
|
|
111
|
+
export interface EnvironmentComparison {
|
|
112
|
+
mismatch: boolean;
|
|
113
|
+
mismatchKeys: string[];
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Compares a committed manifest read result against the environment captured
|
|
117
|
+
* this run, returning which pixel-affecting keys diverged.
|
|
118
|
+
*
|
|
119
|
+
* - `missing` — bootstrap: nothing to violate. `{ mismatch: false }`.
|
|
120
|
+
* - `malformed` — unreadable committed manifest: mismatch-with-note,
|
|
121
|
+
* `mismatchKeys: ['manifest']` (see {@link MALFORMED_MANIFEST_KEY}).
|
|
122
|
+
* - `ok` — diff each {@link PIXEL_AFFECTING_KEYS}. `breakpoints` compares deep
|
|
123
|
+
* and order-sensitively: the manifest records an ordered list, and a
|
|
124
|
+
* reorder (even at identical dimensions) is treated as a change worth a
|
|
125
|
+
* re-approve prompt rather than silently equal — order-sensitivity is the
|
|
126
|
+
* conservative pixel-safety choice. Informational keys
|
|
127
|
+
* (`tuffgalVersion`/`playwrightVersion`) are never compared.
|
|
128
|
+
*/
|
|
129
|
+
export declare function compareEnvironment(expected: ManifestReadResult, actual: EnvironmentManifest): EnvironmentComparison;
|
|
130
|
+
/** The captured browser identity for one run. */
|
|
131
|
+
export interface CapturedBrowser {
|
|
132
|
+
name: string;
|
|
133
|
+
version: string;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Builds the environment manifest describing THIS run — the "actual" side of
|
|
137
|
+
* the comparison and the block a later `approve --from` promotes into the
|
|
138
|
+
* committed manifest. Everything except the live browser version is derived
|
|
139
|
+
* from the resolved config and the host, so the caller only supplies the
|
|
140
|
+
* browser identity it read from the launched Playwright browser.
|
|
141
|
+
*
|
|
142
|
+
* `deviceScaleFactor` is hardcoded `1`: the runner never sets `deviceScaleFactor`
|
|
143
|
+
* on `browser.newContext` (see `runStory.ts`), so Playwright's default of `1`
|
|
144
|
+
* applies. If a future wave makes the scale factor configurable, thread it here
|
|
145
|
+
* and this constant must track it — the manifest's whole job is to notice a DSF
|
|
146
|
+
* change, so a stale hardcode here would silently defeat that.
|
|
147
|
+
*/
|
|
148
|
+
export declare function captureEnvironment(config: ResolvedConfig, browser: CapturedBrowser): EnvironmentManifest;
|
|
149
|
+
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/runner/manifest.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,cAAc,IAAI,CAAC;AAEhC,8EAA8E;AAC9E,eAAO,MAAM,cAAc,IAAI,CAAC;AAEhC,6DAA6D;AAC7D,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,mBAAmB;IAClC,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,sEAAsE;IACtE,cAAc,EAAE,MAAM,CAAC;IACvB,kDAAkD;IAClD,iBAAiB,EAAE,MAAM,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,kBAAkB,EAAE,CAAC;IAClC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,2HAQvB,CAAC;AAEX,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEtE;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,kBAAkB,GAC1B;IAAE,MAAM,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,mBAAmB,CAAA;CAAE,GAC/C;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5C,8EAA8E;AAC9E,eAAO,MAAM,sBAAsB,aAAa,CAAC;AAEjD,yEAAyE;AACzE,eAAO,MAAM,iBAAiB,kBAAkB,CAAC;AAEjD;;;;;;GAMG;AACH,wBAAsB,YAAY,CAChC,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,kBAAkB,CAAC,CAmB7B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CA8CxE;AAED,iFAAiF;AACjF,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,OAAO,CAAC;IAClB,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,kBAAkB,EAC5B,MAAM,EAAE,mBAAmB,GAC1B,qBAAqB,CAqBvB;AA6CD,iDAAiD;AACjD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,eAAe,GACvB,mBAAmB,CAkBrB"}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* Bump when the capture pipeline changes behaviour in a way that shifts pixels
|
|
6
|
+
* for the same input UI — a new default screenshot option, a change to how the
|
|
7
|
+
* clock is frozen, a mask-application tweak, etc. A committed baseline set was
|
|
8
|
+
* rendered under one `CAPTURE_SCHEMA`; a run under a higher value is a pixel-
|
|
9
|
+
* affecting mismatch (see {@link PIXEL_AFFECTING_KEYS}) that prompts re-approve.
|
|
10
|
+
*
|
|
11
|
+
* This is deliberately separate from `schemaVersion` (the manifest *file*
|
|
12
|
+
* format): the file shape can stay stable while capture behaviour evolves, and
|
|
13
|
+
* vice versa.
|
|
14
|
+
*/
|
|
15
|
+
export const CAPTURE_SCHEMA = 1;
|
|
16
|
+
/** Manifest file-format version. Bumped only when this JSON shape changes. */
|
|
17
|
+
export const SCHEMA_VERSION = 1;
|
|
18
|
+
/**
|
|
19
|
+
* The manifest keys whose divergence between the committed manifest and the
|
|
20
|
+
* current run shifts rendered pixels, so a difference must gate the run (exit
|
|
21
|
+
* 3) and prompt a re-approve. `schemaVersion` is excluded (it versions the file
|
|
22
|
+
* shape, not the pixels); `tuffgalVersion`/`playwrightVersion` are excluded as
|
|
23
|
+
* informational. `browser` is excluded because a browser-*name* change is
|
|
24
|
+
* already implied by `browserVersion` (which carries the vendor+version string)
|
|
25
|
+
* and the runner only launches chromium today — comparing the name would add a
|
|
26
|
+
* redundant key with no independent signal.
|
|
27
|
+
*/
|
|
28
|
+
export const PIXEL_AFFECTING_KEYS = [
|
|
29
|
+
'captureSchema',
|
|
30
|
+
'browserVersion',
|
|
31
|
+
'platform',
|
|
32
|
+
'captureMode',
|
|
33
|
+
'breakpoints',
|
|
34
|
+
'deviceScaleFactor',
|
|
35
|
+
'frozenTime',
|
|
36
|
+
];
|
|
37
|
+
/** The sentinel `mismatchKeys` entry for an unreadable committed manifest. */
|
|
38
|
+
export const MALFORMED_MANIFEST_KEY = 'manifest';
|
|
39
|
+
/** The filename of the environment manifest inside `paths.baselines`. */
|
|
40
|
+
export const MANIFEST_FILENAME = 'manifest.json';
|
|
41
|
+
/**
|
|
42
|
+
* Reads and shape-checks `<baselinesDir>/manifest.json`. Tolerant by design
|
|
43
|
+
* (see {@link ManifestReadResult}): a missing file is the bootstrap case, a
|
|
44
|
+
* malformed one is surfaced as a note rather than a crash. Shape validation is
|
|
45
|
+
* intentionally shallow — enough to trust the field reads in {@link
|
|
46
|
+
* compareEnvironment}, not a full structural mirror.
|
|
47
|
+
*/
|
|
48
|
+
export async function readManifest(baselinesDir) {
|
|
49
|
+
let raw;
|
|
50
|
+
try {
|
|
51
|
+
raw = await readFile(join(baselinesDir, MANIFEST_FILENAME), 'utf8');
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return { status: 'missing' };
|
|
55
|
+
}
|
|
56
|
+
let parsed;
|
|
57
|
+
try {
|
|
58
|
+
parsed = JSON.parse(raw);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
const reason = error instanceof Error ? error.message : 'invalid JSON';
|
|
62
|
+
return { status: 'malformed', reason };
|
|
63
|
+
}
|
|
64
|
+
const validation = validateManifestShape(parsed);
|
|
65
|
+
if (validation) {
|
|
66
|
+
return { status: 'malformed', reason: validation };
|
|
67
|
+
}
|
|
68
|
+
return { status: 'ok', manifest: parsed };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Returns a human-readable reason string when `value` is not a well-formed
|
|
72
|
+
* {@link EnvironmentManifest}, or `undefined` when it passes. Checks every key
|
|
73
|
+
* the comparison reads so a truncated or wrong-typed manifest is caught here
|
|
74
|
+
* (surfaced as `malformed`) rather than producing a garbage per-key delta.
|
|
75
|
+
*
|
|
76
|
+
* Exported so `approve --from` can run the same shape check against a candidate
|
|
77
|
+
* artifact's `environment.actual` before promoting it into the committed
|
|
78
|
+
* manifest — a well-formed-but-wrong-shaped environment block must fail closed,
|
|
79
|
+
* not slip into `manifest.json` and silently defeat the drift gate.
|
|
80
|
+
*/
|
|
81
|
+
export function validateManifestShape(value) {
|
|
82
|
+
if (typeof value !== 'object' || value === null) {
|
|
83
|
+
return 'manifest is not an object';
|
|
84
|
+
}
|
|
85
|
+
const manifest = value;
|
|
86
|
+
for (const key of [
|
|
87
|
+
'schemaVersion',
|
|
88
|
+
'captureSchema',
|
|
89
|
+
'deviceScaleFactor',
|
|
90
|
+
]) {
|
|
91
|
+
if (typeof manifest[key] !== 'number') {
|
|
92
|
+
return `\`${key}\` must be a number`;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
for (const key of [
|
|
96
|
+
'tuffgalVersion',
|
|
97
|
+
'playwrightVersion',
|
|
98
|
+
'browser',
|
|
99
|
+
'browserVersion',
|
|
100
|
+
'platform',
|
|
101
|
+
'captureMode',
|
|
102
|
+
'frozenTime',
|
|
103
|
+
]) {
|
|
104
|
+
if (typeof manifest[key] !== 'string') {
|
|
105
|
+
return `\`${key}\` must be a string`;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (!Array.isArray(manifest.breakpoints)) {
|
|
109
|
+
return '`breakpoints` must be an array';
|
|
110
|
+
}
|
|
111
|
+
for (const entry of manifest.breakpoints) {
|
|
112
|
+
if (typeof entry !== 'object' || entry === null) {
|
|
113
|
+
return '`breakpoints[]` must be objects';
|
|
114
|
+
}
|
|
115
|
+
const breakpoint = entry;
|
|
116
|
+
if (typeof breakpoint.name !== 'string') {
|
|
117
|
+
return '`breakpoints[].name` must be a string';
|
|
118
|
+
}
|
|
119
|
+
if (typeof breakpoint.width !== 'number' ||
|
|
120
|
+
typeof breakpoint.height !== 'number') {
|
|
121
|
+
return '`breakpoints[].width`/`height` must be numbers';
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Compares a committed manifest read result against the environment captured
|
|
128
|
+
* this run, returning which pixel-affecting keys diverged.
|
|
129
|
+
*
|
|
130
|
+
* - `missing` — bootstrap: nothing to violate. `{ mismatch: false }`.
|
|
131
|
+
* - `malformed` — unreadable committed manifest: mismatch-with-note,
|
|
132
|
+
* `mismatchKeys: ['manifest']` (see {@link MALFORMED_MANIFEST_KEY}).
|
|
133
|
+
* - `ok` — diff each {@link PIXEL_AFFECTING_KEYS}. `breakpoints` compares deep
|
|
134
|
+
* and order-sensitively: the manifest records an ordered list, and a
|
|
135
|
+
* reorder (even at identical dimensions) is treated as a change worth a
|
|
136
|
+
* re-approve prompt rather than silently equal — order-sensitivity is the
|
|
137
|
+
* conservative pixel-safety choice. Informational keys
|
|
138
|
+
* (`tuffgalVersion`/`playwrightVersion`) are never compared.
|
|
139
|
+
*/
|
|
140
|
+
export function compareEnvironment(expected, actual) {
|
|
141
|
+
if (expected.status === 'missing') {
|
|
142
|
+
return { mismatch: false, mismatchKeys: [] };
|
|
143
|
+
}
|
|
144
|
+
if (expected.status === 'malformed') {
|
|
145
|
+
return { mismatch: true, mismatchKeys: [MALFORMED_MANIFEST_KEY] };
|
|
146
|
+
}
|
|
147
|
+
const manifest = expected.manifest;
|
|
148
|
+
const mismatchKeys = [];
|
|
149
|
+
for (const key of PIXEL_AFFECTING_KEYS) {
|
|
150
|
+
if (key === 'breakpoints') {
|
|
151
|
+
if (!breakpointsEqual(manifest.breakpoints, actual.breakpoints)) {
|
|
152
|
+
mismatchKeys.push(key);
|
|
153
|
+
}
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (manifest[key] !== actual[key]) {
|
|
157
|
+
mismatchKeys.push(key);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return { mismatch: mismatchKeys.length > 0, mismatchKeys };
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Deep, order-sensitive equality of two breakpoint lists: same length, and each
|
|
164
|
+
* position matches on `name`/`width`/`height`. Order-sensitive on purpose — see
|
|
165
|
+
* {@link compareEnvironment}.
|
|
166
|
+
*/
|
|
167
|
+
function breakpointsEqual(a, b) {
|
|
168
|
+
if (a.length !== b.length)
|
|
169
|
+
return false;
|
|
170
|
+
return a.every((entry, index) => {
|
|
171
|
+
const other = b[index];
|
|
172
|
+
return (other !== undefined &&
|
|
173
|
+
entry.name === other.name &&
|
|
174
|
+
entry.width === other.width &&
|
|
175
|
+
entry.height === other.height);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
const requirePackage = createRequire(import.meta.url);
|
|
179
|
+
/**
|
|
180
|
+
* Reads tuffgal's own version from its `package.json`. Resolved relative to this
|
|
181
|
+
* module so it works from both `src/` (dev/test) and the built `dist/` tree —
|
|
182
|
+
* `../../package.json` is the package root in both layouts. Informational only.
|
|
183
|
+
*/
|
|
184
|
+
function readTuffgalVersion() {
|
|
185
|
+
const pkg = requirePackage('../../package.json');
|
|
186
|
+
return pkg.version;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Reads the installed Playwright version from its `package.json` via module
|
|
190
|
+
* resolution (not a hardcoded string), so a dependency bump is reflected
|
|
191
|
+
* automatically. Informational only.
|
|
192
|
+
*/
|
|
193
|
+
function readPlaywrightVersion() {
|
|
194
|
+
const pkg = requirePackage('playwright/package.json');
|
|
195
|
+
return pkg.version;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Builds the environment manifest describing THIS run — the "actual" side of
|
|
199
|
+
* the comparison and the block a later `approve --from` promotes into the
|
|
200
|
+
* committed manifest. Everything except the live browser version is derived
|
|
201
|
+
* from the resolved config and the host, so the caller only supplies the
|
|
202
|
+
* browser identity it read from the launched Playwright browser.
|
|
203
|
+
*
|
|
204
|
+
* `deviceScaleFactor` is hardcoded `1`: the runner never sets `deviceScaleFactor`
|
|
205
|
+
* on `browser.newContext` (see `runStory.ts`), so Playwright's default of `1`
|
|
206
|
+
* applies. If a future wave makes the scale factor configurable, thread it here
|
|
207
|
+
* and this constant must track it — the manifest's whole job is to notice a DSF
|
|
208
|
+
* change, so a stale hardcode here would silently defeat that.
|
|
209
|
+
*/
|
|
210
|
+
export function captureEnvironment(config, browser) {
|
|
211
|
+
return {
|
|
212
|
+
schemaVersion: SCHEMA_VERSION,
|
|
213
|
+
captureSchema: CAPTURE_SCHEMA,
|
|
214
|
+
tuffgalVersion: readTuffgalVersion(),
|
|
215
|
+
playwrightVersion: readPlaywrightVersion(),
|
|
216
|
+
browser: browser.name,
|
|
217
|
+
browserVersion: browser.version,
|
|
218
|
+
platform: process.platform,
|
|
219
|
+
captureMode: config.captureMode,
|
|
220
|
+
breakpoints: config.breakpoints.map((breakpoint) => ({
|
|
221
|
+
name: breakpoint.name,
|
|
222
|
+
width: breakpoint.width,
|
|
223
|
+
height: breakpoint.height,
|
|
224
|
+
})),
|
|
225
|
+
deviceScaleFactor: 1,
|
|
226
|
+
frozenTime: config.frozenTime,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
//# sourceMappingURL=manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../src/runner/manifest.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ResolvedConfig } from '../config.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Which comparison contract a `tuffgal run` executes under. Foundation for the
|
|
4
|
+
* CI-owned-baselines model: `ci` compares actuals against the committed
|
|
5
|
+
* `paths.baselines` set (the source of truth, written only via approval flows),
|
|
6
|
+
* while `local` compares against a per-machine gitignored cache for advisory
|
|
7
|
+
* self-diffing. This wave only resolves + threads the mode; downstream branching
|
|
8
|
+
* on it arrives in later waves.
|
|
9
|
+
*/
|
|
10
|
+
export type RunMode = 'ci' | 'local';
|
|
11
|
+
export interface RunModeInputs {
|
|
12
|
+
/** `--ci` flag was passed. */
|
|
13
|
+
ci: boolean;
|
|
14
|
+
/** `--local` flag was passed. */
|
|
15
|
+
local: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Environment to read the `CI` default from. Injected (rather than reading
|
|
18
|
+
* `process.env` directly) so the resolution matrix is unit-testable without
|
|
19
|
+
* mutating global process state.
|
|
20
|
+
*/
|
|
21
|
+
env: NodeJS.ProcessEnv;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Resolves the run mode from the two mutually-exclusive flags and the `CI`
|
|
25
|
+
* environment variable:
|
|
26
|
+
* - `--ci` and `--local` are mutually exclusive; passing both throws.
|
|
27
|
+
* - An explicit flag always wins over the environment.
|
|
28
|
+
* - With no flag, default to `ci` when `CI` is truthy (non-empty and not an
|
|
29
|
+
* explicit negation — see {@link FALSY_CI}), else `local`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function resolveRunMode(inputs: RunModeInputs): RunMode;
|
|
32
|
+
/**
|
|
33
|
+
* The baseline root a run compares against for the given mode: the committed
|
|
34
|
+
* `paths.baselines` set in CI mode, the per-machine gitignored `paths.localCache`
|
|
35
|
+
* in local mode. This is the single source of the mode→root mapping — both the
|
|
36
|
+
* per-action comparison in `runAction` and the screen-coverage metric in `run`
|
|
37
|
+
* select their root through here, so the two can never drift. Encodes the PRD
|
|
38
|
+
* invariant that a local run never reads `paths.baselines`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function comparisonRootFor(config: ResolvedConfig, mode: RunMode): string;
|
|
41
|
+
//# sourceMappingURL=mode.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mode.d.ts","sourceRoot":"","sources":["../../src/runner/mode.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD;;;;;;;GAOG;AACH,MAAM,MAAM,OAAO,GAAG,IAAI,GAAG,OAAO,CAAC;AAErC,MAAM,WAAW,aAAa;IAC5B,8BAA8B;IAC9B,EAAE,EAAE,OAAO,CAAC;IACZ,iCAAiC;IACjC,KAAK,EAAE,OAAO,CAAC;IACf;;;;OAIG;IACH,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;CACxB;AAYD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAW7D;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,OAAO,GACZ,MAAM,CAER"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strings a CI system might export to explicitly opt OUT of CI behaviour. Most
|
|
3
|
+
* CI providers (GitHub Actions, Travis, CircleCI, GitLab, …) set `CI=true`, but
|
|
4
|
+
* the de-facto convention — as codified by the widely-used `ci-info`/`is-ci`
|
|
5
|
+
* packages — is that any non-empty `CI` value counts as "in CI" unless it is one
|
|
6
|
+
* of these explicit negations. We mirror that so a developer who exports
|
|
7
|
+
* `CI=false` locally still gets local mode.
|
|
8
|
+
*/
|
|
9
|
+
const FALSY_CI = new Set(['', '0', 'false']);
|
|
10
|
+
/**
|
|
11
|
+
* Resolves the run mode from the two mutually-exclusive flags and the `CI`
|
|
12
|
+
* environment variable:
|
|
13
|
+
* - `--ci` and `--local` are mutually exclusive; passing both throws.
|
|
14
|
+
* - An explicit flag always wins over the environment.
|
|
15
|
+
* - With no flag, default to `ci` when `CI` is truthy (non-empty and not an
|
|
16
|
+
* explicit negation — see {@link FALSY_CI}), else `local`.
|
|
17
|
+
*/
|
|
18
|
+
export function resolveRunMode(inputs) {
|
|
19
|
+
if (inputs.ci && inputs.local) {
|
|
20
|
+
throw new Error('Cannot pass both --ci and --local; choose one.');
|
|
21
|
+
}
|
|
22
|
+
if (inputs.ci)
|
|
23
|
+
return 'ci';
|
|
24
|
+
if (inputs.local)
|
|
25
|
+
return 'local';
|
|
26
|
+
const raw = inputs.env.CI;
|
|
27
|
+
if (raw !== undefined && !FALSY_CI.has(raw.toLowerCase())) {
|
|
28
|
+
return 'ci';
|
|
29
|
+
}
|
|
30
|
+
return 'local';
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The baseline root a run compares against for the given mode: the committed
|
|
34
|
+
* `paths.baselines` set in CI mode, the per-machine gitignored `paths.localCache`
|
|
35
|
+
* in local mode. This is the single source of the mode→root mapping — both the
|
|
36
|
+
* per-action comparison in `runAction` and the screen-coverage metric in `run`
|
|
37
|
+
* select their root through here, so the two can never drift. Encodes the PRD
|
|
38
|
+
* invariant that a local run never reads `paths.baselines`.
|
|
39
|
+
*/
|
|
40
|
+
export function comparisonRootFor(config, mode) {
|
|
41
|
+
return mode === 'ci' ? config.paths.baselines : config.paths.localCache;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=mode.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mode.js","sourceRoot":"","sources":["../../src/runner/mode.ts"],"names":[],"mappings":"AAyBA;;;;;;;GAOG;AACH,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,CAAC,EAAE,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC;AAE7C;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,MAAqB;IAClD,IAAI,MAAM,CAAC,EAAE,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;IACpE,CAAC;IACD,IAAI,MAAM,CAAC,EAAE;QAAE,OAAO,IAAI,CAAC;IAC3B,IAAI,MAAM,CAAC,KAAK;QAAE,OAAO,OAAO,CAAC;IACjC,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;IAC1B,IAAI,GAAG,KAAK,SAAS,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;QAC1D,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAC/B,MAAsB,EACtB,IAAa;IAEb,OAAO,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC;AAC1E,CAAC"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { RunMode } from './mode.ts';
|
|
2
|
+
import type { DeletedBaseline, RunResult } from '../schema/result.ts';
|
|
3
|
+
/**
|
|
4
|
+
* A11y companion suffix for a breakpoint-keyed baseline PNG. `<action>/<bp>.png`
|
|
5
|
+
* carries `<action>/<bp>.a11y.yaml` beside it (see `baselineStore.pathsFor`).
|
|
6
|
+
* Exported so `approve --from` derives the same companion filename from the
|
|
7
|
+
* shared constant instead of re-literalling the suffix at its own producer/
|
|
8
|
+
* consumer boundary.
|
|
9
|
+
*/
|
|
10
|
+
export declare const A11Y_SUFFIX = ".a11y.yaml";
|
|
11
|
+
/**
|
|
12
|
+
* Pre-breakpoint (legacy) baseline files: a project baselined before the
|
|
13
|
+
* per-breakpoint layout committed `<action>/0.png` + `<action>/a11y.yaml`. The
|
|
14
|
+
* scan keys these under the synthetic breakpoint name `'legacy'` so a prune step
|
|
15
|
+
* can delete them without re-deriving the layout. Exported so `approve --from`
|
|
16
|
+
* shares the same legacy-layout literals rather than re-declaring them.
|
|
17
|
+
*/
|
|
18
|
+
export declare const LEGACY_PNG = "0.png";
|
|
19
|
+
export declare const LEGACY_A11Y = "a11y.yaml";
|
|
20
|
+
export declare const LEGACY_BREAKPOINT = "legacy";
|
|
21
|
+
/**
|
|
22
|
+
* Whether the orphan scan should run for this invocation. Two gates, both
|
|
23
|
+
* must hold:
|
|
24
|
+
* - CI mode only. Local runs never read `paths.baselines` (they self-diff
|
|
25
|
+
* against the per-machine cache), so there is no committed set to orphan.
|
|
26
|
+
* - Unfiltered runs only. A `--story` filter runs a deliberate subset, so
|
|
27
|
+
* every unselected story's baseline would look orphaned when it is merely
|
|
28
|
+
* unvisited; a filtered run must never mark a live baseline `deleted`.
|
|
29
|
+
*
|
|
30
|
+
* Kept pure and separate from `run.ts` so the CI/local × filtered/unfiltered
|
|
31
|
+
* matrix is unit-testable without launching a browser.
|
|
32
|
+
*/
|
|
33
|
+
export declare function shouldScanForOrphans(mode: RunMode, storyFilter: string | undefined): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* The distinct set of action names that ran at least one result this run. An
|
|
36
|
+
* action directory under `paths.baselines` whose name is absent from this set
|
|
37
|
+
* had no story compare against it — it is a candidate orphan. Derived from the
|
|
38
|
+
* flat `stories[].actions[]` walk so `skipped` entries (an earlier step failed)
|
|
39
|
+
* still count the action as executed: the baseline was reachable this run, the
|
|
40
|
+
* story simply broke before reaching it, which is a failure to surface — not a
|
|
41
|
+
* baseline to retire.
|
|
42
|
+
*/
|
|
43
|
+
export declare function executedActionNames(stories: RunResult['stories']): Set<string>;
|
|
44
|
+
/**
|
|
45
|
+
* Scans `baselinesDir` for orphaned committed baselines: action directories no
|
|
46
|
+
* story executed this run. Detection only — the returned entries carry the
|
|
47
|
+
* absolute paths a later `approve --prune` deletes; this function never touches
|
|
48
|
+
* the filesystem beyond reading directory listings.
|
|
49
|
+
*
|
|
50
|
+
* Caller contract (enforced in `run.ts`, documented here so the module reads
|
|
51
|
+
* standalone): only meaningful for an UNFILTERED CI run. A `--story`-filtered run
|
|
52
|
+
* executes a deliberate subset, so every unselected story's baseline would look
|
|
53
|
+
* orphaned; the caller skips the scan entirely on a filtered run rather than
|
|
54
|
+
* mark live baselines `deleted`. Local mode never reads `paths.baselines` at all,
|
|
55
|
+
* so it never calls this.
|
|
56
|
+
*
|
|
57
|
+
* Layout handled (mirrors `baselineStore.pathsFor`):
|
|
58
|
+
* - breakpoint-keyed `<action>/<breakpoint>.png` (+ `.a11y.yaml` companion)
|
|
59
|
+
* - legacy `<action>/0.png` (+ `a11y.yaml` companion)
|
|
60
|
+
*
|
|
61
|
+
* One {@link DeletedBaseline} per orphaned breakpoint (or the single `'legacy'`
|
|
62
|
+
* group), each bundling the PNG with its a11y companion when present. Files that
|
|
63
|
+
* are neither a recognised PNG nor a matching companion are ignored, so a stray
|
|
64
|
+
* file in an orphaned dir neither crashes the scan nor smuggles an untracked
|
|
65
|
+
* path into the prune list.
|
|
66
|
+
*/
|
|
67
|
+
export declare function scanOrphanedBaselines(baselinesDir: string, executed: Set<string>): Promise<DeletedBaseline[]>;
|
|
68
|
+
//# sourceMappingURL=orphanScan.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orphanScan.d.ts","sourceRoot":"","sources":["../../src/runner/orphanScan.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAEtE;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,eAAe,CAAC;AAExC;;;;;;GAMG;AACH,eAAO,MAAM,UAAU,UAAU,CAAC;AAClC,eAAO,MAAM,WAAW,cAAc,CAAC;AACvC,eAAO,MAAM,iBAAiB,WAAW,CAAC;AAW1C;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,OAAO,EACb,WAAW,EAAE,MAAM,GAAG,SAAS,GAC9B,OAAO,CAET;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,SAAS,CAAC,SAAS,CAAC,GAC5B,GAAG,CAAC,MAAM,CAAC,CAQb;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAsB,qBAAqB,CACzC,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,GACpB,OAAO,CAAC,eAAe,EAAE,CAAC,CAuB5B"}
|