@devp0nt/route0 1.0.0-next.2 → 1.0.0-next.21

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/src/index.test.ts CHANGED
@@ -1,13 +1,32 @@
1
1
  import { describe, expect, expectTypeOf, it } from 'bun:test'
2
- import { Route0 } from './index.js'
2
+ import type {
3
+ AnyRoute,
4
+ CallabelRoute,
5
+ Extended,
6
+ HasParams,
7
+ HasSearch,
8
+ IsChildren,
9
+ IsParent,
10
+ IsSame,
11
+ IsSameParams,
12
+ ParamsInput,
13
+ ParamsOutput,
14
+ SearchInput,
15
+ SearchOutput,
16
+ StrictSearchInput,
17
+ StrictSearchOutput,
18
+ } from './index.js'
19
+ import { Route0, Routes } from './index.js'
3
20
 
4
- describe('route0', () => {
21
+ describe('Route0', () => {
5
22
  it('simple', () => {
6
23
  const route0 = Route0.create('/')
7
24
  const path = route0.get()
8
25
  expect(route0).toBeInstanceOf(Route0)
9
26
  expectTypeOf<typeof path>().toEqualTypeOf<'/'>()
10
27
  expect(path).toBe('/')
28
+ expectTypeOf<HasParams<typeof route0>>().toEqualTypeOf<false>()
29
+ expect(path).toBe(route0.flat())
11
30
  })
12
31
 
13
32
  it('simple, callable', () => {
@@ -16,13 +35,15 @@ describe('route0', () => {
16
35
  expect(route0).toBeInstanceOf(Route0)
17
36
  expectTypeOf<typeof path>().toEqualTypeOf<'/'>()
18
37
  expect(path).toBe('/')
38
+ expect(path).toBe(route0.flat())
19
39
  })
20
40
 
21
- it('simple any query', () => {
41
+ it('simple any search', () => {
22
42
  const route0 = Route0.create('/')
23
- const path = route0.get({ query: { q: '1' } })
43
+ const path = route0.get({ search: { q: '1' } })
24
44
  expectTypeOf<typeof path>().toEqualTypeOf<`/?${string}`>()
25
45
  expect(path).toBe('/?q=1')
46
+ expect(path).toBe(route0.flat({ q: '1' }))
26
47
  })
27
48
 
28
49
  it('params', () => {
@@ -30,35 +51,42 @@ describe('route0', () => {
30
51
  const path = route0.get({ x: '1', y: 2, z: '3' })
31
52
  expectTypeOf<typeof path>().toEqualTypeOf<`/prefix/${string}/some/${string}/${string}`>()
32
53
  expect(path).toBe('/prefix/1/some/2/3')
54
+ expectTypeOf<HasParams<typeof route0>>().toEqualTypeOf<true>()
55
+ expect(path).toBe(route0.flat({ x: '1', y: 2, z: '3' }))
33
56
  })
34
57
 
35
- it('params and any query', () => {
58
+ it('params and any search', () => {
36
59
  const route0 = Route0.create('/prefix/:x/some/:y/:z')
37
- const path = route0.get({ x: '1', y: 2, z: '3', query: { q: '1' } })
60
+ const path = route0.get({ x: '1', y: 2, z: '3', search: { q: '1' } })
38
61
  expectTypeOf<typeof path>().toEqualTypeOf<`/prefix/${string}/some/${string}/${string}?${string}`>()
39
62
  expect(path).toBe('/prefix/1/some/2/3?q=1')
63
+ expect(path).toBe(route0.flat({ x: '1', y: 2, z: '3', q: '1' }))
40
64
  })
41
65
 
42
- it('query', () => {
66
+ it('search', () => {
43
67
  const route0 = Route0.create('/prefix&y&z')
44
- expectTypeOf<(typeof route0)['queryDefinition']>().toEqualTypeOf<{ y: true; z: true }>()
45
- const path = route0.get({ query: { y: '1', z: '2' } })
68
+ expectTypeOf<(typeof route0)['searchDefinition']>().toEqualTypeOf<{ y: true; z: true }>()
69
+ const path = route0.get({ search: { y: '1', z: '2' } })
46
70
  expectTypeOf<typeof path>().toEqualTypeOf<`/prefix?${string}`>()
47
71
  expect(path).toBe('/prefix?y=1&z=2')
72
+ expect(path).toBe(route0.flat({ y: '1', z: '2' }))
48
73
  })
49
74
 
50
- it('params and query', () => {
75
+ it('params and search', () => {
51
76
  const route0 = Route0.create('/prefix/:x/some/:y/:z&z&c')
52
- const path = route0.get({ x: '1', y: '2', z: '3', query: { z: '4', c: '5' } })
77
+ const path = route0.get({ x: '1', y: '2', z: '3', search: { z: '4', c: '5' } })
53
78
  expectTypeOf<typeof path>().toEqualTypeOf<`/prefix/${string}/some/${string}/${string}?${string}`>()
54
79
  expect(path).toBe('/prefix/1/some/2/3?z=4&c=5')
80
+ expect(route0.flat({ x: '1', y: '2', z: '4', c: '5' })).toBe('/prefix/1/some/2/4?z=4&c=5')
55
81
  })
56
82
 
57
- it('params and query and any query', () => {
83
+ it('params and search and any search', () => {
58
84
  const route0 = Route0.create('/prefix/:x/some/:y/:z&z&c')
59
- const path = route0.get({ x: '1', y: '2', z: '3', query: { z: '4', c: '5', o: '6' } })
85
+ const path = route0.get({ x: '1', y: '2', z: '3', search: { z: '4', c: '5', o: '6' } })
60
86
  expectTypeOf<typeof path>().toEqualTypeOf<`/prefix/${string}/some/${string}/${string}?${string}`>()
61
87
  expect(path).toBe('/prefix/1/some/2/3?z=4&c=5&o=6')
88
+ // very strange case
89
+ expect(route0.flat({ x: '1', y: '2', z: '4', c: '5', o: '6' })).toBe('/prefix/1/some/2/4?z=4&c=5&o=6')
62
90
  })
63
91
 
64
92
  it('simple extend', () => {
@@ -67,6 +95,7 @@ describe('route0', () => {
67
95
  const path = route1.get()
68
96
  expectTypeOf<typeof path>().toEqualTypeOf<`/prefix/suffix`>()
69
97
  expect(path).toBe('/prefix/suffix')
98
+ expect(path).toBe(route1.flat())
70
99
  })
71
100
 
72
101
  it('simple extend double slash', () => {
@@ -76,6 +105,7 @@ describe('route0', () => {
76
105
  const path = route2.get()
77
106
  expectTypeOf<typeof path>().toEqualTypeOf<`/suffix1/suffix2`>()
78
107
  expect(path).toBe('/suffix1/suffix2')
108
+ expect(path).toBe(route2.flat())
79
109
  })
80
110
 
81
111
  it('simple extend no slash', () => {
@@ -85,6 +115,7 @@ describe('route0', () => {
85
115
  const path = route2.get()
86
116
  expectTypeOf<typeof path>().toEqualTypeOf<`/suffix1/suffix2`>()
87
117
  expect(path).toBe('/suffix1/suffix2')
118
+ expect(path).toBe(route2.flat())
88
119
  })
89
120
 
90
121
  it('extend with params', () => {
@@ -93,13 +124,14 @@ describe('route0', () => {
93
124
  const path = route1.get({ x: '1', y: '2' })
94
125
  expectTypeOf<typeof path>().toEqualTypeOf<`/prefix/${string}/suffix/${string}`>()
95
126
  expect(path).toBe('/prefix/1/suffix/2')
127
+ expect(path).toBe(route1.flat({ x: '1', y: '2' }))
96
128
  })
97
129
 
98
130
  it('extend with search params', () => {
99
131
  const route0 = Route0.create('/prefix&y&z')
100
132
  const route1 = route0.extend('/suffix&z&c')
101
- const path = route1.get({ query: { y: '2', c: '3', a: '4' } })
102
- expectTypeOf<(typeof route1)['queryDefinition']>().toEqualTypeOf<{
133
+ const path = route1.get({ search: { y: '2', c: '3', a: '4' } })
134
+ expectTypeOf<(typeof route1)['searchDefinition']>().toEqualTypeOf<{
103
135
  z: true
104
136
  c: true
105
137
  }>()
@@ -108,13 +140,14 @@ describe('route0', () => {
108
140
  const path1 = route1.get()
109
141
  expectTypeOf<typeof path1>().toEqualTypeOf<`/prefix/suffix`>()
110
142
  expect(path1).toBe('/prefix/suffix')
143
+ expect(path1).toBe(route1.flat())
111
144
  })
112
145
 
113
- it('extend with params and query', () => {
146
+ it('extend with params and search', () => {
114
147
  const route0 = Route0.create('/prefix/:id&y&z')
115
148
  const route1 = route0.extend('/:sn/suffix&z&c')
116
- const path = route1.get({ id: 'myid', sn: 'mysn', query: { y: '2', c: '3', a: '4' } })
117
- expectTypeOf<(typeof route1)['queryDefinition']>().toEqualTypeOf<{
149
+ const path = route1.get({ id: 'myid', sn: 'mysn', search: { y: '2', c: '3', a: '4' } })
150
+ expectTypeOf<(typeof route1)['searchDefinition']>().toEqualTypeOf<{
118
151
  z: true
119
152
  c: true
120
153
  }>()
@@ -123,13 +156,14 @@ describe('route0', () => {
123
156
  const path1 = route1.get({ id: 'myid', sn: 'mysn' })
124
157
  expectTypeOf<typeof path1>().toEqualTypeOf<`/prefix/${string}/${string}/suffix`>()
125
158
  expect(path1).toBe('/prefix/myid/mysn/suffix')
159
+ expect(path1).toBe(route1.flat({ id: 'myid', sn: 'mysn' }))
126
160
  })
127
161
 
128
- it('extend with params and query, callable', () => {
162
+ it('extend with params and search, callable', () => {
129
163
  const route0 = Route0.create('/prefix/:id&y&z')
130
164
  const route1 = route0.extend('/:sn/suffix&z&c')
131
- const path = route1({ id: 'myid', sn: 'mysn', query: { y: '2', c: '3', a: '4' } })
132
- expectTypeOf<(typeof route1)['queryDefinition']>().toEqualTypeOf<{
165
+ const path = route1({ id: 'myid', sn: 'mysn', search: { y: '2', c: '3', a: '4' } })
166
+ expectTypeOf<(typeof route1)['searchDefinition']>().toEqualTypeOf<{
133
167
  z: true
134
168
  c: true
135
169
  }>()
@@ -138,6 +172,7 @@ describe('route0', () => {
138
172
  const path1 = route1({ id: 'myid', sn: 'mysn' })
139
173
  expectTypeOf<typeof path1>().toEqualTypeOf<`/prefix/${string}/${string}/suffix`>()
140
174
  expect(path1).toBe('/prefix/myid/mysn/suffix')
175
+ expect(path1).toBe(route1.flat({ id: 'myid', sn: 'mysn' }))
141
176
  })
142
177
 
143
178
  it('abs default', () => {
@@ -145,6 +180,7 @@ describe('route0', () => {
145
180
  const path = route0.get({ abs: true })
146
181
  expectTypeOf<typeof path>().toEqualTypeOf<`${string}/path`>()
147
182
  expect(path).toBe('https://example.com/path')
183
+ expect(path).toBe(route0.flat({}, true))
148
184
  })
149
185
 
150
186
  it('abs set', () => {
@@ -152,6 +188,7 @@ describe('route0', () => {
152
188
  const path = route0.get({ abs: true })
153
189
  expectTypeOf<typeof path>().toEqualTypeOf<`${string}/path`>()
154
190
  expect(path).toBe('https://x.com/path')
191
+ expect(path).toBe(route0.flat({}, true))
155
192
  })
156
193
 
157
194
  it('abs override', () => {
@@ -160,6 +197,7 @@ describe('route0', () => {
160
197
  const path = route0.get({ abs: true })
161
198
  expectTypeOf<typeof path>().toEqualTypeOf<`${string}/path`>()
162
199
  expect(path).toBe('https://y.com/path')
200
+ expect(path).toBe(route0.flat({}, true))
163
201
  })
164
202
 
165
203
  it('abs override extend', () => {
@@ -169,38 +207,1047 @@ describe('route0', () => {
169
207
  const path = route1.get({ abs: true })
170
208
  expectTypeOf<typeof path>().toEqualTypeOf<`${string}/path/suffix`>()
171
209
  expect(path).toBe('https://y.com/path/suffix')
210
+ expect(path).toBe(route1.flat({}, true))
172
211
  })
173
212
 
174
- it('abs override many', () => {
175
- const route0 = Route0.create('/path', { baseUrl: 'https://x.com' })
176
- const route1 = route0.extend('/suffix')
177
- const routes = {
178
- r0: route0,
179
- r1: route1,
180
- }
181
- const routes2 = Route0.overrideMany(routes, { baseUrl: 'https://z.com' })
182
- const path = routes2.r1.get({ abs: true })
183
- expectTypeOf<typeof path>().toEqualTypeOf<`${string}/path/suffix`>()
184
- expect(path).toBe('https://z.com/path/suffix')
185
- })
213
+ // it('abs override many', () => {
214
+ // const route0 = Route0.create('/path', { baseUrl: 'https://x.com' })
215
+ // const route1 = route0.extend('/suffix')
216
+ // const routes = {
217
+ // r0: route0,
218
+ // r1: route1,
219
+ // }
220
+ // const routes2 = Route0.overrideMany(routes, { baseUrl: 'https://z.com' })
221
+ // const path = routes2.r1.get({ abs: true })
222
+ // expectTypeOf<typeof path>().toEqualTypeOf<`${string}/path/suffix`>()
223
+ // expect(path).toBe('https://z.com/path/suffix')
224
+ // })
186
225
 
187
226
  it('type errors: require params when defined', () => {
188
227
  const rWith = Route0.create('/a/:id')
189
228
  // @ts-expect-error missing required path params
190
229
  expect(rWith.get()).toBe('/a/undefined')
230
+ // @ts-expect-error missing required path params
231
+ expect(rWith.flat()).toBe('/a/undefined')
191
232
 
192
233
  // @ts-expect-error missing required path params
193
234
  expect(rWith.get({})).toBe('/a/undefined')
235
+ // @ts-expect-error missing required path params
236
+ expect(rWith.flat({})).toBe('/a/undefined')
194
237
  // @ts-expect-error missing required path params (object form abs)
195
238
  expect(rWith.get({ abs: true })).toBe('https://example.com/a/undefined')
196
- // @ts-expect-error missing required path params (object form query)
197
- expect(rWith.get({ query: { q: '1' } })).toBe('/a/undefined?q=1')
239
+ // @ts-expect-error missing required path params (object form abs)
240
+ expect(rWith.flat({}, true)).toBe('https://example.com/a/undefined')
241
+ // @ts-expect-error missing required path params (object form search)
242
+ expect(rWith.get({ search: { q: '1' } })).toBe('/a/undefined?q=1')
243
+ // @ts-expect-error missing required path params (object form search)
244
+ expect(rWith.flat({ q: '1' })).toBe('/a/undefined?q=1')
198
245
 
199
246
  // @ts-expect-error params can not be sent as object value it should be argument
200
247
  rWith.get({ params: { id: '1' } }) // not throw becouse this will not used
248
+ expect(rWith.flat({ id: '1' })).toBe('/a/1')
201
249
 
202
250
  const rNo = Route0.create('/b')
203
251
  // @ts-expect-error no path params allowed for this route (shorthand)
204
252
  expect(rNo.get({ id: '1' })).toBe('/b')
253
+ expect(rNo.flat({ id: '1' })).toBe('/b?id=1')
254
+ })
255
+
256
+ it('really any route assignable to AnyRoute', () => {
257
+ expectTypeOf<Route0<string>>().toExtend<AnyRoute>()
258
+ expectTypeOf<Route0<'/path'>>().toExtend<AnyRoute>()
259
+ expectTypeOf<Route0<'/path/:id'>>().toExtend<AnyRoute>()
260
+ expectTypeOf<Route0<'/path/:id&x'>>().toExtend<AnyRoute>()
261
+ expectTypeOf<CallabelRoute<'/path'>>().toExtend<AnyRoute>()
262
+ expectTypeOf<CallabelRoute<'/path/:id'>>().toExtend<AnyRoute>()
263
+ expectTypeOf<CallabelRoute<'/path/:id&x'>>().toExtend<AnyRoute>()
264
+ expectTypeOf<CallabelRoute>().toExtend<AnyRoute>()
265
+ })
266
+ })
267
+
268
+ describe('type utilities', () => {
269
+ it('HasParams', () => {
270
+ expectTypeOf<HasParams<'/path'>>().toEqualTypeOf<false>()
271
+ expectTypeOf<HasParams<'/path/:id'>>().toEqualTypeOf<true>()
272
+ expectTypeOf<HasParams<'/path/:id/:name'>>().toEqualTypeOf<true>()
273
+
274
+ expectTypeOf<HasParams<Route0<'/path'>>>().toEqualTypeOf<false>()
275
+ expectTypeOf<HasParams<Route0<'/path/:id'>>>().toEqualTypeOf<true>()
276
+ })
277
+
278
+ it('HasSearch', () => {
279
+ expectTypeOf<HasSearch<'/path'>>().toEqualTypeOf<false>()
280
+ expectTypeOf<HasSearch<'/path&x'>>().toEqualTypeOf<true>()
281
+ expectTypeOf<HasSearch<'/path&x&y'>>().toEqualTypeOf<true>()
282
+
283
+ expectTypeOf<HasSearch<Route0<'/path'>>>().toEqualTypeOf<false>()
284
+ expectTypeOf<HasSearch<Route0<'/path&x&y'>>>().toEqualTypeOf<true>()
285
+ })
286
+
287
+ it('ParamsInput', () => {
288
+ expectTypeOf<ParamsInput<'/path'>>().toEqualTypeOf<Record<never, never>>()
289
+ expectTypeOf<ParamsInput<'/path/:id'>>().toEqualTypeOf<{ id: string | number }>()
290
+ expectTypeOf<ParamsInput<'/path/:id/:name'>>().toEqualTypeOf<{ id: string | number; name: string | number }>()
291
+
292
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
293
+ const route = Route0.create('/path/:id/:name')
294
+ expectTypeOf<ParamsInput<typeof route>>().toEqualTypeOf<{ id: string | number; name: string | number }>()
295
+ })
296
+
297
+ it('ParamsOutput', () => {
298
+ expectTypeOf<ParamsOutput<'/path/:id'>>().toEqualTypeOf<{ id: string }>()
299
+ expectTypeOf<ParamsOutput<'/path/:id/:name'>>().toEqualTypeOf<{ id: string; name: string }>()
300
+
301
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
302
+ const route = Route0.create('/path/:id/:name')
303
+ expectTypeOf<ParamsOutput<typeof route>>().toEqualTypeOf<{ id: string; name: string }>()
304
+ })
305
+
306
+ it('SearchInput', () => {
307
+ type T1 = SearchInput<'/path'>
308
+ expectTypeOf<T1>().toEqualTypeOf<Record<string, string | number>>()
309
+
310
+ type T2 = SearchInput<'/path&x&y'>
311
+ expectTypeOf<T2>().toEqualTypeOf<
312
+ Partial<{
313
+ x: string | number
314
+ y: string | number
315
+ }> &
316
+ Record<string, string | number>
317
+ >()
318
+ })
319
+
320
+ it('SearchOutput', () => {
321
+ type T1 = SearchOutput<'/path'>
322
+ expectTypeOf<T1>().toEqualTypeOf<{
323
+ [key: string]: string | undefined
324
+ }>()
325
+
326
+ type T2 = SearchOutput<'/path&x&y'>
327
+ expectTypeOf<T2>().toEqualTypeOf<{
328
+ [key: string]: string | undefined
329
+ x?: string | undefined
330
+ y?: string | undefined
331
+ }>()
332
+ })
333
+
334
+ it('StrictSearchInput', () => {
335
+ type T1 = StrictSearchInput<'/path&x&y'>
336
+ expectTypeOf<T1>().toEqualTypeOf<{ x?: string | number; y?: string | number }>()
337
+ })
338
+
339
+ it('StrictSearchOutput', () => {
340
+ type T1 = StrictSearchOutput<'/path&x&y'>
341
+ expectTypeOf<T1>().toEqualTypeOf<{ x?: string | undefined; y?: string | undefined }>()
342
+ })
343
+
344
+ it('IsParent', () => {
345
+ type T1 = IsParent<'/path/child', '/path'>
346
+ type T2 = IsParent<'/path', '/path/child'>
347
+ type T3 = IsParent<'/other', '/path'>
348
+ type T4 = IsParent<'/path', '/path'>
349
+ expectTypeOf<T1>().toEqualTypeOf<true>()
350
+ expectTypeOf<T2>().toEqualTypeOf<false>()
351
+ expectTypeOf<T3>().toEqualTypeOf<false>()
352
+ expectTypeOf<T4>().toEqualTypeOf<false>()
353
+ })
354
+
355
+ it('IsChildren', () => {
356
+ type T1 = IsChildren<'/path', '/path/child'>
357
+ type T2 = IsChildren<'/path/child', '/path'>
358
+ type T3 = IsChildren<'/path', '/other'>
359
+ type T4 = IsChildren<'/path', '/path'>
360
+ expectTypeOf<T1>().toEqualTypeOf<true>()
361
+ expectTypeOf<T2>().toEqualTypeOf<false>()
362
+ expectTypeOf<T3>().toEqualTypeOf<false>()
363
+ expectTypeOf<T4>().toEqualTypeOf<false>()
364
+ })
365
+
366
+ it('IsSame', () => {
367
+ type T1 = IsSame<'/path', '/path'>
368
+ type T2 = IsSame<'/path', '/path/child'>
369
+ type T3 = IsSame<'/path/child', '/path'>
370
+ expectTypeOf<T1>().toEqualTypeOf<true>()
371
+ expectTypeOf<T2>().toEqualTypeOf<false>()
372
+ expectTypeOf<T3>().toEqualTypeOf<false>()
373
+ })
374
+
375
+ it('IsSameParams', () => {
376
+ type T1 = IsSameParams<'/path', '/other'>
377
+ type T2 = IsSameParams<'/path/:id', '/other/:id'>
378
+ type T3 = IsSameParams<'/path/:id', '/other'>
379
+ type T4 = IsSameParams<'/path/:id', '/other/:name'>
380
+ expectTypeOf<T1>().toEqualTypeOf<true>()
381
+ expectTypeOf<T2>().toEqualTypeOf<true>()
382
+ expectTypeOf<T3>().toEqualTypeOf<false>()
383
+ expectTypeOf<T4>().toEqualTypeOf<false>()
384
+ })
385
+
386
+ it('Extended', () => {
387
+ expectTypeOf<Extended<'/path', '/child'>>().toEqualTypeOf<Route0<'/path/child'>>()
388
+ expectTypeOf<Extended<'/path', '/:id'>>().toEqualTypeOf<Route0<'/path/:id'>>()
389
+ expectTypeOf<Extended<'/path', '&x&y'>>().toEqualTypeOf<Route0<'/path&x&y'>>()
390
+ expectTypeOf<Extended<'/path/:id', '/child&x'>>().toEqualTypeOf<Route0<'/path/:id/child&x'>>()
391
+ expectTypeOf<Extended<undefined, '/path'>>().toEqualTypeOf<Route0<'/path'>>()
392
+
393
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
394
+ const parent = Route0.create('/path')
395
+ expectTypeOf<Extended<typeof parent, '/child'>>().toEqualTypeOf<Route0<'/path/child'>>()
396
+ })
397
+ })
398
+
399
+ describe('getLocation', () => {
400
+ describe('Route0', () => {
401
+ it('.getLocation location of url', () => {
402
+ let loc = Route0.getLocation('/prefix/some/suffix')
403
+ expect(loc).toMatchObject({
404
+ hash: '',
405
+ href: undefined,
406
+ hrefRel: '/prefix/some/suffix',
407
+ abs: false,
408
+ origin: undefined,
409
+ params: undefined,
410
+ pathname: '/prefix/some/suffix',
411
+ searchParams: {},
412
+ search: '',
413
+ })
414
+ loc = Route0.getLocation('/prefix/some/suffix?x=1&z=2')
415
+ expect(loc).toMatchObject({
416
+ hash: '',
417
+ href: undefined,
418
+ hrefRel: '/prefix/some/suffix?x=1&z=2',
419
+ abs: false,
420
+ origin: undefined,
421
+ params: undefined,
422
+ pathname: '/prefix/some/suffix',
423
+ searchParams: { x: '1', z: '2' },
424
+ search: '?x=1&z=2',
425
+ })
426
+ loc = Route0.getLocation('https://example.com/prefix/some/suffix?x=1&z=2')
427
+ expect(loc).toMatchObject({
428
+ hash: '',
429
+ href: 'https://example.com/prefix/some/suffix?x=1&z=2',
430
+ hrefRel: '/prefix/some/suffix?x=1&z=2',
431
+ abs: true,
432
+ origin: 'https://example.com',
433
+ params: undefined,
434
+ pathname: '/prefix/some/suffix',
435
+ searchParams: { x: '1', z: '2' },
436
+ search: '?x=1&z=2',
437
+ })
438
+ })
439
+
440
+ it('#getLocation() exact match', () => {
441
+ const route0 = Route0.create('/prefix/:x/some/:y/:z/suffix')
442
+ let loc = route0.getLocation('/prefix/some/suffix')
443
+ expect(loc.exact).toBe(false)
444
+ expect(loc.parent).toBe(false)
445
+ expect(loc.children).toBe(false)
446
+ expect(loc.params).toMatchObject({})
447
+ loc = route0.getLocation('/prefix/xxx/some/yyy/zzz/suffix')
448
+ expect(loc.exact).toBe(true)
449
+ expect(loc.parent).toBe(false)
450
+ expect(loc.children).toBe(false)
451
+ if (loc.exact) {
452
+ expectTypeOf<typeof loc.params>().toEqualTypeOf<{ x: string; y: string; z: string }>()
453
+ }
454
+ expect(loc.params).toMatchObject({ x: 'xxx', y: 'yyy', z: 'zzz' })
455
+ })
456
+
457
+ it('#getLocation() parent match', () => {
458
+ const route0 = Route0.create('/prefix/:x/some')
459
+ const loc = route0.getLocation('/prefix/xxx/some/extra/path')
460
+ expect(loc.exact).toBe(false)
461
+ expect(loc.parent).toBe(true)
462
+ expect(loc.children).toBe(false)
463
+ })
464
+
465
+ it('#getLocation() children match', () => {
466
+ const route0 = Route0.create('/prefix/some/extra/path')
467
+ const loc = route0.getLocation('/prefix/some')
468
+ expect(loc.exact).toBe(false)
469
+ expect(loc.parent).toBe(false)
470
+ expect(loc.children).toBe(true)
471
+ })
472
+
473
+ it('#getLocation() with host info', () => {
474
+ const route0 = Route0.create('/path')
475
+ const loc = route0.getLocation('https://example.com:8080/path')
476
+ expect(loc.exact).toBe(true)
477
+ expect(loc.origin).toBe('https://example.com:8080')
478
+ expect(loc.host).toBe('example.com:8080')
479
+ expect(loc.hostname).toBe('example.com')
480
+ expect(loc.port).toBe('8080')
481
+ })
482
+
483
+ it('#getLocation() with hash', () => {
484
+ const route0 = Route0.create('/path/:id')
485
+ const loc = route0.getLocation('/path/123#section')
486
+ expect(loc.exact).toBe(true)
487
+ expect(loc.hash).toBe('#section')
488
+ expect(loc.params).toMatchObject({ id: '123' })
489
+ })
490
+ })
491
+
492
+ describe('Routes', () => {
493
+ it('exact match returns ExactLocation', () => {
494
+ const routes = Routes.create({
495
+ home: '/',
496
+ users: '/users',
497
+ userDetail: '/users/:id',
498
+ })
499
+
500
+ const loc = routes.getLocation('/users/123')
501
+ expect(loc.exact).toBe(true)
502
+ expect(loc.parent).toBe(false)
503
+ expect(loc.children).toBe(false)
504
+ expect(loc.pathname).toBe('/users/123')
505
+ expect(Route0.isSame(loc.route, routes.userDetail)).toBe(true)
506
+ if (loc.exact) {
507
+ expect(loc.params).toMatchObject({ id: '123' })
508
+ }
509
+ })
510
+
511
+ it('no exact match returns UnknownLocation (parent case)', () => {
512
+ const routes = Routes.create({
513
+ home: '/',
514
+ users: '/users',
515
+ userDetail: '/users/:id',
516
+ })
517
+
518
+ // '/users/123/posts' is not an exact match for any route
519
+ const loc = routes.getLocation('/users/123/posts')
520
+ expect(loc.exact).toBe(false)
521
+ expect(loc.parent).toBe(false)
522
+ expect(loc.children).toBe(false)
523
+ expect(loc.pathname).toBe('/users/123/posts')
524
+ })
525
+
526
+ it('no exact match returns UnknownLocation (children case)', () => {
527
+ const routes = Routes.create({
528
+ home: '/',
529
+ users: '/users',
530
+ userDetail: '/users/:id/posts',
531
+ })
532
+
533
+ // '/users/123' is not an exact match for any route
534
+ const loc = routes.getLocation('/users/123')
535
+ expect(loc.exact).toBe(false)
536
+ expect(loc.parent).toBe(false)
537
+ expect(loc.children).toBe(false)
538
+ expect(loc.pathname).toBe('/users/123')
539
+ })
540
+
541
+ it('no match returns UnknownLocation', () => {
542
+ const routes = Routes.create({
543
+ home: '/',
544
+ users: '/users',
545
+ })
546
+
547
+ const loc = routes.getLocation('/posts/123')
548
+ expect(loc.exact).toBe(false)
549
+ expect(loc.parent).toBe(false)
550
+ expect(loc.children).toBe(false)
551
+ expect(loc.pathname).toBe('/posts/123')
552
+ expect(loc.params).toBeUndefined()
553
+ })
554
+
555
+ it('matches most specific route', () => {
556
+ const routes = Routes.create({
557
+ userDetail: '/users/:id',
558
+ userPosts: '/users/:id/posts',
559
+ users: '/users',
560
+ })
561
+
562
+ // Should match /users exactly
563
+ const loc1 = routes.getLocation('/users')
564
+ expect(loc1.exact).toBe(true)
565
+ expect(loc1.pathname).toBe('/users')
566
+
567
+ // Should match /users/:id exactly
568
+ const loc2 = routes.getLocation('/users/123')
569
+ expect(loc2.exact).toBe(true)
570
+ if (loc2.exact) {
571
+ expect(loc2.params).toMatchObject({ id: '123' })
572
+ }
573
+
574
+ // Should match /users/:id/posts exactly
575
+ const loc3 = routes.getLocation('/users/123/posts')
576
+ expect(loc3.exact).toBe(true)
577
+ if (loc3.exact) {
578
+ expect(loc3.params).toMatchObject({ id: '123' })
579
+ }
580
+ })
581
+
582
+ it('with search params', () => {
583
+ const routes = Routes.create({
584
+ search: '/search&q&filter',
585
+ users: '/users',
586
+ })
587
+
588
+ const loc = routes.getLocation('/search?q=test&filter=all')
589
+ expect(loc.exact).toBe(true)
590
+ expect(loc.pathname).toBe('/search')
591
+ expect(loc.search).toBe('?q=test&filter=all')
592
+ expect(loc.searchParams).toMatchObject({ q: 'test', filter: 'all' })
593
+ })
594
+
595
+ it('with absolute URL', () => {
596
+ const routes = Routes.create({
597
+ api: '/api/v1',
598
+ users: '/api/v1/users',
599
+ })
600
+
601
+ const loc = routes.getLocation('https://example.com/api/v1/users')
602
+ expect(loc.exact).toBe(true)
603
+ expect(loc.abs).toBe(true)
604
+ expect(loc.origin).toBe('https://example.com')
605
+ expect(loc.pathname).toBe('/api/v1/users')
606
+ expect(loc.href).toBe('https://example.com/api/v1/users')
607
+ })
608
+
609
+ it('with hash', () => {
610
+ const routes = Routes.create({
611
+ userDetail: '/users/:id',
612
+ })
613
+
614
+ const loc = routes.getLocation('/users/123#profile')
615
+ expect(loc.exact).toBe(true)
616
+ expect(loc.hash).toBe('#profile')
617
+ expect(loc.pathname).toBe('/users/123')
618
+ if (loc.exact) {
619
+ expect(loc.params).toMatchObject({ id: '123' })
620
+ }
621
+ })
622
+
623
+ it('with extended routes', () => {
624
+ const api = Route0.create('/api/v1')
625
+ const routes = Routes.create({
626
+ api,
627
+ users: api.extend('/users'),
628
+ userDetail: api.extend('/users/:id'),
629
+ })
630
+
631
+ const loc = routes.getLocation('/api/v1/users/456')
632
+ expect(loc.exact).toBe(true)
633
+ if (loc.exact) {
634
+ expect(loc.params).toMatchObject({ id: '456' })
635
+ }
636
+ })
637
+
638
+ it('root route', () => {
639
+ const routes = Routes.create({
640
+ home: '/',
641
+ about: '/about',
642
+ })
643
+
644
+ const loc = routes.getLocation('/')
645
+ expect(loc.exact).toBe(true)
646
+ expect(loc.pathname).toBe('/')
647
+ })
648
+
649
+ it('with LocationAny object as input', () => {
650
+ const routes = Routes.create({
651
+ userDetail: '/users/:id',
652
+ })
653
+
654
+ const inputLoc = Route0.getLocation('/users/789')
655
+ const loc = routes.getLocation(inputLoc)
656
+ expect(loc.exact).toBe(true)
657
+ if (loc.exact) {
658
+ expect(loc.params).toMatchObject({ id: '789' })
659
+ }
660
+ })
661
+
662
+ it('complex routing with params and search', () => {
663
+ const api = Route0.create('/api/v1')
664
+ const routes = Routes.create({
665
+ api,
666
+ users: api.extend('/users'),
667
+ userDetail: api.extend('/users/:id'),
668
+ userPosts: api.extend('/users/:id/posts&sort&filter'),
669
+ })
670
+
671
+ const loc = routes.getLocation('/api/v1/users/42/posts?sort=date&filter=published&extra=value')
672
+ expect(loc.exact).toBe(true)
673
+ expect(loc.pathname).toBe('/api/v1/users/42/posts')
674
+ expect(loc.searchParams).toMatchObject({
675
+ sort: 'date',
676
+ filter: 'published',
677
+ extra: 'value',
678
+ })
679
+ if (loc.exact) {
680
+ expect(loc.params).toMatchObject({ id: '42' })
681
+ }
682
+ })
683
+ })
684
+ })
685
+
686
+ describe('Routes', () => {
687
+ it('create with string routes', () => {
688
+ const collection = Routes.create({
689
+ home: '/',
690
+ about: '/about',
691
+ contact: '/contact',
692
+ })
693
+
694
+ expect(collection).toBeInstanceOf(Routes)
695
+ const home = collection.home
696
+ const about = collection.about
697
+ const contact = collection.contact
698
+
699
+ expect(home).toBeInstanceOf(Route0)
700
+ expect(about).toBeInstanceOf(Route0)
701
+ expect(contact).toBeInstanceOf(Route0)
702
+
703
+ expect(home.get()).toBe('/')
704
+ expect(about.get()).toBe('/about')
705
+ expect(contact.get()).toBe('/contact')
706
+ })
707
+
708
+ it('create with Route0 instances', () => {
709
+ const homeRoute = Route0.create('/')
710
+ const aboutRoute = Route0.create('/about')
711
+
712
+ const collection = Routes.create({
713
+ home: homeRoute,
714
+ about: aboutRoute,
715
+ })
716
+
717
+ expect(collection.home.get()).toBe('/')
718
+ expect(collection.about.get()).toBe('/about')
719
+ })
720
+
721
+ it('create with mixed string and Route0', () => {
722
+ const aboutRoute = Route0.create('/about')
723
+
724
+ const collection = Routes.create({
725
+ home: '/',
726
+ about: aboutRoute,
727
+ contact: '/contact',
728
+ })
729
+
730
+ expect(collection.home.get()).toBe('/')
731
+ expect(collection.about.get()).toBe('/about')
732
+ expect(collection.contact.get()).toBe('/contact')
733
+ })
734
+
735
+ it('create with params and search', () => {
736
+ const collection = Routes.create({
737
+ user: '/user/:id',
738
+ search: '/search&q&filter',
739
+ userWithSearch: '/user/:id&tab',
740
+ })
741
+
742
+ const user = collection.user
743
+ expect(user.get({ id: '123' } as any)).toBe('/user/123')
744
+
745
+ const search = collection.search
746
+ expect(search.get({ search: { q: 'test', filter: 'all' } })).toBe('/search?q=test&filter=all')
747
+
748
+ const userWithSearch = collection.userWithSearch
749
+ expect(userWithSearch.get({ id: '456', search: { tab: 'posts' } } as any)).toBe('/user/456?tab=posts')
750
+ })
751
+
752
+ it('get maintains route definitions', () => {
753
+ const collection = Routes.create({
754
+ home: '/',
755
+ user: '/user/:id',
756
+ })
757
+
758
+ const home = collection.home
759
+ const user = collection.user
760
+
761
+ // Verify route definitions are preserved
762
+ expect(home.definition).toBe('/')
763
+ expect(user.definition).toBe('/user/:id')
764
+ expect(home.pathDefinition).toBe('/')
765
+ expect(user.pathDefinition).toBe('/user/:id')
766
+
767
+ // Verify params work correctly
768
+ expect(user.get({ id: '123' })).toBe('/user/123')
769
+ })
770
+
771
+ it('override with baseUrl', () => {
772
+ const collection = Routes.create({
773
+ home: '/',
774
+ about: '/about',
775
+ })
776
+
777
+ const overridden = collection.override({ baseUrl: 'https://example.com' })
778
+
779
+ const home = overridden.home
780
+ const about = overridden.about
781
+
782
+ expect(home.get({ abs: true })).toBe('https://example.com')
783
+ expect(about.get({ abs: true })).toBe('https://example.com/about')
784
+ })
785
+
786
+ it('override does not mutate original', () => {
787
+ const collection = Routes.create({
788
+ home: '/',
789
+ })
790
+
791
+ const original = collection.home
792
+ expect(original.get({ abs: true })).toBe('https://example.com')
793
+
794
+ const overridden = collection.override({ baseUrl: 'https://newdomain.com' })
795
+ const newRoute = overridden.home
796
+
797
+ expect(original.get({ abs: true })).toBe('https://example.com')
798
+ expect(newRoute.get({ abs: true })).toBe('https://newdomain.com')
799
+ })
800
+
801
+ it('override with extended routes', () => {
802
+ const apiRoute = Route0.create('/api', { baseUrl: 'https://api.example.com' })
803
+ const usersRoute = apiRoute.extend('/users')
804
+
805
+ const collection = Routes.create({
806
+ api: apiRoute,
807
+ users: usersRoute,
808
+ })
809
+
810
+ expect(collection.api.get({ abs: true })).toBe('https://api.example.com/api')
811
+ expect(collection.api({ abs: true })).toBe('https://api.example.com/api')
812
+ expect(collection.users.get({ abs: true })).toBe('https://api.example.com/api/users')
813
+
814
+ const overridden = collection.override({ baseUrl: 'https://new-api.example.com' })
815
+
816
+ expect(overridden.api.get({ abs: true })).toBe('https://new-api.example.com/api')
817
+ expect(overridden.users.get({ abs: true })).toBe('https://new-api.example.com/api/users')
818
+ })
819
+
820
+ it('hydrate static method', () => {
821
+ const hydrated = Routes._hydrate({
822
+ home: '/',
823
+ user: '/user/:id',
824
+ about: Route0.create('/about'),
825
+ })
826
+
827
+ expect(hydrated.home).toBeInstanceOf(Route0)
828
+ expect(hydrated.user).toBeInstanceOf(Route0)
829
+ expect(hydrated.about).toBeInstanceOf(Route0)
830
+
831
+ expect(hydrated.home.get()).toBe('/')
832
+ expect(hydrated.user.get({ id: '123' })).toBe('/user/123')
833
+ expect(hydrated.about.get()).toBe('/about')
834
+ })
835
+
836
+ it('works with callable routes', () => {
837
+ const collection = Routes.create({
838
+ home: '/',
839
+ user: '/user/:id',
840
+ })
841
+
842
+ const home = collection.home
843
+ const user = collection.user
844
+
845
+ // Routes should be callable
846
+ expect(typeof home).toBe('function')
847
+ expect(typeof user).toBe('function')
848
+ expect(home()).toBe('/')
849
+ expect(user({ id: '789' })).toBe('/user/789')
850
+ })
851
+
852
+ it('complex nested structure', () => {
853
+ const api = Route0.create('/api/v1', { baseUrl: 'https://api.example.com' })
854
+
855
+ const collection = Routes.create({
856
+ root: '/',
857
+ api,
858
+ users: api.extend('/users'),
859
+ userDetail: api.extend('/users/:id'),
860
+ userPosts: api.extend('/users/:id/posts&sort&filter'),
861
+ })
862
+
863
+ expect(collection.root.get()).toBe('/')
864
+ expect(collection.api({ abs: true })).toBe('https://api.example.com/api/v1')
865
+ expect(collection.users.get({ abs: true })).toBe('https://api.example.com/api/v1/users')
866
+
867
+ const userDetailPath: any = collection.userDetail.get({ id: '42', abs: true })
868
+ expect(userDetailPath).toBe('https://api.example.com/api/v1/users/42')
869
+
870
+ const userPostsPath: any = collection.userPosts.get({
871
+ id: '42',
872
+ search: { sort: 'date', filter: 'published' },
873
+ abs: true,
874
+ })
875
+ expect(userPostsPath).toBe('https://api.example.com/api/v1/users/42/posts?sort=date&filter=published')
876
+ })
877
+ })
878
+
879
+ describe('specificity', () => {
880
+ it('isMoreSpecificThan: static vs param', () => {
881
+ const static1 = Route0.create('/a/b')
882
+ const param1 = Route0.create('/a/:id')
883
+
884
+ expect(static1.isMoreSpecificThan(param1)).toBe(true)
885
+ expect(param1.isMoreSpecificThan(static1)).toBe(false)
886
+ })
887
+
888
+ it('isMoreSpecificThan: more static segments wins', () => {
889
+ const twoStatic = Route0.create('/a/b/c')
890
+ const oneStatic = Route0.create('/a/:id/c')
891
+ const noStatic = Route0.create('/a/:id/:name')
892
+
893
+ expect(twoStatic.isMoreSpecificThan(oneStatic)).toBe(true)
894
+ expect(oneStatic.isMoreSpecificThan(twoStatic)).toBe(false)
895
+
896
+ expect(oneStatic.isMoreSpecificThan(noStatic)).toBe(true)
897
+ expect(noStatic.isMoreSpecificThan(oneStatic)).toBe(false)
898
+
899
+ expect(twoStatic.isMoreSpecificThan(noStatic)).toBe(true)
900
+ expect(noStatic.isMoreSpecificThan(twoStatic)).toBe(false)
901
+ })
902
+
903
+ it('isMoreSpecificThan: compares overlapping segments then lexicographically', () => {
904
+ const longer = Route0.create('/a/:id/b/:name')
905
+ const shorter = Route0.create('/a/:id')
906
+
907
+ // Both have same pattern for overlapping segments: static then param
908
+ // Falls back to lexicographic: '/a/:id' < '/a/:id/b/:name'
909
+ expect(longer.isMoreSpecificThan(shorter)).toBe(false)
910
+ expect(shorter.isMoreSpecificThan(longer)).toBe(true)
911
+ })
912
+
913
+ it('isMoreSpecificThan: static at earlier position wins', () => {
914
+ const route1 = Route0.create('/a/static/:param')
915
+ const route2 = Route0.create('/a/:param/static')
916
+
917
+ // Both have 2 static segments and same length
918
+ // route1 has static at position 1, route2 has param at position 1
919
+ expect(route1.isMoreSpecificThan(route2)).toBe(true)
920
+ expect(route2.isMoreSpecificThan(route1)).toBe(false)
921
+ })
922
+
923
+ it('isMoreSpecificThan: lexicographic when completely equal', () => {
924
+ const route1 = Route0.create('/aaa/:id')
925
+ const route2 = Route0.create('/bbb/:id')
926
+
927
+ // Same specificity, lexicographic comparison
928
+ expect(route1.isMoreSpecificThan(route2)).toBe(true)
929
+ expect(route2.isMoreSpecificThan(route1)).toBe(false)
930
+ })
931
+
932
+ it('isMoreSpecificThan: identical routes', () => {
933
+ const route1 = Route0.create('/a/:id')
934
+ const route2 = Route0.create('/a/:id')
935
+
936
+ // Identical routes, lexicographic comparison returns false for equal strings
937
+ expect(route1.isMoreSpecificThan(route2)).toBe(false)
938
+ expect(route2.isMoreSpecificThan(route1)).toBe(false)
939
+ })
940
+
941
+ it('isMoreSpecificThan: root vs other routes', () => {
942
+ const root = Route0.create('/')
943
+ const other = Route0.create('/a')
944
+ const param = Route0.create('/:id')
945
+
946
+ // /a (1 static) vs / (1 static) - both static, lexicographic order
947
+ expect(other.isMoreSpecificThan(root)).toBe(false) // '/' < '/a' lexicographically
948
+ expect(root.isMoreSpecificThan(other)).toBe(true)
949
+
950
+ // /a (1 static) vs /:id (0 static) - static beats param
951
+ expect(other.isMoreSpecificThan(param)).toBe(true)
952
+ expect(param.isMoreSpecificThan(other)).toBe(false)
953
+
954
+ // /:id (0 static) vs / (1 static) - static beats param
955
+ expect(param.isMoreSpecificThan(root)).toBe(false)
956
+ expect(root.isMoreSpecificThan(param)).toBe(true)
957
+ })
958
+
959
+ it('isConflict: checks if routes overlap', () => {
960
+ const routeA = Route0.create('/a/:x')
961
+ const routeB = Route0.create('/a/b')
962
+ const routeC = Route0.create('/a/:c')
963
+ const routeD = Route0.create('/a/d')
964
+ const routeE = Route0.create('/a/b/c')
965
+
966
+ // Same depth, can match
967
+ expect(routeA.isConflict(routeB)).toBe(true)
968
+ expect(routeA.isConflict(routeC)).toBe(true)
969
+ expect(routeA.isConflict(routeD)).toBe(true)
970
+ expect(routeB.isConflict(routeC)).toBe(true)
971
+
972
+ // Different depth, no conflict
973
+ expect(routeA.isConflict(routeE)).toBe(false)
974
+ expect(routeB.isConflict(routeE)).toBe(false)
975
+ })
976
+
977
+ it('isConflict: non-overlapping static routes', () => {
978
+ const route1 = Route0.create('/users')
979
+ const route2 = Route0.create('/posts')
980
+
981
+ // Same depth but different static segments
982
+ expect(route1.isConflict(route2)).toBe(false)
983
+ })
984
+ })
985
+
986
+ describe('regex', () => {
987
+ it('getRegexString: simple route', () => {
988
+ const route = Route0.create('/')
989
+ const regex = route.getRegexString()
990
+ expect(regex).toBe('/')
991
+ expect(new RegExp(`^${regex}$`).test('/')).toBe(true)
992
+ expect(new RegExp(`^${regex}$`).test('/other')).toBe(false)
993
+ })
994
+
995
+ it('getRegexString: static route', () => {
996
+ const route = Route0.create('/users')
997
+ const regex = route.getRegexString()
998
+ expect(regex).toBe('/users')
999
+ expect(new RegExp(`^${regex}$`).test('/users')).toBe(true)
1000
+ expect(new RegExp(`^${regex}$`).test('/users/123')).toBe(false)
1001
+ })
1002
+
1003
+ it('getRegexString: route with single param', () => {
1004
+ const route = Route0.create('/users/:id')
1005
+ const regex = route.getRegexString()
1006
+ expect(regex).toBe('/users/([^/]+)')
1007
+ expect(new RegExp(`^${regex}$`).test('/users/123')).toBe(true)
1008
+ expect(new RegExp(`^${regex}$`).test('/users/abc')).toBe(true)
1009
+ expect(new RegExp(`^${regex}$`).test('/users/123/posts')).toBe(false)
1010
+ expect(new RegExp(`^${regex}$`).test('/users')).toBe(false)
1011
+ })
1012
+
1013
+ it('getRegexString: route with multiple params', () => {
1014
+ const route = Route0.create('/users/:userId/posts/:postId')
1015
+ const regex = route.getRegexString()
1016
+ expect(regex).toBe('/users/([^/]+)/posts/([^/]+)')
1017
+ expect(new RegExp(`^${regex}$`).test('/users/123/posts/456')).toBe(true)
1018
+ expect(new RegExp(`^${regex}$`).test('/users/123/posts')).toBe(false)
1019
+ })
1020
+
1021
+ it('getRegexString: route with special regex chars', () => {
1022
+ const route = Route0.create('/api/v1.0')
1023
+ const regex = route.getRegexString()
1024
+ // The dot should be escaped
1025
+ expect(regex).toBe('/api/v1\\.0')
1026
+ expect(new RegExp(`^${regex}$`).test('/api/v1.0')).toBe(true)
1027
+ expect(new RegExp(`^${regex}$`).test('/api/v100')).toBe(false)
1028
+ })
1029
+
1030
+ it('getRegexString: handles trailing slash', () => {
1031
+ const route = Route0.create('/users/')
1032
+ const regex = route.getRegexString()
1033
+ // Trailing slash should be removed
1034
+ expect(regex).toBe('/users')
1035
+ })
1036
+
1037
+ it('getRegexString: root with trailing slash', () => {
1038
+ const route = Route0.create('/')
1039
+ const regex = route.getRegexString()
1040
+ // Root should keep its slash
1041
+ expect(regex).toBe('/')
1042
+ })
1043
+
1044
+ it('getRegex: simple route', () => {
1045
+ const route = Route0.create('/users')
1046
+ const regex = route.getRegex()
1047
+ expect(regex.test('/users')).toBe(true)
1048
+ expect(regex.test('/users/123')).toBe(false)
1049
+ expect(regex.test('/other')).toBe(false)
1050
+ })
1051
+
1052
+ it('getRegex: route with params', () => {
1053
+ const route = Route0.create('/users/:id')
1054
+ const regex = route.getRegex()
1055
+ expect(regex.test('/users/123')).toBe(true)
1056
+ expect(regex.test('/users/abc')).toBe(true)
1057
+ expect(regex.test('/users/123/posts')).toBe(false)
1058
+ })
1059
+
1060
+ it('static getRegexString: single route', () => {
1061
+ const route = Route0.create('/users/:id')
1062
+ const regex = Route0.getRegexString(route)
1063
+ expect(regex).toBe('/users/([^/]+)')
1064
+ })
1065
+
1066
+ it('static getRegexString: multiple routes', () => {
1067
+ const routes = [Route0.create('/users'), Route0.create('/posts/:id'), Route0.create('/')]
1068
+ const regex = Route0.getRegexString(routes)
1069
+ expect(regex).toBe('/users|/posts/([^/]+)|/')
1070
+ })
1071
+
1072
+ it('static getRegex: single route', () => {
1073
+ const route = Route0.create('/users/:id')
1074
+ const regex = Route0.getRegex(route)
1075
+ expect(regex.test('/users/123')).toBe(true)
1076
+ expect(regex.test('/posts/123')).toBe(false)
1077
+ })
1078
+
1079
+ it('static getRegex: multiple routes', () => {
1080
+ const routes = [Route0.create('/users'), Route0.create('/posts/:id'), Route0.create('/')]
1081
+ const regex = Route0.getRegex(routes)
1082
+ expect(regex.test('/users')).toBe(true)
1083
+ expect(regex.test('/posts/123')).toBe(true)
1084
+ expect(regex.test('/')).toBe(true)
1085
+ expect(regex.test('/other')).toBe(false)
1086
+ })
1087
+
1088
+ it('static getRegex: matches in order', () => {
1089
+ const routes = [Route0.create('/users/special'), Route0.create('/users/:id')]
1090
+ const regex = Route0.getRegex(routes)
1091
+ const match = '/users/special'.match(regex)
1092
+ expect(match).toBeTruthy()
1093
+ // Both could match, but first one should win
1094
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1095
+ expect(match![0]).toBe('/users/special')
1096
+ })
1097
+
1098
+ it('getRegexString works with getLocation', () => {
1099
+ const route = Route0.create('/users/:id/posts/:postId')
1100
+ const loc = route.getLocation('/users/123/posts/456')
1101
+ expect(loc.exact).toBe(true)
1102
+ expect(loc.params).toMatchObject({ id: '123', postId: '456' })
1103
+ })
1104
+
1105
+ it('regex matches what getLocation uses', () => {
1106
+ const route = Route0.create('/api/:version/users/:id')
1107
+ const testPath = '/api/v1/users/42'
1108
+
1109
+ // Test using getLocation
1110
+ const loc = route.getLocation(testPath)
1111
+ expect(loc.exact).toBe(true)
1112
+
1113
+ // Test using getRegex
1114
+ const regex = route.getRegex()
1115
+ expect(regex.test(testPath)).toBe(true)
1116
+ })
1117
+
1118
+ it('static getRegex: complex routing scenario', () => {
1119
+ const api = Route0.create('/api/v1')
1120
+ const routes = [
1121
+ Route0.create('/'),
1122
+ api,
1123
+ api.extend('/users'),
1124
+ api.extend('/users/:id'),
1125
+ api.extend('/posts/:postId'),
1126
+ Route0.create('/:slug'),
1127
+ ]
1128
+
1129
+ const regex = Route0.getRegex(routes)
1130
+
1131
+ expect(regex.test('/')).toBe(true)
1132
+ expect(regex.test('/api/v1')).toBe(true)
1133
+ expect(regex.test('/api/v1/users')).toBe(true)
1134
+ expect(regex.test('/api/v1/users/123')).toBe(true)
1135
+ expect(regex.test('/api/v1/posts/456')).toBe(true)
1136
+ expect(regex.test('/about')).toBe(true) // matches /:slug
1137
+ expect(regex.test('/api/v1/users/123/extra')).toBe(false)
1138
+ })
1139
+ })
1140
+
1141
+ describe('ordering', () => {
1142
+ it('_makeOrdering: orders routes by specificity', () => {
1143
+ const routes = {
1144
+ root: '/',
1145
+ userDetail: '/users/:id',
1146
+ users: '/users',
1147
+ userPosts: '/users/:id/posts',
1148
+ catchAll: '/:slug',
1149
+ }
1150
+
1151
+ const { pathsOrdering: ordering } = Routes._makeOrdering(routes)
1152
+
1153
+ // Expected order:
1154
+ // Depth 1: / then /users (static) then /:slug (param)
1155
+ // Depth 2: /users/:id
1156
+ // Depth 3: /users/:id/posts
1157
+
1158
+ expect(ordering).toEqual(['/', '/users', '/:slug', '/users/:id', '/users/:id/posts'])
1159
+ })
1160
+
1161
+ it('_makeOrdering: handles routes with same specificity', () => {
1162
+ const routes = {
1163
+ about: '/about',
1164
+ contact: '/contact',
1165
+ home: '/home',
1166
+ }
1167
+
1168
+ const { pathsOrdering: ordering } = Routes._makeOrdering(routes)
1169
+
1170
+ // All have same depth and don't conflict
1171
+ // Ordered alphabetically
1172
+ expect(ordering).toEqual(['/about', '/contact', '/home'])
1173
+ })
1174
+
1175
+ it('_makeOrdering: complex nested structure', () => {
1176
+ const api = Route0.create('/api/v1')
1177
+ const routes = {
1178
+ root: '/',
1179
+ api,
1180
+ usersStatic: '/api/v1/users/all',
1181
+ users: api.extend('/users'),
1182
+ userDetail: api.extend('/users/:id'),
1183
+ userPosts: api.extend('/users/:id/posts'),
1184
+ adminUser: '/api/v1/admin/:id',
1185
+ catchAll: '/:slug',
1186
+ }
1187
+
1188
+ const { pathsOrdering: ordering } = Routes._makeOrdering(routes)
1189
+
1190
+ // Expected order:
1191
+ // Depth 1: / (static), /:slug (param)
1192
+ // Depth 2: /api/v1
1193
+ // Depth 3: /api/v1/users (all static)
1194
+ // Depth 4: /api/v1/admin/:id (has param), /api/v1/users/all (all static), /api/v1/users/:id (has param)
1195
+ // Depth 5: /api/v1/users/:id/posts
1196
+
1197
+ expect(ordering).toEqual([
1198
+ '/',
1199
+ '/:slug',
1200
+ '/api/v1',
1201
+ '/api/v1/users',
1202
+ '/api/v1/admin/:id',
1203
+ '/api/v1/users/all',
1204
+ '/api/v1/users/:id',
1205
+ '/api/v1/users/:id/posts',
1206
+ ])
1207
+ })
1208
+
1209
+ it('Routes instance has ordering property', () => {
1210
+ const routes = Routes.create({
1211
+ home: '/',
1212
+ users: '/users',
1213
+ userDetail: '/users/:id',
1214
+ })
1215
+
1216
+ expect(routes.pathsOrdering).toBeDefined()
1217
+ expect(Array.isArray(routes.pathsOrdering)).toBe(true)
1218
+ // Depth 1: /, /users (alphabetically)
1219
+ // Depth 2: /users/:id
1220
+ expect(routes.pathsOrdering).toEqual(['/', '/users', '/users/:id'])
1221
+ })
1222
+
1223
+ it('ordering is preserved after override', () => {
1224
+ const routes = Routes.create({
1225
+ home: '/',
1226
+ users: '/users',
1227
+ userDetail: '/users/:id',
1228
+ })
1229
+
1230
+ const originalOrdering = routes.pathsOrdering
1231
+
1232
+ const overridden = routes.override({ baseUrl: 'https://example.com' })
1233
+
1234
+ expect(overridden.pathsOrdering).toEqual(originalOrdering)
1235
+ expect(overridden.pathsOrdering).toEqual(['/', '/users', '/users/:id'])
1236
+ })
1237
+
1238
+ it('_makeOrdering: handles single route', () => {
1239
+ const routes = {
1240
+ home: '/',
1241
+ }
1242
+
1243
+ const { pathsOrdering: ordering } = Routes._makeOrdering(routes)
1244
+ expect(ordering).toEqual(['/'])
1245
+ })
1246
+
1247
+ it('_makeOrdering: handles empty object', () => {
1248
+ const routes = {}
1249
+
1250
+ const { pathsOrdering: ordering } = Routes._makeOrdering(routes)
1251
+ expect(ordering).toEqual([])
205
1252
  })
206
1253
  })