chore(ci): drop visual regression suite
All checks were successful
Test / vitest (push) Successful in 11m51s
Test / playwright (push) Successful in 16m24s
Test / Notify on failure (push) Has been skipped

Visual snapshots were generated on macOS but CI runs on Linux, and
font hinting differences between the two would always produce false
positives. The job was already continue-on-error and the baselines
weren't giving trustworthy signal — remove the spec, baselines, CI
job, and now-unneeded snapshot config / --ignore-snapshots flag.
Functional e2e coverage in the playwright job is unaffected.
This commit is contained in:
Jennie Robinson Faber 2026-05-01 13:37:06 +01:00
parent 73e67d02bb
commit 84aea08a5f
29 changed files with 1 additions and 239 deletions

View file

@ -66,7 +66,7 @@ jobs:
- name: Server log on failure
if: failure()
run: cat /tmp/server.log || true
- run: npx playwright test --ignore-snapshots
- run: npx playwright test
- uses: actions/upload-artifact@v3
if: failure()
with:
@ -88,59 +88,3 @@ jobs:
-H 'Content-type: application/json' \
--data "{\"text\":\":x: *Ghost Guild CI failed* on \`${{ github.ref_name }}\`\nCommit: ${{ github.sha }}\n${{ github.server_url }}/${{ github.repository }}/actions\"}"
visual:
runs-on: ubuntu-latest
needs: vitest
continue-on-error: true
env:
MONGODB_URI: mongodb://mongo-ci:27017/ghostguild-test
JWT_SECRET: ci-test-jwt-secret
RESEND_API_KEY: re_ci_dummy_not_used
HELCIM_API_TOKEN: helcim_ci_dummy_not_used
OIDC_COOKIE_SECRET: ci-oidc-cookie-secret-not-secret
NUXT_PUBLIC_COMING_SOON: 'false'
NODE_ENV: development
ALLOW_DEV_TEST_ENDPOINTS: 'true'
BASE_URL: http://localhost:3000
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npx playwright install --with-deps chromium
- name: Start MongoDB
run: |
docker rm -f mongo-ci 2>/dev/null || true
docker run -d --name mongo-ci mongo:7
# Forgejo runs each job inside its own container; attach Mongo to
# that container's network so MONGODB_URI=mongodb://mongo-ci:27017
# resolves from inside the runner.
RUNNER_NET=$(docker inspect "$HOSTNAME" --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}' | awk '{print $1}')
docker network connect "$RUNNER_NET" mongo-ci
docker ps
- name: Wait for MongoDB
run: timeout 30 sh -c 'until docker exec mongo-ci mongosh --quiet --eval "1" >/dev/null 2>&1; do sleep 1; done'
- name: MongoDB log on failure
if: failure()
run: docker logs mongo-ci || true
- name: Seed test data
run: node scripts/seed-all.js && node scripts/seed-tags.js
- run: npm run build
- name: Start server
run: node .output/server/index.mjs > /tmp/server.log 2>&1 &
env:
PORT: 3000
- name: Wait for server
run: timeout 30 sh -c 'until curl -sf http://localhost:3000; do sleep 1; done'
- name: Server log on failure
if: failure()
run: cat /tmp/server.log || true
- run: npx playwright test e2e/visual/
- uses: actions/upload-artifact@v3
if: failure()
with:
name: visual-diffs
path: e2e/test-results/
retention-days: 7

Binary file not shown.

Before

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

View file

