@percy/sdk-utils 1.32.10-beta.1 → 1.32.11-beta.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/bundle.js CHANGED
@@ -472,6 +472,62 @@
472
472
  // options,
473
473
  // { callback: true, log }
474
474
  // );
475
+ // Effective in-page timeout for each readiness preset, mirroring PRESETS in
476
+ // @percy/dom's readiness.js. Duplicated here because the presets live in the
477
+ // browser bundle, and the Node side needs the number to size its own deadline.
478
+ const PRESET_TIMEOUT_MS = {
479
+ balanced: 10000,
480
+ strict: 30000,
481
+ fast: 5000
482
+ };
483
+
484
+ // Added to the in-page timeout before the Node side gives up, so a gate that is
485
+ // merely slow (eval round-trip, check teardown) is never pre-empted.
486
+ const READINESS_DEADLINE_GRACE_MS = 3000;
487
+
488
+ // Wall-clock budget the Node side gives the readiness eval.
489
+ //
490
+ // PercyDOM.waitForReady() bounds itself with an in-page `setTimeout`, and every
491
+ // individual check settles on a `setTimeout`/`setInterval`. On a page whose
492
+ // timers are faked and paused -- Playwright's `page.clock.pauseAt()`, sinon
493
+ // `useFakeTimers`, jest fake timers -- neither the checks nor the gate's own
494
+ // timeout can ever fire, so the eval stays pending for the life of the page and
495
+ // hangs the test that called percySnapshot(). Node is the only side of that
496
+ // boundary guaranteed to have a real clock, so the backstop belongs here.
497
+ function readinessDeadlineMs(readinessConfig = {}) {
498
+ let timeout = readinessConfig.timeoutMs ?? readinessConfig.timeout_ms ?? PRESET_TIMEOUT_MS[readinessConfig.preset] ?? PRESET_TIMEOUT_MS.balanced;
499
+ let max = readinessConfig.maxTimeoutMs ?? readinessConfig.max_timeout_ms;
500
+ if (max != null) timeout = Math.min(timeout, max);
501
+ return timeout + READINESS_DEADLINE_GRACE_MS;
502
+ }
503
+
504
+ // Captured once at module load, because the deadline below must not be the very
505
+ // thing a faked clock disables. jest's and sinon's fake timers replace the
506
+ // *global* `setTimeout` binding, and a bare `setTimeout(...)` call resolves that
507
+ // global at call time -- so a consumer whose Node test process has fake timers
508
+ // installed would schedule the deadline on a frozen clock and hang exactly as
509
+ // before, one layer up from the in-page freeze this gate exists to survive.
510
+ //
511
+ // The capture holds for the ordinary case: the SDK imports this module at
512
+ // require time, before a test body or beforeEach reaches `useFakeTimers()`. It
513
+ // is NOT a guarantee. Fake timers installed before this module is first
514
+ // evaluated -- jest's `fakeTimers: { enableGlobally: true }`, a `useFakeTimers()`
515
+ // call in `setupFiles`, or `resetModules()` + a fresh require under an already
516
+ // faked clock -- capture the fake, and the hang returns. Those consumers need
517
+ // `snapshot.readiness.preset: disabled`.
518
+ //
519
+ // Deliberately not `import { setTimeout } from 'node:timers'`, which would be
520
+ // immune to import order too: this file is imported statically by index.js, and
521
+ // index.js is the rollup entry for the browser bundle (see the package's
522
+ // `browser` field). Node-only code in this package is always reached through a
523
+ // lazy `await ({})` -- `http`/`https` in request.js, `./proxy.js` and its
524
+ // `net`/`tls` imports -- precisely to keep builtins out of that graph. A static
525
+ // builtin import here would not even fail the build, since the rollup config
526
+ // suppresses MISSING_NODE_BUILTINS; it would ship a browser bundle that breaks
527
+ // at runtime, which is worse.
528
+ const nativeSetTimeout = globalThis.setTimeout;
529
+ const nativeClearTimeout = globalThis.clearTimeout;
530
+ const READINESS_DEADLINE_HIT = Symbol('readiness-deadline');
475
531
  async function runReadinessGate(evalScript, snapshotOptions = {}, {
476
532
  callback = false,
477
533
  log
@@ -481,13 +537,31 @@
481
537
  const script = waitForReadyScript(config, {
482
538
  callback
483
539
  });
540
+ const deadline = readinessDeadlineMs(config);
541
+ let timer;
484
542
  try {
485
- return await evalScript(script);
543
+ const evaluation = Promise.resolve(evalScript(script));
544
+ // Hitting the deadline abandons `evaluation`, which may still reject much
545
+ // later -- when the driver tears the page down at end of test, say. Swallow
546
+ // that here so it never surfaces as an unhandled rejection.
547
+ evaluation.catch(() => {});
548
+ const result = await Promise.race([evaluation, new Promise(resolve => {
549
+ timer = nativeSetTimeout(() => resolve(READINESS_DEADLINE_HIT), deadline);
550
+ })]);
551
+ if (result === READINESS_DEADLINE_HIT) {
552
+ if (log && typeof log.debug === 'function') {
553
+ log.debug(`waitForReady did not settle within ${deadline}ms, proceeding to serialize. ` + 'If this page fakes or pauses timers (e.g. Playwright page.clock), disable ' + 'the readiness gate with snapshot.readiness.preset: disabled.');
554
+ }
555
+ return null;
556
+ }
557
+ return result;
486
558
  } catch (err) {
487
559
  if (log && typeof log.debug === 'function') {
488
560
  log.debug(`waitForReady failed, proceeding to serialize: ${(err === null || err === void 0 ? void 0 : err.message) || err}`);
489
561
  }
490
562
  return null;
563
+ } finally {
564
+ nativeClearTimeout(timer);
491
565
  }
492
566
  }
493
567
 
@@ -582,6 +656,7 @@
582
656
  waitForReadyScript: waitForReadyScript,
583
657
  getReadinessConfig: getReadinessConfig,
584
658
  isReadinessDisabled: isReadinessDisabled,
659
+ readinessDeadlineMs: readinessDeadlineMs,
585
660
  runReadinessGate: runReadinessGate,
586
661
  'default': index
587
662
  });
