@esmx/router-vue
Version:
Vue integration for @esmx/router - A universal router that works seamlessly with both Vue 2.7+ and Vue 3
480 lines (396 loc) • 14.9 kB
text/typescript
import { type Route, Router, RouterMode } from '@esmx/router';
/**
* @vitest-environment happy-dom
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { nextTick } from 'vue';
import { createApp, defineComponent, getCurrentInstance, h } from 'vue';
import { RouterView } from './router-view';
import {
getRouterViewDepth,
useProvideRouter,
useRoute,
useRouter,
useRouterViewDepth
} from './use';
describe('Router Vue Integration', () => {
let app: ReturnType<typeof createApp>;
let router: Router;
let mountPoint: HTMLElement;
beforeEach(async () => {
// Create a real Router instance
router = new Router({
mode: RouterMode.memory,
routes: [
{ path: '/initial', component: {} },
{ path: '/new-route', component: {} },
{ path: '/user/:id', component: {} },
{ path: '/new-path', component: {} }
],
base: new URL('http://localhost:8000/')
});
// Ensure navigation to initial route is complete
await router.replace('/initial');
// Create mount point
mountPoint = document.createElement('div');
mountPoint.id = 'app';
document.body.appendChild(mountPoint);
});
afterEach(() => {
if (app) {
app.unmount();
}
document.body.removeChild(mountPoint);
// Clean up router
router.destroy();
});
describe('Router and Route Access', () => {
it('should provide router and route access', async () => {
let routerResult: Router | undefined;
let routeResult: Route | undefined;
const TestApp = {
setup() {
useProvideRouter(router);
routerResult = useRouter();
routeResult = useRoute();
return () => h('div', 'Test App');
}
};
app = createApp(TestApp);
app.mount('#app');
// Check retrieved objects
expect(routerResult).toEqual(router);
expect(routeResult).toBeDefined();
expect(routeResult?.path).toBe('/initial');
});
});
describe('Route Reactivity', () => {
it('should update route properties when route changes', async () => {
let routeRef: Route | undefined;
const TestApp = {
setup() {
useProvideRouter(router);
routeRef = useRoute();
return () => h('div', routeRef?.path);
}
};
app = createApp(TestApp);
app.mount('#app');
// Initial state
expect(routeRef?.path).toBe('/initial');
// Save reference to check identity
const initialRouteRef = routeRef;
// Navigate to new route
await router.replace('/new-route');
await nextTick();
// Check that reference is preserved but properties are updated
expect(routeRef).toBe(initialRouteRef);
expect(routeRef?.path).toBe('/new-route');
});
it('should update route params when route changes', async () => {
let routeRef: Route | undefined;
const TestApp = {
setup() {
useProvideRouter(router);
routeRef = useRoute();
return () =>
h('div', [
h('span', routeRef?.path),
h('span', routeRef?.params?.id || 'no-id')
]);
}
};
app = createApp(TestApp);
app.mount('#app');
// Navigate to route with params
await router.replace('/user/123');
await nextTick();
// Check if params are updated
expect(routeRef?.path).toBe('/user/123');
expect(routeRef?.params?.id).toBe('123');
});
it('should automatically update view when route changes', async () => {
// Track render count
const renderCount = { value: 0 };
let routeRef: Route | undefined;
const TestApp = {
setup() {
useProvideRouter(router);
routeRef = useRoute();
return () => {
renderCount.value++;
return h('div', routeRef?.path);
};
}
};
app = createApp(TestApp);
app.mount('#app');
// Initial render
const initialRenderCount = renderCount.value;
expect(routeRef?.path).toBe('/initial');
// Navigate to new route
await router.replace('/new-route');
await nextTick();
// Check if render count increased, confirming view update
expect(renderCount.value).toBeGreaterThan(initialRenderCount);
expect(routeRef?.path).toBe('/new-route');
// Navigate to another route
const previousRenderCount = renderCount.value;
await router.replace('/new-path');
await nextTick();
// Check if render count increased again
expect(renderCount.value).toBeGreaterThan(previousRenderCount);
expect(routeRef?.path).toBe('/new-path');
});
});
describe('Nested Components', () => {
it('should provide route context to child components', async () => {
let parentRoute: Route | undefined;
let childRoute: Route | undefined;
const ChildComponent = {
setup() {
childRoute = useRoute();
return () => h('div', 'Child: ' + childRoute?.path);
}
};
const ParentComponent = {
setup() {
parentRoute = useRoute();
return () =>
h('div', [
h('span', 'Parent: ' + parentRoute?.path),
h(ChildComponent)
]);
}
};
const TestApp = {
setup() {
useProvideRouter(router);
return () => h(ParentComponent);
}
};
app = createApp(TestApp);
app.mount('#app');
expect(parentRoute).toBeDefined();
expect(childRoute).toBeDefined();
expect(parentRoute?.path).toBe('/initial');
expect(childRoute?.path).toBe('/initial');
// Navigate to new path
await router.replace('/new-path');
await nextTick();
// Both parent and child components should see updates
expect(parentRoute?.path).toBe('/new-path');
expect(childRoute?.path).toBe('/new-path');
});
});
describe('RouterView Depth', () => {
it('should get depth in single RouterView', async () => {
let observedDepth: number | undefined;
const LeafProbe = defineComponent({
setup() {
const p = getCurrentInstance()!.proxy as any;
observedDepth = getRouterViewDepth(p);
return () => h('div');
}
});
const Level1 = defineComponent({
setup() {
return () => h('div', [h(LeafProbe)]);
}
});
router = new Router({
mode: RouterMode.memory,
routes: [{ path: '/level1', component: Level1 }],
base: new URL('http://localhost:8000/')
});
await router.replace('/level1');
const TestApp = defineComponent({
setup() {
useProvideRouter(router);
return () => h('div', [h(RouterView)]);
}
});
app = createApp(TestApp);
app.mount('#app');
await nextTick();
expect(observedDepth).toBe(1);
});
it('should get depth in nested RouterView', async () => {
let observedDepth: number | undefined;
const LeafProbe = defineComponent({
setup() {
const p = getCurrentInstance()!.proxy as any;
observedDepth = getRouterViewDepth(p);
return () => h('div');
}
});
const Level1 = defineComponent({
setup() {
return () => h('div', [h(RouterView)]);
}
});
const Leaf = defineComponent({
setup() {
return () => h('div', [h(LeafProbe)]);
}
});
router = new Router({
mode: RouterMode.memory,
routes: [
{
path: '/level1',
component: Level1,
children: [{ path: 'leaf', component: Leaf }]
}
],
base: new URL('http://localhost:8000/')
});
await router.replace('/level1/leaf');
const TestApp = defineComponent({
setup() {
useProvideRouter(router);
return () => h('div', [h(RouterView)]);
}
});
app = createApp(TestApp);
app.mount('#app');
await nextTick();
expect(observedDepth).toBe(2);
});
it('should get depth in double-nested RouterViews', async () => {
let observedDepth: number | undefined;
const LeafProbe = defineComponent({
setup() {
const p = getCurrentInstance()!.proxy as any;
observedDepth = getRouterViewDepth(p);
return () => h('div');
}
});
const Level1 = defineComponent({
setup() {
return () => h('div', [h(RouterView)]);
}
});
const Level2 = defineComponent({
setup() {
return () => h('div', [h(RouterView)]);
}
});
const Leaf = defineComponent({
setup() {
return () => h('div', [h(LeafProbe)]);
}
});
router = new Router({
mode: RouterMode.memory,
routes: [
{
path: '/level1',
component: Level1,
children: [
{
path: 'level2',
component: Level2,
children: [{ path: 'leaf', component: Leaf }]
}
]
}
],
base: new URL('http://localhost:8000/')
});
await router.replace('/level1/level2/leaf');
const TestApp = defineComponent({
setup() {
useProvideRouter(router);
return () => h('div', [h(RouterView)]);
}
});
app = createApp(TestApp);
app.mount('#app');
await nextTick();
expect(observedDepth).toBe(3);
});
it('should throw when no RouterView ancestor exists', async () => {
let callDepth: (() => void) | undefined;
const Probe = defineComponent({
setup() {
const p = getCurrentInstance()!.proxy as any;
callDepth = () => getRouterViewDepth(p);
return () => h('div');
}
});
const TestApp = defineComponent({
setup() {
useProvideRouter(router);
return () => h(Probe);
}
});
app = createApp(TestApp);
app.mount('#app');
await nextTick();
expect(() => callDepth!()).toThrow(
new Error(
'[@esmx/router-vue] RouterView depth not found. Please ensure a RouterView exists in ancestor components.'
)
);
});
it('should return 0 for useRouterViewDepth without RouterView', async () => {
let observed = -1;
const Probe = defineComponent({
setup() {
observed = useRouterViewDepth();
return () => h('div');
}
});
const TestApp = defineComponent({
setup() {
useProvideRouter(router);
return () => h(Probe);
}
});
app = createApp(TestApp);
app.mount('#app');
await nextTick();
expect(observed).toBe(0);
});
it('should reflect depth via useRouterViewDepth at each level', async () => {
let level1Depth = -1;
let level2Depth = -1;
const Level2 = defineComponent({
setup() {
level2Depth = useRouterViewDepth();
return () => h('div');
}
});
const Level1 = defineComponent({
setup() {
level1Depth = useRouterViewDepth();
return () => h('div', [h(RouterView)]);
}
});
router = new Router({
mode: RouterMode.memory,
routes: [
{
path: '/level1',
component: Level1,
children: [{ path: 'level2', component: Level2 }]
}
],
base: new URL('http://localhost:8000/')
});
await router.replace('/level1/level2');
const TestApp = defineComponent({
setup() {
useProvideRouter(router);
return () => h('div', [h(RouterView)]);
}
});
app = createApp(TestApp);
app.mount('#app');
await nextTick();
expect(level1Depth).toBe(1);
expect(level2Depth).toBe(2);
});
});
});