@ -1,180 +0,0 @@
import { test, expect } from '@playwright/test'
import { loginAsAdmin } from '../helpers/auth.js'
import path from 'path'
import fs from 'fs'
const viewports = {
desktop: { width: 1280, height: 720 },
mobile: { width: 375, height: 667 },
}
const publicPages = [
{ name: 'home', path: '/' },
{ name: 'join', path: '/join' },
{ name: 'events', path: '/events' },
{ name: 'coming-soon', path: '/coming-soon' },
// about and members have no auth middleware — accessible publicly
{ name: 'about', path: '/about' },
{ name: 'members', path: '/members' },
]
const authenticatedPages = [
{ name: 'member-dashboard', path: '/member/dashboard' },
{ name: 'member-profile', path: '/member/profile' },
{ name: 'admin-members', path: '/admin/members' },
{ name: 'admin-events-create', path: '/admin/events/create' },
// New authenticated pages
{ name: 'member-account', path: '/member/account' },
{ name: 'connections', path: '/connections' },
{ name: 'admin-dashboard', path: '/admin' },
]
// Pages that need mobile coverage captured while authenticated.
// These cover column-collapse breakpoints critical for the page-shell refactor.
// Snapshots use the -mobile-auth suffix to distinguish from the public mobile loop
// (which also captures about-mobile unauthenticated, so names must not collide).
const authenticatedMobilePages = [
{ name: 'about', path: '/about' },
{ name: 'member-dashboard', path: '/member/dashboard' },
{ name: 'member-profile', path: '/member/profile' },
{ name: 'member-account', path: '/member/account' },
{ name: 'connections', path: '/connections' },
]
// Path where the saved admin auth state (cookies) will be stored within a run.
const authStatePath = path.resolve('e2e/.auth/admin.json')
// Wait for fonts and images to load before taking screenshots
async function waitForStable(page) {
await page.waitForLoadState('networkidle')
// Wait for web fonts to load
await page.evaluate(() => document.fonts.ready)
}
// Common mask selectors for dynamic content
function commonMasks(page) {
return [
// Dates and times throughout the app
page.locator('.event-date'),
page.locator('.event-count'),
page.locator('time'),
page.locator('.member-since'),
// Activity log timestamps
page.locator('.tl-time'),
// Admin dashboard stat values (member counts, revenue, etc.)
page.locator('.stat-val'),
// Recent member join dates in admin dashboard
page.locator('.item-date'),
// Member avatars (ghost images may not load deterministically)
page.locator('.mc-avatar'),
page.locator('.cc-avatar'),
page.locator('.profile-avatar'),
// Member count text in members page filter bar
page.locator('.filter-count'),
// Connections page: filter bar and suggestions vary based on tag/topic
// state and async fetch ordering. Mask them to keep the structural
// (PageShell + page-level) regression coverage stable.
page.locator('.filter-bar'),
page.locator('.skills-bar'),
page.locator('.connections-section'),
page.locator('.loading-state'),
]
}
// All visual tests run serially in a single top-level describe block.
//
// Auth is handled with a beforeAll that saves the cookie to disk once. All
// authenticated sub-describes load from that saved state, avoiding repeated
// /api/dev/test-login calls that exhaust the dev server's MongoDB connections.
test.describe('visual regression', () => {
test.describe.configure({ mode: 'serial' })
// Log in once before all tests and save the auth cookie.
// serial mode guarantees this runs before any test in this describe tree.
test.beforeAll(async ({ browser }) => {
fs.mkdirSync(path.dirname(authStatePath), { recursive: true })
const page = await browser.newPage()
await loginAsAdmin(page)
await page.context().storageState({ path: authStatePath })
await page.close()
})
// ── Public pages (desktop + mobile) ──────────────────────────────────────
test.describe('public pages', () => {
for (const { name, path } of publicPages) {
for (const [viewportName, viewport] of Object.entries(viewports)) {
test(`${name}${viewportName}`, async ({ page }) => {
await page.setViewportSize(viewport)
await page.goto(path)
await waitForStable(page)
await expect(page).toHaveScreenshot(`${name}-${viewportName}.png`, {
maxDiffPixelRatio: 0.01,
mask: commonMasks(page),
})
})
}
}
})
// ── Authenticated pages (desktop) ─────────────────────────────────────────
// Loads saved auth cookie — no repeated /api/dev/test-login calls.
test.describe('authenticated pages', () => {
test.use({ storageState: authStatePath })
for (const { name, path } of authenticatedPages) {
test(`${name} — desktop`, async ({ page }) => {
await page.setViewportSize(viewports.desktop)
await page.goto(path)
await waitForStable(page)
await expect(page).toHaveScreenshot(`${name}-desktop.png`, {
maxDiffPixelRatio: 0.01,
mask: commonMasks(page),
})
})
}
// members-detail: navigate to the test admin's own profile page.
// The test admin is created by /api/dev/test-login (email: test-admin@ghostguild.dev,
// status: active). We fetch their _id from /api/auth/member using the saved cookie.
// Even if showInDirectory is false, the page renders a stable error or profile shell.
test('members-detail — desktop', async ({ page }) => {
await page.setViewportSize(viewports.desktop)
const response = await page.request.get('/api/auth/member')
// /api/auth/member returns the member object directly (not nested under a 'member' key)
const authData = response.ok() ? await response.json() : null
const memberId = authData?._id || authData?.id
if (!memberId) {
// Skip gracefully if we can't retrieve the member ID
test.skip(true, 'Could not retrieve test admin member ID from /api/auth/member')
return
}
await page.goto(`/members/${memberId}`)
await waitForStable(page)
await expect(page).toHaveScreenshot('members-detail-desktop.png', {
maxDiffPixelRatio: 0.01,
mask: commonMasks(page),
})
})
})
// ── Authenticated pages (mobile — column-collapse coverage) ───────────────
// Loads saved auth cookie — no repeated /api/dev/test-login calls.
test.describe('authenticated pages (mobile)', () => {
test.use({ storageState: authStatePath })
for (const { name, path } of authenticatedMobilePages) {
test(`${name} — mobile`, async ({ page }) => {
await page.setViewportSize(viewports.mobile)
await page.goto(path)
await waitForStable(page)
await expect(page).toHaveScreenshot(`${name}-mobile-auth.png`, {
maxDiffPixelRatio: 0.01,
mask: commonMasks(page),
})
})
}
})
})

View file

@ -6,8 +6,6 @@ const BASE_URL = `http://localhost:${PORT}`;
export default defineConfig({
testDir: "./e2e",
outputDir: "e2e/test-results",
snapshotDir: "e2e/__screenshots__",
snapshotPathTemplate: "{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}-{projectName}{ext}",
fullyParallel: false,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 1 : 1,