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