ripple 0.2.115 → 0.2.118

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 (93) hide show
  1. package/package.json +16 -16
  2. package/src/compiler/index.js +20 -1
  3. package/src/compiler/phases/1-parse/index.js +79 -0
  4. package/src/compiler/phases/3-transform/client/index.js +54 -8
  5. package/src/compiler/phases/3-transform/segments.js +107 -60
  6. package/src/compiler/phases/3-transform/server/index.js +21 -11
  7. package/src/compiler/types/index.d.ts +16 -0
  8. package/src/runtime/index-client.js +19 -185
  9. package/src/runtime/index-server.js +24 -0
  10. package/src/runtime/internal/client/bindings.js +443 -0
  11. package/src/runtime/internal/client/index.js +4 -0
  12. package/src/runtime/internal/client/runtime.js +10 -0
  13. package/src/runtime/internal/client/utils.js +0 -8
  14. package/src/runtime/map.js +11 -1
  15. package/src/runtime/set.js +11 -1
  16. package/tests/client/__snapshots__/for.test.ripple.snap +80 -0
  17. package/tests/client/_etc.test.ripple +5 -0
  18. package/tests/client/array/array.copy-within.test.ripple +120 -0
  19. package/tests/client/array/array.derived.test.ripple +495 -0
  20. package/tests/client/array/array.iteration.test.ripple +115 -0
  21. package/tests/client/array/array.mutations.test.ripple +385 -0
  22. package/tests/client/array/array.static.test.ripple +237 -0
  23. package/tests/client/array/array.to-methods.test.ripple +93 -0
  24. package/tests/client/basic/__snapshots__/basic.attributes.test.ripple.snap +60 -0
  25. package/tests/client/basic/__snapshots__/basic.rendering.test.ripple.snap +106 -0
  26. package/tests/client/basic/__snapshots__/basic.text.test.ripple.snap +49 -0
  27. package/tests/client/basic/basic.attributes.test.ripple +474 -0
  28. package/tests/client/basic/basic.collections.test.ripple +94 -0
  29. package/tests/client/basic/basic.components.test.ripple +225 -0
  30. package/tests/client/basic/basic.errors.test.ripple +126 -0
  31. package/tests/client/basic/basic.events.test.ripple +222 -0
  32. package/tests/client/basic/basic.reactivity.test.ripple +476 -0
  33. package/tests/client/basic/basic.rendering.test.ripple +204 -0
  34. package/tests/client/basic/basic.styling.test.ripple +63 -0
  35. package/tests/client/basic/basic.utilities.test.ripple +25 -0
  36. package/tests/client/boundaries.test.ripple +2 -21
  37. package/tests/client/compiler/__snapshots__/compiler.assignments.test.ripple.snap +12 -0
  38. package/tests/client/compiler/__snapshots__/compiler.typescript.test.ripple.snap +22 -0
  39. package/tests/client/compiler/compiler.assignments.test.ripple +112 -0
  40. package/tests/client/compiler/compiler.attributes.test.ripple +95 -0
  41. package/tests/client/compiler/compiler.basic.test.ripple +203 -0
  42. package/tests/client/compiler/compiler.regex.test.ripple +87 -0
  43. package/tests/client/compiler/compiler.typescript.test.ripple +29 -0
  44. package/tests/client/{__snapshots__/composite.test.ripple.snap → composite/__snapshots__/composite.render.test.ripple.snap} +2 -2
  45. package/tests/client/composite/composite.dynamic-components.test.ripple +100 -0
  46. package/tests/client/composite/composite.generics.test.ripple +211 -0
  47. package/tests/client/composite/composite.props.test.ripple +106 -0
  48. package/tests/client/composite/composite.reactivity.test.ripple +184 -0
  49. package/tests/client/composite/composite.render.test.ripple +84 -0
  50. package/tests/client/computed-properties.test.ripple +2 -21
  51. package/tests/client/context.test.ripple +5 -22
  52. package/tests/client/date.test.ripple +1 -20
  53. package/tests/client/dynamic-elements.test.ripple +16 -24
  54. package/tests/client/for.test.ripple +4 -23
  55. package/tests/client/head.test.ripple +11 -23
  56. package/tests/client/html.test.ripple +1 -20
  57. package/tests/client/input-value.test.ripple +11 -31
  58. package/tests/client/map.test.ripple +82 -20
  59. package/tests/client/media-query.test.ripple +10 -23
  60. package/tests/client/object.test.ripple +5 -24
  61. package/tests/client/portal.test.ripple +2 -19
  62. package/tests/client/ref.test.ripple +8 -26
  63. package/tests/client/set.test.ripple +84 -22
  64. package/tests/client/svg.test.ripple +1 -22
  65. package/tests/client/switch.test.ripple +6 -25
  66. package/tests/client/tracked-expression.test.ripple +2 -21
  67. package/tests/client/typescript-generics.test.ripple +0 -21
  68. package/tests/client/url/url.derived.test.ripple +83 -0
  69. package/tests/client/url/url.parsing.test.ripple +165 -0
  70. package/tests/client/url/url.partial-removal.test.ripple +198 -0
  71. package/tests/client/url/url.reactivity.test.ripple +449 -0
  72. package/tests/client/url/url.serialization.test.ripple +50 -0
  73. package/tests/client/url-search-params/url-search-params.derived.test.ripple +84 -0
  74. package/tests/client/url-search-params/url-search-params.initialization.test.ripple +61 -0
  75. package/tests/client/url-search-params/url-search-params.iteration.test.ripple +153 -0
  76. package/tests/client/url-search-params/url-search-params.mutation.test.ripple +343 -0
  77. package/tests/client/url-search-params/url-search-params.retrieval.test.ripple +160 -0
  78. package/tests/client/url-search-params/url-search-params.serialization.test.ripple +53 -0
  79. package/tests/client/url-search-params/url-search-params.tracked-url.test.ripple +55 -0
  80. package/tests/client.d.ts +12 -0
  81. package/tests/server/if.test.ripple +66 -0
  82. package/tests/setup-client.js +28 -0
  83. package/tsconfig.json +4 -2
  84. package/types/index.d.ts +92 -46
  85. package/LICENSE +0 -21
  86. package/tests/client/__snapshots__/basic.test.ripple.snap +0 -117
  87. package/tests/client/__snapshots__/compiler.test.ripple.snap +0 -33
  88. package/tests/client/array.test.ripple +0 -1455
  89. package/tests/client/basic.test.ripple +0 -1892
  90. package/tests/client/compiler.test.ripple +0 -541
  91. package/tests/client/composite.test.ripple +0 -692
  92. package/tests/client/url-search-params.test.ripple +0 -912
  93. package/tests/client/url.test.ripple +0 -954
