@afixt/screenshot-utils 1.0.0 → 1.1.0
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/CHANGELOG.md +36 -2
- package/dist/index.js +169 -14
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +169 -14
- package/dist/index.mjs.map +1 -1
- package/dist/types/capture/element.d.ts.map +1 -1
- package/dist/types/capture/page.d.ts.map +1 -1
- package/dist/types/capture/settle.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/capture/element.js +45 -2
- package/src/capture/page.js +31 -10
- package/src/capture/settle.js +136 -1
- package/src/index.js +3 -0
- package/src/types/index.d.ts +10 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"element.d.ts","sourceRoot":"","sources":["../../../src/capture/element.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"element.d.ts","sourceRoot":"","sources":["../../../src/capture/element.js"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;GAiBG;AACH,wCAJW,OAAO,qBAAqB,EAAE,WAAW,YACzC,OAAO,qBAAqB,EAAE,qBAAqB,GACjD,OAAO,CAAC,OAAO,qBAAqB,EAAE,oBAAoB,CAAC,CA4HvE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"page.d.ts","sourceRoot":"","sources":["../../../src/capture/page.js"],"names":[],"mappings":"AAKA;;;;;;;;;;;GAWG;AACH,qCAJW,OAAO,qBAAqB,EAAE,WAAW,YACzC,OAAO,qBAAqB,EAAE,cAAc,GAC1C,OAAO,CAAC,OAAO,qBAAqB,EAAE,aAAa,CAAC,
|
|
1
|
+
{"version":3,"file":"page.d.ts","sourceRoot":"","sources":["../../../src/capture/page.js"],"names":[],"mappings":"AAKA;;;;;;;;;;;GAWG;AACH,qCAJW,OAAO,qBAAqB,EAAE,WAAW,YACzC,OAAO,qBAAqB,EAAE,cAAc,GAC1C,OAAO,CAAC,OAAO,qBAAqB,EAAE,aAAa,CAAC,CA0EhE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settle.d.ts","sourceRoot":"","sources":["../../../src/capture/settle.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AACH,uCAJW,OAAO,qBAAqB,EAAE,WAAW,YACzC,OAAO,qBAAqB,EAAE,oBAAoB,GAChD,OAAO,CAAC,OAAO,qBAAqB,EAAE,eAAe,CAAC,
|
|
1
|
+
{"version":3,"file":"settle.d.ts","sourceRoot":"","sources":["../../../src/capture/settle.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AACH,uCAJW,OAAO,qBAAqB,EAAE,WAAW,YACzC,OAAO,qBAAqB,EAAE,oBAAoB,GAChD,OAAO,CAAC,OAAO,qBAAqB,EAAE,eAAe,CAAC,CA2BlE"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@afixt/screenshot-utils",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Screenshot capture, transform, compose, tile, and redact utilities for the AFixt/Revenant accessibility platform",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"access": "restricted"
|
|
84
84
|
},
|
|
85
85
|
"engines": {
|
|
86
|
-
"node": ">=20.
|
|
86
|
+
"node": ">=20.9.0"
|
|
87
87
|
},
|
|
88
88
|
"keywords": [
|
|
89
89
|
"screenshot",
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
"revenant"
|
|
102
102
|
],
|
|
103
103
|
"dependencies": {
|
|
104
|
-
"sharp": "0.
|
|
104
|
+
"sharp": "^0.35.3"
|
|
105
105
|
},
|
|
106
106
|
"peerDependencies": {
|
|
107
107
|
"@napi-rs/canvas": "^0.1.0",
|
package/src/capture/element.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { CaptureTimeoutError } = require('../errors/index.js');
|
|
4
|
+
const { waitForStable } = require('./settle.js');
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Capture a single element by selector or xpath.
|
|
@@ -31,6 +32,7 @@ async function captureElement(adapter, options = {}) {
|
|
|
31
32
|
timeout = 5000,
|
|
32
33
|
format = 'png',
|
|
33
34
|
quality = 90,
|
|
35
|
+
waitForStable: settleOpts = false,
|
|
34
36
|
events = null,
|
|
35
37
|
} = options;
|
|
36
38
|
|
|
@@ -77,6 +79,7 @@ async function captureElement(adapter, options = {}) {
|
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
let viewportRect = lookup.viewportRect;
|
|
82
|
+
let documentRect = lookup.documentRect;
|
|
80
83
|
if (scrollIntoView) {
|
|
81
84
|
await adapter.scrollTo({
|
|
82
85
|
x: 0,
|
|
@@ -93,6 +96,17 @@ async function captureElement(adapter, options = {}) {
|
|
|
93
96
|
}
|
|
94
97
|
}
|
|
95
98
|
|
|
99
|
+
// Optional render-stability wait. Critically, we re-read the element rect
|
|
100
|
+
// *after* settling: late reflow (lazy images, web fonts, JS layout) is the
|
|
101
|
+
// cause of blank/wrong-region element crops, so coordinates captured before
|
|
102
|
+
// settle cannot be trusted for the clip.
|
|
103
|
+
const settled = await settleAndReread(adapter, { selector, xpath, timeout, settleOpts });
|
|
104
|
+
const settleMs = settled.settleMs;
|
|
105
|
+
if (settled.rect) {
|
|
106
|
+
viewportRect = settled.rect.viewportRect;
|
|
107
|
+
documentRect = settled.rect.documentRect;
|
|
108
|
+
}
|
|
109
|
+
|
|
96
110
|
const clip = {
|
|
97
111
|
x: Math.max(0, Math.round(viewportRect.x - padding)),
|
|
98
112
|
y: Math.max(0, Math.round(viewportRect.y - padding)),
|
|
@@ -117,12 +131,12 @@ async function captureElement(adapter, options = {}) {
|
|
|
117
131
|
format,
|
|
118
132
|
bytes,
|
|
119
133
|
page: metrics,
|
|
120
|
-
timing: { settleMs
|
|
134
|
+
timing: { settleMs, captureMs, encodeMs: 0, totalMs },
|
|
121
135
|
warnings: [],
|
|
122
136
|
element: {
|
|
123
137
|
selector: selector ?? '',
|
|
124
138
|
xpath: xpath ?? '',
|
|
125
|
-
documentRect
|
|
139
|
+
documentRect,
|
|
126
140
|
viewportRect,
|
|
127
141
|
verified: Boolean(snippet),
|
|
128
142
|
},
|
|
@@ -131,6 +145,35 @@ async function captureElement(adapter, options = {}) {
|
|
|
131
145
|
return result;
|
|
132
146
|
}
|
|
133
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Run waitForStable (when requested) and re-read the element rect afterwards.
|
|
150
|
+
* Kept out of `captureElement` so the main flow stays flat.
|
|
151
|
+
*
|
|
152
|
+
* @param {import('../types/index.d.ts').PageAdapter} adapter
|
|
153
|
+
* @param {{selector?: string, xpath?: string, timeout: number, settleOpts: import('../types/index.d.ts').WaitForStableOptions|boolean}} ctx
|
|
154
|
+
* @returns {Promise<{settleMs: number, rect: {viewportRect: import('../types/index.d.ts').Rect, documentRect: import('../types/index.d.ts').Rect}|null}>}
|
|
155
|
+
*/
|
|
156
|
+
async function settleAndReread(adapter, { selector, xpath, timeout, settleOpts }) {
|
|
157
|
+
if (!settleOpts) {
|
|
158
|
+
return { settleMs: 0, rect: null };
|
|
159
|
+
}
|
|
160
|
+
const settleStart = nowMs();
|
|
161
|
+
await waitForStable(adapter, settleOpts === true ? {} : settleOpts);
|
|
162
|
+
const settleMs = nowMs() - settleStart;
|
|
163
|
+
const reread = await withTimeout(
|
|
164
|
+
adapter.evaluate(findElementInPage, { selector, xpath }),
|
|
165
|
+
timeout,
|
|
166
|
+
{ operation: 'adapter.evaluate(findElement after settle)' }
|
|
167
|
+
);
|
|
168
|
+
if (reread && reread.found) {
|
|
169
|
+
return {
|
|
170
|
+
settleMs,
|
|
171
|
+
rect: { viewportRect: reread.viewportRect, documentRect: reread.documentRect },
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
return { settleMs, rect: null };
|
|
175
|
+
}
|
|
176
|
+
|
|
134
177
|
/**
|
|
135
178
|
* Page-context element lookup. Shipped to the browser via adapter.evaluate.
|
|
136
179
|
* Coverage suppressed for the same reason as `settle.js#runInPage` —
|
package/src/capture/page.js
CHANGED
|
@@ -33,20 +33,12 @@ async function capturePage(adapter, options = {}) {
|
|
|
33
33
|
emit(events, 'capture:start', { format, fullPage });
|
|
34
34
|
|
|
35
35
|
let settleMs = 0;
|
|
36
|
-
|
|
36
|
+
let warnings = [];
|
|
37
37
|
if (settleOpts) {
|
|
38
38
|
const settleStart = nowMs();
|
|
39
39
|
const report = await waitForStable(adapter, settleOpts === true ? {} : settleOpts);
|
|
40
40
|
settleMs = nowMs() - settleStart;
|
|
41
|
-
|
|
42
|
-
warnings.push('fonts did not settle within maxWait');
|
|
43
|
-
}
|
|
44
|
-
if (report.animations === 'timeout') {
|
|
45
|
-
warnings.push('animations did not settle within maxWait');
|
|
46
|
-
}
|
|
47
|
-
if (report.images.failed > 0) {
|
|
48
|
-
warnings.push(`${report.images.failed} image(s) failed to load`);
|
|
49
|
-
}
|
|
41
|
+
warnings = settleWarnings(report);
|
|
50
42
|
}
|
|
51
43
|
|
|
52
44
|
const metrics = await adapter.getMetrics();
|
|
@@ -97,6 +89,35 @@ async function capturePage(adapter, options = {}) {
|
|
|
97
89
|
return result;
|
|
98
90
|
}
|
|
99
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Translate a StabilityReport into non-fatal warning strings.
|
|
94
|
+
*
|
|
95
|
+
* @param {import('../types/index.d.ts').StabilityReport} report
|
|
96
|
+
* @returns {string[]}
|
|
97
|
+
*/
|
|
98
|
+
function settleWarnings(report) {
|
|
99
|
+
const warnings = [];
|
|
100
|
+
if (report.fonts === 'timeout') {
|
|
101
|
+
warnings.push('fonts did not settle within maxWait');
|
|
102
|
+
}
|
|
103
|
+
if (report.animations === 'timeout') {
|
|
104
|
+
warnings.push('animations did not settle within maxWait');
|
|
105
|
+
}
|
|
106
|
+
if (report.images.failed > 0) {
|
|
107
|
+
warnings.push(`${report.images.failed} image(s) failed to load`);
|
|
108
|
+
}
|
|
109
|
+
if (report.backgroundImages && report.backgroundImages.failed > 0) {
|
|
110
|
+
warnings.push(`${report.backgroundImages.failed} background image(s) failed to load`);
|
|
111
|
+
}
|
|
112
|
+
if (report.network === 'timeout') {
|
|
113
|
+
warnings.push('network did not reach idle within maxWait');
|
|
114
|
+
}
|
|
115
|
+
if (report.layoutStable === 'timeout') {
|
|
116
|
+
warnings.push('layout did not stabilize within maxWait');
|
|
117
|
+
}
|
|
118
|
+
return warnings;
|
|
119
|
+
}
|
|
120
|
+
|
|
100
121
|
/**
|
|
101
122
|
* Map the user's desired output format to the format the browser should
|
|
102
123
|
* produce. Returns either the same format (no re-encode) or 'png' (when
|
package/src/capture/settle.js
CHANGED
|
@@ -16,7 +16,10 @@ async function waitForStable(adapter, options = {}) {
|
|
|
16
16
|
const {
|
|
17
17
|
fonts = true,
|
|
18
18
|
images = true,
|
|
19
|
+
backgroundImages = true,
|
|
19
20
|
animations = 'reduce',
|
|
21
|
+
network = 'idle',
|
|
22
|
+
layoutStableMs = 250,
|
|
20
23
|
maxWait = 5000,
|
|
21
24
|
idleMs = 200,
|
|
22
25
|
} = options;
|
|
@@ -25,7 +28,10 @@ async function waitForStable(adapter, options = {}) {
|
|
|
25
28
|
const report = await adapter.evaluate(runInPage, {
|
|
26
29
|
fonts,
|
|
27
30
|
images,
|
|
31
|
+
backgroundImages,
|
|
28
32
|
animations,
|
|
33
|
+
network,
|
|
34
|
+
layoutStableMs,
|
|
29
35
|
maxWait,
|
|
30
36
|
idleMs,
|
|
31
37
|
});
|
|
@@ -39,7 +45,7 @@ async function waitForStable(adapter, options = {}) {
|
|
|
39
45
|
* (no closures over Node-side state) and only use `globalThis.*` so
|
|
40
46
|
* eslint's no-undef passes.
|
|
41
47
|
*
|
|
42
|
-
* @param {{fonts: boolean, images: boolean, animations: 'reduce'|'wait'|'ignore', maxWait: number, idleMs: number}} opts
|
|
48
|
+
* @param {{fonts: boolean, images: boolean, backgroundImages: boolean, animations: 'reduce'|'wait'|'ignore', network: 'idle'|'ignore', layoutStableMs: number, maxWait: number, idleMs: number}} opts
|
|
43
49
|
* @returns {Promise<Omit<import('../types/index.d.ts').StabilityReport, 'totalMs'>>}
|
|
44
50
|
*/
|
|
45
51
|
/* istanbul ignore next -- runs in the browser via adapter.evaluate(); must NOT
|
|
@@ -51,6 +57,9 @@ async function runInPage(opts) {
|
|
|
51
57
|
const report = {
|
|
52
58
|
fonts: 'skipped',
|
|
53
59
|
images: { total: 0, loaded: 0, failed: 0 },
|
|
60
|
+
backgroundImages: 'skipped',
|
|
61
|
+
network: 'skipped',
|
|
62
|
+
layoutStable: 'skipped',
|
|
54
63
|
animations: 'skipped',
|
|
55
64
|
};
|
|
56
65
|
|
|
@@ -115,6 +124,129 @@ async function runInPage(opts) {
|
|
|
115
124
|
await Promise.all(imgs.map(settleOneImage));
|
|
116
125
|
};
|
|
117
126
|
|
|
127
|
+
// Collect unique, non-data CSS background-image URLs, bounded for perf.
|
|
128
|
+
const collectBackgroundUrls = () => {
|
|
129
|
+
const CAP = 200; // large DOMs otherwise scan tens of thousands of nodes
|
|
130
|
+
const urls = new Set();
|
|
131
|
+
const els = doc.querySelectorAll('*');
|
|
132
|
+
for (let i = 0; i < els.length && urls.size < CAP; i += 1) {
|
|
133
|
+
const bg = globalThis.getComputedStyle(els[i]).backgroundImage;
|
|
134
|
+
if (!bg || bg === 'none') {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
for (const match of bg.matchAll(/url\((['"]?)([^'")]+)\1\)/g)) {
|
|
138
|
+
const url = match[2];
|
|
139
|
+
if (url && !url.startsWith('data:')) {
|
|
140
|
+
urls.add(url);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return Array.from(urls).slice(0, CAP);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Load one URL and tally it. Listeners are attached before `src` is set, so
|
|
148
|
+
// even a cached resource fires 'load' on a freshly-created Image — no
|
|
149
|
+
// synchronous complete-branch is needed (which would risk a double count).
|
|
150
|
+
const settleOneBackgroundUrl = url =>
|
|
151
|
+
new Promise(resolve => {
|
|
152
|
+
const img = new globalThis.Image();
|
|
153
|
+
const onload = () => {
|
|
154
|
+
report.backgroundImages.loaded += 1;
|
|
155
|
+
resolve();
|
|
156
|
+
};
|
|
157
|
+
const onerror = () => {
|
|
158
|
+
report.backgroundImages.failed += 1;
|
|
159
|
+
resolve();
|
|
160
|
+
};
|
|
161
|
+
img.addEventListener('load', onload, { once: true });
|
|
162
|
+
img.addEventListener('error', onerror, { once: true });
|
|
163
|
+
img.src = url;
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Heroes and section backgrounds are commonly CSS background-images, not
|
|
167
|
+
// <img>. A capture taken before they paint reads as blank even though
|
|
168
|
+
// fonts.ready has resolved — this check waits for those URLs to load.
|
|
169
|
+
const settleBackgroundImages = async () => {
|
|
170
|
+
if (!opts.backgroundImages || !doc) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const list = collectBackgroundUrls();
|
|
174
|
+
report.backgroundImages = { total: list.length, loaded: 0, failed: 0 };
|
|
175
|
+
if (list.length === 0) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
await withTimeout(
|
|
179
|
+
Promise.all(list.map(settleOneBackgroundUrl)),
|
|
180
|
+
opts.maxWait,
|
|
181
|
+
'backgroundImages'
|
|
182
|
+
).catch(() => {
|
|
183
|
+
// timeout: leave loaded/failed at whatever completed so far
|
|
184
|
+
});
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// Approximate a networkidle gate without owning navigation: wait until the
|
|
188
|
+
// document is 'complete' and no new resource-timing entries appear for a
|
|
189
|
+
// short quiet window. Bounded by maxWait.
|
|
190
|
+
//
|
|
191
|
+
// Known limitation: getEntriesByType('resource') is capped by the resource
|
|
192
|
+
// timing buffer (default 250 entries). On a page with >250 resources the
|
|
193
|
+
// count plateaus and this can report 'idle' early. It errs toward idle
|
|
194
|
+
// (settle is less thorough, never a hang), and the image/background/layout
|
|
195
|
+
// checks still run, so the practical impact is small.
|
|
196
|
+
const settleNetwork = async () => {
|
|
197
|
+
if (opts.network !== 'idle' || !doc) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const perf = globalThis.performance;
|
|
201
|
+
const countResources = () =>
|
|
202
|
+
perf && perf.getEntriesByType ? perf.getEntriesByType('resource').length : 0;
|
|
203
|
+
const quietMs = 200;
|
|
204
|
+
const deadline = Date.now() + opts.maxWait;
|
|
205
|
+
let lastCount = countResources();
|
|
206
|
+
let lastChange = Date.now();
|
|
207
|
+
while (Date.now() < deadline) {
|
|
208
|
+
const count = countResources();
|
|
209
|
+
if (count !== lastCount) {
|
|
210
|
+
lastCount = count;
|
|
211
|
+
lastChange = Date.now();
|
|
212
|
+
}
|
|
213
|
+
if (doc.readyState === 'complete' && Date.now() - lastChange >= quietMs) {
|
|
214
|
+
report.network = 'idle';
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
await new Promise(resolve => {
|
|
218
|
+
setTimeout(resolve, 50);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
report.network = 'timeout';
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// Late reflow is what leaves element crops on stale coordinates. Require the
|
|
225
|
+
// document height to hold steady for layoutStableMs before declaring stable.
|
|
226
|
+
const settleLayout = async () => {
|
|
227
|
+
if (!opts.layoutStableMs || opts.layoutStableMs <= 0 || !doc) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const el = doc.documentElement;
|
|
231
|
+
const deadline = Date.now() + opts.maxWait;
|
|
232
|
+
let lastHeight = el.scrollHeight;
|
|
233
|
+
let stableSince = Date.now();
|
|
234
|
+
while (Date.now() < deadline) {
|
|
235
|
+
await new Promise(resolve => {
|
|
236
|
+
setTimeout(resolve, 50);
|
|
237
|
+
});
|
|
238
|
+
const height = el.scrollHeight;
|
|
239
|
+
if (height !== lastHeight) {
|
|
240
|
+
lastHeight = height;
|
|
241
|
+
stableSince = Date.now();
|
|
242
|
+
} else if (Date.now() - stableSince >= opts.layoutStableMs) {
|
|
243
|
+
report.layoutStable = 'stable';
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
report.layoutStable = 'timeout';
|
|
248
|
+
};
|
|
249
|
+
|
|
118
250
|
const settleAnimations = async () => {
|
|
119
251
|
if (opts.animations === 'ignore' || !doc) {
|
|
120
252
|
return;
|
|
@@ -167,9 +299,12 @@ async function runInPage(opts) {
|
|
|
167
299
|
});
|
|
168
300
|
};
|
|
169
301
|
|
|
302
|
+
await settleNetwork();
|
|
170
303
|
await settleFonts();
|
|
171
304
|
await settleImages();
|
|
305
|
+
await settleBackgroundImages();
|
|
172
306
|
await settleAnimations();
|
|
307
|
+
await settleLayout();
|
|
173
308
|
await settleIdle();
|
|
174
309
|
|
|
175
310
|
return report;
|
package/src/index.js
CHANGED
package/src/types/index.d.ts
CHANGED
|
@@ -53,11 +53,18 @@ export type Severity = 'critical' | 'serious' | 'moderate' | 'minor';
|
|
|
53
53
|
|
|
54
54
|
export type Encoding = 'buffer' | 'stream';
|
|
55
55
|
export type AnimationsPolicy = 'reduce' | 'wait' | 'ignore';
|
|
56
|
+
export type NetworkPolicy = 'idle' | 'ignore';
|
|
56
57
|
|
|
57
58
|
export interface WaitForStableOptions {
|
|
58
59
|
fonts?: boolean;
|
|
59
60
|
images?: boolean;
|
|
61
|
+
/** Wait for CSS background-image URLs to load; default true. */
|
|
62
|
+
backgroundImages?: boolean;
|
|
60
63
|
animations?: AnimationsPolicy;
|
|
64
|
+
/** Resource-timing quiescence gate; default 'idle'. */
|
|
65
|
+
network?: NetworkPolicy;
|
|
66
|
+
/** Require scrollHeight/target-rect steady for this long; 0 disables. Default 250. */
|
|
67
|
+
layoutStableMs?: number;
|
|
61
68
|
/** ms; default 5000. */
|
|
62
69
|
maxWait?: number;
|
|
63
70
|
/** ms; default 200. */
|
|
@@ -150,6 +157,9 @@ export interface ResponsiveCaptureResult {
|
|
|
150
157
|
export interface StabilityReport {
|
|
151
158
|
fonts: 'ready' | 'timeout' | 'skipped';
|
|
152
159
|
images: { total: number; loaded: number; failed: number };
|
|
160
|
+
backgroundImages: { total: number; loaded: number; failed: number } | 'skipped';
|
|
161
|
+
network: 'idle' | 'timeout' | 'skipped';
|
|
162
|
+
layoutStable: 'stable' | 'timeout' | 'skipped';
|
|
153
163
|
animations: 'none' | 'reduced' | 'settled' | 'timeout' | 'skipped';
|
|
154
164
|
totalMs: number;
|
|
155
165
|
}
|