@@ -607,6 +682,7 @@
607
682
  exports.postComparison = postComparison;
608
683
  exports.postPdfSnapshot = postPdfSnapshot;
609
684
  exports.postSnapshot = postSnapshot;
685
+ exports.readinessDeadlineMs = readinessDeadlineMs;
610
686
  exports.request = request;
611
687
  exports.resolveIgnoreSelectors = resolveIgnoreSelectors;
612
688
  exports.resolveMaxFrameDepth = resolveMaxFrameDepth;
package/dist/index.js CHANGED
@@ -92,6 +92,12 @@ Object.defineProperty(exports, "postSnapshot", {
92
92
  return _postSnapshot.default;
93
93
  }
94
94
  });
95
+ Object.defineProperty(exports, "readinessDeadlineMs", {
96
+ enumerable: true,
97
+ get: function () {
98
+ return _serializeDom.readinessDeadlineMs;
99
+ }
100
+ });
95
101
  Object.defineProperty(exports, "request", {
96
102
  enumerable: true,
97
103
  get: function () {
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.default = void 0;
7
7
  exports.getReadinessConfig = getReadinessConfig;
8
8
  exports.isReadinessDisabled = isReadinessDisabled;
9
+ exports.readinessDeadlineMs = readinessDeadlineMs;
9
10
  exports.runReadinessGate = runReadinessGate;
10
11
  exports.waitForReadyScript = waitForReadyScript;
11
12
  var _percyInfo = _interopRequireDefault(require("./percy-info.js"));
@@ -116,6 +117,62 @@ function waitForReadyScript(readinessConfig = {}, {
116
117
  // options,
117
118
  // { callback: true, log }
118
119
  // );
120
+ // Effective in-page timeout for each readiness preset, mirroring PRESETS in
121
+ // @percy/dom's readiness.js. Duplicated here because the presets live in the
122
+ // browser bundle, and the Node side needs the number to size its own deadline.
123
+ const PRESET_TIMEOUT_MS = {
124
+ balanced: 10000,
125
+ strict: 30000,
126
+ fast: 5000
127
+ };
128
+
129
+ // Added to the in-page timeout before the Node side gives up, so a gate that is
130
+ // merely slow (eval round-trip, check teardown) is never pre-empted.
131
+ const READINESS_DEADLINE_GRACE_MS = 3000;
132
+
133
+ // Wall-clock budget the Node side gives the readiness eval.
134
+ //
135
+ // PercyDOM.waitForReady() bounds itself with an in-page `setTimeout`, and every
136
+ // individual check settles on a `setTimeout`/`setInterval`. On a page whose
137
+ // timers are faked and paused -- Playwright's `page.clock.pauseAt()`, sinon
138
+ // `useFakeTimers`, jest fake timers -- neither the checks nor the gate's own
139
+ // timeout can ever fire, so the eval stays pending for the life of the page and
140
+ // hangs the test that called percySnapshot(). Node is the only side of that
141
+ // boundary guaranteed to have a real clock, so the backstop belongs here.
142
+ function readinessDeadlineMs(readinessConfig = {}) {
143
+ let timeout = readinessConfig.timeoutMs ?? readinessConfig.timeout_ms ?? PRESET_TIMEOUT_MS[readinessConfig.preset] ?? PRESET_TIMEOUT_MS.balanced;
144
+ let max = readinessConfig.maxTimeoutMs ?? readinessConfig.max_timeout_ms;
145
+ if (max != null) timeout = Math.min(timeout, max);
146
+ return timeout + READINESS_DEADLINE_GRACE_MS;
147
+ }
148
+
149
+ // Captured once at module load, because the deadline below must not be the very
150
+ // thing a faked clock disables. jest's and sinon's fake timers replace the
151
+ // *global* `setTimeout` binding, and a bare `setTimeout(...)` call resolves that
152
+ // global at call time -- so a consumer whose Node test process has fake timers
153
+ // installed would schedule the deadline on a frozen clock and hang exactly as
154
+ // before, one layer up from the in-page freeze this gate exists to survive.
155
+ //
156
+ // The capture holds for the ordinary case: the SDK imports this module at
157
+ // require time, before a test body or beforeEach reaches `useFakeTimers()`. It
158
+ // is NOT a guarantee. Fake timers installed before this module is first
159
+ // evaluated -- jest's `fakeTimers: { enableGlobally: true }`, a `useFakeTimers()`
160
+ // call in `setupFiles`, or `resetModules()` + a fresh require under an already
161
+ // faked clock -- capture the fake, and the hang returns. Those consumers need
162
+ // `snapshot.readiness.preset: disabled`.
163
+ //
164
+ // Deliberately not `import { setTimeout } from 'node:timers'`, which would be
165
+ // immune to import order too: this file is imported statically by index.js, and
166
+ // index.js is the rollup entry for the browser bundle (see the package's
167
+ // `browser` field). Node-only code in this package is always reached through a
168
+ // lazy `await import(...)` -- `http`/`https` in request.js, `./proxy.js` and its
169
+ // `net`/`tls` imports -- precisely to keep builtins out of that graph. A static
170
+ // builtin import here would not even fail the build, since the rollup config
171
+ // suppresses MISSING_NODE_BUILTINS; it would ship a browser bundle that breaks
172
+ // at runtime, which is worse.
173
+ const nativeSetTimeout = globalThis.setTimeout;
174
+ const nativeClearTimeout = globalThis.clearTimeout;
175
+ const READINESS_DEADLINE_HIT = Symbol('readiness-deadline');
119
176
  async function runReadinessGate(evalScript, snapshotOptions = {}, {
120
177
  callback = false,
121
178
  log
@@ -125,13 +182,31 @@ async function runReadinessGate(evalScript, snapshotOptions = {}, {
125
182
  const script = waitForReadyScript(config, {
126
183
  callback
127
184
  });
185
+ const deadline = readinessDeadlineMs(config);
186
+ let timer;
128
187
  try {
129
- return await evalScript(script);
188
+ const evaluation = Promise.resolve(evalScript(script));
189
+ // Hitting the deadline abandons `evaluation`, which may still reject much
190
+ // later -- when the driver tears the page down at end of test, say. Swallow
191
+ // that here so it never surfaces as an unhandled rejection.
192
+ evaluation.catch(() => {});
193
+ const result = await Promise.race([evaluation, new Promise(resolve => {
194
+ timer = nativeSetTimeout(() => resolve(READINESS_DEADLINE_HIT), deadline);
195
+ })]);
196
+ if (result === READINESS_DEADLINE_HIT) {
197
+ if (log && typeof log.debug === 'function') {
198
+ log.debug(`waitForReady did not settle within ${deadline}ms, proceeding to serialize. ` + 'If this page fakes or pauses timers (e.g. Playwright page.clock), disable ' + 'the readiness gate with snapshot.readiness.preset: disabled.');
199
+ }
200
+ return null;
201
+ }
202
+ return result;
130
203
  } catch (err) {
131
204
  if (log && typeof log.debug === 'function') {
132
205
  log.debug(`waitForReady failed, proceeding to serialize: ${(err === null || err === void 0 ? void 0 : err.message) || err}`);
133
206
  }
134
207
  return null;
208
+ } finally {
209
+ nativeClearTimeout(timer);
135
210
  }
136
211
  }
137
212
  var _default = exports.default = waitForReadyScript;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@percy/sdk-utils",
3
- "version": "1.32.10-beta.1",
3
+ "version": "1.32.11-beta.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -54,5 +54,5 @@
54
54
  "dependencies": {
55
55
  "pac-proxy-agent": "^7.0.2"
56
56
  },
57
- "gitHead": "b90cd9fe29b14fdb7a1e383609654b227adacf17"
57
+ "gitHead": "db20253878f6ee43d2c277698ba021ae41adaa86"
58
58
  }