ripple 0.2.208 → 0.2.210
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/CHANGELOG.md +43 -0
- package/README.md +2 -1
- package/package.json +2 -6
- package/shims/rollup-estree-types.d.ts +1 -1
- package/src/compiler/index.d.ts +1 -0
- package/src/compiler/index.js +7 -1
- package/src/compiler/phases/1-parse/index.js +15 -6
- package/src/compiler/phases/2-analyze/css-analyze.js +100 -104
- package/src/compiler/phases/2-analyze/index.js +215 -2
- package/src/compiler/phases/3-transform/client/index.js +388 -50
- package/src/compiler/phases/3-transform/segments.js +123 -39
- package/src/compiler/phases/3-transform/server/index.js +266 -13
- package/src/compiler/types/index.d.ts +16 -3
- package/src/compiler/utils.js +1 -15
- package/src/constants.js +0 -2
- package/src/helpers.d.ts +4 -0
- package/src/html-tree-validation.js +211 -0
- package/src/jsx-runtime.d.ts +260 -259
- package/src/jsx-runtime.js +12 -12
- package/src/runtime/array.js +17 -17
- package/src/runtime/create-subscriber.js +1 -1
- package/src/runtime/index-client.js +1 -5
- package/src/runtime/index-server.js +15 -0
- package/src/runtime/internal/client/compat.js +3 -3
- package/src/runtime/internal/client/composite.js +6 -1
- package/src/runtime/internal/client/head.js +50 -4
- package/src/runtime/internal/client/html.js +73 -12
- package/src/runtime/internal/client/hydration.js +12 -0
- package/src/runtime/internal/client/index.js +1 -1
- package/src/runtime/internal/client/portal.js +54 -29
- package/src/runtime/internal/client/rpc.js +3 -1
- package/src/runtime/internal/client/switch.js +5 -0
- package/src/runtime/internal/client/template.js +117 -11
- package/src/runtime/internal/client/try.js +1 -0
- package/src/runtime/internal/server/index.js +113 -1
- package/src/runtime/internal/server/rpc.js +4 -4
- package/src/runtime/map.js +2 -2
- package/src/runtime/object.js +6 -6
- package/src/runtime/proxy.js +12 -11
- package/src/runtime/reactive-value.js +9 -1
- package/src/runtime/set.js +12 -7
- package/src/runtime/url-search-params.js +0 -1
- package/src/server/index.js +4 -0
- package/src/utils/hashing.js +15 -0
- package/src/utils/normalize_css_property_name.js +1 -1
- package/tests/client/array/array.mutations.test.ripple +8 -8
- package/tests/client/basic/basic.errors.test.ripple +28 -0
- package/tests/client/basic/basic.events.test.ripple +6 -3
- package/tests/client/basic/basic.utilities.test.ripple +1 -1
- package/tests/client/compiler/compiler.regex.test.ripple +10 -8
- package/tests/client/composite/composite.generics.test.ripple +5 -2
- package/tests/client/dynamic-elements.test.ripple +30 -1
- package/tests/client/function-overload-import.ripple +6 -7
- package/tests/client/html.test.ripple +0 -1
- package/tests/client/object.test.ripple +2 -2
- package/tests/client/portal.test.ripple +3 -3
- package/tests/client/return.test.ripple +2500 -0
- package/tests/client/try.test.ripple +69 -0
- package/tests/client/typescript-generics.test.ripple +1 -1
- package/tests/client/url/url.derived.test.ripple +1 -1
- package/tests/client/url/url.parsing.test.ripple +3 -3
- package/tests/client/url/url.partial-removal.test.ripple +7 -7
- package/tests/client/url/url.reactivity.test.ripple +15 -15
- package/tests/client/url/url.serialization.test.ripple +2 -2
- package/tests/hydration/basic.test.js +23 -0
- package/tests/hydration/build-components.js +10 -4
- package/tests/hydration/compiled/client/basic.js +165 -3
- package/tests/hydration/compiled/client/for.js +1140 -23
- package/tests/hydration/compiled/client/head.js +234 -0
- package/tests/hydration/compiled/client/html.js +135 -0
- package/tests/hydration/compiled/client/portal.js +172 -0
- package/tests/hydration/compiled/client/reactivity.js +3 -1
- package/tests/hydration/compiled/client/return.js +1976 -0
- package/tests/hydration/compiled/client/switch.js +162 -0
- package/tests/hydration/compiled/server/basic.js +249 -0
- package/tests/hydration/compiled/server/events.js +1 -1
- package/tests/hydration/compiled/server/for.js +891 -1
- package/tests/hydration/compiled/server/head.js +291 -0
- package/tests/hydration/compiled/server/html.js +133 -0
- package/tests/hydration/compiled/server/if.js +1 -1
- package/tests/hydration/compiled/server/portal.js +250 -0
- package/tests/hydration/compiled/server/reactivity.js +1 -1
- package/tests/hydration/compiled/server/return.js +1969 -0
- package/tests/hydration/compiled/server/switch.js +130 -0
- package/tests/hydration/components/basic.ripple +55 -0
- package/tests/hydration/components/for.ripple +403 -0
- package/tests/hydration/components/head.ripple +111 -0
- package/tests/hydration/components/html.ripple +38 -0
- package/tests/hydration/components/portal.ripple +49 -0
- package/tests/hydration/components/return.ripple +564 -0
- package/tests/hydration/components/switch.ripple +51 -0
- package/tests/hydration/for.test.js +363 -0
- package/tests/hydration/head.test.js +105 -0
- package/tests/hydration/html.test.js +46 -0
- package/tests/hydration/portal.test.js +71 -0
- package/tests/hydration/return.test.js +544 -0
- package/tests/hydration/switch.test.js +42 -0
- package/tests/server/basic.attributes.test.ripple +1 -1
- package/tests/server/compiler.test.ripple +22 -0
- package/tests/server/composite.test.ripple +5 -2
- package/tests/server/html-nesting-validation.test.ripple +237 -0
- package/tests/server/return.test.ripple +1379 -0
- package/tests/setup-hydration.js +6 -1
- package/tests/utils/escaping.test.js +3 -1
- package/tests/utils/normalize_css_property_name.test.js +0 -1
- package/tests/utils/patterns.test.js +6 -2
- package/tests/utils/sanitize_template_string.test.js +3 -2
- package/types/server.d.ts +16 -0
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { flushSync } from 'ripple';
|
|
3
|
+
import { hydrateComponent, container } from '../setup-hydration.js';
|
|
4
|
+
|
|
5
|
+
// Import server-compiled components
|
|
6
|
+
import * as ServerComponents from './compiled/server/return.js';
|
|
7
|
+
// Import client-compiled components
|
|
8
|
+
import * as ClientComponents from './compiled/client/return.js';
|
|
9
|
+
|
|
10
|
+
describe('hydration > return statements', () => {
|
|
11
|
+
describe('basic returns', () => {
|
|
12
|
+
it('hydrates direct return - skips content after return', async () => {
|
|
13
|
+
await hydrateComponent(ServerComponents.DirectReturn, ClientComponents.DirectReturn);
|
|
14
|
+
expect(container.querySelector('.before')?.textContent).toBe('before');
|
|
15
|
+
expect(container.querySelector('.after')).toBeNull();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('hydrates conditional return - condition true skips rest', async () => {
|
|
19
|
+
await hydrateComponent(
|
|
20
|
+
ServerComponents.ConditionalReturnTrue,
|
|
21
|
+
ClientComponents.ConditionalReturnTrue,
|
|
22
|
+
);
|
|
23
|
+
expect(container.querySelector('.guard')?.textContent).toBe('guard hit');
|
|
24
|
+
expect(container.querySelector('.rest')).toBeNull();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('hydrates conditional return - condition false shows rest', async () => {
|
|
28
|
+
await hydrateComponent(
|
|
29
|
+
ServerComponents.ConditionalReturnFalse,
|
|
30
|
+
ClientComponents.ConditionalReturnFalse,
|
|
31
|
+
);
|
|
32
|
+
expect(container.querySelector('.guard')).toBeNull();
|
|
33
|
+
expect(container.querySelector('.rest')?.textContent).toBe('rest');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('hydrates content before and after return guard', async () => {
|
|
37
|
+
await hydrateComponent(
|
|
38
|
+
ServerComponents.ContentBeforeAfterReturn,
|
|
39
|
+
ClientComponents.ContentBeforeAfterReturn,
|
|
40
|
+
);
|
|
41
|
+
expect(container.querySelector('.before')?.textContent).toBe('before');
|
|
42
|
+
expect(container.querySelector('.guard')?.textContent).toBe('guard');
|
|
43
|
+
expect(container.querySelector('.after')).toBeNull();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('hydrates multiple elements after guard when condition is false', async () => {
|
|
47
|
+
await hydrateComponent(
|
|
48
|
+
ServerComponents.MultipleElementsAfterGuard,
|
|
49
|
+
ClientComponents.MultipleElementsAfterGuard,
|
|
50
|
+
);
|
|
51
|
+
expect(container.querySelector('.guard')).toBeNull();
|
|
52
|
+
expect(container.querySelector('.first')?.textContent).toBe('first');
|
|
53
|
+
expect(container.querySelector('.second')?.textContent).toBe('second');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('multiple sequential returns', () => {
|
|
58
|
+
it('hydrates multiple returns - first hits', async () => {
|
|
59
|
+
await hydrateComponent(
|
|
60
|
+
ServerComponents.MultipleReturnsFirstHits,
|
|
61
|
+
ClientComponents.MultipleReturnsFirstHits,
|
|
62
|
+
);
|
|
63
|
+
expect(container.querySelector('.first')?.textContent).toBe('first guard');
|
|
64
|
+
expect(container.querySelector('.second')).toBeNull();
|
|
65
|
+
expect(container.querySelector('.rest')).toBeNull();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('hydrates multiple returns - second hits', async () => {
|
|
69
|
+
await hydrateComponent(
|
|
70
|
+
ServerComponents.MultipleReturnsSecondHits,
|
|
71
|
+
ClientComponents.MultipleReturnsSecondHits,
|
|
72
|
+
);
|
|
73
|
+
expect(container.querySelector('.first')).toBeNull();
|
|
74
|
+
expect(container.querySelector('.second')?.textContent).toBe('second guard');
|
|
75
|
+
expect(container.querySelector('.rest')).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('hydrates multiple returns - none hit', async () => {
|
|
79
|
+
await hydrateComponent(
|
|
80
|
+
ServerComponents.MultipleReturnsNoneHit,
|
|
81
|
+
ClientComponents.MultipleReturnsNoneHit,
|
|
82
|
+
);
|
|
83
|
+
expect(container.querySelector('.first')).toBeNull();
|
|
84
|
+
expect(container.querySelector('.second')).toBeNull();
|
|
85
|
+
expect(container.querySelector('.rest')?.textContent).toBe('rest');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('hydrates multiple sibling returns at same level', async () => {
|
|
89
|
+
await hydrateComponent(
|
|
90
|
+
ServerComponents.MultipleSiblingReturns,
|
|
91
|
+
ClientComponents.MultipleSiblingReturns,
|
|
92
|
+
);
|
|
93
|
+
expect(container.querySelector('.mode-a')).toBeNull();
|
|
94
|
+
expect(container.querySelector('.mode-b')?.textContent).toBe('mode B');
|
|
95
|
+
expect(container.querySelector('.mode-c')).toBeNull();
|
|
96
|
+
expect(container.querySelector('.default')).toBeNull();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('nested returns', () => {
|
|
101
|
+
it('hydrates nested returns - all conditions true', async () => {
|
|
102
|
+
await hydrateComponent(
|
|
103
|
+
ServerComponents.NestedReturnsAllTrue,
|
|
104
|
+
ClientComponents.NestedReturnsAllTrue,
|
|
105
|
+
);
|
|
106
|
+
expect(container.querySelector('.a')?.textContent).toBe('a is true');
|
|
107
|
+
expect(container.querySelector('.b')?.textContent).toBe('b is true');
|
|
108
|
+
expect(container.querySelector('.rest')).toBeNull();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('hydrates nested returns - inner condition false', async () => {
|
|
112
|
+
await hydrateComponent(
|
|
113
|
+
ServerComponents.NestedReturnsInnerFalse,
|
|
114
|
+
ClientComponents.NestedReturnsInnerFalse,
|
|
115
|
+
);
|
|
116
|
+
expect(container.querySelector('.a')?.textContent).toBe('a is true');
|
|
117
|
+
expect(container.querySelector('.b')).toBeNull();
|
|
118
|
+
expect(container.querySelector('.rest')?.textContent).toBe('rest');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('hydrates nested returns - outer condition false', async () => {
|
|
122
|
+
await hydrateComponent(
|
|
123
|
+
ServerComponents.NestedReturnsOuterFalse,
|
|
124
|
+
ClientComponents.NestedReturnsOuterFalse,
|
|
125
|
+
);
|
|
126
|
+
expect(container.querySelector('.a')).toBeNull();
|
|
127
|
+
expect(container.querySelector('.b')).toBeNull();
|
|
128
|
+
expect(container.querySelector('.rest')?.textContent).toBe('rest');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('hydrates deeply nested returns (3 levels) - all true', async () => {
|
|
132
|
+
await hydrateComponent(
|
|
133
|
+
ServerComponents.DeeplyNestedReturnsAllTrue,
|
|
134
|
+
ClientComponents.DeeplyNestedReturnsAllTrue,
|
|
135
|
+
);
|
|
136
|
+
expect(container.querySelector('.a')?.textContent).toBe('a');
|
|
137
|
+
expect(container.querySelector('.b')?.textContent).toBe('b');
|
|
138
|
+
expect(container.querySelector('.c')?.textContent).toBe('c');
|
|
139
|
+
expect(container.querySelector('.rest')).toBeNull();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('hydrates deeply nested returns (3 levels) - innermost false', async () => {
|
|
143
|
+
await hydrateComponent(
|
|
144
|
+
ServerComponents.DeeplyNestedReturnsInnermostFalse,
|
|
145
|
+
ClientComponents.DeeplyNestedReturnsInnermostFalse,
|
|
146
|
+
);
|
|
147
|
+
expect(container.querySelector('.a')?.textContent).toBe('a');
|
|
148
|
+
expect(container.querySelector('.b')?.textContent).toBe('b');
|
|
149
|
+
expect(container.querySelector('.c')).toBeNull();
|
|
150
|
+
expect(container.querySelector('.rest')?.textContent).toBe('rest');
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('else-if chains with returns', () => {
|
|
155
|
+
it('hydrates else-if chain - first condition', async () => {
|
|
156
|
+
await hydrateComponent(ServerComponents.ElseIfChainFirst, ClientComponents.ElseIfChainFirst);
|
|
157
|
+
expect(container.querySelector('.one')?.textContent).toBe('one');
|
|
158
|
+
expect(container.querySelector('.two')).toBeNull();
|
|
159
|
+
expect(container.querySelector('.other')).toBeNull();
|
|
160
|
+
expect(container.querySelector('.never')).toBeNull();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('hydrates else-if chain - second condition', async () => {
|
|
164
|
+
await hydrateComponent(
|
|
165
|
+
ServerComponents.ElseIfChainSecond,
|
|
166
|
+
ClientComponents.ElseIfChainSecond,
|
|
167
|
+
);
|
|
168
|
+
expect(container.querySelector('.one')).toBeNull();
|
|
169
|
+
expect(container.querySelector('.two')?.textContent).toBe('two');
|
|
170
|
+
expect(container.querySelector('.other')).toBeNull();
|
|
171
|
+
expect(container.querySelector('.never')).toBeNull();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('hydrates else-if chain - else condition', async () => {
|
|
175
|
+
await hydrateComponent(ServerComponents.ElseIfChainElse, ClientComponents.ElseIfChainElse);
|
|
176
|
+
expect(container.querySelector('.one')).toBeNull();
|
|
177
|
+
expect(container.querySelector('.two')).toBeNull();
|
|
178
|
+
expect(container.querySelector('.other')?.textContent).toBe('other');
|
|
179
|
+
expect(container.querySelector('.never')).toBeNull();
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('return with else branches', () => {
|
|
184
|
+
it('hydrates return with else that does not return', async () => {
|
|
185
|
+
await hydrateComponent(
|
|
186
|
+
ServerComponents.ReturnWithElseNoReturn,
|
|
187
|
+
ClientComponents.ReturnWithElseNoReturn,
|
|
188
|
+
);
|
|
189
|
+
expect(container.querySelector('.true')).toBeNull();
|
|
190
|
+
expect(container.querySelector('.false')?.textContent).toBe('condition false');
|
|
191
|
+
expect(container.querySelector('.after')?.textContent).toBe('after if-else');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('hydrates return with else that also returns', async () => {
|
|
195
|
+
await hydrateComponent(
|
|
196
|
+
ServerComponents.ReturnWithElseBothReturn,
|
|
197
|
+
ClientComponents.ReturnWithElseBothReturn,
|
|
198
|
+
);
|
|
199
|
+
expect(container.querySelector('.true')).toBeNull();
|
|
200
|
+
expect(container.querySelector('.false')?.textContent).toBe('condition false');
|
|
201
|
+
expect(container.querySelector('.never')).toBeNull();
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe('reactive returns', () => {
|
|
206
|
+
it('hydrates and toggles reactive return (true to false)', async () => {
|
|
207
|
+
await hydrateComponent(
|
|
208
|
+
ServerComponents.ReactiveReturnTrueToFalse,
|
|
209
|
+
ClientComponents.ReactiveReturnTrueToFalse,
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
// Initially condition is true, so guard is shown, rest is hidden
|
|
213
|
+
expect(container.querySelector('.guard')?.textContent).toBe('guard hit');
|
|
214
|
+
expect(container.querySelector('.rest')).toBeNull();
|
|
215
|
+
|
|
216
|
+
// Toggle condition to false
|
|
217
|
+
container.querySelector('.toggle')?.click();
|
|
218
|
+
flushSync();
|
|
219
|
+
|
|
220
|
+
// Now rest should be shown
|
|
221
|
+
expect(container.querySelector('.guard')).toBeNull();
|
|
222
|
+
expect(container.querySelector('.rest')?.textContent).toBe('rest');
|
|
223
|
+
|
|
224
|
+
// Toggle back to true
|
|
225
|
+
container.querySelector('.toggle')?.click();
|
|
226
|
+
flushSync();
|
|
227
|
+
|
|
228
|
+
expect(container.querySelector('.guard')?.textContent).toBe('guard hit');
|
|
229
|
+
expect(container.querySelector('.rest')).toBeNull();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('hydrates and toggles reactive return (false to true)', async () => {
|
|
233
|
+
await hydrateComponent(
|
|
234
|
+
ServerComponents.ReactiveReturnFalseToTrue,
|
|
235
|
+
ClientComponents.ReactiveReturnFalseToTrue,
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
// Initially condition is false, so rest is shown
|
|
239
|
+
expect(container.querySelector('.guard')).toBeNull();
|
|
240
|
+
expect(container.querySelector('.rest')?.textContent).toBe('rest');
|
|
241
|
+
|
|
242
|
+
// Toggle condition to true
|
|
243
|
+
container.querySelector('.toggle')?.click();
|
|
244
|
+
flushSync();
|
|
245
|
+
|
|
246
|
+
// Now guard should be shown, rest hidden
|
|
247
|
+
expect(container.querySelector('.guard')?.textContent).toBe('guard hit');
|
|
248
|
+
expect(container.querySelector('.rest')).toBeNull();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('hydrates and toggles reactive nested return', async () => {
|
|
252
|
+
await hydrateComponent(
|
|
253
|
+
ServerComponents.ReactiveNestedReturn,
|
|
254
|
+
ClientComponents.ReactiveNestedReturn,
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
// Initially a=true and b=true - shows a, b, hides rest
|
|
258
|
+
expect(container.querySelector('.a')?.textContent).toBe('a');
|
|
259
|
+
expect(container.querySelector('.b')?.textContent).toBe('b');
|
|
260
|
+
expect(container.querySelector('.rest')).toBeNull();
|
|
261
|
+
|
|
262
|
+
// Toggle b to false - rest should appear
|
|
263
|
+
container.querySelector('.toggle')?.click();
|
|
264
|
+
flushSync();
|
|
265
|
+
|
|
266
|
+
expect(container.querySelector('.a')?.textContent).toBe('a');
|
|
267
|
+
expect(container.querySelector('.b')).toBeNull();
|
|
268
|
+
expect(container.querySelector('.rest')?.textContent).toBe('rest');
|
|
269
|
+
|
|
270
|
+
// Toggle b back to true
|
|
271
|
+
container.querySelector('.toggle')?.click();
|
|
272
|
+
flushSync();
|
|
273
|
+
|
|
274
|
+
expect(container.querySelector('.a')?.textContent).toBe('a');
|
|
275
|
+
expect(container.querySelector('.b')?.textContent).toBe('b');
|
|
276
|
+
expect(container.querySelector('.rest')).toBeNull();
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('hydrates and cycles reactive sibling returns across all branches', async () => {
|
|
280
|
+
await hydrateComponent(
|
|
281
|
+
ServerComponents.ReactiveSiblingReturns,
|
|
282
|
+
ClientComponents.ReactiveSiblingReturns,
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
// first
|
|
286
|
+
expect(container.querySelector('.first')?.textContent).toBe('first guard');
|
|
287
|
+
expect(container.querySelector('.second')).toBeNull();
|
|
288
|
+
expect(container.querySelector('.rest')).toBeNull();
|
|
289
|
+
|
|
290
|
+
// second
|
|
291
|
+
container.querySelector('.toggle')?.click();
|
|
292
|
+
flushSync();
|
|
293
|
+
expect(container.querySelector('.first')).toBeNull();
|
|
294
|
+
expect(container.querySelector('.second')?.textContent).toBe('second guard');
|
|
295
|
+
expect(container.querySelector('.rest')).toBeNull();
|
|
296
|
+
|
|
297
|
+
// fallback
|
|
298
|
+
container.querySelector('.toggle')?.click();
|
|
299
|
+
flushSync();
|
|
300
|
+
expect(container.querySelector('.first')).toBeNull();
|
|
301
|
+
expect(container.querySelector('.second')).toBeNull();
|
|
302
|
+
expect(container.querySelector('.rest')?.textContent).toBe('rest');
|
|
303
|
+
|
|
304
|
+
// back to first
|
|
305
|
+
container.querySelector('.toggle')?.click();
|
|
306
|
+
flushSync();
|
|
307
|
+
expect(container.querySelector('.first')?.textContent).toBe('first guard');
|
|
308
|
+
expect(container.querySelector('.second')).toBeNull();
|
|
309
|
+
expect(container.querySelector('.rest')).toBeNull();
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('hydrates nested tracked returns when outer and inner conditions both change', async () => {
|
|
313
|
+
await hydrateComponent(
|
|
314
|
+
ServerComponents.ReactiveOuterInnerReturns,
|
|
315
|
+
ClientComponents.ReactiveOuterInnerReturns,
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
// a=true, b=true
|
|
319
|
+
expect(container.querySelector('.a')?.textContent).toBe('a');
|
|
320
|
+
expect(container.querySelector('.b')?.textContent).toBe('b');
|
|
321
|
+
expect(container.querySelector('.rest')).toBeNull();
|
|
322
|
+
|
|
323
|
+
// b=false => no early return, rest should appear in a-on mode
|
|
324
|
+
container.querySelector('.toggle-b')?.click();
|
|
325
|
+
flushSync();
|
|
326
|
+
expect(container.querySelector('.a')?.textContent).toBe('a');
|
|
327
|
+
expect(container.querySelector('.b')).toBeNull();
|
|
328
|
+
expect(container.querySelector('.rest')?.textContent).toBe('a-on rest');
|
|
329
|
+
|
|
330
|
+
// a=false => outer block disappears, rest switches to a-off mode
|
|
331
|
+
container.querySelector('.toggle-a')?.click();
|
|
332
|
+
flushSync();
|
|
333
|
+
expect(container.querySelector('.a')).toBeNull();
|
|
334
|
+
expect(container.querySelector('.b')).toBeNull();
|
|
335
|
+
expect(container.querySelector('.rest')?.textContent).toBe('a-off rest');
|
|
336
|
+
|
|
337
|
+
// a=true (b still false) => a returns, rest switches back to a-on mode
|
|
338
|
+
container.querySelector('.toggle-a')?.click();
|
|
339
|
+
flushSync();
|
|
340
|
+
expect(container.querySelector('.a')?.textContent).toBe('a');
|
|
341
|
+
expect(container.querySelector('.b')).toBeNull();
|
|
342
|
+
expect(container.querySelector('.rest')?.textContent).toBe('a-on rest');
|
|
343
|
+
|
|
344
|
+
// b=true => early return again, hide rest
|
|
345
|
+
container.querySelector('.toggle-b')?.click();
|
|
346
|
+
flushSync();
|
|
347
|
+
expect(container.querySelector('.a')?.textContent).toBe('a');
|
|
348
|
+
expect(container.querySelector('.b')?.textContent).toBe('b');
|
|
349
|
+
expect(container.querySelector('.rest')).toBeNull();
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('hydrates reactive else-if return chain through return and non-return states', async () => {
|
|
353
|
+
await hydrateComponent(
|
|
354
|
+
ServerComponents.ReactiveElseIfReturns,
|
|
355
|
+
ClientComponents.ReactiveElseIfReturns,
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
// status=0
|
|
359
|
+
expect(container.querySelector('.zero')?.textContent).toBe('zero');
|
|
360
|
+
expect(container.querySelector('.one')).toBeNull();
|
|
361
|
+
expect(container.querySelector('.rest')).toBeNull();
|
|
362
|
+
expect(container.querySelector('.tail')).toBeNull();
|
|
363
|
+
|
|
364
|
+
// status=1
|
|
365
|
+
container.querySelector('.toggle')?.click();
|
|
366
|
+
flushSync();
|
|
367
|
+
expect(container.querySelector('.zero')).toBeNull();
|
|
368
|
+
expect(container.querySelector('.one')?.textContent).toBe('one');
|
|
369
|
+
expect(container.querySelector('.rest')).toBeNull();
|
|
370
|
+
expect(container.querySelector('.tail')).toBeNull();
|
|
371
|
+
|
|
372
|
+
// status=2
|
|
373
|
+
container.querySelector('.toggle')?.click();
|
|
374
|
+
flushSync();
|
|
375
|
+
expect(container.querySelector('.zero')).toBeNull();
|
|
376
|
+
expect(container.querySelector('.one')).toBeNull();
|
|
377
|
+
expect(container.querySelector('.rest')?.textContent).toBe('rest');
|
|
378
|
+
expect(container.querySelector('.tail')?.textContent).toBe('tail');
|
|
379
|
+
|
|
380
|
+
// status=0
|
|
381
|
+
container.querySelector('.toggle')?.click();
|
|
382
|
+
flushSync();
|
|
383
|
+
expect(container.querySelector('.zero')?.textContent).toBe('zero');
|
|
384
|
+
expect(container.querySelector('.one')).toBeNull();
|
|
385
|
+
expect(container.querySelector('.rest')).toBeNull();
|
|
386
|
+
expect(container.querySelector('.tail')).toBeNull();
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('hydrates deeply nested independent returns and keeps trailing root siblings aligned', async () => {
|
|
390
|
+
await hydrateComponent(
|
|
391
|
+
ServerComponents.ReactiveDeepNestedIndependentReturns,
|
|
392
|
+
ClientComponents.ReactiveDeepNestedIndependentReturns,
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
const expect_full_content = () => {
|
|
396
|
+
expect(container.querySelector('.top')?.textContent).toBe('top');
|
|
397
|
+
expect(container.querySelector('.middle')?.textContent).toBe('middle');
|
|
398
|
+
expect(container.querySelector('.nest-1-a')?.textContent).toBe('nest-1-a');
|
|
399
|
+
expect(container.querySelector('.nest-1-b')?.textContent).toBe('nest-1-b');
|
|
400
|
+
expect(container.querySelector('.nest-2-a')?.textContent).toBe('nest-2-a');
|
|
401
|
+
expect(container.querySelector('.nest-2-b')?.textContent).toBe('nest-2-b');
|
|
402
|
+
expect(container.querySelector('.root-1')?.textContent).toBe('root-1');
|
|
403
|
+
expect(container.querySelector('.root-2')?.textContent).toBe('root-2');
|
|
404
|
+
expect(container.querySelector('.root-3')?.textContent).toBe('root-3');
|
|
405
|
+
expect(container.querySelector('.root-4')?.textContent).toBe('root-4');
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const expect_no_hits = () => {
|
|
409
|
+
expect(container.querySelector('.hit-1')).toBeNull();
|
|
410
|
+
expect(container.querySelector('.hit-2')).toBeNull();
|
|
411
|
+
expect(container.querySelector('.hit-3')).toBeNull();
|
|
412
|
+
expect(container.querySelector('.hit-4')).toBeNull();
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
expect_full_content();
|
|
416
|
+
expect_no_hits();
|
|
417
|
+
|
|
418
|
+
// C3 return: deep nested return should hide trailing root siblings
|
|
419
|
+
container.querySelector('.toggle-c3')?.click();
|
|
420
|
+
flushSync();
|
|
421
|
+
expect(container.querySelector('.hit-3')?.textContent).toBe('hit-3');
|
|
422
|
+
expect(container.querySelector('.top')?.textContent).toBe('top');
|
|
423
|
+
expect(container.querySelector('.middle')?.textContent).toBe('middle');
|
|
424
|
+
expect(container.querySelector('.nest-1-a')?.textContent).toBe('nest-1-a');
|
|
425
|
+
expect(container.querySelector('.nest-1-b')?.textContent).toBe('nest-1-b');
|
|
426
|
+
expect(container.querySelector('.nest-2-a')?.textContent).toBe('nest-2-a');
|
|
427
|
+
expect(container.querySelector('.nest-2-b')).toBeNull();
|
|
428
|
+
expect(container.querySelector('.root-1')).toBeNull();
|
|
429
|
+
expect(container.querySelector('.root-2')).toBeNull();
|
|
430
|
+
expect(container.querySelector('.root-3')).toBeNull();
|
|
431
|
+
expect(container.querySelector('.root-4')).toBeNull();
|
|
432
|
+
|
|
433
|
+
container.querySelector('.toggle-c3')?.click();
|
|
434
|
+
flushSync();
|
|
435
|
+
expect_full_content();
|
|
436
|
+
expect_no_hits();
|
|
437
|
+
|
|
438
|
+
// C1 return: earliest return should cut everything below top
|
|
439
|
+
container.querySelector('.toggle-c1')?.click();
|
|
440
|
+
flushSync();
|
|
441
|
+
expect(container.querySelector('.top')?.textContent).toBe('top');
|
|
442
|
+
expect(container.querySelector('.hit-1')?.textContent).toBe('hit-1');
|
|
443
|
+
expect(container.querySelector('.middle')).toBeNull();
|
|
444
|
+
expect(container.querySelector('.nest-1-a')).toBeNull();
|
|
445
|
+
expect(container.querySelector('.nest-1-b')).toBeNull();
|
|
446
|
+
expect(container.querySelector('.nest-2-a')).toBeNull();
|
|
447
|
+
expect(container.querySelector('.nest-2-b')).toBeNull();
|
|
448
|
+
expect(container.querySelector('.root-1')).toBeNull();
|
|
449
|
+
expect(container.querySelector('.root-2')).toBeNull();
|
|
450
|
+
expect(container.querySelector('.root-3')).toBeNull();
|
|
451
|
+
expect(container.querySelector('.root-4')).toBeNull();
|
|
452
|
+
|
|
453
|
+
container.querySelector('.toggle-c1')?.click();
|
|
454
|
+
flushSync();
|
|
455
|
+
expect_full_content();
|
|
456
|
+
expect_no_hits();
|
|
457
|
+
|
|
458
|
+
// C2 return: mid-level nested return should keep upper nested nodes, hide lower/root siblings
|
|
459
|
+
container.querySelector('.toggle-c2')?.click();
|
|
460
|
+
flushSync();
|
|
461
|
+
expect(container.querySelector('.top')?.textContent).toBe('top');
|
|
462
|
+
expect(container.querySelector('.middle')?.textContent).toBe('middle');
|
|
463
|
+
expect(container.querySelector('.nest-1-a')?.textContent).toBe('nest-1-a');
|
|
464
|
+
expect(container.querySelector('.hit-2')?.textContent).toBe('hit-2');
|
|
465
|
+
expect(container.querySelector('.nest-1-b')).toBeNull();
|
|
466
|
+
expect(container.querySelector('.nest-2-a')).toBeNull();
|
|
467
|
+
expect(container.querySelector('.nest-2-b')).toBeNull();
|
|
468
|
+
expect(container.querySelector('.root-1')).toBeNull();
|
|
469
|
+
expect(container.querySelector('.root-2')).toBeNull();
|
|
470
|
+
expect(container.querySelector('.root-3')).toBeNull();
|
|
471
|
+
expect(container.querySelector('.root-4')).toBeNull();
|
|
472
|
+
|
|
473
|
+
container.querySelector('.toggle-c2')?.click();
|
|
474
|
+
flushSync();
|
|
475
|
+
expect_full_content();
|
|
476
|
+
expect_no_hits();
|
|
477
|
+
|
|
478
|
+
// C4 return: deepest return should keep all nested parents but still hide root siblings
|
|
479
|
+
container.querySelector('.toggle-c4')?.click();
|
|
480
|
+
flushSync();
|
|
481
|
+
expect(container.querySelector('.top')?.textContent).toBe('top');
|
|
482
|
+
expect(container.querySelector('.middle')?.textContent).toBe('middle');
|
|
483
|
+
expect(container.querySelector('.nest-1-a')?.textContent).toBe('nest-1-a');
|
|
484
|
+
expect(container.querySelector('.nest-1-b')?.textContent).toBe('nest-1-b');
|
|
485
|
+
expect(container.querySelector('.nest-2-a')?.textContent).toBe('nest-2-a');
|
|
486
|
+
expect(container.querySelector('.nest-2-b')?.textContent).toBe('nest-2-b');
|
|
487
|
+
expect(container.querySelector('.hit-4')?.textContent).toBe('hit-4');
|
|
488
|
+
expect(container.querySelector('.root-1')).toBeNull();
|
|
489
|
+
expect(container.querySelector('.root-2')).toBeNull();
|
|
490
|
+
expect(container.querySelector('.root-3')).toBeNull();
|
|
491
|
+
expect(container.querySelector('.root-4')).toBeNull();
|
|
492
|
+
|
|
493
|
+
container.querySelector('.toggle-c4')?.click();
|
|
494
|
+
flushSync();
|
|
495
|
+
expect_full_content();
|
|
496
|
+
expect_no_hits();
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
describe('return in element scopes', () => {
|
|
501
|
+
it('hydrates return inside nested element scope', async () => {
|
|
502
|
+
await hydrateComponent(
|
|
503
|
+
ServerComponents.ReturnInNestedElement,
|
|
504
|
+
ClientComponents.ReturnInNestedElement,
|
|
505
|
+
);
|
|
506
|
+
expect(container.querySelector('.outer')).not.toBeNull();
|
|
507
|
+
expect(container.querySelector('.label')?.textContent).toBe('outer');
|
|
508
|
+
expect(container.querySelector('.inner')?.textContent).toBe('inner');
|
|
509
|
+
expect(container.querySelector('.after')).toBeNull();
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it('hydrates return with multiple elements before and after', async () => {
|
|
513
|
+
await hydrateComponent(
|
|
514
|
+
ServerComponents.ReturnWithMultipleElements,
|
|
515
|
+
ClientComponents.ReturnWithMultipleElements,
|
|
516
|
+
);
|
|
517
|
+
expect(container.querySelector('.title')?.textContent).toBe('title');
|
|
518
|
+
expect(container.querySelector('.desc')?.textContent).toBe('description');
|
|
519
|
+
expect(container.querySelector('.guard')?.textContent).toBe('guard');
|
|
520
|
+
expect(container.querySelector('.guard-span')?.textContent).toBe('guard span');
|
|
521
|
+
expect(container.querySelector('.footer')).toBeNull();
|
|
522
|
+
expect(container.querySelector('.nav')).toBeNull();
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
describe('return position edge cases', () => {
|
|
527
|
+
it('hydrates return at the beginning of component', async () => {
|
|
528
|
+
await hydrateComponent(
|
|
529
|
+
ServerComponents.ReturnAtBeginning,
|
|
530
|
+
ClientComponents.ReturnAtBeginning,
|
|
531
|
+
);
|
|
532
|
+
expect(container.querySelector('.early')?.textContent).toBe('early exit');
|
|
533
|
+
expect(container.querySelector('.never1')).toBeNull();
|
|
534
|
+
expect(container.querySelector('.never2')).toBeNull();
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it('hydrates return at the end of component', async () => {
|
|
538
|
+
await hydrateComponent(ServerComponents.ReturnAtEnd, ClientComponents.ReturnAtEnd);
|
|
539
|
+
expect(container.querySelector('.first')?.textContent).toBe('first');
|
|
540
|
+
expect(container.querySelector('.second')?.textContent).toBe('second');
|
|
541
|
+
expect(container.querySelector('.third')?.textContent).toBe('third');
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { flushSync } from 'ripple';
|
|
3
|
+
import { hydrateComponent, container } from '../setup-hydration.js';
|
|
4
|
+
|
|
5
|
+
// Import server-compiled components
|
|
6
|
+
import * as ServerComponents from './compiled/server/switch.js';
|
|
7
|
+
// Import client-compiled components
|
|
8
|
+
import * as ClientComponents from './compiled/client/switch.js';
|
|
9
|
+
|
|
10
|
+
describe('hydration > switch blocks', () => {
|
|
11
|
+
it('hydrates static switch block', async () => {
|
|
12
|
+
await hydrateComponent(ServerComponents.SwitchStatic, ClientComponents.SwitchStatic);
|
|
13
|
+
expect(container.querySelector('.status-success')?.textContent).toBe('Success');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('hydrates reactive switch block and updates', async () => {
|
|
17
|
+
await hydrateComponent(ServerComponents.SwitchReactive, ClientComponents.SwitchReactive);
|
|
18
|
+
const button = container.querySelector('.toggle');
|
|
19
|
+
|
|
20
|
+
expect(container.querySelector('.case-a')?.textContent).toBe('Case A');
|
|
21
|
+
|
|
22
|
+
button?.click();
|
|
23
|
+
flushSync();
|
|
24
|
+
expect(container.querySelector('.case-a')).toBeNull();
|
|
25
|
+
expect(container.querySelector('.case-b')?.textContent).toBe('Case B');
|
|
26
|
+
|
|
27
|
+
button?.click();
|
|
28
|
+
flushSync();
|
|
29
|
+
expect(container.querySelector('.case-b')).toBeNull();
|
|
30
|
+
expect(container.querySelector('.case-c')?.textContent).toBe('Case C');
|
|
31
|
+
|
|
32
|
+
button?.click();
|
|
33
|
+
flushSync();
|
|
34
|
+
expect(container.querySelector('.case-c')).toBeNull();
|
|
35
|
+
expect(container.querySelector('.case-a')?.textContent).toBe('Case A');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('hydrates switch block with fallthrough', async () => {
|
|
39
|
+
await hydrateComponent(ServerComponents.SwitchFallthrough, ClientComponents.SwitchFallthrough);
|
|
40
|
+
expect(container.querySelector('.case-1-2')?.textContent).toBe('1 or 2');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -454,6 +454,6 @@ describe('basic server > attribute rendering', () => {
|
|
|
454
454
|
}
|
|
455
455
|
|
|
456
456
|
const { body } = await render(Basic);
|
|
457
|
-
expect(body).toBeHtml('<div data-disabled=""
|
|
457
|
+
expect(body).toBeHtml('<div data-disabled=""></div><input disabled />');
|
|
458
458
|
});
|
|
459
459
|
});
|
|
@@ -42,6 +42,28 @@ function getString(e: string = 'test') {
|
|
|
42
42
|
|
|
43
43
|
expect(result.js.code).toMatchSnapshot();
|
|
44
44
|
});
|
|
45
|
+
|
|
46
|
+
it('throws error for interpolating children as text in SSR mode', () => {
|
|
47
|
+
const source = `
|
|
48
|
+
export component Layout({ children }) {
|
|
49
|
+
<div>{children}</div>
|
|
50
|
+
}`;
|
|
51
|
+
|
|
52
|
+
expect(() => compile(source, 'test.ripple', { mode: 'server' })).toThrow(
|
|
53
|
+
'`children` cannot be rendered using text interpolation. Use `<children />` instead.',
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('throws error for interpolating props.children as text in SSR mode', () => {
|
|
58
|
+
const source = `
|
|
59
|
+
export component Layout(props) {
|
|
60
|
+
<div>{props.children}</div>
|
|
61
|
+
}`;
|
|
62
|
+
|
|
63
|
+
expect(() => compile(source, 'test.ripple', { mode: 'server' })).toThrow(
|
|
64
|
+
'`children` cannot be rendered using text interpolation. Use `<children />` instead.',
|
|
65
|
+
);
|
|
66
|
+
});
|
|
45
67
|
});
|
|
46
68
|
|
|
47
69
|
describe('compiler server block tests', () => {
|
|
@@ -203,8 +203,11 @@ describe('generics', () => {
|
|
|
203
203
|
this.map = new Map<Key, Value>();
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
|
-
const gg = new Mapper
|
|
207
|
-
|
|
206
|
+
const gg = new Mapper<
|
|
207
|
+
// key type
|
|
208
|
+
string /* value type */,
|
|
209
|
+
number
|
|
210
|
+
>();
|
|
208
211
|
|
|
209
212
|
// 33. Map of generic instance as key
|
|
210
213
|
const mm = new Map<TrackedArray<number>, TrackedArray<string>>();
|