Files
MyNote/apps/desktop/tests/e2e/button-all.spec.ts
T
hunter-dev c0ba8df9b6 perf(renderer): 整体优化响应速度 — rAF 合并编辑器刷新/自动保存缓存/粒子引擎空闲自停
同时: 不透明窗口 + 精简主题(8到2)、TabBar 简化、图谱宽松布局、MoreActions 层级修复
2026-09-10 00:41:24 +08:00

350 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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+Shift+`').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+Shift+]').click(); await a.page.waitForTimeout(100);
await a.page.getByTitle('有序列表 Ctrl+Shift+[').click(); await a.page.waitForTimeout(100);
// 代码块
await a.page.getByTitle('代码块 Ctrl+Shift+K').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 });
// 关联文档(快速双向关联:下拉为文档名,按钮 title 建立双向关联)
const select = a.page.locator('select').first();
await select.selectOption('DocB');
await a.page.getByRole('button', { name: '关联', exact: true }).click();
await a.page.waitForTimeout(300);
await expect(a.page.getByTitle('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 close ===
test('TabBar: 关闭标签可用(pin 点已移除)', 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);
// 关闭 Tab2(close 按钮是 span 文本 x;pin 点按钮已按 UI 需求移除)
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();
});
test('TabBar: 关闭全部标签后编辑器应显示空状态', async () => {
const a = await launch(nodePath.join(os.tmpdir(), 'hn-close-all-' + Date.now()));
await setupVault(a, [{ name: 'CloseAllTest', body: '# 关闭全部测试\n正文内容' }]);
// 打开文档
await a.page.getByTestId('filetree').getByText('CloseAllTest', { exact: true }).click();
await expect(a.page.getByTestId('tabbar').getByText('CloseAllTest', { exact: true })).toBeVisible();
// 编辑器应显示文档内容
await expect(a.page.locator('.ProseMirror')).toContainText('关闭全部测试');
// 关闭标签页(span 文本 x)
const tab = a.page.getByTestId('tabbar').locator('div').filter({ hasText: 'CloseAllTest' }).first();
await tab.locator('span').filter({ hasText: 'x' }).click();
await a.page.waitForTimeout(500);
// 标签栏为空 → 当前文档应重置,编辑器显示空状态提示
await expect.poll(() => a.page.evaluate(() => (globalThis as any).__STORE__?.getState?.().currentPath), { timeout: 5000 }).toBeNull();
await expect(a.page.getByText('从左侧选择一篇笔记')).toBeVisible({ timeout: 5000 });
await a.app.close();
});
test('OutlinePanel: 切换文档后大纲自动更新', async () => {
const a = await launch(nodePath.join(os.tmpdir(), 'hn-outline-' + Date.now()));
await setupVault(a, [
{ name: 'DocA', body: '# Title A\n## Sub A' },
{ name: 'DocB', body: '# Title B\n## Sub B\n### Deep B' },
]);
await a.page.getByTestId('filetree').getByText('DocA', { exact: true }).click();
await a.page.waitForTimeout(500);
await expect(a.page.locator('.outline-item', { hasText: 'Title A' })).toBeVisible({ timeout: 5000 });
await expect(a.page.locator('.outline-item', { hasText: 'Sub A' })).toBeVisible({ timeout: 5000 });
await a.page.getByTestId('filetree').getByText('DocB', { exact: true }).click();
await a.page.waitForTimeout(800);
await expect(a.page.locator('.outline-item', { hasText: 'Title B' })).toBeVisible({ timeout: 5000 });
await expect(a.page.locator('.outline-item', { hasText: 'Deep B' })).toBeVisible({ timeout: 5000 });
await a.app.close();
});