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.
Files changed (89) hide show
  1. package/CHANGELOG.md +149 -2
  2. package/README.md +78 -3
  3. package/dist/cli.d.ts +50 -0
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +204 -18
  6. package/dist/cli.js.map +1 -1
  7. package/dist/commands/init.d.ts +1 -1
  8. package/dist/commands/init.d.ts.map +1 -1
  9. package/dist/commands/init.js +11 -1
  10. package/dist/commands/init.js.map +1 -1
  11. package/dist/config.d.ts +38 -1
  12. package/dist/config.d.ts.map +1 -1
  13. package/dist/config.js +20 -1
  14. package/dist/config.js.map +1 -1
  15. package/dist/coverage/screens.d.ts +11 -6
  16. package/dist/coverage/screens.d.ts.map +1 -1
  17. package/dist/coverage/screens.js +12 -7
  18. package/dist/coverage/screens.js.map +1 -1
  19. package/dist/reporter/assets/report.css +415 -51
  20. package/dist/reporter/assets/report.js +502 -68
  21. package/dist/reporter/template.d.ts +8 -1
  22. package/dist/reporter/template.d.ts.map +1 -1
  23. package/dist/reporter/template.js +517 -85
  24. package/dist/reporter/template.js.map +1 -1
  25. package/dist/reporter/writeReport.d.ts +2 -2
  26. package/dist/reporter/writeReport.d.ts.map +1 -1
  27. package/dist/reporter/writeReport.js +3 -3
  28. package/dist/reporter/writeReport.js.map +1 -1
  29. package/dist/runner/approve.d.ts +34 -9
  30. package/dist/runner/approve.d.ts.map +1 -1
  31. package/dist/runner/approve.js +54 -10
  32. package/dist/runner/approve.js.map +1 -1
  33. package/dist/runner/approveFrom.d.ts +58 -0
  34. package/dist/runner/approveFrom.d.ts.map +1 -0
  35. package/dist/runner/approveFrom.js +384 -0
  36. package/dist/runner/approveFrom.js.map +1 -0
  37. package/dist/runner/breakpointPasses.d.ts +39 -0
  38. package/dist/runner/breakpointPasses.d.ts.map +1 -0
  39. package/dist/runner/breakpointPasses.js +107 -0
  40. package/dist/runner/breakpointPasses.js.map +1 -0
  41. package/dist/runner/bridges/database.d.ts +4 -3
  42. package/dist/runner/bridges/database.d.ts.map +1 -1
  43. package/dist/runner/bridges/database.js +4 -3
  44. package/dist/runner/bridges/database.js.map +1 -1
  45. package/dist/runner/exitCode.d.ts +34 -0
  46. package/dist/runner/exitCode.d.ts.map +1 -0
  47. package/dist/runner/exitCode.js +41 -0
  48. package/dist/runner/exitCode.js.map +1 -0
  49. package/dist/runner/manifest.d.ts +149 -0
  50. package/dist/runner/manifest.d.ts.map +1 -0
  51. package/dist/runner/manifest.js +229 -0
  52. package/dist/runner/manifest.js.map +1 -0
  53. package/dist/runner/mode.d.ts +41 -0
  54. package/dist/runner/mode.d.ts.map +1 -0
  55. package/dist/runner/mode.js +43 -0
  56. package/dist/runner/mode.js.map +1 -0
  57. package/dist/runner/orphanScan.d.ts +68 -0
  58. package/dist/runner/orphanScan.d.ts.map +1 -0
  59. package/dist/runner/orphanScan.js +150 -0
  60. package/dist/runner/orphanScan.js.map +1 -0
  61. package/dist/runner/run.d.ts +85 -1
  62. package/dist/runner/run.d.ts.map +1 -1
  63. package/dist/runner/run.js +267 -41
  64. package/dist/runner/run.js.map +1 -1
  65. package/dist/runner/runAction.d.ts +16 -0
  66. package/dist/runner/runAction.d.ts.map +1 -1
  67. package/dist/runner/runAction.js +112 -13
  68. package/dist/runner/runAction.js.map +1 -1
  69. package/dist/runner/runStory.d.ts +15 -4
  70. package/dist/runner/runStory.d.ts.map +1 -1
  71. package/dist/runner/runStory.js +41 -19
  72. package/dist/runner/runStory.js.map +1 -1
  73. package/dist/runner/storyFilter.d.ts +8 -0
  74. package/dist/runner/storyFilter.d.ts.map +1 -1
  75. package/dist/runner/storyFilter.js +11 -0
  76. package/dist/runner/storyFilter.js.map +1 -1
  77. package/dist/schema/action.d.ts +11 -11
  78. package/dist/schema/result.d.ts +125 -6
  79. package/dist/schema/result.d.ts.map +1 -1
  80. package/dist/schema/result.js.map +1 -1
  81. package/dist/screenshots/baselineStore.d.ts +92 -10
  82. package/dist/screenshots/baselineStore.d.ts.map +1 -1
  83. package/dist/screenshots/baselineStore.js +102 -18
  84. package/dist/screenshots/baselineStore.js.map +1 -1
  85. package/dist/screenshots/capture.d.ts +10 -5
  86. package/dist/screenshots/capture.d.ts.map +1 -1
  87. package/dist/screenshots/capture.js +31 -18
  88. package/dist/screenshots/capture.js.map +1 -1
  89. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  import { access, copyFile, mkdir, readFile, rm, writeFile, } from 'node:fs/promises';
