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,954 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mount, flushSync, track } from 'ripple';
|
|
3
|
+
import { TrackedURL } from '../../src/runtime/url.js';
|
|
4
|
+
|
|
5
|
+
describe('TrackedURL', () => {
|
|
6
|
+
let container;
|
|
7
|
+
|
|
8
|
+
function render(component) {
|
|
9
|
+
mount(component, {
|
|
10
|
+
target: container
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
container = document.createElement('div');
|
|
16
|
+
document.body.appendChild(container);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
document.body.removeChild(container);
|
|
21
|
+
container = null;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('creates URL from string with reactivity', () => {
|
|
25
|
+
component URLTest() {
|
|
26
|
+
const url = new TrackedURL('https://example.com:8080/path?foo=bar#section');
|
|
27
|
+
|
|
28
|
+
<pre>{url.href}</pre>
|
|
29
|
+
<pre>{url.protocol}</pre>
|
|
30
|
+
<pre>{url.hostname}</pre>
|
|
31
|
+
<pre>{url.port}</pre>
|
|
32
|
+
<pre>{url.pathname}</pre>
|
|
33
|
+
<pre>{url.search}</pre>
|
|
34
|
+
<pre>{url.hash}</pre>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
render(URLTest);
|
|
38
|
+
|
|
39
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com:8080/path?foo=bar#section');
|
|
40
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('https:');
|
|
41
|
+
expect(container.querySelectorAll('pre')[2].textContent).toBe('example.com');
|
|
42
|
+
expect(container.querySelectorAll('pre')[3].textContent).toBe('8080');
|
|
43
|
+
expect(container.querySelectorAll('pre')[4].textContent).toBe('/path');
|
|
44
|
+
expect(container.querySelectorAll('pre')[5].textContent).toBe('?foo=bar');
|
|
45
|
+
expect(container.querySelectorAll('pre')[6].textContent).toBe('#section');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('creates URL from string with base URL', () => {
|
|
49
|
+
component URLTest() {
|
|
50
|
+
const url = new TrackedURL('/path?query=value', 'https://example.com');
|
|
51
|
+
|
|
52
|
+
<pre>{url.href}</pre>
|
|
53
|
+
<pre>{url.origin}</pre>
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
render(URLTest);
|
|
57
|
+
|
|
58
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/path?query=value');
|
|
59
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('https://example.com');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('handles protocol changes with reactivity', () => {
|
|
63
|
+
component URLTest() {
|
|
64
|
+
const url = new TrackedURL('https://example.com/path');
|
|
65
|
+
|
|
66
|
+
<button onClick={() => url.protocol = 'http:'}>{'Change Protocol'}</button>
|
|
67
|
+
<pre>{url.href}</pre>
|
|
68
|
+
<pre>{url.protocol}</pre>
|
|
69
|
+
<pre>{url.origin}</pre>
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
render(URLTest);
|
|
73
|
+
|
|
74
|
+
const button = container.querySelector('button');
|
|
75
|
+
|
|
76
|
+
// Initial state
|
|
77
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/path');
|
|
78
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('https:');
|
|
79
|
+
expect(container.querySelectorAll('pre')[2].textContent).toBe('https://example.com');
|
|
80
|
+
|
|
81
|
+
// Change protocol
|
|
82
|
+
button.click();
|
|
83
|
+
flushSync();
|
|
84
|
+
|
|
85
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('http://example.com/path');
|
|
86
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('http:');
|
|
87
|
+
expect(container.querySelectorAll('pre')[2].textContent).toBe('http://example.com');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('handles hostname changes with reactivity', () => {
|
|
91
|
+
component URLTest() {
|
|
92
|
+
const url = new TrackedURL('https://example.com/path');
|
|
93
|
+
|
|
94
|
+
<button onClick={() => url.hostname = 'newdomain.com'}>{'Change Hostname'}</button>
|
|
95
|
+
<pre>{url.href}</pre>
|
|
96
|
+
<pre>{url.hostname}</pre>
|
|
97
|
+
<pre>{url.host}</pre>
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
render(URLTest);
|
|
101
|
+
|
|
102
|
+
const button = container.querySelector('button');
|
|
103
|
+
|
|
104
|
+
// Initial state
|
|
105
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/path');
|
|
106
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('example.com');
|
|
107
|
+
expect(container.querySelectorAll('pre')[2].textContent).toBe('example.com');
|
|
108
|
+
|
|
109
|
+
// Change hostname
|
|
110
|
+
button.click();
|
|
111
|
+
flushSync();
|
|
112
|
+
|
|
113
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://newdomain.com/path');
|
|
114
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('newdomain.com');
|
|
115
|
+
expect(container.querySelectorAll('pre')[2].textContent).toBe('newdomain.com');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('handles port changes with reactivity', () => {
|
|
119
|
+
component URLTest() {
|
|
120
|
+
const url = new TrackedURL('https://example.com:8080/path');
|
|
121
|
+
|
|
122
|
+
<button onClick={() => url.port = '9090'}>{'Change Port'}</button>
|
|
123
|
+
<pre>{url.href}</pre>
|
|
124
|
+
<pre>{url.port}</pre>
|
|
125
|
+
<pre>{url.host}</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:8080/path');
|
|
134
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('8080');
|
|
135
|
+
expect(container.querySelectorAll('pre')[2].textContent).toBe('example.com:8080');
|
|
136
|
+
|
|
137
|
+
// Change port
|
|
138
|
+
button.click();
|
|
139
|
+
flushSync();
|
|
140
|
+
|
|
141
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com:9090/path');
|
|
142
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('9090');
|
|
143
|
+
expect(container.querySelectorAll('pre')[2].textContent).toBe('example.com:9090');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('handles host changes with reactivity', () => {
|
|
147
|
+
component URLTest() {
|
|
148
|
+
const url = new TrackedURL('https://example.com:8080/path');
|
|
149
|
+
|
|
150
|
+
<button onClick={() => url.host = 'newdomain.com:9090'}>{'Change Host'}</button>
|
|
151
|
+
<pre>{url.href}</pre>
|
|
152
|
+
<pre>{url.host}</pre>
|
|
153
|
+
<pre>{url.hostname}</pre>
|
|
154
|
+
<pre>{url.port}</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('https://example.com:8080/path');
|
|
163
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('example.com:8080');
|
|
164
|
+
expect(container.querySelectorAll('pre')[2].textContent).toBe('example.com');
|
|
165
|
+
expect(container.querySelectorAll('pre')[3].textContent).toBe('8080');
|
|
166
|
+
|
|
167
|
+
// Change host
|
|
168
|
+
button.click();
|
|
169
|
+
flushSync();
|
|
170
|
+
|
|
171
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://newdomain.com:9090/path');
|
|
172
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('newdomain.com:9090');
|
|
173
|
+
expect(container.querySelectorAll('pre')[2].textContent).toBe('newdomain.com');
|
|
174
|
+
expect(container.querySelectorAll('pre')[3].textContent).toBe('9090');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('handles pathname changes with reactivity', () => {
|
|
178
|
+
component URLTest() {
|
|
179
|
+
const url = new TrackedURL('https://example.com/old-path');
|
|
180
|
+
|
|
181
|
+
<button onClick={() => url.pathname = '/new-path'}>{'Change Pathname'}</button>
|
|
182
|
+
<pre>{url.href}</pre>
|
|
183
|
+
<pre>{url.pathname}</pre>
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
render(URLTest);
|
|
187
|
+
|
|
188
|
+
const button = container.querySelector('button');
|
|
189
|
+
|
|
190
|
+
// Initial state
|
|
191
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/old-path');
|
|
192
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('/old-path');
|
|
193
|
+
|
|
194
|
+
// Change pathname
|
|
195
|
+
button.click();
|
|
196
|
+
flushSync();
|
|
197
|
+
|
|
198
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/new-path');
|
|
199
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('/new-path');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('handles search changes with reactivity', () => {
|
|
203
|
+
component URLTest() {
|
|
204
|
+
const url = new TrackedURL('https://example.com/path?foo=bar');
|
|
205
|
+
|
|
206
|
+
<button onClick={() => url.search = '?baz=qux'}>{'Change Search'}</button>
|
|
207
|
+
<pre>{url.href}</pre>
|
|
208
|
+
<pre>{url.search}</pre>
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
render(URLTest);
|
|
212
|
+
|
|
213
|
+
const button = container.querySelector('button');
|
|
214
|
+
|
|
215
|
+
// Initial state
|
|
216
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/path?foo=bar');
|
|
217
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('?foo=bar');
|
|
218
|
+
|
|
219
|
+
// Change search
|
|
220
|
+
button.click();
|
|
221
|
+
flushSync();
|
|
222
|
+
|
|
223
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/path?baz=qux');
|
|
224
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('?baz=qux');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('handles hash changes with reactivity', () => {
|
|
228
|
+
component URLTest() {
|
|
229
|
+
const url = new TrackedURL('https://example.com/path#section1');
|
|
230
|
+
|
|
231
|
+
<button onClick={() => url.hash = '#section2'}>{'Change Hash'}</button>
|
|
232
|
+
<pre>{url.href}</pre>
|
|
233
|
+
<pre>{url.hash}</pre>
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
render(URLTest);
|
|
237
|
+
|
|
238
|
+
const button = container.querySelector('button');
|
|
239
|
+
|
|
240
|
+
// Initial state
|
|
241
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/path#section1');
|
|
242
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('#section1');
|
|
243
|
+
|
|
244
|
+
// Change hash
|
|
245
|
+
button.click();
|
|
246
|
+
flushSync();
|
|
247
|
+
|
|
248
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/path#section2');
|
|
249
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('#section2');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('handles username changes with reactivity', () => {
|
|
253
|
+
component URLTest() {
|
|
254
|
+
const url = new TrackedURL('https://user:pass@example.com/path');
|
|
255
|
+
|
|
256
|
+
<button onClick={() => url.username = 'newuser'}>{'Change Username'}</button>
|
|
257
|
+
<pre>{url.href}</pre>
|
|
258
|
+
<pre>{url.username}</pre>
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
render(URLTest);
|
|
262
|
+
|
|
263
|
+
const button = container.querySelector('button');
|
|
264
|
+
|
|
265
|
+
// Initial state
|
|
266
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://user:pass@example.com/path');
|
|
267
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('user');
|
|
268
|
+
|
|
269
|
+
// Change username
|
|
270
|
+
button.click();
|
|
271
|
+
flushSync();
|
|
272
|
+
|
|
273
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://newuser:pass@example.com/path');
|
|
274
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('newuser');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('handles password changes with reactivity', () => {
|
|
278
|
+
component URLTest() {
|
|
279
|
+
const url = new TrackedURL('https://user:pass@example.com/path');
|
|
280
|
+
|
|
281
|
+
<button onClick={() => url.password = 'newpass'}>{'Change Password'}</button>
|
|
282
|
+
<pre>{url.href}</pre>
|
|
283
|
+
<pre>{url.password}</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('https://user:pass@example.com/path');
|
|
292
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('pass');
|
|
293
|
+
|
|
294
|
+
// Change password
|
|
295
|
+
button.click();
|
|
296
|
+
flushSync();
|
|
297
|
+
|
|
298
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://user:newpass@example.com/path');
|
|
299
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('newpass');
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('handles href changes with reactivity', () => {
|
|
303
|
+
component URLTest() {
|
|
304
|
+
const url = new TrackedURL('https://example.com/path?foo=bar#section');
|
|
305
|
+
|
|
306
|
+
<button onClick={() => url.href = 'https://newdomain.com:9090/newpath?baz=qux#newsection'}>{'Change Href'}</button>
|
|
307
|
+
<pre>{url.href}</pre>
|
|
308
|
+
<pre>{url.protocol}</pre>
|
|
309
|
+
<pre>{url.hostname}</pre>
|
|
310
|
+
<pre>{url.port}</pre>
|
|
311
|
+
<pre>{url.pathname}</pre>
|
|
312
|
+
<pre>{url.search}</pre>
|
|
313
|
+
<pre>{url.hash}</pre>
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
render(URLTest);
|
|
317
|
+
|
|
318
|
+
const button = container.querySelector('button');
|
|
319
|
+
|
|
320
|
+
// Initial state
|
|
321
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/path?foo=bar#section');
|
|
322
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('https:');
|
|
323
|
+
expect(container.querySelectorAll('pre')[2].textContent).toBe('example.com');
|
|
324
|
+
expect(container.querySelectorAll('pre')[3].textContent).toBe('');
|
|
325
|
+
expect(container.querySelectorAll('pre')[4].textContent).toBe('/path');
|
|
326
|
+
expect(container.querySelectorAll('pre')[5].textContent).toBe('?foo=bar');
|
|
327
|
+
expect(container.querySelectorAll('pre')[6].textContent).toBe('#section');
|
|
328
|
+
|
|
329
|
+
// Change href
|
|
330
|
+
button.click();
|
|
331
|
+
flushSync();
|
|
332
|
+
|
|
333
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://newdomain.com:9090/newpath?baz=qux#newsection');
|
|
334
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('https:');
|
|
335
|
+
expect(container.querySelectorAll('pre')[2].textContent).toBe('newdomain.com');
|
|
336
|
+
expect(container.querySelectorAll('pre')[3].textContent).toBe('9090');
|
|
337
|
+
expect(container.querySelectorAll('pre')[4].textContent).toBe('/newpath');
|
|
338
|
+
expect(container.querySelectorAll('pre')[5].textContent).toBe('?baz=qux');
|
|
339
|
+
expect(container.querySelectorAll('pre')[6].textContent).toBe('#newsection');
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('handles origin property reactivity', () => {
|
|
343
|
+
component URLTest() {
|
|
344
|
+
const url = new TrackedURL('https://example.com:8080/path');
|
|
345
|
+
|
|
346
|
+
<button onClick={() => url.protocol = 'http:'}>{'Change Protocol'}</button>
|
|
347
|
+
<button onClick={() => url.hostname = 'newdomain.com'}>{'Change Hostname'}</button>
|
|
348
|
+
<button onClick={() => url.port = '9090'}>{'Change Port'}</button>
|
|
349
|
+
<pre>{url.origin}</pre>
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
render(URLTest);
|
|
353
|
+
|
|
354
|
+
const buttons = container.querySelectorAll('button');
|
|
355
|
+
|
|
356
|
+
// Initial state
|
|
357
|
+
expect(container.querySelector('pre').textContent).toBe('https://example.com:8080');
|
|
358
|
+
|
|
359
|
+
// Change protocol
|
|
360
|
+
buttons[0].click();
|
|
361
|
+
flushSync();
|
|
362
|
+
expect(container.querySelector('pre').textContent).toBe('http://example.com:8080');
|
|
363
|
+
|
|
364
|
+
// Change hostname
|
|
365
|
+
buttons[1].click();
|
|
366
|
+
flushSync();
|
|
367
|
+
expect(container.querySelector('pre').textContent).toBe('http://newdomain.com:8080');
|
|
368
|
+
|
|
369
|
+
// Change port
|
|
370
|
+
buttons[2].click();
|
|
371
|
+
flushSync();
|
|
372
|
+
expect(container.querySelector('pre').textContent).toBe('http://newdomain.com:9090');
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('handles searchParams changes with reactivity', () => {
|
|
376
|
+
component URLTest() {
|
|
377
|
+
const url = new TrackedURL('https://example.com/path?foo=bar');
|
|
378
|
+
const params = url.searchParams;
|
|
379
|
+
|
|
380
|
+
<button onClick={() => params.set('foo', 'updated')}>{'Update Foo'}</button>
|
|
381
|
+
<button onClick={() => params.append('baz', 'qux')}>{'Add Baz'}</button>
|
|
382
|
+
<pre>{url.href}</pre>
|
|
383
|
+
<pre>{url.search}</pre>
|
|
384
|
+
<pre>{params.get('foo')}</pre>
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
render(URLTest);
|
|
388
|
+
|
|
389
|
+
const updateButton = container.querySelectorAll('button')[0];
|
|
390
|
+
const addButton = container.querySelectorAll('button')[1];
|
|
391
|
+
|
|
392
|
+
// Initial state
|
|
393
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/path?foo=bar');
|
|
394
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('?foo=bar');
|
|
395
|
+
expect(container.querySelectorAll('pre')[2].textContent).toBe('bar');
|
|
396
|
+
|
|
397
|
+
// Update param
|
|
398
|
+
updateButton.click();
|
|
399
|
+
flushSync();
|
|
400
|
+
|
|
401
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/path?foo=updated');
|
|
402
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('?foo=updated');
|
|
403
|
+
expect(container.querySelectorAll('pre')[2].textContent).toBe('updated');
|
|
404
|
+
|
|
405
|
+
// Add param
|
|
406
|
+
addButton.click();
|
|
407
|
+
flushSync();
|
|
408
|
+
|
|
409
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/path?foo=updated&baz=qux');
|
|
410
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('?foo=updated&baz=qux');
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it('handles search property updates reflected in searchParams', () => {
|
|
414
|
+
component URLTest() {
|
|
415
|
+
const url = new TrackedURL('https://example.com/path?foo=bar');
|
|
416
|
+
const params = url.searchParams;
|
|
417
|
+
|
|
418
|
+
<button onClick={() => url.search = '?baz=qux&test=value'}>{'Change Search'}</button>
|
|
419
|
+
<pre>{url.search}</pre>
|
|
420
|
+
<pre>{params.get('foo')}</pre>
|
|
421
|
+
<pre>{params.get('baz')}</pre>
|
|
422
|
+
<pre>{params.get('test')}</pre>
|
|
423
|
+
<pre>{params.size}</pre>
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
render(URLTest);
|
|
427
|
+
|
|
428
|
+
const button = container.querySelector('button');
|
|
429
|
+
|
|
430
|
+
// Initial state
|
|
431
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('?foo=bar');
|
|
432
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('bar');
|
|
433
|
+
expect(container.querySelectorAll('pre')[2].textContent).toBe('');
|
|
434
|
+
expect(container.querySelectorAll('pre')[3].textContent).toBe('');
|
|
435
|
+
expect(container.querySelectorAll('pre')[4].textContent).toBe('1');
|
|
436
|
+
|
|
437
|
+
// Change search
|
|
438
|
+
button.click();
|
|
439
|
+
flushSync();
|
|
440
|
+
|
|
441
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('?baz=qux&test=value');
|
|
442
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('');
|
|
443
|
+
expect(container.querySelectorAll('pre')[2].textContent).toBe('qux');
|
|
444
|
+
expect(container.querySelectorAll('pre')[3].textContent).toBe('value');
|
|
445
|
+
expect(container.querySelectorAll('pre')[4].textContent).toBe('2');
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('handles toString method', () => {
|
|
449
|
+
component URLTest() {
|
|
450
|
+
const url = new TrackedURL('https://example.com/path?foo=bar#section');
|
|
451
|
+
|
|
452
|
+
<button onClick={() => url.pathname = '/newpath'}>{'Change Pathname'}</button>
|
|
453
|
+
<pre>{url.toString()}</pre>
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
render(URLTest);
|
|
457
|
+
|
|
458
|
+
const button = container.querySelector('button');
|
|
459
|
+
|
|
460
|
+
// Initial state
|
|
461
|
+
expect(container.querySelector('pre').textContent).toBe('https://example.com/path?foo=bar#section');
|
|
462
|
+
|
|
463
|
+
// Change pathname
|
|
464
|
+
button.click();
|
|
465
|
+
flushSync();
|
|
466
|
+
|
|
467
|
+
expect(container.querySelector('pre').textContent).toBe('https://example.com/newpath?foo=bar#section');
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it('handles toJSON method', () => {
|
|
471
|
+
component URLTest() {
|
|
472
|
+
const url = new TrackedURL('https://example.com/path?foo=bar');
|
|
473
|
+
|
|
474
|
+
<button onClick={() => url.pathname = '/api'}>{'Change Pathname'}</button>
|
|
475
|
+
<pre>{url.toJSON()}</pre>
|
|
476
|
+
<pre>{JSON.stringify({ url: url.toJSON() })}</pre>
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
render(URLTest);
|
|
480
|
+
|
|
481
|
+
const button = container.querySelector('button');
|
|
482
|
+
|
|
483
|
+
// Initial state
|
|
484
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/path?foo=bar');
|
|
485
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('{"url":"https://example.com/path?foo=bar"}');
|
|
486
|
+
|
|
487
|
+
// Change pathname
|
|
488
|
+
button.click();
|
|
489
|
+
flushSync();
|
|
490
|
+
|
|
491
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/api?foo=bar');
|
|
492
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('{"url":"https://example.com/api?foo=bar"}');
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it('handles multiple URL property changes in sequence', () => {
|
|
496
|
+
component URLTest() {
|
|
497
|
+
const url = new TrackedURL('https://example.com/path');
|
|
498
|
+
|
|
499
|
+
<button onClick={() => {
|
|
500
|
+
url.protocol = 'http:';
|
|
501
|
+
url.hostname = 'newdomain.com';
|
|
502
|
+
url.port = '8080';
|
|
503
|
+
url.pathname = '/api';
|
|
504
|
+
url.search = '?key=value';
|
|
505
|
+
url.hash = '#section';
|
|
506
|
+
}}>{'Change All'}</button>
|
|
507
|
+
<pre>{url.href}</pre>
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
render(URLTest);
|
|
511
|
+
|
|
512
|
+
const button = container.querySelector('button');
|
|
513
|
+
|
|
514
|
+
// Initial state
|
|
515
|
+
expect(container.querySelector('pre').textContent).toBe('https://example.com/path');
|
|
516
|
+
|
|
517
|
+
// Change all properties
|
|
518
|
+
button.click();
|
|
519
|
+
flushSync();
|
|
520
|
+
|
|
521
|
+
expect(container.querySelector('pre').textContent).toBe('http://newdomain.com:8080/api?key=value#section');
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it('handles URL with no port specified', () => {
|
|
525
|
+
component URLTest() {
|
|
526
|
+
const url = new TrackedURL('https://example.com/path');
|
|
527
|
+
|
|
528
|
+
<pre>{url.port}</pre>
|
|
529
|
+
<pre>{url.host}</pre>
|
|
530
|
+
<button onClick={() => url.port = '8080'}>{'Add Port'}</button>
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
render(URLTest);
|
|
534
|
+
|
|
535
|
+
const button = container.querySelector('button');
|
|
536
|
+
|
|
537
|
+
// Initial state - default ports are empty strings
|
|
538
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('');
|
|
539
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('example.com');
|
|
540
|
+
|
|
541
|
+
// Add port
|
|
542
|
+
button.click();
|
|
543
|
+
flushSync();
|
|
544
|
+
|
|
545
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('8080');
|
|
546
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('example.com:8080');
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it('handles URL with no search params', () => {
|
|
550
|
+
component URLTest() {
|
|
551
|
+
const url = new TrackedURL('https://example.com/path');
|
|
552
|
+
|
|
553
|
+
<pre>{url.search}</pre>
|
|
554
|
+
<pre>{url.searchParams.size}</pre>
|
|
555
|
+
<button onClick={() => url.searchParams.append('foo', 'bar')}>{'Add Param'}</button>
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
render(URLTest);
|
|
559
|
+
|
|
560
|
+
const button = container.querySelector('button');
|
|
561
|
+
|
|
562
|
+
// Initial state
|
|
563
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('');
|
|
564
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('0');
|
|
565
|
+
|
|
566
|
+
// Add param
|
|
567
|
+
button.click();
|
|
568
|
+
flushSync();
|
|
569
|
+
|
|
570
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('?foo=bar');
|
|
571
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('1');
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
it('handles URL with no hash', () => {
|
|
575
|
+
component URLTest() {
|
|
576
|
+
const url = new TrackedURL('https://example.com/path');
|
|
577
|
+
|
|
578
|
+
<pre>{url.hash}</pre>
|
|
579
|
+
<button onClick={() => url.hash = '#section'}>{'Add Hash'}</button>
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
render(URLTest);
|
|
583
|
+
|
|
584
|
+
const button = container.querySelector('button');
|
|
585
|
+
|
|
586
|
+
// Initial state
|
|
587
|
+
expect(container.querySelector('pre').textContent).toBe('');
|
|
588
|
+
|
|
589
|
+
// Add hash
|
|
590
|
+
button.click();
|
|
591
|
+
flushSync();
|
|
592
|
+
|
|
593
|
+
expect(container.querySelector('pre').textContent).toBe('#section');
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
it('handles removing port by setting empty string', () => {
|
|
597
|
+
component URLTest() {
|
|
598
|
+
const url = new TrackedURL('https://example.com:8080/path');
|
|
599
|
+
|
|
600
|
+
<button onClick={() => url.port = ''}>{'Remove Port'}</button>
|
|
601
|
+
<pre>{url.href}</pre>
|
|
602
|
+
<pre>{url.port}</pre>
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
render(URLTest);
|
|
606
|
+
|
|
607
|
+
const button = container.querySelector('button');
|
|
608
|
+
|
|
609
|
+
// Initial state
|
|
610
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com:8080/path');
|
|
611
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('8080');
|
|
612
|
+
|
|
613
|
+
// Remove port
|
|
614
|
+
button.click();
|
|
615
|
+
flushSync();
|
|
616
|
+
|
|
617
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/path');
|
|
618
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('');
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
it('handles removing hash by setting empty string', () => {
|
|
622
|
+
component URLTest() {
|
|
623
|
+
const url = new TrackedURL('https://example.com/path#section');
|
|
624
|
+
|
|
625
|
+
<button onClick={() => url.hash = ''}>{'Remove Hash'}</button>
|
|
626
|
+
<pre>{url.href}</pre>
|
|
627
|
+
<pre>{url.hash}</pre>
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
render(URLTest);
|
|
631
|
+
|
|
632
|
+
const button = container.querySelector('button');
|
|
633
|
+
|
|
634
|
+
// Initial state
|
|
635
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/path#section');
|
|
636
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('#section');
|
|
637
|
+
|
|
638
|
+
// Remove hash
|
|
639
|
+
button.click();
|
|
640
|
+
flushSync();
|
|
641
|
+
|
|
642
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/path');
|
|
643
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('');
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
it('handles removing search by setting empty string', () => {
|
|
647
|
+
component URLTest() {
|
|
648
|
+
const url = new TrackedURL('https://example.com/path?foo=bar');
|
|
649
|
+
|
|
650
|
+
<button onClick={() => url.search = ''}>{'Remove Search'}</button>
|
|
651
|
+
<pre>{url.href}</pre>
|
|
652
|
+
<pre>{url.search}</pre>
|
|
653
|
+
<pre>{url.searchParams.size}</pre>
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
render(URLTest);
|
|
657
|
+
|
|
658
|
+
const button = container.querySelector('button');
|
|
659
|
+
|
|
660
|
+
// Initial state
|
|
661
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/path?foo=bar');
|
|
662
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('?foo=bar');
|
|
663
|
+
expect(container.querySelectorAll('pre')[2].textContent).toBe('1');
|
|
664
|
+
|
|
665
|
+
// Remove search
|
|
666
|
+
button.click();
|
|
667
|
+
flushSync();
|
|
668
|
+
|
|
669
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/path');
|
|
670
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('');
|
|
671
|
+
expect(container.querySelectorAll('pre')[2].textContent).toBe('0');
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
it('handles reactive computed properties based on URL', () => {
|
|
675
|
+
component URLTest() {
|
|
676
|
+
const url = new TrackedURL('https://example.com/users/123?tab=profile');
|
|
677
|
+
let userId = track(() => url.pathname.split('/').pop());
|
|
678
|
+
let activeTab = track(() => url.searchParams.get('tab'));
|
|
679
|
+
|
|
680
|
+
<button onClick={() => url.pathname = '/users/456'}>{'Change User'}</button>
|
|
681
|
+
<button onClick={() => url.searchParams.set('tab', 'settings')}>{'Change Tab'}</button>
|
|
682
|
+
<pre>{`User ID: ${@userId}`}</pre>
|
|
683
|
+
<pre>{`Active Tab: ${@activeTab}`}</pre>
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
render(URLTest);
|
|
687
|
+
|
|
688
|
+
const changeUserBtn = container.querySelectorAll('button')[0];
|
|
689
|
+
const changeTabBtn = container.querySelectorAll('button')[1];
|
|
690
|
+
|
|
691
|
+
// Initial state
|
|
692
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('User ID: 123');
|
|
693
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('Active Tab: profile');
|
|
694
|
+
|
|
695
|
+
// Change user
|
|
696
|
+
changeUserBtn.click();
|
|
697
|
+
flushSync();
|
|
698
|
+
|
|
699
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('User ID: 456');
|
|
700
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('Active Tab: profile');
|
|
701
|
+
|
|
702
|
+
// Change tab
|
|
703
|
+
changeTabBtn.click();
|
|
704
|
+
flushSync();
|
|
705
|
+
|
|
706
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('User ID: 456');
|
|
707
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('Active Tab: settings');
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
it('handles URL encoding correctly', () => {
|
|
711
|
+
component URLTest() {
|
|
712
|
+
const url = new TrackedURL('https://example.com/path with spaces?key=value with spaces');
|
|
713
|
+
|
|
714
|
+
<pre>{url.pathname}</pre>
|
|
715
|
+
<pre>{url.search}</pre>
|
|
716
|
+
<pre>{url.href}</pre>
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
render(URLTest);
|
|
720
|
+
|
|
721
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('/path%20with%20spaces');
|
|
722
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('?key=value%20with%20spaces');
|
|
723
|
+
expect(container.querySelectorAll('pre')[2].textContent).toBe('https://example.com/path%20with%20spaces?key=value%20with%20spaces');
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
it('maintains reactivity across multiple components', () => {
|
|
727
|
+
component ParentTest() {
|
|
728
|
+
const url = new TrackedURL('https://example.com/path?count=0');
|
|
729
|
+
|
|
730
|
+
<ChildA url={url} />
|
|
731
|
+
<ChildB url={url} />
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
component ChildA({ url }) {
|
|
735
|
+
<button onClick={() => {
|
|
736
|
+
const current = parseInt(url.searchParams.get('count') || '0', 10);
|
|
737
|
+
url.searchParams.set('count', String(current + 1));
|
|
738
|
+
}}>{'Increment Count'}</button>
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
component ChildB({ url }) {
|
|
742
|
+
let count = track(() => url.searchParams.get('count'));
|
|
743
|
+
|
|
744
|
+
<pre>{url.href}</pre>
|
|
745
|
+
<pre>{@count}</pre>
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
render(ParentTest);
|
|
749
|
+
|
|
750
|
+
const button = container.querySelector('button');
|
|
751
|
+
|
|
752
|
+
// Initial state
|
|
753
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/path?count=0');
|
|
754
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('0');
|
|
755
|
+
|
|
756
|
+
// Increment from child
|
|
757
|
+
button.click();
|
|
758
|
+
flushSync();
|
|
759
|
+
|
|
760
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/path?count=1');
|
|
761
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('1');
|
|
762
|
+
|
|
763
|
+
button.click();
|
|
764
|
+
flushSync();
|
|
765
|
+
|
|
766
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/path?count=2');
|
|
767
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('2');
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
it('handles relative URL paths correctly', () => {
|
|
771
|
+
component URLTest() {
|
|
772
|
+
const url = new TrackedURL('../sibling/path', 'https://example.com/parent/current');
|
|
773
|
+
|
|
774
|
+
<pre>{url.href}</pre>
|
|
775
|
+
<pre>{url.pathname}</pre>
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
render(URLTest);
|
|
779
|
+
|
|
780
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://example.com/sibling/path');
|
|
781
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('/sibling/path');
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
it('handles URL with IPv4 address', () => {
|
|
785
|
+
component URLTest() {
|
|
786
|
+
const url = new TrackedURL('https://192.168.1.1:8080/path');
|
|
787
|
+
|
|
788
|
+
<button onClick={() => url.hostname = '10.0.0.1'}>{'Change IP'}</button>
|
|
789
|
+
<pre>{url.href}</pre>
|
|
790
|
+
<pre>{url.hostname}</pre>
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
render(URLTest);
|
|
794
|
+
|
|
795
|
+
const button = container.querySelector('button');
|
|
796
|
+
|
|
797
|
+
// Initial state
|
|
798
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://192.168.1.1:8080/path');
|
|
799
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('192.168.1.1');
|
|
800
|
+
|
|
801
|
+
// Change IP
|
|
802
|
+
button.click();
|
|
803
|
+
flushSync();
|
|
804
|
+
|
|
805
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('https://10.0.0.1:8080/path');
|
|
806
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('10.0.0.1');
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
it('handles href change updates all properties and searchParams', () => {
|
|
810
|
+
component URLTest() {
|
|
811
|
+
const url = new TrackedURL('https://old.com/old?foo=bar#old');
|
|
812
|
+
const params = url.searchParams;
|
|
813
|
+
|
|
814
|
+
<button onClick={() => url.href = 'https://new.com:9090/new?baz=qux#new'}>{'Change Href'}</button>
|
|
815
|
+
<pre>{params.get('foo')}</pre>
|
|
816
|
+
<pre>{params.get('baz')}</pre>
|
|
817
|
+
<pre>{params.size}</pre>
|
|
818
|
+
<pre>{url.pathname}</pre>
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
render(URLTest);
|
|
822
|
+
|
|
823
|
+
const button = container.querySelector('button');
|
|
824
|
+
|
|
825
|
+
// Initial state
|
|
826
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('bar');
|
|
827
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('');
|
|
828
|
+
expect(container.querySelectorAll('pre')[2].textContent).toBe('1');
|
|
829
|
+
expect(container.querySelectorAll('pre')[3].textContent).toBe('/old');
|
|
830
|
+
|
|
831
|
+
// Change href
|
|
832
|
+
button.click();
|
|
833
|
+
flushSync();
|
|
834
|
+
|
|
835
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('');
|
|
836
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('qux');
|
|
837
|
+
expect(container.querySelectorAll('pre')[2].textContent).toBe('1');
|
|
838
|
+
expect(container.querySelectorAll('pre')[3].textContent).toBe('/new');
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
it('handles URL with localhost', () => {
|
|
842
|
+
component URLTest() {
|
|
843
|
+
const url = new TrackedURL('http://localhost:3000/api/data');
|
|
844
|
+
|
|
845
|
+
<button onClick={() => url.port = '8080'}>{'Change Port'}</button>
|
|
846
|
+
<pre>{url.href}</pre>
|
|
847
|
+
<pre>{url.hostname}</pre>
|
|
848
|
+
<pre>{url.port}</pre>
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
render(URLTest);
|
|
852
|
+
|
|
853
|
+
const button = container.querySelector('button');
|
|
854
|
+
|
|
855
|
+
// Initial state
|
|
856
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('http://localhost:3000/api/data');
|
|
857
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('localhost');
|
|
858
|
+
expect(container.querySelectorAll('pre')[2].textContent).toBe('3000');
|
|
859
|
+
|
|
860
|
+
// Change port
|
|
861
|
+
button.click();
|
|
862
|
+
flushSync();
|
|
863
|
+
|
|
864
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('http://localhost:8080/api/data');
|
|
865
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('localhost');
|
|
866
|
+
expect(container.querySelectorAll('pre')[2].textContent).toBe('8080');
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
it('handles URL with multiple path segments', () => {
|
|
870
|
+
component URLTest() {
|
|
871
|
+
const url = new TrackedURL('https://example.com/api/v1/users/123/profile');
|
|
872
|
+
|
|
873
|
+
<button onClick={() => url.pathname = '/api/v2/users/456/settings'}>{'Change Path'}</button>
|
|
874
|
+
<pre>{url.pathname}</pre>
|
|
875
|
+
<pre>{url.href}</pre>
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
render(URLTest);
|
|
879
|
+
|
|
880
|
+
const button = container.querySelector('button');
|
|
881
|
+
|
|
882
|
+
// Initial state
|
|
883
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('/api/v1/users/123/profile');
|
|
884
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('https://example.com/api/v1/users/123/profile');
|
|
885
|
+
|
|
886
|
+
// Change path
|
|
887
|
+
button.click();
|
|
888
|
+
flushSync();
|
|
889
|
+
|
|
890
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('/api/v2/users/456/settings');
|
|
891
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('https://example.com/api/v2/users/456/settings');
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
it('handles URL with file protocol', () => {
|
|
895
|
+
component URLTest() {
|
|
896
|
+
const url = new TrackedURL('file:///Users/username/documents/file.txt');
|
|
897
|
+
|
|
898
|
+
<pre>{url.protocol}</pre>
|
|
899
|
+
<pre>{url.pathname}</pre>
|
|
900
|
+
<pre>{url.href}</pre>
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
render(URLTest);
|
|
904
|
+
|
|
905
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('file:');
|
|
906
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('/Users/username/documents/file.txt');
|
|
907
|
+
expect(container.querySelectorAll('pre')[2].textContent).toBe('file:///Users/username/documents/file.txt');
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
it('handles hash without leading # character', () => {
|
|
911
|
+
component URLTest() {
|
|
912
|
+
const url = new TrackedURL('https://example.com/path');
|
|
913
|
+
|
|
914
|
+
<button onClick={() => url.hash = 'section'}>{'Set Hash'}</button>
|
|
915
|
+
<pre>{url.hash}</pre>
|
|
916
|
+
<pre>{url.href}</pre>
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
render(URLTest);
|
|
920
|
+
|
|
921
|
+
const button = container.querySelector('button');
|
|
922
|
+
|
|
923
|
+
// Initial state
|
|
924
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('');
|
|
925
|
+
|
|
926
|
+
// Set hash
|
|
927
|
+
button.click();
|
|
928
|
+
flushSync();
|
|
929
|
+
|
|
930
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('#section');
|
|
931
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('https://example.com/path#section');
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
it('handles search without leading ? character', () => {
|
|
935
|
+
component URLTest() {
|
|
936
|
+
const url = new TrackedURL('https://example.com/path');
|
|
937
|
+
|
|
938
|
+
<button onClick={() => url.search = 'foo=bar'}>{'Set Search'}</button>
|
|
939
|
+
<pre>{url.search}</pre>
|
|
940
|
+
<pre>{url.href}</pre>
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
render(URLTest);
|
|
944
|
+
|
|
945
|
+
const button = container.querySelector('button');
|
|
946
|
+
|
|
947
|
+
// Set search
|
|
948
|
+
button.click();
|
|
949
|
+
flushSync();
|
|
950
|
+
|
|
951
|
+
expect(container.querySelectorAll('pre')[0].textContent).toBe('foo=bar');
|
|
952
|
+
expect(container.querySelectorAll('pre')[1].textContent).toBe('https://example.com/path?foo=bar');
|
|
953
|
+
});
|
|
954
|
+
});
|