@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/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.0.0] - 2026-05-29
|
|
11
|
+
|
|
12
|
+
First v1.0.0 release. Every acceptance criterion in `spec.md` § 23 is now
|
|
13
|
+
met — OpenSeadragon end-to-end integration, the ≥90% capture/transform
|
|
14
|
+
coverage gates, the performance suite, and the live browser-adapter tests
|
|
15
|
+
all run green. The public API surface in `src/index.js` is now
|
|
16
|
+
semver-protected.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Coverage provider switched from `v8` to `istanbul`. The v8 provider did not union-merge per-file coverage across test files in this repo — a near-empty barrel import (`index.test.js`) clobbered a module's real coverage from its dedicated test (e.g. `page.js` read 80% alone, 32% in the full run) — which had forced thresholds down to a placeholder 60/50. istanbul merges correctly; browser-shipped code is excluded so its injected counters don't run in the page context (`src/adapters/**`, the `settle.runInPage` / `element.findElementInPage` page-context functions, and the `src/errors/**` barrel re-exports which are covered by `test/errors/*.test.js`).
|
|
21
|
+
- Coverage thresholds raised from 60/50 to an 85/85/90/85 project floor, with per-glob gates requiring ≥90% line + branch on `src/capture/**` and `src/transform/**` (spec § 23). Capture now 92.3% line / 91.1% branch; transform 95.0% / 97.4%; overall 89.8%.
|
|
22
|
+
- Minimum Node widened to `>=20.0.0` (was `>=22.22.1`) to match the spec § 23 / CI Node 20 + 22 matrix; with `engine-strict=true` the old floor would have failed the Node 20 jobs.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- `playwright` added as a dev dependency so its live-browser adapter contract suite actually runs in CI (was skipped whenever Playwright was absent). CI workflows now install the Chromium binary for the integration tests.
|
|
27
|
+
- OpenSeadragon end-to-end integration test for `tileDzi` (spec § 23): generates a real DZI pyramid, serves it plus the OpenSeadragon UMD build over a local HTTP server, and asserts the viewer opens the `.dzi` and loads tiles with no failures in a headless browser — verifying the output is genuinely consumable by the de-facto DZI viewer, not just shaped like one. `openseadragon` added as a dev dependency.
|
|
28
|
+
- Unit-test backfill across `resize`, `crop`, `normalize`, and `captureLongPage` to reach the § 23 capture + transform coverage bar.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- `npm run test:perf` was excluded down to zero test files by the shared Vitest `exclude` glob, so it exited non-zero and the weekly `performance.yml` workflow was effectively always failing. It now runs via a dedicated config; all six § 13.3 ceilings pass.
|
|
33
|
+
- The 500-iteration memory-growth test silently `return`ed (passing vacuously) when `--expose-gc` was absent. The perf config now exposes gc and the test asserts its availability, so it genuinely runs.
|
|
34
|
+
- Browser-teardown `afterAll` hooks no longer rely on the 10 s default timeout (which flaked under parallel Chromium contention, failing the suite as a file-level "Hook timed out"); they now allow 30 s.
|
|
35
|
+
|
|
36
|
+
### Removed
|
|
37
|
+
|
|
38
|
+
- Deprecated `always-auth=true` from `.npmrc` (npm warns it is going away; the per-registry `_authToken` covers publish auth).
|
|
39
|
+
- Unused `@vitest/coverage-v8` dev dependency (provider is now istanbul).
|
|
40
|
+
|
|
10
41
|
## [0.9.0] - 2026-05-22
|
|
11
42
|
|
|
12
43
|
First published release. Phases 0–4 of `PLAN.md` are complete: the full
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> Screenshot capture, transform, compose, tile, and redact utilities for the AFixt/Revenant accessibility platform.
|
|
4
4
|
|
|
5
|
-
Status: **0.9.0** — Phases 0–4 of [`PLAN.md`](./PLAN.md) are complete; the full public API from [`spec.md`](./spec.md) § 8 is implemented and tested. Pre-1.0 while the v1.0.0 acceptance
|
|
5
|
+
Status: **0.9.0** — Phases 0–4 of [`PLAN.md`](./PLAN.md) are complete; the full public API from [`spec.md`](./spec.md) § 8 is implemented and tested (capture and transform at ≥90% line+branch coverage per § 23). Pre-1.0 while the remaining v1.0.0 acceptance items in `spec.md` § 23 land (a live CDP adapter test and the CI matrix running green). The API is expected to stay stable through to 1.0.0.
|
|
6
6
|
|
|
7
7
|
This package owns **all** screenshot operations for the AFixt/Revenant platform: full-page, viewport, element, long-page strip-stitch, multi-viewport capture; crop / resize / convert / normalize transforms; annotate / heatmap / diff / composite; DZI tile pyramids; and PII redaction. It is being extracted from `@afixt/afixt-engine`; the engine will become a consumer.
|
|
8
8
|
|
|
@@ -40,27 +40,27 @@ Screenshot work was scattered across `afixt-engine`, `mortise`, and ad-hoc consu
|
|
|
40
40
|
|
|
41
41
|
## Install
|
|
42
42
|
|
|
43
|
-
| Dependency | Status | Why you'd want it
|
|
44
|
-
| ------------------- | ---------------------------------------------- |
|
|
45
|
-
| `sharp` | Required runtime dep — installed automatically | All raster operations
|
|
46
|
-
| `puppeteer` >=23 | Optional peer | `fromPuppeteerPage` adapter
|
|
47
|
-
| `playwright` >=1.40 | Optional peer | `fromPlaywrightPage` adapter
|
|
48
|
-
| `pixelmatch` | Optional peer | `diff()` function
|
|
49
|
-
| `pngjs` | Optional peer | `pixelmatch` co-dependency
|
|
50
|
-
| `@napi-rs/canvas` | Deferred to v1.1 | Advanced annotation strokes
|
|
43
|
+
| Dependency | Status | Why you'd want it |
|
|
44
|
+
| ------------------- | ---------------------------------------------- | ---------------------------- |
|
|
45
|
+
| `sharp` | Required runtime dep — installed automatically | All raster operations |
|
|
46
|
+
| `puppeteer` >=23 | Optional peer | `fromPuppeteerPage` adapter |
|
|
47
|
+
| `playwright` >=1.40 | Optional peer | `fromPlaywrightPage` adapter |
|
|
48
|
+
| `pixelmatch` | Optional peer | `diff()` function |
|
|
49
|
+
| `pngjs` | Optional peer | `pixelmatch` co-dependency |
|
|
50
|
+
| `@napi-rs/canvas` | Deferred to v1.1 | Advanced annotation strokes |
|
|
51
51
|
|
|
52
|
-
Node `>=
|
|
52
|
+
Node `>=20.0.0` is required (`engine-strict=true`).
|
|
53
53
|
|
|
54
54
|
## PageAdapter
|
|
55
55
|
|
|
56
56
|
The capture functions never import a browser. Consumers wrap their `Page`-like object:
|
|
57
57
|
|
|
58
58
|
```js
|
|
59
|
-
const { fromPuppeteerPage } = require('@afixt/screenshot-utils
|
|
59
|
+
const { fromPuppeteerPage } = require('@afixt/screenshot-utils');
|
|
60
60
|
const adapter = fromPuppeteerPage(page);
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
Anything that satisfies the `PageAdapter` interface works — write your own for CDP-only runtimes. See `docs/adapters.md`
|
|
63
|
+
Anything that satisfies the `PageAdapter` interface works — write your own for CDP-only runtimes. See `docs/adapters.md` for the contract.
|
|
64
64
|
|
|
65
65
|
## Capture functions
|
|
66
66
|
|
|
@@ -77,20 +77,20 @@ Anything that satisfies the `PageAdapter` interface works — write your own for
|
|
|
77
77
|
- `convert(buffer, format, opts)` — re-encode
|
|
78
78
|
- `resize(buffer, opts)` — fit, contain, cover
|
|
79
79
|
- `normalize(buffer, opts)` — strip metadata, fix orientation
|
|
80
|
-
- `annotate(buffer, annotations, opts)` — boxes, labels, severities
|
|
81
|
-
- `heatmap(buffer, points, opts)` — kernel-density overlay
|
|
82
|
-
- `diff(a, b, opts)` — pixelmatch-backed visual diff
|
|
80
|
+
- `annotate(buffer, annotations, opts)` — boxes, labels, severities
|
|
81
|
+
- `heatmap(buffer, points, opts)` — kernel-density overlay
|
|
82
|
+
- `diff(a, b, opts)` — pixelmatch-backed visual diff
|
|
83
83
|
|
|
84
84
|
## Tiling
|
|
85
85
|
|
|
86
|
-
- `tileDzi(buffer, outDir | 'buffers', opts)` — produce a Deep Zoom Image pyramid
|
|
86
|
+
- `tileDzi(buffer, outDir | 'buffers', opts)` — produce a Deep Zoom Image pyramid
|
|
87
87
|
|
|
88
88
|
## Redaction
|
|
89
89
|
|
|
90
|
-
- `redact(buffer, regions, mode, opts)` — blur, mask, or pixelate PII regions
|
|
90
|
+
- `redact(buffer, regions, mode, opts)` — blur, mask, or pixelate PII regions
|
|
91
91
|
- `applyPiiPolicy(buffer, policy)` — apply a policy bundle for repeatable redaction
|
|
92
92
|
|
|
93
|
-
## CLI
|
|
93
|
+
## CLI
|
|
94
94
|
|
|
95
95
|
```bash
|
|
96
96
|
npx @afixt/screenshot-utils crop input.png --rect 0,0,400,300 --out thumb.png
|
|
@@ -103,11 +103,11 @@ npx @afixt/screenshot-utils diff before.png after.png --out diff.png
|
|
|
103
103
|
- Pass `encoding: 'stream'` for memory-sensitive pipelines.
|
|
104
104
|
- Use `cropMany` rather than calling `crop` in a loop — internally bounded parallelism is faster and uses less memory.
|
|
105
105
|
- For long pages, prefer `captureLongPage` over a giant viewport — sharp handles strips better than 30 000-px-tall buffers.
|
|
106
|
-
- See `docs/performance.md`
|
|
106
|
+
- See `docs/performance.md` for ceilings.
|
|
107
107
|
|
|
108
108
|
## Errors
|
|
109
109
|
|
|
110
|
-
Every failure is a named class with a stable `code`, a structured `reason` enum, and a `retriable: boolean` hint. Hierarchy in `spec.md` § 14.1. Catalog in `docs/error-codes.md
|
|
110
|
+
Every failure is a named class with a stable `code`, a structured `reason` enum, and a `retriable: boolean` hint. Hierarchy in `spec.md` § 14.1. Catalog in `docs/error-codes.md`.
|
|
111
111
|
|
|
112
112
|
## Versioning & non-goals
|
|
113
113
|
|
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@ var require_package = __commonJS({
|
|
|
16
16
|
"package.json"(exports, module) {
|
|
17
17
|
module.exports = {
|
|
18
18
|
name: "@afixt/screenshot-utils",
|
|
19
|
-
version: "0.
|
|
19
|
+
version: "1.0.0",
|
|
20
20
|
description: "Screenshot capture, transform, compose, tile, and redact utilities for the AFixt/Revenant accessibility platform",
|
|
21
21
|
type: "commonjs",
|
|
22
22
|
main: "./dist/index.js",
|
|
@@ -99,7 +99,7 @@ var require_package = __commonJS({
|
|
|
99
99
|
access: "restricted"
|
|
100
100
|
},
|
|
101
101
|
engines: {
|
|
102
|
-
node: ">=
|
|
102
|
+
node: ">=20.9.0"
|
|
103
103
|
},
|
|
104
104
|
keywords: [
|
|
105
105
|
"screenshot",
|
|
@@ -117,7 +117,7 @@ var require_package = __commonJS({
|
|
|
117
117
|
"revenant"
|
|
118
118
|
],
|
|
119
119
|
dependencies: {
|
|
120
|
-
sharp: "0.
|
|
120
|
+
sharp: "^0.35.3"
|
|
121
121
|
},
|
|
122
122
|
peerDependencies: {
|
|
123
123
|
"@napi-rs/canvas": "^0.1.0",
|
|
@@ -148,7 +148,7 @@ var require_package = __commonJS({
|
|
|
148
148
|
"@commitlint/config-conventional": "20.5.3",
|
|
149
149
|
"@eslint/js": "10.0.1",
|
|
150
150
|
"@types/node": "22.19.19",
|
|
151
|
-
"@vitest/coverage-
|
|
151
|
+
"@vitest/coverage-istanbul": "4.1.5",
|
|
152
152
|
"clean-jsdoc-theme": "4.3.2",
|
|
153
153
|
eslint: "10.3.0",
|
|
154
154
|
"eslint-plugin-import-x": "4.16.2",
|
|
@@ -167,6 +167,7 @@ var require_package = __commonJS({
|
|
|
167
167
|
"lint-staged": "17.0.2",
|
|
168
168
|
"markdownlint-cli": "0.48.0",
|
|
169
169
|
"npm-run-all2": "8.0.4",
|
|
170
|
+
openseadragon: "6.0.2",
|
|
170
171
|
pixelmatch: "7.1.0",
|
|
171
172
|
playwright: "1.60.0",
|
|
172
173
|
pngjs: "7.0.0",
|
|
@@ -962,7 +963,10 @@ var require_settle = __commonJS({
|
|
|
962
963
|
const {
|
|
963
964
|
fonts = true,
|
|
964
965
|
images = true,
|
|
966
|
+
backgroundImages = true,
|
|
965
967
|
animations = "reduce",
|
|
968
|
+
network = "idle",
|
|
969
|
+
layoutStableMs = 250,
|
|
966
970
|
maxWait = 5e3,
|
|
967
971
|
idleMs = 200
|
|
968
972
|
} = options;
|
|
@@ -970,7 +974,10 @@ var require_settle = __commonJS({
|
|
|
970
974
|
const report = await adapter.evaluate(runInPage, {
|
|
971
975
|
fonts,
|
|
972
976
|
images,
|
|
977
|
+
backgroundImages,
|
|
973
978
|
animations,
|
|
979
|
+
network,
|
|
980
|
+
layoutStableMs,
|
|
974
981
|
maxWait,
|
|
975
982
|
idleMs
|
|
976
983
|
});
|
|
@@ -982,6 +989,9 @@ var require_settle = __commonJS({
|
|
|
982
989
|
const report = {
|
|
983
990
|
fonts: "skipped",
|
|
984
991
|
images: { total: 0, loaded: 0, failed: 0 },
|
|
992
|
+
backgroundImages: "skipped",
|
|
993
|
+
network: "skipped",
|
|
994
|
+
layoutStable: "skipped",
|
|
985
995
|
animations: "skipped"
|
|
986
996
|
};
|
|
987
997
|
const withTimeout = (promise, ms, label) => Promise.race([
|
|
@@ -1036,6 +1046,103 @@ var require_settle = __commonJS({
|
|
|
1036
1046
|
report.images.total = imgs.length;
|
|
1037
1047
|
await Promise.all(imgs.map(settleOneImage));
|
|
1038
1048
|
};
|
|
1049
|
+
const collectBackgroundUrls = () => {
|
|
1050
|
+
const CAP = 200;
|
|
1051
|
+
const urls = /* @__PURE__ */ new Set();
|
|
1052
|
+
const els = doc.querySelectorAll("*");
|
|
1053
|
+
for (let i = 0; i < els.length && urls.size < CAP; i += 1) {
|
|
1054
|
+
const bg = globalThis.getComputedStyle(els[i]).backgroundImage;
|
|
1055
|
+
if (!bg || bg === "none") {
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1058
|
+
for (const match of bg.matchAll(/url\((['"]?)([^'")]+)\1\)/g)) {
|
|
1059
|
+
const url = match[2];
|
|
1060
|
+
if (url && !url.startsWith("data:")) {
|
|
1061
|
+
urls.add(url);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
return Array.from(urls).slice(0, CAP);
|
|
1066
|
+
};
|
|
1067
|
+
const settleOneBackgroundUrl = (url) => new Promise((resolve) => {
|
|
1068
|
+
const img = new globalThis.Image();
|
|
1069
|
+
const onload = () => {
|
|
1070
|
+
report.backgroundImages.loaded += 1;
|
|
1071
|
+
resolve();
|
|
1072
|
+
};
|
|
1073
|
+
const onerror = () => {
|
|
1074
|
+
report.backgroundImages.failed += 1;
|
|
1075
|
+
resolve();
|
|
1076
|
+
};
|
|
1077
|
+
img.addEventListener("load", onload, { once: true });
|
|
1078
|
+
img.addEventListener("error", onerror, { once: true });
|
|
1079
|
+
img.src = url;
|
|
1080
|
+
});
|
|
1081
|
+
const settleBackgroundImages = async () => {
|
|
1082
|
+
if (!opts.backgroundImages || !doc) {
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
const list = collectBackgroundUrls();
|
|
1086
|
+
report.backgroundImages = { total: list.length, loaded: 0, failed: 0 };
|
|
1087
|
+
if (list.length === 0) {
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
await withTimeout(
|
|
1091
|
+
Promise.all(list.map(settleOneBackgroundUrl)),
|
|
1092
|
+
opts.maxWait,
|
|
1093
|
+
"backgroundImages"
|
|
1094
|
+
).catch(() => {
|
|
1095
|
+
});
|
|
1096
|
+
};
|
|
1097
|
+
const settleNetwork = async () => {
|
|
1098
|
+
if (opts.network !== "idle" || !doc) {
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
const perf = globalThis.performance;
|
|
1102
|
+
const countResources = () => perf && perf.getEntriesByType ? perf.getEntriesByType("resource").length : 0;
|
|
1103
|
+
const quietMs = 200;
|
|
1104
|
+
const deadline = Date.now() + opts.maxWait;
|
|
1105
|
+
let lastCount = countResources();
|
|
1106
|
+
let lastChange = Date.now();
|
|
1107
|
+
while (Date.now() < deadline) {
|
|
1108
|
+
const count = countResources();
|
|
1109
|
+
if (count !== lastCount) {
|
|
1110
|
+
lastCount = count;
|
|
1111
|
+
lastChange = Date.now();
|
|
1112
|
+
}
|
|
1113
|
+
if (doc.readyState === "complete" && Date.now() - lastChange >= quietMs) {
|
|
1114
|
+
report.network = "idle";
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
await new Promise((resolve) => {
|
|
1118
|
+
setTimeout(resolve, 50);
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
report.network = "timeout";
|
|
1122
|
+
};
|
|
1123
|
+
const settleLayout = async () => {
|
|
1124
|
+
if (!opts.layoutStableMs || opts.layoutStableMs <= 0 || !doc) {
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
const el = doc.documentElement;
|
|
1128
|
+
const deadline = Date.now() + opts.maxWait;
|
|
1129
|
+
let lastHeight = el.scrollHeight;
|
|
1130
|
+
let stableSince = Date.now();
|
|
1131
|
+
while (Date.now() < deadline) {
|
|
1132
|
+
await new Promise((resolve) => {
|
|
1133
|
+
setTimeout(resolve, 50);
|
|
1134
|
+
});
|
|
1135
|
+
const height = el.scrollHeight;
|
|
1136
|
+
if (height !== lastHeight) {
|
|
1137
|
+
lastHeight = height;
|
|
1138
|
+
stableSince = Date.now();
|
|
1139
|
+
} else if (Date.now() - stableSince >= opts.layoutStableMs) {
|
|
1140
|
+
report.layoutStable = "stable";
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
report.layoutStable = "timeout";
|
|
1145
|
+
};
|
|
1039
1146
|
const settleAnimations = async () => {
|
|
1040
1147
|
if (opts.animations === "ignore" || !doc) {
|
|
1041
1148
|
return;
|
|
@@ -1083,9 +1190,12 @@ var require_settle = __commonJS({
|
|
|
1083
1190
|
setTimeout(resolve, opts.idleMs);
|
|
1084
1191
|
});
|
|
1085
1192
|
};
|
|
1193
|
+
await settleNetwork();
|
|
1086
1194
|
await settleFonts();
|
|
1087
1195
|
await settleImages();
|
|
1196
|
+
await settleBackgroundImages();
|
|
1088
1197
|
await settleAnimations();
|
|
1198
|
+
await settleLayout();
|
|
1089
1199
|
await settleIdle();
|
|
1090
1200
|
return report;
|
|
1091
1201
|
}
|
|
@@ -1116,20 +1226,12 @@ var require_page = __commonJS({
|
|
|
1116
1226
|
const start = nowMs();
|
|
1117
1227
|
emit(events, "capture:start", { format, fullPage });
|
|
1118
1228
|
let settleMs = 0;
|
|
1119
|
-
|
|
1229
|
+
let warnings = [];
|
|
1120
1230
|
if (settleOpts) {
|
|
1121
1231
|
const settleStart = nowMs();
|
|
1122
1232
|
const report = await waitForStable(adapter, settleOpts === true ? {} : settleOpts);
|
|
1123
1233
|
settleMs = nowMs() - settleStart;
|
|
1124
|
-
|
|
1125
|
-
warnings.push("fonts did not settle within maxWait");
|
|
1126
|
-
}
|
|
1127
|
-
if (report.animations === "timeout") {
|
|
1128
|
-
warnings.push("animations did not settle within maxWait");
|
|
1129
|
-
}
|
|
1130
|
-
if (report.images.failed > 0) {
|
|
1131
|
-
warnings.push(`${report.images.failed} image(s) failed to load`);
|
|
1132
|
-
}
|
|
1234
|
+
warnings = settleWarnings(report);
|
|
1133
1235
|
}
|
|
1134
1236
|
const metrics = await adapter.getMetrics();
|
|
1135
1237
|
const browserFormat = decideBrowserFormat(format);
|
|
@@ -1169,6 +1271,28 @@ var require_page = __commonJS({
|
|
|
1169
1271
|
});
|
|
1170
1272
|
return result;
|
|
1171
1273
|
}
|
|
1274
|
+
function settleWarnings(report) {
|
|
1275
|
+
const warnings = [];
|
|
1276
|
+
if (report.fonts === "timeout") {
|
|
1277
|
+
warnings.push("fonts did not settle within maxWait");
|
|
1278
|
+
}
|
|
1279
|
+
if (report.animations === "timeout") {
|
|
1280
|
+
warnings.push("animations did not settle within maxWait");
|
|
1281
|
+
}
|
|
1282
|
+
if (report.images.failed > 0) {
|
|
1283
|
+
warnings.push(`${report.images.failed} image(s) failed to load`);
|
|
1284
|
+
}
|
|
1285
|
+
if (report.backgroundImages && report.backgroundImages.failed > 0) {
|
|
1286
|
+
warnings.push(`${report.backgroundImages.failed} background image(s) failed to load`);
|
|
1287
|
+
}
|
|
1288
|
+
if (report.network === "timeout") {
|
|
1289
|
+
warnings.push("network did not reach idle within maxWait");
|
|
1290
|
+
}
|
|
1291
|
+
if (report.layoutStable === "timeout") {
|
|
1292
|
+
warnings.push("layout did not stabilize within maxWait");
|
|
1293
|
+
}
|
|
1294
|
+
return warnings;
|
|
1295
|
+
}
|
|
1172
1296
|
function decideBrowserFormat(format) {
|
|
1173
1297
|
return format === "webp" ? "png" : format;
|
|
1174
1298
|
}
|
|
@@ -1426,6 +1550,7 @@ var require_long_page = __commonJS({
|
|
|
1426
1550
|
var require_element = __commonJS({
|
|
1427
1551
|
"src/capture/element.js"(exports, module) {
|
|
1428
1552
|
var { CaptureTimeoutError } = require_errors();
|
|
1553
|
+
var { waitForStable } = require_settle();
|
|
1429
1554
|
async function captureElement(adapter, options = {}) {
|
|
1430
1555
|
const {
|
|
1431
1556
|
selector,
|
|
@@ -1437,6 +1562,7 @@ var require_element = __commonJS({
|
|
|
1437
1562
|
timeout = 5e3,
|
|
1438
1563
|
format = "png",
|
|
1439
1564
|
quality = 90,
|
|
1565
|
+
waitForStable: settleOpts = false,
|
|
1440
1566
|
events = null
|
|
1441
1567
|
} = options;
|
|
1442
1568
|
if (!selector && !xpath) {
|
|
@@ -1475,6 +1601,7 @@ var require_element = __commonJS({
|
|
|
1475
1601
|
}
|
|
1476
1602
|
}
|
|
1477
1603
|
let viewportRect = lookup.viewportRect;
|
|
1604
|
+
let documentRect = lookup.documentRect;
|
|
1478
1605
|
if (scrollIntoView) {
|
|
1479
1606
|
await adapter.scrollTo({
|
|
1480
1607
|
x: 0,
|
|
@@ -1489,6 +1616,12 @@ var require_element = __commonJS({
|
|
|
1489
1616
|
viewportRect = reread.viewportRect;
|
|
1490
1617
|
}
|
|
1491
1618
|
}
|
|
1619
|
+
const settled = await settleAndReread(adapter, { selector, xpath, timeout, settleOpts });
|
|
1620
|
+
const settleMs = settled.settleMs;
|
|
1621
|
+
if (settled.rect) {
|
|
1622
|
+
viewportRect = settled.rect.viewportRect;
|
|
1623
|
+
documentRect = settled.rect.documentRect;
|
|
1624
|
+
}
|
|
1492
1625
|
const clip = {
|
|
1493
1626
|
x: Math.max(0, Math.round(viewportRect.x - padding)),
|
|
1494
1627
|
y: Math.max(0, Math.round(viewportRect.y - padding)),
|
|
@@ -1510,12 +1643,12 @@ var require_element = __commonJS({
|
|
|
1510
1643
|
format,
|
|
1511
1644
|
bytes,
|
|
1512
1645
|
page: metrics,
|
|
1513
|
-
timing: { settleMs
|
|
1646
|
+
timing: { settleMs, captureMs, encodeMs: 0, totalMs },
|
|
1514
1647
|
warnings: [],
|
|
1515
1648
|
element: {
|
|
1516
1649
|
selector: selector ?? "",
|
|
1517
1650
|
xpath: xpath ?? "",
|
|
1518
|
-
documentRect
|
|
1651
|
+
documentRect,
|
|
1519
1652
|
viewportRect,
|
|
1520
1653
|
verified: Boolean(snippet)
|
|
1521
1654
|
}
|
|
@@ -1523,6 +1656,26 @@ var require_element = __commonJS({
|
|
|
1523
1656
|
emit(events, "capture:complete", { format, totalMs, mode: "element" });
|
|
1524
1657
|
return result;
|
|
1525
1658
|
}
|
|
1659
|
+
async function settleAndReread(adapter, { selector, xpath, timeout, settleOpts }) {
|
|
1660
|
+
if (!settleOpts) {
|
|
1661
|
+
return { settleMs: 0, rect: null };
|
|
1662
|
+
}
|
|
1663
|
+
const settleStart = nowMs();
|
|
1664
|
+
await waitForStable(adapter, settleOpts === true ? {} : settleOpts);
|
|
1665
|
+
const settleMs = nowMs() - settleStart;
|
|
1666
|
+
const reread = await withTimeout(
|
|
1667
|
+
adapter.evaluate(findElementInPage, { selector, xpath }),
|
|
1668
|
+
timeout,
|
|
1669
|
+
{ operation: "adapter.evaluate(findElement after settle)" }
|
|
1670
|
+
);
|
|
1671
|
+
if (reread && reread.found) {
|
|
1672
|
+
return {
|
|
1673
|
+
settleMs,
|
|
1674
|
+
rect: { viewportRect: reread.viewportRect, documentRect: reread.documentRect }
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
return { settleMs, rect: null };
|
|
1678
|
+
}
|
|
1526
1679
|
function findElementInPage(args) {
|
|
1527
1680
|
const doc = globalThis.document;
|
|
1528
1681
|
let element;
|
|
@@ -2772,7 +2925,10 @@ var require_index = __commonJS({
|
|
|
2772
2925
|
waitForStable: Object.freeze({
|
|
2773
2926
|
fonts: true,
|
|
2774
2927
|
images: true,
|
|
2928
|
+
backgroundImages: true,
|
|
2775
2929
|
animations: "reduce",
|
|
2930
|
+
network: "idle",
|
|
2931
|
+
layoutStableMs: 250,
|
|
2776
2932
|
maxWait: 5e3,
|
|
2777
2933
|
idleMs: 200
|
|
2778
2934
|
})
|