chore(ci): drop visual regression suite
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.
|
Before Width: | Height: | Size: 327 KiB |
|
Before Width: | Height: | Size: 167 KiB |
|
Before Width: | Height: | Size: 168 KiB |
|
Before Width: | Height: | Size: 237 KiB |
|
Before Width: | Height: | Size: 232 KiB |
|
Before Width: | Height: | Size: 307 KiB |
|
Before Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 316 KiB |
|
Before Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 238 KiB |
|
Before Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 287 KiB |
|
Before Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 283 KiB |
|
Before Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 264 KiB |
|
Before Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 204 KiB |
|
Before Width: | Height: | Size: 250 KiB |
|
Before Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 253 KiB |
|
Before Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 283 KiB |
|
Before Width: | Height: | Size: 226 KiB |
|
Before Width: | Height: | Size: 156 KiB |
|
|
@ -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),
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||