2
2
  import { dirname, join, relative } from 'node:path';
3
+ import { PNG } from 'pngjs';
3
4
  /**
4
5
  * Reduces an arbitrary breakpoint name to a filesystem-safe path segment.
5
6
  * Breakpoint names are simple lowercase identifiers today (`mobile`, `tablet`,
@@ -12,33 +13,69 @@ function breakpointSegment(breakpoint) {
12
13
  return breakpoint.toLowerCase().replace(/[^a-z0-9_-]/g, '-');
13
14
  }
14
15
  /**
15
- * Computes deterministic paths for the baseline (committed), actual
16
- * (regenerated each run), and diff (regenerated when a baseline existed and
17
- * the diff was non-zero) PNGs. Centralised so the runner and the CLI's
18
- * `approve` command agree on layout.
16
+ * The single owner of the `<report>/candidates/` path. The runner writes the
17
+ * candidate tree + a `results.json` copy here, and `approve --from` reads that
18
+ * same tree back. Centralised so the literal `'candidates'` segment has one
19
+ * definition instead of drifting across the runner, the store, and approve.
20
+ */
21
+ export function candidatesDir(reportDir) {
22
+ return join(reportDir, 'candidates');
23
+ }
24
+ /**
25
+ * The action-name shape the action schema enforces (`/^[a-z0-9-]+$/`,
26
+ * lowercase-kebab). Re-exported here so the trust boundary in `approve --from`
27
+ * can reject a directory name from an untrusted candidate artifact against the
28
+ * exact same invariant the runner produced its layout under, without importing
29
+ * the zod schema. Kept in lock-step with `schema/action.ts` by intent.
30
+ */
31
+ export const ACTION_NAME_PATTERN = /^[a-z0-9-]+$/;
32
+ /**
33
+ * The filesystem-safe breakpoint segment shape (`/^[a-z0-9_-]+$/`) that {@link
34
+ * breakpointSegment} emits. `approve --from` validates a candidate PNG's
35
+ * breakpoint stem against this so a name outside the runner's own output shape
36
+ * (path separators, dots, traversal) can never reach a write.
37
+ */
38
+ export const BREAKPOINT_SEGMENT_PATTERN = /^[a-z0-9_-]+$/;
39
+ /**
40
+ * Computes deterministic paths for the baseline (the comparison target —
41
+ * committed baselines by default, or the local cache when `comparisonRoot` is
42
+ * supplied), actual (regenerated each run), diff (regenerated when a baseline
43
+ * existed and the diff was non-zero), and candidate (proposed-new-baseline for
44
+ * CI approval) PNGs. Centralised so the runner and the CLI's `approve` command
45
+ * agree on layout.
19
46
  *
20
47
  * Every path is keyed by both action name and breakpoint so per-breakpoint
21
- * captures of the same action stay isolated: baselines nest the breakpoint as a
22
- * filename under the action directory (`<action>/<breakpoint>.png`), while
23
- * report-side artifacts splice it into the filename
24
- * (`<action>.<breakpoint>.actual.png`) since they already share one per-story
25
- * directory.
48
+ * captures of the same action stay isolated: baseline- and candidate-side paths
49
+ * nest the breakpoint as a filename under the action directory
50
+ * (`<action>/<breakpoint>.png`), while report-side actual/diff artifacts splice
51
+ * it into the filename (`<action>.<breakpoint>.actual.png`) since they already
52
+ * share one per-story directory.
53
+ *
54
+ * The baseline side resolves under `comparisonRoot` (default `baselinesDir`), so
55
+ * omitting the root reproduces the committed-baselines layout unchanged; the
56
+ * candidate and report-side paths are always report-rooted and ignore it.
26
57
  */
