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.
- package/package.json +16 -16
- package/src/compiler/index.js +20 -1
- package/src/compiler/phases/1-parse/index.js +79 -0
- package/src/compiler/phases/3-transform/client/index.js +54 -8
- package/src/compiler/phases/3-transform/segments.js +107 -60
- package/src/compiler/phases/3-transform/server/index.js +21 -11
- package/src/compiler/types/index.d.ts +16 -0
- package/src/runtime/index-client.js +19 -185
- package/src/runtime/index-server.js +24 -0
- package/src/runtime/internal/client/bindings.js +443 -0
- package/src/runtime/internal/client/index.js +4 -0
- package/src/runtime/internal/client/runtime.js +10 -0
- package/src/runtime/internal/client/utils.js +0 -8
- package/src/runtime/map.js +11 -1
- package/src/runtime/set.js +11 -1
- package/tests/client/__snapshots__/for.test.ripple.snap +80 -0
- package/tests/client/_etc.test.ripple +5 -0
- package/tests/client/array/array.copy-within.test.ripple +120 -0
- package/tests/client/array/array.derived.test.ripple +495 -0
- package/tests/client/array/array.iteration.test.ripple +115 -0
- package/tests/client/array/array.mutations.test.ripple +385 -0
- package/tests/client/array/array.static.test.ripple +237 -0
- package/tests/client/array/array.to-methods.test.ripple +93 -0
- package/tests/client/basic/__snapshots__/basic.attributes.test.ripple.snap +60 -0
- package/tests/client/basic/__snapshots__/basic.rendering.test.ripple.snap +106 -0
- package/tests/client/basic/__snapshots__/basic.text.test.ripple.snap +49 -0
- package/tests/client/basic/basic.attributes.test.ripple +474 -0
- package/tests/client/basic/basic.collections.test.ripple +94 -0
- package/tests/client/basic/basic.components.test.ripple +225 -0
- package/tests/client/basic/basic.errors.test.ripple +126 -0
- package/tests/client/basic/basic.events.test.ripple +222 -0
- package/tests/client/basic/basic.reactivity.test.ripple +476 -0
- package/tests/client/basic/basic.rendering.test.ripple +204 -0
- package/tests/client/basic/basic.styling.test.ripple +63 -0
- package/tests/client/basic/basic.utilities.test.ripple +25 -0
- package/tests/client/boundaries.test.ripple +2 -21
- package/tests/client/compiler/__snapshots__/compiler.assignments.test.ripple.snap +12 -0
- package/tests/client/compiler/__snapshots__/compiler.typescript.test.ripple.snap +22 -0
- package/tests/client/compiler/compiler.assignments.test.ripple +112 -0
- package/tests/client/compiler/compiler.attributes.test.ripple +95 -0
- package/tests/client/compiler/compiler.basic.test.ripple +203 -0
- package/tests/client/compiler/compiler.regex.test.ripple +87 -0
- package/tests/client/compiler/compiler.typescript.test.ripple +29 -0
- package/tests/client/{__snapshots__/composite.test.ripple.snap → composite/__snapshots__/composite.render.test.ripple.snap} +2 -2
- package/tests/client/composite/composite.dynamic-components.test.ripple +100 -0
- package/tests/client/composite/composite.generics.test.ripple +211 -0
- package/tests/client/composite/composite.props.test.ripple +106 -0
- package/tests/client/composite/composite.reactivity.test.ripple +184 -0
- package/tests/client/composite/composite.render.test.ripple +84 -0
- package/tests/client/computed-properties.test.ripple +2 -21
- package/tests/client/context.test.ripple +5 -22
- package/tests/client/date.test.ripple +1 -20
- package/tests/client/dynamic-elements.test.ripple +16 -24
- package/tests/client/for.test.ripple +4 -23
- package/tests/client/head.test.ripple +11 -23
- package/tests/client/html.test.ripple +1 -20
- package/tests/client/input-value.test.ripple +11 -31
- package/tests/client/map.test.ripple +82 -20
- package/tests/client/media-query.test.ripple +10 -23
- package/tests/client/object.test.ripple +5 -24
- package/tests/client/portal.test.ripple +2 -19
- package/tests/client/ref.test.ripple +8 -26
- package/tests/client/set.test.ripple +84 -22
- package/tests/client/svg.test.ripple +1 -22
- package/tests/client/switch.test.ripple +6 -25
- package/tests/client/tracked-expression.test.ripple +2 -21
- package/tests/client/typescript-generics.test.ripple +0 -21
- package/tests/client/url/url.derived.test.ripple +83 -0
- package/tests/client/url/url.parsing.test.ripple +165 -0
- package/tests/client/url/url.partial-removal.test.ripple +198 -0
- package/tests/client/url/url.reactivity.test.ripple +449 -0
- package/tests/client/url/url.serialization.test.ripple +50 -0
- package/tests/client/url-search-params/url-search-params.derived.test.ripple +84 -0
- package/tests/client/url-search-params/url-search-params.initialization.test.ripple +61 -0
- package/tests/client/url-search-params/url-search-params.iteration.test.ripple +153 -0
- package/tests/client/url-search-params/url-search-params.mutation.test.ripple +343 -0
- package/tests/client/url-search-params/url-search-params.retrieval.test.ripple +160 -0
- package/tests/client/url-search-params/url-search-params.serialization.test.ripple +53 -0
- package/tests/client/url-search-params/url-search-params.tracked-url.test.ripple +55 -0
- package/tests/client.d.ts +12 -0
- package/tests/server/if.test.ripple +66 -0
- package/tests/setup-client.js +28 -0
- package/tsconfig.json +4 -2
- package/types/index.d.ts +92 -46
- package/LICENSE +0 -21
- package/tests/client/__snapshots__/basic.test.ripple.snap +0 -117
- package/tests/client/__snapshots__/compiler.test.ripple.snap +0 -33
- package/tests/client/array.test.ripple +0 -1455
- package/tests/client/basic.test.ripple +0 -1892
- package/tests/client/compiler.test.ripple +0 -541
- package/tests/client/composite.test.ripple +0 -692
- package/tests/client/url-search-params.test.ripple +0 -912
- package/tests/client/url.test.ripple +0 -954
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
import { track, trackSplit, flushSync, effect, untrack } from 'ripple';
|
|
2
|
+
|
|
3
|
+
describe('basic client > reactivity', () => {
|
|
4
|
+
it('renders multiple reactive lexical blocks', () => {
|
|
5
|
+
component Basic() {
|
|
6
|
+
<div>
|
|
7
|
+
let obj = {
|
|
8
|
+
count: track(0)
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
<span>{obj.@count}</span>
|
|
12
|
+
</div>
|
|
13
|
+
<div>
|
|
14
|
+
let b = {
|
|
15
|
+
count: track(0)
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
<button onClick={() => { b.@count-- }}>{'-'}</button>
|
|
19
|
+
<span class='count'>{b.@count}</span>
|
|
20
|
+
<button onClick={() => { b.@count++ }}>{'+'}</button>
|
|
21
|
+
</div>
|
|
22
|
+
}
|
|
23
|
+
render(Basic);
|
|
24
|
+
|
|
25
|
+
const buttons = container.querySelectorAll('button');
|
|
26
|
+
|
|
27
|
+
buttons[0].click();
|
|
28
|
+
flushSync();
|
|
29
|
+
|
|
30
|
+
expect(container.querySelector('.count').textContent).toBe('-1');
|
|
31
|
+
|
|
32
|
+
buttons[1].click();
|
|
33
|
+
flushSync();
|
|
34
|
+
|
|
35
|
+
expect(container.querySelector('.count').textContent).toBe('0');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('renders multiple reactive lexical blocks with complexity', () => {
|
|
39
|
+
component Basic() {
|
|
40
|
+
const count = 'count';
|
|
41
|
+
|
|
42
|
+
<div>
|
|
43
|
+
let obj = {
|
|
44
|
+
count: track(0)
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
<span>{obj[@count]}</span>
|
|
48
|
+
</div>
|
|
49
|
+
<div>
|
|
50
|
+
let b = {
|
|
51
|
+
count: track(0)
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
<button onClick={() => { b[@count]-- }}>{'-'}</button>
|
|
55
|
+
<span class='count'>{b[@count]}</span>
|
|
56
|
+
<button onClick={() => { b[@count]++ }}>{'+'}</button>
|
|
57
|
+
</div>
|
|
58
|
+
}
|
|
59
|
+
render(Basic);
|
|
60
|
+
|
|
61
|
+
const buttons = container.querySelectorAll('button');
|
|
62
|
+
|
|
63
|
+
buttons[0].click();
|
|
64
|
+
flushSync();
|
|
65
|
+
|
|
66
|
+
expect(container.querySelector('.count').textContent).toBe('-1');
|
|
67
|
+
|
|
68
|
+
buttons[1].click();
|
|
69
|
+
flushSync();
|
|
70
|
+
|
|
71
|
+
expect(container.querySelector('.count').textContent).toBe('0');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('renders with computed reactive state', () => {
|
|
75
|
+
component Basic() {
|
|
76
|
+
let count = track(5);
|
|
77
|
+
|
|
78
|
+
<div class='count'>{@count}</div>
|
|
79
|
+
<div class='doubled'>{@count * 2}</div>
|
|
80
|
+
<div class='is-even'>{@count % 2 === 0 ? 'Even' : 'Odd'}</div>
|
|
81
|
+
<button onClick={() => { @count++ }}>{'Increment'}</button>
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
render(Basic);
|
|
85
|
+
|
|
86
|
+
const countDiv = container.querySelector('.count');
|
|
87
|
+
const doubledDiv = container.querySelector('.doubled');
|
|
88
|
+
const evenDiv = container.querySelector('.is-even');
|
|
89
|
+
const button = container.querySelector('button');
|
|
90
|
+
|
|
91
|
+
expect(countDiv.textContent).toBe('5');
|
|
92
|
+
expect(doubledDiv.textContent).toBe('10');
|
|
93
|
+
expect(evenDiv.textContent).toBe('Odd');
|
|
94
|
+
|
|
95
|
+
button.click();
|
|
96
|
+
flushSync();
|
|
97
|
+
|
|
98
|
+
expect(countDiv.textContent).toBe('6');
|
|
99
|
+
expect(doubledDiv.textContent).toBe('12');
|
|
100
|
+
expect(evenDiv.textContent).toBe('Even');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('basic reactivity with standard arrays should work', () => {
|
|
104
|
+
let logs: string[] = [];
|
|
105
|
+
|
|
106
|
+
component App() {
|
|
107
|
+
let first = track(0);
|
|
108
|
+
let second = track(0);
|
|
109
|
+
const arr = [first, second];
|
|
110
|
+
|
|
111
|
+
const total = track(() => arr.reduce((a, b) => a + @b, 0));
|
|
112
|
+
|
|
113
|
+
<button onClick={() => { @first++; }}>{'first:' + @first}</button>
|
|
114
|
+
<button onClick={() => { @second++; }}>{'second: ' + @second}</button>
|
|
115
|
+
|
|
116
|
+
effect(() => {
|
|
117
|
+
let _arr: number[] = [];
|
|
118
|
+
|
|
119
|
+
arr.forEach((item) => {
|
|
120
|
+
_arr.push(@item);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
logs.push(_arr.join(', '));
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
effect(() => {
|
|
127
|
+
if (arr.map(a => @a).includes(1)) {
|
|
128
|
+
logs.push('arr includes 1');
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
<div>{'Sum: ' + @total}</div>
|
|
133
|
+
<div>{'Comma Separated: ' + arr.map(a => @a).join(', ')}</div>
|
|
134
|
+
<div>{'Number to string: ' + arr.map(a => String(@a))}</div>
|
|
135
|
+
<div>{'Even numbers: ' + arr.map(a => @a).filter(a => a % 2 === 0)}</div>
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
render(App);
|
|
139
|
+
flushSync();
|
|
140
|
+
|
|
141
|
+
const buttons = container.querySelectorAll('button');
|
|
142
|
+
const divs = container.querySelectorAll('div');
|
|
143
|
+
|
|
144
|
+
expect(divs[0].textContent).toBe('Sum: 0');
|
|
145
|
+
expect(divs[1].textContent).toBe('Comma Separated: 0, 0');
|
|
146
|
+
expect(divs[2].textContent).toBe('Number to string: 0,0');
|
|
147
|
+
expect(divs[3].textContent).toBe('Even numbers: 0,0');
|
|
148
|
+
expect(logs).toEqual(['0, 0']);
|
|
149
|
+
|
|
150
|
+
buttons[0].click();
|
|
151
|
+
flushSync();
|
|
152
|
+
|
|
153
|
+
expect(divs[0].textContent).toBe('Sum: 1');
|
|
154
|
+
expect(divs[1].textContent).toBe('Comma Separated: 1, 0');
|
|
155
|
+
expect(divs[2].textContent).toBe('Number to string: 1,0');
|
|
156
|
+
expect(divs[3].textContent).toBe('Even numbers: 0');
|
|
157
|
+
expect(logs).toEqual(['0, 0', '1, 0', 'arr includes 1']);
|
|
158
|
+
|
|
159
|
+
buttons[1].click();
|
|
160
|
+
flushSync();
|
|
161
|
+
|
|
162
|
+
expect(divs[0].textContent).toBe('Sum: 2');
|
|
163
|
+
expect(divs[1].textContent).toBe('Comma Separated: 1, 1');
|
|
164
|
+
expect(divs[2].textContent).toBe('Number to string: 1,1');
|
|
165
|
+
expect(divs[3].textContent).toBe('Even numbers: ');
|
|
166
|
+
expect(logs).toEqual(['0, 0', '1, 0', 'arr includes 1', '1, 1', 'arr includes 1']);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
it('uses track get and set where both mutate value', () => {
|
|
172
|
+
component App() {
|
|
173
|
+
let count = track(0, v => v + 1, v => v * 2);
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
<div class='count'>{@count}</div>
|
|
177
|
+
<button onClick={() => { @count++ }}>{'Increment'}</button>
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
render(App);
|
|
181
|
+
|
|
182
|
+
const countDiv = container.querySelector('.count');
|
|
183
|
+
const button = container.querySelector('button');
|
|
184
|
+
|
|
185
|
+
expect(countDiv.textContent).toBe('1');
|
|
186
|
+
|
|
187
|
+
button.click();
|
|
188
|
+
flushSync();
|
|
189
|
+
expect(countDiv.textContent).toBe('5');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('uses track get and set where set only mutates value', () => {
|
|
193
|
+
component App() {
|
|
194
|
+
let count = track(1, v => v, v => v * 2);
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
<div class='count'>{@count}</div>
|
|
198
|
+
<button onClick={() => { @count++ }}>{'Increment'}</button>
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
render(App);
|
|
202
|
+
|
|
203
|
+
const countDiv = container.querySelector('.count');
|
|
204
|
+
const button = container.querySelector('button');
|
|
205
|
+
|
|
206
|
+
expect(countDiv.textContent).toBe('1');
|
|
207
|
+
|
|
208
|
+
button.click();
|
|
209
|
+
flushSync();
|
|
210
|
+
expect(countDiv.textContent).toBe('4');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('uses track get and set where get only mutates value', () => {
|
|
214
|
+
component App() {
|
|
215
|
+
let count = track(0, v => v + 1, v => v);
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
<div class='count'>{@count}</div>
|
|
219
|
+
<button onClick={() => { @count++ }}>{'Increment'}</button>
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
render(App);
|
|
223
|
+
|
|
224
|
+
const countDiv = container.querySelector('.count');
|
|
225
|
+
const button = container.querySelector('button');
|
|
226
|
+
|
|
227
|
+
expect(countDiv.textContent).toBe('1');
|
|
228
|
+
|
|
229
|
+
button.click();
|
|
230
|
+
flushSync();
|
|
231
|
+
expect(countDiv.textContent).toBe('3');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('passes in next and prev to track set function', () => {
|
|
235
|
+
let logs: number[] = [];
|
|
236
|
+
|
|
237
|
+
component App() {
|
|
238
|
+
let count = track(0, v => v, (next, prev) => {
|
|
239
|
+
logs.push(prev, next);
|
|
240
|
+
return next;
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
<button onClick={() => { @count++ }}>{'Increment'}</button>
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
render(App);
|
|
247
|
+
|
|
248
|
+
const button = container.querySelector('button');
|
|
249
|
+
button.click();
|
|
250
|
+
flushSync();
|
|
251
|
+
|
|
252
|
+
expect(logs).toEqual([0, 1]);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("doesn't error on mutating a tracked variable in track() setter", () => {
|
|
256
|
+
component Basic() {
|
|
257
|
+
let count = track(0);
|
|
258
|
+
|
|
259
|
+
const doubled = track(0, undefined, (value) => {
|
|
260
|
+
@count += value;
|
|
261
|
+
return value;
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
<p>{@doubled}</p>
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
render(Basic);
|
|
268
|
+
|
|
269
|
+
expect(error).toBe(undefined);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('unwraps tracked values inside effect', () => {
|
|
273
|
+
let state: { count?: number; } = {};
|
|
274
|
+
|
|
275
|
+
component Basic() {
|
|
276
|
+
let count = track(0);
|
|
277
|
+
|
|
278
|
+
effect(() => {
|
|
279
|
+
state.count = @count;
|
|
280
|
+
})
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
render(Basic);
|
|
284
|
+
flushSync();
|
|
285
|
+
|
|
286
|
+
expect(state.count).toBe(0);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('does not unwrap values with update expressions inside effect', () => {
|
|
290
|
+
let state: {
|
|
291
|
+
initialValue?: number;
|
|
292
|
+
preIncrement?: number;
|
|
293
|
+
postIncrement?: number;
|
|
294
|
+
preDecrement?: number;
|
|
295
|
+
postDecrement?: number;
|
|
296
|
+
finalValue?: number;
|
|
297
|
+
} = {};
|
|
298
|
+
|
|
299
|
+
component Basic() {
|
|
300
|
+
let count = track(5);
|
|
301
|
+
|
|
302
|
+
effect(() => {
|
|
303
|
+
untrack(() => {
|
|
304
|
+
state.initialValue = @count;
|
|
305
|
+
state.preIncrement = ++@count;
|
|
306
|
+
state.postIncrement = @count++;
|
|
307
|
+
state.preDecrement = --@count;
|
|
308
|
+
state.postDecrement = @count--;
|
|
309
|
+
state.finalValue = @count;
|
|
310
|
+
});
|
|
311
|
+
})
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
render(Basic);
|
|
315
|
+
flushSync();
|
|
316
|
+
|
|
317
|
+
expect(state.initialValue).toBe(5);
|
|
318
|
+
expect(state.preIncrement).toBe(6);
|
|
319
|
+
expect(state.postIncrement).toBe(6);
|
|
320
|
+
expect(state.preDecrement).toBe(6);
|
|
321
|
+
expect(state.postDecrement).toBe(6);
|
|
322
|
+
expect(state.finalValue).toBe(5);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
describe('track/trackSplit APIs', () => {
|
|
326
|
+
it('errors on invalid value as null for track with trackSplit', () => {
|
|
327
|
+
component App() {
|
|
328
|
+
let message = track('');
|
|
329
|
+
|
|
330
|
+
try{
|
|
331
|
+
const [a, b, rest] = trackSplit(null, ['a', 'b']);
|
|
332
|
+
} catch(e) {
|
|
333
|
+
@message = (e as Error).message;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
<pre>{@message}</pre>
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
render(App);
|
|
340
|
+
|
|
341
|
+
const pre = container.querySelectorAll('pre')[0];
|
|
342
|
+
expect(pre.textContent).toBe('Invalid value: expected a non-tracked object');
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('errors on invalid value as array for track with trackSplit', () => {
|
|
346
|
+
component App() {
|
|
347
|
+
let message = track('');
|
|
348
|
+
|
|
349
|
+
try{
|
|
350
|
+
const [a, b, rest] = trackSplit([1, 2, 3], ['a', 'b']);
|
|
351
|
+
} catch(e) {
|
|
352
|
+
@message = (e as Error).message;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
<pre>{@message}</pre>
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
render(App);
|
|
359
|
+
|
|
360
|
+
const pre = container.querySelectorAll('pre')[0];
|
|
361
|
+
expect(pre.textContent).toBe('Invalid value: expected a non-tracked object');
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('errors on invalid value as tracked for track with trackSplit', () => {
|
|
365
|
+
component App() {
|
|
366
|
+
const t = track({a: 1, b: 2, c: 3});
|
|
367
|
+
let message = track('');
|
|
368
|
+
|
|
369
|
+
try{
|
|
370
|
+
const [a, b, rest] = trackSplit(t, ['a', 'b']);
|
|
371
|
+
} catch(e) {
|
|
372
|
+
@message = (e as Error).message;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
<pre>{@message}</pre>
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
render(App);
|
|
379
|
+
|
|
380
|
+
const pre = container.querySelectorAll('pre')[0];
|
|
381
|
+
expect(pre.textContent).toBe('Invalid value: expected a non-tracked object');
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('returns undefined for non-existent props in track with trackSplit', () => {
|
|
385
|
+
component App() {
|
|
386
|
+
const [a, b, rest] = trackSplit({a: 1, c: 1}, ['a', 'b']);
|
|
387
|
+
|
|
388
|
+
<pre>{@a}</pre>
|
|
389
|
+
<pre>{String(@b)}</pre>
|
|
390
|
+
<pre>{@rest.c}</pre>
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
render(App);
|
|
394
|
+
|
|
395
|
+
const preA = container.querySelectorAll('pre')[0];
|
|
396
|
+
const preB = container.querySelectorAll('pre')[1];
|
|
397
|
+
const preC = container.querySelectorAll('pre')[2];
|
|
398
|
+
|
|
399
|
+
expect(preA.textContent).toBe('1');
|
|
400
|
+
expect(preB.textContent).toBe('undefined');
|
|
401
|
+
expect(preC.textContent).toBe('1');
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('returns the same tracked object if plain track is called with a tracked object', () => {
|
|
405
|
+
component App() {
|
|
406
|
+
const t = track({a: 1, b: 2, c: 3});
|
|
407
|
+
const doublet = track(t);
|
|
408
|
+
|
|
409
|
+
<pre>{t === doublet}</pre>
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
render(App);
|
|
413
|
+
|
|
414
|
+
const pre = container.querySelectorAll('pre')[0];
|
|
415
|
+
expect(pre.textContent).toBe('true');
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('can retain reactivity for destructure rest via track trackSplit', () => {
|
|
419
|
+
let logs: string[] = [];
|
|
420
|
+
|
|
421
|
+
component App() {
|
|
422
|
+
let count = track(0);
|
|
423
|
+
let name = track('Click Me');
|
|
424
|
+
|
|
425
|
+
function buttonRef(el: HTMLButtonElement) {
|
|
426
|
+
logs.push('ref called');
|
|
427
|
+
return () => {
|
|
428
|
+
logs.push('cleanup ref');
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
<Child
|
|
433
|
+
class="my-button"
|
|
434
|
+
onClick={() => @name === 'Click Me' ? @name = 'Clicked' : @name = 'Click Me'}
|
|
435
|
+
{@count}
|
|
436
|
+
{ref buttonRef}
|
|
437
|
+
>{@name}</Child>
|
|
438
|
+
|
|
439
|
+
<button onClick={() => @count++}>{'Increment Count'}</button>
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
component Child(props: PropsWithChildren<{ count: Tracked<number> }>) {
|
|
443
|
+
const [children, count, rest] = trackSplit(props, ['children', 'count']);
|
|
444
|
+
|
|
445
|
+
if (@count < 2) {
|
|
446
|
+
<button {...@rest}><@children /></button>
|
|
447
|
+
}
|
|
448
|
+
<pre>{@count}</pre>
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
render(App);
|
|
452
|
+
flushSync();
|
|
453
|
+
|
|
454
|
+
const buttonClickMe = container.querySelectorAll('button')[0];
|
|
455
|
+
const buttonIncrement = container.querySelectorAll('button')[1];
|
|
456
|
+
const countPre = container.querySelector('pre');
|
|
457
|
+
|
|
458
|
+
expect(buttonClickMe.textContent).toBe('Click Me');
|
|
459
|
+
expect(countPre.textContent).toBe('0');
|
|
460
|
+
expect(logs).toEqual(['ref called']);
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
buttonClickMe.click();
|
|
464
|
+
buttonIncrement.click();
|
|
465
|
+
flushSync();
|
|
466
|
+
|
|
467
|
+
expect(buttonClickMe.textContent).toBe('Clicked');
|
|
468
|
+
expect(countPre.textContent).toBe('1');
|
|
469
|
+
|
|
470
|
+
buttonIncrement.click();
|
|
471
|
+
flushSync();
|
|
472
|
+
|
|
473
|
+
expect(logs).toEqual(['ref called','cleanup ref']);
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
});
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { track, flushSync } from 'ripple';
|
|
3
|
+
|
|
4
|
+
describe('basic client > rendering & text', () => {
|
|
5
|
+
it('renders static text', () => {
|
|
6
|
+
component Basic() {
|
|
7
|
+
<div>{'Hello World'}</div>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
expect(container).toBeDefined();
|
|
11
|
+
|
|
12
|
+
render(Basic);
|
|
13
|
+
expect(container).toMatchSnapshot();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('renders semi-dynamic text', () => {
|
|
17
|
+
component Basic() {
|
|
18
|
+
let text = 'Hello World';
|
|
19
|
+
|
|
20
|
+
<div>{text}</div>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
render(Basic);
|
|
24
|
+
|
|
25
|
+
expect(container).toMatchSnapshot();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('renders dynamic text', () => {
|
|
29
|
+
component Basic() {
|
|
30
|
+
let text = track('Hello World');
|
|
31
|
+
|
|
32
|
+
<button onClick={() => { @text = 'Hello Ripple' }}>{'Change Text'}</button>
|
|
33
|
+
<div>{@text}</div>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
render(Basic);
|
|
37
|
+
|
|
38
|
+
const button = container.querySelector('button');
|
|
39
|
+
|
|
40
|
+
button.click();
|
|
41
|
+
flushSync();
|
|
42
|
+
|
|
43
|
+
expect(container.querySelector('div').textContent).toEqual('Hello Ripple');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('renders empty string literal', () => {
|
|
47
|
+
component Basic() {
|
|
48
|
+
<div>{''}</div>
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
render(Basic);
|
|
52
|
+
expect(container.querySelector('div').textContent).toEqual('');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('renders empty template literal', () => {
|
|
56
|
+
component Basic() {
|
|
57
|
+
<div>{``}</div>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
render(Basic);
|
|
61
|
+
expect(container.querySelector('div').textContent).toEqual('');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('renders tick template literal for nested children', () => {
|
|
65
|
+
component Child({ level, children }: { level: number, children: any }) {
|
|
66
|
+
if(level == 1) {
|
|
67
|
+
<h1><children /></h1>
|
|
68
|
+
}
|
|
69
|
+
if(level == 2) {
|
|
70
|
+
<h2><children /></h2>
|
|
71
|
+
}
|
|
72
|
+
if(level == 3) {
|
|
73
|
+
<h3><children /></h3>
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
component App() {
|
|
78
|
+
<Child level={1}>{`Heading 1`}</Child>
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
render(App);
|
|
82
|
+
expect(container.querySelector('h1').textContent).toEqual('Heading 1');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('renders simple JS expression logic correctly', () => {
|
|
86
|
+
component Example() {
|
|
87
|
+
let test = {}
|
|
88
|
+
let counter = 0;
|
|
89
|
+
test[counter++] = 'Test';
|
|
90
|
+
|
|
91
|
+
<div>{JSON.stringify(test)}</div>
|
|
92
|
+
<div>{JSON.stringify(counter)}</div>
|
|
93
|
+
}
|
|
94
|
+
render(Example);
|
|
95
|
+
|
|
96
|
+
expect(container).toMatchSnapshot();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('renders with mixed static and dynamic content', () => {
|
|
100
|
+
component Basic() {
|
|
101
|
+
let name = track('World');
|
|
102
|
+
let count = track(0);
|
|
103
|
+
const staticMessage = 'Welcome to Ripple!';
|
|
104
|
+
|
|
105
|
+
<div class='mixed-content'>
|
|
106
|
+
<h1>{staticMessage}</h1>
|
|
107
|
+
<p class='greeting'>{'Hello, ' + @name + '!'}</p>
|
|
108
|
+
<p class='notifications'>{'You have ' + @count + ' notifications'}</p>
|
|
109
|
+
<button onClick={() => { @count++ }}>{'Add Notification'}</button>
|
|
110
|
+
<button onClick={() => { @name = @name === 'World' ? 'User' : 'World' }}>{'Toggle Name'}</button>
|
|
111
|
+
</div>
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
render(Basic);
|
|
115
|
+
|
|
116
|
+
const heading = container.querySelector('h1');
|
|
117
|
+
const greetingP = container.querySelector('.greeting');
|
|
118
|
+
const notificationsP = container.querySelector('.notifications');
|
|
119
|
+
const buttons = container.querySelectorAll('button');
|
|
120
|
+
|
|
121
|
+
expect(heading.textContent).toBe('Welcome to Ripple!');
|
|
122
|
+
expect(greetingP.textContent).toBe('Hello, World!');
|
|
123
|
+
expect(notificationsP.textContent).toBe('You have 0 notifications');
|
|
124
|
+
|
|
125
|
+
buttons[0].click();
|
|
126
|
+
flushSync();
|
|
127
|
+
expect(notificationsP.textContent).toBe('You have 1 notifications');
|
|
128
|
+
|
|
129
|
+
buttons[1].click();
|
|
130
|
+
flushSync();
|
|
131
|
+
expect(greetingP.textContent).toBe('Hello, User!');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('basic operations', () => {
|
|
135
|
+
component App() {
|
|
136
|
+
let count = track(0)
|
|
137
|
+
<div>{@count++}</div>
|
|
138
|
+
<div>{++@count}</div>
|
|
139
|
+
<div>{5}</div>
|
|
140
|
+
<div>{@count}</div>
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
render(App);
|
|
144
|
+
expect(container).toMatchSnapshot();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('renders with conditional rendering using if statements', () => {
|
|
148
|
+
component Basic() {
|
|
149
|
+
let showContent = track(false);
|
|
150
|
+
let userRole = track('guest');
|
|
151
|
+
|
|
152
|
+
<button onClick={() => { @showContent = !@showContent }}>{'Toggle Content'}</button>
|
|
153
|
+
<button onClick={() => { @userRole = @userRole === 'guest' ? 'admin' : 'guest' }}>{'Toggle Role'}</button>
|
|
154
|
+
|
|
155
|
+
<div class='content'>
|
|
156
|
+
if (@showContent) {
|
|
157
|
+
if (@userRole === 'admin') {
|
|
158
|
+
<div class='admin-content'>{'Admin content'}</div>
|
|
159
|
+
} else {
|
|
160
|
+
<div class='user-content'>{'User content'}</div>
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
<div class='no-content'>{'No content'}</div>
|
|
164
|
+
}
|
|
165
|
+
</div>
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
render(Basic);
|
|
169
|
+
|
|
170
|
+
const buttons = container.querySelectorAll('button');
|
|
171
|
+
const contentDiv = container.querySelector('.content');
|
|
172
|
+
|
|
173
|
+
expect(contentDiv.querySelector('.no-content')).toBeTruthy();
|
|
174
|
+
expect(contentDiv.querySelector('.admin-content')).toBeFalsy();
|
|
175
|
+
expect(contentDiv.querySelector('.user-content')).toBeFalsy();
|
|
176
|
+
|
|
177
|
+
buttons[0].click();
|
|
178
|
+
flushSync();
|
|
179
|
+
|
|
180
|
+
expect(contentDiv.querySelector('.no-content')).toBeFalsy();
|
|
181
|
+
expect(contentDiv.querySelector('.user-content')).toBeTruthy();
|
|
182
|
+
expect(contentDiv.querySelector('.admin-content')).toBeFalsy();
|
|
183
|
+
|
|
184
|
+
buttons[1].click();
|
|
185
|
+
flushSync();
|
|
186
|
+
|
|
187
|
+
expect(contentDiv.querySelector('.no-content')).toBeFalsy();
|
|
188
|
+
expect(contentDiv.querySelector('.user-content')).toBeFalsy();
|
|
189
|
+
expect(contentDiv.querySelector('.admin-content')).toBeTruthy();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should handle lexical scopes correctly', () => {
|
|
193
|
+
component App() {
|
|
194
|
+
<section>
|
|
195
|
+
let sectionData = 'Nested scope variable';
|
|
196
|
+
|
|
197
|
+
{sectionData}
|
|
198
|
+
</section>
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
render(App);
|
|
202
|
+
expect(container).toMatchSnapshot();
|
|
203
|
+
});
|
|
204
|
+
});
|