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.
- package/package.json +1 -1
- package/src/compiler/index.js +1 -1
- package/src/compiler/phases/1-parse/index.js +1 -0
- package/src/compiler/phases/2-analyze/index.js +66 -32
- package/src/compiler/phases/3-transform/client/index.js +19 -0
- package/src/compiler/phases/3-transform/server/index.js +50 -37
- package/src/runtime/internal/client/composite.js +2 -4
- package/src/runtime/internal/client/script.js +1 -1
- package/src/runtime/internal/server/index.js +22 -13
- package/src/utils/builders.js +2 -2
- package/tests/client/__snapshots__/for.test.ripple.snap +0 -80
- package/tests/client/basic/__snapshots__/basic.rendering.test.ripple.snap +0 -48
- package/tests/client/basic/basic.errors.test.ripple +16 -0
- package/tests/client/basic/basic.get-set.test.ripple +291 -0
- package/tests/client/dynamic-elements.test.ripple +210 -8
- package/tests/server/await.test.ripple +61 -0
- package/tests/server/for.test.ripple +44 -0
- package/tests/server/if.test.ripple +21 -1
- package/tests/utils/escaping.test.js +102 -0
- package/tests/utils/events.test.js +147 -0
- package/tests/utils/normalize_css_property_name.test.js +43 -0
- package/tests/utils/patterns.test.js +382 -0
- package/tests/utils/sanitize_template_string.test.js +51 -0
- package/types/server.d.ts +25 -3
|
@@ -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')
|
|
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
|
|
157
|
+
let capturedElement = null;
|
|
154
158
|
|
|
155
159
|
component App() {
|
|
156
160
|
let tag = track('article');
|
|
157
161
|
|
|
158
|
-
<@tag {ref (node
|
|
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
|
|
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
|
+
});
|