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,198 @@
1
+ import { flushSync, TrackedURL } from 'ripple';
2
+
3
+ describe('TrackedURL > partials/removal', () => {
4
+ it('handles URL with no port specified', () => {
5
+ component URLTest() {
6
+ const url = new TrackedURL('https://example.com/path');
7
+
8
+ <pre>{url.port}</pre>
9
+ <pre>{url.host}</pre>
10
+ <button onClick={() => url.port = '8080'}>{'Add Port'}</button>
11
+ }
12
+
13
+ render(URLTest);
14
+
15
+ const button = container.querySelector('button');
16
+
17
+ // Initial state - default ports are empty strings
18
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('');
19
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('example.com');
20
+
21
+ // Add port
22
+ button.click();
23
+ flushSync();
24
+
25
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('8080');
26
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('example.com:8080');
27
+ });
28
+
29
+ it('handles URL with no search params', () => {
30
+ component URLTest() {
31
+ const url = new TrackedURL('https://example.com/path');
32
+
33
+ <pre>{url.search}</pre>
34
+ <pre>{url.searchParams.size}</pre>
35
+ <button onClick={() => url.searchParams.append('foo', 'bar')}>{'Add Param'}</button>
36
+ }
37
+
38
+ render(URLTest);
39
+
40
+ const button = container.querySelector('button');
41
+
42
+ // Initial state
43
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('');
44
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('0');
45
+
46
+ // Add param
47
+ button.click();
48
+ flushSync();
49
+
50
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('?foo=bar');
51
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('1');
52
+ });
53
+
54
+ it('handles URL with no hash', () => {
55
+ component URLTest() {
56
+ const url = new TrackedURL('https://example.com/path');
57
+
58
+ <pre>{url.hash}</pre>
59
+ <button onClick={() => url.hash = '#section'}>{'Add Hash'}</button>
60
+ }
61
+
62
+ render(URLTest);
63
+
64
+ const button = container.querySelector('button');
65
+
66
+ // Initial state
67
+ expect(container.querySelector('pre').textContent).toBe('');
68
+
69
+ // Add hash
70
+ button.click();
71
+ flushSync();
72
+
73
+ expect(container.querySelector('pre').textContent).toBe('#section');
74
+ });
75
+
76
+ it('handles removing port by setting empty string', () => {
77
+ component URLTest() {
78
+ const url = new TrackedURL('https://example.com:8080/path');
79
+
80
+ <button onClick={() => url.port = ''}>{'Remove Port'}</button>
81
+ <pre>{url.href}</pre>
82
+ <pre>{url.port}</pre>
83
+ }
84
+
85
+ render(URLTest);
86
+
87
+ const button = container.querySelector('button');
88
+
89
+ // Initial state
90
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com:8080/path');
91
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('8080');
92
+
93
+ // Remove port
94
+ button.click();
95
+ flushSync();
96
+
97
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/path');
98
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('');
99
+ });
100
+
101
+ it('handles removing hash by setting empty string', () => {
102
+ component URLTest() {
103
+ const url = new TrackedURL('https://example.com/path#section');
104
+
105
+ <button onClick={() => url.hash = ''}>{'Remove Hash'}</button>
106
+ <pre>{url.href}</pre>
107
+ <pre>{url.hash}</pre>
108
+ }
109
+
110
+ render(URLTest);
111
+
112
+ const button = container.querySelector('button');
113
+
114
+ // Initial state
115
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/path#section');
116
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('#section');
117
+
118
+ // Remove hash
119
+ button.click();
120
+ flushSync();
121
+
122
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/path');
123
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('');
124
+ });
125
+
126
+ it('handles removing search by setting empty string', () => {
127
+ component URLTest() {
128
+ const url = new TrackedURL('https://example.com/path?foo=bar');
129
+
130
+ <button onClick={() => url.search = ''}>{'Remove Search'}</button>
131
+ <pre>{url.href}</pre>
132
+ <pre>{url.search}</pre>
133
+ <pre>{url.searchParams.size}</pre>
134
+ }
135
+
136
+ render(URLTest);
137
+
138
+ const button = container.querySelector('button');
139
+
140
+ // Initial state
141
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/path?foo=bar');
142
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('?foo=bar');
143
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('1');
144
+
145
+ // Remove search
146
+ button.click();
147
+ flushSync();
148
+
149
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/path');
150
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('');
151
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('0');
152
+ });
153
+
154
+ it('handles hash without leading # character', () => {
155
+ component URLTest() {
156
+ const url = new TrackedURL('https://example.com/path');
157
+
158
+ <button onClick={() => url.hash = 'section'}>{'Set Hash'}</button>
159
+ <pre>{url.hash}</pre>
160
+ <pre>{url.href}</pre>
161
+ }
162
+
163
+ render(URLTest);
164
+
165
+ const button = container.querySelector('button');
166
+
167
+ // Initial state
168
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('');
169
+
170
+ // Set hash
171
+ button.click();
172
+ flushSync();
173
+
174
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('#section');
175
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('https://example.com/path#section');
176
+ });
177
+
178
+ it('handles search without leading ? character', () => {
179
+ component URLTest() {
180
+ const url = new TrackedURL('https://example.com/path');
181
+
182
+ <button onClick={() => url.search = 'foo=bar'}>{'Set Search'}</button>
183
+ <pre>{url.search}</pre>
184
+ <pre>{url.href}</pre>
185
+ }
186
+
187
+ render(URLTest);
188
+
189
+ const button = container.querySelector('button');
190
+
191
+ // Set search
192
+ button.click();
193
+ flushSync();
194
+
195
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=bar');
196
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('https://example.com/path?foo=bar');
197
+ });
198
+ });
@@ -0,0 +1,449 @@
1
+ import { flushSync, TrackedURL } from 'ripple';
2
+
3
+ describe('TrackedURL > reactivity', () => {
4
+ it('handles protocol changes with reactivity', () => {
5
+ component URLTest() {
6
+ const url = new TrackedURL('https://example.com/path');
7
+
8
+ <button onClick={() => url.protocol = 'http:'}>{'Change Protocol'}</button>
9
+ <pre>{url.href}</pre>
10
+ <pre>{url.protocol}</pre>
11
+ <pre>{url.origin}</pre>
12
+ }
13
+
14
+ render(URLTest);
15
+
16
+ const button = container.querySelector('button');
17
+
18
+ // Initial state
19
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/path');
20
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('https:');
21
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('https://example.com');
22
+
23
+ // Change protocol
24
+ button.click();
25
+ flushSync();
26
+
27
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('http://example.com/path');
28
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('http:');
29
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('http://example.com');
30
+ });
31
+
32
+ it('handles hostname changes with reactivity', () => {
33
+ component URLTest() {
34
+ const url = new TrackedURL('https://example.com/path');
35
+
36
+ <button onClick={() => url.hostname = 'newdomain.com'}>{'Change Hostname'}</button>
37
+ <pre>{url.href}</pre>
38
+ <pre>{url.hostname}</pre>
39
+ <pre>{url.host}</pre>
40
+ }
41
+
42
+ render(URLTest);
43
+
44
+ const button = container.querySelector('button');
45
+
46
+ // Initial state
47
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/path');
48
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('example.com');
49
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('example.com');
50
+
51
+ // Change hostname
52
+ button.click();
53
+ flushSync();
54
+
55
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('https://newdomain.com/path');
56
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('newdomain.com');
57
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('newdomain.com');
58
+ });
59
+
60
+ it('handles port changes with reactivity', () => {
61
+ component URLTest() {
62
+ const url = new TrackedURL('https://example.com:8080/path');
63
+
64
+ <button onClick={() => url.port = '9090'}>{'Change Port'}</button>
65
+ <pre>{url.href}</pre>
66
+ <pre>{url.port}</pre>
67
+ <pre>{url.host}</pre>
68
+ }
69
+
70
+ render(URLTest);
71
+
72
+ const button = container.querySelector('button');
73
+
74
+ // Initial state
75
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com:8080/path');
76
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('8080');
77
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('example.com:8080');
78
+
79
+ // Change port
80
+ button.click();
81
+ flushSync();
82
+
83
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com:9090/path');
84
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('9090');
85
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('example.com:9090');
86
+ });
87
+
88
+ it('handles host changes with reactivity', () => {
89
+ component URLTest() {
90
+ const url = new TrackedURL('https://example.com:8080/path');
91
+
92
+ <button onClick={() => url.host = 'newdomain.com:9090'}>{'Change Host'}</button>
93
+ <pre>{url.href}</pre>
94
+ <pre>{url.host}</pre>
95
+ <pre>{url.hostname}</pre>
96
+ <pre>{url.port}</pre>
97
+ }
98
+
99
+ render(URLTest);
100
+
101
+ const button = container.querySelector('button');
102
+
103
+ // Initial state
104
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com:8080/path');
105
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('example.com:8080');
106
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('example.com');
107
+ expect(container.querySelectorAll('pre')[3].textContent).toBe('8080');
108
+
109
+ // Change host
110
+ button.click();
111
+ flushSync();
112
+
113
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('https://newdomain.com:9090/path');
114
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('newdomain.com:9090');
115
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('newdomain.com');
116
+ expect(container.querySelectorAll('pre')[3].textContent).toBe('9090');
117
+ });
118
+
119
+ it('handles pathname changes with reactivity', () => {
120
+ component URLTest() {
121
+ const url = new TrackedURL('https://example.com/old-path');
122
+
123
+ <button onClick={() => url.pathname = '/new-path'}>{'Change Pathname'}</button>
124
+ <pre>{url.href}</pre>
125
+ <pre>{url.pathname}</pre>
126
+ }
127
+
128
+ render(URLTest);
129
+
130
+ const button = container.querySelector('button');
131
+
132
+ // Initial state
133
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/old-path');
134
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('/old-path');
135
+
136
+ // Change pathname
137
+ button.click();
138
+ flushSync();
139
+
140
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/new-path');
141
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('/new-path');
142
+ });
143
+
144
+ it('handles search changes with reactivity', () => {
145
+ component URLTest() {
146
+ const url = new TrackedURL('https://example.com/path?foo=bar');
147
+
148
+ <button onClick={() => url.search = '?baz=qux'}>{'Change Search'}</button>
149
+ <pre>{url.href}</pre>
150
+ <pre>{url.search}</pre>
151
+ }
152
+
153
+ render(URLTest);
154
+
155
+ const button = container.querySelector('button');
156
+
157
+ // Initial state
158
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/path?foo=bar');
159
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('?foo=bar');
160
+
161
+ // Change search
162
+ button.click();
163
+ flushSync();
164
+
165
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/path?baz=qux');
166
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('?baz=qux');
167
+ });
168
+
169
+ it('handles hash changes with reactivity', () => {
170
+ component URLTest() {
171
+ const url = new TrackedURL('https://example.com/path#section1');
172
+
173
+ <button onClick={() => url.hash = '#section2'}>{'Change Hash'}</button>
174
+ <pre>{url.href}</pre>
175
+ <pre>{url.hash}</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('https://example.com/path#section1');
184
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('#section1');
185
+
186
+ // Change hash
187
+ button.click();
188
+ flushSync();
189
+
190
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/path#section2');
191
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('#section2');
192
+ });
193
+
194
+ it('handles username changes with reactivity', () => {
195
+ component URLTest() {
196
+ const url = new TrackedURL('https://user:pass@example.com/path');
197
+
198
+ <button onClick={() => url.username = 'newuser'}>{'Change Username'}</button>
199
+ <pre>{url.href}</pre>
200
+ <pre>{url.username}</pre>
201
+ }
202
+
203
+ render(URLTest);
204
+
205
+ const button = container.querySelector('button');
206
+
207
+ // Initial state
208
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('https://user:pass@example.com/path');
209
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('user');
210
+
211
+ // Change username
212
+ button.click();
213
+ flushSync();
214
+
215
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('https://newuser:pass@example.com/path');
216
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('newuser');
217
+ });
218
+
219
+ it('handles password changes with reactivity', () => {
220
+ component URLTest() {
221
+ const url = new TrackedURL('https://user:pass@example.com/path');
222
+
223
+ <button onClick={() => url.password = 'newpass'}>{'Change Password'}</button>
224
+ <pre>{url.href}</pre>
225
+ <pre>{url.password}</pre>
226
+ }
227
+
228
+ render(URLTest);
229
+
230
+ const button = container.querySelector('button');
231
+
232
+ // Initial state
233
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('https://user:pass@example.com/path');
234
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('pass');
235
+
236
+ // Change password
237
+ button.click();
238
+ flushSync();
239
+
240
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('https://user:newpass@example.com/path');
241
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('newpass');
242
+ });
243
+
244
+ it('handles href changes with reactivity', () => {
245
+ component URLTest() {
246
+ const url = new TrackedURL('https://example.com/path?foo=bar#section');
247
+
248
+ <button onClick={() => url.href = 'https://newdomain.com:9090/newpath?baz=qux#newsection'}>{'Change Href'}</button>
249
+ <pre>{url.href}</pre>
250
+ <pre>{url.protocol}</pre>
251
+ <pre>{url.hostname}</pre>
252
+ <pre>{url.port}</pre>
253
+ <pre>{url.pathname}</pre>
254
+ <pre>{url.search}</pre>
255
+ <pre>{url.hash}</pre>
256
+ }
257
+
258
+ render(URLTest);
259
+
260
+ const button = container.querySelector('button');
261
+
262
+ // Initial state
263
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/path?foo=bar#section');
264
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('https:');
265
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('example.com');
266
+ expect(container.querySelectorAll('pre')[3].textContent).toBe('');
267
+ expect(container.querySelectorAll('pre')[4].textContent).toBe('/path');
268
+ expect(container.querySelectorAll('pre')[5].textContent).toBe('?foo=bar');
269
+ expect(container.querySelectorAll('pre')[6].textContent).toBe('#section');
270
+
271
+ // Change href
272
+ button.click();
273
+ flushSync();
274
+
275
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('https://newdomain.com:9090/newpath?baz=qux#newsection');
276
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('https:');
277
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('newdomain.com');
278
+ expect(container.querySelectorAll('pre')[3].textContent).toBe('9090');
279
+ expect(container.querySelectorAll('pre')[4].textContent).toBe('/newpath');
280
+ expect(container.querySelectorAll('pre')[5].textContent).toBe('?baz=qux');
281
+ expect(container.querySelectorAll('pre')[6].textContent).toBe('#newsection');
282
+ });
283
+
284
+ it('handles origin property reactivity', () => {
285
+ component URLTest() {
286
+ const url = new TrackedURL('https://example.com:8080/path');
287
+
288
+ <button onClick={() => url.protocol = 'http:'}>{'Change Protocol'}</button>
289
+ <button onClick={() => url.hostname = 'newdomain.com'}>{'Change Hostname'}</button>
290
+ <button onClick={() => url.port = '9090'}>{'Change Port'}</button>
291
+ <pre>{url.origin}</pre>
292
+ }
293
+
294
+ render(URLTest);
295
+
296
+ const buttons = container.querySelectorAll('button');
297
+
298
+ // Initial state
299
+ expect(container.querySelector('pre').textContent).toBe('https://example.com:8080');
300
+
301
+ // Change protocol
302
+ buttons[0].click();
303
+ flushSync();
304
+ expect(container.querySelector('pre').textContent).toBe('http://example.com:8080');
305
+
306
+ // Change hostname
307
+ buttons[1].click();
308
+ flushSync();
309
+ expect(container.querySelector('pre').textContent).toBe('http://newdomain.com:8080');
310
+
311
+ // Change port
312
+ buttons[2].click();
313
+ flushSync();
314
+ expect(container.querySelector('pre').textContent).toBe('http://newdomain.com:9090');
315
+ });
316
+ it('handles searchParams changes with reactivity', () => {
317
+ component URLTest() {
318
+ const url = new TrackedURL('https://example.com/path?foo=bar');
319
+ const params = url.searchParams;
320
+
321
+ <button onClick={() => params.set('foo', 'updated')}>{'Update Foo'}</button>
322
+ <button onClick={() => params.append('baz', 'qux')}>{'Add Baz'}</button>
323
+ <pre>{url.href}</pre>
324
+ <pre>{url.search}</pre>
325
+ <pre>{params.get('foo')}</pre>
326
+ }
327
+
328
+ render(URLTest);
329
+
330
+ const updateButton = container.querySelectorAll('button')[0];
331
+ const addButton = container.querySelectorAll('button')[1];
332
+
333
+ // Initial state
334
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/path?foo=bar');
335
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('?foo=bar');
336
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('bar');
337
+
338
+ // Update param
339
+ updateButton.click();
340
+ flushSync();
341
+
342
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/path?foo=updated');
343
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('?foo=updated');
344
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('updated');
345
+
346
+ // Add param
347
+ addButton.click();
348
+ flushSync();
349
+
350
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/path?foo=updated&baz=qux');
351
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('?foo=updated&baz=qux');
352
+ });
353
+
354
+ it('handles search property updates reflected in searchParams', () => {
355
+ component URLTest() {
356
+ const url = new TrackedURL('https://example.com/path?foo=bar');
357
+ const params = url.searchParams;
358
+
359
+ <button onClick={() => url.search = '?baz=qux&test=value'}>{'Change Search'}</button>
360
+ <pre>{url.search}</pre>
361
+ <pre>{params.get('foo')}</pre>
362
+ <pre>{params.get('baz')}</pre>
363
+ <pre>{params.get('test')}</pre>
364
+ <pre>{params.size}</pre>
365
+ }
366
+
367
+ render(URLTest);
368
+
369
+ const button = container.querySelector('button');
370
+
371
+ // Initial state
372
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('?foo=bar');
373
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('bar');
374
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('');
375
+ expect(container.querySelectorAll('pre')[3].textContent).toBe('');
376
+ expect(container.querySelectorAll('pre')[4].textContent).toBe('1');
377
+
378
+ // Change search
379
+ button.click();
380
+ flushSync();
381
+
382
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('?baz=qux&test=value');
383
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('');
384
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('qux');
385
+ expect(container.querySelectorAll('pre')[3].textContent).toBe('value');
386
+ expect(container.querySelectorAll('pre')[4].textContent).toBe('2');
387
+ });
388
+
389
+ it('handles multiple URL property changes in sequence', () => {
390
+ component URLTest() {
391
+ const url = new TrackedURL('https://example.com/path');
392
+
393
+ <button onClick={() => {
394
+ url.protocol = 'http:';
395
+ url.hostname = 'newdomain.com';
396
+ url.port = '8080';
397
+ url.pathname = '/api';
398
+ url.search = '?key=value';
399
+ url.hash = '#section';
400
+ }}>{'Change All'}</button>
401
+ <pre>{url.href}</pre>
402
+ }
403
+
404
+ render(URLTest);
405
+
406
+ const button = container.querySelector('button');
407
+
408
+ // Initial state
409
+ expect(container.querySelector('pre').textContent).toBe('https://example.com/path');
410
+
411
+ // Change all properties
412
+ button.click();
413
+ flushSync();
414
+
415
+ expect(container.querySelector('pre').textContent).toBe('http://newdomain.com:8080/api?key=value#section');
416
+ });
417
+
418
+ it('handles href change updates all properties and searchParams', () => {
419
+ component URLTest() {
420
+ const url = new TrackedURL('https://old.com/old?foo=bar#old');
421
+ const params = url.searchParams;
422
+
423
+ <button onClick={() => url.href = 'https://new.com:9090/new?baz=qux#new'}>{'Change Href'}</button>
424
+ <pre>{params.get('foo')}</pre>
425
+ <pre>{params.get('baz')}</pre>
426
+ <pre>{params.size}</pre>
427
+ <pre>{url.pathname}</pre>
428
+ }
429
+
430
+ render(URLTest);
431
+
432
+ const button = container.querySelector('button');
433
+
434
+ // Initial state
435
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('bar');
436
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('');
437
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('1');
438
+ expect(container.querySelectorAll('pre')[3].textContent).toBe('/old');
439
+
440
+ // Change href
441
+ button.click();
442
+ flushSync();
443
+
444
+ expect(container.querySelectorAll('pre')[0].textContent).toBe('');
445
+ expect(container.querySelectorAll('pre')[1].textContent).toBe('qux');
446
+ expect(container.querySelectorAll('pre')[2].textContent).toBe('1');
447
+ expect(container.querySelectorAll('pre')[3].textContent).toBe('/new');
448
+ });
449
+ });