ripple 0.2.121 → 0.2.125

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,291 @@
1
+ import { track, flushSync, get, set, effect, untrack } from 'ripple';
2
+
3
+ describe('basic client > get/set functions', () => {
4
+ it('gets tracked value', () => {
5
+ component Test() {
6
+ let count = track(0);
7
+
8
+ <div>{get(count)}</div>
9
+ }
10
+
11
+ render(Test);
12
+
13
+ const div = container.querySelector('div');
14
+ expect(div.textContent).toBe('0');
15
+ });
16
+
17
+ it('gets tracked value after mutation', () => {
18
+ component Test() {
19
+ let count = track(0);
20
+
21
+ <p>{get(count)}</p>
22
+ <button onClick={() => @count++}>{'increment'}</button>
23
+ }
24
+
25
+ render(Test);
26
+
27
+ const p = container.querySelector('p');
28
+ expect(p.textContent).toBe('0');
29
+
30
+ const button = container.querySelector('button');
31
+ button.click();
32
+ flushSync();
33
+
34
+ expect(p.textContent).toBe('1');
35
+ });
36
+
37
+ it('gets tracked value after multiple mutations', () => {
38
+ component Test() {
39
+ let count = track(0);
40
+
41
+ <p>{get(count)}</p>
42
+ <button onClick={() => {
43
+ @count++;
44
+ @count++;
45
+ @count++;
46
+ }}>{'increment'}</button>
47
+ }
48
+
49
+ render(Test);
50
+
51
+ const p = container.querySelector('p');
52
+ expect(p.textContent).toBe('0');
53
+
54
+ const button = container.querySelector('button');
55
+ button.click();
56
+ flushSync();
57
+
58
+ expect(p.textContent).toBe('3');
59
+ });
60
+
61
+ it('sets tracked value', () => {
62
+ component Test() {
63
+ let count = track(0);
64
+
65
+ <p>{get(count)}</p>
66
+ <button onClick={() => set(count, 10)}>{'set to 10'}</button>
67
+ }
68
+
69
+ render(Test);
70
+
71
+ const p = container.querySelector('p');
72
+ expect(p.textContent).toBe('0');
73
+
74
+ const button = container.querySelector('button');
75
+ button.click();
76
+ flushSync();
77
+
78
+ expect(p.textContent).toBe('10');
79
+ });
80
+
81
+ it('sets tracked value multiple times', () => {
82
+ component Test() {
83
+ let count = track(0);
84
+
85
+ <p>{get(count)}</p>
86
+ <button onClick={() => {
87
+ set(count, 5);
88
+ set(count, 15);
89
+ set(count, 25);
90
+ }}>{'set multiple times'}</button>
91
+ }
92
+
93
+ render(Test);
94
+
95
+ const p = container.querySelector('p');
96
+ expect(p.textContent).toBe('0');
97
+
98
+ const button = container.querySelector('button');
99
+ button.click();
100
+ flushSync();
101
+
102
+ expect(p.textContent).toBe('25');
103
+ });
104
+
105
+ it('sets tracked value based on previous value', () => {
106
+ component Test() {
107
+ let count = track(0);
108
+
109
+ <p>{get(count)}</p>
110
+ <button onClick={() => set(count, get(count) + 10)}>{'add 10'}</button>
111
+ }
112
+
113
+ render(Test);
114
+
115
+ const p = container.querySelector('p');
116
+ expect(p.textContent).toBe('0');
117
+
118
+ const button = container.querySelector('button');
119
+
120
+ button.click();
121
+ flushSync();
122
+
123
+ expect(p.textContent).toBe('10');
124
+
125
+ button.click();
126
+ flushSync();
127
+
128
+ expect(p.textContent).toBe('20');
129
+ });
130
+
131
+ it('sets tracked value multiple times based on previous value', () => {
132
+ component Test() {
133
+ let count = track(0);
134
+
135
+ <p>{get(count)}</p>
136
+ <button onClick={() => {
137
+ set(count, get(count) + 5);
138
+ set(count, get(count) + 15);
139
+ set(count, get(count) + 25);
140
+ }}>{'add multiple times'}</button>
141
+ }
142
+
143
+ render(Test);
144
+
145
+ const p = container.querySelector('p');
146
+ expect(p.textContent).toBe('0');
147
+
148
+ const button = container.querySelector('button');
149
+
150
+ button.click();
151
+ flushSync();
152
+
153
+ expect(p.textContent).toBe('45');
154
+
155
+ button.click();
156
+ flushSync();
157
+
158
+ expect(p.textContent).toBe('90');
159
+ });
160
+
161
+ function store() {
162
+ return track(0);
163
+ }
164
+
165
+ it('gets value declared outside Ripple component', () => {
166
+ component Test() {
167
+ let count = store();
168
+ <p>{get(count)}</p>
169
+ }
170
+
171
+ render(Test);
172
+
173
+ const p = container.querySelector('p');
174
+ expect(p.textContent).toBe('0');
175
+ });
176
+
177
+ it('sets value declared outside Ripple component', () => {
178
+ component Test() {
179
+ let count = store();
180
+
181
+ <p>{get(count)}</p>
182
+ <button onClick={() => set(count, 50)}>{'set to 50'}</button>
183
+ }
184
+
185
+ render(Test);
186
+
187
+ const p = container.querySelector('p');
188
+ expect(p.textContent).toBe('0');
189
+
190
+ const button = container.querySelector('button');
191
+ button.click();
192
+ flushSync();
193
+
194
+ expect(p.textContent).toBe('50');
195
+ });
196
+
197
+ it('works with effects', () => {
198
+ component Test() {
199
+ let count = track(0);
200
+ let double = track(0);
201
+
202
+ effect(() => {
203
+ set(double, get(count) * 2);
204
+ });
205
+
206
+ <p>{get(double)}</p>
207
+ <button onClick={() => set(count, get(count) + 1)}>{'increment'}</button>
208
+ }
209
+
210
+ render(Test);
211
+
212
+ const p = container.querySelector('p');
213
+ expect(p.textContent).toBe('0');
214
+
215
+ const button = container.querySelector('button');
216
+ button.click();
217
+ flushSync();
218
+
219
+ expect(p.textContent).toBe('2');
220
+
221
+ button.click();
222
+ flushSync();
223
+
224
+ expect(p.textContent).toBe('4');
225
+ });
226
+
227
+ it('works with effects and untrack', () => {
228
+ component Test() {
229
+ let count = track(0);
230
+ let double = track(0);
231
+
232
+ effect(() => {
233
+ untrack(() => {
234
+ set(double, get(count) * 2);
235
+ });
236
+ });
237
+
238
+ <p>{get(double)}</p>
239
+ <button onClick={() => set(count, get(count) + 1)}>{'increment'}</button>
240
+ }
241
+
242
+ render(Test);
243
+
244
+ const p = container.querySelector('p');
245
+ expect(p.textContent).toBe('0');
246
+
247
+ const button = container.querySelector('button');
248
+
249
+ button.click();
250
+ flushSync();
251
+
252
+ expect(p.textContent).toBe('2');
253
+
254
+ button.click();
255
+ flushSync();
256
+
257
+ expect(p.textContent).toBe('2');
258
+ });
259
+
260
+ it("get isn't reactive when declared outside Ripple context", () => {
261
+ let count = store();
262
+
263
+ component Test() {
264
+ <p>{get(count)}</p>
265
+ <button onClick={() => { set(count, get(count) + 1) }}>{'increment'}</button>
266
+ }
267
+
268
+ expect(get(count)).toBe(0);
269
+
270
+ render(Test);
271
+
272
+ const p = container.querySelector('p');
273
+ expect(p.textContent).toBe('0');
274
+ expect(get(count)).toBe(0);
275
+
276
+ const button = container.querySelector('button');
277
+ button.click();
278
+ flushSync();
279
+
280
+ expect(p.textContent).toBe('0');
281
+ expect(get(count)).toBe(1);
282
+ });
283
+
284
+ it('throws on trying to set a value outside Ripple component', () => {
285
+ let count = store();
286
+
287
+ expect(get(count)).toBe(0);
288
+ expect(() => set(count, 1)).toThrow();
289
+ expect(get(count)).toBe(0);
290
+ });
291
+ });
@@ -1,6 +1,7 @@
1
- import { flushSync, track, createRefKey } from 'ripple';
1
+ import { flushSync, track, createRefKey, trackSplit } from 'ripple';
2
2
 
