@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
package/src/commands/setup.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
const chalk = require("chalk");
|
|
3
3
|
const oraModule = require("ora");
|
|
4
4
|
const ora = oraModule.default || oraModule;
|
|
5
|
+
const { execSync } = require("child_process");
|
|
5
6
|
|
|
6
7
|
const {
|
|
7
8
|
writeSettings,
|
|
@@ -100,10 +101,14 @@ async function setupCommand(options = {}) {
|
|
|
100
101
|
if (hasConfig && !force) {
|
|
101
102
|
console.log(chalk.green("✔ Configuration found:"), chalk.cyan("reshot.config.json"));
|
|
102
103
|
} else if (hasConfig && force) {
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
// Patch projectId in existing config instead of wiping scenarios
|
|
105
|
+
const fs = require("fs");
|
|
106
|
+
const path = require("path");
|
|
107
|
+
const configPath = path.join(process.cwd(), "reshot.config.json");
|
|
108
|
+
const existingConfig = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
109
|
+
existingConfig.projectId = existingSettings.projectId;
|
|
110
|
+
fs.writeFileSync(configPath, JSON.stringify(existingConfig, null, 2) + "\n");
|
|
111
|
+
console.log(chalk.green("✔ Configuration updated (projectId synced)"));
|
|
107
112
|
} else {
|
|
108
113
|
// No config - create it
|
|
109
114
|
console.log(chalk.gray("Creating reshot.config.json..."));
|
|
@@ -112,6 +117,29 @@ async function setupCommand(options = {}) {
|
|
|
112
117
|
console.log(chalk.green("✔ Configuration created:"), chalk.cyan("reshot.config.json"));
|
|
113
118
|
}
|
|
114
119
|
|
|
120
|
+
// Step 3.5: Ensure @reshotdev/screenshot is in devDependencies
|
|
121
|
+
const fs = require("fs");
|
|
122
|
+
const path = require("path");
|
|
123
|
+
const pkgJsonPath = path.join(process.cwd(), "package.json");
|
|
124
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
125
|
+
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
|
|
126
|
+
const hasDep =
|
|
127
|
+
pkgJson.devDependencies?.["@reshotdev/screenshot"] ||
|
|
128
|
+
pkgJson.dependencies?.["@reshotdev/screenshot"];
|
|
129
|
+
if (!hasDep) {
|
|
130
|
+
console.log(chalk.gray(" Adding @reshotdev/screenshot to devDependencies..."));
|
|
131
|
+
const usePnpm = fs.existsSync(path.join(process.cwd(), "pnpm-lock.yaml"));
|
|
132
|
+
const useYarn = fs.existsSync(path.join(process.cwd(), "yarn.lock"));
|
|
133
|
+
const cmd = usePnpm ? "pnpm add -D" : useYarn ? "yarn add -D" : "npm install -D";
|
|
134
|
+
try {
|
|
135
|
+
execSync(`${cmd} @reshotdev/screenshot`, { stdio: "inherit" });
|
|
136
|
+
console.log(chalk.green(" ✔ Added @reshotdev/screenshot to devDependencies"));
|
|
137
|
+
} catch {
|
|
138
|
+
console.log(chalk.yellow(" ⚠ Could not auto-install. Run manually: " + cmd + " @reshotdev/screenshot"));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
115
143
|
// Step 4: Success summary
|
|
116
144
|
console.log(chalk.green("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"));
|
|
117
145
|
console.log(chalk.green.bold("✔ Project initialized successfully!"));
|
|
@@ -128,9 +156,9 @@ async function setupCommand(options = {}) {
|
|
|
128
156
|
await uiCommand({ open: true });
|
|
129
157
|
} else {
|
|
130
158
|
console.log(chalk.gray("Next steps:"));
|
|
131
|
-
console.log(chalk.gray(" • Run"), chalk.cyan("reshot
|
|
132
|
-
console.log(chalk.gray(" • Run"), chalk.cyan("reshot
|
|
133
|
-
console.log(chalk.gray(" • Run"), chalk.cyan("reshot
|
|
159
|
+
console.log(chalk.gray(" • Run"), chalk.cyan("reshot run"), chalk.gray("to generate your first local capture"));
|
|
160
|
+
console.log(chalk.gray(" • Run"), chalk.cyan("reshot publish"), chalk.gray("when you are ready for hosted assets and review workflows"));
|
|
161
|
+
console.log(chalk.gray(" • Run"), chalk.cyan("reshot studio"), chalk.gray("to inspect output locally\n"));
|
|
134
162
|
}
|
|
135
163
|
}
|
|
136
164
|
|
|
@@ -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,19 +153,96 @@ 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")
|
|
223
|
+
.option("--refresh-session", "Only refresh the auth session (no recording prompts)")
|
|
163
224
|
.option("--debug", "Enable verbose debug logging")
|
|
164
225
|
.action(async (title, options) => {
|
|
165
226
|
if (options.debug) {
|
|
166
227
|
process.env.RESHOT_DEBUG = "1";
|
|
167
228
|
}
|
|
229
|
+
const resolvedTitle = title || options.name;
|
|
168
230
|
try {
|
|
231
|
+
// If --refresh-session, just sync the session and exit
|
|
232
|
+
if (options.refreshSession) {
|
|
233
|
+
const { autoSyncSessionFromCDP, getDefaultSessionPath } = require("./lib/record-cdp");
|
|
234
|
+
const sessionPath = getDefaultSessionPath();
|
|
235
|
+
console.log(chalk.gray(" Syncing session from active browser..."));
|
|
236
|
+
const result = await autoSyncSessionFromCDP(sessionPath);
|
|
237
|
+
if (result.synced) {
|
|
238
|
+
console.log(chalk.green(" ✔ Session refreshed at " + sessionPath));
|
|
239
|
+
} else {
|
|
240
|
+
console.log(chalk.yellow(" ⚠ No active CDP browser found. Launch Chrome with remote debugging first:"));
|
|
241
|
+
console.log(chalk.gray(" reshot record --browser --refresh-session"));
|
|
242
|
+
}
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
169
246
|
// If --browser flag, launch Chrome first
|
|
170
247
|
if (options.browser) {
|
|
171
248
|
const chromeCommand = require("./commands/chrome");
|
|
@@ -178,7 +255,7 @@ program
|
|
|
178
255
|
}
|
|
179
256
|
|
|
180
257
|
const recordCommand = require("./commands/record");
|
|
181
|
-
await recordCommand(
|
|
258
|
+
await recordCommand(resolvedTitle);
|
|
182
259
|
} catch (error) {
|
|
183
260
|
console.error(chalk.red("Error:"), error.message);
|
|
184
261
|
if (options.debug && error.stack) {
|
|
@@ -259,6 +336,62 @@ program
|
|
|
259
336
|
}
|
|
260
337
|
});
|
|
261
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
|
+
|
|
262
395
|
// CI: CI/CD integration commands
|
|
263
396
|
const ciCommand = program
|
|
264
397
|
.command("ci")
|
|
@@ -337,9 +470,22 @@ Actions:
|
|
|
337
470
|
// ============================================================================
|
|
338
471
|
|
|
339
472
|
// Auth: Standalone authentication (for re-auth scenarios)
|
|
340
|
-
program
|
|
473
|
+
const auth = program
|
|
341
474
|
.command("auth")
|
|
342
|
-
.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`")
|
|
343
489
|
.action(async () => {
|
|
344
490
|
try {
|
|
345
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}`));
|