@miaskiewicz/turbo-dom 0.1.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.
@@ -0,0 +1,148 @@
1
+ // Layer 4 — aggressively but HONESTLY stub the unobservable. Headless runners
2
+ // have no layout, so we don't pretend. Honest absence over a plausible lie.
3
+
4
+ // FileReader — resolves async with a result, enough for upload-handling tests.
5
+ export class FileReader {
6
+ constructor() { this.result = null; this.error = null; this.readyState = 0; this.onload = null; this.onloadend = null; this.onerror = null; this.__listeners = new Map(); }
7
+ addEventListener(t, cb) { (this.__listeners.get(t) || this.__listeners.set(t, []).get(t)).push(cb); }
8
+ removeEventListener(t, cb) { const l = this.__listeners.get(t); if (l) this.__listeners.set(t, l.filter((x) => x !== cb)); }
9
+ __fire(type) {
10
+ const ev = { type, target: this };
11
+ if (typeof this['on' + type] === 'function') this['on' + type](ev);
12
+ for (const cb of this.__listeners.get(type) || []) cb(ev);
13
+ }
14
+ __read(blob, makeResult) {
15
+ this.readyState = 1;
16
+ Promise.resolve().then(async () => {
17
+ try { this.result = await makeResult(blob); } catch (e) { this.error = e; this.readyState = 2; this.__fire('error'); this.__fire('loadend'); return; }
18
+ this.readyState = 2; this.__fire('load'); this.__fire('loadend');
19
+ });
20
+ }
21
+ readAsText(blob) { this.__read(blob, (b) => (b && b.text ? b.text() : String(b))); }
22
+ readAsDataURL(blob) { this.__read(blob, async (b) => { const buf = Buffer.from(b && b.arrayBuffer ? await b.arrayBuffer() : []); return `data:${(b && b.type) || ''};base64,${buf.toString('base64')}`; }); }
23
+ readAsArrayBuffer(blob) { this.__read(blob, (b) => (b && b.arrayBuffer ? b.arrayBuffer() : new ArrayBuffer(0))); }
24
+ abort() { this.readyState = 2; this.__fire('abort'); }
25
+ }
26
+
27
+ // Canvas 2D context — no raster backend; methods are no-ops, measureText returns 0.
28
+ export function makeCanvasStub() {
29
+ const noop = () => {};
30
+ return new Proxy({}, {
31
+ get(_t, k) {
32
+ if (k === 'measureText') return (s) => ({ width: String(s).length * 6 });
33
+ if (k === 'getImageData') return () => ({ data: new Uint8ClampedArray(0), width: 0, height: 0 });
34
+ if (k === 'createLinearGradient' || k === 'createRadialGradient' || k === 'createPattern') return () => ({ addColorStop: noop });
35
+ if (k === 'canvas') return null;
36
+ return noop;
37
+ },
38
+ });
39
+ }
40
+
41
+ // CustomElementRegistry — define/get/whenDefined. No upgrade of generic elements,
42
+ // but enough that defining and awaiting elements doesn't throw.
43
+ export function makeCustomElements() {
44
+ const defs = new Map();
45
+ const waiters = new Map();
46
+ return {
47
+ define(name, ctor) {
48
+ if (defs.has(name)) throw new Error(`'${name}' already defined`);
49
+ defs.set(name, ctor);
50
+ const w = waiters.get(name); if (w) { w.forEach((r) => r(ctor)); waiters.delete(name); }
51
+ },
52
+ get(name) { return defs.get(name); },
53
+ getName(ctor) { for (const [n, c] of defs) if (c === ctor) return n; return null; },
54
+ whenDefined(name) { if (defs.has(name)) return Promise.resolve(defs.get(name)); return new Promise((res) => { const a = waiters.get(name) || []; a.push(res); waiters.set(name, a); }); },
55
+ upgrade() {},
56
+ };
57
+ }
58
+
59
+ export class Storage {
60
+ constructor() { this.__map = new Map(); }
61
+ get length() { return this.__map.size; }
62
+ key(i) { return [...this.__map.keys()][i] ?? null; }
63
+ getItem(k) { return this.__map.has(String(k)) ? this.__map.get(String(k)) : null; }
64
+ setItem(k, v) { this.__map.set(String(k), String(v)); }
65
+ removeItem(k) { this.__map.delete(String(k)); }
66
+ clear() { this.__map.clear(); }
67
+ }
68
+
69
+ export function makeMatchMedia() {
70
+ return (query) => ({
71
+ matches: false, // honest: no real media context in a headless runner
72
+ media: query,
73
+ onchange: null,
74
+ addEventListener() {},
75
+ removeEventListener() {},
76
+ addListener() {}, // deprecated, kept for compatibility
77
+ removeListener() {},
78
+ dispatchEvent() { return false; },
79
+ });
80
+ }
81
+
82
+ // getComputedStyle: honest — reflects ONLY inline + explicitly-set values, never
83
+ // invents cascade/layout numbers. A property that wasn't set reads as ''.
84
+ export function makeGetComputedStyle() {
85
+ return (el) => {
86
+ const style = el && el.style ? el.style : null;
87
+ return new Proxy({}, {
88
+ get(_t, key) {
89
+ if (key === 'getPropertyValue') return (p) => (style ? style.getPropertyValue(p) : '');
90
+ if (key === '__honest') return 'computed style is inline-only; no layout/cascade available';
91
+ if (typeof key !== 'string') return undefined;
92
+ return style ? style[key] : '';
93
+ },
94
+ });
95
+ };
96
+ }
97
+
98
+ class ObserverStub {
99
+ constructor(cb) { this.__cb = cb; }
100
+ observe() {}
101
+ unobserve() {}
102
+ disconnect() {}
103
+ takeRecords() { return []; }
104
+ }
105
+ export class IntersectionObserver extends ObserverStub {
106
+ constructor(cb) { super(cb); this.root = null; this.rootMargin = '0px'; this.thresholds = [0]; }
107
+ }
108
+ export class ResizeObserver extends ObserverStub {}
109
+
110
+ // MutationObserver: real enough to be useful — fires on observed mutations would
111
+ // require hooking every mutation; v1 is an honest queue-based stub that records
112
+ // nothing automatically. Documented as opt-in faithful later.
113
+ export class MutationObserver {
114
+ constructor(cb) { this.__cb = cb; this.__records = []; }
115
+ observe() {}
116
+ disconnect() {}
117
+ takeRecords() { const r = this.__records; this.__records = []; return r; }
118
+ }
119
+
120
+ export function makeLocation(href = 'http://localhost/') {
121
+ const u = new URL(href);
122
+ return {
123
+ get href() { return u.href; }, set href(v) { /* navigation is a no-op in headless */ },
124
+ get protocol() { return u.protocol; },
125
+ get host() { return u.host; },
126
+ get hostname() { return u.hostname; },
127
+ get port() { return u.port; },
128
+ get pathname() { return u.pathname; },
129
+ get search() { return u.search; },
130
+ get hash() { return u.hash; },
131
+ get origin() { return u.origin; },
132
+ assign() {}, replace() {}, reload() {}, toString() { return u.href; },
133
+ };
134
+ }
135
+
136
+ export function makeHistory(location) {
137
+ const stack = [{ state: null, url: location.href }];
138
+ let idx = 0;
139
+ return {
140
+ get length() { return stack.length; },
141
+ get state() { return stack[idx].state; },
142
+ pushState(state, _title, url) { stack.splice(idx + 1); stack.push({ state, url }); idx++; },
143
+ replaceState(state, _title, url) { stack[idx] = { state, url: url ?? stack[idx].url }; },
144
+ back() { if (idx > 0) idx--; },
145
+ forward() { if (idx < stack.length - 1) idx++; },
146
+ go(n) { idx = Math.max(0, Math.min(stack.length - 1, idx + (n || 0))); },
147
+ };
148
+ }
@@ -0,0 +1,168 @@
1
+ // Layer 3 — lazy `window`. A Proxy whose globals are factories that materialize
2
+ // on first `get` and self-replace with the concrete value (one-time Proxy cost
3
+ // per property). A test using only `document.querySelector` never constructs
4
+ // localStorage, IntersectionObserver, matchMedia, etc. The Proxy doubles as a
5
+ // tracer: it records which globals each test actually touches.
6
+
7
+ import {
8
+ Storage, makeMatchMedia, makeGetComputedStyle,
9
+ IntersectionObserver, ResizeObserver,
10
+ FileReader, makeCanvasStub, makeCustomElements,
11
+ makeLocation, makeHistory,
12
+ } from './stubs.mjs';
13
+ import {
14
+ Node, Element, Text, Comment, Document, DocumentFragment, DocumentType, Event, CustomEvent,
15
+ MutationObserver,
16
+ } from './dom.mjs';
17
+ import {
18
+ EventTarget,
19
+ UIEvent, MouseEvent, PointerEvent, KeyboardEvent, InputEvent, FocusEvent,
20
+ CompositionEvent, WheelEvent, TouchEvent, DragEvent, ProgressEvent,
21
+ } from './events.mjs';
22
+
23
+ // Minimal DataTransfer / clipboard primitives some libraries (user-event) need.
24
+ class DataTransfer {
25
+ constructor() { this.__data = new Map(); this.dropEffect = 'none'; this.effectAllowed = 'all'; this.items = []; this.files = []; this.types = []; }
26
+ setData(fmt, data) { this.__data.set(fmt, String(data)); if (!this.types.includes(fmt)) this.types.push(fmt); }
27
+ getData(fmt) { return this.__data.get(fmt) ?? ''; }
28
+ clearData(fmt) { if (fmt) this.__data.delete(fmt); else this.__data.clear(); }
29
+ }
30
+ class ClipboardEvent extends Event {
31
+ constructor(type, init = {}) { super(type, init); this.clipboardData = init.clipboardData ?? new DataTransfer(); }
32
+ }
33
+
34
+ // Capture host functions at module load — BEFORE any installGlobals() can shadow
35
+ // the bare names on globalThis (which would make these delegates call themselves).
36
+ const hostSetTimeout = globalThis.setTimeout;
37
+ const hostClearTimeout = globalThis.clearTimeout;
38
+ const hostSetInterval = globalThis.setInterval;
39
+ const hostClearInterval = globalThis.clearInterval;
40
+ const hostQueueMicrotask = globalThis.queueMicrotask;
41
+ const hostStructuredClone = globalThis.structuredClone;
42
+
43
+ export function createWindow(document, { url = 'http://localhost/' } = {}) {
44
+ const touched = new Set();
45
+ let windowProxy;
46
+
47
+ // Universal globals (touched by ~every test) — eager, no point lazifying.
48
+ const base = {
49
+ document,
50
+ name: '',
51
+ closed: false,
52
+ origin: new URL(url).origin,
53
+ // constructors are cheap class refs — expose eagerly
54
+ Node, Element, Text, Comment, Document, DocumentFragment, DocumentType,
55
+ EventTarget,
56
+ Event, CustomEvent,
57
+ UIEvent, MouseEvent, PointerEvent, KeyboardEvent, InputEvent, FocusEvent,
58
+ CompositionEvent, WheelEvent, TouchEvent, DragEvent, ProgressEvent, ClipboardEvent,
59
+ DataTransfer,
60
+ // generic elements are plain Element → `el instanceof HTMLElement` is true.
61
+ // HTMLIFrameElement MUST be a distinct class so React's iframe-descent loop
62
+ // (`while (el instanceof HTMLIFrameElement)`) terminates on normal elements.
63
+ HTMLElement: Element, SVGElement: Element,
64
+ HTMLIFrameElement: class HTMLIFrameElement extends Element {},
65
+ HTMLInputElement: Element, HTMLTextAreaElement: Element, HTMLSelectElement: Element,
66
+ HTMLOptionElement: Element, HTMLButtonElement: Element, HTMLAnchorElement: Element,
67
+ HTMLFormElement: Element, HTMLImageElement: Element, HTMLCanvasElement: Element,
68
+ HTMLTemplateElement: Element, HTMLLabelElement: Element, HTMLDivElement: Element,
69
+ HTMLSpanElement: Element, HTMLParagraphElement: Element, HTMLUListElement: Element,
70
+ HTMLLIElement: Element, HTMLHeadingElement: Element, HTMLBodyElement: Element,
71
+ HTMLDocument: Document, DocumentFragment, ShadowRoot: DocumentFragment,
72
+ MutationObserver,
73
+ URL: makeURL(), URLSearchParams,
74
+ Blob: globalThis.Blob, File: makeFile(), FileReader,
75
+ customElements: makeCustomElements(),
76
+ AbortController: globalThis.AbortController, AbortSignal: globalThis.AbortSignal,
77
+ TextEncoder: globalThis.TextEncoder, TextDecoder: globalThis.TextDecoder,
78
+ // timers delegate to the captured host fns (NOT the bare names — once these
79
+ // are installed on globalThis the bare names resolve back here → recursion)
80
+ setTimeout: (...a) => hostSetTimeout(...a),
81
+ clearTimeout: (...a) => hostClearTimeout(...a),
82
+ setInterval: (...a) => hostSetInterval(...a),
83
+ clearInterval: (...a) => hostClearInterval(...a),
84
+ queueMicrotask: (...a) => hostQueueMicrotask(...a),
85
+ structuredClone: (...a) => hostStructuredClone(...a),
86
+ getSelection: () => document.getSelection(),
87
+ scrollTo() {}, scroll() {}, scrollBy() {},
88
+ alert() {}, confirm: () => false, prompt: () => null,
89
+ dispatchEvent: (e) => document.dispatchEvent(e),
90
+ addEventListener: (...a) => document.addEventListener(...a),
91
+ removeEventListener: (...a) => document.removeEventListener(...a),
92
+ };
93
+
94
+ // Lazy globals — none constructed until touched.
95
+ const lazy = {
96
+ localStorage: () => new Storage(),
97
+ sessionStorage: () => new Storage(),
98
+ matchMedia: () => makeMatchMedia(),
99
+ getComputedStyle: () => makeGetComputedStyle(),
100
+ IntersectionObserver: () => IntersectionObserver,
101
+ ResizeObserver: () => ResizeObserver,
102
+ requestAnimationFrame: () => (cb) => hostSetTimeout(() => cb(performanceNow()), 0),
103
+ cancelAnimationFrame: () => (id) => hostClearTimeout(id),
104
+ // subsystem grouping: history co-materializes with (and shares) location
105
+ location: () => makeLocation(url),
106
+ history: () => makeHistory(windowProxy.location),
107
+ navigator: () => ({ userAgent: 'turbo-dom/0.0.1', platform: 'turbo-dom', language: 'en-US', languages: ['en-US'], onLine: true }),
108
+ performance: () => ({ now: performanceNow, timeOrigin: 0, mark() {}, measure() {} }),
109
+ Storage: () => Storage,
110
+ devicePixelRatio: () => 1,
111
+ innerWidth: () => 1024,
112
+ innerHeight: () => 768,
113
+ };
114
+
115
+ windowProxy = new Proxy(base, {
116
+ get(t, k) {
117
+ if (k === 'window' || k === 'self' || k === 'globalThis' || k === 'parent' || k === 'top') return windowProxy;
118
+ if (k in t) return t[k];
119
+ const factory = lazy[k];
120
+ if (factory) {
121
+ touched.add(k);
122
+ const v = factory();
123
+ t[k] = v; // self-replace: subsequent reads skip the factory
124
+ return v;
125
+ }
126
+ return undefined;
127
+ },
128
+ set(t, k, v) { t[k] = v; return true; },
129
+ has(t, k) {
130
+ return k in t || k in lazy ||
131
+ k === 'window' || k === 'self' || k === 'globalThis' || k === 'parent' || k === 'top';
132
+ },
133
+ });
134
+
135
+ document.defaultView = windowProxy;
136
+
137
+ return {
138
+ window: windowProxy,
139
+ // which lazy globals this test materialized (the "DOM surface used" report)
140
+ touched: () => [...touched],
141
+ // every global name this window can provide (for environment adapters)
142
+ globalKeys: [...Object.keys(base), ...Object.keys(lazy)],
143
+ // Layer 5: drop materialized global slots, keep the class machinery warm.
144
+ resetGlobals() {
145
+ for (const k of touched) delete base[k];
146
+ touched.clear();
147
+ },
148
+ };
149
+ }
150
+
151
+ function performanceNow() {
152
+ const [s, ns] = process.hrtime();
153
+ return s * 1000 + ns / 1e6;
154
+ }
155
+
156
+ let __objUrlSeq = 0;
157
+ function makeURL() {
158
+ class TurboURL extends URL {}
159
+ TurboURL.createObjectURL = () => `blob:turbo-dom/${++__objUrlSeq}`;
160
+ TurboURL.revokeObjectURL = () => {};
161
+ return TurboURL;
162
+ }
163
+ function makeFile() {
164
+ const B = globalThis.Blob;
165
+ return class File extends B {
166
+ constructor(bits = [], name = 'file', opts = {}) { super(bits, opts); this.name = String(name); this.lastModified = opts.lastModified || 0; }
167
+ };
168
+ }