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,205 @@
/**
* E2E Test Utilities
*
* Shared utilities and helpers for E2E tests
*/
import { Page, expect, APIRequestContext } from '@playwright/test';
// Base URL for API calls
const API_BASE_URL = process.env.VITE_API_URL || 'http://localhost:8000/api/v1';
/**
* Navigate to a page and wait for it to be ready
*/
export async function navigateTo(page: Page, path: string) {
await page.goto(path);
await page.waitForLoadState('networkidle');
}
/**
* Wait for loading states to complete
*/
export async function waitForLoading(page: Page) {
// Wait for any loading text to disappear
const loadingElement = page.getByText('Loading...');
await expect(loadingElement).toHaveCount(0, { timeout: 30000 });
}
/**
* Wait for table to be populated
*/
export async function waitForTableData(page: Page, tableTestId?: string) {
const tableSelector = tableTestId
? `[data-testid="${tableTestId}"] tbody tr`
: 'table tbody tr';
// Wait for at least one row to be present
await page.waitForSelector(tableSelector, { timeout: 10000 });
}
/**
* Create a scenario via API
*/
export async function createScenarioViaAPI(
request: APIRequestContext,
scenario: {
name: string;
description?: string;
tags?: string[];
region: string;
}
) {
const response = await request.post(`${API_BASE_URL}/scenarios`, {
data: scenario,
});
expect(response.ok()).toBeTruthy();
return await response.json();
}
/**
* Delete a scenario via API
*/
export async function deleteScenarioViaAPI(
request: APIRequestContext,
scenarioId: string
) {
const response = await request.delete(`${API_BASE_URL}/scenarios/${scenarioId}`);
// Accept 204 (No Content) or 200 (OK) or 404 (already deleted)
expect([200, 204, 404]).toContain(response.status());
}
/**
* Start a scenario via API
*/
export async function startScenarioViaAPI(
request: APIRequestContext,
scenarioId: string
) {
const response = await request.post(`${API_BASE_URL}/scenarios/${scenarioId}/start`);
expect(response.ok()).toBeTruthy();
return await response.json();
}
/**
* Stop a scenario via API
*/
export async function stopScenarioViaAPI(
request: APIRequestContext,
scenarioId: string
) {
const response = await request.post(`${API_BASE_URL}/scenarios/${scenarioId}/stop`);
expect(response.ok()).toBeTruthy();
return await response.json();
}
/**
* Send test logs to a scenario
*/
export async function sendTestLogs(
request: APIRequestContext,
scenarioId: string,
logs: unknown[]
) {
const response = await request.post(
`${API_BASE_URL}/scenarios/${scenarioId}/ingest`,
{
data: { logs },
}
);
expect(response.ok()).toBeTruthy();
return await response.json();
}
/**
* Generate a unique test scenario name
*/
export function generateTestScenarioName(prefix = 'E2E Test'): string {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
return `${prefix} ${timestamp}`;
}
/**
* Wait for toast notification
*/
export async function waitForToast(page: Page, message?: string) {
const toastSelector = '[data-testid="toast"]'
+ (message ? `:has-text("${message}")` : '');
await page.waitForSelector(toastSelector, { timeout: 10000 });
}
/**
* Click on a navigation link by label
*/
export async function clickNavigation(page: Page, label: string) {
const navLink = page.locator('nav').getByRole('link', { name: label });
await navLink.click();
await page.waitForLoadState('networkidle');
}
/**
* Get scenario by name from the scenarios table
*/
export async function getScenarioRow(page: Page, scenarioName: string) {
return page.locator('table tbody tr').filter({ hasText: scenarioName });
}
/**
* Open scenario actions dropdown
*/
export async function openScenarioActions(page: Page, scenarioName: string) {
const row = await getScenarioRow(page, scenarioName);
const actionsButton = row.locator('button').first();
await actionsButton.click();
return row.locator('[role="menu"]');
}
/**
* Take a screenshot with a descriptive name
*/
export async function takeScreenshot(page: Page, name: string) {
await page.screenshot({
path: `e2e/screenshots/${name}.png`,
fullPage: true,
});
}
/**
* Check if element is visible with retry
*/
export async function isElementVisible(
page: Page,
selector: string,
timeout = 5000
): Promise<boolean> {
try {
await page.waitForSelector(selector, { timeout, state: 'visible' });
return true;
} catch {
return false;
}
}
/**
* Mobile viewport helper
*/
export async function setMobileViewport(page: Page) {
await page.setViewportSize({ width: 375, height: 667 });
}
/**
* Tablet viewport helper
*/
export async function setTabletViewport(page: Page) {
await page.setViewportSize({ width: 768, height: 1024 });
}
/**
* Desktop viewport helper
*/
export async function setDesktopViewport(page: Page) {
await page.setViewportSize({ width: 1280, height: 720 });
}