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 }); } test('keymap store:完整 Typora 列表无冲突', async () => { const tmp = await fs.mkdtemp(nodePath.join(os.tmpdir(), 'hn-ty-')); 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 }); const r = await a.page.evaluate(() => { const store = (window as any).__KEYMAP_STORE__; if (!store) return { error: 'no store' }; const bs = store.getState().bindings; const ids = bs.map((b: any) => b.id); const want = ['newDoc','save','closeTab','reopenTab','openFile','quickOpen','newWindow','saveAs','copyMd','pastePlain','selectLine','selectWord','selectSameFormat','deleteWord','docStart','docEnd','bold','italic','underline','strike','inlineCode','link','insertImage','clearFormat','heading1','heading2','heading3','heading4','heading5','heading6','paragraph','headingUp','headingDown','blockquote','bulletList','orderedList','codeBlock','mathBlock','table','indent','outdent','moveUp','moveDown','toggleSidebar','toggleOutline','toggleInfo','focusMode','typewriter','fullscreen','focusSearch','toggleCheat','toggleSettings','find','replace']; return { count: bs.length, conflictCount: store.getState().conflictCount(), hasAll: want.every((id: string) => ids.includes(id)) }; }); console.log('TYPORA_STORE', JSON.stringify(r)); expect(r.error).toBeUndefined(); expect(r.conflictCount).toBe(0); expect(r.count).toBeGreaterThanOrEqual(50); expect(r.hasAll).toBe(true); await a.app.close(); }); // 编辑器排版快捷键:行内代码 Ctrl+Shift+`、下划线 Ctrl+U、无序/有序列表、代码块、表格、引用 test('Typora 编辑器快捷键:代码/下划线/列表/代码块/表格/引用', async () => { const tmp = await fs.mkdtemp(nodePath.join(os.tmpdir(), 'hn-ty2-')); 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, 'TyporaHK'); const ed = a.page.locator('.ProseMirror'); await ed.click(); const html = async () => (await a.page.locator('.ProseMirror').innerHTML()); // 行内代码 Ctrl+Shift+` await a.page.keyboard.type('code sample'); await a.page.keyboard.press('Control+a'); await a.page.keyboard.press('Control+Shift+Backquote'); await a.page.waitForTimeout(120); expect(await html()).toContain(''); // 清除格式后重试下划线 Ctrl+U await a.page.keyboard.press('Control+a'); await a.page.keyboard.press('Control+\\'); await a.page.keyboard.type('underline text'); await a.page.keyboard.press('Control+a'); await a.page.keyboard.press('Control+u'); await a.page.waitForTimeout(120); expect(await html()).toContain(''); // 无序列表 Ctrl+Shift+] await a.page.keyboard.press('Control+a'); await a.page.keyboard.press('Control+Shift+]'); await a.page.waitForTimeout(120); expect(await html()).toContain(' { const tmp = await fs.mkdtemp(nodePath.join(os.tmpdir(), 'hn-ty3-')); 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, 'BlockHK'); const ed = a.page.locator('.ProseMirror'); await ed.click(); await a.page.keyboard.type('AAAA'); await a.page.keyboard.press('Enter'); await a.page.keyboard.type('BBBB'); // 光标在第二段,Ctrl+Shift+↑ 上移(BBBB 块上移到顶部) await a.page.keyboard.press('Control+Shift+ArrowUp'); await a.page.waitForTimeout(120); const text = await a.page.locator('.ProseMirror').innerText(); expect(text.indexOf('BBBB')).toBeLessThan(text.indexOf('AAAA')); // Ctrl+L 选中当前行 await a.page.keyboard.press('Control+l'); await a.page.waitForTimeout(120); const sel = await a.page.evaluate(() => { const s = window.getSelection(); return s ? s.toString() : ''; }); expect(sel.length).toBeGreaterThan(0); await a.app.close(); }); // 视图快捷键:Ctrl+Shift+L 侧边栏 / Ctrl+Shift+1 大纲 / Ctrl+, 设置 test('Typora 视图:Ctrl+Shift+L 侧边栏 / Ctrl+Shift+1 大纲 / Ctrl+, 设置', async () => { const tmp = await fs.mkdtemp(nodePath.join(os.tmpdir(), 'hn-ty4-')); 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, 'ViewHK'); // Ctrl+Shift+L 隐藏侧边栏(搜索框消失) await a.page.getByPlaceholder(/搜索/).waitFor({ timeout: 5000 }); await a.page.keyboard.press('Control+Shift+l'); await a.page.waitForTimeout(200); await expect(a.page.getByPlaceholder(/搜索/)).not.toBeVisible({ timeout: 4000 }); // 再次 Ctrl+Shift+L 恢复 await a.page.keyboard.press('Control+Shift+l'); await a.page.waitForTimeout(200); await expect(a.page.getByPlaceholder(/搜索/)).toBeVisible({ timeout: 4000 }); // Ctrl+Shift+1 大纲面板(右栏 大纲/信息 按钮可见性切换) const infoBtn = a.page.getByRole('button', { name: '信息' }); const before = await infoBtn.isVisible().catch(() => false); await a.page.keyboard.press('Control+Shift+1'); await a.page.waitForTimeout(200); const after = await infoBtn.isVisible().catch(() => false); expect(before).not.toBe(after); // Ctrl+, 设置 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(); });