ripple 0.2.115 → 0.2.118

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/package.json +16 -16
  2. package/src/compiler/index.js +20 -1
  3. package/src/compiler/phases/1-parse/index.js +79 -0
  4. package/src/compiler/phases/3-transform/client/index.js +54 -8
  5. package/src/compiler/phases/3-transform/segments.js +107 -60
  6. package/src/compiler/phases/3-transform/server/index.js +21 -11
  7. package/src/compiler/types/index.d.ts +16 -0
  8. package/src/runtime/index-client.js +19 -185
  9. package/src/runtime/index-server.js +24 -0
  10. package/src/runtime/internal/client/bindings.js +443 -0
  11. package/src/runtime/internal/client/index.js +4 -0
  12. package/src/runtime/internal/client/runtime.js +10 -0
  13. package/src/runtime/internal/client/utils.js +0 -8
  14. package/src/runtime/map.js +11 -1
  15. package/src/runtime/set.js +11 -1
  16. package/tests/client/__snapshots__/for.test.ripple.snap +80 -0
  17. package/tests/client/_etc.test.ripple +5 -0
  18. package/tests/client/array/array.copy-within.test.ripple +120 -0
  19. package/tests/client/array/array.derived.test.ripple +495 -0
  20. package/tests/client/array/array.iteration.test.ripple +115 -0
  21. package/tests/client/array/array.mutations.test.ripple +385 -0
  22. package/tests/client/array/array.static.test.ripple +237 -0
  23. package/tests/client/array/array.to-methods.test.ripple +93 -0
  24. package/tests/client/basic/__snapshots__/basic.attributes.test.ripple.snap +60 -0
  25. package/tests/client/basic/__snapshots__/basic.rendering.test.ripple.snap +106 -0
  26. package/tests/client/basic/__snapshots__/basic.text.test.ripple.snap +49 -0
  27. package/tests/client/basic/basic.attributes.test.ripple +474 -0
  28. package/tests/client/basic/basic.collections.test.ripple +94 -0
  29. package/tests/client/basic/basic.components.test.ripple +225 -0
  30. package/tests/client/basic/basic.errors.test.ripple +126 -0
  31. package/tests/client/basic/basic.events.test.ripple +222 -0
  32. package/tests/client/basic/basic.reactivity.test.ripple +476 -0
  33. package/tests/client/basic/basic.rendering.test.ripple +204 -0
  34. package/tests/client/basic/basic.styling.test.ripple +63 -0
  35. package/tests/client/basic/basic.utilities.test.ripple +25 -0
  36. package/tests/client/boundaries.test.ripple +2 -21
  37. package/tests/client/compiler/__snapshots__/compiler.assignments.test.ripple.snap +12 -0
  38. package/tests/client/compiler/__snapshots__/compiler.typescript.test.ripple.snap +22 -0
  39. package/tests/client/compiler/compiler.assignments.test.ripple +112 -0
  40. package/tests/client/compiler/compiler.attributes.test.ripple +95 -0
  41. package/tests/client/compiler/compiler.basic.test.ripple +203 -0
  42. package/tests/client/compiler/compiler.regex.test.ripple +87 -0
  43. package/tests/client/compiler/compiler.typescript.test.ripple +29 -0
  44. package/tests/client/{__snapshots__/composite.test.ripple.snap → composite/__snapshots__/composite.render.test.ripple.snap} +2 -2
  45. package/tests/client/composite/composite.dynamic-components.test.ripple +100 -0
  46. package/tests/client/composite/composite.generics.test.ripple +211 -0
  47. package/tests/client/composite/composite.props.test.ripple +106 -0
  48. package/tests/client/composite/composite.reactivity.test.ripple +184 -0
  49. package/tests/client/composite/composite.render.test.ripple +84 -0
  50. package/tests/client/computed-properties.test.ripple +2 -21
  51. package/tests/client/context.test.ripple +5 -22
  52. package/tests/client/date.test.ripple +1 -20
  53. package/tests/client/dynamic-elements.test.ripple +16 -24
  54. package/tests/client/for.test.ripple +4 -23
  55. package/tests/client/head.test.ripple +11 -23
  56. package/tests/client/html.test.ripple +1 -20
  57. package/tests/client/input-value.test.ripple +11 -31
  58. package/tests/client/map.test.ripple +82 -20
  59. package/tests/client/media-query.test.ripple +10 -23
  60. package/tests/client/object.test.ripple +5 -24
  61. package/tests/client/portal.test.ripple +2 -19
  62. package/tests/client/ref.test.ripple +8 -26
  63. package/tests/client/set.test.ripple +84 -22
  64. package/tests/client/svg.test.ripple +1 -22
  65. package/tests/client/switch.test.ripple +6 -25
  66. package/tests/client/tracked-expression.test.ripple +2 -21
  67. package/tests/client/typescript-generics.test.ripple +0 -21
  68. package/tests/client/url/url.derived.test.ripple +83 -0
  69. package/tests/client/url/url.parsing.test.ripple +165 -0
  70. package/tests/client/url/url.partial-removal.test.ripple +198 -0
  71. package/tests/client/url/url.reactivity.test.ripple +449 -0
  72. package/tests/client/url/url.serialization.test.ripple +50 -0
  73. package/tests/client/url-search-params/url-search-params.derived.test.ripple +84 -0
  74. package/tests/client/url-search-params/url-search-params.initialization.test.ripple +61 -0
  75. package/tests/client/url-search-params/url-search-params.iteration.test.ripple +153 -0
  76. package/tests/client/url-search-params/url-search-params.mutation.test.ripple +343 -0
  77. package/tests/client/url-search-params/url-search-params.retrieval.test.ripple +160 -0
  78. package/tests/client/url-search-params/url-search-params.serialization.test.ripple +53 -0
  79. package/tests/client/url-search-params/url-search-params.tracked-url.test.ripple +55 -0
  80. package/tests/client.d.ts +12 -0
  81. package/tests/server/if.test.ripple +66 -0
  82. package/tests/setup-client.js +28 -0
  83. package/tsconfig.json +4 -2
  84. package/types/index.d.ts +92 -46
  85. package/LICENSE +0 -21
  86. package/tests/client/__snapshots__/basic.test.ripple.snap +0 -117
  87. package/tests/client/__snapshots__/compiler.test.ripple.snap +0 -33
  88. package/tests/client/array.test.ripple +0 -1455
  89. package/tests/client/basic.test.ripple +0 -1892
  90. package/tests/client/compiler.test.ripple +0 -541
  91. package/tests/client/composite.test.ripple +0 -692
  92. package/tests/client/url-search-params.test.ripple +0 -912
  93. package/tests/client/url.test.ripple +0 -954
