@furystack/shades 13.2.2 → 15.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 +157 -0
- package/esm/component-factory.spec.d.ts.map +1 -1
- package/esm/component-factory.spec.js +2 -2
- package/esm/component-factory.spec.js.map +1 -1
- 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 +109 -19
- 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 +18 -20
- package/esm/components/nested-route-link.js.map +1 -1
- package/esm/components/nested-route-link.spec.js +67 -17
- 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 +139 -13
- package/esm/components/nested-router.js.map +1 -1
- package/esm/components/nested-router.spec.js +283 -52
- package/esm/components/nested-router.spec.js.map +1 -1
- package/esm/services/location-service.d.ts +39 -42
- package/esm/services/location-service.d.ts.map +1 -1
- package/esm/services/location-service.js +115 -134
- package/esm/services/location-service.js.map +1 -1
- package/esm/services/location-service.spec.js +59 -24
- package/esm/services/location-service.spec.js.map +1 -1
- package/esm/services/route-match-service.d.ts +3 -2
- package/esm/services/route-match-service.d.ts.map +1 -1
- package/esm/services/route-match-service.js +13 -22
- package/esm/services/route-match-service.js.map +1 -1
- package/esm/services/route-match-service.spec.js +42 -49
- 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 +60 -5
- package/esm/services/route-meta-utils.spec.js.map +1 -1
- package/esm/services/screen-service.d.ts +13 -72
- package/esm/services/screen-service.d.ts.map +1 -1
- package/esm/services/screen-service.js +45 -137
- package/esm/services/screen-service.js.map +1 -1
- package/esm/services/screen-service.spec.js +19 -18
- package/esm/services/screen-service.spec.js.map +1 -1
- package/esm/services/spatial-navigation-service.d.ts +17 -39
- package/esm/services/spatial-navigation-service.d.ts.map +1 -1
- package/esm/services/spatial-navigation-service.js +280 -313
- package/esm/services/spatial-navigation-service.js.map +1 -1
- package/esm/services/spatial-navigation-service.spec.js +163 -158
- package/esm/services/spatial-navigation-service.spec.js.map +1 -1
- package/esm/shade.d.ts +1 -1
- package/esm/shade.d.ts.map +1 -1
- package/esm/shade.js +10 -7
- package/esm/shade.js.map +1 -1
- package/esm/vnode.d.ts +1 -1
- package/esm/vnode.js +1 -1
- package/package.json +5 -4
- package/src/component-factory.spec.tsx +3 -2
- 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 +136 -32
- package/src/components/nested-navigate.ts +75 -26
- package/src/components/nested-replace.spec.ts +89 -0
- package/src/components/nested-replace.ts +46 -0
- package/src/components/nested-route-link.spec.tsx +90 -22
- package/src/components/nested-route-link.tsx +54 -29
- package/src/components/nested-route-types.ts +82 -2
- package/src/components/nested-router.spec.tsx +334 -51
- package/src/components/nested-router.tsx +184 -24
- package/src/services/location-service.spec.ts +68 -25
- package/src/services/location-service.tsx +156 -122
- package/src/services/route-match-service.spec.ts +41 -28
- package/src/services/route-match-service.ts +17 -8
- package/src/services/route-meta-utils.spec.ts +78 -10
- package/src/services/route-meta-utils.ts +44 -15
- package/src/services/screen-service.spec.ts +19 -18
- package/src/services/screen-service.ts +65 -110
- package/src/services/spatial-navigation-service.spec.ts +165 -160
- package/src/services/spatial-navigation-service.ts +299 -287
- package/src/shade.ts +11 -8
- 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
|
-
import {
|
|
1
|
+
import { createInjector } 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);
|
|
@@ -277,16 +385,16 @@ describe('NestedRouter lifecycle hooks', () => {
|
|
|
277
385
|
const onLeaveOther = vi.fn(async () => {
|
|
278
386
|
callOrder.push('leave-other');
|
|
279
387
|
});
|
|
280
|
-
await usingAsync(
|
|
388
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
281
389
|
const rootElement = document.getElementById('root');
|
|
282
390
|
initializeShadeRoot({
|
|
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(createInjector(), 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.get(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(createInjector(), 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.get(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(createInjector(), 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.get(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>';
|
|
@@ -387,15 +616,15 @@ describe('NestedRouter latest-wins on rapid navigation', () => {
|
|
|
387
616
|
const onLeaveC = vi.fn(async () => {
|
|
388
617
|
callOrder.push('leave-c');
|
|
389
618
|
});
|
|
390
|
-
await usingAsync(
|
|
619
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
391
620
|
const rootElement = document.getElementById('root');
|
|
392
621
|
initializeShadeRoot({
|
|
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"),
|
|
@@ -447,14 +676,14 @@ describe('NestedRouter lifecycle element scope', () => {
|
|
|
447
676
|
history.pushState(null, '', '/parent/child-a');
|
|
448
677
|
const visitElements = [];
|
|
449
678
|
const leaveElements = [];
|
|
450
|
-
await usingAsync(
|
|
679
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
451
680
|
const rootElement = document.getElementById('root');
|
|
452
681
|
initializeShadeRoot({
|
|
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")),
|
|
@@ -522,15 +751,15 @@ describe('NestedRouter flat routes', () => {
|
|
|
522
751
|
});
|
|
523
752
|
it('should render and navigate between flat (non-nested) Record routes', async () => {
|
|
524
753
|
history.pushState(null, '', '/');
|
|
525
|
-
await usingAsync(
|
|
754
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
526
755
|
const rootElement = document.getElementById('root');
|
|
527
756
|
initializeShadeRoot({
|
|
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") },
|
|
@@ -562,7 +791,7 @@ describe('NestedRouter outlet composition', () => {
|
|
|
562
791
|
});
|
|
563
792
|
it('should compose parent layout wrapping child content via outlet', async () => {
|
|
564
793
|
history.pushState(null, '', '/dashboard/settings');
|
|
565
|
-
await usingAsync(
|
|
794
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
566
795
|
const rootElement = document.getElementById('root');
|
|
567
796
|
initializeShadeRoot({
|
|
568
797
|
injector,
|
|
@@ -595,7 +824,7 @@ describe('NestedRouter outlet composition', () => {
|
|
|
595
824
|
});
|
|
596
825
|
it('should render parent with fallback when navigating to parent URL without a child match', async () => {
|
|
597
826
|
history.pushState(null, '', '/dashboard');
|
|
598
|
-
await usingAsync(
|
|
827
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
599
828
|
const rootElement = document.getElementById('root');
|
|
600
829
|
initializeShadeRoot({
|
|
601
830
|
injector,
|
|
@@ -634,15 +863,15 @@ describe('NestedRouter route param changes', () => {
|
|
|
634
863
|
const onLeaveUser = vi.fn(async () => {
|
|
635
864
|
callOrder.push('leave-user');
|
|
636
865
|
});
|
|
637
|
-
await usingAsync(
|
|
866
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
638
867
|
const rootElement = document.getElementById('root');
|
|
639
868
|
initializeShadeRoot({
|
|
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" },
|
|
@@ -692,14 +921,14 @@ describe('NestedRouter route param changes', () => {
|
|
|
692
921
|
const onLeaveOrg = vi.fn();
|
|
693
922
|
const onVisitDash = vi.fn();
|
|
694
923
|
const onLeaveDash = vi.fn();
|
|
695
|
-
await usingAsync(
|
|
924
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
696
925
|
const rootElement = document.getElementById('root');
|
|
697
926
|
initializeShadeRoot({
|
|
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" },
|
|
@@ -747,9 +976,9 @@ describe('NestedRouter + RouteMatchService integration', () => {
|
|
|
747
976
|
});
|
|
748
977
|
it('should update RouteMatchService with the current match chain on navigation', async () => {
|
|
749
978
|
history.pushState(null, '', '/parent/child-a');
|
|
750
|
-
await usingAsync(
|
|
979
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
751
980
|
const rootElement = document.getElementById('root');
|
|
752
|
-
const routeMatchService = injector.
|
|
981
|
+
const routeMatchService = injector.get(RouteMatchService);
|
|
753
982
|
const parentRoute = {
|
|
754
983
|
meta: { title: 'Parent' },
|
|
755
984
|
component: ({ outlet }) => createComponent("div", null, outlet ?? createComponent("div", null, "parent-index")),
|
|
@@ -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
|
|
@@ -795,9 +1024,9 @@ describe('NestedRouter + RouteMatchService integration', () => {
|
|
|
795
1024
|
});
|
|
796
1025
|
it('should expose match params through RouteMatchService', async () => {
|
|
797
1026
|
history.pushState(null, '', '/users/42');
|
|
798
|
-
await usingAsync(
|
|
1027
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
799
1028
|
const rootElement = document.getElementById('root');
|
|
800
|
-
const routeMatchService = injector.
|
|
1029
|
+
const routeMatchService = injector.get(RouteMatchService);
|
|
801
1030
|
initializeShadeRoot({
|
|
802
1031
|
injector,
|
|
803
1032
|
rootElement,
|
|
@@ -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);
|
|
@@ -875,13 +1106,13 @@ describe('NestedRouter view transitions', () => {
|
|
|
875
1106
|
});
|
|
876
1107
|
it('should call startViewTransition when viewTransition is enabled', async () => {
|
|
877
1108
|
history.pushState(null, '', '/');
|
|
878
|
-
await usingAsync(
|
|
1109
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
879
1110
|
const rootElement = document.getElementById('root');
|
|
880
1111
|
initializeShadeRoot({
|
|
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") },
|
|
@@ -897,13 +1128,13 @@ describe('NestedRouter view transitions', () => {
|
|
|
897
1128
|
});
|
|
898
1129
|
it('should not call startViewTransition when viewTransition is not set', async () => {
|
|
899
1130
|
history.pushState(null, '', '/');
|
|
900
|
-
await usingAsync(
|
|
1131
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
901
1132
|
const rootElement = document.getElementById('root');
|
|
902
1133
|
initializeShadeRoot({
|
|
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") },
|
|
@@ -919,13 +1150,13 @@ describe('NestedRouter view transitions', () => {
|
|
|
919
1150
|
});
|
|
920
1151
|
it('should pass types to startViewTransition when configured', async () => {
|
|
921
1152
|
history.pushState(null, '', '/');
|
|
922
|
-
await usingAsync(
|
|
1153
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
923
1154
|
const rootElement = document.getElementById('root');
|
|
924
1155
|
initializeShadeRoot({
|
|
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") },
|
|
@@ -940,13 +1171,13 @@ describe('NestedRouter view transitions', () => {
|
|
|
940
1171
|
});
|
|
941
1172
|
it('should respect per-route viewTransition: false override', async () => {
|
|
942
1173
|
history.pushState(null, '', '/');
|
|
943
|
-
await usingAsync(
|
|
1174
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
944
1175
|
const rootElement = document.getElementById('root');
|
|
945
1176
|
initializeShadeRoot({
|
|
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"),
|
|
@@ -966,13 +1197,13 @@ describe('NestedRouter view transitions', () => {
|
|
|
966
1197
|
it('should fall back gracefully when startViewTransition is not available', async () => {
|
|
967
1198
|
delete document.startViewTransition;
|
|
968
1199
|
history.pushState(null, '', '/');
|
|
969
|
-
await usingAsync(
|
|
1200
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
970
1201
|
const rootElement = document.getElementById('root');
|
|
971
1202
|
initializeShadeRoot({
|
|
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") },
|