diff --git a/.gitignore b/.gitignore index ef44c61..88f1b97 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/apps/desktop/package.json b/apps/desktop/package.json index abce1f5..ef9c83f 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -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", diff --git a/apps/desktop/playwright.config.ts b/apps/desktop/playwright.config.ts new file mode 100644 index 0000000..79bc144 --- /dev/null +++ b/apps/desktop/playwright.config.ts @@ -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' }, +}); diff --git a/apps/desktop/tests/e2e/roundtrip.spec.ts b/apps/desktop/tests/e2e/roundtrip.spec.ts new file mode 100644 index 0000000..e269bd6 --- /dev/null +++ b/apps/desktop/tests/e2e/roundtrip.spec.ts @@ -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); +}); diff --git a/apps/desktop/tests/e2e/smoke.spec.ts b/apps/desktop/tests/e2e/smoke.spec.ts new file mode 100644 index 0000000..e6679e0 --- /dev/null +++ b/apps/desktop/tests/e2e/smoke.spec.ts @@ -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(); + } +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c0b756a..09c66aa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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