diff --git a/apps/desktop/tests/e2e/button-all.spec.ts b/apps/desktop/tests/e2e/button-all.spec.ts new file mode 100644 index 0000000..706567b --- /dev/null +++ b/apps/desktop/tests/e2e/button-all.spec.ts @@ -0,0 +1,323 @@ +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 setupVault(a: { page: any; invoke: any }, docs: { name: string; body?: string; tags?: string[]; links?: string[] }[] = []) { + const tmp = await fs.mkdtemp(nodePath.join(os.tmpdir(), 'hn-btn-')); + const ud = nodePath.join(tmp, 'ud'); const v = nodePath.join(tmp, 'v'); + await fs.mkdir(ud, { recursive: true }); await fs.mkdir(v, { recursive: true }); + await a.invoke('vault:switch', { root: v }); + for (const d of docs) { + await a.invoke('doc:write', { path: d.name + '.md', meta: { title: d.name, tags: d.tags, links: d.links }, body: d.body ?? ('# ' + d.name) }); + } + await a.page.getByTitle('刷新').click(); + await a.page.waitForTimeout(300); + return { tmp, v }; +} + +// === TitleBar 按钮验证 === +test('TitleBar: 所有按钮可点击', async () => { + const a = await launch(nodePath.join(os.tmpdir(), 'hn-tb-' + Date.now())); + const { v } = await setupVault(a); + // 侧边栏开关 + await a.page.getByTitle('侧边栏开关').click(); await a.page.waitForTimeout(200); + await a.page.getByTitle('侧边栏开关').click(); await a.page.waitForTimeout(200); + // 外观定制 + await a.page.getByTitle('外观定制').click(); + await expect(a.page.getByText('外观定制', { exact: true }).first()).toBeVisible({ timeout: 3000 }); + await a.page.keyboard.press('Escape'); await a.page.waitForTimeout(300); + // 刷新 + await a.page.getByTitle('刷新').click(); await a.page.waitForTimeout(200); + // 切换编辑器/图谱 + await a.page.getByTitle('切换 编辑器/图谱').click(); await a.page.waitForTimeout(300); + await expect(a.page.getByRole('button', { name: '知识图谱' })).toBeVisible({ timeout: 3000 }); + await a.page.getByTitle('切换 编辑器/图谱').click(); await a.page.waitForTimeout(300); + // 标签栏开关 + await a.page.getByTestId('tabbar-toggle').click(); await a.page.waitForTimeout(200); + await a.page.getByTestId('tabbar-toggle').click(); await a.page.waitForTimeout(200); + // 极简模式 + await a.page.getByTitle('极简模式').click(); await a.page.waitForTimeout(200); + await a.page.getByTitle('极简模式').click(); await a.page.waitForTimeout(200); + // 设置 + await a.page.getByTitle('设置').click(); + await expect(a.page.getByText('设置', { exact: true }).first()).toBeVisible({ timeout: 3000 }); + await a.page.keyboard.press('Escape'); await a.page.waitForTimeout(300); + await a.app.close(); +}); + +// === Toolbar 按钮验证 === +test('Toolbar: 新建文档+新建文件夹可用', async () => { + const a = await launch(nodePath.join(os.tmpdir(), 'hn-tb2-' + Date.now())); + const { v } = await setupVault(a); + // 新建文档 + await a.page.getByRole('button', { name: /新建文档/ }).click(); + await a.page.getByPlaceholder(/输入文档标题/).fill('BtnTest1'); + await a.page.getByPlaceholder(/输入文档标题/).press('Enter'); + await expect(a.page.getByTestId('tabbar').getByText('BtnTest1', { exact: true })).toBeVisible({ timeout: 5000 }); + // 新建文件夹 + await a.page.getByRole('button', { name: '新建文件夹' }).click(); + await a.page.getByPlaceholder(/文件夹名称/).fill('TestFolder'); + await a.page.getByPlaceholder(/文件夹名称/).press('Enter'); + await a.page.waitForTimeout(300); + await expect(a.page.getByTestId('filetree').getByText('TestFolder', { exact: true })).toBeVisible({ timeout: 3000 }); + await a.app.close(); +}); + +// === EditorToolbar 按钮验证 === +test('EditorToolbar: 格式按钮可点击生效', async () => { + const a = await launch(nodePath.join(os.tmpdir(), 'hn-et-' + Date.now())); + const { v } = await setupVault(a, [{ name: 'FmtTest', body: 'Hello World' }]); + await a.page.getByTestId('filetree').getByText('FmtTest').click(); + await a.page.waitForTimeout(500); + await a.page.locator('.ProseMirror').click(); + await a.page.keyboard.type('test text'); + // 星标 + await a.page.getByTitle('星标').click(); await a.page.waitForTimeout(200); + // 加粗 + await a.page.getByTitle('加粗 Ctrl+B').click(); await a.page.waitForTimeout(100); + // 斜体 + await a.page.getByTitle('斜体 Ctrl+I').click(); await a.page.waitForTimeout(100); + // 删除线 + await a.page.getByTitle('删除线 Alt+Shift+5').click(); await a.page.waitForTimeout(100); + // 行内代码 + await a.page.getByTitle('行内代码 Ctrl+E').click(); await a.page.waitForTimeout(100); + // 标题 + await a.page.getByTitle('一级标题 Ctrl+1').click(); await a.page.waitForTimeout(100); + await a.page.getByTitle('二级标题 Ctrl+2').click(); await a.page.waitForTimeout(100); + await a.page.getByTitle('三级标题 Ctrl+3').click(); await a.page.waitForTimeout(100); + // 列表 + await a.page.getByTitle('无序列表 Ctrl+L').click(); await a.page.waitForTimeout(100); + await a.page.getByTitle('有序列表 Ctrl+Shift+O').click(); await a.page.waitForTimeout(100); + // 代码块 + await a.page.getByTitle('代码块 Ctrl+Shift+M').click(); await a.page.waitForTimeout(100); + // 撤销/重做 + await a.page.getByTitle('撤销 Ctrl+Z').click(); await a.page.waitForTimeout(100); + await a.page.getByTitle('重做 Ctrl+Y').click(); await a.page.waitForTimeout(100); + // 验证编辑器有内容(按钮没崩溃) + const html = await a.page.locator('.ProseMirror').innerHTML(); + expect(html.length).toBeGreaterThan(0); + await a.app.close(); +}); + +// === EditorToolbar: 链接/图片按钮 === +test('EditorToolbar: 链接气泡按钮可用', async () => { + const a = await launch(nodePath.join(os.tmpdir(), 'hn-lb-' + Date.now())); + const { v } = await setupVault(a, [{ name: 'LinkTest', body: 'some text here' }]); + await a.page.getByTestId('filetree').getByText('LinkTest').click(); + await a.page.waitForTimeout(500); + await a.page.locator('.ProseMirror').click(); + await a.page.keyboard.type('linktext'); + // 选中文字 + await a.page.keyboard.press('Shift+Home'); + // 点击链接按钮 + await a.page.getByTitle('链接').click(); + await a.page.waitForTimeout(300); + // 应弹出输入对话框 + await expect(a.page.getByText('插入链接')).toBeVisible({ timeout: 3000 }); + await a.page.getByPlaceholder('https://example.com').fill('https://test.com'); + await a.page.getByPlaceholder('https://example.com').press('Enter'); + await a.page.waitForTimeout(500); + // 验证链接已插入 + const html = await a.page.locator('.ProseMirror').innerHTML(); + expect(html).toContain('test.com'); + await a.app.close(); +}); + +// === FindReplace 按钮验证 === +test('FindReplace: 查找/替换按钮可用', async () => { + const a = await launch(nodePath.join(os.tmpdir(), 'hn-fr-' + Date.now())); + const { v } = await setupVault(a, [{ name: 'FindTest', body: 'find me find me find me' }]); + await a.page.getByTestId('filetree').getByText('FindTest').click(); + await a.page.waitForTimeout(500); + // Ctrl+F 呼出查找 + await a.page.keyboard.press('Control+f'); + await a.page.waitForTimeout(300); + await a.page.getByPlaceholder('查找').fill('find'); + await a.page.waitForTimeout(300); + // 查找按钮 + await a.page.getByRole('button', { name: '查找' }).click(); + await a.page.waitForTimeout(200); + // 上一个/下一个 + await a.page.getByTitle('下一个').click(); await a.page.waitForTimeout(100); + await a.page.getByTitle('上一个').click(); await a.page.waitForTimeout(100); + // 替换 + await a.page.getByPlaceholder('替换').fill('replaced'); + await a.page.getByRole('button', { name: '替换', exact: true }).click(); + await a.page.waitForTimeout(200); + // 全部替换 + await a.page.getByRole('button', { name: '全部替换' }).click(); + await a.page.waitForTimeout(200); + await a.app.close(); +}); + +// === MoreActions 按钮验证 === +test('MoreActions: 菜单+复制可用', async () => { + const a = await launch(nodePath.join(os.tmpdir(), 'hn-ma-' + Date.now())); + const { v } = await setupVault(a, [{ name: 'MoreTest', body: 'copy this' }]); + await a.page.getByTestId('filetree').getByText('MoreTest').click(); + await a.page.waitForTimeout(500); + // 点击更多操作 + await a.page.getByTitle('更多操作').click(); + await a.page.waitForTimeout(300); + await expect(a.page.getByText('复制 Markdown')).toBeVisible({ timeout: 3000 }); + // 复制 Markdown + await a.page.getByText('复制 Markdown').click(); + await a.page.waitForTimeout(500); + // 应显示成功提示 + await expect(a.page.getByText('已复制 Markdown')).toBeVisible({ timeout: 3000 }); + await a.app.close(); +}); + +// === DocInfoPanel 按钮验证 === +test('DocInfoPanel: 星标+标签+关联可用', async () => { + const a = await launch(nodePath.join(os.tmpdir(), 'hn-di-' + Date.now())); + const { v } = await setupVault(a, [{ name: 'DocA', body: 'doc a' }, { name: 'DocB', body: 'doc b' }]); + await a.page.getByTestId('filetree').getByText('DocA').click(); + await a.page.waitForTimeout(500); + // 切换到信息面板 + await a.page.getByRole('button', { name: '信息', exact: true }).click(); + await a.page.waitForTimeout(300); + // 星标(信息面板内——第2个星标按钮,用 evaluate 精确点击) + await a.page.evaluate(() => { + const stars = document.querySelectorAll('button[title="星标"]'); + if (stars.length >= 2) (stars[1] as HTMLElement).click(); + }); + await a.page.waitForTimeout(200); + // 添加标签 + await a.page.getByPlaceholder('添加标签').fill('testtag'); + await a.page.getByPlaceholder('添加标签').press('Enter'); + await a.page.waitForTimeout(300); + await expect(a.page.getByText('testtag')).toBeVisible({ timeout: 3000 }); + // 关联文档 + const select = a.page.locator('select').first(); + await select.selectOption('DocB.md'); + await a.page.getByRole('button', { name: '关联' }).click(); + await a.page.waitForTimeout(300); + await expect(a.page.getByText('DocB.md')).toBeVisible({ timeout: 3000 }); + await a.app.close(); +}); + +// === SettingsPanel 按钮验证 === +test('SettingsPanel: 页签+开关+按钮可用', async () => { + const a = await launch(nodePath.join(os.tmpdir(), 'hn-sp-' + Date.now())); + await setupVault(a); + await a.page.getByTitle('设置').click(); + await a.page.waitForTimeout(300); + // 页签切换 + await a.page.getByRole('button', { name: '快捷键' }).click(); await a.page.waitForTimeout(200); + await a.page.getByRole('button', { name: '编辑器' }).click(); await a.page.waitForTimeout(200); + await a.page.getByRole('button', { name: '路径' }).click(); await a.page.waitForTimeout(200); + await a.page.getByRole('button', { name: '其他' }).click(); await a.page.waitForTimeout(200); + // 编辑器页:自动换行 toggle(用 evaluate 找到标签旁的按钮) + await a.page.getByRole('button', { name: '编辑器' }).click(); + await a.page.waitForTimeout(200); + await a.page.evaluate(() => { + const labels = Array.from(document.querySelectorAll('span')); + const lbl = labels.find(s => s.textContent === '自动换行'); + if (lbl) { + const row = lbl.parentElement; + const btn = row?.querySelector('button'); + (btn as HTMLElement)?.click(); + } + }); + await a.page.waitForTimeout(200); + // 其他页:性能优先 toggle + await a.page.getByRole('button', { name: '其他' }).click(); + await a.page.getByTestId('perf-toggle').click(); await a.page.waitForTimeout(200); + await a.page.getByTestId('perf-toggle').click(); await a.page.waitForTimeout(200); + // 快捷键页:重置按钮 + await a.page.getByRole('button', { name: '快捷键' }).click(); + await a.page.getByRole('button', { name: '重置默认' }).click(); await a.page.waitForTimeout(200); + await a.app.close(); +}); + +// === FileTree hover buttons + context menu === +test('FileTree: 悬停按钮+右键菜单可用', async () => { + const a = await launch(nodePath.join(os.tmpdir(), 'hn-ft-' + Date.now())); + const { v } = await setupVault(a, [{ name: 'FTDoc', body: 'ft test' }]); + await a.invoke('file:create', { basePath: '', name: 'FTFolder', isFolder: true }); + await a.page.getByTitle('刷新').click(); + await a.page.waitForTimeout(300); + // 悬停文件夹行显示按钮——用 mouse.move 物理触发 hover + const folderSpan = a.page.getByTestId('filetree').getByText('FTFolder', { exact: true }); + const box = await folderSpan.boundingBox(); + if (box) { + await a.page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await a.page.waitForTimeout(300); + } + // 新建文档按钮(此时 hover 状态已激活,按钮已渲染) + await a.page.getByTestId('filetree').getByTitle('新建文档').click(); + await a.page.getByPlaceholder(/文档标题/).fill('SubDoc'); + await a.page.getByPlaceholder(/文档标题/).press('Enter'); + await a.page.waitForTimeout(300); + // 右键菜单 + await a.page.getByTestId('filetree').getByText('FTDoc', { exact: true }).click({ button: 'right' }); + await a.page.waitForTimeout(300); + await expect(a.page.getByText('重命名')).toBeVisible({ timeout: 3000 }); + await a.page.keyboard.press('Escape'); + await a.app.close(); +}); + +// === Graph controls + tabs === +test('Graph: 控件+模式切换可用', async () => { + const a = await launch(nodePath.join(os.tmpdir(), 'hn-gc-' + Date.now())); + const { v } = await setupVault(a, [{ name: 'G1', body: 'g1' }]); + await a.page.getByTitle('切换 编辑器/图谱').click(); + await a.page.waitForTimeout(300); + // 思维导图/知识图谱 切换 + await a.page.getByRole('button', { name: '知识图谱' }).click(); + await a.page.waitForTimeout(1000); + await expect(a.page.getByTestId('graph-controls')).toBeVisible({ timeout: 5000 }); + // 放大/缩小/适配(用 evaluate 点击——测试窗口小,控件在视口边缘) + await a.page.evaluate(() => { + (document.querySelector('button[title="放大"]') as HTMLElement)?.click(); + }); + await a.page.waitForTimeout(200); + await a.page.evaluate(() => { + (document.querySelector('button[title="缩小"]') as HTMLElement)?.click(); + }); + await a.page.waitForTimeout(200); + await a.page.evaluate(() => { + (document.querySelector('[data-testid="fit-view-btn"]') as HTMLElement)?.click(); + }); + await a.page.waitForTimeout(200); + // 切换回思维导图 + await a.page.getByRole('button', { name: '思维导图' }).click(); + await a.page.waitForTimeout(300); + await a.app.close(); +}); + +// === TabBar pin/close === +test('TabBar: 固定+关闭可用', async () => { + const a = await launch(nodePath.join(os.tmpdir(), 'hn-tab-' + Date.now())); + const { v } = await setupVault(a, [{ name: 'Tab1', body: 't1' }, { name: 'Tab2', body: 't2' }]); + await a.page.getByTestId('filetree').getByText('Tab1').click(); + await a.page.waitForTimeout(300); + await a.page.getByTestId('filetree').getByText('Tab2').click(); + await a.page.waitForTimeout(300); + // 固定 Tab1(pin 按钮是 span[title="固定"]●) + const tab1 = a.page.getByTestId('tabbar').locator('div').filter({ hasText: 'Tab1' }).first(); + await tab1.locator('span[title="固定"]').click(); + await a.page.waitForTimeout(300); + // 取消固定 + await tab1.locator('span[title="取消固定"]').click(); + await a.page.waitForTimeout(300); + // 关闭 Tab2(close 按钮是 span 文本 x) + const tab2 = a.page.getByTestId('tabbar').locator('div').filter({ hasText: 'Tab2' }).first(); + await tab2.locator('span').filter({ hasText: 'x' }).click(); + await a.page.waitForTimeout(300); + await expect(a.page.getByTestId('tabbar').getByText('Tab2', { exact: true })).toHaveCount(0, { timeout: 3000 }); + await a.app.close(); +});