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
|
@@ -114,4 +114,367 @@ describe('hydration > for blocks', () => {
|
|
|
114
114
|
expect(user2?.querySelector('.name')?.textContent).toBe('Bob');
|
|
115
115
|
expect(user2?.querySelector('.role')?.textContent).toBe('User');
|
|
116
116
|
});
|
|
117
|
+
|
|
118
|
+
it('hydrates keyed for loop and reorders items', async () => {
|
|
119
|
+
await hydrateComponent(
|
|
120
|
+
ServerComponents.KeyedForLoopReorder,
|
|
121
|
+
ClientComponents.KeyedForLoopReorder,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// Initial order: First, Second, Third
|
|
125
|
+
let listItems = container.querySelectorAll('li');
|
|
126
|
+
expect(listItems[0]?.textContent).toBe('First');
|
|
127
|
+
expect(listItems[1]?.textContent).toBe('Second');
|
|
128
|
+
expect(listItems[2]?.textContent).toBe('Third');
|
|
129
|
+
|
|
130
|
+
// Reorder to: Third, First, Second
|
|
131
|
+
container.querySelector('.reorder')?.click();
|
|
132
|
+
flushSync();
|
|
133
|
+
|
|
134
|
+
listItems = container.querySelectorAll('li');
|
|
135
|
+
expect(listItems[0]?.textContent).toBe('Third');
|
|
136
|
+
expect(listItems[1]?.textContent).toBe('First');
|
|
137
|
+
expect(listItems[2]?.textContent).toBe('Second');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('hydrates keyed for loop and updates item properties', async () => {
|
|
141
|
+
await hydrateComponent(
|
|
142
|
+
ServerComponents.KeyedForLoopUpdate,
|
|
143
|
+
ClientComponents.KeyedForLoopUpdate,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
expect(container.querySelector('.item-1')?.textContent).toBe('Item 1');
|
|
147
|
+
expect(container.querySelector('.item-2')?.textContent).toBe('Item 2');
|
|
148
|
+
|
|
149
|
+
container.querySelector('.update')?.click();
|
|
150
|
+
flushSync();
|
|
151
|
+
|
|
152
|
+
expect(container.querySelector('.item-1')?.textContent).toBe('Updated');
|
|
153
|
+
expect(container.querySelector('.item-2')?.textContent).toBe('Item 2');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('hydrates for loop with mixed add/remove/reorder operations', async () => {
|
|
157
|
+
await hydrateComponent(
|
|
158
|
+
ServerComponents.ForLoopMixedOperations,
|
|
159
|
+
ClientComponents.ForLoopMixedOperations,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// Initial: A, B, C, D
|
|
163
|
+
let listItems = container.querySelectorAll('li');
|
|
164
|
+
expect(listItems.length).toBe(4);
|
|
165
|
+
expect(listItems[0]?.textContent).toBe('A');
|
|
166
|
+
expect(listItems[3]?.textContent).toBe('D');
|
|
167
|
+
|
|
168
|
+
// After shuffle: D, C, A, E
|
|
169
|
+
container.querySelector('.shuffle')?.click();
|
|
170
|
+
flushSync();
|
|
171
|
+
|
|
172
|
+
listItems = container.querySelectorAll('li');
|
|
173
|
+
expect(listItems.length).toBe(4);
|
|
174
|
+
expect(listItems[0]?.textContent).toBe('D');
|
|
175
|
+
expect(listItems[1]?.textContent).toBe('C');
|
|
176
|
+
expect(listItems[2]?.textContent).toBe('A');
|
|
177
|
+
expect(listItems[3]?.textContent).toBe('E');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('hydrates for loop inside if block', async () => {
|
|
181
|
+
await hydrateComponent(ServerComponents.ForLoopInsideIf, ClientComponents.ForLoopInsideIf);
|
|
182
|
+
|
|
183
|
+
// Initially visible with X, Y, Z
|
|
184
|
+
expect(container.querySelector('.list')).not.toBeNull();
|
|
185
|
+
expect(container.querySelectorAll('li').length).toBe(3);
|
|
186
|
+
|
|
187
|
+
// Add item while visible
|
|
188
|
+
container.querySelector('.add')?.click();
|
|
189
|
+
flushSync();
|
|
190
|
+
expect(container.querySelectorAll('li').length).toBe(4);
|
|
191
|
+
|
|
192
|
+
// Hide list
|
|
193
|
+
container.querySelector('.toggle')?.click();
|
|
194
|
+
flushSync();
|
|
195
|
+
expect(container.querySelector('.list')).toBeNull();
|
|
196
|
+
|
|
197
|
+
// Show list again
|
|
198
|
+
container.querySelector('.toggle')?.click();
|
|
199
|
+
flushSync();
|
|
200
|
+
expect(container.querySelector('.list')).not.toBeNull();
|
|
201
|
+
expect(container.querySelectorAll('li').length).toBe(4);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('hydrates for loop transitioning from empty to populated', async () => {
|
|
205
|
+
await hydrateComponent(
|
|
206
|
+
ServerComponents.ForLoopEmptyToPopulated,
|
|
207
|
+
ClientComponents.ForLoopEmptyToPopulated,
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
expect(container.querySelector('.list')?.querySelectorAll('li').length).toBe(0);
|
|
211
|
+
|
|
212
|
+
container.querySelector('.populate')?.click();
|
|
213
|
+
flushSync();
|
|
214
|
+
|
|
215
|
+
const listItems = container.querySelector('.list')?.querySelectorAll('li');
|
|
216
|
+
expect(listItems?.length).toBe(3);
|
|
217
|
+
expect(listItems?.[0]?.textContent).toBe('One');
|
|
218
|
+
expect(listItems?.[1]?.textContent).toBe('Two');
|
|
219
|
+
expect(listItems?.[2]?.textContent).toBe('Three');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('hydrates for loop transitioning from populated to empty', async () => {
|
|
223
|
+
await hydrateComponent(
|
|
224
|
+
ServerComponents.ForLoopPopulatedToEmpty,
|
|
225
|
+
ClientComponents.ForLoopPopulatedToEmpty,
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
expect(container.querySelector('.list')?.querySelectorAll('li').length).toBe(3);
|
|
229
|
+
|
|
230
|
+
container.querySelector('.clear')?.click();
|
|
231
|
+
flushSync();
|
|
232
|
+
|
|
233
|
+
expect(container.querySelector('.list')?.querySelectorAll('li').length).toBe(0);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('hydrates nested for loops with reactivity', async () => {
|
|
237
|
+
await hydrateComponent(
|
|
238
|
+
ServerComponents.NestedForLoopReactive,
|
|
239
|
+
ClientComponents.NestedForLoopReactive,
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
// Initial: 2x2 grid
|
|
243
|
+
expect(container.querySelectorAll('[class^="row-"]').length).toBe(2);
|
|
244
|
+
expect(container.querySelector('.cell-0-0')?.textContent).toBe('1');
|
|
245
|
+
|
|
246
|
+
// Add row
|
|
247
|
+
container.querySelector('.add-row')?.click();
|
|
248
|
+
flushSync();
|
|
249
|
+
expect(container.querySelectorAll('[class^="row-"]').length).toBe(3);
|
|
250
|
+
expect(container.querySelector('.cell-2-0')?.textContent).toBe('5');
|
|
251
|
+
expect(container.querySelector('.cell-2-1')?.textContent).toBe('6');
|
|
252
|
+
|
|
253
|
+
// Update cell
|
|
254
|
+
container.querySelector('.update-cell')?.click();
|
|
255
|
+
flushSync();
|
|
256
|
+
expect(container.querySelector('.cell-0-0')?.textContent).toBe('99');
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('hydrates for loop with deeply nested data', async () => {
|
|
260
|
+
await hydrateComponent(
|
|
261
|
+
ServerComponents.ForLoopDeeplyNested,
|
|
262
|
+
ClientComponents.ForLoopDeeplyNested,
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
// Check department structure
|
|
266
|
+
expect(container.querySelector('.dept-d1 .dept-name')?.textContent).toBe('Engineering');
|
|
267
|
+
expect(container.querySelector('.dept-d2 .dept-name')?.textContent).toBe('Design');
|
|
268
|
+
|
|
269
|
+
// Check team structure
|
|
270
|
+
expect(container.querySelector('.team-t1 .team-name')?.textContent).toBe('Frontend');
|
|
271
|
+
expect(container.querySelector('.team-t2 .team-name')?.textContent).toBe('Backend');
|
|
272
|
+
expect(container.querySelector('.team-t3 .team-name')?.textContent).toBe('UX');
|
|
273
|
+
|
|
274
|
+
// Check members
|
|
275
|
+
const frontendMembers = container.querySelectorAll('.team-t1 .member');
|
|
276
|
+
expect(frontendMembers.length).toBe(2);
|
|
277
|
+
expect(frontendMembers[0]?.textContent).toBe('Alice');
|
|
278
|
+
expect(frontendMembers[1]?.textContent).toBe('Bob');
|
|
279
|
+
|
|
280
|
+
const uxMembers = container.querySelectorAll('.team-t3 .member');
|
|
281
|
+
expect(uxMembers.length).toBe(3);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('hydrates for loop with index that updates on prepend', async () => {
|
|
285
|
+
await hydrateComponent(
|
|
286
|
+
ServerComponents.ForLoopIndexUpdate,
|
|
287
|
+
ClientComponents.ForLoopIndexUpdate,
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
// Initial: [0] First, [1] Second, [2] Third
|
|
291
|
+
let listItems = container.querySelectorAll('li');
|
|
292
|
+
expect(listItems[0]?.textContent).toBe('[0] First');
|
|
293
|
+
expect(listItems[1]?.textContent).toBe('[1] Second');
|
|
294
|
+
expect(listItems[2]?.textContent).toBe('[2] Third');
|
|
295
|
+
|
|
296
|
+
// Prepend: [0] Zeroth, [1] First, [2] Second, [3] Third
|
|
297
|
+
container.querySelector('.prepend')?.click();
|
|
298
|
+
flushSync();
|
|
299
|
+
|
|
300
|
+
listItems = container.querySelectorAll('li');
|
|
301
|
+
expect(listItems.length).toBe(4);
|
|
302
|
+
expect(listItems[0]?.textContent).toBe('[0] Zeroth');
|
|
303
|
+
expect(listItems[1]?.textContent).toBe('[1] First');
|
|
304
|
+
expect(listItems[2]?.textContent).toBe('[2] Second');
|
|
305
|
+
expect(listItems[3]?.textContent).toBe('[3] Third');
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('hydrates keyed for loop with index', async () => {
|
|
309
|
+
await hydrateComponent(
|
|
310
|
+
ServerComponents.KeyedForLoopWithIndex,
|
|
311
|
+
ClientComponents.KeyedForLoopWithIndex,
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
// Initial order
|
|
315
|
+
let listItems = container.querySelectorAll('li');
|
|
316
|
+
expect(listItems[0]?.textContent).toBe('[0] a: Alpha');
|
|
317
|
+
expect(listItems[1]?.textContent).toBe('[1] b: Beta');
|
|
318
|
+
expect(listItems[2]?.textContent).toBe('[2] c: Gamma');
|
|
319
|
+
expect(listItems[0]?.getAttribute('data-index')).toBe('0');
|
|
320
|
+
|
|
321
|
+
// Rotate: Beta, Gamma, Alpha
|
|
322
|
+
container.querySelector('.reorder')?.click();
|
|
323
|
+
flushSync();
|
|
324
|
+
|
|
325
|
+
listItems = container.querySelectorAll('li');
|
|
326
|
+
expect(listItems[0]?.textContent).toBe('[0] b: Beta');
|
|
327
|
+
expect(listItems[1]?.textContent).toBe('[1] c: Gamma');
|
|
328
|
+
expect(listItems[2]?.textContent).toBe('[2] a: Alpha');
|
|
329
|
+
expect(listItems[0]?.getAttribute('data-index')).toBe('0');
|
|
330
|
+
expect(listItems[2]?.getAttribute('data-index')).toBe('2');
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('hydrates for loop with sibling elements', async () => {
|
|
334
|
+
await hydrateComponent(
|
|
335
|
+
ServerComponents.ForLoopWithSiblings,
|
|
336
|
+
ClientComponents.ForLoopWithSiblings,
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
expect(container.querySelector('.before')?.textContent).toBe('Before');
|
|
340
|
+
expect(container.querySelector('.after')?.textContent).toBe('After');
|
|
341
|
+
expect(container.querySelectorAll('[class^="item-"]').length).toBe(2);
|
|
342
|
+
|
|
343
|
+
container.querySelector('.add')?.click();
|
|
344
|
+
flushSync();
|
|
345
|
+
|
|
346
|
+
expect(container.querySelector('.before')?.textContent).toBe('Before');
|
|
347
|
+
expect(container.querySelector('.after')?.textContent).toBe('After');
|
|
348
|
+
expect(container.querySelectorAll('[class^="item-"]').length).toBe(3);
|
|
349
|
+
expect(container.querySelector('.item-C')).not.toBeNull();
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('hydrates for loop items with their own reactive state', async () => {
|
|
353
|
+
await hydrateComponent(ServerComponents.ForLoopItemState, ClientComponents.ForLoopItemState);
|
|
354
|
+
|
|
355
|
+
// Initial state: all unchecked
|
|
356
|
+
const checkboxes = container.querySelectorAll('.checkbox');
|
|
357
|
+
expect(checkboxes.length).toBe(3);
|
|
358
|
+
|
|
359
|
+
expect(container.querySelector('.todo-1 span')?.className).toBe('pending');
|
|
360
|
+
expect(container.querySelector('.todo-2 span')?.className).toBe('pending');
|
|
361
|
+
|
|
362
|
+
// Check the first todo
|
|
363
|
+
/** @type {HTMLInputElement} */ (checkboxes[0])?.click();
|
|
364
|
+
flushSync();
|
|
365
|
+
|
|
366
|
+
expect(container.querySelector('.todo-1 span')?.className).toBe('completed');
|
|
367
|
+
expect(container.querySelector('.todo-2 span')?.className).toBe('pending');
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('hydrates for loop with single item', async () => {
|
|
371
|
+
await hydrateComponent(ServerComponents.ForLoopSingleItem, ClientComponents.ForLoopSingleItem);
|
|
372
|
+
|
|
373
|
+
const listItems = container.querySelectorAll('li');
|
|
374
|
+
expect(listItems.length).toBe(1);
|
|
375
|
+
expect(listItems[0]?.textContent).toBe('Only');
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('hydrates for loop adding at beginning', async () => {
|
|
379
|
+
await hydrateComponent(
|
|
380
|
+
ServerComponents.ForLoopAddAtBeginning,
|
|
381
|
+
ClientComponents.ForLoopAddAtBeginning,
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
let listItems = container.querySelectorAll('li');
|
|
385
|
+
expect(listItems.length).toBe(2);
|
|
386
|
+
expect(listItems[0]?.textContent).toBe('B');
|
|
387
|
+
|
|
388
|
+
container.querySelector('.prepend')?.click();
|
|
389
|
+
flushSync();
|
|
390
|
+
|
|
391
|
+
listItems = container.querySelectorAll('li');
|
|
392
|
+
expect(listItems.length).toBe(3);
|
|
393
|
+
expect(listItems[0]?.textContent).toBe('A');
|
|
394
|
+
expect(listItems[1]?.textContent).toBe('B');
|
|
395
|
+
expect(listItems[2]?.textContent).toBe('C');
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('hydrates for loop adding in middle', async () => {
|
|
399
|
+
await hydrateComponent(
|
|
400
|
+
ServerComponents.ForLoopAddInMiddle,
|
|
401
|
+
ClientComponents.ForLoopAddInMiddle,
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
let listItems = container.querySelectorAll('li');
|
|
405
|
+
expect(listItems.length).toBe(2);
|
|
406
|
+
expect(listItems[0]?.textContent).toBe('A');
|
|
407
|
+
expect(listItems[1]?.textContent).toBe('C');
|
|
408
|
+
|
|
409
|
+
container.querySelector('.insert')?.click();
|
|
410
|
+
flushSync();
|
|
411
|
+
|
|
412
|
+
listItems = container.querySelectorAll('li');
|
|
413
|
+
expect(listItems.length).toBe(3);
|
|
414
|
+
expect(listItems[0]?.textContent).toBe('A');
|
|
415
|
+
expect(listItems[1]?.textContent).toBe('B');
|
|
416
|
+
expect(listItems[2]?.textContent).toBe('C');
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('hydrates for loop removing from middle', async () => {
|
|
420
|
+
await hydrateComponent(
|
|
421
|
+
ServerComponents.ForLoopRemoveFromMiddle,
|
|
422
|
+
ClientComponents.ForLoopRemoveFromMiddle,
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
let listItems = container.querySelectorAll('li');
|
|
426
|
+
expect(listItems.length).toBe(3);
|
|
427
|
+
expect(listItems[1]?.textContent).toBe('B');
|
|
428
|
+
|
|
429
|
+
container.querySelector('.remove-middle')?.click();
|
|
430
|
+
flushSync();
|
|
431
|
+
|
|
432
|
+
listItems = container.querySelectorAll('li');
|
|
433
|
+
expect(listItems.length).toBe(2);
|
|
434
|
+
expect(listItems[0]?.textContent).toBe('A');
|
|
435
|
+
expect(listItems[1]?.textContent).toBe('C');
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it('hydrates for loop with large list', async () => {
|
|
439
|
+
await hydrateComponent(ServerComponents.ForLoopLargeList, ClientComponents.ForLoopLargeList);
|
|
440
|
+
|
|
441
|
+
const listItems = container.querySelectorAll('.large-list li');
|
|
442
|
+
expect(listItems.length).toBe(50);
|
|
443
|
+
expect(listItems[0]?.textContent).toBe('Item 1');
|
|
444
|
+
expect(listItems[49]?.textContent).toBe('Item 50');
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it('hydrates for loop with swap operation', async () => {
|
|
448
|
+
await hydrateComponent(ServerComponents.ForLoopSwap, ClientComponents.ForLoopSwap);
|
|
449
|
+
|
|
450
|
+
let listItems = container.querySelectorAll('li');
|
|
451
|
+
expect(listItems[0]?.textContent).toBe('A');
|
|
452
|
+
expect(listItems[3]?.textContent).toBe('D');
|
|
453
|
+
|
|
454
|
+
container.querySelector('.swap')?.click();
|
|
455
|
+
flushSync();
|
|
456
|
+
|
|
457
|
+
listItems = container.querySelectorAll('li');
|
|
458
|
+
expect(listItems[0]?.textContent).toBe('D');
|
|
459
|
+
expect(listItems[1]?.textContent).toBe('B');
|
|
460
|
+
expect(listItems[2]?.textContent).toBe('C');
|
|
461
|
+
expect(listItems[3]?.textContent).toBe('A');
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('hydrates for loop with reverse operation', async () => {
|
|
465
|
+
await hydrateComponent(ServerComponents.ForLoopReverse, ClientComponents.ForLoopReverse);
|
|
466
|
+
|
|
467
|
+
let listItems = container.querySelectorAll('li');
|
|
468
|
+
expect(listItems[0]?.textContent).toBe('A');
|
|
469
|
+
expect(listItems[3]?.textContent).toBe('D');
|
|
470
|
+
|
|
471
|
+
container.querySelector('.reverse')?.click();
|
|
472
|
+
flushSync();
|
|
473
|
+
|
|
474
|
+
listItems = container.querySelectorAll('li');
|
|
475
|
+
expect(listItems[0]?.textContent).toBe('D');
|
|
476
|
+
expect(listItems[1]?.textContent).toBe('C');
|
|
477
|
+
expect(listItems[2]?.textContent).toBe('B');
|
|
478
|
+
expect(listItems[3]?.textContent).toBe('A');
|
|
479
|
+
});
|
|
117
480
|
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { hydrateComponent, container } from '../setup-hydration.js';
|
|
3
|
+
|
|
4
|
+
// Import server-compiled components
|
|
5
|
+
import * as ServerComponents from './compiled/server/head.js';
|
|
6
|
+
// Import client-compiled components
|
|
7
|
+
import * as ClientComponents from './compiled/client/head.js';
|
|
8
|
+
|
|
9
|
+
describe('hydration > head', () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
// Clean up head elements from previous tests (except title)
|
|
12
|
+
const headChildren = Array.from(document.head.children);
|
|
13
|
+
for (const child of headChildren) {
|
|
14
|
+
if (child.tagName !== 'TITLE') {
|
|
15
|
+
child.remove();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
// Reset title
|
|
19
|
+
document.title = '';
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('hydrates static title element', async () => {
|
|
23
|
+
await hydrateComponent(ServerComponents.StaticTitle, ClientComponents.StaticTitle);
|
|
24
|
+
expect(document.title).toBe('Static Test Title');
|
|
25
|
+
expect(container.innerHTML).toBeHtml('<div>Content</div>');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('hydrates reactive title element', async () => {
|
|
29
|
+
await hydrateComponent(ServerComponents.ReactiveTitle, ClientComponents.ReactiveTitle);
|
|
30
|
+
expect(document.title).toBe('Initial Title');
|
|
31
|
+
expect(container.querySelector('span')?.textContent).toBe('Initial Title');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('hydrates multiple head elements', async () => {
|
|
35
|
+
await hydrateComponent(
|
|
36
|
+
ServerComponents.MultipleHeadElements,
|
|
37
|
+
ClientComponents.MultipleHeadElements,
|
|
38
|
+
);
|
|
39
|
+
expect(document.title).toBe('Page Title');
|
|
40
|
+
|
|
41
|
+
// Check meta tag
|
|
42
|
+
const metaTag = document.querySelector('meta[name="description"]');
|
|
43
|
+
expect(metaTag?.getAttribute('content')).toBe('Page description');
|
|
44
|
+
|
|
45
|
+
// Check link tag
|
|
46
|
+
const linkTag = document.querySelector('link[rel="stylesheet"]');
|
|
47
|
+
expect(linkTag?.getAttribute('href')).toBe('/styles.css');
|
|
48
|
+
|
|
49
|
+
expect(container.innerHTML).toBeHtml('<div>Page content</div>');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('hydrates reactive meta tags', async () => {
|
|
53
|
+
await hydrateComponent(ServerComponents.ReactiveMetaTags, ClientComponents.ReactiveMetaTags);
|
|
54
|
+
expect(document.title).toBe('My Page');
|
|
55
|
+
|
|
56
|
+
// Note: Reactive attributes in head elements are not fully supported yet during hydration
|
|
57
|
+
// The meta tag is created but the content attribute may not be set correctly during hydration
|
|
58
|
+
// This is a known limitation that will be addressed in future updates
|
|
59
|
+
|
|
60
|
+
expect(container.querySelector('div')?.textContent).toBe('Initial description');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('hydrates title with template literal', async () => {
|
|
64
|
+
await hydrateComponent(ServerComponents.TitleWithTemplate, ClientComponents.TitleWithTemplate);
|
|
65
|
+
expect(document.title).toBe('Hello World!');
|
|
66
|
+
expect(container.querySelector('div')?.textContent).toBe('World');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('hydrates empty title', async () => {
|
|
70
|
+
await hydrateComponent(ServerComponents.EmptyTitle, ClientComponents.EmptyTitle);
|
|
71
|
+
expect(document.title).toBe('');
|
|
72
|
+
expect(container.innerHTML).toBeHtml('<div>Empty title test</div>');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('hydrates title with conditional content', async () => {
|
|
76
|
+
await hydrateComponent(ServerComponents.ConditionalTitle, ClientComponents.ConditionalTitle);
|
|
77
|
+
expect(document.title).toBe('App - Main Page');
|
|
78
|
+
expect(container.querySelector('div')?.textContent).toBe('Main Page');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('hydrates title with computed value', async () => {
|
|
82
|
+
await hydrateComponent(ServerComponents.ComputedTitle, ClientComponents.ComputedTitle);
|
|
83
|
+
expect(document.title).toBe('Count: 0');
|
|
84
|
+
expect(container.querySelector('span')?.textContent).toBe('0');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('hydrates multiple head blocks', async () => {
|
|
88
|
+
await hydrateComponent(
|
|
89
|
+
ServerComponents.MultipleHeadBlocks,
|
|
90
|
+
ClientComponents.MultipleHeadBlocks,
|
|
91
|
+
);
|
|
92
|
+
expect(document.title).toBe('First Head');
|
|
93
|
+
|
|
94
|
+
const metaTag = document.querySelector('meta[name="author"]');
|
|
95
|
+
expect(metaTag?.getAttribute('content')).toBe('Test Author');
|
|
96
|
+
|
|
97
|
+
expect(container.innerHTML).toBeHtml('<div>Content</div>');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('hydrates simple head element', async () => {
|
|
101
|
+
await hydrateComponent(ServerComponents.HeadWithStyle, ClientComponents.HeadWithStyle);
|
|
102
|
+
expect(document.title).toBe('Styled Page');
|
|
103
|
+
expect(container.innerHTML).toBeHtml('<div>Styled content</div>');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { hydrateComponent, container } from '../setup-hydration.js';
|
|
3
|
+
|
|
4
|
+
// Import server-compiled components
|
|
5
|
+
import * as ServerComponents from './compiled/server/html.js';
|
|
6
|
+
// Import client-compiled components
|
|
7
|
+
import * as ClientComponents from './compiled/client/html.js';
|
|
8
|
+
|
|
9
|
+
describe('hydration > html tags', () => {
|
|
10
|
+
it('hydrates static html content', async () => {
|
|
11
|
+
await hydrateComponent(ServerComponents.StaticHtml, ClientComponents.StaticHtml);
|
|
12
|
+
expect(container.innerHTML).toBeHtml('<div><p><strong>Bold</strong> text</p></div>');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('hydrates dynamic html content', async () => {
|
|
16
|
+
await hydrateComponent(ServerComponents.DynamicHtml, ClientComponents.DynamicHtml);
|
|
17
|
+
expect(container.innerHTML).toBeHtml('<div><p>Dynamic <span>HTML</span> content</p></div>');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('hydrates empty html content', async () => {
|
|
21
|
+
await hydrateComponent(ServerComponents.EmptyHtml, ClientComponents.EmptyHtml);
|
|
22
|
+
expect(container.innerHTML).toBeHtml('<div></div>');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('hydrates complex nested html', async () => {
|
|
26
|
+
await hydrateComponent(ServerComponents.ComplexHtml, ClientComponents.ComplexHtml);
|
|
27
|
+
expect(container.innerHTML).toBeHtml(
|
|
28
|
+
'<section><div class="nested"><span>Nested <em>content</em></span></div></section>',
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('hydrates multiple html blocks', async () => {
|
|
33
|
+
await hydrateComponent(ServerComponents.MultipleHtml, ClientComponents.MultipleHtml);
|
|
34
|
+
expect(container.innerHTML).toBeHtml(
|
|
35
|
+
'<div><p>First paragraph</p><p>Second paragraph</p></div>',
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('hydrates html with reactivity', async () => {
|
|
40
|
+
const { container } = await hydrateComponent(
|
|
41
|
+
ServerComponents.HtmlWithReactivity,
|
|
42
|
+
ClientComponents.HtmlWithReactivity,
|
|
43
|
+
);
|
|
44
|
+
expect(container.innerHTML).toBeHtml('<div><p>Count: 0</p><button>Increment</button></div>');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } 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/portal.js';
|
|
7
|
+
// Import client-compiled components
|
|
8
|
+
import * as ClientComponents from './compiled/client/portal.js';
|
|
9
|
+
|
|
10
|
+
describe('hydration > portals', () => {
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
// Clean up any leftover portal content from document.body
|
|
13
|
+
const portals = document.body.querySelectorAll('.portal-content');
|
|
14
|
+
portals.forEach((el) => el.remove());
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('hydrates component with portal gracefully without breaking', async () => {
|
|
18
|
+
// The main goal is that hydration doesn't throw errors
|
|
19
|
+
await hydrateComponent(ServerComponents.SimplePortal, ClientComponents.SimplePortal);
|
|
20
|
+
|
|
21
|
+
// Flush any pending updates
|
|
22
|
+
flushSync();
|
|
23
|
+
|
|
24
|
+
// Main content should be in the container
|
|
25
|
+
expect(container.querySelector('.container')).toBeTruthy();
|
|
26
|
+
expect(container.querySelector('h1')?.textContent).toBe('Main Content');
|
|
27
|
+
|
|
28
|
+
// Portal content should NOT be in the container (it's in document.body)
|
|
29
|
+
expect(container.querySelector('.portal-content')).toBeNull();
|
|
30
|
+
|
|
31
|
+
// Note: Portal content rendering to document.body during hydration may vary
|
|
32
|
+
// The important thing is that hydration doesn't break
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('hydrates component with portal and main content', async () => {
|
|
36
|
+
await hydrateComponent(
|
|
37
|
+
ServerComponents.PortalWithMainContent,
|
|
38
|
+
ClientComponents.PortalWithMainContent,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// Flush any pending updates
|
|
42
|
+
flushSync();
|
|
43
|
+
|
|
44
|
+
// Main content and footer should be in container
|
|
45
|
+
expect(container.querySelector('.main-content')?.textContent).toBe('Main page content');
|
|
46
|
+
expect(container.querySelector('.footer')?.textContent).toBe('Footer');
|
|
47
|
+
|
|
48
|
+
// Portal content should be in document.body
|
|
49
|
+
expect(document.body.querySelector('.portal-content')).toBeTruthy();
|
|
50
|
+
expect(document.body.querySelector('.portal-content')?.textContent).toBe('Modal content');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('hydrates nested content with portal gracefully', async () => {
|
|
54
|
+
// The main goal is that hydration doesn't throw errors
|
|
55
|
+
await hydrateComponent(
|
|
56
|
+
ServerComponents.NestedContentWithPortal,
|
|
57
|
+
ClientComponents.NestedContentWithPortal,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// Flush any pending updates
|
|
61
|
+
flushSync();
|
|
62
|
+
|
|
63
|
+
// Nested content should be in container
|
|
64
|
+
expect(container.querySelector('.outer')).toBeTruthy();
|
|
65
|
+
expect(container.querySelector('.inner')).toBeTruthy();
|
|
66
|
+
expect(container.querySelector('span')?.textContent).toBe('Nested content');
|
|
67
|
+
|
|
68
|
+
// Portal content may or may not render during hydration - that's ok
|
|
69
|
+
// The important thing is no hydration errors
|
|
70
|
+
});
|
|
71
|
+
});
|