test: 起 Playwright(electron) E2E 骨架 + 金标准往返测试(建→写→存→重开字节一致)

This commit is contained in:
hunter-dev
2026-08-24 22:35:09 +08:00
parent 016048b029
commit 403a28cb2f
6 changed files with 176 additions and 1 deletions
+5
View File
@@ -12,3 +12,8 @@ packages/*/src/**/*.js
packages/*/src/**/*.d.ts
packages/*/src/**/*.js.map
*.tsbuildinfo
# Playwright E2E artifacts
test-results/
playwright-report/
apps/desktop/tests/e2e/.report/
+4 -1
View File
@@ -7,7 +7,8 @@
"scripts": {
"dev": "electron-vite dev",
"build": "electron-vite build",
"typecheck": "tsc -b"
"typecheck": "tsc -b",
"test:e2e": "electron-vite build && playwright test"
},
"dependencies": {
"@hunter/shared": "workspace:*",
@@ -39,11 +40,13 @@
"zustand": "^5.0.15"
},
"devDependencies": {
"@playwright/test": "^1.62.1",
"@types/node": "^20.0.0",
"@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0",
"electron": "^30.0.0",
"electron-vite": "^2.3.0",
"playwright": "^1.62.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"typescript": "^5.6.0",
+13
View File
@@ -0,0 +1,13 @@
import { defineConfig } from '@playwright/test';
// Electron E2E:测试在 tests/e2e 内用 _electron.launch 启动 built main(dist-electron/main/index.js)
export default defineConfig({
testDir: './tests/e2e',
timeout: 60_000,
expect: { timeout: 15_000 },
fullyParallel: false,
workers: 1,
retries: 0,
reportDir: './tests/e2e/.report',
use: { trace: 'retain-on-failure' },
});
+76
View File
@@ -0,0 +1,76 @@
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');
const VAULT_SWITCH = 'vault:switch';
const FILE_CREATE = 'file:create';
const DOC_WRITE = 'doc:write';
const DOC_READ = 'doc:read';
// 含嵌套强调、转义、4/3 反引号围栏、GFM 表格对齐、任务列表、数学公式
const KNOWN_MD = [
'# 金标准往返语料',
'',
'嵌套强调:**外层 *内层* 收尾** 与 ***粗斜***。',
'',
'转义:写出 \\*不是斜体\\*;代码内 `*不生效*`。',
'',
'4 反引号围栏包裹 3 反引号:',
'',
'````md',
'```js',
'console.log("hi")',
'```',
'````',
'',
'| 左 | 中 | 右 |',
'|:--|:-:|--:|',
'| a | b | c |',
'',
'- [x] 已完成',
'- [ ] 未完成',
'',
'行内 $a^2+b^2=c^2$ 与',
'',
'$$E=mc^2$$',
].join('\n');
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 };
}
test('金标准:建→写→存→重开 字节一致', async () => {
const tmp = await fs.mkdtemp(nodePath.join(os.tmpdir(), 'hn-rt-'));
const userData = nodePath.join(tmp, 'userdata');
const vault = nodePath.join(tmp, 'vault');
await fs.mkdir(userData, { recursive: true });
await fs.mkdir(vault, { recursive: true });
// 建 + 写
const a1 = await launch(userData);
const ud: string = await a1.app.evaluate(({ app }) => app.getPath('userData'));
expect(ud.startsWith(tmp)).toBeTruthy(); // 确认 --user-data-dir 生效,不污染真实数据
await a1.invoke(VAULT_SWITCH, { root: vault });
const created: { path: string } = await a1.invoke(FILE_CREATE, { basePath: '', name: 'roundtrip', isFolder: false });
expect(created.path).toBe('roundtrip.md');
await a1.invoke(DOC_WRITE, { path: 'roundtrip.md', meta: { title: '金标准往返' }, body: KNOWN_MD });
const W1 = await fs.readFile(nodePath.join(vault, 'roundtrip.md'), 'utf8');
await a1.app.close();
// 重开
const a2 = await launch(userData);
const read: { meta: any; body: string } = await a2.invoke(DOC_READ, { path: 'roundtrip.md' });
const W2 = await fs.readFile(nodePath.join(vault, 'roundtrip.md'), 'utf8');
await a2.app.close();
expect(read.body).toBe(KNOWN_MD);
expect(W2).toBe(W1);
});
+37
View File
@@ -0,0 +1,37 @@
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 launchApp() {
const tmp = await fs.mkdtemp(nodePath.join(os.tmpdir(), 'hn-e2e-'));
const userData = nodePath.join(tmp, 'userdata');
const vault = nodePath.join(tmp, 'vault');
await fs.mkdir(userData, { recursive: true });
await fs.mkdir(vault, { recursive: true });
// 预置 config,让 app 直接用临时库(FR-V 隔离)
await fs.writeFile(
nodePath.join(userData, 'config.json'),
JSON.stringify({ vault: { currentRoot: vault, recents: [vault], defaultRoot: vault } }),
'utf8',
);
const app = await electron.launch({ args: ['--user-data-dir=' + userData, MAIN_JS] });
return { app, tmp, vault };
}
test('app launches and renders title bar', async () => {
const { app, tmp } = await launchApp();
try {
const page = await app.firstWindow();
await page.waitForLoadState('domcontentloaded');
await expect(page.getByText('狩夜人笔记').first()).toBeVisible();
const shot = nodePath.join(tmp, 'smoke.png');
await page.screenshot({ path: shot });
console.log('SHOT=' + shot);
} finally {
await app.close();
}
});
+41
View File
@@ -117,6 +117,9 @@ importers:
specifier: ^5.0.15
version: 5.0.15(@types/react@18.3.31)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1))
devDependencies:
'@playwright/test':
specifier: ^1.62.1
version: 1.62.1
'@types/node':
specifier: ^20.0.0
version: 20.19.43
@@ -132,6 +135,9 @@ importers:
electron-vite:
specifier: ^2.3.0
version: 2.3.0(vite@5.4.21(@types/node@20.19.43))
playwright:
specifier: ^1.62.1
version: 1.62.1
react:
specifier: ^18.3.1
version: 18.3.1
@@ -467,6 +473,11 @@ packages:
os: [linux]
libc: [glibc]
'@playwright/test@1.62.1':
resolution: {integrity: sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==}
engines: {node: '>=20'}
hasBin: true
'@rollup/rollup-android-arm-eabi@4.62.5':
resolution: {integrity: sha512-jfkGfTwhQpsiSckPF8r9bU3pn3vyd72NlWaO+TgEO6WPSDnUhXzrNYCHBMOYj0ACaUgjm6eERLF+XV9a6RstoA==}
cpu: [arm]
@@ -1275,6 +1286,11 @@ packages:
resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==}
engines: {node: '>=6 <7 || >=8'}
fsevents@2.3.2:
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -1622,6 +1638,16 @@ packages:
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
playwright-core@1.62.1:
resolution: {integrity: sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==}
engines: {node: '>=20'}
hasBin: true
playwright@1.62.1:
resolution: {integrity: sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==}
engines: {node: '>=20'}
hasBin: true
postcss@8.5.26:
resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==}
engines: {node: ^10 || ^12 || >=14}
@@ -2286,6 +2312,10 @@ snapshots:
'@napi-rs/lzma-linux-x64-gnu@1.5.1':
optional: true
'@playwright/test@1.62.1':
dependencies:
playwright: 1.62.1
'@rollup/rollup-android-arm-eabi@4.62.5':
optional: true
@@ -3090,6 +3120,9 @@ snapshots:
jsonfile: 4.0.0
universalify: 0.1.2
fsevents@2.3.2:
optional: true
fsevents@2.3.3:
optional: true
@@ -3633,6 +3666,14 @@ snapshots:
picocolors@1.1.1: {}
playwright-core@1.62.1: {}
playwright@1.62.1:
dependencies:
playwright-core: 1.62.1
optionalDependencies:
fsevents: 2.3.2
postcss@8.5.26:
dependencies:
nanoid: 3.3.18