This commit is contained in:
2026-10-21 14:42:59 +08:00
commit 954236d3f9
399 changed files with 85885 additions and 0 deletions

View File

@@ -0,0 +1,180 @@
// tests/unit/renderer/lib/drop.test.ts
//
// 守 src/renderer/src/lib/drop.ts 的三条契约(指针原在
// src/renderer/src/components/notes/NotesList.tsx 的「🔒(守卫缺失)」标记里,
// 本文件补齐后该标记已摘除 —— 见 tests/unit/main/lint/stale-test-reference.test.ts
// 的反向守卫:引用的测试已存在却还留着 🔒 标记 ⇒ 红)。
//
// A. getDroppedFilePath —— 路径提取的**优先级**与「拿不到返空串」:
// webUtils.getPathForFile 优先 > legacy File.path 兜底 > ''
// B. collectDroppedPaths —— 顺序保留(与原 FileList 下标对得上)、过滤空串
// C. classifyDroppedNotesFiles —— 笔记面板**三桶**分类:
// paths / invalidTypeNames / noPathNames 严格互斥,
// 「看起来是 .md 但拿不到 OS 路径」必须**独立成桶**(不能被「非 .md」吞掉)
//
// 与既有 drop-bridge-missing.test.ts 的分工:那一份专门锁「桥缺失/桥抛异常」
// 的降级路径(不 mock api 模块);本文件锁**正常路径**下的语义与优先级。
// 两份都不 vi.mock api 模块 —— 直接改写 window.api,api Proxy 每次属性
// 读取实时取 window.api 当前值(见 src/renderer/src/lib/api.ts 头注)。
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import type { Api } from '@shared/api-types';
import {
classifyDroppedNotesFiles,
collectDroppedPaths,
getDroppedFilePath,
} from '../../../../src/renderer/src/lib/drop';
const originalApi = (globalThis as { window?: Window }).window?.api;
/** 造一个只有 name(+ 可选 legacy path)的 File 替身。 */
function fakeFile(name: string, legacyPath?: string): File {
const f = { name } as unknown as File;
if (legacyPath !== undefined) {
(f as unknown as { path?: string }).path = legacyPath;
}
return f;
}
/** 用 map 装一个 getPathForFile:命中返回路径,未命中返回 null。 */
function installBridge(paths: Record<string, string | null>): void {
(globalThis as unknown as { window: { api: Api } }).window.api = {
getPathForFile: (file: File) => paths[file.name] ?? null,
} as unknown as Api;
}
beforeEach(() => {
installBridge({});
});
afterEach(() => {
(globalThis as unknown as { window: { api: Api } }).window.api = originalApi as Api;
});
describe('getDroppedFilePath 路径提取优先级', () => {
it('优先用 webUtils.getPathForFile', () => {
installBridge({ 'a.md': '/from/webutils/a.md' });
const f = fakeFile('a.md', '/legacy/a.md');
expect(getDroppedFilePath(f)).toBe('/from/webutils/a.md');
});
it('webUtils 返回空串 → 退回 legacy file.path', () => {
installBridge({ 'a.md': '' });
expect(getDroppedFilePath(fakeFile('a.md', '/legacy/a.md'))).toBe('/legacy/a.md');
});
it('webUtils 返回 null → 退回 legacy file.path', () => {
installBridge({ 'a.md': null });
expect(getDroppedFilePath(fakeFile('a.md', '/legacy/a.md'))).toBe('/legacy/a.md');
});
it('webUtils 桥缺失 → 退回 legacy file.path', () => {
(globalThis as unknown as { window: { api: Api } }).window.api = {} as unknown as Api;
expect(getDroppedFilePath(fakeFile('a.md', '/legacy/a.md'))).toBe('/legacy/a.md');
});
it('两路都拿不到 → 返空串(绝不抛,也绝不返回 undefined)', () => {
expect(getDroppedFilePath(fakeFile('a.md'))).toBe('');
});
it('legacy path 是空串 → 同样视为拿不到', () => {
expect(getDroppedFilePath(fakeFile('a.md', ''))).toBe('');
});
});
describe('collectDroppedPaths 批量版', () => {
it('顺序保留(与原 FileList 下标对得上)', () => {
installBridge({
'a.md': '/p/a.md',
'b.md': '/p/b.md',
'c.md': '/p/c.md',
});
const paths = collectDroppedPaths([fakeFile('a.md'), fakeFile('b.md'), fakeFile('c.md')]);
expect(paths).toEqual(['/p/a.md', '/p/b.md', '/p/c.md']);
});
it('过滤掉拿不到路径的文件,其余顺序不变', () => {
installBridge({ 'a.md': '/p/a.md', 'c.md': '/p/c.md' });
const paths = collectDroppedPaths([fakeFile('a.md'), fakeFile('b.md'), fakeFile('c.md')]);
expect(paths).toEqual(['/p/a.md', '/p/c.md']);
});
it('空数组 → 空数组', () => {
expect(collectDroppedPaths([])).toEqual([]);
});
});
describe('classifyDroppedNotesFiles 三桶分类', () => {
it('.md → paths 桶', () => {
installBridge({ 'a.md': '/p/a.md' });
expect(classifyDroppedNotesFiles([fakeFile('a.md')])).toEqual({
paths: ['/p/a.md'],
invalidTypeNames: [],
noPathNames: [],
});
});
it('.markdown 也算合法后缀', () => {
installBridge({ 'a.markdown': '/p/a.markdown' });
expect(classifyDroppedNotesFiles([fakeFile('a.markdown')]).paths).toEqual(['/p/a.markdown']);
});
it('后缀大小写不敏感(.MD / .MarkDown 都算 .md)', () => {
installBridge({ 'A.MD': '/p/A.MD', 'B.MarkDown': '/p/B.MarkDown' });
const r = classifyDroppedNotesFiles([fakeFile('A.MD'), fakeFile('B.MarkDown')]);
expect(r.paths).toEqual(['/p/A.MD', '/p/B.MarkDown']);
expect(r.invalidTypeNames).toEqual([]);
expect(r.noPathNames).toEqual([]);
});
it('非 .md/.markdown → invalidTypeNames 桶(不查路径)', () => {
installBridge({ 'a.txt': '/p/a.txt' });
const r = classifyDroppedNotesFiles([fakeFile('a.txt')]);
expect(r.paths).toEqual([]);
expect(r.invalidTypeNames).toEqual(['a.txt']);
expect(r.noPathNames).toEqual([]);
});
it('.md 但拿不到 OS 路径 → noPathNames 桶(独立成桶,不被「非 .md」吞掉)', () => {
installBridge({}); // 桥给不出路径
const r = classifyDroppedNotesFiles([fakeFile('lost.md')]);
expect(r.paths).toEqual([]);
expect(r.invalidTypeNames).toEqual([]);
expect(r.noPathNames).toEqual(['lost.md']);
});
it('三桶互斥:一个文件只落一个桶', () => {
installBridge({ 'ok.md': '/p/ok.md' });
const r = classifyDroppedNotesFiles([
fakeFile('ok.md'),
fakeFile('bad.txt'),
fakeFile('lost.md'),
]);
expect(r).toEqual({
paths: ['/p/ok.md'],
invalidTypeNames: ['bad.txt'],
noPathNames: ['lost.md'],
});
// 三桶元素总数 = 输入文件数 ⇒ 无重复归属
expect(r.paths.length + r.invalidTypeNames.length + r.noPathNames.length).toBe(3);
});
it('空输入 → 三桶全空', () => {
expect(classifyDroppedNotesFiles([])).toEqual({
paths: [],
invalidTypeNames: [],
noPathNames: [],
});
});
it('空名字文件按「非 .md」处理(名字为空 ⇒ 不匹配 .md 后缀)', () => {
// 源码里 `noPathNames.push(f.name || '')` 的 `|| ''` 是**防御性**兜底:
// noPath 桶的前提是后缀已是 .md ⇒ 名字必然非空 ⇒ 该分支实际不可达。
// 这里锁「空名字不落 noPath 桶」这一侧,免得哪天有人把兜底挪到
// invalidType 桶导致 toast 渲染出空行。
installBridge({});
const r = classifyDroppedNotesFiles([fakeFile('')]);
expect(r.noPathNames).toEqual([]);
expect(r.invalidTypeNames).toEqual(['']);
});
});