@gjsify/url 0.0.4 → 0.1.0
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/README.md +29 -3
- package/lib/esm/index.js +442 -4
- package/lib/types/index.d.ts +72 -3
- package/package.json +16 -23
- package/src/index.spec.ts +1081 -85
- package/src/index.ts +555 -3
- package/tsconfig.json +27 -14
- package/tsconfig.tsbuildinfo +1 -0
- package/lib/cjs/index.js +0 -6
- package/test.gjs.mjs +0 -37568
- package/test.node.mjs +0 -379
- package/tsconfig.types.json +0 -8
package/src/index.spec.ts
CHANGED
|
@@ -1,88 +1,1084 @@
|
|
|
1
|
-
// TODO port more tests from https://github.com/defunctzombie/node-url
|
|
2
|
-
|
|
3
1
|
import { describe, it, expect } from '@gjsify/unit';
|
|
4
|
-
import { fileURLToPath,
|
|
2
|
+
import { URL, URLSearchParams, fileURLToPath, pathToFileURL, parse, format, resolve } from 'node:url';
|
|
3
|
+
|
|
4
|
+
// Ported from refs/node-test/parallel/test-url-*.js and refs/node-test/parallel/test-whatwg-url-*.js
|
|
5
|
+
// Original: MIT license, Node.js contributors
|
|
5
6
|
|
|
6
7
|
export default async () => {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
8
|
+
// ==================== URL constructor ====================
|
|
9
|
+
|
|
10
|
+
await describe('URL constructor', async () => {
|
|
11
|
+
await it('should parse HTTP URL', async () => {
|
|
12
|
+
const u = new URL('http://example.com:8080/path?query=1#hash');
|
|
13
|
+
expect(u.protocol).toBe('http:');
|
|
14
|
+
expect(u.hostname).toBe('example.com');
|
|
15
|
+
expect(u.port).toBe('8080');
|
|
16
|
+
expect(u.pathname).toBe('/path');
|
|
17
|
+
expect(u.search).toBe('?query=1');
|
|
18
|
+
expect(u.hash).toBe('#hash');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
await it('should parse HTTPS URL', async () => {
|
|
22
|
+
const u = new URL('https://user:pass@example.com/path');
|
|
23
|
+
expect(u.protocol).toBe('https:');
|
|
24
|
+
expect(u.username).toBe('user');
|
|
25
|
+
expect(u.password).toBe('pass');
|
|
26
|
+
expect(u.hostname).toBe('example.com');
|
|
27
|
+
expect(u.pathname).toBe('/path');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
await it('should parse file URL', async () => {
|
|
31
|
+
const u = new URL('file:///tmp/test.txt');
|
|
32
|
+
expect(u.protocol).toBe('file:');
|
|
33
|
+
expect(u.pathname).toBe('/tmp/test.txt');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
await it('should parse relative URL with base', async () => {
|
|
37
|
+
const u = new URL('/path', 'http://example.com');
|
|
38
|
+
expect(u.pathname).toBe('/path');
|
|
39
|
+
expect(u.hostname).toBe('example.com');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
await it('should throw on invalid URL', async () => {
|
|
43
|
+
expect(() => new URL('not-a-url')).toThrow();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
await it('should support toString', async () => {
|
|
47
|
+
const u = new URL('http://example.com:8080/path');
|
|
48
|
+
const str = u.toString();
|
|
49
|
+
expect(str.includes('example.com')).toBeTruthy();
|
|
50
|
+
expect(str.includes('/path')).toBeTruthy();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
await it('should parse FTP URL', async () => {
|
|
54
|
+
const u = new URL('ftp://files.example.com/pub/docs');
|
|
55
|
+
expect(u.protocol).toBe('ftp:');
|
|
56
|
+
expect(u.hostname).toBe('files.example.com');
|
|
57
|
+
expect(u.pathname).toBe('/pub/docs');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
await it('should parse URL with empty port', async () => {
|
|
61
|
+
const u = new URL('http://example.com:/path');
|
|
62
|
+
expect(u.hostname).toBe('example.com');
|
|
63
|
+
expect(u.port).toBe('');
|
|
64
|
+
expect(u.pathname).toBe('/path');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
await it('should parse URL with only protocol and host', async () => {
|
|
68
|
+
const u = new URL('http://example.com');
|
|
69
|
+
expect(u.protocol).toBe('http:');
|
|
70
|
+
expect(u.hostname).toBe('example.com');
|
|
71
|
+
expect(u.pathname).toBe('/');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
await it('should parse URL with trailing slash', async () => {
|
|
75
|
+
const u = new URL('http://example.com/');
|
|
76
|
+
expect(u.pathname).toBe('/');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
await it('should parse URL with multiple path segments', async () => {
|
|
80
|
+
const u = new URL('http://example.com/a/b/c/d');
|
|
81
|
+
expect(u.pathname).toBe('/a/b/c/d');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
await it('should parse URL with hash only', async () => {
|
|
85
|
+
const u = new URL('http://example.com#frag');
|
|
86
|
+
expect(u.hash).toBe('#frag');
|
|
87
|
+
expect(u.search).toBe('');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
await it('should parse URL with search only', async () => {
|
|
91
|
+
const u = new URL('http://example.com?key=val');
|
|
92
|
+
expect(u.search).toBe('?key=val');
|
|
93
|
+
expect(u.hash).toBe('');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await it('should parse URL with search and hash', async () => {
|
|
97
|
+
const u = new URL('http://example.com?a=1#frag');
|
|
98
|
+
expect(u.search).toBe('?a=1');
|
|
99
|
+
expect(u.hash).toBe('#frag');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
await it('should throw on empty string without base', async () => {
|
|
103
|
+
expect(() => new URL('')).toThrow();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
await it('should resolve relative path with base', async () => {
|
|
107
|
+
const u = new URL('foo/bar', 'http://example.com/base/');
|
|
108
|
+
expect(u.pathname).toBe('/base/foo/bar');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
await it('should resolve .. in relative path with base', async () => {
|
|
112
|
+
const u = new URL('../bar', 'http://example.com/a/b/');
|
|
113
|
+
expect(u.pathname).toBe('/a/bar');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
await it('should handle base with query and hash', async () => {
|
|
117
|
+
const u = new URL('/new', 'http://example.com/old?q=1#h');
|
|
118
|
+
expect(u.pathname).toBe('/new');
|
|
119
|
+
expect(u.search).toBe('');
|
|
120
|
+
expect(u.hash).toBe('');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
await it('should accept URL object as input', async () => {
|
|
124
|
+
const original = new URL('http://example.com/path');
|
|
125
|
+
const copy = new URL(original);
|
|
126
|
+
expect(copy.href).toBe(original.href);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
await it('should accept URL object as base', async () => {
|
|
130
|
+
const base = new URL('http://example.com/base/');
|
|
131
|
+
const u = new URL('relative', base);
|
|
132
|
+
expect(u.hostname).toBe('example.com');
|
|
133
|
+
expect(u.pathname).toBe('/base/relative');
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// ==================== URL properties ====================
|
|
138
|
+
|
|
139
|
+
await describe('URL properties', async () => {
|
|
140
|
+
await it('should return correct searchParams', async () => {
|
|
141
|
+
const u = new URL('http://example.com/?a=1&b=2');
|
|
142
|
+
expect(u.searchParams.get('a')).toBe('1');
|
|
143
|
+
expect(u.searchParams.get('b')).toBe('2');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
await it('should handle empty search', async () => {
|
|
147
|
+
const u = new URL('http://example.com/path');
|
|
148
|
+
expect(u.search).toBe('');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
await it('should handle empty hash', async () => {
|
|
152
|
+
const u = new URL('http://example.com/path');
|
|
153
|
+
expect(u.hash).toBe('');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
await it('should return correct origin for HTTP', async () => {
|
|
157
|
+
const u = new URL('http://example.com:8080/path');
|
|
158
|
+
expect(u.origin).toBe('http://example.com:8080');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
await it('should return correct origin for HTTPS with default port', async () => {
|
|
162
|
+
const u = new URL('https://example.com/path');
|
|
163
|
+
expect(u.origin).toBe('https://example.com');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
await it('should return correct host with port', async () => {
|
|
167
|
+
const u = new URL('http://example.com:9090/');
|
|
168
|
+
expect(u.host).toBe('example.com:9090');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
await it('should return correct host without port', async () => {
|
|
172
|
+
const u = new URL('http://example.com/');
|
|
173
|
+
expect(u.host).toBe('example.com');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
await it('should strip default port 80 for HTTP', async () => {
|
|
177
|
+
const u = new URL('http://example.com:80/path');
|
|
178
|
+
expect(u.port).toBe('');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
await it('should strip default port 443 for HTTPS', async () => {
|
|
182
|
+
const u = new URL('https://example.com:443/path');
|
|
183
|
+
expect(u.port).toBe('');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
await it('should strip default port 21 for FTP', async () => {
|
|
187
|
+
const u = new URL('ftp://example.com:21/pub');
|
|
188
|
+
expect(u.port).toBe('');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
await it('should return non-default port', async () => {
|
|
192
|
+
const u = new URL('http://example.com:3000/');
|
|
193
|
+
expect(u.port).toBe('3000');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
await it('should return username from URL', async () => {
|
|
197
|
+
const u = new URL('http://admin@example.com/');
|
|
198
|
+
expect(u.username).toBe('admin');
|
|
199
|
+
expect(u.password).toBe('');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
await it('should return password from URL', async () => {
|
|
203
|
+
const u = new URL('http://admin:secret@example.com/');
|
|
204
|
+
expect(u.username).toBe('admin');
|
|
205
|
+
expect(u.password).toBe('secret');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
await it('should return empty username and password when absent', async () => {
|
|
209
|
+
const u = new URL('http://example.com/');
|
|
210
|
+
expect(u.username).toBe('');
|
|
211
|
+
expect(u.password).toBe('');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
await it('should return null origin for non-special protocols', async () => {
|
|
215
|
+
const u = new URL('data:text/plain,hello');
|
|
216
|
+
expect(u.origin).toBe('null');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
await it('toJSON should return href', async () => {
|
|
220
|
+
const u = new URL('http://example.com/path');
|
|
221
|
+
expect(u.toJSON()).toBe(u.href);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
await it('toString should return href', async () => {
|
|
225
|
+
const u = new URL('http://example.com/path');
|
|
226
|
+
expect(u.toString()).toBe(u.href);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
await it('should correctly serialize full URL', async () => {
|
|
230
|
+
const u = new URL('http://user:pass@example.com:8080/path?q=1#h');
|
|
231
|
+
expect(u.href).toBe('http://user:pass@example.com:8080/path?q=1#h');
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// ==================== URL special protocols ====================
|
|
236
|
+
|
|
237
|
+
await describe('URL special protocols', async () => {
|
|
238
|
+
await it('should handle http protocol', async () => {
|
|
239
|
+
const u = new URL('http://example.com');
|
|
240
|
+
expect(u.protocol).toBe('http:');
|
|
241
|
+
expect(u.origin).toBe('http://example.com');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
await it('should handle https protocol', async () => {
|
|
245
|
+
const u = new URL('https://example.com');
|
|
246
|
+
expect(u.protocol).toBe('https:');
|
|
247
|
+
expect(u.origin).toBe('https://example.com');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
await it('should handle ftp protocol', async () => {
|
|
251
|
+
const u = new URL('ftp://example.com');
|
|
252
|
+
expect(u.protocol).toBe('ftp:');
|
|
253
|
+
expect(u.origin).toBe('ftp://example.com');
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
await it('should handle file protocol', async () => {
|
|
257
|
+
const u = new URL('file:///path/to/file');
|
|
258
|
+
expect(u.protocol).toBe('file:');
|
|
259
|
+
expect(u.pathname).toBe('/path/to/file');
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
await it('should handle ws protocol', async () => {
|
|
263
|
+
const u = new URL('ws://example.com/socket');
|
|
264
|
+
expect(u.protocol).toBe('ws:');
|
|
265
|
+
expect(u.pathname).toBe('/socket');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
await it('should handle wss protocol', async () => {
|
|
269
|
+
const u = new URL('wss://example.com/socket');
|
|
270
|
+
expect(u.protocol).toBe('wss:');
|
|
271
|
+
expect(u.pathname).toBe('/socket');
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
await it('should handle data URL', async () => {
|
|
275
|
+
const u = new URL('data:text/plain,hello');
|
|
276
|
+
expect(u.protocol).toBe('data:');
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// ==================== URL percent encoding ====================
|
|
281
|
+
|
|
282
|
+
await describe('URL percent encoding', async () => {
|
|
283
|
+
await it('should encode spaces in path', async () => {
|
|
284
|
+
const u = new URL('http://example.com/hello%20world');
|
|
285
|
+
expect(u.pathname).toBe('/hello%20world');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
await it('should handle percent-encoded characters in query', async () => {
|
|
289
|
+
const u = new URL('http://example.com/?name=hello%20world');
|
|
290
|
+
expect(u.searchParams.get('name')).toBe('hello world');
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
await it('should preserve encoded characters in hash', async () => {
|
|
294
|
+
const u = new URL('http://example.com/#section%201');
|
|
295
|
+
expect(u.hash).toBe('#section%201');
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
await it('should handle encoded special characters', async () => {
|
|
299
|
+
const u = new URL('http://example.com/path%3Fwith%23special');
|
|
300
|
+
expect(u.pathname).toBe('/path%3Fwith%23special');
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
await it('should decode percent-encoded username', async () => {
|
|
304
|
+
const u = new URL('http://user%40name@example.com/');
|
|
305
|
+
expect(u.username).toBe('user%40name');
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// ==================== URLSearchParams ====================
|
|
310
|
+
|
|
311
|
+
await describe('URLSearchParams', async () => {
|
|
312
|
+
await it('should construct from string', async () => {
|
|
313
|
+
const params = new URLSearchParams('a=1&b=2');
|
|
314
|
+
expect(params.get('a')).toBe('1');
|
|
315
|
+
expect(params.get('b')).toBe('2');
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
await it('should construct from object', async () => {
|
|
319
|
+
const params = new URLSearchParams({ foo: 'bar', baz: 'qux' });
|
|
320
|
+
expect(params.get('foo')).toBe('bar');
|
|
321
|
+
expect(params.get('baz')).toBe('qux');
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
await it('should construct from entries', async () => {
|
|
325
|
+
const params = new URLSearchParams([['a', '1'], ['b', '2']]);
|
|
326
|
+
expect(params.get('a')).toBe('1');
|
|
327
|
+
expect(params.get('b')).toBe('2');
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
await it('should support get/set/has/delete', async () => {
|
|
331
|
+
const params = new URLSearchParams('a=1');
|
|
332
|
+
expect(params.has('a')).toBeTruthy();
|
|
333
|
+
expect(params.get('a')).toBe('1');
|
|
334
|
+
params.set('a', '2');
|
|
335
|
+
expect(params.get('a')).toBe('2');
|
|
336
|
+
params.delete('a');
|
|
337
|
+
expect(params.has('a')).toBeFalsy();
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
await it('should support append', async () => {
|
|
341
|
+
const params = new URLSearchParams();
|
|
342
|
+
params.append('a', '1');
|
|
343
|
+
params.append('a', '2');
|
|
344
|
+
expect(params.getAll('a').length).toBe(2);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
await it('should support toString', async () => {
|
|
348
|
+
const params = new URLSearchParams({ a: '1', b: '2' });
|
|
349
|
+
const str = params.toString();
|
|
350
|
+
expect(str.includes('a=1')).toBeTruthy();
|
|
351
|
+
expect(str.includes('b=2')).toBeTruthy();
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
await it('should support forEach', async () => {
|
|
355
|
+
const params = new URLSearchParams('a=1&b=2');
|
|
356
|
+
const keys: string[] = [];
|
|
357
|
+
params.forEach((_val, key) => keys.push(key));
|
|
358
|
+
expect(keys.length).toBe(2);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
await it('should support iteration', async () => {
|
|
362
|
+
const params = new URLSearchParams('a=1&b=2');
|
|
363
|
+
const entries: string[][] = [];
|
|
364
|
+
for (const [key, value] of params) {
|
|
365
|
+
entries.push([key, value]);
|
|
366
|
+
}
|
|
367
|
+
expect(entries.length).toBe(2);
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// ==================== URLSearchParams: append ====================
|
|
372
|
+
|
|
373
|
+
await describe('URLSearchParams append', async () => {
|
|
374
|
+
await it('should append multiple values for same key', async () => {
|
|
375
|
+
const params = new URLSearchParams();
|
|
376
|
+
params.append('key', 'val1');
|
|
377
|
+
params.append('key', 'val2');
|
|
378
|
+
params.append('key', 'val3');
|
|
379
|
+
expect(params.getAll('key').length).toBe(3);
|
|
380
|
+
expect(params.get('key')).toBe('val1');
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
await it('should append empty value', async () => {
|
|
384
|
+
const params = new URLSearchParams();
|
|
385
|
+
params.append('key', '');
|
|
386
|
+
expect(params.get('key')).toBe('');
|
|
387
|
+
expect(params.has('key')).toBeTruthy();
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
await it('should append and show in toString', async () => {
|
|
391
|
+
const params = new URLSearchParams();
|
|
392
|
+
params.append('a', '1');
|
|
393
|
+
params.append('b', '2');
|
|
394
|
+
expect(params.toString()).toBe('a=1&b=2');
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// ==================== URLSearchParams: delete ====================
|
|
399
|
+
|
|
400
|
+
await describe('URLSearchParams delete', async () => {
|
|
401
|
+
await it('should delete all values for a key', async () => {
|
|
402
|
+
const params = new URLSearchParams('a=1&a=2&b=3');
|
|
403
|
+
params.delete('a');
|
|
404
|
+
expect(params.has('a')).toBeFalsy();
|
|
405
|
+
expect(params.get('a')).toBeNull();
|
|
406
|
+
expect(params.get('b')).toBe('3');
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
await it('should not throw when deleting nonexistent key', async () => {
|
|
410
|
+
const params = new URLSearchParams('a=1');
|
|
411
|
+
params.delete('b');
|
|
412
|
+
expect(params.toString()).toBe('a=1');
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
await it('should update URL search when deleting all params', async () => {
|
|
416
|
+
const url = new URL('http://domain?var=1&var=2&var=3');
|
|
417
|
+
for (const param of url.searchParams.keys()) {
|
|
418
|
+
url.searchParams.delete(param);
|
|
419
|
+
}
|
|
420
|
+
expect(url.searchParams.toString()).toBe('');
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// ==================== URLSearchParams: get and getAll ====================
|
|
425
|
+
|
|
426
|
+
await describe('URLSearchParams get and getAll', async () => {
|
|
427
|
+
await it('should return null for nonexistent key', async () => {
|
|
428
|
+
const params = new URLSearchParams('a=1');
|
|
429
|
+
expect(params.get('b')).toBeNull();
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
await it('should return first value for duplicate keys', async () => {
|
|
433
|
+
const params = new URLSearchParams('a=1&a=2&a=3');
|
|
434
|
+
expect(params.get('a')).toBe('1');
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
await it('should return all values with getAll', async () => {
|
|
438
|
+
const params = new URLSearchParams('a=1&a=2&a=3');
|
|
439
|
+
const all = params.getAll('a');
|
|
440
|
+
expect(all.length).toBe(3);
|
|
441
|
+
expect(all[0]).toBe('1');
|
|
442
|
+
expect(all[1]).toBe('2');
|
|
443
|
+
expect(all[2]).toBe('3');
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
await it('should return empty array for nonexistent key with getAll', async () => {
|
|
447
|
+
const params = new URLSearchParams('a=1');
|
|
448
|
+
const all = params.getAll('b');
|
|
449
|
+
expect(all.length).toBe(0);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
await it('should handle empty value', async () => {
|
|
453
|
+
const params = new URLSearchParams('a=');
|
|
454
|
+
expect(params.get('a')).toBe('');
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
await it('should handle key without value', async () => {
|
|
458
|
+
const params = new URLSearchParams('a');
|
|
459
|
+
expect(params.get('a')).toBe('');
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// ==================== URLSearchParams: has ====================
|
|
464
|
+
|
|
465
|
+
await describe('URLSearchParams has', async () => {
|
|
466
|
+
await it('should return true for existing key', async () => {
|
|
467
|
+
const params = new URLSearchParams('a=1&b=2');
|
|
468
|
+
expect(params.has('a')).toBeTruthy();
|
|
469
|
+
expect(params.has('b')).toBeTruthy();
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
await it('should return false for nonexistent key', async () => {
|
|
473
|
+
const params = new URLSearchParams('a=1');
|
|
474
|
+
expect(params.has('b')).toBeFalsy();
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
await it('should return true for key with empty value', async () => {
|
|
478
|
+
const params = new URLSearchParams('a=');
|
|
479
|
+
expect(params.has('a')).toBeTruthy();
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
await it('should return false after delete', async () => {
|
|
483
|
+
const params = new URLSearchParams('a=1');
|
|
484
|
+
params.delete('a');
|
|
485
|
+
expect(params.has('a')).toBeFalsy();
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
// ==================== URLSearchParams: set ====================
|
|
490
|
+
|
|
491
|
+
await describe('URLSearchParams set', async () => {
|
|
492
|
+
await it('should set value for new key', async () => {
|
|
493
|
+
const params = new URLSearchParams();
|
|
494
|
+
params.set('a', '1');
|
|
495
|
+
expect(params.get('a')).toBe('1');
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
await it('should replace existing value', async () => {
|
|
499
|
+
const params = new URLSearchParams('a=old');
|
|
500
|
+
params.set('a', 'new');
|
|
501
|
+
expect(params.get('a')).toBe('new');
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
await it('should replace all duplicate values with single value', async () => {
|
|
505
|
+
const params = new URLSearchParams('a=1&a=2&a=3');
|
|
506
|
+
params.set('a', 'single');
|
|
507
|
+
expect(params.get('a')).toBe('single');
|
|
508
|
+
expect(params.getAll('a').length).toBe(1);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
await it('should preserve other keys when setting', async () => {
|
|
512
|
+
const params = new URLSearchParams('a=1&b=2&c=3');
|
|
513
|
+
params.set('b', 'new');
|
|
514
|
+
expect(params.get('a')).toBe('1');
|
|
515
|
+
expect(params.get('b')).toBe('new');
|
|
516
|
+
expect(params.get('c')).toBe('3');
|
|
517
|
+
});
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
// ==================== URLSearchParams: sort ====================
|
|
521
|
+
|
|
522
|
+
await describe('URLSearchParams sort', async () => {
|
|
523
|
+
await it('should sort parameters by key name', async () => {
|
|
524
|
+
const params = new URLSearchParams('z=a&c=d&a=b');
|
|
525
|
+
params.sort();
|
|
526
|
+
const result = params.toString();
|
|
527
|
+
expect(result).toBe('a=b&c=d&z=a');
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
await it('should sort with empty key', async () => {
|
|
531
|
+
const params = new URLSearchParams('z=a&=b&c=d');
|
|
532
|
+
params.sort();
|
|
533
|
+
const keys: string[] = [];
|
|
534
|
+
for (const [k] of params) {
|
|
535
|
+
keys.push(k);
|
|
536
|
+
}
|
|
537
|
+
expect(keys[0]).toBe('');
|
|
538
|
+
expect(keys[1]).toBe('c');
|
|
539
|
+
expect(keys[2]).toBe('z');
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
await it('should sort searchParams from URL', async () => {
|
|
543
|
+
const url = new URL('https://example.com/?z=1&a=2');
|
|
544
|
+
url.searchParams.sort();
|
|
545
|
+
const keys: string[] = [];
|
|
546
|
+
for (const [k] of url.searchParams) {
|
|
547
|
+
keys.push(k);
|
|
548
|
+
}
|
|
549
|
+
expect(keys[0]).toBe('a');
|
|
550
|
+
expect(keys[1]).toBe('z');
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
await it('should be stable sort for same keys', async () => {
|
|
554
|
+
const params = new URLSearchParams('a=2&a=1&a=3');
|
|
555
|
+
params.sort();
|
|
556
|
+
const values = params.getAll('a');
|
|
557
|
+
expect(values[0]).toBe('2');
|
|
558
|
+
expect(values[1]).toBe('1');
|
|
559
|
+
expect(values[2]).toBe('3');
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
// ==================== URLSearchParams: entries, keys, values ====================
|
|
564
|
+
|
|
565
|
+
await describe('URLSearchParams iterators', async () => {
|
|
566
|
+
await it('entries should return key-value pairs', async () => {
|
|
567
|
+
const params = new URLSearchParams('a=1&b=2');
|
|
568
|
+
const result: [string, string][] = [];
|
|
569
|
+
for (const entry of params.entries()) {
|
|
570
|
+
result.push(entry);
|
|
571
|
+
}
|
|
572
|
+
expect(result.length).toBe(2);
|
|
573
|
+
expect(result[0][0]).toBe('a');
|
|
574
|
+
expect(result[0][1]).toBe('1');
|
|
575
|
+
expect(result[1][0]).toBe('b');
|
|
576
|
+
expect(result[1][1]).toBe('2');
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
await it('keys should return all keys', async () => {
|
|
580
|
+
const params = new URLSearchParams('a=1&b=2&c=3');
|
|
581
|
+
const result: string[] = [];
|
|
582
|
+
for (const key of params.keys()) {
|
|
583
|
+
result.push(key);
|
|
584
|
+
}
|
|
585
|
+
expect(result.length).toBe(3);
|
|
586
|
+
expect(result[0]).toBe('a');
|
|
587
|
+
expect(result[1]).toBe('b');
|
|
588
|
+
expect(result[2]).toBe('c');
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
await it('values should return all values', async () => {
|
|
592
|
+
const params = new URLSearchParams('a=1&b=2&c=3');
|
|
593
|
+
const result: string[] = [];
|
|
594
|
+
for (const val of params.values()) {
|
|
595
|
+
result.push(val);
|
|
596
|
+
}
|
|
597
|
+
expect(result.length).toBe(3);
|
|
598
|
+
expect(result[0]).toBe('1');
|
|
599
|
+
expect(result[1]).toBe('2');
|
|
600
|
+
expect(result[2]).toBe('3');
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
await it('Symbol.iterator should be entries', async () => {
|
|
604
|
+
const params = new URLSearchParams('a=1');
|
|
605
|
+
const result: [string, string][] = [];
|
|
606
|
+
for (const entry of params) {
|
|
607
|
+
result.push(entry);
|
|
608
|
+
}
|
|
609
|
+
expect(result.length).toBe(1);
|
|
610
|
+
expect(result[0][0]).toBe('a');
|
|
611
|
+
expect(result[0][1]).toBe('1');
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
await it('keys should include duplicate keys', async () => {
|
|
615
|
+
const params = new URLSearchParams('a=1&a=2');
|
|
616
|
+
const result: string[] = [];
|
|
617
|
+
for (const key of params.keys()) {
|
|
618
|
+
result.push(key);
|
|
619
|
+
}
|
|
620
|
+
expect(result.length).toBe(2);
|
|
621
|
+
expect(result[0]).toBe('a');
|
|
622
|
+
expect(result[1]).toBe('a');
|
|
623
|
+
});
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
// ==================== URLSearchParams: forEach ====================
|
|
627
|
+
|
|
628
|
+
await describe('URLSearchParams forEach', async () => {
|
|
629
|
+
await it('should call callback for each entry', async () => {
|
|
630
|
+
const params = new URLSearchParams('a=1&b=2&c=3');
|
|
631
|
+
let count = 0;
|
|
632
|
+
params.forEach(() => { count++; });
|
|
633
|
+
expect(count).toBe(3);
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
await it('should pass value, key, and searchParams to callback', async () => {
|
|
637
|
+
const params = new URLSearchParams('x=42');
|
|
638
|
+
params.forEach((value, key, parent) => {
|
|
639
|
+
expect(key).toBe('x');
|
|
640
|
+
expect(value).toBe('42');
|
|
641
|
+
expect(parent).toBe(params);
|
|
642
|
+
});
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
await it('should iterate in insertion order', async () => {
|
|
646
|
+
const params = new URLSearchParams();
|
|
647
|
+
params.append('c', '3');
|
|
648
|
+
params.append('a', '1');
|
|
649
|
+
params.append('b', '2');
|
|
650
|
+
const order: string[] = [];
|
|
651
|
+
params.forEach((_v, k) => order.push(k));
|
|
652
|
+
expect(order[0]).toBe('c');
|
|
653
|
+
expect(order[1]).toBe('a');
|
|
654
|
+
expect(order[2]).toBe('b');
|
|
655
|
+
});
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
// ==================== URLSearchParams: toString ====================
|
|
659
|
+
|
|
660
|
+
await describe('URLSearchParams toString', async () => {
|
|
661
|
+
await it('should return empty string for empty params', async () => {
|
|
662
|
+
const params = new URLSearchParams();
|
|
663
|
+
expect(params.toString()).toBe('');
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
await it('should encode spaces as +', async () => {
|
|
667
|
+
const params = new URLSearchParams('q=hello world');
|
|
668
|
+
expect(params.toString()).toBe('q=hello+world');
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
await it('should encode special characters', async () => {
|
|
672
|
+
const params = new URLSearchParams();
|
|
673
|
+
params.append('key', 'value&with=special');
|
|
674
|
+
const str = params.toString();
|
|
675
|
+
expect(str.includes('key=')).toBeTruthy();
|
|
676
|
+
// should not contain raw & or = in value
|
|
677
|
+
expect(str.split('&').length).toBe(1);
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
await it('should handle multiple params', async () => {
|
|
681
|
+
const params = new URLSearchParams('a=1&b=2&c=3');
|
|
682
|
+
expect(params.toString()).toBe('a=1&b=2&c=3');
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
await it('should encode plus signs in values', async () => {
|
|
686
|
+
const params = new URLSearchParams();
|
|
687
|
+
params.set('a', '1+2');
|
|
688
|
+
const str = params.toString();
|
|
689
|
+
// plus signs in values should be encoded
|
|
690
|
+
expect(str.includes('1%2B2') || str.includes('1+2')).toBeTruthy();
|
|
691
|
+
});
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
// ==================== URLSearchParams: size ====================
|
|
695
|
+
|
|
696
|
+
await describe('URLSearchParams size', async () => {
|
|
697
|
+
await it('should return 0 for empty params', async () => {
|
|
698
|
+
const params = new URLSearchParams();
|
|
699
|
+
expect(params.size).toBe(0);
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
await it('should return correct count', async () => {
|
|
703
|
+
const params = new URLSearchParams('a=1&b=2&c=3');
|
|
704
|
+
expect(params.size).toBe(3);
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
await it('should count duplicate keys separately', async () => {
|
|
708
|
+
const params = new URLSearchParams();
|
|
709
|
+
params.append('a', '1');
|
|
710
|
+
params.append('a', '2');
|
|
711
|
+
params.append('b', '3');
|
|
712
|
+
expect(params.size).toBe(3);
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
await it('should decrease after delete', async () => {
|
|
716
|
+
const params = new URLSearchParams('a=1&b=2');
|
|
717
|
+
expect(params.size).toBe(2);
|
|
718
|
+
params.delete('a');
|
|
719
|
+
expect(params.size).toBe(1);
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
await it('should increase after append', async () => {
|
|
723
|
+
const params = new URLSearchParams('a=1');
|
|
724
|
+
expect(params.size).toBe(1);
|
|
725
|
+
params.append('b', '2');
|
|
726
|
+
expect(params.size).toBe(2);
|
|
727
|
+
});
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
// ==================== URLSearchParams: construction edge cases ====================
|
|
731
|
+
|
|
732
|
+
await describe('URLSearchParams construction edge cases', async () => {
|
|
733
|
+
await it('should handle string with leading ?', async () => {
|
|
734
|
+
const params = new URLSearchParams('?a=1&b=2');
|
|
735
|
+
expect(params.get('a')).toBe('1');
|
|
736
|
+
expect(params.get('b')).toBe('2');
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
await it('should handle empty string', async () => {
|
|
740
|
+
const params = new URLSearchParams('');
|
|
741
|
+
expect(params.size).toBe(0);
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
await it('should handle string with only ?', async () => {
|
|
745
|
+
const params = new URLSearchParams('?');
|
|
746
|
+
expect(params.size).toBe(0);
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
await it('should construct from another URLSearchParams', async () => {
|
|
750
|
+
const original = new URLSearchParams('a=1&b=2');
|
|
751
|
+
const copy = new URLSearchParams(original);
|
|
752
|
+
expect(copy.get('a')).toBe('1');
|
|
753
|
+
expect(copy.get('b')).toBe('2');
|
|
754
|
+
// modifying copy should not affect original
|
|
755
|
+
copy.set('a', 'changed');
|
|
756
|
+
expect(original.get('a')).toBe('1');
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
await it('should handle plus signs as spaces', async () => {
|
|
760
|
+
const params = new URLSearchParams('q=hello+world');
|
|
761
|
+
expect(params.get('q')).toBe('hello world');
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
await it('should handle encoded characters', async () => {
|
|
765
|
+
const params = new URLSearchParams('q=%E2%82%AC');
|
|
766
|
+
expect(params.get('q')).toBe('\u20AC'); // Euro sign
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
await it('should handle param without value', async () => {
|
|
770
|
+
const params = new URLSearchParams('key');
|
|
771
|
+
expect(params.has('key')).toBeTruthy();
|
|
772
|
+
expect(params.get('key')).toBe('');
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
await it('should handle multiple equal signs', async () => {
|
|
776
|
+
const params = new URLSearchParams('a=1=2=3');
|
|
777
|
+
expect(params.get('a')).toBe('1=2=3');
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
await it('should handle empty key and value', async () => {
|
|
781
|
+
const params = new URLSearchParams('=');
|
|
782
|
+
expect(params.has('')).toBeTruthy();
|
|
783
|
+
expect(params.get('')).toBe('');
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
await it('should handle object with hasOwnProperty key', async () => {
|
|
787
|
+
const params = new URLSearchParams({ hasOwnProperty: '1' } as Record<string, string>);
|
|
788
|
+
expect(params.get('hasOwnProperty')).toBe('1');
|
|
789
|
+
});
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
// ==================== URL.searchParams integration ====================
|
|
793
|
+
|
|
794
|
+
await describe('URL.searchParams integration', async () => {
|
|
795
|
+
await it('should return searchParams linked to URL', async () => {
|
|
796
|
+
const u = new URL('http://example.com/?a=1');
|
|
797
|
+
const sp = u.searchParams;
|
|
798
|
+
expect(sp.get('a')).toBe('1');
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
await it('should return same searchParams instance', async () => {
|
|
802
|
+
const u = new URL('http://example.com/?a=1');
|
|
803
|
+
expect(u.searchParams).toBe(u.searchParams);
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
await it('should return empty searchParams for no query', async () => {
|
|
807
|
+
const u = new URL('http://example.com/');
|
|
808
|
+
expect(u.searchParams.toString()).toBe('');
|
|
809
|
+
expect(u.searchParams.size).toBe(0);
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
await it('should handle complex query params via URL', async () => {
|
|
813
|
+
const u = new URL('http://example.com/?a=1&b=2&c=3&a=4');
|
|
814
|
+
expect(u.searchParams.get('a')).toBe('1');
|
|
815
|
+
expect(u.searchParams.getAll('a').length).toBe(2);
|
|
816
|
+
expect(u.searchParams.get('b')).toBe('2');
|
|
817
|
+
expect(u.searchParams.get('c')).toBe('3');
|
|
818
|
+
});
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
// ==================== URL relative resolution ====================
|
|
822
|
+
|
|
823
|
+
await describe('URL relative resolution', async () => {
|
|
824
|
+
await it('should resolve absolute path against base', async () => {
|
|
825
|
+
const u = new URL('/new/path', 'http://example.com/old/path');
|
|
826
|
+
expect(u.pathname).toBe('/new/path');
|
|
827
|
+
expect(u.hostname).toBe('example.com');
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
await it('should resolve relative path against base directory', async () => {
|
|
831
|
+
const u = new URL('sub', 'http://example.com/dir/');
|
|
832
|
+
expect(u.pathname).toBe('/dir/sub');
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
await it('should resolve query-only relative URL', async () => {
|
|
836
|
+
const u = new URL('?newquery', 'http://example.com/path');
|
|
837
|
+
expect(u.pathname).toBe('/path');
|
|
838
|
+
expect(u.search).toBe('?newquery');
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
await it('should resolve hash-only relative URL', async () => {
|
|
842
|
+
const u = new URL('#newhash', 'http://example.com/path');
|
|
843
|
+
expect(u.pathname).toBe('/path');
|
|
844
|
+
expect(u.hash).toBe('#newhash');
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
await it('should resolve full URL ignoring base', async () => {
|
|
848
|
+
const u = new URL('http://other.com/page', 'http://example.com/');
|
|
849
|
+
expect(u.hostname).toBe('other.com');
|
|
850
|
+
expect(u.pathname).toBe('/page');
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
await it('should handle double dot path resolution', async () => {
|
|
854
|
+
const u = new URL('../../file', 'http://example.com/a/b/c/');
|
|
855
|
+
expect(u.pathname).toBe('/a/file');
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
await it('should handle single dot path resolution', async () => {
|
|
859
|
+
const u = new URL('./', 'http://example.com/a/b/');
|
|
860
|
+
expect(u.pathname).toBe('/a/b/');
|
|
861
|
+
});
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
// ==================== fileURLToPath ====================
|
|
865
|
+
|
|
866
|
+
await describe('fileURLToPath', async () => {
|
|
867
|
+
const testCases = [
|
|
868
|
+
{ path: '/foo', fileURL: 'file:///foo' },
|
|
869
|
+
{ path: '/FOO', fileURL: 'file:///FOO' },
|
|
870
|
+
{ path: '/dir/foo', fileURL: 'file:///dir/foo' },
|
|
871
|
+
{ path: '/dir/', fileURL: 'file:///dir/' },
|
|
872
|
+
{ path: '/foo.mjs', fileURL: 'file:///foo.mjs' },
|
|
873
|
+
{ path: '/foo bar', fileURL: 'file:///foo%20bar' },
|
|
874
|
+
{ path: '/foo?bar', fileURL: 'file:///foo%3Fbar' },
|
|
875
|
+
{ path: '/foo#bar', fileURL: 'file:///foo%23bar' },
|
|
876
|
+
{ path: '/foo&bar', fileURL: 'file:///foo&bar' },
|
|
877
|
+
{ path: '/foo%bar', fileURL: 'file:///foo%25bar' },
|
|
878
|
+
{ path: '/fóóbàr', fileURL: 'file:///f%C3%B3%C3%B3b%C3%A0r' },
|
|
879
|
+
{ path: '/€', fileURL: 'file:///%E2%82%AC' },
|
|
880
|
+
{ path: '/🚀', fileURL: 'file:///%F0%9F%9A%80' },
|
|
881
|
+
];
|
|
882
|
+
|
|
883
|
+
for (const tc of testCases) {
|
|
884
|
+
await it(`should convert ${tc.fileURL} to ${tc.path}`, async () => {
|
|
885
|
+
expect(fileURLToPath(tc.fileURL)).toBe(tc.path);
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
await it(`should convert URL object for ${tc.path}`, async () => {
|
|
889
|
+
expect(fileURLToPath(new URL(tc.fileURL))).toBe(tc.path);
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
await it('should throw for non-file protocol', async () => {
|
|
894
|
+
expect(() => fileURLToPath('https://example.com')).toThrow();
|
|
895
|
+
});
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
// ==================== pathToFileURL ====================
|
|
899
|
+
|
|
900
|
+
await describe('pathToFileURL', async () => {
|
|
901
|
+
await it('should convert absolute path', async () => {
|
|
902
|
+
const u = pathToFileURL('/foo/bar');
|
|
903
|
+
expect(u.protocol).toBe('file:');
|
|
904
|
+
expect(u.pathname).toBe('/foo/bar');
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
await it('should encode special characters', async () => {
|
|
908
|
+
const u = pathToFileURL('/foo bar');
|
|
909
|
+
expect(u.href).toBe('file:///foo%20bar');
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
await it('should handle root path', async () => {
|
|
913
|
+
const u = pathToFileURL('/');
|
|
914
|
+
expect(u.pathname).toBe('/');
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
await it('should handle path with dots', async () => {
|
|
918
|
+
const u = pathToFileURL('/dir/file.txt');
|
|
919
|
+
expect(u.pathname).toBe('/dir/file.txt');
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
await it('should handle deeply nested path', async () => {
|
|
923
|
+
const u = pathToFileURL('/a/b/c/d/e');
|
|
924
|
+
expect(u.pathname).toBe('/a/b/c/d/e');
|
|
925
|
+
expect(u.protocol).toBe('file:');
|
|
926
|
+
});
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
// ==================== legacy parse ====================
|
|
930
|
+
|
|
931
|
+
await describe('url.parse (legacy)', async () => {
|
|
932
|
+
await it('should parse simple HTTP URL', async () => {
|
|
933
|
+
const parsed = parse('http://example.com/path?query=value#hash');
|
|
934
|
+
expect(parsed.protocol).toBe('http:');
|
|
935
|
+
expect(parsed.hostname).toBe('example.com');
|
|
936
|
+
expect(parsed.pathname).toBe('/path');
|
|
937
|
+
expect(parsed.hash).toBe('#hash');
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
await it('should parse URL with port', async () => {
|
|
941
|
+
const parsed = parse('http://example.com:8080/path');
|
|
942
|
+
expect(parsed.port).toBe('8080');
|
|
943
|
+
expect(parsed.hostname).toBe('example.com');
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
await it('should parse URL with auth', async () => {
|
|
947
|
+
const parsed = parse('http://user:pass@example.com/');
|
|
948
|
+
expect(parsed.auth).toBe('user:pass');
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
await it('should parse URL without path', async () => {
|
|
952
|
+
const parsed = parse('http://example.com');
|
|
953
|
+
expect(parsed.hostname).toBe('example.com');
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
await it('should parse query string', async () => {
|
|
957
|
+
const parsed = parse('http://example.com?foo=bar&baz=quux');
|
|
958
|
+
expect(parsed.search).toBe('?foo=bar&baz=quux');
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
await it('should parse HTTPS URL', async () => {
|
|
962
|
+
const parsed = parse('https://secure.example.com/login');
|
|
963
|
+
expect(parsed.protocol).toBe('https:');
|
|
964
|
+
expect(parsed.hostname).toBe('secure.example.com');
|
|
965
|
+
expect(parsed.pathname).toBe('/login');
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
await it('should handle URL with only hash', async () => {
|
|
969
|
+
const parsed = parse('http://example.com#fragment');
|
|
970
|
+
expect(parsed.hash).toBe('#fragment');
|
|
971
|
+
expect(parsed.hostname).toBe('example.com');
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
await it('should return null for missing protocol', async () => {
|
|
975
|
+
const parsed = parse('/path/to/resource');
|
|
976
|
+
expect(parsed.protocol).toBeNull();
|
|
977
|
+
expect(parsed.pathname).toBe('/path/to/resource');
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
await it('should handle empty search/query', async () => {
|
|
981
|
+
const parsed = parse('http://example.com/path');
|
|
982
|
+
expect(parsed.search).toBeNull();
|
|
983
|
+
expect(parsed.query).toBeNull();
|
|
984
|
+
});
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
// ==================== legacy format ====================
|
|
988
|
+
|
|
989
|
+
await describe('url.format (legacy)', async () => {
|
|
990
|
+
await it('should format parsed URL back to string', async () => {
|
|
991
|
+
const original = 'http://example.com/path?query=1#hash';
|
|
992
|
+
const parsed = parse(original);
|
|
993
|
+
const formatted = format(parsed);
|
|
994
|
+
expect(formatted).toBe(original);
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
await it('should format URL from object', async () => {
|
|
998
|
+
const result = format({
|
|
999
|
+
protocol: 'http:',
|
|
1000
|
+
hostname: 'example.com',
|
|
1001
|
+
pathname: '/path',
|
|
1002
|
+
});
|
|
1003
|
+
expect(result).toBe('http://example.com/path');
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
await it('should format URL with port', async () => {
|
|
1007
|
+
const result = format({
|
|
1008
|
+
protocol: 'http:',
|
|
1009
|
+
hostname: 'example.com',
|
|
1010
|
+
port: '3000',
|
|
1011
|
+
pathname: '/',
|
|
1012
|
+
});
|
|
1013
|
+
expect(result).toBe('http://example.com:3000/');
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
await it('should format URL with search', async () => {
|
|
1017
|
+
const result = format({
|
|
1018
|
+
protocol: 'https:',
|
|
1019
|
+
hostname: 'example.com',
|
|
1020
|
+
pathname: '/search',
|
|
1021
|
+
search: '?q=test',
|
|
1022
|
+
});
|
|
1023
|
+
expect(result).toBe('https://example.com/search?q=test');
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
await it('should format URL with hash', async () => {
|
|
1027
|
+
const result = format({
|
|
1028
|
+
protocol: 'http:',
|
|
1029
|
+
hostname: 'example.com',
|
|
1030
|
+
pathname: '/page',
|
|
1031
|
+
hash: '#section',
|
|
1032
|
+
});
|
|
1033
|
+
expect(result).toBe('http://example.com/page#section');
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
await it('should format URL with all components', async () => {
|
|
1037
|
+
const result = format({
|
|
1038
|
+
protocol: 'http:',
|
|
1039
|
+
hostname: 'example.com',
|
|
1040
|
+
port: '8080',
|
|
1041
|
+
pathname: '/path',
|
|
1042
|
+
search: '?a=1',
|
|
1043
|
+
hash: '#top',
|
|
1044
|
+
});
|
|
1045
|
+
expect(result).toBe('http://example.com:8080/path?a=1#top');
|
|
1046
|
+
});
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
// ==================== legacy resolve ====================
|
|
1050
|
+
|
|
1051
|
+
await describe('url.resolve (legacy)', async () => {
|
|
1052
|
+
await it('should resolve absolute URL', async () => {
|
|
1053
|
+
const result = resolve('http://example.com/a/b', 'http://other.com/c');
|
|
1054
|
+
expect(result.includes('other.com')).toBeTruthy();
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
await it('should resolve relative path', async () => {
|
|
1058
|
+
const result = resolve('http://example.com/a/b', '/c');
|
|
1059
|
+
expect(result.includes('example.com')).toBeTruthy();
|
|
1060
|
+
expect(result.includes('/c')).toBeTruthy();
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
await it('should resolve sibling path', async () => {
|
|
1064
|
+
const result = resolve('http://example.com/a/b', 'c');
|
|
1065
|
+
expect(result).toBe('http://example.com/a/c');
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
await it('should resolve parent path with ..', async () => {
|
|
1069
|
+
const result = resolve('http://example.com/a/b/c', '../d');
|
|
1070
|
+
expect(result).toBe('http://example.com/a/d');
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
await it('should resolve with query string', async () => {
|
|
1074
|
+
const result = resolve('http://example.com/path', '?q=1');
|
|
1075
|
+
expect(result.includes('example.com')).toBeTruthy();
|
|
1076
|
+
expect(result.includes('q=1')).toBeTruthy();
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
await it('should resolve with hash', async () => {
|
|
1080
|
+
const result = resolve('http://example.com/path', '#frag');
|
|
1081
|
+
expect(result.includes('#frag')).toBeTruthy();
|
|
1082
|
+
});
|
|
1083
|
+
});
|
|
1084
|
+
};
|