1152 lines
42 KiB
TypeScript
1152 lines
42 KiB
TypeScript
/**
|
|
* Dashboard Feature Flow Tests
|
|
*
|
|
* These tests verify routing, guards, navigation, cross-screen state, and user flows
|
|
* for the dashboard module. They run with real frontend and mocked contracts.
|
|
*
|
|
* Contracts are defined in apps/website/lib/types/generated
|
|
*
|
|
* @file apps/website/tests/flows/dashboard.test.ts
|
|
*/
|
|
|
|
import { test, expect } from '@playwright/test';
|
|
import { WebsiteAuthManager } from '../../../tests/shared/website/WebsiteAuthManager';
|
|
import { WebsiteRouteManager } from '../../../tests/shared/website/WebsiteRouteManager';
|
|
import { ConsoleErrorCapture } from '../../../tests/shared/website/ConsoleErrorCapture';
|
|
import { HttpDiagnostics } from '../../../tests/shared/website/HttpDiagnostics';
|
|
import { RouteContractSpec } from '../../../tests/shared/website/RouteContractSpec';
|
|
import { RouteScenarioMatrix } from '../../../tests/shared/website/RouteScenarioMatrix';
|
|
|
|
test.describe('Dashboard Feature Flow', () => {
|
|
describe('Dashboard Navigation', () => {
|
|
test('should redirect to login when accessing dashboard without authentication', async ({ page }) => {
|
|
const routeManager = new WebsiteRouteManager(page);
|
|
|
|
// Navigate to /dashboard without authentication
|
|
await page.goto(routeManager.getRoute('/dashboard'));
|
|
|
|
// Verify redirect to login page
|
|
await expect(page).toHaveURL(/.*\/auth\/login/);
|
|
|
|
// Check return URL parameter
|
|
const url = new URL(page.url());
|
|
expect(url.searchParams.get('returnUrl')).toBe('/dashboard');
|
|
});
|
|
|
|
test('should allow access to dashboard with valid authentication', async ({ page }) => {
|
|
const routeManager = new WebsiteRouteManager(page);
|
|
const authManager = new WebsiteAuthManager(page);
|
|
const routeContractSpec = new RouteContractSpec(page);
|
|
|
|
// Mock AuthSessionDTO
|
|
const mockAuthSession = {
|
|
token: 'test-token-123',
|
|
user: {
|
|
userId: 'user-123',
|
|
email: 'test@example.com',
|
|
displayName: 'Test User',
|
|
role: 'user',
|
|
},
|
|
};
|
|
|
|
await routeContractSpec.mockApiCall('Login', mockAuthSession);
|
|
|
|
// Login as user
|
|
await authManager.loginAsUser();
|
|
|
|
// Navigate to /dashboard
|
|
await page.goto(routeManager.getRoute('/dashboard'));
|
|
|
|
// Verify dashboard loads successfully
|
|
await expect(page).toHaveURL(/.*\/dashboard/);
|
|
|
|
// Check for expected dashboard elements
|
|
await expect(page.locator('[data-testid="dashboard"]')).toBeVisible();
|
|
await expect(page.locator('[data-testid="dashboard-header"]')).toBeVisible();
|
|
});
|
|
|
|
test('should navigate from dashboard to races page', async ({ page }) => {
|
|
const routeManager = new WebsiteRouteManager(page);
|
|
const authManager = new WebsiteAuthManager(page);
|
|
const routeContractSpec = new RouteContractSpec(page);
|
|
|
|
// Mock dashboard data
|
|
const mockDashboardData = {
|
|
totalUsers: 150,
|
|
activeUsers: 120,
|
|
totalRaces: 10,
|
|
totalLeagues: 5,
|
|
};
|
|
|
|
await routeContractSpec.mockApiCall('GetDashboardData', mockDashboardData);
|
|
|
|
// Login and navigate to /dashboard
|
|
await authManager.loginAsUser();
|
|
await page.goto(routeManager.getRoute('/dashboard'));
|
|
|
|
// Click "View Full Schedule" button
|
|
await page.locator('[data-testid="view-schedule-button"]').click();
|
|
|
|
// Verify navigation to /races
|
|
await expect(page).toHaveURL(/.*\/races/);
|
|
});
|
|
|
|
test('should handle direct navigation to dashboard routes', async ({ page }) => {
|
|
const routeManager = new WebsiteRouteManager(page);
|
|
const authManager = new WebsiteAuthManager(page);
|
|
const routeContractSpec = new RouteContractSpec(page);
|
|
|
|
// Mock dashboard data
|
|
const mockDashboardData = {
|
|
totalUsers: 150,
|
|
activeUsers: 120,
|
|
totalRaces: 10,
|
|
totalLeagues: 5,
|
|
};
|
|
|
|
await routeContractSpec.mockApiCall('GetDashboardData', mockDashboardData);
|
|
|
|
// Login
|
|
await authManager.loginAsUser();
|
|
|
|
// Attempt direct navigation to /dashboard
|
|
await page.goto(routeManager.getRoute('/dashboard'));
|
|
|
|
// Verify dashboard renders correctly
|
|
await expect(page).toHaveURL(/.*\/dashboard/);
|
|
await expect(page.locator('[data-testid="dashboard"]')).toBeVisible();
|
|
|
|
// Check URL remains /dashboard
|
|
await expect(page).toHaveURL(/.*\/dashboard/);
|
|
});
|
|
});
|
|
|
|
describe('Dashboard Data Flow', () => {
|
|
test('should load and display dashboard overview data', async ({ page }) => {
|
|
const routeManager = new WebsiteRouteManager(page);
|
|
const authManager = new WebsiteAuthManager(page);
|
|
const routeContractSpec = new RouteContractSpec(page);
|
|
|
|
// Mock DashboardPageQuery response with DashboardOverviewDTO
|
|
const mockDashboardData = {
|
|
totalUsers: 150,
|
|
activeUsers: 120,
|
|
totalRaces: 10,
|
|
totalLeagues: 5,
|
|
};
|
|
|
|
await routeContractSpec.mockApiCall('GetDashboardData', mockDashboardData);
|
|
|
|
// Login and navigate to /dashboard
|
|
await authManager.loginAsUser();
|
|
await page.goto(routeManager.getRoute('/dashboard'));
|
|
|
|
// Verify dashboard data is displayed
|
|
await expect(page.locator('[data-testid="total-users"]')).toContainText('150');
|
|
await expect(page.locator('[data-testid="active-users"]')).toContainText('120');
|
|
await expect(page.locator('[data-testid="total-races"]')).toContainText('10');
|
|
await expect(page.locator('[data-testid="total-leagues"]')).toContainText('5');
|
|
});
|
|
|
|
test('should display next race information when available', async ({ page }) => {
|
|
const routeManager = new WebsiteRouteManager(page);
|
|
const authManager = new WebsiteAuthManager(page);
|
|
const routeContractSpec = new RouteContractSpec(page);
|
|
|
|
// Mock DashboardOverviewDTO with nextRace
|
|
const mockDashboardData = {
|
|
totalUsers: 150,
|
|
activeUsers: 120,
|
|
totalRaces: 10,
|
|
totalLeagues: 5,
|
|
nextRace: {
|
|
id: 'race-123',
|
|
track: 'Monza',
|
|
car: 'Ferrari 488 GT3',
|
|
scheduledAt: '2024-01-25T14:00:00Z',
|
|
status: 'scheduled',
|
|
isMyLeague: true,
|
|
},
|
|
};
|
|
|
|
await routeContractSpec.mockApiCall('GetDashboardData', mockDashboardData);
|
|
|
|
// Login and navigate to /dashboard
|
|
await authManager.loginAsUser();
|
|
await page.goto(routeManager.getRoute('/dashboard'));
|
|
|
|
// Verify next race details are shown
|
|
await expect(page.locator('[data-testid="next-race-track"]')).toContainText('Monza');
|
|
await expect(page.locator('[data-testid="next-race-car"]')).toContainText('Ferrari 488 GT3');
|
|
await expect(page.locator('[data-testid="next-race-time"]')).toBeVisible();
|
|
|
|
// Check for "Active Session" panel
|
|
await expect(page.locator('[data-testid="active-session-panel"]')).toBeVisible();
|
|
});
|
|
|
|
test('should handle missing next race gracefully', async ({ page }) => {
|
|
const routeManager = new WebsiteRouteManager(page);
|
|
const authManager = new WebsiteAuthManager(page);
|
|
const routeContractSpec = new RouteContractSpec(page);
|
|
|
|
// Mock DashboardOverviewDTO without nextRace
|
|
const mockDashboardData = {
|
|
totalUsers: 150,
|
|
activeUsers: 120,
|
|
totalRaces: 10,
|
|
totalLeagues: 5,
|
|
};
|
|
|
|
await routeContractSpec.mockApiCall('GetDashboardData', mockDashboardData);
|
|
|
|
// Login and navigate to /dashboard
|
|
await authManager.loginAsUser();
|
|
await page.goto(routeManager.getRoute('/dashboard'));
|
|
|
|
// Verify "Active Session" panel is not displayed
|
|
await expect(page.locator('[data-testid="active-session-panel"]')).not.toBeVisible();
|
|
|
|
// Check UI remains functional
|
|
await expect(page.locator('[data-testid="dashboard"]')).toBeVisible();
|
|
await expect(page.locator('[data-testid="view-schedule-button"]')).toBeEnabled();
|
|
});
|
|
|
|
test('should display upcoming races schedule', async ({ page }) => {
|
|
const routeManager = new WebsiteRouteManager(page);
|
|
const authManager = new WebsiteAuthManager(page);
|
|
const routeContractSpec = new RouteContractSpec(page);
|
|
|
|
// Mock DashboardOverviewDTO with upcomingRaces
|
|
const mockDashboardData = {
|
|
totalUsers: 150,
|
|
activeUsers: 120,
|
|
totalRaces: 10,
|
|
totalLeagues: 5,
|
|
upcomingRaces: [
|
|
{
|
|
id: 'race-123',
|
|
track: 'Monza',
|
|
car: 'Ferrari 488 GT3',
|
|
scheduledAt: '2024-01-25T14:00:00Z',
|
|
status: 'scheduled',
|
|
isMyLeague: true,
|
|
},
|
|
{
|
|
id: 'race-124',
|
|
track: 'Spa',
|
|
car: 'Porsche 911 GT3 R',
|
|
scheduledAt: '2024-01-26T15:00:00Z',
|
|
status: 'scheduled',
|
|
isMyLeague: true,
|
|
},
|
|
],
|
|
};
|
|
|
|
await routeContractSpec.mockApiCall('GetDashboardData', mockDashboardData);
|
|
|
|
// Login and navigate to /dashboard
|
|
await authManager.loginAsUser();
|
|
await page.goto(routeManager.getRoute('/dashboard'));
|
|
|
|
// Verify upcoming races are listed in "Upcoming Schedule"
|
|
await expect(page.locator('[data-testid="upcoming-race-123"]')).toBeVisible();
|
|
await expect(page.locator('[data-testid="upcoming-race-124"]')).toBeVisible();
|
|
|
|
// Check for track, car, timeUntil, and formattedDate for each race
|
|
await expect(page.locator('[data-testid="upcoming-race-123-track"]')).toContainText('Monza');
|
|
await expect(page.locator('[data-testid="upcoming-race-123-car"]')).toContainText('Ferrari 488 GT3');
|
|
await expect(page.locator('[data-testid="upcoming-race-123-time"]')).toBeVisible();
|
|
});
|
|
|
|
test('should handle empty upcoming races list', async ({ page }) => {
|
|
const routeManager = new WebsiteRouteManager(page);
|
|
const authManager = new WebsiteAuthManager(page);
|
|
const routeContractSpec = new RouteContractSpec(page);
|
|
|
|
// Mock DashboardOverviewDTO with empty upcomingRaces
|
|
const mockDashboardData = {
|
|
totalUsers: 150,
|
|
activeUsers: 120,
|
|
totalRaces: 10,
|
|
totalLeagues: 5,
|
|
upcomingRaces: [],
|
|
};
|
|
|
|
await routeContractSpec.mockApiCall('GetDashboardData', mockDashboardData);
|
|
|
|
// Login and navigate to /dashboard
|
|
await authManager.loginAsUser();
|
|
await page.goto(routeManager.getRoute('/dashboard'));
|
|
|
|
// Verify "Upcoming Schedule" shows appropriate empty state
|
|
await expect(page.locator('[data-testid="upcoming-schedule-empty"]')).toBeVisible();
|
|
await expect(page.locator('[data-testid="upcoming-schedule-empty"]')).toContainText(/no upcoming races/i);
|
|
});
|
|
|
|
test('should display league standings', async ({ page }) => {
|
|
const routeManager = new WebsiteRouteManager(page);
|
|
const authManager = new WebsiteAuthManager(page);
|
|
const routeContractSpec = new RouteContractSpec(page);
|
|
|
|
// Mock DashboardOverviewDTO with leagueStandingsSummaries
|
|
const mockDashboardData = {
|
|
totalUsers: 150,
|
|
activeUsers: 120,
|
|
totalRaces: 10,
|
|
totalLeagues: 5,
|
|
leagueStandingsSummaries: [
|
|
{
|
|
leagueId: 'league-1',
|
|
leagueName: 'Pro League',
|
|
position: 3,
|
|
totalDrivers: 50,
|
|
points: 1250,
|
|
},
|
|
{
|
|
leagueId: 'league-2',
|
|
leagueName: 'Amateur League',
|
|
position: 1,
|
|
totalDrivers: 30,
|
|
points: 850,
|
|
},
|
|
],
|
|
};
|
|
|
|
await routeContractSpec.mockApiCall('GetDashboardData', mockDashboardData);
|
|
|
|
// Login and navigate to /dashboard
|
|
await authManager.loginAsUser();
|
|
await page.goto(routeManager.getRoute('/dashboard'));
|
|
|
|
// Verify "Championship Standings" panel shows league data
|
|
await expect(page.locator('[data-testid="league-standing-league-1"]')).toBeVisible();
|
|
await expect(page.locator('[data-testid="league-standing-league-2"]')).toBeVisible();
|
|
|
|
// Check for leagueName, position, totalDrivers, points
|
|
await expect(page.locator('[data-testid="league-name-league-1"]')).toContainText('Pro League');
|
|
await expect(page.locator('[data-testid="league-position-league-1"]')).toContainText('3');
|
|
await expect(page.locator('[data-testid="league-drivers-league-1"]')).toContainText('50');
|
|
await expect(page.locator('[data-testid="league-points-league-1"]')).toContainText('1250');
|
|
});
|
|
|
|
test('should handle empty league standings', async ({ page }) => {
|
|
const routeManager = new WebsiteRouteManager(page);
|
|
const authManager = new WebsiteAuthManager(page);
|
|
const routeContractSpec = new RouteContractSpec(page);
|
|
|
|
// Mock DashboardOverviewDTO with empty leagueStandingsSummaries
|
|
const mockDashboardData = {
|
|
totalUsers: 150,
|
|
activeUsers: 120,
|
|
totalRaces: 10,
|
|
totalLeagues: 5,
|
|
leagueStandingsSummaries: [],
|
|
};
|
|
|
|
await routeContractSpec.mockApiCall('GetDashboardData', mockDashboardData);
|
|
|
|
// Login and navigate to /dashboard
|
|
await authManager.loginAsUser();
|
|
await page.goto(routeManager.getRoute('/dashboard'));
|
|
|
|
// Verify "Championship Standings" shows empty state message
|
|
await expect(page.locator('[data-testid="championship-standings-empty"]')).toBeVisible();
|
|
await expect(page.locator('[data-testid="championship-standings-empty"]')).toContainText(/no league standings/i);
|
|
});
|
|
|
|
test('should display recent activity feed', async ({ page }) => {
|
|
const routeManager = new WebsiteRouteManager(page);
|
|
const authManager = new WebsiteAuthManager(page);
|
|
const routeContractSpec = new RouteContractSpec(page);
|
|
|
|
// Mock DashboardOverviewDTO with feedSummary containing items
|
|
const mockDashboardData = {
|
|
totalUsers: 150,
|
|
activeUsers: 120,
|
|
totalRaces: 10,
|
|
totalLeagues: 5,
|
|
feedSummary: {
|
|
notificationCount: 2,
|
|
items: [
|
|
{
|
|
id: 'feed-1',
|
|
type: 'race_result',
|
|
headline: 'Race completed at Monza',
|
|
body: 'You finished 3rd in the Pro League race',
|
|
timestamp: '2024-01-20T10:00:00Z',
|
|
ctaLabel: 'View Results',
|
|
ctaHref: '/races/race-123',
|
|
},
|
|
{
|
|
id: 'feed-2',
|
|
type: 'league_update',
|
|
headline: 'New league season started',
|
|
body: 'The 2024 season is now live',
|
|
timestamp: '2024-01-19T15:00:00Z',
|
|
ctaLabel: 'View League',
|
|
ctaHref: '/leagues/league-1',
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
await routeContractSpec.mockApiCall('GetDashboardData', mockDashboardData);
|
|
|
|
// Login and navigate to /dashboard
|
|
await authManager.loginAsUser();
|
|
await page.goto(routeManager.getRoute('/dashboard'));
|
|
|
|
// Verify "Recent Activity" panel shows feed items
|
|
await expect(page.locator('[data-testid="feed-item-feed-1"]')).toBeVisible();
|
|
await expect(page.locator('[data-testid="feed-item-feed-2"]')).toBeVisible();
|
|
|
|
// Check for type, headline, formattedTime
|
|
await expect(page.locator('[data-testid="feed-headline-feed-1"]')).toContainText('Race completed at Monza');
|
|
await expect(page.locator('[data-testid="feed-time-feed-1"]')).toBeVisible();
|
|
});
|
|
|
|
test('should handle empty activity feed', async ({ page }) => {
|
|
const routeManager = new WebsiteRouteManager(page);
|
|
const authManager = new WebsiteAuthManager(page);
|
|
const routeContractSpec = new RouteContractSpec(page);
|
|
|
|
// Mock DashboardOverviewDTO with empty feedSummary
|
|
const mockDashboardData = {
|
|
totalUsers: 150,
|
|
activeUsers: 120,
|
|
totalRaces: 10,
|
|
totalLeagues: 5,
|
|
feedSummary: {
|
|
notificationCount: 0,
|
|
items: [],
|
|
},
|
|
};
|
|
|
|
await routeContractSpec.mockApiCall('GetDashboardData', mockDashboardData);
|
|
|
|
// Login and navigate to /dashboard
|
|
await authManager.loginAsUser();
|
|
await page.goto(routeManager.getRoute('/dashboard'));
|
|
|
|
// Verify "Recent Activity" shows empty state message
|
|
await expect(page.locator('[data-testid="recent-activity-empty"]')).toBeVisible();
|
|
await expect(page.locator('[data-testid="recent-activity-empty"]')).toContainText(/no recent activity/i);
|
|
});
|
|
|
|
test('should handle dashboard data loading errors', async ({ page }) => {
|
|
const routeManager = new WebsiteRouteManager(page);
|
|
const authManager = new WebsiteAuthManager(page);
|
|
const routeContractSpec = new RouteContractSpec(page);
|
|
const consoleErrorCapture = new ConsoleErrorCapture(page);
|
|
|
|
// Login as user
|
|
await authManager.loginAsUser();
|
|
|
|
// Mock DashboardPageQuery to return error
|
|
await routeContractSpec.mockApiCall('GetDashboardData', {
|
|
error: 'Internal Server Error',
|
|
status: 500,
|
|
});
|
|
|
|
// Navigate to /dashboard
|
|
await page.goto(routeManager.getRoute('/dashboard'));
|
|
|
|
// Verify error handling (likely redirects to notFound)
|
|
await expect(page).toHaveURL(/.*\/not-found/);
|
|
|
|
// Check error logging
|
|
const errors = consoleErrorCapture.getErrors();
|
|
expect(errors.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test('should handle dashboard access denied (403/401)', async ({ page }) => {
|
|
const routeManager = new WebsiteRouteManager(page);
|
|
const authManager = new WebsiteAuthManager(page);
|
|
const routeContractSpec = new RouteContractSpec(page);
|
|
|
|
// Login as user
|
|
await authManager.loginAsUser();
|
|
|
|
// Mock API to return 403 error
|
|
await routeContractSpec.mockApiCall('GetDashboardData', {
|
|
error: 'Access Denied',
|
|
status: 403,
|
|
message: 'You do not have permission to access this resource',
|
|
});
|
|
|
|
// Navigate to /dashboard
|
|
await page.goto(routeManager.getRoute('/dashboard'));
|
|
|
|
// Verify redirect to login or error page
|
|
await expect(page).toHaveURL(/.*\/auth\/login/);
|
|
});
|
|
|
|
test('should refresh dashboard data on page refresh', async ({ page }) => {
|
|
const routeManager = new WebsiteRouteManager(page);
|
|
const authManager = new WebsiteAuthManager(page);
|
|
const routeContractSpec = new RouteContractSpec(page);
|
|
|
|
// Mock initial dashboard data
|
|
const mockDashboardData = {
|
|
totalUsers: 150,
|
|
activeUsers: 120,
|
|
totalRaces: 10,
|
|
totalLeagues: 5,
|
|
};
|
|
|
|
await routeContractSpec.mockApiCall('GetDashboardData', mockDashboardData);
|
|
|
|
// Login and navigate to /dashboard
|
|
await authManager.loginAsUser();
|
|
await page.goto(routeManager.getRoute('/dashboard'));
|
|
|
|
// Mock refreshed data
|
|
const refreshedData = {
|
|
totalUsers: 155,
|
|
activeUsers: 125,
|
|
totalRaces: 12,
|
|
totalLeagues: 6,
|
|
};
|
|
|
|
await routeContractSpec.mockApiCall('GetDashboardData', refreshedData);
|
|
|
|
// Trigger browser refresh
|
|
await page.reload();
|
|
|
|
// Verify DashboardPageQuery is called again
|
|
// Verify data is reloaded
|
|
await expect(page.locator('[data-testid="total-users"]')).toContainText('155');
|
|
await expect(page.locator('[data-testid="active-users"]')).toContainText('125');
|
|
});
|
|
});
|
|
|
|
describe('Dashboard KPI Display', () => {
|
|
test('should display all KPI items correctly', async ({ page }) => {
|
|
const routeManager = new WebsiteRouteManager(page);
|
|
const authManager = new WebsiteAuthManager(page);
|
|
const routeContractSpec = new RouteContractSpec(page);
|
|
|
|
// Mock DashboardOverviewDTO with driver stats
|
|
const mockDashboardData = {
|
|
totalUsers: 150,
|
|
activeUsers: 120,
|
|
totalRaces: 10,
|
|
totalLeagues: 5,
|
|
currentDriver: {
|
|
id: 'driver-123',
|
|
name: 'Test Driver',
|
|
country: 'USA',
|
|
rating: 1850,
|
|
globalRank: 42,
|
|
totalRaces: 150,
|
|
wins: 25,
|
|
podiums: 60,
|
|
},
|
|
};
|
|
|
|
await routeContractSpec.mockApiCall('GetDashboardData', mockDashboardData);
|
|
|
|
// Login and navigate to /dashboard
|
|
await authManager.loginAsUser();
|
|
await page.goto(routeManager.getRoute('/dashboard'));
|
|
|
|
// Verify KPI row shows: Rating, Rank, Starts, Wins, Podiums, Leagues
|
|
await expect(page.locator('[data-testid="kpi-rating"]')).toContainText('1850');
|
|
await expect(page.locator('[data-testid="kpi-rank"]')).toContainText('42');
|
|
await expect(page.locator('[data-testid="kpi-starts"]')).toContainText('150');
|
|
await expect(page.locator('[data-testid="kpi-wins"]')).toContainText('25');
|
|
await expect(page.locator('[data-testid="kpi-podiums"]')).toContainText('60');
|
|
await expect(page.locator('[data-testid="kpi-leagues"]')).toContainText('5');
|
|
|
|
// Check proper formatting and styling
|
|
await expect(page.locator('[data-testid="kpi-rating"]')).toHaveClass(/intent-primary/);
|
|
await expect(page.locator('[data-testid="kpi-rank"]')).toHaveClass(/intent-warning/);
|
|
});
|
|
|
|
test('should handle missing driver data gracefully', async ({ page }) => {
|
|
const routeManager = new WebsiteRouteManager(page);
|
|
const authManager = new WebsiteAuthManager(page);
|
|
const routeContractSpec = new RouteContractSpec(page);
|
|
|
|
// Mock DashboardOverviewDTO without currentDriver
|
|
const mockDashboardData = {
|
|
totalUsers: 150,
|
|
activeUsers: 120,
|
|
totalRaces: 10,
|
|
totalLeagues: 5,
|
|
};
|
|
|
|
await routeContractSpec.mockApiCall('GetDashboardData', mockDashboardData);
|
|
|
|
// Login and navigate to /dashboard
|
|
await authManager.loginAsUser();
|
|
await page.goto(routeManager.getRoute('/dashboard'));
|
|
|
|
// Verify KPI row handles missing data
|
|
await expect(page.locator('[data-testid="kpi-rating"]')).toContainText('-');
|
|
await expect(page.locator('[data-testid="kpi-rank"]')).toContainText('-');
|
|
await expect(page.locator('[data-testid="kpi-starts"]')).toContainText('-');
|
|
await expect(page.locator('[data-testid="kpi-wins"]')).toContainText('-');
|
|
await expect(page.locator('[data-testid="kpi-podiums"]')).toContainText('-');
|
|
|
|
// Check UI doesn't break
|
|
await expect(page.locator('[data-testid="dashboard"]')).toBeVisible();
|
|
await expect(page.locator('[data-testid="view-schedule-button"]')).toBeEnabled();
|
|
});
|
|
|
|
test('should apply correct intent styling to KPI items', async ({ page }) => {
|
|
const routeManager = new WebsiteRouteManager(page);
|
|
const authManager = new WebsiteAuthManager(page);
|
|
const routeContractSpec = new RouteContractSpec(page);
|
|
|
|
// Mock DashboardOverviewDTO with driver stats
|
|
const mockDashboardData = {
|
|
totalUsers: 150,
|
|
activeUsers: 120,
|
|
totalRaces: 10,
|
|
totalLeagues: 5,
|
|
currentDriver: {
|
|
id: 'driver-123',
|
|
name: 'Test Driver',
|
|
country: 'USA',
|
|
rating: 1850,
|
|
globalRank: 42,
|
|
totalRaces: 150,
|
|
wins: 25,
|
|
podiums: 60,
|
|
},
|
|
};
|
|
|
|
await routeContractSpec.mockApiCall('GetDashboardData', mockDashboardData);
|
|
|
|
// Login and navigate to /dashboard
|
|
await authManager.loginAsUser();
|
|
await page.goto(routeManager.getRoute('/dashboard'));
|
|
|
|
// Verify Rating has primary intent
|
|
await expect(page.locator('[data-testid="kpi-rating"]')).toHaveClass(/intent-primary/);
|
|
|
|
// Verify Rank has warning intent
|
|
await expect(page.locator('[data-testid="kpi-rank"]')).toHaveClass(/intent-warning/);
|
|
|
|
// Verify Wins has success intent
|
|
await expect(page.locator('[data-testid="kpi-wins"]')).toHaveClass(/intent-success/);
|
|
|
|
// Verify Podiums has warning intent
|
|
await expect(page.locator('[data-testid="kpi-podiums"]')).toHaveClass(/intent-warning/);
|
|
});
|
|
});
|
|
|
|
describe('Dashboard Route Guard Integration', () => {
|
|
test('should enforce authentication on dashboard access', async ({ page }) => {
|
|
const routeManager = new WebsiteRouteManager(page);
|
|
|
|
// Navigate to /dashboard without auth
|
|
await page.goto(routeManager.getRoute('/dashboard'));
|
|
|
|
// Verify redirect to /auth/login
|
|
await expect(page).toHaveURL(/.*\/auth\/login/);
|
|
|
|
// Check return URL includes /dashboard
|
|
const url = new URL(page.url());
|
|
expect(url.searchParams.get('returnUrl')).toBe('/dashboard');
|
|
});
|
|
|
|
test('should handle session expiration during dashboard viewing', async ({ page }) => {
|
|
const routeManager = new WebsiteRouteManager(page);
|
|
const authManager = new WebsiteAuthManager(page);
|
|
const routeContractSpec = new RouteContractSpec(page);
|
|
|
|
// Login and navigate to /dashboard
|
|
await authManager.loginAsUser();
|
|
await page.goto(routeManager.getRoute('/dashboard'));
|
|
|
|
// Mock session expiration
|
|
await routeContractSpec.mockApiCall('GetDashboardData', {
|
|
error: 'Unauthorized',
|
|
status: 401,
|
|
message: 'Session expired',
|
|
});
|
|
|
|
// Attempt interaction (e.g., click "View Full Schedule")
|
|
await page.locator('[data-testid="view-schedule-button"]').click();
|
|
|
|
// Verify redirect to login
|
|
await expect(page).toHaveURL(/.*\/auth\/login/);
|
|
});
|
|
|
|
test('should maintain return URL after dashboard authentication', async ({ page }) => {
|
|
const routeManager = new WebsiteRouteManager(page);
|
|
const authManager = new WebsiteAuthManager(page);
|
|
const routeContractSpec = new RouteContractSpec(page);
|
|
|
|
// Attempt to access /dashboard without auth
|
|
await page.goto(routeManager.getRoute('/dashboard'));
|
|
|
|
// Verify redirect to login with return URL
|
|
await expect(page).toHaveURL(/.*\/auth\/login/);
|
|
const url = new URL(page.url());
|
|
expect(url.searchParams.get('returnUrl')).toBe('/dashboard');
|
|
|
|
// Mock dashboard data for after login
|
|
const mockDashboardData = {
|
|
totalUsers: 150,
|
|
activeUsers: 120,
|
|
totalRaces: 10,
|
|
totalLeagues: 5,
|
|
};
|
|
|
|
await routeContractSpec.mockApiCall('GetDashboardData', mockDashboardData);
|
|
|
|
// Login successfully
|
|
await authManager.loginAsUser();
|
|
|
|
// Verify redirect back to /dashboard
|
|
await expect(page).toHaveURL(/.*\/dashboard/);
|
|
});
|
|
|
|
test('should redirect authenticated users away from auth pages', async ({ page }) => {
|
|
const routeManager = new WebsiteRouteManager(page);
|
|
const authManager = new WebsiteAuthManager(page);
|
|
|
|
// Mock existing AuthSessionDTO
|
|
await authManager.loginAsUser();
|
|
|
|
// Navigate to /auth/login
|
|
await page.goto(routeManager.getRoute('/auth/login'));
|
|
|
|
// Verify redirect to /dashboard
|
|
await expect(page).toHaveURL(/.*\/dashboard/);
|
|
});
|
|
});
|
|
|
|
describe('Dashboard Cross-Screen State Management', () => {
|
|
test('should preserve dashboard state when navigating away and back', async ({ page }) => {
|
|
const routeManager = new WebsiteRouteManager(page);
|
|
const authManager = new WebsiteAuthManager(page);
|
|
const routeContractSpec = new RouteContractSpec(page);
|
|
|
|
// Mock dashboard data
|
|
const mockDashboardData = {
|
|
totalUsers: 150,
|
|
activeUsers: 120,
|
|
totalRaces: 10,
|
|
totalLeagues: 5,
|
|
};
|
|
|
|
await routeContractSpec.mockApiCall('GetDashboardData', mockDashboardData);
|
|
|
|
// Login and navigate to /dashboard
|
|
await authManager.loginAsUser();
|
|
await page.goto(routeManager.getRoute('/dashboard'));
|
|
|
|
// Navigate to another page (e.g., /races)
|
|
await page.goto(routeManager.getRoute('/races'));
|
|
|
|
// Navigate back to /dashboard
|
|
await page.goto(routeManager.getRoute('/dashboard'));
|
|
|
|
// Verify data is preserved or reloaded correctly
|
|
await expect(page.locator('[data-testid="total-users"]')).toContainText('150');
|
|
await expect(page.locator('[data-testid="dashboard"]')).toBeVisible();
|
|
});
|
|
|
|
test('should handle concurrent dashboard operations', async ({ page }) => {
|
|
const routeManager = new WebsiteRouteManager(page);
|
|
const authManager = new WebsiteAuthManager(page);
|
|
const routeContractSpec = new RouteContractSpec(page);
|
|
|
|
// Mock dashboard data
|
|
const mockDashboardData = {
|
|
totalUsers: 150,
|
|
activeUsers: 120,
|
|
totalRaces: 10,
|
|
totalLeagues: 5,
|
|
};
|
|
|
|
await routeContractSpec.mockApiCall('GetDashboardData', mockDashboardData);
|
|
|
|
// Login and navigate to /dashboard
|
|
await authManager.loginAsUser();
|
|
await page.goto(routeManager.getRoute('/dashboard'));
|
|
|
|
// Mock refreshed data
|
|
const refreshedData = {
|
|
totalUsers: 155,
|
|
activeUsers: 125,
|
|
totalRaces: 12,
|
|
totalLeagues: 6,
|
|
};
|
|
|
|
await routeContractSpec.mockApiCall('GetDashboardData', refreshedData);
|
|
|
|
// Trigger multiple operations quickly
|
|
const refreshPromise = page.locator('[data-testid="refresh-button"]').click();
|
|
const navigatePromise = page.goto(routeManager.getRoute('/races'));
|
|
|
|
// Wait for all operations
|
|
await Promise.all([refreshPromise, navigatePromise]);
|
|
|
|
// Verify loading states are managed (no stuck spinners)
|
|
await expect(page.locator('[data-testid="loading-spinner"]')).not.toBeVisible();
|
|
|
|
// Verify UI remains usable after concurrent operations
|
|
await expect(page.locator('[data-testid="navigation-menu"]')).toBeVisible();
|
|
});
|
|
|
|
test('should maintain dashboard scroll position on return', async ({ page }) => {
|
|
const routeManager = new WebsiteRouteManager(page);
|
|
const authManager = new WebsiteAuthManager(page);
|
|
const routeContractSpec = new RouteContractSpec(page);
|
|
|
|
// Mock dashboard data with many items to enable scrolling
|
|
const mockDashboardData = {
|
|
totalUsers: 150,
|
|
activeUsers: 120,
|
|
totalRaces: 10,
|
|
totalLeagues: 5,
|
|
upcomingRaces: Array(10).fill(null).map((_, i) => ({
|
|
id: `race-${i}`,
|
|
track: `Track ${i}`,
|
|
car: `Car ${i}`,
|
|
scheduledAt: '2024-01-25T14:00:00Z',
|
|
status: 'scheduled',
|
|
isMyLeague: true,
|
|
})),
|
|
};
|
|
|
|
await routeContractSpec.mockApiCall('GetDashboardData', mockDashboardData);
|
|
|
|
// Login and navigate to /dashboard
|
|
await authManager.loginAsUser();
|
|
await page.goto(routeManager.getRoute('/dashboard'));
|
|
|
|
// Scroll down
|
|
await page.evaluate(() => window.scrollTo(0, 500));
|
|
|
|
// Navigate to /races
|
|
await page.goto(routeManager.getRoute('/races'));
|
|
|
|
// Navigate back to /dashboard
|
|
await page.goto(routeManager.getRoute('/dashboard'));
|
|
|
|
// Verify scroll position is preserved (or at least the page is functional)
|
|
await expect(page.locator('[data-testid="dashboard"]')).toBeVisible();
|
|
// Note: Exact scroll position preservation may depend on browser implementation
|
|
});
|
|
});
|
|
|
|
describe('Dashboard UI State Management', () => {
|
|
test('should show loading states during data operations', async ({ page }) => {
|
|
const routeManager = new WebsiteRouteManager(page);
|
|
const authManager = new WebsiteAuthManager(page);
|
|
const routeContractSpec = new RouteContractSpec(page);
|
|
|
|
// Mock delayed DashboardPageQuery response
|
|
const mockDashboardData = {
|
|
totalUsers: 150,
|
|
activeUsers: 120,
|
|
totalRaces: 10,
|
|
totalLeagues: 5,
|
|
};
|
|
|
|
await routeContractSpec.mockApiCall('GetDashboardData', mockDashboardData, { delay: 500 });
|
|
|
|
// Navigate to /dashboard
|
|
await authManager.loginAsUser();
|
|
await page.goto(routeManager.getRoute('/dashboard'));
|
|
|
|
// Verify loading state is shown
|
|
await expect(page.locator('[data-testid="loading-spinner"]')).toBeVisible();
|
|
|
|
// Wait for loading to complete
|
|
await expect(page.locator('[data-testid="loading-spinner"]')).not.toBeVisible();
|
|
|
|
// Verify data is displayed after loading
|
|
await expect(page.locator('[data-testid="total-users"]')).toContainText('150');
|
|
});
|
|
|
|
test('should handle empty states gracefully', async ({ page }) => {
|
|
const routeManager = new WebsiteRouteManager(page);
|
|
const authManager = new WebsiteAuthManager(page);
|
|
const routeContractSpec = new RouteContractSpec(page);
|
|
|
|
// Mock DashboardOverviewDTO with all empty arrays/nulls
|
|
const mockDashboardData = {
|
|
totalUsers: 150,
|
|
activeUsers: 120,
|
|
totalRaces: 10,
|
|
totalLeagues: 5,
|
|
upcomingRaces: [],
|
|
leagueStandingsSummaries: [],
|
|
feedSummary: {
|
|
notificationCount: 0,
|
|
items: [],
|
|
},
|
|
};
|
|
|
|
await routeContractSpec.mockApiCall('GetDashboardData', mockDashboardData);
|
|
|
|
// Navigate to /dashboard
|
|
await authManager.loginAsUser();
|
|
await page.goto(routeManager.getRoute('/dashboard'));
|
|
|
|
// Verify empty state messages are shown
|
|
await expect(page.locator('[data-testid="upcoming-schedule-empty"]')).toBeVisible();
|
|
await expect(page.locator('[data-testid="championship-standings-empty"]')).toBeVisible();
|
|
await expect(page.locator('[data-testid="recent-activity-empty"]')).toBeVisible();
|
|
|
|
// Verify UI remains functional
|
|
await expect(page.locator('[data-testid="dashboard"]')).toBeVisible();
|
|
await expect(page.locator('[data-testid="view-schedule-button"]')).toBeEnabled();
|
|
});
|
|
|
|
test('should handle error states gracefully', async ({ page }) => {
|
|
const routeManager = new WebsiteRouteManager(page);
|
|
const authManager = new WebsiteAuthManager(page);
|
|
const routeContractSpec = new RouteContractSpec(page);
|
|
const consoleErrorCapture = new ConsoleErrorCapture(page);
|
|
|
|
// Login as user
|
|
await authManager.loginAsUser();
|
|
|
|
// Mock various error scenarios
|
|
await routeContractSpec.mockApiCall('GetDashboardData', {
|
|
error: 'Internal Server Error',
|
|
status: 500,
|
|
});
|
|
|
|
// Navigate to /dashboard
|
|
await page.goto(routeManager.getRoute('/dashboard'));
|
|
|
|
// Verify error handling (redirects to notFound)
|
|
await expect(page).toHaveURL(/.*\/not-found/);
|
|
|
|
// Verify console error was captured
|
|
const errors = consoleErrorCapture.getErrors();
|
|
expect(errors.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test('should handle network connectivity issues', async ({ page }) => {
|
|
const routeManager = new WebsiteRouteManager(page);
|
|
const authManager = new WebsiteAuthManager(page);
|
|
const routeContractSpec = new RouteContractSpec(page);
|
|
const consoleErrorCapture = new ConsoleErrorCapture(page);
|
|
|
|
// Login as user
|
|
await authManager.loginAsUser();
|
|
|
|
// Mock network failure
|
|
await routeContractSpec.mockApiCall('GetDashboardData', {
|
|
error: 'Network Error',
|
|
status: 0,
|
|
});
|
|
|
|
// Navigate to /dashboard
|
|
await page.goto(routeManager.getRoute('/dashboard'));
|
|
|
|
// Verify appropriate error handling
|
|
await expect(page).toHaveURL(/.*\/not-found/);
|
|
|
|
// Check if retry mechanism exists
|
|
// Note: This would depend on the actual implementation
|
|
// For now, verify the error was captured
|
|
const errors = consoleErrorCapture.getErrors();
|
|
expect(errors.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe('Dashboard User Interaction Flows', () => {
|
|
test('should navigate to races when clicking view schedule button', async ({ page }) => {
|
|
const routeManager = new WebsiteRouteManager(page);
|
|
const authManager = new WebsiteAuthManager(page);
|
|
const routeContractSpec = new RouteContractSpec(page);
|
|
|
|
// Mock dashboard data
|
|
const mockDashboardData = {
|
|
totalUsers: 150,
|
|
activeUsers: 120,
|
|
totalRaces: 10,
|
|
totalLeagues: 5,
|
|
};
|
|
|
|
await routeContractSpec.mockApiCall('GetDashboardData', mockDashboardData);
|
|
|
|
// Navigate to /dashboard
|
|
await authManager.loginAsUser();
|
|
await page.goto(routeManager.getRoute('/dashboard'));
|
|
|
|
// Click "View Full Schedule" button
|
|
await page.locator('[data-testid="view-schedule-button"]').click();
|
|
|
|
// Verify navigation to /races
|
|
await expect(page).toHaveURL(/.*\/races/);
|
|
|
|
// Check URL changes correctly
|
|
await expect(page).toHaveURL(/.*\/races/);
|
|
});
|
|
|
|
test('should handle upcoming race item interactions', async ({ page }) => {
|
|
const routeManager = new WebsiteRouteManager(page);
|
|
const authManager = new WebsiteAuthManager(page);
|
|
const routeContractSpec = new RouteContractSpec(page);
|
|
|
|
// Mock DashboardOverviewDTO with upcomingRaces
|
|
const mockDashboardData = {
|
|
totalUsers: 150,
|
|
activeUsers: 120,
|
|
totalRaces: 10,
|
|
totalLeagues: 5,
|
|
upcomingRaces: [
|
|
{
|
|
id: 'race-123',
|
|
track: 'Monza',
|
|
car: 'Ferrari 488 GT3',
|
|
scheduledAt: '2024-01-25T14:00:00Z',
|
|
status: 'scheduled',
|
|
isMyLeague: true,
|
|
},
|
|
],
|
|
};
|
|
|
|
await routeContractSpec.mockApiCall('GetDashboardData', mockDashboardData);
|
|
|
|
// Navigate to /dashboard
|
|
await authManager.loginAsUser();
|
|
await page.goto(routeManager.getRoute('/dashboard'));
|
|
|
|
// Click on an upcoming race item
|
|
await page.locator('[data-testid="upcoming-race-123"]').click();
|
|
|
|
// Verify navigation to race detail page (if applicable)
|
|
// Note: This depends on the actual implementation
|
|
// For now, verify the page navigates somewhere
|
|
await expect(page).not.toHaveURL(/.*\/dashboard/);
|
|
});
|
|
|
|
test('should handle league standing item interactions', async ({ page }) => {
|
|
const routeManager = new WebsiteRouteManager(page);
|
|
const authManager = new WebsiteAuthManager(page);
|
|
const routeContractSpec = new RouteContractSpec(page);
|
|
|
|
// Mock DashboardOverviewDTO with leagueStandingsSummaries
|
|
const mockDashboardData = {
|
|
totalUsers: 150,
|
|
activeUsers: 120,
|
|
totalRaces: 10,
|
|
totalLeagues: 5,
|
|
leagueStandingsSummaries: [
|
|
{
|
|
leagueId: 'league-1',
|
|
leagueName: 'Pro League',
|
|
position: 3,
|
|
totalDrivers: 50,
|
|
points: 1250,
|
|
},
|
|
],
|
|
};
|
|
|
|
await routeContractSpec.mockApiCall('GetDashboardData', mockDashboardData);
|
|
|
|
// Navigate to /dashboard
|
|
await authManager.loginAsUser();
|
|
await page.goto(routeManager.getRoute('/dashboard'));
|
|
|
|
// Click on a league standing item
|
|
await page.locator('[data-testid="league-standing-league-1"]').click();
|
|
|
|
// Verify navigation to league detail page (if applicable)
|
|
// Note: This depends on the actual implementation
|
|
// For now, verify the page navigates somewhere
|
|
await expect(page).not.toHaveURL(/.*\/dashboard/);
|
|
});
|
|
|
|
test('should handle feed item interactions', async ({ page }) => {
|
|
const routeManager = new WebsiteRouteManager(page);
|
|
const authManager = new WebsiteAuthManager(page);
|
|
const routeContractSpec = new RouteContractSpec(page);
|
|
|
|
// Mock DashboardOverviewDTO with feedSummary containing CTAs
|
|
const mockDashboardData = {
|
|
totalUsers: 150,
|
|
activeUsers: 120,
|
|
totalRaces: 10,
|
|
totalLeagues: 5,
|
|
feedSummary: {
|
|
notificationCount: 1,
|
|
items: [
|
|
{
|
|
id: 'feed-1',
|
|
type: 'race_result',
|
|
headline: 'Race completed at Monza',
|
|
body: 'You finished 3rd in the Pro League race',
|
|
timestamp: '2024-01-20T10:00:00Z',
|
|
ctaLabel: 'View Results',
|
|
ctaHref: '/races/race-123',
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
await routeContractSpec.mockApiCall('GetDashboardData', mockDashboardData);
|
|
|
|
// Navigate to /dashboard
|
|
await authManager.loginAsUser();
|
|
await page.goto(routeManager.getRoute('/dashboard'));
|
|
|
|
// Click on feed item with CTA
|
|
await page.locator('[data-testid="feed-item-feed-1"]').click();
|
|
|
|
// Verify navigation to CTA href
|
|
await expect(page).toHaveURL(/.*\/races\/race-123/);
|
|
});
|
|
});
|
|
|
|
describe('Dashboard Performance and Edge Cases', () => {
|
|
it('should handle large amounts of upcoming races', () => {
|
|
// TODO: Implement test
|
|
// - Mock DashboardOverviewDTO with many upcoming races
|
|
// - Navigate to /dashboard
|
|
// - Verify UI handles large list (virtualization, performance)
|
|
// - Check only first 3 races are shown in schedule
|
|
});
|
|
|
|
it('should handle large amounts of league standings', () => {
|
|
// TODO: Implement test
|
|
// - Mock DashboardOverviewDTO with many league standings
|
|
// - Navigate to /dashboard
|
|
// - Verify UI handles large list
|
|
// - Check rendering performance
|
|
});
|
|
|
|
it('should handle large amounts of feed items', () => {
|
|
// TODO: Implement test
|
|
// - Mock DashboardOverviewDTO with many feed items
|
|
// - Navigate to /dashboard
|
|
// - Verify UI handles large list
|
|
// - Check rendering performance
|
|
});
|
|
|
|
it('should handle malformed dashboard data', () => {
|
|
// TODO: Implement test
|
|
// - Mock DashboardPageQuery with malformed data
|
|
// - Navigate to /dashboard
|
|
// - Verify graceful error handling
|
|
// - Check error logging
|
|
});
|
|
|
|
it('should handle dashboard data with special characters', () => {
|
|
// TODO: Implement test
|
|
// - Mock DashboardOverviewDTO with special characters in strings
|
|
// - Navigate to /dashboard
|
|
// - Verify proper rendering and escaping
|
|
});
|
|
|
|
it('should handle dashboard data with very long strings', () => {
|
|
// TODO: Implement test
|
|
// - Mock DashboardOverviewDTO with very long track names, league names, etc.
|
|
// - Navigate to /dashboard
|
|
// - Verify text truncation or wrapping works correctly
|
|
});
|
|
});
|
|
}); |