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 +37 -1
- 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 +5 -1
- package/docs/integration-guide.md +20 -4
- package/docs/workflow-templates/refresh-baseline-on-merge.yml +82 -0
- 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 -145
- package/lib/report.mjs +17 -246
- package/lib/snapdrift-config.mjs +20 -279
- package/lib/stage-artifacts.mjs +2 -123
- package/package.json +10 -2
- package/types/visual-diff-types.d.ts +1 -0
package/lib/snapdrift-config.mjs
CHANGED
|
@@ -1,280 +1,21 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
hasTouch: true
|
|
23
|
-
}
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export const SNAPDRIFT_NAVIGATION_TIMEOUT_MS = 30000;
|
|
27
|
-
export const SNAPDRIFT_SETTLE_DELAY_MS = 300;
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* @param {string[]} names
|
|
31
|
-
* @returns {string | undefined}
|
|
32
|
-
*/
|
|
33
|
-
export function readFirstDefinedEnv(names) {
|
|
34
|
-
for (const name of names) {
|
|
35
|
-
const value = process.env[name];
|
|
36
|
-
if (typeof value === 'string' && value.length > 0) {
|
|
37
|
-
return value;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return undefined;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* @typedef {import('../types/visual-diff-types').VisualRegressionConfig} SnapdriftConfig
|
|
45
|
-
* @typedef {import('../types/visual-diff-types').VisualRegressionRouteConfig} SnapdriftRouteConfig
|
|
46
|
-
*/
|
|
47
|
-
|
|
48
|
-
const VALID_VIEWPORT_PRESETS = new Set(Object.keys(SNAPDRIFT_VIEWPORT_PRESETS));
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* @param {unknown} value
|
|
52
|
-
* @returns {boolean}
|
|
53
|
-
*/
|
|
54
|
-
function isValidCustomViewport(value) {
|
|
55
|
-
if (!isRecord(value)) return false;
|
|
56
|
-
const rec = /** @type {Record<string,unknown>} */ (value);
|
|
57
|
-
return Number.isInteger(rec.width) && /** @type {number} */ (rec.width) > 0 &&
|
|
58
|
-
Number.isInteger(rec.height) && /** @type {number} */ (rec.height) > 0;
|
|
59
|
-
}
|
|
60
|
-
const VALID_DIFF_MODE_SET = new Set(VALID_DIFF_MODES);
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* @param {unknown} value
|
|
64
|
-
* @returns {value is Record<string, unknown>}
|
|
65
|
-
*/
|
|
66
|
-
function isRecord(value) {
|
|
67
|
-
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* @param {unknown} value
|
|
72
|
-
* @returns {value is string}
|
|
73
|
-
*/
|
|
74
|
-
function isNonEmptyString(value) {
|
|
75
|
-
return typeof value === 'string' && value.trim().length > 0;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* @param {unknown} value
|
|
80
|
-
* @returns {value is string[]}
|
|
81
|
-
*/
|
|
82
|
-
function isNonEmptyStringArray(value) {
|
|
83
|
-
return Array.isArray(value) && value.every((item) => isNonEmptyString(item));
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* @param {string | undefined} value
|
|
88
|
-
* @returns {string[]}
|
|
89
|
-
*/
|
|
90
|
-
export function splitCommaList(value) {
|
|
91
|
-
return String(value || '')
|
|
92
|
-
.split(',')
|
|
93
|
-
.map((item) => item.trim())
|
|
94
|
-
.filter(Boolean);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* @param {unknown} value
|
|
99
|
-
* @param {string} sourceLabel
|
|
100
|
-
* @returns {SnapdriftConfig}
|
|
101
|
-
*/
|
|
102
|
-
export function validateSnapdriftConfig(value, sourceLabel = 'inline config') {
|
|
103
|
-
/** @type {string[]} */
|
|
104
|
-
const errors = [];
|
|
105
|
-
|
|
106
|
-
if (!isRecord(value)) {
|
|
107
|
-
throw new Error(`Invalid SnapDrift config at ${sourceLabel}: expected a JSON object.`);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const candidate = /** @type {Record<string, unknown>} */ (value);
|
|
111
|
-
const requiredStringFields = [
|
|
112
|
-
'baselineArtifactName',
|
|
113
|
-
'workingDirectory',
|
|
114
|
-
'baseUrl',
|
|
115
|
-
'resultsFile',
|
|
116
|
-
'manifestFile',
|
|
117
|
-
'screenshotsRoot'
|
|
118
|
-
];
|
|
119
|
-
|
|
120
|
-
for (const fieldName of requiredStringFields) {
|
|
121
|
-
if (!isNonEmptyString(candidate[fieldName])) {
|
|
122
|
-
errors.push(`${fieldName} must be a non-empty string.`);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (!Array.isArray(candidate.routes) || candidate.routes.length === 0) {
|
|
127
|
-
errors.push('routes must contain at least one route definition.');
|
|
128
|
-
} else {
|
|
129
|
-
const routeIds = new Set();
|
|
130
|
-
|
|
131
|
-
for (const [index, route] of candidate.routes.entries()) {
|
|
132
|
-
const routeLabel = `routes[${index}]`;
|
|
133
|
-
if (!isRecord(route)) {
|
|
134
|
-
errors.push(`${routeLabel} must be an object.`);
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (!isNonEmptyString(route.id)) {
|
|
139
|
-
errors.push(`${routeLabel}.id must be a non-empty string.`);
|
|
140
|
-
} else if (routeIds.has(route.id)) {
|
|
141
|
-
errors.push(`${routeLabel}.id duplicates the route id "${route.id}".`);
|
|
142
|
-
} else {
|
|
143
|
-
routeIds.add(route.id);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (!isNonEmptyString(route.path)) {
|
|
147
|
-
errors.push(`${routeLabel}.path must be a non-empty string.`);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const viewportValid = (isNonEmptyString(route.viewport) && VALID_VIEWPORT_PRESETS.has(route.viewport)) ||
|
|
151
|
-
isValidCustomViewport(route.viewport);
|
|
152
|
-
if (!viewportValid) {
|
|
153
|
-
errors.push(`${routeLabel}.viewport must be one of: ${[...VALID_VIEWPORT_PRESETS].join(', ')} or an object with positive integer width and height.`);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (route.changePaths !== undefined && !isNonEmptyStringArray(route.changePaths)) {
|
|
157
|
-
errors.push(`${routeLabel}.changePaths must be an array of non-empty strings when provided.`);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (!isRecord(candidate.diff)) {
|
|
163
|
-
errors.push('diff must be an object.');
|
|
164
|
-
} else {
|
|
165
|
-
if (typeof candidate.diff.threshold !== 'number' || !Number.isFinite(candidate.diff.threshold)) {
|
|
166
|
-
errors.push('diff.threshold must be a finite number.');
|
|
167
|
-
} else if (candidate.diff.threshold < 0 || candidate.diff.threshold > 1) {
|
|
168
|
-
errors.push('diff.threshold must be between 0 and 1.');
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (!isNonEmptyString(candidate.diff.mode) || !VALID_DIFF_MODE_SET.has(candidate.diff.mode)) {
|
|
172
|
-
errors.push(`diff.mode must be one of: ${VALID_DIFF_MODES.join(', ')}.`);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (candidate.selection !== undefined) {
|
|
177
|
-
if (!isRecord(candidate.selection)) {
|
|
178
|
-
errors.push('selection must be an object when provided.');
|
|
179
|
-
} else {
|
|
180
|
-
if (candidate.selection.sharedPrefixes !== undefined && !isNonEmptyStringArray(candidate.selection.sharedPrefixes)) {
|
|
181
|
-
errors.push('selection.sharedPrefixes must be an array of non-empty strings when provided.');
|
|
182
|
-
}
|
|
183
|
-
if (candidate.selection.sharedExact !== undefined && !isNonEmptyStringArray(candidate.selection.sharedExact)) {
|
|
184
|
-
errors.push('selection.sharedExact must be an array of non-empty strings when provided.');
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
if (errors.length > 0) {
|
|
190
|
-
throw new Error(`Invalid SnapDrift config at ${sourceLabel}:\n- ${errors.join('\n- ')}`);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
return /** @type {SnapdriftConfig} */ (/** @type {unknown} */ (value));
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* @param {string | undefined} [configPath]
|
|
198
|
-
* @returns {Promise<{ config: SnapdriftConfig, configPath: string }>}
|
|
199
|
-
*/
|
|
200
|
-
export async function loadSnapdriftConfig(configPath = readFirstDefinedEnv(['SNAPDRIFT_CONFIG_PATH'])) {
|
|
201
|
-
const resolvedConfigPath = configPath ? path.resolve(configPath) : DEFAULT_CONFIG_PATH;
|
|
202
|
-
const raw = await fs.readFile(resolvedConfigPath, 'utf8');
|
|
203
|
-
const parsed = JSON.parse(raw);
|
|
204
|
-
|
|
205
|
-
return {
|
|
206
|
-
config: validateSnapdriftConfig(parsed, resolvedConfigPath),
|
|
207
|
-
configPath: resolvedConfigPath
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* @param {SnapdriftConfig} config
|
|
213
|
-
* @param {string} relativePath
|
|
214
|
-
* @returns {string}
|
|
215
|
-
*/
|
|
216
|
-
export function resolveFromWorkingDirectory(config, relativePath) {
|
|
217
|
-
return path.resolve(config.workingDirectory || '.', relativePath);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* @param {SnapdriftConfig} config
|
|
222
|
-
* @param {Iterable<string>} requestedRouteIds
|
|
223
|
-
* @returns {{ routes: SnapdriftRouteConfig[], selectedRouteIds: string[] }}
|
|
224
|
-
*/
|
|
225
|
-
export function selectConfiguredRoutes(config, requestedRouteIds) {
|
|
226
|
-
const requestedIds = new Set([...requestedRouteIds].filter(Boolean));
|
|
227
|
-
if (requestedIds.size === 0) {
|
|
228
|
-
return {
|
|
229
|
-
routes: [...config.routes],
|
|
230
|
-
selectedRouteIds: config.routes.map((route) => route.id)
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const routes = config.routes.filter((route) => requestedIds.has(route.id));
|
|
235
|
-
const missingRouteIds = [...requestedIds].filter((routeId) => !routes.some((route) => route.id === routeId));
|
|
236
|
-
if (missingRouteIds.length > 0) {
|
|
237
|
-
throw new Error(`Unknown SnapDrift route ids: ${missingRouteIds.join(', ')}`);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
return {
|
|
241
|
-
routes,
|
|
242
|
-
selectedRouteIds: routes.map((route) => route.id)
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* @param {SnapdriftConfig} config
|
|
248
|
-
* @param {string[]} changedFiles
|
|
249
|
-
* @returns {{ shouldRun: boolean, reason: string, selectedRouteIds: string[] }}
|
|
250
|
-
*/
|
|
251
|
-
export function selectRoutesForChangedFiles(config, changedFiles) {
|
|
252
|
-
const sharedPrefixes = config.selection?.sharedPrefixes || [];
|
|
253
|
-
const sharedExact = new Set(config.selection?.sharedExact || []);
|
|
254
|
-
|
|
255
|
-
if (changedFiles.some((file) => sharedExact.has(file) || sharedPrefixes.some((prefix) => file.startsWith(prefix)))) {
|
|
256
|
-
return {
|
|
257
|
-
shouldRun: true,
|
|
258
|
-
reason: 'shared_snapdrift_change',
|
|
259
|
-
selectedRouteIds: config.routes.map((route) => route.id)
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const selectedRouteIds = config.routes
|
|
264
|
-
.filter((route) => (route.changePaths || []).some((prefix) => changedFiles.some((file) => file.startsWith(prefix))))
|
|
265
|
-
.map((route) => route.id);
|
|
266
|
-
|
|
267
|
-
if (selectedRouteIds.length > 0) {
|
|
268
|
-
return {
|
|
269
|
-
shouldRun: true,
|
|
270
|
-
reason: 'scoped_snapdrift_change',
|
|
271
|
-
selectedRouteIds
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
return {
|
|
276
|
-
shouldRun: false,
|
|
277
|
-
reason: changedFiles.length > 0 ? 'no_snapdrift_relevant_changes' : 'no_changed_files',
|
|
278
|
-
selectedRouteIds: []
|
|
279
|
-
};
|
|
280
|
-
}
|
|
2
|
+
// Re-exports from packages — this module is a thin shim.
|
|
3
|
+
|
|
4
|
+
export {
|
|
5
|
+
VIEWPORT_PRESETS as SNAPDRIFT_VIEWPORT_PRESETS,
|
|
6
|
+
SNAPDRIFT_NAVIGATION_TIMEOUT_MS,
|
|
7
|
+
SNAPDRIFT_SETTLE_DELAY_MS,
|
|
8
|
+
validateSnapdriftConfig,
|
|
9
|
+
selectConfiguredRoutes,
|
|
10
|
+
selectRoutesForChangedFiles,
|
|
11
|
+
resolveFromWorkingDirectory,
|
|
12
|
+
splitCommaList,
|
|
13
|
+
VALID_DIFF_MODES
|
|
14
|
+
} from '@snapdrift/manifest';
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
loadSnapdriftConfig,
|
|
18
|
+
readFirstDefinedEnv,
|
|
19
|
+
DEFAULT_CONFIG_PATH,
|
|
20
|
+
SNAPDRIFT_CAPTURE_CONCURRENCY
|
|
21
|
+
} from '@snapdrift/adapter-fs';
|
package/lib/stage-artifacts.mjs
CHANGED
|
@@ -1,125 +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
|
-
|
|
6
|
-
/**
|
|
7
|
-
* @param {'baseline' | 'diff'} artifactType
|
|
8
|
-
* @returns {string}
|
|
9
|
-
*/
|
|
10
|
-
export function getDefaultArtifactBundleDir(artifactType) {
|
|
11
|
-
return artifactType === 'baseline'
|
|
12
|
-
? path.join('qa-artifacts', 'snapdrift', 'bundles', 'baseline')
|
|
13
|
-
: path.join('qa-artifacts', 'snapdrift', 'bundles', 'drift');
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* @param {string} filePath
|
|
18
|
-
* @returns {Promise<boolean>}
|
|
19
|
-
*/
|
|
20
|
-
async function exists(filePath) {
|
|
21
|
-
try {
|
|
22
|
-
await fs.access(filePath);
|
|
23
|
-
return true;
|
|
24
|
-
} catch {
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* @param {string | undefined} sourcePath
|
|
31
|
-
* @param {string} targetPath
|
|
32
|
-
* @returns {Promise<void>}
|
|
33
|
-
*/
|
|
34
|
-
async function copyFileIfPresent(sourcePath, targetPath) {
|
|
35
|
-
if (!sourcePath) {
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (!(await exists(sourcePath))) {
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
44
|
-
await fs.copyFile(sourcePath, targetPath);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* @param {string} sourceDir
|
|
49
|
-
* @param {string} targetDir
|
|
50
|
-
* @returns {Promise<void>}
|
|
51
|
-
*/
|
|
52
|
-
async function copyPngFiles(sourceDir, targetDir) {
|
|
53
|
-
if (!(await exists(sourceDir))) {
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
await fs.mkdir(targetDir, { recursive: true });
|
|
58
|
-
const entries = await fs.readdir(sourceDir, { withFileTypes: true });
|
|
59
|
-
for (const entry of entries) {
|
|
60
|
-
const sourcePath = path.join(sourceDir, entry.name);
|
|
61
|
-
if (entry.isDirectory()) {
|
|
62
|
-
await copyPngFiles(sourcePath, targetDir);
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
if (!entry.isFile() || !entry.name.endsWith('.png')) {
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
await fs.copyFile(sourcePath, path.join(targetDir, entry.name));
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* @param {{
|
|
74
|
-
* artifactType: 'baseline' | 'diff',
|
|
75
|
-
* bundleDir?: string,
|
|
76
|
-
* resultsPath?: string,
|
|
77
|
-
* manifestPath?: string,
|
|
78
|
-
* screenshotsDir?: string,
|
|
79
|
-
* summaryJsonPath?: string,
|
|
80
|
-
* summaryMarkdownPath?: string,
|
|
81
|
-
* reportHtmlPath?: string,
|
|
82
|
-
* baselineResultsPath?: string,
|
|
83
|
-
* currentResultsPath?: string,
|
|
84
|
-
* baselineManifestPath?: string,
|
|
85
|
-
* currentManifestPath?: string,
|
|
86
|
-
* baselineScreenshotsDir?: string,
|
|
87
|
-
* currentScreenshotsDir?: string
|
|
88
|
-
* }} options
|
|
89
|
-
* @returns {Promise<{ bundleDir: string }>}
|
|
90
|
-
*/
|
|
91
|
-
export async function stageArtifacts(options) {
|
|
92
|
-
const resolvedBundleDir = path.resolve(options.bundleDir || getDefaultArtifactBundleDir(options.artifactType));
|
|
93
|
-
|
|
94
|
-
await fs.rm(resolvedBundleDir, { recursive: true, force: true });
|
|
95
|
-
|
|
96
|
-
if (options.artifactType === 'baseline') {
|
|
97
|
-
await fs.mkdir(path.join(resolvedBundleDir, 'screenshots'), { recursive: true });
|
|
98
|
-
await copyFileIfPresent(options.resultsPath, path.join(resolvedBundleDir, 'results.json'));
|
|
99
|
-
await copyFileIfPresent(options.manifestPath, path.join(resolvedBundleDir, 'manifest.json'));
|
|
100
|
-
if (options.screenshotsDir) {
|
|
101
|
-
await copyPngFiles(options.screenshotsDir, path.join(resolvedBundleDir, 'screenshots'));
|
|
102
|
-
}
|
|
103
|
-
} else {
|
|
104
|
-
await fs.mkdir(path.join(resolvedBundleDir, 'baseline', 'screenshots'), { recursive: true });
|
|
105
|
-
await fs.mkdir(path.join(resolvedBundleDir, 'current', 'screenshots'), { recursive: true });
|
|
106
|
-
await copyFileIfPresent(options.summaryJsonPath, path.join(resolvedBundleDir, 'summary.json'));
|
|
107
|
-
await copyFileIfPresent(options.summaryMarkdownPath, path.join(resolvedBundleDir, 'summary.md'));
|
|
108
|
-
await copyFileIfPresent(options.reportHtmlPath, path.join(resolvedBundleDir, 'report.html'));
|
|
109
|
-
await copyFileIfPresent(options.baselineResultsPath, path.join(resolvedBundleDir, 'baseline', 'results.json'));
|
|
110
|
-
await copyFileIfPresent(options.currentResultsPath, path.join(resolvedBundleDir, 'current', 'results.json'));
|
|
111
|
-
await copyFileIfPresent(options.baselineManifestPath, path.join(resolvedBundleDir, 'baseline', 'manifest.json'));
|
|
112
|
-
await copyFileIfPresent(options.currentManifestPath, path.join(resolvedBundleDir, 'current', 'manifest.json'));
|
|
113
|
-
|
|
114
|
-
if (options.baselineScreenshotsDir) {
|
|
115
|
-
await copyPngFiles(options.baselineScreenshotsDir, path.join(resolvedBundleDir, 'baseline', 'screenshots'));
|
|
116
|
-
}
|
|
117
|
-
if (options.currentScreenshotsDir) {
|
|
118
|
-
await copyPngFiles(options.currentScreenshotsDir, path.join(resolvedBundleDir, 'current', 'screenshots'));
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
bundleDir: resolvedBundleDir
|
|
124
|
-
};
|
|
125
|
-
}
|
|
4
|
+
export { stageArtifacts, getDefaultArtifactBundleDir } from '@snapdrift/adapter-fs';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "snapdrift",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "SnapDrift captures, compares, and reports UI drift for Playwright-based app checks.",
|
|
@@ -34,6 +34,9 @@
|
|
|
34
34
|
"engines": {
|
|
35
35
|
"node": ">=22"
|
|
36
36
|
},
|
|
37
|
+
"workspaces": [
|
|
38
|
+
"packages/*"
|
|
39
|
+
],
|
|
37
40
|
"devDependencies": {
|
|
38
41
|
"@eslint/js": "^10.0.1",
|
|
39
42
|
"@types/jest": "^30.0.0",
|
|
@@ -49,7 +52,8 @@
|
|
|
49
52
|
"jest": {
|
|
50
53
|
"testEnvironment": "node",
|
|
51
54
|
"testMatch": [
|
|
52
|
-
"
|
|
55
|
+
"<rootDir>/tests/**/*.test.js",
|
|
56
|
+
"<rootDir>/packages/*/tests/**/*.test.js"
|
|
53
57
|
],
|
|
54
58
|
"transform": {},
|
|
55
59
|
"coverageThreshold": {
|
|
@@ -88,6 +92,10 @@
|
|
|
88
92
|
"registry": "https://registry.npmjs.org"
|
|
89
93
|
},
|
|
90
94
|
"dependencies": {
|
|
95
|
+
"@snapdrift/adapter-fs": "^1.0.0",
|
|
96
|
+
"@snapdrift/adapter-report-md": "^1.0.0",
|
|
97
|
+
"@snapdrift/compare-core": "^1.0.0",
|
|
98
|
+
"@snapdrift/manifest": "^1.0.0",
|
|
91
99
|
"playwright": "^1.59.1",
|
|
92
100
|
"pngjs": "^7.0.0"
|
|
93
101
|
}
|