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:
@@ -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 不变
|
||||
|
||||
Reference in New Issue
Block a user