Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-vue3-bridge-hash-route.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@module-federation/bridge-vue3': patch
---

fix(bridge-vue3): pass hashRoute and memoryRoute to RemoteApp, fix path and redirect prefixing in addBasenameToNestedRoutes
378 changes: 378 additions & 0 deletions packages/bridge/vue3-bridge/__tests__/routeUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -498,4 +498,382 @@ describe('routeUtils', () => {
expect(() => processRoutes({ router: doubleSlashRouter })).not.toThrow();
});
});

describe('processRoutes with hashRoute', () => {
it('should create a hash history when hashRoute is true', () => {
const routes = createNestedRoutes();
const router = createRouter({
history: createWebHistory(),
routes,
});

const result = processRoutes({
router,
hashRoute: true,
});

expect(result.history).toBeDefined();
// Hash history base includes the '#' prefix
expect(result.history.base).toBe('/#');
});

it('should prefix routes with basename when hashRoute is true and basename is provided', () => {
const routes = createNestedRoutes();
const router = createRouter({
history: createWebHistory(),
routes,
});

const result = processRoutes({
router,
basename: '/app',
hashRoute: true,
});

expect(result.history).toBeDefined();
expect(result.routes.length).toBeGreaterThan(0);

// Top-level routes should be prefixed with basename
// Home route path '/' + basename '/app' normalizes to '/app'
const homeRoute = result.routes.find((r) => r.name === 'Home');
expect(homeRoute?.path).toBe('/app');

const dashboardRoute = result.routes.find((r) => r.name === 'Dashboard');
// Paths are joined with '/' and normalized (no double slashes)
expect(dashboardRoute?.path).toBe('/app/dashboard');
});

it('should prefix nested children with basename when hashRoute is true', () => {
const routes = createNestedRoutes();
const router = createRouter({
history: createWebHistory(),
routes,
});

const result = processRoutes({
router,
basename: '/app',
hashRoute: true,
});

const dashboardRoute = result.routes.find((r) => r.name === 'Dashboard');
expect(dashboardRoute).toBeDefined();

if (dashboardRoute?.children) {
const profileRoute = dashboardRoute.children.find(
(child) => child.name === 'Profile',
);
// Paths are now properly joined with '/' separator
expect(profileRoute?.path).toBe('/app/profile');

const settingsRoute = dashboardRoute.children.find(
(child) => child.name === 'Settings',
);
expect(settingsRoute?.path).toBe('/app/settings');

if (settingsRoute?.children) {
const accountRoute = settingsRoute.children.find(
(child) => child.name === 'Account',
);
expect(accountRoute?.path).toBe('/app/account');
}
}
});

it('should not prefix routes when hashRoute is true but no basename', () => {
const routes = createNestedRoutes();
const router = createRouter({
history: createWebHistory(),
routes,
});

const result = processRoutes({
router,
hashRoute: true,
});

const homeRoute = result.routes.find((r) => r.name === 'Home');
expect(homeRoute?.path).toBe('/');

const dashboardRoute = result.routes.find((r) => r.name === 'Dashboard');
expect(dashboardRoute?.path).toBe('/dashboard');
});

it('should prefer memoryRoute over hashRoute when both are set', () => {
const routes = createNestedRoutes();
const router = createRouter({
history: createWebHistory(),
routes,
});

const result = processRoutes({
router,
basename: '/app',
memoryRoute: true,
hashRoute: true,
});

// memoryRoute takes priority (checked first in processRoutes)
// Memory history uses basename as the base
expect(result.history.base).toBe('/app');
});

it('should preserve trailing slashes in paths when prefixing with basename', () => {
const trailingSlashRoutes = [
{
path: '/dashboard/',
name: 'Dashboard',
component: { template: '<div>Dashboard</div>' },
},
{
path: '/settings',
name: 'Settings',
component: { template: '<div>Settings</div>' },
},
];

const router = createRouter({
history: createWebHistory(),
routes: trailingSlashRoutes,
});

const result = processRoutes({
router,
basename: '/app',
hashRoute: true,
});

// Trailing slash should be preserved for /dashboard/
const dashboardRoute = result.routes.find((r) => r.name === 'Dashboard');
expect(dashboardRoute?.path).toBe('/app/dashboard/');

// No trailing slash should remain absent for /settings
const settingsRoute = result.routes.find((r) => r.name === 'Settings');
expect(settingsRoute?.path).toBe('/app/settings');
});

it('should preserve trailing slashes in string redirects', () => {
const redirectRoutes = [
{
path: '/',
redirect: '/dashboard/',
},
{
path: '/dashboard/',
name: 'Dashboard',
component: { template: '<div>Dashboard</div>' },
},
];

const router = createRouter({
history: createWebHistory(),
routes: redirectRoutes,
});

const result = processRoutes({
router,
basename: '/app',
hashRoute: true,
});

const rootRoute = result.routes.find((r) => r.path === '/app');
expect(rootRoute?.redirect).toBe('/app/dashboard/');
});
});

