diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0413019..a90ffbd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ on: jobs: test: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Checkout repository @@ -15,7 +15,7 @@ jobs: - name: Set up Bun uses: oven-sh/setup-bun@v2 with: - bun-version: latest + bun-version: 1.3.10 - name: Install dependencies run: bun install --frozen-lockfile @@ -35,6 +35,12 @@ jobs: - name: Install Playwright Chromium run: cd frontend && bunx playwright install --with-deps chromium + - name: Print visual stack versions + run: cd frontend && bun --version && bunx playwright --version && bunx playwright install --list + + - name: Install deterministic CJK fonts for visual snapshots + run: sudo apt-get update && sudo apt-get install -y fonts-noto-cjk fonts-noto-color-emoji fonts-liberation + - name: Run visual regression run: bun run ui:visual diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index 8e0e8bb..6188df9 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -1,4 +1,4 @@ -import { defineConfig, devices } from '@playwright/test'; +import { defineConfig } from '@playwright/test'; const port = Number(process.env.PW_PORT ?? 4173); const baseURL = process.env.PW_BASE_URL ?? `http://127.0.0.1:${port}`; @@ -13,7 +13,7 @@ export default defineConfig({ animations: 'disabled', caret: 'hide', scale: 'css', - maxDiffPixels: 30, + maxDiffPixelRatio: 0.012, stylePath: './tests/visual/fixtures/screenshot.css', }, }, @@ -23,11 +23,24 @@ export default defineConfig({ ], fullyParallel: false, use: { - ...devices['Desktop Chrome'], baseURL, + deviceScaleFactor: 1, + hasTouch: false, + isMobile: false, locale: 'zh-CN', timezoneId: 'Asia/Shanghai', viewport: { width: 1440, height: 900 }, + launchOptions: { + args: [ + '--disable-gpu', + '--disable-dev-shm-usage', + '--disable-lcd-text', + '--disable-font-subpixel-positioning', + '--font-render-hinting=none', + '--force-color-profile=srgb', + '--hide-scrollbars', + ], + }, trace: 'retain-on-failure', screenshot: 'only-on-failure', video: 'retain-on-failure', diff --git a/frontend/tests/visual/fixtures/screenshot.css b/frontend/tests/visual/fixtures/screenshot.css index 066b8f4..c1cb8d3 100644 --- a/frontend/tests/visual/fixtures/screenshot.css +++ b/frontend/tests/visual/fixtures/screenshot.css @@ -11,6 +11,9 @@ input, textarea, select { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', 'Noto Sans CJK SC', 'Helvetica Neue', Arial, sans-serif !important; + text-rendering: geometricPrecision !important; + -webkit-font-smoothing: antialiased !important; + -moz-osx-font-smoothing: grayscale !important; } code, @@ -22,3 +25,8 @@ pre, .bg-grid-pattern { opacity: 0.04 !important; } + +::-webkit-scrollbar { + width: 0 !important; + height: 0 !important; +} diff --git a/frontend/tests/visual/fixtures/stabilize.ts b/frontend/tests/visual/fixtures/stabilize.ts index c399631..d3295f6 100644 --- a/frontend/tests/visual/fixtures/stabilize.ts +++ b/frontend/tests/visual/fixtures/stabilize.ts @@ -14,9 +14,17 @@ const STABILIZE_STYLE = ` export async function stabilizeVisualState(page: Page) { await page.addStyleTag({ content: STABILIZE_STYLE }); + await page.waitForLoadState('networkidle'); await page.evaluate(() => { window.scrollTo(0, 0); }); + await page.evaluate(async () => { + if (document.fonts) { + await document.fonts.ready; + } + await new Promise((resolve) => requestAnimationFrame(() => resolve())); + await new Promise((resolve) => requestAnimationFrame(() => resolve())); + }); } export async function installVisualNetworkGuards(page: Page) {