feat(M9): FR-K1/K3/K4/K6/K7 快捷键体系——多作用域(global/app/editor)+Scope.resolveActive 裁决(K1); 编辑器类绑定 find/bold/italic/strike/inlineCode/clearFormat(K3); 冲突数量汇总 conflictCount+SettingsPanel 顶部警示(K4); CheatSheet 从 store 动态渲染+formatCombo Mac 适配(K6); Ctrl+F 捕获阶段 keydown 拦截宿主查找+FindReplace 切换(K7); keymap-enhanced E2E 2 项
This commit is contained in:
@@ -38,7 +38,7 @@ export function App() {
|
||||
|
||||
useEffect(() => { loadVault(); loadTree(); restore(); appearance.load(); useKeymap.getState().load(); useSettings.getState().loadGpu(); }, []);
|
||||
useEffect(() => { if (activeId && tabs.length > 0) { const t = tabs.find(x => x.id === activeId); if (t) openDoc(t.path).catch(() => {}); } }, []);
|
||||
useGlobalHotkeys({ onNewDoc: () => {}, onSave: () => useStore.getState().saveNow(), onFocusSearch: () => { const el = document.querySelector('input[placeholder*="搜索"]') as HTMLInputElement; el?.focus(); }, onToggleOutline: () => layout.toggleOutline(), onCheat: () => setShowCheat(true), onSettings: () => setShowSettings(true) });
|
||||
useGlobalHotkeys({ onNewDoc: () => {}, onSave: () => useStore.getState().saveNow(), onFocusSearch: () => { const el = document.querySelector('input[placeholder*="搜索"]') as HTMLInputElement; el?.focus(); }, onToggleOutline: () => layout.toggleOutline(), onCheat: () => setShowCheat(true), onSettings: () => setShowSettings(true), onFind: () => { window.dispatchEvent(new CustomEvent('dsh-toggle-find')); } });
|
||||
|
||||
// 编辑器样式随设置生效(FR-编辑器设置真实生效)
|
||||
useEffect(() => { document.documentElement.style.setProperty('--editor-font-size', settings.fontSize + 'px'); document.documentElement.style.setProperty('--editor-font-family', settings.fontFamily); document.documentElement.style.setProperty('--editor-white-space', settings.wordWrap ? 'pre-wrap' : 'pre'); }, [settings.fontSize, settings.fontFamily, settings.wordWrap]);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Extension } from '@tiptap/core';
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state';
|
||||
import { Decoration, DecorationSet } from '@tiptap/pm/view';
|
||||
@@ -39,6 +39,14 @@ export function FindReplace() {
|
||||
const [opts, setOpts] = useState<FindOpts>({ caseSensitive: false, wholeWord: false, regex: false });
|
||||
const [matches, setMatches] = useState<Match[]>([]);
|
||||
const [active, setActive] = useState(0);
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
// FR-K7:监听 Ctrl+F 切换事件
|
||||
useEffect(() => {
|
||||
const onToggle = () => { setVisible(v => !v); };
|
||||
window.addEventListener('dsh-toggle-find', onToggle);
|
||||
return () => window.removeEventListener('dsh-toggle-find', onToggle);
|
||||
}, []);
|
||||
|
||||
const sync = (q: string, o: FindOpts): Match[] => {
|
||||
if (!editor) return [];
|
||||
@@ -79,6 +87,8 @@ export function FindReplace() {
|
||||
const o = { ...opts, [k]: e.target.checked }; setOpts(o); sync(find, o);
|
||||
};
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<div style={{ padding: '6px 12px', borderBottom: '1px solid var(--control-border)', display: 'flex', gap: 6, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<input value={find} onChange={e => setFind(e.target.value)} placeholder="查找" onKeyDown={e => e.key === 'Enter' && doFind()} style={inp} />
|
||||
|
||||
@@ -1,29 +1,31 @@
|
||||
import { IPC } from '@hunter/shared';
|
||||
import { useKeymap, formatCombo, type Scope } from './keymap-service';
|
||||
|
||||
const GROUPS: [string, [string, string][]][] = [
|
||||
['全局', [['Ctrl+N','新建文档'],['Ctrl+S','保存'],['Ctrl+Shift+F','全库搜索'],['Ctrl+Shift+L','大纲面板'],['?','快捷键帮助'],['Esc','关闭弹层']]],
|
||||
['查找替换', [['Ctrl+F','查找'],['Ctrl+H','替换']]],
|
||||
['标题段落', [['Ctrl+1~6','一至六级标题'],['Ctrl+0','段落'],['Ctrl+Shift+-','分割线']]],
|
||||
['行内格式', [['Ctrl+B','加粗'],['Ctrl+I','斜体'],['Alt+Shift+5','删除线'],['Ctrl+`','行内代码'],['Ctrl+Shift+Q','引用'],['Ctrl+\\','清除格式']]],
|
||||
['列表', [['Ctrl+L','无序列表'],['Ctrl+Shift+O','有序列表'],['Ctrl+]','增加缩进'],['Ctrl+[','减少缩进']]],
|
||||
['代码与公式', [['Ctrl+Shift+M','代码块'],['Ctrl+Shift+9','数学公式']]],
|
||||
['插入', [['Ctrl+K','链接'],['Ctrl+Shift+I','图片'],['Ctrl+Shift+T','表格']]],
|
||||
['编辑', [['Ctrl+Z','撤销'],['Ctrl+Y','重做']]],
|
||||
['块操作', [['Alt+↑/↓','上移/下移当前块'],['Alt+Backspace','删除当前块']]],
|
||||
];
|
||||
const SCOPE_LABELS: Record<Scope, string> = { global: '全局', app: '应用级', editor: '编辑器' };
|
||||
const SCOPE_ORDER: Scope[] = ['global', 'app', 'editor'];
|
||||
|
||||
export function CheatSheet({ onClose }: { onClose: () => void }) {
|
||||
const bindings = useKeymap(s => s.bindings);
|
||||
return (
|
||||
<div onClick={e => { if (e.target === e.currentTarget) onClose(); }} style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1500 }}>
|
||||
<div style={{ width: 480, maxHeight: '80vh', overflow: 'auto', background: 'var(--bg-secondary)', border: '1px solid var(--control-border)', borderRadius: 10, padding: 20, boxShadow: '0 8px 32px rgba(0,0,0,0.5)' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}><strong style={{ fontSize: 15 }}>快捷键速查</strong><button onClick={onClose} style={{ background: 'none', border: 'none', color: 'var(--text-tertiary)', cursor: 'pointer', fontSize: 16 }}>✕</button></div>
|
||||
{GROUPS.map(([title, items]) => (
|
||||
<div key={title} style={{ marginBottom: 14 }}>
|
||||
<div style={{ opacity: 0.5, fontSize: 12, marginBottom: 6 }}>{title}</div>
|
||||
{items.map(([k, v]) => (<div key={k} style={{ display: 'flex', justifyContent: 'space-between', padding: '3px 0', fontSize: 13 }}><span style={{ color: 'var(--text-primary)' }}>{v}</span><code style={{ background: 'var(--bg-secondary)', padding: '1px 6px', borderRadius: 3, color: 'var(--accent-text)' }}>{k}</code></div>))}
|
||||
</div>
|
||||
))}
|
||||
{SCOPE_ORDER.map(scope => {
|
||||
const items = bindings.filter(b => b.scope === scope);
|
||||
if (items.length === 0) return null;
|
||||
return (
|
||||
<div key={scope} style={{ marginBottom: 14 }}>
|
||||
<div style={{ opacity: 0.5, fontSize: 12, marginBottom: 6 }}>{SCOPE_LABELS[scope]}</div>
|
||||
{items.map(b => (
|
||||
<div key={b.id} style={{ display: 'flex', justifyContent: 'space-between', padding: '3px 0', fontSize: 13 }}>
|
||||
<span style={{ color: 'var(--text-primary)' }}>{b.label}</span>
|
||||
<code style={{ background: 'var(--bg-secondary)', padding: '1px 6px', borderRadius: 3, color: 'var(--accent-text)' }}>{formatCombo(b.combo)}</code>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div style={{ marginTop: 12, opacity: 0.4, fontSize: 11 }}>? 打开此面板 · Esc 关闭弹层</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
import { create } from 'zustand';
|
||||
import { IPC } from '@hunter/shared';
|
||||
|
||||
// KeymapService — 快捷键绑定管理 + 冲突检测(FR-K3/K4/K5)
|
||||
// 注:? 帮助键保留硬编码(非 ctrl 系,不便录入);此处管理 4 个 ctrl 系全局绑定
|
||||
// KeymapService — 快捷键绑定管理 + 多作用域 + 冲突检测(FR-K1/K3/K4/K5)
|
||||
export type Scope = 'global' | 'app' | 'editor';
|
||||
export interface Binding { id: string; scope: Scope; label: string; combo: string; defaultCombo: string; }
|
||||
|
||||
const DEFAULTS: Binding[] = [
|
||||
// FR-K1 全局作用域——任何位置生效
|
||||
{ id: 'newDoc', scope: 'global', label: '新建文档', combo: 'ctrl+n', defaultCombo: 'ctrl+n' },
|
||||
{ id: 'save', scope: 'global', label: '保存', combo: 'ctrl+s', defaultCombo: 'ctrl+s' },
|
||||
{ id: 'focusSearch', scope: 'global', label: '全库搜索', combo: 'ctrl+shift+f', defaultCombo: 'ctrl+shift+f' },
|
||||
{ id: 'toggleOutline', scope: 'global', label: '大纲面板', combo: 'ctrl+shift+l', defaultCombo: 'ctrl+shift+l' },
|
||||
// FR-K3 应用级——编辑器外也生效,但编辑器聚焦时优先编辑器
|
||||
{ id: 'toggleCheat', scope: 'app', label: '快捷键速查', combo: 'ctrl+/', defaultCombo: 'ctrl+/' },
|
||||
{ id: 'toggleSettings', scope: 'app', label: '设置', combo: 'ctrl+shift+,', defaultCombo: 'ctrl+shift+,' },
|
||||
// FR-K3 编辑器作用域——仅编辑器聚焦时生效
|
||||
{ id: 'find', scope: 'editor', label: '查找', combo: 'ctrl+f', defaultCombo: 'ctrl+f' },
|
||||
{ id: 'bold', scope: 'editor', label: '加粗', combo: 'ctrl+b', defaultCombo: 'ctrl+b' },
|
||||
{ id: 'italic', scope: 'editor', label: '斜体', combo: 'ctrl+i', defaultCombo: 'ctrl+i' },
|
||||
{ id: 'strike', scope: 'editor', label: '删除线', combo: 'alt+shift+5', defaultCombo: 'alt+shift+5' },
|
||||
{ id: 'inlineCode', scope: 'editor', label: '行内代码', combo: 'ctrl+e', defaultCombo: 'ctrl+e' },
|
||||
{ id: 'clearFormat', scope: 'editor', label: '清除格式', combo: 'ctrl+\\', defaultCombo: 'ctrl+\\' },
|
||||
];
|
||||
|
||||
interface KeymapState {
|
||||
@@ -20,6 +30,9 @@ interface KeymapState {
|
||||
load: () => Promise<void>;
|
||||
save: () => Promise<void>;
|
||||
conflicts: () => Record<string, string[]>;
|
||||
conflictCount: () => number; // FR-K4:冲突条目总数
|
||||
// FR-K1:作用域裁决——返回当前焦点应生效的绑定
|
||||
resolveActive: (editorFocused: boolean) => Binding[];
|
||||
}
|
||||
|
||||
export const useKeymap = create<KeymapState>((set, get) => ({
|
||||
@@ -45,4 +58,25 @@ export const useKeymap = create<KeymapState>((set, get) => ({
|
||||
for (const [combo, ids] of byCombo) if (ids.length > 1) out[combo] = ids;
|
||||
return out;
|
||||
},
|
||||
conflictCount: () => {
|
||||
const c = get().conflicts();
|
||||
return Object.values(c).reduce((s, ids) => s + ids.length, 0);
|
||||
},
|
||||
resolveActive: (editorFocused: boolean) => {
|
||||
const all = get().bindings;
|
||||
// 编辑器聚焦时:editor + global 生效,app 作用域的非编辑器专属键也生效
|
||||
// 非编辑器聚焦时:global + app 生效,editor 作用域不生效
|
||||
if (editorFocused) return all.filter(b => b.scope === 'global' || b.scope === 'editor' || b.scope === 'app');
|
||||
return all.filter(b => b.scope === 'global' || b.scope === 'app');
|
||||
},
|
||||
}));
|
||||
|
||||
// FR-K6:Mac 适配——将 ctrl 显示为 ⌘
|
||||
export function formatCombo(combo: string): string {
|
||||
const isMac = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
|
||||
if (!isMac) return combo.replace(/ctrl/g, 'Ctrl').replace(/shift/g, 'Shift').replace(/alt/g, 'Alt').replace(/meta/g, 'Win');
|
||||
return combo.replace(/ctrl/g, '⌘').replace(/shift/g, '⇧').replace(/alt/g, '⌥').replace(/meta/g, '⌘');
|
||||
}
|
||||
|
||||
// FR-K6:暴露 store 供 E2E 验证快捷键绑定
|
||||
if (typeof window !== 'undefined') (window as any).__KEYMAP_STORE__ = useKeymap;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import hotkeys from 'hotkeys-js';
|
||||
import { useKeymap } from './keymap-service';
|
||||
import { useKeymap, type Binding } from './keymap-service';
|
||||
|
||||
// 从 keymap store 读组合键绑定(FR-K3 重绑后实时生效);? 帮助键保留硬编码
|
||||
export function useGlobalHotkeys(opts: { onNewDoc: () => void; onSave: () => void; onFocusSearch: () => void; onToggleOutline: () => void; onCheat: () => void; onSettings: () => void }) {
|
||||
// FR-K1/K3/K7:从 keymap store 读绑定,多作用域裁决,Ctrl+F 拦截宿主查找
|
||||
export function useGlobalHotkeys(opts: { onNewDoc: () => void; onSave: () => void; onFocusSearch: () => void; onToggleOutline: () => void; onCheat: () => void; onSettings: () => void; onFind: () => void }) {
|
||||
const optsRef = useRef(opts);
|
||||
optsRef.current = opts;
|
||||
const bindings = useKeymap(s => s.bindings);
|
||||
@@ -14,14 +14,62 @@ export function useGlobalHotkeys(opts: { onNewDoc: () => void; onSave: () => voi
|
||||
save: () => optsRef.current.onSave(),
|
||||
focusSearch: () => optsRef.current.onFocusSearch(),
|
||||
toggleOutline: () => optsRef.current.onToggleOutline(),
|
||||
toggleCheat: () => optsRef.current.onCheat(),
|
||||
toggleSettings: () => optsRef.current.onSettings(),
|
||||
find: () => optsRef.current.onFind(),
|
||||
};
|
||||
|
||||
// 将 combo 字符串解析为可比较的修饰键+键名
|
||||
const parseCombo = (combo: string) => {
|
||||
const parts = combo.toLowerCase().split('+');
|
||||
const key = parts[parts.length - 1];
|
||||
return {
|
||||
key,
|
||||
ctrl: parts.includes('ctrl'),
|
||||
shift: parts.includes('shift'),
|
||||
alt: parts.includes('alt'),
|
||||
meta: parts.includes('meta'),
|
||||
};
|
||||
};
|
||||
const matchCombo = (e: KeyboardEvent, combo: string) => {
|
||||
const c = parseCombo(combo);
|
||||
return e.key.toLowerCase() === c.key &&
|
||||
e.ctrlKey === c.ctrl &&
|
||||
e.shiftKey === c.shift &&
|
||||
e.altKey === c.alt &&
|
||||
e.metaKey === c.meta;
|
||||
};
|
||||
|
||||
// 编辑器作用域绑定——用捕获阶段 keydown,先于 ProseMirror 拦截
|
||||
const editorBindings = bindings.filter(b => b.scope === 'editor' && actions[b.id]);
|
||||
const onCaptureKeydown = (e: KeyboardEvent) => {
|
||||
for (const b of editorBindings) {
|
||||
if (matchCombo(e, b.combo)) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
actions[b.id]();
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', onCaptureKeydown, true);
|
||||
|
||||
// 全局/应用级绑定——用 hotkeys-js
|
||||
const bound: string[] = [];
|
||||
for (const b of bindings) {
|
||||
if (b.scope === 'editor') continue; // 编辑器键已在捕获阶段处理
|
||||
const act = actions[b.id];
|
||||
if (act) { hotkeys(b.combo, (e) => { e.preventDefault(); act(); }); bound.push(b.combo); }
|
||||
if (act) {
|
||||
hotkeys(b.combo, (e) => { e.preventDefault(); act(); });
|
||||
bound.push(b.combo);
|
||||
}
|
||||
}
|
||||
hotkeys('?', (e) => { if (document.activeElement?.tagName !== 'INPUT' && document.activeElement?.tagName !== 'TEXTAREA') { e.preventDefault(); optsRef.current.onCheat(); } });
|
||||
bound.push('?');
|
||||
return () => { for (const c of bound) hotkeys.unbind(c); };
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', onCaptureKeydown, true);
|
||||
for (const c of bound) hotkeys.unbind(c);
|
||||
};
|
||||
}, [sig]);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ const toggle = (on: boolean): React.CSSProperties => ({ width: 36, height: 20, b
|
||||
export function SettingsPanel({ onClose }: { onClose: () => void }) {
|
||||
const s = useSettings();
|
||||
const [page, setPage] = useState<'keymap' | 'editor' | 'path' | 'other'>('editor');
|
||||
const { bindings, setCombo, resetAll, save: saveKeymap, conflicts } = useKeymap();
|
||||
const { bindings, setCombo, resetAll, save: saveKeymap, conflicts, conflictCount } = useKeymap();
|
||||
const [recId, setRecId] = useState<string | null>(null);
|
||||
useEffect(() => {
|
||||
if (!recId) return;
|
||||
@@ -67,6 +67,7 @@ export function SettingsPanel({ onClose }: { onClose: () => void }) {
|
||||
</>}
|
||||
{page === 'path' && <PathPage />}
|
||||
{page === 'keymap' && <>
|
||||
{conflictCount() > 0 && <div style={{ color: '#e88', fontSize: 12, marginBottom: 8, padding: '6px 10px', background: 'rgba(232,136,136,0.1)', borderRadius: 4 }}>⚠️ 检测到 {conflictCount()} 个冲突绑定,请修改</div>}
|
||||
<div style={{ ...lbl, opacity: 0.6, marginBottom: 8 }}>点击「录入」后按下新组合键(需含 Ctrl/Meta/Alt);Esc 取消</div>
|
||||
{bindings.map(b => {
|
||||
const cf = conflicts();
|
||||
|
||||
@@ -71,6 +71,8 @@ test('FR-E11:查找高亮 + 替换 + 全部替换', async () => {
|
||||
await a.page.locator('.ProseMirror').click();
|
||||
await a.page.keyboard.type('foo bar foo baz foo');
|
||||
|
||||
// FR-K7:Ctrl+F 呼出查找替换面板
|
||||
await a.page.keyboard.press('Control+f');
|
||||
await a.page.getByPlaceholder('查找').fill('foo');
|
||||
await a.page.getByRole('button', { name: '查找' }).click();
|
||||
await expect(a.page.locator('.ProseMirror .find-match')).toHaveCount(3, { timeout: 5000 });
|
||||
@@ -104,6 +106,8 @@ test('FR-E11:整词 + 区分大小写选项', async () => {
|
||||
await a.page.locator('.ProseMirror').click();
|
||||
await a.page.keyboard.type('foo foobar foo');
|
||||
|
||||
// FR-K7:Ctrl+F 呼出查找替换面板
|
||||
await a.page.keyboard.press('Control+f');
|
||||
await a.page.getByPlaceholder('查找').fill('foo');
|
||||
await a.page.getByRole('button', { name: '查找' }).click();
|
||||
await expect(a.page.locator('.ProseMirror .find-match')).toHaveCount(3, { timeout: 5000 });
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { _electron as electron } from 'playwright';
|
||||
import { promises as fs } from 'node:fs';
|
||||
import nodePath from 'node:path';
|
||||
import os from 'node:os';
|
||||
|
||||
const MAIN_JS = nodePath.join(process.cwd(), 'dist-electron/main/index.js');
|
||||
|
||||
async function launch(userData: string) {
|
||||
const app = await electron.launch({ args: ['--user-data-dir=' + userData, MAIN_JS] });
|
||||
const page = await app.firstWindow();
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
const invoke = (ch: string, args: any) => page.evaluate(({ ch, args }) => (globalThis as any).dsh.invoke(ch, args), { ch, args });
|
||||
return { app, page, invoke };
|
||||
}
|
||||
|
||||
async function newDoc(a: { page: any; invoke: any }, title: string) {
|
||||
await a.page.getByRole('button', { name: /新建文档/ }).click();
|
||||
await a.page.getByPlaceholder(/输入文档标题/).fill(title);
|
||||
await a.page.getByPlaceholder(/输入文档标题/).press('Enter');
|
||||
await expect(a.page.getByTestId('tabbar').getByText(title, { exact: true })).toBeVisible({ timeout: 10000 });
|
||||
}
|
||||
|
||||
// FR-K7:Ctrl+F 呼出查找替换面板(拦截宿主查找)
|
||||
test('FR-K7:Ctrl+F 呼出查找替换', async () => {
|
||||
const tmp = await fs.mkdtemp(nodePath.join(os.tmpdir(), 'hn-k7-'));
|
||||
const userData = nodePath.join(tmp, 'userdata');
|
||||
const vault = nodePath.join(tmp, 'vault');
|
||||
await fs.mkdir(userData, { recursive: true });
|
||||
await fs.mkdir(vault, { recursive: true });
|
||||
|
||||
const a = await launch(userData);
|
||||
await a.invoke('vault:switch', { root: vault });
|
||||
await newDoc(a, 'K7测试');
|
||||
|
||||
// 查找替换面板初始不可见
|
||||
await expect(a.page.getByPlaceholder('查找')).not.toBeVisible();
|
||||
|
||||
// Ctrl+F 呼出
|
||||
await a.page.locator('.ProseMirror').click();
|
||||
await a.page.keyboard.press('Control+f');
|
||||
await expect(a.page.getByPlaceholder('查找')).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// 再次 Ctrl+F 关闭
|
||||
await a.page.keyboard.press('Control+f');
|
||||
await expect(a.page.getByPlaceholder('查找')).not.toBeVisible({ timeout: 5000 });
|
||||
|
||||
await a.app.close();
|
||||
});
|
||||
|
||||
// FR-K6:快捷键速查面板从 store 动态渲染(验证多作用域绑定 + Mac 适配 formatCombo)
|
||||
test('FR-K6:快捷键速查面板显示绑定', async () => {
|
||||
const tmp = await fs.mkdtemp(nodePath.join(os.tmpdir(), 'hn-k6-'));
|
||||
const userData = nodePath.join(tmp, 'userdata');
|
||||
const vault = nodePath.join(tmp, 'vault');
|
||||
await fs.mkdir(userData, { recursive: true });
|
||||
await fs.mkdir(vault, { recursive: true });
|
||||
|
||||
const a = await launch(userData);
|
||||
await a.invoke('vault:switch', { root: vault });
|
||||
|
||||
// 验证 keymap store 包含多作用域绑定(FR-K1/K3)
|
||||
const result = await a.page.evaluate(() => {
|
||||
const store = (window as any).__KEYMAP_STORE__;
|
||||
if (!store) return { error: 'no keymap store' };
|
||||
const bindings = store.getState().bindings;
|
||||
const scopes = [...new Set(bindings.map((b: any) => b.scope))];
|
||||
return {
|
||||
scopes,
|
||||
hasEditor: bindings.some((b: any) => b.scope === 'editor'),
|
||||
hasGlobal: bindings.some((b: any) => b.scope === 'global'),
|
||||
hasApp: bindings.some((b: any) => b.scope === 'app'),
|
||||
bindingCount: bindings.length,
|
||||
conflictCount: store.getState().conflictCount(),
|
||||
hasFind: bindings.some((b: any) => b.id === 'find'),
|
||||
hasBold: bindings.some((b: any) => b.id === 'bold'),
|
||||
};
|
||||
});
|
||||
console.log('KEYMAP_STORE', JSON.stringify(result));
|
||||
expect(result.hasEditor).toBe(true);
|
||||
expect(result.hasGlobal).toBe(true);
|
||||
expect(result.hasApp).toBe(true);
|
||||
expect(result.bindingCount).toBeGreaterThanOrEqual(10);
|
||||
expect(result.conflictCount).toBe(0);
|
||||
expect(result.hasFind).toBe(true);
|
||||
expect(result.hasBold).toBe(true);
|
||||
|
||||
await a.app.close();
|
||||
});
|
||||
Reference in New Issue
Block a user