@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.
@@ -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 { storageStatePath, viewport = { width: 1280, height: 720 } } = options;
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
- // Navigate to projects page (a page that requires auth + data)
276
- await engine.page.goto(`${baseUrl}/app/projects`, {
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
- console.log(chalk.green(" ✔ Auth pre-flight check passed"));
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 { success: failedSteps.length === 0, assets, skippedSteps, duplicatesSkipped, failedSteps, retriedSteps, privacy: privacyMeta, style: styleMeta };
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 { success: false, error: error.message, assets, skippedSteps, failedSteps, retriedSteps };
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.requiresAuth);
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