UNPKG

@esmx/router-vue

Version:

Vue integration for @esmx/router - A universal router that works seamlessly with both Vue 2.7+ and Vue 3

600 lines (492 loc) 19.8 kB
import { type RouteConfig, Router, RouterMode } from '@esmx/router'; /** * @vitest-environment happy-dom */ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { createApp, defineComponent, h, inject, nextTick, provide } from 'vue'; import { RouterView } from './router-view'; import { useProvideRouter } from './use'; // Mock components for testing const HomeComponent = defineComponent({ name: 'HomeComponent', setup() { return () => h('div', { class: 'home' }, 'Home Page'); } }); const AboutComponent = defineComponent({ name: 'AboutComponent', setup() { return () => h('div', { class: 'about' }, 'About Page'); } }); const UserComponent = defineComponent({ name: 'UserComponent', setup() { return () => h('div', { class: 'user' }, 'User Component'); } }); // ES Module component for testing resolveComponent const ESModuleComponent = { __esModule: true, default: defineComponent({ name: 'ESModuleComponent', setup() { return () => h('div', { class: 'es-module' }, 'ES Module Component'); } }) }; describe('router-view.ts - RouterView Component', () => { let router: Router; let testContainer: HTMLElement; beforeEach(async () => { // Create test container testContainer = document.createElement('div'); testContainer.id = 'test-app'; document.body.appendChild(testContainer); // Create test routes const routes: RouteConfig[] = [ { path: '/', component: HomeComponent, meta: { title: 'Home' } }, { path: '/about', component: AboutComponent, meta: { title: 'About' } }, { path: '/users/:id', component: UserComponent, meta: { title: 'User' } }, { path: '/es-module', component: ESModuleComponent, meta: { title: 'ES Module' } } ]; // Create router instance router = new Router({ root: '#test-app', routes, mode: RouterMode.memory, base: new URL('http://localhost:8000/') }); // Initialize router to root path and wait for it to be ready await router.replace('/'); // Wait for route to be fully initialized await new Promise((resolve) => setTimeout(resolve, 10)); }); afterEach(() => { // Clean up test environment if (testContainer.parentNode) { testContainer.parentNode.removeChild(testContainer); } // Destroy router if (router) { router.destroy(); } }); describe('Basic Functionality', () => { it('should render matched route component at depth 0', async () => { const TestApp = defineComponent({ setup() { useProvideRouter(router); return () => h('div', [h(RouterView)]); } }); const app = createApp(TestApp); app.mount(testContainer); await nextTick(); // Check if HomeComponent is rendered expect(testContainer.textContent).toContain('Home Page'); app.unmount(); }); it('should render different components when route changes', async () => { const TestApp = defineComponent({ setup() { useProvideRouter(router); return () => h('div', [h(RouterView)]); } }); const app = createApp(TestApp); app.mount(testContainer); await nextTick(); // Initially should show Home expect(testContainer.textContent).toContain('Home Page'); // Navigate to About await router.push('/about'); await nextTick(); expect(testContainer.textContent).toContain('About Page'); app.unmount(); }); it('should handle routes with parameters', async () => { const TestApp = defineComponent({ setup() { useProvideRouter(router); return () => h('div', [h(RouterView)]); } }); const app = createApp(TestApp); app.mount(testContainer); // Navigate to user route with parameter await router.push('/users/123'); await nextTick(); expect(testContainer.textContent).toContain('User Component'); app.unmount(); }); }); describe('Component Resolution', () => { it('should resolve ES module components correctly', async () => { const TestApp = defineComponent({ setup() { useProvideRouter(router); return () => h('div', [h(RouterView)]); } }); const app = createApp(TestApp); app.mount(testContainer); // Navigate to ES module route await router.push('/es-module'); await nextTick(); expect(testContainer.textContent).toContain('ES Module Component'); app.unmount(); }); it('should handle function components', async () => { const FunctionComponent = () => h('div', 'Function Component'); const routes: RouteConfig[] = [ { path: '/function', component: FunctionComponent, meta: { title: 'Function' } } ]; const functionRouter = new Router({ root: '#test-app', routes, mode: RouterMode.memory, base: new URL('http://localhost:8000/') }); // Initialize the router and wait for it to be ready await functionRouter.replace('/function'); await new Promise((resolve) => setTimeout(resolve, 10)); const TestApp = defineComponent({ setup() { useProvideRouter(functionRouter); return () => h('div', [h(RouterView)]); } }); const app = createApp(TestApp); app.mount(testContainer); await nextTick(); expect(testContainer.textContent).toContain('Function Component'); app.unmount(); functionRouter.destroy(); }); }); describe('Depth Tracking', () => { it('should inject depth 0 by default', async () => { let injectedDepth: number | undefined; // Use the same symbol key that RouterView uses internally const RouterViewDepth = Symbol('RouterViewDepth'); // Create a custom RouterView component that can capture the injected depth const TestRouterView = defineComponent({ name: 'TestRouterView', setup() { injectedDepth = inject(RouterViewDepth, -1); return () => h(RouterView); } }); const TestApp = defineComponent({ setup() { useProvideRouter(router); return () => h('div', [h(TestRouterView)]); } }); const app = createApp(TestApp); app.mount(testContainer); await nextTick(); // TestRouterView should inject the default depth 0 when no parent RouterView exists expect(injectedDepth).toBe(-1); // Default value since no parent RouterView provides depth app.unmount(); }); it('should provide correct depth in nested RouterViews', async () => { let parentDepth: number | undefined; let childDepth: number | undefined; const RouterViewDepth = Symbol('RouterViewDepth'); const ParentTestComponent = defineComponent({ name: 'ParentTestComponent', setup() { parentDepth = inject(RouterViewDepth, -1); provide(RouterViewDepth, 0); // Simulate parent RouterView return () => h('div', [h('span', 'Parent'), h(ChildTestComponent)]); } }); const ChildTestComponent = defineComponent({ name: 'ChildTestComponent', setup() { childDepth = inject(RouterViewDepth, -1); return () => h('div', 'Child'); } }); const TestApp = defineComponent({ setup() { useProvideRouter(router); return () => h('div', [h(ParentTestComponent)]); } }); const app = createApp(TestApp); app.mount(testContainer); await nextTick(); // Parent should see default depth, child should see provided depth expect(parentDepth).toBe(-1); // Default value since no RouterView above expect(childDepth).toBe(0); // Value provided by parent app.unmount(); }); it('should handle nested RouterViews with correct depth', async () => { const Level1Component = defineComponent({ name: 'Level1Component', setup() { return () => h('div', [h('span', 'Level 1'), h(RouterView)]); } }); const Level2Component = defineComponent({ name: 'Level2Component', setup() { return () => h('div', 'Level 2'); } }); const nestedRoutes: RouteConfig[] = [ { path: '/level1', component: Level1Component, children: [ { path: 'level2', component: Level2Component } ] } ]; const nestedRouter = new Router({ root: '#test-app', routes: nestedRoutes, mode: RouterMode.memory, base: new URL('http://localhost:8000/') }); // Initialize the router and wait for it to be ready await nestedRouter.replace('/level1/level2'); await new Promise((resolve) => setTimeout(resolve, 10)); const TestApp = defineComponent({ setup() { useProvideRouter(nestedRouter); return () => h('div', [h(RouterView)]); } }); const app = createApp(TestApp); app.mount(testContainer); await nextTick(); expect(testContainer.textContent).toContain('Level 1'); expect(testContainer.textContent).toContain('Level 2'); app.unmount(); nestedRouter.destroy(); }); }); describe('Edge Cases and Error Handling', () => { it('should render null when no route matches at current depth', async () => { const RouterViewDepth = Symbol('RouterViewDepth'); const DeepRouterView = defineComponent({ name: 'DeepRouterView', setup() { // Inject depth 0 from parent RouterView and provide depth 1 const currentDepth = inject(RouterViewDepth, 0); provide(RouterViewDepth, currentDepth + 1); return () => h(RouterView); } }); const TestApp = defineComponent({ setup() { useProvideRouter(router); return () => h('div', [ h('span', 'App'), h(RouterView), // This renders Home component at depth 0 h(DeepRouterView) // This tries to render at depth 1, but no match ]); } }); const app = createApp(TestApp); app.mount(testContainer); await nextTick(); // Should contain "App" and "Home Page" from the first RouterView // but no additional content from the deep RouterView expect(testContainer.textContent).toContain('App'); expect(testContainer.textContent).toContain('Home Page'); app.unmount(); }); it('should handle null components gracefully', async () => { const routesWithNull: RouteConfig[] = [ { path: '/null-component', component: null as RouteConfig['component'], meta: { title: 'Null Component' } } ]; const nullRouter = new Router({ root: '#test-app', routes: routesWithNull, mode: RouterMode.memory, base: new URL('http://localhost:8000/') }); // Initialize the router and wait for it to be ready await nullRouter.replace('/null-component'); await new Promise((resolve) => setTimeout(resolve, 10)); const TestApp = defineComponent({ setup() { useProvideRouter(nullRouter); return () => h('div', [h('span', 'App'), h(RouterView)]); } }); const app = createApp(TestApp); app.mount(testContainer); await nextTick(); // Verify that only the "App" text is rendered and RouterView renders nothing expect(testContainer.textContent?.trim()).toBe('App'); expect(testContainer.querySelector('div')?.children.length).toBe(1); // Only the span element expect(testContainer.querySelector('span')?.textContent).toBe( 'App' ); app.unmount(); nullRouter.destroy(); }); it('should handle non-existent routes', async () => { // Create a new router instance with a valid initial route const nonExistentRouter = new Router({ root: '#test-app', routes: [ { path: '/', component: null // Initial route with null component } ], mode: RouterMode.memory, base: new URL('http://localhost:8000/') }); // Initialize router with root path await nonExistentRouter.replace('/'); await new Promise((resolve) => setTimeout(resolve, 10)); const TestApp = defineComponent({ setup() { useProvideRouter(nonExistentRouter); return () => h('div', [h('span', 'App'), h(RouterView)]); } }); const app = createApp(TestApp); app.mount(testContainer); // Navigate to non-existent route await nonExistentRouter.push('/non-existent'); await nextTick(); // Wait for any pending route changes await new Promise((resolve) => setTimeout(resolve, 10)); // Verify that only the "App" text is rendered and RouterView renders nothing expect(testContainer.textContent?.trim()).toBe('App'); expect(testContainer.querySelector('div')?.children.length).toBe(1); // Only the span element expect(testContainer.querySelector('span')?.textContent).toBe( 'App' ); app.unmount(); nonExistentRouter.destroy(); }); it('should handle malformed ES modules', async () => { const MalformedModule = { __esModule: true, default: null }; const malformedRoutes: RouteConfig[] = [ { path: '/malformed', component: MalformedModule as RouteConfig['component'], meta: { title: 'Malformed' } } ]; const malformedRouter = new Router({ root: '#test-app', routes: malformedRoutes, mode: RouterMode.memory, base: new URL('http://localhost:8000/') }); // Initialize the router and wait for it to be ready await malformedRouter.replace('/malformed'); await new Promise((resolve) => setTimeout(resolve, 10)); const TestApp = defineComponent({ setup() { useProvideRouter(malformedRouter); return () => h('div', [h('span', 'App'), h(RouterView)]); } }); const app = createApp(TestApp); app.mount(testContainer); await nextTick(); expect(testContainer.textContent).toBe('App'); app.unmount(); malformedRouter.destroy(); }); }); describe('Component Properties', () => { it('should have correct component name', () => { expect(RouterView.name).toBe('RouterView'); }); it('should be a valid Vue component', () => { expect(RouterView).toHaveProperty('setup'); expect(typeof RouterView.setup).toBe('function'); }); it('should not define props', () => { expect(RouterView.props).toBeUndefined(); }); }); describe('Integration Tests', () => { it('should re-render when route changes', async () => { const TestApp = defineComponent({ setup() { useProvideRouter(router); return () => h('div', [h(RouterView)]); } }); const app = createApp(TestApp); app.mount(testContainer); await nextTick(); expect(testContainer.textContent).toContain('Home Page'); await router.push('/about'); await nextTick(); expect(testContainer.textContent).toContain('About Page'); await router.push('/users/123'); await nextTick(); expect(testContainer.textContent).toContain('User Component'); app.unmount(); }); it('should work with router navigation methods', async () => { const TestApp = defineComponent({ setup() { useProvideRouter(router); return () => h('div', [h(RouterView)]); } }); const app = createApp(TestApp); app.mount(testContainer); await router.push('/about'); await nextTick(); expect(testContainer.textContent).toContain('About Page'); await router.replace('/users/456'); await nextTick(); expect(testContainer.textContent).toContain('User Component'); await router.back(); await nextTick(); expect(testContainer.textContent).toContain('Home Page'); app.unmount(); }); }); });