@reshotdev/screenshot 0.0.1-beta.2 → 0.0.1-beta.20
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 +1 -1
- package/README.md +138 -47
- package/package.json +27 -16
- package/src/commands/auth.js +159 -30
- package/src/commands/capture-dom.js +50 -0
- package/src/commands/certify.js +62 -0
- package/src/commands/compose.js +220 -0
- package/src/commands/doctor-release.js +74 -0
- package/src/commands/doctor-target.js +108 -0
- package/src/commands/drifts.js +16 -69
- package/src/commands/import-tests.js +13 -13
- package/src/commands/init.js +16 -277
- package/src/commands/publish.js +484 -257
- package/src/commands/pull.js +302 -35
- package/src/commands/refresh.js +166 -0
- package/src/commands/run.js +292 -12
- package/src/commands/setup-wizard.js +348 -496
- package/src/commands/status.js +334 -126
- package/src/commands/sync.js +28 -236
- package/src/commands/ui.js +1 -1
- package/src/commands/variation.js +194 -0
- package/src/commands/verify-publish.js +46 -0
- package/src/index.js +383 -118
- package/src/lib/api-client.js +172 -60
- package/src/lib/auto-update/refresh.js +598 -0
- package/src/lib/auto-update/scene-runtime.compose.tsx +73 -0
- package/src/lib/auto-update/spec.js +89 -0
- package/src/lib/capture-engine.js +179 -9
- package/src/lib/capture-script-runner.js +639 -214
- package/src/lib/certification.js +887 -0
- package/src/lib/compose-context.js +156 -0
- package/src/lib/compose-pack.js +42 -0
- package/src/lib/compose-runtime.js +34 -0
- package/src/lib/compose-upload.js +142 -0
- package/src/lib/config.js +186 -81
- package/src/lib/dom-capture.js +64 -0
- package/src/lib/ensure-browser.js +147 -0
- package/src/lib/output-path-template.js +3 -3
- package/src/lib/record-cdp.js +288 -16
- package/src/lib/record-clip.js +83 -3
- package/src/lib/record-config.js +1 -5
- package/src/lib/release-doctor.js +321 -0
- package/src/lib/resolve-targets.js +60 -0
- package/src/lib/run-manifest.js +148 -0
- package/src/lib/standalone-mode.js +1 -1
- package/src/lib/storage-providers.js +5 -5
- package/src/lib/style-engine.js +5 -5
- package/src/lib/target-contract.js +292 -0
- package/src/lib/ui-api-helpers.js +118 -0
- package/src/lib/ui-api.js +31 -824
- package/src/lib/ui-asset-cleanup.js +62 -0
- package/src/lib/ui-output-versions.js +165 -0
- package/src/lib/ui-recorder-routes.js +341 -0
- package/src/lib/ui-scenario-metadata.js +161 -0
- package/vendor/compose/dist/auto-update.cjs +5544 -0
- package/vendor/compose/dist/auto-update.mjs +5518 -0
- package/vendor/compose/dist/capture.cjs +1450 -0
- package/vendor/compose/dist/capture.mjs +1416 -0
- package/vendor/compose/dist/eligibility.cjs +5331 -0
- package/vendor/compose/dist/eligibility.mjs +5313 -0
- package/vendor/compose/dist/index.cjs +2046 -0
- package/vendor/compose/dist/index.mjs +1997 -0
- package/vendor/compose/dist/jsx-dev-runtime.cjs +55 -0
- package/vendor/compose/dist/jsx-dev-runtime.mjs +27 -0
- package/vendor/compose/dist/jsx-runtime.cjs +58 -0
- package/vendor/compose/dist/jsx-runtime.mjs +31 -0
- package/vendor/compose/dist/render.cjs +558 -0
- package/vendor/compose/dist/render.mjs +515 -0
- package/vendor/compose/dist/verify-cli.cjs +3806 -0
- package/vendor/compose/dist/verify-cli.mjs +3812 -0
- package/vendor/compose/dist/verify.cjs +3880 -0
- package/vendor/compose/dist/verify.mjs +3858 -0
- package/web/manager/dist/assets/index-D0S2otug.js +507 -0
- package/web/manager/dist/index.html +1 -1
- package/src/commands/ci-run.js +0 -123
- package/src/commands/ci-setup.js +0 -288
- package/src/commands/ingest.js +0 -458
- package/src/commands/setup.js +0 -137
- package/src/commands/validate-docs.js +0 -529
- package/src/lib/playwright-runner.js +0 -252
- package/web/manager/dist/assets/index--ZgioErz.js +0 -507
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Phase 5 auto-update — local per-composition state ("the stored spec").
|
|
2
|
+
//
|
|
3
|
+
// A composition's source screen (URL + viewport), the accepted baseline's
|
|
4
|
+
// structure signature + reference frame, and any outstanding flagged candidate
|
|
5
|
+
// live on disk so a CI `reshot refresh` is reproducible and idempotent. The
|
|
6
|
+
// reference frame is the prior ACCEPTED render's recapture; it advances only when
|
|
7
|
+
// a refresh publishes, so a flagged redesign leaves the old baseline (and the old
|
|
8
|
+
// live clip) untouched.
|
|
9
|
+
|
|
10
|
+
const fs = require("fs-extra");
|
|
11
|
+
const path = require("path");
|
|
12
|
+
|
|
13
|
+
function baseDir() {
|
|
14
|
+
return (
|
|
15
|
+
process.env.RESHOT_AUTO_UPDATE_DIR ||
|
|
16
|
+
path.join(process.cwd(), ".reshot", "auto-update")
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function specDir(compositionId) {
|
|
21
|
+
return path.join(baseDir(), compositionId);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function specPath(compositionId) {
|
|
25
|
+
return path.join(specDir(compositionId), "spec.json");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function referencePngPath(compositionId) {
|
|
29
|
+
return path.join(specDir(compositionId), "reference.png");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function writeSpec(spec) {
|
|
33
|
+
if (!spec || !spec.compositionId) {
|
|
34
|
+
throw new Error("writeSpec requires spec.compositionId");
|
|
35
|
+
}
|
|
36
|
+
await fs.ensureDir(specDir(spec.compositionId));
|
|
37
|
+
await fs.writeJson(specPath(spec.compositionId), spec, { spaces: 2 });
|
|
38
|
+
return spec;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function readSpec(compositionId) {
|
|
42
|
+
const file = specPath(compositionId);
|
|
43
|
+
if (!(await fs.pathExists(file))) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`No auto-update spec for composition ${compositionId} at ${file}. ` +
|
|
46
|
+
"Register it with `reshot refresh --register` (or seed it) first.",
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return fs.readJson(file);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function listSpecs(projectId) {
|
|
53
|
+
const dir = baseDir();
|
|
54
|
+
if (!(await fs.pathExists(dir))) return [];
|
|
55
|
+
const entries = await fs.readdir(dir);
|
|
56
|
+
const specs = [];
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
const file = specPath(entry);
|
|
59
|
+
if (!(await fs.pathExists(file))) continue;
|
|
60
|
+
const spec = await fs.readJson(file);
|
|
61
|
+
if (projectId && spec.projectId !== projectId) continue;
|
|
62
|
+
specs.push(spec);
|
|
63
|
+
}
|
|
64
|
+
// Stable order so CI summaries and idempotence checks are deterministic.
|
|
65
|
+
return specs.sort((a, b) => String(a.slug).localeCompare(String(b.slug)));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function readReferencePng(compositionId) {
|
|
69
|
+
const file = referencePngPath(compositionId);
|
|
70
|
+
if (!(await fs.pathExists(file))) return null;
|
|
71
|
+
return fs.readFile(file);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function writeReferencePng(compositionId, buffer) {
|
|
75
|
+
await fs.ensureDir(specDir(compositionId));
|
|
76
|
+
await fs.writeFile(referencePngPath(compositionId), buffer);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = {
|
|
80
|
+
baseDir,
|
|
81
|
+
specDir,
|
|
82
|
+
specPath,
|
|
83
|
+
referencePngPath,
|
|
84
|
+
writeSpec,
|
|
85
|
+
readSpec,
|
|
86
|
+
listSpecs,
|
|
87
|
+
readReferencePng,
|
|
88
|
+
writeReferencePng,
|
|
89
|
+
};
|
|
@@ -6,6 +6,7 @@ const path = require("path");
|
|
|
6
6
|
const fs = require("fs-extra");
|
|
7
7
|
const chalk = require("chalk");
|
|
8
8
|
const { buildLaunchOptions } = require("./ci-detect");
|
|
9
|
+
const { launchChromium } = require("./ensure-browser");
|
|
9
10
|
const {
|
|
10
11
|
applyVariantToPage,
|
|
11
12
|
applyStorageAndReload,
|
|
@@ -20,7 +21,7 @@ const {
|
|
|
20
21
|
scaleRegionByDPR,
|
|
21
22
|
isSharpAvailable,
|
|
22
23
|
} = require("./image-crop");
|
|
23
|
-
const { sanitizeStorageState } = require("./record-cdp");
|
|
24
|
+
const { sanitizeStorageState, assessSessionHealth } = require("./record-cdp");
|
|
24
25
|
const {
|
|
25
26
|
injectPrivacyMasking,
|
|
26
27
|
removePrivacyMasking,
|
|
@@ -98,6 +99,9 @@ class CaptureEngine {
|
|
|
98
99
|
this.capturedAssets = [];
|
|
99
100
|
this.logger = options.logger || console.log;
|
|
100
101
|
this.headless = options.headless !== false; // Default to headless
|
|
102
|
+
this.injectWorkspaceStore = options.injectWorkspaceStore !== false;
|
|
103
|
+
this.diagnostics = [];
|
|
104
|
+
this.sessionHealth = null;
|
|
101
105
|
|
|
102
106
|
// Storage state path for authenticated sessions
|
|
103
107
|
// If provided, loads cookies/localStorage from file to preserve auth state
|
|
@@ -142,6 +146,13 @@ class CaptureEngine {
|
|
|
142
146
|
// Style configuration (image beautification post-capture)
|
|
143
147
|
this.styleConfig = options.styleConfig || null;
|
|
144
148
|
|
|
149
|
+
// DOM scene (MHTML) capture toggle — emits a self-contained Chromium
|
|
150
|
+
// MHTML bundle alongside each PNG so variations can be rendered from
|
|
151
|
+
// the captured DOM without re-running the live app. Defaults to ON;
|
|
152
|
+
// opt out via reshot.config.json -> { domScene: false } or per-scenario
|
|
153
|
+
// -> { domScene: false }.
|
|
154
|
+
this.domSceneEnabled = options.domScene !== false;
|
|
155
|
+
|
|
145
156
|
// Legacy support for old variant format
|
|
146
157
|
if (!this.variantConfig && options.variant) {
|
|
147
158
|
this.variantConfig = this._convertLegacyVariant(options.variant);
|
|
@@ -226,6 +237,36 @@ class CaptureEngine {
|
|
|
226
237
|
}
|
|
227
238
|
this.logger(chalk.gray(` → Using pre-loaded auth session`));
|
|
228
239
|
} else if (this.storageStatePath && fs.existsSync(this.storageStatePath)) {
|
|
240
|
+
this.sessionHealth = assessSessionHealth(
|
|
241
|
+
this.storageStatePath,
|
|
242
|
+
this.baseUrl,
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
if (!this.sessionHealth.compatible) {
|
|
246
|
+
this.logger(
|
|
247
|
+
chalk.yellow(
|
|
248
|
+
` ⚠ Skipping cached auth session because it does not match ${this.baseUrl || "the current target"}`,
|
|
249
|
+
),
|
|
250
|
+
);
|
|
251
|
+
for (const issue of this.sessionHealth.issues) {
|
|
252
|
+
this.logger(chalk.gray(` ${issue}`));
|
|
253
|
+
}
|
|
254
|
+
this.diagnostics.push({
|
|
255
|
+
type: "auth-session-mismatch",
|
|
256
|
+
sessionPath: this.storageStatePath,
|
|
257
|
+
issues: [...this.sessionHealth.issues],
|
|
258
|
+
});
|
|
259
|
+
return contextOptions;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (this.sessionHealth.stale) {
|
|
263
|
+
this.logger(
|
|
264
|
+
chalk.yellow(
|
|
265
|
+
` ⚠ Cached auth session is ${this.sessionHealth.ageMinutes}m old; validating it during startup.`,
|
|
266
|
+
),
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
229
270
|
// Read and sanitize instead of passing raw file path (Playwright would read it unsanitized)
|
|
230
271
|
try {
|
|
231
272
|
const rawState = JSON.parse(fs.readFileSync(this.storageStatePath, "utf-8"));
|
|
@@ -245,6 +286,10 @@ class CaptureEngine {
|
|
|
245
286
|
)}`
|
|
246
287
|
)
|
|
247
288
|
);
|
|
289
|
+
|
|
290
|
+
for (const warning of this.sessionHealth.warnings) {
|
|
291
|
+
this.logger(chalk.gray(` ${warning}`));
|
|
292
|
+
}
|
|
248
293
|
}
|
|
249
294
|
|
|
250
295
|
return contextOptions;
|
|
@@ -255,9 +300,9 @@ class CaptureEngine {
|
|
|
255
300
|
|
|
256
301
|
const contextOptions = this._buildContextOptions();
|
|
257
302
|
|
|
258
|
-
this.browser = await chromium
|
|
303
|
+
this.browser = await launchChromium(chromium, buildLaunchOptions({
|
|
259
304
|
headless: this.headless,
|
|
260
|
-
}));
|
|
305
|
+
}), this.logger);
|
|
261
306
|
this.context = await this.browser.newContext(contextOptions);
|
|
262
307
|
this.page = await this.context.newPage();
|
|
263
308
|
|
|
@@ -289,7 +334,9 @@ class CaptureEngine {
|
|
|
289
334
|
logVariantSummary(this.variantConfig, this.logger);
|
|
290
335
|
}
|
|
291
336
|
|
|
292
|
-
|
|
337
|
+
if (this.injectWorkspaceStore) {
|
|
338
|
+
await this._injectWorkspaceStore();
|
|
339
|
+
}
|
|
293
340
|
|
|
294
341
|
// Inject privacy masking CSS (after variant injection, before captures)
|
|
295
342
|
this._privacyInjectionOk = true;
|
|
@@ -306,24 +353,76 @@ class CaptureEngine {
|
|
|
306
353
|
this._authResponseDetected = false;
|
|
307
354
|
this.page.on("response", (response) => {
|
|
308
355
|
const status = response.status();
|
|
356
|
+
const url = response.url();
|
|
309
357
|
if (
|
|
310
358
|
(status === 401 || status === 403) &&
|
|
311
359
|
response.request().resourceType() === "document"
|
|
312
360
|
) {
|
|
313
361
|
this._authResponseDetected = true;
|
|
314
362
|
}
|
|
363
|
+
|
|
364
|
+
if (status >= 400 && response.request().resourceType() === "document") {
|
|
365
|
+
this._recordDiagnostic("response_error", "error", {
|
|
366
|
+
url,
|
|
367
|
+
status,
|
|
368
|
+
resourceType: response.request().resourceType(),
|
|
369
|
+
});
|
|
370
|
+
}
|
|
315
371
|
});
|
|
316
372
|
|
|
317
373
|
// Set up error handling
|
|
318
374
|
this.page.on("pageerror", (err) => {
|
|
319
375
|
const firstLine = (err.message || '').split('\n')[0].slice(0, 200);
|
|
376
|
+
this._recordDiagnostic("pageerror", "error", {
|
|
377
|
+
message: err.message || String(err),
|
|
378
|
+
});
|
|
320
379
|
this.logger(chalk.yellow(` [Page Error] ${firstLine}`));
|
|
321
380
|
});
|
|
381
|
+
this.page.on("console", (message) => {
|
|
382
|
+
const type = message.type();
|
|
383
|
+
const text = message.text();
|
|
384
|
+
const severity =
|
|
385
|
+
type === "error"
|
|
386
|
+
? "error"
|
|
387
|
+
: type === "warning"
|
|
388
|
+
? "warning"
|
|
389
|
+
: "info";
|
|
390
|
+
|
|
391
|
+
if (severity === "error" || severity === "warning") {
|
|
392
|
+
this._recordDiagnostic("console", severity, {
|
|
393
|
+
message: text,
|
|
394
|
+
consoleType: type,
|
|
395
|
+
cspViolation: /content security policy|csp/i.test(text),
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
this.page.on("requestfailed", (request) => {
|
|
400
|
+
this._recordDiagnostic("requestfailed", "error", {
|
|
401
|
+
url: request.url(),
|
|
402
|
+
method: request.method(),
|
|
403
|
+
resourceType: request.resourceType(),
|
|
404
|
+
message: request.failure()?.errorText || "Request failed",
|
|
405
|
+
});
|
|
406
|
+
});
|
|
322
407
|
|
|
323
408
|
this.logger(chalk.green(" ✔ Browser initialized"));
|
|
324
409
|
return this;
|
|
325
410
|
}
|
|
326
411
|
|
|
412
|
+
_recordDiagnostic(kind, severity, details = {}) {
|
|
413
|
+
this.diagnostics.push({
|
|
414
|
+
id: `${Date.now()}-${this.diagnostics.length + 1}`,
|
|
415
|
+
kind,
|
|
416
|
+
severity,
|
|
417
|
+
...details,
|
|
418
|
+
capturedAt: new Date().toISOString(),
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
getDiagnostics() {
|
|
423
|
+
return [...this.diagnostics];
|
|
424
|
+
}
|
|
425
|
+
|
|
327
426
|
/**
|
|
328
427
|
* Hide development overlays (Next.js devtools, Vercel toolbar, etc.)
|
|
329
428
|
* Injects CSS to hide common development UI elements before each navigation
|
|
@@ -412,11 +511,11 @@ class CaptureEngine {
|
|
|
412
511
|
projectId = settings.urlVariables?.PROJECT_ID;
|
|
413
512
|
// 2. Check settings projectId
|
|
414
513
|
if (!projectId) projectId = settings.projectId;
|
|
415
|
-
// 3. Check
|
|
514
|
+
// 3. Check reshot.config.json urlVariables
|
|
416
515
|
if (!projectId) {
|
|
417
516
|
try {
|
|
418
|
-
const
|
|
419
|
-
projectId =
|
|
517
|
+
const reshotConfig = config.readConfig() || {};
|
|
518
|
+
projectId = reshotConfig.urlVariables?.PROJECT_ID;
|
|
420
519
|
} catch (_e) {
|
|
421
520
|
// Config may not exist
|
|
422
521
|
}
|
|
@@ -512,10 +611,15 @@ class CaptureEngine {
|
|
|
512
611
|
}
|
|
513
612
|
|
|
514
613
|
// Check for auth redirect after navigation (URL patterns + HTTP 401/403)
|
|
614
|
+
// Skip the check if the scenario explicitly targeted this URL (e.g. capturing /login)
|
|
515
615
|
const currentUrl = this.page.url();
|
|
616
|
+
const targetPath = url.startsWith("http") ? new URL(url).pathname : url;
|
|
617
|
+
const currentPath = (() => { try { return new URL(currentUrl).pathname; } catch { return currentUrl; } })();
|
|
618
|
+
const isIntentionalTarget = currentPath === targetPath;
|
|
516
619
|
const isAuthRedirect =
|
|
517
|
-
|
|
518
|
-
this.
|
|
620
|
+
!isIntentionalTarget &&
|
|
621
|
+
(isAuthRedirectUrl(currentUrl, this._customAuthPatterns) ||
|
|
622
|
+
this._authResponseDetected);
|
|
519
623
|
if (isAuthRedirect) {
|
|
520
624
|
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
625
|
this.logger(chalk.red(` ✖ ${errorMsg}`));
|
|
@@ -527,6 +631,32 @@ class CaptureEngine {
|
|
|
527
631
|
// Wait for network to settle
|
|
528
632
|
await this._waitForStability();
|
|
529
633
|
|
|
634
|
+
// Post-stability auth redirect check: catches SPA redirects that happen
|
|
635
|
+
// after client-side JS has executed (the pre-stability check at domcontentloaded
|
|
636
|
+
// may miss these since JS hasn't finished routing yet)
|
|
637
|
+
if (!isIntentionalTarget) {
|
|
638
|
+
const postStabilityUrl = this.page.url();
|
|
639
|
+
const postStabilityRedirect =
|
|
640
|
+
isAuthRedirectUrl(postStabilityUrl, this._customAuthPatterns) ||
|
|
641
|
+
this._authResponseDetected;
|
|
642
|
+
if (postStabilityRedirect) {
|
|
643
|
+
const errorMsg = `Auth redirect detected after page load: navigated to ${postStabilityUrl}. Session may be expired. Re-run \`reshot record\` to refresh session.`;
|
|
644
|
+
this.logger(chalk.red(` ✖ ${errorMsg}`));
|
|
645
|
+
throw new Error(errorMsg);
|
|
646
|
+
}
|
|
647
|
+
// Also check DOM for login forms (SPA may render login UI without changing URL)
|
|
648
|
+
const hasLoginForm = await this.page.evaluate(() => {
|
|
649
|
+
const h = document.querySelector("h1, h2");
|
|
650
|
+
return h && /sign\s*in|log\s*in/i.test(h.textContent);
|
|
651
|
+
}).catch(() => false);
|
|
652
|
+
if (hasLoginForm) {
|
|
653
|
+
const errorMsg = `Login form detected after page load at ${postStabilityUrl}. Session may be expired. Re-run \`reshot record\` to refresh session.`;
|
|
654
|
+
this.logger(chalk.red(` ✖ ${errorMsg}`));
|
|
655
|
+
throw new Error(errorMsg);
|
|
656
|
+
}
|
|
657
|
+
this._authResponseDetected = false;
|
|
658
|
+
}
|
|
659
|
+
|
|
530
660
|
// Additional wait for theme/variants to fully apply
|
|
531
661
|
// This handles CSS transitions and async re-renders
|
|
532
662
|
if (this.variantConfig && this.variantConfig.injections?.length > 0) {
|
|
@@ -1011,10 +1141,50 @@ class CaptureEngine {
|
|
|
1011
1141
|
// Write the final buffer to file
|
|
1012
1142
|
await fs.writeFile(outputPath, finalBuffer);
|
|
1013
1143
|
|
|
1144
|
+
// ── DOM scene capture (sidecar MHTML) ──────────────────────────────
|
|
1145
|
+
// Capture a self-contained MHTML bundle of the page at the same moment
|
|
1146
|
+
// as the PNG. The bundle re-renders in any Chromium browser without
|
|
1147
|
+
// network access and is the source of truth for variations: marketing
|
|
1148
|
+
// can mutate the captured DOM (swap copy, hide chrome, recrop, rebrand
|
|
1149
|
+
// tenant names) and render new outputs without re-running Playwright
|
|
1150
|
+
// against the live app. Opt out per-scenario or globally via
|
|
1151
|
+
// reshot.config.json -> { domScene: false }.
|
|
1152
|
+
//
|
|
1153
|
+
// The MHTML capture is best-effort — failures are logged but do not
|
|
1154
|
+
// fail the scenario, since the primary PNG already succeeded.
|
|
1155
|
+
let domScenePath = null;
|
|
1156
|
+
let domSceneBytes = null;
|
|
1157
|
+
if (this.domSceneEnabled) {
|
|
1158
|
+
try {
|
|
1159
|
+
const cdp = await this.page.context().newCDPSession(this.page);
|
|
1160
|
+
const { data: mhtml } = await cdp.send("Page.captureSnapshot", {
|
|
1161
|
+
format: "mhtml",
|
|
1162
|
+
});
|
|
1163
|
+
domScenePath = outputPath.replace(/\.png$/i, ".mhtml");
|
|
1164
|
+
await fs.writeFile(domScenePath, mhtml);
|
|
1165
|
+
domSceneBytes = Buffer.byteLength(mhtml, "utf8");
|
|
1166
|
+
this.logger(
|
|
1167
|
+
chalk.gray(
|
|
1168
|
+
` ✓ DOM scene: ${path.basename(domScenePath)} (${(domSceneBytes / 1024).toFixed(0)} KB)`,
|
|
1169
|
+
),
|
|
1170
|
+
);
|
|
1171
|
+
} catch (err) {
|
|
1172
|
+
this.logger(
|
|
1173
|
+
chalk.yellow(
|
|
1174
|
+
` ⚠ DOM scene capture skipped: ${err.message || err}`,
|
|
1175
|
+
),
|
|
1176
|
+
);
|
|
1177
|
+
domScenePath = null;
|
|
1178
|
+
domSceneBytes = null;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1014
1182
|
// Record asset metadata
|
|
1015
1183
|
this.capturedAssets.push({
|
|
1016
1184
|
name,
|
|
1017
1185
|
path: outputPath,
|
|
1186
|
+
domScenePath,
|
|
1187
|
+
domSceneBytes,
|
|
1018
1188
|
description,
|
|
1019
1189
|
capturedAt: new Date().toISOString(),
|
|
1020
1190
|
viewport: this.viewport,
|