/** * E2E Test: Visual Regression Testing * * Tests for: * - Dashboard visual appearance * - Scenario Detail page * - Comparison page * - Reports page * - Dark/Light mode consistency */ import { test, expect } from '@playwright/test'; import { navigateTo, waitForLoading, createScenarioViaAPI, deleteScenarioViaAPI, startScenarioViaAPI, sendTestLogs, generateTestScenarioName, setDesktopViewport, setMobileViewport, } from './utils/test-helpers'; import { newScenarioData } from './fixtures/test-scenarios'; import { testLogs } from './fixtures/test-logs'; import path from 'path'; import fs from 'fs'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Visual regression configuration const BASELINE_DIR = path.join(__dirname, 'screenshots', 'baseline'); const ACTUAL_DIR = path.join(__dirname, 'screenshots', 'actual'); const DIFF_DIR = path.join(__dirname, 'screenshots', 'diff'); // Ensure directories exist [BASELINE_DIR, ACTUAL_DIR, DIFF_DIR].forEach(dir => { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } }); // Threshold for visual differences (0.2 = 20%) const VISUAL_THRESHOLD = 0.2; let testScenarioId: string | null = null; test.describe('Visual Regression - Dashboard', () => { test.beforeAll(async ({ request }) => { // Create a test scenario with data for better visuals const scenario = await createScenarioViaAPI(request, { ...newScenarioData, name: generateTestScenarioName('Visual Test'), }); testScenarioId = scenario.id; await startScenarioViaAPI(request, scenario.id); await sendTestLogs(request, scenario.id, testLogs); }); test.afterAll(async ({ request }) => { if (testScenarioId) { try { await request.post(`http://localhost:8000/api/v1/scenarios/${testScenarioId}/stop`); } catch { // Scenario might not be running } await deleteScenarioViaAPI(request, testScenarioId); } }); test.beforeEach(async ({ page }) => { await setDesktopViewport(page); }); test('dashboard should match baseline - desktop', async ({ page }) => { await navigateTo(page, '/'); await waitForLoading(page); // Wait for all content to stabilize await page.waitForTimeout(1000); const screenshot = await page.screenshot({ fullPage: true }); expect(screenshot).toMatchSnapshot('dashboard-desktop.png', { threshold: VISUAL_THRESHOLD, }); }); test('dashboard should match baseline - mobile', async ({ page }) => { await setMobileViewport(page); await navigateTo(page, '/'); await waitForLoading(page); // Wait for mobile layout to stabilize await page.waitForTimeout(1000); const screenshot = await page.screenshot({ fullPage: true }); expect(screenshot).toMatchSnapshot('dashboard-mobile.png', { threshold: VISUAL_THRESHOLD, }); }); test('dashboard should match baseline - tablet', async ({ page }) => { await page.setViewportSize({ width: 768, height: 1024 }); await navigateTo(page, '/'); await waitForLoading(page); await page.waitForTimeout(1000); const screenshot = await page.screenshot({ fullPage: true }); expect(screenshot).toMatchSnapshot('dashboard-tablet.png', { threshold: VISUAL_THRESHOLD, }); }); }); test.describe('Visual Regression - Scenarios Page', () => { test.beforeEach(async ({ page }) => { await setDesktopViewport(page); }); test('scenarios list should match baseline', async ({ page }) => { await navigateTo(page, '/scenarios'); await waitForLoading(page); await page.waitForTimeout(1000); const screenshot = await page.screenshot({ fullPage: true }); expect(screenshot).toMatchSnapshot('scenarios-list.png', { threshold: VISUAL_THRESHOLD, }); }); test('scenarios list should be responsive - mobile', async ({ page }) => { await setMobileViewport(page); await navigateTo(page, '/scenarios'); await waitForLoading(page); await page.waitForTimeout(1000); const screenshot = await page.screenshot({ fullPage: true }); expect(screenshot).toMatchSnapshot('scenarios-list-mobile.png', { threshold: VISUAL_THRESHOLD, }); }); }); test.describe('Visual Regression - Scenario Detail', () => { test.beforeAll(async ({ request }) => { // Ensure we have a test scenario if (!testScenarioId) { const scenario = await createScenarioViaAPI(request, { ...newScenarioData, name: generateTestScenarioName('Visual Detail Test'), }); testScenarioId = scenario.id; await startScenarioViaAPI(request, scenario.id); await sendTestLogs(request, scenario.id, testLogs); } }); test.beforeEach(async ({ page }) => { await setDesktopViewport(page); }); test('scenario detail should match baseline', async ({ page }) => { await navigateTo(page, `/scenarios/${testScenarioId}`); await waitForLoading(page); // Wait for metrics to load await page.waitForTimeout(2000); const screenshot = await page.screenshot({ fullPage: true }); expect(screenshot).toMatchSnapshot('scenario-detail.png', { threshold: VISUAL_THRESHOLD, }); }); test('scenario detail should be responsive - mobile', async ({ page }) => { await setMobileViewport(page); await navigateTo(page, `/scenarios/${testScenarioId}`); await waitForLoading(page); await page.waitForTimeout(2000); const screenshot = await page.screenshot({ fullPage: true }); expect(screenshot).toMatchSnapshot('scenario-detail-mobile.png', { threshold: VISUAL_THRESHOLD, }); }); }); test.describe('Visual Regression - 404 Page', () => { test.beforeEach(async ({ page }) => { await setDesktopViewport(page); }); test('404 page should match baseline', async ({ page }) => { await navigateTo(page, '/non-existent-page'); await waitForLoading(page); const screenshot = await page.screenshot({ fullPage: true }); expect(screenshot).toMatchSnapshot('404-page.png', { threshold: VISUAL_THRESHOLD, }); }); }); test.describe('Visual Regression - Component Elements', () => { test.beforeEach(async ({ page }) => { await setDesktopViewport(page); await navigateTo(page, '/'); await waitForLoading(page); }); test('header should match baseline', async ({ page }) => { const header = page.locator('header'); await expect(header).toBeVisible(); const screenshot = await header.screenshot(); expect(screenshot).toMatchSnapshot('header.png', { threshold: VISUAL_THRESHOLD, }); }); test('sidebar should match baseline', async ({ page }) => { const sidebar = page.locator('aside'); await expect(sidebar).toBeVisible(); const screenshot = await sidebar.screenshot(); expect(screenshot).toMatchSnapshot('sidebar.png', { threshold: VISUAL_THRESHOLD, }); }); test('stat cards should match baseline', async ({ page }) => { const statCards = page.locator('[class*="grid"] > div').first(); await expect(statCards).toBeVisible(); const screenshot = await statCards.screenshot(); expect(screenshot).toMatchSnapshot('stat-card.png', { threshold: VISUAL_THRESHOLD, }); }); }); test.describe('Visual Regression - Dark Mode', () => { test.beforeEach(async ({ page }) => { await setDesktopViewport(page); }); test('dashboard should render correctly in dark mode', async ({ page }) => { // Enable dark mode by adding class to html element await page.emulateMedia({ colorScheme: 'dark' }); // Also add dark class to root element if the app uses class-based dark mode await page.evaluate(() => { document.documentElement.classList.add('dark'); }); await navigateTo(page, '/'); await waitForLoading(page); await page.waitForTimeout(1000); const screenshot = await page.screenshot({ fullPage: true }); expect(screenshot).toMatchSnapshot('dashboard-dark.png', { threshold: VISUAL_THRESHOLD, }); // Reset await page.emulateMedia({ colorScheme: 'light' }); await page.evaluate(() => { document.documentElement.classList.remove('dark'); }); }); test('scenarios list should render correctly in dark mode', async ({ page }) => { await page.emulateMedia({ colorScheme: 'dark' }); await page.evaluate(() => { document.documentElement.classList.add('dark'); }); await navigateTo(page, '/scenarios'); await waitForLoading(page); await page.waitForTimeout(1000); const screenshot = await page.screenshot({ fullPage: true }); expect(screenshot).toMatchSnapshot('scenarios-list-dark.png', { threshold: VISUAL_THRESHOLD, }); // Reset await page.emulateMedia({ colorScheme: 'light' }); await page.evaluate(() => { document.documentElement.classList.remove('dark'); }); }); }); test.describe('Visual Regression - Loading States', () => { test.beforeEach(async ({ page }) => { await setDesktopViewport(page); }); test('loading state should match baseline', async ({ page }) => { // Navigate and immediately capture before loading completes await page.goto('/scenarios'); // Wait just a moment to catch loading state await page.waitForTimeout(100); const screenshot = await page.screenshot({ fullPage: true }); expect(screenshot).toMatchSnapshot('loading-state.png', { threshold: VISUAL_THRESHOLD, }); }); }); test.describe('Visual Regression - Cross-browser', () => { test.beforeEach(async ({ page }) => { await setDesktopViewport(page); }); test('dashboard renders consistently across browsers', async ({ page, browserName }) => { await navigateTo(page, '/'); await waitForLoading(page); await page.waitForTimeout(1000); const screenshot = await page.screenshot({ fullPage: true }); expect(screenshot).toMatchSnapshot(`dashboard-${browserName}.png`, { threshold: VISUAL_THRESHOLD, }); }); test('scenarios list renders consistently across browsers', async ({ page, browserName }) => { await navigateTo(page, '/scenarios'); await waitForLoading(page); await page.waitForTimeout(1000); const screenshot = await page.screenshot({ fullPage: true }); expect(screenshot).toMatchSnapshot(`scenarios-${browserName}.png`, { threshold: VISUAL_THRESHOLD, }); }); }); // Helper to update baseline screenshots test.describe('Visual Regression - Baseline Management', () => { test.skip(process.env.UPDATE_BASELINE !== 'true', 'Only run when updating baselines'); test('update all baseline screenshots', async ({ page }) => { // This test runs only when UPDATE_BASELINE is set // It generates new baseline screenshots const pages = [ { path: '/', name: 'dashboard-desktop' }, { path: '/scenarios', name: 'scenarios-list' }, ]; for (const { path: pagePath, name } of pages) { await navigateTo(page, pagePath); await waitForLoading(page); await page.waitForTimeout(1000); const screenshot = await page.screenshot({ fullPage: true }); // Save as baseline const baselinePath = path.join(BASELINE_DIR, `${name}.png`); fs.writeFileSync(baselinePath, screenshot); } }); });