snapdrift 0.2.1 → 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 +19 -1
- package/docs/contracts.md +4 -1
- package/lib/capture-routes.mjs +2 -368
- package/lib/cli.mjs +8 -9
- package/lib/compare-results.mjs +21 -642
- package/lib/drift-summary.mjs +3 -154
- package/lib/pr-comment.mjs +2 -147
- package/lib/report.mjs +17 -246
- package/lib/snapdrift-config.mjs +20 -284
- package/lib/stage-artifacts.mjs +2 -123
- package/package.json +10 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
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.
|
|
4
22
|
|
|
5
23
|
## 0.2.1 - 2026-04-13
|
|
6
24
|
|
package/docs/contracts.md
CHANGED
|
@@ -180,9 +180,11 @@ Additional missing-baseline fields: `baselineAvailable`, `currentResultsPath`.
|
|
|
180
180
|
|
|
181
181
|
| Setting | Value |
|
|
182
182
|
|:--------|:------|
|
|
183
|
-
| Navigation wait | `
|
|
183
|
+
| Navigation wait | `load` |
|
|
184
184
|
| Navigation timeout | 30000ms |
|
|
185
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`)|
|
|
186
188
|
|
|
187
189
|
## Local CLI directory layout
|
|
188
190
|
|
|
@@ -232,3 +234,4 @@ These are for custom orchestration only. Wrapper actions set them automatically.
|
|
|
232
234
|
| `SNAPDRIFT_BASELINE_ARTIFACT_NAME` | `compare-results.mjs` | Baseline artifact label to embed in the report |
|
|
233
235
|
| `SNAPDRIFT_BASELINE_SOURCE_SHA` | `compare-results.mjs` | Baseline source SHA to embed in the report |
|
|
234
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. |
|
package/lib/capture-routes.mjs
CHANGED
|
@@ -1,370 +1,4 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
+
// Re-exports from packages — this module is a thin shim.
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { pathToFileURL } from 'node:url';
|
|
6
|
-
import { chromium } from 'playwright';
|
|
7
|
-
import pngjs from 'pngjs';
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
loadSnapdriftConfig,
|
|
11
|
-
readFirstDefinedEnv,
|
|
12
|
-
resolveFromWorkingDirectory,
|
|
13
|
-
selectConfiguredRoutes,
|
|
14
|
-
splitCommaList,
|
|
15
|
-
SNAPDRIFT_VIEWPORT_PRESETS,
|
|
16
|
-
SNAPDRIFT_NAVIGATION_TIMEOUT_MS,
|
|
17
|
-
SNAPDRIFT_SETTLE_DELAY_MS
|
|
18
|
-
} from './snapdrift-config.mjs';
|
|
19
|
-
|
|
20
|
-
const { PNG } = pngjs;
|
|
21
|
-
|
|
22
|
-
/** @typedef {import('../types/visual-diff-types').VisualBaselineResults} BaselineResults */
|
|
23
|
-
/** @typedef {import('../types/visual-diff-types').VisualBaselineRouteResult} BaselineRouteResult */
|
|
24
|
-
/** @typedef {import('../types/visual-diff-types').VisualRegressionRouteConfig} SnapdriftRouteConfig */
|
|
25
|
-
/** @typedef {import('../types/visual-diff-types').VisualScreenshotManifest} ScreenshotManifest */
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* @param {string} targetPath
|
|
29
|
-
* @returns {Promise<void>}
|
|
30
|
-
*/
|
|
31
|
-
async function ensureParentDirectory(targetPath) {
|
|
32
|
-
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* @param {string} targetPath
|
|
37
|
-
* @returns {Promise<void>}
|
|
38
|
-
*/
|
|
39
|
-
async function ensureDirectory(targetPath) {
|
|
40
|
-
await fs.mkdir(targetPath, { recursive: true });
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Strips characters that could cause path traversal or produce invalid filenames.
|
|
45
|
-
* @param {string} id
|
|
46
|
-
* @returns {string}
|
|
47
|
-
*/
|
|
48
|
-
function sanitizeRouteId(id) {
|
|
49
|
-
return id
|
|
50
|
-
.replace(/\.\./g, '_')
|
|
51
|
-
.replace(/[/\\]/g, '_')
|
|
52
|
-
// eslint-disable-next-line no-control-regex
|
|
53
|
-
.replace(/[\x00-\x1f\x7f]/g, '');
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** @typedef {import('../types/visual-diff-types').VisualViewport} VisualViewport */
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Returns a stable string key for a viewport value (preset name or custom dimensions).
|
|
60
|
-
* @param {VisualViewport} viewport
|
|
61
|
-
* @returns {string}
|
|
62
|
-
*/
|
|
63
|
-
function viewportKey(viewport) {
|
|
64
|
-
return typeof viewport === 'string' ? viewport : `custom:${viewport.width}x${viewport.height}`;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Returns a human-readable label for a viewport.
|
|
69
|
-
* @param {VisualViewport} viewport
|
|
70
|
-
* @returns {string}
|
|
71
|
-
*/
|
|
72
|
-
function viewportLabel(viewport) {
|
|
73
|
-
return typeof viewport === 'string' ? viewport : `${viewport.width}x${viewport.height}`;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Returns Playwright context options for a viewport.
|
|
78
|
-
* @param {VisualViewport} viewport
|
|
79
|
-
* @returns {import('playwright').BrowserContextOptions}
|
|
80
|
-
*/
|
|
81
|
-
function viewportContextOptions(viewport) {
|
|
82
|
-
if (typeof viewport === 'string') {
|
|
83
|
-
const preset = SNAPDRIFT_VIEWPORT_PRESETS[viewport];
|
|
84
|
-
return {
|
|
85
|
-
viewport: { width: preset.width, height: preset.height },
|
|
86
|
-
deviceScaleFactor: preset.deviceScaleFactor,
|
|
87
|
-
isMobile: preset.isMobile,
|
|
88
|
-
hasTouch: preset.hasTouch
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
return {
|
|
92
|
-
viewport: { width: viewport.width, height: viewport.height },
|
|
93
|
-
deviceScaleFactor: 1,
|
|
94
|
-
isMobile: false,
|
|
95
|
-
hasTouch: false
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* @param {import('playwright').Browser} browser
|
|
101
|
-
* @returns {Promise<Map<'desktop' | 'mobile', import('playwright').BrowserContext>>}
|
|
102
|
-
*/
|
|
103
|
-
async function createViewportContexts(browser) {
|
|
104
|
-
return new Map([
|
|
105
|
-
['desktop', await browser.newContext({
|
|
106
|
-
viewport: { width: SNAPDRIFT_VIEWPORT_PRESETS.desktop.width, height: SNAPDRIFT_VIEWPORT_PRESETS.desktop.height },
|
|
107
|
-
deviceScaleFactor: SNAPDRIFT_VIEWPORT_PRESETS.desktop.deviceScaleFactor,
|
|
108
|
-
isMobile: SNAPDRIFT_VIEWPORT_PRESETS.desktop.isMobile,
|
|
109
|
-
hasTouch: SNAPDRIFT_VIEWPORT_PRESETS.desktop.hasTouch
|
|
110
|
-
})],
|
|
111
|
-
['mobile', await browser.newContext({
|
|
112
|
-
viewport: { width: SNAPDRIFT_VIEWPORT_PRESETS.mobile.width, height: SNAPDRIFT_VIEWPORT_PRESETS.mobile.height },
|
|
113
|
-
deviceScaleFactor: SNAPDRIFT_VIEWPORT_PRESETS.mobile.deviceScaleFactor,
|
|
114
|
-
isMobile: SNAPDRIFT_VIEWPORT_PRESETS.mobile.isMobile,
|
|
115
|
-
hasTouch: SNAPDRIFT_VIEWPORT_PRESETS.mobile.hasTouch
|
|
116
|
-
})]
|
|
117
|
-
]);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* @param {import('playwright').BrowserContext} context
|
|
122
|
-
* @param {SnapdriftRouteConfig} route
|
|
123
|
-
* @param {string} baseUrl
|
|
124
|
-
* @param {string} screenshotsRoot
|
|
125
|
-
* @returns {Promise<BaselineRouteResult & { manifestEntry?: ScreenshotManifest['screenshots'][number] }>}
|
|
126
|
-
*/
|
|
127
|
-
async function captureRoute(context, route, baseUrl, screenshotsRoot) {
|
|
128
|
-
const startedAt = Date.now();
|
|
129
|
-
|
|
130
|
-
const page = await context.newPage();
|
|
131
|
-
try {
|
|
132
|
-
const targetUrl = new URL(route.path, baseUrl).toString();
|
|
133
|
-
await page.goto(targetUrl, {
|
|
134
|
-
waitUntil: 'networkidle',
|
|
135
|
-
timeout: route.navigationTimeout ?? SNAPDRIFT_NAVIGATION_TIMEOUT_MS
|
|
136
|
-
});
|
|
137
|
-
await page.waitForTimeout(SNAPDRIFT_SETTLE_DELAY_MS);
|
|
138
|
-
|
|
139
|
-
const imagePath = path.join('screenshots', `${sanitizeRouteId(route.id)}.png`);
|
|
140
|
-
const absoluteImagePath = path.join(screenshotsRoot, imagePath);
|
|
141
|
-
await ensureParentDirectory(absoluteImagePath);
|
|
142
|
-
const screenshotBuffer = await page.screenshot({
|
|
143
|
-
path: absoluteImagePath,
|
|
144
|
-
fullPage: true
|
|
145
|
-
});
|
|
146
|
-
const screenshot = PNG.sync.read(screenshotBuffer);
|
|
147
|
-
|
|
148
|
-
return {
|
|
149
|
-
id: route.id,
|
|
150
|
-
path: route.path,
|
|
151
|
-
viewport: route.viewport,
|
|
152
|
-
status: 'passed',
|
|
153
|
-
durationMs: Date.now() - startedAt,
|
|
154
|
-
imagePath,
|
|
155
|
-
width: screenshot.width,
|
|
156
|
-
height: screenshot.height,
|
|
157
|
-
manifestEntry: {
|
|
158
|
-
id: route.id,
|
|
159
|
-
path: route.path,
|
|
160
|
-
viewport: route.viewport,
|
|
161
|
-
imagePath,
|
|
162
|
-
width: screenshot.width,
|
|
163
|
-
height: screenshot.height
|
|
164
|
-
}
|
|
165
|
-
};
|
|
166
|
-
} catch (error) {
|
|
167
|
-
return {
|
|
168
|
-
id: route.id,
|
|
169
|
-
path: route.path,
|
|
170
|
-
viewport: route.viewport,
|
|
171
|
-
status: 'failed',
|
|
172
|
-
durationMs: Date.now() - startedAt,
|
|
173
|
-
error: error instanceof Error ? error.message : String(error)
|
|
174
|
-
};
|
|
175
|
-
} finally {
|
|
176
|
-
await page.close();
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const CAPTURE_MAX_RETRIES = 1;
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* @param {import('playwright').BrowserContext} context
|
|
184
|
-
* @param {SnapdriftRouteConfig} route
|
|
185
|
-
* @param {string} baseUrl
|
|
186
|
-
* @param {string} screenshotsRoot
|
|
187
|
-
* @returns {Promise<BaselineRouteResult & { manifestEntry?: ScreenshotManifest['screenshots'][number] }>}
|
|
188
|
-
*/
|
|
189
|
-
async function captureRouteWithRetry(context, route, baseUrl, screenshotsRoot) {
|
|
190
|
-
let result = await captureRoute(context, route, baseUrl, screenshotsRoot);
|
|
191
|
-
for (let attempt = 1; attempt <= CAPTURE_MAX_RETRIES && result.status !== 'passed'; attempt++) {
|
|
192
|
-
console.log(`[SnapDrift] Retrying route ${route.id} (attempt ${attempt + 1}/${CAPTURE_MAX_RETRIES + 1})...`);
|
|
193
|
-
result = await captureRoute(context, route, baseUrl, screenshotsRoot);
|
|
194
|
-
}
|
|
195
|
-
return result;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Captures all routes for a single viewport context sequentially, logging progress.
|
|
200
|
-
*
|
|
201
|
-
* @param {import('playwright').BrowserContext} context
|
|
202
|
-
* @param {Array<{ route: SnapdriftRouteConfig, originalIndex: number }>} entries
|
|
203
|
-
* @param {string} baseUrl
|
|
204
|
-
* @param {string} screenshotsRoot
|
|
205
|
-
* @param {number} totalRoutes
|
|
206
|
-
* @param {Array<BaselineRouteResult & { manifestEntry?: ScreenshotManifest['screenshots'][number] }>} out
|
|
207
|
-
* @returns {Promise<void>}
|
|
208
|
-
*/
|
|
209
|
-
async function captureViewportRoutes(context, entries, baseUrl, screenshotsRoot, totalRoutes, out) {
|
|
210
|
-
for (const { route, originalIndex } of entries) {
|
|
211
|
-
console.log(`[SnapDrift] Capturing route ${originalIndex + 1}/${totalRoutes}: ${route.id} (${viewportLabel(route.viewport)})`);
|
|
212
|
-
out[originalIndex] = await captureRouteWithRetry(context, route, baseUrl, screenshotsRoot);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* @param {{
|
|
218
|
-
* configPath?: string,
|
|
219
|
-
* routeIds?: Iterable<string>,
|
|
220
|
-
* outDir?: string
|
|
221
|
-
* }} [options]
|
|
222
|
-
* @returns {Promise<{ resultsPath: string, manifestPath: string, screenshotsRoot: string, selectedRouteIds: string[] }>}
|
|
223
|
-
*/
|
|
224
|
-
export async function runBaselineCapture(options = {}) {
|
|
225
|
-
const requestedRouteIds = [...(
|
|
226
|
-
options.routeIds || splitCommaList(readFirstDefinedEnv(['SNAPDRIFT_ROUTE_IDS']))
|
|
227
|
-
)];
|
|
228
|
-
const { config, configPath } = await loadSnapdriftConfig(options.configPath);
|
|
229
|
-
const { routes, selectedRouteIds } = selectConfiguredRoutes(config, requestedRouteIds);
|
|
230
|
-
|
|
231
|
-
// When outDir is provided (e.g. by the local CLI), store all outputs flat inside that directory
|
|
232
|
-
// using just the basename of each configured path. Otherwise, use the config-resolved paths.
|
|
233
|
-
const localOutDir = options.outDir ? path.resolve(options.outDir) : null;
|
|
234
|
-
const resultsPath = localOutDir
|
|
235
|
-
? path.join(localOutDir, path.basename(config.resultsFile))
|
|
236
|
-
: resolveFromWorkingDirectory(config, config.resultsFile);
|
|
237
|
-
const manifestPath = localOutDir
|
|
238
|
-
? path.join(localOutDir, path.basename(config.manifestFile))
|
|
239
|
-
: resolveFromWorkingDirectory(config, config.manifestFile);
|
|
240
|
-
const screenshotsRoot = localOutDir || resolveFromWorkingDirectory(config, config.screenshotsRoot);
|
|
241
|
-
|
|
242
|
-
// Screenshots are written to a `screenshots/` subdirectory inside screenshotsRoot,
|
|
243
|
-
// i.e. the actual PNG files land at `{screenshotsRoot}/screenshots/{id}.png`.
|
|
244
|
-
await Promise.all([
|
|
245
|
-
ensureParentDirectory(resultsPath),
|
|
246
|
-
ensureParentDirectory(manifestPath),
|
|
247
|
-
ensureDirectory(path.join(screenshotsRoot, 'screenshots'))
|
|
248
|
-
]);
|
|
249
|
-
|
|
250
|
-
/** @type {BaselineResults} */
|
|
251
|
-
const results = {
|
|
252
|
-
startedAt: new Date().toISOString(),
|
|
253
|
-
baseUrl: config.baseUrl,
|
|
254
|
-
suite: 'snapdrift-capture',
|
|
255
|
-
configPath: path.relative(path.resolve('.'), configPath),
|
|
256
|
-
manifestPath: path.relative(path.resolve('.'), manifestPath),
|
|
257
|
-
screenshotsRoot: path.relative(path.resolve('.'), screenshotsRoot),
|
|
258
|
-
routes: []
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
/** @type {ScreenshotManifest} */
|
|
262
|
-
const manifest = {
|
|
263
|
-
generatedAt: new Date().toISOString(),
|
|
264
|
-
baseUrl: config.baseUrl,
|
|
265
|
-
screenshots: []
|
|
266
|
-
};
|
|
267
|
-
|
|
268
|
-
const browser = await chromium.launch({ headless: true });
|
|
269
|
-
// Pre-create contexts for the two standard presets; custom viewports are created on demand below.
|
|
270
|
-
const presetContexts = await createViewportContexts(browser);
|
|
271
|
-
/** @type {Map<string, import('playwright').BrowserContext>} */
|
|
272
|
-
const allContexts = new Map(presetContexts);
|
|
273
|
-
let failures = 0;
|
|
274
|
-
|
|
275
|
-
try {
|
|
276
|
-
// Group routes by viewport key, preserving each route's original index for result ordering.
|
|
277
|
-
/** @type {Map<string, Array<{ route: SnapdriftRouteConfig, originalIndex: number }>>} */
|
|
278
|
-
const byViewport = new Map();
|
|
279
|
-
for (const [i, route] of routes.entries()) {
|
|
280
|
-
const key = viewportKey(route.viewport);
|
|
281
|
-
const existing = byViewport.get(key);
|
|
282
|
-
if (existing) {
|
|
283
|
-
existing.push({ route, originalIndex: i });
|
|
284
|
-
} else {
|
|
285
|
-
byViewport.set(key, [{ route, originalIndex: i }]);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Create browser contexts for any custom viewports not covered by presets.
|
|
290
|
-
for (const [key, entries] of byViewport) {
|
|
291
|
-
if (!allContexts.has(key)) {
|
|
292
|
-
try {
|
|
293
|
-
allContexts.set(key, await browser.newContext(viewportContextOptions(entries[0].route.viewport)));
|
|
294
|
-
} catch (err) {
|
|
295
|
-
const message = `Failed to create browser context: ${err instanceof Error ? err.message : String(err)}`;
|
|
296
|
-
for (const { route } of entries) {
|
|
297
|
-
results.routes.push({ id: route.id, path: route.path, viewport: route.viewport, status: 'failed', durationMs: 0, error: message });
|
|
298
|
-
failures += 1;
|
|
299
|
-
}
|
|
300
|
-
throw err;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// Pre-allocate results array; captureViewportRoutes fills slots by originalIndex.
|
|
306
|
-
/** @type {Array<BaselineRouteResult & { manifestEntry?: ScreenshotManifest['screenshots'][number] }>} */
|
|
307
|
-
const captureResults = new Array(routes.length);
|
|
308
|
-
|
|
309
|
-
// Run each viewport group concurrently; routes within a group stay sequential.
|
|
310
|
-
await Promise.all([...byViewport.entries()].map(([key, entries]) => {
|
|
311
|
-
const context = allContexts.get(key);
|
|
312
|
-
if (!context) {
|
|
313
|
-
throw new Error(`No browser context for viewport: ${key}`);
|
|
314
|
-
}
|
|
315
|
-
return captureViewportRoutes(context, entries, config.baseUrl, screenshotsRoot, routes.length, captureResults);
|
|
316
|
-
}));
|
|
317
|
-
|
|
318
|
-
// Merge results in original route order.
|
|
319
|
-
for (const capture of captureResults) {
|
|
320
|
-
results.routes.push({
|
|
321
|
-
id: capture.id,
|
|
322
|
-
path: capture.path,
|
|
323
|
-
viewport: capture.viewport,
|
|
324
|
-
status: capture.status,
|
|
325
|
-
durationMs: capture.durationMs,
|
|
326
|
-
imagePath: capture.imagePath,
|
|
327
|
-
width: capture.width,
|
|
328
|
-
height: capture.height,
|
|
329
|
-
error: capture.error
|
|
330
|
-
});
|
|
331
|
-
if (capture.manifestEntry) {
|
|
332
|
-
manifest.screenshots.push(capture.manifestEntry);
|
|
333
|
-
}
|
|
334
|
-
if (capture.status !== 'passed') {
|
|
335
|
-
failures += 1;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
} finally {
|
|
339
|
-
await Promise.all([...allContexts.values()].map((context) => context.close()));
|
|
340
|
-
await browser.close();
|
|
341
|
-
results.finishedAt = new Date().toISOString();
|
|
342
|
-
results.passed = failures === 0;
|
|
343
|
-
manifest.generatedAt = new Date().toISOString();
|
|
344
|
-
|
|
345
|
-
await Promise.all([
|
|
346
|
-
fs.writeFile(resultsPath, JSON.stringify(results, null, 2)),
|
|
347
|
-
fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2))
|
|
348
|
-
]);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
if (failures > 0) {
|
|
352
|
-
throw new Error(`SnapDrift capture failed for ${failures} route(s).`);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
return {
|
|
356
|
-
resultsPath,
|
|
357
|
-
manifestPath,
|
|
358
|
-
screenshotsRoot,
|
|
359
|
-
selectedRouteIds
|
|
360
|
-
};
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
const isDirectRun = process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url;
|
|
364
|
-
|
|
365
|
-
if (isDirectRun) {
|
|
366
|
-
runBaselineCapture().catch((error) => {
|
|
367
|
-
console.error(error);
|
|
368
|
-
process.exitCode = 1;
|
|
369
|
-
});
|
|
370
|
-
}
|
|
4
|
+
export { runBaselineCapture } from '@snapdrift/adapter-fs';
|
package/lib/cli.mjs
CHANGED
|
@@ -4,10 +4,9 @@ import { execSync } from 'node:child_process';
|
|
|
4
4
|
import fs from 'node:fs/promises';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
|
|
7
|
-
import { runBaselineCapture } from '
|
|
8
|
-
import {
|
|
7
|
+
import { runBaselineCapture, generateDriftReport, loadSnapdriftConfig } from '@snapdrift/adapter-fs';
|
|
8
|
+
import { shouldFailDriftCheck } from '@snapdrift/manifest';
|
|
9
9
|
import { generateHtmlReport } from './report.mjs';
|
|
10
|
-
import { loadSnapdriftConfig } from './snapdrift-config.mjs';
|
|
11
10
|
|
|
12
11
|
/** @typedef {import('../types/visual-diff-types').VisualDiffSummary} DriftSummary */
|
|
13
12
|
|
|
@@ -95,10 +94,10 @@ export function printSummary(summary) {
|
|
|
95
94
|
|
|
96
95
|
const statusIcon =
|
|
97
96
|
summary.status === 'clean'
|
|
98
|
-
? '
|
|
97
|
+
? '✅' // ✅
|
|
99
98
|
: summary.status === 'changes-detected'
|
|
100
|
-
? '
|
|
101
|
-
: '
|
|
99
|
+
? '🟡' // 🟡
|
|
100
|
+
: '❌'; // ❌
|
|
102
101
|
const statusLabel =
|
|
103
102
|
summary.status === 'clean'
|
|
104
103
|
? 'Clean'
|
|
@@ -106,7 +105,7 @@ export function printSummary(summary) {
|
|
|
106
105
|
? 'Drift detected'
|
|
107
106
|
: summary.status ?? 'Unknown';
|
|
108
107
|
|
|
109
|
-
process.stdout.write(`\n${statusIcon} SnapDrift
|
|
108
|
+
process.stdout.write(`\n${statusIcon} SnapDrift — ${statusLabel}\n`);
|
|
110
109
|
process.stdout.write(` Routes: ${total}\n`);
|
|
111
110
|
process.stdout.write(` Matched: ${matched}\n`);
|
|
112
111
|
if (changed > 0) process.stdout.write(` Changed: ${changed}\n`);
|
|
@@ -118,7 +117,7 @@ export function printSummary(summary) {
|
|
|
118
117
|
process.stdout.write('\n Changed routes:\n');
|
|
119
118
|
for (const item of summary.changed) {
|
|
120
119
|
const pct = ((item.mismatchRatio ?? 0) * 100).toFixed(2);
|
|
121
|
-
process.stdout.write(`
|
|
120
|
+
process.stdout.write(` • ${item.id} (${pct}% diff)\n`);
|
|
122
121
|
}
|
|
123
122
|
}
|
|
124
123
|
|
|
@@ -238,4 +237,4 @@ export async function main(argv) {
|
|
|
238
237
|
);
|
|
239
238
|
process.exitCode = 1;
|
|
240
239
|
}
|
|
241
|
-
}
|
|
240
|
+
}
|