buninu-browser 0.0.2 → 0.2.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.
Files changed (61) hide show
  1. package/AGENTS.md +17 -0
  2. package/LICENSES/brotli.txt +19 -0
  3. package/README.md +41 -7
  4. package/THIRD_PARTY_NOTICES.md +30 -6
  5. package/buninu-browser.js +10 -2
  6. package/lib/audio/pcm.js +16 -0
  7. package/lib/audio/piano.js +109 -0
  8. package/lib/audio/pulse-client.js +413 -0
  9. package/lib/cdp-server.js +25 -6
  10. package/lib/document-types.js +308 -0
  11. package/lib/download-directory.js +28 -0
  12. package/lib/download.js +63 -0
  13. package/lib/headless-shell.js +108 -14
  14. package/lib/input/hit-test.js +57 -8
  15. package/lib/layout/text-layout.js +112 -27
  16. package/lib/network/network-service.js +7 -3
  17. package/lib/network/resolver.js +16 -0
  18. package/lib/paint/display-list.js +2 -2
  19. package/lib/render-tree/index.js +13 -0
  20. package/lib/renderer/bindings.js +541 -53
  21. package/lib/renderer/flags.js +8 -5
  22. package/lib/renderer/host.js +110 -9
  23. package/lib/renderer/main.js +35 -3
  24. package/lib/renderer/module-transform.js +30 -23
  25. package/lib/renderer/page-realm.js +226 -22
  26. package/lib/renderer/page-renderer.js +1293 -212
  27. package/lib/screenshot-cli.js +61 -0
  28. package/lib/style/computed-style.js +268 -20
  29. package/lib/user-agent.js +12 -0
  30. package/package.json +1 -1
  31. package/patches/happy-dom/0003-defer-script-events-to-renderer.patch +40 -0
  32. package/patches/happy-dom/0004-insert-adjacent-html-in-order.patch +19 -0
  33. package/patches/happy-dom/0005-invalidate-matches-on-structure-changes.patch +43 -0
  34. package/patches/happy-dom/README.md +12 -0
  35. package/patches/ses/0001-allow-regexp-own-tostring.patch +13 -0
  36. package/patches/ses/README.md +16 -0
  37. package/patches/wasmpeg/0001-windows-wasm-path.patch +14 -0
  38. package/patches/wasmpeg/README.md +28 -0
  39. package/test/computed-style.test.js +52 -0
  40. package/test/document-types.test.js +111 -0
  41. package/test/download.test.js +192 -0
  42. package/test/fixtures/tone.mp3 +0 -0
  43. package/test/flex.test.js +11 -0
  44. package/test/happy-dom-matches.test.js +25 -0
  45. package/test/headless-shell.test.js +27 -0
  46. package/test/hit-test.test.js +37 -0
  47. package/test/page-scripts.test.js +388 -29
  48. package/test/piano.test.js +65 -0
  49. package/test/positioning.test.js +11 -0
  50. package/test/pulse-client.test.js +130 -0
  51. package/test/render-tree.test.js +14 -0
  52. package/test/resolver.test.js +12 -1
  53. package/test/screenshot-cli.test.js +19 -0
  54. package/test/text-layout.test.js +22 -0
  55. package/test/user-agent.test.js +42 -0
  56. package/vendor/happy-dom/packages/happy-dom/src/nodes/element/Element.ts +3 -6
  57. package/vendor/happy-dom/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts +2 -20
  58. package/vendor/happy-dom/packages/happy-dom/src/query-selector/QuerySelector.ts +15 -3
  59. package/vendor/ses/dist/ses.mjs +2 -0
  60. package/vendor/wasmpeg/VENDORING.md +7 -3
  61. package/vendor/wasmpeg/src/js/gpu.js +2 -1
@@ -12,7 +12,7 @@
12
12
  * the renderer schedule style, layout and paint.
13
13
  */
