@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
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
All notable changes to this project will be documented in this file.
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
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).
|
|
6
40
|
|
|
7
41
|
## [0.9.0] - 2026-05-22
|
|
8
42
|
|
package/dist/index.js
CHANGED
|
@@ -99,7 +99,7 @@ var require_package = __commonJS({
|
|
|
99
99
|
access: "restricted"
|
|
100
100
|
},
|
|
101
101
|
engines: {
|
|
102
|
-
node: ">=20.
|
|
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",
|
|
@@ -963,7 +963,10 @@ var require_settle = __commonJS({
|
|
|
963
963
|
const {
|
|
964
964
|
fonts = true,
|
|
965
965
|
images = true,
|
|
966
|
+
backgroundImages = true,
|
|
966
967
|
animations = "reduce",
|
|
968
|
+
network = "idle",
|
|
969
|
+
layoutStableMs = 250,
|
|
967
970
|
maxWait = 5e3,
|
|
968
971
|
idleMs = 200
|
|
969
972
|
} = options;
|
|
@@ -971,7 +974,10 @@ var require_settle = __commonJS({
|
|
|
971
974
|
const report = await adapter.evaluate(runInPage, {
|
|
972
975
|
fonts,
|
|
973
976
|
images,
|
|
977
|
+
backgroundImages,
|
|
974
978
|
animations,
|
|
979
|
+
network,
|
|
980
|
+
layoutStableMs,
|
|
975
981
|
maxWait,
|
|
976
982
|
idleMs
|
|
977
983
|
});
|
|
@@ -983,6 +989,9 @@ var require_settle = __commonJS({
|
|
|
983
989
|
const report = {
|
|
984
990
|
fonts: "skipped",
|
|
985
991
|
images: { total: 0, loaded: 0, failed: 0 },
|
|
992
|
+
backgroundImages: "skipped",
|
|
993
|
+
network: "skipped",
|
|
994
|
+
layoutStable: "skipped",
|
|
986
995
|
animations: "skipped"
|
|
987
996
|
};
|
|
988
997
|
const withTimeout = (promise, ms, label) => Promise.race([
|
|
@@ -1037,6 +1046,103 @@ var require_settle = __commonJS({
|
|
|
1037
1046
|
report.images.total = imgs.length;
|
|
1038
1047
|
await Promise.all(imgs.map(settleOneImage));
|
|
1039
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
|
+
};
|
|
1040
1146
|
const settleAnimations = async () => {
|
|
1041
1147
|
if (opts.animations === "ignore" || !doc) {
|
|
1042
1148
|
return;
|
|
@@ -1084,9 +1190,12 @@ var require_settle = __commonJS({
|
|
|
1084
1190
|
setTimeout(resolve, opts.idleMs);
|
|
1085
1191
|
});
|
|
1086
1192
|
};
|
|
1193
|
+
await settleNetwork();
|
|
1087
1194
|
await settleFonts();
|
|
1088
1195
|
await settleImages();
|
|
1196
|
+
await settleBackgroundImages();
|
|
1089
1197
|
await settleAnimations();
|
|
1198
|
+
await settleLayout();
|
|
1090
1199
|
await settleIdle();
|
|
1091
1200
|
return report;
|
|
1092
1201
|
}
|
|
@@ -1117,20 +1226,12 @@ var require_page = __commonJS({
|
|
|
1117
1226
|
const start = nowMs();
|
|
1118
1227
|
emit(events, "capture:start", { format, fullPage });
|
|
1119
1228
|
let settleMs = 0;
|
|
1120
|
-
|
|
1229
|
+
let warnings = [];
|
|
1121
1230
|
if (settleOpts) {
|
|
1122
1231
|
const settleStart = nowMs();
|
|
1123
1232
|
const report = await waitForStable(adapter, settleOpts === true ? {} : settleOpts);
|
|
1124
1233
|
settleMs = nowMs() - settleStart;
|
|
1125
|
-
|
|
1126
|
-
warnings.push("fonts did not settle within maxWait");
|
|
1127
|
-
}
|
|
1128
|
-
if (report.animations === "timeout") {
|
|
1129
|
-
warnings.push("animations did not settle within maxWait");
|
|
1130
|
-
}
|
|
1131
|
-
if (report.images.failed > 0) {
|
|
1132
|
-
warnings.push(`${report.images.failed} image(s) failed to load`);
|
|
1133
|
-
}
|
|
1234
|
+
warnings = settleWarnings(report);
|
|
1134
1235
|
}
|
|
1135
1236
|
const metrics = await adapter.getMetrics();
|
|
1136
1237
|
const browserFormat = decideBrowserFormat(format);
|
|
@@ -1170,6 +1271,28 @@ var require_page = __commonJS({
|
|
|
1170
1271
|
});
|
|
1171
1272
|
return result;
|
|
1172
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
|
+
}
|
|
1173
1296
|
function decideBrowserFormat(format) {
|
|
1174
1297
|
return format === "webp" ? "png" : format;
|
|
1175
1298
|
}
|
|
@@ -1427,6 +1550,7 @@ var require_long_page = __commonJS({
|
|
|
1427
1550
|
var require_element = __commonJS({
|
|
1428
1551
|
"src/capture/element.js"(exports, module) {
|
|
1429
1552
|
var { CaptureTimeoutError } = require_errors();
|
|
1553
|
+
var { waitForStable } = require_settle();
|
|
1430
1554
|
async function captureElement(adapter, options = {}) {
|
|
1431
1555
|
const {
|
|
1432
1556
|
selector,
|
|
@@ -1438,6 +1562,7 @@ var require_element = __commonJS({
|
|
|
1438
1562
|
timeout = 5e3,
|
|
1439
1563
|
format = "png",
|
|
1440
1564
|
quality = 90,
|
|
1565
|
+
waitForStable: settleOpts = false,
|
|
1441
1566
|
events = null
|
|
1442
1567
|
} = options;
|
|
1443
1568
|
if (!selector && !xpath) {
|
|
@@ -1476,6 +1601,7 @@ var require_element = __commonJS({
|
|
|
1476
1601
|
}
|
|
1477
1602
|
}
|
|
1478
1603
|
let viewportRect = lookup.viewportRect;
|
|
1604
|
+
let documentRect = lookup.documentRect;
|
|
1479
1605
|
if (scrollIntoView) {
|
|
1480
1606
|
await adapter.scrollTo({
|
|
1481
1607
|
x: 0,
|
|
@@ -1490,6 +1616,12 @@ var require_element = __commonJS({
|
|
|
1490
1616
|
viewportRect = reread.viewportRect;
|
|
1491
1617
|
}
|
|
1492
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
|
+
}
|
|
1493
1625
|
const clip = {
|
|
1494
1626
|
x: Math.max(0, Math.round(viewportRect.x - padding)),
|
|
1495
1627
|
y: Math.max(0, Math.round(viewportRect.y - padding)),
|
|
@@ -1511,12 +1643,12 @@ var require_element = __commonJS({
|
|
|
1511
1643
|
format,
|
|
1512
1644
|
bytes,
|
|
1513
1645
|
page: metrics,
|
|
1514
|
-
timing: { settleMs
|
|
1646
|
+
timing: { settleMs, captureMs, encodeMs: 0, totalMs },
|
|
1515
1647
|
warnings: [],
|
|
1516
1648
|
element: {
|
|
1517
1649
|
selector: selector ?? "",
|
|
1518
1650
|
xpath: xpath ?? "",
|
|
1519
|
-
documentRect
|
|
1651
|
+
documentRect,
|
|
1520
1652
|
viewportRect,
|
|
1521
1653
|
verified: Boolean(snippet)
|
|
1522
1654
|
}
|
|
@@ -1524,6 +1656,26 @@ var require_element = __commonJS({
|
|
|
1524
1656
|
emit(events, "capture:complete", { format, totalMs, mode: "element" });
|
|
1525
1657
|
return result;
|
|
1526
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
|
+
}
|
|
1527
1679
|
function findElementInPage(args) {
|
|
1528
1680
|
const doc = globalThis.document;
|
|
1529
1681
|
let element;
|
|
@@ -2773,7 +2925,10 @@ var require_index = __commonJS({
|
|
|
2773
2925
|
waitForStable: Object.freeze({
|
|
2774
2926
|
fonts: true,
|
|
2775
2927
|
images: true,
|
|
2928
|
+
backgroundImages: true,
|
|
2776
2929
|
animations: "reduce",
|
|
2930
|
+
network: "idle",
|
|
2931
|
+
layoutStableMs: 250,
|
|
2777
2932
|
maxWait: 5e3,
|
|
2778
2933
|
idleMs: 200
|
|
2779
2934
|
})
|