feat: add some test case
This commit is contained in:
@@ -40,6 +40,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@vben-core/toolkit": "workspace:*",
|
||||
"@vben-core/typings": "workspace:*"
|
||||
"@vben-core/typings": "workspace:*",
|
||||
"vue-router": "^4.3.2"
|
||||
}
|
||||
}
|
||||
|
171
packages/@vben-core/forward/helpers/src/generator-menus.test.ts
Normal file
171
packages/@vben-core/forward/helpers/src/generator-menus.test.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { generatorMenus } from './generator-menus'; // 替换为您的实际路径
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
// 模拟路由数据
|
||||
const mockRoutes = [
|
||||
{
|
||||
meta: { icon: 'home-icon', title: '首页' },
|
||||
name: 'home',
|
||||
path: '/home',
|
||||
},
|
||||
{
|
||||
meta: { hideChildrenInMenu: true, icon: 'about-icon', title: '关于' },
|
||||
name: 'about',
|
||||
path: '/about',
|
||||
children: [
|
||||
{
|
||||
path: 'team',
|
||||
name: 'team',
|
||||
meta: { icon: 'team-icon', title: '团队' },
|
||||
},
|
||||
],
|
||||
},
|
||||
] as RouteRecordRaw[];
|
||||
|
||||
// 模拟 Vue 路由器实例
|
||||
const mockRouter = {
|
||||
getRoutes: vi.fn(() => [
|
||||
{ name: 'home', path: '/home' },
|
||||
{ name: 'about', path: '/about' },
|
||||
{ name: 'team', path: '/about/team' },
|
||||
]),
|
||||
};
|
||||
|
||||
// Nested route setup to test child inclusion and hideChildrenInMenu functionality
|
||||
|
||||
describe('generatorMenus', () => {
|
||||
it('the correct menu list should be generated according to the route', async () => {
|
||||
const expectedMenus = [
|
||||
{
|
||||
badge: undefined,
|
||||
badgeType: undefined,
|
||||
badgeVariants: undefined,
|
||||
icon: 'home-icon',
|
||||
name: '首页',
|
||||
orderNo: undefined,
|
||||
parent: undefined,
|
||||
parents: undefined,
|
||||
path: '/home',
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
badge: undefined,
|
||||
badgeType: undefined,
|
||||
badgeVariants: undefined,
|
||||
icon: 'about-icon',
|
||||
name: '关于',
|
||||
orderNo: undefined,
|
||||
parent: undefined,
|
||||
parents: undefined,
|
||||
path: '/about',
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
|
||||
const menus = await generatorMenus(mockRoutes, mockRouter as any);
|
||||
expect(menus).toEqual(expectedMenus);
|
||||
});
|
||||
|
||||
it('includes additional meta properties in menu items', async () => {
|
||||
const mockRoutesWithMeta = [
|
||||
{
|
||||
meta: { icon: 'user-icon', orderNo: 1, title: 'Profile' },
|
||||
name: 'profile',
|
||||
path: '/profile',
|
||||
},
|
||||
] as RouteRecordRaw[];
|
||||
|
||||
const menus = await generatorMenus(mockRoutesWithMeta, mockRouter as any);
|
||||
expect(menus).toEqual([
|
||||
{
|
||||
badge: undefined,
|
||||
badgeType: undefined,
|
||||
badgeVariants: undefined,
|
||||
icon: 'user-icon',
|
||||
name: 'Profile',
|
||||
orderNo: 1,
|
||||
parent: undefined,
|
||||
parents: undefined,
|
||||
path: '/profile',
|
||||
children: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('handles dynamic route parameters correctly', async () => {
|
||||
const mockRoutesWithParams = [
|
||||
{
|
||||
meta: { icon: 'details-icon', title: 'User Details' },
|
||||
name: 'userDetails',
|
||||
path: '/users/:userId',
|
||||
},
|
||||
] as RouteRecordRaw[];
|
||||
|
||||
const menus = await generatorMenus(mockRoutesWithParams, mockRouter as any);
|
||||
expect(menus).toEqual([
|
||||
{
|
||||
badge: undefined,
|
||||
badgeType: undefined,
|
||||
badgeVariants: undefined,
|
||||
icon: 'details-icon',
|
||||
name: 'User Details',
|
||||
orderNo: undefined,
|
||||
parent: undefined,
|
||||
parents: undefined,
|
||||
path: '/users/:userId',
|
||||
children: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('processes routes with redirects correctly', async () => {
|
||||
const mockRoutesWithRedirect = [
|
||||
{
|
||||
name: 'redirectedRoute',
|
||||
path: '/old-path',
|
||||
redirect: '/new-path',
|
||||
},
|
||||
{
|
||||
meta: { icon: 'path-icon', title: 'New Path' },
|
||||
name: 'newPath',
|
||||
path: '/new-path',
|
||||
},
|
||||
] as RouteRecordRaw[];
|
||||
|
||||
const menus = await generatorMenus(
|
||||
mockRoutesWithRedirect,
|
||||
mockRouter as any,
|
||||
);
|
||||
console.log(111, menus);
|
||||
|
||||
expect(menus).toEqual([
|
||||
// Assuming your generatorMenus function excludes redirect routes from the menu
|
||||
{
|
||||
badge: undefined,
|
||||
badgeType: undefined,
|
||||
badgeVariants: undefined,
|
||||
icon: undefined,
|
||||
name: 'redirectedRoute',
|
||||
orderNo: undefined,
|
||||
parent: undefined,
|
||||
parents: undefined,
|
||||
path: '/old-path',
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
badge: undefined,
|
||||
badgeType: undefined,
|
||||
badgeVariants: undefined,
|
||||
icon: 'path-icon',
|
||||
name: 'New Path',
|
||||
orderNo: undefined,
|
||||
parent: undefined,
|
||||
parents: undefined,
|
||||
path: '/new-path',
|
||||
children: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
71
packages/@vben-core/forward/helpers/src/generator-menus.ts
Normal file
71
packages/@vben-core/forward/helpers/src/generator-menus.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import type { ExRouteRecordRaw, MenuRecordRaw } from '@vben-core/typings';
|
||||
|
||||
import { mapTree } from '@vben-core/toolkit';
|
||||
import type { RouteRecordRaw, Router } from 'vue-router';
|
||||
|
||||
/**
|
||||
* 根据 routes 生成菜单列表
|
||||
* @param routes
|
||||
*/
|
||||
async function generatorMenus(
|
||||
routes: RouteRecordRaw[],
|
||||
router: Router,
|
||||
): Promise<MenuRecordRaw[]> {
|
||||
// 将路由列表转换为一个以 name 为键的对象映射
|
||||
// 获取所有router最终的path及name
|
||||
const finalRoutesMap: { [key: string]: string } = Object.fromEntries(
|
||||
router.getRoutes().map(({ name, path }) => [name, path]),
|
||||
);
|
||||
|
||||
const menus = mapTree<ExRouteRecordRaw, MenuRecordRaw>(routes, (route) => {
|
||||
// 路由表的路径写法有多种,这里从router获取到最终的path并赋值
|
||||
const path = finalRoutesMap[route.name as string] ?? route.path;
|
||||
|
||||
// 转换为菜单结构
|
||||
// const path = matchRoute?.path ?? route.path;
|
||||
const { meta, name: routeName, redirect, children } = route;
|
||||
const {
|
||||
badge,
|
||||
badgeType,
|
||||
badgeVariants,
|
||||
hideChildrenInMenu = false,
|
||||
icon,
|
||||
orderNo,
|
||||
target,
|
||||
title = '',
|
||||
} = meta || {};
|
||||
|
||||
const name = (title || routeName || '') as string;
|
||||
|
||||
// 隐藏子菜单
|
||||
const resultChildren = hideChildrenInMenu
|
||||
? []
|
||||
: (children as MenuRecordRaw[]);
|
||||
|
||||
// 将菜单的所有父级和父级菜单记录到菜单项内
|
||||
if (resultChildren && resultChildren.length > 0) {
|
||||
resultChildren.forEach((child) => {
|
||||
child.parents = [...(route.parents || []), path];
|
||||
child.parent = path;
|
||||
});
|
||||
}
|
||||
// 隐藏子菜单
|
||||
const resultPath = hideChildrenInMenu ? redirect || path : target || path;
|
||||
return {
|
||||
badge,
|
||||
badgeType,
|
||||
badgeVariants,
|
||||
icon,
|
||||
name,
|
||||
orderNo,
|
||||
parent: route.parent,
|
||||
parents: route.parents,
|
||||
path: resultPath as string,
|
||||
children: resultChildren || [],
|
||||
};
|
||||
});
|
||||
|
||||
return menus;
|
||||
}
|
||||
|
||||
export { generatorMenus };
|
128
packages/@vben-core/forward/helpers/src/generator-routes.test.ts
Normal file
128
packages/@vben-core/forward/helpers/src/generator-routes.test.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { generatorRoutes, hasAuthority, hasVisible } from './generator-routes';
|
||||
|
||||
// Mock 路由数据
|
||||
const mockRoutes = [
|
||||
{
|
||||
meta: {
|
||||
authority: ['admin', 'user'],
|
||||
hideInMenu: false,
|
||||
},
|
||||
path: '/dashboard',
|
||||
children: [
|
||||
{
|
||||
path: '/dashboard/overview',
|
||||
meta: { authority: ['admin'], hideInMenu: false },
|
||||
},
|
||||
{
|
||||
path: '/dashboard/stats',
|
||||
meta: { authority: ['user'], hideInMenu: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
meta: { authority: ['admin'], hideInMenu: false },
|
||||
path: '/settings',
|
||||
},
|
||||
{
|
||||
meta: { hideInMenu: false },
|
||||
path: '/profile',
|
||||
},
|
||||
] as RouteRecordRaw[];
|
||||
|
||||
describe('hasAuthority', () => {
|
||||
it('should return true if there is no authority defined', () => {
|
||||
expect(hasAuthority(mockRoutes[2], ['admin'])).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if the user has the required authority', () => {
|
||||
expect(hasAuthority(mockRoutes[0], ['admin'])).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if the user does not have the required authority', () => {
|
||||
expect(hasAuthority(mockRoutes[1], ['user'])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasVisible', () => {
|
||||
it('should return true if hideInMenu is not set or false', () => {
|
||||
expect(hasVisible(mockRoutes[0])).toBe(true);
|
||||
expect(hasVisible(mockRoutes[2])).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if hideInMenu is true', () => {
|
||||
expect(hasVisible(mockRoutes[0].children?.[1])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generatorRoutes', () => {
|
||||
it('should filter routes based on authority and visibility', async () => {
|
||||
const generatedRoutes = await generatorRoutes(mockRoutes, ['user']);
|
||||
// The user should have access to /dashboard/stats, but it should be filtered out because it's not visible
|
||||
expect(generatedRoutes).toEqual([
|
||||
{
|
||||
meta: { authority: ['admin', 'user'], hideInMenu: false },
|
||||
path: '/dashboard',
|
||||
children: [],
|
||||
},
|
||||
// Note: We expect /settings to be filtered out because the user does not have 'admin' authority
|
||||
{
|
||||
meta: { hideInMenu: false },
|
||||
path: '/profile',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle routes without children', async () => {
|
||||
const generatedRoutes = await generatorRoutes(mockRoutes, ['user']);
|
||||
expect(generatedRoutes).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
path: '/profile', // This route has no children and should be included
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle empty roles array', async () => {
|
||||
const generatedRoutes = await generatorRoutes(mockRoutes, []);
|
||||
expect(generatedRoutes).toEqual(
|
||||
expect.arrayContaining([
|
||||
// Only routes without authority should be included
|
||||
expect.objectContaining({
|
||||
path: '/profile',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(generatedRoutes).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
path: '/dashboard',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
path: '/settings',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle missing meta fields', async () => {
|
||||
const routesWithMissingMeta = [
|
||||
{ path: '/path1' }, // No meta
|
||||
{ meta: {}, path: '/path2' }, // Empty meta
|
||||
{ meta: { authority: ['admin'] }, path: '/path3' }, // Only authority
|
||||
];
|
||||
const generatedRoutes = await generatorRoutes(
|
||||
routesWithMissingMeta as RouteRecordRaw[],
|
||||
['admin'],
|
||||
);
|
||||
expect(generatedRoutes).toEqual([
|
||||
{ path: '/path1' },
|
||||
{ meta: {}, path: '/path2' },
|
||||
{ meta: { authority: ['admin'] }, path: '/path3' },
|
||||
]);
|
||||
});
|
||||
});
|
40
packages/@vben-core/forward/helpers/src/generator-routes.ts
Normal file
40
packages/@vben-core/forward/helpers/src/generator-routes.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { filterTree } from '@vben-core/toolkit';
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
/**
|
||||
* 动态生成路由
|
||||
*/
|
||||
async function generatorRoutes(
|
||||
routes: RouteRecordRaw[],
|
||||
roles: string[],
|
||||
): Promise<RouteRecordRaw[]> {
|
||||
// 根据角色标识过滤路由表,判断当前用户是否拥有指定权限
|
||||
return filterTree(routes, (route) => {
|
||||
return hasVisible(route) && hasAuthority(route, roles);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断路由是否有权限访问
|
||||
* @param route
|
||||
* @param access
|
||||
*/
|
||||
function hasAuthority(route: RouteRecordRaw, access: string[]) {
|
||||
const authority = route.meta?.authority;
|
||||
|
||||
if (!authority) {
|
||||
return true;
|
||||
}
|
||||
return access.some((value) => {
|
||||
return authority.includes(value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断路由是否需要在菜单中显示
|
||||
* @param route
|
||||
*/
|
||||
function hasVisible(route?: RouteRecordRaw) {
|
||||
return !route?.meta?.hideInMenu;
|
||||
}
|
||||
|
||||
export { generatorRoutes, hasAuthority, hasVisible };
|
@@ -1,2 +1,4 @@
|
||||
export * from './flatten-object';
|
||||
export * from './generator-menus';
|
||||
export * from './generator-routes';
|
||||
export * from './nested-object';
|
||||
|
@@ -94,4 +94,22 @@ describe('nestedObject', () => {
|
||||
|
||||
expect(nestedObject(flatObject, 1)).toEqual(expectedNestedObject);
|
||||
});
|
||||
|
||||
it('should correctly nest an object based on the specified level', () => {
|
||||
const obj = {
|
||||
oneFiveSix: 'Value156',
|
||||
oneTwoFour: 'Value124',
|
||||
oneTwoThree: 'Value123',
|
||||
};
|
||||
|
||||
const nested = nestedObject(obj, 2);
|
||||
|
||||
expect(nested).toEqual({
|
||||
one: {
|
||||
fiveSix: 'Value156',
|
||||
twoFour: 'Value124',
|
||||
twoThree: 'Value123',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@vben/tsconfig/library.json",
|
||||
"extends": "@vben/tsconfig/web.json",
|
||||
"compilerOptions": {
|
||||
"types": ["@vben-core/typings/vue-router"]
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
Reference in New Issue
Block a user