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 }); } // 1. Ctrl+N 新建文档(全局) test('快捷键 Ctrl+N 新建文档', async () => { const tmp = await fs.mkdtemp(nodePath.join(os.tmpdir(), 'hn-kn-')); const ud = nodePath.join(tmp, 'ud'); const v = nodePath.join(tmp, 'v'); await fs.mkdir(ud, { recursive: true }); await fs.mkdir(v, { recursive: true }); const a = await launch(ud); await a.invoke('vault:switch', { root: v }); await a.page.getByPlaceholder(/搜索/).click(); // 聚焦到非编辑器区域 await a.page.keyboard.press('Control+n'); await expect(a.page.getByPlaceholder(/文档标题/)).toBeVisible({ timeout: 5000 }); await a.page.keyboard.press('Escape'); await a.app.close(); }); // 2. Ctrl+S 保存(全局) test('快捷键 Ctrl+S 保存', async () => { const tmp = await fs.mkdtemp(nodePath.join(os.tmpdir(), 'hn-ks-')); const ud = nodePath.join(tmp, 'ud'); const v = nodePath.join(tmp, 'v'); await fs.mkdir(ud, { recursive: true }); await fs.mkdir(v, { recursive: true }); const a = await launch(ud); await a.invoke('vault:switch', { root: v }); await newDoc(a, 'SaveTest'); await a.page.locator('.ProseMirror').click(); await a.page.keyboard.type('hello'); await a.page.keyboard.press('Control+s'); await a.page.waitForTimeout(1000); // 检查 store 中 saveState 应为 saved const saveState = await a.page.evaluate(() => (window as any).__STORE__?.getState?.().saveState); expect(saveState).toBe('saved'); const content = await fs.readFile(nodePath.join(v, 'SaveTest.md'), 'utf8'); expect(content).toContain('hello'); await a.app.close(); }); // 3. Ctrl+Shift+F 全库搜索(全局) test('快捷键 Ctrl+Shift+F 聚焦搜索框', async () => { const tmp = await fs.mkdtemp(nodePath.join(os.tmpdir(), 'hn-ksf-')); const ud = nodePath.join(tmp, 'ud'); const v = nodePath.join(tmp, 'v'); await fs.mkdir(ud, { recursive: true }); await fs.mkdir(v, { recursive: true }); const a = await launch(ud); await a.invoke('vault:switch', { root: v }); await newDoc(a, 'SearchHK'); await a.page.locator('.ProseMirror').click(); // 聚焦编辑器 await a.page.keyboard.press('Control+Shift+f'); await a.page.waitForTimeout(300); const focused = await a.page.evaluate(() => { const el = document.activeElement; return el?.getAttribute('placeholder') ?? ''; }); expect(focused).toContain('搜索'); await a.app.close(); }); // 4. Ctrl+Shift+L 大纲面板切换(全局) test('快捷键 Ctrl+Shift+L 大纲面板切换', async () => { const tmp = await fs.mkdtemp(nodePath.join(os.tmpdir(), 'hn-ksl-')); const ud = nodePath.join(tmp, 'ud'); const v = nodePath.join(tmp, 'v'); await fs.mkdir(ud, { recursive: true }); await fs.mkdir(v, { recursive: true }); const a = await launch(ud); await a.invoke('vault:switch', { root: v }); await newDoc(a, 'OutlineTest'); await a.page.locator('.ProseMirror').click(); await a.page.keyboard.type('# Heading 1'); await a.page.waitForTimeout(300); // 切换大纲面板——检查 layout store 的 outlineVisible 状态 const before = await a.page.evaluate(() => { // layout store is not exposed, check if outline tab buttons exist return document.querySelectorAll('button').length > 0; // just a placeholder }); // Use evaluate to check the outline panel visibility via DOM const beforePanel = await a.page.evaluate(() => { const btns = Array.from(document.querySelectorAll('button')); return btns.some(b => b.textContent === '大纲'); }); await a.page.keyboard.press('Control+Shift+1'); await a.page.waitForTimeout(500); const afterPanel = await a.page.evaluate(() => { const btns = Array.from(document.querySelectorAll('button')); return btns.some(b => b.textContent === '大纲'); }); expect(beforePanel).not.toBe(afterPanel); await a.app.close(); }); // 5. Ctrl+/ 快捷键速查(应用级) test('快捷键 Ctrl+/ 速查面板', async () => { const tmp = await fs.mkdtemp(nodePath.join(os.tmpdir(), 'hn-kslash-')); const ud = nodePath.join(tmp, 'ud'); const v = nodePath.join(tmp, 'v'); await fs.mkdir(ud, { recursive: true }); await fs.mkdir(v, { recursive: true }); const a = await launch(ud); await a.invoke('vault:switch', { root: v }); await a.page.keyboard.press('Control+/'); await expect(a.page.getByText('快捷键速查').first()).toBeVisible({ timeout: 5000 }); await a.page.keyboard.press('Escape'); await a.app.close(); }); // 6. Ctrl+Shift+, 设置(应用级) test('快捷键 Ctrl+Shift+, 设置面板', async () => { const tmp = await fs.mkdtemp(nodePath.join(os.tmpdir(), 'hn-ksc-')); const ud = nodePath.join(tmp, 'ud'); const v = nodePath.join(tmp, 'v'); await fs.mkdir(ud, { recursive: true }); await fs.mkdir(v, { recursive: true }); const a = await launch(ud); await a.invoke('vault:switch', { root: v }); await a.page.keyboard.press('Control+Comma'); await expect(a.page.getByText('设置', { exact: true }).first()).toBeVisible({ timeout: 5000 }); await a.page.keyboard.press('Escape'); await a.app.close(); }); // 7. 编辑器快捷键:Ctrl+B 加粗 / Ctrl+I 斜体 / Ctrl+E 行内代码 / Alt+Shift+5 删除线 / Ctrl+\ 清除格式 test('编辑器快捷键:加粗+斜体+行内代码+删除线+清除格式', async () => { const tmp = await fs.mkdtemp(nodePath.join(os.tmpdir(), 'hn-ke-')); const ud = nodePath.join(tmp, 'ud'); const v = nodePath.join(tmp, 'v'); await fs.mkdir(ud, { recursive: true }); await fs.mkdir(v, { recursive: true }); const a = await launch(ud); await a.invoke('vault:switch', { root: v }); await newDoc(a, 'FmtHK'); await a.page.locator('.ProseMirror').click(); await a.page.keyboard.type('format test'); // 全选 await a.page.keyboard.press('Control+a'); // 加粗 await a.page.keyboard.press('Control+b'); await a.page.waitForTimeout(100); let html = await a.page.locator('.ProseMirror').innerHTML(); expect(html).toContain('strong'); // 斜体 await a.page.keyboard.press('Control+i'); await a.page.waitForTimeout(100); html = await a.page.locator('.ProseMirror').innerHTML(); expect(html).toContain('em'); // 行内代码 await a.page.keyboard.press('Control+a'); await a.page.keyboard.press('Control+Shift+Backquote'); await a.page.waitForTimeout(100); html = await a.page.locator('.ProseMirror').innerHTML(); expect(html).toContain('code'); // 清除格式 (Ctrl+\) await a.page.keyboard.press('Control+a'); await a.page.keyboard.press('Control+\\'); await a.page.waitForTimeout(300); html = await a.page.locator('.ProseMirror').innerHTML(); expect(html).not.toContain(''); await a.app.close(); }); // 8. 编辑器快捷键:Ctrl+F 查找(已有测试覆盖,这里快速验证) test('编辑器快捷键 Ctrl+F 查找面板', async () => { const tmp = await fs.mkdtemp(nodePath.join(os.tmpdir(), 'hn-kf-')); const ud = nodePath.join(tmp, 'ud'); const v = nodePath.join(tmp, 'v'); await fs.mkdir(ud, { recursive: true }); await fs.mkdir(v, { recursive: true }); const a = await launch(ud); await a.invoke('vault:switch', { root: v }); await newDoc(a, 'FindHK'); await a.page.locator('.ProseMirror').click(); await a.page.keyboard.press('Control+f'); await expect(a.page.getByPlaceholder('查找')).toBeVisible({ timeout: 5000 }); await a.page.keyboard.press('Control+f'); await expect(a.page.getByPlaceholder('查找')).not.toBeVisible({ timeout: 5000 }); await a.app.close(); });