@reticlehq/browser 3.8.2 → 3.9.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.
@@ -25,6 +25,7 @@ export declare function fireClickSequence(el: ActionTarget, hold?: {
25
25
  prevented: boolean;
26
26
  heldMs: number;
27
27
  }>;
28
+ export declare function scrollIntoViewNow(el: Element, options?: ScrollIntoViewOptions): void;
28
29
  export declare function isOffViewport(el: Element, rect: DOMRect): boolean;
29
30
  export declare function clickGeometry(el: ActionTarget): ClickGeometry;
30
31
  export declare function firePointer(el: Element, type: string, relatedTarget?: EventTarget | null): void;
@@ -88,6 +88,9 @@ export async function fireClickSequence(el, hold, detail, input = PLAIN_CLICK) {
88
88
  function isMeasurable(rect) {
89
89
  return rect.width > 0 || rect.height > 0;
90
90
  }
91
+ export function scrollIntoViewNow(el, options = {}) {
92
+ el.scrollIntoView({ ...options, behavior: 'instant' });
93
+ }
91
94
  export function isOffViewport(el, rect) {
92
95
  const win = el.ownerDocument.defaultView;
93
96
  if (null === win) {
@@ -128,7 +131,7 @@ export function clickGeometry(el) {
128
131
  }
129
132
  let scrolledIntoView = false;
130
133
  if (isOffViewport(el, rect) && 'function' === typeof el.scrollIntoView) {
131
- el.scrollIntoView({ block: 'center', inline: 'center' });
134
+ scrollIntoViewNow(el, { block: 'center', inline: 'center' });
132
135
  scrolledIntoView = true;
133
136
  rect = el.getBoundingClientRect();
134
137
  }
@@ -100,7 +100,7 @@ function optionValueFor(options, named) {
100
100
  const byLabel = options.filter((option) => option.label.trim() === named.trim());
101
101
  return 1 === byLabel.length ? (byLabel[0]?.value ?? named) : named;
102
102
  }
103
- import { NO_GEOMETRY, fireClickSequence, fireTapSequence, clickGeometry, firePointer, firePointerNonBubbling, dragElement, mouseEventFor, assertNotLeaving, clickInputOf, } from './actions-dom.js';
103
+ import { NO_GEOMETRY, fireClickSequence, fireTapSequence, clickGeometry, firePointer, firePointerNonBubbling, dragElement, mouseEventFor, assertNotLeaving, scrollIntoViewNow, clickInputOf, } from './actions-dom.js';
104
104
  import { scrollFrom } from './scroll.js';
105
105
  import { clickHitsNoControl } from './click-target.js';
106
106
  function enabledOf(el) {
@@ -290,7 +290,7 @@ async function dispatchOther(el, action, args) {
290
290
  return !down;
291
291
  }
292
292
  case ActionType.SCROLL_INTO_VIEW:
293
- el.scrollIntoView();
293
+ scrollIntoViewNow(el);
294
294
  return false;
295
295
  case ActionType.ZOOM:
296
296
  throw new Error(ZOOM_NEEDS_REAL_BROWSER_MSG);
@@ -358,9 +358,19 @@ export async function executeAction(ref, action, args = {}) {
358
358
  assertNotLeaving(el);
359
359
  }
360
360
  const anchor = named ? anchorOf(el) : {};
361
+ const prevFocus = activeRef(el);
362
+ if ((ActionType.CLICK === action || ActionType.DBLCLICK === action) && !enabledOf(el)) {
363
+ const active = el.ownerDocument.activeElement;
364
+ if (active !== null &&
365
+ active !== el &&
366
+ active !== el.ownerDocument.body &&
367
+ isActionTarget(active)) {
368
+ active.blur();
369
+ await Promise.resolve();
370
+ }
371
+ }
361
372
  const visible = isVisible(el);
362
373
  const enabled = enabledOf(el);
363
- const prevFocus = activeRef(el);
364
374
  const valueBefore = valueOf(el);
365
375
  const actedField = watchActedField(el);
366
376
  const geometry = CLICK_LIKE.has(action) ? clickGeometry(el) : NO_GEOMETRY;
@@ -1,4 +1,4 @@
1
- import { ComponentStateReason, ElementQuerySchema, INSPECT_IN_VIEWPORT_FIELD, INSPECT_REVEAL_ARG, ReticleCommand, SnapshotMode, TRANSPORT_LIMITS, selectPath, capDepth, } from '@reticlehq/core';
1
+ import { ComponentStateReason, ElementQuerySchema, INSPECT_IN_VIEWPORT_FIELD, INSPECT_REVEAL_ARG, MatchArg, ReticleCommand, SnapshotMode, TRANSPORT_LIMITS, selectPath, capDepth, } from '@reticlehq/core';
2
2
  import { buildSnapshot } from '../dom/snapshot.js';
3
3
  import { paintContextOf } from '../dom/paint-context.js';
4
4
  import { matchQuery, runQuery } from '../dom/query.js';
@@ -19,7 +19,7 @@ import { sanitizeWithReport } from '../security/serialization.js';
19
19
  import { getCapabilities } from '../registry/capabilities.js';
20
20
  import { freezeClock, advanceClock, resetClock, isClockFrozen } from '../timers/clock.js';
21
21
  import { scrollContainer } from '../actions/scroll.js';
22
- import { isOffViewport } from '../actions/actions-dom.js';
22
+ import { isOffViewport, scrollIntoViewNow } from '../actions/actions-dom.js';
23
23
  export const RELOAD_CACHE_BUST_PARAM = '_reticle_reload';
24
24
  function str(value) {
25
25
  return 'string' === typeof value ? value : undefined;
@@ -69,7 +69,7 @@ function inspect(ref, reveal = false) {
69
69
  'function' === typeof el.scrollIntoView &&
70
70
  isOffViewport(el, el.getBoundingClientRect());
71
71
  if (scrolledIntoView) {
72
- el.scrollIntoView({ block: 'center', inline: 'center' });
72
+ scrollIntoViewNow(el, { block: 'center', inline: 'center' });
73
73
  }
74
74
  const rect = el.getBoundingClientRect();
75
75
  const component = identifyComponent(el);
@@ -216,7 +216,7 @@ export function createCommandRegistry() {
216
216
  const limit = args['limit'];
217
217
  return runQuery(queryFromArgs(args), 'number' === typeof limit ? limit : undefined);
218
218
  });
219
- reg.set(ReticleCommand.MATCH, (args) => matchQuery(ElementQuerySchema.parse(record(args['query'])), str(args['state']), undefined, undefined, true === args['diagnose']));
219
+ reg.set(ReticleCommand.MATCH, (args) => matchQuery(ElementQuerySchema.parse(record(args['query'])), str(args['state']), undefined, undefined, true === args['diagnose'], true === args[MatchArg.FULL_TEXT]));
220
220
  reg.set(ReticleCommand.ACT, async (args) => {
221
221
  const action = str(args['action']) ?? '';
222
222
  const result = await executeAction(str(args['ref']) ?? '', action, record(args['args']));
@@ -4,4 +4,4 @@ export declare function getAccessibleName(el: Element): string;
4
4
  export declare function getStates(el: Element, visible?: boolean): ElementState[];
5
5
  export declare function isSensitiveField(el: Element): boolean;
6
6
  export declare function getValue(el: Element): string | undefined;
7
- export declare function describe(el: Element, memo?: Map<Element, boolean>): ElementDescriptor;
7
+ export declare function describe(el: Element, memo?: Map<Element, boolean>, fullText?: boolean): ElementDescriptor;
package/dist/dom/a11y.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { readTestId } from './addressing/testid-attr.js';
2
- import { DEFAULT_TESTID_ATTR, ElementState, REDACTED_VALUE, } from '@reticlehq/core';
2
+ import { DEFAULT_TESTID_ATTR, ElementState, REDACTED_VALUE, TRANSPORT_LIMITS, } from '@reticlehq/core';
3
3
  import { isButton, isImage, isInput, isMeter, isOutput, isProgress, isSelect, isTextArea, } from './realm.js';
4
4
  import { refs } from './addressing/refs.js';
5
5
  import { inspectChart } from './chart.js';
@@ -360,14 +360,14 @@ export function getValue(el) {
360
360
  const valueNow = el.getAttribute('aria-valuenow');
361
361
  return valueNow ?? undefined;
362
362
  }
363
- const MAX_TEXT = 80;
364
- function getVisibleText(el) {
363
+ function getVisibleText(el, fullText) {
365
364
  const text = collapse(el.textContent ?? '');
366
- return text.length > MAX_TEXT ? `${text.slice(0, MAX_TEXT)}…` : text;
365
+ const max = fullText ? TRANSPORT_LIMITS.MAX_FULL_TEXT : TRANSPORT_LIMITS.MAX_DESCRIBED_TEXT;
366
+ return text.length > max ? `${text.slice(0, max)}…` : text;
367
367
  }
368
- export function describe(el, memo) {
368
+ export function describe(el, memo, fullText = false) {
369
369
  const value = getValue(el);
370
- const text = getVisibleText(el);
370
+ const text = getVisibleText(el, fullText);
371
371
  const name = getAccessibleName(el);
372
372
  const visible = isVisible(el, memo);
373
373
  const base = {
@@ -380,7 +380,7 @@ export function describe(el, memo) {
380
380
  if (value !== undefined && value.length > 0) {
381
381
  base.value = value;
382
382
  }
383
- if (text.length > 0 && text !== name) {
383
+ if (text.length > 0 && (fullText || text !== name)) {
384
384
  base.text = text;
385
385
  }
386
386
  const source = formatSource(sourceFromDom(el));
@@ -1,5 +1,8 @@
1
- import { ElementState, type ElementQuery, type MatchResult, type QueryResult } from '@reticlehq/core';
1
+ import { ElementState, type ElementQuery, type MatchResult, type NearestTarget, type QueryResult } from '@reticlehq/core';
2
2
  export declare const ATTR_VALUE_MAX = 512;
3
- export declare function matchQuery(query: ElementQuery, state?: ElementState, limit?: number, wholePhrase?: boolean, diagnose?: boolean): MatchResult;
3
+ export declare function matchQuery(query: ElementQuery, state?: ElementState, limit?: number, wholePhrase?: boolean, diagnose?: boolean, fullText?: boolean): MatchResult;
4
+ export declare const MAX_NEAREST_NAME = 60;
5
+ export declare function nameSimilarity(a: string, b: string): number;
6
+ export declare function nearestTargets(container: HTMLElement, query: ElementQuery): NearestTarget[];
4
7
  export declare function elementsMatching(query: ElementQuery): HTMLElement[];
5
8
  export declare function runQuery(query: ElementQuery, limit?: number): QueryResult;
package/dist/dom/query.js CHANGED
@@ -12,6 +12,7 @@ import { stableSelectorFor } from './stable-selector.js';
12
12
  import { fuzzyVisibleText, normaliseVisibleText, comparableText, foldTypography, textMatches, unreadableMatch, } from './text-match.js';
13
13
  import { pageRoute } from '../security/page-location.js';
14
14
  import { elementsUnder, findIn, isNonRendered, queryByRoleAndName } from './query-finders.js';
15
+ import { INTERACTIVE } from './snapshot.js';
15
16
  const MAX_PRESENT_TESTIDS = 12;
16
17
  function visibleTextOf(el, memo) {
17
18
  if (isNonRendered(el) || isWordsIgnored(el)) {
@@ -149,7 +150,7 @@ function inState(el, state, memo) {
149
150
  return getStates(el, isVisible(el, memo)).includes(state);
150
151
  }
151
152
  const MAX_DESCRIBED = TRANSPORT_LIMITS.MAX_COLLECTION_ITEMS;
152
- export function matchQuery(query, state, limit = MAX_DESCRIBED, wholePhrase = true, diagnose = true) {
153
+ export function matchQuery(query, state, limit = MAX_DESCRIBED, wholePhrase = true, diagnose = true, fullText = false) {
153
154
  const found = findCandidates(query);
154
155
  const scopeMissing = found.scopeMissing;
155
156
  const wanted = wholePhrase && textOnly(query) ? wantedTextOf(query) : undefined;
@@ -159,10 +160,13 @@ export function matchQuery(query, state, limit = MAX_DESCRIBED, wholePhrase = tr
159
160
  const visMemo = new Map();
160
161
  const filtered = state === undefined ? elements : elements.filter((el) => inState(el, state, visMemo));
161
162
  const attrs = query.attrs;
162
- const described = filtered.slice(0, Math.max(0, Math.min(limit, MAX_DESCRIBED)));
163
+ const cap = fullText
164
+ ? Math.min(MAX_DESCRIBED, TRANSPORT_LIMITS.MAX_FULL_TEXT_ELEMENTS)
165
+ : MAX_DESCRIBED;
166
+ const described = filtered.slice(0, Math.max(0, Math.min(limit, cap)));
163
167
  const stampViewport = described.length > 1 || ElementState.IN_VIEWPORT === state;
164
168
  const descriptors = described.map((el) => {
165
- let base = describe(el, visMemo);
169
+ let base = describe(el, visMemo, fullText);
166
170
  if (stampViewport &&
167
171
  isInViewport(el, visMemo) &&
168
172
  !base.states.includes(ElementState.IN_VIEWPORT)) {
@@ -180,6 +184,7 @@ export function matchQuery(query, state, limit = MAX_DESCRIBED, wholePhrase = tr
180
184
  matched: filtered.length > 0,
181
185
  count: filtered.length,
182
186
  elements: descriptors,
187
+ ...(fullText ? { fullText: true } : {}),
183
188
  ...(scopeMissing ? { scopeMissing: true } : {}),
184
189
  ...(covered === undefined ? {} : { coveredBy: covered }),
185
190
  ...(unreadable === undefined ? {} : { unreadable }),
@@ -346,6 +351,81 @@ function nameNearMisses(container, query) {
346
351
  }
347
352
  return out;
348
353
  }
354
+ const MAX_NEAREST_TARGETS = 3;
355
+ export const MAX_NEAREST_NAME = 60;
356
+ function bigrams(value) {
357
+ const padded = ` ${value} `;
358
+ const out = new Set();
359
+ for (let i = 0; i < padded.length - 1; i += 1) {
360
+ out.add(padded.slice(i, i + 2));
361
+ }
362
+ return out;
363
+ }
364
+ export function nameSimilarity(a, b) {
365
+ const left = bigrams(foldTypography(a).toLowerCase());
366
+ const right = bigrams(foldTypography(b).toLowerCase());
367
+ let shared = 0;
368
+ for (const gram of left) {
369
+ if (right.has(gram)) {
370
+ shared += 1;
371
+ }
372
+ }
373
+ return (2 * shared) / (left.size + right.size);
374
+ }
375
+ function askedRole(query) {
376
+ return QueryBy.ROLE === query.by ? query.value : query.role;
377
+ }
378
+ function askedName(query) {
379
+ const value = QueryBy.ROLE === query.by ? undefined : query.value;
380
+ const asked = query.name ??
381
+ value ??
382
+ query.text ??
383
+ query.label ??
384
+ query.placeholder ??
385
+ query.testid ??
386
+ query.alt;
387
+ return asked === undefined ? '' : normaliseVisibleText(asked);
388
+ }
389
+ export function nearestTargets(container, query) {
390
+ const role = askedRole(query);
391
+ const name = askedName(query);
392
+ if (role === undefined && 0 === name.length) {
393
+ return [];
394
+ }
395
+ const memo = new Map();
396
+ const scored = [];
397
+ for (const el of elementsUnder(container)) {
398
+ const elRole = getRole(el);
399
+ const roleMatch = role !== undefined && elRole === role;
400
+ if (!roleMatch && !INTERACTIVE.has(elRole)) {
401
+ continue;
402
+ }
403
+ if (isIgnored(el) || !isVisible(el, memo)) {
404
+ continue;
405
+ }
406
+ const elName = normaliseVisibleText(getAccessibleName(el));
407
+ if (0 === elName.length) {
408
+ continue;
409
+ }
410
+ const score = 0 === name.length ? 0 : nameSimilarity(name, elName);
411
+ if (!roleMatch && 0 === score) {
412
+ continue;
413
+ }
414
+ scored.push({ el, role: elRole, name: elName, roleMatch, score });
415
+ }
416
+ scored.sort((a, b) => Number(b.roleMatch) - Number(a.roleMatch) || b.score - a.score);
417
+ return scored.slice(0, MAX_NEAREST_TARGETS).map((c) => {
418
+ const target = {
419
+ ref: refs.refFor(c.el),
420
+ role: c.role,
421
+ name: c.name.slice(0, MAX_NEAREST_NAME),
422
+ };
423
+ if (c.name.length > MAX_NEAREST_NAME) {
424
+ target.nameTruncated = true;
425
+ }
426
+ return target;
427
+ });
428
+ }
349
429
  function testidFoundUnder(container, query) {
350
430
  if (QueryBy.TESTID !== query.by || undefined === query.value) {
351
431
  return undefined;
@@ -452,6 +532,10 @@ function buildEmptyHint(query, state, diagnose = true) {
452
532
  if (elsewhere !== undefined) {
453
533
  hint.testidFoundUnder = elsewhere;
454
534
  }
535
+ const nearest = diagnose ? nearestTargets(container, query) : [];
536
+ if (nearest.length > 0) {
537
+ hint.nearest = nearest;
538
+ }
455
539
  return hint;
456
540
  }
457
541
  export function elementsMatching(query) {
@@ -464,7 +548,7 @@ export function runQuery(query, limit) {
464
548
  return {
465
549
  elements: result.elements,
466
550
  count: result.count,
467
- hint: buildEmptyHint(query),
551
+ hint: result.hint ?? buildEmptyHint(query),
468
552
  ...scopeFields,
469
553
  };
470
554
  }
@@ -1,4 +1,5 @@
1
1
  import { parentAcrossShadowBoundary } from './shadow-registry.js';
2
+ import { stalledFade } from './visibility.js';
2
3
  export function normaliseVisibleText(value) {
3
4
  return value.replace(/\s+/g, ' ').trim().normalize('NFC');
4
5
  }
@@ -81,7 +82,7 @@ export function unreadableText(el) {
81
82
  let opaque = false;
82
83
  for (let node = el; null !== node; node = parentAcrossShadowBoundary(node)) {
83
84
  const style = view.getComputedStyle(node);
84
- const alpha = Number.parseFloat(style.opacity);
85
+ const alpha = stalledFade(node) ? 1 : Number.parseFloat(style.opacity);
85
86
  opacity *= Number.isNaN(alpha) ? 1 : alpha;
86
87
  if (opaque) {
87
88
  continue;
@@ -1,6 +1,7 @@
1
1
  export declare const HTML_DETAILS_TAG = "details";
2
2
  export declare const HTML_DETAILS_OPEN_ATTRIBUTE = "open";
3
3
  export declare const HTML_SUMMARY_TAG = "summary";
4
+ export declare function stalledFade(el: Element): boolean;
4
5
  export declare function isHiddenByAriaOnly(el: Element): boolean;
5
6
  export declare function isRenderedOutsideAriaHidden(el: Element): boolean;
6
7
  export declare function isInViewport(el: Element, memo?: Map<Element, boolean>): boolean;
@@ -1,3 +1,4 @@
1
+ import { RETICLE_OVERLAY_SELECTOR } from '@reticlehq/core';
1
2
  import { isHtmlElement } from './realm.js';
2
3
  import { parentAcrossShadowBoundary } from './shadow-registry.js';
3
4
  import { clippingBox, overflowAxes, scrolledInto, } from './paint-context.js';
@@ -16,15 +17,45 @@ function hiddenInsideClosedDetails(el) {
16
17
  const summary = Array.from(details.children).find((child) => HTML_SUMMARY_TAG === child.localName);
17
18
  return summary === undefined || !summary.contains(el);
18
19
  }
19
- function stalledFade(el) {
20
+ export function stalledFade(el) {
20
21
  if (!el.ownerDocument.hidden || !('getAnimations' in el)) {
21
22
  return false;
22
23
  }
23
24
  return el.getAnimations().some((a) => 'running' === a.playState || a.pending);
24
25
  }
25
26
  const OVERLAY_ARIA_HIDDEN = 'data-aria-hidden';
27
+ const OPEN_LAYER = '[role="dialog"], [role="alertdialog"], [role="menu"], [role="listbox"], dialog[open], [aria-modal="true"]';
28
+ const UNPAINTED_TAGS = new Set(['script', 'style', 'template', 'noscript', 'link', 'meta']);
26
29
  function hiddenByAnOverlay(el) {
27
- return el.hasAttribute(OVERLAY_ARIA_HIDDEN);
30
+ if (el.hasAttribute(OVERLAY_ARIA_HIDDEN)) {
31
+ return true;
32
+ }
33
+ return hiddenByAModalManager(el);
34
+ }
35
+ function hiddenByAModalManager(el) {
36
+ const body = el.ownerDocument.body;
37
+ if (el.parentElement !== body) {
38
+ return false;
39
+ }
40
+ const others = Array.from(body.children).filter((child) => child !== el &&
41
+ !UNPAINTED_TAGS.has(child.localName) &&
42
+ !child.matches(RETICLE_OVERLAY_SELECTOR));
43
+ const open = others.filter((child) => 'true' !== child.getAttribute('aria-hidden'));
44
+ const layer = open[0];
45
+ return 1 === open.length && layer !== undefined && holdsOpenLayer(layer);
46
+ }
47
+ function holdsOpenLayer(portal) {
48
+ const layer = portal.matches(OPEN_LAYER) ? portal : portal.querySelector(OPEN_LAYER);
49
+ for (let node = layer; null !== node; node = node.parentElement) {
50
+ const style = node.ownerDocument.defaultView?.getComputedStyle(node) ?? null;
51
+ if (selfCssHidden(node, style)) {
52
+ return false;
53
+ }
54
+ if (node === portal) {
55
+ return true;
56
+ }
57
+ }
58
+ return false;
28
59
  }
29
60
  function selfCssHidden(el, style) {
30
61
  if (isHtmlElement(el) && el.hidden) {
@@ -106,6 +137,56 @@ export function isRenderedOutsideAriaHidden(el) {
106
137
  }
107
138
  return true;
108
139
  }
140
+ const BACKFACE_VISIBILITY_HIDDEN = 'hidden';
141
+ function localTransform(style) {
142
+ if ('undefined' === typeof DOMMatrix) {
143
+ return null;
144
+ }
145
+ try {
146
+ const transform = style?.transform;
147
+ return undefined === transform || '' === transform || 'none' === transform
148
+ ? new DOMMatrix()
149
+ : new DOMMatrix(transform);
150
+ }
151
+ catch {
152
+ return null;
153
+ }
154
+ }
155
+ function backFaceTowardViewer(matrix) {
156
+ return Number.isFinite(matrix.m33) && 0 > matrix.m33;
157
+ }
158
+ function backfaceHidden(el, memo) {
159
+ const chain = [];
160
+ let declares = false;
161
+ for (let current = el; null !== current; current = parentAcrossShadowBoundary(current)) {
162
+ const style = ownVisibilityInfo(current, memo).style;
163
+ if (BACKFACE_VISIBILITY_HIDDEN === style?.backfaceVisibility) {
164
+ declares = true;
165
+ }
166
+ chain.push(current);
167
+ }
168
+ if (!declares) {
169
+ return false;
170
+ }
171
+ let composed = null;
172
+ for (let index = chain.length - 1; index >= 0; index -= 1) {
173
+ const current = chain[index];
174
+ if (undefined === current) {
175
+ return false;
176
+ }
177
+ const style = ownVisibilityInfo(current, memo).style;
178
+ const local = localTransform(style);
179
+ if (null === local) {
180
+ return false;
181
+ }
182
+ composed = null === composed ? local : composed.multiply(local);
183
+ if (BACKFACE_VISIBILITY_HIDDEN === style?.backfaceVisibility &&
184
+ backFaceTowardViewer(composed)) {
185
+ return true;
186
+ }
187
+ }
188
+ return false;
189
+ }
109
190
  const CLIPPING_OVERFLOW = new Set(['hidden', 'clip']);
110
191
  const SCROLLING_OVERFLOW = new Set(['auto', 'scroll']);
111
192
  const NON_CLIPPING_DISPLAY = new Set([
@@ -314,7 +395,9 @@ export function isVisible(el, memo) {
314
395
  return cached;
315
396
  }
316
397
  const info = visibilityInfoMemo(memo);
317
- const result = visibleBounds(el, info, undefined, true) !== null && !collapsed(el, info);
398
+ const result = visibleBounds(el, info, undefined, true) !== null &&
399
+ !collapsed(el, info) &&
400
+ !backfaceHidden(el, info);
318
401
  if (memo !== undefined) {
319
402
  memo.set(el, result);
320
403
  }
@@ -228,6 +228,7 @@ export function installNetwork(emit, opts = {}) {
228
228
  }
229
229
  const owner = {};
230
230
  pageSlots()[PAGE_CAPTURE] = owner;
231
+ const installedAt = performance.now();
231
232
  const captureBodies = true === opts.captureBodies;
232
233
  const bodyCapture = new AbortController();
233
234
  const captureErrorBodies = false !== opts.captureErrorBodies;
@@ -550,7 +551,8 @@ export function installNetwork(emit, opts = {}) {
550
551
  return;
551
552
  }
552
553
  const type = entry.initiatorType || 'other';
553
- if ('fetch' === type || 'xmlhttprequest' === type) {
554
+ const patched = 'fetch' === type || 'xmlhttprequest' === type;
555
+ if (patched && entry.startTime >= installedAt) {
554
556
  continue;
555
557
  }
556
558
  const rawUrl = entry.name;