diff --git a/frontend/e2e/visual-regression.spec.ts b/frontend/e2e/visual-regression.spec.ts new file mode 100644 index 0000000..db1a154 --- /dev/null +++ b/frontend/e2e/visual-regression.spec.ts @@ -0,0 +1,45 @@ +import type { APIRequestContext, Page } from '@playwright/test' +import { expect, test } from './fixtures' + +async function csrf(request: APIRequestContext) { + const state = await request.storageState() + return state.cookies.find(cookie => cookie.name === 'dodo_csrf')?.value ?? '' +} + +async function createTask(request: APIRequestContext, baseURL: string, title: string, listId: string) { + const response = await request.post('/api/v1/tasks', { + data: { title, list_id: listId }, + headers: { 'x-csrf-token': await csrf(request), origin: baseURL }, + }) + expect(response.ok(), await response.text()).toBeTruthy() +} + +async function openInbox(page: Page) { + if ((await page.viewportSize())!.width <= 930) { + await page.locator('.topbar').getByRole('button', { name: /展开菜单|收起菜单/ }).click() + } + await page.locator('.sidebar').getByRole('button', { name: '收集箱', exact: true }).click() +} + +test('inbox and task composer match approved visuals', async ({ page, request, baseURL }, testInfo) => { + test.skip(testInfo.project.name !== 'mobile-390', 'approved visual baselines are maintained for mobile-390 only') + await page.clock.install({ time: new Date('2026-09-19T05:00:00.000Z') }) + + const bootstrap = await request.get('/api/v1/bootstrap') + expect(bootstrap.ok()).toBeTruthy() + const inbox = (await bootstrap.json()).lists.find((item: { is_inbox: boolean }) => item.is_inbox) + expect(inbox).toBeTruthy() + + await createTask(request, baseURL!, '整理本周工作记录', inbox.id) + await createTask(request, baseURL!, '检查 Dodo 备份', inbox.id) + await createTask(request, baseURL!, '周末买水果和牛奶', inbox.id) + + await page.goto('/') + await openInbox(page) + await expect(page.locator('.task-row')).toHaveCount(3) + await expect(page).toHaveScreenshot('inbox.png', { fullPage: true }) + + await page.getByRole('button', { name: '添加任务' }).click() + await expect(page.getByRole('dialog', { name: /添加任务/ })).toBeVisible() + await expect(page).toHaveScreenshot('task-composer.png', { fullPage: true }) +}) diff --git a/frontend/e2e/visual-regression.spec.ts-snapshots/inbox-mobile-390.png b/frontend/e2e/visual-regression.spec.ts-snapshots/inbox-mobile-390.png new file mode 100644 index 0000000..79b60ef Binary files /dev/null and b/frontend/e2e/visual-regression.spec.ts-snapshots/inbox-mobile-390.png differ diff --git a/frontend/e2e/visual-regression.spec.ts-snapshots/task-composer-mobile-390.png b/frontend/e2e/visual-regression.spec.ts-snapshots/task-composer-mobile-390.png new file mode 100644 index 0000000..c6ae715 Binary files /dev/null and b/frontend/e2e/visual-regression.spec.ts-snapshots/task-composer-mobile-390.png differ diff --git a/frontend/package.json b/frontend/package.json index 44ab839..eeaeec0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1 +1 @@ -{"name":"dodo-frontend","private":true,"version":"0.1.0","type":"module","packageManager":"pnpm@9.15.9","scripts":{"dev":"vite --host 0.0.0.0","build":"vue-tsc -b && vite build","test":"vitest run --exclude 'e2e/**'","test:e2e:mobile":"node scripts/playwright-mobile.mjs"},"dependencies":{"@vitejs/plugin-vue":"latest","class-variance-authority":"latest","clsx":"latest","lucide-vue-next":"^0.468.0","markdown-it":"^15.0.2","markdown-it-task-lists":"^2.1.1","reka-ui":"latest","tailwind-merge":"latest","vue":"latest","vue-router":"latest"},"devDependencies":{"@playwright/test":"^1.63.0","@tailwindcss/vite":"latest","@types/markdown-it":"^14.2.0","@types/node":"latest","fflate":"^0.8.3","jsdom":"^30.0.1","tailwindcss":"latest","typescript":"^5.7.2","vite":"latest","vitest":"latest","vue-tsc":"latest"},"pnpm":{"onlyBuiltDependencies":["vue-demi"]}} \ No newline at end of file +{"name":"dodo-frontend","private":true,"version":"0.1.0","type":"module","packageManager":"pnpm@9.15.9","scripts":{"dev":"vite --host 0.0.0.0","build":"vue-tsc -b && vite build","test":"vitest run --exclude 'e2e/**'","test:e2e:mobile":"node scripts/playwright-mobile.mjs","test:e2e:visual":"DODO_E2E_PROJECT=mobile-390 playwright test e2e/visual-regression.spec.ts --project mobile-390"},"dependencies":{"@vitejs/plugin-vue":"latest","class-variance-authority":"latest","clsx":"latest","lucide-vue-next":"^0.468.0","markdown-it":"^15.0.2","markdown-it-task-lists":"^2.1.1","reka-ui":"latest","tailwind-merge":"latest","vue":"latest","vue-router":"latest"},"devDependencies":{"@playwright/test":"^1.63.0","@tailwindcss/vite":"latest","@types/markdown-it":"^14.2.0","@types/node":"latest","fflate":"^0.8.3","jsdom":"^30.0.1","tailwindcss":"latest","typescript":"^5.7.2","vite":"latest","vitest":"latest","vue-tsc":"latest"},"pnpm":{"onlyBuiltDependencies":["vue-demi"]}} \ No newline at end of file diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index d94b85e..58cdc5e 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -25,7 +25,15 @@ export default defineConfig({ fullyParallel: false, workers: 1, timeout: 45_000, - expect: { timeout: 8_000 }, + expect: { + timeout: 8_000, + toHaveScreenshot: { + animations: 'disabled', + caret: 'hide', + maxDiffPixelRatio: 0.001, + }, + }, + snapshotPathTemplate: '{testDir}/{testFilePath}-snapshots/{arg}-{projectName}{ext}', outputDir: path.join(runtimeRoot, 'test-results'), reporter: [['line'], ['html', { outputFolder: path.join(runtimeRoot, 'report'), open: 'never' }]], globalSetup: './e2e/global-setup.ts',