feat: 项目结构重构优化

This commit is contained in:
Ray.Hao
2025-12-26 12:35:37 +08:00
parent 65ad4fe59f
commit aa374dd2ba
164 changed files with 11305 additions and 3103 deletions

52
tests/setup.ts Normal file
View File

@@ -0,0 +1,52 @@
/**
* 测试环境全局配置
*/
import { vi } from "vitest";
// Mock window.matchMedia
Object.defineProperty(window, "matchMedia", {
writable: true,
value: vi.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
// Mock IntersectionObserver
global.IntersectionObserver = class IntersectionObserver {
constructor() {}
disconnect() {}
observe() {}
takeRecords() {
return [];
}
unobserve() {}
} as any;
// Mock ResizeObserver
global.ResizeObserver = class ResizeObserver {
constructor() {}
disconnect() {}
observe() {}
unobserve() {}
} as any;
// Mock Element.scrollIntoView
Element.prototype.scrollIntoView = vi.fn();
// Mock console methods to reduce noise in tests
global.console = {
...console,
log: vi.fn(),
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
};

View File

@@ -0,0 +1,114 @@
import { describe, it, expect, beforeEach } from "vitest";
import { mount } from "@vue/test-utils";
import Pagination from "@/components/Pagination/index.vue";
import ElementPlus from "element-plus";
describe("Pagination 组件", () => {
const createWrapper = (props = {}) => {
return mount(Pagination, {
props: {
page: 1,
limit: 10,
total: 100,
...props,
},
global: {
plugins: [ElementPlus],
},
});
};
describe("渲染", () => {
it("应该正确渲染分页组件", () => {
const wrapper = createWrapper();
expect(wrapper.exists()).toBe(true);
expect(wrapper.find(".pagination").exists()).toBe(true);
});
it("应该在 hidden 为 true 时隐藏分页", () => {
const wrapper = createWrapper({ hidden: true });
expect(wrapper.find(".pagination").classes()).toContain("hidden");
});
it("应该在 hidden 为 false 时显示分页", () => {
const wrapper = createWrapper({ hidden: false });
expect(wrapper.find(".pagination").classes()).not.toContain("hidden");
});
});
describe("Props", () => {
it("应该接收 total 属性", () => {
const wrapper = createWrapper({ total: 200 });
expect(wrapper.props("total")).toBe(200);
});
it("应该接收 pageSizes 属性", () => {
const pageSizes = [5, 10, 20, 50];
const wrapper = createWrapper({ pageSizes });
expect(wrapper.props("pageSizes")).toEqual(pageSizes);
});
it("应该使用默认的 pageSizes", () => {
const wrapper = createWrapper();
expect(wrapper.props("pageSizes")).toEqual([10, 20, 30, 50]);
});
it("应该接收 layout 属性", () => {
const layout = "prev, pager, next";
const wrapper = createWrapper({ layout });
expect(wrapper.props("layout")).toBe(layout);
});
it("应该接收 background 属性", () => {
const wrapper = createWrapper({ background: false });
expect(wrapper.props("background")).toBe(false);
});
});
describe("事件", () => {
it("应该在页码改变时触发 pagination 事件", async () => {
const wrapper = createWrapper();
// 模拟页码改变
await wrapper.vm.handleCurrentChange(2);
expect(wrapper.emitted("pagination")).toBeTruthy();
expect(wrapper.emitted("pagination")?.[0]).toEqual([{ page: 2, limit: 10 }]);
});
it("应该在每页条数改变时触发 pagination 事件", async () => {
const wrapper = createWrapper();
// 模拟每页条数改变
await wrapper.vm.handleSizeChange(20);
expect(wrapper.emitted("pagination")).toBeTruthy();
expect(wrapper.emitted("pagination")?.[0]).toEqual([{ page: 1, limit: 20 }]);
});
it("应该在每页条数改变时重置页码为 1", async () => {
const wrapper = createWrapper({ page: 3 });
await wrapper.vm.handleSizeChange(20);
expect(wrapper.emitted("pagination")?.[0]).toEqual([{ page: 1, limit: 20 }]);
});
});
describe("边界情况", () => {
it("应该在 total 为 0 时正常工作", () => {
const wrapper = createWrapper({ total: 0 });
expect(wrapper.exists()).toBe(true);
});
it("应该在当前页超出范围时自动调整", async () => {
const wrapper = createWrapper({ page: 5, limit: 10, total: 100 });
// 修改 total 使当前页超出范围
await wrapper.setProps({ total: 20 });
// 应该触发 pagination 事件,页码调整为最后一页
expect(wrapper.emitted("pagination")).toBeTruthy();
});
});
});

View File

@@ -0,0 +1,62 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { useDictSync } from "@/composables/websocket/useDictSync";
import { useDictStore } from "@/store/modules/dict";
import { setActivePinia, createPinia } from "pinia";
// Mock useStomp
vi.mock("@/composables/websocket/useStomp", () => ({
useStomp: vi.fn(() => ({
isConnected: ref(false),
connect: vi.fn(),
disconnect: vi.fn(),
subscribe: vi.fn(() => "sub-id"),
unsubscribe: vi.fn(),
})),
}));
describe("useDictSync", () => {
beforeEach(() => {
setActivePinia(createPinia());
localStorage.clear();
vi.clearAllMocks();
});
describe("初始化", () => {
it("应该创建字典同步实例", () => {
const dictSync = useDictSync();
expect(dictSync).toBeDefined();
expect(dictSync.initialize).toBeDefined();
expect(dictSync.cleanup).toBeDefined();
});
it("应该初始化 WebSocket 连接", () => {
const dictSync = useDictSync();
dictSync.initialize();
// 验证初始化逻辑(具体实现取决于 useDictSync 的内部逻辑)
expect(dictSync).toBeDefined();
});
});
describe("清理", () => {
it("应该清理 WebSocket 连接", () => {
const dictSync = useDictSync();
dictSync.initialize();
dictSync.cleanup();
// 验证清理逻辑
expect(dictSync).toBeDefined();
});
});
describe("字典同步", () => {
it("应该处理字典更新消息", () => {
const dictSync = useDictSync();
const dictStore = useDictStore();
dictSync.initialize();
// 模拟接收字典更新消息
// 注意:这里需要根据实际的消息格式来测试
expect(dictStore).toBeDefined();
});
});
});

View File

@@ -0,0 +1,88 @@
import { describe, it, expect, beforeEach } from "vitest";
import { useTableSelection } from "@/composables/table/useTableSelection";
describe("useTableSelection", () => {
let selection: ReturnType<typeof useTableSelection>;
beforeEach(() => {
selection = useTableSelection();
});
it("应该初始化为空数组", () => {
expect(selection.selectedIds.value).toEqual([]);
expect(selection.selectedRows.value).toEqual([]);
});
describe("handleSelectionChange()", () => {
it("应该更新选中的行", () => {
const rows = [
{ id: 1, name: "张三" },
{ id: 2, name: "李四" },
];
selection.handleSelectionChange(rows);
expect(selection.selectedRows.value).toEqual(rows);
expect(selection.selectedIds.value).toEqual([1, 2]);
});
it("应该处理空数组", () => {
selection.handleSelectionChange([]);
expect(selection.selectedRows.value).toEqual([]);
expect(selection.selectedIds.value).toEqual([]);
});
it("应该支持自定义 ID 字段", () => {
const customSelection = useTableSelection("userId");
const rows = [
{ userId: "u1", name: "张三" },
{ userId: "u2", name: "李四" },
];
customSelection.handleSelectionChange(rows);
expect(customSelection.selectedIds.value).toEqual(["u1", "u2"]);
});
});
describe("clearSelection()", () => {
it("应该清空选中项", () => {
const rows = [{ id: 1, name: "张三" }];
selection.handleSelectionChange(rows);
selection.clearSelection();
expect(selection.selectedIds.value).toEqual([]);
expect(selection.selectedRows.value).toEqual([]);
});
});
describe("hasSelection", () => {
it("有选中项时应该返回 true", () => {
const rows = [{ id: 1, name: "张三" }];
selection.handleSelectionChange(rows);
expect(selection.hasSelection.value).toBe(true);
});
it("无选中项时应该返回 false", () => {
expect(selection.hasSelection.value).toBe(false);
});
});
describe("selectionCount", () => {
it("应该返回选中项数量", () => {
expect(selection.selectionCount.value).toBe(0);
const rows = [
{ id: 1, name: "张三" },
{ id: 2, name: "李四" },
{ id: 3, name: "王五" },
];
selection.handleSelectionChange(rows);
expect(selection.selectionCount.value).toBe(3);
});
});
});

View File

@@ -0,0 +1,141 @@
import { describe, it, expect, beforeEach } from "vitest";
import { setActivePinia, createPinia } from "pinia";
import { useAppStore } from "@/store/modules/app";
import { DeviceEnum, SidebarStatus } from "@/enums";
describe("useAppStore", () => {
beforeEach(() => {
setActivePinia(createPinia());
localStorage.clear();
});
describe("侧边栏状态", () => {
it("应该切换侧边栏状态", () => {
const store = useAppStore();
const initialState = store.sidebar.opened;
store.toggleSidebar();
expect(store.sidebar.opened).toBe(!initialState);
store.toggleSidebar();
expect(store.sidebar.opened).toBe(initialState);
});
it("应该关闭侧边栏", () => {
const store = useAppStore();
store.openSideBar();
expect(store.sidebar.opened).toBe(true);
store.closeSideBar();
expect(store.sidebar.opened).toBe(false);
});
it("应该打开侧边栏", () => {
const store = useAppStore();
store.closeSideBar();
expect(store.sidebar.opened).toBe(false);
store.openSideBar();
expect(store.sidebar.opened).toBe(true);
});
it("应该持久化侧边栏状态", () => {
const store = useAppStore();
store.openSideBar();
// 创建新的 store 实例模拟页面刷新
const newStore = useAppStore();
expect(newStore.sidebar.opened).toBe(true);
});
});
describe("设备类型", () => {
it("应该切换设备类型", () => {
const store = useAppStore();
store.toggleDevice(DeviceEnum.MOBILE);
expect(store.device).toBe(DeviceEnum.MOBILE);
store.toggleDevice(DeviceEnum.DESKTOP);
expect(store.device).toBe(DeviceEnum.DESKTOP);
});
it("应该持久化设备类型", () => {
const store = useAppStore();
store.toggleDevice(DeviceEnum.MOBILE);
const newStore = useAppStore();
expect(newStore.device).toBe(DeviceEnum.MOBILE);
});
});
describe("组件尺寸", () => {
it("应该修改组件尺寸", () => {
const store = useAppStore();
store.changeSize("large");
expect(store.size).toBe("large");
store.changeSize("small");
expect(store.size).toBe("small");
});
it("应该持久化组件尺寸", () => {
const store = useAppStore();
store.changeSize("large");
const newStore = useAppStore();
expect(newStore.size).toBe("large");
});
});
describe("语言设置", () => {
it("应该修改语言", () => {
const store = useAppStore();
store.changeLanguage("en");
expect(store.language).toBe("en");
store.changeLanguage("zh-cn");
expect(store.language).toBe("zh-cn");
});
it("应该根据语言返回正确的 locale", () => {
const store = useAppStore();
store.changeLanguage("en");
expect(store.locale).toBeDefined();
store.changeLanguage("zh-cn");
expect(store.locale).toBeDefined();
});
it("应该持久化语言设置", () => {
const store = useAppStore();
store.changeLanguage("en");
const newStore = useAppStore();
expect(newStore.language).toBe("en");
});
});
describe("顶部菜单", () => {
it("应该激活顶部菜单", () => {
const store = useAppStore();
store.activeTopMenu("/dashboard");
expect(store.activeTopMenuPath).toBe("/dashboard");
store.activeTopMenu("/system");
expect(store.activeTopMenuPath).toBe("/system");
});
it("应该持久化顶部菜单路径", () => {
const store = useAppStore();
store.activeTopMenu("/dashboard");
const newStore = useAppStore();
expect(newStore.activeTopMenuPath).toBe("/dashboard");
});
});
});

View File

@@ -0,0 +1,134 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { setActivePinia, createPinia } from "pinia";
import { useDictStore } from "@/store/modules/dict";
import DictAPI from "@/api/system/dict";
import type { DictItemOption } from "@/types/api";
// Mock DictAPI
vi.mock("@/api/system/dict", () => ({
default: {
getDictItems: vi.fn(),
},
}));
describe("useDictStore", () => {
beforeEach(() => {
// 创建新的 Pinia 实例
setActivePinia(createPinia());
// 清理 localStorage
localStorage.clear();
// 重置所有 mock
vi.clearAllMocks();
});
describe("字典缓存", () => {
it("应该缓存字典数据", async () => {
const store = useDictStore();
const mockData: DictItemOption[] = [
{ value: "1", label: "选项1" },
{ value: "2", label: "选项2" },
];
vi.mocked(DictAPI.getDictItems).mockResolvedValue(mockData);
await store.loadDictItems("test_dict");
expect(store.getDictItems("test_dict")).toEqual(mockData);
expect(DictAPI.getDictItems).toHaveBeenCalledWith("test_dict");
expect(DictAPI.getDictItems).toHaveBeenCalledTimes(1);
});
it("应该从缓存中获取字典数据,不重复请求", async () => {
const store = useDictStore();
const mockData: DictItemOption[] = [{ value: "1", label: "选项1" }];
vi.mocked(DictAPI.getDictItems).mockResolvedValue(mockData);
// 第一次加载
await store.loadDictItems("test_dict");
// 第二次加载(应该从缓存获取)
await store.loadDictItems("test_dict");
expect(DictAPI.getDictItems).toHaveBeenCalledTimes(1);
expect(store.getDictItems("test_dict")).toEqual(mockData);
});
it("应该防止并发重复请求", async () => {
const store = useDictStore();
const mockData: DictItemOption[] = [{ value: "1", label: "选项1" }];
vi.mocked(DictAPI.getDictItems).mockResolvedValue(mockData);
// 同时发起多个请求
const promises = [
store.loadDictItems("test_dict"),
store.loadDictItems("test_dict"),
store.loadDictItems("test_dict"),
];
await Promise.all(promises);
// 应该只请求一次
expect(DictAPI.getDictItems).toHaveBeenCalledTimes(1);
});
});
describe("字典操作", () => {
it("应该返回空数组当字典不存在时", () => {
const store = useDictStore();
expect(store.getDictItems("non_exist")).toEqual([]);
});
it("应该移除指定字典项", async () => {
const store = useDictStore();
const mockData: DictItemOption[] = [{ value: "1", label: "选项1" }];
vi.mocked(DictAPI.getDictItems).mockResolvedValue(mockData);
await store.loadDictItems("test_dict");
expect(store.getDictItems("test_dict")).toEqual(mockData);
store.removeDictItem("test_dict");
expect(store.getDictItems("test_dict")).toEqual([]);
});
it("应该清空所有字典缓存", async () => {
const store = useDictStore();
const mockData1: DictItemOption[] = [{ value: "1", label: "选项1" }];
const mockData2: DictItemOption[] = [{ value: "2", label: "选项2" }];
vi.mocked(DictAPI.getDictItems)
.mockResolvedValueOnce(mockData1)
.mockResolvedValueOnce(mockData2);
await store.loadDictItems("dict1");
await store.loadDictItems("dict2");
expect(store.getDictItems("dict1")).toEqual(mockData1);
expect(store.getDictItems("dict2")).toEqual(mockData2);
store.clearDictCache();
expect(store.getDictItems("dict1")).toEqual([]);
expect(store.getDictItems("dict2")).toEqual([]);
});
});
describe("错误处理", () => {
it("应该处理请求失败的情况", async () => {
const store = useDictStore();
const error = new Error("Network error");
vi.mocked(DictAPI.getDictItems).mockRejectedValue(error);
await expect(store.loadDictItems("test_dict")).rejects.toThrow("Network error");
// 失败后应该允许重试
const mockData: DictItemOption[] = [{ value: "1", label: "选项1" }];
vi.mocked(DictAPI.getDictItems).mockResolvedValue(mockData);
await store.loadDictItems("test_dict");
expect(store.getDictItems("test_dict")).toEqual(mockData);
});
});
});

View File

@@ -0,0 +1,186 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { setActivePinia, createPinia } from "pinia";
import { useSettingsStore } from "@/store/modules/settings";
import { ThemeMode, SidebarColor } from "@/enums";
// Mock theme utils
vi.mock("@/utils/theme", () => ({
applyTheme: vi.fn(),
generateThemeColors: vi.fn(() => ({})),
toggleDarkMode: vi.fn(),
toggleSidebarColor: vi.fn(),
}));
describe("useSettingsStore", () => {
beforeEach(() => {
setActivePinia(createPinia());
localStorage.clear();
vi.clearAllMocks();
});
describe("界面显示设置", () => {
it("应该切换标签页显示", () => {
const store = useSettingsStore();
const initial = store.showTagsView;
store.showTagsView = !initial;
expect(store.showTagsView).toBe(!initial);
});
it("应该切换 Logo 显示", () => {
const store = useSettingsStore();
const initial = store.showAppLogo;
store.showAppLogo = !initial;
expect(store.showAppLogo).toBe(!initial);
});
it("应该切换水印显示", () => {
const store = useSettingsStore();
const initial = store.showWatermark;
store.showWatermark = !initial;
expect(store.showWatermark).toBe(!initial);
});
it("应该持久化界面显示设置", () => {
const store = useSettingsStore();
store.showTagsView = false;
store.showAppLogo = false;
store.showWatermark = true;
const newStore = useSettingsStore();
expect(newStore.showTagsView).toBe(false);
expect(newStore.showAppLogo).toBe(false);
expect(newStore.showWatermark).toBe(true);
});
});
describe("主题设置", () => {
it("应该切换主题模式", () => {
const store = useSettingsStore();
store.theme = ThemeMode.DARK;
expect(store.theme).toBe(ThemeMode.DARK);
store.theme = ThemeMode.LIGHT;
expect(store.theme).toBe(ThemeMode.LIGHT);
});
it("应该修改主题颜色", () => {
const store = useSettingsStore();
store.themeColor = "#409EFF";
expect(store.themeColor).toBe("#409EFF");
store.themeColor = "#67C23A";
expect(store.themeColor).toBe("#67C23A");
});
it("应该持久化主题设置", () => {
const store = useSettingsStore();
store.theme = ThemeMode.DARK;
store.themeColor = "#409EFF";
const newStore = useSettingsStore();
expect(newStore.theme).toBe(ThemeMode.DARK);
expect(newStore.themeColor).toBe("#409EFF");
});
});
describe("侧边栏配色", () => {
it("应该切换侧边栏配色方案", () => {
const store = useSettingsStore();
store.sidebarColorScheme = SidebarColor.CLASSIC_BLUE;
expect(store.sidebarColorScheme).toBe(SidebarColor.CLASSIC_BLUE);
store.sidebarColorScheme = SidebarColor.THEME_COLOR;
expect(store.sidebarColorScheme).toBe(SidebarColor.THEME_COLOR);
});
it("应该持久化侧边栏配色", () => {
const store = useSettingsStore();
store.sidebarColorScheme = SidebarColor.CLASSIC_BLUE;
const newStore = useSettingsStore();
expect(newStore.sidebarColorScheme).toBe(SidebarColor.CLASSIC_BLUE);
});
});
describe("特殊模式", () => {
it("应该切换灰色模式", () => {
const store = useSettingsStore();
store.grayMode = true;
expect(store.grayMode).toBe(true);
store.grayMode = false;
expect(store.grayMode).toBe(false);
});
it("应该切换色弱模式", () => {
const store = useSettingsStore();
store.colorWeak = true;
expect(store.colorWeak).toBe(true);
store.colorWeak = false;
expect(store.colorWeak).toBe(false);
});
it("应该持久化特殊模式", () => {
const store = useSettingsStore();
store.grayMode = true;
store.colorWeak = true;
const newStore = useSettingsStore();
expect(newStore.grayMode).toBe(true);
expect(newStore.colorWeak).toBe(true);
});
});
describe("AI 助手", () => {
it("应该切换 AI 助手", () => {
const store = useSettingsStore();
store.userEnableAi = true;
expect(store.userEnableAi).toBe(true);
store.userEnableAi = false;
expect(store.userEnableAi).toBe(false);
});
it("应该持久化 AI 助手设置", () => {
const store = useSettingsStore();
store.userEnableAi = true;
const newStore = useSettingsStore();
expect(newStore.userEnableAi).toBe(true);
});
});
describe("重置设置", () => {
it("应该重置所有设置为默认值", () => {
const store = useSettingsStore();
// 修改所有设置
store.showTagsView = false;
store.showAppLogo = false;
store.showWatermark = false;
store.userEnableAi = true;
store.grayMode = true;
store.colorWeak = true;
store.theme = ThemeMode.DARK;
store.themeColor = "#409EFF";
// 重置
store.resetSettings();
// 验证已重置(具体值取决于 defaults
expect(store.userEnableAi).toBe(false);
expect(store.grayMode).toBe(false);
expect(store.colorWeak).toBe(false);
});
});
});

View File

@@ -0,0 +1,157 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { AuthStorage, hasPerm } from "@/utils/auth";
import { Storage } from "@/utils/storage";
import { STORAGE_KEYS, ROLE_ROOT } from "@/constants";
// Mock Storage
vi.mock("@/utils/storage", () => ({
Storage: {
get: vi.fn(),
set: vi.fn(),
remove: vi.fn(),
sessionGet: vi.fn(),
sessionSet: vi.fn(),
sessionRemove: vi.fn(),
},
}));
// Mock useUserStoreHook
vi.mock("@/store/modules/user", () => ({
useUserStoreHook: vi.fn(() => ({
userInfo: {
roles: ["admin"],
perms: ["sys:user:create", "sys:user:update"],
},
})),
}));
describe("Auth 工具函数", () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe("AuthStorage", () => {
describe("getAccessToken()", () => {
it("记住我为 true 时应该从 localStorage 获取", () => {
vi.mocked(Storage.get).mockReturnValueOnce(true); // rememberMe
vi.mocked(Storage.get).mockReturnValueOnce("token123"); // accessToken
const token = AuthStorage.getAccessToken();
expect(Storage.get).toHaveBeenCalledWith(STORAGE_KEYS.REMEMBER_ME, false);
expect(Storage.get).toHaveBeenCalledWith(STORAGE_KEYS.ACCESS_TOKEN, "");
expect(token).toBe("token123");
});
it("记住我为 false 时应该从 sessionStorage 获取", () => {
vi.mocked(Storage.get).mockReturnValueOnce(false); // rememberMe
vi.mocked(Storage.sessionGet).mockReturnValueOnce("session-token");
const token = AuthStorage.getAccessToken();
expect(Storage.sessionGet).toHaveBeenCalledWith(STORAGE_KEYS.ACCESS_TOKEN, "");
expect(token).toBe("session-token");
});
});
describe("getRefreshToken()", () => {
it("记住我为 true 时应该从 localStorage 获取", () => {
vi.mocked(Storage.get).mockReturnValueOnce(true);
vi.mocked(Storage.get).mockReturnValueOnce("refresh123");
const token = AuthStorage.getRefreshToken();
expect(token).toBe("refresh123");
});
it("记住我为 false 时应该从 sessionStorage 获取", () => {
vi.mocked(Storage.get).mockReturnValueOnce(false);
vi.mocked(Storage.sessionGet).mockReturnValueOnce("session-refresh");
const token = AuthStorage.getRefreshToken();
expect(token).toBe("session-refresh");
});
});
describe("setTokens()", () => {
it("记住我为 true 时应该存储到 localStorage", () => {
AuthStorage.setTokens("access123", "refresh123", true);
expect(Storage.set).toHaveBeenCalledWith(STORAGE_KEYS.REMEMBER_ME, true);
expect(Storage.set).toHaveBeenCalledWith(STORAGE_KEYS.ACCESS_TOKEN, "access123");
expect(Storage.set).toHaveBeenCalledWith(STORAGE_KEYS.REFRESH_TOKEN, "refresh123");
});
it("记住我为 false 时应该存储到 sessionStorage", () => {
AuthStorage.setTokens("access123", "refresh123", false);
expect(Storage.set).toHaveBeenCalledWith(STORAGE_KEYS.REMEMBER_ME, false);
expect(Storage.sessionSet).toHaveBeenCalledWith(STORAGE_KEYS.ACCESS_TOKEN, "access123");
expect(Storage.sessionSet).toHaveBeenCalledWith(STORAGE_KEYS.REFRESH_TOKEN, "refresh123");
expect(Storage.remove).toHaveBeenCalledWith(STORAGE_KEYS.ACCESS_TOKEN);
expect(Storage.remove).toHaveBeenCalledWith(STORAGE_KEYS.REFRESH_TOKEN);
});
});
describe("clearAuth()", () => {
it("应该清理所有认证信息", () => {
AuthStorage.clearAuth();
expect(Storage.remove).toHaveBeenCalledWith(STORAGE_KEYS.ACCESS_TOKEN);
expect(Storage.remove).toHaveBeenCalledWith(STORAGE_KEYS.REFRESH_TOKEN);
expect(Storage.sessionRemove).toHaveBeenCalledWith(STORAGE_KEYS.ACCESS_TOKEN);
expect(Storage.sessionRemove).toHaveBeenCalledWith(STORAGE_KEYS.REFRESH_TOKEN);
});
});
describe("getRememberMe()", () => {
it("应该获取记住我状态", () => {
vi.mocked(Storage.get).mockReturnValueOnce(true);
const rememberMe = AuthStorage.getRememberMe();
expect(Storage.get).toHaveBeenCalledWith(STORAGE_KEYS.REMEMBER_ME, false);
expect(rememberMe).toBe(true);
});
});
});
describe("hasPerm()", () => {
it("超级管理员应该拥有所有按钮权限", () => {
const { useUserStoreHook } = await import("@/store/modules/user");
vi.mocked(useUserStoreHook).mockReturnValueOnce({
userInfo: {
roles: [ROLE_ROOT],
perms: [],
},
} as any);
expect(hasPerm("any:permission", "button")).toBe(true);
});
it("应该验证单个按钮权限", () => {
expect(hasPerm("sys:user:create", "button")).toBe(true);
expect(hasPerm("sys:user:delete", "button")).toBe(false);
});
it("应该验证多个按钮权限(或关系)", () => {
expect(hasPerm(["sys:user:create", "sys:user:delete"], "button")).toBe(true);
expect(hasPerm(["sys:user:delete", "sys:user:export"], "button")).toBe(false);
});
it("应该验证角色权限", () => {
expect(hasPerm("admin", "role")).toBe(true);
expect(hasPerm("user", "role")).toBe(false);
});
it("用户信息不完整时应该返回 false", () => {
const { useUserStoreHook } = await import("@/store/modules/user");
vi.mocked(useUserStoreHook).mockReturnValueOnce({
userInfo: {},
} as any);
expect(hasPerm("any:permission")).toBe(false);
});
});
});

View File

@@ -0,0 +1,136 @@
import { describe, it, expect } from "vitest";
import { formatGrowthRate, formatFileSize, formatNumber, formatCurrency } from "@/utils/format";
describe("Format 工具函数", () => {
describe("formatGrowthRate()", () => {
it("应该格式化正增长率", () => {
expect(formatGrowthRate(0.25)).toBe("+25.00%");
expect(formatGrowthRate(0.5)).toBe("+50.00%");
expect(formatGrowthRate(1.5)).toBe("+150.00%");
});
it("应该格式化负增长率", () => {
expect(formatGrowthRate(-0.25)).toBe("-25.00%");
expect(formatGrowthRate(-0.5)).toBe("-50.00%");
});
it("应该格式化零增长率", () => {
expect(formatGrowthRate(0)).toBe("0.00%");
});
it("应该处理小数精度", () => {
expect(formatGrowthRate(0.12345)).toBe("+12.35%");
expect(formatGrowthRate(0.12344)).toBe("+12.34%");
});
it("应该处理 null 和 undefined", () => {
expect(formatGrowthRate(null as any)).toBe("0.00%");
expect(formatGrowthRate(undefined as any)).toBe("0.00%");
});
});
describe("formatFileSize()", () => {
it("应该格式化字节", () => {
expect(formatFileSize(0)).toBe("0 B");
expect(formatFileSize(100)).toBe("100 B");
expect(formatFileSize(1023)).toBe("1023 B");
});
it("应该格式化 KB", () => {
expect(formatFileSize(1024)).toBe("1.00 KB");
expect(formatFileSize(1536)).toBe("1.50 KB");
expect(formatFileSize(10240)).toBe("10.00 KB");
});
it("应该格式化 MB", () => {
expect(formatFileSize(1048576)).toBe("1.00 MB");
expect(formatFileSize(5242880)).toBe("5.00 MB");
});
it("应该格式化 GB", () => {
expect(formatFileSize(1073741824)).toBe("1.00 GB");
expect(formatFileSize(5368709120)).toBe("5.00 GB");
});
it("应该格式化 TB", () => {
expect(formatFileSize(1099511627776)).toBe("1.00 TB");
});
it("应该处理负数", () => {
expect(formatFileSize(-1024)).toBe("0 B");
});
it("应该处理 null 和 undefined", () => {
expect(formatFileSize(null as any)).toBe("0 B");
expect(formatFileSize(undefined as any)).toBe("0 B");
});
});
describe("formatNumber()", () => {
it("应该格式化整数", () => {
expect(formatNumber(1000)).toBe("1,000");
expect(formatNumber(1000000)).toBe("1,000,000");
expect(formatNumber(123456789)).toBe("123,456,789");
});
it("应该格式化小数", () => {
expect(formatNumber(1000.5)).toBe("1,000.5");
expect(formatNumber(1234.56)).toBe("1,234.56");
});
it("应该处理小数位数", () => {
expect(formatNumber(1234.5678, 2)).toBe("1,234.57");
expect(formatNumber(1234.5, 2)).toBe("1,234.50");
expect(formatNumber(1234, 2)).toBe("1,234.00");
});
it("应该处理零", () => {
expect(formatNumber(0)).toBe("0");
expect(formatNumber(0, 2)).toBe("0.00");
});
it("应该处理负数", () => {
expect(formatNumber(-1000)).toBe("-1,000");
expect(formatNumber(-1234.56, 2)).toBe("-1,234.56");
});
it("应该处理 null 和 undefined", () => {
expect(formatNumber(null as any)).toBe("0");
expect(formatNumber(undefined as any)).toBe("0");
});
});
describe("formatCurrency()", () => {
it("应该格式化货币(默认人民币)", () => {
expect(formatCurrency(1000)).toBe("¥1,000.00");
expect(formatCurrency(1234.56)).toBe("¥1,234.56");
expect(formatCurrency(1000000)).toBe("¥1,000,000.00");
});
it("应该格式化美元", () => {
expect(formatCurrency(1000, "USD")).toBe("$1,000.00");
expect(formatCurrency(1234.56, "USD")).toBe("$1,234.56");
});
it("应该格式化欧元", () => {
expect(formatCurrency(1000, "EUR")).toBe("€1,000.00");
});
it("应该格式化日元", () => {
expect(formatCurrency(1000, "JPY")).toBe("¥1,000");
});
it("应该处理零", () => {
expect(formatCurrency(0)).toBe("¥0.00");
});
it("应该处理负数", () => {
expect(formatCurrency(-1000)).toBe("-¥1,000.00");
});
it("应该处理 null 和 undefined", () => {
expect(formatCurrency(null as any)).toBe("¥0.00");
expect(formatCurrency(undefined as any)).toBe("¥0.00");
});
});
});

View File

@@ -0,0 +1,150 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { Storage } from "@/utils/storage";
import { STORAGE_KEYS, APP_PREFIX } from "@/constants";
describe("Storage 工具类", () => {
// 每个测试前清理存储
beforeEach(() => {
localStorage.clear();
sessionStorage.clear();
});
// 每个测试后清理存储
afterEach(() => {
localStorage.clear();
sessionStorage.clear();
});
describe("localStorage 操作", () => {
it("应该能够存储和获取字符串", () => {
Storage.set("test-key", "test-value");
expect(Storage.get("test-key")).toBe("test-value");
});
it("应该能够存储和获取对象", () => {
const testObj = { name: "张三", age: 25 };
Storage.set("test-obj", testObj);
expect(Storage.get("test-obj")).toEqual(testObj);
});
it("应该能够存储和获取数组", () => {
const testArr = [1, 2, 3, 4, 5];
Storage.set("test-arr", testArr);
expect(Storage.get("test-arr")).toEqual(testArr);
});
it("应该能够存储和获取布尔值", () => {
Storage.set("test-bool", true);
expect(Storage.get("test-bool")).toBe(true);
});
it("获取不存在的键应该返回 undefined", () => {
expect(Storage.get("non-existent")).toBeUndefined();
});
it("获取不存在的键应该返回默认值", () => {
expect(Storage.get("non-existent", "default")).toBe("default");
});
it("应该能够删除存储项", () => {
Storage.set("test-key", "test-value");
Storage.remove("test-key");
expect(Storage.get("test-key")).toBeUndefined();
});
});
describe("sessionStorage 操作", () => {
it("应该能够存储和获取字符串", () => {
Storage.sessionSet("test-key", "test-value");
expect(Storage.sessionGet("test-key")).toBe("test-value");
});
it("应该能够存储和获取对象", () => {
const testObj = { name: "李四", age: 30 };
Storage.sessionSet("test-obj", testObj);
expect(Storage.sessionGet("test-obj")).toEqual(testObj);
});
it("获取不存在的键应该返回默认值", () => {
expect(Storage.sessionGet("non-existent", "default")).toBe("default");
});
it("应该能够删除存储项", () => {
Storage.sessionSet("test-key", "test-value");
Storage.sessionRemove("test-key");
expect(Storage.sessionGet("test-key")).toBeUndefined();
});
});
describe("批量清理操作", () => {
it("clear() 应该同时清理 localStorage 和 sessionStorage", () => {
Storage.set("test-key", "local-value");
Storage.sessionSet("test-key", "session-value");
Storage.clear("test-key");
expect(Storage.get("test-key")).toBeUndefined();
expect(Storage.sessionGet("test-key")).toBeUndefined();
});
it("clearMultiple() 应该批量清理多个键", () => {
Storage.set("key1", "value1");
Storage.set("key2", "value2");
Storage.sessionSet("key1", "session1");
Storage.clearMultiple(["key1", "key2"]);
expect(Storage.get("key1")).toBeUndefined();
expect(Storage.get("key2")).toBeUndefined();
expect(Storage.sessionGet("key1")).toBeUndefined();
});
it("clearByPrefix() 应该清理指定前缀的所有存储项", () => {
Storage.set(`${APP_PREFIX}:auth:token`, "token123");
Storage.set(`${APP_PREFIX}:auth:user`, "user123");
Storage.set(`${APP_PREFIX}:ui:theme`, "dark");
Storage.set("other:key", "other-value");
Storage.clearByPrefix(`${APP_PREFIX}:auth:`);
expect(Storage.get(`${APP_PREFIX}:auth:token`)).toBeUndefined();
expect(Storage.get(`${APP_PREFIX}:auth:user`)).toBeUndefined();
expect(Storage.get(`${APP_PREFIX}:ui:theme`)).toBe("dark");
expect(Storage.get("other:key")).toBe("other-value");
});
it("clearAllProject() 应该清理所有项目相关的存储", () => {
Storage.set(STORAGE_KEYS.ACCESS_TOKEN, "token123");
Storage.set(STORAGE_KEYS.THEME, "dark");
Storage.set("other:key", "other-value");
Storage.clearAllProject();
expect(Storage.get(STORAGE_KEYS.ACCESS_TOKEN)).toBeUndefined();
expect(Storage.get(STORAGE_KEYS.THEME)).toBeUndefined();
expect(Storage.get("other:key")).toBe("other-value");
});
});
describe("边界情况", () => {
it("应该能够处理 null 值", () => {
Storage.set("test-null", null);
expect(Storage.get("test-null")).toBeNull();
});
it("应该能够处理空字符串", () => {
Storage.set("test-empty", "");
expect(Storage.get("test-empty")).toBe("");
});
it("应该能够处理数字 0", () => {
Storage.set("test-zero", 0);
expect(Storage.get("test-zero")).toBe(0);
});
it("应该能够处理 false", () => {
Storage.set("test-false", false);
expect(Storage.get("test-false")).toBe(false);
});
});
});

View File

@@ -0,0 +1,177 @@
import { describe, it, expect } from "vitest";
import { isExternal, isValidURL, isEmail, isMobile, VALIDATORS } from "@/utils/validate";
describe("Validate 工具函数", () => {
describe("isExternal()", () => {
it("应该识别外部链接", () => {
expect(isExternal("https://www.example.com")).toBe(true);
expect(isExternal("http://example.com")).toBe(true);
expect(isExternal("//example.com")).toBe(true);
expect(isExternal("mailto:test@example.com")).toBe(true);
expect(isExternal("tel:1234567890")).toBe(true);
});
it("应该识别内部链接", () => {
expect(isExternal("/dashboard")).toBe(false);
expect(isExternal("dashboard")).toBe(false);
expect(isExternal("./dashboard")).toBe(false);
expect(isExternal("../dashboard")).toBe(false);
});
it("应该处理空字符串", () => {
expect(isExternal("")).toBe(false);
});
});
describe("isValidURL()", () => {
it("应该验证有效的 URL", () => {
expect(isValidURL("https://www.example.com")).toBe(true);
expect(isValidURL("http://example.com")).toBe(true);
expect(isValidURL("https://example.com/path?query=1")).toBe(true);
expect(isValidURL("http://localhost:3000")).toBe(true);
});
it("应该拒绝无效的 URL", () => {
expect(isValidURL("not-a-url")).toBe(false);
expect(isValidURL("//example.com")).toBe(false);
expect(isValidURL("/path")).toBe(false);
expect(isValidURL("")).toBe(false);
});
});
describe("isEmail()", () => {
it("应该验证有效的邮箱", () => {
expect(isEmail("test@example.com")).toBe(true);
expect(isEmail("user.name@example.com")).toBe(true);
expect(isEmail("user+tag@example.co.uk")).toBe(true);
expect(isEmail("123@example.com")).toBe(true);
});
it("应该拒绝无效的邮箱", () => {
expect(isEmail("invalid")).toBe(false);
expect(isEmail("@example.com")).toBe(false);
expect(isEmail("user@")).toBe(false);
expect(isEmail("user @example.com")).toBe(false);
expect(isEmail("")).toBe(false);
});
});
describe("isMobile()", () => {
it("应该验证有效的手机号", () => {
expect(isMobile("13800138000")).toBe(true);
expect(isMobile("15912345678")).toBe(true);
expect(isMobile("18612345678")).toBe(true);
expect(isMobile("19912345678")).toBe(true);
});
it("应该拒绝无效的手机号", () => {
expect(isMobile("12345678901")).toBe(false); // 不是 1 开头的有效号段
expect(isMobile("1381234567")).toBe(false); // 少于 11 位
expect(isMobile("138123456789")).toBe(false); // 多于 11 位
expect(isMobile("abcdefghijk")).toBe(false); // 非数字
expect(isMobile("")).toBe(false);
});
});
describe("VALIDATORS 对象", () => {
describe("required 验证器", () => {
it("应该验证必填项", () => {
const callback = vi.fn();
VALIDATORS.required({}, "test", callback);
expect(callback).toHaveBeenCalledWith(new Error("此项为必填项"));
callback.mockClear();
VALIDATORS.required({}, "", callback);
expect(callback).toHaveBeenCalledWith(new Error("此项为必填项"));
callback.mockClear();
VALIDATORS.required({}, null, callback);
expect(callback).toHaveBeenCalledWith(new Error("此项为必填项"));
callback.mockClear();
VALIDATORS.required({}, undefined, callback);
expect(callback).toHaveBeenCalledWith(new Error("此项为必填项"));
});
it("应该通过有效值", () => {
const callback = vi.fn();
VALIDATORS.required({}, "value", callback);
expect(callback).toHaveBeenCalledWith();
callback.mockClear();
VALIDATORS.required({}, 0, callback);
expect(callback).toHaveBeenCalledWith();
callback.mockClear();
VALIDATORS.required({}, false, callback);
expect(callback).toHaveBeenCalledWith();
});
});
describe("email 验证器", () => {
it("应该验证邮箱格式", () => {
const callback = vi.fn();
VALIDATORS.email({}, "invalid", callback);
expect(callback).toHaveBeenCalledWith(new Error("请输入正确的邮箱地址"));
callback.mockClear();
VALIDATORS.email({}, "test@example.com", callback);
expect(callback).toHaveBeenCalledWith();
});
it("应该允许空值", () => {
const callback = vi.fn();
VALIDATORS.email({}, "", callback);
expect(callback).toHaveBeenCalledWith();
callback.mockClear();
VALIDATORS.email({}, null, callback);
expect(callback).toHaveBeenCalledWith();
});
});
describe("mobile 验证器", () => {
it("应该验证手机号格式", () => {
const callback = vi.fn();
VALIDATORS.mobile({}, "12345678901", callback);
expect(callback).toHaveBeenCalledWith(new Error("请输入正确的手机号码"));
callback.mockClear();
VALIDATORS.mobile({}, "13800138000", callback);
expect(callback).toHaveBeenCalledWith();
});
it("应该允许空值", () => {
const callback = vi.fn();
VALIDATORS.mobile({}, "", callback);
expect(callback).toHaveBeenCalledWith();
});
});
describe("url 验证器", () => {
it("应该验证 URL 格式", () => {
const callback = vi.fn();
VALIDATORS.url({}, "not-a-url", callback);
expect(callback).toHaveBeenCalledWith(new Error("请输入正确的 URL 地址"));
callback.mockClear();
VALIDATORS.url({}, "https://example.com", callback);
expect(callback).toHaveBeenCalledWith();
});
it("应该允许空值", () => {
const callback = vi.fn();
VALIDATORS.url({}, "", callback);
expect(callback).toHaveBeenCalledWith();
});
});
});
});