mirror of
https://github.com/LHY0125/PathEditor.git
synced 2026-06-29 01:45:54 +08:00
chore: 同步 v5.0 基础设施完善到 v5.1
从 v5.0 cherry-pick 的开源项目基础设施改进: 新增配置文件: - .editorconfig, .gitattributes, .prettierrc, .markdownlint.json - commitlint.config.js 新增 GitHub 社区文件: - .github/dependabot.yml — 依赖自动更新 - .github/CODEOWNERS — 自动 PR 审查分配 - .github/FUNDING.yml — 开源赞助入口 新增文档: - ROADMAP.md — 路线图 - SUPPORT.md — 帮助指南 - docs/screenshots/ — 应用截图 新增 Git Hooks: - .husky/pre-commit — lint-staged 自动格式化+修复 - .husky/commit-msg — commitlint 校验 CI 强化: - 新增 Prettier 格式检查 - 新增 Vitest 覆盖率 + Codecov 上报 - 保留 v5.1 已有的 rust-cache + jsdom 全局环境 修复: - index.html 标题 v4.0 → v5.1 - PathEditDialog set-state-in-effect 改用 useRef prevOpen 守卫 - merge-preview.test.tsx no-explicit-any 修复 - 所有 TS/TSX 文件 Prettier 格式化统一 v5.1 保留特性: - @tanstack/react-virtual 虚拟滚动 - jsdom 全局测试环境 - Swatinem/rust-cache CI 加速 - 105 测试全部通过
This commit is contained in:
@@ -26,9 +26,7 @@ vi.mock('@/i18n', () => ({
|
||||
|
||||
describe('AnalyzeDialog', () => {
|
||||
it('渲染冲突检测和工具清单标签页,不崩溃', () => {
|
||||
const { container } = render(
|
||||
<AnalyzeDialog open={true} onClose={() => {}} />,
|
||||
);
|
||||
const { container } = render(<AnalyzeDialog open={true} onClose={() => {}} />);
|
||||
const text = container.textContent || '';
|
||||
expect(text).toContain('analyze.conflicts');
|
||||
expect(text).toContain('analyze.tools');
|
||||
|
||||
@@ -7,16 +7,19 @@ vi.mock('@tauri-apps/api/core', () => ({
|
||||
|
||||
// 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;
|
||||
}) },
|
||||
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 type { PathEntry } from '../../src/core/path-entry';
|
||||
@@ -56,7 +59,7 @@ describe('app-store CRUD', () => {
|
||||
it('addPath 追加到 sysPaths', () => {
|
||||
useAppStore.getState().addPath('C:\\test', TargetType.SYSTEM);
|
||||
const s = useAppStore.getState();
|
||||
expect(s.sysPaths.map(e => e.path)).toEqual(['C:\\test']);
|
||||
expect(s.sysPaths.map((e) => e.path)).toEqual(['C:\\test']);
|
||||
expect(s.isModified).toBe(true);
|
||||
expect(s.undoRedo.historyLength).toBe(1);
|
||||
});
|
||||
@@ -64,7 +67,7 @@ describe('app-store CRUD', () => {
|
||||
it('addPath 追加到 userPaths', () => {
|
||||
useAppStore.getState().addPath('D:\\user', TargetType.USER);
|
||||
const s = useAppStore.getState();
|
||||
expect(s.userPaths.map(e => e.path)).toEqual(['D:\\user']);
|
||||
expect(s.userPaths.map((e) => e.path)).toEqual(['D:\\user']);
|
||||
expect(s.sysPaths).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -72,7 +75,7 @@ describe('app-store CRUD', () => {
|
||||
const store = useAppStore.getState();
|
||||
store.addPath('C:\\old', TargetType.SYSTEM);
|
||||
store.editPath(0, 'C:\\new', TargetType.SYSTEM);
|
||||
expect(useAppStore.getState().sysPaths.map(e => e.path)).toEqual(['C:\\new']);
|
||||
expect(useAppStore.getState().sysPaths.map((e) => e.path)).toEqual(['C:\\new']);
|
||||
});
|
||||
|
||||
it('editPath 越界 index 无崩溃', () => {
|
||||
@@ -87,7 +90,7 @@ describe('app-store CRUD', () => {
|
||||
store.addPath('B', TargetType.SYSTEM);
|
||||
store.addPath('C', TargetType.SYSTEM);
|
||||
store.deletePaths([1], TargetType.SYSTEM);
|
||||
expect(useAppStore.getState().sysPaths.map(e => e.path)).toEqual(['A', 'C']);
|
||||
expect(useAppStore.getState().sysPaths.map((e) => e.path)).toEqual(['A', 'C']);
|
||||
expect(useAppStore.getState().selectedIndices).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -98,7 +101,7 @@ describe('app-store CRUD', () => {
|
||||
store.addPath('C', TargetType.USER);
|
||||
store.addPath('D', TargetType.USER);
|
||||
store.deletePaths([1, 3], TargetType.USER);
|
||||
expect(useAppStore.getState().userPaths.map(e => e.path)).toEqual(['A', 'C']);
|
||||
expect(useAppStore.getState().userPaths.map((e) => e.path)).toEqual(['A', 'C']);
|
||||
});
|
||||
|
||||
it('deletePaths 非连续多选删除后可 undo 恢复到正确位置', () => {
|
||||
@@ -108,16 +111,16 @@ describe('app-store CRUD', () => {
|
||||
store.addPath('C', TargetType.SYSTEM);
|
||||
store.addPath('D', TargetType.SYSTEM);
|
||||
store.deletePaths([1, 3], TargetType.SYSTEM);
|
||||
expect(useAppStore.getState().sysPaths.map(e => e.path)).toEqual(['A', 'C']);
|
||||
expect(useAppStore.getState().sysPaths.map((e) => e.path)).toEqual(['A', 'C']);
|
||||
useAppStore.getState().undo();
|
||||
expect(useAppStore.getState().sysPaths.map(e => e.path)).toEqual(['A', 'B', 'C', 'D']);
|
||||
expect(useAppStore.getState().sysPaths.map((e) => e.path)).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.map(e => e.path)).toEqual(['A']);
|
||||
expect(useAppStore.getState().sysPaths.map((e) => e.path)).toEqual(['A']);
|
||||
});
|
||||
|
||||
it('moveUp 正常交换位置', () => {
|
||||
@@ -125,7 +128,7 @@ describe('app-store CRUD', () => {
|
||||
store.addPath('A', TargetType.SYSTEM);
|
||||
store.addPath('B', TargetType.SYSTEM);
|
||||
store.moveUp(1, TargetType.SYSTEM);
|
||||
expect(useAppStore.getState().sysPaths.map(e => e.path)).toEqual(['B', 'A']);
|
||||
expect(useAppStore.getState().sysPaths.map((e) => e.path)).toEqual(['B', 'A']);
|
||||
expect(useAppStore.getState().selectedIndices).toEqual([0]);
|
||||
});
|
||||
|
||||
@@ -133,7 +136,7 @@ describe('app-store CRUD', () => {
|
||||
const store = useAppStore.getState();
|
||||
store.addPath('A', TargetType.USER);
|
||||
store.moveDown(0, TargetType.USER);
|
||||
expect(useAppStore.getState().userPaths.map(e => e.path)).toEqual(['A']);
|
||||
expect(useAppStore.getState().userPaths.map((e) => e.path)).toEqual(['A']);
|
||||
});
|
||||
|
||||
it('cleanPaths 移除无效路径并返回 removed', () => {
|
||||
@@ -143,7 +146,7 @@ describe('app-store CRUD', () => {
|
||||
// is_valid_path_format 拒绝全标点路径
|
||||
const removed = store.cleanPaths(TargetType.SYSTEM, (p) => !p.includes(':::'));
|
||||
expect(removed).toEqual([':::invalid:::']);
|
||||
expect(useAppStore.getState().sysPaths.map(e => e.path)).toEqual(['C:\\valid']);
|
||||
expect(useAppStore.getState().sysPaths.map((e) => e.path)).toEqual(['C:\\valid']);
|
||||
});
|
||||
|
||||
it('replacePaths 整体替换列表', () => {
|
||||
@@ -151,7 +154,7 @@ describe('app-store CRUD', () => {
|
||||
store.addPath('old1', TargetType.USER);
|
||||
store.addPath('old2', TargetType.USER);
|
||||
store.replacePaths(TargetType.USER, ['new1', 'new2', 'new3']);
|
||||
expect(useAppStore.getState().userPaths.map(e => e.path)).toEqual(['new1', 'new2', 'new3']);
|
||||
expect(useAppStore.getState().userPaths.map((e) => e.path)).toEqual(['new1', 'new2', 'new3']);
|
||||
});
|
||||
|
||||
it('clearPaths 清空列表', () => {
|
||||
@@ -187,7 +190,7 @@ describe('undo/redo', () => {
|
||||
store.addPath('test', TargetType.SYSTEM);
|
||||
store.undo();
|
||||
store.redo();
|
||||
expect(useAppStore.getState().sysPaths.map(e => e.path)).toEqual(['test']);
|
||||
expect(useAppStore.getState().sysPaths.map((e) => e.path)).toEqual(['test']);
|
||||
});
|
||||
|
||||
it('undo/redo 正确更新 isModified', () => {
|
||||
@@ -214,8 +217,8 @@ describe('loadPaths', () => {
|
||||
mockedInvoke.mockResolvedValueOnce(['D:\\usr1']);
|
||||
await useAppStore.getState().loadPaths();
|
||||
const s = useAppStore.getState();
|
||||
expect(s.sysPaths.map(e => e.path)).toEqual(['C:\\sys1', 'C:\\sys2']);
|
||||
expect(s.userPaths.map(e => e.path)).toEqual(['D:\\usr1']);
|
||||
expect(s.sysPaths.map((e) => e.path)).toEqual(['C:\\sys1', 'C:\\sys2']);
|
||||
expect(s.userPaths.map((e) => e.path)).toEqual(['D:\\usr1']);
|
||||
expect(s.isLoading).toBe(false);
|
||||
expect(s.isModified).toBe(false);
|
||||
});
|
||||
@@ -249,13 +252,13 @@ describe('savePaths', () => {
|
||||
|
||||
it('部分失败时报告具体 hive 并回读', async () => {
|
||||
mockedInvoke
|
||||
.mockResolvedValueOnce(undefined) // backup_registry
|
||||
.mockResolvedValueOnce(undefined) // save_system_paths
|
||||
.mockRejectedValueOnce('权限不足') // save_user_paths
|
||||
.mockResolvedValueOnce(undefined) // backup_registry
|
||||
.mockResolvedValueOnce(undefined) // save_system_paths
|
||||
.mockRejectedValueOnce('权限不足') // save_user_paths
|
||||
// 以下为 partial 触发的 loadPaths 调用
|
||||
.mockResolvedValueOnce(['A']) // load_system_paths
|
||||
.mockResolvedValueOnce(['B']) // load_user_paths
|
||||
.mockResolvedValueOnce([[], []]); // load_disabled_state
|
||||
.mockResolvedValueOnce(['A']) // load_system_paths
|
||||
.mockResolvedValueOnce(['B']) // load_user_paths
|
||||
.mockResolvedValueOnce([[], []]); // load_disabled_state
|
||||
|
||||
const result = await useAppStore.getState().savePaths();
|
||||
expect(result.kind).toBe('partial');
|
||||
@@ -266,7 +269,9 @@ describe('savePaths', () => {
|
||||
|
||||
it('isSaving 守卫:并发第二次调用直接返回', async () => {
|
||||
let resolveAll: (v: unknown) => void;
|
||||
const pending = new Promise((r) => { resolveAll = r; });
|
||||
const pending = new Promise((r) => {
|
||||
resolveAll = r;
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
mockedInvoke.mockReturnValue(pending as any);
|
||||
|
||||
@@ -292,21 +297,21 @@ describe('initialize', () => {
|
||||
|
||||
it('管理员模式初始化', async () => {
|
||||
mockedInvoke
|
||||
.mockResolvedValueOnce(true) // check_admin
|
||||
.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.map(e => e.path)).toEqual(['S1']);
|
||||
expect(s.userPaths.map(e => e.path)).toEqual(['U1']);
|
||||
expect(s.sysPaths.map((e) => e.path)).toEqual(['S1']);
|
||||
expect(s.userPaths.map((e) => e.path)).toEqual(['U1']);
|
||||
});
|
||||
|
||||
it('非管理员初始化进入只读模式', async () => {
|
||||
mockedInvoke
|
||||
.mockResolvedValueOnce(false) // check_admin
|
||||
.mockResolvedValueOnce([]) // load_system_paths
|
||||
.mockResolvedValueOnce([]); // load_user_paths
|
||||
.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 不变
|
||||
|
||||
@@ -26,8 +26,12 @@ describe('exportToJson', () => {
|
||||
const parsed = JSON.parse(json);
|
||||
expect(parsed.version).toBe('5.1.0');
|
||||
expect(parsed.timestamp).toBeDefined();
|
||||
expect(parsed.system.map((e: { path: string }) => e.path)).toEqual(sampleData.system.map(e => e.path));
|
||||
expect(parsed.user.map((e: { path: string }) => e.path)).toEqual(sampleData.user.map(e => e.path));
|
||||
expect(parsed.system.map((e: { path: string }) => e.path)).toEqual(
|
||||
sampleData.system.map((e) => e.path),
|
||||
);
|
||||
expect(parsed.user.map((e: { path: string }) => e.path)).toEqual(
|
||||
sampleData.user.map((e) => e.path),
|
||||
);
|
||||
expect(parsed.system[0].enabled).toBe(true);
|
||||
expect(parsed.user[0].enabled).toBe(true);
|
||||
});
|
||||
@@ -36,8 +40,8 @@ describe('exportToJson', () => {
|
||||
describe('importFromJson', () => {
|
||||
it('正确导入 JSON', () => {
|
||||
const json = JSON.stringify({
|
||||
system: sampleData.system.map(e => e.path),
|
||||
user: sampleData.user.map(e => e.path),
|
||||
system: sampleData.system.map((e) => e.path),
|
||||
user: sampleData.user.map((e) => e.path),
|
||||
});
|
||||
const result = importFromJson(json);
|
||||
expect(result.system).toEqual(sampleData.system);
|
||||
|
||||
@@ -5,27 +5,27 @@ describe('导入一致性(TS 端)', () => {
|
||||
it('JSON 含 system + user', () => {
|
||||
const json = JSON.stringify({ system: ['C:\\a', 'C:\\b'], user: ['D:\\c'] });
|
||||
const r = importFromJson(json);
|
||||
expect(r.system.map(e => e.path)).toEqual(['C:\\a', 'C:\\b']);
|
||||
expect(r.user.map(e => e.path)).toEqual(['D:\\c']);
|
||||
expect(r.system.map((e) => e.path)).toEqual(['C:\\a', 'C:\\b']);
|
||||
expect(r.user.map((e) => e.path)).toEqual(['D:\\c']);
|
||||
});
|
||||
|
||||
it('CSV system/user 分类', () => {
|
||||
const csv = 'type,path\nsystem,C:\\sys\nuser,D:\\usr\n';
|
||||
const r = importFromCsv(csv);
|
||||
expect(r.system.map(e => e.path)).toEqual(['C:\\sys']);
|
||||
expect(r.user.map(e => e.path)).toEqual(['D:\\usr']);
|
||||
expect(r.system.map((e) => e.path)).toEqual(['C:\\sys']);
|
||||
expect(r.user.map((e) => e.path)).toEqual(['D:\\usr']);
|
||||
});
|
||||
|
||||
it('CSV 含 BOM + header', () => {
|
||||
const csv = 'type,path\nsystem,C:\\x\n';
|
||||
const r = importFromCsv(csv);
|
||||
expect(r.system.map(e => e.path)).toEqual(['C:\\x']);
|
||||
expect(r.system.map((e) => e.path)).toEqual(['C:\\x']);
|
||||
});
|
||||
|
||||
it('TXT 逐行读取,跳过注释', () => {
|
||||
const txt = '# comment\nC:\\a\n\nD:\\b\n';
|
||||
const r = importFromTxt(txt);
|
||||
expect(r.map(e => e.path)).toEqual(['C:\\a', 'D:\\b']);
|
||||
expect(r.map((e) => e.path)).toEqual(['C:\\a', 'D:\\b']);
|
||||
});
|
||||
|
||||
it('JSON 空数据不崩溃', () => {
|
||||
|
||||
@@ -9,9 +9,7 @@ vi.mock('@/store/app-store', () => ({
|
||||
{ path: 'C:\\Windows', enabled: true },
|
||||
{ path: 'C:\\Disabled', enabled: false },
|
||||
],
|
||||
userPaths: [
|
||||
{ path: 'D:\\UserApp', enabled: true },
|
||||
],
|
||||
userPaths: [{ path: 'D:\\UserApp', enabled: true }],
|
||||
searchQuery: '',
|
||||
};
|
||||
return selector(state);
|
||||
@@ -19,7 +17,7 @@ vi.mock('@/store/app-store', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('@tanstack/react-virtual', () => ({
|
||||
useVirtualizer: (options: any) => ({
|
||||
useVirtualizer: (options: Record<string, number>) => ({
|
||||
getVirtualItems: () => {
|
||||
// return an array of objects to mock virtual items
|
||||
return Array.from({ length: options.count }).map((_, index) => ({
|
||||
|
||||
@@ -36,13 +36,19 @@ describe('analyzePaths', () => {
|
||||
|
||||
describe('pathClean', () => {
|
||||
it('移除无效路径', () => {
|
||||
const [kept, removed] = pathClean([pe('C:\\Valid'), pe('C:\\Invalid'), pe('D:\\Valid')], validateFn);
|
||||
expect(kept.map(e => e.path)).toEqual(['C:\\Valid', 'D:\\Valid']);
|
||||
expect(removed.map(e => e.path)).toEqual(['C:\\Invalid']);
|
||||
const [kept, removed] = pathClean(
|
||||
[pe('C:\\Valid'), pe('C:\\Invalid'), pe('D:\\Valid')],
|
||||
validateFn,
|
||||
);
|
||||
expect(kept.map((e) => e.path)).toEqual(['C:\\Valid', 'D:\\Valid']);
|
||||
expect(removed.map((e) => e.path)).toEqual(['C:\\Invalid']);
|
||||
});
|
||||
|
||||
it('移除重复路径保留第一个', () => {
|
||||
const [kept, removed] = pathClean([pe('C:\\Valid'), pe('C:\\Valid'), pe('D:\\Valid')], alwaysValid);
|
||||
const [kept, removed] = pathClean(
|
||||
[pe('C:\\Valid'), pe('C:\\Valid'), pe('D:\\Valid')],
|
||||
alwaysValid,
|
||||
);
|
||||
expect(kept.length).toBe(2);
|
||||
expect(removed.length).toBe(1);
|
||||
});
|
||||
@@ -56,7 +62,7 @@ describe('pathClean', () => {
|
||||
|
||||
it('全部有效无变化', () => {
|
||||
const [kept, removed] = pathClean([pe('C:\\a'), pe('D:\\b')], alwaysValid);
|
||||
expect(kept.map(e => e.path)).toEqual(['C:\\a', 'D:\\b']);
|
||||
expect(kept.map((e) => e.path)).toEqual(['C:\\a', 'D:\\b']);
|
||||
expect(removed.length).toBe(0);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { UndoRedoManager, OperationType, TargetType, type OpRecord } from '../../src/core/undo-redo';
|
||||
import {
|
||||
UndoRedoManager,
|
||||
OperationType,
|
||||
TargetType,
|
||||
type OpRecord,
|
||||
} from '../../src/core/undo-redo';
|
||||
import type { PathEntry } from '../../src/core/path-entry';
|
||||
|
||||
function pe(s: string, enabled: boolean = true): PathEntry {
|
||||
return { path: s, enabled };
|
||||
}
|
||||
|
||||
function makeRecord(type: OperationType, target: TargetType, index: number, count: number, oldPaths: PathEntry[], newPaths: PathEntry[]): OpRecord {
|
||||
function makeRecord(
|
||||
type: OperationType,
|
||||
target: TargetType,
|
||||
index: number,
|
||||
count: number,
|
||||
oldPaths: PathEntry[],
|
||||
newPaths: PathEntry[],
|
||||
): OpRecord {
|
||||
return { type, target, index, count, oldPaths, newPaths };
|
||||
}
|
||||
|
||||
@@ -31,10 +43,10 @@ describe('UndoRedoManager', () => {
|
||||
mgr.push(makeRecord(OperationType.ADD, TargetType.SYSTEM, 2, 1, [], [pe('C:\\NewPath')]));
|
||||
|
||||
const u = mgr.undo(sys, user)!;
|
||||
expect(u[0].map(e => e.path)).toEqual(['C:\\Windows', 'C:\\Program Files']);
|
||||
expect(u[0].map((e) => e.path)).toEqual(['C:\\Windows', 'C:\\Program Files']);
|
||||
|
||||
const r = mgr.redo(...u)!;
|
||||
expect(r[0].map(e => e.path)).toEqual(['C:\\Windows', 'C:\\Program Files', 'C:\\NewPath']);
|
||||
expect(r[0].map((e) => e.path)).toEqual(['C:\\Windows', 'C:\\Program Files', 'C:\\NewPath']);
|
||||
});
|
||||
|
||||
it('DELETE 撤销/重做', () => {
|
||||
@@ -46,11 +58,20 @@ describe('UndoRedoManager', () => {
|
||||
expect(u[0][0].path).toBe(removed.path);
|
||||
|
||||
const r = mgr.redo(...u)!;
|
||||
expect(r[0].map(e => e.path)).toEqual(['C:\\Program Files']);
|
||||
expect(r[0].map((e) => e.path)).toEqual(['C:\\Program Files']);
|
||||
});
|
||||
|
||||
it('EDIT 撤销/重做', () => {
|
||||
mgr.push(makeRecord(OperationType.EDIT, TargetType.SYSTEM, 0, 1, [pe('C:\\Windows')], [pe('C:\\Edited')]));
|
||||
mgr.push(
|
||||
makeRecord(
|
||||
OperationType.EDIT,
|
||||
TargetType.SYSTEM,
|
||||
0,
|
||||
1,
|
||||
[pe('C:\\Windows')],
|
||||
[pe('C:\\Edited')],
|
||||
),
|
||||
);
|
||||
sys[0] = pe('C:\\Edited');
|
||||
|
||||
const u = mgr.undo(sys, user)!;
|
||||
@@ -65,10 +86,10 @@ describe('UndoRedoManager', () => {
|
||||
[sys[0], sys[1]] = [sys[1], sys[0]];
|
||||
|
||||
const u = mgr.undo(sys, user)!;
|
||||
expect(u[0].map(e => e.path)).toEqual(['C:\\Windows', 'C:\\Program Files']);
|
||||
expect(u[0].map((e) => e.path)).toEqual(['C:\\Windows', 'C:\\Program Files']);
|
||||
|
||||
const r = mgr.redo(...u)!;
|
||||
expect(r[0].map(e => e.path)).toEqual(['C:\\Program Files', 'C:\\Windows']);
|
||||
expect(r[0].map((e) => e.path)).toEqual(['C:\\Program Files', 'C:\\Windows']);
|
||||
});
|
||||
|
||||
it('MOVE_DOWN 撤销/重做', () => {
|
||||
@@ -76,7 +97,7 @@ describe('UndoRedoManager', () => {
|
||||
[sys[0], sys[1]] = [sys[1], sys[0]];
|
||||
|
||||
const u = mgr.undo(sys, user)!;
|
||||
expect(u[0].map(e => e.path)).toEqual(['C:\\Windows', 'C:\\Program Files']);
|
||||
expect(u[0].map((e) => e.path)).toEqual(['C:\\Windows', 'C:\\Program Files']);
|
||||
});
|
||||
|
||||
it('CLEAN 撤销/重做', () => {
|
||||
@@ -133,12 +154,12 @@ describe('UndoRedoManager', () => {
|
||||
it('超出栈底/栈顶的安全处理', () => {
|
||||
mgr.push(makeRecord(OperationType.ADD, TargetType.SYSTEM, 2, 1, [], [pe('C:\\NewPath')]));
|
||||
sys.push(pe('C:\\NewPath'));
|
||||
|
||||
|
||||
// undo一次
|
||||
mgr.undo(sys, user);
|
||||
// 再次undo,此时应到达底部返回null
|
||||
expect(mgr.undo(sys, user)).toBeNull();
|
||||
|
||||
|
||||
// redo一次
|
||||
mgr.redo(sys, user);
|
||||
// 再次redo,应到达顶部返回null
|
||||
@@ -160,9 +181,12 @@ describe('UndoRedoManager', () => {
|
||||
// 删除 indices [1, 3](C:\Program Files 和 C:\Extra2)
|
||||
const removed = [sys[1], sys[3]];
|
||||
mgr.push({
|
||||
type: OperationType.DELETE, target: TargetType.SYSTEM,
|
||||
index: 1, count: 2,
|
||||
oldPaths: removed, newPaths: [],
|
||||
type: OperationType.DELETE,
|
||||
target: TargetType.SYSTEM,
|
||||
index: 1,
|
||||
count: 2,
|
||||
oldPaths: removed,
|
||||
newPaths: [],
|
||||
indices: [1, 3],
|
||||
});
|
||||
sys.splice(3, 1);
|
||||
@@ -172,21 +196,29 @@ describe('UndoRedoManager', () => {
|
||||
expect(u[0]).toEqual(old);
|
||||
|
||||
const r = mgr.redo(...u)!;
|
||||
expect(r[0].map(e => e.path)).toEqual(['C:\\Windows', 'C:\\Extra1']);
|
||||
expect(r[0].map((e) => e.path)).toEqual(['C:\\Windows', 'C:\\Extra1']);
|
||||
});
|
||||
|
||||
it('操作 USER 路径', () => {
|
||||
user.push(pe('C:\\NewUserPath'));
|
||||
mgr.push(makeRecord(OperationType.ADD, TargetType.USER, 1, 1, [], [pe('C:\\NewUserPath')]));
|
||||
const u = mgr.undo(sys, user)!;
|
||||
expect(u[1].map(e => e.path)).toEqual(['C:\\Users\\me\\AppData']);
|
||||
expect(u[0].map(e => e.path)).toEqual(['C:\\Windows', 'C:\\Program Files']);
|
||||
expect(u[1].map((e) => e.path)).toEqual(['C:\\Users\\me\\AppData']);
|
||||
expect(u[0].map((e) => e.path)).toEqual(['C:\\Windows', 'C:\\Program Files']);
|
||||
});
|
||||
|
||||
it('TOGGLE 撤销/重做', () => {
|
||||
sys[0] = pe('C:\\Windows', false);
|
||||
mgr.push(makeRecord(OperationType.TOGGLE, TargetType.SYSTEM, 0, 1,
|
||||
[pe('C:\\Windows', true)], [pe('C:\\Windows', false)]));
|
||||
mgr.push(
|
||||
makeRecord(
|
||||
OperationType.TOGGLE,
|
||||
TargetType.SYSTEM,
|
||||
0,
|
||||
1,
|
||||
[pe('C:\\Windows', true)],
|
||||
[pe('C:\\Windows', false)],
|
||||
),
|
||||
);
|
||||
|
||||
const u = mgr.undo(sys, user)!;
|
||||
expect(u[0][0].enabled).toBe(true);
|
||||
@@ -204,9 +236,12 @@ describe('UndoRedoManager', () => {
|
||||
mgr.push({
|
||||
type: OperationType.IMPORT_BOTH,
|
||||
target: TargetType.SYSTEM,
|
||||
index: 0, count: 0,
|
||||
oldPaths: oldSys, newPaths: newSys,
|
||||
oldPathsOther: oldUser, newPathsOther: newUser,
|
||||
index: 0,
|
||||
count: 0,
|
||||
oldPaths: oldSys,
|
||||
newPaths: newSys,
|
||||
oldPathsOther: oldUser,
|
||||
newPathsOther: newUser,
|
||||
});
|
||||
sys = newSys;
|
||||
user = newUser;
|
||||
|
||||
@@ -14,11 +14,13 @@ vi.mock('@tauri-apps/plugin-dialog', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('@/i18n', () => ({
|
||||
default: { t: vi.fn((key: string, opts?: Record<string, unknown>) => {
|
||||
if (key === 'status.deleted') return `已删除 ${opts?.count} 条`;
|
||||
if (key === 'status.saveWarningLongPaths') return 'PATH 长度超限';
|
||||
return key;
|
||||
}) },
|
||||
default: {
|
||||
t: vi.fn((key: string, opts?: Record<string, unknown>) => {
|
||||
if (key === 'status.deleted') return `已删除 ${opts?.count} 条`;
|
||||
if (key === 'status.saveWarningLongPaths') return 'PATH 长度超限';
|
||||
return key;
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/hooks/use-keyboard', () => ({
|
||||
@@ -83,7 +85,9 @@ describe('useAppActions', () => {
|
||||
it('handleNew 打开新建弹窗', async () => {
|
||||
const { useAppActions } = await import('@/hooks/use-app-actions');
|
||||
const { result } = renderHook(() => useAppActions('system', dialogs));
|
||||
act(() => { result.current.handleNew(); });
|
||||
act(() => {
|
||||
result.current.handleNew();
|
||||
});
|
||||
expect(dialogs.setNewDialog).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
@@ -93,9 +97,14 @@ describe('useAppActions', () => {
|
||||
useAppStore.setState({ selectedIndices: [0] });
|
||||
const { useAppActions } = await import('@/hooks/use-app-actions');
|
||||
const { result } = renderHook(() => useAppActions('system', dialogs));
|
||||
act(() => { result.current.handleEdit(); });
|
||||
act(() => {
|
||||
result.current.handleEdit();
|
||||
});
|
||||
expect(dialogs.setEditDialog).toHaveBeenCalledWith({
|
||||
open: true, index: 0, value: 'C:\\Windows', target: TargetType.SYSTEM,
|
||||
open: true,
|
||||
index: 0,
|
||||
value: 'C:\\Windows',
|
||||
target: TargetType.SYSTEM,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -103,7 +112,9 @@ describe('useAppActions', () => {
|
||||
useAppStore.setState({ selectedIndices: [] });
|
||||
const { useAppActions } = await import('@/hooks/use-app-actions');
|
||||
const { result } = renderHook(() => useAppActions('system', dialogs));
|
||||
act(() => { result.current.handleEdit(); });
|
||||
act(() => {
|
||||
result.current.handleEdit();
|
||||
});
|
||||
expect(dialogs.setEditDialog).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -113,15 +124,19 @@ describe('useAppActions', () => {
|
||||
useAppStore.setState({ selectedIndices: [0] });
|
||||
const { useAppActions } = await import('@/hooks/use-app-actions');
|
||||
const { result } = renderHook(() => useAppActions('system', dialogs));
|
||||
act(() => { result.current.handleDelete(); });
|
||||
expect(useAppStore.getState().sysPaths.map(e => e.path)).toEqual(['C:\\Program Files']);
|
||||
act(() => {
|
||||
result.current.handleDelete();
|
||||
});
|
||||
expect(useAppStore.getState().sysPaths.map((e) => e.path)).toEqual(['C:\\Program Files']);
|
||||
});
|
||||
|
||||
it('handleDelete 无选中项不操作', async () => {
|
||||
useAppStore.setState({ selectedIndices: [] });
|
||||
const { useAppActions } = await import('@/hooks/use-app-actions');
|
||||
const { result } = renderHook(() => useAppActions('system', dialogs));
|
||||
act(() => { result.current.handleDelete(); });
|
||||
act(() => {
|
||||
result.current.handleDelete();
|
||||
});
|
||||
expect(useAppStore.getState().sysPaths.length).toBe(2);
|
||||
});
|
||||
|
||||
@@ -131,16 +146,26 @@ describe('useAppActions', () => {
|
||||
useAppStore.setState({ selectedIndices: [1] });
|
||||
const { useAppActions } = await import('@/hooks/use-app-actions');
|
||||
const { result } = renderHook(() => useAppActions('system', dialogs));
|
||||
act(() => { result.current.handleMoveUp(); });
|
||||
expect(useAppStore.getState().sysPaths.map(e => e.path)).toEqual(['C:\\Program Files', 'C:\\Windows']);
|
||||
act(() => {
|
||||
result.current.handleMoveUp();
|
||||
});
|
||||
expect(useAppStore.getState().sysPaths.map((e) => e.path)).toEqual([
|
||||
'C:\\Program Files',
|
||||
'C:\\Windows',
|
||||
]);
|
||||
});
|
||||
|
||||
it('handleMoveDown 下移选中项', async () => {
|
||||
useAppStore.setState({ selectedIndices: [0] });
|
||||
const { useAppActions } = await import('@/hooks/use-app-actions');
|
||||
const { result } = renderHook(() => useAppActions('system', dialogs));
|
||||
act(() => { result.current.handleMoveDown(); });
|
||||
expect(useAppStore.getState().sysPaths.map(e => e.path)).toEqual(['C:\\Program Files', 'C:\\Windows']);
|
||||
act(() => {
|
||||
result.current.handleMoveDown();
|
||||
});
|
||||
expect(useAppStore.getState().sysPaths.map((e) => e.path)).toEqual([
|
||||
'C:\\Program Files',
|
||||
'C:\\Windows',
|
||||
]);
|
||||
});
|
||||
|
||||
// ── handleClean ──
|
||||
@@ -149,8 +174,10 @@ describe('useAppActions', () => {
|
||||
resetStore([pe('C:\\Windows'), pe('invalid_path!@#')]);
|
||||
const { useAppActions } = await import('@/hooks/use-app-actions');
|
||||
const { result } = renderHook(() => useAppActions('system', dialogs));
|
||||
act(() => { result.current.handleClean(); });
|
||||
expect(useAppStore.getState().sysPaths.map(e => e.path)).toEqual(['C:\\Windows']);
|
||||
act(() => {
|
||||
result.current.handleClean();
|
||||
});
|
||||
expect(useAppStore.getState().sysPaths.map((e) => e.path)).toEqual(['C:\\Windows']);
|
||||
expect(useAppStore.getState().statusMessage).toContain('已删除 1 条');
|
||||
});
|
||||
|
||||
@@ -159,15 +186,19 @@ describe('useAppActions', () => {
|
||||
it('handleNewConfirm 添加新路径', async () => {
|
||||
const { useAppActions } = await import('@/hooks/use-app-actions');
|
||||
const { result } = renderHook(() => useAppActions('system', dialogs));
|
||||
act(() => { result.current.handleNewConfirm('C:\\New'); });
|
||||
expect(useAppStore.getState().sysPaths.map(e => e.path)).toContain('C:\\New');
|
||||
act(() => {
|
||||
result.current.handleNewConfirm('C:\\New');
|
||||
});
|
||||
expect(useAppStore.getState().sysPaths.map((e) => e.path)).toContain('C:\\New');
|
||||
expect(dialogs.setNewDialog).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('handleNewConfirm 空白不添加', async () => {
|
||||
const { useAppActions } = await import('@/hooks/use-app-actions');
|
||||
const { result } = renderHook(() => useAppActions('system', dialogs));
|
||||
act(() => { result.current.handleNewConfirm(' '); });
|
||||
act(() => {
|
||||
result.current.handleNewConfirm(' ');
|
||||
});
|
||||
expect(useAppStore.getState().sysPaths.length).toBe(2);
|
||||
});
|
||||
|
||||
@@ -177,7 +208,9 @@ describe('useAppActions', () => {
|
||||
dialogs.editDialog = { open: true, index: 0, value: 'C:\\Windows', target: TargetType.SYSTEM };
|
||||
const { useAppActions } = await import('@/hooks/use-app-actions');
|
||||
const { result } = renderHook(() => useAppActions('system', dialogs));
|
||||
act(() => { result.current.handleEditConfirm('C:\\Edited'); });
|
||||
act(() => {
|
||||
result.current.handleEditConfirm('C:\\Edited');
|
||||
});
|
||||
expect(useAppStore.getState().sysPaths[0].path).toBe('C:\\Edited');
|
||||
});
|
||||
|
||||
@@ -189,19 +222,27 @@ describe('useAppActions', () => {
|
||||
dialogs.importDialog = { open: true, system: sysImport, user: usrImport };
|
||||
const { useAppActions } = await import('@/hooks/use-app-actions');
|
||||
const { result } = renderHook(() => useAppActions('system', dialogs));
|
||||
act(() => { result.current.handleImportSelect('both'); });
|
||||
expect(useAppStore.getState().sysPaths.map(e => e.path)).toEqual(['C:\\ImportSys']);
|
||||
expect(useAppStore.getState().userPaths.map(e => e.path)).toEqual(['D:\\ImportUsr']);
|
||||
act(() => {
|
||||
result.current.handleImportSelect('both');
|
||||
});
|
||||
expect(useAppStore.getState().sysPaths.map((e) => e.path)).toEqual(['C:\\ImportSys']);
|
||||
expect(useAppStore.getState().userPaths.map((e) => e.path)).toEqual(['D:\\ImportUsr']);
|
||||
expect(dialogs.setImportDialog).toHaveBeenCalledWith({ open: false, system: [], user: [] });
|
||||
});
|
||||
|
||||
it('handleImportSelect system 模式只替换 system', async () => {
|
||||
dialogs.importDialog = { open: true, system: [pe('C:\\ImportSys')], user: [pe('D:\\ImportUsr')] };
|
||||
dialogs.importDialog = {
|
||||
open: true,
|
||||
system: [pe('C:\\ImportSys')],
|
||||
user: [pe('D:\\ImportUsr')],
|
||||
};
|
||||
const { useAppActions } = await import('@/hooks/use-app-actions');
|
||||
const { result } = renderHook(() => useAppActions('system', dialogs));
|
||||
act(() => { result.current.handleImportSelect('system'); });
|
||||
expect(useAppStore.getState().sysPaths.map(e => e.path)).toEqual(['C:\\ImportSys']);
|
||||
expect(useAppStore.getState().userPaths.map(e => e.path)).toEqual(['D:\\User']); // 未变
|
||||
act(() => {
|
||||
result.current.handleImportSelect('system');
|
||||
});
|
||||
expect(useAppStore.getState().sysPaths.map((e) => e.path)).toEqual(['C:\\ImportSys']);
|
||||
expect(useAppStore.getState().userPaths.map((e) => e.path)).toEqual(['D:\\User']); // 未变
|
||||
});
|
||||
|
||||
// ── handleSave ──
|
||||
@@ -211,7 +252,9 @@ describe('useAppActions', () => {
|
||||
vi.spyOn(useAppStore.getState(), 'savePaths').mockResolvedValue({ kind: 'success' });
|
||||
const { useAppActions } = await import('@/hooks/use-app-actions');
|
||||
const { result } = renderHook(() => useAppActions('system', dialogs));
|
||||
await act(async () => { await result.current.handleSave(); });
|
||||
await act(async () => {
|
||||
await result.current.handleSave();
|
||||
});
|
||||
// savePaths is called
|
||||
expect(useAppStore.getState().savePaths).toHaveBeenCalled();
|
||||
});
|
||||
@@ -227,7 +270,9 @@ describe('useAppActions', () => {
|
||||
});
|
||||
const { useAppActions } = await import('@/hooks/use-app-actions');
|
||||
const { result } = renderHook(() => useAppActions('system', dialogs));
|
||||
await act(async () => { await result.current.handleSave(); });
|
||||
await act(async () => {
|
||||
await result.current.handleSave();
|
||||
});
|
||||
expect(callCount).toBe(2);
|
||||
expect(mockAsk).toHaveBeenCalled();
|
||||
});
|
||||
@@ -240,7 +285,9 @@ describe('useAppActions', () => {
|
||||
});
|
||||
const { useAppActions } = await import('@/hooks/use-app-actions');
|
||||
const { result } = renderHook(() => useAppActions('system', dialogs));
|
||||
await act(async () => { await result.current.handleSave(); });
|
||||
await act(async () => {
|
||||
await result.current.handleSave();
|
||||
});
|
||||
expect(callCount).toBe(1); // 仅调用一次,不重试
|
||||
expect(mockAsk).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user