feat: 项目结构重构优化
This commit is contained in:
52
tests/setup.ts
Normal file
52
tests/setup.ts
Normal 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(),
|
||||
};
|
||||
114
tests/unit/components/Pagination.test.ts
Normal file
114
tests/unit/components/Pagination.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
62
tests/unit/composables/useDictSync.test.ts
Normal file
62
tests/unit/composables/useDictSync.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
88
tests/unit/composables/useTableSelection.test.ts
Normal file
88
tests/unit/composables/useTableSelection.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
141
tests/unit/store/app.test.ts
Normal file
141
tests/unit/store/app.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
});
|
||||
134
tests/unit/store/dict.test.ts
Normal file
134
tests/unit/store/dict.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
186
tests/unit/store/settings.test.ts
Normal file
186
tests/unit/store/settings.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
157
tests/unit/utils/auth.test.ts
Normal file
157
tests/unit/utils/auth.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
136
tests/unit/utils/format.test.ts
Normal file
136
tests/unit/utils/format.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
});
|
||||
150
tests/unit/utils/storage.test.ts
Normal file
150
tests/unit/utils/storage.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
177
tests/unit/utils/validate.test.ts
Normal file
177
tests/unit/utils/validate.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user