145 lines
6.1 KiB
TypeScript
145 lines
6.1 KiB
TypeScript
// tests/unit/renderer/hooks/use-folder-ops.test.ts
|
|
//
|
|
// 守 r227「文件夹操作 + 粘贴反馈包装」收编契约(收编 ④):
|
|
// NotesList / LiteratureList 里逐字相同的 runFolderOp(r173)/
|
|
// commitFolderEdit(r176)/ pasteTo(0007) 三段收进 hooks/use-folder-ops.ts;
|
|
// 唯一分叉 = log 消息前缀(logTag)与 paste 失败 toast 的 i18n key
|
|
// (pasteFailedKey)—— 两域 locale 树不同。
|
|
//
|
|
// ── 锁两类东西(与 r224-r226 同模式)────────────────────────────────
|
|
// 1. 行为契约(收编前既有行为,对 HEAD 也必须绿):
|
|
// op 失败 ⇒ log + 错误 toast,不吞异常;成功 ⇒ 静默;
|
|
// commitFolderEdit 成功 true / 失败 false(输入框回可编辑态,不卡 busy);
|
|
// pasteTo 失败用注入的 key 弹 toast;pasteToTarget 落点 = 当前落点文件夹;
|
|
// 2. 结构契约:组件源码不得再出现 'folder op failed' / 'folder edit failed'
|
|
// —— 孪生块复活即红(修前红)。
|
|
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
|
import { renderHook } from '@testing-library/react';
|
|
import { readFileSync } from 'fs';
|
|
import { join } from 'path';
|
|
|
|
// i18n —— t 回显 key,便于断言「用的是哪个 key」。
|
|
vi.mock('react-i18next', () => ({
|
|
useTranslation: () => ({
|
|
t: (key: string, opts?: { msg?: string }) => (opts?.msg ? `${key}|${opts.msg}` : key),
|
|
}),
|
|
}));
|
|
|
|
// toast —— 只记录调用,不引入 zustand store。
|
|
const toastCalls: { type: string; message: string }[] = [];
|
|
vi.mock('../../../../src/renderer/src/lib/toast', () => ({
|
|
useToast: () => (opts: { type: string; message: string }) => {
|
|
toastCalls.push(opts);
|
|
},
|
|
}));
|
|
|
|
// logger —— 记录 error 消息(验证 logTag 注入)。
|
|
const logErrors: { msg: string; err: unknown }[] = [];
|
|
vi.mock('../../../../src/renderer/src/lib/logger-client', () => ({
|
|
createLogger: () => ({
|
|
error: (msg: string, err: unknown) => logErrors.push({ msg, err }),
|
|
warn: () => {},
|
|
info: () => {},
|
|
debug: () => {},
|
|
}),
|
|
}));
|
|
|
|
import { useFolderOps } from '../../../../src/renderer/src/hooks/use-folder-ops';
|
|
import type { DiskFolderSidebar } from '../../../../src/renderer/src/hooks/useDiskFolderSidebar';
|
|
|
|
function setup(over: { pasteFailedKey?: string; logTag?: string } = {}) {
|
|
const commitEdit = vi.fn().mockResolvedValue(undefined);
|
|
const folders = {
|
|
commitEdit,
|
|
targetRelPath: 'sub/dir',
|
|
} as unknown as DiskFolderSidebar;
|
|
const pasteClipboard = vi.fn().mockResolvedValue(undefined);
|
|
const hook = renderHook(() =>
|
|
useFolderOps({
|
|
logTag: over.logTag ?? 'NotesList',
|
|
pasteFailedKey: over.pasteFailedKey ?? 'toast.pasteNoteFailed',
|
|
folders,
|
|
pasteClipboard,
|
|
}),
|
|
);
|
|
return { ...hook, commitEdit, pasteClipboard };
|
|
}
|
|
|
|
beforeEach(() => {
|
|
toastCalls.length = 0;
|
|
logErrors.length = 0;
|
|
});
|
|
|
|
describe('useFolderOps(r227 收编块的行为契约)', () => {
|
|
it('runFolderOp:成功静默;失败 ⇒ log + 错误 toast(不吞异常)', async () => {
|
|
const { result } = setup();
|
|
result.current.runFolderOp(() => Promise.resolve('ok'));
|
|
await Promise.resolve();
|
|
expect(toastCalls).toHaveLength(0);
|
|
expect(logErrors).toHaveLength(0);
|
|
|
|
const boom = new Error('boom');
|
|
result.current.runFolderOp(() => Promise.reject(boom));
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
expect(logErrors[0].msg).toBe('NotesList folder op failed');
|
|
expect(logErrors[0].err).toBe(boom);
|
|
expect(toastCalls[0].type).toBe('error');
|
|
expect(toastCalls[0].message).toContain('toast.folderMoveFailed');
|
|
});
|
|
|
|
it('runFolderOp:log 前缀随 logTag 注入(两域各用自己的组件名)', async () => {
|
|
const { result } = setup({ logTag: 'LiteratureList' });
|
|
result.current.runFolderOp(() => Promise.reject(new Error('x')));
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
expect(logErrors[0].msg).toBe('LiteratureList folder op failed');
|
|
});
|
|
|
|
it('commitFolderEdit:成功 ⇒ true 且无 toast;失败 ⇒ false + 错误 toast', async () => {
|
|
const { result, commitEdit } = setup();
|
|
await expect(result.current.commitFolderEdit('newName')).resolves.toBe(true);
|
|
expect(commitEdit).toHaveBeenCalledWith('newName');
|
|
expect(toastCalls).toHaveLength(0);
|
|
|
|
commitEdit.mockRejectedValueOnce(new Error('bad'));
|
|
await expect(result.current.commitFolderEdit('x')).resolves.toBe(false);
|
|
expect(toastCalls[0].type).toBe('error');
|
|
expect(toastCalls[0].message).toContain('toast.folderMoveFailed');
|
|
expect(logErrors[0].msg).toBe('NotesList folder edit failed');
|
|
});
|
|
|
|
it('pasteTo:失败用注入的 i18n key 弹 toast;成功静默', async () => {
|
|
const { result, pasteClipboard } = setup({ pasteFailedKey: 'literature.pasteFailedToast' });
|
|
pasteClipboard.mockRejectedValueOnce(new Error('nope'));
|
|
await result.current.pasteTo('target');
|
|
expect(toastCalls[0].message).toContain('literature.pasteFailedToast');
|
|
expect(logErrors[0].msg).toBe('NotesList paste failed');
|
|
|
|
toastCalls.length = 0;
|
|
logErrors.length = 0;
|
|
await result.current.pasteTo('target');
|
|
expect(toastCalls).toHaveLength(0);
|
|
expect(logErrors).toHaveLength(0);
|
|
});
|
|
|
|
it('pasteToTarget:落点 = 当前落点文件夹(folders.targetRelPath)', () => {
|
|
const { result, pasteClipboard } = setup();
|
|
result.current.pasteToTarget();
|
|
expect(pasteClipboard).toHaveBeenCalledWith('sub/dir');
|
|
});
|
|
});
|
|
|
|
describe('r227 结构契约:孪生块不得复活', () => {
|
|
const ROOT = process.cwd();
|
|
for (const rel of [
|
|
'src/renderer/src/components/notes/NotesList.tsx',
|
|
'src/renderer/src/components/literature/LiteratureList.tsx',
|
|
]) {
|
|
it(`${rel} 走 useFolderOps,不再自带 op 反馈包装`, () => {
|
|
const src = readFileSync(join(ROOT, rel), 'utf-8');
|
|
expect(src, '文件夹操作/粘贴包装必须走 hooks/use-folder-ops').toContain('useFolderOps');
|
|
expect(src, '孪生块复活:组件里又出现 runFolderOp 的实现').not.toContain('folder op failed');
|
|
expect(src, '孪生块复活:组件里又出现 commitFolderEdit 的实现').not.toContain('folder edit failed');
|
|
});
|
|
}
|
|
});
|