14
14
  import {
15
- BindingRealm, defineEventHandlerProperty, eventConstructors, hardenBindings, INTERFACES, nodeOf, windowEventMethods,
15
+ BindingRealm, defineEventHandlerProperty, eventConstructors, eventTargetConstructor, hardenBindings, INTERFACES, mutableBindingPrototype, NodeFilter, nodeOf, windowEventMethods,
16
16
  } from "./bindings.js";
17
17
  import { resolveModuleSpecifier, transformModule } from "./module-transform.js";
18
18
  import { CONSTRUCTOR_HELPER, rewriteClassicScript, rewriteConstructorReads } from "./script-rewrite.js";
@@ -65,6 +65,30 @@ const WINDOW_HANDLER_EVENTS = [
65
65
  ];
66
66
  const hostStorage = new Map();
67
67
 
68
+ function cssEscape(value) {
69
+ const input = String(value);
70
+ let output = "";
71
+ for (let index = 0; index < input.length; index++) {
72
+ const code = input.codePointAt(index);
73
+ const character = String.fromCodePoint(code);
74
+ if (code > 0xffff) index++;
75
+ if (code === 0) { output += "\uFFFD"; continue; }
76
+ if ((code >= 1 && code <= 31) || code === 127
77
+ || (index === 0 && code >= 48 && code <= 57)
78
+ || (index === 1 && input[0] === "-" && code >= 48 && code <= 57)) {
79
+ output += `\\${code.toString(16)} `;
80
+ } else if (index === 0 && character === "-" && input.length === 1) {
81
+ output += "\\-";
82
+ } else if (code >= 128 || character === "-" || character === "_"
83
+ || (code >= 48 && code <= 57) || (code >= 65 && code <= 90) || (code >= 97 && code <= 122)) {
84
+ output += character;
85
+ } else {
86
+ output += `\\${character}`;
87
+ }
88
+ }
89
+ return output;
90
+ }
91
+
68
92
  /**
69
93
  * @param {{
70
94
  * window: object, document: object, mode: "ses" | "host",
@@ -75,6 +99,7 @@ const hostStorage = new Map();
75
99
  * scrollTo(x: number, y: number): void,
76
100
  * computedStyle(element: object): object | null,
77
101
  * navigate(url: string): void,
102
+ * activateLink?(anchor: object): void,
78
103
  * fetchScript(url: string): Promise<string>,
79
104
  * reportError(message: string): void,
80
105
  * consoleMessage?: ((level: string, text: string) => void) | null,
@@ -101,12 +126,24 @@ export function createPageRealm({ window, document, mode, hooks }) {
101
126
  let nextTimerId = 1;
102
127
  let disposed = false;
103
128
  let readyState = "loading";
129
+ // Navigation Timing 1 (performance.timing): epoch milliseconds on the same
130
+ // timeline as performance.now(). Phases this document has not reached yet
131
+ // read 0, as in browsers; the fetch phases all collapse onto the start.
132
+ const timingStart = Math.round(hostPerformance.timeOrigin);
133
+ const timingMarks = { domLoading: Math.round(hostPerformance.timeOrigin + hostPerformance.now()) };
134
+ const markTiming = (...names) => {
135
+ const now = Math.round(hostPerformance.timeOrigin + hostPerformance.now());
136
+ for (const name of names) timingMarks[name] = now;
137
+ };
138
+ let parsingHead = false;
104
139
  let currentScript = null;
105
140
  let pageGlobal = null;
106
141
  let evaluate;
107
142
  let evaluateModuleCode;
108
143
  let importMap = null;
109
144
  const modules = new Map();
145
+ const customElementConstructors = new Map();
146
+ let constructingElement = null;
110
147
  let inlineModuleCount = 0;
111
148
 
112
149
  const reportError = (error, source = currentScript?.src) => {
@@ -146,25 +183,63 @@ export function createPageRealm({ window, document, mode, hooks }) {
146
183
  boundsOf: hooks.boundsOf,
147
184
  viewport: hooks.viewport,
148
185
  scrollTo: hooks.scrollTo,
149
- playVideo: hooks.playVideo,
150
- pauseVideo: hooks.pauseVideo,
151
- videoState: hooks.videoState,
186
+ mediaState: hooks.mediaState,
187
+ playMedia: hooks.playMedia,
188
+ pauseMedia: hooks.pauseMedia,
189
+ seekMedia: hooks.seekMedia,
152
190
  call: callPage,
153
191
  readyState: () => readyState,
192
+ body: () => parsingHead ? null : document.body,
154
193
  documentCookie: () => hooks.documentCookie?.() ?? "",
155
194
  setDocumentCookie: (value) => hooks.setDocumentCookie?.(value),
156
195
  currentScript: () => currentScript,
157
196
  pageGlobal: () => pageGlobal,
158
- targetOf: (real) => real === window ? pageGlobal : null,
197
+ targetOf: (real) => real === window ? pageGlobal : eventTargets.targetOf(real),
159
198
  created: (element) => {
160
199
  if (element.tagName === "SCRIPT") createdScripts.add(element);
161
200
  },
162
201
  inserted: (nodes) => startInsertedScripts(nodes),
202
+ activateLink: (anchor) => hooks.activateLink?.(anchor),
203
+ submitForm: (form, submitter) => hooks.submitForm?.(form, submitter),
163
204
  acceptEvent: (event) => !LIFECYCLE_EVENT_TYPES.has(event.type)
164
205
  || (event.target !== window && event.target !== document)
165
206
  || lifecycleEvents.has(event),
166
207
  });
208
+ const eventTargets = eventTargetConstructor(realm,
209
+ (value) => value === pageGlobal || Boolean(nodeOf(value)));
167
210
  const documentBinding = realm.wrap(document);
211
+ const NodeInterface = function Node() { throw new TypeError("Illegal constructor"); };
212
+ NodeInterface.prototype = mutableBindingPrototype(INTERFACES.Node.prototype);
213
+ Object.defineProperty(NodeInterface.prototype, "constructor", { value: NodeInterface, writable: true, configurable: true });
214
+ realm.nodePrototype = NodeInterface.prototype;
215
+ const parentPrototype = mutableBindingPrototype(Object.getPrototypeOf(INTERFACES.Element.prototype), NodeInterface.prototype);
216
+ const ElementInterface = function Element() { throw new TypeError("Illegal constructor"); };
217
+ ElementInterface.prototype = mutableBindingPrototype(INTERFACES.Element.prototype, parentPrototype);
218
+ Object.defineProperty(ElementInterface.prototype, "constructor", { value: ElementInterface, writable: true, configurable: true });
219
+ const HTMLElementInterface = function HTMLElement() {
220
+ if (!new.target || !constructingElement) throw new TypeError("Illegal constructor");
221
+ return realm.wrap(constructingElement);
222
+ };
223
+ HTMLElementInterface.prototype = mutableBindingPrototype(ElementInterface.prototype);
224
+ Object.defineProperty(HTMLElementInterface.prototype, "constructor", { value: HTMLElementInterface, writable: true, configurable: true });
225
+ realm.elementPrototype = HTMLElementInterface.prototype;
226
+ const TextInterface = function Text() { throw new TypeError("Illegal constructor"); };
227
+ const CharacterDataInterface = function CharacterData() { throw new TypeError("Illegal constructor"); };
228
+ CharacterDataInterface.prototype = mutableBindingPrototype(INTERFACES.CharacterData.prototype, NodeInterface.prototype);
229
+ Object.defineProperty(CharacterDataInterface.prototype, "constructor", { value: CharacterDataInterface, writable: true, configurable: true });
230
+ realm.characterDataPrototype = CharacterDataInterface.prototype;
231
+ TextInterface.prototype = mutableBindingPrototype(INTERFACES.Text.prototype, CharacterDataInterface.prototype);
232
+ Object.defineProperty(TextInterface.prototype, "constructor", { value: TextInterface, writable: true, configurable: true });
233
+ realm.textPrototype = TextInterface.prototype;
234
+ const DocumentFragmentInterface = function DocumentFragment() { throw new TypeError("Illegal constructor"); };
235
+ DocumentFragmentInterface.prototype = mutableBindingPrototype(INTERFACES.DocumentFragment.prototype, parentPrototype);
236
+ Object.defineProperty(DocumentFragmentInterface.prototype, "constructor", { value: DocumentFragmentInterface, writable: true, configurable: true });
237
+ realm.fragmentPrototype = DocumentFragmentInterface.prototype;
238
+ realm.shadowRootPrototype = mutableBindingPrototype(INTERFACES.ShadowRoot.prototype, DocumentFragmentInterface.prototype);
239
+ const DocumentInterface = function Document() { throw new TypeError("Illegal constructor"); };
240
+ DocumentInterface.prototype = mutableBindingPrototype(INTERFACES.Document.prototype, parentPrototype);
241
+ Object.defineProperty(DocumentInterface.prototype, "constructor", { value: DocumentInterface, writable: true, configurable: true });
242
+ Object.setPrototypeOf(documentBinding, DocumentInterface.prototype);
168
243
 
169
244
  const fireLifecycle = (target, event) => {
170
245
  lifecycleEvents.add(event);
@@ -672,10 +747,10 @@ export function createPageRealm({ window, document, mode, hooks }) {
672
747
  };
673
748
  const interfaces = {
674
749
  Window: make("Window", () => false),
675
- EventTarget: make("EventTarget", () => true),
750
+ EventTarget: eventTargets.EventTarget,
676
751
  Comment: make("Comment", (node) => node.nodeType === 8),
677
752
  DocumentType: make("DocumentType", (node) => node.nodeType === 10),
678
- ShadowRoot: make("ShadowRoot", () => false),
753
+ ShadowRoot: make("ShadowRoot", (node) => node instanceof window.ShadowRoot),
679
754
  SVGElement: make("SVGElement", (node) => node.namespaceURI === SVG_NAMESPACE),
680
755
  SVGSVGElement: make("SVGSVGElement", (node) => node.namespaceURI === SVG_NAMESPACE && node.localName === "svg"),
681
756
  HTMLUnknownElement: make("HTMLUnknownElement", () => false),
@@ -691,7 +766,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
691
766
 
692
767
  const viewportValue = (key) => () => hooks.viewport()[key];
693
768
  const { location, setHref } = locationBinding(document.URL, hooks.navigate);
694
- const { History, history } = historyBinding(location, setHref);
769
+ const { History, history, pushEntry } = historyBinding(location, setHref);
695
770
  const { Event, CustomEvent, ...uiEvents } = eventConstructors(realm);
696
771
  const observers = observerConstructors();
697
772
  const network = networkApi();
@@ -771,9 +846,15 @@ export function createPageRealm({ window, document, mode, hooks }) {
771
846
  TextDecoder,
772
847
  URL,
773
848
  URLSearchParams,
849
+ // SES rewrites dynamic import() in evaluated classic scripts to this
850
+ // hook. Resolve it through the same module loader and network boundary as
851
+ // parser-inserted modules.
852
+ __import__: (specifier) => importModule(String(specifier), currentScript?.src || document.URL),
774
853
  performance: {
775
854
  now: () => hostPerformance.now(),
776
855
  timeOrigin: hostPerformance.timeOrigin,
856
+ timing: navigationTiming(timingStart, timingMarks),
857
+ navigation: Object.freeze({ type: 0, redirectCount: 0, TYPE_NAVIGATE: 0, TYPE_RELOAD: 1, TYPE_BACK_FORWARD: 2, TYPE_RESERVED: 255 }),
777
858
  mark: () => {},
778
859
  measure: () => {},
779
860
  getEntriesByType: () => [],
@@ -788,6 +869,24 @@ export function createPageRealm({ window, document, mode, hooks }) {
788
869
  ...uiEvents,
789
870
  ...observers,
790
871
  ...network,
872
+ CSS: {
873
+ escape: cssEscape,
874
+ supports(property, value) {
875
+ // The one-argument condition grammar is not modelled yet. Returning
876
+ // false is the safe feature-detection result for unsupported syntax.
877
+ if (value === undefined) return false;
878
+ const name = String(property);
879
+ const camel = name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
880
+ const style = documentBinding.body?.style ?? documentBinding.documentElement?.style;
881
+ return Boolean(style && (name in style || camel in style));
882
+ },
883
+ },
884
+ NodeFilter,
885
+ // Collections are represented by snapshot arrays internally. Expose the
886
+ // standard collection prototypes used by compatibility shims; their
887
+ // Array methods are generic and work with those snapshots.
888
+ NodeList: class NodeList extends Array {},
889
+ HTMLCollection: class HTMLCollection extends Array {},
791
890
  Image: function Image(width, height) {
792
891
  if (!new.target) throw new TypeError("Constructor Image requires 'new'");
793
892
  const image = document.createElement("img");
@@ -796,17 +895,67 @@ export function createPageRealm({ window, document, mode, hooks }) {
796
895
  return realm.wrap(image);
797
896
  },
798
897
  customElements: {
799
- define: () => {},
800
- get: () => undefined,
801
- whenDefined: () => Promise.resolve(),
802
- upgrade: () => {},
898
+ define(name, constructor) {
899
+ const tag = String(name).toLowerCase();
900
+ if (typeof constructor !== "function") throw new TypeError("Custom element constructor must be a function");
901
+ if (customElementConstructors.has(tag)) throw new DOMException("Custom element already defined", "NotSupportedError");
902
+ const Adapter = class extends window.HTMLElement {
903
+ constructor() {
904
+ super();
905
+ const wrapper = realm.wrap(this);
906
+ Object.setPrototypeOf(wrapper, constructor.prototype);
907
+ const previous = constructingElement;
908
+ constructingElement = this;
909
+ try { Reflect.construct(constructor, []); }
910
+ finally { constructingElement = previous; }
911
+ }
912
+ connectedCallback() {
913
+ const wrapper = realm.wrap(this);
914
+ if (typeof wrapper.connectedCallback === "function") callPage(wrapper.connectedCallback, wrapper, []);
915
+ }
916
+ disconnectedCallback() {
917
+ const wrapper = realm.wrap(this);
918
+ if (typeof wrapper.disconnectedCallback === "function") callPage(wrapper.disconnectedCallback, wrapper, []);
919
+ }
920
+ };
921
+ window.customElements.define(tag, Adapter);
922
+ customElementConstructors.set(tag, constructor);
923
+ // Upgrades change :defined without a DOM mutation.
924
+ hooks.definedChanged?.(tag);
925
+ },
926
+ get: (name) => customElementConstructors.get(String(name).toLowerCase()),
927
+ whenDefined(name) {
928
+ const tag = String(name).toLowerCase();
929
+ return window.customElements.whenDefined(tag).then(() => customElementConstructors.get(tag));
930
+ },
931
+ upgrade(root) {
932
+ const node = nodeOf(root);
933
+ if (node) window.customElements.upgrade(node);
934
+ },
803
935
  },
804
936
  ...INTERFACES,
937
+ Node: NodeInterface,
938
+ Text: TextInterface,
939
+ CharacterData: CharacterDataInterface,
940
+ Element: ElementInterface,
941
+ HTMLElement: HTMLElementInterface,
942
+ Document: DocumentInterface,
943
+ HTMLDocument: DocumentInterface,
944
+ DocumentFragment: DocumentFragmentInterface,
805
945
  ...typeCheckInterfaces(),
806
946
  addEventListener: windowEvents.addEventListener,
807
947
  removeEventListener: windowEvents.removeEventListener,
808
948
  dispatchEvent: windowEvents.dispatchEvent,
809
949
  getComputedStyle: (element) => computedStyleBinding(hooks.computedStyle(nodeOf(element))),
950
+ getSelection: () => ({
951
+ anchorNode: null, anchorOffset: 0, focusNode: null, focusOffset: 0,
952
+ isCollapsed: true, rangeCount: 0, type: "None",
953
+ addRange() {}, collapse() {}, collapseToEnd() {}, collapseToStart() {},
954
+ containsNode: () => false, deleteFromDocument() {}, empty() {}, extend() {},
955
+ getRangeAt: () => { throw new DOMException("Index out of range", "IndexSizeError"); },
956
+ modify() {}, removeAllRanges() {}, removeRange() {}, selectAllChildren() {}, setBaseAndExtent() {},
957
+ toString: () => "",
958
+ }),
810
959
  matchMedia: (query) => {
811
960
  const { width, height, mobile } = hooks.viewport();
812
961
  return {
@@ -1181,11 +1330,16 @@ export function createPageRealm({ window, document, mode, hooks }) {
1181
1330
  // An unresolvable src is reported when the script runs.
1182
1331
  }
1183
1332
  }
1184
- const run = async (script) => {
1333
+ const run = async (script, parserBlocking = false) => {
1185
1334
  if (disposed) return;
1186
1335
  startedScripts.add(script);
1336
+ parsingHead = parserBlocking && document.head?.contains(script);
1187
1337
  if ((script.getAttribute("type") ?? "").trim().toLowerCase() === "module") {
1188
- await runModuleScript(script);
1338
+ try {
1339
+ await runModuleScript(script);
1340
+ } finally {
1341
+ parsingHead = false;
1342
+ }
1189
1343
  return;
1190
1344
  }
1191
1345
  const src = script.getAttribute("src");
@@ -1196,21 +1350,33 @@ export function createPageRealm({ window, document, mode, hooks }) {
1196
1350
  : script.textContent ?? "";
1197
1351
  } catch (error) {
1198
1352
  reportError(error);
1353
+ parsingHead = false;
1199
1354
  return;
1200
1355
  }
1201
- runClassic(script, source);
1356
+ try {
1357
+ runClassic(script, source);
1358
+ } finally {
1359
+ parsingHead = false;
1360
+ }
1202
1361
  };
1203
- for (const script of immediate) await run(script);
1362
+ for (const script of immediate) await run(script, true);
1204
1363
  for (const script of deferred) await run(script);
1205
1364
  if (disposed) return;
1206
1365
  readyState = "interactive";
1366
+ markTiming("domInteractive", "domContentLoadedEventStart");
1207
1367
  runTask(() => fireLifecycle(document, new window.Event("DOMContentLoaded", { bubbles: true })));
1368
+ markTiming("domContentLoadedEventEnd");
1208
1369
  hooks.lifecycle?.("DOMContentLoaded");
1209
1370
  for (const script of asynchronous) await run(script);
1210
1371
  // Scripts inserted so far (and any they insert in turn) delay load.
1211
1372
  while (pendingLoads.size && !disposed) await Promise.allSettled([...pendingLoads]);
1373
+ // The document's images delay load too (HTML "the end", step 8).
1374
+ await hooks.whenImagesLoaded?.();
1375
+ if (disposed) return;
1212
1376
  readyState = "complete";
1377
+ markTiming("domComplete", "loadEventStart");
1213
1378
  runTask(() => fireLifecycle(window, new window.Event("load")));
1379
+ markTiming("loadEventEnd");
1214
1380
  hooks.lifecycle?.("load");
1215
1381
  },
1216
1382
 
@@ -1225,12 +1391,16 @@ export function createPageRealm({ window, document, mode, hooks }) {
1225
1391
  },
1226
1392
 
1227
1393
  /**
1228
- * Same-document navigation to a fragment: updates location and
1229
- * document.URL. When the fragment changed, Happy DOM fires hashchange
1230
- * (HTML §7.4.2.3.3) from a zero-delay timer; the promise settles after it.
1394
+ * Same-document navigation to a fragment (HTML §7.4.2.3.3): updates
1395
+ * location and document.URL, adds a history entry and fires popstate
1396
+ * at once; when the fragment changed, Happy DOM then fires hashchange
1397
+ * from a zero-delay timer. The promise settles after it.
1231
1398
  */
