@devp0nt/route0 1.0.0-next.4 → 1.0.0-next.41
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 +561 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/cjs/index.d.cts +264 -83
- package/dist/esm/index.d.ts +264 -83
- package/dist/esm/index.js +473 -71
- package/dist/esm/index.js.map +1 -1
- package/package.json +45 -19
- package/src/index.test.ts +1698 -35
- package/src/index.ts +1073 -245
- package/dist/cjs/index.js +0 -158
- package/dist/cjs/index.js.map +0 -1
package/src/index.test.ts
CHANGED
|
@@ -1,13 +1,43 @@
|
|
|
1
1
|
import { describe, expect, expectTypeOf, it } from 'bun:test'
|
|
2
|
-
import {
|
|
2
|
+
import type {
|
|
3
|
+
AnyRoute,
|
|
4
|
+
AnyRouteOrDefinition,
|
|
5
|
+
CallabelRoute,
|
|
6
|
+
CanInputBeEmpty,
|
|
7
|
+
Extended,
|
|
8
|
+
FlatInput,
|
|
9
|
+
FlatInputStringOnly,
|
|
10
|
+
FlatOutput,
|
|
11
|
+
HasParams,
|
|
12
|
+
HasSearch,
|
|
13
|
+
IsChildren,
|
|
14
|
+
IsParent,
|
|
15
|
+
IsSame,
|
|
16
|
+
IsSameParams,
|
|
17
|
+
ParamsInput,
|
|
18
|
+
ParamsInputStringOnly,
|
|
19
|
+
ParamsOutput,
|
|
20
|
+
SearchInput,
|
|
21
|
+
SearchInputStringOnly,
|
|
22
|
+
SearchOutput,
|
|
23
|
+
StrictFlatInput,
|
|
24
|
+
StrictFlatInputStringOnly,
|
|
25
|
+
StrictFlatOutput,
|
|
26
|
+
StrictSearchInput,
|
|
27
|
+
StrictSearchInputStringOnly,
|
|
28
|
+
StrictSearchOutput,
|
|
29
|
+
} from './index.js'
|
|
30
|
+
import { Route0, Routes } from './index.js'
|
|
3
31
|
|
|
4
|
-
describe('
|
|
32
|
+
describe('Route0', () => {
|
|
5
33
|
it('simple', () => {
|
|
6
34
|
const route0 = Route0.create('/')
|
|
7
35
|
const path = route0.get()
|
|
8
36
|
expect(route0).toBeInstanceOf(Route0)
|
|
9
37
|
expectTypeOf<typeof path>().toEqualTypeOf<'/'>()
|
|
10
38
|
expect(path).toBe('/')
|
|
39
|
+
expectTypeOf<HasParams<typeof route0>>().toEqualTypeOf<false>()
|
|
40
|
+
expect(path).toBe(route0.flat())
|
|
11
41
|
})
|
|
12
42
|
|
|
13
43
|
it('simple, callable', () => {
|
|
@@ -16,13 +46,15 @@ describe('route0', () => {
|
|
|
16
46
|
expect(route0).toBeInstanceOf(Route0)
|
|
17
47
|
expectTypeOf<typeof path>().toEqualTypeOf<'/'>()
|
|
18
48
|
expect(path).toBe('/')
|
|
49
|
+
expect(path).toBe(route0.flat())
|
|
19
50
|
})
|
|
20
51
|
|
|
21
|
-
it('simple any
|
|
52
|
+
it('simple any search', () => {
|
|
22
53
|
const route0 = Route0.create('/')
|
|
23
|
-
const path = route0.get({
|
|
54
|
+
const path = route0.get({ search: { q: '1' } })
|
|
24
55
|
expectTypeOf<typeof path>().toEqualTypeOf<`/?${string}`>()
|
|
25
56
|
expect(path).toBe('/?q=1')
|
|
57
|
+
expect(path).toBe(route0.flat({ q: '1' }))
|
|
26
58
|
})
|
|
27
59
|
|
|
28
60
|
it('params', () => {
|
|
@@ -30,35 +62,42 @@ describe('route0', () => {
|
|
|
30
62
|
const path = route0.get({ x: '1', y: 2, z: '3' })
|
|
31
63
|
expectTypeOf<typeof path>().toEqualTypeOf<`/prefix/${string}/some/${string}/${string}`>()
|
|
32
64
|
expect(path).toBe('/prefix/1/some/2/3')
|
|
65
|
+
expectTypeOf<HasParams<typeof route0>>().toEqualTypeOf<true>()
|
|
66
|
+
expect(path).toBe(route0.flat({ x: '1', y: 2, z: '3' }))
|
|
33
67
|
})
|
|
34
68
|
|
|
35
|
-
it('params and any
|
|
69
|
+
it('params and any search', () => {
|
|
36
70
|
const route0 = Route0.create('/prefix/:x/some/:y/:z')
|
|
37
|
-
const path = route0.get({ x: '1', y: 2, z: '3',
|
|
71
|
+
const path = route0.get({ x: '1', y: 2, z: '3', search: { q: '1' } })
|
|
38
72
|
expectTypeOf<typeof path>().toEqualTypeOf<`/prefix/${string}/some/${string}/${string}?${string}`>()
|
|
39
73
|
expect(path).toBe('/prefix/1/some/2/3?q=1')
|
|
74
|
+
expect(path).toBe(route0.flat({ x: '1', y: 2, z: '3', q: '1' }))
|
|
40
75
|
})
|
|
41
76
|
|
|
42
|
-
it('
|
|
77
|
+
it('search', () => {
|
|
43
78
|
const route0 = Route0.create('/prefix&y&z')
|
|
44
|
-
expectTypeOf<(typeof route0)['
|
|
45
|
-
const path = route0.get({
|
|
79
|
+
expectTypeOf<(typeof route0)['searchDefinition']>().toEqualTypeOf<{ y: true; z: true }>()
|
|
80
|
+
const path = route0.get({ search: { y: '1', z: '2' } })
|
|
46
81
|
expectTypeOf<typeof path>().toEqualTypeOf<`/prefix?${string}`>()
|
|
47
82
|
expect(path).toBe('/prefix?y=1&z=2')
|
|
83
|
+
expect(path).toBe(route0.flat({ y: '1', z: '2' }))
|
|
48
84
|
})
|
|
49
85
|
|
|
50
|
-
it('params and
|
|
86
|
+
it('params and search', () => {
|
|
51
87
|
const route0 = Route0.create('/prefix/:x/some/:y/:z&z&c')
|
|
52
|
-
const path = route0.get({ x: '1', y: '2', z: '3',
|
|
88
|
+
const path = route0.get({ x: '1', y: '2', z: '3', search: { z: '4', c: '5' } })
|
|
53
89
|
expectTypeOf<typeof path>().toEqualTypeOf<`/prefix/${string}/some/${string}/${string}?${string}`>()
|
|
54
90
|
expect(path).toBe('/prefix/1/some/2/3?z=4&c=5')
|
|
91
|
+
expect(route0.flat({ x: '1', y: '2', z: '4', c: '5' })).toBe('/prefix/1/some/2/4?z=4&c=5')
|
|
55
92
|
})
|
|
56
93
|
|
|
57
|
-
it('params and
|
|
94
|
+
it('params and search and any search', () => {
|
|
58
95
|
const route0 = Route0.create('/prefix/:x/some/:y/:z&z&c')
|
|
59
|
-
const path = route0.get({ x: '1', y: '2', z: '3',
|
|
96
|
+
const path = route0.get({ x: '1', y: '2', z: '3', search: { z: '4', c: '5', o: '6' } })
|
|
60
97
|
expectTypeOf<typeof path>().toEqualTypeOf<`/prefix/${string}/some/${string}/${string}?${string}`>()
|
|
61
98
|
expect(path).toBe('/prefix/1/some/2/3?z=4&c=5&o=6')
|
|
99
|
+
// very strange case
|
|
100
|
+
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
101
|
})
|
|
63
102
|
|
|
64
103
|
it('simple extend', () => {
|
|
@@ -67,15 +106,19 @@ describe('route0', () => {
|
|
|
67
106
|
const path = route1.get()
|
|
68
107
|
expectTypeOf<typeof path>().toEqualTypeOf<`/prefix/suffix`>()
|
|
69
108
|
expect(path).toBe('/prefix/suffix')
|
|
109
|
+
expect(path).toBe(route1.flat())
|
|
70
110
|
})
|
|
71
111
|
|
|
72
112
|
it('simple extend double slash', () => {
|
|
73
113
|
const route0 = Route0.create('/')
|
|
114
|
+
expect(route0.get()).toBe('/')
|
|
74
115
|
const route1 = route0.extend('/suffix1/')
|
|
116
|
+
expect(route1.get()).toBe('/suffix1/')
|
|
75
117
|
const route2 = route1.extend('/suffix2')
|
|
76
118
|
const path = route2.get()
|
|
77
119
|
expectTypeOf<typeof path>().toEqualTypeOf<`/suffix1/suffix2`>()
|
|
78
120
|
expect(path).toBe('/suffix1/suffix2')
|
|
121
|
+
expect(path).toBe(route2.flat())
|
|
79
122
|
})
|
|
80
123
|
|
|
81
124
|
it('simple extend no slash', () => {
|
|
@@ -85,6 +128,37 @@ describe('route0', () => {
|
|
|
85
128
|
const path = route2.get()
|
|
86
129
|
expectTypeOf<typeof path>().toEqualTypeOf<`/suffix1/suffix2`>()
|
|
87
130
|
expect(path).toBe('/suffix1/suffix2')
|
|
131
|
+
expect(path).toBe(route2.flat())
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('simple extend no slash chaos', () => {
|
|
135
|
+
const route0 = Route0.create('/')
|
|
136
|
+
expectTypeOf<(typeof route0)['definition']>().toEqualTypeOf<'/'>()
|
|
137
|
+
expect(route0.get()).toBe('/')
|
|
138
|
+
|
|
139
|
+
const route1 = Route0.create('')
|
|
140
|
+
expectTypeOf<(typeof route1)['definition']>().toEqualTypeOf<''>()
|
|
141
|
+
expect(route1.get()).toBe('')
|
|
142
|
+
|
|
143
|
+
const route2 = route0.extend('/')
|
|
144
|
+
expectTypeOf<(typeof route2)['definition']>().toEqualTypeOf<'/'>()
|
|
145
|
+
expect(route2.get()).toBe('/')
|
|
146
|
+
|
|
147
|
+
const route3 = route1.extend('/')
|
|
148
|
+
expectTypeOf<(typeof route3)['definition']>().toEqualTypeOf<'/'>()
|
|
149
|
+
expect(route3.get()).toBe('/')
|
|
150
|
+
|
|
151
|
+
const route4 = route0.extend('path/')
|
|
152
|
+
expectTypeOf<(typeof route4)['definition']>().toEqualTypeOf<'/path/'>()
|
|
153
|
+
expect(route4.get()).toBe('/path/')
|
|
154
|
+
|
|
155
|
+
const route5 = route1.extend('/path/')
|
|
156
|
+
expectTypeOf<(typeof route5)['definition']>().toEqualTypeOf<'/path/'>()
|
|
157
|
+
expect(route5.get()).toBe('/path/')
|
|
158
|
+
|
|
159
|
+
const route6 = route1.extend('path')
|
|
160
|
+
expectTypeOf<(typeof route6)['definition']>().toEqualTypeOf<'/path'>()
|
|
161
|
+
expect(route6.get()).toBe('/path')
|
|
88
162
|
})
|
|
89
163
|
|
|
90
164
|
it('extend with params', () => {
|
|
@@ -93,13 +167,14 @@ describe('route0', () => {
|
|
|
93
167
|
const path = route1.get({ x: '1', y: '2' })
|
|
94
168
|
expectTypeOf<typeof path>().toEqualTypeOf<`/prefix/${string}/suffix/${string}`>()
|
|
95
169
|
expect(path).toBe('/prefix/1/suffix/2')
|
|
170
|
+
expect(path).toBe(route1.flat({ x: '1', y: '2' }))
|
|
96
171
|
})
|
|
97
172
|
|
|
98
173
|
it('extend with search params', () => {
|
|
99
174
|
const route0 = Route0.create('/prefix&y&z')
|
|
100
175
|
const route1 = route0.extend('/suffix&z&c')
|
|
101
|
-
const path = route1.get({
|
|
102
|
-
expectTypeOf<(typeof route1)['
|
|
176
|
+
const path = route1.get({ search: { y: '2', c: '3', a: '4' } })
|
|
177
|
+
expectTypeOf<(typeof route1)['searchDefinition']>().toEqualTypeOf<{
|
|
103
178
|
z: true
|
|
104
179
|
c: true
|
|
105
180
|
}>()
|
|
@@ -108,13 +183,14 @@ describe('route0', () => {
|
|
|
108
183
|
const path1 = route1.get()
|
|
109
184
|
expectTypeOf<typeof path1>().toEqualTypeOf<`/prefix/suffix`>()
|
|
110
185
|
expect(path1).toBe('/prefix/suffix')
|
|
186
|
+
expect(path1).toBe(route1.flat())
|
|
111
187
|
})
|
|
112
188
|
|
|
113
|
-
it('extend with params and
|
|
189
|
+
it('extend with params and search', () => {
|
|
114
190
|
const route0 = Route0.create('/prefix/:id&y&z')
|
|
115
191
|
const route1 = route0.extend('/:sn/suffix&z&c')
|
|
116
|
-
const path = route1.get({ id: 'myid', sn: 'mysn',
|
|
117
|
-
expectTypeOf<(typeof route1)['
|
|
192
|
+
const path = route1.get({ id: 'myid', sn: 'mysn', search: { y: '2', c: '3', a: '4' } })
|
|
193
|
+
expectTypeOf<(typeof route1)['searchDefinition']>().toEqualTypeOf<{
|
|
118
194
|
z: true
|
|
119
195
|
c: true
|
|
120
196
|
}>()
|
|
@@ -123,13 +199,14 @@ describe('route0', () => {
|
|
|
123
199
|
const path1 = route1.get({ id: 'myid', sn: 'mysn' })
|
|
124
200
|
expectTypeOf<typeof path1>().toEqualTypeOf<`/prefix/${string}/${string}/suffix`>()
|
|
125
201
|
expect(path1).toBe('/prefix/myid/mysn/suffix')
|
|
202
|
+
expect(path1).toBe(route1.flat({ id: 'myid', sn: 'mysn' }))
|
|
126
203
|
})
|
|
127
204
|
|
|
128
|
-
it('extend with params and
|
|
205
|
+
it('extend with params and search, callable', () => {
|
|
129
206
|
const route0 = Route0.create('/prefix/:id&y&z')
|
|
130
207
|
const route1 = route0.extend('/:sn/suffix&z&c')
|
|
131
|
-
const path = route1({ id: 'myid', sn: 'mysn',
|
|
132
|
-
expectTypeOf<(typeof route1)['
|
|
208
|
+
const path = route1({ id: 'myid', sn: 'mysn', search: { y: '2', c: '3', a: '4' } })
|
|
209
|
+
expectTypeOf<(typeof route1)['searchDefinition']>().toEqualTypeOf<{
|
|
133
210
|
z: true
|
|
134
211
|
c: true
|
|
135
212
|
}>()
|
|
@@ -138,6 +215,7 @@ describe('route0', () => {
|
|
|
138
215
|
const path1 = route1({ id: 'myid', sn: 'mysn' })
|
|
139
216
|
expectTypeOf<typeof path1>().toEqualTypeOf<`/prefix/${string}/${string}/suffix`>()
|
|
140
217
|
expect(path1).toBe('/prefix/myid/mysn/suffix')
|
|
218
|
+
expect(path1).toBe(route1.flat({ id: 'myid', sn: 'mysn' }))
|
|
141
219
|
})
|
|
142
220
|
|
|
143
221
|
it('abs default', () => {
|
|
@@ -145,6 +223,7 @@ describe('route0', () => {
|
|
|
145
223
|
const path = route0.get({ abs: true })
|
|
146
224
|
expectTypeOf<typeof path>().toEqualTypeOf<`${string}/path`>()
|
|
147
225
|
expect(path).toBe('https://example.com/path')
|
|
226
|
+
expect(path).toBe(route0.flat({}, true))
|
|
148
227
|
})
|
|
149
228
|
|
|
150
229
|
it('abs set', () => {
|
|
@@ -152,6 +231,7 @@ describe('route0', () => {
|
|
|
152
231
|
const path = route0.get({ abs: true })
|
|
153
232
|
expectTypeOf<typeof path>().toEqualTypeOf<`${string}/path`>()
|
|
154
233
|
expect(path).toBe('https://x.com/path')
|
|
234
|
+
expect(path).toBe(route0.flat({}, true))
|
|
155
235
|
})
|
|
156
236
|
|
|
157
237
|
it('abs override', () => {
|
|
@@ -160,6 +240,7 @@ describe('route0', () => {
|
|
|
160
240
|
const path = route0.get({ abs: true })
|
|
161
241
|
expectTypeOf<typeof path>().toEqualTypeOf<`${string}/path`>()
|
|
162
242
|
expect(path).toBe('https://y.com/path')
|
|
243
|
+
expect(path).toBe(route0.flat({}, true))
|
|
163
244
|
})
|
|
164
245
|
|
|
165
246
|
it('abs override extend', () => {
|
|
@@ -169,38 +250,1620 @@ describe('route0', () => {
|
|
|
169
250
|
const path = route1.get({ abs: true })
|
|
170
251
|
expectTypeOf<typeof path>().toEqualTypeOf<`${string}/path/suffix`>()
|
|
171
252
|
expect(path).toBe('https://y.com/path/suffix')
|
|
253
|
+
expect(path).toBe(route1.flat({}, true))
|
|
172
254
|
})
|
|
173
255
|
|
|
174
|
-
it('abs override many', () => {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
})
|
|
256
|
+
// it('abs override many', () => {
|
|
257
|
+
// const route0 = Route0.create('/path', { baseUrl: 'https://x.com' })
|
|
258
|
+
// const route1 = route0.extend('/suffix')
|
|
259
|
+
// const routes = {
|
|
260
|
+
// r0: route0,
|
|
261
|
+
// r1: route1,
|
|
262
|
+
// }
|
|
263
|
+
// const routes2 = Route0._.overrideMany(routes, { baseUrl: 'https://z.com' })
|
|
264
|
+
// const path = routes2.r1.get({ abs: true })
|
|
265
|
+
// expectTypeOf<typeof path>().toEqualTypeOf<`${string}/path/suffix`>()
|
|
266
|
+
// expect(path).toBe('https://z.com/path/suffix')
|
|
267
|
+
// })
|
|
186
268
|
|
|
187
269
|
it('type errors: require params when defined', () => {
|
|
188
270
|
const rWith = Route0.create('/a/:id')
|
|
189
271
|
// @ts-expect-error missing required path params
|
|
190
272
|
expect(rWith.get()).toBe('/a/undefined')
|
|
273
|
+
// @ts-expect-error missing required path params
|
|
274
|
+
expect(rWith.flat()).toBe('/a/undefined')
|
|
191
275
|
|
|
192
276
|
// @ts-expect-error missing required path params
|
|
193
277
|
expect(rWith.get({})).toBe('/a/undefined')
|
|
278
|
+
// @ts-expect-error missing required path params
|
|
279
|
+
expect(rWith.flat({})).toBe('/a/undefined')
|
|
194
280
|
// @ts-expect-error missing required path params (object form abs)
|
|
195
281
|
expect(rWith.get({ abs: true })).toBe('https://example.com/a/undefined')
|
|
196
|
-
// @ts-expect-error missing required path params (object form
|
|
197
|
-
expect(rWith.
|
|
282
|
+
// @ts-expect-error missing required path params (object form abs)
|
|
283
|
+
expect(rWith.flat({}, true)).toBe('https://example.com/a/undefined')
|
|
284
|
+
// @ts-expect-error missing required path params (object form search)
|
|
285
|
+
expect(rWith.get({ search: { q: '1' } })).toBe('/a/undefined?q=1')
|
|
286
|
+
// @ts-expect-error missing required path params (object form search)
|
|
287
|
+
expect(rWith.flat({ q: '1' })).toBe('/a/undefined?q=1')
|
|
198
288
|
|
|
199
289
|
// @ts-expect-error params can not be sent as object value it should be argument
|
|
200
290
|
rWith.get({ params: { id: '1' } }) // not throw becouse this will not used
|
|
291
|
+
expect(rWith.flat({ id: '1' })).toBe('/a/1')
|
|
201
292
|
|
|
202
293
|
const rNo = Route0.create('/b')
|
|
203
294
|
// @ts-expect-error no path params allowed for this route (shorthand)
|
|
204
295
|
expect(rNo.get({ id: '1' })).toBe('/b')
|
|
296
|
+
expect(rNo.flat({ id: '1' })).toBe('/b?id=1')
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
it('really any route assignable to AnyRoute', () => {
|
|
300
|
+
expectTypeOf<Route0<string>>().toExtend<AnyRoute>()
|
|
301
|
+
expectTypeOf<Route0<string>>().toExtend<AnyRouteOrDefinition>()
|
|
302
|
+
expectTypeOf<Route0<'/path'>>().toExtend<AnyRoute>()
|
|
303
|
+
expectTypeOf<Route0<'/path'>>().toExtend<AnyRouteOrDefinition>()
|
|
304
|
+
expectTypeOf<Route0<'/path/:id'>>().toExtend<AnyRoute>()
|
|
305
|
+
expectTypeOf<Route0<'/path/:id'>>().toExtend<AnyRouteOrDefinition>()
|
|
306
|
+
expectTypeOf<Route0<'/path/:id&x'>>().toExtend<AnyRoute>()
|
|
307
|
+
expectTypeOf<CallabelRoute<'/path'>>().toExtend<AnyRouteOrDefinition>()
|
|
308
|
+
expectTypeOf<CallabelRoute<'/path'>>().toExtend<AnyRoute>()
|
|
309
|
+
expectTypeOf<CallabelRoute<'/path'>>().toExtend<AnyRouteOrDefinition>()
|
|
310
|
+
expectTypeOf<CallabelRoute<'/path/:id'>>().toExtend<AnyRoute>()
|
|
311
|
+
expectTypeOf<CallabelRoute<'/path/:id'>>().toExtend<AnyRouteOrDefinition>()
|
|
312
|
+
expectTypeOf<CallabelRoute<'/path/:id&x'>>().toExtend<AnyRoute>()
|
|
313
|
+
expectTypeOf<CallabelRoute<'/path/:id&x'>>().toExtend<AnyRouteOrDefinition>()
|
|
314
|
+
expectTypeOf<CallabelRoute>().toExtend<AnyRoute>()
|
|
315
|
+
expectTypeOf<CallabelRoute>().toExtend<AnyRouteOrDefinition>()
|
|
316
|
+
|
|
317
|
+
const route = Route0.create('/path')
|
|
318
|
+
expectTypeOf<typeof route>().toExtend<AnyRoute>()
|
|
319
|
+
expectTypeOf<typeof route>().toExtend<AnyRouteOrDefinition>()
|
|
320
|
+
|
|
321
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
322
|
+
const route2 = route.extend('/path2')
|
|
323
|
+
expectTypeOf<typeof route2>().toExtend<AnyRoute>()
|
|
324
|
+
expectTypeOf<typeof route2>().toExtend<AnyRouteOrDefinition>()
|
|
325
|
+
|
|
326
|
+
// Test that specific CallabelRoute with literal path IS assignable to AnyRouteOrDefinition
|
|
327
|
+
expectTypeOf<CallabelRoute<'/ideas/best'>>().toExtend<AnyRouteOrDefinition>()
|
|
328
|
+
|
|
329
|
+
// Test actual function parameter assignment scenario
|
|
330
|
+
const testFn = (_route: AnyRouteOrDefinition) => {
|
|
331
|
+
// intentionally empty
|
|
332
|
+
}
|
|
333
|
+
const callableRoute = Route0.create('/ideas/best')
|
|
334
|
+
testFn(callableRoute) // This should work
|
|
335
|
+
|
|
336
|
+
// Test with params
|
|
337
|
+
const routeWithParams = Route0.create('/ideas/:id')
|
|
338
|
+
testFn(routeWithParams) // This should also work
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
it('clone, from, create', () => {
|
|
342
|
+
const route = Route0.create('/path')
|
|
343
|
+
const clonedRoute = route.clone()
|
|
344
|
+
expect(clonedRoute.get()).toBe('/path')
|
|
345
|
+
expect(clonedRoute).not.toBe(route)
|
|
346
|
+
expect(clonedRoute.definition).toBe(route.definition)
|
|
347
|
+
|
|
348
|
+
const createdRoute = Route0.create(route)
|
|
349
|
+
expect(createdRoute.get()).toBe('/path')
|
|
350
|
+
expect(createdRoute).not.toBe(route)
|
|
351
|
+
expect(createdRoute.definition).toBe(route.definition)
|
|
352
|
+
|
|
353
|
+
const fromRoute = Route0.from(route)
|
|
354
|
+
expect(fromRoute.get()).toBe('/path')
|
|
355
|
+
expect(fromRoute).toBe(route)
|
|
356
|
+
expect(fromRoute.definition).toBe(route.definition)
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
it('x', () => {
|
|
360
|
+
const a = Route0.create('/')
|
|
361
|
+
const b = a.extend('/b')
|
|
362
|
+
const c = b.extend('/:c')
|
|
363
|
+
const d = c.extend('/x')
|
|
364
|
+
expect(Route0.from(a.definition).definition).toBe('/')
|
|
365
|
+
expect(Route0.from(b.definition).definition).toBe('/b')
|
|
366
|
+
expect(Route0.from(c.definition).definition).toBe('/b/:c')
|
|
367
|
+
expect(Route0.from(d.definition).definition).toBe('/b/:c/x')
|
|
368
|
+
expect(Route0.from('/').definition).toBe('/')
|
|
369
|
+
expect(Route0.from('/b').definition).toBe('/b')
|
|
370
|
+
expect(Route0.from('/b/:c').definition).toBe('/b/:c')
|
|
371
|
+
expect(Route0.from('/b/:c/x').definition).toBe('/b/:c/x')
|
|
372
|
+
})
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
describe('type utilities', () => {
|
|
376
|
+
it('HasParams', () => {
|
|
377
|
+
expectTypeOf<HasParams<'/path'>>().toEqualTypeOf<false>()
|
|
378
|
+
expectTypeOf<HasParams<'/path/:id'>>().toEqualTypeOf<true>()
|
|
379
|
+
expectTypeOf<HasParams<'/path/:id/:name'>>().toEqualTypeOf<true>()
|
|
380
|
+
|
|
381
|
+
expectTypeOf<HasParams<Route0<'/path'>>>().toEqualTypeOf<false>()
|
|
382
|
+
expectTypeOf<HasParams<Route0<'/path/:id'>>>().toEqualTypeOf<true>()
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
it('HasSearch', () => {
|
|
386
|
+
expectTypeOf<HasSearch<'/path'>>().toEqualTypeOf<false>()
|
|
387
|
+
expectTypeOf<HasSearch<'/path&x'>>().toEqualTypeOf<true>()
|
|
388
|
+
expectTypeOf<HasSearch<'/path&x&y'>>().toEqualTypeOf<true>()
|
|
389
|
+
|
|
390
|
+
expectTypeOf<HasSearch<Route0<'/path'>>>().toEqualTypeOf<false>()
|
|
391
|
+
expectTypeOf<HasSearch<Route0<'/path&x&y'>>>().toEqualTypeOf<true>()
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
it('ParamsInput', () => {
|
|
395
|
+
expectTypeOf<ParamsInput<'/path'>>().toEqualTypeOf<Record<never, never>>()
|
|
396
|
+
expectTypeOf<ParamsInput<'/path/:id'>>().toEqualTypeOf<{ id: string | number }>()
|
|
397
|
+
expectTypeOf<ParamsInput<'/path/:id/:name'>>().toEqualTypeOf<{ id: string | number; name: string | number }>()
|
|
398
|
+
|
|
399
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
400
|
+
const route = Route0.create('/path/:id/:name')
|
|
401
|
+
expectTypeOf<ParamsInput<typeof route>>().toEqualTypeOf<{ id: string | number; name: string | number }>()
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
it('ParamsOutput', () => {
|
|
405
|
+
expectTypeOf<ParamsOutput<'/path/:id'>>().toEqualTypeOf<{ id: string }>()
|
|
406
|
+
expectTypeOf<ParamsOutput<'/path/:id/:name'>>().toEqualTypeOf<{ id: string; name: string }>()
|
|
407
|
+
|
|
408
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
409
|
+
const route = Route0.create('/path/:id/:name')
|
|
410
|
+
expectTypeOf<ParamsOutput<typeof route>>().toEqualTypeOf<{ id: string; name: string }>()
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
it('SearchInput', () => {
|
|
414
|
+
type T1 = SearchInput<'/path'>
|
|
415
|
+
expectTypeOf<T1>().toEqualTypeOf<Record<string, string | number>>()
|
|
416
|
+
|
|
417
|
+
type T2 = SearchInput<'/path&x&y'>
|
|
418
|
+
expectTypeOf<T2>().toEqualTypeOf<
|
|
419
|
+
Partial<{
|
|
420
|
+
x: string | number
|
|
421
|
+
y: string | number
|
|
422
|
+
}> &
|
|
423
|
+
Record<string, string | number>
|
|
424
|
+
>()
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
it('SearchOutput', () => {
|
|
428
|
+
type T1 = SearchOutput<'/path'>
|
|
429
|
+
expectTypeOf<T1>().toEqualTypeOf<{
|
|
430
|
+
[key: string]: string | undefined
|
|
431
|
+
}>()
|
|
432
|
+
|
|
433
|
+
type T2 = SearchOutput<'/path&x&y'>
|
|
434
|
+
expectTypeOf<T2>().toEqualTypeOf<{
|
|
435
|
+
[key: string]: string | undefined
|
|
436
|
+
x?: string | undefined
|
|
437
|
+
y?: string | undefined
|
|
438
|
+
}>()
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
it('StrictSearchInput', () => {
|
|
442
|
+
type T1 = StrictSearchInput<'/path&x&y'>
|
|
443
|
+
expectTypeOf<T1>().toEqualTypeOf<{ x?: string | number; y?: string | number }>()
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
it('StrictSearchOutput', () => {
|
|
447
|
+
type T1 = StrictSearchOutput<'/path&x&y'>
|
|
448
|
+
expectTypeOf<T1>().toEqualTypeOf<{ x?: string | undefined; y?: string | undefined }>()
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
it('FlatInput', () => {
|
|
452
|
+
type T1 = FlatInput<'/path&x&y'>
|
|
453
|
+
expectTypeOf<T1>().toEqualTypeOf<
|
|
454
|
+
Partial<{
|
|
455
|
+
x: string | number
|
|
456
|
+
y: string | number
|
|
457
|
+
}> &
|
|
458
|
+
Record<string, string | number>
|
|
459
|
+
>()
|
|
460
|
+
|
|
461
|
+
type T2 = FlatInput<'/path/:id&x&y'>
|
|
462
|
+
expectTypeOf<T2>().toEqualTypeOf<
|
|
463
|
+
{
|
|
464
|
+
id: string | number
|
|
465
|
+
} & Partial<{
|
|
466
|
+
x: string | number
|
|
467
|
+
y: string | number
|
|
468
|
+
}> &
|
|
469
|
+
Record<string, string | number>
|
|
470
|
+
>()
|
|
471
|
+
})
|
|
472
|
+
it('StrictFlatInput', () => {
|
|
473
|
+
type T1 = StrictFlatInput<'/path&x&y'>
|
|
474
|
+
expectTypeOf<T1>().toEqualTypeOf<{ x?: string | number; y?: string | number }>()
|
|
475
|
+
type T2 = StrictFlatInput<'/path/:id&x&y'>
|
|
476
|
+
expectTypeOf<T2>().toEqualTypeOf<
|
|
477
|
+
Partial<{
|
|
478
|
+
x: string | number
|
|
479
|
+
y: string | number
|
|
480
|
+
}> & {
|
|
481
|
+
id: string | number
|
|
482
|
+
}
|
|
483
|
+
>()
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
it('FlatOutput', () => {
|
|
487
|
+
type T1 = FlatOutput<'/path&x&y'>
|
|
488
|
+
expectTypeOf<T1>().toEqualTypeOf<{
|
|
489
|
+
[x: string]: string | undefined
|
|
490
|
+
x?: string | undefined
|
|
491
|
+
y?: string | undefined
|
|
492
|
+
}>()
|
|
493
|
+
|
|
494
|
+
type T2 = FlatOutput<'/path/:id&x&y'>
|
|
495
|
+
expectTypeOf<T2>().toEqualTypeOf<
|
|
496
|
+
{
|
|
497
|
+
id: string
|
|
498
|
+
} & {
|
|
499
|
+
[x: string]: string | undefined
|
|
500
|
+
x?: string | undefined
|
|
501
|
+
y?: string | undefined
|
|
502
|
+
}
|
|
503
|
+
>()
|
|
504
|
+
})
|
|
505
|
+
it('StrictFlatOutput', () => {
|
|
506
|
+
type T1 = StrictFlatOutput<'/path&x&y'>
|
|
507
|
+
expectTypeOf<T1>().toEqualTypeOf<{ x?: string | undefined; y?: string | undefined }>()
|
|
508
|
+
type T2 = StrictFlatOutput<'/path/:id&x&y'>
|
|
509
|
+
expectTypeOf<T2>().toEqualTypeOf<
|
|
510
|
+
{ id: string } & Partial<{
|
|
511
|
+
x?: string | undefined
|
|
512
|
+
y?: string | undefined
|
|
513
|
+
}>
|
|
514
|
+
>()
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
it('ParamsInputStringOnly', () => {
|
|
518
|
+
expectTypeOf<ParamsInputStringOnly<'/path'>>().toEqualTypeOf<Record<never, never>>()
|
|
519
|
+
expectTypeOf<ParamsInputStringOnly<'/path/:id'>>().toEqualTypeOf<{ id: string }>()
|
|
520
|
+
expectTypeOf<ParamsInputStringOnly<'/path/:id/:name'>>().toEqualTypeOf<{ id: string; name: string }>()
|
|
521
|
+
|
|
522
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
523
|
+
const route = Route0.create('/path/:id/:name')
|
|
524
|
+
expectTypeOf<ParamsInputStringOnly<typeof route>>().toEqualTypeOf<{ id: string; name: string }>()
|
|
525
|
+
})
|
|
526
|
+
|
|
527
|
+
it('SearchInputStringOnly', () => {
|
|
528
|
+
type T1 = SearchInputStringOnly<'/path'>
|
|
529
|
+
expectTypeOf<T1>().toEqualTypeOf<Record<string, string>>()
|
|
530
|
+
|
|
531
|
+
type T2 = SearchInputStringOnly<'/path&x&y'>
|
|
532
|
+
expectTypeOf<T2>().toEqualTypeOf<
|
|
533
|
+
Partial<{
|
|
534
|
+
x: string
|
|
535
|
+
y: string
|
|
536
|
+
}> &
|
|
537
|
+
Record<string, string>
|
|
538
|
+
>()
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
it('StrictSearchInputStringOnly', () => {
|
|
542
|
+
type T1 = StrictSearchInputStringOnly<'/path&x&y'>
|
|
543
|
+
expectTypeOf<T1>().toEqualTypeOf<{ x?: string; y?: string }>()
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
it('FlatInputStringOnly', () => {
|
|
547
|
+
type T1 = FlatInputStringOnly<'/path&x&y'>
|
|
548
|
+
expectTypeOf<T1>().toEqualTypeOf<
|
|
549
|
+
Partial<{
|
|
550
|
+
x: string
|
|
551
|
+
y: string
|
|
552
|
+
}> &
|
|
553
|
+
Record<string, string>
|
|
554
|
+
>()
|
|
555
|
+
|
|
556
|
+
type T2 = FlatInputStringOnly<'/path/:id&x&y'>
|
|
557
|
+
expectTypeOf<T2>().toEqualTypeOf<
|
|
558
|
+
{
|
|
559
|
+
id: string
|
|
560
|
+
} & Partial<{
|
|
561
|
+
x: string
|
|
562
|
+
y: string
|
|
563
|
+
}> &
|
|
564
|
+
Record<string, string>
|
|
565
|
+
>()
|
|
566
|
+
})
|
|
567
|
+
it('StrictFlatInputStringOnly', () => {
|
|
568
|
+
type T1 = StrictFlatInputStringOnly<'/path&x&y'>
|
|
569
|
+
expectTypeOf<T1>().toEqualTypeOf<{ x?: string; y?: string }>()
|
|
570
|
+
type T2 = StrictFlatInputStringOnly<'/path/:id&x&y'>
|
|
571
|
+
expectTypeOf<T2>().toEqualTypeOf<
|
|
572
|
+
Partial<{
|
|
573
|
+
x: string
|
|
574
|
+
y: string
|
|
575
|
+
}> & {
|
|
576
|
+
id: string
|
|
577
|
+
}
|
|
578
|
+
>()
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
it('CanInputBeEmpty', () => {
|
|
582
|
+
type T1 = CanInputBeEmpty<'/path'>
|
|
583
|
+
expectTypeOf<T1>().toEqualTypeOf<true>()
|
|
584
|
+
type T2 = CanInputBeEmpty<'/path/:id'>
|
|
585
|
+
expectTypeOf<T2>().toEqualTypeOf<false>()
|
|
586
|
+
type T3 = CanInputBeEmpty<'/path&x&y'>
|
|
587
|
+
expectTypeOf<T3>().toEqualTypeOf<true>()
|
|
588
|
+
type T4 = CanInputBeEmpty<'/path/:id&x&y'>
|
|
589
|
+
expectTypeOf<T4>().toEqualTypeOf<false>()
|
|
590
|
+
})
|
|
591
|
+
|
|
592
|
+
it('IsParent', () => {
|
|
593
|
+
type T1 = IsParent<'/path/child', '/path'>
|
|
594
|
+
type T2 = IsParent<'/path', '/path/child'>
|
|
595
|
+
type T3 = IsParent<'/other', '/path'>
|
|
596
|
+
type T4 = IsParent<'/path', '/path'>
|
|
597
|
+
expectTypeOf<T1>().toEqualTypeOf<true>()
|
|
598
|
+
expectTypeOf<T2>().toEqualTypeOf<false>()
|
|
599
|
+
expectTypeOf<T3>().toEqualTypeOf<false>()
|
|
600
|
+
expectTypeOf<T4>().toEqualTypeOf<false>()
|
|
601
|
+
})
|
|
602
|
+
|
|
603
|
+
it('IsChildren', () => {
|
|
604
|
+
type T1 = IsChildren<'/path', '/path/child'>
|
|
605
|
+
type T2 = IsChildren<'/path/child', '/path'>
|
|
606
|
+
type T3 = IsChildren<'/path', '/other'>
|
|
607
|
+
type T4 = IsChildren<'/path', '/path'>
|
|
608
|
+
expectTypeOf<T1>().toEqualTypeOf<true>()
|
|
609
|
+
expectTypeOf<T2>().toEqualTypeOf<false>()
|
|
610
|
+
expectTypeOf<T3>().toEqualTypeOf<false>()
|
|
611
|
+
expectTypeOf<T4>().toEqualTypeOf<false>()
|
|
612
|
+
})
|
|
613
|
+
|
|
614
|
+
it('IsSame', () => {
|
|
615
|
+
type T1 = IsSame<'/path', '/path'>
|
|
616
|
+
type T2 = IsSame<'/path', '/path/child'>
|
|
617
|
+
type T3 = IsSame<'/path/child', '/path'>
|
|
618
|
+
expectTypeOf<T1>().toEqualTypeOf<true>()
|
|
619
|
+
expectTypeOf<T2>().toEqualTypeOf<false>()
|
|
620
|
+
expectTypeOf<T3>().toEqualTypeOf<false>()
|
|
621
|
+
})
|
|
622
|
+
|
|
623
|
+
it('IsSameParams', () => {
|
|
624
|
+
type T1 = IsSameParams<'/path', '/other'>
|
|
625
|
+
type T2 = IsSameParams<'/path/:id', '/other/:id'>
|
|
626
|
+
type T3 = IsSameParams<'/path/:id', '/other'>
|
|
627
|
+
type T4 = IsSameParams<'/path/:id', '/other/:name'>
|
|
628
|
+
expectTypeOf<T1>().toEqualTypeOf<true>()
|
|
629
|
+
expectTypeOf<T2>().toEqualTypeOf<true>()
|
|
630
|
+
expectTypeOf<T3>().toEqualTypeOf<false>()
|
|
631
|
+
expectTypeOf<T4>().toEqualTypeOf<false>()
|
|
632
|
+
})
|
|
633
|
+
|
|
634
|
+
it('Extended', () => {
|
|
635
|
+
expectTypeOf<Extended<'/path', '/child'>>().toEqualTypeOf<Route0<'/path/child'>>()
|
|
636
|
+
expectTypeOf<Extended<'/path', '/:id'>>().toEqualTypeOf<Route0<'/path/:id'>>()
|
|
637
|
+
expectTypeOf<Extended<'/path', '&x&y'>>().toEqualTypeOf<Route0<'/path&x&y'>>()
|
|
638
|
+
expectTypeOf<Extended<'/path/:id', '/child&x'>>().toEqualTypeOf<Route0<'/path/:id/child&x'>>()
|
|
639
|
+
expectTypeOf<Extended<undefined, '/path'>>().toEqualTypeOf<Route0<'/path'>>()
|
|
640
|
+
|
|
641
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
642
|
+
const parent = Route0.create('/path')
|
|
643
|
+
expectTypeOf<Extended<typeof parent, '/child'>>().toEqualTypeOf<Route0<'/path/child'>>()
|
|
644
|
+
})
|
|
645
|
+
})
|
|
646
|
+
|
|
647
|
+
describe('getLocation', () => {
|
|
648
|
+
describe('Route0', () => {
|
|
649
|
+
it('.getLocation location of url', () => {
|
|
650
|
+
let loc = Route0.getLocation('/prefix/some/suffix')
|
|
651
|
+
expect(loc).toMatchObject({
|
|
652
|
+
hash: '',
|
|
653
|
+
href: undefined,
|
|
654
|
+
hrefRel: '/prefix/some/suffix',
|
|
655
|
+
abs: false,
|
|
656
|
+
origin: undefined,
|
|
657
|
+
params: undefined,
|
|
658
|
+
pathname: '/prefix/some/suffix',
|
|
659
|
+
searchParams: {},
|
|
660
|
+
search: '',
|
|
661
|
+
})
|
|
662
|
+
loc = Route0.getLocation('/prefix/some/suffix?x=1&z=2')
|
|
663
|
+
expect(loc).toMatchObject({
|
|
664
|
+
hash: '',
|
|
665
|
+
href: undefined,
|
|
666
|
+
hrefRel: '/prefix/some/suffix?x=1&z=2',
|
|
667
|
+
abs: false,
|
|
668
|
+
origin: undefined,
|
|
669
|
+
params: undefined,
|
|
670
|
+
pathname: '/prefix/some/suffix',
|
|
671
|
+
searchParams: { x: '1', z: '2' },
|
|
672
|
+
search: '?x=1&z=2',
|
|
673
|
+
})
|
|
674
|
+
loc = Route0.getLocation('https://example.com/prefix/some/suffix?x=1&z=2')
|
|
675
|
+
expect(loc).toMatchObject({
|
|
676
|
+
hash: '',
|
|
677
|
+
href: 'https://example.com/prefix/some/suffix?x=1&z=2',
|
|
678
|
+
hrefRel: '/prefix/some/suffix?x=1&z=2',
|
|
679
|
+
abs: true,
|
|
680
|
+
origin: 'https://example.com',
|
|
681
|
+
params: undefined,
|
|
682
|
+
pathname: '/prefix/some/suffix',
|
|
683
|
+
searchParams: { x: '1', z: '2' },
|
|
684
|
+
search: '?x=1&z=2',
|
|
685
|
+
})
|
|
686
|
+
})
|
|
687
|
+
|
|
688
|
+
it('.toRelLocation', () => {
|
|
689
|
+
const loc = Route0.toRelLocation(Route0.getLocation('https://example.com/prefix/some/suffix?x=1&z=2'))
|
|
690
|
+
expect(loc).toMatchObject({
|
|
691
|
+
hash: '',
|
|
692
|
+
href: undefined,
|
|
693
|
+
hrefRel: '/prefix/some/suffix?x=1&z=2',
|
|
694
|
+
abs: false,
|
|
695
|
+
origin: undefined,
|
|
696
|
+
params: undefined,
|
|
697
|
+
pathname: '/prefix/some/suffix',
|
|
698
|
+
searchParams: { x: '1', z: '2' },
|
|
699
|
+
search: '?x=1&z=2',
|
|
700
|
+
})
|
|
701
|
+
const sameLoc = Route0.toRelLocation(loc)
|
|
702
|
+
expect(sameLoc).toMatchObject(loc)
|
|
703
|
+
})
|
|
704
|
+
|
|
705
|
+
it('.toAbsLocation', () => {
|
|
706
|
+
const loc = Route0.toRelLocation(Route0.getLocation('https://example.com/prefix/some/suffix?x=1&z=2'))
|
|
707
|
+
const absLoc = Route0.toAbsLocation(loc, 'https://example2.com')
|
|
708
|
+
expect(absLoc).toMatchObject({
|
|
709
|
+
hash: '',
|
|
710
|
+
href: 'https://example2.com/prefix/some/suffix?x=1&z=2',
|
|
711
|
+
hrefRel: '/prefix/some/suffix?x=1&z=2',
|
|
712
|
+
abs: true,
|
|
713
|
+
origin: 'https://example2.com',
|
|
714
|
+
params: undefined,
|
|
715
|
+
pathname: '/prefix/some/suffix',
|
|
716
|
+
searchParams: { x: '1', z: '2' },
|
|
717
|
+
search: '?x=1&z=2',
|
|
718
|
+
})
|
|
719
|
+
const sameLoc = Route0.toRelLocation(loc)
|
|
720
|
+
expect(sameLoc).toMatchObject(loc)
|
|
721
|
+
})
|
|
722
|
+
|
|
723
|
+
it('#getLocation() exact match', () => {
|
|
724
|
+
const route0 = Route0.create('/prefix/:x/some/:y/:z/suffix')
|
|
725
|
+
let loc = route0.getLocation('/prefix/some/suffix')
|
|
726
|
+
expect(loc.exact).toBe(false)
|
|
727
|
+
expect(loc.parent).toBe(false)
|
|
728
|
+
expect(loc.children).toBe(false)
|
|
729
|
+
expect(loc.params).toMatchObject({})
|
|
730
|
+
loc = route0.getLocation('/prefix/xxx/some/yyy/zzz/suffix')
|
|
731
|
+
expect(loc.exact).toBe(true)
|
|
732
|
+
expect(loc.parent).toBe(false)
|
|
733
|
+
expect(loc.children).toBe(false)
|
|
734
|
+
if (loc.exact) {
|
|
735
|
+
expectTypeOf<typeof loc.params>().toEqualTypeOf<{ x: string; y: string; z: string }>()
|
|
736
|
+
}
|
|
737
|
+
expect(loc.params).toMatchObject({ x: 'xxx', y: 'yyy', z: 'zzz' })
|
|
738
|
+
})
|
|
739
|
+
|
|
740
|
+
it('#getLocation() parent match', () => {
|
|
741
|
+
expect(Route0.create('/prefix/xxx/some').getLocation('/prefix/xxx/some/extra/path')).toMatchObject({
|
|
742
|
+
exact: false,
|
|
743
|
+
parent: true,
|
|
744
|
+
children: false,
|
|
745
|
+
})
|
|
746
|
+
expect(Route0.create('/prefix/:x/some').getLocation('/prefix/xxx/some/extra/path')).toMatchObject({
|
|
747
|
+
exact: false,
|
|
748
|
+
parent: true,
|
|
749
|
+
children: false,
|
|
750
|
+
})
|
|
751
|
+
expect(Route0.create('/:y/:x/some').getLocation('/prefix/xxx/some/extra/path')).toMatchObject({
|
|
752
|
+
exact: false,
|
|
753
|
+
parent: true,
|
|
754
|
+
children: false,
|
|
755
|
+
})
|
|
756
|
+
})
|
|
757
|
+
|
|
758
|
+
it('#getLocation() children match', () => {
|
|
759
|
+
expect(Route0.create('/prefix/some/extra/path').getLocation('/prefix/some')).toMatchObject({
|
|
760
|
+
exact: false,
|
|
761
|
+
parent: false,
|
|
762
|
+
children: true,
|
|
763
|
+
})
|
|
764
|
+
expect(Route0.create('/prefix/some/extra/:id').getLocation('/prefix/some')).toMatchObject({
|
|
765
|
+
exact: false,
|
|
766
|
+
parent: false,
|
|
767
|
+
children: true,
|
|
768
|
+
})
|
|
769
|
+
expect(Route0.create('/:prefix/some/extra/:id').getLocation('/prefix/some')).toMatchObject({
|
|
770
|
+
exact: false,
|
|
771
|
+
parent: false,
|
|
772
|
+
children: true,
|
|
773
|
+
})
|
|
774
|
+
})
|
|
775
|
+
|
|
776
|
+
it('#getLocation() with host info', () => {
|
|
777
|
+
const route0 = Route0.create('/path')
|
|
778
|
+
const loc = route0.getLocation('https://example.com:8080/path')
|
|
779
|
+
expect(loc.exact).toBe(true)
|
|
780
|
+
expect(loc.origin).toBe('https://example.com:8080')
|
|
781
|
+
expect(loc.host).toBe('example.com:8080')
|
|
782
|
+
expect(loc.hostname).toBe('example.com')
|
|
783
|
+
expect(loc.port).toBe('8080')
|
|
784
|
+
})
|
|
785
|
+
|
|
786
|
+
it('#getLocation() with hash', () => {
|
|
787
|
+
const route0 = Route0.create('/path/:id')
|
|
788
|
+
const loc = route0.getLocation('/path/123#section')
|
|
789
|
+
expect(loc.exact).toBe(true)
|
|
790
|
+
expect(loc.hash).toBe('#section')
|
|
791
|
+
expect(loc.params).toMatchObject({ id: '123' })
|
|
792
|
+
})
|
|
793
|
+
|
|
794
|
+
it('.getLocation accepts URL instance (absolute)', () => {
|
|
795
|
+
const url = new URL('https://example.com/prefix/some/suffix?x=1&z=2#hash')
|
|
796
|
+
const loc = Route0.getLocation(url)
|
|
797
|
+
expect(loc).toMatchObject({
|
|
798
|
+
hash: '#hash',
|
|
799
|
+
href: 'https://example.com/prefix/some/suffix?x=1&z=2#hash',
|
|
800
|
+
hrefRel: '/prefix/some/suffix?x=1&z=2#hash',
|
|
801
|
+
abs: true,
|
|
802
|
+
origin: 'https://example.com',
|
|
803
|
+
pathname: '/prefix/some/suffix',
|
|
804
|
+
searchParams: { x: '1', z: '2' },
|
|
805
|
+
search: '?x=1&z=2',
|
|
806
|
+
})
|
|
807
|
+
})
|
|
808
|
+
|
|
809
|
+
it('.getLocation accepts URL instance (relative with base)', () => {
|
|
810
|
+
const url = new URL('/prefix/some/suffix?x=1&z=2', 'https://example.com')
|
|
811
|
+
const loc = Route0.getLocation(url)
|
|
812
|
+
expect(loc).toMatchObject({
|
|
813
|
+
hash: '',
|
|
814
|
+
href: 'https://example.com/prefix/some/suffix?x=1&z=2',
|
|
815
|
+
hrefRel: '/prefix/some/suffix?x=1&z=2',
|
|
816
|
+
abs: true,
|
|
817
|
+
origin: 'https://example.com',
|
|
818
|
+
pathname: '/prefix/some/suffix',
|
|
819
|
+
searchParams: { x: '1', z: '2' },
|
|
820
|
+
search: '?x=1&z=2',
|
|
821
|
+
})
|
|
822
|
+
})
|
|
823
|
+
})
|
|
824
|
+
|
|
825
|
+
describe('Routes', () => {
|
|
826
|
+
it('exact match returns ExactLocation', () => {
|
|
827
|
+
const routes = Routes.create({
|
|
828
|
+
home: '/',
|
|
829
|
+
users: '/users',
|
|
830
|
+
userDetail: '/users/:id',
|
|
831
|
+
})
|
|
832
|
+
|
|
833
|
+
const loc = routes._.getLocation('/users/123')
|
|
834
|
+
expect(loc.exact).toBe(true)
|
|
835
|
+
expect(loc.parent).toBe(false)
|
|
836
|
+
expect(loc.children).toBe(false)
|
|
837
|
+
expect(loc.pathname).toBe('/users/123')
|
|
838
|
+
expect(Route0.isSame(loc.route, routes.userDetail)).toBe(true)
|
|
839
|
+
if (loc.exact) {
|
|
840
|
+
expect(loc.params).toMatchObject({ id: '123' })
|
|
841
|
+
}
|
|
842
|
+
})
|
|
843
|
+
|
|
844
|
+
it('no exact match returns UnknownLocation (parent case)', () => {
|
|
845
|
+
const routes = Routes.create({
|
|
846
|
+
home: '/',
|
|
847
|
+
users: '/users',
|
|
848
|
+
userDetail: '/users/:id',
|
|
849
|
+
})
|
|
850
|
+
|
|
851
|
+
// '/users/123/posts' is not an exact match for any route
|
|
852
|
+
const loc = routes._.getLocation('/users/123/posts')
|
|
853
|
+
expect(loc.exact).toBe(false)
|
|
854
|
+
expect(loc.parent).toBe(false)
|
|
855
|
+
expect(loc.children).toBe(false)
|
|
856
|
+
expect(loc.pathname).toBe('/users/123/posts')
|
|
857
|
+
})
|
|
858
|
+
|
|
859
|
+
it('no exact match returns UnknownLocation (children case)', () => {
|
|
860
|
+
const routes = Routes.create({
|
|
861
|
+
home: '/',
|
|
862
|
+
users: '/users',
|
|
863
|
+
userDetail: '/users/:id/posts',
|
|
864
|
+
})
|
|
865
|
+
|
|
866
|
+
// '/users/123' is not an exact match for any route
|
|
867
|
+
const loc = routes._.getLocation('/users/123')
|
|
868
|
+
expect(loc.exact).toBe(false)
|
|
869
|
+
expect(loc.parent).toBe(false)
|
|
870
|
+
expect(loc.children).toBe(false)
|
|
871
|
+
expect(loc.pathname).toBe('/users/123')
|
|
872
|
+
})
|
|
873
|
+
|
|
874
|
+
it('no match returns UnknownLocation', () => {
|
|
875
|
+
const routes = Routes.create({
|
|
876
|
+
home: '/',
|
|
877
|
+
users: '/users',
|
|
878
|
+
})
|
|
879
|
+
|
|
880
|
+
const loc = routes._.getLocation('/posts/123')
|
|
881
|
+
expect(loc.exact).toBe(false)
|
|
882
|
+
expect(loc.parent).toBe(false)
|
|
883
|
+
expect(loc.children).toBe(false)
|
|
884
|
+
expect(loc.pathname).toBe('/posts/123')
|
|
885
|
+
expect(loc.params).toBeUndefined()
|
|
886
|
+
})
|
|
887
|
+
|
|
888
|
+
it('matches most specific route', () => {
|
|
889
|
+
const routes = Routes.create({
|
|
890
|
+
userDetail: '/users/:id',
|
|
891
|
+
userPosts: '/users/:id/posts',
|
|
892
|
+
users: '/users',
|
|
893
|
+
})
|
|
894
|
+
|
|
895
|
+
// Should match /users exactly
|
|
896
|
+
const loc1 = routes._.getLocation('/users')
|
|
897
|
+
expect(loc1.exact).toBe(true)
|
|
898
|
+
expect(loc1.pathname).toBe('/users')
|
|
899
|
+
|
|
900
|
+
// Should match /users/:id exactly
|
|
901
|
+
const loc2 = routes._.getLocation('/users/123')
|
|
902
|
+
expect(loc2.exact).toBe(true)
|
|
903
|
+
if (loc2.exact) {
|
|
904
|
+
expect(loc2.params).toMatchObject({ id: '123' })
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Should match /users/:id/posts exactly
|
|
908
|
+
const loc3 = routes._.getLocation('/users/123/posts')
|
|
909
|
+
expect(loc3.exact).toBe(true)
|
|
910
|
+
if (loc3.exact) {
|
|
911
|
+
expect(loc3.params).toMatchObject({ id: '123' })
|
|
912
|
+
}
|
|
913
|
+
})
|
|
914
|
+
|
|
915
|
+
it('with search params', () => {
|
|
916
|
+
const routes = Routes.create({
|
|
917
|
+
search: '/search&q&filter',
|
|
918
|
+
users: '/users',
|
|
919
|
+
})
|
|
920
|
+
|
|
921
|
+
const loc = routes._.getLocation('/search?q=test&filter=all')
|
|
922
|
+
expect(loc.exact).toBe(true)
|
|
923
|
+
expect(loc.pathname).toBe('/search')
|
|
924
|
+
expect(loc.search).toBe('?q=test&filter=all')
|
|
925
|
+
expect(loc.searchParams).toMatchObject({ q: 'test', filter: 'all' })
|
|
926
|
+
})
|
|
927
|
+
|
|
928
|
+
it('with absolute URL', () => {
|
|
929
|
+
const routes = Routes.create({
|
|
930
|
+
api: '/api/v1',
|
|
931
|
+
users: '/api/v1/users',
|
|
932
|
+
})
|
|
933
|
+
|
|
934
|
+
const loc = routes._.getLocation('https://example.com/api/v1/users')
|
|
935
|
+
expect(loc.exact).toBe(true)
|
|
936
|
+
expect(loc.abs).toBe(true)
|
|
937
|
+
expect(loc.origin).toBe('https://example.com')
|
|
938
|
+
expect(loc.pathname).toBe('/api/v1/users')
|
|
939
|
+
expect(loc.href).toBe('https://example.com/api/v1/users')
|
|
940
|
+
})
|
|
941
|
+
|
|
942
|
+
it('with hash', () => {
|
|
943
|
+
const routes = Routes.create({
|
|
944
|
+
userDetail: '/users/:id',
|
|
945
|
+
})
|
|
946
|
+
|
|
947
|
+
const loc = routes._.getLocation('/users/123#profile')
|
|
948
|
+
expect(loc.exact).toBe(true)
|
|
949
|
+
expect(loc.hash).toBe('#profile')
|
|
950
|
+
expect(loc.pathname).toBe('/users/123')
|
|
951
|
+
if (loc.exact) {
|
|
952
|
+
expect(loc.params).toMatchObject({ id: '123' })
|
|
953
|
+
}
|
|
954
|
+
})
|
|
955
|
+
|
|
956
|
+
it('with extended routes', () => {
|
|
957
|
+
const api = Route0.create('/api/v1')
|
|
958
|
+
const routes = Routes.create({
|
|
959
|
+
api,
|
|
960
|
+
users: api.extend('/users'),
|
|
961
|
+
userDetail: api.extend('/users/:id'),
|
|
962
|
+
})
|
|
963
|
+
|
|
964
|
+
const loc = routes._.getLocation('/api/v1/users/456')
|
|
965
|
+
expect(loc.exact).toBe(true)
|
|
966
|
+
if (loc.exact) {
|
|
967
|
+
expect(loc.params).toMatchObject({ id: '456' })
|
|
968
|
+
}
|
|
969
|
+
})
|
|
970
|
+
|
|
971
|
+
it('root route', () => {
|
|
972
|
+
const routes = Routes.create({
|
|
973
|
+
home: '/',
|
|
974
|
+
about: '/about',
|
|
975
|
+
})
|
|
976
|
+
|
|
977
|
+
const loc = routes._.getLocation('/')
|
|
978
|
+
expect(loc.exact).toBe(true)
|
|
979
|
+
expect(loc.pathname).toBe('/')
|
|
980
|
+
})
|
|
981
|
+
|
|
982
|
+
it('with AnyLocation object as input', () => {
|
|
983
|
+
const routes = Routes.create({
|
|
984
|
+
userDetail: '/users/:id',
|
|
985
|
+
})
|
|
986
|
+
|
|
987
|
+
const inputLoc = Route0.getLocation('/users/789')
|
|
988
|
+
const loc = routes._.getLocation(inputLoc)
|
|
989
|
+
expect(loc.exact).toBe(true)
|
|
990
|
+
if (loc.exact) {
|
|
991
|
+
expect(loc.params).toMatchObject({ id: '789' })
|
|
992
|
+
}
|
|
993
|
+
})
|
|
994
|
+
|
|
995
|
+
it('complex routing with params and search', () => {
|
|
996
|
+
const api = Route0.create('/api/v1')
|
|
997
|
+
const routes = Routes.create({
|
|
998
|
+
api,
|
|
999
|
+
users: api.extend('/users'),
|
|
1000
|
+
userDetail: api.extend('/users/:id'),
|
|
1001
|
+
userPosts: api.extend('/users/:id/posts&sort&filter'),
|
|
1002
|
+
})
|
|
1003
|
+
|
|
1004
|
+
const loc = routes._.getLocation('/api/v1/users/42/posts?sort=date&filter=published&extra=value')
|
|
1005
|
+
expect(loc.exact).toBe(true)
|
|
1006
|
+
expect(loc.pathname).toBe('/api/v1/users/42/posts')
|
|
1007
|
+
expect(loc.searchParams).toMatchObject({
|
|
1008
|
+
sort: 'date',
|
|
1009
|
+
filter: 'published',
|
|
1010
|
+
extra: 'value',
|
|
1011
|
+
})
|
|
1012
|
+
if (loc.exact) {
|
|
1013
|
+
expect(loc.params).toMatchObject({ id: '42' })
|
|
1014
|
+
}
|
|
1015
|
+
})
|
|
1016
|
+
|
|
1017
|
+
it('get location for extedned routes', () => {
|
|
1018
|
+
const a = Route0.create('/')
|
|
1019
|
+
const b = a.extend('/b')
|
|
1020
|
+
const c = b.extend('/:c')
|
|
1021
|
+
const d = c.extend('/x')
|
|
1022
|
+
const routes = Routes.create({
|
|
1023
|
+
a,
|
|
1024
|
+
b,
|
|
1025
|
+
c,
|
|
1026
|
+
d,
|
|
1027
|
+
})
|
|
1028
|
+
|
|
1029
|
+
const loc = routes._.getLocation('/b/test')
|
|
1030
|
+
expect(loc.exact).toBe(true)
|
|
1031
|
+
expect(loc.route).toBe('/b/:c')
|
|
1032
|
+
})
|
|
1033
|
+
})
|
|
1034
|
+
})
|
|
1035
|
+
|
|
1036
|
+
describe('Routes', () => {
|
|
1037
|
+
it('create with string routes', () => {
|
|
1038
|
+
const collection = Routes.create({
|
|
1039
|
+
home: '/',
|
|
1040
|
+
about: '/about',
|
|
1041
|
+
contact: '/contact',
|
|
1042
|
+
})
|
|
1043
|
+
|
|
1044
|
+
expect(collection).toBeInstanceOf(Routes)
|
|
1045
|
+
const home = collection.home
|
|
1046
|
+
const about = collection.about
|
|
1047
|
+
const contact = collection.contact
|
|
1048
|
+
|
|
1049
|
+
expect(home).toBeInstanceOf(Route0)
|
|
1050
|
+
expect(about).toBeInstanceOf(Route0)
|
|
1051
|
+
expect(contact).toBeInstanceOf(Route0)
|
|
1052
|
+
|
|
1053
|
+
expect(home.get()).toBe('/')
|
|
1054
|
+
expect(about.get()).toBe('/about')
|
|
1055
|
+
expect(contact.get()).toBe('/contact')
|
|
1056
|
+
})
|
|
1057
|
+
|
|
1058
|
+
it('create with Route0 instances', () => {
|
|
1059
|
+
const homeRoute = Route0.create('/')
|
|
1060
|
+
const aboutRoute = Route0.create('/about')
|
|
1061
|
+
|
|
1062
|
+
const collection = Routes.create({
|
|
1063
|
+
home: homeRoute,
|
|
1064
|
+
about: aboutRoute,
|
|
1065
|
+
})
|
|
1066
|
+
|
|
1067
|
+
expect(collection.home.get()).toBe('/')
|
|
1068
|
+
expect(collection.about.get()).toBe('/about')
|
|
1069
|
+
})
|
|
1070
|
+
|
|
1071
|
+
it('create with mixed string and Route0', () => {
|
|
1072
|
+
const aboutRoute = Route0.create('/about')
|
|
1073
|
+
|
|
1074
|
+
const collection = Routes.create({
|
|
1075
|
+
home: '/',
|
|
1076
|
+
about: aboutRoute,
|
|
1077
|
+
contact: '/contact',
|
|
1078
|
+
})
|
|
1079
|
+
|
|
1080
|
+
expect(collection.home.get()).toBe('/')
|
|
1081
|
+
expect(collection.about.get()).toBe('/about')
|
|
1082
|
+
expect(collection.contact.get()).toBe('/contact')
|
|
1083
|
+
})
|
|
1084
|
+
|
|
1085
|
+
it('create with params and search', () => {
|
|
1086
|
+
const collection = Routes.create({
|
|
1087
|
+
user: '/user/:id',
|
|
1088
|
+
search: '/search&q&filter',
|
|
1089
|
+
userWithSearch: '/user/:id&tab',
|
|
1090
|
+
})
|
|
1091
|
+
|
|
1092
|
+
const user = collection.user
|
|
1093
|
+
expect(user.get({ id: '123' } as any)).toBe('/user/123')
|
|
1094
|
+
|
|
1095
|
+
const search = collection.search
|
|
1096
|
+
expect(search.get({ search: { q: 'test', filter: 'all' } })).toBe('/search?q=test&filter=all')
|
|
1097
|
+
|
|
1098
|
+
const userWithSearch = collection.userWithSearch
|
|
1099
|
+
expect(userWithSearch.get({ id: '456', search: { tab: 'posts' } } as any)).toBe('/user/456?tab=posts')
|
|
1100
|
+
})
|
|
1101
|
+
|
|
1102
|
+
it('get maintains route definitions', () => {
|
|
1103
|
+
const collection = Routes.create({
|
|
1104
|
+
home: '/',
|
|
1105
|
+
user: '/user/:id',
|
|
1106
|
+
})
|
|
1107
|
+
|
|
1108
|
+
const home = collection.home
|
|
1109
|
+
const user = collection.user
|
|
1110
|
+
|
|
1111
|
+
// Verify route definitions are preserved
|
|
1112
|
+
expect(home.definition).toBe('/')
|
|
1113
|
+
expect(user.definition).toBe('/user/:id')
|
|
1114
|
+
expect(home.pathDefinition).toBe('/')
|
|
1115
|
+
expect(user.pathDefinition).toBe('/user/:id')
|
|
1116
|
+
|
|
1117
|
+
// Verify params work correctly
|
|
1118
|
+
expect(user.get({ id: '123' })).toBe('/user/123')
|
|
1119
|
+
})
|
|
1120
|
+
|
|
1121
|
+
it('override with baseUrl', () => {
|
|
1122
|
+
const collection = Routes.create({
|
|
1123
|
+
home: '/',
|
|
1124
|
+
about: '/about',
|
|
1125
|
+
})
|
|
1126
|
+
|
|
1127
|
+
const overridden = collection._.override({ baseUrl: 'https://example.com' })
|
|
1128
|
+
|
|
1129
|
+
const home = overridden.home
|
|
1130
|
+
const about = overridden.about
|
|
1131
|
+
|
|
1132
|
+
expect(home.get({ abs: true })).toBe('https://example.com')
|
|
1133
|
+
expect(about.get({ abs: true })).toBe('https://example.com/about')
|
|
1134
|
+
})
|
|
1135
|
+
|
|
1136
|
+
it('override does not mutate original', () => {
|
|
1137
|
+
const collection = Routes.create({
|
|
1138
|
+
home: '/',
|
|
1139
|
+
})
|
|
1140
|
+
|
|
1141
|
+
const original = collection.home
|
|
1142
|
+
expect(original.get({ abs: true })).toBe('https://example.com')
|
|
1143
|
+
|
|
1144
|
+
const overridden = collection._.override({ baseUrl: 'https://newdomain.com' })
|
|
1145
|
+
const newRoute = overridden.home
|
|
1146
|
+
|
|
1147
|
+
expect(original.get({ abs: true })).toBe('https://example.com')
|
|
1148
|
+
expect(newRoute.get({ abs: true })).toBe('https://newdomain.com')
|
|
1149
|
+
})
|
|
1150
|
+
|
|
1151
|
+
it('override with extended routes', () => {
|
|
1152
|
+
const apiRoute = Route0.create('/api', { baseUrl: 'https://api.example.com' })
|
|
1153
|
+
const usersRoute = apiRoute.extend('/users')
|
|
1154
|
+
|
|
1155
|
+
const collection = Routes.create({
|
|
1156
|
+
api: apiRoute,
|
|
1157
|
+
users: usersRoute,
|
|
1158
|
+
})
|
|
1159
|
+
|
|
1160
|
+
expect(collection.api.get({ abs: true })).toBe('https://api.example.com/api')
|
|
1161
|
+
expect(collection.api({ abs: true })).toBe('https://api.example.com/api')
|
|
1162
|
+
expect(collection.users.get({ abs: true })).toBe('https://api.example.com/api/users')
|
|
1163
|
+
|
|
1164
|
+
const overridden = collection._.override({ baseUrl: 'https://new-api.example.com' })
|
|
1165
|
+
|
|
1166
|
+
expect(overridden.api.get({ abs: true })).toBe('https://new-api.example.com/api')
|
|
1167
|
+
expect(overridden.users.get({ abs: true })).toBe('https://new-api.example.com/api/users')
|
|
1168
|
+
})
|
|
1169
|
+
|
|
1170
|
+
it('hydrate static method', () => {
|
|
1171
|
+
const hydrated = Routes._.hydrate({
|
|
1172
|
+
home: '/',
|
|
1173
|
+
user: '/user/:id',
|
|
1174
|
+
about: Route0.create('/about'),
|
|
1175
|
+
})
|
|
1176
|
+
|
|
1177
|
+
expect(hydrated.home).toBeInstanceOf(Route0)
|
|
1178
|
+
expect(hydrated.user).toBeInstanceOf(Route0)
|
|
1179
|
+
expect(hydrated.about).toBeInstanceOf(Route0)
|
|
1180
|
+
|
|
1181
|
+
expect(hydrated.home.get()).toBe('/')
|
|
1182
|
+
expect(hydrated.user.get({ id: '123' })).toBe('/user/123')
|
|
1183
|
+
expect(hydrated.about.get()).toBe('/about')
|
|
1184
|
+
})
|
|
1185
|
+
|
|
1186
|
+
it('works with callable routes', () => {
|
|
1187
|
+
const collection = Routes.create({
|
|
1188
|
+
home: '/',
|
|
1189
|
+
user: '/user/:id',
|
|
1190
|
+
})
|
|
1191
|
+
|
|
1192
|
+
const home = collection.home
|
|
1193
|
+
const user = collection.user
|
|
1194
|
+
|
|
1195
|
+
// Routes should be callable
|
|
1196
|
+
expect(typeof home).toBe('function')
|
|
1197
|
+
expect(typeof user).toBe('function')
|
|
1198
|
+
expect(home()).toBe('/')
|
|
1199
|
+
expect(user({ id: '789' })).toBe('/user/789')
|
|
1200
|
+
})
|
|
1201
|
+
|
|
1202
|
+
it('complex nested structure', () => {
|
|
1203
|
+
const api = Route0.create('/api/v1', { baseUrl: 'https://api.example.com' })
|
|
1204
|
+
|
|
1205
|
+
const collection = Routes.create({
|
|
1206
|
+
root: '/',
|
|
1207
|
+
api,
|
|
1208
|
+
users: api.extend('/users'),
|
|
1209
|
+
userDetail: api.extend('/users/:id'),
|
|
1210
|
+
userPosts: api.extend('/users/:id/posts&sort&filter'),
|
|
1211
|
+
})
|
|
1212
|
+
|
|
1213
|
+
expect(collection.root.get()).toBe('/')
|
|
1214
|
+
expect(collection.api({ abs: true })).toBe('https://api.example.com/api/v1')
|
|
1215
|
+
expect(collection.users.get({ abs: true })).toBe('https://api.example.com/api/v1/users')
|
|
1216
|
+
|
|
1217
|
+
const userDetailPath: any = collection.userDetail.get({ id: '42', abs: true })
|
|
1218
|
+
expect(userDetailPath).toBe('https://api.example.com/api/v1/users/42')
|
|
1219
|
+
|
|
1220
|
+
const userPostsPath: any = collection.userPosts.get({
|
|
1221
|
+
id: '42',
|
|
1222
|
+
search: { sort: 'date', filter: 'published' },
|
|
1223
|
+
abs: true,
|
|
1224
|
+
})
|
|
1225
|
+
expect(userPostsPath).toBe('https://api.example.com/api/v1/users/42/posts?sort=date&filter=published')
|
|
1226
|
+
})
|
|
1227
|
+
})
|
|
1228
|
+
|
|
1229
|
+
describe('specificity', () => {
|
|
1230
|
+
it('isMoreSpecificThan: static vs param', () => {
|
|
1231
|
+
const static1 = Route0.create('/a/b')
|
|
1232
|
+
const param1 = Route0.create('/a/:id')
|
|
1233
|
+
|
|
1234
|
+
expect(static1.isMoreSpecificThan(param1)).toBe(true)
|
|
1235
|
+
expect(param1.isMoreSpecificThan(static1)).toBe(false)
|
|
1236
|
+
})
|
|
1237
|
+
|
|
1238
|
+
it('isMoreSpecificThan: more static segments wins', () => {
|
|
1239
|
+
const twoStatic = Route0.create('/a/b/c')
|
|
1240
|
+
const oneStatic = Route0.create('/a/:id/c')
|
|
1241
|
+
const noStatic = Route0.create('/a/:id/:name')
|
|
1242
|
+
|
|
1243
|
+
expect(twoStatic.isMoreSpecificThan(oneStatic)).toBe(true)
|
|
1244
|
+
expect(oneStatic.isMoreSpecificThan(twoStatic)).toBe(false)
|
|
1245
|
+
|
|
1246
|
+
expect(oneStatic.isMoreSpecificThan(noStatic)).toBe(true)
|
|
1247
|
+
expect(noStatic.isMoreSpecificThan(oneStatic)).toBe(false)
|
|
1248
|
+
|
|
1249
|
+
expect(twoStatic.isMoreSpecificThan(noStatic)).toBe(true)
|
|
1250
|
+
expect(noStatic.isMoreSpecificThan(twoStatic)).toBe(false)
|
|
1251
|
+
})
|
|
1252
|
+
|
|
1253
|
+
it('isMoreSpecificThan: compares overlapping segments then lexicographically', () => {
|
|
1254
|
+
const longer = Route0.create('/a/:id/b/:name')
|
|
1255
|
+
const shorter = Route0.create('/a/:id')
|
|
1256
|
+
|
|
1257
|
+
// Both have same pattern for overlapping segments: static then param
|
|
1258
|
+
// Falls back to lexicographic: '/a/:id' < '/a/:id/b/:name'
|
|
1259
|
+
expect(longer.isMoreSpecificThan(shorter)).toBe(false)
|
|
1260
|
+
expect(shorter.isMoreSpecificThan(longer)).toBe(true)
|
|
1261
|
+
})
|
|
1262
|
+
|
|
1263
|
+
it('isMoreSpecificThan: static at earlier position wins', () => {
|
|
1264
|
+
const route1 = Route0.create('/a/static/:param')
|
|
1265
|
+
const route2 = Route0.create('/a/:param/static')
|
|
1266
|
+
|
|
1267
|
+
// Both have 2 static segments and same length
|
|
1268
|
+
// route1 has static at position 1, route2 has param at position 1
|
|
1269
|
+
expect(route1.isMoreSpecificThan(route2)).toBe(true)
|
|
1270
|
+
expect(route2.isMoreSpecificThan(route1)).toBe(false)
|
|
1271
|
+
})
|
|
1272
|
+
|
|
1273
|
+
it('isMoreSpecificThan: lexicographic when completely equal', () => {
|
|
1274
|
+
const route1 = Route0.create('/aaa/:id')
|
|
1275
|
+
const route2 = Route0.create('/bbb/:id')
|
|
1276
|
+
|
|
1277
|
+
// Same specificity, lexicographic comparison
|
|
1278
|
+
expect(route1.isMoreSpecificThan(route2)).toBe(true)
|
|
1279
|
+
expect(route2.isMoreSpecificThan(route1)).toBe(false)
|
|
1280
|
+
})
|
|
1281
|
+
|
|
1282
|
+
it('isMoreSpecificThan: identical routes', () => {
|
|
1283
|
+
const route1 = Route0.create('/a/:id')
|
|
1284
|
+
const route2 = Route0.create('/a/:id')
|
|
1285
|
+
|
|
1286
|
+
// Identical routes, lexicographic comparison returns false for equal strings
|
|
1287
|
+
expect(route1.isMoreSpecificThan(route2)).toBe(false)
|
|
1288
|
+
expect(route2.isMoreSpecificThan(route1)).toBe(false)
|
|
1289
|
+
})
|
|
1290
|
+
|
|
1291
|
+
it('isMoreSpecificThan: root vs other routes', () => {
|
|
1292
|
+
const root = Route0.create('/')
|
|
1293
|
+
const other = Route0.create('/a')
|
|
1294
|
+
const param = Route0.create('/:id')
|
|
1295
|
+
|
|
1296
|
+
// /a (1 static) vs / (1 static) - both static, lexicographic order
|
|
1297
|
+
expect(other.isMoreSpecificThan(root)).toBe(false) // '/' < '/a' lexicographically
|
|
1298
|
+
expect(root.isMoreSpecificThan(other)).toBe(true)
|
|
1299
|
+
|
|
1300
|
+
// /a (1 static) vs /:id (0 static) - static beats param
|
|
1301
|
+
expect(other.isMoreSpecificThan(param)).toBe(true)
|
|
1302
|
+
expect(param.isMoreSpecificThan(other)).toBe(false)
|
|
1303
|
+
|
|
1304
|
+
// /:id (0 static) vs / (1 static) - static beats param
|
|
1305
|
+
expect(param.isMoreSpecificThan(root)).toBe(false)
|
|
1306
|
+
expect(root.isMoreSpecificThan(param)).toBe(true)
|
|
1307
|
+
})
|
|
1308
|
+
|
|
1309
|
+
it('isConflict: checks if routes overlap', () => {
|
|
1310
|
+
const routeA = Route0.create('/a/:x')
|
|
1311
|
+
const routeB = Route0.create('/a/b')
|
|
1312
|
+
const routeC = Route0.create('/a/:c')
|
|
1313
|
+
const routeD = Route0.create('/a/d')
|
|
1314
|
+
const routeE = Route0.create('/a/b/c')
|
|
1315
|
+
|
|
1316
|
+
// Same depth, can match
|
|
1317
|
+
expect(routeA.isConflict(routeB)).toBe(true)
|
|
1318
|
+
expect(routeA.isConflict(routeC)).toBe(true)
|
|
1319
|
+
expect(routeA.isConflict(routeD)).toBe(true)
|
|
1320
|
+
expect(routeB.isConflict(routeC)).toBe(true)
|
|
1321
|
+
|
|
1322
|
+
// Different depth, no conflict
|
|
1323
|
+
expect(routeA.isConflict(routeE)).toBe(false)
|
|
1324
|
+
expect(routeB.isConflict(routeE)).toBe(false)
|
|
1325
|
+
})
|
|
1326
|
+
|
|
1327
|
+
it('isConflict: non-overlapping static routes', () => {
|
|
1328
|
+
const route1 = Route0.create('/users')
|
|
1329
|
+
const route2 = Route0.create('/posts')
|
|
1330
|
+
|
|
1331
|
+
// Same depth but different static segments
|
|
1332
|
+
expect(route1.isConflict(route2)).toBe(false)
|
|
1333
|
+
})
|
|
1334
|
+
})
|
|
1335
|
+
|
|
1336
|
+
describe('regex', () => {
|
|
1337
|
+
it('getRegexString: simple route', () => {
|
|
1338
|
+
const route = Route0.create('/')
|
|
1339
|
+
const regex = route.getRegexString()
|
|
1340
|
+
expect(regex).toBe('^/?$')
|
|
1341
|
+
expect(new RegExp(regex).test('/')).toBe(true)
|
|
1342
|
+
expect(new RegExp(regex).test('/other')).toBe(false)
|
|
1343
|
+
})
|
|
1344
|
+
|
|
1345
|
+
it('getRegexString: static route', () => {
|
|
1346
|
+
const route = Route0.create('/users')
|
|
1347
|
+
const regex = route.getRegexString()
|
|
1348
|
+
expect(regex).toBe('^/users/?$')
|
|
1349
|
+
expect(new RegExp(regex).test('/users')).toBe(true)
|
|
1350
|
+
expect(new RegExp(regex).test('/users/123')).toBe(false)
|
|
1351
|
+
})
|
|
1352
|
+
|
|
1353
|
+
it('getRegexString: route with single param', () => {
|
|
1354
|
+
const route = Route0.create('/users/:id')
|
|
1355
|
+
const regex = route.getRegexString()
|
|
1356
|
+
expect(regex).toBe('^/users/([^/]+)/?$')
|
|
1357
|
+
expect(new RegExp(regex).test('/users/123')).toBe(true)
|
|
1358
|
+
expect(new RegExp(regex).test('/users/abc')).toBe(true)
|
|
1359
|
+
expect(new RegExp(regex).test('/users/123/posts')).toBe(false)
|
|
1360
|
+
expect(new RegExp(regex).test('/users')).toBe(false)
|
|
1361
|
+
})
|
|
1362
|
+
|
|
1363
|
+
it('getRegexString: route with multiple params', () => {
|
|
1364
|
+
const route = Route0.create('/users/:userId/posts/:postId')
|
|
1365
|
+
const regex = route.getRegexString()
|
|
1366
|
+
expect(regex).toBe('^/users/([^/]+)/posts/([^/]+)/?$')
|
|
1367
|
+
expect(new RegExp(regex).test('/users/123/posts/456')).toBe(true)
|
|
1368
|
+
expect(new RegExp(regex).test('/users/123/posts')).toBe(false)
|
|
1369
|
+
})
|
|
1370
|
+
|
|
1371
|
+
it('getRegexString: route with special regex chars', () => {
|
|
1372
|
+
const route = Route0.create('/api/v1.0')
|
|
1373
|
+
const regex = route.getRegexString()
|
|
1374
|
+
// The dot should be escaped
|
|
1375
|
+
expect(regex).toBe('^/api/v1\\.0/?$')
|
|
1376
|
+
expect(new RegExp(regex).test('/api/v1.0')).toBe(true)
|
|
1377
|
+
expect(new RegExp(regex).test('/api/v100')).toBe(false)
|
|
1378
|
+
})
|
|
1379
|
+
|
|
1380
|
+
it('getRegexString: handles trailing slash', () => {
|
|
1381
|
+
const route = Route0.create('/users/')
|
|
1382
|
+
const regex = route.getRegexString()
|
|
1383
|
+
// Trailing slash should be removed from pattern, but optional slash added in regex
|
|
1384
|
+
expect(regex).toBe('^/users/?$')
|
|
1385
|
+
})
|
|
1386
|
+
|
|
1387
|
+
it('getRegexString: root with trailing slash', () => {
|
|
1388
|
+
const route = Route0.create('/')
|
|
1389
|
+
const regex = route.getRegexString()
|
|
1390
|
+
// Root returns pattern for empty string with optional slash
|
|
1391
|
+
expect(regex).toBe('^/?$')
|
|
1392
|
+
})
|
|
1393
|
+
|
|
1394
|
+
it('getRegex: simple route', () => {
|
|
1395
|
+
const route = Route0.create('/users')
|
|
1396
|
+
const regex = route.getRegex()
|
|
1397
|
+
expect(regex.test('/users')).toBe(true)
|
|
1398
|
+
expect(regex.test('/users/123')).toBe(false)
|
|
1399
|
+
expect(regex.test('/other')).toBe(false)
|
|
1400
|
+
})
|
|
1401
|
+
|
|
1402
|
+
it('getRegex: route with params', () => {
|
|
1403
|
+
const route = Route0.create('/users/:id')
|
|
1404
|
+
const regex = route.getRegex()
|
|
1405
|
+
expect(regex.test('/users/123')).toBe(true)
|
|
1406
|
+
expect(regex.test('/users/abc')).toBe(true)
|
|
1407
|
+
expect(regex.test('/users/123/posts')).toBe(false)
|
|
1408
|
+
})
|
|
1409
|
+
|
|
1410
|
+
it('static getRegexString: multiple routes', () => {
|
|
1411
|
+
const routes = [Route0.create('/users'), Route0.create('/posts/:id'), Route0.create('/')]
|
|
1412
|
+
const regex = Route0.getRegexStringGroup(routes)
|
|
1413
|
+
expect(regex).toBe('(^/users/?$|^/posts/([^/]+)/?$|^/?$)')
|
|
1414
|
+
})
|
|
1415
|
+
|
|
1416
|
+
it('static getRegexGroup: multiple routes', () => {
|
|
1417
|
+
const routes = [Route0.create('/users'), Route0.create('/posts/:id'), Route0.create('/')]
|
|
1418
|
+
const regex = Route0.getRegexGroup(routes)
|
|
1419
|
+
expect(regex.test('/users')).toBe(true)
|
|
1420
|
+
expect(regex.test('/posts/123')).toBe(true)
|
|
1421
|
+
expect(regex.test('/')).toBe(true)
|
|
1422
|
+
expect(regex.test('/other')).toBe(false)
|
|
1423
|
+
})
|
|
1424
|
+
|
|
1425
|
+
it('static getRegexGroup: matches in order', () => {
|
|
1426
|
+
const routes = [Route0.create('/users/special'), Route0.create('/users/:id')]
|
|
1427
|
+
const regex = Route0.getRegexGroup(routes)
|
|
1428
|
+
const match = '/users/special'.match(regex)
|
|
1429
|
+
expect(match).toBeTruthy()
|
|
1430
|
+
// Both could match, but first one should win
|
|
1431
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1432
|
+
expect(match![0]).toBe('/users/special')
|
|
1433
|
+
})
|
|
1434
|
+
|
|
1435
|
+
it('getRegexString works with getLocation', () => {
|
|
1436
|
+
const route = Route0.create('/users/:id/posts/:postId')
|
|
1437
|
+
const loc = route.getLocation('/users/123/posts/456')
|
|
1438
|
+
expect(loc.exact).toBe(true)
|
|
1439
|
+
expect(loc.params).toMatchObject({ id: '123', postId: '456' })
|
|
1440
|
+
})
|
|
1441
|
+
|
|
1442
|
+
it('regex matches what getLocation uses', () => {
|
|
1443
|
+
const route = Route0.create('/api/:version/users/:id')
|
|
1444
|
+
const testPath = '/api/v1/users/42'
|
|
1445
|
+
|
|
1446
|
+
// Test using getLocation
|
|
1447
|
+
const loc = route.getLocation(testPath)
|
|
1448
|
+
expect(loc.exact).toBe(true)
|
|
1449
|
+
|
|
1450
|
+
// Test using getRegex
|
|
1451
|
+
const regex = route.getRegex()
|
|
1452
|
+
expect(regex.test(testPath)).toBe(true)
|
|
1453
|
+
})
|
|
1454
|
+
|
|
1455
|
+
it('static getRegexGroup: complex routing scenario', () => {
|
|
1456
|
+
const api = Route0.create('/api/v1')
|
|
1457
|
+
const routes = [
|
|
1458
|
+
Route0.create('/'),
|
|
1459
|
+
api,
|
|
1460
|
+
api.extend('/users'),
|
|
1461
|
+
api.extend('/users/:id'),
|
|
1462
|
+
api.extend('/posts/:postId'),
|
|
1463
|
+
Route0.create('/:slug'),
|
|
1464
|
+
]
|
|
1465
|
+
|
|
1466
|
+
const regex = Route0.getRegexGroup(routes)
|
|
1467
|
+
|
|
1468
|
+
expect(regex.test('/')).toBe(true)
|
|
1469
|
+
expect(regex.test('/api/v1')).toBe(true)
|
|
1470
|
+
expect(regex.test('/api/v1/users')).toBe(true)
|
|
1471
|
+
expect(regex.test('/api/v1/users/123')).toBe(true)
|
|
1472
|
+
expect(regex.test('/api/v1/posts/456')).toBe(true)
|
|
1473
|
+
expect(regex.test('/about')).toBe(true) // matches /:slug
|
|
1474
|
+
expect(regex.test('/api/v1/users/123/extra')).toBe(false)
|
|
1475
|
+
})
|
|
1476
|
+
|
|
1477
|
+
it('getRegex: handles trailing slash correctly', () => {
|
|
1478
|
+
const route = Route0.create('/users')
|
|
1479
|
+
const regex = route.getRegex()
|
|
1480
|
+
expect(regex.test('/users')).toBe(true)
|
|
1481
|
+
expect(regex.test('/users/')).toBe(true) // trailing slash should match
|
|
1482
|
+
expect(regex.test('/users//')).toBe(false) // double slash should not match
|
|
1483
|
+
expect(regex.test('/users/abc')).toBe(false) // additional segment should not match
|
|
1484
|
+
})
|
|
1485
|
+
|
|
1486
|
+
it('getRegex: route with params and trailing slash', () => {
|
|
1487
|
+
const route = Route0.create('/users/:id')
|
|
1488
|
+
const regex = route.getRegex()
|
|
1489
|
+
expect(regex.test('/users/123')).toBe(true)
|
|
1490
|
+
expect(regex.test('/users/123/')).toBe(true) // trailing slash should match
|
|
1491
|
+
expect(regex.test('/users/123/abc')).toBe(false) // additional segment should not match
|
|
1492
|
+
expect(regex.test('/users/')).toBe(false) // missing param
|
|
1493
|
+
})
|
|
1494
|
+
|
|
1495
|
+
it('getRegex: root route edge cases', () => {
|
|
1496
|
+
const route = Route0.create('/')
|
|
1497
|
+
const regex = route.getRegex()
|
|
1498
|
+
expect(regex.test('/')).toBe(true)
|
|
1499
|
+
expect(regex.test('')).toBe(true) // empty string should match root
|
|
1500
|
+
expect(regex.test('//')).toBe(false) // double slash should not match
|
|
1501
|
+
expect(regex.test('/users')).toBe(false) // non-root should not match
|
|
1502
|
+
})
|
|
1503
|
+
|
|
1504
|
+
it('getRegexString: handles multiple special regex characters', () => {
|
|
1505
|
+
const route1 = Route0.create('/api/v1.0')
|
|
1506
|
+
const route2 = Route0.create('/path(with)parens')
|
|
1507
|
+
const route3 = Route0.create('/path[with]brackets')
|
|
1508
|
+
const route4 = Route0.create('/path*with*asterisks')
|
|
1509
|
+
const route5 = Route0.create('/path+with+pluses')
|
|
1510
|
+
const route6 = Route0.create('/path?with?question')
|
|
1511
|
+
const route7 = Route0.create('/path^with^caret')
|
|
1512
|
+
const route8 = Route0.create('/path$with$dollar')
|
|
1513
|
+
|
|
1514
|
+
expect(route1.getRegexString()).toBe('^/api/v1\\.0/?$')
|
|
1515
|
+
expect(route2.getRegexString()).toBe('^/path\\(with\\)parens/?$')
|
|
1516
|
+
expect(route3.getRegexString()).toBe('^/path\\[with\\]brackets/?$')
|
|
1517
|
+
expect(route4.getRegexString()).toBe('^/path\\*with\\*asterisks/?$')
|
|
1518
|
+
expect(route5.getRegexString()).toBe('^/path\\+with\\+pluses/?$')
|
|
1519
|
+
expect(route6.getRegexString()).toBe('^/path\\?with\\?question/?$')
|
|
1520
|
+
expect(route7.getRegexString()).toBe('^/path\\^with\\^caret/?$')
|
|
1521
|
+
expect(route8.getRegexString()).toBe('^/path\\$with\\$dollar/?$')
|
|
1522
|
+
})
|
|
1523
|
+
|
|
1524
|
+
it('getRegex: works with escaped special characters', () => {
|
|
1525
|
+
const route = Route0.create('/api/v1.0/users')
|
|
1526
|
+
const regex = route.getRegex()
|
|
1527
|
+
expect(regex.test('/api/v1.0/users')).toBe(true)
|
|
1528
|
+
expect(regex.test('/api/v1.0/users/')).toBe(true)
|
|
1529
|
+
expect(regex.test('/api/v100/users')).toBe(false) // dot must be literal
|
|
1530
|
+
expect(regex.test('/api/v1.0/users/extra')).toBe(false)
|
|
1531
|
+
})
|
|
1532
|
+
|
|
1533
|
+
it('static getRegexGroup: handles routes with overlapping patterns', () => {
|
|
1534
|
+
const routes = [
|
|
1535
|
+
Route0.create('/users/special'),
|
|
1536
|
+
Route0.create('/users/:id'),
|
|
1537
|
+
Route0.create('/users/:id/edit'),
|
|
1538
|
+
Route0.create('/users'),
|
|
1539
|
+
]
|
|
1540
|
+
const regex = Route0.getRegexGroup(routes)
|
|
1541
|
+
|
|
1542
|
+
// Should match in order - first specific route wins
|
|
1543
|
+
expect(regex.test('/users/special')).toBe(true)
|
|
1544
|
+
expect(regex.test('/users/123')).toBe(true)
|
|
1545
|
+
expect(regex.test('/users/123/edit')).toBe(true)
|
|
1546
|
+
expect(regex.test('/users')).toBe(true)
|
|
1547
|
+
expect(regex.test('/users/special/extra')).toBe(false)
|
|
1548
|
+
expect(regex.test('/users/123/extra')).toBe(false)
|
|
1549
|
+
})
|
|
1550
|
+
|
|
1551
|
+
it('static getRegexGroup: prevents partial matches', () => {
|
|
1552
|
+
const routes = [Route0.create('/api'), Route0.create('/api/v1'), Route0.create('/api/v1/users')]
|
|
1553
|
+
const regex = Route0.getRegexGroup(routes)
|
|
1554
|
+
|
|
1555
|
+
expect(regex.test('/api')).toBe(true)
|
|
1556
|
+
expect(regex.test('/api/')).toBe(true)
|
|
1557
|
+
expect(regex.test('/api/v1')).toBe(true)
|
|
1558
|
+
expect(regex.test('/api/v1/')).toBe(true)
|
|
1559
|
+
expect(regex.test('/api/v1/users')).toBe(true)
|
|
1560
|
+
expect(regex.test('/api/v1/users/')).toBe(true)
|
|
1561
|
+
expect(regex.test('/api/v2')).toBe(false)
|
|
1562
|
+
expect(regex.test('/api/v1/users/123')).toBe(false) // should not match /api/v1/users
|
|
1563
|
+
})
|
|
1564
|
+
|
|
1565
|
+
it('static getRegexGroup: handles root with other routes', () => {
|
|
1566
|
+
const routes = [Route0.create('/'), Route0.create('/home'), Route0.create('/about')]
|
|
1567
|
+
const regex = Route0.getRegexGroup(routes)
|
|
1568
|
+
|
|
1569
|
+
expect(regex.test('/')).toBe(true)
|
|
1570
|
+
expect(regex.test('')).toBe(true)
|
|
1571
|
+
expect(regex.test('/home')).toBe(true)
|
|
1572
|
+
expect(regex.test('/about')).toBe(true)
|
|
1573
|
+
expect(regex.test('/other')).toBe(false)
|
|
1574
|
+
})
|
|
1575
|
+
|
|
1576
|
+
it('static getRegexGroup: multiple routes with params', () => {
|
|
1577
|
+
const routes = [
|
|
1578
|
+
Route0.create('/posts/:id'),
|
|
1579
|
+
Route0.create('/users/:id'),
|
|
1580
|
+
Route0.create('/categories/:category/posts/:id'),
|
|
1581
|
+
]
|
|
1582
|
+
const regex = Route0.getRegexGroup(routes)
|
|
1583
|
+
|
|
1584
|
+
expect(regex.test('/posts/123')).toBe(true)
|
|
1585
|
+
expect(regex.test('/posts/123/')).toBe(true)
|
|
1586
|
+
expect(regex.test('/users/456')).toBe(true)
|
|
1587
|
+
expect(regex.test('/categories/tech/posts/789')).toBe(true)
|
|
1588
|
+
expect(regex.test('/posts/123/comments')).toBe(false)
|
|
1589
|
+
expect(regex.test('/users/456/posts')).toBe(false)
|
|
1590
|
+
})
|
|
1591
|
+
|
|
1592
|
+
it('getRegex: prevents matching beyond route definition', () => {
|
|
1593
|
+
const route = Route0.create('/users/:id/posts/:postId')
|
|
1594
|
+
const regex = route.getRegex()
|
|
1595
|
+
|
|
1596
|
+
expect(regex.test('/users/1/posts/2')).toBe(true)
|
|
1597
|
+
expect(regex.test('/users/1/posts/2/')).toBe(true)
|
|
1598
|
+
expect(regex.test('/users/1/posts/2/comments')).toBe(false)
|
|
1599
|
+
expect(regex.test('/users/1/posts/2/comments/3')).toBe(false)
|
|
1600
|
+
expect(regex.test('/users/1/posts')).toBe(false)
|
|
1601
|
+
expect(regex.test('/users/1')).toBe(false)
|
|
1602
|
+
})
|
|
1603
|
+
|
|
1604
|
+
it('static getRegexGroup: routes should not match partial segments', () => {
|
|
1605
|
+
const routes = [Route0.create('/admin'), Route0.create('/admin/users'), Route0.create('/admin/settings')]
|
|
1606
|
+
const regex = Route0.getRegexGroup(routes)
|
|
1607
|
+
|
|
1608
|
+
expect(regex.test('/admin')).toBe(true)
|
|
1609
|
+
expect(regex.test('/admin/users')).toBe(true)
|
|
1610
|
+
expect(regex.test('/admin/settings')).toBe(true)
|
|
1611
|
+
expect(regex.test('/admin/users/123')).toBe(false) // /admin/users should not match longer paths
|
|
1612
|
+
expect(regex.test('/admins')).toBe(false) // should not match partial word
|
|
1613
|
+
expect(regex.test('/admin-extra')).toBe(false)
|
|
1614
|
+
})
|
|
1615
|
+
|
|
1616
|
+
it('getRegex: handles consecutive slashes correctly', () => {
|
|
1617
|
+
const route = Route0.create('/users')
|
|
1618
|
+
const regex = route.getRegex()
|
|
1619
|
+
|
|
1620
|
+
expect(regex.test('/users')).toBe(true)
|
|
1621
|
+
expect(regex.test('/users/')).toBe(true)
|
|
1622
|
+
expect(regex.test('//users')).toBe(false) // double leading slash
|
|
1623
|
+
expect(regex.test('/users//')).toBe(false) // double trailing slash
|
|
1624
|
+
expect(regex.test('///users')).toBe(false) // triple slash
|
|
1625
|
+
})
|
|
1626
|
+
|
|
1627
|
+
it('static getRegexGroup: handles very similar route patterns', () => {
|
|
1628
|
+
const routes = [
|
|
1629
|
+
Route0.create('/a/b'),
|
|
1630
|
+
Route0.create('/a/:param'),
|
|
1631
|
+
Route0.create('/a/b/c'),
|
|
1632
|
+
Route0.create('/a/b/:param'),
|
|
1633
|
+
]
|
|
1634
|
+
const regex = Route0.getRegexGroup(routes)
|
|
1635
|
+
|
|
1636
|
+
expect(regex.test('/a/b')).toBe(true)
|
|
1637
|
+
expect(regex.test('/a/xyz')).toBe(true) // matches /a/:param
|
|
1638
|
+
expect(regex.test('/a/b/c')).toBe(true)
|
|
1639
|
+
expect(regex.test('/a/b/xyz')).toBe(true) // matches /a/b/:param
|
|
1640
|
+
expect(regex.test('/a/b/c/d')).toBe(false)
|
|
1641
|
+
})
|
|
1642
|
+
|
|
1643
|
+
it('getRegex: handles params with special characters', () => {
|
|
1644
|
+
const route = Route0.create('/users/:id')
|
|
1645
|
+
const regex = route.getRegex()
|
|
1646
|
+
|
|
1647
|
+
// Params should match URL-encoded characters
|
|
1648
|
+
expect(regex.test('/users/123')).toBe(true)
|
|
1649
|
+
expect(regex.test('/users/user-123')).toBe(true)
|
|
1650
|
+
expect(regex.test('/users/user_123')).toBe(true)
|
|
1651
|
+
expect(regex.test('/users/user.123')).toBe(true)
|
|
1652
|
+
expect(regex.test('/users/user%20123')).toBe(true) // URL encoded space
|
|
1653
|
+
expect(regex.test('/users/user%2F123')).toBe(true) // URL encoded slash (but this is a param value)
|
|
1654
|
+
})
|
|
1655
|
+
|
|
1656
|
+
it('static getRegexGroup: ensures exact match boundaries', () => {
|
|
1657
|
+
const routes = [
|
|
1658
|
+
Route0.create('/test'),
|
|
1659
|
+
Route0.create('/testing'),
|
|
1660
|
+
Route0.create('/testing/:id'),
|
|
1661
|
+
Route0.create('/testing/:id/xxx'),
|
|
1662
|
+
]
|
|
1663
|
+
const regex = Route0.getRegexGroup(routes)
|
|
1664
|
+
|
|
1665
|
+
expect(regex.test('/test')).toBe(true)
|
|
1666
|
+
expect(regex.test('/test/')).toBe(true)
|
|
1667
|
+
expect(regex.test('/testing')).toBe(true)
|
|
1668
|
+
expect(regex.test('/testing/')).toBe(true)
|
|
1669
|
+
expect(regex.test('/testing/123')).toBe(true)
|
|
1670
|
+
expect(regex.test('/testing/123/xxx')).toBe(true)
|
|
1671
|
+
expect(regex.test('/test/ing')).toBe(false) // should not partially match
|
|
1672
|
+
expect(regex.test('/tested')).toBe(false) // should not match longer word
|
|
1673
|
+
})
|
|
1674
|
+
|
|
1675
|
+
it('getRegexString: handles empty segments', () => {
|
|
1676
|
+
const route = Route0.create('/users//posts')
|
|
1677
|
+
const regex = route.getRegexString()
|
|
1678
|
+
// Double slashes should be normalized to single slash
|
|
1679
|
+
expect(regex).toContain('/users')
|
|
1680
|
+
expect(regex).toContain('/posts')
|
|
1681
|
+
})
|
|
1682
|
+
|
|
1683
|
+
it('static getRegexGroup: root route should not interfere with other routes', () => {
|
|
1684
|
+
const routes = [
|
|
1685
|
+
Route0.create('/'),
|
|
1686
|
+
Route0.create('/root'),
|
|
1687
|
+
Route0.create('/root/:id'),
|
|
1688
|
+
Route0.create('/root/:id/xxx'),
|
|
1689
|
+
]
|
|
1690
|
+
const regex = Route0.getRegexGroup(routes)
|
|
1691
|
+
|
|
1692
|
+
expect(regex.test('/')).toBe(true)
|
|
1693
|
+
expect(regex.test('')).toBe(true)
|
|
1694
|
+
expect(regex.test('/root')).toBe(true)
|
|
1695
|
+
expect(regex.test('/root/')).toBe(true)
|
|
1696
|
+
expect(regex.test('/rootx')).toBe(false)
|
|
1697
|
+
expect(regex.test('/root/123')).toBe(true)
|
|
1698
|
+
expect(regex.test('/root/123/xxx')).toBe(true)
|
|
1699
|
+
expect(regex.test('/root/123/yyy')).toBe(false)
|
|
1700
|
+
expect(regex.test('/rooting')).toBe(false)
|
|
1701
|
+
})
|
|
1702
|
+
})
|
|
1703
|
+
|
|
1704
|
+
describe('ordering', () => {
|
|
1705
|
+
it('_makeOrdering: orders routes by specificity', () => {
|
|
1706
|
+
const routes = {
|
|
1707
|
+
root: '/',
|
|
1708
|
+
userDetail: '/users/:id',
|
|
1709
|
+
users: '/users',
|
|
1710
|
+
userPosts: '/users/:id/posts',
|
|
1711
|
+
catchAll: '/:slug',
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
const { pathsOrdering: ordering } = Routes._.makeOrdering(routes)
|
|
1715
|
+
|
|
1716
|
+
// Expected order:
|
|
1717
|
+
// Depth 1: / then /users (static) then /:slug (param)
|
|
1718
|
+
// Depth 2: /users/:id
|
|
1719
|
+
// Depth 3: /users/:id/posts
|
|
1720
|
+
|
|
1721
|
+
expect(ordering).toEqual(['/', '/users', '/:slug', '/users/:id', '/users/:id/posts'])
|
|
1722
|
+
})
|
|
1723
|
+
|
|
1724
|
+
it('_makeOrdering: handles routes with same specificity', () => {
|
|
1725
|
+
const routes = {
|
|
1726
|
+
about: '/about',
|
|
1727
|
+
contact: '/contact',
|
|
1728
|
+
home: '/home',
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
const { pathsOrdering: ordering } = Routes._.makeOrdering(routes)
|
|
1732
|
+
|
|
1733
|
+
// All have same depth and don't conflict
|
|
1734
|
+
// Ordered alphabetically
|
|
1735
|
+
expect(ordering).toEqual(['/about', '/contact', '/home'])
|
|
1736
|
+
})
|
|
1737
|
+
|
|
1738
|
+
it('_makeOrdering: complex nested structure', () => {
|
|
1739
|
+
const api = Route0.create('/api/v1')
|
|
1740
|
+
const routes = {
|
|
1741
|
+
root: '/',
|
|
1742
|
+
api,
|
|
1743
|
+
usersStatic: '/api/v1/users/all',
|
|
1744
|
+
users: api.extend('/users'),
|
|
1745
|
+
userDetail: api.extend('/users/:id'),
|
|
1746
|
+
userPosts: api.extend('/users/:id/posts'),
|
|
1747
|
+
adminUser: '/api/v1/admin/:id',
|
|
1748
|
+
catchAll: '/:slug',
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
const { pathsOrdering: ordering } = Routes._.makeOrdering(routes)
|
|
1752
|
+
|
|
1753
|
+
// Expected order:
|
|
1754
|
+
// Depth 1: / (static), /:slug (param)
|
|
1755
|
+
// Depth 2: /api/v1
|
|
1756
|
+
// Depth 3: /api/v1/users (all static)
|
|
1757
|
+
// Depth 4: /api/v1/admin/:id (has param), /api/v1/users/all (all static), /api/v1/users/:id (has param)
|
|
1758
|
+
// Depth 5: /api/v1/users/:id/posts
|
|
1759
|
+
|
|
1760
|
+
expect(ordering).toEqual([
|
|
1761
|
+
'/',
|
|
1762
|
+
'/:slug',
|
|
1763
|
+
'/api/v1',
|
|
1764
|
+
'/api/v1/users',
|
|
1765
|
+
'/api/v1/admin/:id',
|
|
1766
|
+
'/api/v1/users/all',
|
|
1767
|
+
'/api/v1/users/:id',
|
|
1768
|
+
'/api/v1/users/:id/posts',
|
|
1769
|
+
])
|
|
1770
|
+
})
|
|
1771
|
+
|
|
1772
|
+
it('Routes instance has ordering property', () => {
|
|
1773
|
+
const routes = Routes.create({
|
|
1774
|
+
home: '/',
|
|
1775
|
+
users: '/users',
|
|
1776
|
+
userDetail: '/users/:id',
|
|
1777
|
+
})
|
|
1778
|
+
|
|
1779
|
+
expect(routes._.pathsOrdering).toBeDefined()
|
|
1780
|
+
expect(Array.isArray(routes._.pathsOrdering)).toBe(true)
|
|
1781
|
+
// Depth 1: /, /users (alphabetically)
|
|
1782
|
+
// Depth 2: /users/:id
|
|
1783
|
+
expect(routes._.pathsOrdering).toEqual(['/', '/users', '/users/:id'])
|
|
1784
|
+
})
|
|
1785
|
+
|
|
1786
|
+
it('ordering is preserved after override', () => {
|
|
1787
|
+
const routes = Routes.create({
|
|
1788
|
+
home: '/',
|
|
1789
|
+
users: '/users',
|
|
1790
|
+
userDetail: '/users/:id',
|
|
1791
|
+
})
|
|
1792
|
+
|
|
1793
|
+
const originalOrdering = routes._.pathsOrdering
|
|
1794
|
+
|
|
1795
|
+
const overridden = routes._.override({ baseUrl: 'https://example.com' })
|
|
1796
|
+
|
|
1797
|
+
expect(overridden._.pathsOrdering).toEqual(originalOrdering)
|
|
1798
|
+
expect(overridden._.pathsOrdering).toEqual(['/', '/users', '/users/:id'])
|
|
1799
|
+
})
|
|
1800
|
+
|
|
1801
|
+
it('_makeOrdering: handles single route', () => {
|
|
1802
|
+
const routes = {
|
|
1803
|
+
home: '/',
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
const { pathsOrdering: ordering } = Routes._.makeOrdering(routes)
|
|
1807
|
+
expect(ordering).toEqual(['/'])
|
|
1808
|
+
})
|
|
1809
|
+
|
|
1810
|
+
it('_makeOrdering: handles empty object', () => {
|
|
1811
|
+
const routes = {}
|
|
1812
|
+
|
|
1813
|
+
const { pathsOrdering: ordering } = Routes._.makeOrdering(routes)
|
|
1814
|
+
expect(ordering).toEqual([])
|
|
1815
|
+
})
|
|
1816
|
+
})
|
|
1817
|
+
|
|
1818
|
+
describe('relations: isSame, isParent, isChildren', () => {
|
|
1819
|
+
it('isSame: same static path', () => {
|
|
1820
|
+
const a = Route0.create('/a')
|
|
1821
|
+
const b = Route0.create('/a')
|
|
1822
|
+
expect(a.isSame(b)).toBe(true)
|
|
1823
|
+
})
|
|
1824
|
+
|
|
1825
|
+
it('isSame: ignores param names but respects structure', () => {
|
|
1826
|
+
const r1 = Route0.create('/users/:id')
|
|
1827
|
+
const r2 = Route0.create('/users/:userId')
|
|
1828
|
+
const r3 = Route0.create('/users')
|
|
1829
|
+
const r4 = Route0.create('/users/:id/posts')
|
|
1830
|
+
expect((r1 as any).isSame(r2 as any)).toBe(true)
|
|
1831
|
+
expect((r1 as any).isSame(r3 as any)).toBe(false)
|
|
1832
|
+
expect((r1 as any).isSame(r4 as any)).toBe(false)
|
|
1833
|
+
})
|
|
1834
|
+
|
|
1835
|
+
it('isParent: true when left is ancestor of right', () => {
|
|
1836
|
+
expect(Route0.create('/').isParent(Route0.create('/path/child'))).toBe(true)
|
|
1837
|
+
expect(Route0.create('/path').isParent(Route0.create('/path/child'))).toBe(true)
|
|
1838
|
+
expect(Route0.create('/users/:id').isParent('/users/:id/posts')).toBe(true)
|
|
1839
|
+
expect(Route0.create('/').isParent(Route0.create('/users/:id/posts'))).toBe(true)
|
|
1840
|
+
expect(Route0.create('/').isParent(Route0.create('/users/:id'))).toBe(true)
|
|
1841
|
+
})
|
|
1842
|
+
|
|
1843
|
+
it('isParent: false for reverse, equal, or unrelated', () => {
|
|
1844
|
+
expect(Route0.create('/path/child').isParent(Route0.create('/path'))).toBe(false)
|
|
1845
|
+
expect(Route0.create('/path').isParent(Route0.create('/path'))).toBe(false)
|
|
1846
|
+
expect(Route0.create('/a').isParent(Route0.create('/b'))).toBe(false)
|
|
1847
|
+
})
|
|
1848
|
+
|
|
1849
|
+
it('isChildren: true when left is descendant of right', () => {
|
|
1850
|
+
expect(Route0.create('/path/child').isChildren(Route0.create('/path'))).toBe(true)
|
|
1851
|
+
expect(Route0.create('/users/:id/posts').isChildren(Route0.create('/users/:id'))).toBe(true)
|
|
1852
|
+
expect(Route0.create('/users/:id/posts').isChildren(Route0.create('/'))).toBe(true)
|
|
1853
|
+
})
|
|
1854
|
+
|
|
1855
|
+
it('isChildren: false for reverse, equal, or unrelated', () => {
|
|
1856
|
+
expect(Route0.create('/path').isChildren(Route0.create('/path/child'))).toBe(false)
|
|
1857
|
+
expect(Route0.create('/path').isChildren(Route0.create('/path'))).toBe(false)
|
|
1858
|
+
expect(Route0.create('/a').isChildren(Route0.create('/b'))).toBe(false)
|
|
1859
|
+
})
|
|
1860
|
+
|
|
1861
|
+
it('static isSame: works with strings and undefined', () => {
|
|
1862
|
+
expect(Route0.isSame('/a/:id', Route0.create('/a/:name'))).toBe(true)
|
|
1863
|
+
expect(Route0.isSame('/a', '/a')).toBe(true)
|
|
1864
|
+
expect(Route0.isSame('/a', '/b')).toBe(false)
|
|
1865
|
+
expect(Route0.isSame(undefined, undefined)).toBe(true)
|
|
1866
|
+
expect(Route0.isSame(undefined, '/a')).toBe(false)
|
|
1867
|
+
expect(Route0.isSame('/a', undefined)).toBe(false)
|
|
205
1868
|
})
|
|
206
1869
|
})
|