@esmx/router-vue 3.0.0-rc.29 → 3.0.0-rc.31

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/src/use.test.ts CHANGED
@@ -1,616 +1,217 @@
1
- import { type Route, type RouteConfig, Router, RouterMode } from '@esmx/router';
1
+ import { type Route, Router, RouterMode } from '@esmx/router';
2
2
  /**
3
3
  * @vitest-environment happy-dom
4
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', () => {
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
6
+ import { nextTick } from 'vue';
7
+ import { createApp, h } from 'vue';
8
+ import { useProvideRouter, useRoute, useRouter } from './use';
9
+
10
+ describe('Router Vue Integration', () => {
11
+ let app: ReturnType<typeof createApp>;
25
12
  let router: Router;
26
- let testContainer: HTMLElement;
13
+ let mountPoint: HTMLElement;
27
14
 
28
15
  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
16
+ // Create a real Router instance
49
17
  router = new Router({
50
- root: '#test-app',
51
- routes,
52
18
  mode: RouterMode.memory,
19
+ routes: [
20
+ { path: '/initial', component: {} },
21
+ { path: '/new-route', component: {} },
22
+ { path: '/user/:id', component: {} },
23
+ { path: '/new-path', component: {} }
24
+ ],
53
25
  base: new URL('http://localhost:3000/')
54
26
  });
55
27
 
56
- // Initialize router to root path
57
- await router.replace('/');
28
+ // Ensure navigation to initial route is complete
29
+ await router.replace('/initial');
30
+
31
+ // Create mount point
32
+ mountPoint = document.createElement('div');
33
+ mountPoint.id = 'app';
34
+ document.body.appendChild(mountPoint);
58
35
  });
59
36
 
60
37
  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();
38
+ if (app) {
39
+ app.unmount();
69
40
  }
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
- });
41
+ document.body.removeChild(mountPoint);
148
42
 
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
- });
43
+ // Clean up router
44
+ router.destroy();
187
45
  });
188
46
 
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);
47
+ describe('Router and Route Access', () => {
48
+ it('should provide router and route access', async () => {
49
+ let routerResult: Router | undefined;
50
+ let routeResult: Route | undefined;
205
51
 
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({
52
+ const TestApp = {
213
53
  setup() {
214
54
  useProvideRouter(router);
215
- return () => '<div>App</div>';
55
+ routerResult = useRouter();
56
+ routeResult = useRoute();
57
+ return () => h('div', 'Test App');
216
58
  }
217
- });
218
-
219
- const vm = app.mount(testContainer);
220
- currentRoute = getRoute(vm);
59
+ };
221
60
 
222
- expect(currentRoute).toBeTruthy();
223
- expect(currentRoute!.path).toBe('/');
224
- expect(currentRoute!.meta.title).toBe('Home');
61
+ app = createApp(TestApp);
62
+ app.mount('#app');
225
63
 
226
- app.unmount();
64
+ // Check retrieved objects
65
+ expect(routerResult).toEqual(router);
66
+ expect(routeResult).toBeDefined();
67
+ expect(routeResult?.path).toBe('/initial');
227
68
  });
228
69
  });
229
70
 
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
- });
71
+ describe('Route Reactivity', () => {
72
+ it('should update route properties when route changes', async () => {
73
+ let routeRef: Route | undefined;
249
74
 
250
- // Parent component that provides router
251
- const ParentComponent = defineComponent({
252
- name: 'ParentComponent',
253
- components: { ChildComponent },
75
+ const TestApp = {
254
76
  setup() {
255
77
  useProvideRouter(router);
256
- return () => h(ChildComponent);
78
+ routeRef = useRoute();
79
+ return () => h('div', routeRef?.path);
257
80
  }
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;
81
+ };
274
82
 
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
- });
83
+ app = createApp(TestApp);
84
+ app.mount('#app');
284
85
 
285
- const MiddleComponent = defineComponent({
286
- name: 'MiddleComponent',
287
- components: { DeepChildComponent },
288
- setup() {
289
- return () => h(DeepChildComponent);
290
- }
291
- });
86
+ // Initial state
87
+ expect(routeRef?.path).toBe('/initial');
292
88
 
293
- const TopComponent = defineComponent({
294
- name: 'TopComponent',
295
- components: { MiddleComponent },
296
- setup() {
297
- useProvideRouter(router);
298
- return () => h(MiddleComponent);
299
- }
300
- });
89
+ // Save reference to check identity
90
+ const initialRouteRef = routeRef;
301
91
 
302
- const app = createApp(TopComponent);
303
- app.mount(testContainer);
92
+ // Navigate to new route
93
+ await router.replace('/new-route');
304
94
  await nextTick();
305
95
 
306
- expect(deepChildRouter).toBe(router);
307
- app.unmount();
96
+ // Check that reference is preserved but properties are updated
97
+ expect(routeRef).toBe(initialRouteRef);
98
+ expect(routeRef?.path).toBe('/new-route');
308
99
  });
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
100
 
378
- app.unmount();
379
- });
101
+ it('should update route params when route changes', async () => {
102
+ let routeRef: Route | undefined;
380
103
 
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({
104
+ const TestApp = {
427
105
  setup() {
428
106
  useProvideRouter(router);
429
- return () => '<div>App</div>';
107
+ routeRef = useRoute();
108
+ return () =>
109
+ h('div', [
110
+ h('span', routeRef?.path),
111
+ h('span', routeRef?.params?.id || 'no-id')
112
+ ]);
430
113
  }
431
- });
432
-
433
- app.mount(testContainer);
434
-
435
- // Initial route
436
- expect(router.route.path).toBe('/');
437
- expect(router.route.meta.title).toBe('Home');
114
+ };
438
115
 
439
- // Navigate to different route
440
- await router.push('/about');
116
+ app = createApp(TestApp);
117
+ app.mount('#app');
441
118
 
442
- // Route should be updated
443
- expect(router.route.path).toBe('/about');
444
- expect(router.route.meta.title).toBe('About');
119
+ // Navigate to route with params
120
+ await router.replace('/user/123');
121
+ await nextTick();
445
122
 
446
- app.unmount();
123
+ // Check if params are updated
124
+ expect(routeRef?.path).toBe('/user/123');
125
+ expect(routeRef?.params?.id).toBe('123');
447
126
  });
448
- });
449
127
 
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;
128
+ it('should automatically update view when route changes', async () => {
129
+ // Track render count
130
+ const renderCount = { value: 0 };
131
+ let routeRef: Route | undefined;
455
132
 
456
- const TestComponent = defineComponent({
133
+ const TestApp = {
457
134
  setup() {
458
135
  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>';
136
+ routeRef = useRoute();
137
+ return () => {
138
+ renderCount.value++;
139
+ return h('div', routeRef?.path);
140
+ };
469
141
  }
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
- });
142
+ };
495
143
 
496
- it('should handle route updates reactively', async () => {
497
- let routeRef: Route | null = null;
144
+ app = createApp(TestApp);
145
+ app.mount('#app');
498
146
 
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
- });
147
+ // Initial render
148
+ const initialRenderCount = renderCount.value;
149
+ expect(routeRef?.path).toBe('/initial');
507
150
 
508
- const app = createApp(TestComponent);
509
- app.mount(testContainer);
151
+ // Navigate to new route
152
+ await router.replace('/new-route');
510
153
  await nextTick();
511
154
 
512
- // Initial route
513
- expect(routeRef).toBeTruthy();
514
- expect(routeRef!.path).toBe('/');
155
+ // Check if render count increased, confirming view update
156
+ expect(renderCount.value).toBeGreaterThan(initialRenderCount);
157
+ expect(routeRef?.path).toBe('/new-route');
515
158
 
516
- // Navigate and check if route is updated
517
- await router.push('/about');
159
+ // Navigate to another route
160
+ const previousRenderCount = renderCount.value;
161
+ await router.replace('/new-path');
518
162
  await nextTick();
519
163
 
520
- expect(routeRef!.path).toBe('/about');
521
-
522
- app.unmount();
164
+ // Check if render count increased again
165
+ expect(renderCount.value).toBeGreaterThan(previousRenderCount);
166
+ expect(routeRef?.path).toBe('/new-path');
523
167
  });
524
168
  });
525
169
 
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;
170
+ describe('Nested Components', () => {
171
+ it('should provide route context to child components', async () => {
172
+ let parentRoute: Route | undefined;
173
+ let childRoute: Route | undefined;
532
174
 
533
- const ChildComponent = defineComponent({
534
- name: 'ChildComponent',
175
+ const ChildComponent = {
535
176
  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');
177
+ childRoute = useRoute();
178
+ return () => h('div', 'Child: ' + childRoute?.path);
540
179
  }
541
- });
180
+ };
542
181
 
543
- const ParentComponent = defineComponent({
544
- name: 'ParentComponent',
182
+ const ParentComponent = {
545
183
  setup() {
546
- // This parent does NOT provide router context
547
- // So child will need to traverse up to grandparent
548
- return () => h(ChildComponent);
184
+ parentRoute = useRoute();
185
+ return () =>
186
+ h('div', [
187
+ h('span', 'Parent: ' + parentRoute?.path),
188
+ h(ChildComponent)
189
+ ]);
549
190
  }
550
- });
191
+ };
551
192
 
552
- const GrandParentComponent = defineComponent({
553
- name: 'GrandParentComponent',
193
+ const TestApp = {
554
194
  setup() {
555
195
  useProvideRouter(router);
556
196
  return () => h(ParentComponent);
557
197
  }
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
198
  };
575
199
 
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);
200
+ app = createApp(TestApp);
201
+ app.mount('#app');
579
202
 
580
- expect(childRouterResult).toBe(router);
581
- expect(childRouterResult).toBeInstanceOf(Router);
203
+ expect(parentRoute).toBeDefined();
204
+ expect(childRoute).toBeDefined();
205
+ expect(parentRoute?.path).toBe('/initial');
206
+ expect(childRoute?.path).toBe('/initial');
582
207
 
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);
208
+ // Navigate to new path
209
+ await router.replace('/new-path');
210
+ await nextTick();
612
211
 
613
- app.unmount();
212
+ // Both parent and child components should see updates
213
+ expect(parentRoute?.path).toBe('/new-path');
214
+ expect(childRoute?.path).toBe('/new-path');
614
215
  });
615
216
  });
616
217
  });