buninu-browser 0.2.3 → 0.2.4

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 (105) hide show
  1. package/lib/cdp-server.js +47 -14
  2. package/lib/happy-dom/document-base.js +33 -0
  3. package/lib/happy-dom/parser.js +15 -6
  4. package/lib/happy-dom/selector-matcher.js +2 -2
  5. package/lib/happy-dom/selector-pseudo-adapter.js +19 -0
  6. package/lib/happy-dom/svg-attribute-adapter.js +50 -0
  7. package/lib/headless-shell.js +9 -6
  8. package/lib/input/hit-test.js +67 -19
  9. package/lib/layout/text-layout.js +213 -91
  10. package/lib/media/byte-pipe.js +2 -2
  11. package/lib/media/position-audio.js +10 -1
  12. package/lib/media/source-snapshots.js +126 -0
  13. package/lib/media/video-keyframe.js +17 -4
  14. package/lib/media/webm-stream.js +106 -0
  15. package/lib/network/network-service.js +268 -120
  16. package/lib/paint/display-list.js +8 -28
  17. package/lib/paint/frame-cache.js +25 -0
  18. package/lib/paint/frame.js +42 -13
  19. package/lib/paint/paragraph-text.js +24 -0
  20. package/lib/paint/raster-readback.js +41 -0
  21. package/lib/paint/rounded-rect.js +82 -0
  22. package/lib/paint/shared-frame-pool.js +105 -0
  23. package/lib/render-tree/index.js +38 -6
  24. package/lib/render-tree/svg.js +15 -7
  25. package/lib/renderer/bindings.js +233 -80
  26. package/lib/renderer/broadcast-channel.js +76 -0
  27. package/lib/renderer/canvas-2d.js +257 -14
  28. package/lib/renderer/canvas-text.js +120 -0
  29. package/lib/renderer/compositor.js +13 -11
  30. package/lib/renderer/custom-element-reactions.js +25 -1
  31. package/lib/renderer/font-loading.js +227 -0
  32. package/lib/renderer/host.js +28 -4
  33. package/lib/renderer/image-resources.js +65 -0
  34. package/lib/renderer/interfaces.js +28 -0
  35. package/lib/renderer/layout-observers.js +236 -0
  36. package/lib/renderer/main.js +30 -4
  37. package/lib/renderer/media-source.js +126 -55
  38. package/lib/renderer/page-realm.js +270 -98
  39. package/lib/renderer/page-renderer.js +647 -235
  40. package/lib/renderer/prepared-script-cache.js +50 -0
  41. package/lib/renderer/rendered-text.js +108 -0
  42. package/lib/renderer/rewrite-worker.js +4 -3
  43. package/lib/renderer/script-rewrite.js +13 -2
  44. package/lib/renderer/scroll-into-view.js +28 -0
  45. package/lib/renderer/slot-assignment.js +60 -0
  46. package/lib/renderer/video-presentation.js +23 -4
  47. package/lib/renderer/workers.js +2 -2
  48. package/lib/style/computed-style.js +611 -104
  49. package/lib/style/selector-traits.js +49 -5
  50. package/package.json +1 -1
  51. package/test/broadcast-channel.test.js +26 -0
  52. package/test/canvas-2d.test.js +83 -2
  53. package/test/canvas-paint.test.js +9 -0
  54. package/test/canvas-text.test.js +41 -0
  55. package/test/cdp-stream.test.js +56 -0
  56. package/test/computed-style-binding.test.js +151 -0
  57. package/test/computed-style.test.js +258 -1
  58. package/test/display-list.test.js +12 -0
  59. package/test/document-base.test.js +87 -0
  60. package/test/dom-platform.test.js +461 -0
  61. package/test/fetch-lifecycle.test.js +161 -0
  62. package/test/fixtures/canvas-paint.js +117 -0
  63. package/test/fixtures/canvas-text.js +42 -0
  64. package/test/fixtures/image-lifetime.js +90 -0
  65. package/test/fixtures/media-seek-snapshot.js +77 -0
  66. package/test/fixtures/mse-audio-chirp.webm +0 -0
  67. package/test/fixtures/mse-switch-large.mp4 +0 -0
  68. package/test/fixtures/mse-switch-small.mp4 +0 -0
  69. package/test/fixtures/renderer-memory.js +110 -0
  70. package/test/fixtures/summary-style.js +28 -0
  71. package/test/font-loading.test.js +97 -0
  72. package/test/grid.test.js +58 -0
  73. package/test/hit-test.test.js +21 -1
  74. package/test/image-lifetime.test.js +12 -0
  75. package/test/image-resources.test.js +94 -0
  76. package/test/local-media.test.js +84 -0
  77. package/test/media-seek-snapshot.test.js +11 -0
  78. package/test/media-source-config.test.js +98 -0
  79. package/test/media-source-webm-accounting.test.js +98 -0
  80. package/test/media-source-webm.test.js +108 -0
  81. package/test/navigation.test.js +1 -0
  82. package/test/network-service.test.js +388 -0
  83. package/test/page-scripts.test.js +1 -1
  84. package/test/paint-frame.test.js +103 -0
  85. package/test/paint-paragraph-cache.test.js +87 -0
  86. package/test/position-audio.test.js +24 -0
  87. package/test/positioning.test.js +22 -0
  88. package/test/prepared-script-cache.test.js +45 -0
  89. package/test/pulse-client.test.js +10 -3
  90. package/test/raster-readback.test.js +40 -0
  91. package/test/render-tree.test.js +28 -0
  92. package/test/renderer-memory.test.js +11 -0
  93. package/test/selector-traits.test.js +24 -0
  94. package/test/shared-frame-pool.test.js +63 -0
  95. package/test/style-sheets.test.js +51 -0
  96. package/test/svg.test.js +29 -0
  97. package/test/text-layout.test.js +106 -0
  98. package/test/video-keyframe.test.js +61 -0
  99. package/test/video-presentation.test.js +69 -0
  100. package/tools/browser-lab.js +78 -0
  101. package/tools/browser-soak.js +75 -0
  102. package/tools/network-reference.js +49 -0
  103. package/tools/process-memory.js +38 -0
  104. package/tools/script-benchmark.js +25 -0
  105. package/ai-debug.log +0 -5537
