@reshotdev/screenshot 0.0.1-beta.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/LICENSE +190 -0
- package/README.md +388 -0
- package/package.json +64 -0
- package/src/commands/auth.js +259 -0
- package/src/commands/chrome.js +140 -0
- package/src/commands/ci-run.js +123 -0
- package/src/commands/ci-setup.js +288 -0
- package/src/commands/drifts.js +423 -0
- package/src/commands/import-tests.js +309 -0
- package/src/commands/ingest.js +458 -0
- package/src/commands/init.js +633 -0
- package/src/commands/publish.js +1721 -0
- package/src/commands/pull.js +303 -0
- package/src/commands/record.js +94 -0
- package/src/commands/run.js +476 -0
- package/src/commands/setup-wizard.js +740 -0
- package/src/commands/setup.js +137 -0
- package/src/commands/status.js +275 -0
- package/src/commands/sync.js +621 -0
- package/src/commands/ui.js +248 -0
- package/src/commands/validate-docs.js +529 -0
- package/src/index.js +462 -0
- package/src/lib/api-client.js +815 -0
- package/src/lib/capture-engine.js +1623 -0
- package/src/lib/capture-script-runner.js +3120 -0
- package/src/lib/ci-detect.js +137 -0
- package/src/lib/config.js +1240 -0
- package/src/lib/diff-engine.js +642 -0
- package/src/lib/hash.js +74 -0
- package/src/lib/image-crop.js +396 -0
- package/src/lib/matrix.js +89 -0
- package/src/lib/output-path-template.js +318 -0
- package/src/lib/playwright-runner.js +252 -0
- package/src/lib/polished-clip.js +553 -0
- package/src/lib/privacy-engine.js +408 -0
- package/src/lib/progress-tracker.js +142 -0
- package/src/lib/record-browser-injection.js +654 -0
- package/src/lib/record-cdp.js +612 -0
- package/src/lib/record-clip.js +343 -0
- package/src/lib/record-config.js +623 -0
- package/src/lib/record-screenshot.js +360 -0
- package/src/lib/record-terminal.js +123 -0
- package/src/lib/recorder-service.js +781 -0
- package/src/lib/secrets.js +51 -0
- package/src/lib/selector-strategies.js +859 -0
- package/src/lib/standalone-mode.js +400 -0
- package/src/lib/storage-providers.js +569 -0
- package/src/lib/style-engine.js +684 -0
- package/src/lib/ui-api.js +4677 -0
- package/src/lib/ui-assets.js +373 -0
- package/src/lib/ui-executor.js +587 -0
- package/src/lib/variant-injector.js +591 -0
- package/src/lib/viewport-presets.js +454 -0
- package/src/lib/worker-pool.js +118 -0
- package/web/cropper/index.html +436 -0
- package/web/manager/dist/assets/index--ZgioErz.js +507 -0
- package/web/manager/dist/assets/index-n468W0Wr.css +1 -0
- package/web/manager/dist/index.html +27 -0
- package/web/subtitle-editor/index.html +295 -0
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
// run.js - Execute all scenarios from config using the robust capture engine
|
|
2
|
+
const chalk = require("chalk");
|
|
3
|
+
const fs = require("fs-extra");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const config = require("../lib/config");
|
|
6
|
+
const { runAllScenarios, generateVersionTimestamp, detectOptimalConcurrency } = require("../lib/capture-script-runner");
|
|
7
|
+
const { getBaselines } = require("../lib/api-client");
|
|
8
|
+
const {
|
|
9
|
+
downloadBaselines,
|
|
10
|
+
compareImages,
|
|
11
|
+
writeManifest,
|
|
12
|
+
compareWithPreviousVersion,
|
|
13
|
+
compareWithCloudBaselines,
|
|
14
|
+
writeLocalDiffManifest,
|
|
15
|
+
CACHE_DIR,
|
|
16
|
+
} = require("../lib/diff-engine");
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Generate all variant combinations from dimensions config
|
|
20
|
+
* @param {Object} dimensions - Dimensions configuration
|
|
21
|
+
* @param {string[]} dimensionKeys - Which dimensions to expand (default: all)
|
|
22
|
+
* @returns {Object[]} Array of variant objects
|
|
23
|
+
*/
|
|
24
|
+
function generateVariantCombinations(dimensions, dimensionKeys = null) {
|
|
25
|
+
if (!dimensions || Object.keys(dimensions).length === 0) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Use provided dimension keys or all available
|
|
30
|
+
const keysToUse = dimensionKeys || Object.keys(dimensions);
|
|
31
|
+
|
|
32
|
+
// Filter to only dimensions that have options
|
|
33
|
+
const validKeys = keysToUse.filter(key => {
|
|
34
|
+
const dim = dimensions[key];
|
|
35
|
+
return dim?.options && Object.keys(dim.options).length > 0;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (validKeys.length === 0) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Get options for each dimension
|
|
43
|
+
const dimensionOptions = validKeys.map((key) => {
|
|
44
|
+
const dim = dimensions[key];
|
|
45
|
+
return Object.keys(dim.options).map((optKey) => ({
|
|
46
|
+
dimension: key,
|
|
47
|
+
option: optKey,
|
|
48
|
+
}));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Generate cartesian product of all dimension options
|
|
52
|
+
const cartesian = (...arrays) => {
|
|
53
|
+
return arrays.reduce(
|
|
54
|
+
(acc, arr) => acc.flatMap((combo) => arr.map((item) => [...combo, item])),
|
|
55
|
+
[[]]
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const combinations = cartesian(...dimensionOptions);
|
|
60
|
+
|
|
61
|
+
// Convert to variant objects
|
|
62
|
+
return combinations.map((combo) => {
|
|
63
|
+
const variant = {};
|
|
64
|
+
for (const { dimension, option } of combo) {
|
|
65
|
+
variant[dimension] = option;
|
|
66
|
+
}
|
|
67
|
+
return variant;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Run scenarios from config
|
|
73
|
+
* @param {Object} options - Run options
|
|
74
|
+
* @param {string[]} options.scenarioKeys - Filter to specific scenario keys (optional)
|
|
75
|
+
* @param {boolean} options.headless - Run in headless mode (default: true)
|
|
76
|
+
* @param {Object} options.variant - Override variant for all scenarios (e.g., { locale: 'ko', role: 'admin' })
|
|
77
|
+
* @param {boolean} options.allVariants - Run all configured variant combinations
|
|
78
|
+
* @param {boolean} options.noVariants - Skip variant expansion entirely
|
|
79
|
+
* @param {string} options.format - Override output format: 'step-by-step-images' | 'summary-video'
|
|
80
|
+
* @param {boolean} options.diff - Enable baseline diffing (default: use config)
|
|
81
|
+
* @param {boolean} options.cloud - Enable cloud baseline sync (default: false, use local)
|
|
82
|
+
* @param {number} options.concurrency - Number of parallel browser workers (optional)
|
|
83
|
+
*/
|
|
84
|
+
async function runCommand(options = {}) {
|
|
85
|
+
const {
|
|
86
|
+
scenarioKeys,
|
|
87
|
+
headless = true,
|
|
88
|
+
variant,
|
|
89
|
+
allVariants = false,
|
|
90
|
+
noVariants = false,
|
|
91
|
+
noPrivacy = false,
|
|
92
|
+
noStyle = false,
|
|
93
|
+
format,
|
|
94
|
+
diff,
|
|
95
|
+
cloud = false,
|
|
96
|
+
concurrency,
|
|
97
|
+
noExit = false,
|
|
98
|
+
} = options;
|
|
99
|
+
|
|
100
|
+
const docSyncConfig = config.readConfig();
|
|
101
|
+
const diffingConfig = config.getDiffingConfig();
|
|
102
|
+
|
|
103
|
+
// Check feature toggles
|
|
104
|
+
const features = docSyncConfig._metadata?.features || { visuals: true };
|
|
105
|
+
if (features.visuals !== true) {
|
|
106
|
+
console.log(
|
|
107
|
+
chalk.yellow(
|
|
108
|
+
"ā Visual generation is disabled for this project. Skipping scenario execution."
|
|
109
|
+
)
|
|
110
|
+
);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Determine if diffing is enabled
|
|
115
|
+
// CLI flag takes precedence, then config, default is TRUE (always diff locally)
|
|
116
|
+
const shouldDiff =
|
|
117
|
+
diff !== undefined ? diff : diffingConfig.enabled !== false;
|
|
118
|
+
|
|
119
|
+
// Parse variant if passed as JSON string
|
|
120
|
+
let variantOverride = variant;
|
|
121
|
+
if (typeof variant === "string") {
|
|
122
|
+
try {
|
|
123
|
+
variantOverride = JSON.parse(variant);
|
|
124
|
+
} catch (e) {
|
|
125
|
+
console.error(chalk.red(`Invalid variant JSON: ${variant}`));
|
|
126
|
+
process.exitCode = 1;
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// If format is specified, override scenarios' output format
|
|
132
|
+
let configToUse = docSyncConfig;
|
|
133
|
+
if (format) {
|
|
134
|
+
console.log(chalk.cyan(`š· Using capture format: ${format}\n`));
|
|
135
|
+
configToUse = {
|
|
136
|
+
...docSyncConfig,
|
|
137
|
+
scenarios: (docSyncConfig.scenarios || []).map((scenario) => ({
|
|
138
|
+
...scenario,
|
|
139
|
+
output: {
|
|
140
|
+
...scenario.output,
|
|
141
|
+
format,
|
|
142
|
+
},
|
|
143
|
+
})),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Determine concurrency: CLI flag > config > auto-detect
|
|
148
|
+
const autoConcurrency = detectOptimalConcurrency();
|
|
149
|
+
const effectiveConcurrency = concurrency || docSyncConfig.concurrency || autoConcurrency;
|
|
150
|
+
if (!concurrency && !docSyncConfig.concurrency) {
|
|
151
|
+
console.log(chalk.gray(` Auto-detected concurrency: ${effectiveConcurrency} (${require("os").cpus().length} CPUs, ${Math.round(require("os").freemem() / 1024 / 1024)}MB free)\n`));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ============================================
|
|
155
|
+
// VARIANT EXPANSION: Run all variant combinations if configured
|
|
156
|
+
// ============================================
|
|
157
|
+
const variantsConfig = docSyncConfig.variants || {};
|
|
158
|
+
const dimensions = variantsConfig.dimensions || {};
|
|
159
|
+
const hasVariants = Object.keys(dimensions).length > 0;
|
|
160
|
+
|
|
161
|
+
// Determine if we should expand variants:
|
|
162
|
+
// - If specific variant is provided: use it only
|
|
163
|
+
// - If --no-variants: skip expansion, run without variants
|
|
164
|
+
// - If --all-variants or variants are configured: expand all combinations
|
|
165
|
+
const shouldExpandVariants = !noVariants && !variantOverride && hasVariants;
|
|
166
|
+
|
|
167
|
+
if (shouldExpandVariants) {
|
|
168
|
+
const combinations = generateVariantCombinations(dimensions);
|
|
169
|
+
|
|
170
|
+
if (combinations.length > 0) {
|
|
171
|
+
console.log(chalk.cyan(`šØ Expanding ${combinations.length} variant combination(s):\n`));
|
|
172
|
+
|
|
173
|
+
// Show variant combinations
|
|
174
|
+
for (const combo of combinations) {
|
|
175
|
+
const label = Object.entries(combo)
|
|
176
|
+
.map(([dim, opt]) => {
|
|
177
|
+
const dimension = dimensions[dim];
|
|
178
|
+
const option = dimension?.options?.[opt];
|
|
179
|
+
return option?.name || opt;
|
|
180
|
+
})
|
|
181
|
+
.join(' ⢠');
|
|
182
|
+
console.log(chalk.gray(` ā ${label}`));
|
|
183
|
+
}
|
|
184
|
+
console.log();
|
|
185
|
+
|
|
186
|
+
// Generate a shared timestamp for all variant runs
|
|
187
|
+
const sharedTimestamp = generateVersionTimestamp();
|
|
188
|
+
console.log(chalk.gray(`š Shared timestamp: ${sharedTimestamp}\n`));
|
|
189
|
+
|
|
190
|
+
// Run scenarios for each variant combination
|
|
191
|
+
const allResults = [];
|
|
192
|
+
let allSuccess = true;
|
|
193
|
+
|
|
194
|
+
for (const variantCombo of combinations) {
|
|
195
|
+
const variantLabel = Object.entries(variantCombo)
|
|
196
|
+
.map(([dim, opt]) => {
|
|
197
|
+
const dimension = dimensions[dim];
|
|
198
|
+
const option = dimension?.options?.[opt];
|
|
199
|
+
return option?.name || opt;
|
|
200
|
+
})
|
|
201
|
+
.join(' ⢠');
|
|
202
|
+
|
|
203
|
+
console.log(chalk.cyan(`\nāāā Variant: ${variantLabel} āāā\n`));
|
|
204
|
+
|
|
205
|
+
const result = await runAllScenarios(configToUse, {
|
|
206
|
+
scenarioKeys,
|
|
207
|
+
headless,
|
|
208
|
+
variantOverride: variantCombo,
|
|
209
|
+
concurrency: effectiveConcurrency,
|
|
210
|
+
sharedTimestamp,
|
|
211
|
+
noPrivacy,
|
|
212
|
+
noStyle,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
allResults.push({ variant: variantCombo, ...result });
|
|
216
|
+
if (!result.success) {
|
|
217
|
+
allSuccess = false;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Summary
|
|
222
|
+
console.log(chalk.cyan(`\nāāā Variant Capture Summary āāā\n`));
|
|
223
|
+
for (const result of allResults) {
|
|
224
|
+
const variantLabel = Object.entries(result.variant)
|
|
225
|
+
.map(([dim, opt]) => {
|
|
226
|
+
const dimension = dimensions[dim];
|
|
227
|
+
const option = dimension?.options?.[opt];
|
|
228
|
+
return option?.name || opt;
|
|
229
|
+
})
|
|
230
|
+
.join(' ⢠');
|
|
231
|
+
const statusIcon = result.success ? 'ā' : 'ā';
|
|
232
|
+
const statusColor = result.success ? chalk.green : chalk.red;
|
|
233
|
+
const assetCount = result.results?.reduce((sum, r) => sum + (r.assets?.length || 0), 0) || 0;
|
|
234
|
+
console.log(statusColor(` ${statusIcon} ${variantLabel}: ${assetCount} assets`));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (!allSuccess) {
|
|
238
|
+
process.exitCode = 1;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (!allSuccess) {
|
|
242
|
+
process.exitCode = 1;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Exit after variant expansion completes unless called programmatically
|
|
246
|
+
if (!noExit) {
|
|
247
|
+
process.exit(allSuccess ? 0 : 1);
|
|
248
|
+
}
|
|
249
|
+
return { success: allSuccess, results: allResults };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Single variant or no variants - use original logic
|
|
254
|
+
const result = await runAllScenarios(configToUse, {
|
|
255
|
+
scenarioKeys,
|
|
256
|
+
headless,
|
|
257
|
+
variantOverride,
|
|
258
|
+
concurrency: effectiveConcurrency,
|
|
259
|
+
noPrivacy,
|
|
260
|
+
noStyle,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// ============================================
|
|
264
|
+
// POST-PROCESSING: Local Version-to-Version Diffing
|
|
265
|
+
// ============================================
|
|
266
|
+
const outputBaseDir = path.join(
|
|
267
|
+
process.cwd(),
|
|
268
|
+
docSyncConfig.assetDir || ".reshot/output"
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
// ============================================
|
|
272
|
+
// CLOUD BASELINE SYNC: Download approved baselines from platform
|
|
273
|
+
// ============================================
|
|
274
|
+
let cloudBaselines = null;
|
|
275
|
+
if (cloud && shouldDiff) {
|
|
276
|
+
console.log(chalk.cyan("\nāļø Syncing cloud baselines...\n"));
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
const settings = config.readSettings();
|
|
280
|
+
const projectId =
|
|
281
|
+
settings?.projectId || docSyncConfig._metadata?.projectId;
|
|
282
|
+
const apiKey = process.env.RESHOT_API_KEY || settings?.apiKey;
|
|
283
|
+
|
|
284
|
+
if (projectId && apiKey) {
|
|
285
|
+
const baselineUrls = await getBaselines(projectId, apiKey);
|
|
286
|
+
const baselineCount = Object.keys(baselineUrls).length;
|
|
287
|
+
|
|
288
|
+
if (baselineCount > 0) {
|
|
289
|
+
console.log(
|
|
290
|
+
chalk.gray(` Found ${baselineCount} approved baseline(s) in cloud`)
|
|
291
|
+
);
|
|
292
|
+
cloudBaselines = await downloadBaselines(baselineUrls);
|
|
293
|
+
console.log(
|
|
294
|
+
chalk.green(
|
|
295
|
+
` ā Downloaded ${
|
|
296
|
+
Object.keys(cloudBaselines).length
|
|
297
|
+
} baseline(s)\n`
|
|
298
|
+
)
|
|
299
|
+
);
|
|
300
|
+
} else {
|
|
301
|
+
console.log(
|
|
302
|
+
chalk.gray(
|
|
303
|
+
" No approved baselines found in cloud (first publish?)\n"
|
|
304
|
+
)
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
} else {
|
|
308
|
+
console.log(
|
|
309
|
+
chalk.yellow(
|
|
310
|
+
" ā Cloud sync requires authentication. Run 'reshot auth' first.\n"
|
|
311
|
+
)
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
} catch (error) {
|
|
315
|
+
console.log(
|
|
316
|
+
chalk.yellow(` ā Cloud baseline sync failed: ${error.message}`)
|
|
317
|
+
);
|
|
318
|
+
console.log(
|
|
319
|
+
chalk.gray(" Falling back to local version-to-version diffing.\n")
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (shouldDiff && result.results) {
|
|
325
|
+
const diffMode = cloudBaselines
|
|
326
|
+
? "cloud baselines"
|
|
327
|
+
: "previous local versions";
|
|
328
|
+
console.log(
|
|
329
|
+
chalk.cyan(`\nš Computing visual diffs against ${diffMode}...\n`)
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
let totalScenarios = 0;
|
|
333
|
+
let totalDiffs = 0;
|
|
334
|
+
let totalCompared = 0;
|
|
335
|
+
let totalNew = 0;
|
|
336
|
+
|
|
337
|
+
for (const scenarioResult of result.results) {
|
|
338
|
+
if (!scenarioResult.success) continue;
|
|
339
|
+
|
|
340
|
+
const scenarioKey = scenarioResult.key;
|
|
341
|
+
const currentTimestamp = scenarioResult.timestamp;
|
|
342
|
+
const currentOutputDir = scenarioResult.outputDir; // Full path including variant
|
|
343
|
+
|
|
344
|
+
if (!currentTimestamp || !currentOutputDir) {
|
|
345
|
+
console.log(
|
|
346
|
+
chalk.gray(
|
|
347
|
+
` ā ${scenarioKey}: No timestamp/outputDir found, skipping diff`
|
|
348
|
+
)
|
|
349
|
+
);
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
totalScenarios++;
|
|
354
|
+
console.log(chalk.white(` š ${scenarioKey}:`));
|
|
355
|
+
|
|
356
|
+
let diffData;
|
|
357
|
+
|
|
358
|
+
if (cloudBaselines && Object.keys(cloudBaselines).length > 0) {
|
|
359
|
+
// Use cloud baselines for comparison
|
|
360
|
+
diffData = await compareWithCloudBaselines(
|
|
361
|
+
currentOutputDir,
|
|
362
|
+
scenarioKey,
|
|
363
|
+
cloudBaselines,
|
|
364
|
+
diffingConfig
|
|
365
|
+
);
|
|
366
|
+
console.log(chalk.gray(` ā³ Comparing against cloud baselines`));
|
|
367
|
+
} else {
|
|
368
|
+
// Fall back to local version-to-version comparison
|
|
369
|
+
diffData = await compareWithPreviousVersion(
|
|
370
|
+
outputBaseDir,
|
|
371
|
+
scenarioKey,
|
|
372
|
+
currentTimestamp,
|
|
373
|
+
diffingConfig
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
if (!diffData.previousVersion) {
|
|
377
|
+
console.log(
|
|
378
|
+
chalk.gray(` ā³ First version - no previous version to compare`)
|
|
379
|
+
);
|
|
380
|
+
console.log(
|
|
381
|
+
chalk.gray(
|
|
382
|
+
` (Diff percentages will appear after your next run)\n`
|
|
383
|
+
)
|
|
384
|
+
);
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
console.log(
|
|
389
|
+
chalk.gray(` ā³ Comparing against ${diffData.previousVersion}`)
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Log individual results
|
|
394
|
+
for (const [assetKey, assetResult] of Object.entries(diffData.results)) {
|
|
395
|
+
if (assetResult.status === "new") {
|
|
396
|
+
console.log(chalk.blue(` ā New: ${assetKey}`));
|
|
397
|
+
totalNew++;
|
|
398
|
+
} else if (assetResult.hasDiff) {
|
|
399
|
+
const percentage = (assetResult.score * 100).toFixed(2);
|
|
400
|
+
const reason = assetResult.reason ? ` (${assetResult.reason})` : "";
|
|
401
|
+
console.log(
|
|
402
|
+
chalk.yellow(
|
|
403
|
+
` ā Changed: ${assetKey} - ${percentage}%${reason}`
|
|
404
|
+
)
|
|
405
|
+
);
|
|
406
|
+
totalDiffs++;
|
|
407
|
+
} else {
|
|
408
|
+
console.log(chalk.green(` ā Unchanged: ${assetKey}`));
|
|
409
|
+
}
|
|
410
|
+
totalCompared++;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Write diff manifest to the ACTUAL output directory (includes variant)
|
|
414
|
+
await writeLocalDiffManifest(currentOutputDir, diffData);
|
|
415
|
+
|
|
416
|
+
console.log(""); // Blank line between scenarios
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Summary
|
|
420
|
+
console.log(chalk.cyan("š Diff Summary:"));
|
|
421
|
+
console.log(chalk.white(` Scenarios: ${totalScenarios}`));
|
|
422
|
+
console.log(chalk.white(` Assets compared: ${totalCompared}`));
|
|
423
|
+
if (totalNew > 0) {
|
|
424
|
+
console.log(chalk.blue(` New assets: ${totalNew}`));
|
|
425
|
+
}
|
|
426
|
+
if (totalDiffs > 0) {
|
|
427
|
+
console.log(chalk.yellow(` Changed: ${totalDiffs}`));
|
|
428
|
+
}
|
|
429
|
+
console.log(
|
|
430
|
+
chalk.green(` Unchanged: ${totalCompared - totalDiffs - totalNew}`)
|
|
431
|
+
);
|
|
432
|
+
console.log("");
|
|
433
|
+
|
|
434
|
+
// Helpful guidance about diff percentages
|
|
435
|
+
if (totalNew > 0 && totalCompared === totalNew) {
|
|
436
|
+
console.log(
|
|
437
|
+
chalk.gray(
|
|
438
|
+
"š” Tip: All assets are new (first capture). Diff percentages will"
|
|
439
|
+
)
|
|
440
|
+
);
|
|
441
|
+
console.log(
|
|
442
|
+
chalk.gray(
|
|
443
|
+
" appear in the platform after your next run & publish cycle."
|
|
444
|
+
)
|
|
445
|
+
);
|
|
446
|
+
console.log("");
|
|
447
|
+
} else if (totalDiffs > 0) {
|
|
448
|
+
console.log(
|
|
449
|
+
chalk.gray(
|
|
450
|
+
"š” Tip: Changed assets will show diff percentages in the platform"
|
|
451
|
+
)
|
|
452
|
+
);
|
|
453
|
+
console.log(chalk.gray(" after you run 'reshot publish'."));
|
|
454
|
+
console.log("");
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (!result.success) {
|
|
459
|
+
const failed = result.results.filter((r) => !r.success);
|
|
460
|
+
console.error(chalk.red(`\nā ${failed.length} scenario(s) failed`));
|
|
461
|
+
process.exitCode = 1;
|
|
462
|
+
} else {
|
|
463
|
+
console.log(chalk.green("\n⨠All scenarios completed successfully!"));
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
console.log(chalk.gray(`\nOutput saved to: ${outputBaseDir}`));
|
|
467
|
+
|
|
468
|
+
// Ensure process exits cleanly (Playwright CDP connections can keep event loop alive)
|
|
469
|
+
if (!noExit) {
|
|
470
|
+
setImmediate(() => process.exit(process.exitCode || 0));
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return { success: result.success, results: result.results };
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
module.exports = runCommand;
|