buninu-browser 0.2.0 → 0.2.2

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 (80) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +74 -9
  3. package/bunfig.toml +1 -0
  4. package/buninu-browser.js +4 -0
  5. package/index.js +5 -0
  6. package/lib/audio/piano.js +24 -1
  7. package/lib/audio/pulse-client.js +120 -12
  8. package/lib/cdp-server.js +19 -12
  9. package/lib/happy-dom/parser.js +24 -4
  10. package/lib/happy-dom/selector-matcher.js +83 -0
  11. package/lib/headless-shell.js +120 -28
  12. package/lib/input/hit-test.js +25 -7
  13. package/lib/layout/text-layout.js +365 -54
  14. package/lib/log.js +5 -2
  15. package/lib/media/byte-pipe.js +82 -0
  16. package/lib/media/position-audio.js +42 -0
  17. package/lib/network/network-service.js +326 -46
  18. package/lib/paint/display-list.js +322 -21
  19. package/lib/paint/gradient.js +11 -5
  20. package/lib/render-tree/index.js +62 -2
  21. package/lib/render-tree/svg.js +2 -1
  22. package/lib/renderer/bindings.js +989 -72
  23. package/lib/renderer/canvas-2d.js +92 -0
  24. package/lib/renderer/host.js +159 -9
  25. package/lib/renderer/interfaces.js +215 -0
  26. package/lib/renderer/main.js +160 -12
  27. package/lib/renderer/media-source.js +819 -0
  28. package/lib/renderer/messaging.js +411 -0
  29. package/lib/renderer/native-functions.js +113 -0
  30. package/lib/renderer/page-realm.js +1407 -200
  31. package/lib/renderer/page-renderer.js +1899 -394
  32. package/lib/renderer/rejections.js +31 -0
  33. package/lib/renderer/rewrite-worker.js +22 -0
  34. package/lib/renderer/script-rewrite.js +805 -57
  35. package/lib/renderer/trusted-types.js +129 -0
  36. package/lib/renderer/workers.js +327 -0
  37. package/lib/screenshot-cli.js +2 -0
  38. package/lib/style/computed-style.js +463 -242
  39. package/lib/style/selector-keys.js +302 -0
  40. package/lib/style/selector-traits.js +240 -0
  41. package/lib/user-agent.js +14 -2
  42. package/package.json +4 -1
  43. package/patches/happy-dom/0002-use-bun-platform-adapters.patch +139 -19
  44. package/patches/happy-dom/0006-parse-css-escapes-in-selectors.patch +115 -0
  45. package/patches/happy-dom/README.md +26 -0
  46. package/patches/ses/0002-embedder-native-functions-source-text-sloppy-code.patch +106 -0
  47. package/patches/ses/0003-cache-sloppy-evaluators.patch +47 -0
  48. package/patches/ses/README.md +4 -0
  49. package/test/canvas-2d.test.js +55 -0
  50. package/test/compositor.test.js +3 -1
  51. package/test/computed-style.test.js +98 -1
  52. package/test/display-list.test.js +74 -1
  53. package/test/download.test.js +35 -0
  54. package/test/fixtures/long-video.mp4 +0 -0
  55. package/test/fixtures/media-sync.js +57 -0
  56. package/test/happy-dom-platform-adapters.test.js +25 -0
  57. package/test/headless-shell.test.js +29 -0
  58. package/test/hit-test.test.js +25 -1
  59. package/test/log.test.js +4 -1
  60. package/test/media-source.test.js +156 -0
  61. package/test/media-sync.test.js +12 -0
  62. package/test/mobile.test.js +2 -2
  63. package/test/navigation.test.js +61 -0
  64. package/test/network-service.test.js +81 -0
  65. package/test/page-scripts.test.js +1094 -7
  66. package/test/position-audio.test.js +67 -0
  67. package/test/positioning.test.js +95 -1
  68. package/test/pulse-client.test.js +69 -0
  69. package/test/render-tree.test.js +22 -1
  70. package/test/renderer-host.test.js +5 -2
  71. package/test/screenshot-cli.test.js +25 -1
  72. package/test/script-rewrite.test.js +111 -6
  73. package/test/selector-keys.test.js +39 -0
  74. package/test/selector-traits.test.js +19 -0
  75. package/test/setup.js +5 -0
  76. package/test/text-layout.test.js +69 -1
  77. package/test/user-agent.test.js +60 -4
  78. package/vendor/happy-dom/packages/happy-dom/src/nodes/html-image-element/HTMLImageElement.ts +52 -22
  79. package/vendor/happy-dom/packages/happy-dom/src/query-selector/SelectorParser.ts +18 -17
  80. package/vendor/ses/dist/ses.mjs +52 -6
@@ -23,13 +23,21 @@ export class BindingRealm {
23
23
  #wrappers = new WeakMap();
24
24
  #listeners = new WeakMap();
25
25
  #events = new WeakMap();
26
+ #trustedEvents = new WeakSet();
27
+ // MessageEvent fields (data, origin, lastEventId, source, ports) by engine
28
+ // event: page values stay on this side, as CustomEvent detail does.
29
+ #messages = new WeakMap();
26
30
  #liveViews = new WeakMap();
31
+ #ranges = new WeakMap();
27
32
  #hooks;
28
33
  #window;
29
34
  eventPrototype = null;
30
35
  elementPrototype = null;
36
+ /** @type {((node: object) => object) | null} the prototype for an element's wrapper */
37
+ elementPrototypeOf = null;
31
38
  fragmentPrototype = null;
32
39
  shadowRootPrototype = null;
40
+ rangePrototype = null;
33
41
  textPrototype = null;
34
42
  characterDataPrototype = null;
35
43
  nodePrototype = null;
@@ -71,7 +79,9 @@ export class BindingRealm {
71
79
  : node.nodeType === 8 ? CharacterDataBinding
72
80
  : NodeBinding;
73
81
  wrapper = new Binding(CONSTRUCT, node, this);
74
- if (Binding === ElementBinding && this.elementPrototype) Object.setPrototypeOf(wrapper, this.elementPrototype);
82
+ if (Binding === ElementBinding && this.elementPrototype) {
83
+ Object.setPrototypeOf(wrapper, this.elementPrototypeOf?.(node) ?? this.elementPrototype);
84
+ }
75
85
  if (Binding === FragmentBinding && this.fragmentPrototype) Object.setPrototypeOf(wrapper, this.fragmentPrototype);
76
86
  if (Binding === ShadowRootBinding && this.shadowRootPrototype) Object.setPrototypeOf(wrapper, this.shadowRootPrototype);
77
87
  if (Binding === TextBinding && this.textPrototype) Object.setPrototypeOf(wrapper, this.textPrototype);
@@ -107,10 +117,15 @@ export class BindingRealm {
107
117
  return null;
108
118
  },
109
119
  };
120
+ // Indices and length read through; other properties a page sets are
121
+ // expandos kept on the target (they shadow item/namedItem, as an own
122
+ // property on a browser's collection does).
123
+ const expando = (target, property) => property !== "length" && !isIndex(property) && Object.hasOwn(target, property);
110
124
  view = new Proxy([], {
111
125
  get(target, property, receiver) {
112
126
  if (property === "length") return collection.length;
113
127
  if (isIndex(property)) return at(Number(property));
128
+ if (expando(target, property)) return target[property];
114
129
  if (Object.hasOwn(methods, property)) return methods[property];
115
130
  return Reflect.get(target, property, receiver);
116
131
  },
@@ -121,12 +136,23 @@ export class BindingRealm {
121
136
  if (isIndex(property) && Number(property) < collection.length) {
122
137
  return { value: at(Number(property)), writable: false, enumerable: true, configurable: true };
123
138
  }
124
- return undefined;
139
+ return expando(target, property) ? Reflect.getOwnPropertyDescriptor(target, property) : undefined;
140
+ },
141
+ ownKeys: (target) => [...Array.from({ length: collection.length }, (_, index) => String(index)), "length",
142
+ ...Reflect.ownKeys(target).filter((key) => expando(target, key))],
143
+ set(target, property, value) {
144
+ if (property === "length" || isIndex(property)) return false;
145
+ if (expando(target, property)) {
146
+ const descriptor = Reflect.getOwnPropertyDescriptor(target, property);
147
+ if (!("value" in descriptor)) return descriptor.set ? (descriptor.set.call(view, value), true) : false;
148
+ if (!descriptor.writable) return false;
149
+ }
150
+ return Reflect.defineProperty(target, property, expando(target, property)
151
+ ? { value } : { value, writable: true, enumerable: true, configurable: true });
125
152
  },
126
- ownKeys: () => [...Array.from({ length: collection.length }, (_, index) => String(index)), "length"],
127
- set: () => false,
128
- defineProperty: () => false,
129
- deleteProperty: () => false,
153
+ defineProperty: (target, property, descriptor) =>
154
+ property !== "length" && !isIndex(property) && Reflect.defineProperty(target, property, descriptor),
155
+ deleteProperty: (target, property) => !isIndex(property) && property !== "length" && Reflect.deleteProperty(target, property),
130
156
  });
131
157
  this.#liveViews.set(collection, view);
132
158
  return view;
@@ -136,13 +162,59 @@ export class BindingRealm {
136
162
  return Array.from(nodes ?? [], (node) => this.wrap(node));
137
163
  }
138
164
 
165
+ /** A MessageEvent for the page: an engine event carrying page-side fields. */
166
+ messageEvent(type, { data = null, origin = "", lastEventId = "", source = null, ports = [] } = {}, init = {}) {
167
+ const event = new this.#window.Event(String(type), init);
168
+ this.#messages.set(event, Object.freeze({ data, origin: String(origin), lastEventId: String(lastEventId), source,
169
+ ports: Object.freeze([...ports]) }));
170
+ return this.wrapEvent(event);
171
+ }
172
+
173
+ /** The engine event behind a page event, or null. */
174
+ unwrapEvent(value) {
175
+ return EventBinding.unwrap(value);
176
+ }
177
+
178
+ /** Marks an event created by the browser's input path as trusted. */
179
+ markTrusted(value) {
180
+ const event = EventBinding.unwrap(value) ?? (value && typeof value === "object" ? value : null);
181
+ if (event) this.#trustedEvents.add(event);
182
+ }
183
+
184
+ isTrusted(event) {
185
+ return this.#trustedEvents.has(event);
186
+ }
187
+
188
+ /** The MessageEvent fields of an engine event, if it is one. */
189
+ messageOf(event) {
190
+ return this.#messages.get(event) ?? null;
191
+ }
192
+
193
+ /** The page's Range for an engine Range (one wrapper each). */
194
+ wrapRange(range) {
195
+ if (!range) return null;
196
+ let wrapper = this.#ranges.get(range);
197
+ if (!wrapper) {
198
+ wrapper = new RangeBinding(CONSTRUCT, range, this);
199
+ if (this.rangePrototype) Object.setPrototypeOf(wrapper, this.rangePrototype);
200
+ this.#ranges.set(range, wrapper);
201
+ }
202
+ return wrapper;
203
+ }
204
+
139
205
  /** One wrapper per event, so listeners see the dispatched object (and its detail). */
