UNPKG

@esmx/router-vue

Version:

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

575 lines (494 loc) 19.9 kB
import type { Route, RouteConfig } from '@esmx/router'; import { Router, RouterMode } from '@esmx/router'; /** * @vitest-environment happy-dom */ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { createApp, defineComponent, h, nextTick } from 'vue'; import { RouterPlugin } from './plugin'; import { RouterLink } from './router-link'; import { RouterView } from './router-view'; import { useProvideRouter } from './use'; describe('plugin.ts - RouterPlugin', () => { let router: Router; let app: ReturnType<typeof createApp>; let container: HTMLElement; beforeEach(async () => { // Create DOM container container = document.createElement('div'); container.id = 'test-app'; document.body.appendChild(container); // Create test routes with render functions const routes: RouteConfig[] = [ { path: '/', component: defineComponent({ name: 'Home', render: () => h('div', 'Home Page') }), meta: { title: 'Home' } }, { path: '/about', component: defineComponent({ name: 'About', render: () => h('div', 'About Page') }), meta: { title: 'About' } } ]; // Create and initialize router router = new Router({ mode: RouterMode.memory, routes, base: new URL('http://localhost:8000/') }); await router.replace('/'); await nextTick(); }); afterEach(() => { if (app) { app.unmount(); } if (router) { router.destroy(); } if (container?.parentNode) { container.parentNode.removeChild(container); } }); describe('Plugin Installation', () => { it('should install plugin without errors', () => { app = createApp({ setup() { useProvideRouter(router); return () => h('div', 'Test App'); } }); expect(() => { app.use(RouterPlugin); }).not.toThrow(); }); it('should throw error for invalid Vue app instance', () => { const invalidApp = {}; expect(() => { RouterPlugin.install(invalidApp); }).toThrow('[@esmx/router-vue] Invalid Vue app instance'); }); it('should throw error for null app instance', () => { expect(() => { RouterPlugin.install(null); }).toThrow('[@esmx/router-vue] Invalid Vue app instance'); }); it('should throw error for undefined app instance', () => { expect(() => { RouterPlugin.install(undefined); }).toThrow('[@esmx/router-vue] Invalid Vue app instance'); }); }); describe('Global Properties Injection', () => { it('should inject $router and $route properties', async () => { app = createApp({ setup() { useProvideRouter(router); return () => h('div', 'Test App'); } }); app.use(RouterPlugin); app.mount(container); await nextTick(); // Check that properties are defined using descriptors to avoid triggering getters const globalProperties = app.config.globalProperties; const routerDescriptor = Object.getOwnPropertyDescriptor( globalProperties, '$router' ); const routeDescriptor = Object.getOwnPropertyDescriptor( globalProperties, '$route' ); expect(routerDescriptor).toBeDefined(); expect(routeDescriptor).toBeDefined(); expect(routerDescriptor?.get).toBeDefined(); expect(routeDescriptor?.get).toBeDefined(); expect(typeof routerDescriptor?.get).toBe('function'); expect(typeof routeDescriptor?.get).toBe('function'); }); it('should provide reactive $route property', async () => { app = createApp({ setup() { useProvideRouter(router); return () => h('div', 'Test App'); } }); app.use(RouterPlugin); app.mount(container); await nextTick(); // Navigate to different route await router.push('/about'); await nextTick(); // Check that global properties are reactive (structure test) const globalProperties = app.config.globalProperties; const routeDescriptor = Object.getOwnPropertyDescriptor( globalProperties, '$route' ); expect(routeDescriptor?.get).toBeDefined(); expect(typeof routeDescriptor?.get).toBe('function'); // Verify the descriptor is properly configured for reactivity expect(routeDescriptor?.enumerable).toBe(false); expect(routeDescriptor?.configurable).toBe(false); }); it('should provide consistent $router instance across components', async () => { const ChildComponent = defineComponent({ render() { return h('div', 'Child'); } }); app = createApp({ setup() { useProvideRouter(router); return () => h('div', [h('div', 'Parent'), h(ChildComponent)]); } }); app.use(RouterPlugin); app.mount(container); await nextTick(); // Verify that global properties are consistently available const globalProperties = app.config.globalProperties; const routerDescriptor = Object.getOwnPropertyDescriptor( globalProperties, '$router' ); const routeDescriptor = Object.getOwnPropertyDescriptor( globalProperties, '$route' ); expect(routerDescriptor).toBeDefined(); expect(routeDescriptor).toBeDefined(); expect(routerDescriptor?.get).toBeDefined(); expect(typeof routerDescriptor?.get).toBe('function'); expect(routeDescriptor?.get).toBeDefined(); expect(typeof routeDescriptor?.get).toBe('function'); }); it('should actually call $router getter when accessed in component', async () => { let routerResult: Router | null = null; const TestComponent = defineComponent({ mounted() { // This will trigger the $router getter defined in the plugin routerResult = this.$router; }, render() { return h('div', 'Test Component'); } }); app = createApp({ setup() { useProvideRouter(router); return () => h(TestComponent); } }); app.use(RouterPlugin); app.mount(container); await nextTick(); // Verify the getter was called and returned correct value expect(routerResult).toEqual(router); expect(routerResult).toBeInstanceOf(Router); }); it('should actually call $route getter when accessed in component', async () => { let routeResult: Route | null = null; const TestComponent = defineComponent({ mounted() { routeResult = this.$route; }, render() { return h('div', 'Test Component'); } }); app = createApp({ setup() { useProvideRouter(router); return () => h(TestComponent); } }); app.use(RouterPlugin); app.mount(container); await nextTick(); // Navigate to ensure route state is set await router.push('/about'); await nextTick(); // Verify the getter was called and returned correct value expect(routeResult).toBeTruthy(); expect(routeResult).toHaveProperty('path', '/about'); expect(routeResult).toHaveProperty('meta.title', 'About'); }); }); describe('Component Registration', () => { it('should register components with correct names', () => { app = createApp({ setup() { useProvideRouter(router); return () => h('div', 'Test App'); } }); app.use(RouterPlugin); const globalComponents = app._context.components; expect(globalComponents).toHaveProperty('RouterLink'); expect(globalComponents).toHaveProperty('RouterView'); expect(globalComponents.RouterLink).toBe(RouterLink); expect(globalComponents.RouterView).toBe(RouterView); }); it('should register RouterLink component for global use', async () => { app = createApp({ setup() { useProvideRouter(router); return () => h('div', 'Test App with RouterLink available'); } }); app.use(RouterPlugin); app.mount(container); await nextTick(); // Verify the component is registered globally const globalComponents = app._context.components; expect(globalComponents.RouterLink).toBeDefined(); expect(typeof globalComponents.RouterLink).toBe('object'); }); it('should register RouterView component for global use', async () => { app = createApp({ setup() { useProvideRouter(router); return () => h('div', 'Test App with RouterView available'); } }); app.use(RouterPlugin); app.mount(container); await nextTick(); // Verify the component is registered globally const globalComponents = app._context.components; expect(globalComponents.RouterView).toBeDefined(); expect(typeof globalComponents.RouterView).toBe('object'); }); }); describe('Error Handling', () => { it('should handle missing router context in global properties', () => { // Create a mock component instance without router context const mockComponent = { $: { provides: {} } }; // Simulate accessing $router without context const target = {}; Object.defineProperties(target, { $router: { get() { // This simulates the getter function from the plugin return require('./use').getRouter(mockComponent); } } }); expect(() => { (target as Record<string, unknown>).$router; }).toThrow(); }); it('should handle missing router context in $route property', () => { // Create a mock component instance without router context const mockComponent = { $: { provides: {} } }; // Simulate accessing $route without context const target = {}; Object.defineProperties(target, { $route: { get() { // This simulates the getter function from the plugin return require('./use').getRoute(mockComponent); } } }); expect(() => { (target as Record<string, unknown>).$route; }).toThrow(); }); }); describe('Plugin Integration', () => { it('should work with multiple plugin installations', () => { app = createApp({ setup() { useProvideRouter(router); return () => h('div', 'Test App'); } }); // Install plugin multiple times app.use(RouterPlugin); app.use(RouterPlugin); expect(() => { app.mount(container); }).not.toThrow(); }); it('should maintain global properties after installation', async () => { app = createApp({ setup() { useProvideRouter(router); return () => h('div', 'Test App'); } }); app.use(RouterPlugin); app.mount(container); await nextTick(); // Check that global properties are accessible using descriptors const globalProperties = app.config.globalProperties; const routerDescriptor = Object.getOwnPropertyDescriptor( globalProperties, '$router' ); const routeDescriptor = Object.getOwnPropertyDescriptor( globalProperties, '$route' ); expect(routerDescriptor).toBeDefined(); expect(routeDescriptor).toBeDefined(); expect(routerDescriptor?.get).toBeDefined(); expect(routeDescriptor?.get).toBeDefined(); expect(typeof routerDescriptor?.get).toBe('function'); expect(typeof routeDescriptor?.get).toBe('function'); }); }); describe('Type Safety', () => { it('should provide properly typed global properties', async () => { app = createApp({ setup() { useProvideRouter(router); return () => h('div', 'Test App'); } }); app.use(RouterPlugin); app.mount(container); await nextTick(); // Check type safety through property descriptors const globalProperties = app.config.globalProperties; const routerDescriptor = Object.getOwnPropertyDescriptor( globalProperties, '$router' ); const routeDescriptor = Object.getOwnPropertyDescriptor( globalProperties, '$route' ); expect(routerDescriptor).toBeDefined(); expect(routeDescriptor).toBeDefined(); expect(typeof routerDescriptor?.get).toBe('function'); expect(typeof routeDescriptor?.get).toBe('function'); // Verify properties exist in global properties expect( Object.prototype.hasOwnProperty.call( globalProperties, '$router' ) ).toBe(true); expect( Object.prototype.hasOwnProperty.call(globalProperties, '$route') ).toBe(true); }); it('should provide correct component types', () => { app = createApp({ setup() { useProvideRouter(router); return () => h('div', 'Test App'); } }); app.use(RouterPlugin); const globalComponents = app._context.components; // Check component properties exist expect(globalComponents.RouterLink.name).toBe('RouterLink'); expect(globalComponents.RouterView.name).toBe('RouterView'); // Check if components have setup functions (safely) const routerLinkComponent = globalComponents.RouterLink as Record< string, unknown >; const routerViewComponent = globalComponents.RouterView as Record< string, unknown >; expect(typeof routerLinkComponent.setup).toBe('function'); expect(typeof routerViewComponent.setup).toBe('function'); }); }); describe('Advanced Plugin Features', () => { it('should support property descriptor configuration', () => { interface TestApp { config: { globalProperties: Record<string, unknown>; }; component: ( name: string, component: Record<string, unknown> ) => void; } const testApp: TestApp = { config: { globalProperties: {} }, component: ( name: string, component: Record<string, unknown> ) => { // Mock component registration } }; RouterPlugin.install(testApp); const routerDescriptor = Object.getOwnPropertyDescriptor( testApp.config.globalProperties, '$router' ); const routeDescriptor = Object.getOwnPropertyDescriptor( testApp.config.globalProperties, '$route' ); // Check descriptor properties - Object.defineProperties sets these to false by default expect(routerDescriptor?.get).toBeDefined(); expect(routerDescriptor?.enumerable).toBe(false); // Default value from Object.defineProperty expect(routerDescriptor?.configurable).toBe(false); // Default value from Object.defineProperty expect(routeDescriptor?.get).toBeDefined(); expect(routeDescriptor?.enumerable).toBe(false); // Default value from Object.defineProperty expect(routeDescriptor?.configurable).toBe(false); // Default value from Object.defineProperty }); it('should handle different app instance structures', () => { // Test with minimal app structure interface MinimalApp { config: { globalProperties: Record<string, unknown>; }; component: () => void; } const minimalApp: MinimalApp = { config: { globalProperties: {} }, component: () => {} }; expect(() => { RouterPlugin.install(minimalApp); }).not.toThrow(); // Verify property descriptors are properly set using descriptors const routerDescriptor = Object.getOwnPropertyDescriptor( minimalApp.config.globalProperties, '$router' ); const routeDescriptor = Object.getOwnPropertyDescriptor( minimalApp.config.globalProperties, '$route' ); expect(routerDescriptor).toBeDefined(); expect(routeDescriptor).toBeDefined(); expect(routerDescriptor?.get).toBeDefined(); expect(routeDescriptor?.get).toBeDefined(); expect(typeof routerDescriptor?.get).toBe('function'); expect(typeof routeDescriptor?.get).toBe('function'); }); }); });