diff --git a/.gitignore b/.gitignore index 6f27eb0..8916e2a 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ scripts/*.js # Playwright e2e/test-results/ playwright-report/ +e2e/.auth/ # Worktrees .worktrees/ diff --git a/e2e/__screenshots__/visual/pages.spec.js-snapshots/about-desktop-chromium-darwin.png b/e2e/__screenshots__/visual/pages.spec.js-snapshots/about-desktop-chromium-darwin.png new file mode 100644 index 0000000..f1a80e7 Binary files /dev/null and b/e2e/__screenshots__/visual/pages.spec.js-snapshots/about-desktop-chromium-darwin.png differ diff --git a/e2e/__screenshots__/visual/pages.spec.js-snapshots/about-mobile-chromium-darwin.png b/e2e/__screenshots__/visual/pages.spec.js-snapshots/about-mobile-chromium-darwin.png new file mode 100644 index 0000000..8c4ad03 Binary files /dev/null and b/e2e/__screenshots__/visual/pages.spec.js-snapshots/about-mobile-chromium-darwin.png differ diff --git a/e2e/__screenshots__/visual/pages.spec.js-snapshots/admin-dashboard-desktop-chromium-darwin.png b/e2e/__screenshots__/visual/pages.spec.js-snapshots/admin-dashboard-desktop-chromium-darwin.png new file mode 100644 index 0000000..512bbce Binary files /dev/null and b/e2e/__screenshots__/visual/pages.spec.js-snapshots/admin-dashboard-desktop-chromium-darwin.png differ diff --git a/e2e/__screenshots__/visual/pages.spec.js-snapshots/connections-desktop-chromium-darwin.png b/e2e/__screenshots__/visual/pages.spec.js-snapshots/connections-desktop-chromium-darwin.png new file mode 100644 index 0000000..3dfa88b Binary files /dev/null and b/e2e/__screenshots__/visual/pages.spec.js-snapshots/connections-desktop-chromium-darwin.png differ diff --git a/e2e/__screenshots__/visual/pages.spec.js-snapshots/connections-mobile-chromium-darwin.png b/e2e/__screenshots__/visual/pages.spec.js-snapshots/connections-mobile-chromium-darwin.png new file mode 100644 index 0000000..6579dba Binary files /dev/null and b/e2e/__screenshots__/visual/pages.spec.js-snapshots/connections-mobile-chromium-darwin.png differ diff --git a/e2e/__screenshots__/visual/pages.spec.js-snapshots/member-account-desktop-chromium-darwin.png b/e2e/__screenshots__/visual/pages.spec.js-snapshots/member-account-desktop-chromium-darwin.png new file mode 100644 index 0000000..3b2b546 Binary files /dev/null and b/e2e/__screenshots__/visual/pages.spec.js-snapshots/member-account-desktop-chromium-darwin.png differ diff --git a/e2e/__screenshots__/visual/pages.spec.js-snapshots/member-account-mobile-chromium-darwin.png b/e2e/__screenshots__/visual/pages.spec.js-snapshots/member-account-mobile-chromium-darwin.png new file mode 100644 index 0000000..0df264e Binary files /dev/null and b/e2e/__screenshots__/visual/pages.spec.js-snapshots/member-account-mobile-chromium-darwin.png differ diff --git a/e2e/__screenshots__/visual/pages.spec.js-snapshots/member-activity-desktop-chromium-darwin.png b/e2e/__screenshots__/visual/pages.spec.js-snapshots/member-activity-desktop-chromium-darwin.png new file mode 100644 index 0000000..c3d2e31 Binary files /dev/null and b/e2e/__screenshots__/visual/pages.spec.js-snapshots/member-activity-desktop-chromium-darwin.png differ diff --git a/e2e/__screenshots__/visual/pages.spec.js-snapshots/member-dashboard-mobile-chromium-darwin.png b/e2e/__screenshots__/visual/pages.spec.js-snapshots/member-dashboard-mobile-chromium-darwin.png new file mode 100644 index 0000000..6e2b770 Binary files /dev/null and b/e2e/__screenshots__/visual/pages.spec.js-snapshots/member-dashboard-mobile-chromium-darwin.png differ diff --git a/e2e/__screenshots__/visual/pages.spec.js-snapshots/member-profile-mobile-chromium-darwin.png b/e2e/__screenshots__/visual/pages.spec.js-snapshots/member-profile-mobile-chromium-darwin.png new file mode 100644 index 0000000..13a9843 Binary files /dev/null and b/e2e/__screenshots__/visual/pages.spec.js-snapshots/member-profile-mobile-chromium-darwin.png differ diff --git a/e2e/__screenshots__/visual/pages.spec.js-snapshots/members-desktop-chromium-darwin.png b/e2e/__screenshots__/visual/pages.spec.js-snapshots/members-desktop-chromium-darwin.png new file mode 100644 index 0000000..6087be6 Binary files /dev/null and b/e2e/__screenshots__/visual/pages.spec.js-snapshots/members-desktop-chromium-darwin.png differ diff --git a/e2e/__screenshots__/visual/pages.spec.js-snapshots/members-detail-desktop-chromium-darwin.png b/e2e/__screenshots__/visual/pages.spec.js-snapshots/members-detail-desktop-chromium-darwin.png new file mode 100644 index 0000000..6f05986 Binary files /dev/null and b/e2e/__screenshots__/visual/pages.spec.js-snapshots/members-detail-desktop-chromium-darwin.png differ diff --git a/e2e/__screenshots__/visual/pages.spec.js-snapshots/members-mobile-chromium-darwin.png b/e2e/__screenshots__/visual/pages.spec.js-snapshots/members-mobile-chromium-darwin.png new file mode 100644 index 0000000..e6f8bb8 Binary files /dev/null and b/e2e/__screenshots__/visual/pages.spec.js-snapshots/members-mobile-chromium-darwin.png differ diff --git a/e2e/visual/pages.spec.js b/e2e/visual/pages.spec.js index d00c458..1fd3bf5 100644 --- a/e2e/visual/pages.spec.js +++ b/e2e/visual/pages.spec.js @@ -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), + }) + }) + } + }) })