diff --git a/tests/unit/app-store.test.ts b/tests/unit/app-store.test.ts index c93d41b..2fcc31c 100644 --- a/tests/unit/app-store.test.ts +++ b/tests/unit/app-store.test.ts @@ -19,6 +19,12 @@ vi.mock('@/i18n', () => ({ }) }, })); +import type { PathEntry } from '../../src/core/path-entry'; + +function pe(s: string, enabled: boolean = true): PathEntry { + return { path: s, enabled }; +} + import { useAppStore } from '@/store/app-store'; import { UndoRedoManager, TargetType } from '@/core/undo-redo'; import { invoke } from '@tauri-apps/api/core'; @@ -50,7 +56,7 @@ describe('app-store CRUD', () => { it('addPath 追加到 sysPaths', () => { useAppStore.getState().addPath('C:\\test', TargetType.SYSTEM); const s = useAppStore.getState(); - expect(s.sysPaths).toEqual(['C:\\test']); + expect(s.sysPaths.map(e => e.path)).toEqual(['C:\\test']); expect(s.isModified).toBe(true); expect(s.undoRedo.historyLength).toBe(1); }); @@ -58,7 +64,7 @@ describe('app-store CRUD', () => { it('addPath 追加到 userPaths', () => { useAppStore.getState().addPath('D:\\user', TargetType.USER); const s = useAppStore.getState(); - expect(s.userPaths).toEqual(['D:\\user']); + expect(s.userPaths.map(e => e.path)).toEqual(['D:\\user']); expect(s.sysPaths).toEqual([]); }); @@ -66,7 +72,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).toEqual(['C:\\new']); + expect(useAppStore.getState().sysPaths.map(e => e.path)).toEqual(['C:\\new']); }); it('editPath 越界 index 无崩溃', () => { @@ -81,7 +87,7 @@ describe('app-store CRUD', () => { 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().sysPaths.map(e => e.path)).toEqual(['A', 'C']); expect(useAppStore.getState().selectedIndices).toEqual([]); }); @@ -92,7 +98,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).toEqual(['A', 'C']); + expect(useAppStore.getState().userPaths.map(e => e.path)).toEqual(['A', 'C']); }); it('deletePaths 非连续多选删除后可 undo 恢复到正确位置', () => { @@ -102,16 +108,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).toEqual(['A', 'C']); + expect(useAppStore.getState().sysPaths.map(e => e.path)).toEqual(['A', 'C']); useAppStore.getState().undo(); - expect(useAppStore.getState().sysPaths).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).toEqual(['A']); + expect(useAppStore.getState().sysPaths.map(e => e.path)).toEqual(['A']); }); it('moveUp 正常交换位置', () => { @@ -119,7 +125,7 @@ describe('app-store CRUD', () => { 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().sysPaths.map(e => e.path)).toEqual(['B', 'A']); expect(useAppStore.getState().selectedIndices).toEqual([0]); }); @@ -127,7 +133,7 @@ describe('app-store CRUD', () => { const store = useAppStore.getState(); store.addPath('A', TargetType.USER); store.moveDown(0, TargetType.USER); - expect(useAppStore.getState().userPaths).toEqual(['A']); + expect(useAppStore.getState().userPaths.map(e => e.path)).toEqual(['A']); }); it('cleanPaths 移除无效路径并返回 removed', () => { @@ -137,7 +143,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).toEqual(['C:\\valid']); + expect(useAppStore.getState().sysPaths.map(e => e.path)).toEqual(['C:\\valid']); }); it('replacePaths 整体替换列表', () => { @@ -145,7 +151,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).toEqual(['new1', 'new2', 'new3']); + expect(useAppStore.getState().userPaths.map(e => e.path)).toEqual(['new1', 'new2', 'new3']); }); it('clearPaths 清空列表', () => { @@ -181,7 +187,7 @@ describe('undo/redo', () => { store.addPath('test', TargetType.SYSTEM); store.undo(); store.redo(); - expect(useAppStore.getState().sysPaths).toEqual(['test']); + expect(useAppStore.getState().sysPaths.map(e => e.path)).toEqual(['test']); }); it('undo/redo 正确更新 isModified', () => { @@ -208,8 +214,8 @@ describe('loadPaths', () => { 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.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); }); @@ -285,8 +291,8 @@ describe('initialize', () => { await useAppStore.getState().initialize(); const s = useAppStore.getState(); expect(s.isAdmin).toBe(true); - expect(s.sysPaths).toEqual(['S1']); - expect(s.userPaths).toEqual(['U1']); + expect(s.sysPaths.map(e => e.path)).toEqual(['S1']); + expect(s.userPaths.map(e => e.path)).toEqual(['U1']); }); it('非管理员初始化进入只读模式', async () => { diff --git a/tests/unit/import-export.test.ts b/tests/unit/import-export.test.ts index 5c957cf..96decc2 100644 --- a/tests/unit/import-export.test.ts +++ b/tests/unit/import-export.test.ts @@ -9,30 +9,41 @@ import { detectExportFormat, flattenImportResult, } from '../../src/core/import-export'; +import type { PathEntry } from '../../src/core/path-entry'; + +function pe(s: string, enabled: boolean = true): PathEntry { + return { path: s, enabled }; +} const sampleData = { - system: ['C:\\Windows', 'C:\\Program Files'], - user: ['C:\\Users\\me\\AppData'], + system: [pe('C:\\Windows'), pe('C:\\Program Files')], + user: [pe('C:\\Users\\me\\AppData')], }; describe('exportToJson', () => { it('导出结构化 JSON', () => { - const json = exportToJson(sampleData); + const json = exportToJson({ + system: sampleData.system.map(e => e.path), + user: sampleData.user.map(e => e.path), + }); const parsed = JSON.parse(json); expect(parsed.version).toBe('1.0'); expect(parsed.type).toBe('PathEditor'); - expect(parsed.system).toEqual(sampleData.system); - expect(parsed.user).toEqual(sampleData.user); + expect(parsed.system).toEqual(sampleData.system.map(e => e.path)); + expect(parsed.user).toEqual(sampleData.user.map(e => e.path)); expect(parsed.exported).toBeDefined(); }); }); describe('importFromJson', () => { it('正确导入 JSON', () => { - const json = JSON.stringify(sampleData); + const json = JSON.stringify({ + system: sampleData.system.map(e => e.path), + user: sampleData.user.map(e => e.path), + }); const result = importFromJson(json); - expect(result.system).toEqual(sampleData.system); - expect(result.user).toEqual(sampleData.user); + expect(result.system).toEqual(sampleData.system.map(e => e.path)); + expect(result.user).toEqual(sampleData.user.map(e => e.path)); }); it('过滤空字符串', () => { @@ -44,7 +55,10 @@ describe('importFromJson', () => { describe('exportToCsv', () => { it('导出 CSV 含 BOM', () => { - const csv = exportToCsv(sampleData); + const csv = exportToCsv({ + system: sampleData.system.map(e => e.path), + user: sampleData.user.map(e => e.path), + }); expect(csv.startsWith('')).toBe(true); expect(csv).toContain('type,path'); expect(csv).toContain('system,C:\\Windows'); diff --git a/tests/unit/path-manager.test.ts b/tests/unit/path-manager.test.ts index 1e4833b..7ccf946 100644 --- a/tests/unit/path-manager.test.ts +++ b/tests/unit/path-manager.test.ts @@ -1,30 +1,35 @@ import { describe, it, expect } from 'vitest'; import { pathClean } from '../../src/core/path-manager'; +import type { PathEntry } from '../../src/core/path-entry'; + +function pe(s: string, enabled: boolean = true): PathEntry { + return { path: s, enabled }; +} const alwaysValid = () => true; const validateFn = (path: string) => !path.includes('Invalid'); describe('pathClean', () => { it('移除无效路径', () => { - const [kept, removed] = pathClean(['C:\\Valid', 'C:\\Invalid', 'D:\\Valid'], validateFn); - expect(kept).toEqual(['C:\\Valid', 'D:\\Valid']); - expect(removed).toEqual(['C:\\Invalid']); + const [kept, removed] = pathClean([pe('C:\\Valid'), pe('C:\\Invalid'), pe('D:\\Valid')], validateFn as any); + 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(['C:\\Valid', 'C:\\Valid', 'D:\\Valid'], alwaysValid); + const [kept, removed] = pathClean([pe('C:\\Valid'), pe('C:\\Valid'), pe('D:\\Valid')], alwaysValid as any); expect(kept.length).toBe(2); expect(removed.length).toBe(1); }); it('全部有效无变化', () => { - const [kept, removed] = pathClean(['C:\\a', 'D:\\b'], alwaysValid); - expect(kept).toEqual(['C:\\a', 'D:\\b']); + const [kept, removed] = pathClean([pe('C:\\a'), pe('D:\\b')], alwaysValid as any); + expect(kept.map(e => e.path)).toEqual(['C:\\a', 'D:\\b']); expect(removed.length).toBe(0); }); it('全部无效全部移除', () => { - const [kept, removed] = pathClean(['C:\\Invalid1', 'C:\\Invalid2'], validateFn); + const [kept, removed] = pathClean([pe('C:\\Invalid1'), pe('C:\\Invalid2')], validateFn as any); expect(kept.length).toBe(0); expect(removed.length).toBe(2); }); diff --git a/tests/unit/undo-redo.test.ts b/tests/unit/undo-redo.test.ts index b3b90ba..ca4258f 100644 --- a/tests/unit/undo-redo.test.ts +++ b/tests/unit/undo-redo.test.ts @@ -1,19 +1,24 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { UndoRedoManager, OperationType, TargetType, type OpRecord } from '../../src/core/undo-redo'; +import type { PathEntry } from '../../src/core/path-entry'; -function makeRecord(type: OperationType, target: TargetType, index: number, count: number, oldPaths: string[], newPaths: string[]): OpRecord { +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 { return { type, target, index, count, oldPaths, newPaths }; } describe('UndoRedoManager', () => { let mgr: UndoRedoManager; - let sys: string[]; - let user: string[]; + let sys: PathEntry[]; + let user: PathEntry[]; beforeEach(() => { mgr = new UndoRedoManager(50); - sys = ['C:\\Windows', 'C:\\Program Files']; - user = ['C:\\Users\\me\\AppData']; + sys = [pe('C:\\Windows'), pe('C:\\Program Files')]; + user = [pe('C:\\Users\\me\\AppData')]; }); it('初始不可撤销不可重做', () => { @@ -22,14 +27,14 @@ describe('UndoRedoManager', () => { }); it('ADD 撤销/重做', () => { - sys.push('C:\\NewPath'); - mgr.push(makeRecord(OperationType.ADD, TargetType.SYSTEM, 2, 1, [], ['C:\\NewPath'])); + sys.push(pe('C:\\NewPath')); + mgr.push(makeRecord(OperationType.ADD, TargetType.SYSTEM, 2, 1, [], [pe('C:\\NewPath')])); const u = mgr.undo(sys, user)!; - expect(u[0]).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]).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 撤销/重做', () => { @@ -38,21 +43,21 @@ describe('UndoRedoManager', () => { sys.splice(0, 1); const u = mgr.undo(sys, user)!; - expect(u[0][0]).toBe(removed); + expect(u[0][0].path).toBe(removed.path); const r = mgr.redo(...u)!; - expect(r[0]).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, ['C:\\Windows'], ['C:\\Edited'])); - sys[0] = '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)!; - expect(u[0][0]).toBe('C:\\Windows'); + expect(u[0][0].path).toBe('C:\\Windows'); const r = mgr.redo(...u)!; - expect(r[0][0]).toBe('C:\\Edited'); + expect(r[0][0].path).toBe('C:\\Edited'); }); it('MOVE_UP 撤销/重做', () => { @@ -60,10 +65,10 @@ describe('UndoRedoManager', () => { [sys[0], sys[1]] = [sys[1], sys[0]]; const u = mgr.undo(sys, user)!; - expect(u[0]).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]).toEqual(['C:\\Program Files', 'C:\\Windows']); + expect(r[0].map(e => e.path)).toEqual(['C:\\Program Files', 'C:\\Windows']); }); it('MOVE_DOWN 撤销/重做', () => { @@ -71,12 +76,12 @@ describe('UndoRedoManager', () => { [sys[0], sys[1]] = [sys[1], sys[0]]; const u = mgr.undo(sys, user)!; - expect(u[0]).toEqual(['C:\\Windows', 'C:\\Program Files']); + expect(u[0].map(e => e.path)).toEqual(['C:\\Windows', 'C:\\Program Files']); }); it('CLEAN 撤销/重做', () => { const old = [...sys]; - const cleaned = ['C:\\Windows']; + const cleaned = [pe('C:\\Windows')]; mgr.push(makeRecord(OperationType.CLEAN, TargetType.SYSTEM, 0, 2, old, cleaned)); sys = cleaned; @@ -101,7 +106,7 @@ describe('UndoRedoManager', () => { it('IMPORT 撤销/重做', () => { const old = [...sys]; - const imported = ['C:\\New1', 'C:\\New2']; + const imported = [pe('C:\\New1'), pe('C:\\New2')]; mgr.push(makeRecord(OperationType.IMPORT, TargetType.SYSTEM, 0, 2, old, imported)); sys = imported; @@ -113,24 +118,24 @@ describe('UndoRedoManager', () => { }); it('新操作后截断重做分支', () => { - mgr.push(makeRecord(OperationType.ADD, TargetType.SYSTEM, 0, 1, [], ['first'])); + mgr.push(makeRecord(OperationType.ADD, TargetType.SYSTEM, 0, 1, [], [pe('first')])); mgr.undo(sys, user); expect(mgr.canRedo()).toBe(true); - mgr.push(makeRecord(OperationType.ADD, TargetType.SYSTEM, 0, 1, [], ['second'])); + mgr.push(makeRecord(OperationType.ADD, TargetType.SYSTEM, 0, 1, [], [pe('second')])); expect(mgr.canRedo()).toBe(false); }); it('超出最大历史容量时移除最旧记录', () => { const small = new UndoRedoManager(3); for (let i = 0; i < 5; i++) { - small.push(makeRecord(OperationType.ADD, TargetType.SYSTEM, 0, 1, [], [`path_${i}`])); + small.push(makeRecord(OperationType.ADD, TargetType.SYSTEM, 0, 1, [], [pe(`path_${i}`)])); } expect(small.historyLength).toBe(3); }); it('非连续多选 DELETE 撤销恢复到原始位置', () => { // 扩展初始数组 - sys.push('C:\\Extra1', 'C:\\Extra2'); + sys.push(pe('C:\\Extra1'), pe('C:\\Extra2')); const old = [...sys]; // 删除 indices [1, 3](C:\Program Files 和 C:\Extra2) const removed = [sys[1], sys[3]]; @@ -147,14 +152,26 @@ describe('UndoRedoManager', () => { expect(u[0]).toEqual(old); const r = mgr.redo(...u)!; - expect(r[0]).toEqual(['C:\\Windows', 'C:\\Extra1']); + expect(r[0].map(e => e.path)).toEqual(['C:\\Windows', 'C:\\Extra1']); }); it('操作 USER 路径', () => { - user.push('C:\\NewUserPath'); - mgr.push(makeRecord(OperationType.ADD, TargetType.USER, 1, 1, [], ['C:\\NewUserPath'])); + 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]).toEqual(['C:\\Users\\me\\AppData']); - expect(u[0]).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)])); + + const u = mgr.undo(sys, user)!; + expect(u[0][0].enabled).toBe(true); + + const r = mgr.redo(...u)!; + expect(r[0][0].enabled).toBe(false); }); });