@reshotdev/screenshot 0.0.1-beta.7 → 0.0.1-beta.9
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/README.md +63 -5
- package/package.json +1 -1
- package/src/commands/auth.js +106 -22
- package/src/commands/certify.js +54 -0
- package/src/commands/ci-setup.js +3 -3
- package/src/commands/doctor-target.js +42 -0
- package/src/commands/publish.js +45 -10
- package/src/commands/pull.js +252 -22
- package/src/commands/setup-wizard.js +187 -29
- package/src/commands/setup.js +35 -7
- package/src/commands/verify-publish.js +46 -0
- package/src/index.js +149 -3
- package/src/lib/api-client.js +64 -23
- package/src/lib/capture-engine.js +64 -3
- package/src/lib/capture-script-runner.js +96 -10
- package/src/lib/certification.js +739 -0
- package/src/lib/config.js +16 -3
- package/src/lib/record-cdp.js +16 -2
- package/src/lib/target-contract.js +278 -0
- package/web/manager/dist/assets/{index-8H7P9ANi.js → index-D2qqcFNN.js} +1 -1
- package/web/manager/dist/index.html +1 -1
|
@@ -251,7 +251,11 @@ async function executeWithRetry(engine, readySelector, options = {}) {
|
|
|
251
251
|
* @returns {Promise<{ok: boolean, message?: string}>}
|
|
252
252
|
*/
|
|
253
253
|
async function preflightAuthCheck(baseUrl, options = {}) {
|
|
254
|
-
const {
|
|
254
|
+
const {
|
|
255
|
+
storageStatePath,
|
|
256
|
+
viewport = { width: 1280, height: 720 },
|
|
257
|
+
authCheckUrl = "/app/projects",
|
|
258
|
+
} = options;
|
|
255
259
|
|
|
256
260
|
if (!storageStatePath || !fs.existsSync(storageStatePath)) {
|
|
257
261
|
return { ok: true }; // No session to verify
|
|
@@ -272,8 +276,12 @@ async function preflightAuthCheck(baseUrl, options = {}) {
|
|
|
272
276
|
try {
|
|
273
277
|
await engine.init();
|
|
274
278
|
|
|
275
|
-
|
|
276
|
-
|
|
279
|
+
const preflightPath = authCheckUrl.startsWith("http")
|
|
280
|
+
? authCheckUrl
|
|
281
|
+
: `${baseUrl}${authCheckUrl}`;
|
|
282
|
+
|
|
283
|
+
// Navigate to a known authenticated page and validate session/data loading.
|
|
284
|
+
await engine.page.goto(preflightPath, {
|
|
277
285
|
waitUntil: "domcontentloaded",
|
|
278
286
|
timeout: 15000,
|
|
279
287
|
});
|
|
@@ -290,6 +298,19 @@ async function preflightAuthCheck(baseUrl, options = {}) {
|
|
|
290
298
|
};
|
|
291
299
|
}
|
|
292
300
|
|
|
301
|
+
// Also detect login page via DOM (catches SPA redirects where URL hasn't changed)
|
|
302
|
+
const hasLoginForm = await engine.page.evaluate(() => {
|
|
303
|
+
const h = document.querySelector("h1, h2");
|
|
304
|
+
return h && /sign\s*in|log\s*in/i.test(h.textContent);
|
|
305
|
+
}).catch(() => false);
|
|
306
|
+
if (hasLoginForm) {
|
|
307
|
+
return {
|
|
308
|
+
ok: false,
|
|
309
|
+
message:
|
|
310
|
+
"Auth session expired (login form detected). Run `reshot record` to refresh.",
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
293
314
|
// Wait for data to settle
|
|
294
315
|
await engine.page.waitForTimeout(3000);
|
|
295
316
|
await engine._waitForStability();
|
|
@@ -303,7 +324,18 @@ async function preflightAuthCheck(baseUrl, options = {}) {
|
|
|
303
324
|
};
|
|
304
325
|
}
|
|
305
326
|
|
|
306
|
-
|
|
327
|
+
// Save refreshed session back so scenarios use fresh cookies
|
|
328
|
+
if (storageStatePath && engine.context) {
|
|
329
|
+
try {
|
|
330
|
+
const refreshedState = await engine.context.storageState();
|
|
331
|
+
fs.writeFileSync(storageStatePath, JSON.stringify(refreshedState, null, 2));
|
|
332
|
+
console.log(chalk.green(" ✔ Auth pre-flight check passed (session refreshed)"));
|
|
333
|
+
} catch (_saveErr) {
|
|
334
|
+
console.log(chalk.green(" ✔ Auth pre-flight check passed"));
|
|
335
|
+
}
|
|
336
|
+
} else {
|
|
337
|
+
console.log(chalk.green(" ✔ Auth pre-flight check passed"));
|
|
338
|
+
}
|
|
307
339
|
return { ok: true };
|
|
308
340
|
} catch (e) {
|
|
309
341
|
// If the error is an auth redirect thrown by the engine, handle gracefully
|
|
@@ -407,6 +439,14 @@ async function retryInteractiveStep(engine, action, params, context) {
|
|
|
407
439
|
}
|
|
408
440
|
}
|
|
409
441
|
|
|
442
|
+
function promoteLastGotoUrl(lastGotoUrl, currentUrl) {
|
|
443
|
+
if (!currentUrl || currentUrl === "about:blank") {
|
|
444
|
+
return lastGotoUrl;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return currentUrl !== lastGotoUrl ? currentUrl : lastGotoUrl;
|
|
448
|
+
}
|
|
449
|
+
|
|
410
450
|
/**
|
|
411
451
|
* Calculate a perceptual hash for an image buffer
|
|
412
452
|
* This is a simple hash based on resizing the image to a small grid
|
|
@@ -843,6 +883,7 @@ async function runScenarioWithStepByStepCapture(scenario, options = {}) {
|
|
|
843
883
|
waitForReady: scenario.waitForReady || null, // Custom loading-state hook
|
|
844
884
|
privacyConfig: hasPrivacy ? scenarioPrivacyConfig : null, // Privacy masking
|
|
845
885
|
styleConfig: hasStyle ? scenarioStyleConfig : null, // Image beautification
|
|
886
|
+
injectWorkspaceStore: scenario.needsWorkspaceInjection !== false,
|
|
846
887
|
logger: quiet ? () => {} : (msg) => console.log(msg),
|
|
847
888
|
});
|
|
848
889
|
|
|
@@ -1267,6 +1308,11 @@ async function runScenarioWithStepByStepCapture(scenario, options = {}) {
|
|
|
1267
1308
|
}
|
|
1268
1309
|
}
|
|
1269
1310
|
|
|
1311
|
+
// Promote the current page URL after successful interactive steps so
|
|
1312
|
+
// retries restore the page we actually navigated to, not the last
|
|
1313
|
+
// explicit goto target.
|
|
1314
|
+
lastGotoUrl = promoteLastGotoUrl(lastGotoUrl, engine.page.url());
|
|
1315
|
+
|
|
1270
1316
|
// Wait for animations/transitions - longer wait for multi-step flows
|
|
1271
1317
|
const isMultiStep = script.length > 3;
|
|
1272
1318
|
await engine.page.waitForTimeout(isMultiStep ? 500 : 150);
|
|
@@ -1310,6 +1356,12 @@ async function runScenarioWithStepByStepCapture(scenario, options = {}) {
|
|
|
1310
1356
|
` Hint: If data isn't loading, run 'reshot record' to refresh your session`
|
|
1311
1357
|
)
|
|
1312
1358
|
);
|
|
1359
|
+
failedSteps.push({
|
|
1360
|
+
stepIndex: stepIndex + 1,
|
|
1361
|
+
action: "waitFor",
|
|
1362
|
+
target: params.target,
|
|
1363
|
+
error: errMsg,
|
|
1364
|
+
});
|
|
1313
1365
|
}
|
|
1314
1366
|
} else if (waitResult.status === "timeout") {
|
|
1315
1367
|
if (!isOptional) {
|
|
@@ -1323,9 +1375,14 @@ async function runScenarioWithStepByStepCapture(scenario, options = {}) {
|
|
|
1323
1375
|
` Hint: If content isn't loading, run 'reshot record' to refresh your session`
|
|
1324
1376
|
)
|
|
1325
1377
|
);
|
|
1378
|
+
failedSteps.push({
|
|
1379
|
+
stepIndex: stepIndex + 1,
|
|
1380
|
+
action: "waitFor",
|
|
1381
|
+
target: params.target,
|
|
1382
|
+
error: `Element not found within ${waitTimeout}ms`,
|
|
1383
|
+
});
|
|
1326
1384
|
}
|
|
1327
1385
|
}
|
|
1328
|
-
// Continue with next steps - the scenario may still capture partial state
|
|
1329
1386
|
continue;
|
|
1330
1387
|
}
|
|
1331
1388
|
|
|
@@ -1421,7 +1478,17 @@ async function runScenarioWithStepByStepCapture(scenario, options = {}) {
|
|
|
1421
1478
|
// Non-critical — don't fail the capture
|
|
1422
1479
|
}
|
|
1423
1480
|
|
|
1424
|
-
return {
|
|
1481
|
+
return {
|
|
1482
|
+
success: failedSteps.length === 0,
|
|
1483
|
+
assets,
|
|
1484
|
+
skippedSteps,
|
|
1485
|
+
duplicatesSkipped,
|
|
1486
|
+
failedSteps,
|
|
1487
|
+
retriedSteps,
|
|
1488
|
+
privacy: privacyMeta,
|
|
1489
|
+
style: styleMeta,
|
|
1490
|
+
diagnostics: engine.getDiagnostics(),
|
|
1491
|
+
};
|
|
1425
1492
|
})(); // End of scenarioExecution async IIFE
|
|
1426
1493
|
|
|
1427
1494
|
// Race scenario execution against timeout
|
|
@@ -1450,7 +1517,15 @@ async function runScenarioWithStepByStepCapture(scenario, options = {}) {
|
|
|
1450
1517
|
// Ignore
|
|
1451
1518
|
}
|
|
1452
1519
|
|
|
1453
|
-
return {
|
|
1520
|
+
return {
|
|
1521
|
+
success: false,
|
|
1522
|
+
error: error.message,
|
|
1523
|
+
assets,
|
|
1524
|
+
skippedSteps,
|
|
1525
|
+
failedSteps,
|
|
1526
|
+
retriedSteps,
|
|
1527
|
+
diagnostics: engine.getDiagnostics(),
|
|
1528
|
+
};
|
|
1454
1529
|
} finally {
|
|
1455
1530
|
await engine.close();
|
|
1456
1531
|
}
|
|
@@ -2584,6 +2659,7 @@ async function runScenarioWithEngine(scenario, options = {}) {
|
|
|
2584
2659
|
storageStatePath: hasSession ? sessionPath : null, // Use saved session if available
|
|
2585
2660
|
storageStateData, // Pre-loaded auth state
|
|
2586
2661
|
hideDevtools: true, // Always hide dev overlays in captures
|
|
2662
|
+
injectWorkspaceStore: scenario.needsWorkspaceInjection !== false,
|
|
2587
2663
|
logger: quiet ? () => {} : (msg) => console.log(msg),
|
|
2588
2664
|
});
|
|
2589
2665
|
|
|
@@ -2595,7 +2671,7 @@ async function runScenarioWithEngine(scenario, options = {}) {
|
|
|
2595
2671
|
chalk.green(`\n ✔ Scenario completed: ${assets.length} assets captured`)
|
|
2596
2672
|
);
|
|
2597
2673
|
|
|
2598
|
-
return { success: true, assets };
|
|
2674
|
+
return { success: true, assets, diagnostics: engine.getDiagnostics() };
|
|
2599
2675
|
} catch (error) {
|
|
2600
2676
|
console.error(chalk.red(`\n ❌ Scenario failed: ${error.message}`));
|
|
2601
2677
|
|
|
@@ -2615,7 +2691,7 @@ async function runScenarioWithEngine(scenario, options = {}) {
|
|
|
2615
2691
|
// Ignore screenshot errors
|
|
2616
2692
|
}
|
|
2617
2693
|
|
|
2618
|
-
return { success: false, error: error.message };
|
|
2694
|
+
return { success: false, error: error.message, diagnostics: engine.getDiagnostics() };
|
|
2619
2695
|
} finally {
|
|
2620
2696
|
await engine.close();
|
|
2621
2697
|
}
|
|
@@ -2860,7 +2936,14 @@ async function runAllScenarios(config, options = {}) {
|
|
|
2860
2936
|
// Run auth pre-flight check if any scenario requires auth
|
|
2861
2937
|
const captureConfig = getCaptureConfig(config.capture || {});
|
|
2862
2938
|
const scenarios = config.scenarios || [];
|
|
2863
|
-
const hasAuthScenarios = scenarios.some((s) => s.
|
|
2939
|
+
const hasAuthScenarios = scenarios.some((s) => s.captureClass !== "public");
|
|
2940
|
+
const authPreflightScenario = scenarios.find(
|
|
2941
|
+
(scenario) => scenario.captureClass === "live-auth" || scenario.requiresAuth,
|
|
2942
|
+
);
|
|
2943
|
+
const authCheckUrl =
|
|
2944
|
+
config.target?.authPreflightUrl ||
|
|
2945
|
+
authPreflightScenario?.url ||
|
|
2946
|
+
"/app/projects";
|
|
2864
2947
|
|
|
2865
2948
|
if (captureConfig.preflightCheck && hasAuthScenarios) {
|
|
2866
2949
|
const sessionPath = getDefaultSessionPath();
|
|
@@ -2871,6 +2954,7 @@ async function runAllScenarios(config, options = {}) {
|
|
|
2871
2954
|
{
|
|
2872
2955
|
storageStatePath: sessionPath,
|
|
2873
2956
|
viewport: config.viewport || { width: 1280, height: 720 },
|
|
2957
|
+
authCheckUrl,
|
|
2874
2958
|
}
|
|
2875
2959
|
);
|
|
2876
2960
|
if (!preflightResult.ok) {
|
|
@@ -3138,6 +3222,7 @@ async function runAllScenarios(config, options = {}) {
|
|
|
3138
3222
|
|
|
3139
3223
|
module.exports = {
|
|
3140
3224
|
convertLegacySteps,
|
|
3225
|
+
substituteUrlVariables,
|
|
3141
3226
|
runScenarioWithEngine,
|
|
3142
3227
|
runScenarioWithStepByStepCapture,
|
|
3143
3228
|
runScenarioWithVideoCapture,
|
|
@@ -3149,6 +3234,7 @@ module.exports = {
|
|
|
3149
3234
|
waitForVisualStability,
|
|
3150
3235
|
// Error detection & retry
|
|
3151
3236
|
retryInteractiveStep,
|
|
3237
|
+
promoteLastGotoUrl,
|
|
3152
3238
|
executeWithRetry,
|
|
3153
3239
|
preflightAuthCheck,
|
|
3154
3240
|
// New exports for output templating
|