@afixt/screenshot-utils 0.9.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 +31 -0
- package/README.md +20 -20
- package/dist/index.js +172 -16
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +172 -16
- package/dist/index.mjs.map +1 -1
- package/dist/types/adapters/cdp.d.ts +28 -0
- package/dist/types/adapters/cdp.d.ts.map +1 -0
- package/dist/types/adapters/index.d.ts +5 -0
- package/dist/types/adapters/index.d.ts.map +1 -0
- package/dist/types/adapters/playwright.d.ts +28 -0
- package/dist/types/adapters/playwright.d.ts.map +1 -0
- package/dist/types/adapters/puppeteer.d.ts +20 -0
- package/dist/types/adapters/puppeteer.d.ts.map +1 -0
- package/dist/types/capture/element.d.ts +20 -0
- package/dist/types/capture/element.d.ts.map +1 -0
- package/dist/types/capture/index.d.ts +8 -0
- package/dist/types/capture/index.d.ts.map +1 -0
- package/dist/types/capture/long-page.d.ts +16 -0
- package/dist/types/capture/long-page.d.ts.map +1 -0
- package/dist/types/capture/page.d.ts +14 -0
- package/dist/types/capture/page.d.ts.map +1 -0
- package/dist/types/capture/responsive.d.ts +20 -0
- package/dist/types/capture/responsive.d.ts.map +1 -0
- package/dist/types/capture/settle.d.ts +14 -0
- package/dist/types/capture/settle.d.ts.map +1 -0
- package/dist/types/capture/viewport.d.ts +14 -0
- package/dist/types/capture/viewport.d.ts.map +1 -0
- package/dist/types/cli/index.d.ts +10 -0
- package/dist/types/cli/index.d.ts.map +1 -0
- package/dist/types/compose/annotate.d.ts +16 -0
- package/dist/types/compose/annotate.d.ts.map +1 -0
- package/dist/types/compose/diff.d.ts +15 -0
- package/dist/types/compose/diff.d.ts.map +1 -0
- package/dist/types/compose/heatmap.d.ts +17 -0
- package/dist/types/compose/heatmap.d.ts.map +1 -0
- package/dist/types/compose/index.d.ts +6 -0
- package/dist/types/compose/index.d.ts.map +1 -0
- package/dist/types/compose/theme.d.ts +17 -0
- package/dist/types/compose/theme.d.ts.map +1 -0
- package/dist/types/errors/adapter.d.ts +33 -0
- package/dist/types/errors/adapter.d.ts.map +1 -0
- package/dist/types/errors/base.d.ts +37 -0
- package/dist/types/errors/base.d.ts.map +1 -0
- package/dist/types/errors/capture.d.ts +109 -0
- package/dist/types/errors/capture.d.ts.map +1 -0
- package/dist/types/errors/compose.d.ts +35 -0
- package/dist/types/errors/compose.d.ts.map +1 -0
- package/dist/types/errors/index.d.ts +29 -0
- package/dist/types/errors/index.d.ts.map +1 -0
- package/dist/types/errors/optional.d.ts +23 -0
- package/dist/types/errors/optional.d.ts.map +1 -0
- package/dist/types/errors/redaction.d.ts +21 -0
- package/dist/types/errors/redaction.d.ts.map +1 -0
- package/dist/types/errors/tile.d.ts +29 -0
- package/dist/types/errors/tile.d.ts.map +1 -0
- package/dist/types/errors/transform.d.ts +35 -0
- package/dist/types/errors/transform.d.ts.map +1 -0
- package/dist/types/index.d.ts +58 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/redact/index.d.ts +5 -0
- package/dist/types/redact/index.d.ts.map +1 -0
- package/dist/types/redact/policy.d.ts +29 -0
- package/dist/types/redact/policy.d.ts.map +1 -0
- package/dist/types/redact/redact.d.ts +29 -0
- package/dist/types/redact/redact.d.ts.map +1 -0
- package/dist/types/tile/dzi.d.ts +18 -0
- package/dist/types/tile/dzi.d.ts.map +1 -0
- package/dist/types/tile/index.d.ts +3 -0
- package/dist/types/tile/index.d.ts.map +1 -0
- package/dist/types/transform/convert.d.ts +13 -0
- package/dist/types/transform/convert.d.ts.map +1 -0
- package/dist/types/transform/crop.d.ts +33 -0
- package/dist/types/transform/crop.d.ts.map +1 -0
- package/dist/types/transform/index.d.ts +7 -0
- package/dist/types/transform/index.d.ts.map +1 -0
- package/dist/types/transform/normalize.d.ts +14 -0
- package/dist/types/transform/normalize.d.ts.map +1 -0
- package/dist/types/transform/resize.d.ts +14 -0
- package/dist/types/transform/resize.d.ts.map +1 -0
- package/package.json +5 -4
- package/src/capture/element.js +48 -6
- package/src/capture/page.js +31 -10
- package/src/capture/settle.js +139 -4
- package/src/index.js +3 -0
- package/src/types/index.d.ts +10 -0
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,17 +45,21 @@ 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
|
-
/*
|
|
46
|
-
|
|
51
|
+
/* istanbul ignore next -- runs in the browser via adapter.evaluate(); must NOT
|
|
52
|
+
be instrumented (injected counters don't exist in the page context) and is
|
|
53
|
+
exercised by settle-integration.test.js. */
|
|
47
54
|
async function runInPage(opts) {
|
|
48
55
|
const doc = globalThis.document;
|
|
49
56
|
/** @type {Omit<import('../types/index.d.ts').StabilityReport, 'totalMs'>} */
|
|
50
57
|
const report = {
|
|
51
58
|
fonts: 'skipped',
|
|
52
59
|
images: { total: 0, loaded: 0, failed: 0 },
|
|
60
|
+
backgroundImages: 'skipped',
|
|
61
|
+
network: 'skipped',
|
|
62
|
+
layoutStable: 'skipped',
|
|
53
63
|
animations: 'skipped',
|
|
54
64
|
};
|
|
55
65
|
|
|
@@ -114,6 +124,129 @@ async function runInPage(opts) {
|
|
|
114
124
|
await Promise.all(imgs.map(settleOneImage));
|
|
115
125
|
};
|
|
116
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
|
+
|
|
117
250
|
const settleAnimations = async () => {
|
|
118
251
|
if (opts.animations === 'ignore' || !doc) {
|
|
119
252
|
return;
|
|
@@ -166,14 +299,16 @@ async function runInPage(opts) {
|
|
|
166
299
|
});
|
|
167
300
|
};
|
|
168
301
|
|
|
302
|
+
await settleNetwork();
|
|
169
303
|
await settleFonts();
|
|
170
304
|
await settleImages();
|
|
305
|
+
await settleBackgroundImages();
|
|
171
306
|
await settleAnimations();
|
|
307
|
+
await settleLayout();
|
|
172
308
|
await settleIdle();
|
|
173
309
|
|
|
174
310
|
return report;
|
|
175
311
|
}
|
|
176
|
-
/* v8 ignore stop */
|
|
177
312
|
|
|
178
313
|
function nowMs() {
|
|
179
314
|
return Number(process.hrtime.bigint() / 1_000_000n);
|
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
|
}
|