@specific.dev/spectest 0.73.0 → 0.73.1

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/browser.js CHANGED
@@ -236,11 +236,17 @@ async function applySafeAreaInsets(cdp, d) {
236
236
  // ────────────────────────────────────────────────────────────────────────
237
237
  // rrweb bootstrap
238
238
  // ────────────────────────────────────────────────────────────────────────
239
- // Vendored `@rrweb/record` UMD bundle (rrweb 2.x). Its global `rrwebRecord`
240
- // is a module object — the record function is `rrwebRecord.record` (the
241
- // bootstrap resolves both this and the legacy function-shaped global). Read
242
- // once at module init — the SDK ships this file in the base snapshot so
243
- // no network fetch happens inside the VM.
239
+ // Vendored `@rrweb/record` UMD bundle (rrweb 2.x), BUILT FROM OUR FORK by
240
+ // `scripts/rrweb-record/build.sh` (specific-dev/rrweb, branch `spectest` =
241
+ // the `rrweb@2.1.0` tag + upstream PR 1802). Never copy the npm dist over
242
+ // it: upstream's `@rrweb/utils` lists `querySelector` as an accessor, so its
243
+ // pristine-prototype test always fails and `record()` appends an <iframe>
244
+ // to every document — measured at ~1 s per navigation (see the top-frame
245
+ // gate on PAGE_INIT_SCRIPT below). Its global `rrwebRecord` is a module
246
+ // object — the record function is `rrwebRecord.record` (the bootstrap
247
+ // resolves both this and the legacy function-shaped global). Read once at
248
+ // module init — the SDK ships this file in the base snapshot so no network
249
+ // fetch happens inside the VM.
244
250
  const RRWEB_BUNDLE = (() => {
245
251
  try {
246
252
  const here = path.dirname(fileURLToPath(import.meta.url));
@@ -711,8 +717,22 @@ const RRWEB_BOOTSTRAP = `
711
717
  // call expression — the TypeError kills everything after the bundle, so
712
718
  // the globals define but the bootstrap never runs (zero rrweb events,
713
719
  // empty replays).
720
+ //
721
+ // The whole script runs in the TOP frame only. `Page.addScriptToEvaluateOnNewDocument`
722
+ // fires for every frame, and until 2026-09-06 that was a storm: rrweb's
723
+ // `getUntaintedPrototype` appends a hidden <iframe> to read pristine DOM
724
+ // prototypes, this script then ran inside it, the child's `record()` (which
725
+ // treats every frame as an emitting frame unless `recordCrossOriginIframes`
726
+ // is set) appended an iframe of its own, and so on to Chromium's frame-depth
727
+ // cap — 44 nested frames and 45 executions of ~180 KB of JS per navigation,
728
+ // ~0.8–1.3 s inside the page's `load` handler even for a one-line document
729
+ // (`goto` p50 was ~1.5 s in every project). The vendored bundle's fork patch
730
+ // removes the iframe on an unpatched page; this gate bounds it on a page
731
+ // whose prototypes really are patched (zone.js). Nothing is lost: rrweb
732
+ // records same-origin iframes from the parent, and a cross-origin child's
733
+ // events were never drained (they land in the child's own buffer).
714
734
  const PAGE_INIT_SCRIPT = RRWEB_BUNDLE
715
- ? `${RRWEB_BUNDLE}\n;\n${RRWEB_CONSOLE_PLUGIN_BUNDLE}\n;\n${RRWEB_BOOTSTRAP}`
735
+ ? `if (window === window.top) {\n${RRWEB_BUNDLE}\n;\n${RRWEB_CONSOLE_PLUGIN_BUNDLE}\n;\n${RRWEB_BOOTSTRAP}\n}`
716
736
  : "";
717
737
  // Page-side expression that atomically swaps in a fresh buffer and
718
738
  // returns the old one. Wrapped as a single expression so view.evaluate's
@@ -131,13 +131,13 @@ const testableAccessors = {
131
131
  "ownerDocument"
132
132
  ],
133
133
  ShadowRoot: ["host", "styleSheets"],
134
- Element: ["shadowRoot", "querySelector", "querySelectorAll"],
134
+ Element: ["shadowRoot"],
135
135
  MutationObserver: []
136
136
  };
137
137
  const testableMethods = {
138
138
  Node: ["contains", "getRootNode"],
139
139
  ShadowRoot: ["getSelection"],
140
- Element: [],
140
+ Element: ["querySelector", "querySelectorAll"],
141
141
  MutationObserver: ["constructor"]
142
142
  };
143
143
  const untaintedBasePrototype = {};
@@ -263,10 +263,10 @@ function shadowRoot(n) {
263
263
  return getUntaintedAccessor("Element", n, "shadowRoot");
264
264
  }
265
265
  function querySelector(n, selectors) {
266
- return getUntaintedAccessor("Element", n, "querySelector")(selectors);
266
+ return getUntaintedMethod("Element", n, "querySelector")(selectors);
267
267
  }
268
268
  function querySelectorAll(n, selectors) {
269
- return getUntaintedAccessor("Element", n, "querySelectorAll")(selectors);
269
+ return getUntaintedMethod("Element", n, "querySelectorAll")(selectors);
270
270
  }
271
271
  function mutationObserverCtor() {
272
272
  var _a2;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@specific.dev/spectest",
3
- "version": "0.73.0",
3
+ "version": "0.73.1",
4
4
  "description": "Spectest SDK for defining test environments in TypeScript.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/browser.ts CHANGED
@@ -615,11 +615,17 @@ async function applySafeAreaInsets(
615
615
  // rrweb bootstrap
616
616
  // ────────────────────────────────────────────────────────────────────────
617
617
 
618
- // Vendored `@rrweb/record` UMD bundle (rrweb 2.x). Its global `rrwebRecord`
619
- // is a module object — the record function is `rrwebRecord.record` (the
620
- // bootstrap resolves both this and the legacy function-shaped global). Read
621
- // once at module init — the SDK ships this file in the base snapshot so
622
- // no network fetch happens inside the VM.
618
+ // Vendored `@rrweb/record` UMD bundle (rrweb 2.x), BUILT FROM OUR FORK by
619
+ // `scripts/rrweb-record/build.sh` (specific-dev/rrweb, branch `spectest` =
620
+ // the `rrweb@2.1.0` tag + upstream PR 1802). Never copy the npm dist over
621
+ // it: upstream's `@rrweb/utils` lists `querySelector` as an accessor, so its
622
+ // pristine-prototype test always fails and `record()` appends an <iframe>
623
+ // to every document — measured at ~1 s per navigation (see the top-frame
624
+ // gate on PAGE_INIT_SCRIPT below). Its global `rrwebRecord` is a module
625
+ // object — the record function is `rrwebRecord.record` (the bootstrap
626
+ // resolves both this and the legacy function-shaped global). Read once at
627
+ // module init — the SDK ships this file in the base snapshot so no network
628
+ // fetch happens inside the VM.
623
629
  const RRWEB_BUNDLE: string = (() => {
624
630
  try {
625
631
  const here = path.dirname(fileURLToPath(import.meta.url));
@@ -1094,8 +1100,22 @@ const RRWEB_BOOTSTRAP = `
1094
1100
  // call expression — the TypeError kills everything after the bundle, so
1095
1101
  // the globals define but the bootstrap never runs (zero rrweb events,
1096
1102
  // empty replays).
1103
+ //
1104
+ // The whole script runs in the TOP frame only. `Page.addScriptToEvaluateOnNewDocument`
1105
+ // fires for every frame, and until 2026-09-06 that was a storm: rrweb's
1106
+ // `getUntaintedPrototype` appends a hidden <iframe> to read pristine DOM
1107
+ // prototypes, this script then ran inside it, the child's `record()` (which
1108
+ // treats every frame as an emitting frame unless `recordCrossOriginIframes`
1109
+ // is set) appended an iframe of its own, and so on to Chromium's frame-depth
1110
+ // cap — 44 nested frames and 45 executions of ~180 KB of JS per navigation,
1111
+ // ~0.8–1.3 s inside the page's `load` handler even for a one-line document
1112
+ // (`goto` p50 was ~1.5 s in every project). The vendored bundle's fork patch
1113
+ // removes the iframe on an unpatched page; this gate bounds it on a page
1114
+ // whose prototypes really are patched (zone.js). Nothing is lost: rrweb
1115
+ // records same-origin iframes from the parent, and a cross-origin child's
1116
+ // events were never drained (they land in the child's own buffer).
1097
1117
  const PAGE_INIT_SCRIPT = RRWEB_BUNDLE
1098
- ? `${RRWEB_BUNDLE}\n;\n${RRWEB_CONSOLE_PLUGIN_BUNDLE}\n;\n${RRWEB_BOOTSTRAP}`
1118
+ ? `if (window === window.top) {\n${RRWEB_BUNDLE}\n;\n${RRWEB_CONSOLE_PLUGIN_BUNDLE}\n;\n${RRWEB_BOOTSTRAP}\n}`
1099
1119
  : "";
1100
1120
 
1101
1121
  // Page-side expression that atomically swaps in a fresh buffer and
@@ -131,13 +131,13 @@ const testableAccessors = {
131
131
  "ownerDocument"
132
132
  ],
133
133
  ShadowRoot: ["host", "styleSheets"],
134
- Element: ["shadowRoot", "querySelector", "querySelectorAll"],
134
+ Element: ["shadowRoot"],
135
135
  MutationObserver: []
136
136
  };
137
137
  const testableMethods = {
138
138
  Node: ["contains", "getRootNode"],
139
139
  ShadowRoot: ["getSelection"],
140
- Element: [],
140
+ Element: ["querySelector", "querySelectorAll"],
141
141
  MutationObserver: ["constructor"]
142
142
  };
143
143
  const untaintedBasePrototype = {};
@@ -263,10 +263,10 @@ function shadowRoot(n) {
263
263
  return getUntaintedAccessor("Element", n, "shadowRoot");
264
264
  }
265
265
  function querySelector(n, selectors) {
266
- return getUntaintedAccessor("Element", n, "querySelector")(selectors);
266
+ return getUntaintedMethod("Element", n, "querySelector")(selectors);
267
267
  }
268
268
  function querySelectorAll(n, selectors) {
269
- return getUntaintedAccessor("Element", n, "querySelectorAll")(selectors);
269
+ return getUntaintedMethod("Element", n, "querySelectorAll")(selectors);
270
270
  }
271
271
  function mutationObserverCtor() {
272
272
  var _a2;