Implement comprehensive visual regression stability improvements

This commit is contained in:
adilallo
2025-09-03 11:01:52 -06:00
parent 24c8fc525e
commit 5a7295ff5d
3 changed files with 120 additions and 34 deletions
+54 -28
View File
@@ -47,8 +47,15 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: { node-version: 20, cache: npm } with: { node-version: 20, cache: npm }
- name: Cache Playwright
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: ms-playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- run: npm ci - run: npm ci
- run: npx playwright install --with-deps ${{ matrix.browser }} - run: npx playwright install ${{ matrix.browser }}
env:
PLAYWRIGHT_BROWSERS_PATH: ~/.cache/ms-playwright
- run: npm run build - run: npm run build
- name: E2E (start + test + teardown) - name: E2E (start + test + teardown)
@@ -80,13 +87,6 @@ jobs:
echo "🧪 Running E2E tests for ${{ matrix.browser }}..." echo "🧪 Running E2E tests for ${{ matrix.browser }}..."
BASE_URL="http://$HOST:$PORT" npx playwright test --project=${{ matrix.browser }} --reporter=list || TEST_EXIT_CODE=$? BASE_URL="http://$HOST:$PORT" npx playwright test --project=${{ matrix.browser }} --reporter=list || TEST_EXIT_CODE=$?
# Generate baseline snapshots only on main branch in CI environment
if [ "${{ gitea.ref }}" = "refs/heads/main" ]; then
echo "🌱 Generating baseline snapshots for ${{ matrix.browser }} in CI environment..."
PLAYWRIGHT_UPDATE_SNAPSHOTS=1 BASE_URL="http://$HOST:$PORT" npx playwright test tests/e2e/visual-regression.spec.ts --project=${{ matrix.browser }}
echo "✅ Baseline snapshots generated for ${{ matrix.browser }} in CI"
fi
# Teardown # Teardown
echo "🧹 Cleaning up server..." echo "🧹 Cleaning up server..."
kill "$SVPID" 2>/dev/null || true kill "$SVPID" 2>/dev/null || true
@@ -110,28 +110,21 @@ jobs:
path: playwright-${{ matrix.browser }}.tgz path: playwright-${{ matrix.browser }}.tgz
retention-days: 30 retention-days: 30
- name: Commit baseline snapshots (if generated)
if: gitea.ref == 'refs/heads/main' && always()
run: |
if [ -n "$(git status --porcelain tests/e2e/visual-regression.spec.ts-snapshots/)" ]; then
echo "🔄 Committing baseline snapshots for ${{ matrix.browser }} generated in CI..."
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add tests/e2e/visual-regression.spec.ts-snapshots/
git commit -m "Generate baseline snapshots for ${{ matrix.browser }} in CI environment" || true
echo "✅ Baseline snapshots committed for ${{ matrix.browser }}"
else
echo "️ No new baseline snapshots to commit for ${{ matrix.browser }}"
fi
visual-regression: visual-regression:
runs-on: [self-hosted, macos-latest] runs-on: [self-hosted, macos-latest]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: { node-version: 20, cache: npm } with: { node-version: 20, cache: npm }
- name: Cache Playwright
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: ms-playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- run: npm ci - run: npm ci
- run: npx playwright install --with-deps - run: npx playwright install
env:
PLAYWRIGHT_BROWSERS_PATH: ~/.cache/ms-playwright
- run: npm run build - run: npm run build
# 1) Sanity check that the build exists # 1) Sanity check that the build exists
- name: Verify Next build output - name: Verify Next build output
@@ -165,11 +158,7 @@ jobs:
curl -fsS "http://$HOST:$PORT" >/dev/null curl -fsS "http://$HOST:$PORT" >/dev/null
echo "✅ App is responding at http://$HOST:$PORT" echo "✅ App is responding at http://$HOST:$PORT"
# Seed snapshots on main branch only (one-time setup)
if [ "${{ gitea.ref }}" = "refs/heads/main" ]; then
echo "🌱 Seeding snapshots on main branch..."
PLAYWRIGHT_UPDATE_SNAPSHOTS=1 npx playwright test tests/e2e/visual-regression.spec.ts --project=chromium
fi
# Run visual regression tests # Run visual regression tests
echo "🧪 Running visual regression tests..." echo "🧪 Running visual regression tests..."
@@ -293,6 +282,43 @@ jobs:
name: lhci-results name: lhci-results
path: lhci-results path: lhci-results
seed-vr-snapshots:
if: gitea.ref == 'refs/heads/main'
runs-on: [self-hosted, macos-latest]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: npm }
- name: Cache Playwright
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: ms-playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- run: npm ci
- run: npx playwright install
env:
PLAYWRIGHT_BROWSERS_PATH: ~/.cache/ms-playwright
- run: npm run build
- name: Start app + wait
run: |
node node_modules/next/dist/bin/next start -p 3010 -H 127.0.0.1 > .next/runner.log 2>&1 &
npx wait-on -t 120000 tcp:127.0.0.1:3010
- name: Generate snapshots for ALL projects
env:
{
PLAYWRIGHT_UPDATE_SNAPSHOTS: "1",
BASE_URL: "http://127.0.0.1:3010",
}
run: npx playwright test tests/e2e/visual-regression.spec.ts --project=chromium --project=firefox --project=webkit --project=mobile
- name: Commit snapshots
run: |
if [ -n "$(git status --porcelain tests/e2e/visual-regression.spec.ts-snapshots/)" ]; then
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add tests/e2e/visual-regression.spec.ts-snapshots/
git commit -m "Seed Playwright VR snapshots (CI, all projects)"
fi
storybook: storybook:
runs-on: [self-hosted, macos-latest] runs-on: [self-hosted, macos-latest]
steps: steps:
+12 -5
View File
@@ -22,7 +22,6 @@ export default defineConfig({
// Deterministic rendering defaults to eliminate environment drift // Deterministic rendering defaults to eliminate environment drift
colorScheme: "light", colorScheme: "light",
viewport: { width: 1280, height: 800 }, viewport: { width: 1280, height: 800 },
deviceScaleFactor: 1, // Keep DPR=1 for stable anti-aliasing
timezoneId: "UTC", // Freeze timezone timezoneId: "UTC", // Freeze timezone
locale: "en-US", // Freeze locale locale: "en-US", // Freeze locale
headless: true, headless: true,
@@ -46,28 +45,36 @@ export default defineConfig({
name: "chromium", name: "chromium",
use: { use: {
...devices["Desktop Chrome"], ...devices["Desktop Chrome"],
deviceScaleFactor: 1, // Override device scale for consistency // Let device preset own the DPR for stable anti-aliasing
launchOptions: {
args: [
"--force-color-profile=srgb",
"--disable-skia-runtime-opts",
"--font-render-hinting=none",
"--disable-lcd-text",
],
},
}, },
}, },
{ {
name: "firefox", name: "firefox",
use: { use: {
...devices["Desktop Firefox"], ...devices["Desktop Firefox"],
deviceScaleFactor: 1, // Override device scale for consistency // Let device preset own the DPR for stable anti-aliasing
}, },
}, },
{ {
name: "webkit", name: "webkit",
use: { use: {
...devices["Desktop Safari"], ...devices["Desktop Safari"],
deviceScaleFactor: 1, // Override device scale for consistency // Let device preset own the DPR for stable anti-aliasing
}, },
}, },
{ {
name: "mobile", name: "mobile",
use: { use: {
...devices["iPhone 13"], ...devices["iPhone 13"],
deviceScaleFactor: 1, // Override device scale for consistency // Let device preset own the DPR for stable anti-aliasing
}, },
}, },
], ],
+54 -1
View File
@@ -1,21 +1,67 @@
import { test, expect } from "@playwright/test"; import { test, expect } from "@playwright/test";
test.describe("Visual Regression Tests", () => { test.describe("Visual Regression Tests", () => {
async function settle(page: any) {
await page.evaluate(() => {
window.scrollTo(0, window.scrollY); // ensure a frame boundary
void document.body.getBoundingClientRect();
});
await page.waitForTimeout(50);
}
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
// Add deterministic CSS to normalize rendering
await page.addStyleTag({
content: `
/* stop caret and selection flicker */
* { caret-color: transparent !important; }
::selection { background: transparent !important; }
/* hide scrollbars */
::-webkit-scrollbar { display: none !important; }
html { scrollbar-width: none !important; }
/* stabilize font rasterization */
* {
text-rendering: geometricPrecision !important;
-webkit-font-smoothing: antialiased !important;
-moz-osx-font-smoothing: grayscale !important;
}
`,
});
await page.goto("/"); await page.goto("/");
// Wait for all content to load // Wait for all content to load
await page.waitForLoadState("networkidle"); await page.waitForLoadState("networkidle");
// Make sure we've really got the webfonts before shots
await page.evaluate(async () => {
// @ts-ignore
if (document.fonts && document.fonts.status !== "loaded") {
// @ts-ignore
await document.fonts.ready;
}
});
}); });
test("homepage full page screenshot", async ({ page }) => { test("homepage full page screenshot", async ({ page }) => {
// Stabilize layout before screenshot
await settle(page);
// Take full page screenshot // Take full page screenshot
await expect(page).toHaveScreenshot("homepage-full.png", { await expect(page).toHaveScreenshot("homepage-full.png", {
fullPage: true, fullPage: true,
animations: "disabled", animations: "disabled",
scale: "css",
}); });
}); });
test("homepage viewport screenshot", async ({ page }) => { test("homepage viewport screenshot", async ({ page }) => {
// Stabilize layout before screenshot
await page.evaluate(() => {
window.scrollTo(0, 0);
// Force layout & a frame boundary
void document.body.getBoundingClientRect();
});
await page.waitForTimeout(50); // give the compositor one tick
// Take viewport screenshot // Take viewport screenshot
await expect(page).toHaveScreenshot("homepage-viewport.png", { await expect(page).toHaveScreenshot("homepage-viewport.png", {
animations: "disabled", animations: "disabled",
@@ -27,6 +73,13 @@ test.describe("Visual Regression Tests", () => {
await page.locator("text=Collaborate").scrollIntoViewIfNeeded(); await page.locator("text=Collaborate").scrollIntoViewIfNeeded();
await page.waitForTimeout(500); // Wait for animations await page.waitForTimeout(500); // Wait for animations
// Stabilize layout before screenshot
await page.evaluate(() => {
// Force layout & a frame boundary
void document.body.getBoundingClientRect();
});
await page.waitForTimeout(50); // give the compositor one tick
const heroSection = page.locator("section").first(); const heroSection = page.locator("section").first();
await expect(heroSection).toHaveScreenshot("hero-banner.png", { await expect(heroSection).toHaveScreenshot("hero-banner.png", {
animations: "disabled", animations: "disabled",
@@ -360,7 +413,7 @@ test.describe("Visual Regression Tests", () => {
await page.evaluate(() => { await page.evaluate(() => {
document.documentElement.style.setProperty( document.documentElement.style.setProperty(
"--prefers-reduced-motion", "--prefers-reduced-motion",
"reduce", "reduce"
); );
}); });