27
58
  export function pathsFor(options) {
28
59
  const storySlug = options.storyFile.replace(/\.json$/i, '');
29
60
  const bp = breakpointSegment(options.breakpoint);
61
+ const comparisonRoot = options.comparisonRoot ?? options.baselinesDir;
30
62
  return {
31
- baseline: join(options.baselinesDir, options.actionName, `${bp}.png`),
63
+ baseline: join(comparisonRoot, options.actionName, `${bp}.png`),
32
64
  actual: join(options.reportDir, 'screenshots', storySlug, `${options.actionName}.${bp}.actual.png`),
33
65
  diff: join(options.reportDir, 'screenshots', storySlug, `${options.actionName}.${bp}.diff.png`),
34
- a11yBaseline: join(options.baselinesDir, options.actionName, `${bp}.a11y.yaml`),
66
+ a11yBaseline: join(comparisonRoot, options.actionName, `${bp}.a11y.yaml`),
35
67
  a11yActual: join(options.reportDir, 'screenshots', storySlug, `${options.actionName}.${bp}.a11y.yaml`),
68
+ // Candidate tree mirrors the baseline layout under the report so a CI
69
+ // approval is a plain copy of `<report>/candidates/` into `paths.baselines`.
70
+ candidate: join(candidatesDir(options.reportDir), options.actionName, `${bp}.png`),
71
+ a11yCandidate: join(candidatesDir(options.reportDir), options.actionName, `${bp}.a11y.yaml`),
36
72
  // Pre-breakpoint locations. A project baselined before this feature has
37
73
  // its only committed snapshot here; the runner reads `baseline` first and
38
74
  // falls back to these when the breakpoint-keyed file is absent, so existing
39
- // baselines keep matching instead of all reading as `new`.
40
- legacyBaseline: join(options.baselinesDir, options.actionName, '0.png'),
41
- legacyA11yBaseline: join(options.baselinesDir, options.actionName, 'a11y.yaml'),
75
+ // baselines keep matching instead of all reading as `new`. Rooted at the
76
+ // comparison root like the breakpoint-keyed baseline above.
77
+ legacyBaseline: join(comparisonRoot, options.actionName, '0.png'),
78
+ legacyA11yBaseline: join(comparisonRoot, options.actionName, 'a11y.yaml'),
42
79
  };
43
80
  }
44
81
  /**
@@ -86,9 +123,47 @@ export async function writeText(path, content) {
86
123
  await mkdir(dirname(path), { recursive: true });
87
124
  await writeFile(path, content, 'utf8');
88
125
  }
126
+ /**
127
+ * Losslessly recompresses a PNG buffer. Playwright emits PNGs tuned for capture
128
+ * speed, not size; re-encoding through pngjs applies its default
129
+ * `deflateLevel: 9` plus adaptive per-scanline filtering (filter type chosen per
130
+ * row from the full `[0..4]` set), which typically shrinks the file with zero
131
+ * pixel change. The decode→re-encode round-trip is lossless: pngjs reads and
132
+ * writes 8-bit RGBA, so the recompressed buffer decodes to a byte-identical
133
+ * pixel array (`recompress.test.ts` asserts this).
134
+ *
135
+ * Two safety guarantees:
136
+ * - Never grows a file. A source that is already densely packed (e.g. a
137
+ * future oxipng-optimised baseline flowing through `approve --from`) can
138
+ * re-encode *larger*; in that case the original buffer is returned unchanged.
139
+ * - Never corrupts. Any decode/encode failure (a non-PNG buffer, a codec edge
140
+ * case) is swallowed and the original buffer is returned, so a recompress
141
+ * miss degrades to "write the bytes we were given" rather than a torn file.
142
+ *
143
+ * This is the single lossless-recompress seam for every PNG the tool writes:
144
+ * `writePng` routes through it, so baseline, candidate, cache, actual, and diff
145
+ * writes all inherit it without per-callsite wiring. Deterministic — pngjs's
146
+ * encoder is a pure function of the pixel data and the fixed options above.
147
+ *
148
+ * Deliberately NOT palette-quantised and never lossy: colour type and bit depth
149
+ * are preserved by the RGBA round-trip.
150
+ */
151
+ export function recompressPng(png) {
152
+ let recompressed;
153
+ try {
154
+ recompressed = PNG.sync.write(PNG.sync.read(png));
155
+ }
156
+ catch {
157
+ // Not a decodable PNG, or an encoder edge case — write what we were given
158
+ // rather than risk emitting a corrupt or truncated file.
159
+ return png;
160
+ }
161
+ // A recompress that grows the file is a loss, not a win — keep the smaller.
162
+ return recompressed.length < png.length ? recompressed : png;
163
+ }
89
164
  export async function writePng(path, png) {
90
165
  await mkdir(dirname(path), { recursive: true });
91
- await writeFile(path, png);
166
+ await writeFile(path, recompressPng(png));
92
167
  }
