Files
MyNote/apps/desktop/tests/e2e/fix-regression.spec.ts
T

145 lines
7.4 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');
await page.waitForTimeout(1000);
const invoke = (ch: string, args: any) => page.evaluate(({ ch, args }) => (globalThis as any).dsh.invoke(ch, args), { ch, args });
return { app, page, invoke };
}
// 回归:文件列表缩进逐级递进 + 每行带 SVG 图标(FR-反馈1)
test('回归:文件列表缩进递进且带图标', async () => {
const tmp = await fs.mkdtemp(nodePath.join(os.tmpdir(), 'hn-reg1-'));
const userData = nodePath.join(tmp, 'ud'); const vault = nodePath.join(tmp, 'v');
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 a.invoke('file:create', { basePath: '', name: '一层', isFolder: true });
await a.invoke('file:create', { basePath: '一层', name: '二层', isFolder: true });
await a.invoke('file:create', { basePath: '一层/二层', name: '深层文档.md', isFolder: false });
await a.invoke('file:create', { basePath: '', name: '根文档.md', isFolder: false });
await a.page.evaluate(() => (globalThis as any).__STORE__?.getState?.().loadTree());
await a.page.waitForTimeout(1200);
const rows = await a.page.evaluate(() => {
const ft = document.querySelector('[data-testid="filetree"]');
const out: { padLeft: string; svgs: number; text: string }[] = [];
for (const el of ft?.querySelectorAll('div') ?? []) {
const cs = getComputedStyle(el);
if (cs.cursor === 'pointer' && cs.position === 'relative') {
out.push({ padLeft: cs.paddingLeft, svgs: el.querySelectorAll('svg').length, text: el.textContent.trim().slice(0, 20) });
}
}
return out;
});
const pad = rows.map(r => parseInt(r.padLeft, 10));
// 根文档(0级) vs 一层(0级) vs 二层(1级) vs 深层文档(2级)
expect(rows.length).toBeGreaterThanOrEqual(4);
const rootPad = pad[0];
const deepRow = rows.find(r => r.text.includes('深层文档'));
const l2Row = rows.find(r => r.text.includes('二层'));
expect(deepRow).toBeTruthy();
expect(l2Row).toBeTruthy();
expect(parseInt(deepRow!.padLeft, 10)).toBeGreaterThan(parseInt(l2Row!.padLeft, 10));
expect(parseInt(l2Row!.padLeft, 10)).toBeGreaterThan(rootPad);
// 每个文件夹/文档行都有 SVG 图标
for (const r of rows) expect(r.svgs).toBeGreaterThan(0);
await a.app.close();
});
// 回归:代码块复制携带 Markdown 围栏(FR-反馈3)
test('回归:复制代码块携带 ```language 围栏', async () => {
const tmp = await fs.mkdtemp(nodePath.join(os.tmpdir(), 'hn-reg3-'));
const userData = nodePath.join(tmp, 'ud'); const vault = nodePath.join(tmp, 'v');
await fs.mkdir(userData, { recursive: true }); await fs.mkdir(vault, { recursive: true });
const a = await launch(userData);
await a.invoke('vault:switch', { root: vault });
const body = '# 复制测试\n\n```bash\necho hello\nls -la\n```';
await a.invoke('doc:write', { path: 'copy.md', meta: {}, body });
await a.page.evaluate((p: string) => (globalThis as any).__STORE__?.getState?.().openDoc(p), 'copy.md');
await a.page.waitForTimeout(1500);
// 拦截剪贴板写入
await a.page.evaluate(() => {
(globalThis as any).__cap = [];
navigator.clipboard.writeText = (t: string) => { (globalThis as any).__cap.push(t); return Promise.resolve(); };
});
const copyBtn = a.page.locator('.ProseMirror button', { hasText: '复制' }).first();
await expect(copyBtn).toBeVisible({ timeout: 10000 });
await copyBtn.click();
await a.page.waitForTimeout(400);
const text = await a.page.evaluate(() => (globalThis as any).__cap?.[0] ?? null);
expect(text).toBe('```bash\necho hello\nls -la\n```');
await a.app.close();
});
// 回归:长文档底部书写时光标不贴底(FR-反馈4)
test('回归:长文档底部书写光标距底留白', async () => {
const tmp = await fs.mkdtemp(nodePath.join(os.tmpdir(), 'hn-reg4-'));
const userData = nodePath.join(tmp, 'ud'); const vault = nodePath.join(tmp, 'v');
await fs.mkdir(userData, { recursive: true }); await fs.mkdir(vault, { recursive: true });
const a = await launch(userData);
await a.invoke('vault:switch', { root: vault });
const paras: string[] = [];
for (let i = 1; i <= 600; i++) paras.push('段落 ' + i + ' 内容内容内容内容内容内容内容内容内容内容内容内容内容内容');
const body = '# 长文档\n\n' + paras.join('\n\n');
await a.invoke('doc:write', { path: 'long.md', meta: {}, body });
await a.page.evaluate((p: string) => (globalThis as any).__STORE__?.getState?.().openDoc(p), 'long.md');
await a.page.waitForTimeout(1500);
const editor = a.page.locator('.ProseMirror');
await editor.click();
await a.page.keyboard.press('Control+End');
await a.page.waitForTimeout(400);
await a.page.keyboard.type('底部输入内容');
await a.page.waitForTimeout(700);
const m = await a.page.evaluate(() => {
const prose = document.querySelector('.ProseMirror');
const sc = prose?.closest('div[style]') as HTMLElement | null;
if (!sc) return null;
const rect = sc.getBoundingClientRect();
const sel = window.getSelection();
const range = sel && sel.rangeCount ? sel.getRangeAt(0) : null;
const caret = range ? range.getBoundingClientRect() : null;
return caret ? Math.round(rect.bottom - caret.bottom) : null;
});
expect(m).not.toBeNull();
// 光标至少距底部 30px 留白(修复前为 0~5px 贴底)
expect(m!).toBeGreaterThanOrEqual(30);
await a.app.close();
});
// 回归:代码块语法高亮不再出现红色(FR-反馈2)
test('回归:代码块语法高亮无红色', async () => {
const tmp = await fs.mkdtemp(nodePath.join(os.tmpdir(), 'hn-reg2-'));
const userData = nodePath.join(tmp, 'ud'); const vault = nodePath.join(tmp, 'v');
await fs.mkdir(userData, { recursive: true }); await fs.mkdir(vault, { recursive: true });
const a = await launch(userData);
await a.invoke('vault:switch', { root: vault });
const body = '# 高亮\n\n```bash\ntime for in\n# 中文注释\n```\n\n```markdown\n## 标题\n- 列表项\n```';
await a.invoke('doc:write', { path: 'hl.md', meta: {}, body });
await a.page.evaluate((p: string) => (globalThis as any).__STORE__?.getState?.().openDoc(p), 'hl.md');
await a.page.waitForTimeout(1500);
const reds = await a.page.evaluate(() => {
const prose = document.querySelector('.ProseMirror');
const bad: { cls: string; color: string; text: string }[] = [];
for (const el of prose?.querySelectorAll('.hljs-keyword, .hljs-section, .hljs-tag, .hljs-doctag, .hljs-deletion') ?? []) {
const c = getComputedStyle(el).color;
const mm = c.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
if (!mm) continue;
const r = +mm[1], g = +mm[2], b = +mm[3];
if (r > 130 && r > g * 1.4 && r > b * 1.4) {
bad.push({ cls: String(el.className), color: c, text: (el.textContent || '').slice(0, 20) });
}
}
return bad;
});
expect(reds).toEqual([]);
await a.app.close();
});