Backend (@backend-dev): - ReportService with PDF/CSV generation (reportlab, pandas) - Report API endpoints (POST, GET, DELETE, download with rate limiting) - Professional PDF templates with branding and tables - Storage management with auto-cleanup Frontend (@frontend-dev): - Recharts integration: CostBreakdown, TimeSeries, ComparisonBar - Scenario comparison: multi-select, compare page with side-by-side layout - Reports UI: generation form, list with status badges, download - Dark/Light mode: ThemeProvider, toggle, CSS variables - Responsive design for all components QA (@qa-engineer): - E2E testing setup with Playwright - 100 test cases across 7 spec files - Visual regression baselines - CI/CD workflow configuration - ES modules fixes Documentation: - Add todo.md with testing checklist and future roadmap - Update kickoff prompt for v0.4.0 27 tasks completed, 100% v0.4.0 delivery Closes: v0.4.0 milestone
391 lines
11 KiB
TypeScript
391 lines
11 KiB
TypeScript
/**
|
|
* 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);
|
|
}
|
|
});
|
|
});
|