@miaskiewicz/turbo-dom 0.1.7 → 0.1.9

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.7",
3
+ "version": "0.1.9",
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",
@@ -17,9 +17,10 @@ import { installGlobals } from './install.mjs';
17
17
 
18
18
  const environment = {
19
19
  name: 'turbo-dom',
20
- // vitest 3/4 read `viteEnvironment`; older versions read `transformMode`.
20
+ // vitest 3/4 read `viteEnvironment` (this). vitest <3 used `transformMode: 'web'`
21
+ // — omitted here to avoid the v4 deprecation warning; on vitest <3 set
22
+ // `test.transformMode` (or add transformMode:'web' to this object) if needed.
21
23
  viteEnvironment: 'client',
22
- transformMode: 'web',
23
24
 
24
25
  setup(global, options) {
25
26
  const opts = (options && options.turboDom) || {};
@@ -573,14 +573,25 @@ export class Element extends Node {
573
573
  }
574
574
  }
575
575
  focus() {
576
- this.ownerDocument.__setActive(this);
577
- this.dispatchEvent(new Event('focus'));
578
- this.dispatchEvent(new Event('focusin', { bubbles: true }));
576
+ const doc = this.ownerDocument;
577
+ const prev = doc.__active;
578
+ if (prev === this) return;
579
+ // moving focus blurs the previously-focused element first
580
+ if (prev && prev !== doc.body && typeof prev.dispatchEvent === 'function') {
581
+ doc.__active = null;
582
+ prev.dispatchEvent(new FocusEvent('blur', { relatedTarget: this }));
583
+ prev.dispatchEvent(new FocusEvent('focusout', { bubbles: true, relatedTarget: this }));
584
+ }
585
+ doc.__setActive(this);
586
+ this.dispatchEvent(new FocusEvent('focus', { relatedTarget: prev || null }));
587
+ this.dispatchEvent(new FocusEvent('focusin', { bubbles: true, relatedTarget: prev || null }));
579
588
  }
580
589
  blur() {
581
- this.ownerDocument.__setActive(this.ownerDocument.body);
582
- this.dispatchEvent(new Event('blur'));
583
- this.dispatchEvent(new Event('focusout', { bubbles: true }));
590
+ const doc = this.ownerDocument;
591
+ if (doc.__active !== this) return;
592
+ doc.__setActive(doc.body);
593
+ this.dispatchEvent(new FocusEvent('blur'));
594
+ this.dispatchEvent(new FocusEvent('focusout', { bubbles: true }));
584
595
  }
585
596
  getBoundingClientRect() { return zeroRect(); }
586
597
  getClientRects() { return []; }
@@ -807,35 +818,42 @@ export class XMLSerializer {
807
818
 
808
819
  // minimal inline-style CSSOM (honest: only inline + explicitly set props)
809
820
  function makeStyle(el) {
821
+ // parse → { values: Map<prop,value>, prio: Map<prop,'important'|''> } (handles !important)
810
822
  const parse = () => {
811
- const map = new Map();
823
+ const values = new Map();
824
+ const prio = new Map();
812
825
  for (const decl of (el.getAttribute('style') || '').split(';')) {
813
826
  const i = decl.indexOf(':');
814
827
  if (i === -1) continue;
815
828
  const prop = decl.slice(0, i).trim();
816
- const val = decl.slice(i + 1).trim();
817
- if (prop) map.set(prop, val);
829
+ let val = decl.slice(i + 1).trim();
830
+ if (!prop) continue;
831
+ const m = /\s*!\s*important\s*$/i.exec(val);
832
+ if (m) { val = val.slice(0, m.index).trim(); prio.set(prop, 'important'); }
833
+ values.set(prop, val);
818
834
  }
819
- return map;
835
+ return { values, prio };
836
+ };
837
+ const write = ({ values, prio }) => {
838
+ el.setAttribute('style', [...values].map(([k, v]) => `${k}: ${v}${prio.get(k) === 'important' ? ' !important' : ''}`).join('; '));
820
839
  };
821
- const write = (map) => el.setAttribute('style', [...map].map(([k, v]) => `${k}: ${v}`).join('; '));
822
840
  return new Proxy({}, {
823
841
  get(_t, key) {
824
- if (key === 'getPropertyValue') return (p) => parse().get(p) ?? '';
825
- if (key === 'getPropertyPriority') return () => ''; // we don't model !important
826
- if (key === 'setProperty') return (p, v) => { const m = parse(); m.set(p, v); write(m); };
827
- if (key === 'removeProperty') return (p) => { const m = parse(); const v = m.get(p) ?? ''; m.delete(p); write(m); return v; };
828
- if (key === 'item') return (i) => [...parse().keys()][i] ?? '';
829
- if (key === 'length') return parse().size;
842
+ if (key === 'getPropertyValue') return (p) => parse().values.get(p) ?? '';
843
+ if (key === 'getPropertyPriority') return (p) => parse().prio.get(p) ?? '';
844
+ if (key === 'setProperty') return (p, v, priority) => { const s = parse(); s.values.set(p, String(v)); if (priority) s.prio.set(p, 'important'); else s.prio.delete(p); write(s); };
845
+ if (key === 'removeProperty') return (p) => { const s = parse(); const v = s.values.get(p) ?? ''; s.values.delete(p); s.prio.delete(p); write(s); return v; };
846
+ if (key === 'item') return (i) => [...parse().values.keys()][i] ?? '';
847
+ if (key === 'length') return parse().values.size;
830
848
  if (key === 'cssText') return el.getAttribute('style') || '';
831
- if (key === 'cssFloat') return parse().get('float') ?? '';
832
- if (key === Symbol.iterator) { const keys = [...parse().keys()]; return keys[Symbol.iterator].bind(keys); }
849
+ if (key === 'cssFloat') return parse().values.get('float') ?? '';
850
+ if (key === Symbol.iterator) { const keys = [...parse().values.keys()]; return keys[Symbol.iterator].bind(keys); }
833
851
  if (typeof key !== 'string') return undefined;
834
- return parse().get(kebab(key)) ?? '';
852
+ return parse().values.get(kebab(key)) ?? '';
835
853
  },
836
854
  set(_t, key, value) {
837
855
  if (key === 'cssText') { el.setAttribute('style', String(value)); return true; }
838
- const m = parse(); m.set(kebab(key), String(value)); write(m); return true;
856
+ const s = parse(); s.values.set(kebab(key), String(value)); write(s); return true;
839
857
  },
840
858
  has(_t, key) { return typeof key === 'string'; },
841
859
  });
@@ -31,6 +31,32 @@ class ClipboardEvent extends Event {
31
31
  constructor(type, init = {}) { super(type, init); this.clipboardData = init.clipboardData ?? new DataTransfer(); }
32
32
  }
33
33
 
34
+ // FormData that preserves File/Blob identity. Node's global FormData clones
35
+ // entries into anonymous Blobs (losing the File reference + filename); tests that
36
+ // assert `fd.get('file') === file` need the original kept.
37
+ class TurboFormData {
38
+ constructor() { this.__entries = []; }
39
+ append(name, value, filename) { this.__entries.push([String(name), this.__wrap(value, filename)]); }
40
+ set(name, value, filename) { this.delete(name); this.append(name, value, filename); }
41
+ __wrap(value, filename) {
42
+ // string values coerce; File/Blob are kept by reference (optionally re-named)
43
+ if (value == null || typeof value !== 'object') return String(value);
44
+ if (filename !== undefined && globalThis.Blob && value instanceof globalThis.Blob && value.name !== filename) {
45
+ const F = makeFile(); return new F([value], String(filename), { type: value.type });
46
+ }
47
+ return value;
48
+ }
49
+ get(name) { const e = this.__entries.find((x) => x[0] === String(name)); return e ? e[1] : null; }
50
+ getAll(name) { return this.__entries.filter((x) => x[0] === String(name)).map((x) => x[1]); }
51
+ has(name) { return this.__entries.some((x) => x[0] === String(name)); }
52
+ delete(name) { this.__entries = this.__entries.filter((x) => x[0] !== String(name)); }
53
+ forEach(cb, thisArg) { for (const [k, v] of this.__entries) cb.call(thisArg, v, k, this); }
54
+ *entries() { for (const e of this.__entries) yield [e[0], e[1]]; }
55
+ *keys() { for (const e of this.__entries) yield e[0]; }
56
+ *values() { for (const e of this.__entries) yield e[1]; }
57
+ [Symbol.iterator]() { return this.entries(); }
58
+ }
59
+
34
60
  // Capture host functions at module load — BEFORE any installGlobals() can shadow
35
61
  // the bare names on globalThis (which would make these delegates call themselves).
36
62
  const hostSetTimeout = globalThis.setTimeout;
@@ -78,7 +104,7 @@ export function createWindow(document, { url = 'http://localhost/' } = {}) {
78
104
  // web platform globals Node already provides
79
105
  fetch: globalThis.fetch ? (...a) => globalThis.fetch(...a) : undefined,
80
106
  Headers: globalThis.Headers, Request: globalThis.Request, Response: globalThis.Response,
81
- FormData: globalThis.FormData, ReadableStream: globalThis.ReadableStream,
107
+ FormData: TurboFormData, ReadableStream: globalThis.ReadableStream,
82
108
  crypto: globalThis.crypto, Crypto: globalThis.Crypto, SubtleCrypto: globalThis.SubtleCrypto,
83
109
  btoa: (s) => Buffer.from(String(s), 'binary').toString('base64'),
84
110
  atob: (s) => Buffer.from(String(s), 'base64').toString('binary'),
@@ -102,6 +128,9 @@ export function createWindow(document, { url = 'http://localhost/' } = {}) {
102
128
  structuredClone: (...a) => hostStructuredClone(...a),
103
129
  getSelection: () => document.getSelection(),
104
130
  scrollTo() {}, scroll() {}, scrollBy() {},
131
+ // window methods libraries/tests spy on — must exist as own props for vi.spyOn
132
+ open: () => null, close() {}, stop() {}, print() {}, focus() {}, blur() {},
133
+ moveTo() {}, moveBy() {}, resizeTo() {}, resizeBy() {},
105
134
  alert() {}, confirm: () => false, prompt: () => null,
106
135
  dispatchEvent: (e) => document.dispatchEvent(e),
107
136
  addEventListener: (...a) => document.addEventListener(...a),