@miaskiewicz/turbo-dom 0.1.2 → 0.1.3
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miaskiewicz/turbo-dom",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Faster, more spec-correct DOM for test runners — native html5ever (Rust/WASM) parser + lazy copy-on-write DOM. A drop-in-style alternative to jsdom/happy-dom for vitest & jest.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
package/src/runtime/dom.mjs
CHANGED
|
@@ -133,6 +133,49 @@ export class Node extends EventTarget {
|
|
|
133
133
|
return false;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
get isConnected() {
|
|
137
|
+
let n = this;
|
|
138
|
+
while (n.parentNode) n = n.parentNode;
|
|
139
|
+
return n.nodeType === DOCUMENT_NODE;
|
|
140
|
+
}
|
|
141
|
+
getRootNode() {
|
|
142
|
+
let n = this;
|
|
143
|
+
while (n.parentNode) n = n.parentNode;
|
|
144
|
+
return n;
|
|
145
|
+
}
|
|
146
|
+
normalize() {
|
|
147
|
+
const kids = this.__children();
|
|
148
|
+
for (let i = kids.length - 1; i > 0; i--) {
|
|
149
|
+
if (kids[i].nodeType === TEXT_NODE && kids[i - 1].nodeType === TEXT_NODE) {
|
|
150
|
+
kids[i - 1]._data += kids[i].data; kids[i].parentNode = null; kids.splice(i, 1);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
for (let i = kids.length - 1; i >= 0; i--) {
|
|
154
|
+
if (kids[i].nodeType === TEXT_NODE && kids[i].data === '') { kids[i].parentNode = null; kids.splice(i, 1); }
|
|
155
|
+
else if (kids[i].nodeType === ELEMENT_NODE) kids[i].normalize();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
replaceChildren(...nodes) { this.__kids = []; for (const n of nodes) this.appendChild(typeof n === 'string' ? this.ownerDocument.createTextNode(n) : n); }
|
|
159
|
+
// bitmask: 1 DISCONNECTED, 2 PRECEDING, 4 FOLLOWING, 8 CONTAINS, 16 CONTAINED_BY
|
|
160
|
+
compareDocumentPosition(other) {
|
|
161
|
+
if (other === this) return 0;
|
|
162
|
+
if (this.contains(other)) return 16 + 4;
|
|
163
|
+
if (other.contains(this)) return 8 + 2;
|
|
164
|
+
// document order via a flat walk from the common root
|
|
165
|
+
const root = this.getRootNode();
|
|
166
|
+
const order = [];
|
|
167
|
+
(function walk(n) { order.push(n); for (const c of (n.__children ? n.__children() : [])) walk(c); })(root);
|
|
168
|
+
const a = order.indexOf(this), b = order.indexOf(other);
|
|
169
|
+
if (a === -1 || b === -1) return 1;
|
|
170
|
+
return a < b ? 4 : 2;
|
|
171
|
+
}
|
|
172
|
+
cloneNode(deep = false) {
|
|
173
|
+
// base fallback; Element/Text/Comment override
|
|
174
|
+
const n = new this.constructor(this.ownerDocument);
|
|
175
|
+
if (deep) for (const c of this.__children()) n.appendChild(c.cloneNode(true));
|
|
176
|
+
return n;
|
|
177
|
+
}
|
|
178
|
+
|
|
136
179
|
get textContent() {
|
|
137
180
|
let s = '';
|
|
138
181
|
for (const c of this.__children()) {
|
|
@@ -160,11 +203,33 @@ class CharacterData extends Node {
|
|
|
160
203
|
get length() { return this._data.length; }
|
|
161
204
|
get textContent() { return this._data; }
|
|
162
205
|
set textContent(v) { this.data = v; }
|
|
206
|
+
substringData(offset, count) { return this._data.slice(offset, offset + count); }
|
|
207
|
+
appendData(s) { this.data = this._data + s; }
|
|
208
|
+
insertData(offset, s) { this.data = this._data.slice(0, offset) + s + this._data.slice(offset); }
|
|
209
|
+
deleteData(offset, count) { this.data = this._data.slice(0, offset) + this._data.slice(offset + count); }
|
|
210
|
+
replaceData(offset, count, s) { this.data = this._data.slice(0, offset) + s + this._data.slice(offset + count); }
|
|
211
|
+
before(...nodes) { const p = this.parentNode; if (p) for (const n of nodes) p.insertBefore(typeof n === 'string' ? this.ownerDocument.createTextNode(n) : n, this); }
|
|
212
|
+
after(...nodes) { const p = this.parentNode; if (!p) return; const ref = this.nextSibling; for (const n of nodes) p.insertBefore(typeof n === 'string' ? this.ownerDocument.createTextNode(n) : n, ref); }
|
|
213
|
+
replaceWith(...nodes) { const p = this.parentNode; if (!p) return; const ref = this.nextSibling; this.remove(); for (const n of nodes) p.insertBefore(typeof n === 'string' ? this.ownerDocument.createTextNode(n) : n, ref); }
|
|
163
214
|
}
|
|
164
215
|
|
|
165
216
|
export class Text extends CharacterData {
|
|
166
217
|
get nodeType() { return TEXT_NODE; }
|
|
167
218
|
get nodeName() { return '#text'; }
|
|
219
|
+
get wholeText() {
|
|
220
|
+
let s = this._data, n = this.previousSibling;
|
|
221
|
+
while (n && n.nodeType === TEXT_NODE) { s = n.data + s; n = n.previousSibling; }
|
|
222
|
+
n = this.nextSibling;
|
|
223
|
+
while (n && n.nodeType === TEXT_NODE) { s += n.data; n = n.nextSibling; }
|
|
224
|
+
return s;
|
|
225
|
+
}
|
|
226
|
+
splitText(offset) {
|
|
227
|
+
const rest = this._data.slice(offset);
|
|
228
|
+
this.data = this._data.slice(0, offset);
|
|
229
|
+
const node = new Text(this.ownerDocument, rest);
|
|
230
|
+
if (this.parentNode) this.parentNode.insertBefore(node, this.nextSibling);
|
|
231
|
+
return node;
|
|
232
|
+
}
|
|
168
233
|
cloneNode() { return new Text(this.ownerDocument, this._data); }
|
|
169
234
|
}
|
|
170
235
|
|
|
@@ -462,6 +527,54 @@ export class Element extends Node {
|
|
|
462
527
|
getBoundingClientRect() { return zeroRect(); }
|
|
463
528
|
getClientRects() { return []; }
|
|
464
529
|
scrollIntoView() {}
|
|
530
|
+
scroll() {} scrollTo() {} scrollBy() {}
|
|
531
|
+
// honest zero geometry (no layout)
|
|
532
|
+
get offsetWidth() { return 0; } get offsetHeight() { return 0; }
|
|
533
|
+
get offsetTop() { return 0; } get offsetLeft() { return 0; }
|
|
534
|
+
get offsetParent() { return null; }
|
|
535
|
+
get clientWidth() { return 0; } get clientHeight() { return 0; }
|
|
536
|
+
get clientTop() { return 0; } get clientLeft() { return 0; }
|
|
537
|
+
get scrollWidth() { return 0; } get scrollHeight() { return 0; }
|
|
538
|
+
get scrollTop() { return 0; } set scrollTop(_v) {} get scrollLeft() { return 0; } set scrollLeft(_v) {}
|
|
539
|
+
|
|
540
|
+
// namespaced attributes
|
|
541
|
+
getAttributeNS(_ns, name) { return this.getAttribute(name); }
|
|
542
|
+
setAttributeNS(_ns, name, value) { this.setAttribute(name, value); }
|
|
543
|
+
hasAttributeNS(_ns, name) { return this.hasAttribute(name); }
|
|
544
|
+
removeAttributeNS(_ns, name) { this.removeAttribute(name); }
|
|
545
|
+
getAttributeNode(name) { const a = this.__attrs.find((x) => x.name === name); return a ? { name: a.name, value: a.value, ownerElement: this } : null; }
|
|
546
|
+
|
|
547
|
+
// adjacency
|
|
548
|
+
insertAdjacentElement(position, el) {
|
|
549
|
+
const p = this.parentNode;
|
|
550
|
+
switch (position) {
|
|
551
|
+
case 'beforebegin': if (p) p.insertBefore(el, this); break;
|
|
552
|
+
case 'afterbegin': this.insertBefore(el, this.firstChild); break;
|
|
553
|
+
case 'beforeend': this.appendChild(el); break;
|
|
554
|
+
case 'afterend': if (p) p.insertBefore(el, this.nextSibling); break;
|
|
555
|
+
}
|
|
556
|
+
return el;
|
|
557
|
+
}
|
|
558
|
+
insertAdjacentText(position, text) { this.insertAdjacentElement(position, this.ownerDocument.createTextNode(text)); }
|
|
559
|
+
|
|
560
|
+
// commonly-reflected properties
|
|
561
|
+
get tabIndex() { return this.hasAttribute('tabindex') ? parseInt(this.getAttribute('tabindex'), 10) || 0 : (/^(a|button|input|select|textarea)$/.test(this.localName) ? 0 : -1); }
|
|
562
|
+
set tabIndex(v) { this.setAttribute('tabindex', String(v)); }
|
|
563
|
+
get title() { return this.getAttribute('title') ?? ''; } set title(v) { this.setAttribute('title', v); }
|
|
564
|
+
get lang() { return this.getAttribute('lang') ?? ''; } set lang(v) { this.setAttribute('lang', v); }
|
|
565
|
+
get dir() { return this.getAttribute('dir') ?? ''; } set dir(v) { this.setAttribute('dir', v); }
|
|
566
|
+
get hidden() { return this.hasAttribute('hidden'); } set hidden(v) { if (v) this.setAttribute('hidden', ''); else this.removeAttribute('hidden'); }
|
|
567
|
+
get role() { return this.getAttribute('role'); } set role(v) { this.setAttribute('role', v); }
|
|
568
|
+
get contentEditable() { return this.getAttribute('contenteditable') ?? 'inherit'; } set contentEditable(v) { this.setAttribute('contenteditable', v); }
|
|
569
|
+
get isContentEditable() { return this.getAttribute('contenteditable') === 'true' || this.getAttribute('contenteditable') === ''; }
|
|
570
|
+
get hreflang() { return this.getAttribute('hreflang') ?? ''; }
|
|
571
|
+
get target() { return this.getAttribute('target') ?? ''; } set target(v) { this.setAttribute('target', v); }
|
|
572
|
+
|
|
573
|
+
// pointer capture + animations (no-op honest stubs)
|
|
574
|
+
setPointerCapture() {} releasePointerCapture() {} hasPointerCapture() { return false; }
|
|
575
|
+
animate() { return { play() {}, pause() {}, cancel() {}, finish() {}, finished: Promise.resolve(), onfinish: null, cancel_: null }; }
|
|
576
|
+
getAnimations() { return []; }
|
|
577
|
+
requestFullscreen() { return Promise.resolve(); }
|
|
465
578
|
|
|
466
579
|
// canvas (no raster backend — honest no-op context)
|
|
467
580
|
getContext(type) { return this.localName === 'canvas' ? (this.__ctx ||= makeCanvasStub()) : null; }
|
|
@@ -586,6 +699,54 @@ function makeSelection() {
|
|
|
586
699
|
};
|
|
587
700
|
}
|
|
588
701
|
|
|
702
|
+
// TreeWalker / NodeIterator over the DOM (doubles for both — common subset).
|
|
703
|
+
class TreeWalker {
|
|
704
|
+
constructor(root, whatToShow, filter) {
|
|
705
|
+
this.root = root; this.whatToShow = whatToShow >>> 0; this.filter = filter; this.currentNode = root;
|
|
706
|
+
this.referenceNode = root; this.pointerBeforeReferenceNode = true;
|
|
707
|
+
}
|
|
708
|
+
__show(node) {
|
|
709
|
+
const bit = node.nodeType === ELEMENT_NODE ? 1 : node.nodeType === TEXT_NODE ? 4 : node.nodeType === COMMENT_NODE ? 128 : 0xffffffff;
|
|
710
|
+
if (!(this.whatToShow & bit) && this.whatToShow !== 0xffffffff) return 3; // FILTER_SKIP
|
|
711
|
+
if (this.filter) {
|
|
712
|
+
const fn = typeof this.filter === 'function' ? this.filter : this.filter.acceptNode;
|
|
713
|
+
return fn.call(this.filter, node);
|
|
714
|
+
}
|
|
715
|
+
return 1; // FILTER_ACCEPT
|
|
716
|
+
}
|
|
717
|
+
__flat() {
|
|
718
|
+
const out = [];
|
|
719
|
+
(function walk(n) { for (const c of (n.__children ? n.__children() : [])) { out.push(c); walk(c); } })(this.root);
|
|
720
|
+
return out.filter((n) => this.__show(n) === 1);
|
|
721
|
+
}
|
|
722
|
+
nextNode() { const all = this.__flat(); const i = all.indexOf(this.currentNode); const next = all[i + 1] ?? (this.currentNode === this.root ? all[0] : null); this.currentNode = next; this.referenceNode = next; return next ?? null; }
|
|
723
|
+
previousNode() { const all = this.__flat(); const i = all.indexOf(this.currentNode); const prev = i > 0 ? all[i - 1] : null; if (prev) { this.currentNode = prev; this.referenceNode = prev; } return prev ?? null; }
|
|
724
|
+
firstChild() { const k = (this.currentNode.__children ? this.currentNode.__children() : []).filter((n) => this.__show(n) === 1); if (k[0]) { this.currentNode = k[0]; return k[0]; } return null; }
|
|
725
|
+
lastChild() { const k = (this.currentNode.__children ? this.currentNode.__children() : []).filter((n) => this.__show(n) === 1); const last = k[k.length - 1]; if (last) { this.currentNode = last; return last; } return null; }
|
|
726
|
+
parentNode() { const p = this.currentNode.parentNode; if (p && p !== this.root.parentNode) { this.currentNode = p; return p; } return null; }
|
|
727
|
+
nextSibling() { let n = this.currentNode.nextSibling; while (n && this.__show(n) !== 1) n = n.nextSibling; if (n) this.currentNode = n; return n ?? null; }
|
|
728
|
+
previousSibling() { let n = this.currentNode.previousSibling; while (n && this.__show(n) !== 1) n = n.previousSibling; if (n) this.currentNode = n; return n ?? null; }
|
|
729
|
+
detach() {}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// DOMParser — parses a string into a real Document via the native parser.
|
|
733
|
+
export class DOMParser {
|
|
734
|
+
parseFromString(str, type = 'text/html') {
|
|
735
|
+
if (type === 'text/html') return parseDocument(String(str));
|
|
736
|
+
// XML-ish: parse as a fragment wrapped in a document
|
|
737
|
+
const doc = parseDocument(`<!doctype html><html><body>${str}</body></html>`);
|
|
738
|
+
return doc;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// XMLSerializer — serializes a node back to markup.
|
|
743
|
+
export class XMLSerializer {
|
|
744
|
+
serializeToString(node) {
|
|
745
|
+
if (node.nodeType === DOCUMENT_NODE || node.nodeType === DOCUMENT_FRAGMENT_NODE) return serializeInner(node);
|
|
746
|
+
return serializeOuter(node);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
589
750
|
// minimal inline-style CSSOM (honest: only inline + explicitly set props)
|
|
590
751
|
function makeStyle(el) {
|
|
591
752
|
const parse = () => {
|
|
@@ -817,10 +978,44 @@ export class Document extends Node {
|
|
|
817
978
|
createDocumentFragment() { return new DocumentFragment(this); }
|
|
818
979
|
createEvent() { return new Event(''); }
|
|
819
980
|
createRange() { return new Range(this); }
|
|
981
|
+
createAttribute(name) { return { name, value: '', ownerElement: null }; }
|
|
982
|
+
createComment(data) { return new Comment(this, String(data)); }
|
|
820
983
|
getSelection() { if (!this.__selection) this.__selection = makeSelection(); return this.__selection; }
|
|
821
984
|
importNode(node, deep) { return node.cloneNode(deep); }
|
|
822
985
|
adoptNode(node) { if (node.parentNode) node.parentNode.removeChild(node); node.ownerDocument = this; return node; }
|
|
823
986
|
|
|
987
|
+
// TreeWalker / NodeIterator (whatToShow: 1 elements, 4 text, 0xFFFFFFFF all)
|
|
988
|
+
createTreeWalker(root, whatToShow = 0xffffffff, filter = null) { return new TreeWalker(root, whatToShow, filter); }
|
|
989
|
+
createNodeIterator(root, whatToShow = 0xffffffff, filter = null) { return new TreeWalker(root, whatToShow, filter); }
|
|
990
|
+
|
|
991
|
+
getElementsByName(name) {
|
|
992
|
+
const self = this; return liveHTMLCollection(() => collectByTag(self, '*').filter((e) => e.getAttribute('name') === name));
|
|
993
|
+
}
|
|
994
|
+
elementFromPoint() { return null; }
|
|
995
|
+
elementsFromPoint() { return []; }
|
|
996
|
+
execCommand() { return false; }
|
|
997
|
+
queryCommandSupported() { return false; }
|
|
998
|
+
queryCommandEnabled() { return false; }
|
|
999
|
+
hasFocus() { return true; }
|
|
1000
|
+
write() {} writeln() {} open() { return this; } close() {}
|
|
1001
|
+
|
|
1002
|
+
get title() { const t = this.querySelector('title'); return t ? t.textContent : (this.__title || ''); }
|
|
1003
|
+
set title(v) { const t = this.querySelector('title'); if (t) t.textContent = v; else this.__title = String(v); }
|
|
1004
|
+
get location() { return this.defaultView ? this.defaultView.location : null; }
|
|
1005
|
+
get baseURI() { return (this.defaultView && this.defaultView.location && this.defaultView.location.href) || 'about:blank'; }
|
|
1006
|
+
get URL() { return this.baseURI; }
|
|
1007
|
+
get documentURI() { return this.baseURI; }
|
|
1008
|
+
get scrollingElement() { return this.documentElement; }
|
|
1009
|
+
get fullscreenElement() { return null; }
|
|
1010
|
+
get implementation() {
|
|
1011
|
+
const self = this;
|
|
1012
|
+
return {
|
|
1013
|
+
createHTMLDocument(title) { const d = parseDocument(`<!doctype html><html><head><title>${title || ''}</title></head><body></body></html>`); return d; },
|
|
1014
|
+
createDocumentType: (name, pub, sys) => new DocumentType(self, name, pub, sys),
|
|
1015
|
+
hasFeature: () => true,
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
|
|
824
1019
|
// ---- queries ----
|
|
825
1020
|
getElementById(id) {
|
|
826
1021
|
let found = null;
|
package/src/runtime/window.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
} from './stubs.mjs';
|
|
13
13
|
import {
|
|
14
14
|
Node, Element, Text, Comment, Document, DocumentFragment, DocumentType, Event, CustomEvent,
|
|
15
|
-
MutationObserver,
|
|
15
|
+
MutationObserver, DOMParser, XMLSerializer,
|
|
16
16
|
} from './dom.mjs';
|
|
17
17
|
import {
|
|
18
18
|
EventTarget,
|
|
@@ -69,12 +69,29 @@ export function createWindow(document, { url = 'http://localhost/' } = {}) {
|
|
|
69
69
|
HTMLSpanElement: Element, HTMLParagraphElement: Element, HTMLUListElement: Element,
|
|
70
70
|
HTMLLIElement: Element, HTMLHeadingElement: Element, HTMLBodyElement: Element,
|
|
71
71
|
HTMLDocument: Document, DocumentFragment, ShadowRoot: DocumentFragment,
|
|
72
|
-
MutationObserver,
|
|
72
|
+
MutationObserver, DOMParser, XMLSerializer,
|
|
73
73
|
URL: makeURL(), URLSearchParams,
|
|
74
74
|
Blob: globalThis.Blob, File: makeFile(), FileReader,
|
|
75
75
|
customElements: makeCustomElements(),
|
|
76
76
|
AbortController: globalThis.AbortController, AbortSignal: globalThis.AbortSignal,
|
|
77
77
|
TextEncoder: globalThis.TextEncoder, TextDecoder: globalThis.TextDecoder,
|
|
78
|
+
// web platform globals Node already provides
|
|
79
|
+
fetch: globalThis.fetch ? (...a) => globalThis.fetch(...a) : undefined,
|
|
80
|
+
Headers: globalThis.Headers, Request: globalThis.Request, Response: globalThis.Response,
|
|
81
|
+
FormData: globalThis.FormData, ReadableStream: globalThis.ReadableStream,
|
|
82
|
+
crypto: globalThis.crypto, Crypto: globalThis.Crypto, SubtleCrypto: globalThis.SubtleCrypto,
|
|
83
|
+
btoa: (s) => Buffer.from(String(s), 'binary').toString('base64'),
|
|
84
|
+
atob: (s) => Buffer.from(String(s), 'base64').toString('binary'),
|
|
85
|
+
MessageChannel: globalThis.MessageChannel, MessagePort: globalThis.MessagePort,
|
|
86
|
+
BroadcastChannel: globalThis.BroadcastChannel, EventSource: globalThis.EventSource,
|
|
87
|
+
reportError: (e) => { /* swallow; tests assert via handlers */ void e; },
|
|
88
|
+
requestIdleCallback: (cb) => hostSetTimeout(() => cb({ didTimeout: false, timeRemaining: () => 50 }), 0),
|
|
89
|
+
cancelIdleCallback: (id) => hostClearTimeout(id),
|
|
90
|
+
CSS: { supports: () => true, escape: (s) => String(s).replace(/[^a-zA-Z0-9_-]/g, (c) => '\\' + c) },
|
|
91
|
+
XMLHttpRequest: makeXHR(),
|
|
92
|
+
Image: function Image(w, h) { const img = document.createElement('img'); if (w != null) img.setAttribute('width', w); if (h != null) img.setAttribute('height', h); return img; },
|
|
93
|
+
Audio: function Audio(src) { const a = document.createElement('audio'); if (src) a.setAttribute('src', src); return a; },
|
|
94
|
+
Worker: class Worker { constructor() {} postMessage() {} terminate() {} addEventListener() {} removeEventListener() {} },
|
|
78
95
|
// timers delegate to the captured host fns (NOT the bare names — once these
|
|
79
96
|
// are installed on globalThis the bare names resolve back here → recursion)
|
|
80
97
|
setTimeout: (...a) => hostSetTimeout(...a),
|
|
@@ -104,12 +121,26 @@ export function createWindow(document, { url = 'http://localhost/' } = {}) {
|
|
|
104
121
|
// subsystem grouping: history co-materializes with (and shares) location
|
|
105
122
|
location: () => makeLocation(url),
|
|
106
123
|
history: () => makeHistory(windowProxy.location),
|
|
107
|
-
navigator: () => ({
|
|
108
|
-
|
|
124
|
+
navigator: () => ({
|
|
125
|
+
userAgent: 'Mozilla/5.0 (turbo-dom) AppleWebKit/537.36',
|
|
126
|
+
platform: 'turbo-dom', vendor: '', language: 'en-US', languages: ['en-US'],
|
|
127
|
+
onLine: true, cookieEnabled: true, doNotTrack: null, maxTouchPoints: 0,
|
|
128
|
+
hardwareConcurrency: 4, deviceMemory: 8, webdriver: false,
|
|
129
|
+
clipboard: { readText: async () => '', writeText: async () => {}, read: async () => [], write: async () => {} },
|
|
130
|
+
permissions: { query: async () => ({ state: 'prompt', addEventListener() {}, removeEventListener() {} }) },
|
|
131
|
+
sendBeacon: () => true, vibrate: () => false,
|
|
132
|
+
}),
|
|
133
|
+
performance: () => ({ now: performanceNow, timeOrigin: 0, mark() {}, measure() {}, getEntriesByName: () => [], getEntriesByType: () => [], clearMarks() {}, clearMeasures() {} }),
|
|
109
134
|
Storage: () => Storage,
|
|
110
135
|
devicePixelRatio: () => 1,
|
|
111
136
|
innerWidth: () => 1024,
|
|
112
137
|
innerHeight: () => 768,
|
|
138
|
+
outerWidth: () => 1024,
|
|
139
|
+
outerHeight: () => 768,
|
|
140
|
+
scrollX: () => 0, scrollY: () => 0, pageXOffset: () => 0, pageYOffset: () => 0,
|
|
141
|
+
screenX: () => 0, screenY: () => 0, screenLeft: () => 0, screenTop: () => 0,
|
|
142
|
+
screen: () => ({ width: 1024, height: 768, availWidth: 1024, availHeight: 768, colorDepth: 24, pixelDepth: 24, orientation: { type: 'landscape-primary', angle: 0, addEventListener() {}, removeEventListener() {} } }),
|
|
143
|
+
visualViewport: () => ({ width: 1024, height: 768, scale: 1, offsetLeft: 0, offsetTop: 0, pageLeft: 0, pageTop: 0, addEventListener() {}, removeEventListener() {} }),
|
|
113
144
|
};
|
|
114
145
|
|
|
115
146
|
windowProxy = new Proxy(base, {
|
|
@@ -166,3 +197,32 @@ function makeFile() {
|
|
|
166
197
|
constructor(bits = [], name = 'file', opts = {}) { super(bits, opts); this.name = String(name); this.lastModified = opts.lastModified || 0; }
|
|
167
198
|
};
|
|
168
199
|
}
|
|
200
|
+
|
|
201
|
+
// Minimal XMLHttpRequest backed by fetch — enough that libraries that construct
|
|
202
|
+
// one and issue a request don't crash. No-network setups still get a clean object.
|
|
203
|
+
function makeXHR() {
|
|
204
|
+
return class XMLHttpRequest {
|
|
205
|
+
constructor() {
|
|
206
|
+
this.readyState = 0; this.status = 0; this.statusText = ''; this.response = ''; this.responseText = '';
|
|
207
|
+
this.responseType = ''; this.timeout = 0; this.withCredentials = false;
|
|
208
|
+
this.onreadystatechange = null; this.onload = null; this.onerror = null; this.onabort = null;
|
|
209
|
+
this.__headers = {}; this.__method = 'GET'; this.__url = ''; this.__listeners = new Map(); this.__aborted = false;
|
|
210
|
+
}
|
|
211
|
+
open(method, url) { this.__method = method; this.__url = url; this.readyState = 1; this.__fire('readystatechange'); }
|
|
212
|
+
setRequestHeader(k, v) { this.__headers[k] = v; }
|
|
213
|
+
getResponseHeader() { return null; }
|
|
214
|
+
getAllResponseHeaders() { return ''; }
|
|
215
|
+
addEventListener(t, cb) { const l = this.__listeners.get(t) || []; l.push(cb); this.__listeners.set(t, l); }
|
|
216
|
+
removeEventListener(t, cb) { const l = this.__listeners.get(t); if (l) this.__listeners.set(t, l.filter((x) => x !== cb)); }
|
|
217
|
+
__fire(type) { const ev = { type, target: this }; if (typeof this['on' + type] === 'function') this['on' + type](ev); for (const cb of this.__listeners.get(type) || []) cb(ev); }
|
|
218
|
+
abort() { this.__aborted = true; this.readyState = 0; this.__fire('abort'); }
|
|
219
|
+
send(body) {
|
|
220
|
+
if (!globalThis.fetch) { this.readyState = 4; this.status = 0; this.__fire('error'); this.__fire('loadend'); return; }
|
|
221
|
+
globalThis.fetch(this.__url, { method: this.__method, headers: this.__headers, body }).then(async (res) => {
|
|
222
|
+
if (this.__aborted) return;
|
|
223
|
+
this.status = res.status; this.statusText = res.statusText; this.responseText = await res.text(); this.response = this.responseText;
|
|
224
|
+
this.readyState = 4; this.__fire('readystatechange'); this.__fire('load'); this.__fire('loadend');
|
|
225
|
+
}).catch(() => { if (this.__aborted) return; this.readyState = 4; this.status = 0; this.__fire('error'); this.__fire('loadend'); });
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
Binary file
|