3
3
  describe('dynamic DOM elements', () => {
4
+
4
5
  it('renders static dynamic element', () => {
5
6
  component App() {
6
7
  let tag = track('div');
@@ -24,14 +25,17 @@ describe('dynamic DOM elements', () => {
24
25
  <@tag id="dynamic">{'Hello World'}</@tag>
25
26
  }
26
27
  render(App);
28
+
27
29
  // Initially should be a div
28
30
  let dynamicElement = container.querySelector('#dynamic');
29
31
  expect(dynamicElement.tagName).toBe('DIV');
30
32
  expect(dynamicElement.textContent).toBe('Hello World');
33
+
31
34
  // Click button to change tag
32
35
  const button = container.querySelector('button');
33
36
  button.click();
34
37
  flushSync();
38
+
35
39
  // Should now be a span
36
40
  dynamicElement = container.querySelector('#dynamic');
37
41
  expect(dynamicElement.tagName).toBe('SPAN');
@@ -46,7 +50,7 @@ describe('dynamic DOM elements', () => {
46
50
  }
47
51
  render(App);
48
52
 
49
- const element = container.querySelector('input') as HTMLInputElement;
53
+ const element = container.querySelector('input');
50
54
  expect(element).toBeTruthy();
51
55
  expect(element.type).toBe('text');
52
56
  expect(element.value).toBe('test');
@@ -109,8 +113,8 @@ describe('dynamic DOM elements', () => {
109
113
  component App() {
110
114
  let tag = track('span');
111
115
 
112
- <@tag style={{
113
- color: 'red',
116
+ <@tag style={{
117
+ color: 'red',
114
118
  fontSize: '16px',
115
119
  fontWeight: 'bold'
116
120
  }}>
@@ -150,17 +154,18 @@ describe('dynamic DOM elements', () => {
150
154
  });
151
155
 
152
156
  it('handles dynamic element with ref', () => {
153
- let capturedElement: HTMLArticleElement | undefined;
157
+ let capturedElement = null;
154
158
 
155
159
  component App() {
156
160
  let tag = track('article');
157
161
 
158
- <@tag {ref (node: HTMLArticleElement) => { capturedElement = node; }} id="ref-test">
162
+ <@tag {ref (node) => { capturedElement = node; }} id="ref-test">
159
163
  {'Element with ref'}
160
164
  </@tag>
161
165
  }
162
166
  render(App);
163
167
  flushSync();
168
+
164
169
  expect(capturedElement).toBeTruthy();
165
170
  expect(capturedElement.tagName).toBe('ARTICLE');
166
171
  expect(capturedElement.id).toBe('ref-test');
@@ -171,7 +176,7 @@ describe('dynamic DOM elements', () => {
171
176
  component App() {
172
177
  let tag = track('header');
173
178
 
174
- function elementRef(node: HTMLHeaderElement) {
179
+ function elementRef(node) {
175
180
  // Set an attribute on the element to prove ref was called
176
181
  node.setAttribute('data-spread-ref-called', 'true');
177
182
  node.setAttribute('data-spread-ref-tag', node.tagName.toLowerCase());
@@ -196,4 +201,201 @@ describe('dynamic DOM elements', () => {
196
201
  expect(element.id).toBe('spread-ref-test');
197
202
  expect(element.className).toBe('ref-element');
198
203
  });
199
- });
204
+
205
+ it('has reactive attributes on dynamic elements', () => {
206
+ component App() {
207
+ let tag = track('div');
208
+ let count = track(0);
209
+
210
+ <button onClick={() => { @count++; }}>{'Increment'}</button>
211
+ <@tag
212
+ id={@count % 2 ? 'even' : 'odd'}
213
+ class={@count % 2 ? 'even-class' : 'odd-class'}
214
+ data-count={@count}
215
+ >
216
+ {'Count: '}{@count}
217
+ </@tag>
218
+ }
219
+
220
+ render(App);
221
+
222
+ const button = container.querySelector('button');
223
+ const element = container.querySelector('div');
224
+
225
+ // Initial state
226
+ expect(element.id).toBe('odd');
227
+ expect(element.className).toBe('odd-class');
228
+ expect(element.getAttribute('data-count')).toBe('0');
229
+ expect(element.textContent).toBe('Count: 0');
230
+
231
+ // Click to increment
232
+ button.click();
233
+ flushSync();
234
+
235
+ // Attributes should be reactive and update
236
+ expect(element.id).toBe('even');
237
+ expect(element.className).toBe('even-class');
238
+ expect(element.getAttribute('data-count')).toBe('1');
239
+ expect(element.textContent).toBe('Count: 1');
240
+
241
+ // Click again
242
+ button.click();
243
+ flushSync();
244
+
245
+ // Should toggle back
246
+ expect(element.id).toBe('odd');
247
+ expect(element.className).toBe('odd-class');
248
+ expect(element.getAttribute('data-count')).toBe('2');
249
+ expect(element.textContent).toBe('Count: 2');
250
+ });
251
+
252
+ it('applies scoped CSS to dynamic elements', () => {
253
+ component App() {
254
+ let tag = track('div');
255
+
256
+ <@tag class="test-class">{'Dynamic element'}</@tag>
257
+
258
+ <style>
259
+ .test-class {
260
+ color: red;
261
+ }
262
+ </style>
263
+ }
264
+
265
+ render(App);
266
+
267
+ const element = container.querySelector('div');
268
+ expect(element).toBeTruthy();
269
+ console.log(element);
270
+ expect(element.classList.contains('test-class')).toBe(true);
271
+
272
+ // Check if scoped CSS class is present - THIS MIGHT FAIL if CSS pruning issue exists
273
+ const classes = Array.from(element.classList);
274
+ const hasScopedClass = classes.some(cls => cls.startsWith('ripple-'));
275
+ expect(hasScopedClass).toBe(true);
276
+ });
277
+
278
+ it('applies scoped CSS to dynamic elements with reactive classes', () => {
279
+ component App() {
280
+ let tag = track('button');
281
+ let count = track(0);
282
+
283
+ <@tag
284
+ class={@count % 2 ? 'even' : 'odd'}
285
+ id={@count % 2 ? 'even' : 'odd'}
286
+ onClick={() => { @count++; }}
287
+ >
288
+ {'Count: '}{@count}
289
+ </@tag>
290
+
291
+ <style>
292
+ .even {
293
+ background-color: green;
294
+ color: white;
295
+ }
296
+ .odd {
297
+ background-color: red;
298
+ color: white;
299
+ }
300
+ </style>
301
+ }
302
+
303
+ render(App);
304
+
305
+ const button = container.querySelector('button');
306
+ expect(button).toBeTruthy();
307
+
308
+ // Initial state: should be odd (count=0, 0%2=false)
309
+ expect(button.classList.contains('odd')).toBe(true);
310
+ expect(button.classList.contains('even')).toBe(false);
311
+ expect(button.id).toBe('odd');
312
+ expect(button.textContent).toBe('Count: 0');
313
+
314
+ // Check if scoped CSS hash is applied to dynamic element
315
+ const classes = Array.from(button.classList);
316
+ const hasScopedClass = classes.some(cls => cls.startsWith('ripple-'));
317
+ expect(hasScopedClass).toBe(true);
318
+
319
+ // Click to increment
320
+ button.click();
321
+ flushSync();
322
+
323
+ // Should now be even (count=1, 1%2=true)
324
+ expect(button.classList.contains('even')).toBe(true);
325
+ expect(button.classList.contains('odd')).toBe(false);
326
+ expect(button.id).toBe('even');
327
+ expect(button.textContent).toBe('Count: 1');
328
+
329
+ // Scoped CSS class should still be present
330
+ const newClasses = Array.from(button.classList);
331
+ const stillHasScopedClass = newClasses.some(cls => cls.startsWith('ripple-'));
332
+ expect(stillHasScopedClass).toBe(true);
333
+
334
+ // Click again
335
+ button.click();
336
+ flushSync();
337
+
338
+ // Should toggle back to odd (count=2, 2%2=false)
339
+ expect(button.classList.contains('odd')).toBe(true);
340
+ expect(button.classList.contains('even')).toBe(false);
341
+ expect(button.id).toBe('odd');
342
+ expect(button.textContent).toBe('Count: 2');
343
+ });
344
+
345
+ it('handles spread attributes with class and CSS scoping ', () => {
346
+ component DyanamicButton(props) {
347
+ const tag = track('button');
348
+ const [children, rest] = trackSplit(props, ['children']);
349
+ <@tag {...@rest}>{@rest.class}</@tag>
350
+
351
+ <style>
352
+ .even {
353
+ background-color: green;
354
+ }
355
+ .odd {
356
+ background-color: red;
357
+ }
358
+ </style>
359
+ }
360
+
361
+ component App() {
362
+ const count = track(0);
363
+ <DyanamicButton
364
+ class={@count % 2 ? 'even' : 'odd'}
365
+ id={@count % 2 ? 'even' : 'odd'}
366
+ onClick={() => { @count++; }}
367
+ />
368
+ }
369
+
370
+ render(App);
371
+
372
+ const button = container.querySelector('button');
373
+ expect(button).toBeTruthy();
374
+
375
+
376
+ // Initial state: should be odd (count=0, 0%2=false)
377
+ expect(button.classList.contains('odd')).toBe(true);
378
+ expect(button.classList.contains('even')).toBe(false);
379
+ expect(button.id).toBe('odd');
380
+
381
+ // Check if scoped CSS hash is applied (this is the critical test)
382
+ const classes = Array.from(button.classList);
383
+ const hasScopedClass = classes.some(cls => cls.startsWith('ripple-'));
384
+ expect(hasScopedClass).toBe(true);
385
+
386
+ // Click to increment
387
+ button.click();
388
+ flushSync();
389
+
390
+ // Should now be even (count=1, 1%2=true)
391
+ expect(button.classList.contains('even')).toBe(true);
392
+ expect(button.classList.contains('odd')).toBe(false);
393
+ expect(button.id).toBe('even');
394
+
395
+ // Both classes should still be present
396
+ const newClasses = Array.from(button.classList);
397
+ const stillHasScopedClass = newClasses.some(cls => cls.startsWith('ripple-'));
398
+ expect(stillHasScopedClass).toBe(true);
399
+ expect(newClasses.includes('even')).toBe(true);
400
+ });
401
+ });
@@ -0,0 +1,61 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { render } from 'ripple/server';
3
+ import { track, set, get } from 'ripple';
4
+
5
+ describe('await in control flow', () => {
6
+ it('should handle await inside if statement', async () => {
7
+ component App() {
8
+ let condition = true;
9
+ let data = track('loading');
10
+
11
+ if (condition) {
12
+ await new Promise(resolve => setTimeout(() => {
13
+ @data = 'loaded';
14
+ resolve();
15
+ }, 10));
16
+ }
17
+
18
+ <div>{@data}</div>
19
+ }
20
+
21
+ const { body } = await render(App);
22
+ expect(body).toBe('<div>loaded</div>');
23
+ });
24
+
25
+ it('should handle await inside for...of loop', async () => {
26
+ component App() {
27
+ const items = [1, 2, 3];
28
+ let result = '';
29
+
30
+ for (const item of items) {
31
+ await new Promise(resolve => setTimeout(resolve, 5));
32
+ result += item;
33
+ }
34
+
35
+ <div>{result}</div>
36
+ }
37
+
38
+ const { body } = await render(App);
39
+ expect(body).toBe('<div>123</div>');
40
+ });
41
+
42
+ it('should handle await inside switch statement', async () => {
43
+ component App() {
44
+ let value = 'b';
45
+
46
+ switch (value) {
47
+ case 'a':
48
+ <div>{'Case A'}</div>
49
+ break;
50
+ case 'b':
51
+ await new Promise(resolve => setTimeout(resolve, 10));
52
+ <div>{'Case B'}</div>
53
+ break;
54
+ default:
55
+ <div>{'Default Case'}</div>
56
+ }
57
+ }
58
+
59
+ const { body } = await render(App);
60
+ expect(body).toBe('<div>Case B</div>');
61
+ });});
@@ -0,0 +1,44 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { render } from 'ripple/server';
3
+
4
+ describe('for statements in SSR', () => {
5
+ it('renders a simple static array', async () => {
6
+ component App() {
7
+ const items = ['Item 1', 'Item 2', 'Item 3'];
8
+
9
+ for (const item of items) {
10
+ <div class={item}>{item}</div>
11
+ }
12
+ }
13
+
14
+ const { body } = await render(App);
15
+ expect(body).toBe('<div class="Item 1">Item 1</div><div class="Item 2">Item 2</div><div class="Item 3">Item 3</div>');
16
+ });
17
+
18
+ it('renders nested for...of loops', async () => {
19
+ component App() {
20
+ const groups = [
21
+ {
22
+ name: 'Group 1',
23
+ items: ['Item 1.1', 'Item 1.2']
24
+ },
25
+ {
26
+ name: 'Group 2',
27
+ items: ['Item 2.1', 'Item 2.2']
28
+ }
29
+ ];
30
+
31
+ for (const group of groups) {
32
+ <h1>{group.name}</h1>
33
+ <ul>
34
+ for (const item of group.items) {
35
+ <li>{item}</li>
36
+ }
37
+ </ul>
38
+ }
39
+ }
40
+
41
+ const { body } = await render(App);
42
+ expect(body).toBe('<h1>Group 1</h1><ul><li>Item 1.1</li><li>Item 1.2</li></ul><h1>Group 2</h1><ul><li>Item 2.1</li><li>Item 2.2</li></ul>');
43
+ });
44
+ });
@@ -63,4 +63,24 @@ describe('if statements in SSR', () => {
63
63
  const { body } = await render(App);
64
64
  expect(body).toBe('<div>Default Case</div>');
65
65
  });
66
- });
66
+
67
+ it('renders nested if-else blocks correctly', async () => {
68
+ component App() {
69
+ let outer = true;
70
+ let inner = false;
71
+
72
+ if (outer) {
73
+ if (inner) {
74
+ <div>{'Outer true, Inner true'}</div>
75
+ } else {
76
+ <div>{'Outer true, Inner false'}</div>
77
+ }
78
+ } else {
79
+ <div>{'Outer false'}</div>
80
+ }
81
+ }
82
+
83
+ const { body } = await render(App);
84
+ expect(body).toBe('<div>Outer true, Inner false</div>');
85
+ });
86
+ });