@reshotdev/screenshot 0.0.1-beta.8 ā 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 +54 -2
- 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 +35 -8
- package/src/commands/pull.js +227 -21
- package/src/commands/setup-wizard.js +187 -29
- package/src/commands/setup.js +3 -3
- package/src/commands/verify-publish.js +46 -0
- package/src/index.js +135 -5
- package/src/lib/api-client.js +64 -23
- package/src/lib/capture-engine.js +64 -3
- package/src/lib/capture-script-runner.js +72 -8
- 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
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const chalk = require("chalk");
|
|
4
|
+
const { runVerifyPublish } = require("../lib/certification");
|
|
5
|
+
|
|
6
|
+
async function verifyPublishCommand(options = {}) {
|
|
7
|
+
const scenarioKeys = options.scenarios
|
|
8
|
+
? String(options.scenarios)
|
|
9
|
+
.split(",")
|
|
10
|
+
.map((value) => value.trim())
|
|
11
|
+
.filter(Boolean)
|
|
12
|
+
: null;
|
|
13
|
+
|
|
14
|
+
const report = await runVerifyPublish({
|
|
15
|
+
scenarioKeys,
|
|
16
|
+
tag: options.tag,
|
|
17
|
+
message: options.message,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
if (options.json) {
|
|
21
|
+
console.log(JSON.stringify(report, null, 2));
|
|
22
|
+
} else {
|
|
23
|
+
console.log(chalk.cyan("\nš¦ Publish Verification\n"));
|
|
24
|
+
console.log(
|
|
25
|
+
report.ok
|
|
26
|
+
? chalk.green(" ā Publish, pull, and hosted delivery verified")
|
|
27
|
+
: chalk.red(" ā Publish verification failed"),
|
|
28
|
+
);
|
|
29
|
+
console.log(chalk.gray(` Published: ${report.publishResult.assetsProcessed || 0}`));
|
|
30
|
+
console.log(chalk.gray(` Pull repairs: ${report.pullResult.normalizationRepairs || 0}`));
|
|
31
|
+
const failedChecks = (report.deliveryChecks || []).filter((check) => !check.ok);
|
|
32
|
+
if (failedChecks.length > 0) {
|
|
33
|
+
for (const check of failedChecks.slice(0, 10)) {
|
|
34
|
+
console.log(chalk.red(` ā ${check.scenario}/${check.assetKey}: ${check.reason}`));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!report.ok) {
|
|
40
|
+
process.exitCode = 1;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return report;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = verifyPublishCommand;
|
package/src/index.js
CHANGED
|
@@ -153,10 +153,70 @@ program
|
|
|
153
153
|
}
|
|
154
154
|
});
|
|
155
155
|
|
|
156
|
+
// Capture: Compatibility alias for older docs and snippets
|
|
157
|
+
program
|
|
158
|
+
.command("capture")
|
|
159
|
+
.description("Compatibility alias for `reshot run` (prefer `reshot run` and `reshot publish`)")
|
|
160
|
+
.option("-s, --scenarios <keys>", "Comma-separated list of scenario keys")
|
|
161
|
+
.option("--mode <mode>", "Compatibility mode: preview | publish", "preview")
|
|
162
|
+
.option("--config <path>", "Compatibility config path (defaults to reshot.config.json)")
|
|
163
|
+
.option("--base-url <url>", "Temporary base URL override for compatibility flows")
|
|
164
|
+
.option("--tag <tag>", "Version tag when mode=publish")
|
|
165
|
+
.option("-m, --message <message>", "Publish message when mode=publish")
|
|
166
|
+
.option("--no-headless", "Run browser in visible mode")
|
|
167
|
+
.option("-c, --concurrency <n>", "Number of parallel browser workers", parseInt)
|
|
168
|
+
.option("--debug", "Enable verbose debug logging")
|
|
169
|
+
.action(async (options) => {
|
|
170
|
+
if (options.debug) {
|
|
171
|
+
process.env.RESHOT_DEBUG = "1";
|
|
172
|
+
}
|
|
173
|
+
if (options.config && options.config !== "reshot.config.json") {
|
|
174
|
+
console.log(
|
|
175
|
+
chalk.yellow(
|
|
176
|
+
"Compatibility mode currently uses reshot.config.json from the working directory.",
|
|
177
|
+
),
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
if (options.baseUrl) {
|
|
181
|
+
process.env.RESHOT_BASE_URL = options.baseUrl;
|
|
182
|
+
console.log(
|
|
183
|
+
chalk.gray(
|
|
184
|
+
`Using temporary base URL override for this run: ${options.baseUrl}`,
|
|
185
|
+
),
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
const runCommand = require("./commands/run");
|
|
190
|
+
const scenarioKeys = options.scenarios
|
|
191
|
+
? options.scenarios.split(",").map((s) => s.trim())
|
|
192
|
+
: null;
|
|
193
|
+
await runCommand({
|
|
194
|
+
scenarioKeys,
|
|
195
|
+
headless: options.headless,
|
|
196
|
+
concurrency: options.concurrency,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
if (String(options.mode || "preview").toLowerCase() === "publish") {
|
|
200
|
+
const publishCommand = require("./commands/publish");
|
|
201
|
+
await publishCommand({
|
|
202
|
+
tag: options.tag,
|
|
203
|
+
message: options.message,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.error(chalk.red("Error:"), error.message);
|
|
208
|
+
if (options.debug && error.stack) {
|
|
209
|
+
console.error(chalk.gray(error.stack));
|
|
210
|
+
}
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
156
215
|
// Record: Interactive visual recording from live browser
|
|
157
216
|
program
|
|
158
217
|
.command("record [title]")
|
|
159
218
|
.description("Interactively record visuals from a live browser session")
|
|
219
|
+
.option("--name <title>", "Compatibility alias for the scenario title")
|
|
160
220
|
.option("--browser", "Launch Chrome with remote debugging before recording")
|
|
161
221
|
.option("-p, --port <port>", "Chrome debugging port (default: 9222)")
|
|
162
222
|
.option("--url <url>", "URL to open when launching browser")
|
|
@@ -166,14 +226,15 @@ program
|
|
|
166
226
|
if (options.debug) {
|
|
167
227
|
process.env.RESHOT_DEBUG = "1";
|
|
168
228
|
}
|
|
229
|
+
const resolvedTitle = title || options.name;
|
|
169
230
|
try {
|
|
170
231
|
// If --refresh-session, just sync the session and exit
|
|
171
232
|
if (options.refreshSession) {
|
|
172
233
|
const { autoSyncSessionFromCDP, getDefaultSessionPath } = require("./lib/record-cdp");
|
|
173
234
|
const sessionPath = getDefaultSessionPath();
|
|
174
235
|
console.log(chalk.gray(" Syncing session from active browser..."));
|
|
175
|
-
const
|
|
176
|
-
if (synced) {
|
|
236
|
+
const result = await autoSyncSessionFromCDP(sessionPath);
|
|
237
|
+
if (result.synced) {
|
|
177
238
|
console.log(chalk.green(" ā Session refreshed at " + sessionPath));
|
|
178
239
|
} else {
|
|
179
240
|
console.log(chalk.yellow(" ā No active CDP browser found. Launch Chrome with remote debugging first:"));
|
|
@@ -194,7 +255,7 @@ program
|
|
|
194
255
|
}
|
|
195
256
|
|
|
196
257
|
const recordCommand = require("./commands/record");
|
|
197
|
-
await recordCommand(
|
|
258
|
+
await recordCommand(resolvedTitle);
|
|
198
259
|
} catch (error) {
|
|
199
260
|
console.error(chalk.red("Error:"), error.message);
|
|
200
261
|
if (options.debug && error.stack) {
|
|
@@ -275,6 +336,62 @@ program
|
|
|
275
336
|
}
|
|
276
337
|
});
|
|
277
338
|
|
|
339
|
+
// Doctor: Validate target contract and readiness
|
|
340
|
+
const doctor = program.command("doctor").description("Validate target configuration and readiness");
|
|
341
|
+
|
|
342
|
+
doctor
|
|
343
|
+
.command("target")
|
|
344
|
+
.description("Audit the certified target contract before capture")
|
|
345
|
+
.option("-s, --scenarios <keys>", "Comma-separated list of scenario keys")
|
|
346
|
+
.option("--json", "Output JSON report")
|
|
347
|
+
.action(async (options) => {
|
|
348
|
+
try {
|
|
349
|
+
const doctorTargetCommand = require("./commands/doctor-target");
|
|
350
|
+
await doctorTargetCommand(options);
|
|
351
|
+
} catch (error) {
|
|
352
|
+
console.error(chalk.red("Error:"), error.message);
|
|
353
|
+
process.exit(1);
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// Verify: Publish/pull/hosted delivery verification
|
|
358
|
+
const verify = program.command("verify").description("Verify publish and delivery flows");
|
|
359
|
+
|
|
360
|
+
verify
|
|
361
|
+
.command("publish")
|
|
362
|
+
.description("Verify publish, pull, and hosted asset delivery")
|
|
363
|
+
.option("-s, --scenarios <keys>", "Comma-separated list of scenario keys")
|
|
364
|
+
.option("--tag <tag>", "Version tag for verification publish")
|
|
365
|
+
.option("-m, --message <message>", "Publish message override")
|
|
366
|
+
.option("--json", "Output JSON report")
|
|
367
|
+
.action(async (options) => {
|
|
368
|
+
try {
|
|
369
|
+
const verifyPublishCommand = require("./commands/verify-publish");
|
|
370
|
+
await verifyPublishCommand(options);
|
|
371
|
+
} catch (error) {
|
|
372
|
+
console.error(chalk.red("Error:"), error.message);
|
|
373
|
+
process.exit(1);
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// Certify: full certified-target release gate
|
|
378
|
+
program
|
|
379
|
+
.command("certify")
|
|
380
|
+
.description("Run the full certified target pipeline")
|
|
381
|
+
.option("-s, --scenarios <keys>", "Comma-separated list of scenario keys")
|
|
382
|
+
.option("--tag <tag>", "Version tag for certification publish")
|
|
383
|
+
.option("-m, --message <message>", "Publish message override")
|
|
384
|
+
.option("--json", "Output JSON report")
|
|
385
|
+
.action(async (options) => {
|
|
386
|
+
try {
|
|
387
|
+
const certifyCommand = require("./commands/certify");
|
|
388
|
+
await certifyCommand(options);
|
|
389
|
+
} catch (error) {
|
|
390
|
+
console.error(chalk.red("Error:"), error.message);
|
|
391
|
+
process.exit(1);
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
|
|
278
395
|
// CI: CI/CD integration commands
|
|
279
396
|
const ciCommand = program
|
|
280
397
|
.command("ci")
|
|
@@ -353,9 +470,22 @@ Actions:
|
|
|
353
470
|
// ============================================================================
|
|
354
471
|
|
|
355
472
|
// Auth: Standalone authentication (for re-auth scenarios)
|
|
356
|
-
program
|
|
473
|
+
const auth = program
|
|
357
474
|
.command("auth")
|
|
358
|
-
.description("Authenticate with Reshot Cloud")
|
|
475
|
+
.description("Authenticate with Reshot Cloud (or set RESHOT_API_KEY + RESHOT_PROJECT_ID env vars)")
|
|
476
|
+
.action(async () => {
|
|
477
|
+
try {
|
|
478
|
+
const authCommand = require("./commands/auth");
|
|
479
|
+
await authCommand();
|
|
480
|
+
} catch (error) {
|
|
481
|
+
console.error(chalk.red("Error:"), error.message);
|
|
482
|
+
process.exit(1);
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
auth
|
|
487
|
+
.command("login")
|
|
488
|
+
.description("Compatibility alias for `reshot auth`")
|
|
359
489
|
.action(async () => {
|
|
360
490
|
try {
|
|
361
491
|
const authCommand = require("./commands/auth");
|
package/src/lib/api-client.js
CHANGED
|
@@ -5,6 +5,51 @@ const fs = require("fs");
|
|
|
5
5
|
|
|
6
6
|
const PRODUCTION_API_URL = "https://reshot.dev/api";
|
|
7
7
|
|
|
8
|
+
function summarizeApiBody(body) {
|
|
9
|
+
if (body == null) return "";
|
|
10
|
+
if (typeof body === "string") return body.trim();
|
|
11
|
+
if (typeof body !== "object") return String(body);
|
|
12
|
+
|
|
13
|
+
const candidates = [
|
|
14
|
+
body.message,
|
|
15
|
+
body.error?.message,
|
|
16
|
+
body.error,
|
|
17
|
+
body.reason,
|
|
18
|
+
body.details,
|
|
19
|
+
].filter(Boolean);
|
|
20
|
+
|
|
21
|
+
for (const candidate of candidates) {
|
|
22
|
+
if (typeof candidate === "string" && candidate.trim()) {
|
|
23
|
+
return candidate.trim();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
return JSON.stringify(body);
|
|
29
|
+
} catch {
|
|
30
|
+
return String(body);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function createApiError(kind, endpoint, error) {
|
|
35
|
+
const status = error.response?.status || null;
|
|
36
|
+
const statusText = error.response?.statusText || "";
|
|
37
|
+
const bodySummary = summarizeApiBody(error.response?.data);
|
|
38
|
+
const baseMessage = bodySummary || error.message || "Unknown API error";
|
|
39
|
+
const statusLabel = status ? `${status}${statusText ? ` ${statusText}` : ""}` : "request failed";
|
|
40
|
+
const wrapped = new Error(`${kind} (${statusLabel}) at ${endpoint}: ${baseMessage}`);
|
|
41
|
+
wrapped.response = error.response;
|
|
42
|
+
wrapped.code = error.code;
|
|
43
|
+
wrapped.reshot = {
|
|
44
|
+
kind,
|
|
45
|
+
endpoint,
|
|
46
|
+
status,
|
|
47
|
+
statusText,
|
|
48
|
+
bodySummary,
|
|
49
|
+
};
|
|
50
|
+
return wrapped;
|
|
51
|
+
}
|
|
52
|
+
|
|
8
53
|
function getApiBaseUrl() {
|
|
9
54
|
// 1. Explicit env var override (for CI or local dev)
|
|
10
55
|
if (process.env.RESHOT_API_BASE_URL) {
|
|
@@ -210,20 +255,7 @@ async function publishAssetsV1(apiKey, metadata, assets) {
|
|
|
210
255
|
});
|
|
211
256
|
return response.data;
|
|
212
257
|
} catch (error) {
|
|
213
|
-
|
|
214
|
-
const status = error.response.status;
|
|
215
|
-
const errorMsg = error.response.data?.error || error.message;
|
|
216
|
-
|
|
217
|
-
// Create an error that preserves the response for auth detection
|
|
218
|
-
const err = new Error(
|
|
219
|
-
status === 401 || status === 403
|
|
220
|
-
? `Authentication failed: ${errorMsg}`
|
|
221
|
-
: `Failed to publish assets: ${errorMsg}`,
|
|
222
|
-
);
|
|
223
|
-
err.response = error.response;
|
|
224
|
-
throw err;
|
|
225
|
-
}
|
|
226
|
-
throw new Error(`Failed to publish assets: ${error.message}`);
|
|
258
|
+
throw createApiError("publish_ingest_failure", `${baseUrl}/v1/publish`, error);
|
|
227
259
|
}
|
|
228
260
|
}
|
|
229
261
|
|
|
@@ -700,15 +732,23 @@ async function exportVisuals(projectId, options = {}) {
|
|
|
700
732
|
}
|
|
701
733
|
|
|
702
734
|
return withRetry(async () => {
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
735
|
+
try {
|
|
736
|
+
const response = await axios.get(
|
|
737
|
+
`${baseUrl}/projects/${projectId}/visuals/export`,
|
|
738
|
+
{
|
|
739
|
+
params: { format, status },
|
|
740
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
741
|
+
timeout: 60000,
|
|
742
|
+
},
|
|
743
|
+
);
|
|
744
|
+
return response.data;
|
|
745
|
+
} catch (error) {
|
|
746
|
+
throw createApiError(
|
|
747
|
+
"export_visuals_failure",
|
|
748
|
+
`${baseUrl}/projects/${projectId}/visuals/export`,
|
|
749
|
+
error,
|
|
750
|
+
);
|
|
751
|
+
}
|
|
712
752
|
});
|
|
713
753
|
}
|
|
714
754
|
|
|
@@ -838,6 +878,7 @@ module.exports = {
|
|
|
838
878
|
getProjectConfig,
|
|
839
879
|
postChangelogDrafts,
|
|
840
880
|
getApiBaseUrl,
|
|
881
|
+
createApiError,
|
|
841
882
|
syncPushAssets,
|
|
842
883
|
getSyncStatus,
|
|
843
884
|
// New transactional flow
|
|
@@ -98,6 +98,8 @@ class CaptureEngine {
|
|
|
98
98
|
this.capturedAssets = [];
|
|
99
99
|
this.logger = options.logger || console.log;
|
|
100
100
|
this.headless = options.headless !== false; // Default to headless
|
|
101
|
+
this.injectWorkspaceStore = options.injectWorkspaceStore !== false;
|
|
102
|
+
this.diagnostics = [];
|
|
101
103
|
|
|
102
104
|
// Storage state path for authenticated sessions
|
|
103
105
|
// If provided, loads cookies/localStorage from file to preserve auth state
|
|
@@ -289,7 +291,9 @@ class CaptureEngine {
|
|
|
289
291
|
logVariantSummary(this.variantConfig, this.logger);
|
|
290
292
|
}
|
|
291
293
|
|
|
292
|
-
|
|
294
|
+
if (this.injectWorkspaceStore) {
|
|
295
|
+
await this._injectWorkspaceStore();
|
|
296
|
+
}
|
|
293
297
|
|
|
294
298
|
// Inject privacy masking CSS (after variant injection, before captures)
|
|
295
299
|
this._privacyInjectionOk = true;
|
|
@@ -306,24 +310,76 @@ class CaptureEngine {
|
|
|
306
310
|
this._authResponseDetected = false;
|
|
307
311
|
this.page.on("response", (response) => {
|
|
308
312
|
const status = response.status();
|
|
313
|
+
const url = response.url();
|
|
309
314
|
if (
|
|
310
315
|
(status === 401 || status === 403) &&
|
|
311
316
|
response.request().resourceType() === "document"
|
|
312
317
|
) {
|
|
313
318
|
this._authResponseDetected = true;
|
|
314
319
|
}
|
|
320
|
+
|
|
321
|
+
if (status >= 400 && response.request().resourceType() === "document") {
|
|
322
|
+
this._recordDiagnostic("response_error", "error", {
|
|
323
|
+
url,
|
|
324
|
+
status,
|
|
325
|
+
resourceType: response.request().resourceType(),
|
|
326
|
+
});
|
|
327
|
+
}
|
|
315
328
|
});
|
|
316
329
|
|
|
317
330
|
// Set up error handling
|
|
318
331
|
this.page.on("pageerror", (err) => {
|
|
319
332
|
const firstLine = (err.message || '').split('\n')[0].slice(0, 200);
|
|
333
|
+
this._recordDiagnostic("pageerror", "error", {
|
|
334
|
+
message: err.message || String(err),
|
|
335
|
+
});
|
|
320
336
|
this.logger(chalk.yellow(` [Page Error] ${firstLine}`));
|
|
321
337
|
});
|
|
338
|
+
this.page.on("console", (message) => {
|
|
339
|
+
const type = message.type();
|
|
340
|
+
const text = message.text();
|
|
341
|
+
const severity =
|
|
342
|
+
type === "error"
|
|
343
|
+
? "error"
|
|
344
|
+
: type === "warning"
|
|
345
|
+
? "warning"
|
|
346
|
+
: "info";
|
|
347
|
+
|
|
348
|
+
if (severity === "error" || severity === "warning") {
|
|
349
|
+
this._recordDiagnostic("console", severity, {
|
|
350
|
+
message: text,
|
|
351
|
+
consoleType: type,
|
|
352
|
+
cspViolation: /content security policy|csp/i.test(text),
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
this.page.on("requestfailed", (request) => {
|
|
357
|
+
this._recordDiagnostic("requestfailed", "error", {
|
|
358
|
+
url: request.url(),
|
|
359
|
+
method: request.method(),
|
|
360
|
+
resourceType: request.resourceType(),
|
|
361
|
+
message: request.failure()?.errorText || "Request failed",
|
|
362
|
+
});
|
|
363
|
+
});
|
|
322
364
|
|
|
323
365
|
this.logger(chalk.green(" ā Browser initialized"));
|
|
324
366
|
return this;
|
|
325
367
|
}
|
|
326
368
|
|
|
369
|
+
_recordDiagnostic(kind, severity, details = {}) {
|
|
370
|
+
this.diagnostics.push({
|
|
371
|
+
id: `${Date.now()}-${this.diagnostics.length + 1}`,
|
|
372
|
+
kind,
|
|
373
|
+
severity,
|
|
374
|
+
...details,
|
|
375
|
+
capturedAt: new Date().toISOString(),
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
getDiagnostics() {
|
|
380
|
+
return [...this.diagnostics];
|
|
381
|
+
}
|
|
382
|
+
|
|
327
383
|
/**
|
|
328
384
|
* Hide development overlays (Next.js devtools, Vercel toolbar, etc.)
|
|
329
385
|
* Injects CSS to hide common development UI elements before each navigation
|
|
@@ -512,10 +568,15 @@ class CaptureEngine {
|
|
|
512
568
|
}
|
|
513
569
|
|
|
514
570
|
// Check for auth redirect after navigation (URL patterns + HTTP 401/403)
|
|
571
|
+
// Skip the check if the scenario explicitly targeted this URL (e.g. capturing /login)
|
|
515
572
|
const currentUrl = this.page.url();
|
|
573
|
+
const targetPath = url.startsWith("http") ? new URL(url).pathname : url;
|
|
574
|
+
const currentPath = (() => { try { return new URL(currentUrl).pathname; } catch { return currentUrl; } })();
|
|
575
|
+
const isIntentionalTarget = currentPath === targetPath;
|
|
516
576
|
const isAuthRedirect =
|
|
517
|
-
|
|
518
|
-
this.
|
|
577
|
+
!isIntentionalTarget &&
|
|
578
|
+
(isAuthRedirectUrl(currentUrl, this._customAuthPatterns) ||
|
|
579
|
+
this._authResponseDetected);
|
|
519
580
|
if (isAuthRedirect) {
|
|
520
581
|
const errorMsg = `Auth redirect detected: navigated to ${currentUrl}. Session may be expired. Re-run \`reshot record\` to refresh session, or export a fresh Playwright storage state to .reshot/auth-state.json.`;
|
|
521
582
|
this.logger(chalk.red(` ā ${errorMsg}`));
|
|
@@ -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();
|
|
@@ -418,6 +439,14 @@ async function retryInteractiveStep(engine, action, params, context) {
|
|
|
418
439
|
}
|
|
419
440
|
}
|
|
420
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
|
+
|
|
421
450
|
/**
|
|
422
451
|
* Calculate a perceptual hash for an image buffer
|
|
423
452
|
* This is a simple hash based on resizing the image to a small grid
|
|
@@ -854,6 +883,7 @@ async function runScenarioWithStepByStepCapture(scenario, options = {}) {
|
|
|
854
883
|
waitForReady: scenario.waitForReady || null, // Custom loading-state hook
|
|
855
884
|
privacyConfig: hasPrivacy ? scenarioPrivacyConfig : null, // Privacy masking
|
|
856
885
|
styleConfig: hasStyle ? scenarioStyleConfig : null, // Image beautification
|
|
886
|
+
injectWorkspaceStore: scenario.needsWorkspaceInjection !== false,
|
|
857
887
|
logger: quiet ? () => {} : (msg) => console.log(msg),
|
|
858
888
|
});
|
|
859
889
|
|
|
@@ -1278,6 +1308,11 @@ async function runScenarioWithStepByStepCapture(scenario, options = {}) {
|
|
|
1278
1308
|
}
|
|
1279
1309
|
}
|
|
1280
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
|
+
|
|
1281
1316
|
// Wait for animations/transitions - longer wait for multi-step flows
|
|
1282
1317
|
const isMultiStep = script.length > 3;
|
|
1283
1318
|
await engine.page.waitForTimeout(isMultiStep ? 500 : 150);
|
|
@@ -1443,7 +1478,17 @@ async function runScenarioWithStepByStepCapture(scenario, options = {}) {
|
|
|
1443
1478
|
// Non-critical ā don't fail the capture
|
|
1444
1479
|
}
|
|
1445
1480
|
|
|
1446
|
-
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
|
+
};
|
|
1447
1492
|
})(); // End of scenarioExecution async IIFE
|
|
1448
1493
|
|
|
1449
1494
|
// Race scenario execution against timeout
|
|
@@ -1472,7 +1517,15 @@ async function runScenarioWithStepByStepCapture(scenario, options = {}) {
|
|
|
1472
1517
|
// Ignore
|
|
1473
1518
|
}
|
|
1474
1519
|
|
|
1475
|
-
return {
|
|
1520
|
+
return {
|
|
1521
|
+
success: false,
|
|
1522
|
+
error: error.message,
|
|
1523
|
+
assets,
|
|
1524
|
+
skippedSteps,
|
|
1525
|
+
failedSteps,
|
|
1526
|
+
retriedSteps,
|
|
1527
|
+
diagnostics: engine.getDiagnostics(),
|
|
1528
|
+
};
|
|
1476
1529
|
} finally {
|
|
1477
1530
|
await engine.close();
|
|
1478
1531
|
}
|
|
@@ -2606,6 +2659,7 @@ async function runScenarioWithEngine(scenario, options = {}) {
|
|
|
2606
2659
|
storageStatePath: hasSession ? sessionPath : null, // Use saved session if available
|
|
2607
2660
|
storageStateData, // Pre-loaded auth state
|
|
2608
2661
|
hideDevtools: true, // Always hide dev overlays in captures
|
|
2662
|
+
injectWorkspaceStore: scenario.needsWorkspaceInjection !== false,
|
|
2609
2663
|
logger: quiet ? () => {} : (msg) => console.log(msg),
|
|
2610
2664
|
});
|
|
2611
2665
|
|
|
@@ -2617,7 +2671,7 @@ async function runScenarioWithEngine(scenario, options = {}) {
|
|
|
2617
2671
|
chalk.green(`\n ā Scenario completed: ${assets.length} assets captured`)
|
|
2618
2672
|
);
|
|
2619
2673
|
|
|
2620
|
-
return { success: true, assets };
|
|
2674
|
+
return { success: true, assets, diagnostics: engine.getDiagnostics() };
|
|
2621
2675
|
} catch (error) {
|
|
2622
2676
|
console.error(chalk.red(`\n ā Scenario failed: ${error.message}`));
|
|
2623
2677
|
|
|
@@ -2637,7 +2691,7 @@ async function runScenarioWithEngine(scenario, options = {}) {
|
|
|
2637
2691
|
// Ignore screenshot errors
|
|
2638
2692
|
}
|
|
2639
2693
|
|
|
2640
|
-
return { success: false, error: error.message };
|
|
2694
|
+
return { success: false, error: error.message, diagnostics: engine.getDiagnostics() };
|
|
2641
2695
|
} finally {
|
|
2642
2696
|
await engine.close();
|
|
2643
2697
|
}
|
|
@@ -2882,7 +2936,14 @@ async function runAllScenarios(config, options = {}) {
|
|
|
2882
2936
|
// Run auth pre-flight check if any scenario requires auth
|
|
2883
2937
|
const captureConfig = getCaptureConfig(config.capture || {});
|
|
2884
2938
|
const scenarios = config.scenarios || [];
|
|
2885
|
-
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";
|
|
2886
2947
|
|
|
2887
2948
|
if (captureConfig.preflightCheck && hasAuthScenarios) {
|
|
2888
2949
|
const sessionPath = getDefaultSessionPath();
|
|
@@ -2893,6 +2954,7 @@ async function runAllScenarios(config, options = {}) {
|
|
|
2893
2954
|
{
|
|
2894
2955
|
storageStatePath: sessionPath,
|
|
2895
2956
|
viewport: config.viewport || { width: 1280, height: 720 },
|
|
2957
|
+
authCheckUrl,
|
|
2896
2958
|
}
|
|
2897
2959
|
);
|
|
2898
2960
|
if (!preflightResult.ok) {
|
|
@@ -3160,6 +3222,7 @@ async function runAllScenarios(config, options = {}) {
|
|
|
3160
3222
|
|
|
3161
3223
|
module.exports = {
|
|
3162
3224
|
convertLegacySteps,
|
|
3225
|
+
substituteUrlVariables,
|
|
3163
3226
|
runScenarioWithEngine,
|
|
3164
3227
|
runScenarioWithStepByStepCapture,
|
|
3165
3228
|
runScenarioWithVideoCapture,
|
|
@@ -3171,6 +3234,7 @@ module.exports = {
|
|
|
3171
3234
|
waitForVisualStability,
|
|
3172
3235
|
// Error detection & retry
|
|
3173
3236
|
retryInteractiveStep,
|
|
3237
|
+
promoteLastGotoUrl,
|
|
3174
3238
|
executeWithRetry,
|
|
3175
3239
|
preflightAuthCheck,
|
|
3176
3240
|
// New exports for output templating
|