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"]
|
||||
}
|
||||
|
@@ -45,7 +45,6 @@ const defaultPreferences: Preferences = {
|
||||
split: true,
|
||||
styleType: 'rounded',
|
||||
},
|
||||
|
||||
shortcutKeys: { enable: true },
|
||||
sidebar: {
|
||||
collapse: false,
|
||||
@@ -56,17 +55,14 @@ const defaultPreferences: Preferences = {
|
||||
hidden: false,
|
||||
width: 240,
|
||||
},
|
||||
|
||||
tabbar: {
|
||||
enable: true,
|
||||
keepAlive: true,
|
||||
showIcon: true,
|
||||
},
|
||||
|
||||
theme: {
|
||||
colorPrimary: 'hsl(211 91% 39%)',
|
||||
},
|
||||
|
||||
transition: {
|
||||
enable: true,
|
||||
name: 'fade-slide',
|
||||
|
268
packages/@vben-core/forward/preferences/src/preferences.test.ts
Normal file
268
packages/@vben-core/forward/preferences/src/preferences.test.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { defaultPreferences } from './config';
|
||||
import { PreferenceManager, isDarkTheme } from './preferences';
|
||||
|
||||
describe('preferences', () => {
|
||||
let preferenceManager: PreferenceManager;
|
||||
vi.mock('@vben-core/cache', () => {
|
||||
return {
|
||||
StorageManager: vi.fn().mockImplementation(() => {
|
||||
return {
|
||||
getItem: vi.fn(),
|
||||
removeItem: vi.fn(),
|
||||
setItem: vi.fn(),
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
// 模拟 window.matchMedia 方法
|
||||
vi.stubGlobal(
|
||||
'matchMedia',
|
||||
vi.fn().mockImplementation((query) => ({
|
||||
addEventListener: vi.fn(),
|
||||
addListener: vi.fn(), // Deprecated
|
||||
dispatchEvent: vi.fn(),
|
||||
matches: query === '(prefers-color-scheme: dark)',
|
||||
media: query,
|
||||
onchange: null,
|
||||
removeEventListener: vi.fn(),
|
||||
removeListener: vi.fn(), // Deprecated
|
||||
})),
|
||||
);
|
||||
beforeEach(() => {
|
||||
preferenceManager = new PreferenceManager();
|
||||
});
|
||||
|
||||
it('initPreferences should initialize preferences with overrides and namespace', async () => {
|
||||
const overrides = { theme: { colorPrimary: 'hsl(211 91% 39%)' } };
|
||||
const namespace = 'testNamespace';
|
||||
|
||||
await preferenceManager.initPreferences({ namespace, overrides });
|
||||
|
||||
expect(preferenceManager.getPreferences().theme.colorPrimary).toBe(
|
||||
overrides.theme.colorPrimary,
|
||||
);
|
||||
});
|
||||
|
||||
it('loads default preferences if no saved preferences found', () => {
|
||||
const preferences = preferenceManager.getPreferences();
|
||||
expect(preferences).toEqual(defaultPreferences);
|
||||
});
|
||||
|
||||
it('initializes preferences with overrides', async () => {
|
||||
const overrides: any = {
|
||||
app: {
|
||||
locale: 'en-US',
|
||||
themeMode: 'light',
|
||||
},
|
||||
};
|
||||
await preferenceManager.initPreferences({
|
||||
namespace: 'testNamespace',
|
||||
overrides,
|
||||
});
|
||||
|
||||
// 等待防抖动操作完成
|
||||
// await new Promise((resolve) => setTimeout(resolve, 300)); // 等待100毫秒
|
||||
|
||||
const expected = {
|
||||
...defaultPreferences,
|
||||
app: {
|
||||
...defaultPreferences.app,
|
||||
...overrides.app,
|
||||
},
|
||||
};
|
||||
|
||||
expect(preferenceManager.getPreferences()).toEqual(expected);
|
||||
});
|
||||
|
||||
it('updates theme mode correctly', () => {
|
||||
preferenceManager.updatePreferences({
|
||||
app: { themeMode: 'light' },
|
||||
});
|
||||
|
||||
expect(preferenceManager.getPreferences().app.themeMode).toBe('light');
|
||||
});
|
||||
|
||||
it('updates color modes correctly', () => {
|
||||
preferenceManager.updatePreferences({
|
||||
app: { colorGrayMode: true, colorWeakMode: true },
|
||||
});
|
||||
|
||||
expect(preferenceManager.getPreferences().app.colorGrayMode).toBe(true);
|
||||
expect(preferenceManager.getPreferences().app.colorWeakMode).toBe(true);
|
||||
});
|
||||
|
||||
it('resets preferences to default', () => {
|
||||
// 先更新一些偏好设置
|
||||
preferenceManager.updatePreferences({
|
||||
app: { themeMode: 'light' },
|
||||
});
|
||||
|
||||
// 然后重置偏好设置
|
||||
preferenceManager.resetPreferences();
|
||||
|
||||
expect(preferenceManager.getPreferences()).toEqual(defaultPreferences);
|
||||
});
|
||||
|
||||
it('updates isMobile correctly', () => {
|
||||
// 模拟移动端状态
|
||||
vi.stubGlobal(
|
||||
'matchMedia',
|
||||
vi.fn().mockImplementation((query) => ({
|
||||
addEventListener: vi.fn(),
|
||||
addListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
matches: query === '(max-width: 768px)',
|
||||
media: query,
|
||||
onchange: null,
|
||||
removeEventListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
})),
|
||||
);
|
||||
|
||||
preferenceManager.updatePreferences({
|
||||
app: { isMobile: true },
|
||||
});
|
||||
|
||||
expect(preferenceManager.getPreferences().app.isMobile).toBe(true);
|
||||
});
|
||||
|
||||
it('updates the locale preference correctly', () => {
|
||||
preferenceManager.updatePreferences({
|
||||
app: { locale: 'en-US' },
|
||||
});
|
||||
|
||||
expect(preferenceManager.getPreferences().app.locale).toBe('en-US');
|
||||
});
|
||||
|
||||
it('updates the sidebar width correctly', () => {
|
||||
preferenceManager.updatePreferences({
|
||||
sidebar: { width: 200 },
|
||||
});
|
||||
|
||||
expect(preferenceManager.getPreferences().sidebar.width).toBe(200);
|
||||
});
|
||||
it('updates the sidebar collapse state correctly', () => {
|
||||
preferenceManager.updatePreferences({
|
||||
sidebar: { collapse: true },
|
||||
});
|
||||
|
||||
expect(preferenceManager.getPreferences().sidebar.collapse).toBe(true);
|
||||
});
|
||||
it('updates the navigation style type correctly', () => {
|
||||
preferenceManager.updatePreferences({
|
||||
navigation: { styleType: 'flat' },
|
||||
} as any);
|
||||
|
||||
expect(preferenceManager.getPreferences().navigation.styleType).toBe(
|
||||
'flat',
|
||||
);
|
||||
});
|
||||
|
||||
it('resets preferences to default correctly', () => {
|
||||
// 先更新一些偏好设置
|
||||
preferenceManager.updatePreferences({
|
||||
app: { locale: 'en-US', themeMode: 'light' },
|
||||
sidebar: { collapse: true, width: 200 },
|
||||
});
|
||||
|
||||
// 然后重置偏好设置
|
||||
preferenceManager.resetPreferences();
|
||||
|
||||
expect(preferenceManager.getPreferences()).toEqual(defaultPreferences);
|
||||
});
|
||||
|
||||
it('does not update undefined preferences', () => {
|
||||
const originalPreferences = preferenceManager.getPreferences();
|
||||
|
||||
preferenceManager.updatePreferences({
|
||||
app: { nonexistentField: 'value' },
|
||||
} as any);
|
||||
|
||||
expect(preferenceManager.getPreferences()).toEqual(originalPreferences);
|
||||
});
|
||||
|
||||
it('reverts to default when a preference field is deleted', () => {
|
||||
preferenceManager.updatePreferences({
|
||||
app: { locale: 'en-US' },
|
||||
});
|
||||
|
||||
preferenceManager.updatePreferences({
|
||||
app: { locale: undefined },
|
||||
});
|
||||
|
||||
expect(preferenceManager.getPreferences().app.locale).toBe('en-US');
|
||||
});
|
||||
|
||||
it('ignores updates with invalid preference value types', () => {
|
||||
const originalPreferences = preferenceManager.getPreferences();
|
||||
|
||||
preferenceManager.updatePreferences({
|
||||
app: { isMobile: 'true' as unknown as boolean }, // 错误类型
|
||||
});
|
||||
|
||||
expect(preferenceManager.getPreferences()).toEqual(originalPreferences);
|
||||
});
|
||||
|
||||
it('merges nested preference objects correctly', () => {
|
||||
preferenceManager.updatePreferences({
|
||||
app: { name: 'New App Name' },
|
||||
});
|
||||
|
||||
const expected = {
|
||||
...defaultPreferences,
|
||||
app: {
|
||||
...defaultPreferences.app,
|
||||
name: 'New App Name',
|
||||
},
|
||||
};
|
||||
|
||||
expect(preferenceManager.getPreferences()).toEqual(expected);
|
||||
});
|
||||
|
||||
it('applies updates immediately after initialization', async () => {
|
||||
const overrides: any = {
|
||||
app: {
|
||||
locale: 'en-US',
|
||||
},
|
||||
};
|
||||
|
||||
await preferenceManager.initPreferences(overrides);
|
||||
|
||||
preferenceManager.updatePreferences({
|
||||
app: { themeMode: 'light' },
|
||||
});
|
||||
|
||||
expect(preferenceManager.getPreferences().app.themeMode).toBe('light');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isDarkTheme', () => {
|
||||
it('should return true for dark theme', () => {
|
||||
expect(isDarkTheme('dark')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for light theme', () => {
|
||||
expect(isDarkTheme('light')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return system preference for auto theme', () => {
|
||||
vi.spyOn(window, 'matchMedia').mockImplementation((query) => ({
|
||||
addEventListener: vi.fn(),
|
||||
addListener: vi.fn(), // Deprecated
|
||||
dispatchEvent: vi.fn(),
|
||||
matches: query === '(prefers-color-scheme: dark)',
|
||||
media: query,
|
||||
onchange: null,
|
||||
removeEventListener: vi.fn(),
|
||||
removeListener: vi.fn(), // Deprecated
|
||||
}));
|
||||
|
||||
expect(isDarkTheme('auto')).toBe(true);
|
||||
expect(window.matchMedia).toHaveBeenCalledWith(
|
||||
'(prefers-color-scheme: dark)',
|
||||
);
|
||||
});
|
||||
});
|
@@ -85,15 +85,21 @@ class PreferenceManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从缓存中加载偏好设置。如果缓存中没有找到对应的偏好设置,则返回默认偏好设置。
|
||||
*/
|
||||
private loadCachedPreferences() {
|
||||
return this.cache?.getItem(STORAGE_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载偏好设置
|
||||
* 从缓存中加载偏好设置。如果缓存中没有找到对应的偏好设置,则返回默认偏好设置。
|
||||
* @returns {Preferences} 加载的偏好设置
|
||||
*/
|
||||
private loadPreferences(): Preferences {
|
||||
const savedPreferences = this.cache?.getItem(STORAGE_KEY);
|
||||
return savedPreferences || { ...defaultPreferences };
|
||||
private loadPreferences(): Preferences | null {
|
||||
return this.loadCachedPreferences() || { ...defaultPreferences };
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听状态和系统偏好设置的变化。
|
||||
*/
|
||||
@@ -239,7 +245,7 @@ class PreferenceManager {
|
||||
this.initialPreferences = merge({}, overrides, defaultPreferences);
|
||||
|
||||
// 加载并合并当前存储的偏好设置
|
||||
const mergedPreference = merge({}, this.loadPreferences(), overrides);
|
||||
const mergedPreference = merge({}, this.loadCachedPreferences(), overrides);
|
||||
|
||||
// 更新偏好设置
|
||||
this.updatePreferences(mergedPreference);
|
||||
@@ -274,9 +280,10 @@ class PreferenceManager {
|
||||
* @param updates - 要更新的偏好设置
|
||||
*/
|
||||
public updatePreferences(updates: DeepPartial<Preferences>) {
|
||||
const mergedState = merge(updates, markRaw(this.state));
|
||||
const mergedState = merge({}, updates, markRaw(this.state));
|
||||
|
||||
Object.assign(this.state, mergedState);
|
||||
|
||||
Object.assign(this.flattenedState, flattenObject(this.state));
|
||||
|
||||
// 根据更新的键值执行相应的操作
|
||||
@@ -286,4 +293,4 @@ class PreferenceManager {
|
||||
}
|
||||
|
||||
const preferencesManager = new PreferenceManager();
|
||||
export { isDarkTheme, preferencesManager };
|
||||
export { PreferenceManager, isDarkTheme, preferencesManager };
|
||||
|
@@ -1,24 +1,85 @@
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import {
|
||||
// beforeEach,
|
||||
describe,
|
||||
// expect,
|
||||
it,
|
||||
} from 'vitest';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
// import { useAccessStore } from '../modules/access';
|
||||
import { useAccessStore } from './access';
|
||||
|
||||
describe('useAccessStore', () => {
|
||||
it('app Name with test', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
// let referenceStore = usePreferencesStore();
|
||||
});
|
||||
|
||||
// beforeEach(() => {
|
||||
// referenceStore = usePreferencesStore();
|
||||
// });
|
||||
it('updates accessMenus state', () => {
|
||||
const store = useAccessStore();
|
||||
expect(store.accessMenus).toEqual([]);
|
||||
store.setAccessMenus([{ name: 'Dashboard', path: '/dashboard' }]);
|
||||
expect(store.accessMenus).toEqual([
|
||||
{ name: 'Dashboard', path: '/dashboard' },
|
||||
]);
|
||||
});
|
||||
|
||||
// expect(referenceStore.appName).toBe('vben-admin');
|
||||
// referenceStore.setAppName('vbenAdmin');
|
||||
// expect(referenceStore.getAppName).toBe('vbenAdmin');
|
||||
it('updates userInfo and userRoles state', () => {
|
||||
const store = useAccessStore();
|
||||
expect(store.userInfo).toBeNull();
|
||||
expect(store.userRoles).toEqual([]);
|
||||
|
||||
const userInfo: any = { name: 'John Doe', roles: [{ value: 'admin' }] };
|
||||
store.setUserInfo(userInfo);
|
||||
|
||||
expect(store.userInfo).toEqual(userInfo);
|
||||
expect(store.userRoles).toEqual(['admin']);
|
||||
});
|
||||
|
||||
it('returns correct userInfo', () => {
|
||||
const store = useAccessStore();
|
||||
const userInfo: any = { name: 'Jane Doe', roles: [{ value: 'user' }] };
|
||||
store.setUserInfo(userInfo);
|
||||
expect(store.getUserInfo).toEqual(userInfo);
|
||||
});
|
||||
|
||||
it('updates accessToken state correctly', () => {
|
||||
const store = useAccessStore();
|
||||
expect(store.accessToken).toBeNull(); // 初始状态
|
||||
store.setAccessToken('abc123');
|
||||
expect(store.accessToken).toBe('abc123');
|
||||
});
|
||||
|
||||
// 测试重置用户信息时的行为
|
||||
it('clears userInfo and userRoles when setting null userInfo', () => {
|
||||
const store = useAccessStore();
|
||||
store.setUserInfo({
|
||||
roles: [{ roleName: 'User', value: 'user' }],
|
||||
} as any);
|
||||
expect(store.userInfo).not.toBeNull();
|
||||
expect(store.userRoles.length).toBeGreaterThan(0);
|
||||
|
||||
store.setUserInfo(null as any); // 重置用户信息
|
||||
expect(store.userInfo).toBeNull();
|
||||
expect(store.userRoles).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns the correct accessToken', () => {
|
||||
const store = useAccessStore();
|
||||
store.setAccessToken('xyz789');
|
||||
expect(store.getAccessToken).toBe('xyz789');
|
||||
});
|
||||
|
||||
// 测试在没有用户角色时返回空数组
|
||||
it('returns an empty array for userRoles if not set', () => {
|
||||
const store = useAccessStore();
|
||||
expect(store.getUserRoles).toEqual([]);
|
||||
});
|
||||
|
||||
// 测试设置空的访问菜单列表
|
||||
it('handles empty accessMenus correctly', () => {
|
||||
const store = useAccessStore();
|
||||
store.setAccessMenus([]);
|
||||
expect(store.accessMenus).toEqual([]);
|
||||
});
|
||||
|
||||
// 测试设置空的访问路由列表
|
||||
it('handles empty accessRoutes correctly', () => {
|
||||
const store = useAccessStore();
|
||||
store.setAccessRoutes([]);
|
||||
expect(store.accessRoutes).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
309
packages/@vben-core/forward/stores/src/modules/tabs.test.ts
Normal file
309
packages/@vben-core/forward/stores/src/modules/tabs.test.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
|
||||
import { useTabsStore } from './tabs';
|
||||
|
||||
describe('useAccessStore', () => {
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [],
|
||||
});
|
||||
router.push = vi.fn();
|
||||
router.replace = vi.fn();
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('adds a new tab', () => {
|
||||
const store = useTabsStore();
|
||||
const tab: any = {
|
||||
fullPath: '/home',
|
||||
meta: {},
|
||||
name: 'Home',
|
||||
path: '/home',
|
||||
};
|
||||
store.addTab(tab);
|
||||
expect(store.tabs.length).toBe(1);
|
||||
expect(store.tabs[0]).toEqual(tab);
|
||||
});
|
||||
|
||||
it('adds a new tab if it does not exist', () => {
|
||||
const store = useTabsStore();
|
||||
const newTab: any = {
|
||||
fullPath: '/new',
|
||||
meta: {},
|
||||
name: 'New',
|
||||
path: '/new',
|
||||
};
|
||||
store.addTab(newTab);
|
||||
expect(store.tabs).toContainEqual(newTab);
|
||||
});
|
||||
|
||||
it('updates an existing tab instead of adding a new one', () => {
|
||||
const store = useTabsStore();
|
||||
const initialTab: any = {
|
||||
fullPath: '/existing',
|
||||
meta: {},
|
||||
name: 'Existing',
|
||||
path: '/existing',
|
||||
query: {},
|
||||
};
|
||||
store.tabs.push(initialTab);
|
||||
const updatedTab = { ...initialTab, query: { id: '1' } };
|
||||
store.addTab(updatedTab);
|
||||
expect(store.tabs.length).toBe(1);
|
||||
expect(store.tabs[0].query).toEqual({ id: '1' });
|
||||
});
|
||||
|
||||
it('closes all tabs', async () => {
|
||||
const store = useTabsStore();
|
||||
store.tabs = [
|
||||
{ fullPath: '/home', meta: {}, name: 'Home', path: '/home' },
|
||||
] as any;
|
||||
router.replace = vi.fn(); // 使用 vitest 的 mock 函数
|
||||
|
||||
await store.closeAllTabs(router);
|
||||
|
||||
expect(store.tabs.length).toBe(0); // 假设没有固定的标签页
|
||||
// expect(router.replace).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns all tabs including affix tabs', () => {
|
||||
const store = useTabsStore();
|
||||
store.tabs = [
|
||||
{ fullPath: '/home', meta: {}, name: 'Home', path: '/home' },
|
||||
] as any;
|
||||
store.affixTabs = [
|
||||
{ meta: { hideInTab: false }, path: '/dashboard' },
|
||||
] as any;
|
||||
|
||||
const result = store.getTabs;
|
||||
expect(result.length).toBe(2);
|
||||
expect(result.find((tab) => tab.path === '/dashboard')).toBeDefined();
|
||||
});
|
||||
|
||||
it('closes a non-affix tab', () => {
|
||||
const store = useTabsStore();
|
||||
const tab: any = {
|
||||
fullPath: '/closable',
|
||||
meta: {},
|
||||
name: 'Closable',
|
||||
path: '/closable',
|
||||
};
|
||||
store.tabs.push(tab);
|
||||
store._close(tab);
|
||||
expect(store.tabs.length).toBe(0);
|
||||
});
|
||||
|
||||
it('does not close an affix tab', () => {
|
||||
const store = useTabsStore();
|
||||
const affixTab: any = {
|
||||
fullPath: '/affix',
|
||||
meta: { affixTab: true },
|
||||
name: 'Affix',
|
||||
path: '/affix',
|
||||
};
|
||||
store.tabs.push(affixTab);
|
||||
store._close(affixTab);
|
||||
expect(store.tabs.length).toBe(1); // Affix tab should not be closed
|
||||
});
|
||||
|
||||
it('returns all cache tabs', () => {
|
||||
const store = useTabsStore();
|
||||
store.cacheTabs.add('Home');
|
||||
store.cacheTabs.add('About');
|
||||
expect(store.getCacheTabs).toEqual(['Home', 'About']);
|
||||
});
|
||||
|
||||
it('returns all tabs, including affix tabs', () => {
|
||||
const store = useTabsStore();
|
||||
const normalTab: any = {
|
||||
fullPath: '/normal',
|
||||
meta: {},
|
||||
name: 'Normal',
|
||||
path: '/normal',
|
||||
};
|
||||
const affixTab: any = {
|
||||
fullPath: '/affix',
|
||||
meta: { affixTab: true },
|
||||
name: 'Affix',
|
||||
path: '/affix',
|
||||
};
|
||||
store.tabs.push(normalTab);
|
||||
store.affixTabs.push(affixTab);
|
||||
expect(store.getTabs).toContainEqual(normalTab);
|
||||
// expect(store.getTabs).toContainEqual(affixTab);
|
||||
});
|
||||
|
||||
it('navigates to a specific tab', async () => {
|
||||
const store = useTabsStore();
|
||||
const tab: any = { meta: {}, name: 'Dashboard', path: '/dashboard' };
|
||||
|
||||
await store._goToTab(tab, router);
|
||||
|
||||
expect(router.replace).toHaveBeenCalledWith({
|
||||
params: {},
|
||||
path: '/dashboard',
|
||||
query: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('closes multiple tabs by paths', async () => {
|
||||
const store = useTabsStore();
|
||||
store.addTab({
|
||||
fullPath: '/home',
|
||||
meta: {},
|
||||
name: 'Home',
|
||||
path: '/home',
|
||||
} as any);
|
||||
store.addTab({
|
||||
fullPath: '/about',
|
||||
meta: {},
|
||||
name: 'About',
|
||||
path: '/about',
|
||||
} as any);
|
||||
store.addTab({
|
||||
fullPath: '/contact',
|
||||
meta: {},
|
||||
name: 'Contact',
|
||||
path: '/contact',
|
||||
} as any);
|
||||
|
||||
await store._bulkCloseByPaths(['/home', '/contact']);
|
||||
|
||||
expect(store.tabs).toHaveLength(1);
|
||||
expect(store.tabs[0].name).toBe('About');
|
||||
});
|
||||
|
||||
it('closes all tabs to the left of the specified tab', async () => {
|
||||
const store = useTabsStore();
|
||||
store.addTab({
|
||||
fullPath: '/home',
|
||||
meta: {},
|
||||
name: 'Home',
|
||||
path: '/home',
|
||||
} as any);
|
||||
store.addTab({
|
||||
fullPath: '/about',
|
||||
meta: {},
|
||||
name: 'About',
|
||||
path: '/about',
|
||||
} as any);
|
||||
const targetTab: any = {
|
||||
fullPath: '/contact',
|
||||
meta: {},
|
||||
name: 'Contact',
|
||||
path: '/contact',
|
||||
};
|
||||
store.addTab(targetTab);
|
||||
|
||||
await store.closeLeftTabs(targetTab);
|
||||
|
||||
expect(store.tabs).toHaveLength(1);
|
||||
expect(store.tabs[0].name).toBe('Contact');
|
||||
});
|
||||
|
||||
it('closes all tabs except the specified tab', async () => {
|
||||
const store = useTabsStore();
|
||||
store.addTab({
|
||||
fullPath: '/home',
|
||||
meta: {},
|
||||
name: 'Home',
|
||||
path: '/home',
|
||||
} as any);
|
||||
const targetTab: any = {
|
||||
fullPath: '/about',
|
||||
meta: {},
|
||||
name: 'About',
|
||||
path: '/about',
|
||||
};
|
||||
store.addTab(targetTab);
|
||||
store.addTab({
|
||||
fullPath: '/contact',
|
||||
meta: {},
|
||||
name: 'Contact',
|
||||
path: '/contact',
|
||||
} as any);
|
||||
|
||||
await store.closeOtherTabs(targetTab);
|
||||
|
||||
expect(store.tabs).toHaveLength(1);
|
||||
expect(store.tabs[0].name).toBe('About');
|
||||
});
|
||||
|
||||
it('closes all tabs to the right of the specified tab', async () => {
|
||||
const store = useTabsStore();
|
||||
const targetTab: any = {
|
||||
fullPath: '/home',
|
||||
meta: {},
|
||||
name: 'Home',
|
||||
path: '/home',
|
||||
};
|
||||
store.addTab(targetTab);
|
||||
store.addTab({
|
||||
fullPath: '/about',
|
||||
meta: {},
|
||||
name: 'About',
|
||||
path: '/about',
|
||||
} as any);
|
||||
store.addTab({
|
||||
fullPath: '/contact',
|
||||
meta: {},
|
||||
name: 'Contact',
|
||||
path: '/contact',
|
||||
} as any);
|
||||
|
||||
await store.closeRightTabs(targetTab);
|
||||
|
||||
expect(store.tabs).toHaveLength(1);
|
||||
expect(store.tabs[0].name).toBe('Home');
|
||||
});
|
||||
|
||||
it('closes the tab with the specified key', async () => {
|
||||
const store = useTabsStore();
|
||||
const keyToClose = '/about';
|
||||
store.addTab({
|
||||
fullPath: '/home',
|
||||
meta: {},
|
||||
name: 'Home',
|
||||
path: '/home',
|
||||
} as any);
|
||||
store.addTab({
|
||||
fullPath: keyToClose,
|
||||
meta: {},
|
||||
name: 'About',
|
||||
path: '/about',
|
||||
} as any);
|
||||
store.addTab({
|
||||
fullPath: '/contact',
|
||||
meta: {},
|
||||
name: 'Contact',
|
||||
path: '/contact',
|
||||
} as any);
|
||||
|
||||
await store.closeTabByKey(keyToClose, router);
|
||||
|
||||
expect(store.tabs).toHaveLength(2);
|
||||
expect(
|
||||
store.tabs.find((tab) => tab.fullPath === keyToClose),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('refreshes the current tab', async () => {
|
||||
const store = useTabsStore();
|
||||
const currentTab: any = {
|
||||
fullPath: '/dashboard',
|
||||
meta: { name: 'Dashboard' },
|
||||
name: 'Dashboard',
|
||||
path: '/dashboard',
|
||||
};
|
||||
router.currentRoute.value = currentTab;
|
||||
|
||||
await store.refreshTab(router);
|
||||
|
||||
expect(store.excludeCacheTabs.has('Dashboard')).toBe(false);
|
||||
expect(store.renderRouteView).toBe(true);
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user