140
206
  wrapEvent(event, detail = undefined) {
141
207
  if (!event) return null;
142
208
  let wrapper = this.#events.get(event);
143
209
  if (!wrapper) {
144
210
  wrapper = new EventBinding(CONSTRUCT, event, this, detail);
145
- if (this.eventPrototype) Object.setPrototypeOf(wrapper, this.eventPrototype);
211
+ if (this.eventPrototype) {
212
+ const prototype = this.eventPrototypes?.get(event.constructor?.name) ?? this.eventPrototype;
213
+ Object.setPrototypeOf(wrapper, prototype);
214
+ // isTrusted is [LegacyUnforgeable]: an own accessor of every event.
215
+ const isTrusted = Object.getOwnPropertyDescriptor(EventBinding.prototype, "isTrusted");
216
+ Object.defineProperty(wrapper, "isTrusted", { get: isTrusted.get, enumerable: true, configurable: false });
217
+ }
146
218
  this.#events.set(event, wrapper);
147
219
  }
148
220
  return wrapper;
@@ -172,8 +244,15 @@ export function mutableBindingPrototype(base, parent = base) {
172
244
  const descriptor = Object.getOwnPropertyDescriptor(base, key);
173
245
  descriptor.configurable = true;
174
246
  if ("value" in descriptor) descriptor.writable = true;
247
+ // Web IDL attributes and operations are enumerable (class members are not).
248
+ if (typeof key === "string") descriptor.enumerable = true;
175
249
  Object.defineProperty(prototype, key, descriptor);
176
250
  }
251
+ // An own, writable constructor (callers set the page's): an inherited one
252
+ // from a hardened class would be read-only, and then a page subclass's
253
+ // `Sub.prototype = Object.create(Base.prototype); Sub.prototype.constructor = Sub`
254
+ // fails (the override mistake).
255
+ Object.defineProperty(prototype, "constructor", { value: base.constructor, writable: true, configurable: true });
177
256
  return prototype;
178
257
  }
179
258
 
@@ -189,8 +268,8 @@ function toNode(wrapper, value) {
189
268
  }
190
269
 
191
270
  /** The page changed `wrapper`'s node (state the style engine may read: attributes, checked, value). */
