@esmx/router-vue 3.0.0-rc.17 → 3.0.0-rc.19

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.
Files changed (55) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +563 -0
  3. package/README.zh-CN.md +563 -0
  4. package/dist/index.d.ts +6 -4
  5. package/dist/index.mjs +11 -4
  6. package/dist/index.test.d.ts +1 -0
  7. package/dist/index.test.mjs +206 -0
  8. package/dist/plugin.d.ts +55 -11
  9. package/dist/plugin.mjs +32 -16
  10. package/dist/plugin.test.d.ts +1 -0
  11. package/dist/plugin.test.mjs +436 -0
  12. package/dist/router-link.d.ts +202 -0
  13. package/dist/router-link.mjs +84 -0
  14. package/dist/router-link.test.d.ts +1 -0
  15. package/dist/router-link.test.mjs +456 -0
  16. package/dist/router-view.d.ts +30 -0
  17. package/dist/router-view.mjs +17 -0
  18. package/dist/router-view.test.d.ts +1 -0
  19. package/dist/router-view.test.mjs +459 -0
  20. package/dist/use.d.ts +198 -3
  21. package/dist/use.mjs +75 -9
  22. package/dist/use.test.d.ts +1 -0
  23. package/dist/use.test.mjs +461 -0
  24. package/dist/util.d.ts +7 -0
  25. package/dist/util.mjs +24 -0
  26. package/dist/util.test.d.ts +1 -0
  27. package/dist/util.test.mjs +319 -0
  28. package/dist/vue2.d.ts +13 -0
  29. package/dist/vue2.mjs +0 -0
  30. package/dist/vue3.d.ts +13 -0
  31. package/dist/vue3.mjs +0 -0
  32. package/package.json +31 -14
  33. package/src/index.test.ts +263 -0
  34. package/src/index.ts +16 -4
  35. package/src/plugin.test.ts +574 -0
  36. package/src/plugin.ts +86 -31
  37. package/src/router-link.test.ts +569 -0
  38. package/src/router-link.ts +148 -0
  39. package/src/router-view.test.ts +599 -0
  40. package/src/router-view.ts +61 -0
  41. package/src/use.test.ts +616 -0
  42. package/src/use.ts +307 -11
  43. package/src/util.test.ts +418 -0
  44. package/src/util.ts +32 -0
  45. package/src/vue2.ts +16 -0
  46. package/src/vue3.ts +15 -0
  47. package/dist/link.d.ts +0 -101
  48. package/dist/link.mjs +0 -103
  49. package/dist/symbols.d.ts +0 -3
  50. package/dist/symbols.mjs +0 -3
  51. package/dist/view.d.ts +0 -21
  52. package/dist/view.mjs +0 -75
  53. package/src/link.ts +0 -177
  54. package/src/symbols.ts +0 -8
  55. package/src/view.ts +0 -95
