mirror of
https://github.com/LHY0125/PathEditor.git
synced 2026-06-29 01:45:54 +08:00
8ff02fd88b
这些是正当的 React 模式(对话框状态重置、ref 同步避免重复注册、测试 mock) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
302 lines
10 KiB
TypeScript
302 lines
10 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||
|
||
// Mock @tauri-apps/api/core
|
||
vi.mock('@tauri-apps/api/core', () => ({
|
||
invoke: vi.fn(),
|
||
}));
|
||
|
||
// Mock i18n
|
||
vi.mock('@/i18n', () => ({
|
||
default: { t: vi.fn((key: string, opts?: Record<string, unknown>) => {
|
||
if (key === 'status.loaded') return `已加载 ${opts?.sysCount} 条系统 PATH,${opts?.userCount} 条用户 PATH`;
|
||
if (key === 'status.error') return '加载失败';
|
||
if (key === 'status.saving') return '正在保存...';
|
||
if (key === 'status.saved') return '保存成功';
|
||
if (key === 'status.warning_backup') return '备份失败,但保存继续';
|
||
if (key === 'status.readonly') return '只读模式';
|
||
if (key === 'status.deleted') return `已删除 ${opts?.count} 条路径`;
|
||
return key;
|
||
}) },
|
||
}));
|
||
|
||
import { useAppStore } from '@/store/app-store';
|
||
import { UndoRedoManager, TargetType } from '@/core/undo-redo';
|
||
import { invoke } from '@tauri-apps/api/core';
|
||
|
||
const mockedInvoke = vi.mocked(invoke);
|
||
|
||
function resetStore() {
|
||
useAppStore.setState({
|
||
sysPaths: [],
|
||
userPaths: [],
|
||
undoRedo: new UndoRedoManager(50),
|
||
_savedSys: [],
|
||
_savedUser: [],
|
||
isModified: false,
|
||
isLoading: false,
|
||
isSaving: false,
|
||
selectedIndices: [],
|
||
searchQuery: '',
|
||
statusMessage: '',
|
||
});
|
||
}
|
||
|
||
describe('app-store CRUD', () => {
|
||
beforeEach(() => {
|
||
vi.clearAllMocks();
|
||
resetStore();
|
||
});
|
||
|
||
it('addPath 追加到 sysPaths', () => {
|
||
useAppStore.getState().addPath('C:\\test', TargetType.SYSTEM);
|
||
const s = useAppStore.getState();
|
||
expect(s.sysPaths).toEqual(['C:\\test']);
|
||
expect(s.isModified).toBe(true);
|
||
expect(s.undoRedo.historyLength).toBe(1);
|
||
});
|
||
|
||
it('addPath 追加到 userPaths', () => {
|
||
useAppStore.getState().addPath('D:\\user', TargetType.USER);
|
||
const s = useAppStore.getState();
|
||
expect(s.userPaths).toEqual(['D:\\user']);
|
||
expect(s.sysPaths).toEqual([]);
|
||
});
|
||
|
||
it('editPath 替换正确位置', () => {
|
||
const store = useAppStore.getState();
|
||
store.addPath('C:\\old', TargetType.SYSTEM);
|
||
store.editPath(0, 'C:\\new', TargetType.SYSTEM);
|
||
expect(useAppStore.getState().sysPaths).toEqual(['C:\\new']);
|
||
});
|
||
|
||
it('editPath 越界 index 无崩溃', () => {
|
||
expect(() => {
|
||
useAppStore.getState().editPath(99, 'X', TargetType.SYSTEM);
|
||
}).not.toThrow();
|
||
});
|
||
|
||
it('deletePaths 单选删除', () => {
|
||
const store = useAppStore.getState();
|
||
store.addPath('A', TargetType.SYSTEM);
|
||
store.addPath('B', TargetType.SYSTEM);
|
||
store.addPath('C', TargetType.SYSTEM);
|
||
store.deletePaths([1], TargetType.SYSTEM);
|
||
expect(useAppStore.getState().sysPaths).toEqual(['A', 'C']);
|
||
expect(useAppStore.getState().selectedIndices).toEqual([]);
|
||
});
|
||
|
||
it('deletePaths 多选删除(逆序排序一次 undo 覆盖)', () => {
|
||
const store = useAppStore.getState();
|
||
store.addPath('A', TargetType.USER);
|
||
store.addPath('B', TargetType.USER);
|
||
store.addPath('C', TargetType.USER);
|
||
store.addPath('D', TargetType.USER);
|
||
store.deletePaths([1, 3], TargetType.USER);
|
||
expect(useAppStore.getState().userPaths).toEqual(['A', 'C']);
|
||
});
|
||
|
||
it('deletePaths 非连续多选删除后可 undo 恢复到正确位置', () => {
|
||
const store = useAppStore.getState();
|
||
store.addPath('A', TargetType.SYSTEM);
|
||
store.addPath('B', TargetType.SYSTEM);
|
||
store.addPath('C', TargetType.SYSTEM);
|
||
store.addPath('D', TargetType.SYSTEM);
|
||
store.deletePaths([1, 3], TargetType.SYSTEM);
|
||
expect(useAppStore.getState().sysPaths).toEqual(['A', 'C']);
|
||
useAppStore.getState().undo();
|
||
expect(useAppStore.getState().sysPaths).toEqual(['A', 'B', 'C', 'D']);
|
||
});
|
||
|
||
it('moveUp index=0 无操作', () => {
|
||
const store = useAppStore.getState();
|
||
store.addPath('A', TargetType.SYSTEM);
|
||
store.moveUp(0, TargetType.SYSTEM);
|
||
expect(useAppStore.getState().sysPaths).toEqual(['A']);
|
||
});
|
||
|
||
it('moveUp 正常交换位置', () => {
|
||
const store = useAppStore.getState();
|
||
store.addPath('A', TargetType.SYSTEM);
|
||
store.addPath('B', TargetType.SYSTEM);
|
||
store.moveUp(1, TargetType.SYSTEM);
|
||
expect(useAppStore.getState().sysPaths).toEqual(['B', 'A']);
|
||
expect(useAppStore.getState().selectedIndices).toEqual([0]);
|
||
});
|
||
|
||
it('moveDown 末位无操作', () => {
|
||
const store = useAppStore.getState();
|
||
store.addPath('A', TargetType.USER);
|
||
store.moveDown(0, TargetType.USER);
|
||
expect(useAppStore.getState().userPaths).toEqual(['A']);
|
||
});
|
||
|
||
it('cleanPaths 移除无效路径并返回 removed', () => {
|
||
const store = useAppStore.getState();
|
||
store.addPath('C:\\valid', TargetType.SYSTEM);
|
||
store.addPath(':::invalid:::', TargetType.SYSTEM);
|
||
// is_valid_path_format 拒绝全标点路径
|
||
const removed = store.cleanPaths(TargetType.SYSTEM, (p) => !p.includes(':::'));
|
||
expect(removed).toEqual([':::invalid:::']);
|
||
expect(useAppStore.getState().sysPaths).toEqual(['C:\\valid']);
|
||
});
|
||
|
||
it('replacePaths 整体替换列表', () => {
|
||
const store = useAppStore.getState();
|
||
store.addPath('old1', TargetType.USER);
|
||
store.addPath('old2', TargetType.USER);
|
||
store.replacePaths(TargetType.USER, ['new1', 'new2', 'new3']);
|
||
expect(useAppStore.getState().userPaths).toEqual(['new1', 'new2', 'new3']);
|
||
});
|
||
|
||
it('clearPaths 清空列表', () => {
|
||
const store = useAppStore.getState();
|
||
store.addPath('A', TargetType.SYSTEM);
|
||
store.addPath('B', TargetType.SYSTEM);
|
||
store.clearPaths(TargetType.SYSTEM);
|
||
expect(useAppStore.getState().sysPaths).toEqual([]);
|
||
});
|
||
|
||
it('clearPaths 空列表无操作', () => {
|
||
const store = useAppStore.getState();
|
||
store.clearPaths(TargetType.USER);
|
||
expect(useAppStore.getState().undoRedo.historyLength).toBe(0);
|
||
});
|
||
});
|
||
|
||
describe('undo/redo', () => {
|
||
beforeEach(() => {
|
||
vi.clearAllMocks();
|
||
resetStore();
|
||
});
|
||
|
||
it('undo 恢复操作前状态', () => {
|
||
useAppStore.getState().addPath('test', TargetType.SYSTEM);
|
||
expect(useAppStore.getState().sysPaths.length).toBe(1);
|
||
useAppStore.getState().undo();
|
||
expect(useAppStore.getState().sysPaths).toEqual([]);
|
||
});
|
||
|
||
it('redo 回到操作后状态', () => {
|
||
const store = useAppStore.getState();
|
||
store.addPath('test', TargetType.SYSTEM);
|
||
store.undo();
|
||
store.redo();
|
||
expect(useAppStore.getState().sysPaths).toEqual(['test']);
|
||
});
|
||
|
||
it('undo/redo 正确更新 isModified', () => {
|
||
const store = useAppStore.getState();
|
||
// 设置已保存快照
|
||
useAppStore.setState({ _savedSys: [], _savedUser: [] });
|
||
store.addPath('test', TargetType.SYSTEM);
|
||
expect(useAppStore.getState().isModified).toBe(true);
|
||
store.undo();
|
||
expect(useAppStore.getState().isModified).toBe(false);
|
||
store.redo();
|
||
expect(useAppStore.getState().isModified).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('loadPaths', () => {
|
||
beforeEach(() => {
|
||
vi.clearAllMocks();
|
||
resetStore();
|
||
});
|
||
|
||
it('成功加载', async () => {
|
||
mockedInvoke.mockResolvedValueOnce(['C:\\sys1', 'C:\\sys2']);
|
||
mockedInvoke.mockResolvedValueOnce(['D:\\usr1']);
|
||
await useAppStore.getState().loadPaths();
|
||
const s = useAppStore.getState();
|
||
expect(s.sysPaths).toEqual(['C:\\sys1', 'C:\\sys2']);
|
||
expect(s.userPaths).toEqual(['D:\\usr1']);
|
||
expect(s.isLoading).toBe(false);
|
||
expect(s.isModified).toBe(false);
|
||
});
|
||
|
||
it('加载失败时 isLoading 重置', async () => {
|
||
mockedInvoke.mockRejectedValueOnce(new Error('reg error'));
|
||
mockedInvoke.mockResolvedValueOnce([]);
|
||
await useAppStore.getState().loadPaths();
|
||
const s = useAppStore.getState();
|
||
expect(s.isLoading).toBe(false);
|
||
expect(s.statusMessage).toContain('加载失败');
|
||
});
|
||
});
|
||
|
||
describe('savePaths', () => {
|
||
beforeEach(() => {
|
||
vi.clearAllMocks();
|
||
resetStore();
|
||
useAppStore.setState({ sysPaths: ['A'], userPaths: ['B'] });
|
||
});
|
||
|
||
it('保存成功', async () => {
|
||
mockedInvoke.mockResolvedValue(undefined);
|
||
await useAppStore.getState().savePaths();
|
||
const s = useAppStore.getState();
|
||
expect(s.isSaving).toBe(false);
|
||
expect(s.isModified).toBe(false);
|
||
expect(s.statusMessage).toBe('保存成功');
|
||
});
|
||
|
||
it('部分失败时报告具体 hive', async () => {
|
||
mockedInvoke
|
||
.mockResolvedValueOnce(undefined) // backup_registry
|
||
.mockResolvedValueOnce(undefined) // save_system_paths
|
||
.mockRejectedValueOnce('权限不足'); // save_user_paths
|
||
await useAppStore.getState().savePaths();
|
||
const s = useAppStore.getState();
|
||
expect(s.isSaving).toBe(false);
|
||
expect(s.statusMessage).toContain('用户 PATH 保存失败');
|
||
});
|
||
|
||
it('isSaving 守卫:并发第二次调用直接返回', async () => {
|
||
let resolveAll: (v: unknown) => void;
|
||
const pending = new Promise((r) => { resolveAll = r; });
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
mockedInvoke.mockReturnValue(pending as any);
|
||
|
||
// 第一次调用(不等它完成,停在 Promise.allSettled)
|
||
const p1 = useAppStore.getState().savePaths();
|
||
// 第二次调用应被 isSaving 守卫拦截(此时 isSaving=true)
|
||
const r2 = useAppStore.getState().savePaths();
|
||
|
||
// 第二次调用同步返回 undefined(被守卫拦截)
|
||
await expect(r2).resolves.toBeUndefined();
|
||
|
||
// 放行第一次调用的所有 invoke
|
||
resolveAll!(undefined);
|
||
await p1;
|
||
});
|
||
});
|
||
|
||
describe('initialize', () => {
|
||
beforeEach(() => {
|
||
vi.clearAllMocks();
|
||
resetStore();
|
||
});
|
||
|
||
it('管理员模式初始化', async () => {
|
||
mockedInvoke
|
||
.mockResolvedValueOnce(true) // check_admin
|
||
.mockResolvedValueOnce(['S1']) // load_system_paths
|
||
.mockResolvedValueOnce(['U1']); // load_user_paths
|
||
await useAppStore.getState().initialize();
|
||
const s = useAppStore.getState();
|
||
expect(s.isAdmin).toBe(true);
|
||
expect(s.sysPaths).toEqual(['S1']);
|
||
expect(s.userPaths).toEqual(['U1']);
|
||
});
|
||
|
||
it('非管理员初始化进入只读模式', async () => {
|
||
mockedInvoke
|
||
.mockResolvedValueOnce(false) // check_admin
|
||
.mockResolvedValueOnce([]) // load_system_paths
|
||
.mockResolvedValueOnce([]); // load_user_paths
|
||
await useAppStore.getState().initialize();
|
||
expect(useAppStore.getState().isAdmin).toBe(false);
|
||
// statusMessage 被后续 loadPaths 覆盖为加载完成消息,但 isAdmin=false 不变
|
||
});
|
||
});
|