Files
mygit/e2e/visual-regression/capture-baselines.spec.ts
T
DuckQ1u 93d1b7c3d3
Copilot Setup Steps / copilot-setup-steps (push) Has been cancelled
first commit
2026-04-22 19:51:20 +07:00

242 lines
8.5 KiB
TypeScript

import {expect, test} from '@playwright/test';
/**
* Visual regression baselines for Ghost Admin.
*
* Each test navigates to a Ghost Admin screen, hides dynamic content
* (timestamps, notifications, avatars, animations) so they don't cause
* false diffs, and captures a full-page screenshot.
*
* Usage:
* # Generate / update baselines (run on main branch BEFORE migration)
* npx playwright test -c e2e/visual-regression --update-snapshots
*
* # Compare against baselines (run AFTER migration changes)
* npx playwright test -c e2e/visual-regression
*/
interface Screen {
name: string;
path: string;
/** Optional selector to wait for before screenshotting */
waitFor?: string;
/** Extra time to wait after load (ms) — use sparingly */
extraWait?: number;
/** If true, capture viewport-only screenshot instead of full page */
viewportOnly?: boolean;
}
// ---------------------------------------------------------------------------
// Full-page screens
// ---------------------------------------------------------------------------
const SCREENS: Screen[] = [
// Core Ember pages (HIGH risk — CSS class collisions with .flex, .hidden, etc.)
{name: 'dashboard', path: '/ghost/#/dashboard'},
{name: 'posts-list', path: '/ghost/#/posts'},
{name: 'pages-list', path: '/ghost/#/pages'},
{name: 'tags-list', path: '/ghost/#/tags'},
{name: 'tags-new', path: '/ghost/#/tags/new', extraWait: 1000},
{name: 'members-list', path: '/ghost/#/members'},
{name: 'members-activity', path: '/ghost/#/members-activity', extraWait: 1000},
// Editor
{name: 'editor-new-post', path: '/ghost/#/editor/post', extraWait: 2000},
// Settings (full page — captures top portion)
{name: 'settings', path: '/ghost/#/settings'},
// Analytics / Stats pages (React)
{name: 'analytics-overview', path: '/ghost/#/stats', extraWait: 1500},
{name: 'analytics-web', path: '/ghost/#/stats/web', extraWait: 1500},
{name: 'analytics-growth', path: '/ghost/#/stats/growth', extraWait: 1500},
{name: 'analytics-newsletters', path: '/ghost/#/stats/newsletters', extraWait: 1500},
// ActivityPub pages (React)
{name: 'activitypub-inbox', path: '/ghost/#/activitypub', extraWait: 1500},
{name: 'activitypub-feed', path: '/ghost/#/activitypub/feed', extraWait: 1500},
{name: 'activitypub-profile', path: '/ghost/#/activitypub/profile', extraWait: 1500},
{name: 'activitypub-notifications', path: '/ghost/#/activitypub/notifications', extraWait: 1500}
];
// ---------------------------------------------------------------------------
// Settings sections — scrolled into view individually
// ---------------------------------------------------------------------------
interface SettingsSection {
name: string;
/** The data-testid attribute on the section's TopLevelGroup */
testId: string;
}
const SETTINGS_SECTIONS: SettingsSection[] = [
// General
{name: 'settings-title-description', testId: 'title-and-description'},
{name: 'settings-timezone', testId: 'timezone'},
{name: 'settings-publication-language', testId: 'publication-language'},
{name: 'settings-staff', testId: 'users'},
{name: 'settings-social-accounts', testId: 'social-accounts'},
// Site
{name: 'settings-design', testId: 'design'},
{name: 'settings-theme', testId: 'theme'},
{name: 'settings-navigation', testId: 'navigation'},
{name: 'settings-announcement-bar', testId: 'announcement-bar'},
// Membership
{name: 'settings-portal', testId: 'portal'},
{name: 'settings-tiers', testId: 'tiers'},
{name: 'settings-analytics', testId: 'analytics'},
// Email newsletters
{name: 'settings-enable-newsletters', testId: 'enable-newsletters'},
{name: 'settings-newsletters', testId: 'newsletters'},
{name: 'settings-default-recipients', testId: 'default-recipients'},
{name: 'settings-mailgun', testId: 'mailgun'},
// Growth
{name: 'settings-recommendations', testId: 'recommendations'},
{name: 'settings-embed-signup-form', testId: 'embed-signup-form'},
// Advanced
{name: 'settings-integrations', testId: 'integrations'},
{name: 'settings-migration', testId: 'migrationtools'},
{name: 'settings-code-injection', testId: 'code-injection'},
{name: 'settings-labs', testId: 'labs'},
{name: 'settings-history', testId: 'history'}
];
// ---------------------------------------------------------------------------
// CSS injected into every page to hide flaky dynamic content
// ---------------------------------------------------------------------------
const HIDE_DYNAMIC_CONTENT = `
/* Timestamps and relative dates */
[data-testid="timestamp"],
[data-test-date],
time,
.gh-content-entry-date,
.gh-members-list-joined,
.gh-post-list-updated,
.gh-post-list-date {
visibility: hidden !important;
}
/* Notifications and toasts */
.gh-notification,
.gh-alerts,
[data-testid="toast"] {
display: none !important;
}
/* Animations and transitions */
*, *::before, *::after {
animation-duration: 0s !important;
animation-delay: 0s !important;
transition-duration: 0s !important;
transition-delay: 0s !important;
}
/* Avatars (can vary between runs) */
.gh-member-avatar img,
.gh-author-avatar img {
visibility: hidden !important;
}
/* Loading spinners */
.gh-loading-spinner,
.gh-loading-orb {
display: none !important;
}
/* Charts and graphs (data-dependent, flaky) */
.recharts-surface,
.recharts-wrapper,
canvas {
visibility: hidden !important;
}
/* Scrollbars (platform-dependent rendering) */
::-webkit-scrollbar {
display: none !important;
}
* {
scrollbar-width: none !important;
}
/* Editor carets and cursors */
.koenig-cursor,
.ProseMirror .ProseMirror-cursor,
[data-lexical-editor] .cursor,
.kg-prose caret-color {
caret-color: transparent !important;
}
`;
// ---------------------------------------------------------------------------
// Full-page screen tests
// ---------------------------------------------------------------------------
for (const screen of SCREENS) {
test(`visual baseline: ${screen.name}`, async ({page}) => {
await page.goto(screen.path);
await page.waitForLoadState('load');
if (screen.waitFor) {
await page.waitForSelector(screen.waitFor, {timeout: 15_000});
}
if (screen.extraWait) {
await page.waitForTimeout(screen.extraWait);
}
await page.addStyleTag({content: HIDE_DYNAMIC_CONTENT});
await page.waitForTimeout(500);
await expect(page).toHaveScreenshot(`${screen.name}.png`, {
fullPage: !screen.viewportOnly,
maxDiffPixelRatio: 0.001
});
});
}
// ---------------------------------------------------------------------------
// Settings section tests — scroll to each section and capture viewport
// ---------------------------------------------------------------------------
test.describe('settings sections', () => {
for (const section of SETTINGS_SECTIONS) {
test(`visual baseline: ${section.name}`, async ({page}) => {
await page.goto('/ghost/#/settings');
await page.waitForLoadState('load');
// Wait for Settings React app to mount
await page.waitForSelector('[data-testid="title-and-description"]', {timeout: 15_000});
// Extra settle time for settings to fully render
await page.waitForTimeout(1500);
await page.addStyleTag({content: HIDE_DYNAMIC_CONTENT});
// Scroll the section into view within the settings scroller
const scrolled = await page.evaluate((testId) => {
const sectionEl = document.querySelector(`[data-testid="${testId}"]`);
if (!sectionEl) {
return false;
}
sectionEl.scrollIntoView({block: 'start'});
return true;
}, section.testId);
if (!scrolled) {
// Section might not exist (conditional on config like Stripe)
test.skip();
return;
}
// Allow scroll + re-render to settle
await page.waitForTimeout(500);
await expect(page).toHaveScreenshot(`${section.name}.png`, {
fullPage: false,
maxDiffPixelRatio: 0.001
});
});
}
});