@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,11 +1,13 @@
|
|
|
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';
|
|
8
|
-
import {
|
|
9
|
+
import { LocationService } from '../services/location-service.js';
|
|
10
|
+
import { buildMatchChain, enrichMatchChain, findDivergenceIndex, hasQueryOrHashChanged, NestedRouter, renderMatchChain, resolveViewTransition, } from './nested-router.js';
|
|
9
11
|
import { NestedRouteLink } from './nested-route-link.js';
|
|
10
12
|
describe('buildMatchChain', () => {
|
|
11
13
|
it('should match a simple leaf route', () => {
|
|
@@ -122,6 +124,8 @@ describe('findDivergenceIndex', () => {
|
|
|
122
124
|
const makeEntry = (id, params = {}) => ({
|
|
123
125
|
route: { component: () => createComponent("div", null, id) },
|
|
124
126
|
match: { path: '/', params },
|
|
127
|
+
query: null,
|
|
128
|
+
hash: undefined,
|
|
125
129
|
});
|
|
126
130
|
it('should return 0 for completely different chains', () => {
|
|
127
131
|
const oldChain = [makeEntry(1)];
|
|
@@ -142,10 +146,106 @@ describe('findDivergenceIndex', () => {
|
|
|
142
146
|
});
|
|
143
147
|
it('should detect divergence from changed params', () => {
|
|
144
148
|
const route = { component: () => createComponent("div", null) };
|
|
145
|
-
const oldChain = [
|
|
146
|
-
|
|
149
|
+
const oldChain = [
|
|
150
|
+
{ route, match: { path: '/', params: { id: '1' } }, query: null, hash: undefined },
|
|
151
|
+
];
|
|
152
|
+
const newChain = [
|
|
153
|
+
{ route, match: { path: '/', params: { id: '2' } }, query: null, hash: undefined },
|
|
154
|
+
];
|
|
147
155
|
expect(findDivergenceIndex(oldChain, newChain)).toBe(0);
|
|
148
156
|
});
|
|
157
|
+
it('should ignore query and hash differences when deciding divergence', () => {
|
|
158
|
+
const route = { component: () => createComponent("div", null) };
|
|
159
|
+
const oldChain = [{ route, match: { path: '/', params: {} }, query: { page: 1 }, hash: 'a' }];
|
|
160
|
+
const newChain = [{ route, match: { path: '/', params: {} }, query: { page: 2 }, hash: 'b' }];
|
|
161
|
+
expect(findDivergenceIndex(oldChain, newChain)).toBe(1);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
describe('enrichMatchChain', () => {
|
|
165
|
+
it('should return the input chain unchanged when no entry declares query or hash', () => {
|
|
166
|
+
const route = { component: () => createComponent("div", null) };
|
|
167
|
+
const chain = [{ route, match: { path: '/', params: {} }, query: null, hash: undefined }];
|
|
168
|
+
const result = enrichMatchChain(chain, {}, '');
|
|
169
|
+
expect(result).toBe(chain);
|
|
170
|
+
});
|
|
171
|
+
it('should populate query from the route validator', () => {
|
|
172
|
+
const route = {
|
|
173
|
+
component: () => createComponent("div", null),
|
|
174
|
+
query: (raw) => (typeof raw.page === 'number' ? { page: raw.page } : null),
|
|
175
|
+
};
|
|
176
|
+
const chain = [{ route, match: { path: '/', params: {} }, query: null, hash: undefined }];
|
|
177
|
+
const result = enrichMatchChain(chain, { page: 3 }, '');
|
|
178
|
+
expect(result).not.toBe(chain);
|
|
179
|
+
expect(result[0].query).toEqual({ page: 3 });
|
|
180
|
+
});
|
|
181
|
+
it('should set query to null when the validator rejects the current search', () => {
|
|
182
|
+
const route = {
|
|
183
|
+
component: () => createComponent("div", null),
|
|
184
|
+
query: (raw) => (typeof raw.page === 'number' ? { page: raw.page } : null),
|
|
185
|
+
};
|
|
186
|
+
const chain = [{ route, match: { path: '/', params: {} }, query: null, hash: undefined }];
|
|
187
|
+
const result = enrichMatchChain(chain, { page: 'nope' }, '');
|
|
188
|
+
expect(result[0].query).toBeNull();
|
|
189
|
+
});
|
|
190
|
+
it('should populate hash only when the current hash is listed in the declared tuple', () => {
|
|
191
|
+
const route = {
|
|
192
|
+
component: () => createComponent("div", null),
|
|
193
|
+
hash: ['a', 'b'],
|
|
194
|
+
};
|
|
195
|
+
const chain = [{ route, match: { path: '/', params: {} }, query: null, hash: undefined }];
|
|
196
|
+
expect(enrichMatchChain(chain, {}, 'a')[0].hash).toBe('a');
|
|
197
|
+
expect(enrichMatchChain(chain, {}, 'unknown')[0].hash).toBeUndefined();
|
|
198
|
+
});
|
|
199
|
+
it('should leave entries without a declared schema as null/undefined', () => {
|
|
200
|
+
const bareRoute = { component: () => createComponent("div", null) };
|
|
201
|
+
const declaringRoute = {
|
|
202
|
+
component: () => createComponent("div", null),
|
|
203
|
+
hash: ['a'],
|
|
204
|
+
};
|
|
205
|
+
const chain = [
|
|
206
|
+
{ route: bareRoute, match: { path: '/', params: {} }, query: null, hash: undefined },
|
|
207
|
+
{ route: declaringRoute, match: { path: '/', params: {} }, query: null, hash: undefined },
|
|
208
|
+
];
|
|
209
|
+
const result = enrichMatchChain(chain, {}, 'a');
|
|
210
|
+
expect(result[0].query).toBeNull();
|
|
211
|
+
expect(result[0].hash).toBeUndefined();
|
|
212
|
+
expect(result[1].hash).toBe('a');
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
describe('hasQueryOrHashChanged', () => {
|
|
216
|
+
const makeEntry = (query, hash) => ({
|
|
217
|
+
route: { component: () => createComponent("div", null) },
|
|
218
|
+
match: { path: '/', params: {} },
|
|
219
|
+
query,
|
|
220
|
+
hash,
|
|
221
|
+
});
|
|
222
|
+
it('should return false when both chains are empty', () => {
|
|
223
|
+
expect(hasQueryOrHashChanged([], [])).toBe(false);
|
|
224
|
+
});
|
|
225
|
+
it('should return false for identical chains', () => {
|
|
226
|
+
const oldChain = [makeEntry({ page: 1 }, 'a')];
|
|
227
|
+
const newChain = [makeEntry({ page: 1 }, 'a')];
|
|
228
|
+
expect(hasQueryOrHashChanged(oldChain, newChain)).toBe(false);
|
|
229
|
+
});
|
|
230
|
+
it('should return true when the hash differs', () => {
|
|
231
|
+
expect(hasQueryOrHashChanged([makeEntry(null, 'a')], [makeEntry(null, 'b')])).toBe(true);
|
|
232
|
+
});
|
|
233
|
+
it('should return true when the query differs', () => {
|
|
234
|
+
expect(hasQueryOrHashChanged([makeEntry({ page: 1 }, undefined)], [makeEntry({ page: 2 }, undefined)])).toBe(true);
|
|
235
|
+
});
|
|
236
|
+
it('should treat equal query objects as unchanged regardless of key order', () => {
|
|
237
|
+
const oldChain = [makeEntry({ a: 1, b: 2 }, undefined)];
|
|
238
|
+
const newChain = [makeEntry({ b: 2, a: 1 }, undefined)];
|
|
239
|
+
expect(hasQueryOrHashChanged(oldChain, newChain)).toBe(false);
|
|
240
|
+
});
|
|
241
|
+
it('should detect query diffs on arrays', () => {
|
|
242
|
+
expect(hasQueryOrHashChanged([makeEntry([1, 2], undefined)], [makeEntry([1, 3], undefined)])).toBe(true);
|
|
243
|
+
});
|
|
244
|
+
it('should ignore chain entries beyond the shorter length', () => {
|
|
245
|
+
const oldChain = [makeEntry(null, 'a')];
|
|
246
|
+
const newChain = [makeEntry(null, 'a'), makeEntry({ x: 1 }, 'b')];
|
|
247
|
+
expect(hasQueryOrHashChanged(oldChain, newChain)).toBe(false);
|
|
248
|
+
});
|
|
149
249
|
});
|
|
150
250
|
describe('renderMatchChain', () => {
|
|
151
251
|
it('should render a single leaf route with outlet undefined', () => {
|
|
@@ -156,6 +256,8 @@ describe('renderMatchChain', () => {
|
|
|
156
256
|
{
|
|
157
257
|
route: { component: componentFn },
|
|
158
258
|
match: { path: '/leaf', params: {} },
|
|
259
|
+
query: null,
|
|
260
|
+
hash: undefined,
|
|
159
261
|
},
|
|
160
262
|
];
|
|
161
263
|
const result = renderMatchChain(chain, '/leaf');
|
|
@@ -163,6 +265,8 @@ describe('renderMatchChain', () => {
|
|
|
163
265
|
expect(componentFn).toHaveBeenCalledWith({
|
|
164
266
|
currentUrl: '/leaf',
|
|
165
267
|
match: { path: '/leaf', params: {} },
|
|
268
|
+
query: null,
|
|
269
|
+
hash: undefined,
|
|
166
270
|
outlet: undefined,
|
|
167
271
|
});
|
|
168
272
|
expect(result.chainElements).toHaveLength(1);
|
|
@@ -186,10 +290,14 @@ describe('renderMatchChain', () => {
|
|
|
186
290
|
{
|
|
187
291
|
route: { component: parentComponent },
|
|
188
292
|
match: { path: '/parent', params: {} },
|
|
293
|
+
query: null,
|
|
294
|
+
hash: undefined,
|
|
189
295
|
},
|
|
190
296
|
{
|
|
191
297
|
route: { component: childComponent },
|
|
192
298
|
match: { path: '/child', params: {} },
|
|
299
|
+
query: null,
|
|
300
|
+
hash: undefined,
|
|
193
301
|
},
|
|
194
302
|
];
|
|
195
303
|
const result = renderMatchChain(chain, '/parent/child');
|
|
@@ -210,9 +318,9 @@ describe('renderMatchChain', () => {
|
|
|
210
318
|
const child = makeComponent();
|
|
211
319
|
const parent = makeComponent();
|
|
212
320
|
const chain = [
|
|
213
|
-
{ route: { component: parent }, match: { path: '/a', params: {} } },
|
|
214
|
-
{ route: { component: child }, match: { path: '/b', params: {} } },
|
|
215
|
-
{ route: { component: grandchild }, match: { path: '/c', params: {} } },
|
|
321
|
+
{ route: { component: parent }, match: { path: '/a', params: {} }, query: null, hash: undefined },
|
|
322
|
+
{ route: { component: child }, match: { path: '/b', params: {} }, query: null, hash: undefined },
|
|
323
|
+
{ route: { component: grandchild }, match: { path: '/c', params: {} }, query: null, hash: undefined },
|
|
216
324
|
];
|
|
217
325
|
renderMatchChain(chain, '/a/b/c');
|
|
218
326
|
expect(urls).toEqual(['/a/b/c', '/a/b/c', '/a/b/c']);
|
|
@@ -226,9 +334,9 @@ describe('renderMatchChain', () => {
|
|
|
226
334
|
"parent",
|
|
227
335
|
outlet));
|
|
228
336
|
const chain = [
|
|
229
|
-
{ route: { component: parentComponent }, match: { path: '/a', params: {} } },
|
|
230
|
-
{ route: { component: childComponent }, match: { path: '/b', params: {} } },
|
|
231
|
-
{ route: { component: () => grandchildEl }, match: { path: '/c', params: {} } },
|
|
337
|
+
{ route: { component: parentComponent }, match: { path: '/a', params: {} }, query: null, hash: undefined },
|
|
338
|
+
{ route: { component: childComponent }, match: { path: '/b', params: {} }, query: null, hash: undefined },
|
|
339
|
+
{ route: { component: () => grandchildEl }, match: { path: '/c', params: {} }, query: null, hash: undefined },
|
|
232
340
|
];
|
|
233
341
|
const result = renderMatchChain(chain, '/a/b/c');
|
|
234
342
|
expect(result.chainElements).toHaveLength(3);
|
|
@@ -283,10 +391,10 @@ describe('NestedRouter lifecycle hooks', () => {
|
|
|
283
391
|
injector,
|
|
284
392
|
rootElement,
|
|
285
393
|
jsxElement: (createComponent("div", null,
|
|
286
|
-
createComponent(NestedRouteLink, { id: "child-a",
|
|
287
|
-
createComponent(NestedRouteLink, { id: "child-b",
|
|
288
|
-
createComponent(NestedRouteLink, { id: "other",
|
|
289
|
-
createComponent(NestedRouteLink, { id: "nowhere",
|
|
394
|
+
createComponent(NestedRouteLink, { id: "child-a", path: "/parent/child-a" }, "child-a"),
|
|
395
|
+
createComponent(NestedRouteLink, { id: "child-b", path: "/parent/child-b" }, "child-b"),
|
|
396
|
+
createComponent(NestedRouteLink, { id: "other", path: "/other" }, "other"),
|
|
397
|
+
createComponent(NestedRouteLink, { id: "nowhere", path: "/nowhere" }, "nowhere"),
|
|
290
398
|
createComponent(NestedRouter, { routes: {
|
|
291
399
|
'/parent': {
|
|
292
400
|
component: ({ outlet }) => createComponent("div", { id: "wrapper" }, outlet ?? createComponent("div", { id: "content" }, "parent-index")),
|
|
@@ -358,6 +466,127 @@ describe('NestedRouter lifecycle hooks', () => {
|
|
|
358
466
|
});
|
|
359
467
|
});
|
|
360
468
|
});
|
|
469
|
+
describe('NestedRouter query / hash re-render', () => {
|
|
470
|
+
beforeEach(() => {
|
|
471
|
+
document.body.innerHTML = '<div id="root"></div>';
|
|
472
|
+
});
|
|
473
|
+
afterEach(() => {
|
|
474
|
+
document.body.innerHTML = '';
|
|
475
|
+
});
|
|
476
|
+
it('should re-render the chain on a hash-only change without firing lifecycle hooks', async () => {
|
|
477
|
+
history.pushState(null, '', '/tabs');
|
|
478
|
+
const onVisit = vi.fn(async () => { });
|
|
479
|
+
const onLeave = vi.fn(async () => { });
|
|
480
|
+
const componentFn = vi.fn(({ hash }) => createComponent("div", { id: "content" },
|
|
481
|
+
"hash=",
|
|
482
|
+
hash ?? 'none'));
|
|
483
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
484
|
+
const rootElement = document.getElementById('root');
|
|
485
|
+
initializeShadeRoot({
|
|
486
|
+
injector,
|
|
487
|
+
rootElement,
|
|
488
|
+
jsxElement: (createComponent(NestedRouter, { routes: {
|
|
489
|
+
'/tabs': {
|
|
490
|
+
component: componentFn,
|
|
491
|
+
onVisit,
|
|
492
|
+
onLeave,
|
|
493
|
+
hash: ['a', 'b'],
|
|
494
|
+
},
|
|
495
|
+
} })),
|
|
496
|
+
});
|
|
497
|
+
await flushUpdates();
|
|
498
|
+
expect(document.getElementById('content')?.innerHTML).toBe('hash=none');
|
|
499
|
+
expect(onVisit).toBeCalledTimes(1);
|
|
500
|
+
expect(onLeave).not.toBeCalled();
|
|
501
|
+
const locationService = injector.getInstance(LocationService);
|
|
502
|
+
locationService.navigate('/tabs#a');
|
|
503
|
+
await flushUpdates();
|
|
504
|
+
await flushUpdates();
|
|
505
|
+
expect(document.getElementById('content')?.innerHTML).toBe('hash=a');
|
|
506
|
+
expect(onVisit).toBeCalledTimes(1);
|
|
507
|
+
expect(onLeave).not.toBeCalled();
|
|
508
|
+
locationService.navigate('/tabs#b');
|
|
509
|
+
await flushUpdates();
|
|
510
|
+
await flushUpdates();
|
|
511
|
+
expect(document.getElementById('content')?.innerHTML).toBe('hash=b');
|
|
512
|
+
expect(onVisit).toBeCalledTimes(1);
|
|
513
|
+
expect(onLeave).not.toBeCalled();
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
it('should re-render the chain on a query-only change without firing lifecycle hooks', async () => {
|
|
517
|
+
history.pushState(null, '', '/list');
|
|
518
|
+
const onVisit = vi.fn(async () => { });
|
|
519
|
+
const onLeave = vi.fn(async () => { });
|
|
520
|
+
const componentFn = vi.fn(({ query }) => (createComponent("div", { id: "content" },
|
|
521
|
+
"page=",
|
|
522
|
+
query?.page ?? 'none')));
|
|
523
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
524
|
+
const rootElement = document.getElementById('root');
|
|
525
|
+
initializeShadeRoot({
|
|
526
|
+
injector,
|
|
527
|
+
rootElement,
|
|
528
|
+
jsxElement: (createComponent(NestedRouter, { routes: {
|
|
529
|
+
'/list': {
|
|
530
|
+
component: componentFn,
|
|
531
|
+
onVisit,
|
|
532
|
+
onLeave,
|
|
533
|
+
query: (raw) => (typeof raw.page === 'number' ? { page: raw.page } : null),
|
|
534
|
+
},
|
|
535
|
+
} })),
|
|
536
|
+
});
|
|
537
|
+
await flushUpdates();
|
|
538
|
+
expect(document.getElementById('content')?.innerHTML).toBe('page=none');
|
|
539
|
+
expect(onVisit).toBeCalledTimes(1);
|
|
540
|
+
const locationService = injector.getInstance(LocationService);
|
|
541
|
+
locationService.navigate(`/list?page=${serializeValue(2)}`);
|
|
542
|
+
await flushUpdates();
|
|
543
|
+
await flushUpdates();
|
|
544
|
+
expect(document.getElementById('content')?.innerHTML).toBe('page=2');
|
|
545
|
+
expect(onVisit).toBeCalledTimes(1);
|
|
546
|
+
expect(onLeave).not.toBeCalled();
|
|
547
|
+
locationService.navigate(`/list?page=${serializeValue(5)}`);
|
|
548
|
+
await flushUpdates();
|
|
549
|
+
await flushUpdates();
|
|
550
|
+
expect(document.getElementById('content')?.innerHTML).toBe('page=5');
|
|
551
|
+
expect(onVisit).toBeCalledTimes(1);
|
|
552
|
+
expect(onLeave).not.toBeCalled();
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
it('should coalesce path + query + hash bursts into a single updateUrl', async () => {
|
|
556
|
+
history.pushState(null, '', '/a');
|
|
557
|
+
const aComponent = vi.fn(() => createComponent("div", { id: "content" }, "a"));
|
|
558
|
+
const bComponent = vi.fn(({ query, hash }) => (createComponent("div", { id: "content" },
|
|
559
|
+
"b-page=",
|
|
560
|
+
query?.page ?? 'none',
|
|
561
|
+
"-hash=",
|
|
562
|
+
hash ?? 'none')));
|
|
563
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
564
|
+
const rootElement = document.getElementById('root');
|
|
565
|
+
initializeShadeRoot({
|
|
566
|
+
injector,
|
|
567
|
+
rootElement,
|
|
568
|
+
jsxElement: (createComponent(NestedRouter, { routes: {
|
|
569
|
+
'/a': { component: aComponent },
|
|
570
|
+
'/b': {
|
|
571
|
+
component: bComponent,
|
|
572
|
+
hash: ['x'],
|
|
573
|
+
query: (raw) => (typeof raw.page === 'number' ? { page: raw.page } : null),
|
|
574
|
+
},
|
|
575
|
+
} })),
|
|
576
|
+
});
|
|
577
|
+
await flushUpdates();
|
|
578
|
+
expect(document.getElementById('content')?.innerHTML).toBe('a');
|
|
579
|
+
bComponent.mockClear();
|
|
580
|
+
injector.getInstance(LocationService).navigate(`/b?page=${serializeValue(7)}#x`);
|
|
581
|
+
await flushUpdates();
|
|
582
|
+
await flushUpdates();
|
|
583
|
+
expect(document.getElementById('content')?.innerHTML).toBe('b-page=7-hash=x');
|
|
584
|
+
// A single logical navigation should render the /b component exactly once,
|
|
585
|
+
// not three times (once per observable subscription).
|
|
586
|
+
expect(bComponent).toHaveBeenCalledTimes(1);
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
});
|
|
361
590
|
describe('NestedRouter latest-wins on rapid navigation', () => {
|
|
362
591
|
beforeEach(() => {
|
|
363
592
|
document.body.innerHTML = '<div id="root"></div>';
|
|
@@ -393,9 +622,9 @@ describe('NestedRouter latest-wins on rapid navigation', () => {
|
|
|
393
622
|
injector,
|
|
394
623
|
rootElement,
|
|
395
624
|
jsxElement: (createComponent("div", null,
|
|
396
|
-
createComponent(NestedRouteLink, { id: "go-a",
|
|
397
|
-
createComponent(NestedRouteLink, { id: "go-b",
|
|
398
|
-
createComponent(NestedRouteLink, { id: "go-c",
|
|
625
|
+
createComponent(NestedRouteLink, { id: "go-a", path: "/route-a" }, "a"),
|
|
626
|
+
createComponent(NestedRouteLink, { id: "go-b", path: "/route-b" }, "b"),
|
|
627
|
+
createComponent(NestedRouteLink, { id: "go-c", path: "/route-c" }, "c"),
|
|
399
628
|
createComponent(NestedRouter, { routes: {
|
|
400
629
|
'/route-a': {
|
|
401
630
|
component: () => createComponent("div", { id: "content" }, "route-a"),
|
|
@@ -453,8 +682,8 @@ describe('NestedRouter lifecycle element scope', () => {
|
|
|
453
682
|
injector,
|
|
454
683
|
rootElement,
|
|
455
684
|
jsxElement: (createComponent("div", null,
|
|
456
|
-
createComponent(NestedRouteLink, { id: "child-a",
|
|
457
|
-
createComponent(NestedRouteLink, { id: "child-b",
|
|
685
|
+
createComponent(NestedRouteLink, { id: "child-a", path: "/parent/child-a" }, "child-a"),
|
|
686
|
+
createComponent(NestedRouteLink, { id: "child-b", path: "/parent/child-b" }, "child-b"),
|
|
458
687
|
createComponent(NestedRouter, { routes: {
|
|
459
688
|
'/parent': {
|
|
460
689
|
component: ({ outlet }) => createComponent("div", { id: "wrapper" }, outlet ?? createComponent("span", null, "index")),
|
|
@@ -528,9 +757,9 @@ describe('NestedRouter flat routes', () => {
|
|
|
528
757
|
injector,
|
|
529
758
|
rootElement,
|
|
530
759
|
jsxElement: (createComponent("div", null,
|
|
531
|
-
createComponent(NestedRouteLink, { id: "home",
|
|
532
|
-
createComponent(NestedRouteLink, { id: "about",
|
|
533
|
-
createComponent(NestedRouteLink, { id: "contact",
|
|
760
|
+
createComponent(NestedRouteLink, { id: "home", path: "/" }, "home"),
|
|
761
|
+
createComponent(NestedRouteLink, { id: "about", path: "/about" }, "about"),
|
|
762
|
+
createComponent(NestedRouteLink, { id: "contact", path: "/contact" }, "contact"),
|
|
534
763
|
createComponent(NestedRouter, { routes: {
|
|
535
764
|
'/about': { component: () => createComponent("div", { id: "content" }, "about-page") },
|
|
536
765
|
'/contact': { component: () => createComponent("div", { id: "content" }, "contact-page") },
|
|
@@ -640,9 +869,9 @@ describe('NestedRouter route param changes', () => {
|
|
|
640
869
|
injector,
|
|
641
870
|
rootElement,
|
|
642
871
|
jsxElement: (createComponent("div", null,
|
|
643
|
-
createComponent(NestedRouteLink, { id: "user-1",
|
|
644
|
-
createComponent(NestedRouteLink, { id: "user-2",
|
|
645
|
-
createComponent(NestedRouteLink, { id: "user-3",
|
|
872
|
+
createComponent(NestedRouteLink, { id: "user-1", path: "/users/1" }, "User 1"),
|
|
873
|
+
createComponent(NestedRouteLink, { id: "user-2", path: "/users/2" }, "User 2"),
|
|
874
|
+
createComponent(NestedRouteLink, { id: "user-3", path: "/users/3" }, "User 3"),
|
|
646
875
|
createComponent(NestedRouter, { routes: {
|
|
647
876
|
'/users/:id': {
|
|
648
877
|
component: ({ match }) => createComponent("div", { id: "content" },
|
|
@@ -698,8 +927,8 @@ describe('NestedRouter route param changes', () => {
|
|
|
698
927
|
injector,
|
|
699
928
|
rootElement,
|
|
700
929
|
jsxElement: (createComponent("div", null,
|
|
701
|
-
createComponent(NestedRouteLink, { id: "alpha-dash",
|
|
702
|
-
createComponent(NestedRouteLink, { id: "beta-dash",
|
|
930
|
+
createComponent(NestedRouteLink, { id: "alpha-dash", path: "/org/alpha/dashboard" }, "Alpha Dashboard"),
|
|
931
|
+
createComponent(NestedRouteLink, { id: "beta-dash", path: "/org/beta/dashboard" }, "Beta Dashboard"),
|
|
703
932
|
createComponent(NestedRouter, { routes: {
|
|
704
933
|
'/org/:orgId': {
|
|
705
934
|
component: ({ match, outlet }) => (createComponent("div", { id: "org" },
|
|
@@ -768,9 +997,9 @@ describe('NestedRouter + RouteMatchService integration', () => {
|
|
|
768
997
|
injector,
|
|
769
998
|
rootElement,
|
|
770
999
|
jsxElement: (createComponent("div", null,
|
|
771
|
-
createComponent(NestedRouteLink, { id: "child-a",
|
|
772
|
-
createComponent(NestedRouteLink, { id: "child-b",
|
|
773
|
-
createComponent(NestedRouteLink, { id: "nowhere",
|
|
1000
|
+
createComponent(NestedRouteLink, { id: "child-a", path: "/parent/child-a" }, "child-a"),
|
|
1001
|
+
createComponent(NestedRouteLink, { id: "child-b", path: "/parent/child-b" }, "child-b"),
|
|
1002
|
+
createComponent(NestedRouteLink, { id: "nowhere", path: "/nowhere" }, "nowhere"),
|
|
774
1003
|
createComponent(NestedRouter, { routes: { '/parent': parentRoute }, notFound: createComponent("div", { id: "content" }, "not found") }))),
|
|
775
1004
|
});
|
|
776
1005
|
// Initial load
|
|
@@ -823,6 +1052,8 @@ describe('resolveViewTransition', () => {
|
|
|
823
1052
|
const makeEntry = (viewTransition) => ({
|
|
824
1053
|
route: { component: () => createComponent("div", null), viewTransition },
|
|
825
1054
|
match: { path: '/', params: {} },
|
|
1055
|
+
query: null,
|
|
1056
|
+
hash: undefined,
|
|
826
1057
|
});
|
|
827
1058
|
it('should return false when router config is undefined and route has no override', () => {
|
|
828
1059
|
expect(resolveViewTransition(undefined, [makeEntry()])).toBe(false);
|
|
@@ -881,7 +1112,7 @@ describe('NestedRouter view transitions', () => {
|
|
|
881
1112
|
injector,
|
|
882
1113
|
rootElement,
|
|
883
1114
|
jsxElement: (createComponent("div", null,
|
|
884
|
-
createComponent(NestedRouteLink, { id: "go-about",
|
|
1115
|
+
createComponent(NestedRouteLink, { id: "go-about", path: "/about" }, "about"),
|
|
885
1116
|
createComponent(NestedRouter, { viewTransition: true, routes: {
|
|
886
1117
|
'/about': { component: () => createComponent("div", { id: "content" }, "about") },
|
|
887
1118
|
'/': { component: () => createComponent("div", { id: "content" }, "home") },
|
|
@@ -903,7 +1134,7 @@ describe('NestedRouter view transitions', () => {
|
|
|
903
1134
|
injector,
|
|
904
1135
|
rootElement,
|
|
905
1136
|
jsxElement: (createComponent("div", null,
|
|
906
|
-
createComponent(NestedRouteLink, { id: "go-about",
|
|
1137
|
+
createComponent(NestedRouteLink, { id: "go-about", path: "/about" }, "about"),
|
|
907
1138
|
createComponent(NestedRouter, { routes: {
|
|
908
1139
|
'/about': { component: () => createComponent("div", { id: "content" }, "about") },
|
|
909
1140
|
'/': { component: () => createComponent("div", { id: "content" }, "home") },
|
|
@@ -925,7 +1156,7 @@ describe('NestedRouter view transitions', () => {
|
|
|
925
1156
|
injector,
|
|
926
1157
|
rootElement,
|
|
927
1158
|
jsxElement: (createComponent("div", null,
|
|
928
|
-
createComponent(NestedRouteLink, { id: "go-about",
|
|
1159
|
+
createComponent(NestedRouteLink, { id: "go-about", path: "/about" }, "about"),
|
|
929
1160
|
createComponent(NestedRouter, { viewTransition: { types: ['slide'] }, routes: {
|
|
930
1161
|
'/about': { component: () => createComponent("div", { id: "content" }, "about") },
|
|
931
1162
|
'/': { component: () => createComponent("div", { id: "content" }, "home") },
|
|
@@ -946,7 +1177,7 @@ describe('NestedRouter view transitions', () => {
|
|
|
946
1177
|
injector,
|
|
947
1178
|
rootElement,
|
|
948
1179
|
jsxElement: (createComponent("div", null,
|
|
949
|
-
createComponent(NestedRouteLink, { id: "go-about",
|
|
1180
|
+
createComponent(NestedRouteLink, { id: "go-about", path: "/about" }, "about"),
|
|
950
1181
|
createComponent(NestedRouter, { viewTransition: true, routes: {
|
|
951
1182
|
'/about': {
|
|
952
1183
|
component: () => createComponent("div", { id: "content" }, "about"),
|
|
@@ -972,7 +1203,7 @@ describe('NestedRouter view transitions', () => {
|
|
|
972
1203
|
injector,
|
|
973
1204
|
rootElement,
|
|
974
1205
|
jsxElement: (createComponent("div", null,
|
|
975
|
-
createComponent(NestedRouteLink, { id: "go-about",
|
|
1206
|
+
createComponent(NestedRouteLink, { id: "go-about", path: "/about" }, "about"),
|
|
976
1207
|
createComponent(NestedRouter, { viewTransition: true, routes: {
|
|
977
1208
|
'/about': { component: () => createComponent("div", { id: "content" }, "about") },
|
|
978
1209
|
'/': { component: () => createComponent("div", { id: "content" }, "home") },
|