@@ -0,0 +1,616 @@
1
+ import { type Route, 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 {
7
+ createApp,
8
+ defineComponent,
9
+ getCurrentInstance,
10
+ h,
11
+ nextTick,
12
+ ref
13
+ } from 'vue';
14
+ import {
15
+ type VueInstance,
16
+ getRoute,
17
+ getRouter,
18
+ useLink,
19
+ useProvideRouter,
20
+ useRoute,
21
+ useRouter
22
+ } from './use';
23
+
24
+ describe('use.ts - Vue Router Integration', () => {
25
+ let router: Router;
26
+ let testContainer: HTMLElement;
27
+
28
+ beforeEach(async () => {
29
+ // Create test container
30
+ testContainer = document.createElement('div');
31
+ testContainer.id = 'test-app';
32
+ document.body.appendChild(testContainer);
33
+
34
+ // Create test routes
35
+ const routes: RouteConfig[] = [
36
+ {
37
+ path: '/',
38
+ component: defineComponent({ template: '<div>Home</div>' }),
39
+ meta: { title: 'Home' }
40
+ },
41
+ {
42
+ path: '/about',
43
+ component: defineComponent({ template: '<div>About</div>' }),
44
+ meta: { title: 'About' }
45
+ }
46
+ ];
47
+
48
+ // Create router instance
49
+ router = new Router({
50
+ root: '#test-app',
51
+ routes,
52
+ mode: RouterMode.memory,
53
+ base: new URL('http://localhost:3000/')
54
+ });
55
+
56
+ // Initialize router to root path
57
+ await router.replace('/');
58
+ });
59
+
60
+ afterEach(() => {
61
+ // Clean up test environment
62
+ if (testContainer.parentNode) {
63
+ testContainer.parentNode.removeChild(testContainer);
64
+ }
65
+
66
+ // Destroy router
67
+ if (router) {
68
+ router.destroy();
69
+ }
70
+ });
71
+
72
+ describe('Error Handling - Context Not Found', () => {
73
+ const contextNotFoundError =
74
+ '[@esmx/router-vue] Router context not found. ' +
75
+ 'Please ensure useProvideRouter() is called in a parent component.';
76
+
77
+ const contextErrorTestCases = [
78
+ {
79
+ name: 'getRouter called without router context',
80
+ test: () => {
81
+ const app = createApp({ template: '<div>Test</div>' });
82
+ const vm = app.mount(testContainer);
83
+ const result = () => getRouter(vm);
84
+ app.unmount();
85
+ return result;
86
+ }
87
+ },
88
+ {
89
+ name: 'getRoute called without router context',
90
+ test: () => {
91
+ const app = createApp({ template: '<div>Test</div>' });
92
+ const vm = app.mount(testContainer);
93
+ const result = () => getRoute(vm);
94
+ app.unmount();
95
+ return result;
96
+ }
97
+ }
98
+ ];
99
+
100
+ contextErrorTestCases.forEach(({ name, test }) => {
101
+ it(`should throw error when ${name}`, () => {
102
+ expect(test()).toThrow(contextNotFoundError);
103
+ });
104
+ });
105
+
106
+ const compositionContextErrorTestCases = [
107
+ {
108
+ name: 'useRouter called without router context',
109
+ setupFn: () => {
110
+ expect(() => useRouter()).toThrow(contextNotFoundError);
111
+ }
112
+ },
113
+ {
114
+ name: 'useRoute called without router context',
115
+ setupFn: () => {
116
+ expect(() => useRoute()).toThrow(contextNotFoundError);
117
+ }
118
+ },
119
+ {
120
+ name: 'useLink called without router context',
121
+ setupFn: () => {
122
+ expect(() =>
123
+ useLink({
124
+ to: '/about',
125
+ type: 'push',
126
+ exact: 'include'
127
+ })
128
+ ).toThrow(contextNotFoundError);
129
+ }
130
+ }
131
+ ];
132
+
133
+ compositionContextErrorTestCases.forEach(({ name, setupFn }) => {
134
+ it(`should throw error when ${name}`, () => {
135
+ const TestComponent = defineComponent({
136
+ setup() {
137
+ setupFn();
138
+ return () => '<div>Test</div>';
139
+ }
140
+ });
141
+
142
+ const app = createApp(TestComponent);
143
+ app.mount(testContainer);
144
+ app.unmount();
145
+ });
146
+ });
147
+ });
148
+
149
+ describe('Error Handling - Setup Only', () => {
150
+ const setupOnlyTestCases = [
151
+ {
152
+ name: 'useRouter called outside setup()',
153
+ fn: () => useRouter(),
154
+ expectedError:
155
+ '[@esmx/router-vue] useRouter() can only be called during setup()'
156
+ },
157
+ {
158
+ name: 'useRoute called outside setup()',
159
+ fn: () => useRoute(),
160
+ expectedError:
161
+ '[@esmx/router-vue] useRoute() can only be called during setup()'
162
+ },
163
+ {
164
+ name: 'useLink called outside setup()',
165
+ fn: () =>
166
+ useLink({
167
+ to: '/about',
168
+ type: 'push',
169
+ exact: 'include'
170
+ }),
171
+ expectedError:
172
+ '[@esmx/router-vue] useRouter() can only be called during setup()'
173
+ },
174
+ {
175
+ name: 'useProvideRouter called outside setup()',
176
+ fn: () => useProvideRouter(router),
177
+ expectedError:
178
+ '[@esmx/router-vue] useProvideRouter() can only be called during setup()'
179
+ }
180
+ ];
181
+
182
+ setupOnlyTestCases.forEach(({ name, fn, expectedError }) => {
183
+ it(`should throw error when ${name}`, () => {
184
+ expect(fn).toThrow(expectedError);
185
+ });
186
+ });
187
+ });
188
+
189
+ describe('Basic Functionality', () => {
190
+ it('should provide router context and return router instance from getRouter', async () => {
191
+ let routerInstance: Router | null = null;
192
+
193
+ const app = createApp({
194
+ setup() {
195
+ useProvideRouter(router);
196
+ return () => '<div>App</div>';
197
+ }
198
+ });
199
+
200
+ const vm = app.mount(testContainer);
201
+ routerInstance = getRouter(vm);
202
+
203
+ expect(routerInstance).toBe(router);
204
+ expect(routerInstance).toBeInstanceOf(Router);
205
+
206
+ app.unmount();
207
+ });
208
+
209
+ it('should provide router context and return current route from getRoute', async () => {
210
+ let currentRoute: Route | null = null;
211
+
212
+ const app = createApp({
213
+ setup() {
214
+ useProvideRouter(router);
215
+ return () => '<div>App</div>';
216
+ }
217
+ });
218
+
219
+ const vm = app.mount(testContainer);
220
+ currentRoute = getRoute(vm);
221
+
222
+ expect(currentRoute).toBeTruthy();
223
+ expect(currentRoute!.path).toBe('/');
224
+ expect(currentRoute!.meta.title).toBe('Home');
225
+
226
+ app.unmount();
227
+ });
228
+ });
229
+
230
+ describe('Setup() Support - useRouter in setup()', () => {
231
+ it('should allow useRouter to work in setup() via provide/inject', async () => {
232
+ let routerInstance: Router | null = null;
233
+ let childRoute: Route | null = null;
234
+
235
+ // Child component that uses router in setup()
236
+ const ChildComponent = defineComponent({
237
+ name: 'ChildComponent',
238
+ setup() {
239
+ // This should now work in setup() thanks to provide/inject
240
+ routerInstance = useRouter();
241
+ childRoute = useRoute();
242
+
243
+ expect(routerInstance).toBe(router);
244
+ expect(childRoute.path).toBe('/');
245
+
246
+ return () => h('div', 'Child Component');
247
+ }
248
+ });
249
+
250
+ // Parent component that provides router
251
+ const ParentComponent = defineComponent({
252
+ name: 'ParentComponent',
253
+ components: { ChildComponent },
254
+ setup() {
255
+ useProvideRouter(router);
256
+ return () => h(ChildComponent);
257
+ }
258
+ });
259
+
260
+ const app = createApp(ParentComponent);
261
+ app.mount(testContainer);
262
+ await nextTick();
263
+
264
+ // Verify that setup() calls succeeded
265
+ expect(routerInstance).toBe(router);
266
+ expect(childRoute).toBeTruthy();
267
+ expect(childRoute!.path).toBe('/');
268
+
269
+ app.unmount();
270
+ });
271
+
272
+ it('should work with nested components in setup()', async () => {
273
+ let deepChildRouter: Router | null = null;
274
+
275
+ const DeepChildComponent = defineComponent({
276
+ name: 'DeepChildComponent',
277
+ setup() {
278
+ // Should work even in deeply nested components
279
+ deepChildRouter = useRouter();
280
+ expect(deepChildRouter).toBe(router);
281
+ return () => h('div', 'Deep Child');
282
+ }
283
+ });
284
+
285
+ const MiddleComponent = defineComponent({
286
+ name: 'MiddleComponent',
287
+ components: { DeepChildComponent },
288
+ setup() {
289
+ return () => h(DeepChildComponent);
290
+ }
291
+ });
292
+
293
+ const TopComponent = defineComponent({
294
+ name: 'TopComponent',
295
+ components: { MiddleComponent },
296
+ setup() {
297
+ useProvideRouter(router);
298
+ return () => h(MiddleComponent);
299
+ }
300
+ });
301
+
302
+ const app = createApp(TopComponent);
303
+ app.mount(testContainer);
304
+ await nextTick();
305
+
306
+ expect(deepChildRouter).toBe(router);
307
+ app.unmount();
308
+ });
309
+ });
310
+
311
+ describe('Component Hierarchy Context Finding - Investigation', () => {
312
+ it('should investigate component hierarchy traversal with logging', async () => {
313
+ let childRouterResult: Router | null = null;
314
+ let parentVmInstance: VueInstance | null = null;
315
+ let childVmInstance: VueInstance | null = null;
316
+
317
+ // Create a child component that doesn't have direct router context
318
+ const ChildComponent = defineComponent({
319
+ name: 'ChildComponent',
320
+ setup(_, { expose }) {
321
+ // Get current instance for investigation
322
+ const instance = getCurrentInstance();
323
+ childVmInstance = instance?.proxy || null;
324
+
325
+ try {
326
+ // This should trigger hierarchy traversal
327
+ childRouterResult = useRouter();
328
+ } catch (error: unknown) {
329
+ expect((error as Error).message).toContain(
330
+ 'Router context not found'
331
+ );
332
+ }
333
+
334
+ expose({ childVmInstance });
335
+ return () => '<div>Child Component</div>';
336
+ }
337
+ });
338
+
339
+ // Create a parent component that provides router context
340
+ const ParentComponent = defineComponent({
341
+ name: 'ParentComponent',
342
+ components: { ChildComponent },
343
+ setup(_, { expose }) {
344
+ // Get current instance for investigation
345
+ const instance = getCurrentInstance();
346
+ parentVmInstance = instance?.proxy || null;
347
+
348
+ // Provide router context at parent level
349
+ useProvideRouter(router);
350
+
351
+ expose({ parentVmInstance });
352
+ return () => h(ChildComponent);
353
+ }
354
+ });
355
+
356
+ const app = createApp(ParentComponent);
357
+ const mountedApp = app.mount(testContainer);
358
+ await nextTick();
359
+
360
+ // Investigate the actual component relationship
361
+ if (childVmInstance && parentVmInstance) {
362
+ // Check if router context exists on parent
363
+ const parentHasContext =
364
+ !!(parentVmInstance as Record<symbol, unknown>)[
365
+ Symbol.for('router-context')
366
+ ] ||
367
+ Object.getOwnPropertySymbols(parentVmInstance).some((sym) =>
368
+ sym.toString().includes('router-context')
369
+ );
370
+ expect(parentHasContext).toBe(true);
371
+ }
372
+
373
+ // The test expectation
374
+ if (childRouterResult) {
375
+ expect(childRouterResult).toBe(router);
376
+ }
377
+
378
+ app.unmount();
379
+ });
380
+
381
+ it('should investigate direct getRouter call with component instances', async () => {
382
+ let parentInstance: VueInstance | null = null;
383
+ let childInstance: VueInstance | null = null;
384
+
385
+ const ChildComponent = defineComponent({
386
+ name: 'ChildComponent',
387
+ setup() {
388
+ const instance = getCurrentInstance();
389
+ childInstance = instance?.proxy || null;
390
+ return () => '<div>Child</div>';
391
+ }
392
+ });
393
+
394
+ const ParentComponent = defineComponent({
395
+ name: 'ParentComponent',
396
+ components: { ChildComponent },
397
+ setup() {
398
+ const instance = getCurrentInstance();
399
+ parentInstance = instance?.proxy || null;
400
+ useProvideRouter(router);
401
+ return () => h(ChildComponent);
402
+ }
403
+ });
404
+
405
+ const app = createApp(ParentComponent);
406
+ app.mount(testContainer);
407
+ await nextTick();
408
+
409
+ if (childInstance && parentInstance) {
410
+ try {
411
+ const routerFromChild = getRouter(childInstance);
412
+ expect(routerFromChild).toBe(router);
413
+ } catch (error: unknown) {
414
+ expect((error as Error).message).toContain(
415
+ 'Router context not found'
416
+ );
417
+ }
418
+ }
419
+
420
+ app.unmount();
421
+ });
422
+ });
423
+
424
+ describe('Navigation', () => {
425
+ it('should handle router navigation correctly', async () => {
426
+ const app = createApp({
427
+ setup() {
428
+ useProvideRouter(router);
429
+ return () => '<div>App</div>';
430
+ }
431
+ });
432
+
433
+ app.mount(testContainer);
434
+
435
+ // Initial route
436
+ expect(router.route.path).toBe('/');
437
+ expect(router.route.meta.title).toBe('Home');
438
+
439
+ // Navigate to different route
440
+ await router.push('/about');
441
+
442
+ // Route should be updated
443
+ expect(router.route.path).toBe('/about');
444
+ expect(router.route.meta.title).toBe('About');
445
+
446
+ app.unmount();
447
+ });
448
+ });
449
+
450
+ describe('Composition API Integration', () => {
451
+ it('should work with composition API functions in same component', async () => {
452
+ let compositionRouter: Router | null = null;
453
+ let compositionRoute: Route | null = null;
454
+ let linkResolver: ReturnType<typeof useLink> | null = null;
455
+
456
+ const TestComponent = defineComponent({
457
+ setup() {
458
+ useProvideRouter(router);
459
+
460
+ compositionRouter = useRouter();
461
+ compositionRoute = useRoute();
462
+ linkResolver = useLink({
463
+ to: '/about',
464
+ type: 'push',
465
+ exact: 'include'
466
+ });
467
+
468
+ return () => '<div>Test Component</div>';
469
+ }
470
+ });
471
+
472
+ const app = createApp(TestComponent);
473
+ app.mount(testContainer);
474
+ await nextTick();
475
+
476
+ // Test useRouter result
477
+ expect(compositionRouter).toBe(router);
478
+
479
+ // Test useRoute result
480
+ expect(compositionRoute).toBeTruthy();
481
+ expect(compositionRoute!.path).toBe('/');
482
+ expect(compositionRoute!.meta.title).toBe('Home');
483
+
484
+ // Test useLink result
485
+ expect(linkResolver).toBeTruthy();
486
+ expect(linkResolver!.value).toBeTruthy();
487
+
488
+ const link = linkResolver!.value;
489
+ expect(link).toHaveProperty('attributes');
490
+ expect(link).toHaveProperty('getEventHandlers');
491
+ expect(link).toHaveProperty('isActive');
492
+
493
+ app.unmount();
494
+ });
495
+
496
+ it('should handle route updates reactively', async () => {
497
+ let routeRef: Route | null = null;
498
+
499
+ const TestComponent = defineComponent({
500
+ setup() {
501
+ useProvideRouter(router);
502
+ const route = useRoute();
503
+ routeRef = route;
504
+ return () => `<div>Current: ${route.path}</div>`;
505
+ }
506
+ });
507
+
508
+ const app = createApp(TestComponent);
509
+ app.mount(testContainer);
510
+ await nextTick();
511
+
512
+ // Initial route
513
+ expect(routeRef).toBeTruthy();
514
+ expect(routeRef!.path).toBe('/');
515
+
516
+ // Navigate and check if route is updated
517
+ await router.push('/about');
518
+ await nextTick();
519
+
520
+ expect(routeRef!.path).toBe('/about');
521
+
522
+ app.unmount();
523
+ });
524
+ });
525
+
526
+ describe('Deep Component Hierarchy', () => {
527
+ it('should cover deep component hierarchy traversal (multi-level parent chain)', async () => {
528
+ // This test specifically targets lines 59-60: the while loop continuation in findRouterContext
529
+ // Create: GrandParent (has router) -> Parent (no router) -> Child (needs router)
530
+
531
+ let childRouterResult: Router | null = null;
532
+
533
+ const ChildComponent = defineComponent({
534
+ name: 'ChildComponent',
535
+ setup() {
536
+ // This will trigger the while loop in findRouterContext
537
+ // First check: parent (no context) -> continue loop (lines 59-60)
538
+ // Second check: grandparent (has context) -> found!
539
+ return () => h('div', 'Deep Child');
540
+ }
541
+ });
542
+
543
+ const ParentComponent = defineComponent({
544
+ name: 'ParentComponent',
545
+ setup() {
546
+ // This parent does NOT provide router context
547
+ // So child will need to traverse up to grandparent
548
+ return () => h(ChildComponent);
549
+ }
550
+ });
551
+
552
+ const GrandParentComponent = defineComponent({
553
+ name: 'GrandParentComponent',
554
+ setup() {
555
+ useProvideRouter(router);
556
+ return () => h(ParentComponent);
557
+ }
558
+ });
559
+
560
+ const app = createApp(GrandParentComponent);
561
+ const mountedApp = app.mount(testContainer);
562
+ await nextTick();
563
+
564
+ // Manually traverse the component tree to find the deep child
565
+ // This simulates what happens when getRouter is called on a deeply nested component
566
+ const deepChildInstance = mountedApp;
567
+
568
+ // Create a mock deep child instance with proper parent chain
569
+ const mockDeepChild = {
570
+ $parent: {
571
+ // This is the middle parent (no router context)
572
+ $parent: mountedApp // This is the grandparent (has router context)
573
+ }
574
+ };
575
+
576
+ // This call will traverse: Child -> Parent (no context) -> GrandParent (has context)
577
+ // This should hit lines 59-60 (current = current.$parent; })
578
+ childRouterResult = getRouter(mockDeepChild as VueInstance);
579
+
580
+ expect(childRouterResult).toBe(router);
581
+ expect(childRouterResult).toBeInstanceOf(Router);
582
+
583
+ app.unmount();
584
+ });
585
+
586
+ it('should handle component hierarchy traversal with manual parent chain setup', () => {
587
+ // Create a chain: Root (has router) -> Middle (no router) -> Leaf (needs router)
588
+ // This ensures the while loop executes multiple iterations
589
+
590
+ const app = createApp({
591
+ setup() {
592
+ useProvideRouter(router);
593
+ return () => h('div', 'Root');
594
+ }
595
+ });
596
+
597
+ const rootInstance = app.mount(testContainer);
598
+
599
+ // Create a mock component hierarchy that requires traversal
600
+ const leafInstance = {
601
+ $parent: {
602
+ // Middle level - no router context
603
+ $parent: rootInstance // Root level - has router context
604
+ }
605
+ };
606
+
607
+ // This should traverse the parent chain and find the router in the root
608
+ const foundRouter = getRouter(leafInstance as VueInstance);
609
+
610
+ expect(foundRouter).toBe(router);
611
+ expect(foundRouter).toBeInstanceOf(Router);
612
+
613
+ app.unmount();
614
+ });
615
+ });
616
+ });