@devp0nt/route0 1.0.0-next.2 → 1.0.0-next.20
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/dist/cjs/index.cjs +345 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/cjs/index.d.cts +208 -85
- package/dist/esm/index.d.ts +208 -85
- package/dist/esm/index.js +256 -70
- package/dist/esm/index.js.map +1 -1
- package/package.json +42 -15
- package/src/index.test.ts +730 -35
- package/src/index.ts +729 -244
- package/dist/cjs/index.js +0 -158
- package/dist/cjs/index.js.map +0 -1
package/src/index.test.ts
CHANGED
|
@@ -1,13 +1,32 @@
|
|
|
1
1
|
import { describe, expect, expectTypeOf, it } from 'bun:test'
|
|
2
|
-
import {
|
|
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('
|
|
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
|
|
41
|
+
it('simple any search', () => {
|
|
22
42
|
const route0 = Route0.create('/')
|
|
23
|
-
const path = route0.get({
|
|
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
|
|
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',
|
|
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('
|
|
66
|
+
it('search', () => {
|
|
43
67
|
const route0 = Route0.create('/prefix&y&z')
|
|
44
|
-
expectTypeOf<(typeof route0)['
|
|
45
|
-
const path = route0.get({
|
|
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
|
|
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',
|
|
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?c=5')
|
|
55
81
|
})
|
|
56
82
|
|
|
57
|
-
it('params and
|
|
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',
|
|
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?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({
|
|
102
|
-
expectTypeOf<(typeof route1)['
|
|
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
|
|
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',
|
|
117
|
-
expectTypeOf<(typeof route1)['
|
|
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
|
|
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',
|
|
132
|
-
expectTypeOf<(typeof route1)['
|
|
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,695 @@ 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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
|
197
|
-
expect(rWith.
|
|
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
|
+
it('.getLocation location of url', () => {
|
|
268
|
+
let loc = Route0.getLocation('/prefix/some/suffix')
|
|
269
|
+
expect(loc).toMatchObject({
|
|
270
|
+
hash: '',
|
|
271
|
+
href: undefined,
|
|
272
|
+
hrefRel: '/prefix/some/suffix',
|
|
273
|
+
abs: false,
|
|
274
|
+
origin: undefined,
|
|
275
|
+
params: undefined,
|
|
276
|
+
pathname: '/prefix/some/suffix',
|
|
277
|
+
searchParams: {},
|
|
278
|
+
search: '',
|
|
279
|
+
})
|
|
280
|
+
loc = Route0.getLocation('/prefix/some/suffix?x=1&z=2')
|
|
281
|
+
expect(loc).toMatchObject({
|
|
282
|
+
hash: '',
|
|
283
|
+
href: undefined,
|
|
284
|
+
hrefRel: '/prefix/some/suffix?x=1&z=2',
|
|
285
|
+
abs: false,
|
|
286
|
+
origin: undefined,
|
|
287
|
+
params: undefined,
|
|
288
|
+
pathname: '/prefix/some/suffix',
|
|
289
|
+
searchParams: { x: '1', z: '2' },
|
|
290
|
+
search: '?x=1&z=2',
|
|
291
|
+
})
|
|
292
|
+
loc = Route0.getLocation('https://example.com/prefix/some/suffix?x=1&z=2')
|
|
293
|
+
expect(loc).toMatchObject({
|
|
294
|
+
hash: '',
|
|
295
|
+
href: 'https://example.com/prefix/some/suffix?x=1&z=2',
|
|
296
|
+
hrefRel: '/prefix/some/suffix?x=1&z=2',
|
|
297
|
+
abs: true,
|
|
298
|
+
origin: 'https://example.com',
|
|
299
|
+
params: undefined,
|
|
300
|
+
pathname: '/prefix/some/suffix',
|
|
301
|
+
searchParams: { x: '1', z: '2' },
|
|
302
|
+
search: '?x=1&z=2',
|
|
303
|
+
})
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
it('#getLocation() exact match', () => {
|
|
307
|
+
const route0 = Route0.create('/prefix/:x/some/:y/:z/suffix')
|
|
308
|
+
let loc = route0.getLocation('/prefix/some/suffix')
|
|
309
|
+
expect(loc.exact).toBe(false)
|
|
310
|
+
expect(loc.parent).toBe(false)
|
|
311
|
+
expect(loc.children).toBe(false)
|
|
312
|
+
expect(loc.params).toMatchObject({})
|
|
313
|
+
loc = route0.getLocation('/prefix/xxx/some/yyy/zzz/suffix')
|
|
314
|
+
expect(loc.exact).toBe(true)
|
|
315
|
+
expect(loc.parent).toBe(false)
|
|
316
|
+
expect(loc.children).toBe(false)
|
|
317
|
+
if (loc.exact) {
|
|
318
|
+
expectTypeOf<typeof loc.params>().toEqualTypeOf<{ x: string; y: string; z: string }>()
|
|
319
|
+
}
|
|
320
|
+
expect(loc.params).toMatchObject({ x: 'xxx', y: 'yyy', z: 'zzz' })
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
it('#getLocation() parent match', () => {
|
|
324
|
+
const route0 = Route0.create('/prefix/:x/some')
|
|
325
|
+
const loc = route0.getLocation('/prefix/xxx/some/extra/path')
|
|
326
|
+
expect(loc.exact).toBe(false)
|
|
327
|
+
expect(loc.parent).toBe(true)
|
|
328
|
+
expect(loc.children).toBe(false)
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
it('#getLocation() children match', () => {
|
|
332
|
+
const route0 = Route0.create('/prefix/some/extra/path')
|
|
333
|
+
const loc = route0.getLocation('/prefix/some')
|
|
334
|
+
expect(loc.exact).toBe(false)
|
|
335
|
+
expect(loc.parent).toBe(false)
|
|
336
|
+
expect(loc.children).toBe(true)
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
it('#getLocation() with host info', () => {
|
|
340
|
+
const route0 = Route0.create('/path')
|
|
341
|
+
const loc = route0.getLocation('https://example.com:8080/path')
|
|
342
|
+
expect(loc.exact).toBe(true)
|
|
343
|
+
expect(loc.origin).toBe('https://example.com:8080')
|
|
344
|
+
expect(loc.host).toBe('example.com:8080')
|
|
345
|
+
expect(loc.hostname).toBe('example.com')
|
|
346
|
+
expect(loc.port).toBe('8080')
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
it('#getLocation() with hash', () => {
|
|
350
|
+
const route0 = Route0.create('/path/:id')
|
|
351
|
+
const loc = route0.getLocation('/path/123#section')
|
|
352
|
+
expect(loc.exact).toBe(true)
|
|
353
|
+
expect(loc.hash).toBe('#section')
|
|
354
|
+
expect(loc.params).toMatchObject({ id: '123' })
|
|
355
|
+
})
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
describe('type utilities', () => {
|
|
359
|
+
it('HasParams', () => {
|
|
360
|
+
expectTypeOf<HasParams<'/path'>>().toEqualTypeOf<false>()
|
|
361
|
+
expectTypeOf<HasParams<'/path/:id'>>().toEqualTypeOf<true>()
|
|
362
|
+
expectTypeOf<HasParams<'/path/:id/:name'>>().toEqualTypeOf<true>()
|
|
363
|
+
|
|
364
|
+
expectTypeOf<HasParams<Route0<'/path'>>>().toEqualTypeOf<false>()
|
|
365
|
+
expectTypeOf<HasParams<Route0<'/path/:id'>>>().toEqualTypeOf<true>()
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
it('HasSearch', () => {
|
|
369
|
+
expectTypeOf<HasSearch<'/path'>>().toEqualTypeOf<false>()
|
|
370
|
+
expectTypeOf<HasSearch<'/path&x'>>().toEqualTypeOf<true>()
|
|
371
|
+
expectTypeOf<HasSearch<'/path&x&y'>>().toEqualTypeOf<true>()
|
|
372
|
+
|
|
373
|
+
expectTypeOf<HasSearch<Route0<'/path'>>>().toEqualTypeOf<false>()
|
|
374
|
+
expectTypeOf<HasSearch<Route0<'/path&x&y'>>>().toEqualTypeOf<true>()
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
it('ParamsInput', () => {
|
|
378
|
+
expectTypeOf<ParamsInput<'/path'>>().toEqualTypeOf<Record<never, never>>()
|
|
379
|
+
expectTypeOf<ParamsInput<'/path/:id'>>().toEqualTypeOf<{ id: string | number }>()
|
|
380
|
+
expectTypeOf<ParamsInput<'/path/:id/:name'>>().toEqualTypeOf<{ id: string | number; name: string | number }>()
|
|
381
|
+
|
|
382
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
383
|
+
const route = Route0.create('/path/:id/:name')
|
|
384
|
+
expectTypeOf<ParamsInput<typeof route>>().toEqualTypeOf<{ id: string | number; name: string | number }>()
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
it('ParamsOutput', () => {
|
|
388
|
+
expectTypeOf<ParamsOutput<'/path/:id'>>().toEqualTypeOf<{ id: string }>()
|
|
389
|
+
expectTypeOf<ParamsOutput<'/path/:id/:name'>>().toEqualTypeOf<{ id: string; name: string }>()
|
|
390
|
+
|
|
391
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
392
|
+
const route = Route0.create('/path/:id/:name')
|
|
393
|
+
expectTypeOf<ParamsOutput<typeof route>>().toEqualTypeOf<{ id: string; name: string }>()
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
it('SearchInput', () => {
|
|
397
|
+
type T1 = SearchInput<'/path'>
|
|
398
|
+
expectTypeOf<T1>().toEqualTypeOf<Record<string, string | number>>()
|
|
399
|
+
|
|
400
|
+
type T2 = SearchInput<'/path&x&y'>
|
|
401
|
+
expectTypeOf<T2>().toEqualTypeOf<
|
|
402
|
+
Partial<{
|
|
403
|
+
x: string | number
|
|
404
|
+
y: string | number
|
|
405
|
+
}> &
|
|
406
|
+
Record<string, string | number>
|
|
407
|
+
>()
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
it('SearchOutput', () => {
|
|
411
|
+
type T1 = SearchOutput<'/path'>
|
|
412
|
+
expectTypeOf<T1>().toEqualTypeOf<{
|
|
413
|
+
[key: string]: string | undefined
|
|
414
|
+
}>()
|
|
415
|
+
|
|
416
|
+
type T2 = SearchOutput<'/path&x&y'>
|
|
417
|
+
expectTypeOf<T2>().toEqualTypeOf<{
|
|
418
|
+
[key: string]: string | undefined
|
|
419
|
+
x?: string | undefined
|
|
420
|
+
y?: string | undefined
|
|
421
|
+
}>()
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
it('StrictSearchInput', () => {
|
|
425
|
+
type T1 = StrictSearchInput<'/path&x&y'>
|
|
426
|
+
expectTypeOf<T1>().toEqualTypeOf<{ x?: string | number; y?: string | number }>()
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
it('StrictSearchOutput', () => {
|
|
430
|
+
type T1 = StrictSearchOutput<'/path&x&y'>
|
|
431
|
+
expectTypeOf<T1>().toEqualTypeOf<{ x?: string | undefined; y?: string | undefined }>()
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
it('IsParent', () => {
|
|
435
|
+
type T1 = IsParent<'/path/child', '/path'>
|
|
436
|
+
type T2 = IsParent<'/path', '/path/child'>
|
|
437
|
+
type T3 = IsParent<'/other', '/path'>
|
|
438
|
+
type T4 = IsParent<'/path', '/path'>
|
|
439
|
+
expectTypeOf<T1>().toEqualTypeOf<true>()
|
|
440
|
+
expectTypeOf<T2>().toEqualTypeOf<false>()
|
|
441
|
+
expectTypeOf<T3>().toEqualTypeOf<false>()
|
|
442
|
+
expectTypeOf<T4>().toEqualTypeOf<false>()
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
it('IsChildren', () => {
|
|
446
|
+
type T1 = IsChildren<'/path', '/path/child'>
|
|
447
|
+
type T2 = IsChildren<'/path/child', '/path'>
|
|
448
|
+
type T3 = IsChildren<'/path', '/other'>
|
|
449
|
+
type T4 = IsChildren<'/path', '/path'>
|
|
450
|
+
expectTypeOf<T1>().toEqualTypeOf<true>()
|
|
451
|
+
expectTypeOf<T2>().toEqualTypeOf<false>()
|
|
452
|
+
expectTypeOf<T3>().toEqualTypeOf<false>()
|
|
453
|
+
expectTypeOf<T4>().toEqualTypeOf<false>()
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
it('IsSame', () => {
|
|
457
|
+
type T1 = IsSame<'/path', '/path'>
|
|
458
|
+
type T2 = IsSame<'/path', '/path/child'>
|
|
459
|
+
type T3 = IsSame<'/path/child', '/path'>
|
|
460
|
+
expectTypeOf<T1>().toEqualTypeOf<true>()
|
|
461
|
+
expectTypeOf<T2>().toEqualTypeOf<false>()
|
|
462
|
+
expectTypeOf<T3>().toEqualTypeOf<false>()
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
it('IsSameParams', () => {
|
|
466
|
+
type T1 = IsSameParams<'/path', '/other'>
|
|
467
|
+
type T2 = IsSameParams<'/path/:id', '/other/:id'>
|
|
468
|
+
type T3 = IsSameParams<'/path/:id', '/other'>
|
|
469
|
+
type T4 = IsSameParams<'/path/:id', '/other/:name'>
|
|
470
|
+
expectTypeOf<T1>().toEqualTypeOf<true>()
|
|
471
|
+
expectTypeOf<T2>().toEqualTypeOf<true>()
|
|
472
|
+
expectTypeOf<T3>().toEqualTypeOf<false>()
|
|
473
|
+
expectTypeOf<T4>().toEqualTypeOf<false>()
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
it('Extended', () => {
|
|
477
|
+
expectTypeOf<Extended<'/path', '/child'>>().toEqualTypeOf<Route0<'/path/child'>>()
|
|
478
|
+
expectTypeOf<Extended<'/path', '/:id'>>().toEqualTypeOf<Route0<'/path/:id'>>()
|
|
479
|
+
expectTypeOf<Extended<'/path', '&x&y'>>().toEqualTypeOf<Route0<'/path&x&y'>>()
|
|
480
|
+
expectTypeOf<Extended<'/path/:id', '/child&x'>>().toEqualTypeOf<Route0<'/path/:id/child&x'>>()
|
|
481
|
+
expectTypeOf<Extended<undefined, '/path'>>().toEqualTypeOf<Route0<'/path'>>()
|
|
482
|
+
|
|
483
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
484
|
+
const parent = Route0.create('/path')
|
|
485
|
+
expectTypeOf<Extended<typeof parent, '/child'>>().toEqualTypeOf<Route0<'/path/child'>>()
|
|
486
|
+
})
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
describe('Routes', () => {
|
|
490
|
+
it('create with string routes', () => {
|
|
491
|
+
const collection = Routes.create({
|
|
492
|
+
home: '/',
|
|
493
|
+
about: '/about',
|
|
494
|
+
contact: '/contact',
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
expect(collection).toBeInstanceOf(Routes)
|
|
498
|
+
const home = collection.home
|
|
499
|
+
const about = collection.about
|
|
500
|
+
const contact = collection.contact
|
|
501
|
+
|
|
502
|
+
expect(home).toBeInstanceOf(Route0)
|
|
503
|
+
expect(about).toBeInstanceOf(Route0)
|
|
504
|
+
expect(contact).toBeInstanceOf(Route0)
|
|
505
|
+
|
|
506
|
+
expect(home.get()).toBe('/')
|
|
507
|
+
expect(about.get()).toBe('/about')
|
|
508
|
+
expect(contact.get()).toBe('/contact')
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
it('create with Route0 instances', () => {
|
|
512
|
+
const homeRoute = Route0.create('/')
|
|
513
|
+
const aboutRoute = Route0.create('/about')
|
|
514
|
+
|
|
515
|
+
const collection = Routes.create({
|
|
516
|
+
home: homeRoute,
|
|
517
|
+
about: aboutRoute,
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
expect(collection.home.get()).toBe('/')
|
|
521
|
+
expect(collection.about.get()).toBe('/about')
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
it('create with mixed string and Route0', () => {
|
|
525
|
+
const aboutRoute = Route0.create('/about')
|
|
526
|
+
|
|
527
|
+
const collection = Routes.create({
|
|
528
|
+
home: '/',
|
|
529
|
+
about: aboutRoute,
|
|
530
|
+
contact: '/contact',
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
expect(collection.home.get()).toBe('/')
|
|
534
|
+
expect(collection.about.get()).toBe('/about')
|
|
535
|
+
expect(collection.contact.get()).toBe('/contact')
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
it('create with params and search', () => {
|
|
539
|
+
const collection = Routes.create({
|
|
540
|
+
user: '/user/:id',
|
|
541
|
+
search: '/search&q&filter',
|
|
542
|
+
userWithSearch: '/user/:id&tab',
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
const user = collection.user
|
|
546
|
+
expect(user.get({ id: '123' } as any)).toBe('/user/123')
|
|
547
|
+
|
|
548
|
+
const search = collection.search
|
|
549
|
+
expect(search.get({ search: { q: 'test', filter: 'all' } })).toBe('/search?q=test&filter=all')
|
|
550
|
+
|
|
551
|
+
const userWithSearch = collection.userWithSearch
|
|
552
|
+
expect(userWithSearch.get({ id: '456', search: { tab: 'posts' } } as any)).toBe('/user/456?tab=posts')
|
|
553
|
+
})
|
|
554
|
+
|
|
555
|
+
it('get maintains route definitions', () => {
|
|
556
|
+
const collection = Routes.create({
|
|
557
|
+
home: '/',
|
|
558
|
+
user: '/user/:id',
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
const home = collection.home
|
|
562
|
+
const user = collection.user
|
|
563
|
+
|
|
564
|
+
// Verify route definitions are preserved
|
|
565
|
+
expect(home.pathOriginal).toBe('/')
|
|
566
|
+
expect(user.pathOriginal).toBe('/user/:id')
|
|
567
|
+
expect(home.pathDefinition).toBe('/')
|
|
568
|
+
expect(user.pathDefinition).toBe('/user/:id')
|
|
569
|
+
|
|
570
|
+
// Verify params work correctly
|
|
571
|
+
expect(user.get({ id: '123' })).toBe('/user/123')
|
|
572
|
+
})
|
|
573
|
+
|
|
574
|
+
it('override with baseUrl', () => {
|
|
575
|
+
const collection = Routes.create({
|
|
576
|
+
home: '/',
|
|
577
|
+
about: '/about',
|
|
578
|
+
})
|
|
579
|
+
|
|
580
|
+
const overridden = collection.override({ baseUrl: 'https://example.com' })
|
|
581
|
+
|
|
582
|
+
const home = overridden.home
|
|
583
|
+
const about = overridden.about
|
|
584
|
+
|
|
585
|
+
expect(home.get({ abs: true })).toBe('https://example.com')
|
|
586
|
+
expect(about.get({ abs: true })).toBe('https://example.com/about')
|
|
587
|
+
})
|
|
588
|
+
|
|
589
|
+
it('override does not mutate original', () => {
|
|
590
|
+
const collection = Routes.create({
|
|
591
|
+
home: '/',
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
const original = collection.home
|
|
595
|
+
expect(original.get({ abs: true })).toBe('https://example.com')
|
|
596
|
+
|
|
597
|
+
const overridden = collection.override({ baseUrl: 'https://newdomain.com' })
|
|
598
|
+
const newRoute = overridden.home
|
|
599
|
+
|
|
600
|
+
expect(original.get({ abs: true })).toBe('https://example.com')
|
|
601
|
+
expect(newRoute.get({ abs: true })).toBe('https://newdomain.com')
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
it('override with extended routes', () => {
|
|
605
|
+
const apiRoute = Route0.create('/api', { baseUrl: 'https://api.example.com' })
|
|
606
|
+
const usersRoute = apiRoute.extend('/users')
|
|
607
|
+
|
|
608
|
+
const collection = Routes.create({
|
|
609
|
+
api: apiRoute,
|
|
610
|
+
users: usersRoute,
|
|
611
|
+
})
|
|
612
|
+
|
|
613
|
+
expect(collection.api.get({ abs: true })).toBe('https://api.example.com/api')
|
|
614
|
+
expect(collection.api({ abs: true })).toBe('https://api.example.com/api')
|
|
615
|
+
expect(collection.users.get({ abs: true })).toBe('https://api.example.com/api/users')
|
|
616
|
+
|
|
617
|
+
const overridden = collection.override({ baseUrl: 'https://new-api.example.com' })
|
|
618
|
+
|
|
619
|
+
expect(overridden.api.get({ abs: true })).toBe('https://new-api.example.com/api')
|
|
620
|
+
expect(overridden.users.get({ abs: true })).toBe('https://new-api.example.com/api/users')
|
|
621
|
+
})
|
|
622
|
+
|
|
623
|
+
it('hydrate static method', () => {
|
|
624
|
+
const hydrated = Routes._hydrate({
|
|
625
|
+
home: '/',
|
|
626
|
+
user: '/user/:id',
|
|
627
|
+
about: Route0.create('/about'),
|
|
628
|
+
})
|
|
629
|
+
|
|
630
|
+
expect(hydrated.home).toBeInstanceOf(Route0)
|
|
631
|
+
expect(hydrated.user).toBeInstanceOf(Route0)
|
|
632
|
+
expect(hydrated.about).toBeInstanceOf(Route0)
|
|
633
|
+
|
|
634
|
+
expect(hydrated.home.get()).toBe('/')
|
|
635
|
+
expect(hydrated.user.get({ id: '123' })).toBe('/user/123')
|
|
636
|
+
expect(hydrated.about.get()).toBe('/about')
|
|
637
|
+
})
|
|
638
|
+
|
|
639
|
+
it('works with callable routes', () => {
|
|
640
|
+
const collection = Routes.create({
|
|
641
|
+
home: '/',
|
|
642
|
+
user: '/user/:id',
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
const home = collection.home
|
|
646
|
+
const user = collection.user
|
|
647
|
+
|
|
648
|
+
// Routes should be callable
|
|
649
|
+
expect(typeof home).toBe('function')
|
|
650
|
+
expect(typeof user).toBe('function')
|
|
651
|
+
expect(home()).toBe('/')
|
|
652
|
+
expect(user({ id: '789' })).toBe('/user/789')
|
|
653
|
+
})
|
|
654
|
+
|
|
655
|
+
it('complex nested structure', () => {
|
|
656
|
+
const api = Route0.create('/api/v1', { baseUrl: 'https://api.example.com' })
|
|
657
|
+
|
|
658
|
+
const collection = Routes.create({
|
|
659
|
+
root: '/',
|
|
660
|
+
api,
|
|
661
|
+
users: api.extend('/users'),
|
|
662
|
+
userDetail: api.extend('/users/:id'),
|
|
663
|
+
userPosts: api.extend('/users/:id/posts&sort&filter'),
|
|
664
|
+
})
|
|
665
|
+
|
|
666
|
+
expect(collection.root.get()).toBe('/')
|
|
667
|
+
expect(collection.api({ abs: true })).toBe('https://api.example.com/api/v1')
|
|
668
|
+
expect(collection.users.get({ abs: true })).toBe('https://api.example.com/api/v1/users')
|
|
669
|
+
|
|
670
|
+
const userDetailPath: any = collection.userDetail.get({ id: '42', abs: true })
|
|
671
|
+
expect(userDetailPath).toBe('https://api.example.com/api/v1/users/42')
|
|
672
|
+
|
|
673
|
+
const userPostsPath: any = collection.userPosts.get({
|
|
674
|
+
id: '42',
|
|
675
|
+
search: { sort: 'date', filter: 'published' },
|
|
676
|
+
abs: true,
|
|
677
|
+
})
|
|
678
|
+
expect(userPostsPath).toBe('https://api.example.com/api/v1/users/42/posts?sort=date&filter=published')
|
|
679
|
+
})
|
|
680
|
+
})
|
|
681
|
+
|
|
682
|
+
describe('specificity', () => {
|
|
683
|
+
it('isMoreSpecificThan: static vs param', () => {
|
|
684
|
+
const static1 = Route0.create('/a/b')
|
|
685
|
+
const param1 = Route0.create('/a/:id')
|
|
686
|
+
|
|
687
|
+
expect(static1.isMoreSpecificThan(param1)).toBe(true)
|
|
688
|
+
expect(param1.isMoreSpecificThan(static1)).toBe(false)
|
|
689
|
+
})
|
|
690
|
+
|
|
691
|
+
it('isMoreSpecificThan: more static segments wins', () => {
|
|
692
|
+
const twoStatic = Route0.create('/a/b/c')
|
|
693
|
+
const oneStatic = Route0.create('/a/:id/c')
|
|
694
|
+
const noStatic = Route0.create('/a/:id/:name')
|
|
695
|
+
|
|
696
|
+
expect(twoStatic.isMoreSpecificThan(oneStatic)).toBe(true)
|
|
697
|
+
expect(oneStatic.isMoreSpecificThan(twoStatic)).toBe(false)
|
|
698
|
+
|
|
699
|
+
expect(oneStatic.isMoreSpecificThan(noStatic)).toBe(true)
|
|
700
|
+
expect(noStatic.isMoreSpecificThan(oneStatic)).toBe(false)
|
|
701
|
+
|
|
702
|
+
expect(twoStatic.isMoreSpecificThan(noStatic)).toBe(true)
|
|
703
|
+
expect(noStatic.isMoreSpecificThan(twoStatic)).toBe(false)
|
|
704
|
+
})
|
|
705
|
+
|
|
706
|
+
it('isMoreSpecificThan: compares overlapping segments then lexicographically', () => {
|
|
707
|
+
const longer = Route0.create('/a/:id/b/:name')
|
|
708
|
+
const shorter = Route0.create('/a/:id')
|
|
709
|
+
|
|
710
|
+
// Both have same pattern for overlapping segments: static then param
|
|
711
|
+
// Falls back to lexicographic: '/a/:id' < '/a/:id/b/:name'
|
|
712
|
+
expect(longer.isMoreSpecificThan(shorter)).toBe(false)
|
|
713
|
+
expect(shorter.isMoreSpecificThan(longer)).toBe(true)
|
|
714
|
+
})
|
|
715
|
+
|
|
716
|
+
it('isMoreSpecificThan: static at earlier position wins', () => {
|
|
717
|
+
const route1 = Route0.create('/a/static/:param')
|
|
718
|
+
const route2 = Route0.create('/a/:param/static')
|
|
719
|
+
|
|
720
|
+
// Both have 2 static segments and same length
|
|
721
|
+
// route1 has static at position 1, route2 has param at position 1
|
|
722
|
+
expect(route1.isMoreSpecificThan(route2)).toBe(true)
|
|
723
|
+
expect(route2.isMoreSpecificThan(route1)).toBe(false)
|
|
724
|
+
})
|
|
725
|
+
|
|
726
|
+
it('isMoreSpecificThan: lexicographic when completely equal', () => {
|
|
727
|
+
const route1 = Route0.create('/aaa/:id')
|
|
728
|
+
const route2 = Route0.create('/bbb/:id')
|
|
729
|
+
|
|
730
|
+
// Same specificity, lexicographic comparison
|
|
731
|
+
expect(route1.isMoreSpecificThan(route2)).toBe(true)
|
|
732
|
+
expect(route2.isMoreSpecificThan(route1)).toBe(false)
|
|
733
|
+
})
|
|
734
|
+
|
|
735
|
+
it('isMoreSpecificThan: identical routes', () => {
|
|
736
|
+
const route1 = Route0.create('/a/:id')
|
|
737
|
+
const route2 = Route0.create('/a/:id')
|
|
738
|
+
|
|
739
|
+
// Identical routes, lexicographic comparison returns false for equal strings
|
|
740
|
+
expect(route1.isMoreSpecificThan(route2)).toBe(false)
|
|
741
|
+
expect(route2.isMoreSpecificThan(route1)).toBe(false)
|
|
742
|
+
})
|
|
743
|
+
|
|
744
|
+
it('isMoreSpecificThan: root vs other routes', () => {
|
|
745
|
+
const root = Route0.create('/')
|
|
746
|
+
const other = Route0.create('/a')
|
|
747
|
+
const param = Route0.create('/:id')
|
|
748
|
+
|
|
749
|
+
// /a (1 static) vs / (1 static) - both static, lexicographic order
|
|
750
|
+
expect(other.isMoreSpecificThan(root)).toBe(false) // '/' < '/a' lexicographically
|
|
751
|
+
expect(root.isMoreSpecificThan(other)).toBe(true)
|
|
752
|
+
|
|
753
|
+
// /a (1 static) vs /:id (0 static) - static beats param
|
|
754
|
+
expect(other.isMoreSpecificThan(param)).toBe(true)
|
|
755
|
+
expect(param.isMoreSpecificThan(other)).toBe(false)
|
|
756
|
+
|
|
757
|
+
// /:id (0 static) vs / (1 static) - static beats param
|
|
758
|
+
expect(param.isMoreSpecificThan(root)).toBe(false)
|
|
759
|
+
expect(root.isMoreSpecificThan(param)).toBe(true)
|
|
760
|
+
})
|
|
761
|
+
|
|
762
|
+
it('isConflict: checks if routes overlap', () => {
|
|
763
|
+
const routeA = Route0.create('/a/:x')
|
|
764
|
+
const routeB = Route0.create('/a/b')
|
|
765
|
+
const routeC = Route0.create('/a/:c')
|
|
766
|
+
const routeD = Route0.create('/a/d')
|
|
767
|
+
const routeE = Route0.create('/a/b/c')
|
|
768
|
+
|
|
769
|
+
// Same depth, can match
|
|
770
|
+
expect(routeA.isConflict(routeB)).toBe(true)
|
|
771
|
+
expect(routeA.isConflict(routeC)).toBe(true)
|
|
772
|
+
expect(routeA.isConflict(routeD)).toBe(true)
|
|
773
|
+
expect(routeB.isConflict(routeC)).toBe(true)
|
|
774
|
+
|
|
775
|
+
// Different depth, no conflict
|
|
776
|
+
expect(routeA.isConflict(routeE)).toBe(false)
|
|
777
|
+
expect(routeB.isConflict(routeE)).toBe(false)
|
|
778
|
+
})
|
|
779
|
+
|
|
780
|
+
it('isConflict: non-overlapping static routes', () => {
|
|
781
|
+
const route1 = Route0.create('/users')
|
|
782
|
+
const route2 = Route0.create('/posts')
|
|
783
|
+
|
|
784
|
+
// Same depth but different static segments
|
|
785
|
+
expect(route1.isConflict(route2)).toBe(false)
|
|
786
|
+
})
|
|
787
|
+
})
|
|
788
|
+
|
|
789
|
+
describe('ordering', () => {
|
|
790
|
+
it('_makeOrdering: orders routes by specificity', () => {
|
|
791
|
+
const routes = {
|
|
792
|
+
root: '/',
|
|
793
|
+
userDetail: '/users/:id',
|
|
794
|
+
users: '/users',
|
|
795
|
+
userPosts: '/users/:id/posts',
|
|
796
|
+
catchAll: '/:slug',
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const ordering = Routes._makeOrdering(routes)
|
|
800
|
+
|
|
801
|
+
// Expected order:
|
|
802
|
+
// Depth 1: / then /users (static) then /:slug (param)
|
|
803
|
+
// Depth 2: /users/:id
|
|
804
|
+
// Depth 3: /users/:id/posts
|
|
805
|
+
|
|
806
|
+
expect(ordering).toEqual(['/', '/users', '/:slug', '/users/:id', '/users/:id/posts'])
|
|
807
|
+
})
|
|
808
|
+
|
|
809
|
+
it('_makeOrdering: handles routes with same specificity', () => {
|
|
810
|
+
const routes = {
|
|
811
|
+
about: '/about',
|
|
812
|
+
contact: '/contact',
|
|
813
|
+
home: '/home',
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
const ordering = Routes._makeOrdering(routes)
|
|
817
|
+
|
|
818
|
+
// All have same depth and don't conflict
|
|
819
|
+
// Ordered alphabetically
|
|
820
|
+
expect(ordering).toEqual(['/about', '/contact', '/home'])
|
|
821
|
+
})
|
|
822
|
+
|
|
823
|
+
it('_makeOrdering: complex nested structure', () => {
|
|
824
|
+
const api = Route0.create('/api/v1')
|
|
825
|
+
const routes = {
|
|
826
|
+
root: '/',
|
|
827
|
+
api,
|
|
828
|
+
usersStatic: '/api/v1/users/all',
|
|
829
|
+
users: api.extend('/users'),
|
|
830
|
+
userDetail: api.extend('/users/:id'),
|
|
831
|
+
userPosts: api.extend('/users/:id/posts'),
|
|
832
|
+
adminUser: '/api/v1/admin/:id',
|
|
833
|
+
catchAll: '/:slug',
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
const ordering = Routes._makeOrdering(routes)
|
|
837
|
+
|
|
838
|
+
// Expected order:
|
|
839
|
+
// Depth 1: / (static), /:slug (param)
|
|
840
|
+
// Depth 2: /api/v1
|
|
841
|
+
// Depth 3: /api/v1/users (all static)
|
|
842
|
+
// Depth 4: /api/v1/admin/:id (has param), /api/v1/users/all (all static), /api/v1/users/:id (has param)
|
|
843
|
+
// Depth 5: /api/v1/users/:id/posts
|
|
844
|
+
|
|
845
|
+
expect(ordering).toEqual([
|
|
846
|
+
'/',
|
|
847
|
+
'/:slug',
|
|
848
|
+
'/api/v1',
|
|
849
|
+
'/api/v1/users',
|
|
850
|
+
'/api/v1/admin/:id',
|
|
851
|
+
'/api/v1/users/all',
|
|
852
|
+
'/api/v1/users/:id',
|
|
853
|
+
'/api/v1/users/:id/posts',
|
|
854
|
+
])
|
|
855
|
+
})
|
|
856
|
+
|
|
857
|
+
it('Routes instance has ordering property', () => {
|
|
858
|
+
const routes = Routes.create({
|
|
859
|
+
home: '/',
|
|
860
|
+
users: '/users',
|
|
861
|
+
userDetail: '/users/:id',
|
|
862
|
+
})
|
|
863
|
+
|
|
864
|
+
expect(routes.ordering).toBeDefined()
|
|
865
|
+
expect(Array.isArray(routes.ordering)).toBe(true)
|
|
866
|
+
// Depth 1: /, /users (alphabetically)
|
|
867
|
+
// Depth 2: /users/:id
|
|
868
|
+
expect(routes.ordering).toEqual(['/', '/users', '/users/:id'])
|
|
869
|
+
})
|
|
870
|
+
|
|
871
|
+
it('ordering is preserved after override', () => {
|
|
872
|
+
const routes = Routes.create({
|
|
873
|
+
home: '/',
|
|
874
|
+
users: '/users',
|
|
875
|
+
userDetail: '/users/:id',
|
|
876
|
+
})
|
|
877
|
+
|
|
878
|
+
const originalOrdering = routes.ordering
|
|
879
|
+
|
|
880
|
+
const overridden = routes.override({ baseUrl: 'https://example.com' })
|
|
881
|
+
|
|
882
|
+
expect(overridden.ordering).toEqual(originalOrdering)
|
|
883
|
+
expect(overridden.ordering).toEqual(['/', '/users', '/users/:id'])
|
|
884
|
+
})
|
|
885
|
+
|
|
886
|
+
it('_makeOrdering: handles single route', () => {
|
|
887
|
+
const routes = {
|
|
888
|
+
home: '/',
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
const ordering = Routes._makeOrdering(routes)
|
|
892
|
+
expect(ordering).toEqual(['/'])
|
|
893
|
+
})
|
|
894
|
+
|
|
895
|
+
it('_makeOrdering: handles empty object', () => {
|
|
896
|
+
const routes = {}
|
|
897
|
+
|
|
898
|
+
const ordering = Routes._makeOrdering(routes)
|
|
899
|
+
expect(ordering).toEqual([])
|
|
205
900
|
})
|
|
206
901
|
})
|