@esmx/router-vue 3.0.0-rc.17 → 3.0.0-rc.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +563 -0
- package/README.zh-CN.md +563 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.mjs +11 -4
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.mjs +206 -0
- package/dist/plugin.d.ts +61 -11
- package/dist/plugin.mjs +32 -16
- package/dist/plugin.test.d.ts +1 -0
- package/dist/plugin.test.mjs +436 -0
- package/dist/router-link.d.ts +202 -0
- package/dist/router-link.mjs +84 -0
- package/dist/router-link.test.d.ts +1 -0
- package/dist/router-link.test.mjs +456 -0
- package/dist/router-view.d.ts +31 -0
- package/dist/router-view.mjs +17 -0
- package/dist/router-view.test.d.ts +1 -0
- package/dist/router-view.test.mjs +459 -0
- package/dist/use.d.ts +198 -3
- package/dist/use.mjs +75 -9
- package/dist/use.test.d.ts +1 -0
- package/dist/use.test.mjs +461 -0
- package/dist/util.d.ts +7 -0
- package/dist/util.mjs +24 -0
- package/dist/util.test.d.ts +1 -0
- package/dist/util.test.mjs +319 -0
- package/dist/vue2.d.ts +13 -0
- package/dist/vue2.mjs +0 -0
- package/dist/vue3.d.ts +13 -0
- package/dist/vue3.mjs +0 -0
- package/package.json +31 -14
- package/src/index.test.ts +263 -0
- package/src/index.ts +16 -4
- package/src/plugin.test.ts +574 -0
- package/src/plugin.ts +92 -31
- package/src/router-link.test.ts +569 -0
- package/src/router-link.ts +148 -0
- package/src/router-view.test.ts +599 -0
- package/src/router-view.ts +62 -0
- package/src/use.test.ts +616 -0
- package/src/use.ts +307 -11
- package/src/util.test.ts +418 -0
- package/src/util.ts +32 -0
- package/src/vue2.ts +16 -0
- package/src/vue3.ts +15 -0
- package/dist/link.d.ts +0 -101
- package/dist/link.mjs +0 -103
- package/dist/symbols.d.ts +0 -3
- package/dist/symbols.mjs +0 -3
- package/dist/view.d.ts +0 -21
- package/dist/view.mjs +0 -75
- package/src/link.ts +0 -177
- package/src/symbols.ts +0 -8
- package/src/view.ts +0 -95
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
import { type RouteConfig, Router, RouterMode } from '@esmx/router';
|
|
2
|
+
/**
|
|
3
|
+
* @vitest-environment happy-dom
|
|
4
|
+
*/
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
6
|
+
import { createApp, defineComponent, h, inject, nextTick, provide } from 'vue';
|
|
7
|
+
import { RouterView } from './router-view';
|
|
8
|
+
import { useProvideRouter } from './use';
|
|
9
|
+
|
|
10
|
+
// Mock components for testing
|
|
11
|
+
const HomeComponent = defineComponent({
|
|
12
|
+
name: 'HomeComponent',
|
|
13
|
+
setup() {
|
|
14
|
+
return () => h('div', { class: 'home' }, 'Home Page');
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const AboutComponent = defineComponent({
|
|
19
|
+
name: 'AboutComponent',
|
|
20
|
+
setup() {
|
|
21
|
+
return () => h('div', { class: 'about' }, 'About Page');
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const UserComponent = defineComponent({
|
|
26
|
+
name: 'UserComponent',
|
|
27
|
+
setup() {
|
|
28
|
+
return () => h('div', { class: 'user' }, 'User Component');
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// ES Module component for testing resolveComponent
|
|
33
|
+
const ESModuleComponent = {
|
|
34
|
+
__esModule: true,
|
|
35
|
+
default: defineComponent({
|
|
36
|
+
name: 'ESModuleComponent',
|
|
37
|
+
setup() {
|
|
38
|
+
return () =>
|
|
39
|
+
h('div', { class: 'es-module' }, 'ES Module Component');
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
describe('router-view.ts - RouterView Component', () => {
|
|
45
|
+
let router: Router;
|
|
46
|
+
let testContainer: HTMLElement;
|
|
47
|
+
|
|
48
|
+
beforeEach(async () => {
|
|
49
|
+
// Create test container
|
|
50
|
+
testContainer = document.createElement('div');
|
|
51
|
+
testContainer.id = 'test-app';
|
|
52
|
+
document.body.appendChild(testContainer);
|
|
53
|
+
|
|
54
|
+
// Create test routes
|
|
55
|
+
const routes: RouteConfig[] = [
|
|
56
|
+
{
|
|
57
|
+
path: '/',
|
|
58
|
+
component: HomeComponent,
|
|
59
|
+
meta: { title: 'Home' }
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
path: '/about',
|
|
63
|
+
component: AboutComponent,
|
|
64
|
+
meta: { title: 'About' }
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
path: '/users/:id',
|
|
68
|
+
component: UserComponent,
|
|
69
|
+
meta: { title: 'User' }
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
path: '/es-module',
|
|
73
|
+
component: ESModuleComponent,
|
|
74
|
+
meta: { title: 'ES Module' }
|
|
75
|
+
}
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
// Create router instance
|
|
79
|
+
router = new Router({
|
|
80
|
+
root: '#test-app',
|
|
81
|
+
routes,
|
|
82
|
+
mode: RouterMode.memory,
|
|
83
|
+
base: new URL('http://localhost:3000/')
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Initialize router to root path and wait for it to be ready
|
|
87
|
+
await router.replace('/');
|
|
88
|
+
// Wait for route to be fully initialized
|
|
89
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
afterEach(() => {
|
|
93
|
+
// Clean up test environment
|
|
94
|
+
if (testContainer.parentNode) {
|
|
95
|
+
testContainer.parentNode.removeChild(testContainer);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Destroy router
|
|
99
|
+
if (router) {
|
|
100
|
+
router.destroy();
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('Basic Functionality', () => {
|
|
105
|
+
it('should render matched route component at depth 0', async () => {
|
|
106
|
+
const TestApp = defineComponent({
|
|
107
|
+
setup() {
|
|
108
|
+
useProvideRouter(router);
|
|
109
|
+
return () => h('div', [h(RouterView)]);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const app = createApp(TestApp);
|
|
114
|
+
app.mount(testContainer);
|
|
115
|
+
await nextTick();
|
|
116
|
+
|
|
117
|
+
// Check if HomeComponent is rendered
|
|
118
|
+
expect(testContainer.textContent).toContain('Home Page');
|
|
119
|
+
|
|
120
|
+
app.unmount();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should render different components when route changes', async () => {
|
|
124
|
+
const TestApp = defineComponent({
|
|
125
|
+
setup() {
|
|
126
|
+
useProvideRouter(router);
|
|
127
|
+
return () => h('div', [h(RouterView)]);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const app = createApp(TestApp);
|
|
132
|
+
app.mount(testContainer);
|
|
133
|
+
await nextTick();
|
|
134
|
+
|
|
135
|
+
// Initially should show Home
|
|
136
|
+
expect(testContainer.textContent).toContain('Home Page');
|
|
137
|
+
|
|
138
|
+
// Navigate to About
|
|
139
|
+
await router.push('/about');
|
|
140
|
+
await nextTick();
|
|
141
|
+
|
|
142
|
+
expect(testContainer.textContent).toContain('About Page');
|
|
143
|
+
|
|
144
|
+
app.unmount();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should handle routes with parameters', async () => {
|
|
148
|
+
const TestApp = defineComponent({
|
|
149
|
+
setup() {
|
|
150
|
+
useProvideRouter(router);
|
|
151
|
+
return () => h('div', [h(RouterView)]);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const app = createApp(TestApp);
|
|
156
|
+
app.mount(testContainer);
|
|
157
|
+
|
|
158
|
+
// Navigate to user route with parameter
|
|
159
|
+
await router.push('/users/123');
|
|
160
|
+
await nextTick();
|
|
161
|
+
|
|
162
|
+
expect(testContainer.textContent).toContain('User Component');
|
|
163
|
+
|
|
164
|
+
app.unmount();
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe('Component Resolution', () => {
|
|
169
|
+
it('should resolve ES module components correctly', async () => {
|
|
170
|
+
const TestApp = defineComponent({
|
|
171
|
+
setup() {
|
|
172
|
+
useProvideRouter(router);
|
|
173
|
+
return () => h('div', [h(RouterView)]);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const app = createApp(TestApp);
|
|
178
|
+
app.mount(testContainer);
|
|
179
|
+
|
|
180
|
+
// Navigate to ES module route
|
|
181
|
+
await router.push('/es-module');
|
|
182
|
+
await nextTick();
|
|
183
|
+
|
|
184
|
+
expect(testContainer.textContent).toContain('ES Module Component');
|
|
185
|
+
|
|
186
|
+
app.unmount();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should handle function components', async () => {
|
|
190
|
+
const FunctionComponent = () => h('div', 'Function Component');
|
|
191
|
+
|
|
192
|
+
const routes: RouteConfig[] = [
|
|
193
|
+
{
|
|
194
|
+
path: '/function',
|
|
195
|
+
component: FunctionComponent,
|
|
196
|
+
meta: { title: 'Function' }
|
|
197
|
+
}
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
const functionRouter = new Router({
|
|
201
|
+
root: '#test-app',
|
|
202
|
+
routes,
|
|
203
|
+
mode: RouterMode.memory,
|
|
204
|
+
base: new URL('http://localhost:3000/')
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Initialize the router and wait for it to be ready
|
|
208
|
+
await functionRouter.replace('/function');
|
|
209
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
210
|
+
|
|
211
|
+
const TestApp = defineComponent({
|
|
212
|
+
setup() {
|
|
213
|
+
useProvideRouter(functionRouter);
|
|
214
|
+
return () => h('div', [h(RouterView)]);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const app = createApp(TestApp);
|
|
219
|
+
app.mount(testContainer);
|
|
220
|
+
await nextTick();
|
|
221
|
+
|
|
222
|
+
expect(testContainer.textContent).toContain('Function Component');
|
|
223
|
+
|
|
224
|
+
app.unmount();
|
|
225
|
+
functionRouter.destroy();
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe('Depth Tracking', () => {
|
|
230
|
+
it('should inject depth 0 by default', async () => {
|
|
231
|
+
let injectedDepth: number | undefined;
|
|
232
|
+
|
|
233
|
+
// Use the same symbol key that RouterView uses internally
|
|
234
|
+
const RouterViewDepthKey = Symbol('RouterViewDepth');
|
|
235
|
+
|
|
236
|
+
// Create a custom RouterView component that can capture the injected depth
|
|
237
|
+
const TestRouterView = defineComponent({
|
|
238
|
+
name: 'TestRouterView',
|
|
239
|
+
setup() {
|
|
240
|
+
injectedDepth = inject(RouterViewDepthKey, -1);
|
|
241
|
+
return () => h(RouterView);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const TestApp = defineComponent({
|
|
246
|
+
setup() {
|
|
247
|
+
useProvideRouter(router);
|
|
248
|
+
return () => h('div', [h(TestRouterView)]);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const app = createApp(TestApp);
|
|
253
|
+
app.mount(testContainer);
|
|
254
|
+
await nextTick();
|
|
255
|
+
|
|
256
|
+
// TestRouterView should inject the default depth 0 when no parent RouterView exists
|
|
257
|
+
expect(injectedDepth).toBe(-1); // Default value since no parent RouterView provides depth
|
|
258
|
+
|
|
259
|
+
app.unmount();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('should provide correct depth in nested RouterViews', async () => {
|
|
263
|
+
let parentDepth: number | undefined;
|
|
264
|
+
let childDepth: number | undefined;
|
|
265
|
+
|
|
266
|
+
const RouterViewDepthKey = Symbol('RouterViewDepth');
|
|
267
|
+
|
|
268
|
+
const ParentTestComponent = defineComponent({
|
|
269
|
+
name: 'ParentTestComponent',
|
|
270
|
+
setup() {
|
|
271
|
+
parentDepth = inject(RouterViewDepthKey, -1);
|
|
272
|
+
provide(RouterViewDepthKey, 0); // Simulate parent RouterView
|
|
273
|
+
return () =>
|
|
274
|
+
h('div', [h('span', 'Parent'), h(ChildTestComponent)]);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const ChildTestComponent = defineComponent({
|
|
279
|
+
name: 'ChildTestComponent',
|
|
280
|
+
setup() {
|
|
281
|
+
childDepth = inject(RouterViewDepthKey, -1);
|
|
282
|
+
return () => h('div', 'Child');
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const TestApp = defineComponent({
|
|
287
|
+
setup() {
|
|
288
|
+
useProvideRouter(router);
|
|
289
|
+
return () => h('div', [h(ParentTestComponent)]);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const app = createApp(TestApp);
|
|
294
|
+
app.mount(testContainer);
|
|
295
|
+
await nextTick();
|
|
296
|
+
|
|
297
|
+
// Parent should see default depth, child should see provided depth
|
|
298
|
+
expect(parentDepth).toBe(-1); // Default value since no RouterView above
|
|
299
|
+
expect(childDepth).toBe(0); // Value provided by parent
|
|
300
|
+
|
|
301
|
+
app.unmount();
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('should handle nested RouterViews with correct depth', async () => {
|
|
305
|
+
const Level1Component = defineComponent({
|
|
306
|
+
name: 'Level1Component',
|
|
307
|
+
setup() {
|
|
308
|
+
return () =>
|
|
309
|
+
h('div', [h('span', 'Level 1'), h(RouterView)]);
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const Level2Component = defineComponent({
|
|
314
|
+
name: 'Level2Component',
|
|
315
|
+
setup() {
|
|
316
|
+
return () => h('div', 'Level 2');
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
const nestedRoutes: RouteConfig[] = [
|
|
321
|
+
{
|
|
322
|
+
path: '/level1',
|
|
323
|
+
component: Level1Component,
|
|
324
|
+
children: [
|
|
325
|
+
{
|
|
326
|
+
path: 'level2',
|
|
327
|
+
component: Level2Component
|
|
328
|
+
}
|
|
329
|
+
]
|
|
330
|
+
}
|
|
331
|
+
];
|
|
332
|
+
|
|
333
|
+
const nestedRouter = new Router({
|
|
334
|
+
root: '#test-app',
|
|
335
|
+
routes: nestedRoutes,
|
|
336
|
+
mode: RouterMode.memory,
|
|
337
|
+
base: new URL('http://localhost:3000/')
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// Initialize the router and wait for it to be ready
|
|
341
|
+
await nestedRouter.replace('/level1/level2');
|
|
342
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
343
|
+
|
|
344
|
+
const TestApp = defineComponent({
|
|
345
|
+
setup() {
|
|
346
|
+
useProvideRouter(nestedRouter);
|
|
347
|
+
return () => h('div', [h(RouterView)]);
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const app = createApp(TestApp);
|
|
352
|
+
app.mount(testContainer);
|
|
353
|
+
await nextTick();
|
|
354
|
+
|
|
355
|
+
expect(testContainer.textContent).toContain('Level 1');
|
|
356
|
+
expect(testContainer.textContent).toContain('Level 2');
|
|
357
|
+
|
|
358
|
+
app.unmount();
|
|
359
|
+
nestedRouter.destroy();
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
describe('Edge Cases and Error Handling', () => {
|
|
364
|
+
it('should render null when no route matches at current depth', async () => {
|
|
365
|
+
const RouterViewDepthKey = Symbol('RouterViewDepth');
|
|
366
|
+
|
|
367
|
+
const DeepRouterView = defineComponent({
|
|
368
|
+
name: 'DeepRouterView',
|
|
369
|
+
setup() {
|
|
370
|
+
// Inject depth 0 from parent RouterView and provide depth 1
|
|
371
|
+
const currentDepth = inject(RouterViewDepthKey, 0);
|
|
372
|
+
provide(RouterViewDepthKey, currentDepth + 1);
|
|
373
|
+
return () => h(RouterView);
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
const TestApp = defineComponent({
|
|
378
|
+
setup() {
|
|
379
|
+
useProvideRouter(router);
|
|
380
|
+
return () =>
|
|
381
|
+
h('div', [
|
|
382
|
+
h('span', 'App'),
|
|
383
|
+
h(RouterView), // This renders Home component at depth 0
|
|
384
|
+
h(DeepRouterView) // This tries to render at depth 1, but no match
|
|
385
|
+
]);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
const app = createApp(TestApp);
|
|
390
|
+
app.mount(testContainer);
|
|
391
|
+
await nextTick();
|
|
392
|
+
|
|
393
|
+
// Should contain "App" and "Home Page" from the first RouterView
|
|
394
|
+
// but no additional content from the deep RouterView
|
|
395
|
+
expect(testContainer.textContent).toContain('App');
|
|
396
|
+
expect(testContainer.textContent).toContain('Home Page');
|
|
397
|
+
|
|
398
|
+
app.unmount();
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('should handle null components gracefully', async () => {
|
|
402
|
+
const routesWithNull: RouteConfig[] = [
|
|
403
|
+
{
|
|
404
|
+
path: '/null-component',
|
|
405
|
+
component: null as RouteConfig['component'],
|
|
406
|
+
meta: { title: 'Null Component' }
|
|
407
|
+
}
|
|
408
|
+
];
|
|
409
|
+
|
|
410
|
+
const nullRouter = new Router({
|
|
411
|
+
root: '#test-app',
|
|
412
|
+
routes: routesWithNull,
|
|
413
|
+
mode: RouterMode.memory,
|
|
414
|
+
base: new URL('http://localhost:3000/')
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
// Initialize the router and wait for it to be ready
|
|
418
|
+
await nullRouter.replace('/null-component');
|
|
419
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
420
|
+
|
|
421
|
+
const TestApp = defineComponent({
|
|
422
|
+
setup() {
|
|
423
|
+
useProvideRouter(nullRouter);
|
|
424
|
+
return () => h('div', [h('span', 'App'), h(RouterView)]);
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
const app = createApp(TestApp);
|
|
429
|
+
app.mount(testContainer);
|
|
430
|
+
await nextTick();
|
|
431
|
+
|
|
432
|
+
// Verify that only the "App" text is rendered and RouterView renders nothing
|
|
433
|
+
expect(testContainer.textContent?.trim()).toBe('App');
|
|
434
|
+
expect(testContainer.querySelector('div')?.children.length).toBe(1); // Only the span element
|
|
435
|
+
expect(testContainer.querySelector('span')?.textContent).toBe(
|
|
436
|
+
'App'
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
app.unmount();
|
|
440
|
+
nullRouter.destroy();
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('should handle non-existent routes', async () => {
|
|
444
|
+
// Create a new router instance with a valid initial route
|
|
445
|
+
const nonExistentRouter = new Router({
|
|
446
|
+
root: '#test-app',
|
|
447
|
+
routes: [
|
|
448
|
+
{
|
|
449
|
+
path: '/',
|
|
450
|
+
component: null // Initial route with null component
|
|
451
|
+
}
|
|
452
|
+
],
|
|
453
|
+
mode: RouterMode.memory,
|
|
454
|
+
base: new URL('http://localhost:3000/')
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// Initialize router with root path
|
|
458
|
+
await nonExistentRouter.replace('/');
|
|
459
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
460
|
+
|
|
461
|
+
const TestApp = defineComponent({
|
|
462
|
+
setup() {
|
|
463
|
+
useProvideRouter(nonExistentRouter);
|
|
464
|
+
return () => h('div', [h('span', 'App'), h(RouterView)]);
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
const app = createApp(TestApp);
|
|
469
|
+
app.mount(testContainer);
|
|
470
|
+
|
|
471
|
+
// Navigate to non-existent route
|
|
472
|
+
await nonExistentRouter.push('/non-existent');
|
|
473
|
+
await nextTick();
|
|
474
|
+
|
|
475
|
+
// Wait for any pending route changes
|
|
476
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
477
|
+
|
|
478
|
+
// Verify that only the "App" text is rendered and RouterView renders nothing
|
|
479
|
+
expect(testContainer.textContent?.trim()).toBe('App');
|
|
480
|
+
expect(testContainer.querySelector('div')?.children.length).toBe(1); // Only the span element
|
|
481
|
+
expect(testContainer.querySelector('span')?.textContent).toBe(
|
|
482
|
+
'App'
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
app.unmount();
|
|
486
|
+
nonExistentRouter.destroy();
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it('should handle malformed ES modules', async () => {
|
|
490
|
+
const MalformedModule = {
|
|
491
|
+
__esModule: true,
|
|
492
|
+
default: null
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
const malformedRoutes: RouteConfig[] = [
|
|
496
|
+
{
|
|
497
|
+
path: '/malformed',
|
|
498
|
+
component: MalformedModule as RouteConfig['component'],
|
|
499
|
+
meta: { title: 'Malformed' }
|
|
500
|
+
}
|
|
501
|
+
];
|
|
502
|
+
|
|
503
|
+
const malformedRouter = new Router({
|
|
504
|
+
root: '#test-app',
|
|
505
|
+
routes: malformedRoutes,
|
|
506
|
+
mode: RouterMode.memory,
|
|
507
|
+
base: new URL('http://localhost:3000/')
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
// Initialize the router and wait for it to be ready
|
|
511
|
+
await malformedRouter.replace('/malformed');
|
|
512
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
513
|
+
|
|
514
|
+
const TestApp = defineComponent({
|
|
515
|
+
setup() {
|
|
516
|
+
useProvideRouter(malformedRouter);
|
|
517
|
+
return () => h('div', [h('span', 'App'), h(RouterView)]);
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
const app = createApp(TestApp);
|
|
522
|
+
app.mount(testContainer);
|
|
523
|
+
await nextTick();
|
|
524
|
+
|
|
525
|
+
expect(testContainer.textContent).toBe('App');
|
|
526
|
+
|
|
527
|
+
app.unmount();
|
|
528
|
+
malformedRouter.destroy();
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
describe('Component Properties', () => {
|
|
533
|
+
it('should have correct component name', () => {
|
|
534
|
+
expect(RouterView.name).toBe('RouterView');
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it('should be a valid Vue component', () => {
|
|
538
|
+
expect(RouterView).toHaveProperty('setup');
|
|
539
|
+
expect(typeof RouterView.setup).toBe('function');
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it('should not define props', () => {
|
|
543
|
+
expect(RouterView.props).toBeUndefined();
|
|
544
|
+
});
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
describe('Integration Tests', () => {
|
|
548
|
+
it('should re-render when route changes', async () => {
|
|
549
|
+
const TestApp = defineComponent({
|
|
550
|
+
setup() {
|
|
551
|
+
useProvideRouter(router);
|
|
552
|
+
return () => h('div', [h(RouterView)]);
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
const app = createApp(TestApp);
|
|
557
|
+
app.mount(testContainer);
|
|
558
|
+
await nextTick();
|
|
559
|
+
|
|
560
|
+
expect(testContainer.textContent).toContain('Home Page');
|
|
561
|
+
|
|
562
|
+
await router.push('/about');
|
|
563
|
+
await nextTick();
|
|
564
|
+
expect(testContainer.textContent).toContain('About Page');
|
|
565
|
+
|
|
566
|
+
await router.push('/users/123');
|
|
567
|
+
await nextTick();
|
|
568
|
+
expect(testContainer.textContent).toContain('User Component');
|
|
569
|
+
|
|
570
|
+
app.unmount();
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
it('should work with router navigation methods', async () => {
|
|
574
|
+
const TestApp = defineComponent({
|
|
575
|
+
setup() {
|
|
576
|
+
useProvideRouter(router);
|
|
577
|
+
return () => h('div', [h(RouterView)]);
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
const app = createApp(TestApp);
|
|
582
|
+
app.mount(testContainer);
|
|
583
|
+
|
|
584
|
+
await router.push('/about');
|
|
585
|
+
await nextTick();
|
|
586
|
+
expect(testContainer.textContent).toContain('About Page');
|
|
587
|
+
|
|
588
|
+
await router.replace('/users/456');
|
|
589
|
+
await nextTick();
|
|
590
|
+
expect(testContainer.textContent).toContain('User Component');
|
|
591
|
+
|
|
592
|
+
await router.back();
|
|
593
|
+
await nextTick();
|
|
594
|
+
expect(testContainer.textContent).toContain('Home Page');
|
|
595
|
+
|
|
596
|
+
app.unmount();
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { defineComponent, h, inject, provide } from 'vue';
|
|
2
|
+
import { useRoute } from './use';
|
|
3
|
+
import { resolveComponent } from './util';
|
|
4
|
+
|
|
5
|
+
const RouterViewDepthKey = Symbol('RouterViewDepth');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* RouterView component for rendering matched route components.
|
|
9
|
+
* Acts as a placeholder where route components are rendered based on the current route.
|
|
10
|
+
* Supports nested routing with proper depth tracking using Vue's provide/inject mechanism.
|
|
11
|
+
*
|
|
12
|
+
* @param props - Component properties (RouterView accepts no props)
|
|
13
|
+
* @param context - Vue setup context (not used)
|
|
14
|
+
* @param context.slots - Component slots (not used)
|
|
15
|
+
* @returns Vue render function that renders the matched route component at current depth
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
*
|
|
19
|
+
* ```vue
|
|
20
|
+
* <template>
|
|
21
|
+
* <div id="app">
|
|
22
|
+
* <!-- Navigation links -->
|
|
23
|
+
* <nav>
|
|
24
|
+
* <RouterLink to="/">Home</RouterLink>
|
|
25
|
+
* <RouterLink to="/about">About</RouterLink>
|
|
26
|
+
* <RouterLink to="/users">Users</RouterLink>
|
|
27
|
+
* </nav>
|
|
28
|
+
*
|
|
29
|
+
* <!-- Root level route components render here -->
|
|
30
|
+
* <RouterView />
|
|
31
|
+
* </div>
|
|
32
|
+
* </template>
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export const RouterView = defineComponent({
|
|
36
|
+
name: 'RouterView',
|
|
37
|
+
setup() {
|
|
38
|
+
const route = useRoute();
|
|
39
|
+
|
|
40
|
+
// Get current RouterView depth from parent RouterView (if any)
|
|
41
|
+
// This enables proper nested routing by tracking how deep we are in the component tree
|
|
42
|
+
const depth = inject(RouterViewDepthKey, 0);
|
|
43
|
+
|
|
44
|
+
// Provide depth + 1 to child RouterView components
|
|
45
|
+
// This ensures each nested RouterView renders the correct route component
|
|
46
|
+
provide(RouterViewDepthKey, depth + 1);
|
|
47
|
+
|
|
48
|
+
return () => {
|
|
49
|
+
// Get the matched route configuration at current depth
|
|
50
|
+
// route.matched is an array of matched route configs from parent to child
|
|
51
|
+
const matchedRoute = route.matched[depth];
|
|
52
|
+
|
|
53
|
+
// Resolve the component, handling ES module format if necessary
|
|
54
|
+
const component = matchedRoute
|
|
55
|
+
? resolveComponent(matchedRoute.component)
|
|
56
|
+
: null;
|
|
57
|
+
|
|
58
|
+
// Render the component or null if no match at this depth
|
|
59
|
+
return component ? h(component) : null;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
});
|