feat: implement v0.4.0 - Reports, Charts, Comparison, Dark Mode, E2E Testing
Some checks failed
E2E Tests / Run E2E Tests (push) Has been cancelled
E2E Tests / Visual Regression Tests (push) Has been cancelled
E2E Tests / Smoke Tests (push) Has been cancelled

Backend (@backend-dev):
- Add ReportService with PDF/CSV generation (reportlab, pandas)
- Implement Report API endpoints (POST, GET, DELETE, download)
- Add ReportRepository and schemas
- Configure storage with auto-cleanup (30 days)
- Rate limiting: 10 downloads/minute
- Professional PDF templates with charts support

Frontend (@frontend-dev):
- Integrate Recharts for data visualization
- Add CostBreakdown, TimeSeries, ComparisonBar charts
- Implement scenario comparison page with multi-select
- Add dark/light mode toggle with ThemeProvider
- Create Reports page with generation form and list
- Add new UI components: checkbox, dialog, tabs, label, skeleton
- Implement useComparison and useReports hooks

QA (@qa-engineer):
- Setup Playwright E2E testing framework
- Create 7 test spec files with 94 test cases
- Add visual regression testing with baselines
- Configure multi-browser testing (Chrome, Firefox, WebKit)
- Add mobile responsive tests
- Create test fixtures and helpers
- Setup GitHub Actions CI workflow

Documentation (@spec-architect):
- Create detailed kanban-v0.4.0.md with 27 tasks
- Update progress.md with v0.4.0 tracking
- Create v0.4.0 planning prompt

Features:
 PDF/CSV Report Generation
 Interactive Charts (Pie, Area, Bar)
 Scenario Comparison (2-4 scenarios)
 Dark/Light Mode Toggle
 E2E Test Suite (94 tests)

Dependencies added:
- Backend: reportlab, pandas, slowapi
- Frontend: recharts, date-fns, @radix-ui/react-checkbox/dialog/tabs
- Testing: @playwright/test

27 tasks completed, 100% v0.4.0 implementation
This commit is contained in:
Luca Sacchi Ricciardi
2026-04-07 16:11:47 +02:00
parent 311a576f40
commit a5fc85897b
63 changed files with 9218 additions and 246 deletions

View File

@@ -0,0 +1,386 @@
/**
* 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,
setMobileViewport,
setDesktopViewport,
} from './utils/test-helpers';
import { testLogs } from './fixtures/test-logs';
import { newScenarioData } from './fixtures/test-scenarios';
import path from 'path';
import fs from 'fs';
// 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);
}
});
});