192
- function touch(wrapper) {
193
- realmOf(wrapper).hooks.touch(NodeBinding.unwrap(wrapper));
271
+ function touch(wrapper, stateFeatures = []) {
272
+ realmOf(wrapper).hooks.touch(NodeBinding.unwrap(wrapper), stateFeatures);
194
273
  }
195
274
 
196
275
  /**
@@ -204,6 +283,23 @@ function inserted(wrapper, nodes) {
204
283
  }
205
284
 
206
285
  /** A fragment empties itself on insertion, so list its children beforehand. */
286
+ const XML_NAMESPACE = "http://www.w3.org/XML/1998/namespace";
287
+ const XMLNS_NAMESPACE = "http://www.w3.org/2000/xmlns/";
288
+
289
+ function locateNamespace(node, prefix) {
290
+ if (prefix === "xml") return XML_NAMESPACE;
291
+ if (prefix === "xmlns") return XMLNS_NAMESPACE;
292
+ if (node.nodeType === 9) node = node.documentElement;
293
+ else if (node.nodeType !== 1) node = node.parentElement ?? node.ownerElement ?? null;
294
+ for (let element = node; element; element = element.parentElement) {
295
+ if (element.namespaceURI !== null && (element.prefix ?? null) === prefix) return element.namespaceURI;
296
+ const attribute = prefix === null ? element.getAttributeNS?.(XMLNS_NAMESPACE, "xmlns") ?? element.getAttribute("xmlns")
297
+ : element.getAttributeNS?.(XMLNS_NAMESPACE, prefix);
298
+ if (attribute !== null && attribute !== undefined) return attribute || null;
299
+ }
300
+ return null;
301
+ }
302
+
207
303
  function insertionList(nodes) {
208
304
  return nodes.flatMap((node) => node.nodeType === 11 ? [...node.childNodes] : [node]);
209
305
  }
@@ -249,6 +345,19 @@ class NodeBinding {
249
345
  return this.#node.nodeName;
250
346
  }
251
347
 
348
+ /** The document base URL (HTML §2.4.1): the first <base href>, else the document's URL. */
349
+ get baseURI() {
350
+ const document = this.#node.nodeType === 9 ? this.#node : this.#node.ownerDocument;
351
+ const url = document?.URL ?? "about:blank";
352
+ const href = document?.querySelector?.("base[href]")?.getAttribute("href");
353
+ if (href == null) return url;
354
+ try {
355
+ return new URL(href, url).href;
356
+ } catch {
357
+ return url;
358
+ }
359
+ }
360
+
252
361
  get nodeValue() {
253
362
  return this.#node.nodeValue ?? null;
254
363
  }
@@ -314,6 +423,53 @@ class NodeBinding {
314
423
  return node ? this.#node.contains(node) : false;
315
424
  }
316
425
 
426
+ /** Where `other` is relative to this node, as DOCUMENT_POSITION_* bits (DOM §4.4). */
427
+ compareDocumentPosition(other) {
428
+ const node = nodeOf(other);
429
+ if (!node) throw new TypeError("Failed to execute 'compareDocumentPosition' on 'Node': parameter 1 is not of type 'Node'.");
430
+ return this.#node.compareDocumentPosition(node);
431
+ }
432
+
433
+ getRootNode(options = undefined) {
434
+ return this.#realm.wrap(this.#node.getRootNode({ composed: Boolean(options?.composed) }));
435
+ }
436
+
437
+ isSameNode(other) {
438
+ return other === this || (nodeOf(other) !== null && nodeOf(other) === this.#node);
439
+ }
440
+
441
+ isEqualNode(other) {
442
+ const node = nodeOf(other);
443
+ return node ? this.#node.isEqualNode(node) : false;
444
+ }
445
+
446
+ normalize() {
447
+ this.#node.normalize();
448
+ this.#realm.hooks.touch(this.#node);
449
+ }
450
+
451
+ // DOM §4.4 "locate a namespace" and "locate a namespace prefix".
452
+ lookupNamespaceURI(prefix) {
453
+ return locateNamespace(this.#node, prefix === null || prefix === undefined || prefix === "" ? null : String(prefix));
454
+ }
455
+
456
+ lookupPrefix(namespace) {
457
+ if (namespace === null || namespace === undefined || namespace === "") return null;
458
+ let element = this.#node.nodeType === 1 ? this.#node
459
+ : this.#node.nodeType === 9 ? this.#node.documentElement : this.#node.parentElement ?? this.#node.ownerElement ?? null;
460
+ for (; element; element = element.parentElement) {
461
+ if (element.namespaceURI === String(namespace) && element.prefix) return element.prefix;
462
+ for (const attribute of element.attributes ?? []) {
463
+ if (attribute.prefix === "xmlns" && attribute.value === String(namespace)) return attribute.localName;
464
+ }
465
+ }
466
+ return null;
467
+ }
468
+
469
+ isDefaultNamespace(namespace) {
470
+ return locateNamespace(this.#node, null) === (namespace === undefined || namespace === "" ? null : namespace);
471
+ }
472
+
317
473
  appendChild(child) {
318
474
  const node = requireNode(child);
319
475
  const list = insertionList([node]);
@@ -348,11 +504,11 @@ class NodeBinding {
348
504
  return this.#realm.wrap(this.#node.cloneNode(Boolean(deep)));
349
505
  }
350
506
 
351
- addEventListener(type, listener, options) {
507
+ addEventListener(type, listener, options = undefined) {
352
508
  addListener(this, this.#node, type, listener, options);
353
509
  }
354
510
 
355
- removeEventListener(type, listener, options) {
511
+ removeEventListener(type, listener, options = undefined) {
356
512
  removeListener(this, this.#node, type, listener, options);
357
513
  }
358
514
 
@@ -370,6 +526,18 @@ function requireNode(value) {
370
526
  }
371
527
 
372
528
  /** Text and comments; frameworks read comment data (React's hydration markers). */
529
+ // Node constants (DOM §4.4) on instances too, and the one the class lacks.
530
+ for (const [name, value] of Object.entries({
531
+ ELEMENT_NODE: 1, ATTRIBUTE_NODE: 2, TEXT_NODE: 3, CDATA_SECTION_NODE: 4, PROCESSING_INSTRUCTION_NODE: 7,
532
+ ENTITY_REFERENCE_NODE: 5, ENTITY_NODE: 6, COMMENT_NODE: 8, DOCUMENT_NODE: 9, DOCUMENT_TYPE_NODE: 10,
533
+ DOCUMENT_FRAGMENT_NODE: 11, NOTATION_NODE: 12,
534
+ DOCUMENT_POSITION_DISCONNECTED: 1, DOCUMENT_POSITION_PRECEDING: 2, DOCUMENT_POSITION_FOLLOWING: 4,
535
+ DOCUMENT_POSITION_CONTAINS: 8, DOCUMENT_POSITION_CONTAINED_BY: 16, DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC: 32,
536
+ })) {
537
+ if (!Object.hasOwn(NodeBinding, name)) Object.defineProperty(NodeBinding, name, { value, enumerable: true });
538
+ if (!Object.hasOwn(NodeBinding.prototype, name)) Object.defineProperty(NodeBinding.prototype, name, { value, enumerable: true });
539
+ }
540
+
373
541
  class CharacterDataBinding extends NodeBinding {
374
542
  get data() {
375
543
  return nodeOf(this).data;
@@ -417,6 +585,10 @@ class ParentBinding extends NodeBinding {
417
585
  return realmOf(this).live(nodeOf(this).getElementsByTagName(String(name)));
418
586
  }
419
587
 
588
+ getElementsByTagNameNS(namespace, localName) {
589
+ return realmOf(this).live(nodeOf(this).getElementsByTagNameNS(namespace === undefined ? null : namespace, String(localName)));
590
+ }
591
+
420
592
  getElementsByClassName(names) {
421
593
  return realmOf(this).live(nodeOf(this).getElementsByClassName(String(names)));
422
594
  }
@@ -476,6 +648,7 @@ class ElementBinding extends ParentBinding {
476
648
  #style = null;
477
649
  #classList = null;
478
650
  #dataset = null;
651
+ #attributes = null;
479
652
 
480
653
  get tagName() {
481
654
  return nodeOf(this).tagName;
@@ -490,11 +663,39 @@ class ElementBinding extends ParentBinding {
490
663
  return realmOf(this).wrap(nodeOf(this).shadowRoot);
491
664
  }
492
665
 
666
+ // HTMLIFrameElement: its document's window and, when same-origin, its
667
+ // document (see messaging.js).
668
+ get contentWindow() {
669
+ const node = nodeOf(this);
670
+ return node.tagName === "IFRAME" ? realmOf(this).hooks.contentWindow?.(node) ?? null : undefined;
671
+ }
672
+
673
+ get contentDocument() {
674
+ const node = nodeOf(this);
675
+ return node.tagName === "IFRAME" ? realmOf(this).hooks.contentDocument?.(node) ?? null : undefined;
676
+ }
677
+
678
+ // One property for two interfaces: HTMLTemplateElement.content (read-only,
679
+ // the template's fragment) and HTMLMetaElement.content (the reflected
680
+ // attribute, which scripts set). Other elements have none: an assignment
681
+ // stays on the wrapper as a plain property.
493
682
  get content() {
494
- const content = nodeOf(this).content;
683
+ const node = nodeOf(this);
684
+ if (node.tagName === "META") return node.getAttribute("content") ?? "";
685
+ const content = node.content;
495
686
  return content === undefined ? undefined : realmOf(this).wrap(content);
496
687
  }
497
688
 
689
+ set content(value) {
690
+ const node = nodeOf(this);
691
+ if (node.tagName === "META") {
692
+ node.setAttribute("content", String(value));
693
+ touch(this);
694
+ } else if (node.tagName !== "TEMPLATE") {
695
+ Object.defineProperty(this, "content", { value, writable: true, enumerable: true, configurable: true });
696
+ }
697
+ }
698
+
498
699
  // HTMLMediaElement (video and audio): the subset the renderer plays.
499
700
  get paused() {
500
701
  return isMedia(this) ? realmOf(this).hooks.mediaState?.(nodeOf(this)).paused ?? true : undefined;
@@ -528,6 +729,19 @@ class ElementBinding extends ParentBinding {
528
729
  if (isMedia(this)) realmOf(this).hooks.pauseMedia?.(nodeOf(this));
529
730
  }
530
731
 
732
+ /** Restarts loading (HTML §4.8.11.5): playback stops and starts again from the beginning when played. */
733
+ load() {
734
+ if (!isMedia(this)) return;
735
+ realmOf(this).hooks.pauseMedia?.(nodeOf(this));
736
+ void realmOf(this).hooks.seekMedia?.(nodeOf(this), 0);
737
+ }
738
+
739
+ canPlayType(type) {
740
+ if (!isMedia(this)) return undefined;
741
+ // The decoder (FFmpeg) handles the common containers and codecs.
742
+ return /^(video|audio)\/(mp4|webm|ogg|mpeg|wav|x-wav|aac|mp3|flac)\b/i.test(String(type)) ? "maybe" : "";
743
+ }
744
+
531
745
  get localName() {
532
746
  return nodeOf(this).localName;
533
747
  }
@@ -573,10 +787,47 @@ class ElementBinding extends ParentBinding {
573
787
  return this.#dataset;
574
788
  }
575
789
 
790
+ get attributes() {
791
+ this.#attributes ??= attributesBinding(this);
792
+ return this.#attributes;
793
+ }
794
+
576
795
  getAttribute(name) {
577
796
  return nodeOf(this).getAttribute(String(name));
578
797
  }
579
798
 
799
+ getAttributeNodeNS(_namespace, localName) {
800
+ return this.getAttributeNode(String(localName));
801
+ }
802
+
803
+ // CSSOM View §6.1: false when the element or an ancestor has no box or is
804
+ // visibility: hidden (with the visibilityProperty option).
805
+ checkVisibility(options = undefined) {
806
+ const realm = realmOf(this);
807
+ const node = nodeOf(this);
808
+ if (!node.isConnected) return false;
809
+ for (let element = node; element; element = element.parentElement) {
810
+ if (realm.hooks.computedStyle?.(element)?.display === "none") return false;
811
+ }
812
+ if ((options?.visibilityProperty || options?.checkVisibilityCSS)
813
+ && realm.hooks.computedStyle?.(node)?.visibility === "hidden") return false;
814
+ return true;
815
+ }
816
+
817
+ // Web Animations: no animations run outside CSS transitions/animations.
818
+ getAnimations() {
819
+ return [];
820
+ }
821
+
822
+ // Pointer capture (Pointer Events §10): a pointer this element has captured.
823
+ hasPointerCapture(_pointerId) {
824
+ return false;
825
+ }
826
+
827
+ setPointerCapture(_pointerId) {}
828
+
829
+ releasePointerCapture(_pointerId) {}
830
+
580
831
  getAttributeNode(name) {
581
832
  const element = nodeOf(this);
582
833
  const attribute = element.getAttributeNode(String(name));
@@ -623,7 +874,30 @@ class ElementBinding extends ParentBinding {
623
874
  return nodeOf(this).hasAttribute(String(name));
624
875
  }
625
876
 
626
- toggleAttribute(name, force) {
877
+ hasAttributes() {
878
+ return nodeOf(this).getAttributeNames().length > 0;
879
+ }
880
+
881
+ // Namespaced attributes (DOM §4.9): xlink:href and friends in SVG.
882
+ getAttributeNS(namespace, localName) {
883
+ return nodeOf(this).getAttributeNS(namespace === undefined ? null : namespace && String(namespace), String(localName));
884
+ }
885
+
886
+ hasAttributeNS(namespace, localName) {
887
+ return nodeOf(this).hasAttributeNS(namespace === undefined ? null : namespace && String(namespace), String(localName));
888
+ }
889
+
890
+ setAttributeNS(namespace, qualifiedName, value) {
891
+ nodeOf(this).setAttributeNS(namespace === undefined ? null : namespace && String(namespace), String(qualifiedName), String(value));
892
+ touch(this);
893
+ }
894
+
895
+ removeAttributeNS(namespace, localName) {
896
+ nodeOf(this).removeAttributeNS(namespace === undefined ? null : namespace && String(namespace), String(localName));
897
+ touch(this);
898
+ }
899
+
900
+ toggleAttribute(name, force = undefined) {
627
901
  const result = nodeOf(this).toggleAttribute(String(name), force === undefined ? undefined : Boolean(force));
628
902
  touch(this);
629
903
  return result;
@@ -770,28 +1044,104 @@ class ElementBinding extends ParentBinding {
770
1044
  return Math.round(this.getBoundingClientRect().height);
771
1045
  }
772
1046
 
1047
+ /**
1048
+ * CSSOM View §7: the nearest ancestor that is positioned (or that
1049
+ * contains absolutely positioned boxes, as a transformed one does), or a
1050
+ * td, th or table for an element that is not positioned itself, else the
1051
+ * body. Null for the root, the body, fixed boxes and elements without a box.
1052
+ */
1053
+ get offsetParent() {
1054
+ const realm = realmOf(this);
1055
+ const node = nodeOf(this);
1056
+ const document = node.ownerDocument;
1057
+ if (!node.isConnected || node === document?.documentElement || node === document?.body) return null;
1058
+ const style = realm.hooks.computedStyle?.(node);
1059
+ if (!style || style.display === "none" || style.position === "fixed") return null;
1060
+ const positioned = style.position !== "static";
1061
+ for (let ancestor = node.parentElement; ancestor; ancestor = ancestor.parentElement) {
1062
+ if (ancestor === document.body) return realm.wrap(ancestor);
1063
+ const ancestorStyle = realm.hooks.computedStyle?.(ancestor);
1064
+ if (ancestorStyle?.display === "none") return null;
1065
+ if (ancestorStyle && (ancestorStyle.position !== "static" || ancestorStyle.transform)) return realm.wrap(ancestor);
1066
+ if (!positioned && (ancestor.tagName === "TD" || ancestor.tagName === "TH" || ancestor.tagName === "TABLE")) {
1067
+ return realm.wrap(ancestor);
1068
+ }
1069
+ }
1070
+ return null;
1071
+ }
1072
+
1073
+ // The border edge's distance from the offsetParent's padding edge, or
1074
+ // from the document's origin when that is the body or there is none.
773
1075
  get offsetLeft() {
774
- const { viewport } = realmOf(this).hooks;
775
- return Math.round(this.getBoundingClientRect().left + viewport().scrollX);
1076
+ return offsetCoordinate(this, "left");
776
1077
  }
777
1078
 
778
1079
  get offsetTop() {
779
- const { viewport } = realmOf(this).hooks;
780
- return Math.round(this.getBoundingClientRect().top + viewport().scrollY);
1080
+ return offsetCoordinate(this, "top");
781
1081
  }
782
1082
 
783
- // The root element's client size is the viewport (CSSOM View §6).
1083
+ // The root element's client size is the viewport (CSSOM View §6); a
1084
+ // scroll container's is its padding box.
784
1085
  get clientWidth() {
785
1086
  const root = nodeOf(this) === nodeOf(this).ownerDocument?.documentElement;
786
- return root ? realmOf(this).hooks.viewport().width : this.offsetWidth;
1087
+ if (root) return realmOf(this).hooks.viewport().width;
1088
+ const scroller = elementScroll(this);
1089
+ return scroller ? Math.round(scroller.clientWidth) : this.offsetWidth;
787
1090
  }
788
1091
 
789
1092
  get clientHeight() {
790
1093
  const root = nodeOf(this) === nodeOf(this).ownerDocument?.documentElement;
791
- return root ? realmOf(this).hooks.viewport().height : this.offsetHeight;
1094
+ if (root) return realmOf(this).hooks.viewport().height;
1095
+ const scroller = elementScroll(this);
1096
+ return scroller ? Math.round(scroller.clientHeight) : this.offsetHeight;
1097
+ }
1098
+
1099
+ // Scroll positions (CSSOM View §6): a scroll container's own; the root
1100
+ // element and body stand for the viewport's.
1101
+ get scrollTop() {
1102
+ if (scrollsViewport(this)) return realmOf(this).hooks.viewport().scrollY;
1103
+ return elementScroll(this)?.y ?? 0;
1104
+ }
1105
+
1106
+ set scrollTop(value) {
1107
+ this.scrollTo(this.scrollLeft, Number(value));
1108
+ }
1109
+
1110
+ get scrollLeft() {
1111
+ if (scrollsViewport(this)) return realmOf(this).hooks.viewport().scrollX;
1112
+ return elementScroll(this)?.x ?? 0;
1113
+ }
1114
+
1115
+ set scrollLeft(value) {
1116
+ this.scrollTo(Number(value), this.scrollTop);
1117
+ }
1118
+
1119
+ scrollTo(x = undefined, y = undefined) {
1120
+ const options = typeof x === "object" && x !== null ? x : null;
1121
+ const left = options ? options.left ?? this.scrollLeft : Number(x);
1122
+ const top = options ? options.top ?? this.scrollTop : Number(y);
1123
+ const realm = realmOf(this);
1124
+ if (scrollsViewport(this)) realm.hooks.scrollTo(Number.isFinite(left) ? left : 0, Number.isFinite(top) ? top : 0);
1125
+ else realm.hooks.setElementScroll?.(nodeOf(this), Number.isFinite(left) ? left : 0, Number.isFinite(top) ? top : 0);
1126
+ }
1127
+
1128
+ scroll(x = undefined, y = undefined) {
1129
+ this.scrollTo(x, y);
1130
+ }
1131
+
1132
+ scrollBy(x = undefined, y = undefined) {
1133
+ const options = typeof x === "object" && x !== null ? x : null;
1134
+ const dx = Number(options ? options.left ?? 0 : x) || 0;
1135
+ const dy = Number(options ? options.top ?? 0 : y) || 0;
1136
+ this.scrollTo(this.scrollLeft + dx, this.scrollTop + dy);
1137
+ }
1138
+
1139
+ // Forms, tables and selects (undefined on other elements, as in browsers).
1140
+ get elements() {
1141
+ const node = nodeOf(this);
1142
+ return node.tagName === "FORM" ? realmOf(this).live(node.elements) : undefined;
792
1143
  }
793
1144
 
794
- // Tables and selects (undefined on other elements, as in browsers).
795
1145
  get rows() {
796
1146
  const rows = nodeOf(this).rows;
797
1147
  return rows === undefined ? undefined : realmOf(this).wrapAll(rows);
@@ -842,29 +1192,71 @@ class ElementBinding extends ParentBinding {
842
1192
  set selectedIndex(value) {
843
1193
  if (nodeOf(this).tagName !== "SELECT") return;
844
1194
  nodeOf(this).selectedIndex = Number(value);
845
- touch(this);
1195
+ touch(this, [":state"]);
846
1196
  }
847
1197
 
848
- // Overflow inside elements is not scrollable here, so the scroll size is the
849
- // box size; the root element spans at least the viewport.
1198
+ // A scroll container's scroll size is how far its content reaches; other
1199
+ // boxes' is their own size; the root element spans at least the viewport.
850
1200
  get scrollWidth() {
1201
+ const scroller = elementScroll(this);
1202
+ if (scroller) return Math.round(scroller.scrollWidth);
851
1203
  const root = nodeOf(this) === nodeOf(this).ownerDocument?.documentElement;
852
1204
  return Math.max(this.offsetWidth, root ? realmOf(this).hooks.viewport().width : 0);
853
1205
  }
854
1206
 
855
1207
  get scrollHeight() {
1208
+ const scroller = elementScroll(this);
1209
+ if (scroller) return Math.round(scroller.scrollHeight);
856
1210
  const root = nodeOf(this) === nodeOf(this).ownerDocument?.documentElement;
857
1211
  return Math.max(this.offsetHeight, root ? realmOf(this).hooks.viewport().height : 0);
858
1212
  }
859
1213
 
860
1214
  scrollIntoView() {
861
1215
  const realm = realmOf(this);
1216
+ if (realm.hooks.scrollIntoView) {
1217
+ realm.hooks.scrollIntoView(nodeOf(this));
1218
+ return;
1219
+ }
862
1220
  const rect = this.getBoundingClientRect();
863
1221
  const { scrollX, scrollY } = realm.hooks.viewport();
864
1222
  realm.hooks.scrollTo(scrollX, scrollY + rect.top);
865
1223
  }
866
1224
  }
867
1225
 
1226
+ /** A scroll container's scroll state, or null (see the renderer's scrollerGeometry). */
1227
+ function offsetCoordinate(wrapper, side) {
1228
+ const realm = realmOf(wrapper);
1229
+ const node = nodeOf(wrapper);
1230
+ // CSSOM View §7: the body's offsets are zero.
1231
+ if (node === node.ownerDocument?.body) return 0;
1232
+ const rect = realm.hooks.boundsOf(node);
1233
+ if (!rect) return 0;
1234
+ const { scrollX, scrollY } = realm.hooks.viewport();
1235
+ const own = side === "left" ? rect.left + scrollX : rect.top + scrollY;
1236
+ const parent = wrapper.offsetParent;
1237
+ if (!parent || nodeOf(parent) === node.ownerDocument?.body) return Math.round(own);
1238
+ const parentNode = nodeOf(parent);
1239
+ const parentRect = realm.hooks.boundsOf(parentNode);
1240
+ if (!parentRect) return Math.round(own);
1241
+ const border = realm.hooks.computedStyle?.(parentNode)?.borderWidths?.[side === "left" ? 3 : 0] ?? 0;
1242
+ // A box scrolled inside its offsetParent keeps its layout offset.
1243
+ const scrolled = elementScroll(parent);
1244
+ const scroll = side === "left" ? scrolled?.x ?? 0 : scrolled?.y ?? 0;
1245
+ const origin = (side === "left" ? parentRect.left + scrollX : parentRect.top + scrollY) + border;
1246
+ return Math.round(own - origin + scroll);
1247
+ }
1248
+
1249
+ function elementScroll(wrapper) {
1250
+ return realmOf(wrapper).hooks.elementScroll?.(nodeOf(wrapper)) ?? null;
1251
+ }
1252
+
1253
+ /** The root element and body scroll the viewport (CSSOM View §6, the scrolling element). */
1254
+ function scrollsViewport(wrapper) {
1255
+ const node = nodeOf(wrapper);
1256
+ const document = node.ownerDocument;
1257
+ return node === document?.documentElement || node === document?.body;
1258
+ }
1259
+
868
1260
  function isMedia(wrapper) {
869
1261
  return wrapper.tagName === "VIDEO" || wrapper.tagName === "AUDIO";
870
1262
  }
@@ -926,7 +1318,10 @@ for (const property of REFLECTED_PROPERTIES) {
926
1318
  node[property] = typeof node[property] === "boolean" ? Boolean(value)
927
1319
  : typeof node[property] === "number" ? Number(value)
928
1320
  : String(value);
929
- touch(this);
1321
+ touch(this, [":state"]);
1322
+ // A media src may be a MediaSource blob URL. Attachment runs now so
1323
+ // sourceopen is queued after this setter returns.
1324
+ if (property === "src" && isMedia(this)) realmOf(this).hooks.attachMediaSrc?.(node, node.src);
930
1325
  },
931
1326
  });
932
1327
  }
@@ -952,6 +1347,30 @@ for (const property of ["origin", "protocol", "username", "password", "host", "h
952
1347
  }
953
1348
 
954
1349
  class DocumentBinding extends ParentBinding {
1350
+ #implementation = null;
1351
+
1352
+ /**
1353
+ * document.implementation (DOM §4.5.1): documents scripts build off-screen
1354
+ * (template parsing, sanitizers, polyfills). They share this window and are
1355
+ * never rendered.
1356
+ */
1357
+ get implementation() {
1358
+ const realm = realmOf(this);
1359
+ const node = nodeOf(this);
1360
+ this.#implementation ??= Object.freeze({
1361
+ createHTMLDocument: (title = undefined) => realm.wrap(node.implementation.createHTMLDocument(
1362
+ title === undefined ? undefined : String(title))),
1363
+ createDocument: (namespace, qualifiedName, doctype = null) => realm.wrap(node.implementation.createDocument(
1364
+ namespace === null || namespace === undefined ? null : String(namespace),
1365
+ qualifiedName === null || qualifiedName === undefined ? "" : String(qualifiedName),
1366
+ doctype ? nodeOf(doctype) : null)),
1367
+ createDocumentType: (qualifiedName, publicId, systemId) => realm.wrap(node.implementation.createDocumentType(
1368
+ String(qualifiedName), String(publicId), String(systemId))),
1369
+ hasFeature: () => true,
1370
+ });
1371
+ return this.#implementation;
1372
+ }
1373
+
955
1374
  get documentElement() {
956
1375
  return realmOf(this).wrap(nodeOf(this).documentElement);
957
1376
  }
@@ -1023,6 +1442,49 @@ class DocumentBinding extends ParentBinding {
1023
1442
  return realmOf(this).wrap(nodeOf(this).activeElement);
1024
1443
  }
1025
1444
 
1445
+ /**
1446
+ * document.all is an HTMLAllCollection that is falsy and loosely equal to
1447
+ * undefined (HTML §7.2.6 [[IsHTMLDDA]]), which script cannot create. null
1448
+ * keeps what pages test, `!document.all`, `document.all == null` and
1449
+ * `value !== document.all` for undefined values (polymer_resin's check
1450
+ * before sanitizing); only typeof differs.
1451
+ */
1452
+ get all() {
1453
+ return null;
1454
+ }
1455
+
1456
+ /**
1457
+ * document.styleSheets (CSSOM §6.2): one entry per <style> and loaded-or-not
1458
+ * <link rel=stylesheet>, in document order. Their rules are not exposed
1459
+ * (cssRules is empty), as for a sheet scripts may not read.
1460
+ */
1461
+ get styleSheets() {
1462
+ const realm = realmOf(this);
1463
+ const sheets = [...nodeOf(this).querySelectorAll("style, link[rel~='stylesheet']")].map((element) => {
1464
+ const href = element.tagName === "LINK" ? element.getAttribute("href") : null;
1465
+ let resolved = null;
1466
+ if (href !== null) try { resolved = new URL(href, nodeOf(this).URL).href; } catch {}
1467
+ return Object.freeze({
1468
+ type: "text/css",
1469
+ href: resolved,
1470
+ title: element.getAttribute("title"),
1471
+ disabled: false,
1472
+ ownerNode: realm.wrap(element),
1473
+ media: Object.freeze({ mediaText: element.getAttribute("media") ?? "", length: element.hasAttribute("media") ? 1 : 0 }),
1474
+ cssRules: Object.freeze([]),
1475
+ rules: Object.freeze([]),
1476
+ });
1477
+ });
1478
+ const list = [...sheets];
1479
+ list.item = (index) => sheets[Number(index) >>> 0] ?? null;
1480
+ return Object.freeze(list);
1481
+ }
1482
+
1483
+ /** The topmost element painted at a viewport point (CSSOM View §5.1), or null. */
1484
+ elementFromPoint(x, y) {
1485
+ return realmOf(this).wrap(realmOf(this).hooks.elementAt?.(Number(x), Number(y)) ?? null);
1486
+ }
1487
+
1026
1488
  get currentScript() {
1027
1489
  return realmOf(this).wrap(realmOf(this).hooks.currentScript?.() ?? null);
1028
1490
  }
@@ -1083,8 +1545,27 @@ class DocumentBinding extends ParentBinding {
1083
1545
  return realmOf(this).wrap(nodeOf(this).createDocumentFragment());
1084
1546
  }
1085
1547
 
1086
- createEvent(_type) {
1087
- return realmOf(this).wrapEvent(new (realmOf(this).window.Event)(""));
1548
+ createRange() {
1549
+ return realmOf(this).wrapRange(nodeOf(this).createRange());
1550
+ }
1551
+
1552
+ /** A copy of a node (from any document, a template's content say) owned by this one (DOM §4.5). */
1553
+ importNode(node, deep = false) {
1554
+ const options = typeof deep === "object" && deep !== null ? Boolean(deep.deep) : Boolean(deep);
1555
+ return realmOf(this).wrap(nodeOf(this).importNode(requireNode(node), options));
1556
+ }
1557
+
1558
+ /** Moves a node into this document, out of wherever it was. */
1559
+ adoptNode(node) {
1560
+ return realmOf(this).wrap(nodeOf(this).adoptNode(requireNode(node)));
1561
+ }
1562
+
1563
+ createEvent(type) {
1564
+ const name = CREATE_EVENT_TYPES.get(String(type).toLowerCase());
1565
+ if (!name) throw new DOMException(`The provided event type ("${type}") is invalid.`, "NotSupportedError");
1566
+ const window = realmOf(this).window;
1567
+ const RealEvent = window[name] ?? window.Event;
1568
+ return realmOf(this).wrapEvent(new RealEvent(""));
1088
1569
  }
1089
1570
 
1090
1571
  hasFocus() {
@@ -1145,15 +1626,121 @@ class EventBinding {
1145
1626
  }
1146
1627
 
1147
1628
  get isTrusted() {
1148
- return Boolean(this.#event.isTrusted);
1629
+ return this.#realm.isTrusted(this.#event);
1149
1630
  }
1150
1631
 
1632
+ // Happy DOM stamps events with the engine's clock; pages see their document's timeline.
1151
1633
  get timeStamp() {
1152
- return this.#event.timeStamp;
1634
+ return this.#event.timeStamp - (this.#realm.hooks.timeOriginOffset?.() ?? 0);
1153
1635
  }
1154
1636
 
1155
1637
  get detail() {
1156
- return this.#detail ?? null;
1638
+ return this.#detail ?? (typeof this.#event.detail === "number" ? this.#event.detail : null);
1639
+ }
1640
+
1641
+ get composed() {
1642
+ return Boolean(this.#event.composed);
1643
+ }
1644
+
1645
+ // Legacy aliases of defaultPrevented and stopPropagation (DOM §2.2).
1646
+ get returnValue() {
1647
+ return !this.#event.defaultPrevented;
1648
+ }
1649
+
1650
+ set returnValue(value) {
1651
+ if (!value) this.preventDefault();
1652
+ }
1653
+
1654
+ get cancelBubble() {
1655
+ return Boolean(this.#event[Symbol.for("buninu.propagationStopped")]);
1656
+ }
1657
+
1658
+ set cancelBubble(value) {
1659
+ if (value) {
1660
+ this.#event[Symbol.for("buninu.propagationStopped")] = true;
1661
+ this.stopPropagation();
1662
+ }
1663
+ }
1664
+
1665
+ // UIEvent: the window for events the browser dispatches, null for constructed ones.
1666
+ get view() {
1667
+ return this.#realm.isTrusted(this.#event) ? this.#realm.hooks.targetOf?.(this.#realm.window) ?? null : null;
1668
+ }
1669
+
1670
+ get which() {
1671
+ if (typeof this.#event.keyCode === "number" && this.#event.keyCode) return this.#event.keyCode;
1672
+ return typeof this.#event.button === "number" ? this.#event.button + 1 : 0;
1673
+ }
1674
+
1675
+ // MouseEvent coordinates (CSSOM View §12): page = client + scroll; x/y alias client.
1676
+ get pageX() {
1677
+ return this.clientX + (this.#realm.hooks.viewport?.().scrollX ?? 0);
1678
+ }
1679
+
1680
+ get pageY() {
1681
+ return this.clientY + (this.#realm.hooks.viewport?.().scrollY ?? 0);
1682
+ }
1683
+
1684
+ get x() {
1685
+ return this.clientX;
1686
+ }
1687
+
1688
+ get y() {
1689
+ return this.clientY;
1690
+ }
1691
+
1692
+ get offsetX() {
1693
+ return this.#event.offsetX ?? 0;
1694
+ }
1695
+
1696
+ get offsetY() {
1697
+ return this.#event.offsetY ?? 0;
1698
+ }
1699
+
1700
+ get movementX() {
1701
+ return this.#event.movementX ?? 0;
1702
+ }
1703
+
1704
+ get movementY() {
1705
+ return this.#event.movementY ?? 0;
1706
+ }
1707
+
1708
+ get relatedTarget() {
1709
+ return this.#realm.wrap(this.#event.relatedTarget ?? null) ?? null;
1710
+ }
1711
+
1712
+ getModifierState(key) {
1713
+ const name = String(key);
1714
+ return name === "Alt" ? this.altKey : name === "Control" ? this.ctrlKey : name === "Shift" ? this.shiftKey
1715
+ : name === "Meta" ? this.metaKey : false;
1716
+ }
1717
+
1718
+ // Legacy initialisers (UI Events §B): only before dispatch, like initEvent.
1719
+ initUIEvent(type, bubbles = false, cancelable = false, _view = null, detail = 0) {
1720
+ this.initEvent(type, bubbles, cancelable);
1721
+ if (this.#detail === undefined) this.#detail = Number(detail) || 0;
1722
+ }
1723
+
1724
+ initMouseEvent(type, bubbles = false, cancelable = false, view = null, detail = 0) {
1725
+ this.initUIEvent(type, bubbles, cancelable, view, detail);
1726
+ }
1727
+
1728
+ // A plain Event has no detail of its own: assigning one (Polymer's fire()
1729
+ // does) makes an own property, as in browsers, instead of failing on the getter.
1730
+ set detail(value) {
1731
+ Object.defineProperty(this, "detail", { value, writable: true, configurable: true, enumerable: true });
1732
+ }
1733
+
1734
+ /** Legacy initialisation of an event from document.createEvent() (DOM §2.5); not once dispatched. */
1735
+ initEvent(type, bubbles = false, cancelable = false) {
1736
+ if (this.#event.eventPhase !== 0) return;
1737
+ this.#event.initEvent(String(type), Boolean(bubbles), Boolean(cancelable));
1738
+ }
1739
+
1740
+ initCustomEvent(type, bubbles = false, cancelable = false, detail = null) {
1741
+ if (this.#event.eventPhase !== 0) return;
1742
+ this.#event.initEvent(String(type), Boolean(bubbles), Boolean(cancelable));
1743
+ this.#detail = detail;
1157
1744
  }
1158
1745
 
1159
1746
  // PopStateEvent and HashChangeEvent fields; the engine only fires popstate
@@ -1237,7 +1824,26 @@ class EventBinding {
1237
1824
  }
1238
1825
 
1239
1826
  get data() {
1240
- return this.#event.data ?? null;
1827
+ const message = this.#realm.messageOf(this.#event);
1828
+ return message ? message.data : this.#event.data ?? null;
1829
+ }
1830
+
1831
+ // MessageEvent fields; undefined on other events, as in browsers.
1832
+ get origin() {
1833
+ return this.#realm.messageOf(this.#event)?.origin;
1834
+ }
1835
+
1836
+ get lastEventId() {
1837
+ return this.#realm.messageOf(this.#event)?.lastEventId;
1838
+ }
1839
+
1840
+ get source() {
1841
+ const message = this.#realm.messageOf(this.#event);
1842
+ return message ? message.source : undefined;
1843
+ }
1844
+
1845
+ get ports() {
1846
+ return this.#realm.messageOf(this.#event)?.ports;
1241
1847
  }
1242
1848
 
1243
1849
  get inputType() {
@@ -1516,50 +2122,152 @@ const UNRENDERED_STYLE_PROPERTIES = new Set([
1516
2122
  "viewTimelineAxis", "viewTimelineInset", "viewTimelineName",
1517
2123
  ]);
1518
2124
 
2125
+ /** Each element.style proxy → its element, declaration block and realm. */
2126
+ const styleStates = new WeakMap();
2127
+
2128
+ function styleState(self) {
2129
+ const state = styleStates.get(self);
2130
+ if (!state) throw new TypeError("Illegal invocation");
2131
+ return state;
2132
+ }
2133
+
1519
2134
  /**
1520
- * element.style: a proxy that reads and writes the real declaration block
1521
- * with string values only. Mutations go to the style attribute, which the
1522
- * style engine reads.
2135
+ * CSSStyleDeclaration (CSSOM §6.6) for one realm: the operations and the
2136
+ * cssText/length accessors live on its prototype, as in browsers, so
2137
+ * element.style.setProperty is the interface's (native-looking) method.
1523
2138
  */
1524
- function styleBinding(node, realm) {
1525
- const style = node.style;
1526
- const toKebab = (property) => property.startsWith("--") ? property
1527
- : property.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
1528
- const methods = {
1529
- getPropertyValue: (property) => style.getPropertyValue(String(property)),
1530
- setProperty: (property, value, priority = "") => {
2139
+ export function cssStyleDeclarationInterface(realm) {
2140
+ if (realm.cssStyleDeclaration) return realm.cssStyleDeclaration;
2141
+ const CSSStyleDeclaration = { CSSStyleDeclaration: function () { throw new TypeError("Illegal constructor"); } }.CSSStyleDeclaration;
2142
+ const members = {
2143
+ get cssText() { return styleState(this).style.cssText; },
2144
+ set cssText(value) {
2145
+ const { style, node, realm: owner } = styleState(this);
2146
+ const before = style.cssText;
2147
+ style.cssText = String(value);
2148
+ if (style.cssText !== before) owner.hooks.touch(node);
2149
+ },
2150
+ get length() { return styleState(this).style.length; },
2151
+ get parentRule() { return null; },
2152
+ get cssFloat() { return styleState(this).style.getPropertyValue("float"); },
2153
+ set cssFloat(value) { this.setProperty("float", value); },
2154
+ item(index) { return styleState(this).style.item?.(Number(index) >>> 0) ?? ""; },
2155
+ getPropertyValue(property) { return styleState(this).style.getPropertyValue(String(property)); },
2156
+ getPropertyPriority(property) { return styleState(this).style.getPropertyPriority?.(String(property)) ?? ""; },
2157
+ setProperty(property, value, priority = "") {
2158
+ const { style, node, realm: owner } = styleState(this);
1531
2159
  const before = style.cssText;
1532
2160
  style.setProperty(String(property), value === null ? "" : String(value), String(priority));
1533
- if (style.cssText !== before) realm.hooks.touch(node);
2161
+ if (style.cssText !== before) owner.hooks.touch(node);
1534
2162
  },
1535
- removeProperty: (property) => {
2163
+ removeProperty(property) {
2164
+ const { style, node, realm: owner } = styleState(this);
1536
2165
  const previous = style.removeProperty(String(property));
1537
- realm.hooks.touch(node);
2166
+ owner.hooks.touch(node);
1538
2167
  return previous;
1539
2168
  },
1540
2169
  };
1541
- return new Proxy(Object.create(null), {
1542
- get(_target, property) {
1543
- if (typeof property !== "string") return undefined;
1544
- if (Object.hasOwn(methods, property)) return methods[property];
1545
- if (property === "cssText") return style.cssText;
1546
- if (property === "length") return style.length;
2170
+ const prototype = {};
2171
+ for (const key of Reflect.ownKeys(members)) {
2172
+ Object.defineProperty(prototype, key, { ...Object.getOwnPropertyDescriptor(members, key), enumerable: true, configurable: true });
2173
+ }
2174
+ Object.defineProperty(prototype, "constructor", { value: CSSStyleDeclaration, writable: true, configurable: true });
2175
+ Object.defineProperty(prototype, Symbol.toStringTag, { value: "CSSStyleDeclaration", configurable: true });
2176
+ Object.defineProperty(CSSStyleDeclaration, "prototype", { value: prototype });
2177
+ realm.cssStyleDeclaration = CSSStyleDeclaration;
2178
+ return CSSStyleDeclaration;
2179
+ }
2180
+
2181
+ /**
2182
+ * element.style: a proxy over a CSSStyleDeclaration that reads and writes
2183
+ * the real declaration block, by property name, with string values only.
2184
+ * Mutations go to the style attribute, which the style engine reads.
2185
+ */
2186
+ function styleBinding(node, realm) {
2187
+ const style = node.style;
2188
+ const prototype = cssStyleDeclarationInterface(realm).prototype;
2189
+ const toKebab = (property) => property.startsWith("--") ? property
2190
+ : property.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
2191
+ const proxy = new Proxy(Object.create(prototype), {
2192
+ get(target, property, receiver) {
2193
+ if (typeof property !== "string" || property in prototype) return Reflect.get(target, property, receiver);
2194
+ if (/^(0|[1-9]\d*)$/.test(property)) return style.item?.(Number(property)) ?? undefined;
1547
2195
  return style.getPropertyValue(toKebab(property));
1548
2196
  },
1549
- set(_target, property, value) {
1550
- if (typeof property !== "string") return false;
2197
+ set(target, property, value, receiver) {
2198
+ if (typeof property !== "string" || property in prototype) return Reflect.set(target, property, value, receiver);
1551
2199
  const before = style.cssText;
1552
- if (property === "cssText") style.cssText = String(value);
1553
- else style.setProperty(toKebab(property), value === null ? "" : String(value));
2200
+ style.setProperty(toKebab(property), value === null ? "" : String(value));
1554
2201
  if (style.cssText !== before) realm.hooks.touch(node);
1555
2202
  return true;
1556
2203
  },
1557
2204
  has: (_target, property) => {
1558
- if (typeof property !== "string") return false;
2205
+ if (typeof property !== "string") return property in prototype;
1559
2206
  const camel = property.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
1560
- return Object.hasOwn(methods, property) || property === "cssText" || property === "length"
1561
- || (property in style && !UNRENDERED_STYLE_PROPERTIES.has(camel));
2207
+ return property in prototype || (property in style && !UNRENDERED_STYLE_PROPERTIES.has(camel));
2208
+ },
2209
+ defineProperty: () => false,
2210
+ deleteProperty: () => false,
2211
+ setPrototypeOf: () => false,
2212
+ });
2213
+ styleStates.set(proxy, { node, style, realm });
2214
+ return proxy;
2215
+ }
2216
+
2217
+ /**
2218
+ * element.attributes (DOM §4.9.1 NamedNodeMap): live, by index and by name;
2219
+ * its entries are the element's Attr views (see getAttributeNode).
2220
+ */
2221
+ function attributesBinding(owner) {
2222
+ const names = () => nodeOf(owner).getAttributeNames();
2223
+ const nameAt = (index) => names()[index] ?? null;
2224
+ const methods = {
2225
+ item: (index) => {
2226
+ const name = nameAt(Number(index) >>> 0);
2227
+ return name === null ? null : owner.getAttributeNode(name);
2228
+ },
2229
+ getNamedItem: (name) => owner.getAttributeNode(String(name)),
2230
+ getNamedItemNS: (_namespace, localName) => owner.getAttributeNode(String(localName)),
2231
+ removeNamedItem(name) {
2232
+ const attribute = owner.getAttributeNode(String(name));
2233
+ if (!attribute) throw new DOMException(`No attribute named '${name}'`, "NotFoundError");
2234
+ const removed = { name: attribute.name, value: attribute.value, nodeType: 2, specified: true, ownerElement: null };
2235
+ owner.removeAttribute(attribute.name);
2236
+ return removed;
2237
+ },
2238
+ setNamedItem(attribute) {
2239
+ const previous = owner.getAttributeNode(String(attribute?.name));
2240
+ owner.setAttribute(String(attribute?.name), String(attribute?.value ?? ""));
2241
+ return previous;
2242
+ },
2243
+ *[Symbol.iterator]() {
2244
+ for (const name of names()) yield owner.getAttributeNode(name);
2245
+ },
2246
+ };
2247
+ const index = (property) => typeof property === "string" && /^(0|[1-9]\d*)$/.test(property) ? Number(property) : null;
2248
+ return new Proxy(Object.create(null), {
2249
+ get(_target, property) {
2250
+ if (property === "length") return names().length;
2251
+ if (Object.hasOwn(methods, property)) return methods[property];
2252
+ if (property === Symbol.toStringTag) return "NamedNodeMap";
2253
+ if (typeof property !== "string") return undefined;
2254
+ const at = index(property);
2255
+ if (at !== null) return methods.item(at);
2256
+ return nodeOf(owner).hasAttribute(property) ? owner.getAttributeNode(property) : undefined;
2257
+ },
2258
+ has(_target, property) {
2259
+ if (property === "length" || Object.hasOwn(methods, property)) return true;
2260
+ const at = index(property);
2261
+ return at !== null ? at < names().length : typeof property === "string" && nodeOf(owner).hasAttribute(property);
2262
+ },
2263
+ ownKeys: () => [...names().map((_name, at) => String(at)), ...names()],
2264
+ getOwnPropertyDescriptor(_target, property) {
2265
+ const at = index(property);
2266
+ const value = at !== null ? methods.item(at)
2267
+ : typeof property === "string" && nodeOf(owner).hasAttribute(property) ? owner.getAttributeNode(property) : null;
2268
+ return value ? { value, writable: false, enumerable: at !== null, configurable: true } : undefined;
1562
2269
  },
2270
+ set: () => false,
1563
2271
  defineProperty: () => false,
1564
2272
  deleteProperty: () => false,
1565
2273
  getPrototypeOf: () => null,
@@ -1601,16 +2309,38 @@ function datasetBinding(node, realm) {
1601
2309
  });
1602
2310
  }
1603
2311
 
2312
+ /**
2313
+ * Flattens addEventListener's options (DOM §2.7 "flatten more"): the
2314
+ * dictionary's members are read once each, in IDL order (capture, then
2315
+ * once, passive and signal), before anything else happens, as pages detect
2316
+ * support for an option by watching its getter run.
2317
+ */
2318
+ function listenerOptions(options) {
2319
+ if (typeof options !== "object" || options === null) {
2320
+ return { capture: Boolean(options), once: false, passive: null, signal: null };
2321
+ }
2322
+ const capture = Boolean(options.capture);
2323
+ const once = Boolean(options.once);
2324
+ const passiveValue = options.passive;
2325
+ const signal = options.signal;
2326
+ if (signal !== undefined && !(signal instanceof AbortSignal)) {
2327
+ throw new TypeError("Failed to read the 'signal' property from 'AddEventListenerOptions': Failed to convert value to 'AbortSignal'.");
2328
+ }
2329
+ return { capture, once, passive: passiveValue === undefined ? null : Boolean(passiveValue), signal: signal ?? null };
2330
+ }
2331
+
1604
2332
  function addListener(wrapper, target, type, listener, options) {
2333
+ const { capture, once, signal } = listenerOptions(options);
1605
2334
  if (typeof listener !== "function" && (listener === null || typeof listener !== "object")) return;
2335
+ if (signal?.aborted) return;
1606
2336
  const realm = realmOf(wrapper) ?? wrapper;
1607
- const capture = typeof options === "boolean" ? options : Boolean(options?.capture);
1608
- const once = typeof options === "object" && options !== null && Boolean(options.once);
1609
2337
  const listeners = realm.listenerFor(target, String(type), listener, capture);
1610
2338
  if (listeners.has(listener)) return;
2339
+ // An aborted signal removes the listener (DOM §2.7 "add an event listener", step 6).
2340
+ signal?.addEventListener("abort", () => removeListener(wrapper, target, type, listener, capture), { once: true });
1611
2341
  const host = (event) => {
1612
2342
  if (realm.hooks.acceptEvent && !realm.hooks.acceptEvent(event)) return;
1613
- if (once) removeListener(wrapper, target, type, listener, options);
2343
+ if (once) removeListener(wrapper, target, type, listener, capture);
1614
2344
  const binding = realm.wrapEvent(event);
1615
2345
  const thisArg = realm.wrap(event.currentTarget) ?? wrapper;
1616
2346
  if (typeof listener === "function") realm.hooks.call(listener, thisArg, [binding]);
@@ -1622,7 +2352,7 @@ function addListener(wrapper, target, type, listener, options) {
1622
2352
 
1623
2353
  function removeListener(wrapper, target, type, listener, options) {
1624
2354
  const realm = realmOf(wrapper) ?? wrapper;
1625
- const capture = typeof options === "boolean" ? options : Boolean(options?.capture);
2355
+ const capture = typeof options === "object" && options !== null ? Boolean(options.capture) : Boolean(options);
1626
2356
  const listeners = realm.listenerFor(target, String(type), listener, capture);
1627
2357
  const host = listeners.get(listener);
1628
2358
  if (!host) return;
@@ -1645,16 +2375,102 @@ export function windowEventMethods(realm, pageGlobal) {
1645
2375
  };
1646
2376
  }
1647
2377
 
2378
+ /**
2379
+ * Range (DOM §5.5) over an engine Range: boundary points and the content
2380
+ * operations; nodes go in and out wrapped. Its client rects are those of the
2381
+ * smallest element containing it.
2382
+ */
2383
+ class RangeBinding {
2384
+ static START_TO_START = 0;
2385
+ static START_TO_END = 1;
2386
+ static END_TO_END = 2;
2387
+ static END_TO_START = 3;
2388
+
2389
+ #range;
2390
+ #realm;
2391
+
2392
+ constructor(key, range, realm) {
2393
+ if (key !== CONSTRUCT) throw new TypeError("Illegal constructor");
2394
+ this.#range = range;
2395
+ this.#realm = realm;
2396
+ }
2397
+
2398
+ static rangeOf(value) {
2399
+ if (value === null || typeof value !== "object" || !(#range in value)) throw new TypeError("parameter is not of type 'Range'");
2400
+ return value.#range;
2401
+ }
2402
+
2403
+ get startContainer() { return this.#realm.wrap(this.#range.startContainer); }
2404
+ get startOffset() { return this.#range.startOffset; }
2405
+ get endContainer() { return this.#realm.wrap(this.#range.endContainer); }
2406
+ get endOffset() { return this.#range.endOffset; }
2407
+ get collapsed() { return this.#range.collapsed; }
2408
+ get commonAncestorContainer() { return this.#realm.wrap(this.#range.commonAncestorContainer); }
2409
+
2410
+ setStart(node, offset) { this.#range.setStart(requireNode(node), Number(offset) >>> 0); }
2411
+ setEnd(node, offset) { this.#range.setEnd(requireNode(node), Number(offset) >>> 0); }
2412
+ setStartBefore(node) { this.#range.setStartBefore(requireNode(node)); }
2413
+ setStartAfter(node) { this.#range.setStartAfter(requireNode(node)); }
2414
+ setEndBefore(node) { this.#range.setEndBefore(requireNode(node)); }
2415
+ setEndAfter(node) { this.#range.setEndAfter(requireNode(node)); }
2416
+ selectNode(node) { this.#range.selectNode(requireNode(node)); }
2417
+ selectNodeContents(node) { this.#range.selectNodeContents(requireNode(node)); }
2418
+ collapse(toStart = false) { this.#range.collapse(Boolean(toStart)); }
2419
+ detach() {}
2420
+
2421
+ cloneRange() { return this.#realm.wrapRange(this.#range.cloneRange()); }
2422
+ compareBoundaryPoints(how, other) { return this.#range.compareBoundaryPoints(Number(how), RangeBinding.rangeOf(other)); }
2423
+ comparePoint(node, offset) { return this.#range.comparePoint(requireNode(node), Number(offset) >>> 0); }
2424
+ isPointInRange(node, offset) { return this.#range.isPointInRange(requireNode(node), Number(offset) >>> 0); }
2425
+ intersectsNode(node) { return this.#range.intersectsNode(requireNode(node)); }
2426
+
2427
+ cloneContents() { return this.#realm.wrap(this.#range.cloneContents()); }
2428
+ extractContents() { return this.#realm.wrap(this.#range.extractContents()); }
2429
+ deleteContents() { this.#range.deleteContents(); }
2430
+ insertNode(node) { this.#range.insertNode(requireNode(node)); }
2431
+ surroundContents(node) { this.#range.surroundContents(requireNode(node)); }
2432
+ createContextualFragment(html) { return this.#realm.wrap(this.#range.createContextualFragment(String(html))); }
2433
+
2434
+ getBoundingClientRect() {
2435
+ let node = this.#range.commonAncestorContainer;
2436
+ if (node && node.nodeType !== 1) node = node.parentElement;
2437
+ const rect = node ? this.#realm.hooks.boundsOf(node) : null;
2438
+ return domRect(rect ?? { left: 0, top: 0, width: 0, height: 0 });
2439
+ }
2440
+
2441
+ getClientRects() { return [this.getBoundingClientRect()]; }
2442
+ toString() { return this.#range.toString(); }
2443
+ }
2444
+
2445
+ /** Page-constructible Range (`new Range()`), its prototype shared by document.createRange()'s. */
2446
+ export function rangeConstructor(realm) {
2447
+ function Range() {
2448
+ if (!new.target) throw new TypeError("Constructor Range requires 'new'");
2449
+ return realm.wrapRange(new realm.window.Range());
2450
+ }
2451
+ realm.rangePrototype = mutableBindingPrototype(RangeBinding.prototype);
2452
+ Object.defineProperty(realm.rangePrototype, "constructor", { value: Range, writable: true, configurable: true });
2453
+ Range.prototype = realm.rangePrototype;
2454
+ for (const name of ["START_TO_START", "START_TO_END", "END_TO_END", "END_TO_START"]) {
2455
+ Object.defineProperty(Range, name, { value: RangeBinding[name], enumerable: true });
2456
+ Object.defineProperty(realm.rangePrototype, name, { value: RangeBinding[name], enumerable: true });
2457
+ }
2458
+ return Range;
2459
+ }
2460
+
1648
2461
  /**
1649
2462
  * Page-constructible EventTarget (`new EventTarget()` and subclasses). Each
1650
2463
  * instance is backed by its own Happy DOM EventTarget; `targetOf` maps that
1651
2464
  * back to the page object for event.target and the listener's `this`.
2465
+ * EventTarget.prototype's methods work on every event target, as in browsers
2466
+ * (Node.prototype and Window.prototype inherit them): `targetOfPage` gives the
2467
+ * engine target behind a node or the window.
1652
2468
  */
1653
- export function eventTargetConstructor(realm, isTarget) {
2469
+ export function eventTargetConstructor(realm, isTarget, targetOfPage = () => null) {
1654
2470
  const backing = new WeakMap();
1655
2471
  const owners = new WeakMap();
1656
2472
  const realOf = (self) => {
1657
- const real = backing.get(self);
2473
+ const real = backing.get(self) ?? targetOfPage(self);
1658
2474
  if (!real) throw new TypeError("Illegal invocation");
1659
2475
  return real;
1660
2476
  };
@@ -1668,10 +2484,10 @@ export function eventTargetConstructor(realm, isTarget) {
1668
2484
  }
1669
2485
  EventTarget.prototype = {
1670
2486
  constructor: EventTarget,
1671
- addEventListener(type, listener, options) {
2487
+ addEventListener(type, listener, options = undefined) {
1672
2488
  addListener(realm, realOf(this), type, listener, options);
1673
2489
  },
1674
- removeEventListener(type, listener, options) {
2490
+ removeEventListener(type, listener, options = undefined) {
1675
2491
  removeListener(realm, realOf(this), type, listener, options);
1676
2492
  },
1677
2493
  dispatchEvent(event) {
@@ -1688,6 +2504,30 @@ export function eventTargetConstructor(realm, isTarget) {
1688
2504
  return { EventTarget, targetOf: (real) => owners.get(real) ?? null };
1689
2505
  }
1690
2506
 
2507
+ const EVENT_PHASES = Object.freeze({ NONE: 0, CAPTURING_PHASE: 1, AT_TARGET: 2, BUBBLING_PHASE: 3 });
2508
+ const MODIFIERS = ["altKey", "ctrlKey", "shiftKey", "metaKey", "getModifierState"];
2509
+ const MOUSE_MEMBERS = ["screenX", "screenY", "clientX", "clientY", "pageX", "pageY", "x", "y", "offsetX", "offsetY",
2510
+ "movementX", "movementY", "button", "buttons", "relatedTarget", "initMouseEvent", ...MODIFIERS];
2511
+ /** Event interface → [parent interface, members it owns], parents first. */
2512
+ const EVENT_INTERFACES = {
2513
+ CustomEvent: ["Event", ["detail", "initCustomEvent"]],
2514
+ UIEvent: ["Event", ["detail", "view", "which", "initUIEvent"]],
2515
+ MouseEvent: ["UIEvent", MOUSE_MEMBERS],
2516
+ PointerEvent: ["MouseEvent", ["pointerType"]],
2517
+ WheelEvent: ["MouseEvent", ["deltaX", "deltaY", "deltaMode"]],
2518
+ KeyboardEvent: ["UIEvent", ["key", "code", "repeat", ...MODIFIERS]],
2519
+ FocusEvent: ["UIEvent", ["relatedTarget"]],
2520
+ InputEvent: ["UIEvent", ["data", "inputType"]],
2521
+ MessageEvent: ["Event", ["data", "origin", "lastEventId", "source", "ports"]],
2522
+ PopStateEvent: ["Event", ["state"]],
2523
+ HashChangeEvent: ["Event", ["oldURL", "newURL"]],
2524
+ };
2525
+ /** document.createEvent() interface names (DOM §4.5): legacy aliases included. */
2526
+ const CREATE_EVENT_TYPES = new Map([["event", "Event"], ["events", "Event"], ["htmlevents", "Event"],
2527
+ ["customevent", "CustomEvent"], ["uievent", "UIEvent"], ["uievents", "UIEvent"], ["mouseevent", "MouseEvent"],
2528
+ ["mouseevents", "MouseEvent"], ["keyboardevent", "KeyboardEvent"], ["focusevent", "FocusEvent"],
2529
+ ["messageevent", "MessageEvent"], ["hashchangeevent", "HashChangeEvent"], ["popstateevent", "PopStateEvent"]]);
2530
+
1691
2531
  const UI_EVENT_TYPES = ["UIEvent", "MouseEvent", "PointerEvent", "WheelEvent", "KeyboardEvent", "FocusEvent", "InputEvent"];
1692
2532
  const UI_EVENT_FIELDS = [
1693
2533
  ...["detail", "clientX", "clientY", "screenX", "screenY", "button", "buttons", "deltaX", "deltaY", "deltaZ", "deltaMode",
@@ -1714,8 +2554,32 @@ export function eventConstructors(realm) {
1714
2554
  return realm.wrapEvent(new window.CustomEvent(String(type), init(options)), options?.detail);
1715
2555
  }
1716
2556
  realm.eventPrototype = mutableBindingPrototype(EventBinding.prototype);
2557
+ Object.defineProperty(realm.eventPrototype, "constructor", { value: Event, writable: true, configurable: true });
1717
2558
  Event.prototype = realm.eventPrototype;
1718
- CustomEvent.prototype = realm.eventPrototype;
2559
+ for (const [name, value] of Object.entries(EVENT_PHASES)) {
2560
+ Object.defineProperty(Event, name, { value, enumerable: true });
2561
+ Object.defineProperty(realm.eventPrototype, name, { value, enumerable: true });
2562
+ }
2563
+ // The event interfaces (UI Events, HTML): each its own prototype, with its
2564
+ // members moved off the shared binding prototype onto it.
2565
+ realm.eventPrototypes = new Map([["Event", realm.eventPrototype]]);
2566
+ const moved = new Set();
2567
+ for (const [name, [parentName, members]] of Object.entries(EVENT_INTERFACES)) {
2568
+ const prototype = Object.create(realm.eventPrototypes.get(parentName));
2569
+ for (const member of members) {
2570
+ const descriptor = Object.getOwnPropertyDescriptor(realm.eventPrototype, member);
2571
+ if (!descriptor) continue;
2572
+ Object.defineProperty(prototype, member, descriptor);
2573
+ moved.add(member);
2574
+ }
2575
+ Object.defineProperty(prototype, Symbol.toStringTag, { value: name, configurable: true });
2576
+ realm.eventPrototypes.set(name, prototype);
2577
+ }
2578
+ for (const member of moved) delete realm.eventPrototype[member];
2579
+ // An own property of each event instead (see RealmBindings.wrapEvent).
2580
+ delete realm.eventPrototype.isTrusted;
2581
+ CustomEvent.prototype = realm.eventPrototypes.get("CustomEvent");
2582
+ Object.defineProperty(CustomEvent.prototype, "constructor", { value: CustomEvent, writable: true, configurable: true });
1719
2583
  // UI event subclasses: their init dictionaries are copied as primitives only
1720
2584
  // (view and relatedTarget are dropped).
1721
2585
  const uiEvents = {};
@@ -1731,10 +2595,46 @@ export function eventConstructors(realm) {
1731
2595
  return realm.wrapEvent(new RealEvent(String(type), fields));
1732
2596
  };
1733
2597
  Object.defineProperty(Constructor, "name", { value: name });
1734
- Constructor.prototype = realm.eventPrototype;
2598
+ Constructor.prototype = realm.eventPrototypes.get(name) ?? realm.eventPrototype;
2599
+ Object.defineProperty(Constructor.prototype, "constructor", { value: Constructor, writable: true, configurable: true });
1735
2600
  uiEvents[name] = Constructor;
1736
2601
  }
1737
- return { Event, CustomEvent, ...uiEvents };
2602
+ function MessageEvent(type, options = undefined) {
2603
+ if (!new.target) throw new TypeError("Constructor MessageEvent requires 'new'");
2604
+ return realm.messageEvent(type, {
2605
+ data: options?.data ?? null,
2606
+ origin: options?.origin ?? "",
2607
+ lastEventId: options?.lastEventId ?? "",
2608
+ source: options?.source ?? null,
2609
+ ports: Array.isArray(options?.ports) ? options.ports : [],
2610
+ }, init(options));
2611
+ }
2612
+ MessageEvent.prototype = realm.eventPrototypes.get("MessageEvent");
2613
+ Object.defineProperty(MessageEvent.prototype, "constructor", { value: MessageEvent, writable: true, configurable: true });
2614
+ // PromiseRejectionEvent (HTML §8.1.5.7): promise and reason stay page values in the binding.
2615
+ const rejections = new WeakMap();
2616
+ function PromiseRejectionEvent(type, options) {
2617
+ if (!new.target) throw new TypeError("Failed to construct 'PromiseRejectionEvent': Please use the 'new' operator, this DOM object constructor cannot be called as a function.");
2618
+ if (arguments.length < 2) {
2619
+ throw new TypeError(`Failed to construct 'PromiseRejectionEvent': 2 arguments required, but only ${arguments.length} present.`);
2620
+ }
2621
+ if (options?.promise === undefined) {
2622
+ throw new TypeError("Failed to construct 'PromiseRejectionEvent': Failed to read the 'promise' property from 'PromiseRejectionEventInit': Required member is undefined.");
2623
+ }
2624
+ const wrapper = realm.wrapEvent(new window.Event(String(type), init(options)));
2625
+ Object.setPrototypeOf(wrapper, PromiseRejectionEvent.prototype);
2626
+ rejections.set(wrapper, { promise: options.promise, reason: options.reason });
2627
+ return wrapper;
2628
+ }
2629
+ PromiseRejectionEvent.prototype = Object.create(realm.eventPrototype, {
2630
+ constructor: { value: PromiseRejectionEvent, writable: true, configurable: true },
2631
+ promise: { get: Object.getOwnPropertyDescriptor({ get promise() { return rejections.get(this)?.promise; } }, "promise").get,
2632
+ enumerable: true, configurable: true },
2633
+ reason: { get: Object.getOwnPropertyDescriptor({ get reason() { return rejections.get(this)?.reason; } }, "reason").get,
2634
+ enumerable: true, configurable: true },
2635
+ [Symbol.toStringTag]: { value: "PromiseRejectionEvent", configurable: true },
2636
+ });
2637
+ return { Event, CustomEvent, MessageEvent, PromiseRejectionEvent, ...uiEvents };
1738
2638
  }
1739
2639
 
1740
2640
  function domRect({ left, top, width, height }) {
@@ -1755,12 +2655,29 @@ function cssEscape(value) {
1755
2655
  * Freezes the binding classes so no page can change the shared prototypes.
1756
2656
  * Must run after SES lockdown() (which provides `harden`).
1757
2657
  */
2658
+ const BINDINGS = [NodeBinding, CharacterDataBinding, TextBinding, ParentBinding, FragmentBinding, ShadowRootBinding, ElementBinding,
2659
+ DocumentBinding, EventBinding, ClassListBinding, TreeWalkerBinding, RangeBinding];
2660
+
2661
+ // Members defined with Object.defineProperty({ get() {...} }) are named
2662
+ // "get"; Web IDL names an attribute's accessors "get x" / "set x" and an
2663
+ // operation after itself. Named here, while the functions are still mutable.
2664
+ for (const Binding of BINDINGS) {
2665
+ for (const key of Reflect.ownKeys(Binding.prototype)) {
2666
+ if (key === "constructor") continue;
2667
+ const name = typeof key === "symbol" ? `[${key.description ?? ""}]` : key;
2668
+ const descriptor = Object.getOwnPropertyDescriptor(Binding.prototype, key);
2669
+ for (const [fn, expected] of [[descriptor.get, `get ${name}`], [descriptor.set, `set ${name}`], [descriptor.value, name]]) {
2670
+ if (typeof fn !== "function" || fn.name === expected) continue;
2671
+ if (Object.getOwnPropertyDescriptor(fn, "name")?.configurable !== false) {
2672
+ Object.defineProperty(fn, "name", { value: expected, configurable: true });
2673
+ }
2674
+ }
2675
+ }
2676
+ }
2677
+
1758
2678
  export function hardenBindings() {
1759
2679
  if (typeof harden !== "function") return;
1760
- for (const Binding of [NodeBinding, CharacterDataBinding, TextBinding, ParentBinding, FragmentBinding, ShadowRootBinding, ElementBinding,
1761
- DocumentBinding, EventBinding, ClassListBinding, TreeWalkerBinding]) {
1762
- harden(Binding);
1763
- }
2680
+ for (const Binding of BINDINGS) harden(Binding);
1764
2681
  }
1765
2682
 
1766
2683
  /** Interface objects for `instanceof` checks; all throw "Illegal constructor" when called. */