@@ -10,8 +10,16 @@
10
10
  *
11
11
  * Coverage is the commonly used subset of DOM, not the full WebIDL surface.
12
12
  */
13
+ import { renderedText, setRenderedText } from "./rendered-text.js";
14
+ import { assignedSlot, slotAssignedNodes, assignSlotNodes } from "./slot-assignment.js";
15
+ import { scrollIntoViewOptions, scrollAlignmentDelta } from "./scroll-into-view.js";
16
+ import { documentBaseURL } from "../happy-dom/document-base.js";
17
+ import { resetCanvas } from "./canvas-2d.js";
13
18
 
14
19
  const CONSTRUCT = Symbol("binding construction key");
20
+ const isCanvasSizeAttribute = (node, name, namespace = null) => !namespace
21
+ && node.namespaceURI === "http://www.w3.org/1999/xhtml" && node.tagName === "CANVAS"
22
+ && (name === "width" || name === "height");
15
23
  const REFLECTED_PROPERTIES = [
16
24
  "value", "checked", "disabled", "type", "name", "href", "src", "alt", "title",
17
25
  "placeholder", "selected", "readOnly", "required", "min", "max", "step", "rel", "target",
@@ -42,6 +50,9 @@ export class BindingRealm {
42
50
  textPrototype = null;
43
51
  characterDataPrototype = null;
44
52
  nodePrototype = null;
53
+ datasetPrototype = null;
54
+ wrapValidity = null;
55
+ documentPrototype = null;
45
56
 
46
57
  /**
47
58
  * @param {object} window the Happy DOM window of the document
@@ -88,6 +99,7 @@ export class BindingRealm {
88
99
  if (Binding === TextBinding && this.textPrototype) Object.setPrototypeOf(wrapper, this.textPrototype);
89
100
  if (Binding === CharacterDataBinding && this.characterDataPrototype) Object.setPrototypeOf(wrapper, this.characterDataPrototype);
90
101
  if (Binding === NodeBinding && this.nodePrototype) Object.setPrototypeOf(wrapper, this.nodePrototype);
102
+ if (Binding === DocumentBinding && this.documentPrototype) Object.setPrototypeOf(wrapper, this.documentPrototype);
91
103
  this.#wrappers.set(node, wrapper);
92
104
  }
93
105
  return wrapper;
@@ -210,7 +222,7 @@ export class BindingRealm {
210
222
  if (!wrapper) {
211
223
  wrapper = new EventBinding(CONSTRUCT, event, this, detail);
212
224
  if (this.eventPrototype) {
213
- const prototype = this.eventPrototypes?.get(event.constructor?.name) ?? this.eventPrototype;
225
+ const prototype = this.eventPrototypes?.get(this.#messages.has(event) ? "MessageEvent" : event.constructor?.name) ?? this.eventPrototype;
214
226
  Object.setPrototypeOf(wrapper, prototype);
215
227
  // isTrusted is [LegacyUnforgeable]: an own accessor of every event.
216
228
  const isTrusted = Object.getOwnPropertyDescriptor(EventBinding.prototype, "isTrusted");
@@ -348,15 +360,7 @@ class NodeBinding {
348
360
 
349
361
  /** The document base URL (HTML §2.4.1): the first <base href>, else the document's URL. */
350
362
  get baseURI() {
351
- const document = this.#node.nodeType === 9 ? this.#node : this.#node.ownerDocument;
352
- const url = document?.URL ?? "about:blank";
353
- const href = document?.querySelector?.("base[href]")?.getAttribute("href");
354
- if (href == null) return url;
355
- try {
356
- return new URL(href, url).href;
357
- } catch {
358
- return url;
359
- }
363
+ return documentBaseURL(this.#node);
360
364
  }
361
365
 
362
366
  get nodeValue() {
@@ -554,7 +558,9 @@ class CharacterDataBinding extends NodeBinding {
554
558
  }
555
559
  }
556
560
 
557
- class TextBinding extends CharacterDataBinding {}
561
+ class TextBinding extends CharacterDataBinding {
562
+ get assignedSlot() { return realmOf(this).wrap(assignedSlot(nodeOf(this))); }
563
+ }
558
564
 
559
565
  /** Methods shared by elements, documents and fragments (ParentNode). */
560
566
  class ParentBinding extends NodeBinding {
@@ -635,6 +641,9 @@ class ShadowRootBinding extends FragmentBinding {
635
641
  return nodeOf(this).mode;
636
642
  }
637
643
 
644
+ get delegatesFocus() { return nodeOf(this).delegatesFocus; }
645
+ get slotAssignment() { return nodeOf(this).slotAssignment; }
646
+
638
647
  get innerHTML() {
639
648
  return nodeOf(this).innerHTML;
640
649
  }
@@ -650,20 +659,49 @@ class ElementBinding extends ParentBinding {
650
659
  #classList = null;
651
660
  #dataset = null;
652
661
  #attributes = null;
662
+ #validity = null;
663
+ #clickInProgress = false;
653
664
 
654
665
  get tagName() {
655
666
  return nodeOf(this).tagName;
656
667
  }
657
668
 
658
669
  attachShadow(init) {
659
- const mode = init?.mode === "closed" ? "closed" : "open";
660
- return realmOf(this).wrap(nodeOf(this).attachShadow({ mode }));
670
+ const mode = String(init?.mode);
671
+ if (mode !== "open" && mode !== "closed") throw new TypeError("Shadow root mode must be open or closed");
672
+ const slotAssignment = String(init?.slotAssignment ?? "named");
673
+ if (slotAssignment !== "named" && slotAssignment !== "manual") throw new TypeError("Invalid slot assignment mode");
674
+ // Happy DOM spells this internal option delegateFocus (without the s).
675
+ return realmOf(this).wrap(nodeOf(this).attachShadow({ mode, slotAssignment, delegateFocus: Boolean(init?.delegatesFocus) }));
661
676
  }
662
677
 
663
678
  get shadowRoot() {
664
679
  return realmOf(this).wrap(nodeOf(this).shadowRoot);
665
680
  }
666
681
 
682
+ get assignedSlot() { return realmOf(this).wrap(assignedSlot(nodeOf(this))); }
683
+
684
+ get slot() { return nodeOf(this).getAttribute("slot") ?? ""; }
685
+ set slot(value) { this.setAttribute("slot", String(value)); }
686
+
687
+ assignedNodes(options = {}) {
688
+ const realm = realmOf(this);
689
+ return slotAssignedNodes(nodeOf(this), Boolean(options?.flatten)).map(node => realm.wrap(node));
690
+ }
691
+
692
+ assignedElements(options = {}) {
693
+ const realm = realmOf(this);
694
+ return slotAssignedNodes(nodeOf(this), Boolean(options?.flatten)).filter(node => node.nodeType === 1).map(node => realm.wrap(node));
695
+ }
696
+
697
+ assign(...nodes) {
698
+ const realm = realmOf(this);
699
+ assignSlotNodes(nodeOf(this), nodes.map(node => nodeOf(node)), slot => {
700
+ queueMicrotask(() => slot.dispatchEvent(new realm.window.Event("slotchange", { bubbles: true })));
701
+ });
702
+ touch(this);
703
+ }
704
+
667
705
  // HTMLIFrameElement: its document's window and, when same-origin, its
668
706
  // document (see messaging.js).
669
707
  get contentWindow() {
@@ -747,6 +785,9 @@ class ElementBinding extends ParentBinding {
747
785
  return nodeOf(this).localName;
748
786
  }
749
787
 
788
+ get namespaceURI() { return nodeOf(this).namespaceURI; }
789
+ get prefix() { return nodeOf(this).prefix; }
790
+
750
791
  get id() {
751
792
  return nodeOf(this).id;
752
793
  }
@@ -839,18 +880,23 @@ class ElementBinding extends ParentBinding {
839
880
  const attribute = element.getAttributeNode(String(name));
840
881
  if (!attribute) return null;
841
882
  const ownerElement = this;
883
+ const setValue = value => {
884
+ attribute.value = String(value);
885
+ if (attribute.ownerElement === element && isCanvasSizeAttribute(element, attribute.localName, attribute.namespaceURI)) resetCanvas(element);
886
+ touch(ownerElement);
887
+ };
842
888
  return {
843
889
  get name() { return attribute.name; },
844
890
  get localName() { return attribute.localName; },
845
891
  get namespaceURI() { return attribute.namespaceURI; },
846
892
  get prefix() { return attribute.prefix; },
847
893
  get value() { return attribute.value; },
848
- set value(value) { attribute.value = String(value); touch(ownerElement); },
894
+ set value(value) { setValue(value); },
849
895
  get nodeName() { return attribute.name; },
850
896
  get nodeValue() { return attribute.value; },
851
- set nodeValue(value) { attribute.value = String(value ?? ""); touch(ownerElement); },
897
+ set nodeValue(value) { setValue(value ?? ""); },
852
898
  get textContent() { return attribute.value; },
853
- set textContent(value) { attribute.value = String(value ?? ""); touch(ownerElement); },
899
+ set textContent(value) { setValue(value ?? ""); },
854
900
  ownerElement,
855
901
  specified: true,
856
902
  nodeType: 2,
@@ -861,8 +907,10 @@ class ElementBinding extends ParentBinding {
861
907
  const node = nodeOf(this);
862
908
  const attribute = String(name);
863
909
  const next = String(value);
864
- if (node.getAttribute(attribute) !== next) {
910
+ const resetsCanvas = isCanvasSizeAttribute(node, attribute.toLowerCase());
911
+ if (node.getAttribute(attribute) !== next || resetsCanvas) {
865
912
  node.setAttribute(attribute, next);
913
+ if (resetsCanvas) resetCanvas(node);
866
914
  touch(this);
867
915
  }
868
916
  }
@@ -872,6 +920,7 @@ class ElementBinding extends ParentBinding {
872
920
  const attribute = String(name);
873
921
  if (node.hasAttribute(attribute)) {
874
922
  node.removeAttribute(attribute);
923
+ if (isCanvasSizeAttribute(node, attribute.toLowerCase())) resetCanvas(node);
875
924
  touch(this);
876
925
  }
877
926
  }
@@ -894,17 +943,25 @@ class ElementBinding extends ParentBinding {
894
943
  }
895
944
 
896
945
  setAttributeNS(namespace, qualifiedName, value) {
897
- nodeOf(this).setAttributeNS(namespace === undefined ? null : namespace && String(namespace), String(qualifiedName), String(value));
946
+ const node = nodeOf(this), ns = namespace === undefined ? null : namespace && String(namespace), name = String(qualifiedName);
947
+ node.setAttributeNS(ns, name, String(value));
948
+ if (isCanvasSizeAttribute(node, name, ns)) resetCanvas(node);
898
949
  touch(this);
899
950
  }
900
951
 
901
952
  removeAttributeNS(namespace, localName) {
902
- nodeOf(this).removeAttributeNS(namespace === undefined ? null : namespace && String(namespace), String(localName));
953
+ const node = nodeOf(this), ns = namespace === undefined ? null : namespace && String(namespace), name = String(localName);
954
+ const existed = node.hasAttributeNS(ns, name);
955
+ node.removeAttributeNS(ns, name);
956
+ if (existed && isCanvasSizeAttribute(node, name, ns)) resetCanvas(node);
903
957
  touch(this);
904
958
  }
905
959
 
906
960
  toggleAttribute(name, force = undefined) {
907
- const result = nodeOf(this).toggleAttribute(String(name), force === undefined ? undefined : Boolean(force));
961
+ const node = nodeOf(this), attribute = String(name);
962
+ const existed = node.hasAttribute(attribute);
963
+ const result = node.toggleAttribute(attribute, force === undefined ? undefined : Boolean(force));
964
+ if (existed !== result && isCanvasSizeAttribute(node, attribute.toLowerCase())) resetCanvas(node);
908
965
  touch(this);
909
966
  return result;
910
967
  }
@@ -928,11 +985,13 @@ class ElementBinding extends ParentBinding {
928
985
  }
929
986
 
930
987
  get innerText() {
931
- return nodeOf(this).textContent;
988
+ const node = nodeOf(this);
989
+ const realm = realmOf(this);
990
+ return node.ownerDocument !== realm.window.document ? node.textContent : renderedText(node, realm.hooks.computedStyle);
932
991
  }
933
992
 
934
993
  set innerText(value) {
935
- nodeOf(this).textContent = String(value);
994
+ setRenderedText(nodeOf(this), value);
936
995
  touch(this);
937
996
  }
938
997
 
@@ -998,11 +1057,18 @@ class ElementBinding extends ParentBinding {
998
1057
  click() {
999
1058
  const realm = realmOf(this);
1000
1059
  const element = nodeOf(this);
1001
- const event = new realm.window.MouseEvent("click", { bubbles: true, cancelable: true });
1002
- element.dispatchEvent(event);
1003
- if (!event.defaultPrevented) {
1004
- const anchor = element.closest?.("a[href]");
1005
- if (anchor) realm.hooks.activateLink?.(anchor);
1060
+ if (this.#clickInProgress || element.matches(":disabled")) return;
1061
+ const anchor = element.closest?.("a[href]");
1062
+ const Event = realm.window.PointerEvent ?? realm.window.MouseEvent;
1063
+ const event = new Event("click", { bubbles: true, cancelable: true, composed: true });
1064
+ const activationState = realm.hooks.activationState?.(element);
1065
+ this.#clickInProgress = true;
1066
+ try {
1067
+ element.dispatchEvent(event);
1068
+ if (!event.defaultPrevented && anchor?.hasAttribute("href")) realm.hooks.activateLink?.(anchor);
1069
+ } finally {
1070
+ this.#clickInProgress = false;
1071
+ realm.hooks.activationChanged?.(activationState);
1006
1072
  }
1007
1073
  }
1008
1074
 
@@ -1016,21 +1082,42 @@ class ElementBinding extends ParentBinding {
1016
1082
  const form = nodeOf(this);
1017
1083
  if (form.tagName !== "FORM") return;
1018
1084
  const realSubmitter = submitter == null ? null : nodeOf(submitter);
1085
+ if (realSubmitter && !((realSubmitter.tagName === "BUTTON" && realSubmitter.type === "submit")
1086
+ || (realSubmitter.tagName === "INPUT" && ["submit", "image"].includes(realSubmitter.type)))) {
1087
+ throw new TypeError("The submitter must be a submit button");
1088
+ }
1019
1089
  if (realSubmitter && realSubmitter.form !== form) throw new DOMException("Submitter is not owned by this form", "NotFoundError");
1020
- if (!form.checkValidity()) return;
1090
+ if (!form.noValidate && !realSubmitter?.formNoValidate && !form.checkValidity()) return;
1021
1091
  const event = new realm.window.SubmitEvent("submit", {
1022
- bubbles: true, cancelable: true, submitter: realSubmitter ?? form,
1092
+ bubbles: true, cancelable: true, submitter: realSubmitter,
1023
1093
  });
1024
1094
  form.dispatchEvent(event);
1025
1095
  if (!event.defaultPrevented) realm.hooks.submitForm?.(form, realSubmitter);
1026
1096
  }
1027
1097
 
1098
+ get validity() { return this.#validity ??= realmOf(this).wrapValidity(nodeOf(this).validity); }
1099
+
1100
+ setCustomValidity(message) {
1101
+ if (!arguments.length) throw new TypeError("setCustomValidity requires a message");
1102
+ nodeOf(this).setCustomValidity(String(message));
1103
+ touch(this, [":state"]);
1104
+ }
1105
+
1106
+ checkValidity() { return nodeOf(this).checkValidity(); }
1107
+ reportValidity() { return nodeOf(this).reportValidity(); }
1108
+
1028
1109
  focus() {
1029
- nodeOf(this).focus();
1110
+ const node = nodeOf(this);
1111
+ const previous = node.ownerDocument.activeElement;
1112
+ node.focus();
1113
+ if (node.ownerDocument.activeElement !== previous) touch(this, [":focus", ":focus-visible"]);
1030
1114
  }
1031
1115
 
1032
1116
  blur() {
1033
- nodeOf(this).blur();
1117
+ const node = nodeOf(this);
1118
+ const previous = node.ownerDocument.activeElement;
1119
+ node.blur();
1120
+ if (node.ownerDocument.activeElement !== previous) touch(this, [":focus", ":focus-visible"]);
1034
1121
  }
1035
1122
 
1036
1123
  getBoundingClientRect() {
@@ -1039,7 +1126,10 @@ class ElementBinding extends ParentBinding {
1039
1126
  }
1040
1127
 
1041
1128
  getClientRects() {
1042
- return [this.getBoundingClientRect()];
1129
+ const realm = realmOf(this), node = nodeOf(this);
1130
+ if (!node.isConnected || realm.hooks.computedStyle?.(node)?.display === "contents") return [];
1131
+ const rect = realm.hooks.boundsOf(node);
1132
+ return rect ? [domRect(rect)] : [];
1043
1133
  }
1044
1134
 
1045
1135
  get offsetWidth() {
@@ -1217,15 +1307,17 @@ class ElementBinding extends ParentBinding {
1217
1307
  return Math.max(this.offsetHeight, root ? realmOf(this).hooks.viewport().height : 0);
1218
1308
  }
1219
1309
 
1220
- scrollIntoView() {
1310
+ scrollIntoView(options = undefined) {
1221
1311
  const realm = realmOf(this);
1312
+ const normalized = scrollIntoViewOptions(options);
1222
1313
  if (realm.hooks.scrollIntoView) {
1223
- realm.hooks.scrollIntoView(nodeOf(this));
1314
+ realm.hooks.scrollIntoView(nodeOf(this), normalized);
1224
1315
  return;
1225
1316
  }
1226
1317
  const rect = this.getBoundingClientRect();
1227
- const { scrollX, scrollY } = realm.hooks.viewport();
1228
- realm.hooks.scrollTo(scrollX, scrollY + rect.top);
1318
+ const { scrollX, scrollY, width, height } = realm.hooks.viewport();
1319
+ realm.hooks.scrollTo(scrollX + scrollAlignmentDelta(rect.left, rect.width, width, normalized.inline),
1320
+ scrollY + scrollAlignmentDelta(rect.top, rect.height, height, normalized.block));
1229
1321
  }
1230
1322
  }
1231
1323
 
@@ -1272,7 +1364,7 @@ export const EVENT_HANDLER_NAMES = Object.freeze([
1272
1364
  "click", "dblclick", "mousedown", "mouseup", "mousemove", "mouseover", "mouseout", "mouseenter", "mouseleave",
1273
1365
  "keydown", "keyup", "keypress", "beforeinput", "input", "change", "submit", "reset", "focus", "blur", "load", "error",
1274
1366
  "abort", "scroll", "resize", "wheel", "contextmenu", "touchstart", "touchend", "touchmove",
1275
- "pointerdown", "pointerup", "pointermove", "animationend", "transitionend", "readystatechange",
1367
+ "pointerdown", "pointerup", "pointermove", "animationend", "transitionend", "readystatechange", "slotchange", "toggle", "invalid",
1276
1368
  ]);
1277
1369
 
1278
1370
  /**
@@ -1316,7 +1408,16 @@ for (const property of REFLECTED_PROPERTIES) {
1316
1408
  configurable: true,
1317
1409
  enumerable: true,
1318
1410
  get() {
1319
- const value = nodeOf(this)[property];
1411
+ const node = nodeOf(this);
1412
+ const value = node[property];
1413
+ if (typeof value === "string" && ["src", "href", "action", "poster"].includes(property)) {
1414
+ const attribute = node.getAttribute(property);
1415
+ if (property === "action" && !attribute) return node.ownerDocument.URL;
1416
+ if (attribute !== null) {
1417
+ try { return new URL(attribute, node.tagName === "BASE" ? node.ownerDocument.URL : documentBaseURL(node)).href; }
1418
+ catch { return attribute; }
1419
+ }
1420
+ }
1320
1421
  return value === null || typeof value !== "object" ? value : String(value);
1321
1422
  },
1322
1423
  set(value) {
@@ -1364,12 +1465,32 @@ class DocumentBinding extends ParentBinding {
1364
1465
  const realm = realmOf(this);
1365
1466
  const node = nodeOf(this);
1366
1467
  this.#implementation ??= Object.freeze({
1367
- createHTMLDocument: (title = undefined) => realm.wrap(node.implementation.createHTMLDocument(
1368
- title === undefined ? undefined : String(title))),
1369
- createDocument: (namespace, qualifiedName, doctype = null) => realm.wrap(node.implementation.createDocument(
1370
- namespace === null || namespace === undefined ? null : String(namespace),
1371
- qualifiedName === null || qualifiedName === undefined ? "" : String(qualifiedName),
1372
- doctype ? nodeOf(doctype) : null)),
1468
+ createHTMLDocument: (title = undefined) => {
1469
+ const created = node.implementation.createHTMLDocument();
1470
+ const html = created.createElement("html");
1471
+ const head = created.createElement("head");
1472
+ const body = created.createElement("body");
1473
+ created.replaceChildren(created.implementation.createDocumentType("html", "", ""), html);
1474
+ html.append(head, body);
1475
+ if (title !== undefined) {
1476
+ const titleElement = created.createElement("title");
1477
+ titleElement.textContent = String(title);
1478
+ head.appendChild(titleElement);
1479
+ }
1480
+ return realm.wrap(created);
1481
+ },
1482
+ createDocument: (namespace, qualifiedName, doctype = null) => {
1483
+ const created = new realm.window.XMLDocument();
1484
+ const uri = namespace === null || namespace === undefined ? null : String(namespace);
1485
+ const name = qualifiedName === null || qualifiedName === undefined ? "" : String(qualifiedName);
1486
+ if (doctype !== null) created.appendChild(requireNode(doctype));
1487
+ if (name) created.appendChild(created.createElementNS(uri, name));
1488
+ Object.defineProperty(created, "contentType", {
1489
+ value: uri === "http://www.w3.org/1999/xhtml" ? "application/xhtml+xml"
1490
+ : uri === "http://www.w3.org/2000/svg" ? "image/svg+xml" : "application/xml", configurable: true,
1491
+ });
1492
+ return realm.wrap(created);
1493
+ },
1373
1494
  createDocumentType: (qualifiedName, publicId, systemId) => realm.wrap(node.implementation.createDocumentType(
1374
1495
  String(qualifiedName), String(publicId), String(systemId))),
1375
1496
  hasFeature: () => true,
@@ -1381,13 +1502,16 @@ class DocumentBinding extends ParentBinding {
1381
1502
  return realmOf(this).wrap(nodeOf(this).documentElement);
1382
1503
  }
1383
1504
 
1505
+ get doctype() { return realmOf(this).wrap(nodeOf(this).doctype); }
1506
+
1384
1507
  get head() {
1385
1508
  return realmOf(this).wrap(nodeOf(this).head);
1386
1509
  }
1387
1510
 
1388
1511
  get body() {
1389
1512
  const realm = realmOf(this);
1390
- return realm.wrap(realm.hooks.body ? realm.hooks.body() : nodeOf(this).body);
1513
+ const node = nodeOf(this);
1514
+ return realm.wrap(node === realm.window.document && realm.hooks.body ? realm.hooks.body() : node.body);
1391
1515
  }
1392
1516
 
1393
1517
  get title() {
@@ -1399,7 +1523,8 @@ class DocumentBinding extends ParentBinding {
1399
1523
  }
1400
1524
 
1401
1525
  get readyState() {
1402
- return realmOf(this).hooks.readyState?.() ?? "complete";
1526
+ const realm = realmOf(this);
1527
+ return nodeOf(this) === realm.window.document ? realm.hooks.readyState?.() ?? "complete" : "complete";
1403
1528
  }
1404
1529
 
1405
1530
  // Page Visibility: the renderer only runs the page its client is showing.
@@ -1419,8 +1544,11 @@ class DocumentBinding extends ParentBinding {
1419
1544
  return nodeOf(this).URL;
1420
1545
  }
1421
1546
 
1547
+ get referrer() { return nodeOf(this).referrer ?? ""; }
1548
+
1422
1549
  get location() {
1423
- return realmOf(this).hooks.pageGlobal?.()?.location ?? null;
1550
+ const realm = realmOf(this);
1551
+ return nodeOf(this) === realm.window.document ? realm.hooks.pageGlobal?.()?.location ?? null : null;
1424
1552
  }
1425
1553
 
1426
1554
  get domain() {
@@ -1431,17 +1559,21 @@ class DocumentBinding extends ParentBinding {
1431
1559
  return "UTF-8";
1432
1560
  }
1433
1561
 
1562
+ get contentType() { return nodeOf(this).contentType; }
1563
+
1434
1564
  get compatMode() {
1435
1565
  return "CSS1Compat";
1436
1566
  }
1437
1567
 
1438
1568
  /** Non-HttpOnly cookies from the controller's jar; writes go back to the jar. */
1439
1569
  get cookie() {
1440
- return realmOf(this).hooks.documentCookie?.() ?? "";
1570
+ const realm = realmOf(this);
1571
+ return nodeOf(this) === realm.window.document ? realm.hooks.documentCookie?.() ?? "" : "";
1441
1572
  }
1442
1573
 
1443
1574
  set cookie(value) {
1444
- realmOf(this).hooks.setDocumentCookie?.(String(value));
1575
+ const realm = realmOf(this);
1576
+ if (nodeOf(this) === realm.window.document) realm.hooks.setDocumentCookie?.(String(value));
1445
1577
  }
1446
1578
 
1447
1579
  get activeElement() {
@@ -1469,7 +1601,7 @@ class DocumentBinding extends ParentBinding {
1469
1601
  const sheets = [...nodeOf(this).querySelectorAll("style, link[rel~='stylesheet']")].map((element) => {
1470
1602
  const href = element.tagName === "LINK" ? element.getAttribute("href") : null;
1471
1603
  let resolved = null;
1472
- if (href !== null) try { resolved = new URL(href, nodeOf(this).URL).href; } catch {}
1604
+ if (href !== null) try { resolved = new URL(href, element.baseURI || nodeOf(this).URL).href; } catch {}
1473
1605
  return Object.freeze({
1474
1606
  type: "text/css",
1475
1607
  href: resolved,
@@ -1492,11 +1624,13 @@ class DocumentBinding extends ParentBinding {
1492
1624
  }
1493
1625
 
1494
1626
  get currentScript() {
1495
- return realmOf(this).wrap(realmOf(this).hooks.currentScript?.() ?? null);
1627
+ const realm = realmOf(this);
1628
+ return nodeOf(this) === realm.window.document ? realm.wrap(realm.hooks.currentScript?.() ?? null) : null;
1496
1629
  }
1497
1630
 
1498
1631
  get defaultView() {
1499
- return realmOf(this).hooks.pageGlobal?.() ?? null;
1632
+ const realm = realmOf(this);
1633
+ return nodeOf(this) === realm.window.document ? realm.hooks.pageGlobal?.() ?? null : null;
1500
1634
  }
1501
1635
 
1502
1636
  get scripts() {
@@ -1715,6 +1849,8 @@ class EventBinding {
1715
1849
  return this.#realm.wrap(this.#event.relatedTarget ?? null) ?? null;
1716
1850
  }
1717
1851
 
1852
+ get submitter() { return this.#realm.wrap(this.#event.submitter ?? null); }
1853
+
1718
1854
  getModifierState(key) {
1719
1855
  const name = String(key);
1720
1856
  return name === "Alt" ? this.altKey : name === "Control" ? this.ctrlKey : name === "Shift" ? this.shiftKey
@@ -1731,12 +1867,6 @@ class EventBinding {
1731
1867
  this.initUIEvent(type, bubbles, cancelable, view, detail);
1732
1868
  }
1733
1869
 
1734
- // A plain Event has no detail of its own: assigning one (Polymer's fire()
1735
- // does) makes an own property, as in browsers, instead of failing on the getter.
1736
- set detail(value) {
1737
- Object.defineProperty(this, "detail", { value, writable: true, configurable: true, enumerable: true });
1738
- }
1739
-
1740
1870
  /** Legacy initialisation of an event from document.createEvent() (DOM §2.5); not once dispatched. */
1741
1871
  initEvent(type, bubbles = false, cancelable = false) {
1742
1872
  if (this.#event.eventPhase !== 0) return;
@@ -2287,34 +2417,45 @@ function attributesBinding(owner) {
2287
2417
  /** element.dataset: data-* attributes as camelCase string properties. */
2288
2418
  function datasetBinding(node, realm) {
2289
2419
  const attribute = (property) => `data-${property.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}`;
2290
- return new Proxy(Object.create(null), {
2291
- get(_target, property) {
2292
- if (typeof property !== "string") return undefined;
2293
- return node.getAttribute(attribute(property)) ?? undefined;
2294
- },
2295
- set(_target, property, value) {
2296
- if (typeof property !== "string") return false;
2297
- node.setAttribute(attribute(property), String(value));
2420
+ const supported = (property) => typeof property === "string" && !/-[a-z]/.test(property)
2421
+ && node.hasAttribute(attribute(property));
2422
+ const set = (target, property, value) => {
2423
+ if (typeof property !== "string") return Reflect.set(target, property, value);
2424
+ if (/-[a-z]/.test(property)) throw new DOMException("The property name must not contain a hyphen followed by a lowercase letter.", "SyntaxError");
2425
+ const name = attribute(property);
2426
+ const next = String(value);
2427
+ if (node.getAttribute(name) !== next) {
2428
+ node.setAttribute(name, next);
2298
2429
  realm.hooks.touch(node);
2299
- return true;
2430
+ }
2431
+ return true;
2432
+ };
2433
+ return new Proxy(Object.create(realm.datasetPrototype), {
2434
+ get(target, property, receiver) {
2435
+ return supported(property) ? node.getAttribute(attribute(property)) : Reflect.get(target, property, receiver);
2300
2436
  },
2301
- has: (_target, property) => typeof property === "string" && node.hasAttribute(attribute(property)),
2302
- deleteProperty(_target, property) {
2303
- if (typeof property !== "string") return false;
2437
+ set,
2438
+ has: (target, property) => supported(property) || Reflect.has(target, property),
2439
+ deleteProperty(target, property) {
2440
+ if (!supported(property)) return Reflect.deleteProperty(target, property);
2304
2441
  node.removeAttribute(attribute(property));
2305
2442
  realm.hooks.touch(node);
2306
2443
  return true;
2307
2444
  },
2308
- ownKeys: () => node.getAttributeNames()
2445
+ ownKeys: (target) => [...node.getAttributeNames()
2309
2446
  .filter((name) => name.startsWith("data-"))
2310
- .map((name) => name.slice(5).replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())),
2311
- getOwnPropertyDescriptor(_target, property) {
2312
- const value = typeof property === "string" ? node.getAttribute(attribute(property)) : null;
2313
- return value === null ? undefined : { value, writable: true, enumerable: true, configurable: true };
2447
+ .map((name) => name.slice(5).replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())), ...Reflect.ownKeys(target)],
2448
+ getOwnPropertyDescriptor(target, property) {
2449
+ return supported(property)
2450
+ ? { value: node.getAttribute(attribute(property)), writable: true, enumerable: true, configurable: true }
2451
+ : Reflect.getOwnPropertyDescriptor(target, property);
2314
2452
  },
2315
- defineProperty: () => false,
2316
- getPrototypeOf: () => null,
2317
- setPrototypeOf: () => false,
2453
+ defineProperty(target, property, descriptor) {
2454
+ if (typeof property !== "string") return Reflect.defineProperty(target, property, descriptor);
2455
+ if (!("value" in descriptor) || descriptor.configurable === false) return false;
2456
+ return set(target, property, descriptor.value);
2457
+ },
2458
+ preventExtensions: () => false,
2318
2459
  });
2319
2460
  }
2320
2461
 
@@ -2530,6 +2671,7 @@ const EVENT_INTERFACES = {
2530
2671
  MessageEvent: ["Event", ["data", "origin", "lastEventId", "source", "ports"]],
2531
2672
  PopStateEvent: ["Event", ["state"]],
2532
2673
  HashChangeEvent: ["Event", ["oldURL", "newURL"]],
2674
+ SubmitEvent: ["Event", ["submitter"]],
2533
2675
  };
2534
2676
  /** document.createEvent() interface names (DOM §4.5): legacy aliases included. */
2535
2677
  const CREATE_EVENT_TYPES = new Map([["event", "Event"], ["events", "Event"], ["htmlevents", "Event"],
@@ -2562,7 +2704,10 @@ export function eventConstructors(realm) {
2562
2704
  // detail stays in the binding: page values never enter Happy DOM.
2563
2705
  return realm.wrapEvent(new window.CustomEvent(String(type), init(options)), options?.detail);
2564
2706
  }
2565
- realm.eventPrototype = mutableBindingPrototype(EventBinding.prototype);
2707
+ // Members moved to UI event subclasses must not remain inherited through
2708
+ // the frozen engine binding. Plain Events allow expandos such as React's
2709
+ // afterblur.relatedTarget, and Event.prototype directly inherits Object.
2710
+ realm.eventPrototype = mutableBindingPrototype(EventBinding.prototype, Object.prototype);
2566
2711
  Object.defineProperty(realm.eventPrototype, "constructor", { value: Event, writable: true, configurable: true });
2567
2712
  Event.prototype = realm.eventPrototype;
2568
2713
  for (const [name, value] of Object.entries(EVENT_PHASES)) {
@@ -2620,6 +2765,14 @@ export function eventConstructors(realm) {
2620
2765
  }
2621
2766
  MessageEvent.prototype = realm.eventPrototypes.get("MessageEvent");
2622
2767
  Object.defineProperty(MessageEvent.prototype, "constructor", { value: MessageEvent, writable: true, configurable: true });
2768
+ function SubmitEvent(type, options = undefined) {
2769
+ if (!new.target) throw new TypeError("Constructor SubmitEvent requires 'new'");
2770
+ const submitter = options?.submitter == null ? null : nodeOf(options.submitter);
2771
+ if (submitter && submitter.nodeType !== 1) throw new TypeError("The submitter must be an element");
2772
+ return realm.wrapEvent(new window.SubmitEvent(String(type), { ...init(options), submitter }));
2773
+ }
2774
+ SubmitEvent.prototype = realm.eventPrototypes.get("SubmitEvent");
2775
+ Object.defineProperty(SubmitEvent.prototype, "constructor", { value: SubmitEvent, writable: true, configurable: true });
2623
2776
  // PromiseRejectionEvent (HTML §8.1.5.7): promise and reason stay page values in the binding.
2624
2777
  const rejections = new WeakMap();
2625
2778
  function PromiseRejectionEvent(type, options) {
@@ -2643,7 +2796,7 @@ export function eventConstructors(realm) {
2643
2796
  enumerable: true, configurable: true },
2644
2797
  [Symbol.toStringTag]: { value: "PromiseRejectionEvent", configurable: true },
2645
2798
  });
2646
- return { Event, CustomEvent, MessageEvent, PromiseRejectionEvent, ...uiEvents };
2799
+ return { Event, CustomEvent, MessageEvent, SubmitEvent, PromiseRejectionEvent, ...uiEvents };
2647
2800
  }
2648
2801
 
2649
2802
  function domRect({ left, top, width, height }) {