Adds 13 new visual regression baselines: - Public: about (desktop + mobile), members (desktop + mobile) - Authenticated desktop: member-account, member-activity, connections, admin-dashboard, members-detail - Authenticated mobile: member-dashboard, member-profile, member-account, connections Switches to a single serial test.describe with a beforeAll that logs in once and saves the auth cookie via storageState. This avoids repeated /api/dev/test-login calls that exhausted the dev server's MongoDB connections under parallel execution. Masks added: .tl-time, .stat-val, .item-date, .mc-avatar, .cc-avatar, .profile-avatar, .filter-count — covering activity timestamps, stat values, member join dates, avatars, and member counts.
173 lines
6.7 KiB
JavaScript
173 lines
6.7 KiB
JavaScript
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: 'member-activity', path: '/member/activity' },
|
|
{ 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.
|
|
// Note: 'about' mobile is already captured by the public pages loop (no auth middleware),
|
|
// so it is not duplicated here to avoid snapshot name collisions.
|
|
const authenticatedMobilePages = [
|
|
{ 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'),
|
|
]
|
|
}
|
|
|
|
// 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.png`, {
|
|
maxDiffPixelRatio: 0.01,
|
|
mask: commonMasks(page),
|
|
})
|
|
})
|
|
}
|
|
})
|
|
})
|