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.
@@ -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
+ });