@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.
Files changed (86) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +20 -20
  3. package/dist/index.js +172 -16
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +172 -16
  6. package/dist/index.mjs.map +1 -1
  7. package/dist/types/adapters/cdp.d.ts +28 -0
  8. package/dist/types/adapters/cdp.d.ts.map +1 -0
  9. package/dist/types/adapters/index.d.ts +5 -0
  10. package/dist/types/adapters/index.d.ts.map +1 -0
  11. package/dist/types/adapters/playwright.d.ts +28 -0
  12. package/dist/types/adapters/playwright.d.ts.map +1 -0
  13. package/dist/types/adapters/puppeteer.d.ts +20 -0
  14. package/dist/types/adapters/puppeteer.d.ts.map +1 -0
  15. package/dist/types/capture/element.d.ts +20 -0
  16. package/dist/types/capture/element.d.ts.map +1 -0
  17. package/dist/types/capture/index.d.ts +8 -0
  18. package/dist/types/capture/index.d.ts.map +1 -0
  19. package/dist/types/capture/long-page.d.ts +16 -0
  20. package/dist/types/capture/long-page.d.ts.map +1 -0
  21. package/dist/types/capture/page.d.ts +14 -0
  22. package/dist/types/capture/page.d.ts.map +1 -0
  23. package/dist/types/capture/responsive.d.ts +20 -0
  24. package/dist/types/capture/responsive.d.ts.map +1 -0
  25. package/dist/types/capture/settle.d.ts +14 -0
  26. package/dist/types/capture/settle.d.ts.map +1 -0
  27. package/dist/types/capture/viewport.d.ts +14 -0
  28. package/dist/types/capture/viewport.d.ts.map +1 -0
  29. package/dist/types/cli/index.d.ts +10 -0
  30. package/dist/types/cli/index.d.ts.map +1 -0
  31. package/dist/types/compose/annotate.d.ts +16 -0
  32. package/dist/types/compose/annotate.d.ts.map +1 -0
  33. package/dist/types/compose/diff.d.ts +15 -0
  34. package/dist/types/compose/diff.d.ts.map +1 -0
  35. package/dist/types/compose/heatmap.d.ts +17 -0
  36. package/dist/types/compose/heatmap.d.ts.map +1 -0
  37. package/dist/types/compose/index.d.ts +6 -0
  38. package/dist/types/compose/index.d.ts.map +1 -0
  39. package/dist/types/compose/theme.d.ts +17 -0
  40. package/dist/types/compose/theme.d.ts.map +1 -0
  41. package/dist/types/errors/adapter.d.ts +33 -0
  42. package/dist/types/errors/adapter.d.ts.map +1 -0
  43. package/dist/types/errors/base.d.ts +37 -0
  44. package/dist/types/errors/base.d.ts.map +1 -0
  45. package/dist/types/errors/capture.d.ts +109 -0
  46. package/dist/types/errors/capture.d.ts.map +1 -0
  47. package/dist/types/errors/compose.d.ts +35 -0
  48. package/dist/types/errors/compose.d.ts.map +1 -0
  49. package/dist/types/errors/index.d.ts +29 -0
  50. package/dist/types/errors/index.d.ts.map +1 -0
  51. package/dist/types/errors/optional.d.ts +23 -0
  52. package/dist/types/errors/optional.d.ts.map +1 -0
  53. package/dist/types/errors/redaction.d.ts +21 -0
  54. package/dist/types/errors/redaction.d.ts.map +1 -0
  55. package/dist/types/errors/tile.d.ts +29 -0
  56. package/dist/types/errors/tile.d.ts.map +1 -0
  57. package/dist/types/errors/transform.d.ts +35 -0
  58. package/dist/types/errors/transform.d.ts.map +1 -0
  59. package/dist/types/index.d.ts +58 -0
  60. package/dist/types/index.d.ts.map +1 -0
  61. package/dist/types/redact/index.d.ts +5 -0
  62. package/dist/types/redact/index.d.ts.map +1 -0
  63. package/dist/types/redact/policy.d.ts +29 -0
  64. package/dist/types/redact/policy.d.ts.map +1 -0
  65. package/dist/types/redact/redact.d.ts +29 -0
  66. package/dist/types/redact/redact.d.ts.map +1 -0
  67. package/dist/types/tile/dzi.d.ts +18 -0
  68. package/dist/types/tile/dzi.d.ts.map +1 -0
  69. package/dist/types/tile/index.d.ts +3 -0
  70. package/dist/types/tile/index.d.ts.map +1 -0
  71. package/dist/types/transform/convert.d.ts +13 -0
  72. package/dist/types/transform/convert.d.ts.map +1 -0
  73. package/dist/types/transform/crop.d.ts +33 -0
  74. package/dist/types/transform/crop.d.ts.map +1 -0
  75. package/dist/types/transform/index.d.ts +7 -0
  76. package/dist/types/transform/index.d.ts.map +1 -0
  77. package/dist/types/transform/normalize.d.ts +14 -0
  78. package/dist/types/transform/normalize.d.ts.map +1 -0
  79. package/dist/types/transform/resize.d.ts +14 -0
  80. package/dist/types/transform/resize.d.ts.map +1 -0
  81. package/package.json +5 -4
  82. package/src/capture/element.js +48 -6
  83. package/src/capture/page.js +31 -10
  84. package/src/capture/settle.js +139 -4
  85. package/src/index.js +3 -0
  86. 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 bar in `spec.md` § 23 is finished (OpenSeadragon e2e, 80% coverage, perf suite in CI). The API is expected to stay stable through to 1.0.0.
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 (Phase 2) |
48
- | `pixelmatch` | Optional peer | `diff()` function (Phase 4) |
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 `>=22.22.1` is required (`engine-strict=true`).
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/adapters/puppeteer');
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` (Phase 4) for the contract.
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 (Phase 4)
81
- - `heatmap(buffer, points, opts)` — kernel-density overlay (Phase 4)
82
- - `diff(a, b, opts)` — pixelmatch-backed visual diff (Phase 4)
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 (Phase 2)
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 (Phase 4)
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 (Phase 4)
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` (Phase 4) for ceilings.
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` (Phase 4).
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.9.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: ">=22.22.1"
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.34.5"
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-v8": "4.1.5",
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
- const warnings = [];
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
- if (report.fonts === "timeout") {
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: 0, captureMs, encodeMs: 0, totalMs },
1646
+ timing: { settleMs, captureMs, encodeMs: 0, totalMs },
1514
1647
  warnings: [],
1515
1648
  element: {
1516
1649
  selector: selector ?? "",
1517
1650
  xpath: xpath ?? "",
1518
- documentRect: lookup.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
  })