146 lines
7.4 KiB
TypeScript
146 lines
7.4 KiB
TypeScript
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();
|
|
});
|
|
|
|
// 回归:代码块复制应为纯代码内容,不附带 ```language 围栏(用户反馈修正)
|
|
test('回归:复制代码块为纯内容(无围栏)', 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('echo hello\nls -la');
|
|
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();
|
|
});
|