snapdrift 0.2.0 → 0.2.1
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 +18 -0
- package/README.md +3 -3
- package/actions/baseline/action.yml +1 -1
- package/actions/comment/action.yml +15 -1
- package/actions/pr-diff/action.yml +17 -3
- package/actions/resolve-baseline/action.yml +1 -1
- package/docs/contracts.md +1 -0
- package/docs/integration-guide.md +20 -4
- package/docs/workflow-templates/refresh-baseline-on-merge.yml +82 -0
- package/lib/capture-routes.mjs +1 -1
- package/lib/pr-comment.mjs +9 -7
- package/lib/snapdrift-config.mjs +5 -0
- package/package.json +1 -1
- package/types/visual-diff-types.d.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.2.1 - 2026-04-13
|
|
6
|
+
|
|
7
|
+
### Infrastructure
|
|
8
|
+
|
|
9
|
+
- **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.
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
- **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.
|
|
14
|
+
- **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.
|
|
15
|
+
- **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.
|
|
16
|
+
|
|
17
|
+
### Fixes
|
|
18
|
+
|
|
19
|
+
- **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.
|
|
20
|
+
- **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).
|
|
21
|
+
- **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.
|
|
22
|
+
|
|
5
23
|
## 0.2.0 - 2026-04-08
|
|
6
24
|
|
|
7
25
|
### Features
|
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
|
|
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
|
|
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
|
-
-
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
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
|
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
+
# ────────────────────────────────────────────────────────────────────────
|
package/lib/capture-routes.mjs
CHANGED
|
@@ -132,7 +132,7 @@ async function captureRoute(context, route, baseUrl, screenshotsRoot) {
|
|
|
132
132
|
const targetUrl = new URL(route.path, baseUrl).toString();
|
|
133
133
|
await page.goto(targetUrl, {
|
|
134
134
|
waitUntil: 'networkidle',
|
|
135
|
-
timeout: SNAPDRIFT_NAVIGATION_TIMEOUT_MS
|
|
135
|
+
timeout: route.navigationTimeout ?? SNAPDRIFT_NAVIGATION_TIMEOUT_MS
|
|
136
136
|
});
|
|
137
137
|
await page.waitForTimeout(SNAPDRIFT_SETTLE_DELAY_MS);
|
|
138
138
|
|
package/lib/pr-comment.mjs
CHANGED
|
@@ -32,10 +32,12 @@ const STATUS_LABELS = {
|
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* @param {Record<string, unknown>} summary
|
|
35
|
-
* @param {{ artifactName?: string, runUrl?: string }} [meta]
|
|
35
|
+
* @param {{ artifactName?: string, runUrl?: string, maxChangedRows?: number, maxErrorRows?: number }} [meta]
|
|
36
36
|
* @returns {string}
|
|
37
37
|
*/
|
|
38
38
|
export function buildReportCommentBody(summary, meta = {}) {
|
|
39
|
+
const maxChangedRows = meta.maxChangedRows ?? 20;
|
|
40
|
+
const maxErrorRows = meta.maxErrorRows ?? 10;
|
|
39
41
|
const status = /** @type {string} */ (summary.status) || 'incomplete';
|
|
40
42
|
const statusIcon = STATUS_ICONS[status] || '⚠️';
|
|
41
43
|
const statusLabel = STATUS_LABELS[status] || status;
|
|
@@ -66,15 +68,15 @@ export function buildReportCommentBody(summary, meta = {}) {
|
|
|
66
68
|
lines.push('');
|
|
67
69
|
lines.push('| Route | Viewport | Error |');
|
|
68
70
|
lines.push('|:------|:---------|:------|');
|
|
69
|
-
for (const item of errors.slice(0,
|
|
71
|
+
for (const item of errors.slice(0, maxErrorRows)) {
|
|
70
72
|
lines.push(`| ${escapeMarkdown(item.id)} | ${escapeMarkdown(item.viewport)} | ${escapeMarkdown(item.message)} |`);
|
|
71
73
|
}
|
|
72
|
-
if (errorCount >
|
|
74
|
+
if (errorCount > maxErrorRows) {
|
|
73
75
|
lines.push('');
|
|
74
76
|
const runLinkSuffix = meta.runUrl && /^https?:\/\//.test(meta.runUrl)
|
|
75
77
|
? ` — [View full report →](${meta.runUrl})`
|
|
76
78
|
: '';
|
|
77
|
-
lines.push(`*...and ${errorCount -
|
|
79
|
+
lines.push(`*...and ${errorCount - maxErrorRows} more*${runLinkSuffix}`);
|
|
78
80
|
}
|
|
79
81
|
lines.push('');
|
|
80
82
|
lines.push('</details>');
|
|
@@ -87,18 +89,18 @@ export function buildReportCommentBody(summary, meta = {}) {
|
|
|
87
89
|
lines.push('');
|
|
88
90
|
lines.push('| Route | Viewport | Mismatch |');
|
|
89
91
|
lines.push('|:------|:---------|:---------|');
|
|
90
|
-
for (const item of changed.slice(0,
|
|
92
|
+
for (const item of changed.slice(0, maxChangedRows)) {
|
|
91
93
|
const percentChanged = typeof item.mismatchRatio === 'number'
|
|
92
94
|
? `${(item.mismatchRatio * 100).toFixed(2)}%`
|
|
93
95
|
: 'n/a';
|
|
94
96
|
lines.push(`| ${escapeMarkdown(item.id)} | ${escapeMarkdown(item.viewport)} | ${percentChanged} |`);
|
|
95
97
|
}
|
|
96
|
-
if (changed.length >
|
|
98
|
+
if (changed.length > maxChangedRows) {
|
|
97
99
|
lines.push('');
|
|
98
100
|
const runLinkSuffix = meta.runUrl && /^https?:\/\//.test(meta.runUrl)
|
|
99
101
|
? ` — [View full report →](${meta.runUrl})`
|
|
100
102
|
: '';
|
|
101
|
-
lines.push(`*...and ${changed.length -
|
|
103
|
+
lines.push(`*...and ${changed.length - maxChangedRows} more*${runLinkSuffix}`);
|
|
102
104
|
}
|
|
103
105
|
lines.push('');
|
|
104
106
|
lines.push('</details>');
|
package/lib/snapdrift-config.mjs
CHANGED
|
@@ -156,6 +156,11 @@ export function validateSnapdriftConfig(value, sourceLabel = 'inline config') {
|
|
|
156
156
|
if (route.changePaths !== undefined && !isNonEmptyStringArray(route.changePaths)) {
|
|
157
157
|
errors.push(`${routeLabel}.changePaths must be an array of non-empty strings when provided.`);
|
|
158
158
|
}
|
|
159
|
+
|
|
160
|
+
if (route.navigationTimeout !== undefined &&
|
|
161
|
+
(!Number.isInteger(route.navigationTimeout) || /** @type {number} */ (route.navigationTimeout) <= 0)) {
|
|
162
|
+
errors.push(`${routeLabel}.navigationTimeout must be a positive integer when provided.`);
|
|
163
|
+
}
|
|
159
164
|
}
|
|
160
165
|
}
|
|
161
166
|
|
package/package.json
CHANGED