126 lines
5.0 KiB
TypeScript
126 lines
5.0 KiB
TypeScript
// tests/unit/renderer/lib/click-select-route.test.ts
|
|
//
|
|
// 守 r228「修饰键点击路由单一真相源」契约:
|
|
//
|
|
// 旧状态(实测分叉):NotesList / LiteratureList 的 handleSelect 各写一份
|
|
// mode 分支。文献域的 toggle / range 分支会 setActiveId(让 listbox 本地
|
|
// 键盘光标 / aria-activedescendant 跟手),**笔记域漏了** ⇒ 笔记侧
|
|
// Ctrl+点击后光标仍停在旧行,紧接着按 F2 会重命名**旧行**而不是刚点的那
|
|
// 行(r160 B7 那次「click 后把焦点搬到 listbox」的修复只进了文献侧)。
|
|
//
|
|
// 修法不是给笔记补一行 setActiveId(下次还会分叉),而是把路由本体上收到
|
|
// lib/multi-select.applyClickSelectRoute,两域共用一份 ⇒ 分叉不可能再长
|
|
// 出来(同 isReallyGone 上收 utils/file-check.ts 的做法)。
|
|
//
|
|
// 锁两类东西:
|
|
// 1. 行为契约(applyClickSelectRoute):
|
|
// toggle ⇒ toggleSelect + setActiveId(光标跟手),不动主选中相关动作;
|
|
// range ⇒ selectRange + setActiveId,同上;
|
|
// single ⇒ selectOnly + select(设主选中),**不**调 setActiveId
|
|
// (两域的 activeId 靠「selectedId 变化 → effect 同步」跟随,
|
|
// 在路由里再写一次属重复);
|
|
// 2. 端到端一条:修饰键 → 模式 → 路由动作串起来(防「模式解析与路由
|
|
// 各改各的」)。
|
|
import { describe, expect, it } from 'vitest';
|
|
import { readFileSync } from 'fs';
|
|
import { join } from 'path';
|
|
import {
|
|
applyClickSelectRoute,
|
|
resolveClickSelectMode,
|
|
type ClickSelectMode,
|
|
} from '../../../../src/renderer/src/lib/multi-select';
|
|
|
|
function makeActions() {
|
|
const calls: string[] = [];
|
|
// audit-r229:参数类型放宽到 `string | null` —— 路由契约里 setActiveId 的
|
|
// 签名是 `(id: TId | null) => void`,而 applyClickSelectRoute 的 TId
|
|
// 由字面量 id 推断成 'n1' / 'n2' 这类**窄字面量类型** ⇒ mock 若只写
|
|
// `(id: string) => void`,把 mock 传给路由时 TS2345 报错
|
|
// (peer 在途文件的 typecheck 红,本轮顺手收口)。函数参数逆变 ⇒
|
|
// 宽参类型可赋给窄参类型,行为一字未改。
|
|
const rec = (name: string) => (id: string | null) => {
|
|
calls.push(`${name}:${id}`);
|
|
};
|
|
return {
|
|
calls,
|
|
actions: {
|
|
toggleSelect: rec('toggle'),
|
|
selectRange: rec('range'),
|
|
selectOnly: rec('only'),
|
|
select: rec('select'),
|
|
setActiveId: rec('active'),
|
|
},
|
|
};
|
|
}
|
|
|
|
describe('applyClickSelectRoute(r228 路由单一真相源)', () => {
|
|
it('toggle:切多选成员 + 键盘光标跟手;不动主选中', () => {
|
|
const { calls, actions } = makeActions();
|
|
applyClickSelectRoute('toggle', 'n1', actions);
|
|
expect(calls).toEqual(['toggle:n1', 'active:n1']);
|
|
});
|
|
|
|
it('range:范围选择 + 键盘光标跟手;不动主选中', () => {
|
|
const { calls, actions } = makeActions();
|
|
applyClickSelectRoute('range', 'n2', actions);
|
|
expect(calls).toEqual(['range:n2', 'active:n2']);
|
|
});
|
|
|
|
it('single:作用集收成该行 + 设主选中;不调 setActiveId', () => {
|
|
const { calls, actions } = makeActions();
|
|
applyClickSelectRoute('single', 'n3', actions);
|
|
expect(calls).toEqual(['only:n3', 'select:n3']);
|
|
});
|
|
|
|
it('端到端:修饰键 → 模式 → 动作(Ctrl=toggle / Shift=range / 其它=single)', () => {
|
|
const run = (e: { ctrlKey: boolean; metaKey: boolean; shiftKey: boolean }) => {
|
|
const { calls, actions } = makeActions();
|
|
applyClickSelectRoute(resolveClickSelectMode(e), 'x', actions);
|
|
return calls;
|
|
};
|
|
expect(run({ ctrlKey: true, metaKey: false, shiftKey: false })).toEqual([
|
|
'toggle:x',
|
|
'active:x',
|
|
]);
|
|
expect(run({ ctrlKey: false, metaKey: true, shiftKey: false })).toEqual([
|
|
'toggle:x',
|
|
'active:x',
|
|
]);
|
|
expect(run({ ctrlKey: false, metaKey: false, shiftKey: true })).toEqual([
|
|
'range:x',
|
|
'active:x',
|
|
]);
|
|
expect(run({ ctrlKey: false, metaKey: false, shiftKey: false })).toEqual([
|
|
'only:x',
|
|
'select:x',
|
|
]);
|
|
});
|
|
|
|
it('模式解析的取值域就是路由的三分支(新增模式必须同时改路由)', () => {
|
|
const modes: ClickSelectMode[] = ['single', 'toggle', 'range'];
|
|
for (const m of modes) {
|
|
const { calls, actions } = makeActions();
|
|
applyClickSelectRoute(m, 'id', actions);
|
|
expect(calls.length).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('r228 结构契约:两域必须共用同一份路由(孪生分支不得复活)', () => {
|
|
const ROOT = process.cwd();
|
|
const FILES = [
|
|
'src/renderer/src/components/notes/NotesList.tsx',
|
|
'src/renderer/src/components/literature/LiteratureList.tsx',
|
|
];
|
|
for (const rel of FILES) {
|
|
it(`${rel} 走 lib/multi-select.applyClickSelectRoute`, () => {
|
|
const src = readFileSync(join(ROOT, rel), 'utf-8');
|
|
expect(src, '点击路由必须走 lib/multi-select 的单一真相源').toContain('applyClickSelectRoute');
|
|
expect(
|
|
src,
|
|
'孪生块复活:组件里又内联了 mode 分支(两域会再次分叉)',
|
|
).not.toContain("if (mode === 'toggle')");
|
|
});
|
|
}
|
|
});
|