1232
1399
  navigateToFragment(url) {
1400
+ const same = url === location.href;
1233
1401
  setHref(url);
1402
+ pushEntry(same);
1403
+ runTask(() => window.dispatchEvent(new window.PopStateEvent("popstate", { state: null })));
1234
1404
  window.happyDOM?.setURL?.(url);
1235
1405
  return new Promise((resolve) => hostSetTimeout(resolve, 0));
1236
1406
  },
@@ -1256,6 +1426,30 @@ export function createPageRealm({ window, document, mode, hooks }) {
1256
1426
  };
1257
1427
  }
1258
1428
 
1429
+ const NAVIGATION_TIMING_FIELDS = [
1430
+ "navigationStart", "unloadEventStart", "unloadEventEnd", "redirectStart", "redirectEnd", "fetchStart",
1431
+ "domainLookupStart", "domainLookupEnd", "connectStart", "connectEnd", "secureConnectionStart", "requestStart",
1432
+ "responseStart", "responseEnd", "domLoading", "domInteractive", "domContentLoadedEventStart",
1433
+ "domContentLoadedEventEnd", "domComplete", "loadEventStart", "loadEventEnd",
1434
+ ];
1435
+ // Phases before the document exists: no unload, redirect or secure connection
1436
+ // information is kept, and the network phases start with the navigation.
1437
+ const AT_START = new Set(["fetchStart", "domainLookupStart", "domainLookupEnd", "connectStart", "connectEnd",
1438
+ "requestStart", "responseStart", "responseEnd"]);
1439
+
1440
+ /** performance.timing: live getters over the document's lifecycle marks. */
1441
+ function navigationTiming(start, marks) {
1442
+ const timing = {};
1443
+ for (const field of NAVIGATION_TIMING_FIELDS) {
1444
+ Object.defineProperty(timing, field, {
1445
+ enumerable: true,
1446
+ get: () => field === "navigationStart" || AT_START.has(field) ? start : marks[field] ?? 0,
1447
+ });
1448
+ }
1449
+ timing.toJSON = () => Object.fromEntries(NAVIGATION_TIMING_FIELDS.map((field) => [field, timing[field]]));
1450
+ return Object.freeze(timing);
1451
+ }
1452
+
1259
1453
  function installGlobalAccessors(target, accessors) {
1260
1454
  for (const [name, get] of Object.entries(accessors)) {
1261
1455
  Object.defineProperty(target, name, { get, configurable: true, enumerable: true });
@@ -1352,7 +1546,12 @@ function historyBinding(location, setHref) {
1352
1546
  state = data ?? null;
1353
1547
  },
1354
1548
  };
1355
- return { History, history: Object.create(History.prototype) };
1549
+ // A fragment navigation adds an entry with no state (a same-URL one replaces it).
1550
+ const pushEntry = (replace) => {
1551
+ state = null;
1552
+ if (!replace) length++;
1553
+ };
1554
+ return { History, history: Object.create(History.prototype), pushEntry };
1356
1555
  }
1357
1556
 
1358
1557
  /** window.location; `setHref` moves it for same-document (fragment) navigations. */
@@ -1434,8 +1633,13 @@ function computedStyleBinding(style) {
1434
1633
  whiteSpace: style.whiteSpace,
1435
1634
  boxSizing: style.boxSizing,
1436
1635
  zIndex: String(style.zIndex),
1437
- visibility: "visible",
1438
- opacity: "1",
1636
+ visibility: style.visibility ?? "visible",
1637
+ opacity: String(style.opacity ?? 1),
1638
+ pointerEvents: style.pointerEvents ?? "auto",
1639
+ // Boxes never scroll here; a clipping box reads as hidden.
1640
+ overflow: style.overflowX === "clip" || style.overflowY === "clip" ? "hidden" : "visible",
1641
+ overflowX: style.overflowX === "clip" ? "hidden" : "visible",
1642
+ overflowY: style.overflowY === "clip" ? "hidden" : "visible",
1439
1643
  } : {};
1440
1644
  const kebab = (name) => name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
1441
1645
  const byKebab = Object.fromEntries(Object.entries(values).map(([name, value]) => [kebab(name), value]));