test(visual): expand snapshot coverage for member-area pages
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.
This commit is contained in:
parent
728414fffc
commit
774c124969
15 changed files with 131 additions and 30 deletions
|
|
@ -1,5 +1,7 @@
|
|||
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 },
|
||||
|
|
@ -11,6 +13,9 @@ const publicPages = [
|
|||
{ 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 = [
|
||||
|
|
@ -18,8 +23,27 @@ const authenticatedPages = [
|
|||
{ 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')
|
||||
|
|
@ -27,47 +51,123 @@ async function waitForStable(page) {
|
|||
await page.evaluate(() => document.fonts.ready)
|
||||
}
|
||||
|
||||
test.describe('visual regression — 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)
|
||||
// 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}-${viewportName}.png`, {
|
||||
await expect(page).toHaveScreenshot(`${name}-desktop.png`, {
|
||||
maxDiffPixelRatio: 0.01,
|
||||
mask: [
|
||||
// Mask dynamic content like dates and counts
|
||||
page.locator('.event-date'),
|
||||
page.locator('.event-count'),
|
||||
page.locator('time'),
|
||||
],
|
||||
mask: commonMasks(page),
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test.describe('visual regression — authenticated pages', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await loginAsAdmin(page)
|
||||
})
|
||||
|
||||
for (const { name, path } of authenticatedPages) {
|
||||
test(`${name} — desktop`, async ({ 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)
|
||||
await page.goto(path)
|
||||
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(`${name}-desktop.png`, {
|
||||
await expect(page).toHaveScreenshot('members-detail-desktop.png', {
|
||||
maxDiffPixelRatio: 0.01,
|
||||
mask: [
|
||||
page.locator('.event-date'),
|
||||
page.locator('time'),
|
||||
page.locator('.member-since'),
|
||||
],
|
||||
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),
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue