snapdrift 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,42 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.3.0 - 2026-05-26
4
+
5
+ ### Infrastructure
6
+
7
+ - **Package extraction (Phase 1a)** — extracted `@snapdrift/manifest` (schema, validation, indexing, viewport presets) and `@snapdrift/compare-core` (pure pixel comparison, ignore-region masking) as standalone npm packages with zero runtime deps beyond `pngjs`.
8
+ - **Package extraction (Phase 1b)** — extracted `@snapdrift/adapter-fs` (all filesystem I/O: capture, compare, config, staging, drift report generation) and `@snapdrift/adapter-report-md` (pure markdown/HTML report generators, zero runtime deps) as standalone npm packages.
9
+ - **Lib/ shim refactor** — all `lib/*.mjs` modules are now thin re-export shims that delegate to packages. `lib/report.mjs` wires the default filesystem `imageReader` for HTML reports. Backward-compatible — all export surfaces preserved.
10
+ - **`generateDiffImage` ignore regions** — `@snapdrift/compare-core` now overlays ignored pixel regions with semi-transparent gray instead of skipping them.
11
+ - **Publish workflow** — `.github/workflows/publish.yml` now publishes all four `@snapdrift/*` workspace packages in dependency order before the root `snapdrift` package.
12
+
13
+ ### Performance
14
+
15
+ - **Faster screenshot capture** — navigation now waits for `load` instead of `networkidle`, removing a guaranteed ≥500 ms floor per route. A 20-route run saves ~10 s in navigation waits alone.
16
+ - **Concurrent route capture** — routes within the same viewport are now captured in parallel (up to `SNAPDRIFT_CAPTURE_CONCURRENCY` at a time, default 5) rather than sequentially, giving a further 3–5× throughput improvement on multi-route suites.
17
+ - **Deterministic animation handling** — screenshots are taken with `animations: 'disabled'` so Playwright finishes/cancels CSS animations before capture, replacing reliance on the settle delay for animation timing.
18
+
19
+ ### Features
20
+
21
+ - **`SNAPDRIFT_CAPTURE_CONCURRENCY` env var** — controls the maximum number of routes captured concurrently within a viewport context (positive integer, default `5`). Set to `1` to restore serial-per-viewport behaviour for apps with shared session or auth state.
22
+
23
+ ## 0.2.1 - 2026-04-13
24
+
25
+ ### Infrastructure
26
+
27
+ - **Integration test for compare → stage pipeline** — `tests/integration/capture-compare-pipeline.test.js` exercises the full `generateDriftReport → stageArtifacts` data flow with synthetic PNG fixtures (no live browser needed), catching interface mismatches between modules that unit tests alone would miss.
28
+
29
+ ### Features
30
+
31
+ - **Configurable PR comment truncation limits** — `actions/pr-diff` and `actions/comment` now accept optional `max-changed-rows` (default: 20) and `max-error-rows` (default: 10) inputs, letting teams control how many rows appear in the drift and error tables before the overflow note.
32
+ - **Per-route navigation timeout** — routes now accept an optional `navigationTimeout` (positive integer, ms) that overrides the global 30 000 ms default for that route. Useful for slow SSR pages that need more time or fast static pages that should fail fast.
33
+ - **Baseline refresh workflow template** — `docs/workflow-templates/refresh-baseline-on-merge.yml` is a drop-in workflow template for consumer repos that automatically republishes the SnapDrift baseline on every push to the default branch. Supports an optional label gate to avoid republishing on every push.
34
+
35
+ ### Fixes
36
+
37
+ - **Baseline refresh template trigger** — switched from `pull_request: closed` to `push` on the default branch so published artifacts are actually discoverable by the baseline resolver in `actions/pr-diff`, which hardcodes `event:'push'` when querying workflow runs.
38
+ - **Artifact action pins** — `actions/upload-artifact` and `actions/download-artifact` in composite action YAMLs are now pinned to v7 SHAs, matching the CI workflow (the v0.2.0 changelog noted this upgrade but only the CI workflow was updated at the time).
39
+ - **Stale version references** — README and integration guide examples now reference `@v0.2.1` instead of the outdated `@v0.1.0`. README "Current constraints" updated to reflect custom viewport support.
4
40
 
5
41
  ## 0.2.0 - 2026-04-08
6
42
 
package/README.md CHANGED
@@ -44,7 +44,7 @@ You keep ownership of checkout, build, startup, readiness, and teardown. SnapDri
44
44
 
45
45
  ```yaml
46
46
  - name: SnapDrift Baseline
47
- uses: ranacseruet/snapdrift/actions/baseline@v0.1.0
47
+ uses: ranacseruet/snapdrift/actions/baseline@v0.2.1
48
48
  with:
49
49
  repo-config-path: .github/snapdrift.json
50
50
  ```
@@ -53,7 +53,7 @@ You keep ownership of checkout, build, startup, readiness, and teardown. SnapDri
53
53
 
54
54
  ```yaml
55
55
  - name: SnapDrift Report
56
- uses: ranacseruet/snapdrift/actions/pr-diff@v0.1.0
56
+ uses: ranacseruet/snapdrift/actions/pr-diff@v0.2.1
57
57
  with:
58
58
  github-token: ${{ secrets.GITHUB_TOKEN }}
59
59
  repo-config-path: .github/snapdrift.json
@@ -90,7 +90,7 @@ Start with `report-only` while baselines settle. Move to `fail-on-changes` or st
90
90
 
91
91
  - Ubuntu runners only
92
92
  - Full-page capture only
93
- - Fixed viewport presets: `desktop` (1440×900) and `mobile` (390×844)
93
+ - Viewport presets: `desktop` (1440×900) and `mobile` (390×844), or custom `{ "width": number, "height": number }`
94
94
  - One global `diff.threshold`
95
95
  - Dimension shifts are reported separately from pixel drift
96
96
 
@@ -134,7 +134,7 @@ runs:
134
134
  EOF
135
135
 
136
136
  - if: inputs.upload-artifact == 'true'
137
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
137
+ uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
138
138
  with:
139
139
  name: ${{ steps.config.outputs.artifact_name }}
140
140
  retention-days: ${{ inputs.artifact-retention-days }}
@@ -20,6 +20,14 @@ inputs:
20
20
  description: Artifact label to include in the comment.
21
21
  required: false
22
22
  default: ''
23
+ max-changed-rows:
24
+ description: Maximum number of changed-route rows shown in the PR comment before truncation.
25
+ required: false
26
+ default: '20'
27
+ max-error-rows:
28
+ description: Maximum number of error rows shown in the PR comment before truncation.
29
+ required: false
30
+ default: '10'
23
31
 
24
32
  runs:
25
33
  using: composite
@@ -34,6 +42,8 @@ runs:
34
42
  VISUAL_DIFF_RUN_URL: ${{ inputs.run-url }}
35
43
  VISUAL_DIFF_ARTIFACT_NAME: ${{ inputs.artifact-name }}
36
44
  INPUT_PR_NUMBER: ${{ inputs.pr-number }}
45
+ MAX_CHANGED_ROWS: ${{ inputs.max-changed-rows }}
46
+ MAX_ERROR_ROWS: ${{ inputs.max-error-rows }}
37
47
  with:
38
48
  github-token: ${{ inputs.github-token }}
39
49
  script: |
@@ -59,9 +69,13 @@ runs:
59
69
  const summary = JSON.parse(fs.readFileSync(summaryPath, 'utf8'));
60
70
  const runUrl = process.env.VISUAL_DIFF_RUN_URL || `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
61
71
 
72
+ const maxChangedRows = Number(process.env.MAX_CHANGED_ROWS) || 20;
73
+ const maxErrorRows = Number(process.env.MAX_ERROR_ROWS) || 10;
62
74
  const body = buildReportCommentBody(summary, {
63
75
  artifactName: process.env.VISUAL_DIFF_ARTIFACT_NAME || '',
64
- runUrl
76
+ runUrl,
77
+ maxChangedRows,
78
+ maxErrorRows
65
79
  });
66
80
 
67
81
  const comments = await github.paginate(github.rest.issues.listComments, {
@@ -45,6 +45,14 @@ inputs:
45
45
  description: Whether to publish the SnapDrift PR report.
46
46
  required: false
47
47
  default: 'true'
48
+ max-changed-rows:
49
+ description: Maximum number of changed-route rows shown in the PR comment before truncation.
50
+ required: false
51
+ default: '20'
52
+ max-error-rows:
53
+ description: Maximum number of error rows shown in the PR comment before truncation.
54
+ required: false
55
+ default: '10'
48
56
 
49
57
  outputs:
50
58
  should-run:
@@ -254,7 +262,7 @@ runs:
254
262
  }
255
263
 
256
264
  - if: steps.scope.outputs.should_run == 'true' && steps.baseline.outputs.found == 'true'
257
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
265
+ uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
258
266
  with:
259
267
  github-token: ${{ inputs.github-token }}
260
268
  repository: ${{ inputs.baseline-repository || github.repository }}
@@ -467,7 +475,7 @@ runs:
467
475
  await fs.appendFile(process.env.GITHUB_OUTPUT, `bundle_dir=${result.bundleDir}\n`);
468
476
  EOF
469
477
 
470
- - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
478
+ - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
471
479
  with:
472
480
  name: ${{ steps.context.outputs.artifact_name }}
473
481
  retention-days: ${{ inputs.artifact-retention-days }}
@@ -490,6 +498,8 @@ runs:
490
498
  SUMMARY_PATH: ${{ steps.compare.outputs.summary_path || steps.skipped_baseline.outputs.summary_path || steps.skipped_scope.outputs.summary_path }}
491
499
  ARTIFACT_NAME: ${{ steps.context.outputs.artifact_name }}
492
500
  RUN_URL: ${{ steps.context.outputs.run_url }}
501
+ MAX_CHANGED_ROWS: ${{ inputs.max-changed-rows }}
502
+ MAX_ERROR_ROWS: ${{ inputs.max-error-rows }}
493
503
  with:
494
504
  github-token: ${{ inputs.github-token }}
495
505
  script: |
@@ -525,9 +535,13 @@ runs:
525
535
  const { buildReportCommentBody } = await import(pathToFileURL(modulePath));
526
536
 
527
537
  const summary = JSON.parse(fs.readFileSync(summaryPath, 'utf8'));
538
+ const maxChangedRows = Number(process.env.MAX_CHANGED_ROWS) || 20;
539
+ const maxErrorRows = Number(process.env.MAX_ERROR_ROWS) || 10;
528
540
  const body = buildReportCommentBody(summary, {
529
541
  artifactName: process.env.ARTIFACT_NAME || '',
530
- runUrl: process.env.RUN_URL || ''
542
+ runUrl: process.env.RUN_URL || '',
543
+ maxChangedRows,
544
+ maxErrorRows
531
545
  });
532
546
 
533
547
  const comments = await github.paginate(github.rest.issues.listComments, {
@@ -135,7 +135,7 @@ runs:
135
135
  }
136
136
 
137
137
  - if: steps.resolve.outputs.found == 'true'
138
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
138
+ uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
139
139
  with:
140
140
  github-token: ${{ inputs.github-token }}
141
141
  repository: ${{ inputs.repository || github.repository }}
package/docs/contracts.md CHANGED
@@ -26,6 +26,7 @@ SnapDrift reads runtime behavior from `.github/snapdrift.json` by default.
26
26
  | `path` | `string` | URL path appended to `baseUrl` |
27
27
  | `viewport` | `string` or `object` | Preset name (`"desktop"`, `"mobile"`) or a custom object `{ "width": number, "height": number }` |
28
28
  | `changePaths` | `string[]` | Optional prefixes used for changed-file scoping |
29
+ | `navigationTimeout` | `number` | Optional per-route navigation timeout in ms (overrides the 30 000 ms global default) |
29
30
 
30
31
  ### Optional fields
31
32
 
@@ -179,9 +180,11 @@ Additional missing-baseline fields: `baselineAvailable`, `currentResultsPath`.
179
180
 
180
181
  | Setting | Value |
181
182
  |:--------|:------|
182
- | Navigation wait | `networkidle` |
183
+ | Navigation wait | `load` |
183
184
  | Navigation timeout | 30000ms |
184
185
  | Settle delay | 300ms |
186
+ | Screenshot animations | `disabled` (Playwright finishes/cancels CSS animations before capture) |
187
+ | Capture concurrency | 5 routes per viewport (overridable via `SNAPDRIFT_CAPTURE_CONCURRENCY`)|
185
188
 
186
189
  ## Local CLI directory layout
187
190
 
@@ -231,3 +234,4 @@ These are for custom orchestration only. Wrapper actions set them automatically.
231
234
  | `SNAPDRIFT_BASELINE_ARTIFACT_NAME` | `compare-results.mjs` | Baseline artifact label to embed in the report |
232
235
  | `SNAPDRIFT_BASELINE_SOURCE_SHA` | `compare-results.mjs` | Baseline source SHA to embed in the report |
233
236
  | `SNAPDRIFT_ENFORCE_OUTCOME` | `compare-results.mjs` | Set to `0` to disable enforcement in direct CLI usage |
237
+ | `SNAPDRIFT_CAPTURE_CONCURRENCY` | `capture-routes.mjs` | Max concurrent route captures per viewport context (positive integer, default `5`). Set to `1` to restore serial behaviour for apps with shared session/auth state. |
@@ -46,7 +46,7 @@ SnapDrift owns route selection, capture, comparison, skipped-report generation,
46
46
 
47
47
  ```yaml
48
48
  - name: SnapDrift Baseline
49
- uses: ranacseruet/snapdrift/actions/baseline@v0.1.0
49
+ uses: ranacseruet/snapdrift/actions/baseline@v0.2.1
50
50
  with:
51
51
  repo-config-path: .github/snapdrift.json
52
52
  artifact-retention-days: '30'
@@ -70,7 +70,7 @@ Then add SnapDrift after the app is running:
70
70
 
71
71
  ```yaml
72
72
  - name: SnapDrift Report
73
- uses: ranacseruet/snapdrift/actions/pr-diff@v0.1.0
73
+ uses: ranacseruet/snapdrift/actions/pr-diff@v0.2.1
74
74
  with:
75
75
  github-token: ${{ secrets.GITHUB_TOKEN }}
76
76
  repo-config-path: .github/snapdrift.json
@@ -111,7 +111,7 @@ jobs:
111
111
  done
112
112
 
113
113
  - name: SnapDrift Report
114
- uses: ranacseruet/snapdrift/actions/pr-diff@v0.1.0
114
+ uses: ranacseruet/snapdrift/actions/pr-diff@v0.2.1
115
115
  with:
116
116
  github-token: ${{ secrets.GITHUB_TOKEN }}
117
117
  repo-config-path: .github/snapdrift.json
@@ -150,7 +150,7 @@ jobs:
150
150
  done
151
151
 
152
152
  - name: SnapDrift Report
153
- uses: ranacseruet/snapdrift/actions/pr-diff@v0.1.0
153
+ uses: ranacseruet/snapdrift/actions/pr-diff@v0.2.1
154
154
  with:
155
155
  github-token: ${{ secrets.GITHUB_TOKEN }}
156
156
  repo-config-path: .github/snapdrift.json
@@ -178,6 +178,8 @@ jobs:
178
178
  | `baseline-workflow-id` | Override the baseline workflow id |
179
179
  | `baseline-branch` | Override the baseline branch |
180
180
  | `comment-on-pr` | Set to `false` to suppress the PR report |
181
+ | `max-changed-rows` | Max changed-route rows shown before truncation (default: `20`) |
182
+ | `max-error-rows` | Max error rows shown before truncation (default: `10`) |
181
183
 
182
184
  ## Low-level actions
183
185
 
@@ -207,6 +209,20 @@ The CLI reads the same `.github/snapdrift.json` config used by the Actions workf
207
209
 
208
210
  See the [Local CLI guide](local-cli.md) for full command reference, flags, directory layout, and examples.
209
211
 
212
+ ## Refresh the baseline automatically
213
+
214
+ After an intentional layout change or dimension shift merges, the baseline must be republished before SnapDrift can compare like-for-like frames again. Without automation, this is a manual step.
215
+
216
+ Use the provided workflow template to refresh the baseline automatically on every push to your default branch (i.e. every merge):
217
+
218
+ **`docs/workflow-templates/refresh-baseline-on-merge.yml`**
219
+
220
+ Drop a copy into your repo at `.github/workflows/snapdrift-refresh-baseline.yml`, then fill in the `TODO` blocks with your app's build and start steps — the same steps you use in your PR workflow. The template uses a `push` trigger on the default branch so published artifacts are discoverable by the baseline resolver in `actions/pr-diff`.
221
+
222
+ ### Label-gated refreshes
223
+
224
+ If you don't want to republish the baseline on every push, the template includes a commented `if` condition that gates on a label (e.g. `snapdrift:refresh-baseline`). A preceding job can check the most recent merged PR for the label and set an output to control whether the baseline refresh runs.
225
+
210
226
  ## Troubleshooting
211
227
 
212
228
  **"No non-expired SnapDrift baseline artifact was found"**
@@ -0,0 +1,82 @@
1
+ # SnapDrift — Baseline Refresh on Merge
2
+ #
3
+ # Drop this file into your consumer repo at:
4
+ # .github/workflows/snapdrift-refresh-baseline.yml
5
+ #
6
+ # What it does:
7
+ # Automatically re-publishes the SnapDrift baseline on every push to your
8
+ # default branch. Since merging a PR produces a push event, this keeps the
9
+ # baseline in sync after intentional layout changes, dimension shifts, or
10
+ # any other UI refactor lands.
11
+ #
12
+ # Prerequisites:
13
+ # - A working `.github/snapdrift.json` in your repo
14
+ # - The same build + start steps used in your PR workflow
15
+ #
16
+ # Uses a push trigger so published artifacts are discoverable by the baseline
17
+ # resolver in actions/pr-diff (which filters on event: 'push'). A merge into
18
+ # the default branch produces a push event, so this fires exactly when needed.
19
+ #
20
+ # Optional — refresh only on a specific label:
21
+ # If you don't want to refresh on every push, you can gate on a label applied
22
+ # before merging. Use the pull_request: labeled event in a separate workflow to
23
+ # set an output, or simply restrict this workflow to specific paths.
24
+ # See the commented block below for a label-based gate using a preceding job.
25
+
26
+ name: SnapDrift Refresh Baseline
27
+
28
+ on:
29
+ push:
30
+ branches: [main]
31
+
32
+ permissions:
33
+ contents: read
34
+ actions: write # required to upload the baseline artifact
35
+
36
+ jobs:
37
+ refresh-baseline:
38
+ # Optional: gate on a label to avoid refreshing on every push.
39
+ # Requires a preceding job that checks for the label on the most recent
40
+ # merged PR and sets an output. Replace "snapdrift:refresh-baseline" with
41
+ # your label name.
42
+ # if: needs.check-label.outputs.refresh == 'true'
43
+
44
+ runs-on: ubuntu-latest
45
+
46
+ steps:
47
+ - uses: actions/checkout@v4
48
+
49
+ # ── TODO: add your app's build + dependency steps here ──────────────────
50
+ # Example for a Node app:
51
+ #
52
+ # - uses: actions/setup-node@v4
53
+ # with:
54
+ # node-version: '22'
55
+ # cache: npm
56
+ # - run: npm ci
57
+ # - run: npm run build
58
+ # ────────────────────────────────────────────────────────────────────────
59
+
60
+ # ── TODO: start your app and wait for it to be ready ────────────────────
61
+ # SnapDrift needs a running server to capture screenshots.
62
+ # Replace the URL and port to match your app's baseUrl from snapdrift.json.
63
+ #
64
+ # - name: Start app
65
+ # run: |
66
+ # npm start &
67
+ # for i in $(seq 1 45); do
68
+ # curl -sf http://127.0.0.1:8080 && break || sleep 1
69
+ # done
70
+ # ────────────────────────────────────────────────────────────────────────
71
+
72
+ - name: Publish SnapDrift baseline
73
+ uses: ranacseruet/snapdrift/actions/baseline@v0.2.1
74
+ with:
75
+ repo-config-path: .github/snapdrift.json
76
+ artifact-retention-days: '30'
77
+
78
+ # ── TODO: shut down your app if needed ───────────────────────────────────
79
+ # - name: Stop app
80
+ # if: always()
81
+ # run: pkill -f "npm start" || true
82
+ # ────────────────────────────────────────────────────────────────────────