snapdrift 0.12.0 → 0.14.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 +113 -0
- package/README.md +7 -3
- package/action.yml +2 -2
- package/actions/comment/action.yml +8 -37
- package/actions/pr-diff/action.yml +24 -121
- package/actions/scope/action.yml +15 -73
- package/docs/contracts.md +148 -8
- package/docs/integration-guide.md +18 -12
- package/docs/local-cli.md +67 -5
- package/docs/workflow-templates/refresh-baseline-on-merge.yml +1 -1
- package/lib/cli.mjs +4 -0
- package/lib/github-requests.mjs +134 -0
- package/lib/outage-policy.mjs +49 -14
- package/lib/provider.mjs +19 -1
- package/lib/snap-provider.mjs +80 -11
- package/package.json +6 -5
- package/types/visual-diff-types.d.ts +29 -2
package/lib/outage-policy.mjs
CHANGED
|
@@ -73,10 +73,21 @@ function createBaselineLookupFallbackError(outageError, phase, lookupMessage) {
|
|
|
73
73
|
* @returns {boolean}
|
|
74
74
|
*/
|
|
75
75
|
export function hasLocalScreenshots(providerName, config) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
return describeCaptureArtifacts(providerName, undefined, config).localScreenshots;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Prefer provider-returned artifacts; infer capabilities only for legacy results.
|
|
81
|
+
*
|
|
82
|
+
* @param {string} providerName
|
|
83
|
+
* @param {Partial<CaptureResult>} [result]
|
|
84
|
+
* @param {VisualRegressionConfig} [config]
|
|
85
|
+
* @returns {{ localScreenshots: boolean, artifactsRoot?: string }}
|
|
86
|
+
*/
|
|
87
|
+
export function describeCaptureArtifacts(providerName, result, config) {
|
|
88
|
+
if (result?.artifacts) return result.artifacts;
|
|
89
|
+
const localScreenshots = providerName !== 'snap' || isLocalBaseUrl(config?.baseUrl);
|
|
90
|
+
return { localScreenshots, artifactsRoot: localScreenshots ? result?.screenshotsRoot : undefined };
|
|
80
91
|
}
|
|
81
92
|
|
|
82
93
|
/**
|
|
@@ -97,7 +108,8 @@ export function hasLocalScreenshots(providerName, config) {
|
|
|
97
108
|
* outcome: 'captured' | 'skipped',
|
|
98
109
|
* providerName: string,
|
|
99
110
|
* result?: CaptureResult,
|
|
100
|
-
* localScreenshots: boolean
|
|
111
|
+
* localScreenshots: boolean,
|
|
112
|
+
* artifacts: ReturnType<typeof describeCaptureArtifacts>
|
|
101
113
|
* }>}
|
|
102
114
|
*/
|
|
103
115
|
export async function captureWithPolicy(options) {
|
|
@@ -108,16 +120,18 @@ export async function captureWithPolicy(options) {
|
|
|
108
120
|
|
|
109
121
|
try {
|
|
110
122
|
const result = await options.provider.capture(options.captureOptions);
|
|
123
|
+
const artifacts = describeCaptureArtifacts(providerName, result, options.config);
|
|
111
124
|
return {
|
|
112
125
|
outcome: 'captured',
|
|
113
126
|
providerName,
|
|
114
127
|
result,
|
|
115
|
-
|
|
128
|
+
artifacts,
|
|
129
|
+
localScreenshots: artifacts.localScreenshots
|
|
116
130
|
};
|
|
117
131
|
} catch (error) {
|
|
118
132
|
if (error instanceof SnapSkipError) {
|
|
119
133
|
onSkip(error);
|
|
120
|
-
return { outcome: 'skipped', providerName, localScreenshots: false };
|
|
134
|
+
return { outcome: 'skipped', providerName, localScreenshots: false, artifacts: { localScreenshots: false } };
|
|
121
135
|
}
|
|
122
136
|
if (error instanceof SnapFallbackError) {
|
|
123
137
|
onFallback(error);
|
|
@@ -127,7 +141,8 @@ export async function captureWithPolicy(options) {
|
|
|
127
141
|
const result = await createLocalProvider().capture(options.captureOptions);
|
|
128
142
|
// The rest of the pipeline must be told the *effective* provider, not the
|
|
129
143
|
// configured one — otherwise it rebuilds SnapProvider around local output.
|
|
130
|
-
|
|
144
|
+
const artifacts = describeCaptureArtifacts('local', result, options.config);
|
|
145
|
+
return { outcome: 'captured', providerName: 'local', result, localScreenshots: artifacts.localScreenshots, artifacts };
|
|
131
146
|
}
|
|
132
147
|
throw error;
|
|
133
148
|
}
|
|
@@ -147,12 +162,19 @@ export async function captureWithPolicy(options) {
|
|
|
147
162
|
* does not, so we recapture locally first rather than diffing a manifest of
|
|
148
163
|
* zero-dimension placeholders.
|
|
149
164
|
*
|
|
165
|
+
* Capability precedence: legacy `localScreenshots` overrides `artifacts`, then
|
|
166
|
+
* `captureResult.artifacts`, then provider/config inference. Without any of those
|
|
167
|
+
* inputs (including config), preserve the legacy local-screenshots default.
|
|
168
|
+
*
|
|
150
169
|
* @param {{
|
|
151
170
|
* provider: VisualProvider,
|
|
152
171
|
* providerName?: string,
|
|
153
172
|
* diffOptions: DiffOptions,
|
|
173
|
+
* captureResult?: CaptureResult,
|
|
154
174
|
* captureOptions?: CaptureOptions,
|
|
155
175
|
* localScreenshots?: boolean,
|
|
176
|
+
* artifacts?: ReturnType<typeof describeCaptureArtifacts>,
|
|
177
|
+
* config?: VisualRegressionConfig,
|
|
156
178
|
* baselineAvailable?: boolean,
|
|
157
179
|
* createLocalProvider?: () => VisualProvider,
|
|
158
180
|
* onSkip?: (error: SnapSkipError) => void,
|
|
@@ -166,11 +188,23 @@ export async function captureWithPolicy(options) {
|
|
|
166
188
|
* outcome: 'diffed' | 'skipped' | 'baseline-unavailable',
|
|
167
189
|
* providerName: string,
|
|
168
190
|
* result?: DiffResult,
|
|
169
|
-
* recapture?: CaptureResult
|
|
191
|
+
* recapture?: CaptureResult,
|
|
192
|
+
* localScreenshots: boolean,
|
|
193
|
+
* artifacts: ReturnType<typeof describeCaptureArtifacts>
|
|
170
194
|
* }>}
|
|
171
195
|
*/
|
|
172
196
|
export async function diffWithPolicy(options) {
|
|
173
197
|
const providerName = options.providerName || 'local';
|
|
198
|
+
/** @type {Partial<CaptureResult>} */
|
|
199
|
+
const captureResult = options.captureResult || { screenshotsRoot: options.diffOptions.currentRunDir };
|
|
200
|
+
const described = options.artifacts || (captureResult.artifacts || options.config
|
|
201
|
+
? describeCaptureArtifacts(providerName, captureResult, options.config)
|
|
202
|
+
: { localScreenshots: true, artifactsRoot: captureResult.screenshotsRoot });
|
|
203
|
+
let artifacts = {
|
|
204
|
+
...described,
|
|
205
|
+
localScreenshots: options.localScreenshots ?? described.localScreenshots
|
|
206
|
+
};
|
|
207
|
+
if (!artifacts.localScreenshots) artifacts.artifactsRoot = undefined;
|
|
174
208
|
const createLocalProvider = options.createLocalProvider || (() => createProvider('local'));
|
|
175
209
|
const onSkip = options.onSkip || noop;
|
|
176
210
|
const onFallback = options.onFallback || noop;
|
|
@@ -179,11 +213,11 @@ export async function diffWithPolicy(options) {
|
|
|
179
213
|
|
|
180
214
|
try {
|
|
181
215
|
const result = await options.provider.diff(options.diffOptions);
|
|
182
|
-
return { outcome: 'diffed', providerName, result };
|
|
216
|
+
return { outcome: 'diffed', providerName, result, artifacts, localScreenshots: artifacts.localScreenshots };
|
|
183
217
|
} catch (error) {
|
|
184
218
|
if (error instanceof SnapSkipError) {
|
|
185
219
|
onSkip(error);
|
|
186
|
-
return { outcome: 'skipped', providerName };
|
|
220
|
+
return { outcome: 'skipped', providerName, artifacts, localScreenshots: artifacts.localScreenshots };
|
|
187
221
|
}
|
|
188
222
|
if (!(error instanceof SnapFallbackError)) {
|
|
189
223
|
throw error;
|
|
@@ -199,7 +233,7 @@ export async function diffWithPolicy(options) {
|
|
|
199
233
|
if (options.baselineResolutionStatus === 'error') {
|
|
200
234
|
throw createBaselineLookupFallbackError(error, 'diff', options.baselineResolutionMessage);
|
|
201
235
|
}
|
|
202
|
-
return { outcome: 'baseline-unavailable', providerName: 'local' };
|
|
236
|
+
return { outcome: 'baseline-unavailable', providerName: 'local', artifacts, localScreenshots: artifacts.localScreenshots };
|
|
203
237
|
}
|
|
204
238
|
|
|
205
239
|
if (options.baselineResolutionStatus === 'error') {
|
|
@@ -211,7 +245,7 @@ export async function diffWithPolicy(options) {
|
|
|
211
245
|
/** @type {CaptureResult | undefined} */
|
|
212
246
|
let recapture;
|
|
213
247
|
|
|
214
|
-
if (
|
|
248
|
+
if (!artifacts.localScreenshots) {
|
|
215
249
|
if (!options.captureOptions) {
|
|
216
250
|
throw new Error(
|
|
217
251
|
'Cannot fall back to a local diff: the current capture has no screenshots on disk ' +
|
|
@@ -221,6 +255,7 @@ export async function diffWithPolicy(options) {
|
|
|
221
255
|
}
|
|
222
256
|
onRecapture();
|
|
223
257
|
recapture = await localProvider.capture(options.captureOptions);
|
|
258
|
+
artifacts = describeCaptureArtifacts('local', recapture, options.config);
|
|
224
259
|
diffOptions = {
|
|
225
260
|
...options.diffOptions,
|
|
226
261
|
currentResultsPath: recapture.resultsPath,
|
|
@@ -230,7 +265,7 @@ export async function diffWithPolicy(options) {
|
|
|
230
265
|
}
|
|
231
266
|
|
|
232
267
|
const result = await localProvider.diff(diffOptions);
|
|
233
|
-
return { outcome: 'diffed', providerName: 'local', result, recapture };
|
|
268
|
+
return { outcome: 'diffed', providerName: 'local', result, recapture, artifacts, localScreenshots: artifacts.localScreenshots };
|
|
234
269
|
}
|
|
235
270
|
}
|
|
236
271
|
|
package/lib/provider.mjs
CHANGED
|
@@ -35,7 +35,8 @@ export class LocalProvider {
|
|
|
35
35
|
* @returns {Promise<CaptureResult>}
|
|
36
36
|
*/
|
|
37
37
|
async capture(options) {
|
|
38
|
-
|
|
38
|
+
const result = await runBaselineCapture(options);
|
|
39
|
+
return { ...result, artifacts: { localScreenshots: true, artifactsRoot: result.screenshotsRoot } };
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
/**
|
|
@@ -58,6 +59,7 @@ export class LocalProvider {
|
|
|
58
59
|
/**
|
|
59
60
|
* @param {FetchBaselineOptions} _options
|
|
60
61
|
* @returns {Promise<never>}
|
|
62
|
+
* Guard with providerSupports(providerName, 'fetchLatestBaseline') before calling.
|
|
61
63
|
* @throws {Error} Not yet implemented for LocalProvider.
|
|
62
64
|
* Use the pr-diff action for baseline resolution, or wait for a future release.
|
|
63
65
|
*/
|
|
@@ -80,6 +82,22 @@ export class LocalProvider {
|
|
|
80
82
|
}
|
|
81
83
|
}
|
|
82
84
|
|
|
85
|
+
export const PROVIDER_CAPABILITIES = Object.freeze({
|
|
86
|
+
local: Object.freeze({ capture: true, diff: true, publishBaseline: true, fetchLatestBaseline: false, buildCommentBody: true }),
|
|
87
|
+
snap: Object.freeze({ capture: true, diff: true, publishBaseline: true, fetchLatestBaseline: true, buildCommentBody: true })
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @param {string} providerName
|
|
92
|
+
* @param {string} capability
|
|
93
|
+
* @returns {boolean}
|
|
94
|
+
*/
|
|
95
|
+
export function providerSupports(providerName, capability) {
|
|
96
|
+
return Object.hasOwn(PROVIDER_CAPABILITIES, providerName) &&
|
|
97
|
+
Object.hasOwn(PROVIDER_CAPABILITIES[providerName], capability) &&
|
|
98
|
+
PROVIDER_CAPABILITIES[providerName][capability] === true;
|
|
99
|
+
}
|
|
100
|
+
|
|
83
101
|
/** @type {{ local: (config?: VisualRegressionConfig) => LocalProvider, snap: (config: VisualRegressionConfig) => SnapProvider }} */
|
|
84
102
|
const PROVIDER_FACTORIES = {
|
|
85
103
|
local: () => new LocalProvider(),
|
package/lib/snap-provider.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import path from 'node:path';
|
|
|
10
10
|
|
|
11
11
|
import { makeMarkdown, buildReportCommentBody } from '@snapdrift/adapter-report-md';
|
|
12
12
|
import { loadSnapdriftConfig, runBaselineCapture } from '@snapdrift/adapter-fs';
|
|
13
|
+
import { COMPARISON_FALLBACK_REASONS, COMPARISON_ROW_KINDS } from '@snapdrift/compare-core';
|
|
13
14
|
import {
|
|
14
15
|
selectConfiguredRoutes,
|
|
15
16
|
splitCommaList,
|
|
@@ -148,7 +149,7 @@ function hostedDiffError(detail) {
|
|
|
148
149
|
function isComparisonPolicy(value) {
|
|
149
150
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
150
151
|
const policy = /** @type {Record<string, any>} */ (value);
|
|
151
|
-
return policy.version === 1 && typeof policy.threshold === 'number' && Number.isFinite(policy.threshold) && policy.threshold >= 0 && policy.threshold <= 1;
|
|
152
|
+
return (policy.version === 1 || policy.version === 2) && typeof policy.threshold === 'number' && Number.isFinite(policy.threshold) && policy.threshold >= 0 && policy.threshold <= 1;
|
|
152
153
|
}
|
|
153
154
|
|
|
154
155
|
/** @param {unknown} value @returns {value is ComparisonMetadata} */
|
|
@@ -170,15 +171,66 @@ function isComparisonMetadata(value) {
|
|
|
170
171
|
return false;
|
|
171
172
|
}
|
|
172
173
|
const [baseline, current, canvas] = dimensions;
|
|
173
|
-
|
|
174
|
+
const baseMetadataValid = (
|
|
174
175
|
typeof comparison.dimensionsChanged === 'boolean' &&
|
|
175
176
|
comparison.dimensionsChanged === (baseline.width !== current.width || baseline.height !== current.height) &&
|
|
176
|
-
canvas.width === Math.max(baseline.width, current.width) &&
|
|
177
|
-
canvas.height === Math.max(baseline.height, current.height) &&
|
|
178
177
|
Number.isInteger(comparison.totalPixels) &&
|
|
179
|
-
comparison.totalPixels >= 0
|
|
180
|
-
comparison.totalPixels <= canvas.width * canvas.height
|
|
178
|
+
comparison.totalPixels >= 0
|
|
181
179
|
);
|
|
180
|
+
if (!baseMetadataValid) return false;
|
|
181
|
+
if (comparison.policyVersion !== undefined && comparison.policyVersion !== 2) return false;
|
|
182
|
+
|
|
183
|
+
if (comparison.policyVersion !== 2) {
|
|
184
|
+
if (comparison.mode !== undefined || comparison.rowMapping !== undefined || comparison.fallbackReason !== undefined) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
return (
|
|
188
|
+
canvas.width === Math.max(baseline.width, current.width) &&
|
|
189
|
+
canvas.height === Math.max(baseline.height, current.height) &&
|
|
190
|
+
comparison.totalPixels <= canvas.width * canvas.height
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (comparison.mode === 'coordinate-fallback') {
|
|
195
|
+
return (
|
|
196
|
+
comparison.rowMapping === undefined &&
|
|
197
|
+
COMPARISON_FALLBACK_REASONS.includes(comparison.fallbackReason) &&
|
|
198
|
+
canvas.width === Math.max(baseline.width, current.width) &&
|
|
199
|
+
canvas.height === Math.max(baseline.height, current.height) &&
|
|
200
|
+
comparison.totalPixels <= canvas.width * canvas.height
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (comparison.mode !== 'vertical-aligned' || comparison.fallbackReason !== undefined || !Array.isArray(comparison.rowMapping) || baseline.width !== current.width || canvas.width !== baseline.width) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
let outputStart = 0;
|
|
208
|
+
let baselineStart = 0;
|
|
209
|
+
let currentStart = 0;
|
|
210
|
+
for (const segment of comparison.rowMapping) {
|
|
211
|
+
if (!segment || !COMPARISON_ROW_KINDS.includes(segment.kind) || !Number.isInteger(segment.outputStart) || segment.outputStart !== outputStart || !Number.isInteger(segment.length) || segment.length <= 0) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
const hasBaseline = segment.kind === 'matched' || segment.kind === 'changed' || segment.kind === 'deleted';
|
|
215
|
+
const hasCurrent = segment.kind === 'matched' || segment.kind === 'changed' || segment.kind === 'inserted';
|
|
216
|
+
if (hasBaseline !== (Number.isInteger(segment.baselineStart)) || hasCurrent !== (Number.isInteger(segment.currentStart))) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
if (
|
|
220
|
+
(hasBaseline && segment.baselineStart !== baselineStart) ||
|
|
221
|
+
(!hasBaseline && segment.baselineStart !== undefined) ||
|
|
222
|
+
(hasCurrent && segment.currentStart !== currentStart) ||
|
|
223
|
+
(!hasCurrent && segment.currentStart !== undefined)
|
|
224
|
+
) {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
if (hasBaseline && (segment.baselineStart < 0 || segment.baselineStart + segment.length > baseline.height)) return false;
|
|
228
|
+
if (hasCurrent && (segment.currentStart < 0 || segment.currentStart + segment.length > current.height)) return false;
|
|
229
|
+
outputStart += segment.length;
|
|
230
|
+
if (hasBaseline) baselineStart += segment.length;
|
|
231
|
+
if (hasCurrent) currentStart += segment.length;
|
|
232
|
+
}
|
|
233
|
+
return canvas.height === outputStart && baselineStart === baseline.height && currentStart === current.height && canvas.height >= Math.max(baseline.height, current.height) && canvas.height <= baseline.height + current.height && comparison.totalPixels === canvas.width * canvas.height;
|
|
182
234
|
}
|
|
183
235
|
|
|
184
236
|
/**
|
|
@@ -221,12 +273,17 @@ function comparisonFromCapture(capture) {
|
|
|
221
273
|
return isComparisonMetadata(candidate) ? candidate : undefined;
|
|
222
274
|
}
|
|
223
275
|
|
|
276
|
+
/** @param {ComparisonMetadata} comparison @param {ComparisonPolicy} expected */
|
|
277
|
+
function comparisonMatchesPolicy(comparison, expected) {
|
|
278
|
+
return expected.version === 2 ? comparison.policyVersion === 2 : comparison.policyVersion === undefined;
|
|
279
|
+
}
|
|
280
|
+
|
|
224
281
|
/** @param {unknown} actual @param {ComparisonPolicy} expected @param {string} context */
|
|
225
282
|
function assertComparisonPolicyAcknowledged(actual, expected, context) {
|
|
226
|
-
if (!isComparisonPolicy(actual) || actual.threshold !== expected.threshold) {
|
|
283
|
+
if (!isComparisonPolicy(actual) || actual.version !== expected.version || actual.threshold !== expected.threshold) {
|
|
227
284
|
throw new SnapUpgradeRequiredError(
|
|
228
|
-
`${context} did not acknowledge comparisonPolicy
|
|
229
|
-
'Upgrade the Snap server before enabling
|
|
285
|
+
`${context} did not acknowledge comparisonPolicy v${expected.version} with threshold ${expected.threshold}. ` +
|
|
286
|
+
'Upgrade the Snap server before enabling this comparison policy.'
|
|
230
287
|
);
|
|
231
288
|
}
|
|
232
289
|
}
|
|
@@ -948,6 +1005,7 @@ export class SnapProvider {
|
|
|
948
1005
|
resultsPath,
|
|
949
1006
|
manifestPath,
|
|
950
1007
|
screenshotsRoot: outDir,
|
|
1008
|
+
artifacts: { localScreenshots: false, artifactsRoot: undefined },
|
|
951
1009
|
selectedRouteIds
|
|
952
1010
|
};
|
|
953
1011
|
}
|
|
@@ -1963,7 +2021,7 @@ export class SnapProvider {
|
|
|
1963
2021
|
};
|
|
1964
2022
|
await fs.writeFile(localCapture.resultsPath, JSON.stringify(snapResults, null, 2));
|
|
1965
2023
|
|
|
1966
|
-
return localCapture;
|
|
2024
|
+
return { ...localCapture, artifacts: { localScreenshots: true, artifactsRoot: localCapture.screenshotsRoot } };
|
|
1967
2025
|
}
|
|
1968
2026
|
|
|
1969
2027
|
/**
|
|
@@ -2246,7 +2304,18 @@ export class SnapProvider {
|
|
|
2246
2304
|
expected.routePath,
|
|
2247
2305
|
expected.viewport,
|
|
2248
2306
|
`Snap capture "${expected.routeId}" is missing complete comparison metadata; ` +
|
|
2249
|
-
|
|
2307
|
+
`upgrade the Snap server and diff Lambda before enabling comparisonPolicy v${comparisonPolicy.version}.`
|
|
2308
|
+
);
|
|
2309
|
+
continue;
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
if (comparisonPolicy && comparison && !comparisonMatchesPolicy(comparison, comparisonPolicy)) {
|
|
2313
|
+
addError(
|
|
2314
|
+
expected.routeId,
|
|
2315
|
+
expected.routePath,
|
|
2316
|
+
expected.viewport,
|
|
2317
|
+
`Snap capture "${expected.routeId}" returned comparison metadata for a different policy than v${comparisonPolicy.version}; ` +
|
|
2318
|
+
'upgrade the Snap server and diff Lambda before enabling this comparison policy.'
|
|
2250
2319
|
);
|
|
2251
2320
|
continue;
|
|
2252
2321
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "snapdrift",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "SnapDrift captures, compares, and reports UI drift for Playwright-based app checks.",
|
|
@@ -81,6 +81,7 @@
|
|
|
81
81
|
"./lib/cli.mjs": "./lib/cli.mjs",
|
|
82
82
|
"./lib/provider.mjs": "./lib/provider.mjs",
|
|
83
83
|
"./lib/outage-policy.mjs": "./lib/outage-policy.mjs",
|
|
84
|
+
"./lib/github-requests.mjs": "./lib/github-requests.mjs",
|
|
84
85
|
"./lib/snap-provider.mjs": "./lib/snap-provider.mjs",
|
|
85
86
|
"./lib/migrate-baselines.mjs": "./lib/migrate-baselines.mjs",
|
|
86
87
|
"./lib/init-from-action.mjs": "./lib/init-from-action.mjs",
|
|
@@ -102,10 +103,10 @@
|
|
|
102
103
|
"registry": "https://registry.npmjs.org"
|
|
103
104
|
},
|
|
104
105
|
"dependencies": {
|
|
105
|
-
"@snapdrift/adapter-fs": "^1.
|
|
106
|
-
"@snapdrift/adapter-report-md": "^1.
|
|
107
|
-
"@snapdrift/compare-core": "^1.
|
|
108
|
-
"@snapdrift/manifest": "^1.
|
|
106
|
+
"@snapdrift/adapter-fs": "^1.5.0",
|
|
107
|
+
"@snapdrift/adapter-report-md": "^1.4.0",
|
|
108
|
+
"@snapdrift/compare-core": "^1.5.0",
|
|
109
|
+
"@snapdrift/manifest": "^1.6.0",
|
|
109
110
|
"js-yaml": "^5.4.0",
|
|
110
111
|
"playwright": "^1.59.1",
|
|
111
112
|
"pngjs": "^7.0.0"
|
|
@@ -15,11 +15,19 @@ export interface VisualRegressionSelectionConfig {
|
|
|
15
15
|
sharedExact?: string[];
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
/** @deprecated Coordinate-based comparison policy. Retained for existing configurations; use `ComparisonPolicyV2` for new configurations. */
|
|
19
|
+
export interface ComparisonPolicyV1 {
|
|
19
20
|
version: 1;
|
|
20
21
|
threshold: number;
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
export interface ComparisonPolicyV2 {
|
|
25
|
+
version: 2;
|
|
26
|
+
threshold: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type ComparisonPolicy = ComparisonPolicyV1 | ComparisonPolicyV2;
|
|
30
|
+
|
|
23
31
|
export interface ComparisonDimensions {
|
|
24
32
|
width: number;
|
|
25
33
|
height: number;
|
|
@@ -31,6 +39,22 @@ export interface ComparisonMetadata {
|
|
|
31
39
|
canvas: ComparisonDimensions;
|
|
32
40
|
dimensionsChanged: boolean;
|
|
33
41
|
totalPixels: number;
|
|
42
|
+
policyVersion?: 2;
|
|
43
|
+
mode?: 'vertical-aligned' | 'coordinate-fallback';
|
|
44
|
+
rowMapping?: ComparisonRowMapping[];
|
|
45
|
+
fallbackReason?: ComparisonFallbackReason;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type ComparisonFallbackReason = 'width-mismatch' | 'alignment-limit' | 'ambiguous' | 'verification-failed' | 'ignore-regions';
|
|
49
|
+
export const COMPARISON_FALLBACK_REASONS: readonly ['width-mismatch', 'alignment-limit', 'ambiguous', 'verification-failed', 'ignore-regions'];
|
|
50
|
+
export type ComparisonRowKind = 'matched' | 'changed' | 'inserted' | 'deleted';
|
|
51
|
+
export const COMPARISON_ROW_KINDS: readonly ['matched', 'changed', 'inserted', 'deleted'];
|
|
52
|
+
export interface ComparisonRowMapping {
|
|
53
|
+
outputStart: number;
|
|
54
|
+
length: number;
|
|
55
|
+
kind: ComparisonRowKind;
|
|
56
|
+
baselineStart?: number;
|
|
57
|
+
currentStart?: number;
|
|
34
58
|
}
|
|
35
59
|
|
|
36
60
|
export interface VisualRegressionRouteConfig {
|
|
@@ -52,6 +76,7 @@ export interface VisualRegressionConfig {
|
|
|
52
76
|
diff: {
|
|
53
77
|
threshold: number;
|
|
54
78
|
mode: 'report-only' | 'fail-on-changes' | 'fail-on-incomplete' | 'strict';
|
|
79
|
+
/** Deprecated v1 remains the compatibility default; new configurations should use v2. */
|
|
55
80
|
comparisonPolicy?: ComparisonPolicy;
|
|
56
81
|
};
|
|
57
82
|
selection?: VisualRegressionSelectionConfig;
|
|
@@ -110,7 +135,7 @@ export interface SnapRunMetadata {
|
|
|
110
135
|
runId: string;
|
|
111
136
|
projectId: string;
|
|
112
137
|
purpose: 'baseline' | 'capture' | 'diff';
|
|
113
|
-
/** Exact comparison policy acknowledged by Snap for
|
|
138
|
+
/** Exact comparison policy acknowledged by Snap; v1 is deprecated for compatibility. */
|
|
114
139
|
comparisonPolicy?: ComparisonPolicy;
|
|
115
140
|
/** CI source branch for hosted baseline runs; omitted for ordinary diff runs. */
|
|
116
141
|
refBranch?: string;
|
|
@@ -192,7 +217,9 @@ export interface VisualDiffSummary {
|
|
|
192
217
|
changed: VisualDiffChangedItem[];
|
|
193
218
|
missing: VisualDiffMissingItem[];
|
|
194
219
|
errors: VisualDiffErrorItem[];
|
|
220
|
+
captureCompatibility?: import('@snapdrift/manifest').CaptureCompatibility;
|
|
195
221
|
dimensionChanges: VisualDiffDimensionItem[];
|
|
222
|
+
/** The exact policy used for the comparison; v1 is deprecated and retained for compatibility. */
|
|
196
223
|
comparisonPolicy?: ComparisonPolicy;
|
|
197
224
|
message?: string;
|
|
198
225
|
/** Link to the provider's run detail page. Set by SnapProvider during diff(); undefined for LocalProvider. */
|