@nordcraft/runtime 1.0.88 → 1.0.90

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.
Files changed (66) hide show
  1. package/dist/custom-element.main.esm.js +24 -24
  2. package/dist/custom-element.main.esm.js.map +4 -4
  3. package/dist/page.main.esm.js +3 -3
  4. package/dist/page.main.esm.js.map +4 -4
  5. package/dist/src/components/createComponent.js +37 -36
  6. package/dist/src/components/createComponent.js.map +1 -1
  7. package/dist/src/components/createElement.js +0 -2
  8. package/dist/src/components/createElement.js.map +1 -1
  9. package/dist/src/components/createNode.js +12 -4
  10. package/dist/src/components/createNode.js.map +1 -1
  11. package/dist/src/components/createNode.test.d.ts +1 -0
  12. package/dist/src/components/createNode.test.js +608 -0
  13. package/dist/src/components/createNode.test.js.map +1 -0
  14. package/dist/src/components/meta.d.ts +3 -0
  15. package/dist/src/components/meta.js +18 -0
  16. package/dist/src/components/meta.js.map +1 -0
  17. package/dist/src/components/meta.test.d.ts +1 -0
  18. package/dist/src/components/meta.test.js +80 -0
  19. package/dist/src/components/meta.test.js.map +1 -0
  20. package/dist/src/components/renderComponent.js +2 -4
  21. package/dist/src/components/renderComponent.js.map +1 -1
  22. package/dist/src/editor/postMessageToEditor.d.ts +2 -0
  23. package/dist/src/editor/postMessageToEditor.js +4 -0
  24. package/dist/src/editor/postMessageToEditor.js.map +1 -0
  25. package/dist/src/editor-preview.main.js +12 -13
  26. package/dist/src/editor-preview.main.js.map +1 -1
  27. package/dist/src/page.main.js +46 -59
  28. package/dist/src/page.main.js.map +1 -1
  29. package/dist/src/styles/CustomPropertyStyleSheet.d.ts +0 -1
  30. package/dist/src/styles/CustomPropertyStyleSheet.js +1 -1
  31. package/dist/src/styles/CustomPropertyStyleSheet.js.map +1 -1
  32. package/dist/src/styles/CustomPropertyStyleSheet.test.js +2 -5
  33. package/dist/src/styles/CustomPropertyStyleSheet.test.js.map +1 -1
  34. package/dist/src/utils/BatchQueue.d.ts +1 -0
  35. package/dist/src/utils/BatchQueue.js +2 -1
  36. package/dist/src/utils/BatchQueue.js.map +1 -1
  37. package/dist/src/utils/getComponent.d.ts +5 -0
  38. package/dist/src/utils/getComponent.js +8 -0
  39. package/dist/src/utils/getComponent.js.map +1 -0
  40. package/dist/src/utils/getComponent.test.d.ts +1 -0
  41. package/dist/src/utils/getComponent.test.js +24 -0
  42. package/dist/src/utils/getComponent.test.js.map +1 -0
  43. package/dist/src/utils/markSelectedElement.d.ts +1 -0
  44. package/dist/src/utils/markSelectedElement.js +9 -0
  45. package/dist/src/utils/markSelectedElement.js.map +1 -0
  46. package/dist/src/utils/subscribeCustomProperty.d.ts +3 -3
  47. package/dist/src/utils/subscribeCustomProperty.js +2 -3
  48. package/dist/src/utils/subscribeCustomProperty.js.map +1 -1
  49. package/package.json +3 -3
  50. package/src/components/createComponent.ts +57 -51
  51. package/src/components/createElement.ts +0 -2
  52. package/src/components/createNode.test.ts +686 -0
  53. package/src/components/createNode.ts +17 -6
  54. package/src/components/meta.test.ts +90 -0
  55. package/src/components/meta.ts +23 -0
  56. package/src/components/renderComponent.ts +2 -4
  57. package/src/editor/postMessageToEditor.ts +5 -0
  58. package/src/editor-preview.main.ts +12 -15
  59. package/src/page.main.ts +47 -59
  60. package/src/styles/CustomPropertyStyleSheet.test.ts +2 -7
  61. package/src/styles/CustomPropertyStyleSheet.ts +1 -2
  62. package/src/utils/BatchQueue.ts +2 -2
  63. package/src/utils/getComponent.test.ts +29 -0
  64. package/src/utils/getComponent.ts +15 -0
  65. package/src/utils/markSelectedElement.ts +8 -0
  66. package/src/utils/subscribeCustomProperty.ts +1 -5