93
168
  export async function deleteIfExists(path) {
94
169
  try {
@@ -98,9 +173,18 @@ export async function deleteIfExists(path) {
98
173
  // file was not there; nothing to do
99
174
  }
100
175
  }
101
- export async function copyToBaseline(actualPath, baselinePath) {
102
- await mkdir(dirname(baselinePath), { recursive: true });
103
- await copyFile(actualPath, baselinePath);
176
+ /**
177
+ * 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 was produced
179
+ * by `writePng`, which already ran `recompressPng`, so the bytes on disk are the
180
+ * recompressed ones. Copying them forward keeps the destination losslessly
181
+ * recompressed for free. The only non-test caller is `approve` refreshing the
182
+ * per-machine cache; `approve --from` does NOT use this — it reads each source
183
+ * once during validation and writes the retained buffer through {@link writePng}.
184
+ */
185
+ export async function copyRecompressedPng(actualPath, destinationPath) {
186
+ await mkdir(dirname(destinationPath), { recursive: true });
187
+ await copyFile(actualPath, destinationPath);
104
188
  }
105
189
  /**
106
190
  * Turns an absolute screenshot path into a path that is relative to the
@@ -1 +1 @@
1
- {"version":3,"file":"baselineStore.js","sourceRoot":"","sources":["../../src/screenshots/baselineStore.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"baselineStore.js","sourceRoot":"","sources":["../../src/screenshots/baselineStore.ts"],"names":[],"mappings":"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"}
@@ -1,9 +1,14 @@
1
1
  import type { Locator, Page } from 'playwright';
2
+ import type { CaptureMode } from '../config.ts';
2
3
  /**
3
- * Full-page screenshot with animations disabled, caret hidden, and any masks
4
- * applied so the same UI renders to bit-identical pixels across runs. Masks
5
- * are Playwright locators whose bounding boxes are blacked out before the
6
- * image is encoded — use them to neutralise randomised or time-based regions.
4
+ * Screenshot with animations disabled, caret hidden, and any masks applied so
5
+ * 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
+ * use them to neutralise randomised or time-based regions.
8
+ *
9
+ * `mode` controls scope: `viewport` (default) captures only the page's current
10
+ * viewport box — what the user sees above the fold — while `fullPage`
11
+ * composites the entire scrollable document however tall.
7
12
  */
8
- export declare function capturePage(page: Page, masks?: Locator[]): Promise<Buffer>;
13
+ export declare function capturePage(page: Page, masks?: Locator[], mode?: CaptureMode): Promise<Buffer>;
9
14
  //# 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;AAEhD;;;;;GAKG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,IAAI,EACV,KAAK,GAAE,OAAO,EAAO,GACpB,OAAO,CAAC,MAAM,CAAC,CAqBjB"}
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;;;;;;;;;GASG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,IAAI,EACV,KAAK,GAAE,OAAO,EAAO,EACrB,IAAI,GAAE,WAAwB,GAC7B,OAAO,CAAC,MAAM,CAAC,CA8BjB"}
@@ -1,24 +1,37 @@
1
1
  /**
2
- * Full-page screenshot with animations disabled, caret hidden, and any masks
3
- * applied so the same UI renders to bit-identical pixels across runs. Masks
4
- * are Playwright locators whose bounding boxes are blacked out before the
5
- * image is encoded — use them to neutralise randomised or time-based regions.
2
+ * Screenshot with animations disabled, caret hidden, and any masks applied so
3
+ * 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
+ * use them to neutralise randomised or time-based regions.
6
+ *
7
+ * `mode` controls scope: `viewport` (default) captures only the page's current
8
+ * viewport box — what the user sees above the fold — while `fullPage`
9
+ * composites the entire scrollable document however tall.
6
10
  */
7
- export async function capturePage(page, masks = []) {
8
- // A `fullPage` screenshot composites the whole document, but it does so at
9
- // the page's current scroll offset. `position: sticky` / `fixed` elements
10
- // resolve their offset against that scroll position, so a page captured
11
- // mid-scroll renders its sticky chrome (sidebars, headers) shifted down by
12
- // `scrollY` — the same UI produces a different image purely because an
13
- // earlier step left the viewport scrolled. Resetting to the origin first
14
- // pins those elements to their static-baseline position so the capture is
15
- // deterministic. `instant` defeats any `scroll-behavior: smooth` the page
16
- // sets, which would otherwise animate and reintroduce a timing race.
17
- // String form so the snippet runs in the page (DOM) context without
18
- // pulling the DOM lib into this Node-side module's type environment.
19
- await page.evaluate(`window.scrollTo({ top: 0, left: 0, behavior: 'instant' })`);
11
+ export async function capturePage(page, masks = [], mode = 'viewport') {
12
+ // `fullPage` composites the whole scrollable document into one image. When
13
+ // the document is captured at a non-zero offset, `position: sticky` / `fixed`
14
+ // chrome (sidebars, headers) resolves against that scroll position and lands
15
+ // shifted down by `scrollY` in the stitch — the same UI produces a different
16
+ // image purely because an earlier step left the viewport scrolled. Resetting
17
+ // to the origin first pins those elements to their static-baseline position
18
+ // so the capture is deterministic. `instant` defeats any `scroll-behavior:
19
+ // smooth` the page sets, which would otherwise animate and reintroduce a
20
+ // timing race.
21
+ //
22
+ // A `viewport` shot captures exactly the box the flow scrolled to, so it must
23
+ // NOT reset — a story that scrolls to a below-the-fold section (a Settings
24
+ // panel, a success banner) would otherwise capture the top of the page
25
+ // instead of the region it navigated to. The flow lands at a deterministic
26
+ // offset, so the viewport shot is deterministic without any reset.
27
+ //
28
+ // String form so the snippet runs in the page (DOM) context without pulling
29
+ // the DOM lib into this Node-side module's type environment.
30
+ if (mode === 'fullPage') {
31
+ await page.evaluate(`window.scrollTo({ top: 0, left: 0, behavior: 'instant' })`);
32
+ }
20
33
  return page.screenshot({
21
- fullPage: true,
34
+ fullPage: mode === 'fullPage',
22
35
  animations: 'disabled',
23
36
  caret: 'hide',
24
37
  mask: masks,
@@ -1 +1 @@
1
- {"version":3,"file":"capture.js","sourceRoot":"","sources":["../../src/screenshots/capture.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,IAAU,EACV,QAAmB,EAAE;IAErB,2EAA2E;IAC3E,0EAA0E;IAC1E,wEAAwE;IACxE,2EAA2E;IAC3E,uEAAuE;IACvE,yEAAyE;IACzE,0EAA0E;IAC1E,0EAA0E;IAC1E,qEAAqE;IACrE,oEAAoE;IACpE,qEAAqE;IACrE,MAAM,IAAI,CAAC,QAAQ,CACjB,2DAA2D,CAC5D,CAAC;IACF,OAAO,IAAI,CAAC,UAAU,CAAC;QACrB,QAAQ,EAAE,IAAI;QACd,UAAU,EAAE,UAAU;QACtB,KAAK,EAAE,MAAM;QACb,IAAI,EAAE,KAAK;KACZ,CAAC,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"capture.js","sourceRoot":"","sources":["../../src/screenshots/capture.ts"],"names":[],"mappings":"AAGA;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,IAAU,EACV,QAAmB,EAAE,EACrB,OAAoB,UAAU;IAE9B,2EAA2E;IAC3E,8EAA8E;IAC9E,6EAA6E;IAC7E,6EAA6E;IAC7E,6EAA6E;IAC7E,4EAA4E;IAC5E,2EAA2E;IAC3E,yEAAyE;IACzE,eAAe;IACf,EAAE;IACF,8EAA8E;IAC9E,2EAA2E;IAC3E,uEAAuE;IACvE,2EAA2E;IAC3E,mEAAmE;IACnE,EAAE;IACF,4EAA4E;IAC5E,6DAA6D;IAC7D,IAAI,IAAI,KAAK,UAAU,EAAE,CAAC;QACxB,MAAM,IAAI,CAAC,QAAQ,CACjB,2DAA2D,CAC5D,CAAC;IACJ,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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tuffgal",
3
- "version": "0.1.0-alpha.9",
3
+ "version": "0.2.0-alpha.2",
4
4
  "description": "JSON-driven visual regression for web apps.",
5
5
  "license": "MIT",
6
6
  "author": "Nick Schneble",