@@ -1,1892 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { mount, flushSync, effect, track, trackSplit, untrack, tick } from 'ripple';
3
- import { compile } from 'ripple/compiler';
4
- import { TRACKED_ARRAY } from '../../src/runtime/internal/client/constants.js';
5
-
6
- describe('basic client', () => {
7
- let container;
8
- let error;
9
-
10
- function render(component) {
11
- mount(component, {
12
- target: container
13
- });
14
- }
15
-
16
- beforeEach(() => {
17
- container = document.createElement('div');
18
- document.body.appendChild(container);
19
- error = undefined;
20
- });
21
-
22
- afterEach(() => {
23
- document.body.removeChild(container);
24
- container = null;
25
- error = undefined;
26
- });
27
-
28
- it('render static text', () => {
29
- component Basic() {
30
- <div>{'Hello World'}</div>
31
- }
32
-
33
- render(Basic);
34
- expect(container).toMatchSnapshot();
35
- });
36
-
37
- it('render static attributes', () => {
38
- component Basic() {
39
- <div class='foo' id='bar' style='color: red;'>{'Hello World'}</div>
40
- }
41
-
42
- render(Basic);
43
-
44
- expect(container).toMatchSnapshot();
45
- });
46
-
47
- it('render semi-dynamic text', () => {
48
- component Basic() {
49
- let text = 'Hello World';
50
-
51
- <div>{text}</div>
52
- }
53
-
54
- render(Basic);
55
-
56
- expect(container).toMatchSnapshot();
57
- });
58
-
59
- it('render dynamic text', () => {
60
- component Basic() {
61
- let text = track('Hello World');
62
-
63
- <button onClick={() => { @text = 'Hello Ripple' }}>{'Change Text'}</button>
64
- <div>{@text}</div>
65
- }
66
-
67
- render(Basic);
68
-
69
- const button = container.querySelector('button');
70
-
71
- button.click();
72
- flushSync();
73
-
74
- expect(container.querySelector('div').textContent).toEqual('Hello Ripple');
75
- });
76
-
77
- it('render empty string literal', () => {
78
- component Basic() {
79
- <div>{''}</div>
80
- }
81
-
82
- render(Basic);
83
- expect(container.querySelector('div').textContent).toEqual('');
84
- });
85
-
86
- it('render empty template literal', () => {
87
- component Basic() {
88
- <div>{``}</div>
89
- }
90
-
91
- render(Basic);
92
- expect(container.querySelector('div').textContent).toEqual('');
93
- });
94
-
95
- it('render tick template literal for nested children', () => {
96
- component Child({ level, children }) {
97
- if(level == 1) {
98
- <h1><children /></h1>
99
- }
100
- if(level == 2) {
101
- <h2><children /></h2>
102
- }
103
- if(level == 3) {
104
- <h3><children /></h3>
105
- }
106
- }
107
-
108
- component App() {
109
- <Child level={1}>{`Heading 1`}</Child>
110
- }
111
-
112
- render(App);
113
- expect(container.querySelector('h1').textContent).toEqual('Heading 1');
114
- });
115
-
116
- it('render dynamic class attribute', () => {
117
- component Basic() {
118
- let active = track(false);
119
-
120
- <button onClick={() => { @active = !@active }}>{'Toggle'}</button>
121
- <div class={@active ? 'active' : 'inactive'}>{'Dynamic Class'}</div>
122
-
123
- <style>
124
- .active {
125
- color: green;
126
- }
127
- </style>
128
- }
129
-
130
- render(Basic);
131
-
132
- const button = container.querySelector('button');
133
- const div = container.querySelector('div');
134
-
135
- expect(Array.from(div.classList).some(className => className.startsWith('ripple-'))).toBe(true);
136
- expect(div.classList.contains('inactive')).toBe(true);
137
-
138
- button.click();
139
- flushSync();
140
- expect(div.classList.contains('active')).toBe(true);
141
-
142
- button.click();
143
- flushSync();
144
-
145
- expect(div.classList.contains('inactive')).toBe(true);
146
- });
147
-
148
- it('render class attribute with array, nested array, nested object', () => {
149
- component Basic() {
150
- <div class={[
151
- 'foo',
152
- 'bar',
153
- true && 'baz',
154
- false && 'aaa',
155
- null && 'bbb',
156
- [
157
- 'ccc',
158
- 'ddd',
159
- { eee: true, fff: false }
160
- ]
161
- ]}>
162
- {'Class Array'}
163
- </div>
164
-
165
- <style>
166
- .foo {
167
- color: red;
168
- }
169
- </style>
170
- }
171
-
172
- render(Basic);
173
-
174
- const div = container.querySelector('div');
175
-
176
- expect(Array.from(div.classList).some(className => className.startsWith('ripple-'))).toBe(true);
177
- expect(div.classList.contains('foo')).toBe(true);
178
- expect(div.classList.contains('bar')).toBe(true);
179
- expect(div.classList.contains('baz')).toBe(true);
180
- expect(div.classList.contains('aaa')).toBe(false);
181
- expect(div.classList.contains('bbb')).toBe(false);
182
- expect(div.classList.contains('ccc')).toBe(true);
183
- expect(div.classList.contains('ddd')).toBe(true);
184
- expect(div.classList.contains('eee')).toBe(true);
185
- expect(div.classList.contains('fff')).toBe(false);
186
- });
187
-
188
- it('render dynamic class object', () => {
189
- component Basic() {
190
- let active = track(false);
191
-
192
- <button onClick={() => { @active = !@active }}>{'Toggle'}</button>
193
- <div class={{ active: @active, inactive: !@active }}>{'Dynamic Class'}</div>
194
-
195
- <style>
196
- .active {
197
- color: green;
198
- }
199
- </style>
200
- }
201
-
202
- render(Basic);
203
-
204
- const button = container.querySelector('button');
205
- const div = container.querySelector('div');
206
-
207
- expect(Array.from(div.classList).some(className => className.startsWith('ripple-'))).toBe(true);
208
- expect(div.classList.contains('inactive')).toBe(true);
209
- expect(div.classList.contains('active')).toBe(false);
210
-
211
- button.click();
212
- flushSync();
213
- expect(div.classList.contains('inactive')).toBe(false);
214
- expect(div.classList.contains('active')).toBe(true);
215
-
216
- button.click();
217
- flushSync();
218
-
219
- expect(div.classList.contains('inactive')).toBe(true);
220
- expect(div.classList.contains('active')).toBe(false);
221
- });
222
-
223
- it('render dynamic id attribute', () => {
224
- component Basic() {
225
- let count = track(0);
226
-
227
- <button onClick={() => { @count++ }}>{'Increment'}</button>
228
- <div id={`item-${@count}`}>{'Dynamic ID'}</div>
229
- }
230
-
231
- render(Basic);
232
-
233
- const button = container.querySelector('button');
234
- const div = container.querySelector('div');
235
-
236
- expect(div.id).toBe('item-0');
237
-
238
- button.click();
239
- flushSync();
240
-
241
- expect(div.id).toBe('item-1');
242
-
243
- button.click();
244
- flushSync();
245
-
246
- expect(div.id).toBe('item-2');
247
- });
248
-
249
- it('render dynamic style attribute', () => {
250
- component Basic() {
251
- let color = track('red');
252
-
253
- <button onClick={() => { @color = @color === 'red' ? 'blue' : 'red' }}>{'Change Color'}</button>
254
- <div style={`color: ${@color}; font-weight: bold;`}>{'Dynamic Style'}</div>
255
- }
256
-
257
- render(Basic);
258
-
259
- const button = container.querySelector('button');
260
- const div = container.querySelector('div');
261
-
262
- expect(div.style.color).toBe('red');
263
- expect(div.style.fontWeight).toBe('bold');
264
-
265
- button.click();
266
- flushSync();
267
-
268
- expect(div.style.color).toBe('blue');
269
- expect(div.style.fontWeight).toBe('bold');
270
- });
271
-
272
- it('render style attribute as dynamic object', () => {
273
- component Basic() {
274
- let color = track('red');
275
-
276
- <button onClick={() => { @color = @color === 'red' ? 'blue' : 'red' }}>{'Change Color'}</button>
277
- <div style={{
278
- color: @color,
279
- fontWeight: 'bold',
280
- }}>{'Dynamic Style'}</div>
281
- }
282
-
283
- render(Basic);
284
-
285
- const button = container.querySelector('button');
286
- const div = container.querySelector('div');
287
-
288
- expect(div.style.color).toBe('red');
289
- expect(div.style.fontWeight).toBe('bold');
290
-
291
- button.click();
292
- flushSync();
293
-
294
- expect(div.style.color).toBe('blue');
295
- expect(div.style.fontWeight).toBe('bold');
296
- });
297
-
298
- it('render tracked variable as style attribute', () => {
299
- component Basic() {
300
- let style = track({ color: 'red', fontWeight: 'bold' });
301
-
302
- function toggleColor() {
303
- @style = { ...@style, color: @style.color === 'red' ? 'blue' : 'red' };
304
- }
305
-
306
- <button onClick={toggleColor}>{'Change Color'}</button>
307
- <div style={@style}>{'Dynamic Style'}</div>
308
- }
309
-
310
- render(Basic);
311
-
312
- const button = container.querySelector('button');
313
- const div = container.querySelector('div');
314
-
315
- expect(div.style.color).toBe('red');
316
- expect(div.style.fontWeight).toBe('bold');
317
-
318
- button.click();
319
- flushSync();
320
-
321
- expect(div.style.color).toBe('blue');
322
- expect(div.style.fontWeight).toBe('bold');
323
- });
324
-
325
- it('render tracked object as style attribute', () => {
326
- component Basic() {
327
- let style = #{ color: 'red', fontWeight: 'bold' };
328
-
329
- function toggleColor() {
330
- @style.color = @style.color === 'red' ? 'blue' : 'red';
331
- }
332
-
333
- <button onClick={toggleColor}>{'Change Color'}</button>
334
- <div style={{ ...@style }}>{'Dynamic Style'}</div>
335
- }
336
-
337
- render(Basic);
338
-
339
- const button = container.querySelector('button');
340
- const div = container.querySelector('div');
341
-
342
- expect(div.style.color).toBe('red');
343
- expect(div.style.fontWeight).toBe('bold');
344
-
345
- button.click();
346
- flushSync();
347
-
348
- expect(div.style.color).toBe('blue');
349
- expect(div.style.fontWeight).toBe('bold');
350
- });
351
-
352
- it('render spread attributes with style and class', () => {
353
- component Basic() {
354
- const attributes = {
355
- style: { color: 'red', fontWeight: 'bold' },
356
- class: ['foo', false && 'bar'],
357
- };
358
-
359
- <div {...attributes}>{'Attributes with style and class'}</div>
360
- }
361
-
362
- render(Basic);
363
-
364
- const div = container.querySelector('div');
365
-
366
- expect(div.style.color).toBe('red');
367
- expect(div.style.fontWeight).toBe('bold');
368
-
369
- expect(div.classList.contains('foo')).toBe(true);
370
- expect(div.classList.contains('bar')).toBe(false);
371
- });
372
-
373
- it('render spread props without duplication', () => {
374
- component App() {
375
- const checkBoxProp = {name:'car'}
376
-
377
- <div>
378
- <input {...checkBoxProp} type="checkbox" id="vehicle1" value="Bike" />
379
- </div>
380
- }
381
-
382
- render(App);
383
-
384
- const input = container.querySelector('input');
385
- const html = container.innerHTML;
386
-
387
- expect(input.getAttribute('name')).toBe('car');
388
- expect(input.getAttribute('type')).toBe('checkbox');
389
- expect(input.getAttribute('id')).toBe('vehicle1');
390
- expect(input.getAttribute('value')).toBe('Bike');
391
-
392
- expect(html).not.toContain('type="checkbox"type="checkbox"');
393
- expect(html).not.toContain('value="Bike"value="Bike"');
394
-
395
- expect(container).toMatchSnapshot();
396
- });
397
-
398
- it('render dynamic boolean attributes', () => {
399
- component Basic() {
400
- let disabled = track(false);
401
- let checked = track(false);
402
-
403
- <button onClick={() => {
404
- @disabled = !@disabled;
405
- @checked = !@checked;
406
- }}>{'Toggle'}</button>
407
- <input type='checkbox' disabled={@disabled} checked={@checked} />
408
- }
409
-
410
- render(Basic);
411
-
412
- const button = container.querySelector('button');
413
- const input = container.querySelector('input');
414
-
415
- expect(input.disabled).toBe(false);
416
- expect(input.checked).toBe(false);
417
-
418
- button.click();
419
- flushSync();
420
-
421
- expect(input.disabled).toBe(true);
422
- expect(input.checked).toBe(true);
423
- });
424
-
425
- it('render multiple dynamic attributes', () => {
426
- component Basic() {
427
- let theme = track('light');
428
- let size = track('medium');
429
-
430
- <button
431
- onClick={() => {
432
- @theme = @theme === 'light' ? 'dark' : 'light';
433
- @size = @size === 'medium' ? 'large' : 'medium';
434
- }}
435
- >{'Toggle Theme & Size'}</button>
436
- <div class={`theme-${@theme} size-${@size}`} data-theme={@theme} data-size={@size}>{'Multiple Dynamic Attributes'}</div>
437
- }
438
-
439
- render(Basic);
440
-
441
- const button = container.querySelector('button');
442
- const div = container.querySelector('div');
443
-
444
- expect(div.className).toBe('theme-light size-medium');
445
- expect(div.getAttribute('data-theme')).toBe('light');
446
- expect(div.getAttribute('data-size')).toBe('medium');
447
-
448
- button.click();
449
- flushSync();
450
-
451
- expect(div.className).toBe('theme-dark size-large');
452
- expect(div.getAttribute('data-theme')).toBe('dark');
453
- expect(div.getAttribute('data-size')).toBe('large');
454
- });
455
-
456
- it('render conditional attributes', () => {
457
- component Basic() {
458
- let showTitle = track(false);
459
- let showAria = track(false);
460
-
461
- <button onClick={() => {
462
- @showTitle = !@showTitle;
463
- @showAria = !@showAria;
464
- }}>{'Toggle Attributes'}</button>
465
- <div
466
- title={@showTitle ? 'This is a title' : null}
467
- aria-label={@showAria ? 'Accessible label' : null}
468
- >{'Conditional Attributes'}</div>
469
- }
470
-
471
- render(Basic);
472
-
473
- const button = container.querySelector('button');
474
- const div = container.querySelector('div');
475
-
476
- expect(div.hasAttribute('title')).toBe(false);
477
- expect(div.hasAttribute('aria-label')).toBe(false);
478
-
479
- button.click();
480
- flushSync();
481
-
482
- expect(div.getAttribute('title')).toBe('This is a title');
483
- expect(div.getAttribute('aria-label')).toBe('Accessible label');
484
-
485
- button.click();
486
- flushSync();
487
-
488
- expect(div.hasAttribute('title')).toBe(false);
489
- expect(div.hasAttribute('aria-label')).toBe(false);
490
- });
491
-
492
- it('render spread attributes', () => {
493
- component Basic() {
494
- let attrs = track({
495
- class: 'initial',
496
- id: 'test-1'
497
- });
498
-
499
- <button
500
- onClick={() => {
501
- @attrs = {
502
- class: 'updated',
503
- id: 'test-2',
504
- 'data-extra': 'value'
505
- };
506
- }}
507
- >{'Update Attributes'}</button>
508
- <div {...@attrs}>{'Spread Attributes'}</div>
509
- }
510
-
511
- render(Basic);
512
-
513
- const button = container.querySelector('button');
514
- const div = container.querySelector('div');
515
-
516
- expect(div.className).toBe('initial');
517
- expect(div.id).toBe('test-1');
518
- expect(div.hasAttribute('data-extra')).toBe(false);
519
-
520
- button.click();
521
- flushSync();
522
-
523
- expect(div.className).toBe('updated');
524
- expect(div.id).toBe('test-2');
525
- expect(div.getAttribute('data-extra')).toBe('value');
526
- });
527
-
528
- it('renders multiple reactive lexical blocks', () => {
529
- component Basic() {
530
- <div>
531
- let obj = {
532
- count: track(0)
533
- };
534
-
535
- <span>{obj.@count}</span>
536
- </div>
537
- <div>
538
- let b = {
539
- count: track(0)
540
- };
541
-
542
- <button onClick={() => { b.@count-- }}>{'-'}</button>
543
- <span class='count'>{b.@count}</span>
544
- <button onClick={() => { b.@count++ }}>{'+'}</button>
545
- </div>
546
- }
547
- render(Basic);
548
-
549
- const buttons = container.querySelectorAll('button');
550
-
551
- buttons[0].click();
552
- flushSync();
553
-
554
- expect(container.querySelector('.count').textContent).toBe('-1');
555
-
556
- buttons[1].click();
557
- flushSync();
558
-
559
- expect(container.querySelector('.count').textContent).toBe('0');
560
- });
561
-
562
- it('renders multiple reactive lexical blocks with complexity', () => {
563
-
564
- component Basic() {
565
- const count = 'count';
566
-
567
- <div>
568
- let obj = {
569
- count: track(0)
570
- };
571
-
572
- <span>{obj[@count]}</span>
573
- </div>
574
- <div>
575
- let b = {
576
- count: track(0)
577
- };
578
-
579
- <button onClick={() => { b[@count]-- }}>{'-'}</button>
580
- <span class='count'>{b[@count]}</span>
581
- <button onClick={() => { b[@count]++ }}>{'+'}</button>
582
- </div>
583
- }
584
- render(Basic);
585
-
586
- const buttons = container.querySelectorAll('button');
587
-
588
- buttons[0].click();
589
- flushSync();
590
-
591
- expect(container.querySelector('.count').textContent).toBe('-1');
592
-
593
- buttons[1].click();
594
- flushSync();
595
-
596
- expect(container.querySelector('.count').textContent).toBe('0');
597
- });
598
-
599
- it('renders with different event types', () => {
600
- component Basic() {
601
- let focusCount = track(0);
602
- let clickCount = track(0);
603
-
604
- <button
605
- onFocus={() => { @focusCount++ }}
606
- onClick={() => { @clickCount++ }}
607
- >{'Test Button'}</button>
608
- <div class='focus-count'>{@focusCount}</div>
609
- <div class='click-count'>{@clickCount}</div>
610
- }
611
-
612
- render(Basic);
613
-
614
- const button = container.querySelector('button');
615
- const focusDiv = container.querySelector('.focus-count');
616
- const clickDiv = container.querySelector('.click-count');
617
-
618
- button.dispatchEvent(new Event('focus'));
619
- flushSync();
620
- expect(focusDiv.textContent).toBe('1');
621
-
622
- button.click();
623
- flushSync();
624
- expect(clickDiv.textContent).toBe('1');
625
- });
626
-
627
- it('renders with capture events', () => {
628
- component Basic() {
629
- let captureClicks = track(0);
630
- let bubbleClicks = track(0);
631
-
632
- <div onClickCapture={() => { @captureClicks++ }}>
633
- <button onClick={() => { @bubbleClicks++ }}>{'Click me'}</button>
634
- <div class='capture-count'>{@captureClicks}</div>
635
- <div class='bubble-count'>{@bubbleClicks}</div>
636
- </div>
637
- }
638
-
639
- render(Basic);
640
-
641
- const button = container.querySelector('button');
642
- const captureDiv = container.querySelector('.capture-count');
643
- const bubbleDiv = container.querySelector('.bubble-count');
644
-
645
- button.click();
646
- flushSync();
647
-
648
- expect(captureDiv.textContent).toBe('1');
649
- expect(bubbleDiv.textContent).toBe('1');
650
- });
651
-
652
- it('renders with event listeners in spread props', () => {
653
- component Basic() {
654
- let count = track(0);
655
-
656
- const minus = {
657
- onClick() {
658
- @count--
659
- }
660
- }
661
-
662
- const plus = {
663
- onClick() {
664
- @count++
665
- }
666
- }
667
-
668
- <div>
669
- <button {...minus} class='minus'>{'-'}</button>
670
- <span class='count'>{@count}</span>
671
- <button {...plus} class='plus'>{'+'}</button>
672
- </div>
673
- }
674
-
675
- render(Basic);
676
-
677
- const minusButton = container.querySelector('.minus');
678
- const plusButton = container.querySelector('.plus');
679
- const countSpan = container.querySelector('.count');
680
-
681
- expect(countSpan.textContent).toBe('0');
682
-
683
- // Test that the buttons don't have string onclick attributes
684
- expect(minusButton.getAttribute('onclick')).toBe(null);
685
- expect(plusButton.getAttribute('onclick')).toBe(null);
686
-
687
- // Test that the event handlers work
688
- minusButton.click();
689
- flushSync();
690
- expect(countSpan.textContent).toBe('-1');
691
-
692
- plusButton.click();
693
- flushSync();
694
- expect(countSpan.textContent).toBe('0');
695
-
696
- plusButton.click();
697
- flushSync();
698
- expect(countSpan.textContent).toBe('1');
699
- });
700
-
701
- it('handles both delegated and non-delegated events in spread props', () => {
702
- component Basic() {
703
- let clickCount = track(0);
704
- let focusCount = track(0);
705
-
706
- const mixedHandler = {
707
- onClick() { // Delegated event
708
- @clickCount++
709
- },
710
- onFocus() { // Non-delegated event
711
- @focusCount++
712
- }
713
- }
714
-
715
- <div>
716
- <button {...mixedHandler} class='mixed-button'>{'Test'}</button>
717
- <span class='click-count'>{@clickCount}</span>
718
- <span class='focus-count'>{@focusCount}</span>
719
- </div>
720
- }
721
-
722
- render(Basic);
723
-
724
- const button = container.querySelector('.mixed-button');
725
- const clickSpan = container.querySelector('.click-count');
726
- const focusSpan = container.querySelector('.focus-count');
727
-
728
- expect(clickSpan.textContent).toBe('0');
729
- expect(focusSpan.textContent).toBe('0');
730
-
731
- // Test delegated event (click)
732
- button.click();
733
- flushSync();
734
- expect(clickSpan.textContent).toBe('1');
735
-
736
- // Test non-delegated event (focus)
737
- button.dispatchEvent(new Event('focus'));
738
- flushSync();
739
- expect(focusSpan.textContent).toBe('1');
740
- });
741
-
742
- it('renders with component composition and children', () => {
743
- component Card(props) {
744
- <div class='card'>
745
- <props.children />
746
- </div>
747
- }
748
-
749
- component Basic() {
750
- <Card>
751
- component children() {
752
- <p>{'Card content here'}</p>
753
- }
754
- </Card>
755
- }
756
-
757
- render(Basic);
758
-
759
- const card = container.querySelector('.card');
760
- const paragraph = card.querySelector('p');
761
-
762
- expect(card).toBeTruthy();
763
- expect(paragraph.textContent).toBe('Card content here');
764
- });
765
-
766
- it('renders with error handling simulation', () => {
767
- component Basic() {
768
- let hasError = track(false);
769
- let errorMessage = track('');
770
-
771
- const triggerError = () => {
772
- try {
773
- throw new Error('Test error');
774
- } catch (e) {
775
- @hasError = true;
776
- @errorMessage = e.message;
777
- }
778
- };
779
-
780
- <div>
781
- <button onClick={triggerError}>{'Trigger Error'}</button>
782
- if (@hasError) {
783
- <div class='error'>{'Error caught: ' + @errorMessage}</div>
784
- } else {
785
- <div class='success'>{'No error'}</div>
786
- }
787
- </div>
788
- }
789
-
790
- render(Basic);
791
-
792
- const button = container.querySelector('button');
793
- const successDiv = container.querySelector('.success');
794
-
795
- expect(successDiv).toBeTruthy();
796
- expect(successDiv.textContent).toBe('No error');
797
-
798
- button.click();
799
- flushSync();
800
-
801
- const errorDiv = container.querySelector('.error');
802
- expect(errorDiv).toBeTruthy();
803
- expect(errorDiv.textContent).toBe('Error caught: Test error');
804
- });
805
-
806
- it('renders with computed reactive state', () => {
807
- component Basic() {
808
- let count = track(5);
809
-
810
- <div class='count'>{@count}</div>
811
- <div class='doubled'>{@count * 2}</div>
812
- <div class='is-even'>{@count % 2 === 0 ? 'Even' : 'Odd'}</div>
813
- <button onClick={() => { @count++ }}>{'Increment'}</button>
814
- }
815
-
816
- render(Basic);
817
-
818
- const countDiv = container.querySelector('.count');
819
- const doubledDiv = container.querySelector('.doubled');
820
- const evenDiv = container.querySelector('.is-even');
821
- const button = container.querySelector('button');
822
-
823
- expect(countDiv.textContent).toBe('5');
824
- expect(doubledDiv.textContent).toBe('10');
825
- expect(evenDiv.textContent).toBe('Odd');
826
-
827
- button.click();
828
- flushSync();
829
-
830
- expect(countDiv.textContent).toBe('6');
831
- expect(doubledDiv.textContent).toBe('12');
832
- expect(evenDiv.textContent).toBe('Even');
833
- });
834
-
835
- it('renders simple JS expression logic correctly', () => {
836
- component Example() {
837
- let test = {}
838
- let counter = 0;
839
- test[counter++] = 'Test';
840
-
841
- <div>{JSON.stringify(test)}</div>
842
- <div>{JSON.stringify(counter)}</div>
843
- }
844
- render(Example);
845
-
846
- expect(container).toMatchSnapshot();
847
- });
848
-
849
- it('renders with simple reactive objects', () => {
850
- component Basic() {
851
- let user = track({
852
- name: 'John',
853
- age: 25
854
- });
855
-
856
- <div class='name'>{@user.name}</div>
857
- <div class='age'>{@user.age}</div>
858
- <button onClick={() => {
859
- @user = {...@user, name: 'Jane', age: 30}
860
- }}>{'Update User'}</button>
861
- }
862
-
863
- render(Basic);
864
-
865
- const nameDiv = container.querySelector('.name');
866
- const ageDiv = container.querySelector('.age');
867
- const button = container.querySelector('button');
868
-
869
- expect(nameDiv.textContent).toBe('John');
870
- expect(ageDiv.textContent).toBe('25');
871
-
872
- button.click();
873
- flushSync();
874
-
875
- expect(nameDiv.textContent).toBe('Jane');
876
- expect(ageDiv.textContent).toBe('30');
877
- });
878
-
879
- it('renders with nested reactive objects', () => {
880
- component Basic() {
881
- let user = track({
882
- name: track('John'),
883
- age: track(25)
884
- });
885
-
886
- <div class='name'>{@user.@name}</div>
887
- <div class='age'>{@user.@age}</div>
888
- <button onClick={() => {
889
- @user.@name = 'Jane';
890
- @user.@age = 30;
891
- }}>{'Update User'}</button>
892
- }
893
-
894
- render(Basic);
895
-
896
- const nameDiv = container.querySelector('.name');
897
- const ageDiv = container.querySelector('.age');
898
- const button = container.querySelector('button');
899
-
900
- expect(nameDiv.textContent).toBe('John');
901
- expect(ageDiv.textContent).toBe('25');
902
-
903
- button.click();
904
- flushSync();
905
-
906
- expect(nameDiv.textContent).toBe('Jane');
907
- expect(ageDiv.textContent).toBe('30');
908
- });
909
-
910
- it('renders with conditional rendering using if statements', () => {
911
- component Basic() {
912
- let showContent = track(false);
913
- let userRole = track('guest');
914
-
915
- <button onClick={() => { @showContent = !@showContent }}>{'Toggle Content'}</button>
916
- <button onClick={() => { @userRole = @userRole === 'guest' ? 'admin' : 'guest' }}>{'Toggle Role'}</button>
917
-
918
- <div class='content'>
919
- if (@showContent) {
920
- if (@userRole === 'admin') {
921
- <div class='admin-content'>{'Admin content'}</div>
922
- } else {
923
- <div class='user-content'>{'User content'}</div>
924
- }
925
- } else {
926
- <div class='no-content'>{'No content'}</div>
927
- }
928
- </div>
929
- }
930
-
931
- render(Basic);
932
-
933
- const buttons = container.querySelectorAll('button');
934
- const contentDiv = container.querySelector('.content');
935
-
936
- expect(contentDiv.querySelector('.no-content')).toBeTruthy();
937
- expect(contentDiv.querySelector('.admin-content')).toBeFalsy();
938
- expect(contentDiv.querySelector('.user-content')).toBeFalsy();
939
-
940
- buttons[0].click();
941
- flushSync();
942
-
943
- expect(contentDiv.querySelector('.no-content')).toBeFalsy();
944
- expect(contentDiv.querySelector('.user-content')).toBeTruthy();
945
- expect(contentDiv.querySelector('.admin-content')).toBeFalsy();
946
-
947
- buttons[1].click();
948
- flushSync();
949
-
950
- expect(contentDiv.querySelector('.no-content')).toBeFalsy();
951
- expect(contentDiv.querySelector('.user-content')).toBeFalsy();
952
- expect(contentDiv.querySelector('.admin-content')).toBeTruthy();
953
- });
954
-
955
- it('renders with nested components and prop passing', () => {
956
- component Button(props) {
957
- <button class={props.variant} onClick={props.onClick}>
958
- {props.label}
959
- </button>
960
- }
961
-
962
- component Card(props) {
963
- <div class='card'>
964
- <h3>{props.title}</h3>
965
- <p>{props.content}</p>
966
- <Button variant='primary' label={props.buttonText} onClick={props.onAction} />
967
- </div>
968
- }
969
-
970
- component Basic() {
971
- let clicked = track(false);
972
-
973
- <Card
974
- title='Test Card'
975
- content='This is a test card'
976
- buttonText='Click me'
977
- onAction={() => @clicked = true}
978
- />
979
- <div class='status'>{@clicked ? 'Clicked' : 'Not clicked'}</div>
980
- }
981
-
982
- render(Basic);
983
-
984
- const card = container.querySelector('.card');
985
- const title = card.querySelector('h3');
986
- const content = card.querySelector('p');
987
- const button = card.querySelector('button');
988
- const status = container.querySelector('.status');
989
-
990
- expect(title.textContent).toBe('Test Card');
991
- expect(content.textContent).toBe('This is a test card');
992
- expect(button.textContent).toBe('Click me');
993
- expect(button.className).toBe('primary');
994
- expect(status.textContent).toBe('Not clicked');
995
-
996
- button.click();
997
- flushSync();
998
-
999
- expect(status.textContent).toBe('Clicked');
1000
- });
1001
-
1002
- it('renders with complex event handling and state updates', () => {
1003
- component Basic() {
1004
- let counter = track(0);
1005
- let history = track([]);
1006
- let isEven = track(true);
1007
-
1008
- const handleIncrement = () => {
1009
- @counter++;
1010
- @history = [...@history, `Inc to ${@counter}`];
1011
- @isEven = @counter % 2 === 0;
1012
- };
1013
-
1014
- const handleDecrement = () => {
1015
- @counter--;
1016
- @history = [...@history, `Dec to ${@counter}`];
1017
- @isEven = @counter % 2 === 0;
1018
- };
1019
-
1020
- const handleReset = () => {
1021
- @counter = 0;
1022
- @history = [...@history, 'Reset'];
1023
- @isEven = true;
1024
- };
1025
-
1026
- <div class='counter'>{@counter}</div>
1027
- <div class='parity'>{@isEven ? 'Even' : 'Odd'}</div>
1028
- <div class='history-count'>{@history.length}</div>
1029
-
1030
- <button class='inc-btn' onClick={handleIncrement}>{'+'}</button>
1031
- <button class='dec-btn' onClick={handleDecrement}>{'-'}</button>
1032
- <button class='reset-btn' onClick={handleReset}>{'Reset'}</button>
1033
- }
1034
-
1035
- render(Basic);
1036
-
1037
- const counterDiv = container.querySelector('.counter');
1038
- const parityDiv = container.querySelector('.parity');
1039
- const historyDiv = container.querySelector('.history-count');
1040
- const incBtn = container.querySelector('.inc-btn');
1041
- const decBtn = container.querySelector('.dec-btn');
1042
- const resetBtn = container.querySelector('.reset-btn');
1043
-
1044
- expect(counterDiv.textContent).toBe('0');
1045
- expect(parityDiv.textContent).toBe('Even');
1046
- expect(historyDiv.textContent).toBe('0');
1047
-
1048
- incBtn.click();
1049
- flushSync();
1050
-
1051
- expect(counterDiv.textContent).toBe('1');
1052
- expect(parityDiv.textContent).toBe('Odd');
1053
- expect(historyDiv.textContent).toBe('1');
1054
-
1055
- incBtn.click();
1056
- flushSync();
1057
-
1058
- expect(counterDiv.textContent).toBe('2');
1059
- expect(parityDiv.textContent).toBe('Even');
1060
- expect(historyDiv.textContent).toBe('2');
1061
-
1062
- decBtn.click();
1063
- flushSync();
1064
-
1065
- expect(counterDiv.textContent).toBe('1');
1066
- expect(parityDiv.textContent).toBe('Odd');
1067
- expect(historyDiv.textContent).toBe('3');
1068
-
1069
- resetBtn.click();
1070
- flushSync();
1071
-
1072
- expect(counterDiv.textContent).toBe('0');
1073
- expect(parityDiv.textContent).toBe('Even');
1074
- expect(historyDiv.textContent).toBe('4');
1075
- });
1076
-
1077
- it('renders with reactive component props', () => {
1078
- component ChildComponent(props) {
1079
- <div class='child-content'>{props.@text}</div>
1080
- <div class='child-count'>{props.@count}</div>
1081
- }
1082
-
1083
- component Basic() {
1084
- let message = track('Hello');
1085
- let number = track(1);
1086
-
1087
- <ChildComponent text={message} count={number} />
1088
- <button onClick={() => {
1089
- @message = @message === 'Hello' ? 'Goodbye' : 'Hello';
1090
- @number++;
1091
- }}>{'Update Props'}</button>
1092
- }
1093
-
1094
- render(Basic);
1095
-
1096
- const contentDiv = container.querySelector('.child-content');
1097
- const countDiv = container.querySelector('.child-count');
1098
- const button = container.querySelector('button');
1099
-
1100
- expect(contentDiv.textContent).toBe('Hello');
1101
- expect(countDiv.textContent).toBe('1');
1102
-
1103
- button.click();
1104
- flushSync();
1105
-
1106
- expect(contentDiv.textContent).toBe('Goodbye');
1107
- expect(countDiv.textContent).toBe('2');
1108
-
1109
- button.click();
1110
- flushSync();
1111
-
1112
- expect(contentDiv.textContent).toBe('Hello');
1113
- expect(countDiv.textContent).toBe('3');
1114
- });
1115
-
1116
- it('renders with styling scoped to component', () => {
1117
- component Basic() {
1118
- <div class='styled-container'>
1119
- <h1>{'Styled heading'}</h1>
1120
- <p class='text'>{'Styled paragraph'}</p>
1121
- </div>
1122
-
1123
- <style>
1124
- .styled-container {
1125
- background-color: rgb(0, 0, 255);
1126
- padding: 16px;
1127
- }
1128
-
1129
- h1 {
1130
- color: rgb(255, 255, 255);
1131
- font-size: 32px;
1132
- }
1133
-
1134
- .text {
1135
- color: rgb(200, 200, 200);
1136
- font-size: 14px;
1137
- }
1138
- </style>
1139
- }
1140
-
1141
- render(Basic);
1142
-
1143
- const styledContainer = container.querySelector('.styled-container');
1144
- const heading = styledContainer.querySelector('h1');
1145
- const paragraph = styledContainer.querySelector('.text');
1146
-
1147
- expect(styledContainer).toBeTruthy();
1148
- expect(heading.textContent).toBe('Styled heading');
1149
- expect(paragraph.textContent).toBe('Styled paragraph');
1150
- });
1151
-
1152
- it('renders with keyframes in styling scoped to component', () => {
1153
- const source = `export component Basic() {
1154
- <div>
1155
- <p>{'Styled paragraph'}</p>
1156
- </div>
1157
-
1158
- <style>
1159
- div {
1160
- animation-name: anim;
1161
- }
1162
-
1163
- @keyframes anim {}
1164
-
1165
- p {
1166
- animation-name: anim;
1167
- }
1168
- </style>
1169
- }`;
1170
-
1171
- const { css } = compile(source, 'test.ripple');
1172
- const name = css.match(/@keyframes\s+([a-zA-Z0-9_-]+)\s*\{/)[1];
1173
- expect(css.match(new RegExp(name, 'g'))?.length).toEqual(3);
1174
- });
1175
-
1176
- it('renders with mixed static and dynamic content', () => {
1177
- component Basic() {
1178
- let name = track('World');
1179
- let count = track(0);
1180
- const staticMessage = 'Welcome to Ripple!';
1181
-
1182
- <div class='mixed-content'>
1183
- <h1>{staticMessage}</h1>
1184
- <p class='greeting'>{'Hello, ' + @name + '!'}</p>
1185
- <p class='notifications'>{'You have ' + @count + ' notifications'}</p>
1186
- <button onClick={() => { @count++ }}>{'Add Notification'}</button>
1187
- <button onClick={() => { @name = @name === 'World' ? 'User' : 'World' }}>{'Toggle Name'}</button>
1188
- </div>
1189
- }
1190
-
1191
- render(Basic);
1192
-
1193
- const heading = container.querySelector('h1');
1194
- const greetingP = container.querySelector('.greeting');
1195
- const notificationsP = container.querySelector('.notifications');
1196
- const buttons = container.querySelectorAll('button');
1197
-
1198
- expect(heading.textContent).toBe('Welcome to Ripple!');
1199
- expect(greetingP.textContent).toBe('Hello, World!');
1200
- expect(notificationsP.textContent).toBe('You have 0 notifications');
1201
-
1202
- buttons[0].click();
1203
- flushSync();
1204
- expect(notificationsP.textContent).toBe('You have 1 notifications');
1205
-
1206
- buttons[1].click();
1207
- flushSync();
1208
- expect(greetingP.textContent).toBe('Hello, User!');
1209
- });
1210
-
1211
- it('renders with reactive attributes with nested reactive attributes', () => {
1212
- component App() {
1213
- let value = track('parent-class');
1214
-
1215
- <p class={@value}>{'Colored parent value'}</p>
1216
-
1217
- <div>
1218
- let nested = track('nested-class');
1219
-
1220
- <p class={@nested}>{'Colored nested value'}</p>
1221
- </div>
1222
- }
1223
-
1224
- render(App);
1225
-
1226
- const paragraphs = container.querySelectorAll('p');
1227
-
1228
- expect(paragraphs[0].className).toBe('parent-class');
1229
- expect(paragraphs[1].className).toBe('nested-class');
1230
- });
1231
-
1232
- it('should throw error for unclosed tag', () => {
1233
- const malformedCode = `export default component Example() {
1234
- <div></span>
1235
- }`;
1236
- expect(() => {
1237
- compile(malformedCode, 'test.ripple', 'test.ripple');
1238
- }).toThrow('Expected closing tag to match opening tag');
1239
- });
1240
-
1241
- it('should throw error for completely unclosed tag', () => {
1242
- const malformedCode = `export default component Example() {
1243
- <div>content
1244
- }`;
1245
-
1246
- expect(() => {
1247
- compile(malformedCode, 'test.ripple', 'test.ripple');
1248
- }).toThrow('Unclosed tag');
1249
- });
1250
-
1251
- it('basic reactivity with standard arrays should work', () => {
1252
- let logs = [];
1253
-
1254
- component App() {
1255
- let first = track(0);
1256
- let second = track(0);
1257
- const arr = [first, second];
1258
-
1259
- const total = track(() => arr.reduce((a, b) => a + @b, 0));
1260
-
1261
- <button onClick={() => { @first++; }}>{'first:' + @first}</button>
1262
- <button onClick={() => { @second++; }}>{'second: ' + @second}</button>
1263
-
1264
- effect(() => {
1265
- let _arr = [];
1266
-
1267
- arr.forEach((item) => {
1268
- _arr.push(@item);
1269
- });
1270
-
1271
- logs.push(_arr.join(', '));
1272
- });
1273
-
1274
- effect(() => {
1275
- if (arr.map(a => @a).includes(1)) {
1276
- logs.push('arr includes 1');
1277
- }
1278
- });
1279
-
1280
- <div>{'Sum: ' + @total}</div>
1281
- <div>{'Comma Separated: ' + arr.map(a => @a).join(', ')}</div>
1282
- <div>{'Number to string: ' + arr.map(a => String(@a))}</div>
1283
- <div>{'Even numbers: ' + arr.map(a => @a).filter(a => a % 2 === 0)}</div>
1284
- }
1285
-
1286
- render(App);
1287
- flushSync();
1288
-
1289
- const buttons = container.querySelectorAll('button');
1290
- const divs = container.querySelectorAll('div');
1291
-
1292
- expect(divs[0].textContent).toBe('Sum: 0');
1293
- expect(divs[1].textContent).toBe('Comma Separated: 0, 0');
1294
- expect(divs[2].textContent).toBe('Number to string: 0,0');
1295
- expect(divs[3].textContent).toBe('Even numbers: 0,0');
1296
- expect(logs).toEqual(['0, 0']);
1297
-
1298
- buttons[0].click();
1299
- flushSync();
1300
-
1301
- expect(divs[0].textContent).toBe('Sum: 1');
1302
- expect(divs[1].textContent).toBe('Comma Separated: 1, 0');
1303
- expect(divs[2].textContent).toBe('Number to string: 1,0');
1304
- expect(divs[3].textContent).toBe('Even numbers: 0');
1305
- expect(logs).toEqual(['0, 0', '1, 0', 'arr includes 1']);
1306
-
1307
- buttons[1].click();
1308
- flushSync();
1309
-
1310
- expect(divs[0].textContent).toBe('Sum: 2');
1311
- expect(divs[1].textContent).toBe('Comma Separated: 1, 1');
1312
- expect(divs[2].textContent).toBe('Number to string: 1,1');
1313
- expect(divs[3].textContent).toBe('Even numbers: ');
1314
- expect(logs).toEqual(['0, 0', '1, 0', 'arr includes 1', '1, 1', 'arr includes 1']);
1315
- });
1316
-
1317
- it('it retains this context with bracketed prop functions and keeps original chaining', () => {
1318
- component App() {
1319
- const SYMBOL_PROP = Symbol();
1320
- let hasError = track(false);
1321
- const obj = {
1322
- count: track(0),
1323
- increment() {
1324
- this.@count++;
1325
- },
1326
- [SYMBOL_PROP]() {
1327
- this.@count++;
1328
- },
1329
- arr: [() => obj.@count++, () => obj.@count--],
1330
- };
1331
-
1332
- const obj2 = null;
1333
-
1334
- <button onClick={() => obj['increment']()}>{'Increment'}</button>
1335
- <button onClick={() => obj[SYMBOL_PROP]()}>{'Increment'}</button>
1336
- <button
1337
- onClick={() => {
1338
- @hasError = false;
1339
- try {
1340
- obj['nonexistent']();
1341
- } catch {
1342
- @hasError = true;
1343
- }
1344
- }}
1345
- >{'Nonexistent'}</button>
1346
- <button
1347
- onClick={() => {
1348
- @hasError = false;
1349
- try {
1350
- obj['nonexistent']?.();
1351
- } catch {
1352
- @hasError = true;
1353
- }
1354
- }}
1355
- >{'Nonexistent chaining'}</button>
1356
- <button
1357
- onClick={() => {
1358
- @hasError = false;
1359
- try {
1360
- obj2['nonexistent']();
1361
- } catch {
1362
- @hasError = true;
1363
- }
1364
- }}
1365
- >{'Object null'}</button>
1366
- <button
1367
- onClick={() => {
1368
- @hasError = false;
1369
- try {
1370
- obj2?.['nonexistent']?.();
1371
- } catch {
1372
- @hasError = true;
1373
- }
1374
- }}
1375
- >{'Object null chained'}</button>
1376
- <button onClick={() => obj.arr[obj.arr.length - 1]()}>{'BinaryExpression prop'}</button>
1377
-
1378
- <span>{obj.@count}</span>
1379
- <span>{@hasError}</span>
1380
- }
1381
-
1382
- render(App);
1383
-
1384
- const button1 = container.querySelectorAll('button')[0];
1385
- const button2 = container.querySelectorAll('button')[1];
1386
- const button3 = container.querySelectorAll('button')[2];
1387
- const button4 = container.querySelectorAll('button')[3];
1388
- const button5 = container.querySelectorAll('button')[4];
1389
- const button6 = container.querySelectorAll('button')[5];
1390
- const button7 = container.querySelectorAll('button')[6];
1391
-
1392
- const countSpan = container.querySelectorAll('span')[0];
1393
- const errorSpan = container.querySelectorAll('span')[1];
1394
-
1395
- expect(countSpan.textContent).toBe('0');
1396
- expect(errorSpan.textContent).toBe('false');
1397
-
1398
- button1.click();
1399
- flushSync();
1400
-
1401
- expect(countSpan.textContent).toBe('1');
1402
-
1403
- button2.click();
1404
- flushSync();
1405
-
1406
- expect(countSpan.textContent).toBe('2');
1407
-
1408
- button3.click();
1409
- flushSync();
1410
- expect(errorSpan.textContent).toBe('true');
1411
-
1412
- button4.click();
1413
- flushSync();
1414
- expect(errorSpan.textContent).toBe('false');
1415
-
1416
- button5.click();
1417
- flushSync();
1418
- expect(errorSpan.textContent).toBe('true');
1419
-
1420
- button6.click();
1421
- flushSync();
1422
- expect(errorSpan.textContent).toBe('false');
1423
-
1424
- button7.click();
1425
- flushSync();
1426
- expect(countSpan.textContent).toBe('1');
1427
- });
1428
-
1429
- it('handles boolean attributes with no prop value provides', () => {
1430
- component App() {
1431
- <div class="container">
1432
- <button onClick={() => console.log("clicked!")} disabled>{"Button"}</button>
1433
- <input type="checkbox" checked />
1434
- </div>
1435
- }
1436
-
1437
- render(App);
1438
- expect(container).toMatchSnapshot();
1439
- });
1440
-
1441
- it('basic operations', () => {
1442
- component App() {
1443
- let count = track(0)
1444
- <div>{@count++}</div>
1445
- <div>{++@count}</div>
1446
- <div>{5}</div>
1447
- <div>{@count}</div>
1448
- }
1449
-
1450
- render(App);
1451
- expect(container).toMatchSnapshot();
1452
- });
1453
-
1454
- it('can retain reactivity for destructure rest via track trackSplit', () => {
1455
- let logs = [];
1456
-
1457
- component App() {
1458
- let count = track(0);
1459
- let name = track('Click Me');
1460
-
1461
- function buttonRef(el) {
1462
- logs.push('ref called');
1463
- return () => {
1464
- logs.push('cleanup ref');
1465
- };
1466
- }
1467
-
1468
- <Child
1469
- class="my-button"
1470
- onClick={() => @name === 'Click Me' ? @name = 'Clicked' : @name = 'Click Me'}
1471
- {@count}
1472
- {ref buttonRef}
1473
- >{@name}</Child>
1474
-
1475
- <button onClick={() => @count++}>{'Increment Count'}</button>
1476
- }
1477
-
1478
- component Child(props: PropsWithChildren<{ count: Tracked<number> }>) {
1479
- const [children, count, rest] = trackSplit(props, ['children', 'count']);
1480
-
1481
- if (@count < 2) {
1482
- <button {...@rest}><@children /></button>
1483
- }
1484
- <pre>{@count}</pre>
1485
- }
1486
-
1487
- render(App);
1488
- flushSync();
1489
-
1490
- const buttonClickMe = container.querySelectorAll('button')[0];
1491
- const buttonIncrement = container.querySelectorAll('button')[1];
1492
- const countPre = container.querySelector('pre');
1493
-
1494
- expect(buttonClickMe.textContent).toBe('Click Me');
1495
- expect(countPre.textContent).toBe('0');
1496
- expect(logs).toEqual(['ref called']);
1497
-
1498
-
1499
- buttonClickMe.click();
1500
- buttonIncrement.click();
1501
- flushSync();
1502
-
1503
- expect(buttonClickMe.textContent).toBe('Clicked');
1504
- expect(countPre.textContent).toBe('1');
1505
-
1506
- buttonIncrement.click();
1507
- flushSync();
1508
-
1509
- expect(logs).toEqual(['ref called','cleanup ref']);
1510
- });
1511
-
1512
- it('errors on invalid value as null for track with trackSplit', () => {
1513
- component App() {
1514
- let message = track('');
1515
-
1516
- try{
1517
- const [a, b, rest] = trackSplit(null, ['a', 'b']);
1518
- } catch(e) {
1519
- @message = e.message;
1520
- }
1521
-
1522
- <pre>{@message}</pre>
1523
- }
1524
-
1525
- render(App);
1526
-
1527
- const pre = container.querySelectorAll('pre')[0];
1528
- expect(pre.textContent).toBe('Invalid value: expected a non-tracked object');
1529
- });
1530
-
1531
- it('errors on invalid value as array for track with trackSplit', () => {
1532
- component App() {
1533
- let message = track('');
1534
-
1535
- try{
1536
- const [a, b, rest] = trackSplit([1, 2, 3], ['a', 'b']);
1537
- } catch(e) {
1538
- @message = e.message;
1539
- }
1540
-
1541
- <pre>{@message}</pre>
1542
- }
1543
-
1544
- render(App);
1545
-
1546
- const pre = container.querySelectorAll('pre')[0];
1547
- expect(pre.textContent).toBe('Invalid value: expected a non-tracked object');
1548
- });
1549
-
1550
- it('errors on invalid value as tracked for track with trackSplit', () => {
1551
- component App() {
1552
- const t = track({a: 1, b: 2, c: 3});
1553
- let message = track('');
1554
-
1555
- try{
1556
- const [a, b, rest] = trackSplit(t, ['a', 'b']);
1557
- } catch(e) {
1558
- @message = e.message;
1559
- }
1560
-
1561
- <pre>{@message}</pre>
1562
- }
1563
-
1564
- render(App);
1565
-
1566
- const pre = container.querySelectorAll('pre')[0];
1567
- expect(pre.textContent).toBe('Invalid value: expected a non-tracked object');
1568
- });
1569
-
1570
- it('returns undefined for non-existent props in track with trackSplit', () => {
1571
- component App() {
1572
- const [a, b, rest] = trackSplit({a: 1, c: 1}, ['a', 'b']);
1573
-
1574
- <pre>{@a}</pre>
1575
- <pre>{String(@b)}</pre>
1576
- <pre>{@rest.c}</pre>
1577
- }
1578
-
1579
- render(App);
1580
-
1581
- const preA = container.querySelectorAll('pre')[0];
1582
- const preB = container.querySelectorAll('pre')[1];
1583
- const preC = container.querySelectorAll('pre')[2];
1584
-
1585
- expect(preA.textContent).toBe('1');
1586
- expect(preB.textContent).toBe('undefined');
1587
- expect(preC.textContent).toBe('1');
1588
- });
1589
-
1590
- it('returns the same tracked object if plain track is called with a tracked object', () => {
1591
- component App() {
1592
- const t = track({a: 1, b: 2, c: 3});
1593
- const doublet = track(t);
1594
-
1595
- <pre>{t === doublet}</pre>
1596
- }
1597
-
1598
- render(App);
1599
-
1600
- const pre = container.querySelectorAll('pre')[0];
1601
- expect(pre.textContent).toBe('true');
1602
- });
1603
-
1604
- it('should handle lexical scopes correctly', () => {
1605
- component App() {
1606
- <section>
1607
- let sectionData = 'Nested scope variable';
1608
-
1609
- {sectionData}
1610
- </section>
1611
- }
1612
-
1613
- render(App);
1614
- expect(container).toMatchSnapshot();
1615
- });
1616
-
1617
- it('uses track get and set where both mutate value', () => {
1618
- component App() {
1619
- let count = track(0, v => v + 1, v => v * 2);
1620
-
1621
-
1622
- <div class='count'>{@count}</div>
1623
- <button onClick={() => { @count++ }}>{'Increment'}</button>
1624
- }
1625
-
1626
- render(App);
1627
-
1628
- const countDiv = container.querySelector('.count');
1629
- const button = container.querySelector('button');
1630
-
1631
- expect(countDiv.textContent).toBe('1');
1632
-
1633
- button.click();
1634
- flushSync();
1635
- expect(countDiv.textContent).toBe('5');
1636
- });
1637
-
1638
- it('uses track get and set where set only mutates value', () => {
1639
- component App() {
1640
- let count = track(1, v => v, v => v * 2);
1641
-
1642
-
1643
- <div class='count'>{@count}</div>
1644
- <button onClick={() => { @count++ }}>{'Increment'}</button>
1645
- }
1646
-
1647
- render(App);
1648
-
1649
- const countDiv = container.querySelector('.count');
1650
- const button = container.querySelector('button');
1651
-
1652
- expect(countDiv.textContent).toBe('1');
1653
-
1654
- button.click();
1655
- flushSync();
1656
- expect(countDiv.textContent).toBe('4');
1657
- });
1658
-
1659
- it('uses track get and set where get only mutates value', () => {
1660
- component App() {
1661
- let count = track(0, v => v + 1, v => v);
1662
-
1663
-
1664
- <div class='count'>{@count}</div>
1665
- <button onClick={() => { @count++ }}>{'Increment'}</button>
1666
- }
1667
-
1668
- render(App);
1669
-
1670
- const countDiv = container.querySelector('.count');
1671
- const button = container.querySelector('button');
1672
-
1673
- expect(countDiv.textContent).toBe('1');
1674
-
1675
- button.click();
1676
- flushSync();
1677
- expect(countDiv.textContent).toBe('3');
1678
- });
1679
-
1680
- it('passes in next and prev to track set function', () => {
1681
- let logs = [];
1682
-
1683
- component App() {
1684
- let count = track(0, v => v, (next, prev) => {
1685
- logs.push(prev, next);
1686
- return next;
1687
- });
1688
-
1689
- <button onClick={() => { @count++ }}>{'Increment'}</button>
1690
- }
1691
-
1692
- render(App);
1693
-
1694
- const button = container.querySelector('button');
1695
- button.click();
1696
- flushSync();
1697
-
1698
- expect(logs).toEqual([0, 1]);
1699
- });
1700
-
1701
- it('works as a reactive TrackedArray when constructed using # syntactic sugar', () => {
1702
- component App() {
1703
- const array = #[1, 2, 3];
1704
-
1705
- <pre>{String(array[3])}</pre>
1706
- <pre>{array[0]}</pre>
1707
- <pre>{TRACKED_ARRAY in array}</pre>
1708
-
1709
- <button onClick={() => { array.push(array.length + 1); array[0] = array[0] + 1 }}>{'Add'}</button>
1710
- }
1711
-
1712
- render(App);
1713
-
1714
- const pre1 = container.querySelectorAll('pre')[0];
1715
- const pre2 = container.querySelectorAll('pre')[1];
1716
- const pre3 = container.querySelectorAll('pre')[2];
1717
- const button = container.querySelector('button');
1718
-
1719
- expect(pre1.textContent).toBe('undefined');
1720
- expect(pre2.textContent).toBe('1');
1721
- expect(pre3.textContent).toBe('true');
1722
-
1723
- button.click();
1724
- flushSync();
1725
-
1726
- expect(pre1.textContent).toBe('4');
1727
- expect(pre2.textContent).toBe('2');
1728
- });
1729
-
1730
- it('handles boolean props correctly', () => {
1731
- component App() {
1732
- <div data-disabled />
1733
-
1734
- <Child isDisabled />
1735
- }
1736
-
1737
- component Child({ isDisabled }) {
1738
- <input disabled={isDisabled} />
1739
- }
1740
-
1741
- render(App);
1742
- expect(container).toMatchSnapshot();
1743
- });
1744
-
1745
- it('tick function', async () => {
1746
- let resolve;
1747
- const promise = new Promise((res) => (resolve = res));
1748
-
1749
- component Basic() {
1750
- let value = track(0);
1751
- effect(() => {
1752
- untrack(() => {
1753
- @value++;
1754
- tick().then(() => resolve());
1755
- });
1756
- });
1757
- <p>{@value}</p>
1758
- }
1759
- render(Basic);
1760
-
1761
- const p = container.querySelector('p');
1762
- expect(p.textContent).toBe('0');
1763
- await promise;
1764
- expect(p.textContent).toBe('1');
1765
- });
1766
-
1767
- it('errors on mutating tracked value inside computed track() evaluation', () => {
1768
- component Basic() {
1769
- let count = track(0);
1770
-
1771
- const doubled = track(() => {
1772
- try {
1773
- @count *= 2;
1774
- } catch (e) {
1775
- error = e.message;
1776
- }
1777
- });
1778
-
1779
- <p>{@doubled}</p>
1780
- }
1781
-
1782
- render(Basic);
1783
-
1784
- expect(error).toBe('Assignments or updates to tracked values are not allowed during computed "track(() => ...)" evaluation');
1785
- });
1786
-
1787
- it('errors on mutating tracked value inside untrack() in computed track() evaluation', () => {
1788
- component Basic() {
1789
- let count = track(0);
1790
-
1791
- const doubled = track(() => {
1792
- try {
1793
- untrack(() => {
1794
- @count *= 2;
1795
- });
1796
- } catch (e) {
1797
- error = e.message;
1798
- }
1799
- });
1800
-
1801
- <p>{@doubled}</p>
1802
- }
1803
-
1804
- render(Basic);
1805
-
1806
- expect(error).toBe('Assignments or updates to tracked values are not allowed during computed "track(() => ...)" evaluation');
1807
- });
1808
-
1809
- it("errors on mutating a tracked variable in track() getter", () => {
1810
- component Basic() {
1811
- let count = track(0);
1812
-
1813
- const doubled = track(0, (value) => {
1814
- try {
1815
- @count += 1;
1816
- } catch (e) {
1817
- error = e.message;
1818
- }
1819
- return value;
1820
- });
1821
-
1822
- <p>{@doubled}</p>
1823
- }
1824
-
1825
- render(Basic);
1826
-
1827
- expect(error).toBe('Assignments or updates to tracked values are not allowed during computed "track(() => ...)" evaluation');
1828
- });
1829
-
1830
- it("doesn't error on mutating a tracked variable in track() setter", () => {
1831
- component Basic() {
1832
- let count = track(0);
1833
-
1834
- const doubled = track(0, undefined, (value) => {
1835
- @count += value;
1836
- return value;
1837
- });
1838
-
1839
- <p>{@doubled}</p>
1840
- }
1841
-
1842
- render(Basic);
1843
-
1844
- expect(error).toBe(undefined);
1845
- });
1846
-
1847
- it('unwraps tracked values inside effect', () => {
1848
- let state = {};
1849
-
1850
- component Basic() {
1851
- let count = track(0);
1852
-
1853
- effect(() => {
1854
- state.count = @count;
1855
- })
1856
- }
1857
-
1858
- render(Basic);
1859
- flushSync();
1860
-
1861
- expect(state.count).toBe(0);
1862
- });
1863
-
1864
- it('does not unwrap values with update expressions inside effect', () => {
1865
- let state = {};
1866
-
1867
- component Basic() {
1868
- let count = track(5);
1869
-
1870
- effect(() => {
1871
- untrack(() => {
1872
- state.initialValue = @count;
1873
- state.preIncrement = ++@count;
1874
- state.postIncrement = @count++;
1875
- state.preDecrement = --@count;
1876
- state.postDecrement = @count--;
1877
- state.finalValue = @count;
1878
- });
1879
- })
1880
- }
1881
-
1882
- render(Basic);
1883
- flushSync();
1884
-
1885
- expect(state.initialValue).toBe(5);
1886
- expect(state.preIncrement).toBe(6);
1887
- expect(state.postIncrement).toBe(6);
1888
- expect(state.preDecrement).toBe(6);
1889
- expect(state.postDecrement).toBe(6);
1890
- expect(state.finalValue).toBe(5);
1891
- });
1892
- });