describe('processRoutes with memoryRoute', () => {
it('should create a memory history when memoryRoute is true', () => {
const routes = createNestedRoutes();
const router = createRouter({
history: createWebHistory(),
routes,
});

const result = processRoutes({
router,
memoryRoute: true,
});

expect(result.history).toBeDefined();
expect(result.routes.length).toBeGreaterThan(0);
});

it('should pass basename to memory history', () => {
const routes = createNestedRoutes();
const router = createRouter({
history: createWebHistory(),
routes,
});

const result = processRoutes({
router,
basename: '/app',
memoryRoute: true,
});

expect(result.history).toBeDefined();
expect(result.history.base).toBe('/app');
});

it('should not prefix route paths when using memoryRoute', () => {
const routes = createNestedRoutes();
const router = createRouter({
history: createWebHistory(),
routes,
});

const result = processRoutes({
router,
basename: '/app',
memoryRoute: true,
});

// Unlike hashRoute, memoryRoute does NOT prefix routes — it passes basename to createMemoryHistory
const homeRoute = result.routes.find((r) => r.name === 'Home');
expect(homeRoute?.path).toBe('/');

const dashboardRoute = result.routes.find((r) => r.name === 'Dashboard');
expect(dashboardRoute?.path).toBe('/dashboard');
});
});

describe('processRoutes default (web history)', () => {
it('should create web history with basename by default', () => {
const routes = createNestedRoutes();
const router = createRouter({
history: createWebHistory(),
routes,
});

const result = processRoutes({
router,
basename: '/app',
});

expect(result.history).toBeDefined();
expect(result.history.base).toBe('/app');
});

it('should not prefix route paths in default mode', () => {
const routes = createNestedRoutes();
const router = createRouter({
history: createWebHistory(),
routes,
});

const result = processRoutes({
router,
basename: '/app',
});

const homeRoute = result.routes.find((r) => r.name === 'Home');
expect(homeRoute?.path).toBe('/');

const dashboardRoute = result.routes.find((r) => r.name === 'Dashboard');
expect(dashboardRoute?.path).toBe('/dashboard');
});
});

describe('processRoutes hashRoute with redirects', () => {
it('should prefix string redirects with basename', () => {
const redirectRoutes = [
{
path: '/',
redirect: '/dashboard',
},
{
path: '/dashboard',
name: 'Dashboard',
component: { template: '<div>Dashboard</div>' },
},
{
path: '/about',
name: 'About',
component: { template: '<div>About</div>' },
},
];

const router = createRouter({
history: createWebHistory(),
routes: redirectRoutes,
});

const result = processRoutes({
router,
basename: '/barber',
hashRoute: true,
});

// The redirect route should have its redirect target prefixed
const rootRoute = result.routes.find((r) => r.path === '/barber');
expect(rootRoute).toBeDefined();
expect(rootRoute?.redirect).toBe('/barber/dashboard');

// Other routes should be prefixed normally
const dashboardRoute = result.routes.find((r) => r.name === 'Dashboard');
expect(dashboardRoute?.path).toBe('/barber/dashboard');

const aboutRoute = result.routes.find((r) => r.name === 'About');
expect(aboutRoute?.path).toBe('/barber/about');
});

it('should prefix object redirects with path property', () => {
const redirectRoutes = [
{
path: '/',
redirect: { path: '/dashboard' },
},
{
path: '/dashboard',
name: 'Dashboard',
component: { template: '<div>Dashboard</div>' },
},
];

const router = createRouter({
history: createWebHistory(),
routes: redirectRoutes,
});

const result = processRoutes({
router,
basename: '/app',
hashRoute: true,
});

const rootRoute = result.routes.find((r) => r.path === '/app');
expect(rootRoute).toBeDefined();
expect(rootRoute?.redirect).toEqual({ path: '/app/dashboard' });
});

it('should not modify named redirects (no path property)', () => {
const redirectRoutes = [
{
path: '/',
redirect: { name: 'Dashboard' },
},
{
path: '/dashboard',
name: 'Dashboard',
component: { template: '<div>Dashboard</div>' },
},
];

const router = createRouter({
history: createWebHistory(),
routes: redirectRoutes,
});

const result = processRoutes({
router,
basename: '/app',
hashRoute: true,
});

const rootRoute = result.routes.find((r) => r.path === '/app');
expect(rootRoute).toBeDefined();
// Named redirects should be untouched — Vue Router resolves by name, not path
expect(rootRoute?.redirect).toEqual({ name: 'Dashboard' });
});
});
});
Loading
Loading