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 openAppearance(a: { page: any }) { await a.page.getByTitle('外观定制').click(); await expect(a.page.getByText('外观定制')).toBeVisible({ timeout: 5000 }); } // FR-A7:Esc 关闭外观面板并回滚 test('FR-A7:Esc 关闭外观面板', async () => { const tmp = await fs.mkdtemp(nodePath.join(os.tmpdir(), 'hn-a7-')); 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 a = await launch(userData); await a.invoke('vault:switch', { root: vault }); await openAppearance(a); await a.page.keyboard.press('Escape'); await expect(a.page.getByText('外观定制')).not.toBeVisible({ timeout: 5000 }); await a.app.close(); }); // FR-A6:恢复主题默认按钮 + save 剥离等于默认的项 test('FR-A6:恢复主题默认 + 保存剥离默认值', async () => { const tmp = await fs.mkdtemp(nodePath.join(os.tmpdir(), 'hn-a6-')); 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 a = await launch(userData); await a.invoke('vault:switch', { root: vault }); await openAppearance(a); // 改一个颜色参数 -> dirty const accentInput = a.page.locator('input[type="color"]').first(); await accentInput.fill('#ff0000'); await expect(a.page.getByText('更改尚未保存')).toBeVisible({ timeout: 5000 }); // 恢复主题默认 -> 应恢复 await a.page.getByRole('button', { name: '恢复主题默认' }).click(); await expect(a.page.getByText('所有设置已保存')).toBeVisible({ timeout: 3000 }); // 保存后 config.json 的 params 应为空(全等于默认) await a.page.getByRole('button', { name: '保存外观' }).click(); await expect(a.page.getByText('外观定制')).not.toBeVisible({ timeout: 5000 }); const cfgPath = nodePath.join(userData, 'config.json'); const cfg = JSON.parse(await fs.readFile(cfgPath, 'utf-8')); // params 应为空对象或只含非默认项 const paramKeys = Object.keys(cfg.appearance?.params ?? {}); expect(paramKeys.length).toBe(0); await a.app.close(); }); // FR-A10:背景图压缩为 WebP(检查 data URL 前缀) test('FR-A10:背景图压缩为 WebP', async () => { const tmp = await fs.mkdtemp(nodePath.join(os.tmpdir(), 'hn-a10-')); 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 a = await launch(userData); await a.invoke('vault:switch', { root: vault }); // 创建一个小测试 PNG const pngData = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=='; const pngBuf = Buffer.from(pngData, 'base64'); const imgPath = nodePath.join(tmp, 'test.png'); await fs.writeFile(imgPath, pngBuf); await openAppearance(a); // 上传图片 const fileInput = a.page.locator('input[type="file"]'); await fileInput.setInputFiles(imgPath); await a.page.waitForTimeout(1000); // 检查 bgImage 在 store 中是 WebP data URL const bgImage = await a.page.evaluate(() => { // 从背景图 img 元素获取 src const img = document.querySelector('img[alt="bg"]') as HTMLImageElement; return img?.src ?? ''; }); // 应为 data:image/webp 开头 expect(bgImage.startsWith('data:image/webp')).toBeTruthy(); await a.app.close(); });