@@ -0,0 +1,686 @@
1
+ import type { ComponentData } from '@nordcraft/core/dist/component/component.types'
2
+ import type { ToddleEnv } from '@nordcraft/core/dist/formula/formula'
3
+ import { describe, expect, test } from 'bun:test'
4
+ import '../../happydom'
5
+ import { signal } from '../signal/signal'
6
+ import type { ComponentContext } from '../types'
7
+ import { customPropertiesStylesheet } from '../utils/subscribeCustomProperty'
8
+ import { createNode } from './createNode'
9
+
10
+ describe('createNode()', () => {
11
+ test('it can render basic nodes', () => {
12
+ const parentElement = document.createElement('div')
13
+ const nodes = createNode({
14
+ ctx: {
15
+ isRootComponent: false,
16
+ component: {
17
+ name: 'My Component',
18
+ nodes: {
19
+ 'test-node-id': {
20
+ type: 'element',
21
+ tag: 'div',
22
+ children: ['test-node-id.0', 'test-node-id.1'],
23
+ attrs: {},
24
+ events: {},
25
+ },
26
+ 'test-node-id.0': {
27
+ type: 'text',
28
+ value: { type: 'value', value: 'Item 1' },
29
+ },
30
+ 'test-node-id.1': {
31
+ type: 'text',
32
+ value: { type: 'value', value: 'Item 2' },
33
+ },
34
+ },
35
+ },
36
+ } as Partial<ComponentContext> as any,
37
+ namespace: 'http://www.w3.org/1999/xhtml',
38
+ dataSignal: signal<ComponentData>({
39
+ Attributes: {},
40
+ Variables: {},
41
+ }),
42
+ path: 'test-node',
43
+ id: 'test-node-id',
44
+ parentElement,
45
+ instance: {},
46
+ })
47
+ expect(nodes.length).toBe(1)
48
+ expect((nodes[0] as Element).outerHTML).toMatchInlineSnapshot(
49
+ `"<div data-node-id="test-node-id" data-id="test-node" data-component="My Component" class="cYXIdv"><span data-node-id="test-node-id.0" data-id="test-node.0" data-component="My Component" data-node-type="text">Item 1</span><span data-node-id="test-node-id.1" data-id="test-node.1" data-component="My Component" data-node-type="text">Item 2</span></div>"`,
50
+ )
51
+ })
52
+
53
+ test('repeat nodes should keep the same node references when items are shuffled/added/removed', () => {
54
+ // In this test, we update the signal to change the order of the content in the repeat.
55
+ // We use repeatKey, so the nodes should simply be moved in the DOM instead of recreated.
56
+ const parentElement = document.createElement('div')
57
+ document.body.appendChild(parentElement)
58
+ const item1 = { id: 'i1', name: 'Item 1' }
59
+ const item2 = { id: 'i2', name: 'Item 2' }
60
+ const item3 = { id: 'i3', name: 'Item 3' }
61
+
62
+ const dataSignal = signal<ComponentData>({
63
+ Attributes: {},
64
+ Variables: {
65
+ items: [item1, item2, item3],
66
+ },
67
+ })
68
+
69
+ const ctx: ComponentContext = {
70
+ isRootComponent: false,
71
+ component: {
72
+ name: 'My Component',
73
+ nodes: {
74
+ 'repeat-node-id': {
75
+ type: 'element',
76
+ tag: 'div',
77
+ repeat: { type: 'path', path: ['Variables', 'items'] },
78
+ repeatKey: { type: 'path', path: ['ListItem', 'Item', 'id'] },
79
+ children: ['text-node-id'],
80
+ attrs: {},
81
+ events: {},
82
+ customProperties: {
83
+ '--color': {
84
+ syntax: {
85
+ type: 'primitive',
86
+ name: 'color',
87
+ },
88
+ formula: {
89
+ type: 'value',
90
+ value: 'red',
91
+ },
92
+ },
93
+ },
94
+ },
95
+ 'text-node-id': {
96
+ type: 'text',
97
+ value: { type: 'path', path: ['ListItem', 'Item', 'name'] },
98
+ },
99
+ },
100
+ },
101
+ root: document,
102
+ formulaCache: {},
103
+ env: { runtime: 'page' } as ToddleEnv,
104
+ } as Partial<ComponentContext> as ComponentContext
105
+
106
+ const nodes = createNode({
107
+ ctx,
108
+ namespace: 'http://www.w3.org/1999/xhtml',
109
+ dataSignal,
110
+ path: '0.0.0',
111
+ id: 'repeat-node-id',
112
+ parentElement,
113
+ instance: {},
114
+ })
115
+
116
+ // Custom properties stylesheet should be created and have the custom property from the node
117
+ const sheet = customPropertiesStylesheet?.getStyleSheet()
118
+ expect(sheet).toBeTruthy()
119
+ expect(sheet?.cssRules.length).toBe(3)
120
+ expect(sheet?.cssRules[0].cssText).toBe(
121
+ '[data-id="0.0.0"] { --color: red; }',
122
+ )
123
+
124
+ parentElement.append(...nodes)
125
+
126
+ expect(parentElement.children.length).toBe(3)
127
+ const element1 = parentElement.children[0]
128
+ const element2 = parentElement.children[1]
129
+ const element3 = parentElement.children[2]
130
+
131
+ expect(element1.textContent).toBe('Item 1')
132
+ expect(element1.getAttribute('data-id')).toBe('0.0.0')
133
+ expect(element2.textContent).toBe('Item 2')
134
+ expect(element2.getAttribute('data-id')).toBe('0.0.0(1)')
135
+ expect(element3.textContent).toBe('Item 3')
136
+ expect(element3.getAttribute('data-id')).toBe('0.0.0(2)')
137
+
138
+ // Shuffle the items: [3, 1, 2]
139
+ dataSignal.update((data) => {
140
+ return {
141
+ ...data,
142
+ Variables: {
143
+ ...data.Variables,
144
+ items: [item3, item1, item2],
145
+ },
146
+ }
147
+ })
148
+
149
+ // After update, elements should be shuffled but the same objects
150
+ expect(parentElement.children.length).toBe(3)
151
+
152
+ // Check text content first
153
+ expect(parentElement.children[0].textContent).toBe('Item 3')
154
+ expect(parentElement.children[1].textContent).toBe('Item 1')
155
+ expect(parentElement.children[2].textContent).toBe('Item 2')
156
+
157
+ // Check identities (do not use test-toBe for DOM nodes is annoying to compare and we are only interested in the reference match)
158
+ expect(parentElement.children[0] === element3).toBeTruthy()
159
+ expect(parentElement.children[0].getAttribute('data-id')).toBe(`0.0.0(2)`)
160
+ expect(parentElement.children[1] === element1).toBeTruthy()
161
+ expect(parentElement.children[1].getAttribute('data-id')).toBe(`0.0.0`)
162
+ expect(parentElement.children[2] === element2).toBeTruthy()
163
+ expect(parentElement.children[2].getAttribute('data-id')).toBe(`0.0.0(1)`)
164
+
165
+ // Remove last item in the list
166
+ dataSignal.update((data) => {
167
+ return {
168
+ ...data,
169
+ Variables: {
170
+ ...data.Variables,
171
+ items: [item3, item1],
172
+ },
173
+ }
174
+ })
175
+
176
+ expect(parentElement.children.length).toBe(2)
177
+ expect(parentElement.children[0] === element3).toBeTruthy()
178
+ expect(parentElement.children[0].getAttribute('data-id')).toBe(`0.0.0(2)`)
179
+ expect(parentElement.children[1] === element1).toBeTruthy()
180
+ expect(parentElement.children[1].getAttribute('data-id')).toBe(`0.0.0`)
181
+
182
+ // Make sure element 2 is removed from the DOM
183
+ expect(element2.parentElement === null).toBeTruthy()
184
+
185
+ // Should have cleaned the unused custom property from the stylesheet
186
+ expect(sheet?.cssRules.length).toBe(2)
187
+
188
+ // The rules should be correct for the remaining elements
189
+ expect(sheet?.cssRules[0].cssText).toBe(
190
+ '[data-id="0.0.0"] { --color: red; }',
191
+ )
192
+ expect(sheet?.cssRules[1].cssText).toBe(
193
+ '[data-id="0.0.0(2)"] { --color: red; }',
194
+ )
195
+
196
+ // Add the same item multiple times
197
+ dataSignal.update((data) => {
198
+ return {
199
+ ...data,
200
+ Variables: {
201
+ ...data.Variables,
202
+ items: [item3, item1, item3, item3],
203
+ },
204
+ }
205
+ })
206
+
207
+ // Should have 4 elements and each should have a different data-id, as a duplicate repeatKey should undo any reuse optimizations
208
+ expect(parentElement.children.length).toBe(4)
209
+ // Expect all data-ids to be unique
210
+ const dataIds = new Set<string>()
211
+ for (const child of parentElement.children) {
212
+ const dataId = child.getAttribute('data-id')
213
+ expect(dataId).toBeTruthy()
214
+ expect(
215
+ dataIds.has(dataId!),
216
+ `Duplicate data-id found: ${dataId}, all ids: ${[...parentElement.children].map((c) => c.getAttribute('data-id'))}`,
217
+ ).toBeFalsy()
218
+ dataIds.add(dataId!)
219
+ }
220
+
221
+ // Custom properties stylesheet should be cleaned up after all nodes are removed
222
+ dataSignal.destroy()
223
+ expect(sheet?.cssRules.length).toBe(0)
224
+ })
225
+
226
+ test('conditional nodes should remove and recreate elements on toggle', () => {
227
+ const parentElement = document.createElement('div')
228
+ document.body.appendChild(parentElement)
229
+ const dataSignal = signal<ComponentData>({
230
+ Attributes: { show: true },
231
+ })
232
+
233
+ const ctx = {
234
+ isRootComponent: false,
235
+ component: {
236
+ name: 'My Component',
237
+ nodes: {
238
+ 'conditional-node': {
239
+ type: 'element',
240
+ tag: 'div',
241
+ condition: { type: 'path', path: ['Attributes', 'show'] },
242
+ children: ['child-node'],
243
+ attrs: {},
244
+ events: {},
245
+ },
246
+ 'child-node': {
247
+ type: 'text',
248
+ value: { type: 'value', value: 'Hello' },
249
+ },
250
+ },
251
+ },
252
+ root: document,
253
+ env: { runtime: 'page' },
254
+ formulaCache: {},
255
+ toddle: {
256
+ getCustomFormula: () => undefined,
257
+ },
258
+ } as any as ComponentContext
259
+
260
+ const nodes = createNode({
261
+ ctx,
262
+ namespace: 'http://www.w3.org/1999/xhtml',
263
+ dataSignal,
264
+ path: 'cond',
265
+ id: 'conditional-node',
266
+ parentElement,
267
+ instance: {},
268
+ })
269
+
270
+ parentElement.append(...nodes)
271
+ expect(parentElement.children.length).toBe(1)
272
+ const firstElement = parentElement.children[0]
273
+ expect(firstElement.textContent).toBe('Hello')
274
+
275
+ // Toggle off
276
+ dataSignal.update((data) => ({
277
+ ...data,
278
+ Attributes: { ...data.Attributes, show: false },
279
+ }))
280
+ expect(parentElement.children.length).toBe(0)
281
+ expect(firstElement.parentElement).toBeNull()
282
+
283
+ // Toggle on again
284
+ dataSignal.update((data) => ({
285
+ ...data,
286
+ Attributes: { ...data.Attributes, show: true },
287
+ }))
288
+ expect(parentElement.children.length).toBe(1)
289
+ const secondElement = parentElement.children[0]
290
+ expect(secondElement.textContent).toBe('Hello')
291
+ // Identity should be different because conditional nodes recreate on toggle
292
+ expect(secondElement === firstElement).toBeFalsy()
293
+ })
294
+
295
+ test('nested repeats should maintain node references for unchanged items', () => {
296
+ const parentElement = document.createElement('div')
297
+ document.body.appendChild(parentElement)
298
+
299
+ const itemA = {
300
+ id: 'A',
301
+ subItems: [
302
+ { id: '1', val: 'A1' },
303
+ { id: '2', val: 'A2' },
304
+ ],
305
+ }
306
+ const itemB = {
307
+ id: 'B',
308
+ subItems: [
309
+ { id: '3', val: 'B3' },
310
+ { id: '4', val: 'B4' },
311
+ ],
312
+ }
313
+
314
+ const dataSignal = signal<ComponentData>({
315
+ Attributes: {},
316
+ Variables: {
317
+ items: [itemA, itemB],
318
+ },
319
+ })
320
+
321
+ const ctx = {
322
+ isRootComponent: false,
323
+ component: {
324
+ name: 'My Component',
325
+ nodes: {
326
+ 'outer-repeat': {
327
+ type: 'element',
328
+ tag: 'div',
329
+ repeat: { type: 'path', path: ['Variables', 'items'] },
330
+ repeatKey: { type: 'path', path: ['ListItem', 'Item', 'id'] },
331
+ children: ['inner-repeat'],
332
+ attrs: {
333
+ class: { type: 'path', path: ['ListItem', 'Item', 'id'] },
334
+ },
335
+ events: {},
336
+ },
337
+ 'inner-repeat': {
338
+ type: 'element',
339
+ tag: 'span',
340
+ repeat: { type: 'path', path: ['ListItem', 'Item', 'subItems'] },
341
+ repeatKey: { type: 'path', path: ['ListItem', 'Item', 'id'] },
342
+ children: ['text-node'],
343
+ attrs: {},
344
+ events: {},
345
+ },
346
+ 'text-node': {
347
+ type: 'text',
348
+ value: { type: 'path', path: ['ListItem', 'Item', 'val'] },
349
+ },
350
+ },
351
+ },
352
+ root: document,
353
+ env: { runtime: 'page' },
354
+ formulaCache: {},
355
+ toddle: {
356
+ getCustomFormula: () => undefined,
357
+ },
358
+ } as any as ComponentContext
359
+
360
+ const nodes = createNode({
361
+ ctx,
362
+ namespace: 'http://www.w3.org/1999/xhtml',
363
+ dataSignal,
364
+ path: 'nested',
365
+ id: 'outer-repeat',
366
+ parentElement,
367
+ instance: {},
368
+ })
369
+
370
+ parentElement.append(...nodes)
371
+
372
+ const outerA = parentElement.querySelector('.A')!
373
+ const outerB = parentElement.querySelector('.B')!
374
+ const innerA1 = outerA.children[0]
375
+ const innerB3 = outerB.children[0]
376
+
377
+ expect(innerA1.textContent).toBe('A1')
378
+ expect(innerB3.textContent).toBe('B3')
379
+
380
+ // Shuffle outer items
381
+ dataSignal.update((data) => ({
382
+ ...data,
383
+ Variables: {
384
+ ...data.Variables,
385
+ items: [itemB, itemA],
386
+ },
387
+ }))
388
+
389
+ expect(parentElement.children[0] === outerB).toBeTruthy()
390
+ expect(parentElement.children[1] === outerA).toBeTruthy()
391
+
392
+ // Inner items should still be the same objects
393
+ expect(outerB.children[0] === innerB3).toBeTruthy()
394
+ expect(outerA.children[0] === innerA1).toBeTruthy()
395
+
396
+ // Update subItems of B
397
+ const newItemB = {
398
+ ...itemB,
399
+ subItems: [
400
+ { id: '4', val: 'B4' },
401
+ { id: '3', val: 'B3' },
402
+ ],
403
+ }
404
+ dataSignal.update((data) => ({
405
+ ...data,
406
+ Variables: {
407
+ ...data.Variables,
408
+ items: [newItemB, itemA],
409
+ },
410
+ }))
411
+
412
+ // Outer B should still be same reference?
413
+ // Wait, createNode reuses by key. newItemB has same ID 'B'.
414
+ expect(parentElement.children[0] === outerB).toBeTruthy()
415
+
416
+ // Inner sequence of B should have changed, but inner elements preserved
417
+ expect(outerB.children[0].textContent).toBe('B4')
418
+ expect(outerB.children[1].textContent).toBe('B3')
419
+ expect(outerB.children[1] === innerB3).toBeTruthy()
420
+ })
421
+
422
+ test('repeat nodes should handle prepending items efficiently', () => {
423
+ const parentElement = document.createElement('div')
424
+ document.body.appendChild(parentElement)
425
+
426
+ const item2 = { id: '2' }
427
+ const item3 = { id: '3' }
428
+
429
+ const dataSignal = signal<ComponentData>({
430
+ Attributes: {},
431
+ Variables: { items: [item2, item3] },
432
+ })
433
+
434
+ const ctx = {
435
+ isRootComponent: false,
436
+ component: {
437
+ name: 'My Component',
438
+ nodes: {
439
+ repeat: {
440
+ type: 'element',
441
+ tag: 'div',
442
+ repeat: { type: 'path', path: ['Variables', 'items'] },
443
+ repeatKey: { type: 'path', path: ['ListItem', 'Item', 'id'] },
444
+ children: ['text'],
445
+ attrs: {},
446
+ events: {},
447
+ },
448
+ text: {
449
+ type: 'text',
450
+ value: { type: 'path', path: ['ListItem', 'Item', 'id'] },
451
+ },
452
+ },
453
+ },
454
+ root: document,
455
+ env: { runtime: 'page' },
456
+ formulaCache: {},
457
+ toddle: { getCustomFormula: () => undefined },
458
+ } as any as ComponentContext
459
+
460
+ const nodes = createNode({
461
+ ctx,
462
+ namespace: 'http://www.w3.org/1999/xhtml',
463
+ dataSignal,
464
+ path: 'prepend',
465
+ id: 'repeat',
466
+ parentElement,
467
+ instance: {},
468
+ })
469
+ parentElement.append(...nodes)
470
+
471
+ const el2 = parentElement.children[0]
472
+ const el3 = parentElement.children[1]
473
+
474
+ // Prepend item 1
475
+ const item1 = { id: '1' }
476
+ dataSignal.update((data) => ({
477
+ ...data,
478
+ Variables: { ...data.Variables, items: [item1, item2, item3] },
479
+ }))
480
+
481
+ expect(parentElement.children.length).toBe(3)
482
+ expect(parentElement.children[0].textContent).toBe('1')
483
+ expect(parentElement.children[1] === el2).toBeTruthy()
484
+ expect(parentElement.children[2] === el3).toBeTruthy()
485
+ })
486
+
487
+ test('repeat nodes with duplicate keys should still render all items but without reuse optimizations', () => {
488
+ const parentElement = document.createElement('div')
489
+ document.body.appendChild(parentElement)
490
+
491
+ const item1 = { id: 'i1', name: 'Item 1' }
492
+ const item2 = { id: 'i2', name: 'Item 2' }
493
+ const item3 = { id: 'i3', name: 'Item 3' }
494
+
495
+ const dataSignal = signal<ComponentData>({
496
+ Attributes: {},
497
+ Variables: {
498
+ items: [],
499
+ },
500
+ })
501
+
502
+ const ctx = {
503
+ isRootComponent: false,
504
+ component: {
505
+ name: 'My Component',
506
+ nodes: {
507
+ repeat: {
508
+ type: 'element',
509
+ tag: 'div',
510
+ repeat: { type: 'path', path: ['Variables', 'items'] },
511
+ repeatKey: { type: 'path', path: ['ListItem', 'Item', 'id'] },
512
+ children: ['text'],
513
+ attrs: {},
514
+ events: {},
515
+ },
516
+ text: {
517
+ type: 'text',
518
+ value: { type: 'path', path: ['ListItem', 'Item', 'name'] },
519
+ },
520
+ },
521
+ },
522
+ root: document,
523
+ env: { runtime: 'page' },
524
+ formulaCache: {},
525
+ toddle: { getCustomFormula: () => undefined },
526
+ } as any as ComponentContext
527
+
528
+ const nodes = createNode({
529
+ ctx,
530
+ namespace: 'http://www.w3.org/1999/xhtml',
531
+ dataSignal,
532
+ id: 'repeat',
533
+ path: '0',
534
+ instance: {},
535
+ parentElement,
536
+ })
537
+ parentElement.append(...nodes)
538
+
539
+ // Update with duplicate keys
540
+ dataSignal.update((data) => ({
541
+ ...data,
542
+ Variables: { ...data.Variables, items: [item1] },
543
+ }))
544
+
545
+ expect(parentElement.children.length).toBe(1)
546
+ expect(parentElement.children[0].textContent).toBe('Item 1')
547
+
548
+ // Update with all items, including duplicate key
549
+ dataSignal.update((data) => ({
550
+ ...data,
551
+ Variables: { ...data.Variables, items: [item1, item2] },
552
+ }))
553
+
554
+ dataSignal.update((data) => ({
555
+ ...data,
556
+ Variables: { ...data.Variables, items: [item1, item2, item3] },
557
+ }))
558
+
559
+ expect(parentElement.children.length).toBe(3)
560
+ expect(parentElement.children[0].textContent).toBe('Item 1')
561
+ expect(parentElement.children[1].textContent).toBe('Item 2')
562
+ expect(parentElement.children[2].textContent).toBe('Item 3')
563
+
564
+ expect(parentElement.children[0].getAttribute('data-id')).toBe('0')
565
+ expect(parentElement.children[1].getAttribute('data-id')).toBe('0(1)')
566
+ expect(parentElement.children[2].getAttribute('data-id')).toBe('0(2)')
567
+ })
568
+
569
+ test('it should have correct order of custom properties overrides if a component root has deep instance styling', () => {
570
+ const parentElement = document.createElement('div')
571
+ document.body.appendChild(parentElement)
572
+
573
+ const outermostColor = 'red'
574
+ const middleColor = 'blue'
575
+ const innermostColor = 'green'
576
+
577
+ const ctx: ComponentContext = {
578
+ isRootComponent: false,
579
+ component: {
580
+ name: 'My_Component',
581
+ nodes: {
582
+ root: {
583
+ type: 'component',
584
+ attrs: {},
585
+ events: {},
586
+ children: [],
587
+ name: 'first_wrapper',
588
+ customProperties: {
589
+ '--color': {
590
+ syntax: {
591
+ type: 'primitive',
592
+ name: 'color',
593
+ },
594
+ formula: {
595
+ type: 'value',
596
+ value: 'red',
597
+ },
598
+ },
599
+ },
600
+ },
601
+ },
602
+ },
603
+ root: document,
604
+ formulaCache: {},
605
+ env: { runtime: 'preview' } as ToddleEnv,
606
+ stores: {
607
+ theme: signal(null),
608
+ },
609
+ components: [
610
+ {
611
+ name: 'first_wrapper',
612
+ nodes: {
613
+ root: {
614
+ type: 'component',
615
+ attrs: {},
616
+ events: {},
617
+ children: [],
618
+ name: 'second_wrapper',
619
+ customProperties: {
620
+ '--color': {
621
+ syntax: {
622
+ type: 'primitive',
623
+ name: 'color',
624
+ },
625
+ formula: {
626
+ type: 'value',
627
+ value: 'blue',
628
+ },
629
+ },
630
+ },
631
+ },
632
+ },
633
+ },
634
+ {
635
+ name: 'second_wrapper',
636
+ nodes: {
637
+ root: {
638
+ type: 'element',
639
+ tag: 'div',
640
+ children: ['text-node'],
641
+ attrs: {},
642
+ events: {},
643
+ customProperties: {
644
+ '--color': {
645
+ syntax: {
646
+ type: 'primitive',
647
+ name: 'color',
648
+ },
649
+ formula: {
650
+ type: 'value',
651
+ value: 'green',
652
+ },
653
+ },
654
+ },
655
+ },
656
+ 'text-node': {
657
+ type: 'text',
658
+ value: { type: 'value', value: 'Hello' },
659
+ },
660
+ },
661
+ },
662
+ ],
663
+ } as Partial<ComponentContext> as ComponentContext
664
+
665
+ createNode({
666
+ ctx,
667
+ namespace: 'http://www.w3.org/1999/xhtml',
668
+ dataSignal: signal<ComponentData>({
669
+ Attributes: {},
670
+ Variables: {},
671
+ }),
672
+ path: '0',
673
+ id: 'root',
674
+ parentElement,
675
+ instance: {},
676
+ })
677
+
678
+ const sheet = customPropertiesStylesheet?.getStyleSheet()
679
+ // Test that the order makes sense
680
+ expect(Array.from(sheet?.cssRules ?? []).map((r) => r.cssText)).toEqual([
681
+ `[data-id="0"] { --color: ${innermostColor}; }`,
682
+ `[data-id="0"].first_wrapper\\:root { --color: ${middleColor}; }`,
683
+ `[data-id="0"].My_Component\\:root { --color: ${outermostColor}; }`,
684
+ ])
685
+ })
686
+ })