@furystack/shades 13.2.1 → 14.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +150 -0
- package/esm/components/define-nested-routes.d.ts +27 -0
- package/esm/components/define-nested-routes.d.ts.map +1 -0
- package/esm/components/define-nested-routes.js +26 -0
- package/esm/components/define-nested-routes.js.map +1 -0
- package/esm/components/define-nested-routes.spec.d.ts +2 -0
- package/esm/components/define-nested-routes.spec.d.ts.map +1 -0
- package/esm/components/define-nested-routes.spec.js +81 -0
- package/esm/components/define-nested-routes.spec.js.map +1 -0
- package/esm/components/index.d.ts +3 -3
- package/esm/components/index.d.ts.map +1 -1
- package/esm/components/index.js +3 -3
- package/esm/components/index.js.map +1 -1
- package/esm/components/nested-hooks.d.ts +62 -0
- package/esm/components/nested-hooks.d.ts.map +1 -0
- package/esm/components/nested-hooks.js +73 -0
- package/esm/components/nested-hooks.js.map +1 -0
- package/esm/components/nested-hooks.spec.d.ts +2 -0
- package/esm/components/nested-hooks.spec.d.ts.map +1 -0
- package/esm/components/nested-hooks.spec.js +211 -0
- package/esm/components/nested-hooks.spec.js.map +1 -0
- package/esm/components/nested-navigate.d.ts +52 -21
- package/esm/components/nested-navigate.d.ts.map +1 -1
- package/esm/components/nested-navigate.js +32 -17
- package/esm/components/nested-navigate.js.map +1 -1
- package/esm/components/nested-navigate.spec.js +102 -12
- package/esm/components/nested-navigate.spec.js.map +1 -1
- package/esm/components/nested-replace.d.ts +36 -0
- package/esm/components/nested-replace.d.ts.map +1 -0
- package/esm/components/nested-replace.js +38 -0
- package/esm/components/nested-replace.js.map +1 -0
- package/esm/components/nested-replace.spec.d.ts +2 -0
- package/esm/components/nested-replace.spec.d.ts.map +1 -0
- package/esm/components/nested-replace.spec.js +59 -0
- package/esm/components/nested-replace.spec.js.map +1 -0
- package/esm/components/nested-route-link.d.ts +36 -26
- package/esm/components/nested-route-link.d.ts.map +1 -1
- package/esm/components/nested-route-link.js +17 -19
- package/esm/components/nested-route-link.js.map +1 -1
- package/esm/components/nested-route-link.spec.js +61 -11
- package/esm/components/nested-route-link.spec.js.map +1 -1
- package/esm/components/nested-route-types.d.ts +79 -2
- package/esm/components/nested-route-types.d.ts.map +1 -1
- package/esm/components/nested-router.d.ts +77 -7
- package/esm/components/nested-router.d.ts.map +1 -1
- package/esm/components/nested-router.js +137 -11
- package/esm/components/nested-router.js.map +1 -1
- package/esm/components/nested-router.spec.js +265 -34
- package/esm/components/nested-router.spec.js.map +1 -1
- package/esm/services/location-service.spec.js +33 -0
- package/esm/services/location-service.spec.js.map +1 -1
- package/esm/services/route-match-service.spec.js +4 -0
- package/esm/services/route-match-service.spec.js.map +1 -1
- package/esm/services/route-meta-utils.d.ts +28 -6
- package/esm/services/route-meta-utils.d.ts.map +1 -1
- package/esm/services/route-meta-utils.js +18 -4
- package/esm/services/route-meta-utils.js.map +1 -1
- package/esm/services/route-meta-utils.spec.js +57 -2
- package/esm/services/route-meta-utils.spec.js.map +1 -1
- package/esm/vnode.d.ts +1 -1
- package/esm/vnode.js +1 -1
- package/package.json +9 -9
- package/src/components/define-nested-routes.spec.ts +91 -0
- package/src/components/define-nested-routes.ts +27 -0
- package/src/components/index.ts +3 -3
- package/src/components/nested-hooks.spec.ts +254 -0
- package/src/components/nested-hooks.ts +109 -0
- package/src/components/nested-navigate.spec.ts +128 -25
- package/src/components/nested-navigate.ts +75 -26
- package/src/components/nested-replace.spec.ts +88 -0
- package/src/components/nested-replace.ts +46 -0
- package/src/components/nested-route-link.spec.tsx +84 -16
- package/src/components/nested-route-link.tsx +53 -28
- package/src/components/nested-route-types.ts +82 -2
- package/src/components/nested-router.spec.tsx +316 -33
- package/src/components/nested-router.tsx +182 -22
- package/src/services/location-service.spec.ts +39 -0
- package/src/services/route-match-service.spec.ts +4 -0
- package/src/services/route-meta-utils.spec.ts +75 -7
- package/src/services/route-meta-utils.ts +44 -15
- package/src/vnode.ts +1 -1
- package/esm/components/link-to-route.d.ts +0 -10
- package/esm/components/link-to-route.d.ts.map +0 -1
- package/esm/components/link-to-route.js +0 -16
- package/esm/components/link-to-route.js.map +0 -1
- package/esm/components/link-to-route.spec.d.ts +0 -2
- package/esm/components/link-to-route.spec.d.ts.map +0 -1
- package/esm/components/link-to-route.spec.js +0 -30
- package/esm/components/link-to-route.spec.js.map +0 -1
- package/esm/components/route-link.d.ts +0 -10
- package/esm/components/route-link.d.ts.map +0 -1
- package/esm/components/route-link.js +0 -25
- package/esm/components/route-link.js.map +0 -1
- package/esm/components/route-link.spec.d.ts +0 -2
- package/esm/components/route-link.spec.d.ts.map +0 -1
- package/esm/components/route-link.spec.js +0 -34
- package/esm/components/route-link.spec.js.map +0 -1
- package/esm/components/router.d.ts +0 -44
- package/esm/components/router.d.ts.map +0 -1
- package/esm/components/router.js +0 -66
- package/esm/components/router.js.map +0 -1
- package/esm/components/router.spec.d.ts +0 -2
- package/esm/components/router.spec.d.ts.map +0 -1
- package/esm/components/router.spec.js +0 -161
- package/esm/components/router.spec.js.map +0 -1
- package/src/components/link-to-route.spec.tsx +0 -45
- package/src/components/link-to-route.tsx +0 -26
- package/src/components/route-link.spec.tsx +0 -44
- package/src/components/route-link.tsx +0 -29
- package/src/components/router.spec.tsx +0 -217
- package/src/components/router.tsx +0 -103
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { Injector } from '@furystack/inject'
|
|
2
|
+
import { serializeValue } from '@furystack/rest'
|
|
2
3
|
import { sleepAsync, usingAsync } from '@furystack/utils'
|
|
3
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
4
5
|
import { initializeShadeRoot } from '../initialize.js'
|
|
5
6
|
import { createComponent } from '../shade-component.js'
|
|
6
7
|
import { flushUpdates } from '../shade.js'
|
|
7
8
|
import { RouteMatchService } from '../services/route-match-service.js'
|
|
9
|
+
import { LocationService } from '../services/location-service.js'
|
|
8
10
|
import {
|
|
9
11
|
buildMatchChain,
|
|
12
|
+
enrichMatchChain,
|
|
10
13
|
findDivergenceIndex,
|
|
14
|
+
hasQueryOrHashChanged,
|
|
11
15
|
NestedRouter,
|
|
12
16
|
renderMatchChain,
|
|
13
17
|
resolveViewTransition,
|
|
@@ -138,6 +142,8 @@ describe('findDivergenceIndex', () => {
|
|
|
138
142
|
const makeEntry = (id: number, params: Record<string, string> = {}): MatchChainEntry => ({
|
|
139
143
|
route: { component: () => <div>{id}</div> },
|
|
140
144
|
match: { path: '/', params },
|
|
145
|
+
query: null,
|
|
146
|
+
hash: undefined,
|
|
141
147
|
})
|
|
142
148
|
|
|
143
149
|
it('should return 0 for completely different chains', () => {
|
|
@@ -162,10 +168,120 @@ describe('findDivergenceIndex', () => {
|
|
|
162
168
|
|
|
163
169
|
it('should detect divergence from changed params', () => {
|
|
164
170
|
const route: NestedRoute = { component: () => <div /> }
|
|
165
|
-
const oldChain: MatchChainEntry[] = [
|
|
166
|
-
|
|
171
|
+
const oldChain: MatchChainEntry[] = [
|
|
172
|
+
{ route, match: { path: '/', params: { id: '1' } }, query: null, hash: undefined },
|
|
173
|
+
]
|
|
174
|
+
const newChain: MatchChainEntry[] = [
|
|
175
|
+
{ route, match: { path: '/', params: { id: '2' } }, query: null, hash: undefined },
|
|
176
|
+
]
|
|
167
177
|
expect(findDivergenceIndex(oldChain, newChain)).toBe(0)
|
|
168
178
|
})
|
|
179
|
+
|
|
180
|
+
it('should ignore query and hash differences when deciding divergence', () => {
|
|
181
|
+
const route: NestedRoute = { component: () => <div /> }
|
|
182
|
+
const oldChain: MatchChainEntry[] = [{ route, match: { path: '/', params: {} }, query: { page: 1 }, hash: 'a' }]
|
|
183
|
+
const newChain: MatchChainEntry[] = [{ route, match: { path: '/', params: {} }, query: { page: 2 }, hash: 'b' }]
|
|
184
|
+
expect(findDivergenceIndex(oldChain, newChain)).toBe(1)
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
describe('enrichMatchChain', () => {
|
|
189
|
+
it('should return the input chain unchanged when no entry declares query or hash', () => {
|
|
190
|
+
const route: NestedRoute = { component: () => <div /> }
|
|
191
|
+
const chain: MatchChainEntry[] = [{ route, match: { path: '/', params: {} }, query: null, hash: undefined }]
|
|
192
|
+
const result = enrichMatchChain(chain, {}, '')
|
|
193
|
+
expect(result).toBe(chain)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('should populate query from the route validator', () => {
|
|
197
|
+
const route: NestedRoute<unknown, { page: number }> = {
|
|
198
|
+
component: () => <div />,
|
|
199
|
+
query: (raw): { page: number } | null => (typeof raw.page === 'number' ? { page: raw.page } : null),
|
|
200
|
+
}
|
|
201
|
+
const chain: MatchChainEntry[] = [{ route, match: { path: '/', params: {} }, query: null, hash: undefined }]
|
|
202
|
+
const result = enrichMatchChain(chain, { page: 3 }, '')
|
|
203
|
+
expect(result).not.toBe(chain)
|
|
204
|
+
expect(result[0].query).toEqual({ page: 3 })
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('should set query to null when the validator rejects the current search', () => {
|
|
208
|
+
const route: NestedRoute<unknown, { page: number }> = {
|
|
209
|
+
component: () => <div />,
|
|
210
|
+
query: (raw): { page: number } | null => (typeof raw.page === 'number' ? { page: raw.page } : null),
|
|
211
|
+
}
|
|
212
|
+
const chain: MatchChainEntry[] = [{ route, match: { path: '/', params: {} }, query: null, hash: undefined }]
|
|
213
|
+
const result = enrichMatchChain(chain, { page: 'nope' }, '')
|
|
214
|
+
expect(result[0].query).toBeNull()
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('should populate hash only when the current hash is listed in the declared tuple', () => {
|
|
218
|
+
const route: NestedRoute<unknown, any, readonly ['a', 'b']> = {
|
|
219
|
+
component: () => <div />,
|
|
220
|
+
hash: ['a', 'b'] as const,
|
|
221
|
+
}
|
|
222
|
+
const chain: MatchChainEntry[] = [{ route, match: { path: '/', params: {} }, query: null, hash: undefined }]
|
|
223
|
+
expect(enrichMatchChain(chain, {}, 'a')[0].hash).toBe('a')
|
|
224
|
+
expect(enrichMatchChain(chain, {}, 'unknown')[0].hash).toBeUndefined()
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('should leave entries without a declared schema as null/undefined', () => {
|
|
228
|
+
const bareRoute: NestedRoute = { component: () => <div /> }
|
|
229
|
+
const declaringRoute: NestedRoute<unknown, any, readonly ['a']> = {
|
|
230
|
+
component: () => <div />,
|
|
231
|
+
hash: ['a'] as const,
|
|
232
|
+
}
|
|
233
|
+
const chain: MatchChainEntry[] = [
|
|
234
|
+
{ route: bareRoute, match: { path: '/', params: {} }, query: null, hash: undefined },
|
|
235
|
+
{ route: declaringRoute, match: { path: '/', params: {} }, query: null, hash: undefined },
|
|
236
|
+
]
|
|
237
|
+
const result = enrichMatchChain(chain, {}, 'a')
|
|
238
|
+
expect(result[0].query).toBeNull()
|
|
239
|
+
expect(result[0].hash).toBeUndefined()
|
|
240
|
+
expect(result[1].hash).toBe('a')
|
|
241
|
+
})
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
describe('hasQueryOrHashChanged', () => {
|
|
245
|
+
const makeEntry = (query: unknown, hash: string | undefined): MatchChainEntry => ({
|
|
246
|
+
route: { component: () => <div /> },
|
|
247
|
+
match: { path: '/', params: {} },
|
|
248
|
+
query,
|
|
249
|
+
hash,
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it('should return false when both chains are empty', () => {
|
|
253
|
+
expect(hasQueryOrHashChanged([], [])).toBe(false)
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('should return false for identical chains', () => {
|
|
257
|
+
const oldChain = [makeEntry({ page: 1 }, 'a')]
|
|
258
|
+
const newChain = [makeEntry({ page: 1 }, 'a')]
|
|
259
|
+
expect(hasQueryOrHashChanged(oldChain, newChain)).toBe(false)
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
it('should return true when the hash differs', () => {
|
|
263
|
+
expect(hasQueryOrHashChanged([makeEntry(null, 'a')], [makeEntry(null, 'b')])).toBe(true)
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it('should return true when the query differs', () => {
|
|
267
|
+
expect(hasQueryOrHashChanged([makeEntry({ page: 1 }, undefined)], [makeEntry({ page: 2 }, undefined)])).toBe(true)
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it('should treat equal query objects as unchanged regardless of key order', () => {
|
|
271
|
+
const oldChain = [makeEntry({ a: 1, b: 2 }, undefined)]
|
|
272
|
+
const newChain = [makeEntry({ b: 2, a: 1 }, undefined)]
|
|
273
|
+
expect(hasQueryOrHashChanged(oldChain, newChain)).toBe(false)
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
it('should detect query diffs on arrays', () => {
|
|
277
|
+
expect(hasQueryOrHashChanged([makeEntry([1, 2], undefined)], [makeEntry([1, 3], undefined)])).toBe(true)
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('should ignore chain entries beyond the shorter length', () => {
|
|
281
|
+
const oldChain = [makeEntry(null, 'a')]
|
|
282
|
+
const newChain = [makeEntry(null, 'a'), makeEntry({ x: 1 }, 'b')]
|
|
283
|
+
expect(hasQueryOrHashChanged(oldChain, newChain)).toBe(false)
|
|
284
|
+
})
|
|
169
285
|
})
|
|
170
286
|
|
|
171
287
|
describe('renderMatchChain', () => {
|
|
@@ -177,6 +293,8 @@ describe('renderMatchChain', () => {
|
|
|
177
293
|
{
|
|
178
294
|
route: { component: componentFn },
|
|
179
295
|
match: { path: '/leaf', params: {} },
|
|
296
|
+
query: null,
|
|
297
|
+
hash: undefined,
|
|
180
298
|
},
|
|
181
299
|
]
|
|
182
300
|
|
|
@@ -186,6 +304,8 @@ describe('renderMatchChain', () => {
|
|
|
186
304
|
expect(componentFn).toHaveBeenCalledWith({
|
|
187
305
|
currentUrl: '/leaf',
|
|
188
306
|
match: { path: '/leaf', params: {} },
|
|
307
|
+
query: null,
|
|
308
|
+
hash: undefined,
|
|
189
309
|
outlet: undefined,
|
|
190
310
|
})
|
|
191
311
|
expect(result.chainElements).toHaveLength(1)
|
|
@@ -208,10 +328,14 @@ describe('renderMatchChain', () => {
|
|
|
208
328
|
{
|
|
209
329
|
route: { component: parentComponent },
|
|
210
330
|
match: { path: '/parent', params: {} },
|
|
331
|
+
query: null,
|
|
332
|
+
hash: undefined,
|
|
211
333
|
},
|
|
212
334
|
{
|
|
213
335
|
route: { component: childComponent },
|
|
214
336
|
match: { path: '/child', params: {} },
|
|
337
|
+
query: null,
|
|
338
|
+
hash: undefined,
|
|
215
339
|
},
|
|
216
340
|
]
|
|
217
341
|
|
|
@@ -240,9 +364,9 @@ describe('renderMatchChain', () => {
|
|
|
240
364
|
const parent = makeComponent()
|
|
241
365
|
|
|
242
366
|
const chain: MatchChainEntry[] = [
|
|
243
|
-
{ route: { component: parent }, match: { path: '/a', params: {} } },
|
|
244
|
-
{ route: { component: child }, match: { path: '/b', params: {} } },
|
|
245
|
-
{ route: { component: grandchild }, match: { path: '/c', params: {} } },
|
|
367
|
+
{ route: { component: parent }, match: { path: '/a', params: {} }, query: null, hash: undefined },
|
|
368
|
+
{ route: { component: child }, match: { path: '/b', params: {} }, query: null, hash: undefined },
|
|
369
|
+
{ route: { component: grandchild }, match: { path: '/c', params: {} }, query: null, hash: undefined },
|
|
246
370
|
]
|
|
247
371
|
|
|
248
372
|
renderMatchChain(chain, '/a/b/c')
|
|
@@ -256,9 +380,9 @@ describe('renderMatchChain', () => {
|
|
|
256
380
|
const parentComponent = vi.fn(({ outlet }: { outlet?: JSX.Element }) => <main>parent{outlet}</main>)
|
|
257
381
|
|
|
258
382
|
const chain: MatchChainEntry[] = [
|
|
259
|
-
{ route: { component: parentComponent }, match: { path: '/a', params: {} } },
|
|
260
|
-
{ route: { component: childComponent }, match: { path: '/b', params: {} } },
|
|
261
|
-
{ route: { component: () => grandchildEl }, match: { path: '/c', params: {} } },
|
|
383
|
+
{ route: { component: parentComponent }, match: { path: '/a', params: {} }, query: null, hash: undefined },
|
|
384
|
+
{ route: { component: childComponent }, match: { path: '/b', params: {} }, query: null, hash: undefined },
|
|
385
|
+
{ route: { component: () => grandchildEl }, match: { path: '/c', params: {} }, query: null, hash: undefined },
|
|
262
386
|
]
|
|
263
387
|
|
|
264
388
|
const result = renderMatchChain(chain, '/a/b/c')
|
|
@@ -322,16 +446,16 @@ describe('NestedRouter lifecycle hooks', () => {
|
|
|
322
446
|
rootElement,
|
|
323
447
|
jsxElement: (
|
|
324
448
|
<div>
|
|
325
|
-
<NestedRouteLink id="child-a"
|
|
449
|
+
<NestedRouteLink id="child-a" path="/parent/child-a">
|
|
326
450
|
child-a
|
|
327
451
|
</NestedRouteLink>
|
|
328
|
-
<NestedRouteLink id="child-b"
|
|
452
|
+
<NestedRouteLink id="child-b" path="/parent/child-b">
|
|
329
453
|
child-b
|
|
330
454
|
</NestedRouteLink>
|
|
331
|
-
<NestedRouteLink id="other"
|
|
455
|
+
<NestedRouteLink id="other" path="/other">
|
|
332
456
|
other
|
|
333
457
|
</NestedRouteLink>
|
|
334
|
-
<NestedRouteLink id="nowhere"
|
|
458
|
+
<NestedRouteLink id="nowhere" path="/nowhere">
|
|
335
459
|
nowhere
|
|
336
460
|
</NestedRouteLink>
|
|
337
461
|
<NestedRouter
|
|
@@ -417,6 +541,163 @@ describe('NestedRouter lifecycle hooks', () => {
|
|
|
417
541
|
})
|
|
418
542
|
})
|
|
419
543
|
|
|
544
|
+
describe('NestedRouter query / hash re-render', () => {
|
|
545
|
+
beforeEach(() => {
|
|
546
|
+
document.body.innerHTML = '<div id="root"></div>'
|
|
547
|
+
})
|
|
548
|
+
afterEach(() => {
|
|
549
|
+
document.body.innerHTML = ''
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
it('should re-render the chain on a hash-only change without firing lifecycle hooks', async () => {
|
|
553
|
+
history.pushState(null, '', '/tabs')
|
|
554
|
+
|
|
555
|
+
const onVisit = vi.fn(async () => {})
|
|
556
|
+
const onLeave = vi.fn(async () => {})
|
|
557
|
+
const componentFn = vi.fn(({ hash }: { hash: string | undefined }) => <div id="content">hash={hash ?? 'none'}</div>)
|
|
558
|
+
|
|
559
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
560
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
561
|
+
|
|
562
|
+
initializeShadeRoot({
|
|
563
|
+
injector,
|
|
564
|
+
rootElement,
|
|
565
|
+
jsxElement: (
|
|
566
|
+
<NestedRouter
|
|
567
|
+
routes={{
|
|
568
|
+
'/tabs': {
|
|
569
|
+
component: componentFn,
|
|
570
|
+
onVisit,
|
|
571
|
+
onLeave,
|
|
572
|
+
hash: ['a', 'b'] as const,
|
|
573
|
+
},
|
|
574
|
+
}}
|
|
575
|
+
/>
|
|
576
|
+
),
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
await flushUpdates()
|
|
580
|
+
expect(document.getElementById('content')?.innerHTML).toBe('hash=none')
|
|
581
|
+
expect(onVisit).toBeCalledTimes(1)
|
|
582
|
+
expect(onLeave).not.toBeCalled()
|
|
583
|
+
|
|
584
|
+
const locationService = injector.getInstance(LocationService)
|
|
585
|
+
locationService.navigate('/tabs#a')
|
|
586
|
+
await flushUpdates()
|
|
587
|
+
await flushUpdates()
|
|
588
|
+
|
|
589
|
+
expect(document.getElementById('content')?.innerHTML).toBe('hash=a')
|
|
590
|
+
expect(onVisit).toBeCalledTimes(1)
|
|
591
|
+
expect(onLeave).not.toBeCalled()
|
|
592
|
+
|
|
593
|
+
locationService.navigate('/tabs#b')
|
|
594
|
+
await flushUpdates()
|
|
595
|
+
await flushUpdates()
|
|
596
|
+
|
|
597
|
+
expect(document.getElementById('content')?.innerHTML).toBe('hash=b')
|
|
598
|
+
expect(onVisit).toBeCalledTimes(1)
|
|
599
|
+
expect(onLeave).not.toBeCalled()
|
|
600
|
+
})
|
|
601
|
+
})
|
|
602
|
+
|
|
603
|
+
it('should re-render the chain on a query-only change without firing lifecycle hooks', async () => {
|
|
604
|
+
history.pushState(null, '', '/list')
|
|
605
|
+
|
|
606
|
+
const onVisit = vi.fn(async () => {})
|
|
607
|
+
const onLeave = vi.fn(async () => {})
|
|
608
|
+
const componentFn = vi.fn(({ query }: { query: { page: number } | null }) => (
|
|
609
|
+
<div id="content">page={query?.page ?? 'none'}</div>
|
|
610
|
+
))
|
|
611
|
+
|
|
612
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
613
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
614
|
+
|
|
615
|
+
initializeShadeRoot({
|
|
616
|
+
injector,
|
|
617
|
+
rootElement,
|
|
618
|
+
jsxElement: (
|
|
619
|
+
<NestedRouter
|
|
620
|
+
routes={{
|
|
621
|
+
'/list': {
|
|
622
|
+
component: componentFn,
|
|
623
|
+
onVisit,
|
|
624
|
+
onLeave,
|
|
625
|
+
query: (raw): { page: number } | null => (typeof raw.page === 'number' ? { page: raw.page } : null),
|
|
626
|
+
},
|
|
627
|
+
}}
|
|
628
|
+
/>
|
|
629
|
+
),
|
|
630
|
+
})
|
|
631
|
+
|
|
632
|
+
await flushUpdates()
|
|
633
|
+
expect(document.getElementById('content')?.innerHTML).toBe('page=none')
|
|
634
|
+
expect(onVisit).toBeCalledTimes(1)
|
|
635
|
+
|
|
636
|
+
const locationService = injector.getInstance(LocationService)
|
|
637
|
+
locationService.navigate(`/list?page=${serializeValue(2)}`)
|
|
638
|
+
await flushUpdates()
|
|
639
|
+
await flushUpdates()
|
|
640
|
+
|
|
641
|
+
expect(document.getElementById('content')?.innerHTML).toBe('page=2')
|
|
642
|
+
expect(onVisit).toBeCalledTimes(1)
|
|
643
|
+
expect(onLeave).not.toBeCalled()
|
|
644
|
+
|
|
645
|
+
locationService.navigate(`/list?page=${serializeValue(5)}`)
|
|
646
|
+
await flushUpdates()
|
|
647
|
+
await flushUpdates()
|
|
648
|
+
|
|
649
|
+
expect(document.getElementById('content')?.innerHTML).toBe('page=5')
|
|
650
|
+
expect(onVisit).toBeCalledTimes(1)
|
|
651
|
+
expect(onLeave).not.toBeCalled()
|
|
652
|
+
})
|
|
653
|
+
})
|
|
654
|
+
|
|
655
|
+
it('should coalesce path + query + hash bursts into a single updateUrl', async () => {
|
|
656
|
+
history.pushState(null, '', '/a')
|
|
657
|
+
|
|
658
|
+
const aComponent = vi.fn(() => <div id="content">a</div>)
|
|
659
|
+
const bComponent = vi.fn(({ query, hash }: { query: { page: number } | null; hash: string | undefined }) => (
|
|
660
|
+
<div id="content">
|
|
661
|
+
b-page={query?.page ?? 'none'}-hash={hash ?? 'none'}
|
|
662
|
+
</div>
|
|
663
|
+
))
|
|
664
|
+
|
|
665
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
666
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
667
|
+
|
|
668
|
+
initializeShadeRoot({
|
|
669
|
+
injector,
|
|
670
|
+
rootElement,
|
|
671
|
+
jsxElement: (
|
|
672
|
+
<NestedRouter
|
|
673
|
+
routes={{
|
|
674
|
+
'/a': { component: aComponent },
|
|
675
|
+
'/b': {
|
|
676
|
+
component: bComponent,
|
|
677
|
+
hash: ['x'] as const,
|
|
678
|
+
query: (raw): { page: number } | null => (typeof raw.page === 'number' ? { page: raw.page } : null),
|
|
679
|
+
},
|
|
680
|
+
}}
|
|
681
|
+
/>
|
|
682
|
+
),
|
|
683
|
+
})
|
|
684
|
+
|
|
685
|
+
await flushUpdates()
|
|
686
|
+
expect(document.getElementById('content')?.innerHTML).toBe('a')
|
|
687
|
+
bComponent.mockClear()
|
|
688
|
+
|
|
689
|
+
injector.getInstance(LocationService).navigate(`/b?page=${serializeValue(7)}#x`)
|
|
690
|
+
await flushUpdates()
|
|
691
|
+
await flushUpdates()
|
|
692
|
+
|
|
693
|
+
expect(document.getElementById('content')?.innerHTML).toBe('b-page=7-hash=x')
|
|
694
|
+
// A single logical navigation should render the /b component exactly once,
|
|
695
|
+
// not three times (once per observable subscription).
|
|
696
|
+
expect(bComponent).toHaveBeenCalledTimes(1)
|
|
697
|
+
})
|
|
698
|
+
})
|
|
699
|
+
})
|
|
700
|
+
|
|
420
701
|
describe('NestedRouter latest-wins on rapid navigation', () => {
|
|
421
702
|
beforeEach(() => {
|
|
422
703
|
document.body.innerHTML = '<div id="root"></div>'
|
|
@@ -458,13 +739,13 @@ describe('NestedRouter latest-wins on rapid navigation', () => {
|
|
|
458
739
|
rootElement,
|
|
459
740
|
jsxElement: (
|
|
460
741
|
<div>
|
|
461
|
-
<NestedRouteLink id="go-a"
|
|
742
|
+
<NestedRouteLink id="go-a" path="/route-a">
|
|
462
743
|
a
|
|
463
744
|
</NestedRouteLink>
|
|
464
|
-
<NestedRouteLink id="go-b"
|
|
745
|
+
<NestedRouteLink id="go-b" path="/route-b">
|
|
465
746
|
b
|
|
466
747
|
</NestedRouteLink>
|
|
467
|
-
<NestedRouteLink id="go-c"
|
|
748
|
+
<NestedRouteLink id="go-c" path="/route-c">
|
|
468
749
|
c
|
|
469
750
|
</NestedRouteLink>
|
|
470
751
|
<NestedRouter
|
|
@@ -540,10 +821,10 @@ describe('NestedRouter lifecycle element scope', () => {
|
|
|
540
821
|
rootElement,
|
|
541
822
|
jsxElement: (
|
|
542
823
|
<div>
|
|
543
|
-
<NestedRouteLink id="child-a"
|
|
824
|
+
<NestedRouteLink id="child-a" path="/parent/child-a">
|
|
544
825
|
child-a
|
|
545
826
|
</NestedRouteLink>
|
|
546
|
-
<NestedRouteLink id="child-b"
|
|
827
|
+
<NestedRouteLink id="child-b" path="/parent/child-b">
|
|
547
828
|
child-b
|
|
548
829
|
</NestedRouteLink>
|
|
549
830
|
<NestedRouter
|
|
@@ -633,13 +914,13 @@ describe('NestedRouter flat routes', () => {
|
|
|
633
914
|
rootElement,
|
|
634
915
|
jsxElement: (
|
|
635
916
|
<div>
|
|
636
|
-
<NestedRouteLink id="home"
|
|
917
|
+
<NestedRouteLink id="home" path="/">
|
|
637
918
|
home
|
|
638
919
|
</NestedRouteLink>
|
|
639
|
-
<NestedRouteLink id="about"
|
|
920
|
+
<NestedRouteLink id="about" path="/about">
|
|
640
921
|
about
|
|
641
922
|
</NestedRouteLink>
|
|
642
|
-
<NestedRouteLink id="contact"
|
|
923
|
+
<NestedRouteLink id="contact" path="/contact">
|
|
643
924
|
contact
|
|
644
925
|
</NestedRouteLink>
|
|
645
926
|
<NestedRouter
|
|
@@ -792,13 +1073,13 @@ describe('NestedRouter route param changes', () => {
|
|
|
792
1073
|
rootElement,
|
|
793
1074
|
jsxElement: (
|
|
794
1075
|
<div>
|
|
795
|
-
<NestedRouteLink id="user-1"
|
|
1076
|
+
<NestedRouteLink id="user-1" path="/users/1">
|
|
796
1077
|
User 1
|
|
797
1078
|
</NestedRouteLink>
|
|
798
|
-
<NestedRouteLink id="user-2"
|
|
1079
|
+
<NestedRouteLink id="user-2" path="/users/2">
|
|
799
1080
|
User 2
|
|
800
1081
|
</NestedRouteLink>
|
|
801
|
-
<NestedRouteLink id="user-3"
|
|
1082
|
+
<NestedRouteLink id="user-3" path="/users/3">
|
|
802
1083
|
User 3
|
|
803
1084
|
</NestedRouteLink>
|
|
804
1085
|
<NestedRouter
|
|
@@ -868,10 +1149,10 @@ describe('NestedRouter route param changes', () => {
|
|
|
868
1149
|
rootElement,
|
|
869
1150
|
jsxElement: (
|
|
870
1151
|
<div>
|
|
871
|
-
<NestedRouteLink id="alpha-dash"
|
|
1152
|
+
<NestedRouteLink id="alpha-dash" path="/org/alpha/dashboard">
|
|
872
1153
|
Alpha Dashboard
|
|
873
1154
|
</NestedRouteLink>
|
|
874
|
-
<NestedRouteLink id="beta-dash"
|
|
1155
|
+
<NestedRouteLink id="beta-dash" path="/org/beta/dashboard">
|
|
875
1156
|
Beta Dashboard
|
|
876
1157
|
</NestedRouteLink>
|
|
877
1158
|
<NestedRouter
|
|
@@ -957,13 +1238,13 @@ describe('NestedRouter + RouteMatchService integration', () => {
|
|
|
957
1238
|
rootElement,
|
|
958
1239
|
jsxElement: (
|
|
959
1240
|
<div>
|
|
960
|
-
<NestedRouteLink id="child-a"
|
|
1241
|
+
<NestedRouteLink id="child-a" path="/parent/child-a">
|
|
961
1242
|
child-a
|
|
962
1243
|
</NestedRouteLink>
|
|
963
|
-
<NestedRouteLink id="child-b"
|
|
1244
|
+
<NestedRouteLink id="child-b" path="/parent/child-b">
|
|
964
1245
|
child-b
|
|
965
1246
|
</NestedRouteLink>
|
|
966
|
-
<NestedRouteLink id="nowhere"
|
|
1247
|
+
<NestedRouteLink id="nowhere" path="/nowhere">
|
|
967
1248
|
nowhere
|
|
968
1249
|
</NestedRouteLink>
|
|
969
1250
|
<NestedRouter routes={{ '/parent': parentRoute }} notFound={<div id="content">not found</div>} />
|
|
@@ -1030,6 +1311,8 @@ describe('resolveViewTransition', () => {
|
|
|
1030
1311
|
const makeEntry = (viewTransition?: boolean | { types?: string[] }): MatchChainEntry => ({
|
|
1031
1312
|
route: { component: () => <div />, viewTransition },
|
|
1032
1313
|
match: { path: '/', params: {} },
|
|
1314
|
+
query: null,
|
|
1315
|
+
hash: undefined,
|
|
1033
1316
|
})
|
|
1034
1317
|
|
|
1035
1318
|
it('should return false when router config is undefined and route has no override', () => {
|
|
@@ -1103,7 +1386,7 @@ describe('NestedRouter view transitions', () => {
|
|
|
1103
1386
|
rootElement,
|
|
1104
1387
|
jsxElement: (
|
|
1105
1388
|
<div>
|
|
1106
|
-
<NestedRouteLink id="go-about"
|
|
1389
|
+
<NestedRouteLink id="go-about" path="/about">
|
|
1107
1390
|
about
|
|
1108
1391
|
</NestedRouteLink>
|
|
1109
1392
|
<NestedRouter
|
|
@@ -1139,7 +1422,7 @@ describe('NestedRouter view transitions', () => {
|
|
|
1139
1422
|
rootElement,
|
|
1140
1423
|
jsxElement: (
|
|
1141
1424
|
<div>
|
|
1142
|
-
<NestedRouteLink id="go-about"
|
|
1425
|
+
<NestedRouteLink id="go-about" path="/about">
|
|
1143
1426
|
about
|
|
1144
1427
|
</NestedRouteLink>
|
|
1145
1428
|
<NestedRouter
|
|
@@ -1174,7 +1457,7 @@ describe('NestedRouter view transitions', () => {
|
|
|
1174
1457
|
rootElement,
|
|
1175
1458
|
jsxElement: (
|
|
1176
1459
|
<div>
|
|
1177
|
-
<NestedRouteLink id="go-about"
|
|
1460
|
+
<NestedRouteLink id="go-about" path="/about">
|
|
1178
1461
|
about
|
|
1179
1462
|
</NestedRouteLink>
|
|
1180
1463
|
<NestedRouter
|
|
@@ -1209,7 +1492,7 @@ describe('NestedRouter view transitions', () => {
|
|
|
1209
1492
|
rootElement,
|
|
1210
1493
|
jsxElement: (
|
|
1211
1494
|
<div>
|
|
1212
|
-
<NestedRouteLink id="go-about"
|
|
1495
|
+
<NestedRouteLink id="go-about" path="/about">
|
|
1213
1496
|
about
|
|
1214
1497
|
</NestedRouteLink>
|
|
1215
1498
|
<NestedRouter
|
|
@@ -1250,7 +1533,7 @@ describe('NestedRouter view transitions', () => {
|
|
|
1250
1533
|
rootElement,
|
|
1251
1534
|
jsxElement: (
|
|
1252
1535
|
<div>
|
|
1253
|
-
<NestedRouteLink id="go-about"
|
|
1536
|
+
<NestedRouteLink id="go-about" path="/about">
|
|
1254
1537
|
about
|
|
1255
1538
|
</NestedRouteLink>
|
|
1256
1539
|
<NestedRouter
|