ripple 0.2.105 → 0.2.107
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/compiler/phases/1-parse/index.js +18 -4
- package/src/compiler/phases/2-analyze/index.js +45 -4
- package/src/compiler/phases/3-transform/client/index.js +87 -12
- package/src/runtime/create-subscriber.js +34 -0
- package/src/runtime/index-client.js +8 -0
- package/src/runtime/index-server.js +27 -2
- package/src/runtime/internal/client/constants.js +13 -12
- package/src/runtime/internal/client/index.js +2 -0
- package/src/runtime/internal/client/runtime.js +19 -19
- package/src/runtime/internal/client/switch.js +32 -0
- package/src/runtime/media-query.js +39 -0
- package/src/runtime/reactive-value.js +21 -0
- package/src/runtime/url-search-params.js +147 -0
- package/src/runtime/url.js +164 -0
- package/src/utils/builders.js +33 -0
- package/tests/client/__snapshots__/compiler.test.ripple.snap +12 -0
- package/tests/client/basic.test.ripple +46 -1
- package/tests/client/compiler.test.ripple +33 -0
- package/tests/client/media-query.test.ripple +130 -0
- package/tests/client/url-search-params.test.ripple +912 -0
- package/tests/client/url.test.ripple +954 -0
- package/types/index.d.ts +41 -11
|
@@ -0,0 +1,912 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mount, flushSync, track, effect } from 'ripple';
|
|
3
|
+
import { TrackedURLSearchParams } from '../../src/runtime/url-search-params.js';
|
|
4
|
+
import { TrackedURL } from '../../src/runtime/url.js';
|
|
5
|
+
|
|
6
|
+
describe('TrackedURLSearchParams', () => {
|
|
7
|
+
let container;
|
|
8
|
+
|
|
9
|
+
function render(component) {
|
|
10
|
+
mount(component, {
|
|
11
|
+
target: container
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
container = document.createElement('div');
|
|
17
|
+
document.body.appendChild(container);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
document.body.removeChild(container);
|
|
22
|
+
container = null;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('creates empty URLSearchParams with reactivity', () => {
|
|
26
|
+
component URLTest() {
|
|
27
|
+
const params = new TrackedURLSearchParams();
|
|
28
|
+
|
|
29
|
+
<pre>{params.toString()}</pre>
|
|
30
|
+
<pre>{params.size}</pre>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
render(URLTest);
|
|
34
|
+
|
|
35
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('');
|
|
36
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('0');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('creates URLSearchParams from string with reactivity', () => {
|
|
40
|
+
component URLTest() {
|
|
41
|
+
const params = new TrackedURLSearchParams('foo=bar&baz=qux');
|
|
42
|
+
|
|
43
|
+
<pre>{params.toString()}</pre>
|
|
44
|
+
<pre>{params.size}</pre>
|
|
45
|
+
<pre>{params.get('foo')}</pre>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
render(URLTest);
|
|
49
|
+
|
|
50
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=bar&baz=qux');
|
|
51
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('2');
|
|
52
|
+
expect(container.querySelectorAll('pre')[2].textContent).toBe('bar');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('creates URLSearchParams from object with reactivity', () => {
|
|
56
|
+
component URLTest() {
|
|
57
|
+
const params = new TrackedURLSearchParams({ foo: 'bar', baz: 'qux' });
|
|
58
|
+
|
|
59
|
+
<pre>{params.toString()}</pre>
|
|
60
|
+
<pre>{params.size}</pre>
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
render(URLTest);
|
|
64
|
+
|
|
65
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=bar&baz=qux');
|
|
66
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('2');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('handles append operation with reactivity', () => {
|
|
70
|
+
component URLTest() {
|
|
71
|
+
const params = new TrackedURLSearchParams('foo=bar');
|
|
72
|
+
|
|
73
|
+
<button onClick={() => params.append('baz', 'qux')}>{'append'}</button>
|
|
74
|
+
<pre>{params.toString()}</pre>
|
|
75
|
+
<pre>{params.size}</pre>
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
render(URLTest);
|
|
79
|
+
|
|
80
|
+
const button = container.querySelector('button');
|
|
81
|
+
|
|
82
|
+
// Initial state
|
|
83
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=bar');
|
|
84
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('1');
|
|
85
|
+
|
|
86
|
+
// Test append
|
|
87
|
+
button.click();
|
|
88
|
+
flushSync();
|
|
89
|
+
|
|
90
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=bar&baz=qux');
|
|
91
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('2');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('handles append with multiple values for same key', () => {
|
|
95
|
+
component URLTest() {
|
|
96
|
+
const params = new TrackedURLSearchParams('foo=bar');
|
|
97
|
+
let allFoo = track(() => params.getAll('foo'));
|
|
98
|
+
|
|
99
|
+
<button onClick={() => params.append('foo', 'baz')}>{'append foo'}</button>
|
|
100
|
+
<pre>{params.toString()}</pre>
|
|
101
|
+
<pre>{JSON.stringify(@allFoo)}</pre>
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
render(URLTest);
|
|
105
|
+
|
|
106
|
+
const button = container.querySelector('button');
|
|
107
|
+
|
|
108
|
+
// Initial state
|
|
109
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=bar');
|
|
110
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('["bar"]');
|
|
111
|
+
|
|
112
|
+
// Test append
|
|
113
|
+
button.click();
|
|
114
|
+
flushSync();
|
|
115
|
+
|
|
116
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=bar&foo=baz');
|
|
117
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('["bar","baz"]');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('handles delete operation with reactivity', () => {
|
|
121
|
+
component URLTest() {
|
|
122
|
+
const params = new TrackedURLSearchParams('foo=bar&baz=qux');
|
|
123
|
+
|
|
124
|
+
<button onClick={() => params.delete('foo')}>{'delete foo'}</button>
|
|
125
|
+
<pre>{params.toString()}</pre>
|
|
126
|
+
<pre>{params.size}</pre>
|
|
127
|
+
<pre>{params.has('foo').toString()}</pre>
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
render(URLTest);
|
|
131
|
+
|
|
132
|
+
const button = container.querySelector('button');
|
|
133
|
+
|
|
134
|
+
// Initial state
|
|
135
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=bar&baz=qux');
|
|
136
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('2');
|
|
137
|
+
expect(container.querySelectorAll('pre')[2].textContent).toBe('true');
|
|
138
|
+
|
|
139
|
+
// Test delete
|
|
140
|
+
button.click();
|
|
141
|
+
flushSync();
|
|
142
|
+
|
|
143
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('baz=qux');
|
|
144
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('1');
|
|
145
|
+
expect(container.querySelectorAll('pre')[2].textContent).toBe('false');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('handles delete with specific value', () => {
|
|
149
|
+
component URLTest() {
|
|
150
|
+
const params = new TrackedURLSearchParams('foo=bar&foo=baz&foo=qux');
|
|
151
|
+
|
|
152
|
+
<button onClick={() => params.delete('foo', 'baz')}>{'delete foo=baz'}</button>
|
|
153
|
+
<pre>{params.toString()}</pre>
|
|
154
|
+
<pre>{params.size}</pre>
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
render(URLTest);
|
|
158
|
+
|
|
159
|
+
const button = container.querySelector('button');
|
|
160
|
+
|
|
161
|
+
// Initial state
|
|
162
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=bar&foo=baz&foo=qux');
|
|
163
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('3');
|
|
164
|
+
|
|
165
|
+
// Test delete specific value
|
|
166
|
+
button.click();
|
|
167
|
+
flushSync();
|
|
168
|
+
|
|
169
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=bar&foo=qux');
|
|
170
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('2');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('handles delete when key does not exist', () => {
|
|
174
|
+
component URLTest() {
|
|
175
|
+
const params = new TrackedURLSearchParams('foo=bar');
|
|
176
|
+
let reactiveSize = track(() => params.size);
|
|
177
|
+
|
|
178
|
+
<button onClick={() => params.delete('nonexistent')}>{'delete nonexistent'}</button>
|
|
179
|
+
<pre>{params.toString()}</pre>
|
|
180
|
+
<pre>{@reactiveSize}</pre>
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
render(URLTest);
|
|
184
|
+
|
|
185
|
+
const button = container.querySelector('button');
|
|
186
|
+
|
|
187
|
+
// Initial state
|
|
188
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=bar');
|
|
189
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('1');
|
|
190
|
+
|
|
191
|
+
// Test delete nonexistent - should not trigger reactivity
|
|
192
|
+
button.click();
|
|
193
|
+
flushSync();
|
|
194
|
+
|
|
195
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=bar');
|
|
196
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('1');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('handles get operation with reactivity', () => {
|
|
200
|
+
component URLTest() {
|
|
201
|
+
const params = new TrackedURLSearchParams('foo=bar&baz=qux');
|
|
202
|
+
let foo = track(() => params.get('foo'));
|
|
203
|
+
let baz = track(() => params.get('baz'));
|
|
204
|
+
|
|
205
|
+
<button onClick={() => params.set('foo', 'updated')}>{'update foo'}</button>
|
|
206
|
+
<pre>{@foo}</pre>
|
|
207
|
+
<pre>{@baz}</pre>
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
render(URLTest);
|
|
211
|
+
|
|
212
|
+
const button = container.querySelector('button');
|
|
213
|
+
|
|
214
|
+
// Initial state
|
|
215
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('bar');
|
|
216
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('qux');
|
|
217
|
+
|
|
218
|
+
// Test update
|
|
219
|
+
button.click();
|
|
220
|
+
flushSync();
|
|
221
|
+
|
|
222
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('updated');
|
|
223
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('qux');
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('handles get for nonexistent key', () => {
|
|
227
|
+
component URLTest() {
|
|
228
|
+
const params = new TrackedURLSearchParams('foo=bar');
|
|
229
|
+
let nonexistent = track(() => params.get('nonexistent'));
|
|
230
|
+
|
|
231
|
+
<pre>{String(@nonexistent)}</pre>
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
render(URLTest);
|
|
235
|
+
|
|
236
|
+
expect(container.querySelector('pre').textContent).toBe('null');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('handles getAll operation with reactivity', () => {
|
|
240
|
+
component URLTest() {
|
|
241
|
+
const params = new TrackedURLSearchParams('foo=bar&foo=baz');
|
|
242
|
+
let allFoo = track(() => params.getAll('foo'));
|
|
243
|
+
|
|
244
|
+
<button onClick={() => params.append('foo', 'qux')}>{'append foo'}</button>
|
|
245
|
+
<pre>{JSON.stringify(@allFoo)}</pre>
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
render(URLTest);
|
|
249
|
+
|
|
250
|
+
const button = container.querySelector('button');
|
|
251
|
+
|
|
252
|
+
// Initial state
|
|
253
|
+
expect(container.querySelector('pre').textContent).toBe('["bar","baz"]');
|
|
254
|
+
|
|
255
|
+
// Test append
|
|
256
|
+
button.click();
|
|
257
|
+
flushSync();
|
|
258
|
+
|
|
259
|
+
expect(container.querySelector('pre').textContent).toBe('["bar","baz","qux"]');
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('handles has operation with reactivity', () => {
|
|
263
|
+
component URLTest() {
|
|
264
|
+
const params = new TrackedURLSearchParams('foo=bar');
|
|
265
|
+
let hasFoo = track(() => params.has('foo'));
|
|
266
|
+
let hasBaz = track(() => params.has('baz'));
|
|
267
|
+
|
|
268
|
+
<button onClick={() => params.append('baz', 'qux')}>{'add baz'}</button>
|
|
269
|
+
<button onClick={() => params.delete('foo')}>{'delete foo'}</button>
|
|
270
|
+
<pre>{@hasFoo.toString()}</pre>
|
|
271
|
+
<pre>{@hasBaz.toString()}</pre>
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
render(URLTest);
|
|
275
|
+
|
|
276
|
+
const addButton = container.querySelectorAll('button')[0];
|
|
277
|
+
const deleteButton = container.querySelectorAll('button')[1];
|
|
278
|
+
|
|
279
|
+
// Initial state
|
|
280
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('true');
|
|
281
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('false');
|
|
282
|
+
|
|
283
|
+
// Test add
|
|
284
|
+
addButton.click();
|
|
285
|
+
flushSync();
|
|
286
|
+
|
|
287
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('true');
|
|
288
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('true');
|
|
289
|
+
|
|
290
|
+
// Test delete
|
|
291
|
+
deleteButton.click();
|
|
292
|
+
flushSync();
|
|
293
|
+
|
|
294
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('false');
|
|
295
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('true');
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('handles has with specific value', () => {
|
|
299
|
+
component URLTest() {
|
|
300
|
+
const params = new TrackedURLSearchParams('foo=bar&foo=baz');
|
|
301
|
+
let hasBarValue = track(() => params.has('foo', 'bar'));
|
|
302
|
+
let hasQuxValue = track(() => params.has('foo', 'qux'));
|
|
303
|
+
|
|
304
|
+
<button onClick={() => params.append('foo', 'qux')}>{'add qux'}</button>
|
|
305
|
+
<pre>{@hasBarValue.toString()}</pre>
|
|
306
|
+
<pre>{@hasQuxValue.toString()}</pre>
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
render(URLTest);
|
|
310
|
+
|
|
311
|
+
const button = container.querySelector('button');
|
|
312
|
+
|
|
313
|
+
// Initial state
|
|
314
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('true');
|
|
315
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('false');
|
|
316
|
+
|
|
317
|
+
// Test add
|
|
318
|
+
button.click();
|
|
319
|
+
flushSync();
|
|
320
|
+
|
|
321
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('true');
|
|
322
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('true');
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('handles set operation with reactivity', () => {
|
|
326
|
+
component URLTest() {
|
|
327
|
+
const params = new TrackedURLSearchParams('foo=bar');
|
|
328
|
+
|
|
329
|
+
<button onClick={() => params.set('foo', 'updated')}>{'update foo'}</button>
|
|
330
|
+
<button onClick={() => params.set('baz', 'qux')}>{'add baz'}</button>
|
|
331
|
+
<pre>{params.toString()}</pre>
|
|
332
|
+
<pre>{params.size}</pre>
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
render(URLTest);
|
|
336
|
+
|
|
337
|
+
const updateButton = container.querySelectorAll('button')[0];
|
|
338
|
+
const addButton = container.querySelectorAll('button')[1];
|
|
339
|
+
|
|
340
|
+
// Initial state
|
|
341
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=bar');
|
|
342
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('1');
|
|
343
|
+
|
|
344
|
+
// Test update
|
|
345
|
+
updateButton.click();
|
|
346
|
+
flushSync();
|
|
347
|
+
|
|
348
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=updated');
|
|
349
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('1');
|
|
350
|
+
|
|
351
|
+
// Test add new key
|
|
352
|
+
addButton.click();
|
|
353
|
+
flushSync();
|
|
354
|
+
|
|
355
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=updated&baz=qux');
|
|
356
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('2');
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('handles set with multiple existing values', () => {
|
|
360
|
+
component URLTest() {
|
|
361
|
+
const params = new TrackedURLSearchParams('foo=bar&foo=baz&foo=qux');
|
|
362
|
+
let allFoo = track(() => params.getAll('foo'));
|
|
363
|
+
|
|
364
|
+
<button onClick={() => params.set('foo', 'single')}>{'set foo'}</button>
|
|
365
|
+
<pre>{params.toString()}</pre>
|
|
366
|
+
<pre>{JSON.stringify(@allFoo)}</pre>
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
render(URLTest);
|
|
370
|
+
|
|
371
|
+
const button = container.querySelector('button');
|
|
372
|
+
|
|
373
|
+
// Initial state
|
|
374
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=bar&foo=baz&foo=qux');
|
|
375
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('["bar","baz","qux"]');
|
|
376
|
+
|
|
377
|
+
// Test set - should replace all values
|
|
378
|
+
button.click();
|
|
379
|
+
flushSync();
|
|
380
|
+
|
|
381
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=single');
|
|
382
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('["single"]');
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('handles set when value is the same', () => {
|
|
386
|
+
component URLTest() {
|
|
387
|
+
const params = new TrackedURLSearchParams('foo=bar');
|
|
388
|
+
let reactiveString = track(() => params.toString());
|
|
389
|
+
|
|
390
|
+
<button onClick={() => params.set('foo', 'bar')}>{'set same value'}</button>
|
|
391
|
+
<pre>{@reactiveString}</pre>
|
|
392
|
+
<pre>{params.size}</pre>
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
render(URLTest);
|
|
396
|
+
|
|
397
|
+
const button = container.querySelector('button');
|
|
398
|
+
|
|
399
|
+
// Test set same value - should not trigger reactivity changes
|
|
400
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=bar');
|
|
401
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('1');
|
|
402
|
+
|
|
403
|
+
button.click();
|
|
404
|
+
flushSync();
|
|
405
|
+
|
|
406
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=bar');
|
|
407
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('1');
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('handles sort operation with reactivity', () => {
|
|
411
|
+
component URLTest() {
|
|
412
|
+
const params = new TrackedURLSearchParams('z=last&a=first&m=middle');
|
|
413
|
+
|
|
414
|
+
<button onClick={() => params.sort()}>{'sort'}</button>
|
|
415
|
+
<pre>{params.toString()}</pre>
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
render(URLTest);
|
|
419
|
+
|
|
420
|
+
const button = container.querySelector('button');
|
|
421
|
+
|
|
422
|
+
// Initial state
|
|
423
|
+
expect(container.querySelector('pre').textContent).toBe('z=last&a=first&m=middle');
|
|
424
|
+
|
|
425
|
+
// Test sort
|
|
426
|
+
button.click();
|
|
427
|
+
flushSync();
|
|
428
|
+
|
|
429
|
+
expect(container.querySelector('pre').textContent).toBe('a=first&m=middle&z=last');
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it('handles keys method with reactivity', () => {
|
|
433
|
+
component URLTest() {
|
|
434
|
+
const params = new TrackedURLSearchParams('foo=bar&baz=qux');
|
|
435
|
+
let keys = track(() => Array.from(params.keys()));
|
|
436
|
+
|
|
437
|
+
<button onClick={() => params.append('new', 'value')}>{'add param'}</button>
|
|
438
|
+
<pre>{JSON.stringify(@keys)}</pre>
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
render(URLTest);
|
|
442
|
+
|
|
443
|
+
const button = container.querySelector('button');
|
|
444
|
+
|
|
445
|
+
// Initial state
|
|
446
|
+
expect(container.querySelector('pre').textContent).toBe('["foo","baz"]');
|
|
447
|
+
|
|
448
|
+
// Test add
|
|
449
|
+
button.click();
|
|
450
|
+
flushSync();
|
|
451
|
+
|
|
452
|
+
expect(container.querySelector('pre').textContent).toBe('["foo","baz","new"]');
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it('handles values method with reactivity', () => {
|
|
456
|
+
component URLTest() {
|
|
457
|
+
const params = new TrackedURLSearchParams('foo=bar&baz=qux');
|
|
458
|
+
let values = track(() => Array.from(params.values()));
|
|
459
|
+
|
|
460
|
+
<button onClick={() => params.set('foo', 'updated')}>{'update foo'}</button>
|
|
461
|
+
<pre>{JSON.stringify(@values)}</pre>
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
render(URLTest);
|
|
465
|
+
|
|
466
|
+
const button = container.querySelector('button');
|
|
467
|
+
|
|
468
|
+
// Initial state
|
|
469
|
+
expect(container.querySelector('pre').textContent).toBe('["bar","qux"]');
|
|
470
|
+
|
|
471
|
+
// Test update
|
|
472
|
+
button.click();
|
|
473
|
+
flushSync();
|
|
474
|
+
|
|
475
|
+
expect(container.querySelector('pre').textContent).toBe('["updated","qux"]');
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it('handles entries method with reactivity', () => {
|
|
479
|
+
component URLTest() {
|
|
480
|
+
const params = new TrackedURLSearchParams('foo=bar&baz=qux');
|
|
481
|
+
let entries = track(() => Array.from(params.entries()));
|
|
482
|
+
|
|
483
|
+
<button onClick={() => params.append('new', 'value')}>{'add param'}</button>
|
|
484
|
+
<pre>{JSON.stringify(@entries)}</pre>
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
render(URLTest);
|
|
488
|
+
|
|
489
|
+
const button = container.querySelector('button');
|
|
490
|
+
|
|
491
|
+
// Initial state
|
|
492
|
+
expect(container.querySelector('pre').textContent).toBe('[["foo","bar"],["baz","qux"]]');
|
|
493
|
+
|
|
494
|
+
// Test add
|
|
495
|
+
button.click();
|
|
496
|
+
flushSync();
|
|
497
|
+
|
|
498
|
+
expect(container.querySelector('pre').textContent).toBe('[["foo","bar"],["baz","qux"],["new","value"]]');
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it('handles Symbol.iterator with reactivity', () => {
|
|
502
|
+
component URLTest() {
|
|
503
|
+
const params = new TrackedURLSearchParams('foo=bar&baz=qux');
|
|
504
|
+
let entries = track(() => Array.from(params));
|
|
505
|
+
|
|
506
|
+
<button onClick={() => params.delete('foo')}>{'delete foo'}</button>
|
|
507
|
+
<pre>{JSON.stringify(@entries)}</pre>
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
render(URLTest);
|
|
511
|
+
|
|
512
|
+
const button = container.querySelector('button');
|
|
513
|
+
|
|
514
|
+
// Initial state
|
|
515
|
+
expect(container.querySelector('pre').textContent).toBe('[["foo","bar"],["baz","qux"]]');
|
|
516
|
+
|
|
517
|
+
// Test delete
|
|
518
|
+
button.click();
|
|
519
|
+
flushSync();
|
|
520
|
+
|
|
521
|
+
expect(container.querySelector('pre').textContent).toBe('[["baz","qux"]]');
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it('handles iteration with for...of', () => {
|
|
525
|
+
component URLTest() {
|
|
526
|
+
const params = new TrackedURLSearchParams('foo=bar&baz=qux');
|
|
527
|
+
|
|
528
|
+
<button onClick={() => params.append('new', 'value')}>{'add param'}</button>
|
|
529
|
+
|
|
530
|
+
for (const [key, value] of params) {
|
|
531
|
+
<pre>{`${key}=${value}`}</pre>
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
render(URLTest);
|
|
536
|
+
|
|
537
|
+
const button = container.querySelector('button');
|
|
538
|
+
|
|
539
|
+
// Initial state
|
|
540
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=bar');
|
|
541
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('baz=qux');
|
|
542
|
+
|
|
543
|
+
// Test add
|
|
544
|
+
button.click();
|
|
545
|
+
flushSync();
|
|
546
|
+
|
|
547
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=bar');
|
|
548
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('baz=qux');
|
|
549
|
+
expect(container.querySelectorAll('pre')[2].textContent).toBe('new=value');
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it('handles size property with reactivity', () => {
|
|
553
|
+
component URLTest() {
|
|
554
|
+
const params = new TrackedURLSearchParams('foo=bar');
|
|
555
|
+
let size = track(() => params.size);
|
|
556
|
+
|
|
557
|
+
<button onClick={() => params.append('baz', 'qux')}>{'add'}</button>
|
|
558
|
+
<button onClick={() => params.delete('foo')}>{'delete'}</button>
|
|
559
|
+
<pre>{@size}</pre>
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
render(URLTest);
|
|
563
|
+
|
|
564
|
+
const addButton = container.querySelectorAll('button')[0];
|
|
565
|
+
const deleteButton = container.querySelectorAll('button')[1];
|
|
566
|
+
|
|
567
|
+
// Initial state
|
|
568
|
+
expect(container.querySelector('pre').textContent).toBe('1');
|
|
569
|
+
|
|
570
|
+
// Test add
|
|
571
|
+
addButton.click();
|
|
572
|
+
flushSync();
|
|
573
|
+
|
|
574
|
+
expect(container.querySelector('pre').textContent).toBe('2');
|
|
575
|
+
|
|
576
|
+
// Test delete
|
|
577
|
+
deleteButton.click();
|
|
578
|
+
flushSync();
|
|
579
|
+
|
|
580
|
+
expect(container.querySelector('pre').textContent).toBe('1');
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it('handles toString method with reactivity', () => {
|
|
584
|
+
component URLTest() {
|
|
585
|
+
const params = new TrackedURLSearchParams('foo=bar');
|
|
586
|
+
let string = track(() => params.toString());
|
|
587
|
+
|
|
588
|
+
<button onClick={() => params.append('baz', 'qux')}>{'add'}</button>
|
|
589
|
+
<pre>{@string}</pre>
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
render(URLTest);
|
|
593
|
+
|
|
594
|
+
const button = container.querySelector('button');
|
|
595
|
+
|
|
596
|
+
// Initial state
|
|
597
|
+
expect(container.querySelector('pre').textContent).toBe('foo=bar');
|
|
598
|
+
|
|
599
|
+
// Test add
|
|
600
|
+
button.click();
|
|
601
|
+
flushSync();
|
|
602
|
+
|
|
603
|
+
expect(container.querySelector('pre').textContent).toBe('foo=bar&baz=qux');
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it('handles special characters encoding', () => {
|
|
607
|
+
component URLTest() {
|
|
608
|
+
const params = new TrackedURLSearchParams();
|
|
609
|
+
|
|
610
|
+
<button onClick={() => params.set('key', 'value with spaces')}>{'add spaces'}</button>
|
|
611
|
+
<button onClick={() => params.set('special', '!@#$%^&*()')}>{'add special'}</button>
|
|
612
|
+
<pre>{params.toString()}</pre>
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
render(URLTest);
|
|
616
|
+
|
|
617
|
+
const spacesButton = container.querySelectorAll('button')[0];
|
|
618
|
+
const specialButton = container.querySelectorAll('button')[1];
|
|
619
|
+
|
|
620
|
+
// Test spaces
|
|
621
|
+
spacesButton.click();
|
|
622
|
+
flushSync();
|
|
623
|
+
|
|
624
|
+
expect(container.querySelector('pre').textContent).toBe('key=value+with+spaces');
|
|
625
|
+
|
|
626
|
+
// Test special characters
|
|
627
|
+
specialButton.click();
|
|
628
|
+
flushSync();
|
|
629
|
+
|
|
630
|
+
expect(container.querySelector('pre').textContent).toContain('special');
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it('handles multiple operations in sequence', () => {
|
|
634
|
+
component URLTest() {
|
|
635
|
+
const params = new TrackedURLSearchParams();
|
|
636
|
+
|
|
637
|
+
<button onClick={() => {
|
|
638
|
+
params.append('a', '1');
|
|
639
|
+
params.append('b', '2');
|
|
640
|
+
params.set('a', '10');
|
|
641
|
+
params.delete('b');
|
|
642
|
+
params.append('c', '3');
|
|
643
|
+
params.sort();
|
|
644
|
+
}}>{'complex operations'}</button>
|
|
645
|
+
<pre>{params.toString()}</pre>
|
|
646
|
+
<pre>{params.size}</pre>
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
render(URLTest);
|
|
650
|
+
|
|
651
|
+
const button = container.querySelector('button');
|
|
652
|
+
|
|
653
|
+
// Initial state
|
|
654
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('');
|
|
655
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('0');
|
|
656
|
+
|
|
657
|
+
// Test complex operations
|
|
658
|
+
button.click();
|
|
659
|
+
flushSync();
|
|
660
|
+
|
|
661
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('a=10&c=3');
|
|
662
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('2');
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
it('integrates with TrackedURL', () => {
|
|
666
|
+
component URLTest() {
|
|
667
|
+
const url = new TrackedURL('https://example.com?foo=bar');
|
|
668
|
+
const params = url.searchParams;
|
|
669
|
+
|
|
670
|
+
<button onClick={() => params.append('baz', 'qux')}>{'add param'}</button>
|
|
671
|
+
<pre>{url.href}</pre>
|
|
672
|
+
<pre>{params.toString()}</pre>
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
render(URLTest);
|
|
676
|
+
|
|
677
|
+
const button = container.querySelector('button');
|
|
678
|
+
|
|
679
|
+
// Initial state
|
|
680
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/?foo=bar');
|
|
681
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('foo=bar');
|
|
682
|
+
|
|
683
|
+
// Test add param - should update URL
|
|
684
|
+
button.click();
|
|
685
|
+
flushSync();
|
|
686
|
+
|
|
687
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/?foo=bar&baz=qux');
|
|
688
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('foo=bar&baz=qux');
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
it('handles empty search string in URL', () => {
|
|
692
|
+
component URLTest() {
|
|
693
|
+
const url = new TrackedURL('https://example.com');
|
|
694
|
+
const params = url.searchParams;
|
|
695
|
+
|
|
696
|
+
<button onClick={() => params.append('foo', 'bar')}>{'add first param'}</button>
|
|
697
|
+
<pre>{url.href}</pre>
|
|
698
|
+
<pre>{params.size}</pre>
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
render(URLTest);
|
|
702
|
+
|
|
703
|
+
const button = container.querySelector('button');
|
|
704
|
+
|
|
705
|
+
// Initial state - no search params
|
|
706
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/');
|
|
707
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('0');
|
|
708
|
+
|
|
709
|
+
// Test add first param
|
|
710
|
+
button.click();
|
|
711
|
+
flushSync();
|
|
712
|
+
|
|
713
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/?foo=bar');
|
|
714
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('1');
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
it('handles clearing all params via delete', () => {
|
|
718
|
+
component URLTest() {
|
|
719
|
+
const url = new TrackedURL('https://example.com?foo=bar&baz=qux');
|
|
720
|
+
const params = url.searchParams;
|
|
721
|
+
|
|
722
|
+
<button onClick={() => {
|
|
723
|
+
params.delete('foo');
|
|
724
|
+
params.delete('baz');
|
|
725
|
+
}}>{'clear all'}</button>
|
|
726
|
+
<pre>{url.href}</pre>
|
|
727
|
+
<pre>{params.size}</pre>
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
render(URLTest);
|
|
731
|
+
|
|
732
|
+
const button = container.querySelector('button');
|
|
733
|
+
|
|
734
|
+
// Initial state
|
|
735
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/?foo=bar&baz=qux');
|
|
736
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('2');
|
|
737
|
+
|
|
738
|
+
// Test clear all
|
|
739
|
+
button.click();
|
|
740
|
+
flushSync();
|
|
741
|
+
|
|
742
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/');
|
|
743
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('0');
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
it('handles reactive computed properties based on search params', () => {
|
|
747
|
+
component URLTest() {
|
|
748
|
+
const params = new TrackedURLSearchParams('page=1&limit=10');
|
|
749
|
+
let page = track(() => parseInt(params.get('page') || '1', 10));
|
|
750
|
+
let limit = track(() => parseInt(params.get('limit') || '10', 10));
|
|
751
|
+
let offset = track(() => (@page - 1) * @limit);
|
|
752
|
+
|
|
753
|
+
<button onClick={() => params.set('page', '2')}>{'next page'}</button>
|
|
754
|
+
<button onClick={() => params.set('page', '1')}>{'first page'}</button>
|
|
755
|
+
<pre>{`Page: ${@page}`}</pre>
|
|
756
|
+
<pre>{`Limit: ${@limit}`}</pre>
|
|
757
|
+
<pre>{`Offset: ${@offset}`}</pre>
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
render(URLTest);
|
|
761
|
+
|
|
762
|
+
const nextButton = container.querySelectorAll('button')[0];
|
|
763
|
+
const firstButton = container.querySelectorAll('button')[1];
|
|
764
|
+
|
|
765
|
+
// Initial state
|
|
766
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('Page: 1');
|
|
767
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('Limit: 10');
|
|
768
|
+
expect(container.querySelectorAll('pre')[2].textContent).toBe('Offset: 0');
|
|
769
|
+
|
|
770
|
+
// Test next page
|
|
771
|
+
nextButton.click();
|
|
772
|
+
flushSync();
|
|
773
|
+
|
|
774
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('Page: 2');
|
|
775
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('Limit: 10');
|
|
776
|
+
expect(container.querySelectorAll('pre')[2].textContent).toBe('Offset: 10');
|
|
777
|
+
|
|
778
|
+
// Test first page
|
|
779
|
+
firstButton.click();
|
|
780
|
+
flushSync();
|
|
781
|
+
|
|
782
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('Page: 1');
|
|
783
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('Limit: 10');
|
|
784
|
+
expect(container.querySelectorAll('pre')[2].textContent).toBe('Offset: 0');
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
it('handles duplicate keys with different values', () => {
|
|
788
|
+
component URLTest() {
|
|
789
|
+
const params = new TrackedURLSearchParams();
|
|
790
|
+
let tags = track(() => params.getAll('tag'));
|
|
791
|
+
|
|
792
|
+
<button onClick={() => params.append('tag', 'javascript')}>{'add js'}</button>
|
|
793
|
+
<button onClick={() => params.append('tag', 'typescript')}>{'add ts'}</button>
|
|
794
|
+
<button onClick={() => params.append('tag', 'ripple')}>{'add ripple'}</button>
|
|
795
|
+
<pre>{JSON.stringify(@tags)}</pre>
|
|
796
|
+
<pre>{params.size}</pre>
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
render(URLTest);
|
|
800
|
+
|
|
801
|
+
const jsButton = container.querySelectorAll('button')[0];
|
|
802
|
+
const tsButton = container.querySelectorAll('button')[1];
|
|
803
|
+
const rippleButton = container.querySelectorAll('button')[2];
|
|
804
|
+
|
|
805
|
+
// Initial state
|
|
806
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('[]');
|
|
807
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('0');
|
|
808
|
+
|
|
809
|
+
// Add tags sequentially
|
|
810
|
+
jsButton.click();
|
|
811
|
+
flushSync();
|
|
812
|
+
|
|
813
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('["javascript"]');
|
|
814
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('1');
|
|
815
|
+
|
|
816
|
+
tsButton.click();
|
|
817
|
+
flushSync();
|
|
818
|
+
|
|
819
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('["javascript","typescript"]');
|
|
820
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('2');
|
|
821
|
+
|
|
822
|
+
rippleButton.click();
|
|
823
|
+
flushSync();
|
|
824
|
+
|
|
825
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('["javascript","typescript","ripple"]');
|
|
826
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('3');
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
it('handles URL-encoded characters correctly', () => {
|
|
830
|
+
component URLTest() {
|
|
831
|
+
const params = new TrackedURLSearchParams('name=John+Doe&email=john%40example.com');
|
|
832
|
+
|
|
833
|
+
<pre>{params.get('name')}</pre>
|
|
834
|
+
<pre>{params.get('email')}</pre>
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
render(URLTest);
|
|
838
|
+
|
|
839
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('John Doe');
|
|
840
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('john@example.com');
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
it('maintains reactivity across multiple components', () => {
|
|
844
|
+
component ParentTest() {
|
|
845
|
+
const params = new TrackedURLSearchParams('count=0');
|
|
846
|
+
|
|
847
|
+
<ChildA params={params} />
|
|
848
|
+
<ChildB params={params} />
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
component ChildA({ params }) {
|
|
852
|
+
<button onClick={() => {
|
|
853
|
+
const current = parseInt(params.get('count') || '0', 10);
|
|
854
|
+
params.set('count', String(current + 1));
|
|
855
|
+
}}>{'increment'}</button>
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
component ChildB({ params }) {
|
|
859
|
+
let count = track(() => params.get('count'));
|
|
860
|
+
|
|
861
|
+
<pre>{@count}</pre>
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
render(ParentTest);
|
|
865
|
+
|
|
866
|
+
const button = container.querySelector('button');
|
|
867
|
+
|
|
868
|
+
// Initial state
|
|
869
|
+
expect(container.querySelector('pre').textContent).toBe('0');
|
|
870
|
+
|
|
871
|
+
// Test increment from child component
|
|
872
|
+
button.click();
|
|
873
|
+
flushSync();
|
|
874
|
+
|
|
875
|
+
expect(container.querySelector('pre').textContent).toBe('1');
|
|
876
|
+
|
|
877
|
+
button.click();
|
|
878
|
+
flushSync();
|
|
879
|
+
|
|
880
|
+
expect(container.querySelector('pre').textContent).toBe('2');
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
it('handles forEach iteration', () => {
|
|
884
|
+
component URLTest() {
|
|
885
|
+
const params = new TrackedURLSearchParams('a=1&b=2&c=3');
|
|
886
|
+
let sum = track(() => {
|
|
887
|
+
let total = 0;
|
|
888
|
+
// Access the params reactively through entries
|
|
889
|
+
for (const [key, value] of params.entries()) {
|
|
890
|
+
total += parseInt(value, 10);
|
|
891
|
+
}
|
|
892
|
+
return total;
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
<button onClick={() => params.append('d', '4')}>{'add d=4'}</button>
|
|
896
|
+
<pre>{@sum}</pre>
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
render(URLTest);
|
|
900
|
+
|
|
901
|
+
const button = container.querySelector('button');
|
|
902
|
+
|
|
903
|
+
// Initial state: 1 + 2 + 3 = 6
|
|
904
|
+
expect(container.querySelector('pre').textContent).toBe('6');
|
|
905
|
+
|
|
906
|
+
// Add d=4, sum should be 10
|
|
907
|
+
button.click();
|
|
908
|
+
flushSync();
|
|
909
|
+
|
|
910
|
+
expect(container.querySelector('pre').textContent).toBe('10');
|
|
911
|
+
});
|
|
912
|
+
});
|