ripple 0.2.108 → 0.2.110

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.
@@ -1,4 +1,4 @@
1
- /** @import { Block } from '#client' */
1
+ /** @import { Block, Tracked } from '#client' */
2
2
 
3
3
  import { IS_CONTROLLED, IS_INDEXED } from '../../../constants.js';
4
4
  import { branch, destroy_block, destroy_block_children, render } from './blocks.js';
@@ -12,31 +12,45 @@ import { array_from, is_array } from './utils.js';
12
12
  * @param {Node} anchor
13
13
  * @param {V} value
14
14
  * @param {number} index
15
- * @param {(anchor: Node, value: V, index?: any) => Block} render_fn
15
+ * @param {(anchor: Node, value: V | Tracked, index?: any) => Block} render_fn
16
16
  * @param {boolean} is_indexed
17
+ * @param {boolean} is_keyed
17
18
  * @returns {Block}
18
19
  */
19
- function create_item(anchor, value, index, render_fn, is_indexed) {
20
+ function create_item(anchor, value, index, render_fn, is_indexed, is_keyed) {
20
21
  var b = branch(() => {
21
22
  var tracked_index;
23
+ /** @type {V | Tracked} */
24
+ var tracked_value = value;
22
25
 
23
- if (is_indexed) {
26
+ if (is_indexed || is_keyed) {
24
27
  var block = /** @type {Block} */ (active_block);
25
28
 
26
29
  if (block.s === null) {
27
- tracked_index = tracked(index, block);
30
+ if (is_indexed) {
31
+ tracked_index = tracked(index, block);
32
+ }
33
+ if (is_keyed) {
34
+ tracked_value = tracked(value, block);
35
+ }
28
36
 
29
37
  block.s = {
30
38
  start: null,
31
39
  end: null,
32
40
  i: tracked_index,
41
+ v: tracked_value,
33
42
  };
34
43
  } else {
35
- tracked_index = block.s.i;
44
+ if (is_indexed) {
45
+ tracked_index = block.s.i;
46
+ }
47
+ if (is_keyed) {
48
+ tracked_index = block.s.v;
49
+ }
36
50
  }
37
- render_fn(anchor, value, tracked_index);
51
+ render_fn(anchor, tracked_value, tracked_index);
38
52
  } else {
39
- render_fn(anchor, value);
53
+ render_fn(anchor, tracked_value);
40
54
  }
41
55
  });
42
56
  return b;
@@ -87,7 +101,7 @@ function collection_to_array(collection) {
87
101
  * @template V
88
102
  * @param {Element} node
89
103
  * @param {() => V[] | Iterable<V>} get_collection
90
- * @param {(anchor: Node, value: V, index?: any) => Block} render_fn
104
+ * @param {(anchor: Node, value: V | Tracked, index?: any) => Block} render_fn
91
105
  * @param {number} flags
92
106
  * @returns {void}
93
107
  */
@@ -116,7 +130,7 @@ export function for_block(node, get_collection, render_fn, flags) {
116
130
  * @template K
117
131
  * @param {Element} node
118
132
  * @param {() => V[] | Iterable<V>} get_collection
119
- * @param {(anchor: Node, value: V, index?: any) => Block} render_fn
133
+ * @param {(anchor: Node, value: V | Tracked, index?: any) => Block} render_fn
120
134
  * @param {number} flags
121
135
  * @param {(item: V) => K} [get_key]
122
136
  * @returns {void}
@@ -175,13 +189,22 @@ function update_index(block, index) {
175
189
  set(block.s.i, index, block);
176
190
  }
177
191
 
192
+ /**
193
+ * @param {Block} block
194
+ * @param {any} value
195
+ * @returns {void}
196
+ */
197
+ function update_value(block, value) {
198
+ set(block.s.v, value, block);
199
+ }
200
+
178
201
  /**
179
202
  * @template V
180
203
  * @template K
181
204
  * @param {Element | Text} anchor
182
205
  * @param {Block} block
183
206
  * @param {V[]} b
184
- * @param {(anchor: Node, value: V, index?: any) => Block} render_fn
207
+ * @param {(anchor: Node, value: V | Tracked, index?: any) => Block} render_fn
185
208
  * @param {boolean} is_controlled
186
209
  * @param {boolean} is_indexed
187
210
  * @param {(item: V) => K} get_key
@@ -236,7 +259,7 @@ function reconcile_by_key(anchor, block, b, render_fn, is_controlled, is_indexed
236
259
  // Fast-path for create
237
260
  if (a_length === 0) {
238
261
  for (; j < b_length; j++) {
239
- b_blocks[j] = create_item(anchor, b[j], j, render_fn, is_indexed);
262
+ b_blocks[j] = create_item(anchor, b[j], j, render_fn, is_indexed, true);
240
263
  }
241
264
  state.array = b;
242
265
  state.blocks = b_blocks;
@@ -261,6 +284,7 @@ function reconcile_by_key(anchor, block, b, render_fn, is_controlled, is_indexed
261
284
  if (is_indexed) {
262
285
  update_index(b_block, j);
263
286
  }
287
+ update_value(b_block, b_val);
264
288
  ++j;
265
289
  if (j > a_end || j > b_end) {
266
290
  break outer;
@@ -282,6 +306,7 @@ function reconcile_by_key(anchor, block, b, render_fn, is_controlled, is_indexed
282
306
  if (is_indexed) {
283
307
  update_index(b_block, b_end);
284
308
  }
309
+ update_value(b_block, b_val);
285
310
  a_end--;
286
311
  b_end--;
287
312
  if (j > a_end || j > b_end) {
@@ -301,7 +326,7 @@ function reconcile_by_key(anchor, block, b, render_fn, is_controlled, is_indexed
301
326
  while (j <= b_end) {
302
327
  b_val = b[j];
303
328
  var target = j >= a_length ? anchor : a_blocks[j].s.start;
304
- b_blocks[j] = create_item(target, b_val, j, render_fn, is_indexed);
329
+ b_blocks[j] = create_item(target, b_val, j, render_fn, is_indexed, true);
305
330
  j++;
306
331
  }
307
332
  }
@@ -348,6 +373,7 @@ function reconcile_by_key(anchor, block, b, render_fn, is_controlled, is_indexed
348
373
  if (is_indexed) {
349
374
  update_index(b_block, j);
350
375
  }
376
+ update_value(b_block, b_val);
351
377
  ++patched;
352
378
  break;
353
379
  }
@@ -387,9 +413,11 @@ function reconcile_by_key(anchor, block, b, render_fn, is_controlled, is_indexed
387
413
  pos = j;
388
414
  }
389
415
  block = b_blocks[j] = a_blocks[i];
416
+ b_val = b[j];
390
417
  if (is_indexed) {
391
418
  update_index(block, j);
392
419
  }
420
+ update_value(b_block, b_val);
393
421
  ++patched;
394
422
  } else if (!fast_path_removal) {
395
423
  destroy_block(a_blocks[i]);
@@ -417,7 +445,7 @@ function reconcile_by_key(anchor, block, b, render_fn, is_controlled, is_indexed
417
445
  next_pos = pos + 1;
418
446
 
419
447
  var target = next_pos < b_length ? b_blocks[next_pos].s.start : anchor;
420
- b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed);
448
+ b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed, true);
421
449
  } else if (j < 0 || i !== seq[j]) {
422
450
  pos = i + b_start;
423
451
  b_val = b[pos];
@@ -437,7 +465,7 @@ function reconcile_by_key(anchor, block, b, render_fn, is_controlled, is_indexed
437
465
  next_pos = pos + 1;
438
466
 
439
467
  var target = next_pos < b_length ? b_blocks[next_pos].s.start : anchor;
440
- b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed);
468
+ b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed, true);
441
469
  }
442
470
  }
443
471
  }
@@ -452,7 +480,7 @@ function reconcile_by_key(anchor, block, b, render_fn, is_controlled, is_indexed
452
480
  * @param {Element | Text} anchor
453
481
  * @param {Block} block
454
482
  * @param {V[]} b
455
- * @param {(anchor: Node, value: V, index?: any) => Block} render_fn
483
+ * @param {(anchor: Node, value: V | Tracked, index?: any) => Block} render_fn
456
484
  * @param {boolean} is_controlled
457
485
  * @param {boolean} is_indexed
458
486
  * @returns {void}
@@ -505,7 +533,7 @@ function reconcile_by_ref(anchor, block, b, render_fn, is_controlled, is_indexed
505
533
  // Fast-path for create
506
534
  if (a_length === 0) {
507
535
  for (; j < b_length; j++) {
508
- b_blocks[j] = create_item(anchor, b[j], j, render_fn, is_indexed);
536
+ b_blocks[j] = create_item(anchor, b[j], j, render_fn, is_indexed, false);
509
537
  }
510
538
  state.array = b;
511
539
  state.blocks = b_blocks;
@@ -560,7 +588,7 @@ function reconcile_by_ref(anchor, block, b, render_fn, is_controlled, is_indexed
560
588
  while (j <= b_end) {
561
589
  b_val = b[j];
562
590
  var target = j >= a_length ? anchor : a_blocks[j].s.start;
563
- b_blocks[j] = create_item(target, b_val, j, render_fn, is_indexed);
591
+ b_blocks[j] = create_item(target, b_val, j, render_fn, is_indexed, false);
564
592
  j++;
565
593
  }
566
594
  }
@@ -673,7 +701,7 @@ function reconcile_by_ref(anchor, block, b, render_fn, is_controlled, is_indexed
673
701
  next_pos = pos + 1;
674
702
 
675
703
  var target = next_pos < b_length ? b_blocks[next_pos].s.start : anchor;
676
- b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed);
704
+ b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed, false);
677
705
  } else if (j < 0 || i !== seq[j]) {
678
706
  pos = i + b_start;
679
707
  b_val = b[pos];
@@ -693,7 +721,7 @@ function reconcile_by_ref(anchor, block, b, render_fn, is_controlled, is_indexed
693
721
  next_pos = pos + 1;
694
722
 
695
723
  var target = next_pos < b_length ? b_blocks[next_pos].s.start : anchor;
696
- b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed);
724
+ b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed, false);
697
725
  }
698
726
  }
699
727
  }
@@ -70,3 +70,5 @@ export { head } from './head.js';
70
70
  export { script } from './script.js';
71
71
 
72
72
  export { html } from './html.js';
73
+
74
+ export { rpc } from './rpc.js';
@@ -128,39 +128,66 @@ export function apply_styles(element, newStyles) {
128
128
  * @returns {void}
129
129
  */
130
130
  export function set_attributes(element, attributes) {
131
+ let found_enumerable_keys = false;
132
+
131
133
  for (const key in attributes) {
132
134
  if (key === 'children') continue;
135
+ found_enumerable_keys = true;
133
136
 
134
137
  let value = attributes[key];
135
-
136
138
  if (is_tracked_object(value)) {
137
139
  value = get(value);
138
140
  }
141
+ set_attribute_helper(element, key, value);
142
+ }
139
143
 
140
- if (key === 'class') {
141
- const is_html = element.namespaceURI === 'http://www.w3.org/1999/xhtml';
142
- set_class(/** @type {HTMLElement} */ (element), value, undefined, is_html);
143
- } else if (key === '#class') {
144
- // Special case for static class when spreading props
145
- element.classList.add(value);
146
- } else if (is_event_attribute(key)) {
147
- // Handle event handlers in spread props
148
- const event_name = get_attribute_event_name(key);
149
-
150
- if (is_delegated(event_name)) {
151
- // Use delegation for delegated events
152
- /** @type {any} */ (element)['__' + event_name] = value;
153
- delegate([event_name]);
154
- } else {
155
- // Use addEventListener for non-delegated events
156
- event(event_name, element, value);
144
+ // Only if no enumerable keys but attributes object exists
145
+ // This handles spread_props Proxy objects from dynamic elements with {...spread}
146
+ if (!found_enumerable_keys && attributes) {
147
+ const allKeys = Reflect.ownKeys(attributes);
148
+ for (const key of allKeys) {
149
+ if (key === 'children') continue;
150
+ if (typeof key === 'symbol') continue; // Skip symbols - handled by apply_element_spread
151
+
152
+ let value = attributes[key];
153
+ if (is_tracked_object(value)) {
154
+ value = get(value);
157
155
  }
158
- } else {
159
- set_attribute(element, key, value);
156
+ set_attribute_helper(element, key, value);
160
157
  }
161
158
  }
162
159
  }
163
160
 
161
+ /**
162
+ * Helper function to set a single attribute
163
+ * @param {Element} element
164
+ * @param {string} key
165
+ * @param {any} value
166
+ */
167
+ function set_attribute_helper(element, key, value) {
168
+ if (key === 'class') {
169
+ const is_html = element.namespaceURI === 'http://www.w3.org/1999/xhtml';
170
+ set_class(/** @type {HTMLElement} */ (element), value, undefined, is_html);
171
+ } else if (key === '#class') {
172
+ // Special case for static class when spreading props
173
+ element.classList.add(value);
174
+ } else if (typeof key === 'string' && is_event_attribute(key)) {
175
+ // Handle event handlers in spread props
176
+ const event_name = get_attribute_event_name(key);
177
+
178
+ if (is_delegated(event_name)) {
179
+ // Use delegation for delegated events
180
+ /** @type {any} */ (element)['__' + event_name] = value;
181
+ delegate([event_name]);
182
+ } else {
183
+ // Use addEventListener for non-delegated events
184
+ event(event_name, element, value);
185
+ }
186
+ } else {
187
+ set_attribute(element, key, value);
188
+ }
189
+ }
190
+
164
191
  /**
165
192
  * @param {import('clsx').ClassValue} value
166
193
  * @param {string} [hash]
@@ -0,0 +1,14 @@
1
+
2
+ /**
3
+ * @param {string} hash
4
+ * @param {any[]} args
5
+ */
6
+ export function rpc(hash, args) {
7
+ return fetch('/_$_ripple_rpc_$_/' + hash, {
8
+ method: 'POST',
9
+ headers: {
10
+ 'Content-Type': 'application/json'
11
+ },
12
+ body: JSON.stringify(args)
13
+ }).then(res => res.json());
14
+ }
@@ -220,6 +220,17 @@ export function export_default(declaration) {
220
220
  return { type: 'ExportDefaultDeclaration', declaration };
221
221
  }
222
222
 
223
+ /**
224
+ * @param {ESTree.Declaration | null} declaration
225
+ * @param {ESTree.ExportSpecifier[]} [specifiers]
226
+ * @param {ESTree.ImportAttribute[]} [attributes]
227
+ * @param {ESTree.Literal | null} [source]
228
+ * @returns {ESTree.ExportNamedDeclaration}
229
+ */
230
+ export function export_builder(declaration, specifiers = [], attributes = [], source = null) {
231
+ return { type: 'ExportNamedDeclaration', declaration, specifiers, attributes, source };
232
+ }
233
+
223
234
  /**
224
235
  * @param {ESTree.Identifier} id
225
236
  * @param {ESTree.Pattern[]} params
@@ -581,16 +592,17 @@ export function method(kind, key, params, body, computed = false, is_static = fa
581
592
  * @param {ESTree.Identifier | null} id
582
593
  * @param {ESTree.Pattern[]} params
583
594
  * @param {ESTree.BlockStatement} body
595
+ * @param {boolean} async
584
596
  * @returns {ESTree.FunctionExpression}
585
597
  */
586
- function function_builder(id, params, body) {
598
+ function function_builder(id, params, body, async = false) {
587
599
  return {
588
600
  type: 'FunctionExpression',
589
601
  id,
590
602
  params,
591
603
  body,
592
604
  generator: false,
593
- async: false,
605
+ async,
594
606
  metadata: /** @type {any} */ (null), // should not be used by codegen
595
607
  };
596
608
  }
@@ -812,6 +824,7 @@ export {
812
824
  let_builder as let,
813
825
  const_builder as const,
814
826
  var_builder as var,
827
+ export_builder as export,
815
828
  true_instance as true,
816
829
  false_instance as false,
817
830
  break_statement as break,
@@ -1,5 +1,18 @@
1
1
  // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
+ exports[`compiler success tests > compiles TSInstantiationExpression 1`] = `
4
+ "import * as _$_ from 'ripple/internal/client';
5
+
6
+ function makeBox(value) {
7
+ return { value };
8
+ }
9
+
10
+ const makeStringBox = (makeBox);
11
+ const stringBox = makeStringBox('abc');
12
+ const ErrorMap = (Map);
13
+ const errorMap = new ErrorMap();"
14
+ `;
15
+
3
16
  exports[`compiler success tests > compiles tracked values in effect with assignment expression 1`] = `"state.count = _$_.get(count);"`;
4
17
 
5
18
  exports[`compiler success tests > compiles tracked values in effect with update expressions 1`] = `
@@ -198,7 +198,7 @@ exports[`for statements > correctly handles the index in a for...of loop 3`] = `
198
198
  </div>
199
199
  `;
200
200
 
201
- exports[`for statements > handles reversing an array manually 1`] = `
201
+ exports[`for statements > handles updating with new objects with same key 1`] = `
202
202
  <div>
203
203
  <!---->
204
204
  <div>
@@ -218,17 +218,17 @@ exports[`for statements > handles reversing an array manually 1`] = `
218
218
  </div>
219
219
  `;
220
220
 
221
- exports[`for statements > handles reversing an array manually 2`] = `
221
+ exports[`for statements > handles updating with new objects with same key 2`] = `
222
222
  <div>
223
223
  <!---->
224
224
  <div>
225
- 0:Item 3
225
+ 0:Item 1!
226
226
  </div>
227
227
  <div>
228
- 1:Item 2
228
+ 1:Item 2!
229
229
  </div>
230
230
  <div>
231
- 2:Item 1
231
+ 2:Item 3!
232
232
  </div>
233
233
  <!---->
234
234
  <button>
@@ -495,4 +495,19 @@ effect(() => {
495
495
  const effectMatch = result.js.code.match(/effect\(\(\) => \{([\s\S]+?)\n\t\}\)\)/);
496
496
  expect(effectMatch[1].trim()).toMatchSnapshot();
497
497
  });
498
+
499
+ it('compiles TSInstantiationExpression', () => {
500
+ const source =
501
+ `function makeBox<T>(value: T) {
502
+ return { value };
503
+ }
504
+ const makeStringBox = makeBox<string>;
505
+ const stringBox = makeStringBox('abc');
506
+ const ErrorMap = Map<string, Error>;
507
+ const errorMap = new ErrorMap();`;
508
+
509
+ const result = compile(source, 'test.ripple', { mode: 'client' });
510
+
511
+ expect(result.js.code).toMatchSnapshot();
512
+ });
498
513
  });
@@ -659,4 +659,35 @@ describe('composite components', () => {
659
659
 
660
660
  expect(container.querySelector('#container').textContent).toBe('I am child 1');
661
661
  });
662
+
663
+ it('mutating a tracked value prop should work as intended', () => {
664
+ const logs = [];
665
+
666
+ component Counter({count}) {
667
+ effect(() => {
668
+ logs.push(@count);
669
+ })
670
+
671
+ <button onClick={() => @count = @count + 1}>{'+'}</button>
672
+ }
673
+
674
+ component App() {
675
+ const count = track(0);
676
+
677
+ <div>
678
+ <Counter count={count} />
679
+ </div>
680
+ }
681
+
682
+ render(App);
683
+ flushSync();
684
+
685
+ expect(logs).toEqual([0]);
686
+
687
+ const button = container.querySelector('button');
688
+ button.click();
689
+ flushSync();
690
+
691
+ expect(logs).toEqual([0, 1]);
692
+ })
662
693
  });
@@ -0,0 +1,207 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mount, flushSync, track, createRefKey } from 'ripple';
3
+
4
+ describe('dynamic DOM elements', () => {
5
+ let container;
6
+
7
+ function render(component) {
8
+ mount(component, {
9
+ target: container,
10
+ });
11
+ }
12
+
13
+ beforeEach(() => {
14
+ container = document.createElement('div');
15
+ document.body.appendChild(container);
16
+ });
17
+ afterEach(() => {
18
+ document.body.removeChild(container);
19
+ container = null;
20
+ });
21
+ it('renders static dynamic element', () => {
22
+ component App() {
23
+ let tag = track('div');
24
+
25
+ <@tag>{'Hello World'}</@tag>
26
+ }
27
+ render(App);
28
+
29
+ const element = container.querySelector('div');
30
+ expect(element).toBeTruthy();
31
+ expect(element.textContent).toBe('Hello World');
32
+ });
33
+ it('renders reactive dynamic element', () => {
34
+ component App() {
35
+ let tag = track('div');
36
+
37
+ <button onClick={() => {
38
+ @tag = 'span';
39
+ }}>{'Change Tag'}</button>
40
+ <@tag id="dynamic">{'Hello World'}</@tag>
41
+ }
42
+ render(App);
43
+ // Initially should be a div
44
+ let dynamicElement = container.querySelector('#dynamic');
45
+ expect(dynamicElement.tagName).toBe('DIV');
46
+ expect(dynamicElement.textContent).toBe('Hello World');
47
+ // Click button to change tag
48
+ const button = container.querySelector('button');
49
+ button.click();
50
+ flushSync();
51
+ // Should now be a span
52
+ dynamicElement = container.querySelector('#dynamic');
53
+ expect(dynamicElement.tagName).toBe('SPAN');
54
+ expect(dynamicElement.textContent).toBe('Hello World');
55
+ });
56
+ it('renders self-closing dynamic element', () => {
57
+ component App() {
58
+ let tag = track('input');
59
+
60
+ <@tag type="text" value="test" />
61
+ }
62
+ render(App);
63
+
64
+ const element = container.querySelector('input');
65
+ expect(element).toBeTruthy();
66
+ expect(element.type).toBe('text');
67
+ expect(element.value).toBe('test');
68
+ });
69
+ it('handles dynamic element with attributes', () => {
70
+ component App() {
71
+ let tag = track('div');
72
+ let className = track('test-class');
73
+
74
+ <@tag class={@className} id="test" data-testid="dynamic-element">{'Content'}</@tag>
75
+ }
76
+ render(App);
77
+
78
+ const element = container.querySelector('#test');
79
+ expect(element.tagName).toBe('DIV');
80
+ expect(element.className).toBe('test-class');
81
+ expect(element.getAttribute('data-testid')).toBe('dynamic-element');
82
+ expect(element.textContent).toBe('Content');
83
+ });
84
+ it('handles nested dynamic elements', () => {
85
+ component App() {
86
+ let outerTag = track('div');
87
+ let innerTag = track('span');
88
+
89
+ <@outerTag class="outer">
90
+ <@innerTag class="inner">{'Nested content'}</@innerTag>
91
+ </@outerTag>
92
+ }
93
+ render(App);
94
+
95
+ const outer = container.querySelector('.outer');
96
+ const inner = container.querySelector('.inner');
97
+
98
+ expect(outer.tagName).toBe('DIV');
99
+ expect(inner.tagName).toBe('SPAN');
100
+ expect(inner.textContent).toBe('Nested content');
101
+ expect(outer.contains(inner)).toBe(true);
102
+ });
103
+ it('handles dynamic element with class object', () => {
104
+ component App() {
105
+ let tag = track('div');
106
+ let active = track(true);
107
+
108
+ <@tag class={{ active: @active, 'dynamic-element': true }}>
109
+ {'Element with class object'}
110
+ </@tag>
111
+ }
112
+ render(App);
113
+
114
+ const element = container.querySelector('div');
115
+ expect(element).toBeTruthy();
116
+ expect(element.classList.contains('active')).toBe(true);
117
+ expect(element.classList.contains('dynamic-element')).toBe(true);
118
+ });
119
+ it('handles dynamic element with style object', () => {
120
+ component App() {
121
+ let tag = track('span');
122
+
123
+ <@tag style={{
124
+ color: 'red',
125
+ fontSize: '16px',
126
+ fontWeight: 'bold'
127
+ }}>
128
+ {'Styled dynamic element'}
129
+ </@tag>
130
+ }
131
+ render(App);
132
+
133
+ const element = container.querySelector('span');
134
+ expect(element).toBeTruthy();
135
+ expect(element.style.color).toBe('red');
136
+ expect(element.style.fontSize).toBe('16px');
137
+ expect(element.style.fontWeight).toBe('bold');
138
+ });
139
+ it('handles dynamic element with spread attributes', () => {
140
+ component App() {
141
+ let tag = track('section');
142
+ const attrs = {
143
+ id: 'spread-section',
144
+ 'data-testid': 'spread-test',
145
+ class: 'spread-class',
146
+ };
147
+
148
+ <@tag {...attrs} data-extra="additional">
149
+ {'Element with spread attributes'}
150
+ </@tag>
151
+ }
152
+ render(App);
153
+
154
+ const element = container.querySelector('section');
155
+ expect(element).toBeTruthy();
156
+ expect(element.id).toBe('spread-section');
157
+ expect(element.getAttribute('data-testid')).toBe('spread-test');
158
+ expect(element.className).toBe('spread-class');
159
+ expect(element.getAttribute('data-extra')).toBe('additional');
160
+ });
161
+ it('handles dynamic element with ref', () => {
162
+ let capturedElement = null;
163
+
164
+ component App() {
165
+ let tag = track('article');
166
+
167
+ <@tag {ref (node) => { capturedElement = node; }} id="ref-test">
168
+ {'Element with ref'}
169
+ </@tag>
170
+ }
171
+ render(App);
172
+ flushSync();
173
+ expect(capturedElement).toBeTruthy();
174
+ expect(capturedElement.tagName).toBe('ARTICLE');
175
+ expect(capturedElement.id).toBe('ref-test');
176
+ expect(capturedElement.textContent).toBe('Element with ref');
177
+ });
178
+ it('handles dynamic element with createRefKey in spread', () => {
179
+ component App() {
180
+ let tag = track('header');
181
+
182
+ function elementRef(node) {
183
+ // Set an attribute on the element to prove ref was called
184
+ node.setAttribute('data-spread-ref-called', 'true');
185
+ node.setAttribute('data-spread-ref-tag', node.tagName.toLowerCase());
186
+ }
187
+
188
+ const dynamicProps = {
189
+ id: 'spread-ref-test',
190
+ class: 'ref-element',
191
+ [createRefKey()]: elementRef
192
+ };
193
+
194
+ <@tag {...dynamicProps}>{'Element with spread ref'}</@tag>
195
+ }
196
+ render(App);
197
+ flushSync();
198
+
199
+ // Check that the spread ref was called by verifying attributes were set
200
+ const element = container.querySelector('header');
201
+ expect(element).toBeTruthy();
202
+ expect(element.getAttribute('data-spread-ref-called')).toBe('true');
203
+ expect(element.getAttribute('data-spread-ref-tag')).toBe('header');
204
+ expect(element.id).toBe('spread-ref-test');
205
+ expect(element.className).toBe('ref-element');
206
+ });
207
+ });