@@ -0,0 +1,50 @@
1
+ import { flushSync, TrackedURL } from 'ripple';
2
+
3
+ describe('TrackedURL > serialization', () => {
4
+ it('handles toString method', () => {
5
+ component URLTest() {
6
+ const url = new TrackedURL('https://example.com/path?foo=bar#section');
7
+
8
+ <button onClick={() => url.pathname = '/newpath'}>{'Change Pathname'}</button>
9
+ <pre>{url.toString()}</pre>
10
+ }
11
+
12
+ render(URLTest);
13
+
14
+ const button = container.querySelector('button');
15
+
16
+ // Initial state
17
+ expect(container.querySelector('pre').textContent).toBe('https://example.com/path?foo=bar#section');
18
+
19
+ // Change pathname
20
+ button.click();
21
+ flushSync();
22
+
23
+ expect(container.querySelector('pre').textContent).toBe('https://example.com/newpath?foo=bar#section');
24
+ });
25
+
26
+ it('handles toJSON method', () => {
27
+ component URLTest() {
28
+ const url = new TrackedURL('https://example.com/path?foo=bar');
29
+
30
+ <button onClick={() => url.pathname = '/api'}>{'Change Pathname'}</button>
31
+ <pre>{url.toJSON()}</pre>
32
+ <pre>{JSON.stringify({ url: url.toJSON() })}</pre>
33
+ }
34
+
35
+ render(URLTest);
36
+
37
+ const button = container.querySelector('button');
38
+
39
+ // Initial state
40
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/path?foo=bar');
41
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('{"url":"https://example.com/path?foo=bar"}');
42
+
43
+ // Change pathname
44
+ button.click();
45
+ flushSync();
46
+
47
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/api?foo=bar');
48
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('{"url":"https://example.com/api?foo=bar"}');
49
+ });
50
+ });
@@ -0,0 +1,84 @@
1
+ import { flushSync, track, TrackedURL, TrackedURLSearchParams } from 'ripple';
2
+
3
+ describe('TrackedURLSearchParams > derived', () => {
4
+ it('handles reactive computed properties based on search params', () => {
5
+ component URLTest() {
6
+ const params = new TrackedURLSearchParams('page=1&limit=10');
7
+ let page = track(() => parseInt(params.get('page') || '1', 10));
8
+ let limit = track(() => parseInt(params.get('limit') || '10', 10));
9
+ let offset = track(() => (@page - 1) * @limit);
10
+
11
+ <button onClick={() => params.set('page', '2')}>{'next page'}</button>
12
+ <button onClick={() => params.set('page', '1')}>{'first page'}</button>
13
+ <pre>{`Page: ${@page}`}</pre>
14
+ <pre>{`Limit: ${@limit}`}</pre>
15
+ <pre>{`Offset: ${@offset}`}</pre>
16
+ }
17
+
18
+ render(URLTest);
19
+
20
+ const nextButton = container.querySelectorAll('button')[0];
21
+ const firstButton = container.querySelectorAll('button')[1];
22
+
23
+ // Initial state
24
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('Page: 1');
25
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('Limit: 10');
26
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('Offset: 0');
27
+
28
+ // Test next page
29
+ nextButton.click();
30
+ flushSync();
31
+
32
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('Page: 2');
33
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('Limit: 10');
34
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('Offset: 10');
35
+
36
+ // Test first page
37
+ firstButton.click();
38
+ flushSync();
39
+
40
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('Page: 1');
41
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('Limit: 10');
42
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('Offset: 0');
43
+ });
44
+
45
+ it('maintains reactivity across multiple components', () => {
46
+ component ParentTest() {
47
+ const params = new TrackedURLSearchParams('count=0');
48
+
49
+ <ChildA params={params} />
50
+ <ChildB params={params} />
51
+ }
52
+
53
+ component ChildA({ params }: { params: TrackedURLSearchParams }) {
54
+ <button onClick={() => {
55
+ const current = parseInt(params.get('count') || '0', 10);
56
+ params.set('count', String(current + 1));
57
+ }}>{'increment'}</button>
58
+ }
59
+
60
+ component ChildB({ params }: { params: TrackedURLSearchParams }) {
61
+ let count = track(() => params.get('count'));
62
+
63
+ <pre>{@count}</pre>
64
+ }
65
+
66
+ render(ParentTest);
67
+
68
+ const button = container.querySelector('button');
69
+
70
+ // Initial state
71
+ expect(container.querySelector('pre').textContent).toBe('0');
72
+
73
+ // Test increment from child component
74
+ button.click();
75
+ flushSync();
76
+
77
+ expect(container.querySelector('pre').textContent).toBe('1');
78
+
79
+ button.click();
80
+ flushSync();
81
+
82
+ expect(container.querySelector('pre').textContent).toBe('2');
83
+ });
84
+ });
@@ -0,0 +1,61 @@
1
+ import { TrackedURLSearchParams } from 'ripple';
2
+
3
+ describe('TrackedURLSearchParams > initialization', () => {
4
+ it('creates empty URLSearchParams with reactivity', () => {
5
+ component URLTest() {
6
+ const params = new TrackedURLSearchParams();
7
+
8
+ <pre>{params.toString()}</pre>
9
+ <pre>{params.size}</pre>
10
+ }
11
+
12
+ render(URLTest);
13
+
14
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('');
15
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('0');
16
+ });
17
+
18
+ it('creates URLSearchParams from string with reactivity', () => {
19
+ component URLTest() {
20
+ const params = new TrackedURLSearchParams('foo=bar&baz=qux');
21
+
22
+ <pre>{params.toString()}</pre>
23
+ <pre>{params.size}</pre>
24
+ <pre>{params.get('foo')}</pre>
25
+ }
26
+
27
+ render(URLTest);
28
+
29
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=bar&baz=qux');
30
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('2');
31
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('bar');
32
+ });
33
+
34
+ it('creates URLSearchParams from object with reactivity', () => {
35
+ component URLTest() {
36
+ const params = new TrackedURLSearchParams({ foo: 'bar', baz: 'qux' });
37
+
38
+ <pre>{params.toString()}</pre>
39
+ <pre>{params.size}</pre>
40
+ }
41
+
42
+ render(URLTest);
43
+
44
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=bar&baz=qux');
45
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('2');
46
+ });
47
+
48
+ it('handles URL-encoded characters correctly', () => {
49
+ component URLTest() {
50
+ const params = new TrackedURLSearchParams('name=John+Doe&email=john%40example.com');
51
+
52
+ <pre>{params.get('name')}</pre>
53
+ <pre>{params.get('email')}</pre>
54
+ }
55
+
56
+ render(URLTest);
57
+
58
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('John Doe');
59
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('john@example.com');
60
+ });
61
+ });
@@ -0,0 +1,153 @@
1
+ import { flushSync, track, TrackedURLSearchParams } from 'ripple';
2
+
3
+ describe('TrackedURLSearchParams > iteration', () => {
4
+ it('handles keys method with reactivity', () => {
5
+ component URLTest() {
6
+ const params = new TrackedURLSearchParams('foo=bar&baz=qux');
7
+ let keys = track(() => Array.from(params.keys()));
8
+
9
+ <button onClick={() => params.append('new', 'value')}>{'add param'}</button>
10
+ <pre>{JSON.stringify(@keys)}</pre>
11
+ }
12
+
13
+ render(URLTest);
14
+
15
+ const button = container.querySelector('button');
16
+
17
+ // Initial state
18
+ expect(container.querySelector('pre').textContent).toBe('["foo","baz"]');
19
+
20
+ // Test add
21
+ button.click();
22
+ flushSync();
23
+
24
+ expect(container.querySelector('pre').textContent).toBe('["foo","baz","new"]');
25
+ });
26
+
27
+ it('handles values method with reactivity', () => {
28
+ component URLTest() {
29
+ const params = new TrackedURLSearchParams('foo=bar&baz=qux');
30
+ let values = track(() => Array.from(params.values()));
31
+
32
+ <button onClick={() => params.set('foo', 'updated')}>{'update foo'}</button>
33
+ <pre>{JSON.stringify(@values)}</pre>
34
+ }
35
+
36
+ render(URLTest);
37
+
38
+ const button = container.querySelector('button');
39
+
40
+ // Initial state
41
+ expect(container.querySelector('pre').textContent).toBe('["bar","qux"]');
42
+
43
+ // Test update
44
+ button.click();
45
+ flushSync();
46
+
47
+ expect(container.querySelector('pre').textContent).toBe('["updated","qux"]');
48
+ });
49
+
50
+ it('handles entries method with reactivity', () => {
51
+ component URLTest() {
52
+ const params = new TrackedURLSearchParams('foo=bar&baz=qux');
53
+ let entries = track(() => Array.from(params.entries()));
54
+
55
+ <button onClick={() => params.append('new', 'value')}>{'add param'}</button>
56
+ <pre>{JSON.stringify(@entries)}</pre>
57
+ }
58
+
59
+ render(URLTest);
60
+
61
+ const button = container.querySelector('button');
62
+
63
+ // Initial state
64
+ expect(container.querySelector('pre').textContent).toBe('[["foo","bar"],["baz","qux"]]');
65
+
66
+ // Test add
67
+ button.click();
68
+ flushSync();
69
+
70
+ expect(container.querySelector('pre').textContent).toBe('[["foo","bar"],["baz","qux"],["new","value"]]');
71
+ });
72
+
73
+ it('handles Symbol.iterator with reactivity', () => {
74
+ component URLTest() {
75
+ const params = new TrackedURLSearchParams('foo=bar&baz=qux');
76
+ let entries = track(() => Array.from(params));
77
+
78
+ <button onClick={() => params.delete('foo')}>{'delete foo'}</button>
79
+ <pre>{JSON.stringify(@entries)}</pre>
80
+ }
81
+
82
+ render(URLTest);
83
+
84
+ const button = container.querySelector('button');
85
+
86
+ // Initial state
87
+ expect(container.querySelector('pre').textContent).toBe('[["foo","bar"],["baz","qux"]]');
88
+
89
+ // Test delete
90
+ button.click();
91
+ flushSync();
92
+
93
+ expect(container.querySelector('pre').textContent).toBe('[["baz","qux"]]');
94
+ });
95
+
96
+ it('handles iteration with for...of', () => {
97
+ component URLTest() {
98
+ const params = new TrackedURLSearchParams('foo=bar&baz=qux');
99
+
100
+ <button onClick={() => params.append('new', 'value')}>{'add param'}</button>
101
+
102
+ for (const [key, value] of params) {
103
+ <pre>{`${key}=${value}`}</pre>
104
+ }
105
+ }
106
+
107
+ render(URLTest);
108
+
109
+ const button = container.querySelector('button');
110
+
111
+ // Initial state
112
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=bar');
113
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('baz=qux');
114
+
115
+ // Test add
116
+ button.click();
117
+ flushSync();
118
+
119
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=bar');
120
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('baz=qux');
121
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('new=value');
122
+ });
123
+
124
+ it('handles forEach iteration', () => {
125
+ component URLTest() {
126
+ const params = new TrackedURLSearchParams('a=1&b=2&c=3');
127
+ let sum = track(() => {
128
+ let total = 0;
129
+ // Access the params reactively through entries
130
+ for (const [key, value] of params.entries()) {
131
+ total += parseInt(value, 10);
132
+ }
133
+ return total;
134
+ });
135
+
136
+ <button onClick={() => params.append('d', '4')}>{'add d=4'}</button>
137
+ <pre>{@sum}</pre>
138
+ }
139
+
140
+ render(URLTest);
141
+
142
+ const button = container.querySelector('button');
143
+
144
+ // Initial state: 1 + 2 + 3 = 6
145
+ expect(container.querySelector('pre').textContent).toBe('6');
146
+
147
+ // Add d=4, sum should be 10
148
+ button.click();
149
+ flushSync();
150
+
151
+ expect(container.querySelector('pre').textContent).toBe('10');
152
+ });
153
+ });
@@ -0,0 +1,343 @@
1
+ import { flushSync, track, TrackedURL, TrackedURLSearchParams } from 'ripple';
2
+
3
+ describe('TrackedURLSearchParams > mutation', () => {
4
+ it('handles append operation with reactivity', () => {
5
+ component URLTest() {
6
+ const params = new TrackedURLSearchParams('foo=bar');
7
+
8
+ <button onClick={() => params.append('baz', 'qux')}>{'append'}</button>
9
+ <pre>{params.toString()}</pre>
10
+ <pre>{params.size}</pre>
11
+ }
12
+
13
+ render(URLTest);
14
+
15
+ const button = container.querySelector('button');
16
+
17
+ // Initial state
18
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=bar');
19
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('1');
20
+
21
+ // Test append
22
+ button.click();
23
+ flushSync();
24
+
25
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=bar&baz=qux');
26
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('2');
27
+ });
28
+
29
+ it('handles append with multiple values for same key', () => {
30
+ component URLTest() {
31
+ const params = new TrackedURLSearchParams('foo=bar');
32
+ let allFoo = track(() => params.getAll('foo'));
33
+
34
+ <button onClick={() => params.append('foo', 'baz')}>{'append foo'}</button>
35
+ <pre>{params.toString()}</pre>
36
+ <pre>{JSON.stringify(@allFoo)}</pre>
37
+ }
38
+
39
+ render(URLTest);
40
+
41
+ const button = container.querySelector('button');
42
+
43
+ // Initial state
44
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=bar');
45
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('["bar"]');
46
+
47
+ // Test append
48
+ button.click();
49
+ flushSync();
50
+
51
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=bar&foo=baz');
52
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('["bar","baz"]');
53
+ });
54
+
55
+ it('handles delete operation with reactivity', () => {
56
+ component URLTest() {
57
+ const params = new TrackedURLSearchParams('foo=bar&baz=qux');
58
+
59
+ <button onClick={() => params.delete('foo')}>{'delete foo'}</button>
60
+ <pre>{params.toString()}</pre>
61
+ <pre>{params.size}</pre>
62
+ <pre>{params.has('foo').toString()}</pre>
63
+ }
64
+
65
+ render(URLTest);
66
+
67
+ const button = container.querySelector('button');
68
+
69
+ // Initial state
70
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=bar&baz=qux');
71
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('2');
72
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('true');
73
+
74
+ // Test delete
75
+ button.click();
76
+ flushSync();
77
+
78
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('baz=qux');
79
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('1');
80
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('false');
81
+ });
82
+
83
+ it('handles delete with specific value', () => {
84
+ component URLTest() {
85
+ const params = new TrackedURLSearchParams('foo=bar&foo=baz&foo=qux');
86
+
87
+ <button onClick={() => params.delete('foo', 'baz')}>{'delete foo=baz'}</button>
88
+ <pre>{params.toString()}</pre>
89
+ <pre>{params.size}</pre>
90
+ }
91
+
92
+ render(URLTest);
93
+
94
+ const button = container.querySelector('button');
95
+
96
+ // Initial state
97
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=bar&foo=baz&foo=qux');
98
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('3');
99
+
100
+ // Test delete specific value
101
+ button.click();
102
+ flushSync();
103
+
104
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=bar&foo=qux');
105
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('2');
106
+ });
107
+
108
+ it('handles delete when key does not exist', () => {
109
+ component URLTest() {
110
+ const params = new TrackedURLSearchParams('foo=bar');
111
+ let reactiveSize = track(() => params.size);
112
+
113
+ <button onClick={() => params.delete('nonexistent')}>{'delete nonexistent'}</button>
114
+ <pre>{params.toString()}</pre>
115
+ <pre>{@reactiveSize}</pre>
116
+ }
117
+
118
+ render(URLTest);
119
+
120
+ const button = container.querySelector('button');
121
+
122
+ // Initial state
123
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=bar');
124
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('1');
125
+
126
+ // Test delete nonexistent - should not trigger reactivity
127
+ button.click();
128
+ flushSync();
129
+
130
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=bar');
131
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('1');
132
+ });
133
+
134
+ it('handles set operation with reactivity', () => {
135
+ component URLTest() {
136
+ const params = new TrackedURLSearchParams('foo=bar');
137
+
138
+ <button onClick={() => params.set('foo', 'updated')}>{'update foo'}</button>
139
+ <button onClick={() => params.set('baz', 'qux')}>{'add baz'}</button>
140
+ <pre>{params.toString()}</pre>
141
+ <pre>{params.size}</pre>
142
+ }
143
+
144
+ render(URLTest);
145
+
146
+ const updateButton = container.querySelectorAll('button')[0];
147
+ const addButton = container.querySelectorAll('button')[1];
148
+
149
+ // Initial state
150
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=bar');
151
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('1');
152
+
153
+ // Test update
154
+ updateButton.click();
155
+ flushSync();
156
+
157
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=updated');
158
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('1');
159
+
160
+ // Test add new key
161
+ addButton.click();
162
+ flushSync();
163
+
164
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=updated&baz=qux');
165
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('2');
166
+ });
167
+
168
+ it('handles set with multiple existing values', () => {
169
+ component URLTest() {
170
+ const params = new TrackedURLSearchParams('foo=bar&foo=baz&foo=qux');
171
+ let allFoo = track(() => params.getAll('foo'));
172
+
173
+ <button onClick={() => params.set('foo', 'single')}>{'set foo'}</button>
174
+ <pre>{params.toString()}</pre>
175
+ <pre>{JSON.stringify(@allFoo)}</pre>
176
+ }
177
+
178
+ render(URLTest);
179
+
180
+ const button = container.querySelector('button');
181
+
182
+ // Initial state
183
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=bar&foo=baz&foo=qux');
184
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('["bar","baz","qux"]');
185
+
186
+ // Test set - should replace all values
187
+ button.click();
188
+ flushSync();
189
+
190
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=single');
191
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('["single"]');
192
+ });
193
+
194
+ it('handles set when value is the same', () => {
195
+ component URLTest() {
196
+ const params = new TrackedURLSearchParams('foo=bar');
197
+ let reactiveString = track(() => params.toString());
198
+
199
+ <button onClick={() => params.set('foo', 'bar')}>{'set same value'}</button>
200
+ <pre>{@reactiveString}</pre>
201
+ <pre>{params.size}</pre>
202
+ }
203
+
204
+ render(URLTest);
205
+
206
+ const button = container.querySelector('button');
207
+
208
+ // Test set same value - should not trigger reactivity changes
209
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=bar');
210
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('1');
211
+
212
+ button.click();
213
+ flushSync();
214
+
215
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=bar');
216
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('1');
217
+ });
218
+
219
+ it('handles sort operation with reactivity', () => {
220
+ component URLTest() {
221
+ const params = new TrackedURLSearchParams('z=last&a=first&m=middle');
222
+
223
+ <button onClick={() => params.sort()}>{'sort'}</button>
224
+ <pre>{params.toString()}</pre>
225
+ }
226
+
227
+ render(URLTest);
228
+
229
+ const button = container.querySelector('button');
230
+
231
+ // Initial state
232
+ expect(container.querySelector('pre').textContent).toBe('z=last&a=first&m=middle');
233
+
234
+ // Test sort
235
+ button.click();
236
+ flushSync();
237
+
238
+ expect(container.querySelector('pre').textContent).toBe('a=first&m=middle&z=last');
239
+ });
240
+
241
+ it('handles clearing all params via delete', () => {
242
+ component URLTest() {
243
+ const url = new TrackedURL('https://example.com?foo=bar&baz=qux');
244
+ const params = url.searchParams;
245
+
246
+ <button onClick={() => {
247
+ params.delete('foo');
248
+ params.delete('baz');
249
+ }}>{'clear all'}</button>
250
+ <pre>{url.href}</pre>
251
+ <pre>{params.size}</pre>
252
+ }
253
+
254
+ render(URLTest);
255
+
256
+ const button = container.querySelector('button');
257
+
258
+ // Initial state
259
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/?foo=bar&baz=qux');
260
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('2');
261
+
262
+ // Test clear all
263
+ button.click();
264
+ flushSync();
265
+
266
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/');
267
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('0');
268
+ });
269
+
270
+ it('handles multiple operations in sequence', () => {
271
+ component URLTest() {
272
+ const params = new TrackedURLSearchParams();
273
+
274
+ <button onClick={() => {
275
+ params.append('a', '1');
276
+ params.append('b', '2');
277
+ params.set('a', '10');
278
+ params.delete('b');
279
+ params.append('c', '3');
280
+ params.sort();
281
+ }}>{'complex operations'}</button>
282
+ <pre>{params.toString()}</pre>
283
+ <pre>{params.size}</pre>
284
+ }
285
+
286
+ render(URLTest);
287
+
288
+ const button = container.querySelector('button');
289
+
290
+ // Initial state
291
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('');
292
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('0');
293
+
294
+ // Test complex operations
295
+ button.click();
296
+ flushSync();
297
+
298
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('a=10&c=3');
299
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('2');
300
+ });
301
+
302
+ it('handles duplicate keys with different values', () => {
303
+ component URLTest() {
304
+ const params = new TrackedURLSearchParams();
305
+ let tags = track(() => params.getAll('tag'));
306
+
307
+ <button onClick={() => params.append('tag', 'javascript')}>{'add js'}</button>
308
+ <button onClick={() => params.append('tag', 'typescript')}>{'add ts'}</button>
309
+ <button onClick={() => params.append('tag', 'ripple')}>{'add ripple'}</button>
310
+ <pre>{JSON.stringify(@tags)}</pre>
311
+ <pre>{params.size}</pre>
312
+ }
313
+
314
+ render(URLTest);
315
+
316
+ const jsButton = container.querySelectorAll('button')[0];
317
+ const tsButton = container.querySelectorAll('button')[1];
318
+ const rippleButton = container.querySelectorAll('button')[2];
319
+
320
+ // Initial state
321
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('[]');
322
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('0');
323
+
324
+ // Add tags sequentially
325
+ jsButton.click();
326
+ flushSync();
327
+
328
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('["javascript"]');
329
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('1');
330
+
331
+ tsButton.click();
332
+ flushSync();
333
+
334
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('["javascript","typescript"]');
335
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('2');
336
+
337
+ rippleButton.click();
338
+ flushSync();
339
+
340
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('["javascript","typescript","ripple"]');
341
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('3');
342
+ });
343
+ });