pulse-js-framework 1.7.3 → 1.7.5

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,920 @@
1
+ /**
2
+ * Pulse DOM Adapter - Abstraction layer for DOM operations
3
+ *
4
+ * This module provides a pluggable DOM abstraction that enables:
5
+ * - Server-Side Rendering (SSR) with virtual DOM implementations
6
+ * - Simplified testing without browser environment or heavy mocks
7
+ * - Platform-specific optimizations
8
+ *
9
+ * The adapter pattern decouples Pulse from direct browser DOM dependencies,
10
+ * allowing the same reactive code to run in Node.js, Deno, or custom environments.
11
+ */
12
+
13
+ // ============================================================================
14
+ // DOM Adapter Interface
15
+ // ============================================================================
16
+
17
+ /**
18
+ * Abstract DOM adapter interface.
19
+ * Implementations must provide all methods for DOM manipulation.
20
+ *
21
+ * @interface DOMAdapter
22
+ */
23
+
24
+ /**
25
+ * @typedef {Object} DOMAdapter
26
+ * @property {function(string): Element} createElement - Create an element
27
+ * @property {function(string): Text} createTextNode - Create a text node
28
+ * @property {function(string): Comment} createComment - Create a comment node
29
+ * @property {function(): DocumentFragment} createDocumentFragment - Create a fragment
30
+ * @property {function(string): Element|null} querySelector - Query the document
31
+ * @property {function(Element, string, string): void} setAttribute - Set attribute
32
+ * @property {function(Element, string): void} removeAttribute - Remove attribute
33
+ * @property {function(Element, string): string|null} getAttribute - Get attribute
34
+ * @property {function(Node, Node): void} appendChild - Append child to parent
35
+ * @property {function(Node, Node, Node): void} insertBefore - Insert before reference
36
+ * @property {function(Node): void} removeNode - Remove node from parent
37
+ * @property {function(Node): Node|null} getParentNode - Get parent node
38
+ * @property {function(Node): Node|null} getNextSibling - Get next sibling
39
+ * @property {function(Node): Node|null} getFirstChild - Get first child
40
+ * @property {function(Element, string): void} addClass - Add CSS class
41
+ * @property {function(Element, string): void} removeClass - Remove CSS class
42
+ * @property {function(Element, string, *): void} setStyle - Set style property
43
+ * @property {function(Element, string): *} getStyle - Get style property
44
+ * @property {function(Element, string, *): void} setProperty - Set DOM property
45
+ * @property {function(Element, string): *} getProperty - Get DOM property
46
+ * @property {function(Element, string, Function, Object=): void} addEventListener - Add event listener
47
+ * @property {function(Element, string, Function, Object=): void} removeEventListener - Remove event listener
48
+ * @property {function(Node, string): void} setTextContent - Set text content
49
+ * @property {function(Node): string} getTextContent - Get text content
50
+ * @property {function(*): boolean} isNode - Check if value is a Node
51
+ * @property {function(*): boolean} isElement - Check if value is an Element
52
+ * @property {function(Function): void} queueMicrotask - Queue a microtask
53
+ * @property {function(Function, number): number} setTimeout - Set timeout
54
+ * @property {function(number): void} clearTimeout - Clear timeout
55
+ */
56
+
57
+ // ============================================================================
58
+ // Browser DOM Adapter
59
+ // ============================================================================
60
+
61
+ /**
62
+ * Browser DOM adapter - default implementation using native browser APIs.
63
+ * This is the production adapter for client-side rendering.
64
+ *
65
+ * @implements {DOMAdapter}
66
+ */
67
+ export class BrowserDOMAdapter {
68
+ /**
69
+ * Create an element with the specified tag name.
70
+ * @param {string} tagName - The tag name (e.g., 'div', 'span')
71
+ * @returns {Element} The created element
72
+ */
73
+ createElement(tagName) {
74
+ return document.createElement(tagName);
75
+ }
76
+
77
+ /**
78
+ * Create a text node with the specified content.
79
+ * @param {string} text - The text content
80
+ * @returns {Text} The created text node
81
+ */
82
+ createTextNode(text) {
83
+ return document.createTextNode(text);
84
+ }
85
+
86
+ /**
87
+ * Create a comment node with the specified content.
88
+ * @param {string} data - The comment content
89
+ * @returns {Comment} The created comment node
90
+ */
91
+ createComment(data) {
92
+ return document.createComment(data);
93
+ }
94
+
95
+ /**
96
+ * Create a document fragment for batched DOM operations.
97
+ * @returns {DocumentFragment} The created fragment
98
+ */
99
+ createDocumentFragment() {
100
+ return document.createDocumentFragment();
101
+ }
102
+
103
+ /**
104
+ * Query the document for an element matching the selector.
105
+ * @param {string} selector - CSS selector
106
+ * @returns {Element|null} The matched element or null
107
+ */
108
+ querySelector(selector) {
109
+ return document.querySelector(selector);
110
+ }
111
+
112
+ /**
113
+ * Set an attribute on an element.
114
+ * @param {Element} element - Target element
115
+ * @param {string} name - Attribute name
116
+ * @param {string} value - Attribute value
117
+ */
118
+ setAttribute(element, name, value) {
119
+ element.setAttribute(name, value);
120
+ }
121
+
122
+ /**
123
+ * Remove an attribute from an element.
124
+ * @param {Element} element - Target element
125
+ * @param {string} name - Attribute name
126
+ */
127
+ removeAttribute(element, name) {
128
+ element.removeAttribute(name);
129
+ }
130
+
131
+ /**
132
+ * Get an attribute value from an element.
133
+ * @param {Element} element - Target element
134
+ * @param {string} name - Attribute name
135
+ * @returns {string|null} The attribute value or null
136
+ */
137
+ getAttribute(element, name) {
138
+ return element.getAttribute(name);
139
+ }
140
+
141
+ /**
142
+ * Append a child node to a parent.
143
+ * @param {Node} parent - Parent node
144
+ * @param {Node} child - Child node to append
145
+ */
146
+ appendChild(parent, child) {
147
+ parent.appendChild(child);
148
+ }
149
+
150
+ /**
151
+ * Insert a node before a reference node.
152
+ * @param {Node} parent - Parent node
153
+ * @param {Node} newNode - Node to insert
154
+ * @param {Node} refNode - Reference node (insert before this)
155
+ */
156
+ insertBefore(parent, newNode, refNode) {
157
+ parent.insertBefore(newNode, refNode);
158
+ }
159
+
160
+ /**
161
+ * Remove a node from its parent.
162
+ * @param {Node} node - Node to remove
163
+ */
164
+ removeNode(node) {
165
+ node.remove();
166
+ }
167
+
168
+ /**
169
+ * Get the parent node of a node.
170
+ * @param {Node} node - Target node
171
+ * @returns {Node|null} The parent node or null
172
+ */
173
+ getParentNode(node) {
174
+ return node.parentNode;
175
+ }
176
+
177
+ /**
178
+ * Get the next sibling of a node.
179
+ * @param {Node} node - Target node
180
+ * @returns {Node|null} The next sibling or null
181
+ */
182
+ getNextSibling(node) {
183
+ return node.nextSibling;
184
+ }
185
+
186
+ /**
187
+ * Get the first child of a node.
188
+ * @param {Node} node - Target node
189
+ * @returns {Node|null} The first child or null
190
+ */
191
+ getFirstChild(node) {
192
+ return node.firstChild;
193
+ }
194
+
195
+ /**
196
+ * Add a CSS class to an element.
197
+ * @param {Element} element - Target element
198
+ * @param {string} className - Class name to add
199
+ */
200
+ addClass(element, className) {
201
+ element.classList.add(className);
202
+ }
203
+
204
+ /**
205
+ * Remove a CSS class from an element.
206
+ * @param {Element} element - Target element
207
+ * @param {string} className - Class name to remove
208
+ */
209
+ removeClass(element, className) {
210
+ element.classList.remove(className);
211
+ }
212
+
213
+ /**
214
+ * Set a style property on an element.
215
+ * @param {Element} element - Target element
216
+ * @param {string} prop - Style property name
217
+ * @param {*} value - Style value
218
+ */
219
+ setStyle(element, prop, value) {
220
+ element.style[prop] = value;
221
+ }
222
+
223
+ /**
224
+ * Get a style property from an element.
225
+ * @param {Element} element - Target element
226
+ * @param {string} prop - Style property name
227
+ * @returns {*} The style value
228
+ */
229
+ getStyle(element, prop) {
230
+ return element.style[prop];
231
+ }
232
+
233
+ /**
234
+ * Set a DOM property on an element.
235
+ * @param {Element} element - Target element
236
+ * @param {string} prop - Property name
237
+ * @param {*} value - Property value
238
+ */
239
+ setProperty(element, prop, value) {
240
+ element[prop] = value;
241
+ }
242
+
243
+ /**
244
+ * Get a DOM property from an element.
245
+ * @param {Element} element - Target element
246
+ * @param {string} prop - Property name
247
+ * @returns {*} The property value
248
+ */
249
+ getProperty(element, prop) {
250
+ return element[prop];
251
+ }
252
+
253
+ /**
254
+ * Add an event listener to an element.
255
+ * @param {Element} element - Target element
256
+ * @param {string} event - Event name
257
+ * @param {Function} handler - Event handler
258
+ * @param {Object} [options] - Event listener options
259
+ */
260
+ addEventListener(element, event, handler, options) {
261
+ element.addEventListener(event, handler, options);
262
+ }
263
+
264
+ /**
265
+ * Remove an event listener from an element.
266
+ * @param {Element} element - Target element
267
+ * @param {string} event - Event name
268
+ * @param {Function} handler - Event handler
269
+ * @param {Object} [options] - Event listener options
270
+ */
271
+ removeEventListener(element, event, handler, options) {
272
+ element.removeEventListener(event, handler, options);
273
+ }
274
+
275
+ /**
276
+ * Set the text content of a node.
277
+ * @param {Node} node - Target node
278
+ * @param {string} text - Text content
279
+ */
280
+ setTextContent(node, text) {
281
+ node.textContent = text;
282
+ }
283
+
284
+ /**
285
+ * Get the text content of a node.
286
+ * @param {Node} node - Target node
287
+ * @returns {string} The text content
288
+ */
289
+ getTextContent(node) {
290
+ return node.textContent;
291
+ }
292
+
293
+ /**
294
+ * Check if a value is a Node.
295
+ * @param {*} value - Value to check
296
+ * @returns {boolean} True if value is a Node
297
+ */
298
+ isNode(value) {
299
+ return value instanceof Node;
300
+ }
301
+
302
+ /**
303
+ * Check if a value is an Element.
304
+ * @param {*} value - Value to check
305
+ * @returns {boolean} True if value is an Element
306
+ */
307
+ isElement(value) {
308
+ return value instanceof Element;
309
+ }
310
+
311
+ /**
312
+ * Queue a function to run as a microtask.
313
+ * @param {Function} fn - Function to queue
314
+ */
315
+ queueMicrotask(fn) {
316
+ queueMicrotask(fn);
317
+ }
318
+
319
+ /**
320
+ * Set a timeout.
321
+ * @param {Function} fn - Function to call
322
+ * @param {number} delay - Delay in milliseconds
323
+ * @returns {number} Timer ID
324
+ */
325
+ setTimeout(fn, delay) {
326
+ return setTimeout(fn, delay);
327
+ }
328
+
329
+ /**
330
+ * Clear a timeout.
331
+ * @param {number} timerId - Timer ID to clear
332
+ */
333
+ clearTimeout(timerId) {
334
+ clearTimeout(timerId);
335
+ }
336
+
337
+ /**
338
+ * Get the tag name of an element (lowercase).
339
+ * @param {Element} element - Target element
340
+ * @returns {string} The tag name
341
+ */
342
+ getTagName(element) {
343
+ return element.tagName.toLowerCase();
344
+ }
345
+
346
+ /**
347
+ * Get the type attribute of an input element.
348
+ * @param {Element} element - Target element
349
+ * @returns {string|undefined} The type attribute
350
+ */
351
+ getInputType(element) {
352
+ return element.type?.toLowerCase();
353
+ }
354
+ }
355
+
356
+ // ============================================================================
357
+ // Mock DOM Adapter for Testing
358
+ // ============================================================================
359
+
360
+ /**
361
+ * Simple mock node for testing without a browser environment.
362
+ * Provides minimal DOM-like interface for unit tests.
363
+ */
364
+ export class MockNode {
365
+ constructor(type, data = '') {
366
+ this.nodeType = type;
367
+ this.nodeName = '';
368
+ this.textContent = data;
369
+ this.parentNode = null;
370
+ this.childNodes = [];
371
+ this.nextSibling = null;
372
+ this.previousSibling = null;
373
+ this._eventListeners = new Map();
374
+ }
375
+
376
+ get firstChild() {
377
+ return this.childNodes[0] || null;
378
+ }
379
+
380
+ appendChild(child) {
381
+ if (child.parentNode) {
382
+ child.parentNode.removeChild(child);
383
+ }
384
+ child.parentNode = this;
385
+ if (this.childNodes.length > 0) {
386
+ const lastChild = this.childNodes[this.childNodes.length - 1];
387
+ lastChild.nextSibling = child;
388
+ child.previousSibling = lastChild;
389
+ }
390
+ child.nextSibling = null;
391
+ this.childNodes.push(child);
392
+ return child;
393
+ }
394
+
395
+ insertBefore(newNode, refNode) {
396
+ if (newNode.parentNode) {
397
+ newNode.parentNode.removeChild(newNode);
398
+ }
399
+ const index = refNode ? this.childNodes.indexOf(refNode) : this.childNodes.length;
400
+ if (index === -1) {
401
+ return this.appendChild(newNode);
402
+ }
403
+
404
+ newNode.parentNode = this;
405
+ this.childNodes.splice(index, 0, newNode);
406
+
407
+ // Update sibling references
408
+ this._updateSiblings();
409
+ return newNode;
410
+ }
411
+
412
+ removeChild(child) {
413
+ const index = this.childNodes.indexOf(child);
414
+ if (index !== -1) {
415
+ this.childNodes.splice(index, 1);
416
+ child.parentNode = null;
417
+ this._updateSiblings();
418
+ }
419
+ return child;
420
+ }
421
+
422
+ remove() {
423
+ if (this.parentNode) {
424
+ this.parentNode.removeChild(this);
425
+ }
426
+ }
427
+
428
+ _updateSiblings() {
429
+ for (let i = 0; i < this.childNodes.length; i++) {
430
+ const node = this.childNodes[i];
431
+ node.previousSibling = this.childNodes[i - 1] || null;
432
+ node.nextSibling = this.childNodes[i + 1] || null;
433
+ }
434
+ }
435
+
436
+ addEventListener(event, handler, options) {
437
+ if (!this._eventListeners.has(event)) {
438
+ this._eventListeners.set(event, []);
439
+ }
440
+ this._eventListeners.get(event).push({ handler, options });
441
+ }
442
+
443
+ removeEventListener(event, handler, options) {
444
+ const listeners = this._eventListeners.get(event);
445
+ if (listeners) {
446
+ const index = listeners.findIndex(l => l.handler === handler);
447
+ if (index !== -1) {
448
+ listeners.splice(index, 1);
449
+ }
450
+ }
451
+ }
452
+
453
+ dispatchEvent(event) {
454
+ const listeners = this._eventListeners.get(event.type);
455
+ if (listeners) {
456
+ for (const { handler } of listeners) {
457
+ handler(event);
458
+ }
459
+ }
460
+ }
461
+ }
462
+
463
+ /**
464
+ * Mock Element extending MockNode with element-specific features.
465
+ */
466
+ export class MockElement extends MockNode {
467
+ constructor(tagName) {
468
+ super(1); // Element node type
469
+ this.tagName = tagName.toUpperCase();
470
+ this.nodeName = this.tagName;
471
+ this.id = '';
472
+ this.className = '';
473
+ this._attributes = new Map();
474
+ this._style = {};
475
+ this.type = undefined;
476
+ this.value = '';
477
+ this.checked = false;
478
+ }
479
+
480
+ get classList() {
481
+ const self = this;
482
+ return {
483
+ add(className) {
484
+ const classes = self.className ? self.className.split(' ') : [];
485
+ if (!classes.includes(className)) {
486
+ classes.push(className);
487
+ self.className = classes.join(' ');
488
+ }
489
+ },
490
+ remove(className) {
491
+ const classes = self.className ? self.className.split(' ') : [];
492
+ const index = classes.indexOf(className);
493
+ if (index !== -1) {
494
+ classes.splice(index, 1);
495
+ self.className = classes.join(' ');
496
+ }
497
+ },
498
+ contains(className) {
499
+ const classes = self.className ? self.className.split(' ') : [];
500
+ return classes.includes(className);
501
+ },
502
+ toggle(className, force) {
503
+ if (force === undefined) {
504
+ force = !this.contains(className);
505
+ }
506
+ if (force) {
507
+ this.add(className);
508
+ } else {
509
+ this.remove(className);
510
+ }
511
+ return force;
512
+ }
513
+ };
514
+ }
515
+
516
+ get style() {
517
+ return this._style;
518
+ }
519
+
520
+ setAttribute(name, value) {
521
+ this._attributes.set(name, String(value));
522
+ if (name === 'id') this.id = value;
523
+ if (name === 'class') this.className = value;
524
+ if (name === 'type') this.type = value;
525
+ }
526
+
527
+ getAttribute(name) {
528
+ return this._attributes.get(name) ?? null;
529
+ }
530
+
531
+ removeAttribute(name) {
532
+ this._attributes.delete(name);
533
+ if (name === 'id') this.id = '';
534
+ if (name === 'class') this.className = '';
535
+ }
536
+
537
+ hasAttribute(name) {
538
+ return this._attributes.has(name);
539
+ }
540
+ }
541
+
542
+ /**
543
+ * Mock Text node for testing.
544
+ */
545
+ export class MockTextNode extends MockNode {
546
+ constructor(text) {
547
+ super(3); // Text node type
548
+ this.nodeName = '#text';
549
+ this.textContent = text;
550
+ this.data = text;
551
+ }
552
+ }
553
+
554
+ /**
555
+ * Mock Comment node for testing.
556
+ */
557
+ export class MockCommentNode extends MockNode {
558
+ constructor(data) {
559
+ super(8); // Comment node type
560
+ this.nodeName = '#comment';
561
+ this.textContent = data;
562
+ this.data = data;
563
+ }
564
+ }
565
+
566
+ /**
567
+ * Mock DocumentFragment for testing.
568
+ */
569
+ export class MockDocumentFragment extends MockNode {
570
+ constructor() {
571
+ super(11); // Document fragment node type
572
+ this.nodeName = '#document-fragment';
573
+ }
574
+ }
575
+
576
+ /**
577
+ * Mock DOM adapter for testing without browser environment.
578
+ * Provides a lightweight, synchronous DOM simulation.
579
+ *
580
+ * @implements {DOMAdapter}
581
+ */
582
+ export class MockDOMAdapter {
583
+ constructor() {
584
+ this._document = new MockElement('html');
585
+ this._body = new MockElement('body');
586
+ this._document.appendChild(this._body);
587
+ this._timers = new Map();
588
+ this._timerIdCounter = 0;
589
+ this._microtaskQueue = [];
590
+ }
591
+
592
+ createElement(tagName) {
593
+ return new MockElement(tagName);
594
+ }
595
+
596
+ createTextNode(text) {
597
+ return new MockTextNode(text);
598
+ }
599
+
600
+ createComment(data) {
601
+ return new MockCommentNode(data);
602
+ }
603
+
604
+ createDocumentFragment() {
605
+ return new MockDocumentFragment();
606
+ }
607
+
608
+ querySelector(selector) {
609
+ // Simple selector matching for testing
610
+ // In production, you'd want a more complete implementation
611
+ return this._findElement(this._body, selector);
612
+ }
613
+
614
+ _findElement(root, selector) {
615
+ // Basic ID selector support
616
+ if (selector.startsWith('#')) {
617
+ const id = selector.slice(1);
618
+ return this._findById(root, id);
619
+ }
620
+ // Basic class selector support
621
+ if (selector.startsWith('.')) {
622
+ const className = selector.slice(1);
623
+ return this._findByClass(root, className);
624
+ }
625
+ // Basic tag selector support
626
+ return this._findByTag(root, selector);
627
+ }
628
+
629
+ _findById(node, id) {
630
+ if (node.id === id) return node;
631
+ for (const child of node.childNodes || []) {
632
+ const found = this._findById(child, id);
633
+ if (found) return found;
634
+ }
635
+ return null;
636
+ }
637
+
638
+ _findByClass(node, className) {
639
+ if (node.classList?.contains(className)) return node;
640
+ for (const child of node.childNodes || []) {
641
+ const found = this._findByClass(child, className);
642
+ if (found) return found;
643
+ }
644
+ return null;
645
+ }
646
+
647
+ _findByTag(node, tagName) {
648
+ if (node.tagName?.toLowerCase() === tagName.toLowerCase()) return node;
649
+ for (const child of node.childNodes || []) {
650
+ const found = this._findByTag(child, tagName);
651
+ if (found) return found;
652
+ }
653
+ return null;
654
+ }
655
+
656
+ setAttribute(element, name, value) {
657
+ element.setAttribute(name, value);
658
+ }
659
+
660
+ removeAttribute(element, name) {
661
+ element.removeAttribute(name);
662
+ }
663
+
664
+ getAttribute(element, name) {
665
+ return element.getAttribute(name);
666
+ }
667
+
668
+ appendChild(parent, child) {
669
+ // Handle DocumentFragment - move all children
670
+ if (child instanceof MockDocumentFragment) {
671
+ const children = [...child.childNodes];
672
+ for (const c of children) {
673
+ parent.appendChild(c);
674
+ }
675
+ return child;
676
+ }
677
+ return parent.appendChild(child);
678
+ }
679
+
680
+ insertBefore(parent, newNode, refNode) {
681
+ // Handle DocumentFragment - move all children
682
+ if (newNode instanceof MockDocumentFragment) {
683
+ const children = [...newNode.childNodes];
684
+ for (const c of children) {
685
+ parent.insertBefore(c, refNode);
686
+ }
687
+ return newNode;
688
+ }
689
+ return parent.insertBefore(newNode, refNode);
690
+ }
691
+
692
+ removeNode(node) {
693
+ node.remove();
694
+ }
695
+
696
+ getParentNode(node) {
697
+ return node.parentNode;
698
+ }
699
+
700
+ getNextSibling(node) {
701
+ return node.nextSibling;
702
+ }
703
+
704
+ getFirstChild(node) {
705
+ return node.firstChild;
706
+ }
707
+
708
+ addClass(element, className) {
709
+ element.classList.add(className);
710
+ }
711
+
712
+ removeClass(element, className) {
713
+ element.classList.remove(className);
714
+ }
715
+
716
+ setStyle(element, prop, value) {
717
+ element.style[prop] = value;
718
+ }
719
+
720
+ getStyle(element, prop) {
721
+ return element.style[prop];
722
+ }
723
+
724
+ setProperty(element, prop, value) {
725
+ element[prop] = value;
726
+ }
727
+
728
+ getProperty(element, prop) {
729
+ return element[prop];
730
+ }
731
+
732
+ addEventListener(element, event, handler, options) {
733
+ element.addEventListener(event, handler, options);
734
+ }
735
+
736
+ removeEventListener(element, event, handler, options) {
737
+ element.removeEventListener(event, handler, options);
738
+ }
739
+
740
+ setTextContent(node, text) {
741
+ node.textContent = text;
742
+ if (node.data !== undefined) {
743
+ node.data = text;
744
+ }
745
+ }
746
+
747
+ getTextContent(node) {
748
+ return node.textContent;
749
+ }
750
+
751
+ isNode(value) {
752
+ return value instanceof MockNode;
753
+ }
754
+
755
+ isElement(value) {
756
+ return value instanceof MockElement;
757
+ }
758
+
759
+ queueMicrotask(fn) {
760
+ this._microtaskQueue.push(fn);
761
+ }
762
+
763
+ setTimeout(fn, delay) {
764
+ const id = ++this._timerIdCounter;
765
+ this._timers.set(id, { fn, delay, type: 'timeout' });
766
+ return id;
767
+ }
768
+
769
+ clearTimeout(timerId) {
770
+ this._timers.delete(timerId);
771
+ }
772
+
773
+ getTagName(element) {
774
+ return element.tagName.toLowerCase();
775
+ }
776
+
777
+ getInputType(element) {
778
+ return element.type?.toLowerCase();
779
+ }
780
+
781
+ // Test helpers
782
+
783
+ /**
784
+ * Flush all pending microtasks (synchronously for testing).
785
+ */
786
+ flushMicrotasks() {
787
+ while (this._microtaskQueue.length > 0) {
788
+ const fn = this._microtaskQueue.shift();
789
+ fn();
790
+ }
791
+ }
792
+
793
+ /**
794
+ * Run all pending timeouts (synchronously for testing).
795
+ */
796
+ runAllTimers() {
797
+ for (const [id, { fn }] of this._timers) {
798
+ fn();
799
+ this._timers.delete(id);
800
+ }
801
+ }
802
+
803
+ /**
804
+ * Get the mock document body for inspection.
805
+ */
806
+ getBody() {
807
+ return this._body;
808
+ }
809
+
810
+ /**
811
+ * Reset the mock DOM state.
812
+ */
813
+ reset() {
814
+ this._body.childNodes = [];
815
+ this._timers.clear();
816
+ this._microtaskQueue = [];
817
+ }
818
+ }
819
+
820
+ // ============================================================================
821
+ // Global Adapter Management
822
+ // ============================================================================
823
+
824
+ /**
825
+ * The currently active DOM adapter.
826
+ * Defaults to BrowserDOMAdapter in browser environments.
827
+ * @type {DOMAdapter}
828
+ */
829
+ let activeAdapter = null;
830
+
831
+ /**
832
+ * Get the currently active DOM adapter.
833
+ * Lazily initializes BrowserDOMAdapter if in browser environment.
834
+ *
835
+ * @returns {DOMAdapter} The active DOM adapter
836
+ * @throws {Error} If no adapter is set and not in browser environment
837
+ */
838
+ export function getAdapter() {
839
+ if (!activeAdapter) {
840
+ // Auto-initialize in browser environment
841
+ if (typeof document !== 'undefined') {
842
+ activeAdapter = new BrowserDOMAdapter();
843
+ } else {
844
+ throw new Error(
845
+ '[Pulse] No DOM adapter configured. ' +
846
+ 'In non-browser environments, call setAdapter() with a MockDOMAdapter or custom implementation.'
847
+ );
848
+ }
849
+ }
850
+ return activeAdapter;
851
+ }
852
+
853
+ /**
854
+ * Set the active DOM adapter.
855
+ * Use this to configure SSR, testing, or custom rendering targets.
856
+ *
857
+ * @param {DOMAdapter} adapter - The adapter to use
858
+ *
859
+ * @example
860
+ * // For SSR
861
+ * import { setAdapter, MockDOMAdapter } from 'pulse-js-framework/runtime/dom-adapter';
862
+ * setAdapter(new MockDOMAdapter());
863
+ *
864
+ * // For testing
865
+ * beforeEach(() => {
866
+ * setAdapter(new MockDOMAdapter());
867
+ * });
868
+ */
869
+ export function setAdapter(adapter) {
870
+ activeAdapter = adapter;
871
+ }
872
+
873
+ /**
874
+ * Reset the adapter to browser default (or null in non-browser).
875
+ * Useful for cleanup after tests.
876
+ */
877
+ export function resetAdapter() {
878
+ activeAdapter = null;
879
+ }
880
+
881
+ /**
882
+ * Run a function with a temporary DOM adapter.
883
+ * The previous adapter is restored after the function completes.
884
+ *
885
+ * @param {DOMAdapter} adapter - The adapter to use temporarily
886
+ * @param {Function} fn - The function to run
887
+ * @returns {*} The return value of fn
888
+ *
889
+ * @example
890
+ * const result = withAdapter(new MockDOMAdapter(), () => {
891
+ * return el('div.test', 'Hello');
892
+ * });
893
+ */
894
+ export function withAdapter(adapter, fn) {
895
+ const prevAdapter = activeAdapter;
896
+ activeAdapter = adapter;
897
+ try {
898
+ return fn();
899
+ } finally {
900
+ activeAdapter = prevAdapter;
901
+ }
902
+ }
903
+
904
+ // ============================================================================
905
+ // Exports
906
+ // ============================================================================
907
+
908
+ export default {
909
+ BrowserDOMAdapter,
910
+ MockDOMAdapter,
911
+ MockNode,
912
+ MockElement,
913
+ MockTextNode,
914
+ MockCommentNode,
915
+ MockDocumentFragment,
916
+ getAdapter,
917
+ setAdapter,
918
+ resetAdapter,
919
+ withAdapter
920
+ };