@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/dist/index.mjs CHANGED
@@ -97,7 +97,7 @@ var require_package = __commonJS({
97
97
  access: "restricted"
98
98
  },
99
99
  engines: {
100
- node: ">=20.0.0"
100
+ node: ">=20.9.0"
101
101
  },
102
102
  keywords: [
103
103
  "screenshot",
@@ -115,7 +115,7 @@ var require_package = __commonJS({
115
115
  "revenant"
116
116
  ],
117
117
  dependencies: {
118
- sharp: "0.34.5"
118
+ sharp: "^0.35.3"
119
119
  },
120
120
  peerDependencies: {
121
121
  "@napi-rs/canvas": "^0.1.0",
@@ -961,7 +961,10 @@ var require_settle = __commonJS({
961
961
  const {
962
962
  fonts = true,
963
963
  images = true,
964
+ backgroundImages = true,
964
965
  animations = "reduce",
966
+ network = "idle",
967
+ layoutStableMs = 250,
965
968
  maxWait = 5e3,
966
969
  idleMs = 200
967
970
  } = options;
@@ -969,7 +972,10 @@ var require_settle = __commonJS({
969
972
  const report = await adapter.evaluate(runInPage, {
970
973
  fonts,
971
974
  images,
975
+ backgroundImages,
972
976
  animations,
977
+ network,
978
+ layoutStableMs,
973
979
  maxWait,
974
980
  idleMs
975
981
  });
@@ -981,6 +987,9 @@ var require_settle = __commonJS({
981
987
  const report = {
982
988
  fonts: "skipped",
983
989
  images: { total: 0, loaded: 0, failed: 0 },
990
+ backgroundImages: "skipped",
991
+ network: "skipped",
992
+ layoutStable: "skipped",
984
993
  animations: "skipped"
985
994
  };
986
995
  const withTimeout = (promise, ms, label) => Promise.race([
@@ -1035,6 +1044,103 @@ var require_settle = __commonJS({
1035
1044
  report.images.total = imgs.length;
1036
1045
  await Promise.all(imgs.map(settleOneImage));
1037
1046
  };
1047
+ const collectBackgroundUrls = () => {
1048
+ const CAP = 200;
1049
+ const urls = /* @__PURE__ */ new Set();
1050
+ const els = doc.querySelectorAll("*");
1051
+ for (let i = 0; i < els.length && urls.size < CAP; i += 1) {
1052
+ const bg = globalThis.getComputedStyle(els[i]).backgroundImage;
1053
+ if (!bg || bg === "none") {
1054
+ continue;
1055
+ }
1056
+ for (const match of bg.matchAll(/url\((['"]?)([^'")]+)\1\)/g)) {
1057
+ const url = match[2];
1058
+ if (url && !url.startsWith("data:")) {
1059
+ urls.add(url);
1060
+ }
1061
+ }
1062
+ }
1063
+ return Array.from(urls).slice(0, CAP);
1064
+ };
1065
+ const settleOneBackgroundUrl = (url) => new Promise((resolve) => {
1066
+ const img = new globalThis.Image();
1067
+ const onload = () => {
1068
+ report.backgroundImages.loaded += 1;
1069
+ resolve();
1070
+ };
1071
+ const onerror = () => {
1072
+ report.backgroundImages.failed += 1;
1073
+ resolve();
1074
+ };
1075
+ img.addEventListener("load", onload, { once: true });
1076
+ img.addEventListener("error", onerror, { once: true });
1077
+ img.src = url;
1078
+ });
1079
+ const settleBackgroundImages = async () => {
1080
+ if (!opts.backgroundImages || !doc) {
1081
+ return;
1082
+ }
1083
+ const list = collectBackgroundUrls();
1084
+ report.backgroundImages = { total: list.length, loaded: 0, failed: 0 };
1085
+ if (list.length === 0) {
1086
+ return;
1087
+ }
1088
+ await withTimeout(
1089
+ Promise.all(list.map(settleOneBackgroundUrl)),
1090
+ opts.maxWait,
1091
+ "backgroundImages"
1092
+ ).catch(() => {
1093
+ });
1094
+ };
1095
+ const settleNetwork = async () => {
1096
+ if (opts.network !== "idle" || !doc) {
1097
+ return;
1098
+ }
1099
+ const perf = globalThis.performance;
1100
+ const countResources = () => perf && perf.getEntriesByType ? perf.getEntriesByType("resource").length : 0;
1101
+ const quietMs = 200;
1102
+ const deadline = Date.now() + opts.maxWait;
1103
+ let lastCount = countResources();
1104
+ let lastChange = Date.now();
1105
+ while (Date.now() < deadline) {
1106
+ const count = countResources();
1107
+ if (count !== lastCount) {
1108
+ lastCount = count;
1109
+ lastChange = Date.now();
1110
+ }
1111
+ if (doc.readyState === "complete" && Date.now() - lastChange >= quietMs) {
1112
+ report.network = "idle";
1113
+ return;
1114
+ }
1115
+ await new Promise((resolve) => {
1116
+ setTimeout(resolve, 50);
1117
+ });
1118
+ }
1119
+ report.network = "timeout";
1120
+ };
1121
+ const settleLayout = async () => {
1122
+ if (!opts.layoutStableMs || opts.layoutStableMs <= 0 || !doc) {
1123
+ return;
1124
+ }
1125
+ const el = doc.documentElement;
1126
+ const deadline = Date.now() + opts.maxWait;
1127
+ let lastHeight = el.scrollHeight;
1128
+ let stableSince = Date.now();
1129
+ while (Date.now() < deadline) {
1130
+ await new Promise((resolve) => {
1131
+ setTimeout(resolve, 50);
1132
+ });
1133
+ const height = el.scrollHeight;
1134
+ if (height !== lastHeight) {
1135
+ lastHeight = height;
1136
+ stableSince = Date.now();
1137
+ } else if (Date.now() - stableSince >= opts.layoutStableMs) {
1138
+ report.layoutStable = "stable";
1139
+ return;
1140
+ }
1141
+ }
1142
+ report.layoutStable = "timeout";
1143
+ };
1038
1144
  const settleAnimations = async () => {
1039
1145
  if (opts.animations === "ignore" || !doc) {
1040
1146
  return;
@@ -1082,9 +1188,12 @@ var require_settle = __commonJS({
1082
1188
  setTimeout(resolve, opts.idleMs);
1083
1189
  });
1084
1190
  };
1191
+ await settleNetwork();
1085
1192
  await settleFonts();
1086
1193
  await settleImages();
1194
+ await settleBackgroundImages();
1087
1195
  await settleAnimations();
1196
+ await settleLayout();
1088
1197
  await settleIdle();
1089
1198
  return report;
1090
1199
  }
@@ -1115,20 +1224,12 @@ var require_page = __commonJS({
1115
1224
  const start = nowMs();
1116
1225
  emit(events, "capture:start", { format, fullPage });
1117
1226
  let settleMs = 0;
1118
- const warnings = [];
1227
+ let warnings = [];
1119
1228
  if (settleOpts) {
1120
1229
  const settleStart = nowMs();
1121
1230
  const report = await waitForStable(adapter, settleOpts === true ? {} : settleOpts);
1122
1231
  settleMs = nowMs() - settleStart;
1123
- if (report.fonts === "timeout") {
1124
- warnings.push("fonts did not settle within maxWait");
1125
- }
1126
- if (report.animations === "timeout") {
1127
- warnings.push("animations did not settle within maxWait");
1128
- }
1129
- if (report.images.failed > 0) {
1130
- warnings.push(`${report.images.failed} image(s) failed to load`);
1131
- }
1232
+ warnings = settleWarnings(report);
1132
1233
  }
1133
1234
  const metrics = await adapter.getMetrics();
1134
1235
  const browserFormat = decideBrowserFormat(format);
@@ -1168,6 +1269,28 @@ var require_page = __commonJS({
1168
1269
  });
1169
1270
  return result;
1170
1271
  }
1272
+ function settleWarnings(report) {
1273
+ const warnings = [];
1274
+ if (report.fonts === "timeout") {
1275
+ warnings.push("fonts did not settle within maxWait");
1276
+ }
1277
+ if (report.animations === "timeout") {
1278
+ warnings.push("animations did not settle within maxWait");
1279
+ }
1280
+ if (report.images.failed > 0) {
1281
+ warnings.push(`${report.images.failed} image(s) failed to load`);
1282
+ }
1283
+ if (report.backgroundImages && report.backgroundImages.failed > 0) {
1284
+ warnings.push(`${report.backgroundImages.failed} background image(s) failed to load`);
1285
+ }
1286
+ if (report.network === "timeout") {
1287
+ warnings.push("network did not reach idle within maxWait");
1288
+ }
1289
+ if (report.layoutStable === "timeout") {
1290
+ warnings.push("layout did not stabilize within maxWait");
1291
+ }
1292
+ return warnings;
1293
+ }
1171
1294
  function decideBrowserFormat(format) {
1172
1295
  return format === "webp" ? "png" : format;
1173
1296
  }
@@ -1425,6 +1548,7 @@ var require_long_page = __commonJS({
1425
1548
  var require_element = __commonJS({
1426
1549
  "src/capture/element.js"(exports, module) {
1427
1550
  var { CaptureTimeoutError } = require_errors();
1551
+ var { waitForStable } = require_settle();
1428
1552
  async function captureElement(adapter, options = {}) {
1429
1553
  const {
1430
1554
  selector,
@@ -1436,6 +1560,7 @@ var require_element = __commonJS({
1436
1560
  timeout = 5e3,
1437
1561
  format = "png",
1438
1562
  quality = 90,
1563
+ waitForStable: settleOpts = false,
1439
1564
  events = null
1440
1565
  } = options;
1441
1566
  if (!selector && !xpath) {
@@ -1474,6 +1599,7 @@ var require_element = __commonJS({
1474
1599
  }
1475
1600
  }
1476
1601
  let viewportRect = lookup.viewportRect;
1602
+ let documentRect = lookup.documentRect;
1477
1603
  if (scrollIntoView) {
1478
1604
  await adapter.scrollTo({
1479
1605
  x: 0,
@@ -1488,6 +1614,12 @@ var require_element = __commonJS({
1488
1614
  viewportRect = reread.viewportRect;
1489
1615
  }
1490
1616
  }
1617
+ const settled = await settleAndReread(adapter, { selector, xpath, timeout, settleOpts });
1618
+ const settleMs = settled.settleMs;
1619
+ if (settled.rect) {
1620
+ viewportRect = settled.rect.viewportRect;
1621
+ documentRect = settled.rect.documentRect;
1622
+ }
1491
1623
  const clip = {
1492
1624
  x: Math.max(0, Math.round(viewportRect.x - padding)),
1493
1625
  y: Math.max(0, Math.round(viewportRect.y - padding)),
@@ -1509,12 +1641,12 @@ var require_element = __commonJS({
1509
1641
  format,
1510
1642
  bytes,
1511
1643
  page: metrics,
1512
- timing: { settleMs: 0, captureMs, encodeMs: 0, totalMs },
1644
+ timing: { settleMs, captureMs, encodeMs: 0, totalMs },
1513
1645
  warnings: [],
1514
1646
  element: {
1515
1647
  selector: selector ?? "",
1516
1648
  xpath: xpath ?? "",
1517
- documentRect: lookup.documentRect,
1649
+ documentRect,
1518
1650
  viewportRect,
1519
1651
  verified: Boolean(snippet)
1520
1652
  }
@@ -1522,6 +1654,26 @@ var require_element = __commonJS({
1522
1654
  emit(events, "capture:complete", { format, totalMs, mode: "element" });
1523
1655
  return result;
1524
1656
  }
1657
+ async function settleAndReread(adapter, { selector, xpath, timeout, settleOpts }) {
1658
+ if (!settleOpts) {
1659
+ return { settleMs: 0, rect: null };
1660
+ }
1661
+ const settleStart = nowMs();
1662
+ await waitForStable(adapter, settleOpts === true ? {} : settleOpts);
1663
+ const settleMs = nowMs() - settleStart;
1664
+ const reread = await withTimeout(
1665
+ adapter.evaluate(findElementInPage, { selector, xpath }),
1666
+ timeout,
1667
+ { operation: "adapter.evaluate(findElement after settle)" }
1668
+ );
1669
+ if (reread && reread.found) {
1670
+ return {
1671
+ settleMs,
1672
+ rect: { viewportRect: reread.viewportRect, documentRect: reread.documentRect }
1673
+ };
1674
+ }
1675
+ return { settleMs, rect: null };
1676
+ }
1525
1677
  function findElementInPage(args) {
1526
1678
  const doc = globalThis.document;
1527
1679
  let element;
@@ -2771,7 +2923,10 @@ var require_index = __commonJS({
2771
2923
  waitForStable: Object.freeze({
2772
2924
  fonts: true,
2773
2925
  images: true,
2926
+ backgroundImages: true,
2774
2927
  animations: "reduce",
2928
+ network: "idle",
2929
+ layoutStableMs: 250,
2775
2930
  maxWait: 5e3,
2776
2931
  idleMs: 200
2777
2932
  })