website refactor
This commit is contained in:
782
tests/e2e/api/league-api.test.ts
Normal file
782
tests/e2e/api/league-api.test.ts
Normal file
@@ -0,0 +1,782 @@
|
||||
/**
|
||||
* League API Tests
|
||||
*
|
||||
* This test suite performs comprehensive API testing for league-related endpoints.
|
||||
* It validates:
|
||||
* - Response structure matches expected DTO
|
||||
* - Required fields are present
|
||||
* - Data types are correct
|
||||
* - Edge cases (empty results, missing data)
|
||||
* - Business logic (sorting, filtering, calculations)
|
||||
*
|
||||
* This test is designed to run in the Docker e2e environment and can be executed with:
|
||||
* npm run test:e2e:website (which runs everything in Docker)
|
||||
*/
|
||||
|
||||
import { test, expect, request } from '@playwright/test';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
|
||||
interface TestResult {
|
||||
endpoint: string;
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
||||
status: number;
|
||||
success: boolean;
|
||||
error?: string;
|
||||
response?: unknown;
|
||||
hasPresenterError: boolean;
|
||||
responseTime: number;
|
||||
}
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL ?? process.env.NEXT_PUBLIC_API_BASE_URL ?? 'http://localhost:3101';
|
||||
|
||||
// Auth file paths
|
||||
const USER_AUTH_FILE = path.join(__dirname, '.auth/user-session.json');
|
||||
const ADMIN_AUTH_FILE = path.join(__dirname, '.auth/admin-session.json');
|
||||
|
||||
test.describe('League API Tests', () => {
|
||||
const allResults: TestResult[] = [];
|
||||
let testResults: TestResult[] = [];
|
||||
|
||||
test.beforeAll(async () => {
|
||||
console.log(`[LEAGUE API] Testing API at: ${API_BASE_URL}`);
|
||||
|
||||
// Verify auth files exist
|
||||
const userAuthExists = await fs.access(USER_AUTH_FILE).then(() => true).catch(() => false);
|
||||
const adminAuthExists = await fs.access(ADMIN_AUTH_FILE).then(() => true).catch(() => false);
|
||||
|
||||
if (!userAuthExists || !adminAuthExists) {
|
||||
throw new Error('Auth files not found. Run global setup first.');
|
||||
}
|
||||
|
||||
console.log('[LEAGUE API] Auth files verified');
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await generateReport();
|
||||
});
|
||||
|
||||
test('League Discovery Endpoints - Public endpoints', async ({ request }) => {
|
||||
testResults = [];
|
||||
const endpoints = [
|
||||
{ method: 'GET' as const, path: '/leagues/all-with-capacity', name: 'Get all leagues with capacity' },
|
||||
{ method: 'GET' as const, path: '/leagues/all-with-capacity-and-scoring', name: 'Get all leagues with capacity and scoring' },
|
||||
{ method: 'GET' as const, path: '/leagues/total-leagues', name: 'Get total leagues count' },
|
||||
{ method: 'GET' as const, path: '/leagues/all', name: 'Get all leagues (alias)' },
|
||||
{ method: 'GET' as const, path: '/leagues/available', name: 'Get available leagues (alias)' },
|
||||
];
|
||||
|
||||
console.log(`\n[LEAGUE API] Testing ${endpoints.length} league discovery endpoints...`);
|
||||
|
||||
for (const endpoint of endpoints) {
|
||||
await testEndpoint(request, endpoint);
|
||||
}
|
||||
|
||||
// Check for failures
|
||||
const failures = testResults.filter(r => !r.success);
|
||||
if (failures.length > 0) {
|
||||
console.log('\n❌ FAILURES FOUND:');
|
||||
failures.forEach(r => {
|
||||
console.log(` ${r.method} ${r.endpoint} - ${r.status} - ${r.error || r.response}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Assert all endpoints succeeded
|
||||
expect(failures.length).toBe(0);
|
||||
});
|
||||
|
||||
test('League Discovery - Response structure validation', async ({ request }) => {
|
||||
testResults = [];
|
||||
|
||||
// Test /leagues/all-with-capacity
|
||||
const allLeaguesResponse = await request.get(`${API_BASE_URL}/leagues/all-with-capacity`);
|
||||
expect(allLeaguesResponse.ok()).toBe(true);
|
||||
|
||||
const allLeaguesData = await allLeaguesResponse.json();
|
||||
expect(allLeaguesData).toHaveProperty('leagues');
|
||||
expect(allLeaguesData).toHaveProperty('totalCount');
|
||||
expect(Array.isArray(allLeaguesData.leagues)).toBe(true);
|
||||
expect(typeof allLeaguesData.totalCount).toBe('number');
|
||||
|
||||
// Validate league structure if leagues exist
|
||||
if (allLeaguesData.leagues.length > 0) {
|
||||
const league = allLeaguesData.leagues[0];
|
||||
expect(league).toHaveProperty('id');
|
||||
expect(league).toHaveProperty('name');
|
||||
expect(league).toHaveProperty('description');
|
||||
expect(league).toHaveProperty('ownerId');
|
||||
expect(league).toHaveProperty('createdAt');
|
||||
expect(league).toHaveProperty('settings');
|
||||
expect(league.settings).toHaveProperty('maxDrivers');
|
||||
expect(league).toHaveProperty('usedSlots');
|
||||
|
||||
// Validate data types
|
||||
expect(typeof league.id).toBe('string');
|
||||
expect(typeof league.name).toBe('string');
|
||||
expect(typeof league.description).toBe('string');
|
||||
expect(typeof league.ownerId).toBe('string');
|
||||
expect(typeof league.createdAt).toBe('string');
|
||||
expect(typeof league.settings.maxDrivers).toBe('number');
|
||||
expect(typeof league.usedSlots).toBe('number');
|
||||
|
||||
// Validate business logic: usedSlots <= maxDrivers
|
||||
expect(league.usedSlots).toBeLessThanOrEqual(league.settings.maxDrivers);
|
||||
}
|
||||
|
||||
// Test /leagues/all-with-capacity-and-scoring
|
||||
const scoredLeaguesResponse = await request.get(`${API_BASE_URL}/leagues/all-with-capacity-and-scoring`);
|
||||
expect(scoredLeaguesResponse.ok()).toBe(true);
|
||||
|
||||
const scoredLeaguesData = await scoredLeaguesResponse.json();
|
||||
expect(scoredLeaguesData).toHaveProperty('leagues');
|
||||
expect(scoredLeaguesData).toHaveProperty('totalCount');
|
||||
expect(Array.isArray(scoredLeaguesData.leagues)).toBe(true);
|
||||
|
||||
// Validate scoring structure if leagues exist
|
||||
if (scoredLeaguesData.leagues.length > 0) {
|
||||
const league = scoredLeaguesData.leagues[0];
|
||||
expect(league).toHaveProperty('scoring');
|
||||
expect(league.scoring).toHaveProperty('gameId');
|
||||
expect(league.scoring).toHaveProperty('scoringPresetId');
|
||||
|
||||
// Validate data types
|
||||
expect(typeof league.scoring.gameId).toBe('string');
|
||||
expect(typeof league.scoring.scoringPresetId).toBe('string');
|
||||
}
|
||||
|
||||
// Test /leagues/total-leagues
|
||||
const totalResponse = await request.get(`${API_BASE_URL}/leagues/total-leagues`);
|
||||
expect(totalResponse.ok()).toBe(true);
|
||||
|
||||
const totalData = await totalResponse.json();
|
||||
expect(totalData).toHaveProperty('totalLeagues');
|
||||
expect(typeof totalData.totalLeagues).toBe('number');
|
||||
expect(totalData.totalLeagues).toBeGreaterThanOrEqual(0);
|
||||
|
||||
// Validate consistency: totalCount from all-with-capacity should match totalLeagues
|
||||
expect(allLeaguesData.totalCount).toBe(totalData.totalLeagues);
|
||||
|
||||
testResults.push({
|
||||
endpoint: '/leagues/all-with-capacity',
|
||||
method: 'GET',
|
||||
status: allLeaguesResponse.status(),
|
||||
success: true,
|
||||
hasPresenterError: false,
|
||||
responseTime: 0,
|
||||
});
|
||||
|
||||
testResults.push({
|
||||
endpoint: '/leagues/all-with-capacity-and-scoring',
|
||||
method: 'GET',
|
||||
status: scoredLeaguesResponse.status(),
|
||||
success: true,
|
||||
hasPresenterError: false,
|
||||
responseTime: 0,
|
||||
});
|
||||
|
||||
testResults.push({
|
||||
endpoint: '/leagues/total-leagues',
|
||||
method: 'GET',
|
||||
status: totalResponse.status(),
|
||||
success: true,
|
||||
hasPresenterError: false,
|
||||
responseTime: 0,
|
||||
});
|
||||
|
||||
allResults.push(...testResults);
|
||||
});
|
||||
|
||||
test('League Detail Endpoints - Public endpoints', async ({ request }) => {
|
||||
testResults = [];
|
||||
|
||||
// First, get a valid league ID from the discovery endpoint
|
||||
const discoveryResponse = await request.get(`${API_BASE_URL}/leagues/all-with-capacity`);
|
||||
const discoveryData = await discoveryResponse.json();
|
||||
|
||||
if (discoveryData.leagues.length === 0) {
|
||||
console.log('[LEAGUE API] No leagues found, skipping detail endpoint tests');
|
||||
return;
|
||||
}
|
||||
|
||||
const leagueId = discoveryData.leagues[0].id;
|
||||
|
||||
const endpoints = [
|
||||
{ method: 'GET' as const, path: `/leagues/${leagueId}`, name: 'Get league details' },
|
||||
{ method: 'GET' as const, path: `/leagues/${leagueId}/seasons`, name: 'Get league seasons' },
|
||||
{ method: 'GET' as const, path: `/leagues/${leagueId}/stats`, name: 'Get league stats' },
|
||||
{ method: 'GET' as const, path: `/leagues/${leagueId}/memberships`, name: 'Get league memberships' },
|
||||
];
|
||||
|
||||
console.log(`\n[LEAGUE API] Testing ${endpoints.length} league detail endpoints for league ${leagueId}...`);
|
||||
|
||||
for (const endpoint of endpoints) {
|
||||
await testEndpoint(request, endpoint);
|
||||
}
|
||||
|
||||
// Check for failures
|
||||
const failures = testResults.filter(r => !r.success);
|
||||
if (failures.length > 0) {
|
||||
console.log('\n❌ FAILURES FOUND:');
|
||||
failures.forEach(r => {
|
||||
console.log(` ${r.method} ${r.endpoint} - ${r.status} - ${r.error || r.response}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Assert all endpoints succeeded
|
||||
expect(failures.length).toBe(0);
|
||||
});
|
||||
|
||||
test('League Detail - Response structure validation', async ({ request }) => {
|
||||
testResults = [];
|
||||
|
||||
// First, get a valid league ID from the discovery endpoint
|
||||
const discoveryResponse = await request.get(`${API_BASE_URL}/leagues/all-with-capacity`);
|
||||
const discoveryData = await discoveryResponse.json();
|
||||
|
||||
if (discoveryData.leagues.length === 0) {
|
||||
console.log('[LEAGUE API] No leagues found, skipping detail validation tests');
|
||||
return;
|
||||
}
|
||||
|
||||
const leagueId = discoveryData.leagues[0].id;
|
||||
|
||||
// Test /leagues/{id}
|
||||
const leagueResponse = await request.get(`${API_BASE_URL}/leagues/${leagueId}`);
|
||||
expect(leagueResponse.ok()).toBe(true);
|
||||
|
||||
const leagueData = await leagueResponse.json();
|
||||
expect(leagueData).toHaveProperty('id');
|
||||
expect(leagueData).toHaveProperty('name');
|
||||
expect(leagueData).toHaveProperty('description');
|
||||
expect(leagueData).toHaveProperty('ownerId');
|
||||
expect(leagueData).toHaveProperty('createdAt');
|
||||
|
||||
// Validate data types
|
||||
expect(typeof leagueData.id).toBe('string');
|
||||
expect(typeof leagueData.name).toBe('string');
|
||||
expect(typeof leagueData.description).toBe('string');
|
||||
expect(typeof leagueData.ownerId).toBe('string');
|
||||
expect(typeof leagueData.createdAt).toBe('string');
|
||||
|
||||
// Validate ID matches requested ID
|
||||
expect(leagueData.id).toBe(leagueId);
|
||||
|
||||
// Test /leagues/{id}/seasons
|
||||
const seasonsResponse = await request.get(`${API_BASE_URL}/leagues/${leagueId}/seasons`);
|
||||
expect(seasonsResponse.ok()).toBe(true);
|
||||
|
||||
const seasonsData = await seasonsResponse.json();
|
||||
expect(Array.isArray(seasonsData)).toBe(true);
|
||||
|
||||
// Validate season structure if seasons exist
|
||||
if (seasonsData.length > 0) {
|
||||
const season = seasonsData[0];
|
||||
expect(season).toHaveProperty('id');
|
||||
expect(season).toHaveProperty('name');
|
||||
expect(season).toHaveProperty('status');
|
||||
|
||||
// Validate data types
|
||||
expect(typeof season.id).toBe('string');
|
||||
expect(typeof season.name).toBe('string');
|
||||
expect(typeof season.status).toBe('string');
|
||||
}
|
||||
|
||||
// Test /leagues/{id}/stats
|
||||
const statsResponse = await request.get(`${API_BASE_URL}/leagues/${leagueId}/stats`);
|
||||
expect(statsResponse.ok()).toBe(true);
|
||||
|
||||
const statsData = await statsResponse.json();
|
||||
expect(statsData).toHaveProperty('memberCount');
|
||||
expect(statsData).toHaveProperty('raceCount');
|
||||
expect(statsData).toHaveProperty('avgSOF');
|
||||
|
||||
// Validate data types
|
||||
expect(typeof statsData.memberCount).toBe('number');
|
||||
expect(typeof statsData.raceCount).toBe('number');
|
||||
expect(typeof statsData.avgSOF).toBe('number');
|
||||
|
||||
// Validate business logic: counts should be non-negative
|
||||
expect(statsData.memberCount).toBeGreaterThanOrEqual(0);
|
||||
expect(statsData.raceCount).toBeGreaterThanOrEqual(0);
|
||||
expect(statsData.avgSOF).toBeGreaterThanOrEqual(0);
|
||||
|
||||
// Test /leagues/{id}/memberships
|
||||
const membershipsResponse = await request.get(`${API_BASE_URL}/leagues/${leagueId}/memberships`);
|
||||
expect(membershipsResponse.ok()).toBe(true);
|
||||
|
||||
const membershipsData = await membershipsResponse.json();
|
||||
expect(membershipsData).toHaveProperty('members');
|
||||
expect(Array.isArray(membershipsData.members)).toBe(true);
|
||||
|
||||
// Validate membership structure if members exist
|
||||
if (membershipsData.members.length > 0) {
|
||||
const member = membershipsData.members[0];
|
||||
expect(member).toHaveProperty('driverId');
|
||||
expect(member).toHaveProperty('role');
|
||||
expect(member).toHaveProperty('joinedAt');
|
||||
|
||||
// Validate data types
|
||||
expect(typeof member.driverId).toBe('string');
|
||||
expect(typeof member.role).toBe('string');
|
||||
expect(typeof member.joinedAt).toBe('string');
|
||||
|
||||
// Validate business logic: at least one owner must exist
|
||||
const hasOwner = membershipsData.members.some((m: any) => m.role === 'owner');
|
||||
expect(hasOwner).toBe(true);
|
||||
}
|
||||
|
||||
testResults.push({
|
||||
endpoint: `/leagues/${leagueId}`,
|
||||
method: 'GET',
|
||||
status: leagueResponse.status(),
|
||||
success: true,
|
||||
hasPresenterError: false,
|
||||
responseTime: 0,
|
||||
});
|
||||
|
||||
testResults.push({
|
||||
endpoint: `/leagues/${leagueId}/seasons`,
|
||||
method: 'GET',
|
||||
status: seasonsResponse.status(),
|
||||
success: true,
|
||||
hasPresenterError: false,
|
||||
responseTime: 0,
|
||||
});
|
||||
|
||||
testResults.push({
|
||||
endpoint: `/leagues/${leagueId}/stats`,
|
||||
method: 'GET',
|
||||
status: statsResponse.status(),
|
||||
success: true,
|
||||
hasPresenterError: false,
|
||||
responseTime: 0,
|
||||
});
|
||||
|
||||
testResults.push({
|
||||
endpoint: `/leagues/${leagueId}/memberships`,
|
||||
method: 'GET',
|
||||
status: membershipsResponse.status(),
|
||||
success: true,
|
||||
hasPresenterError: false,
|
||||
responseTime: 0,
|
||||
});
|
||||
|
||||
allResults.push(...testResults);
|
||||
});
|
||||
|
||||
test('League Schedule Endpoints - Public endpoints', async ({ request }) => {
|
||||
testResults = [];
|
||||
|
||||
// First, get a valid league ID from the discovery endpoint
|
||||
const discoveryResponse = await request.get(`${API_BASE_URL}/leagues/all-with-capacity`);
|
||||
const discoveryData = await discoveryResponse.json();
|
||||
|
||||
if (discoveryData.leagues.length === 0) {
|
||||
console.log('[LEAGUE API] No leagues found, skipping schedule endpoint tests');
|
||||
return;
|
||||
}
|
||||
|
||||
const leagueId = discoveryData.leagues[0].id;
|
||||
|
||||
const endpoints = [
|
||||
{ method: 'GET' as const, path: `/leagues/${leagueId}/schedule`, name: 'Get league schedule' },
|
||||
];
|
||||
|
||||
console.log(`\n[LEAGUE API] Testing ${endpoints.length} league schedule endpoints for league ${leagueId}...`);
|
||||
|
||||
for (const endpoint of endpoints) {
|
||||
await testEndpoint(request, endpoint);
|
||||
}
|
||||
|
||||
// Check for failures
|
||||
const failures = testResults.filter(r => !r.success);
|
||||
if (failures.length > 0) {
|
||||
console.log('\n❌ FAILURES FOUND:');
|
||||
failures.forEach(r => {
|
||||
console.log(` ${r.method} ${r.endpoint} - ${r.status} - ${r.error || r.response}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Assert all endpoints succeeded
|
||||
expect(failures.length).toBe(0);
|
||||
});
|
||||
|
||||
test('League Schedule - Response structure validation', async ({ request }) => {
|
||||
testResults = [];
|
||||
|
||||
// First, get a valid league ID from the discovery endpoint
|
||||
const discoveryResponse = await request.get(`${API_BASE_URL}/leagues/all-with-capacity`);
|
||||
const discoveryData = await discoveryResponse.json();
|
||||
|
||||
if (discoveryData.leagues.length === 0) {
|
||||
console.log('[LEAGUE API] No leagues found, skipping schedule validation tests');
|
||||
return;
|
||||
}
|
||||
|
||||
const leagueId = discoveryData.leagues[0].id;
|
||||
|
||||
// Test /leagues/{id}/schedule
|
||||
const scheduleResponse = await request.get(`${API_BASE_URL}/leagues/${leagueId}/schedule`);
|
||||
expect(scheduleResponse.ok()).toBe(true);
|
||||
|
||||
const scheduleData = await scheduleResponse.json();
|
||||
expect(scheduleData).toHaveProperty('seasonId');
|
||||
expect(scheduleData).toHaveProperty('races');
|
||||
expect(Array.isArray(scheduleData.races)).toBe(true);
|
||||
|
||||
// Validate data types
|
||||
expect(typeof scheduleData.seasonId).toBe('string');
|
||||
|
||||
// Validate race structure if races exist
|
||||
if (scheduleData.races.length > 0) {
|
||||
const race = scheduleData.races[0];
|
||||
expect(race).toHaveProperty('id');
|
||||
expect(race).toHaveProperty('track');
|
||||
expect(race).toHaveProperty('car');
|
||||
expect(race).toHaveProperty('scheduledAt');
|
||||
|
||||
// Validate data types
|
||||
expect(typeof race.id).toBe('string');
|
||||
expect(typeof race.track).toBe('string');
|
||||
expect(typeof race.car).toBe('string');
|
||||
expect(typeof race.scheduledAt).toBe('string');
|
||||
|
||||
// Validate business logic: races should be sorted by scheduledAt
|
||||
const scheduledTimes = scheduleData.races.map((r: any) => new Date(r.scheduledAt).getTime());
|
||||
const sortedTimes = [...scheduledTimes].sort((a, b) => a - b);
|
||||
expect(scheduledTimes).toEqual(sortedTimes);
|
||||
}
|
||||
|
||||
testResults.push({
|
||||
endpoint: `/leagues/${leagueId}/schedule`,
|
||||
method: 'GET',
|
||||
status: scheduleResponse.status(),
|
||||
success: true,
|
||||
hasPresenterError: false,
|
||||
responseTime: 0,
|
||||
});
|
||||
|
||||
allResults.push(...testResults);
|
||||
});
|
||||
|
||||
test('League Standings Endpoints - Public endpoints', async ({ request }) => {
|
||||
testResults = [];
|
||||
|
||||
// First, get a valid league ID from the discovery endpoint
|
||||
const discoveryResponse = await request.get(`${API_BASE_URL}/leagues/all-with-capacity`);
|
||||
const discoveryData = await discoveryResponse.json();
|
||||
|
||||
if (discoveryData.leagues.length === 0) {
|
||||
console.log('[LEAGUE API] No leagues found, skipping standings endpoint tests');
|
||||
return;
|
||||
}
|
||||
|
||||
const leagueId = discoveryData.leagues[0].id;
|
||||
|
||||
const endpoints = [
|
||||
{ method: 'GET' as const, path: `/leagues/${leagueId}/standings`, name: 'Get league standings' },
|
||||
];
|
||||
|
||||
console.log(`\n[LEAGUE API] Testing ${endpoints.length} league standings endpoints for league ${leagueId}...`);
|
||||
|
||||
for (const endpoint of endpoints) {
|
||||
await testEndpoint(request, endpoint);
|
||||
}
|
||||
|
||||
// Check for failures
|
||||
const failures = testResults.filter(r => !r.success);
|
||||
if (failures.length > 0) {
|
||||
console.log('\n❌ FAILURES FOUND:');
|
||||
failures.forEach(r => {
|
||||
console.log(` ${r.method} ${r.endpoint} - ${r.status} - ${r.error || r.response}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Assert all endpoints succeeded
|
||||
expect(failures.length).toBe(0);
|
||||
});
|
||||
|
||||
test('League Standings - Response structure validation', async ({ request }) => {
|
||||
testResults = [];
|
||||
|
||||
// First, get a valid league ID from the discovery endpoint
|
||||
const discoveryResponse = await request.get(`${API_BASE_URL}/leagues/all-with-capacity`);
|
||||
const discoveryData = await discoveryResponse.json();
|
||||
|
||||
if (discoveryData.leagues.length === 0) {
|
||||
console.log('[LEAGUE API] No leagues found, skipping standings validation tests');
|
||||
return;
|
||||
}
|
||||
|
||||
const leagueId = discoveryData.leagues[0].id;
|
||||
|
||||
// Test /leagues/{id}/standings
|
||||
const standingsResponse = await request.get(`${API_BASE_URL}/leagues/${leagueId}/standings`);
|
||||
expect(standingsResponse.ok()).toBe(true);
|
||||
|
||||
const standingsData = await standingsResponse.json();
|
||||
expect(standingsData).toHaveProperty('standings');
|
||||
expect(Array.isArray(standingsData.standings)).toBe(true);
|
||||
|
||||
// Validate standing structure if standings exist
|
||||
if (standingsData.standings.length > 0) {
|
||||
const standing = standingsData.standings[0];
|
||||
expect(standing).toHaveProperty('position');
|
||||
expect(standing).toHaveProperty('driverId');
|
||||
expect(standing).toHaveProperty('points');
|
||||
expect(standing).toHaveProperty('races');
|
||||
|
||||
// Validate data types
|
||||
expect(typeof standing.position).toBe('number');
|
||||
expect(typeof standing.driverId).toBe('string');
|
||||
expect(typeof standing.points).toBe('number');
|
||||
expect(typeof standing.races).toBe('number');
|
||||
|
||||
// Validate business logic: position must be sequential starting from 1
|
||||
const positions = standingsData.standings.map((s: any) => s.position);
|
||||
const expectedPositions = Array.from({ length: positions.length }, (_, i) => i + 1);
|
||||
expect(positions).toEqual(expectedPositions);
|
||||
|
||||
// Validate business logic: points must be non-negative
|
||||
expect(standing.points).toBeGreaterThanOrEqual(0);
|
||||
|
||||
// Validate business logic: races count must be non-negative
|
||||
expect(standing.races).toBeGreaterThanOrEqual(0);
|
||||
}
|
||||
|
||||
testResults.push({
|
||||
endpoint: `/leagues/${leagueId}/standings`,
|
||||
method: 'GET',
|
||||
status: standingsResponse.status(),
|
||||
success: true,
|
||||
hasPresenterError: false,
|
||||
responseTime: 0,
|
||||
});
|
||||
|
||||
allResults.push(...testResults);
|
||||
});
|
||||
|
||||
test('Edge Cases - Invalid league IDs', async ({ request }) => {
|
||||
testResults = [];
|
||||
|
||||
const endpoints = [
|
||||
{ method: 'GET' as const, path: '/leagues/non-existent-league-id', name: 'Get non-existent league' },
|
||||
{ method: 'GET' as const, path: '/leagues/non-existent-league-id/seasons', name: 'Get seasons for non-existent league' },
|
||||
{ method: 'GET' as const, path: '/leagues/non-existent-league-id/stats', name: 'Get stats for non-existent league' },
|
||||
{ method: 'GET' as const, path: '/leagues/non-existent-league-id/schedule', name: 'Get schedule for non-existent league' },
|
||||
{ method: 'GET' as const, path: '/leagues/non-existent-league-id/standings', name: 'Get standings for non-existent league' },
|
||||
{ method: 'GET' as const, path: '/leagues/non-existent-league-id/memberships', name: 'Get memberships for non-existent league' },
|
||||
];
|
||||
|
||||
console.log(`\n[LEAGUE API] Testing ${endpoints.length} edge case endpoints with invalid IDs...`);
|
||||
|
||||
for (const endpoint of endpoints) {
|
||||
await testEndpoint(request, endpoint);
|
||||
}
|
||||
|
||||
// Check for failures
|
||||
const failures = testResults.filter(r => !r.success);
|
||||
if (failures.length > 0) {
|
||||
console.log('\n❌ FAILURES FOUND:');
|
||||
failures.forEach(r => {
|
||||
console.log(` ${r.method} ${r.endpoint} - ${r.status} - ${r.error || r.response}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Assert all endpoints succeeded (404 is acceptable for non-existent resources)
|
||||
expect(failures.length).toBe(0);
|
||||
});
|
||||
|
||||
test('Edge Cases - Empty results', async ({ request }) => {
|
||||
testResults = [];
|
||||
|
||||
// Test discovery endpoints with filters (if available)
|
||||
// Note: The current API doesn't seem to have filter parameters, but we test the base endpoints
|
||||
|
||||
const endpoints = [
|
||||
{ method: 'GET' as const, path: '/leagues/all-with-capacity', name: 'Get all leagues (empty check)' },
|
||||
{ method: 'GET' as const, path: '/leagues/all-with-capacity-and-scoring', name: 'Get all leagues with scoring (empty check)' },
|
||||
];
|
||||
|
||||
console.log(`\n[LEAGUE API] Testing ${endpoints.length} endpoints for empty result handling...`);
|
||||
|
||||
for (const endpoint of endpoints) {
|
||||
await testEndpoint(request, endpoint);
|
||||
}
|
||||
|
||||
// Check for failures
|
||||
const failures = testResults.filter(r => !r.success);
|
||||
if (failures.length > 0) {
|
||||
console.log('\n❌ FAILURES FOUND:');
|
||||
failures.forEach(r => {
|
||||
console.log(` ${r.method} ${r.endpoint} - ${r.status} - ${r.error || r.response}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Assert all endpoints succeeded
|
||||
expect(failures.length).toBe(0);
|
||||
});
|
||||
|
||||
async function testEndpoint(
|
||||
request: import('@playwright/test').APIRequestContext,
|
||||
endpoint: { method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; path: string; name?: string; body?: unknown; requiresAuth?: boolean }
|
||||
): Promise<void> {
|
||||
const startTime = Date.now();
|
||||
const fullUrl = `${API_BASE_URL}${endpoint.path}`;
|
||||
|
||||
console.log(`\n[TEST] ${endpoint.method} ${endpoint.path} (${endpoint.name || 'Unknown'})`);
|
||||
|
||||
try {
|
||||
let response;
|
||||
const headers: Record<string, string> = {};
|
||||
|
||||
// Playwright's request context handles cookies automatically
|
||||
// No need to set Authorization header for cookie-based auth
|
||||
|
||||
switch (endpoint.method) {
|
||||
case 'GET':
|
||||
response = await request.get(fullUrl, { headers });
|
||||
break;
|
||||
case 'POST':
|
||||
response = await request.post(fullUrl, { data: endpoint.body || {}, headers });
|
||||
break;
|
||||
case 'PUT':
|
||||
response = await request.put(fullUrl, { data: endpoint.body || {}, headers });
|
||||
break;
|
||||
case 'DELETE':
|
||||
response = await request.delete(fullUrl, { headers });
|
||||
break;
|
||||
case 'PATCH':
|
||||
response = await request.patch(fullUrl, { data: endpoint.body || {}, headers });
|
||||
break;
|
||||
}
|
||||
|
||||
const responseTime = Date.now() - startTime;
|
||||
const status = response.status();
|
||||
const body = await response.json().catch(() => null);
|
||||
const bodyText = await response.text().catch(() => '');
|
||||
|
||||
// Check for presenter errors
|
||||
const hasPresenterError =
|
||||
bodyText.includes('Presenter not presented') ||
|
||||
bodyText.includes('presenter not presented') ||
|
||||
(body && body.message && body.message.includes('Presenter not presented')) ||
|
||||
(body && body.error && body.error.includes('Presenter not presented'));
|
||||
|
||||
// Success is 200-299 status, or 404 for non-existent resources, and no presenter error
|
||||
const isNotFound = status === 404;
|
||||
const success = (status >= 200 && status < 300 || isNotFound) && !hasPresenterError;
|
||||
|
||||
const result: TestResult = {
|
||||
endpoint: endpoint.path,
|
||||
method: endpoint.method,
|
||||
status,
|
||||
success,
|
||||
hasPresenterError,
|
||||
responseTime,
|
||||
response: body || bodyText.substring(0, 200),
|
||||
};
|
||||
|
||||
if (!success) {
|
||||
result.error = body?.message || bodyText.substring(0, 200);
|
||||
}
|
||||
|
||||
testResults.push(result);
|
||||
allResults.push(result);
|
||||
|
||||
if (hasPresenterError) {
|
||||
console.log(` ❌ PRESENTER ERROR: ${status} - ${body?.message || bodyText.substring(0, 100)}`);
|
||||
} else if (success) {
|
||||
console.log(` ✅ ${status} (${responseTime}ms)`);
|
||||
} else {
|
||||
console.log(` ⚠️ ${status} (${responseTime}ms) - ${body?.message || 'Error'}`);
|
||||
}
|
||||
|
||||
} catch (error: unknown) {
|
||||
const responseTime = Date.now() - startTime;
|
||||
const errorString = error instanceof Error ? error.message : String(error);
|
||||
|
||||
const result: TestResult = {
|
||||
endpoint: endpoint.path,
|
||||
method: endpoint.method,
|
||||
status: 0,
|
||||
success: false,
|
||||
hasPresenterError: false,
|
||||
responseTime,
|
||||
error: errorString,
|
||||
};
|
||||
|
||||
// Check if it's a presenter error
|
||||
if (errorString.includes('Presenter not presented')) {
|
||||
result.hasPresenterError = true;
|
||||
console.log(` ❌ PRESENTER ERROR (exception): ${errorString}`);
|
||||
} else {
|
||||
console.log(` ❌ EXCEPTION: ${errorString}`);
|
||||
}
|
||||
|
||||
testResults.push(result);
|
||||
allResults.push(result);
|
||||
}
|
||||
}
|
||||
|
||||
async function generateReport(): Promise<void> {
|
||||
const summary = {
|
||||
total: allResults.length,
|
||||
success: allResults.filter(r => r.success).length,
|
||||
failed: allResults.filter(r => !r.success).length,
|
||||
presenterErrors: allResults.filter(r => r.hasPresenterError).length,
|
||||
avgResponseTime: allResults.reduce((sum, r) => sum + r.responseTime, 0) / allResults.length || 0,
|
||||
};
|
||||
|
||||
const report = {
|
||||
timestamp: new Date().toISOString(),
|
||||
summary,
|
||||
results: allResults,
|
||||
failures: allResults.filter(r => !r.success),
|
||||
};
|
||||
|
||||
// Write JSON report
|
||||
const jsonPath = path.join(__dirname, '../../../league-api-test-report.json');
|
||||
await fs.writeFile(jsonPath, JSON.stringify(report, null, 2));
|
||||
|
||||
// Write Markdown report
|
||||
const mdPath = path.join(__dirname, '../../../league-api-test-report.md');
|
||||
let md = `# League API Test Report\n\n`;
|
||||
md += `**Generated:** ${new Date().toISOString()}\n`;
|
||||
md += `**API Base URL:** ${API_BASE_URL}\n\n`;
|
||||
|
||||
md += `## Summary\n\n`;
|
||||
md += `- **Total Endpoints:** ${summary.total}\n`;
|
||||
md += `- **✅ Success:** ${summary.success}\n`;
|
||||
md += `- **❌ Failed:** ${summary.failed}\n`;
|
||||
md += `- **⚠️ Presenter Errors:** ${summary.presenterErrors}\n`;
|
||||
md += `- **Avg Response Time:** ${summary.avgResponseTime.toFixed(2)}ms\n\n`;
|
||||
|
||||
if (summary.presenterErrors > 0) {
|
||||
md += `## Presenter Errors\n\n`;
|
||||
const presenterFailures = allResults.filter(r => r.hasPresenterError);
|
||||
presenterFailures.forEach((r, i) => {
|
||||
md += `${i + 1}. **${r.method} ${r.endpoint}**\n`;
|
||||
md += ` - Status: ${r.status}\n`;
|
||||
md += ` - Error: ${r.error || 'No error message'}\n\n`;
|
||||
});
|
||||
}
|
||||
|
||||
if (summary.failed > 0 && summary.presenterErrors < summary.failed) {
|
||||
md += `## Other Failures\n\n`;
|
||||
const otherFailures = allResults.filter(r => !r.success && !r.hasPresenterError);
|
||||
otherFailures.forEach((r, i) => {
|
||||
md += `${i + 1}. **${r.method} ${r.endpoint}**\n`;
|
||||
md += ` - Status: ${r.status}\n`;
|
||||
md += ` - Error: ${r.error || 'No error message'}\n\n`;
|
||||
});
|
||||
}
|
||||
|
||||
await fs.writeFile(mdPath, md);
|
||||
|
||||
console.log(`\n📊 Reports generated:`);
|
||||
console.log(` JSON: ${jsonPath}`);
|
||||
console.log(` Markdown: ${mdPath}`);
|
||||
console.log(`\nSummary: ${summary.success}/${summary.total} passed, ${summary.presenterErrors} presenter errors`);
|
||||
}
|
||||
});
|
||||
628
tests/e2e/website/league-pages.e2e.test.ts
Normal file
628
tests/e2e/website/league-pages.e2e.test.ts
Normal file
@@ -0,0 +1,628 @@
|
||||
import { test, expect, Browser, APIRequestContext } from '@playwright/test';
|
||||
import { WebsiteAuthManager, AuthContext } from '../../shared/website/WebsiteAuthManager';
|
||||
import { ConsoleErrorCapture } from '../../shared/website/ConsoleErrorCapture';
|
||||
import { WebsiteRouteManager } from '../../shared/website/WebsiteRouteManager';
|
||||
|
||||
/**
|
||||
* E2E Tests for League Pages with Data Validation
|
||||
*
|
||||
* Tests cover:
|
||||
* 1. /leagues (Discovery Page) - League cards, filters, quick actions
|
||||
* 2. /leagues/[id] (Overview Page) - Stats, next race, season progress
|
||||
* 3. /leagues/[id]/schedule (Schedule Page) - Race list, registration, admin controls
|
||||
* 4. /leagues/[id]/standings (Standings Page) - Trend indicators, stats, team toggle
|
||||
* 5. /leagues/[id]/roster (Roster Page) - Driver cards, admin actions
|
||||
*/
|
||||
|
||||
test.describe('League Pages - E2E with Data Validation', () => {
|
||||
const routeManager = new WebsiteRouteManager();
|
||||
const leagueId = routeManager.resolvePathTemplate('/leagues/[id]', { id: WebsiteRouteManager.IDs.LEAGUE });
|
||||
|
||||
const CONSOLE_ALLOWLIST = [
|
||||
/Download the React DevTools/i,
|
||||
/Next.js-specific warning/i,
|
||||
/Failed to load resource: the server responded with a status of 404/i,
|
||||
/Failed to load resource: the server responded with a status of 403/i,
|
||||
/Failed to load resource: the server responded with a status of 401/i,
|
||||
/Failed to load resource: the server responded with a status of 500/i,
|
||||
/net::ERR_NAME_NOT_RESOLVED/i,
|
||||
/net::ERR_CONNECTION_CLOSED/i,
|
||||
/net::ERR_ACCESS_DENIED/i,
|
||||
/Minified React error #418/i,
|
||||
/Event/i,
|
||||
/An error occurred in the Server Components render/i,
|
||||
/Route Error Boundary/i,
|
||||
];
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
const allowedHosts = [
|
||||
new URL(process.env.PLAYWRIGHT_BASE_URL || 'http://website:3000').host,
|
||||
new URL(process.env.API_BASE_URL || 'http://api:3000').host,
|
||||
];
|
||||
|
||||
await page.route('**/*', (route) => {
|
||||
const url = new URL(route.request().url());
|
||||
if (allowedHosts.includes(url.host) || url.protocol === 'data:') {
|
||||
route.continue();
|
||||
} else {
|
||||
route.abort('accessdenied');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('1. /leagues (Discovery Page)', () => {
|
||||
test('Unauthenticated user can view league discovery page', async ({ page }) => {
|
||||
const capture = new ConsoleErrorCapture(page);
|
||||
capture.setAllowlist(CONSOLE_ALLOWLIST);
|
||||
|
||||
await page.goto('/leagues', { waitUntil: 'commit', timeout: 15000 });
|
||||
|
||||
// Verify page loads successfully
|
||||
expect(page.url()).toContain('/leagues');
|
||||
|
||||
// Verify featured leagues section displays
|
||||
await expect(page.getByTestId('featured-leagues-section')).toBeVisible();
|
||||
|
||||
// Verify league cards are present
|
||||
const leagueCards = page.getByTestId('league-card');
|
||||
await expect(leagueCards.first()).toBeVisible();
|
||||
|
||||
// Verify league cards show correct metadata
|
||||
const firstCard = leagueCards.first();
|
||||
await expect(firstCard.getByTestId('league-card-title')).toBeVisible();
|
||||
await expect(firstCard.getByTestId('league-card-next-race')).toBeVisible();
|
||||
await expect(firstCard.getByTestId('league-card-active-drivers')).toBeVisible();
|
||||
|
||||
// Verify category filters are present
|
||||
await expect(page.getByTestId('category-filters')).toBeVisible();
|
||||
|
||||
// Verify Quick Join/Follow buttons are present
|
||||
await expect(page.getByTestId('quick-join-button')).toBeVisible();
|
||||
|
||||
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('Authenticated user can view league discovery page', async ({ browser, request }) => {
|
||||
const { context, page } = await WebsiteAuthManager.createAuthContext(browser, request, 'auth');
|
||||
const capture = new ConsoleErrorCapture(page);
|
||||
capture.setAllowlist(CONSOLE_ALLOWLIST);
|
||||
|
||||
try {
|
||||
await page.goto('/leagues', { waitUntil: 'commit', timeout: 15000 });
|
||||
|
||||
// Verify page loads successfully
|
||||
expect(page.url()).toContain('/leagues');
|
||||
|
||||
// Verify featured leagues section displays
|
||||
await expect(page.getByTestId('featured-leagues-section')).toBeVisible();
|
||||
|
||||
// Verify league cards are present
|
||||
const leagueCards = page.getByTestId('league-card');
|
||||
await expect(leagueCards.first()).toBeVisible();
|
||||
|
||||
// Verify league cards show correct metadata
|
||||
const firstCard = leagueCards.first();
|
||||
await expect(firstCard.getByTestId('league-card-title')).toBeVisible();
|
||||
await expect(firstCard.getByTestId('league-card-next-race')).toBeVisible();
|
||||
await expect(firstCard.getByTestId('league-card-active-drivers')).toBeVisible();
|
||||
|
||||
// Verify category filters are present
|
||||
await expect(page.getByTestId('category-filters')).toBeVisible();
|
||||
|
||||
// Verify Quick Join/Follow buttons are present
|
||||
await expect(page.getByTestId('quick-join-button')).toBeVisible();
|
||||
|
||||
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Category filters work correctly', async ({ page }) => {
|
||||
const capture = new ConsoleErrorCapture(page);
|
||||
capture.setAllowlist(CONSOLE_ALLOWLIST);
|
||||
|
||||
await page.goto('/leagues', { waitUntil: 'commit', timeout: 15000 });
|
||||
|
||||
// Verify category filters are present
|
||||
await expect(page.getByTestId('category-filters')).toBeVisible();
|
||||
|
||||
// Click on a category filter
|
||||
const filterButton = page.getByTestId('category-filter-all');
|
||||
await filterButton.click();
|
||||
|
||||
// Wait for filter to apply
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Verify league cards are still visible after filtering
|
||||
const leagueCards = page.getByTestId('league-card');
|
||||
await expect(leagueCards.first()).toBeVisible();
|
||||
|
||||
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('2. /leagues/[id] (Overview Page)', () => {
|
||||
test('Unauthenticated user can view league overview', async ({ page }) => {
|
||||
const capture = new ConsoleErrorCapture(page);
|
||||
capture.setAllowlist(CONSOLE_ALLOWLIST);
|
||||
|
||||
await page.goto(leagueId, { waitUntil: 'commit', timeout: 15000 });
|
||||
|
||||
// Verify page loads successfully
|
||||
expect(page.url()).toContain('/leagues/');
|
||||
|
||||
// Verify league name is displayed
|
||||
await expect(page.getByTestId('league-detail-title')).toBeVisible();
|
||||
|
||||
// Verify stats section displays
|
||||
await expect(page.getByTestId('league-stats-section')).toBeVisible();
|
||||
|
||||
// Verify Next Race countdown displays correctly
|
||||
await expect(page.getByTestId('next-race-countdown')).toBeVisible();
|
||||
|
||||
// Verify Season progress bar shows correct percentage
|
||||
await expect(page.getByTestId('season-progress-bar')).toBeVisible();
|
||||
|
||||
// Verify Activity feed shows recent activity
|
||||
await expect(page.getByTestId('activity-feed')).toBeVisible();
|
||||
|
||||
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('Authenticated user can view league overview', async ({ browser, request }) => {
|
||||
const { context, page } = await WebsiteAuthManager.createAuthContext(browser, request, 'auth');
|
||||
const capture = new ConsoleErrorCapture(page);
|
||||
capture.setAllowlist(CONSOLE_ALLOWLIST);
|
||||
|
||||
try {
|
||||
await page.goto(leagueId, { waitUntil: 'commit', timeout: 15000 });
|
||||
|
||||
// Verify page loads successfully
|
||||
expect(page.url()).toContain('/leagues/');
|
||||
|
||||
// Verify league name is displayed
|
||||
await expect(page.getByTestId('league-detail-title')).toBeVisible();
|
||||
|
||||
// Verify stats section displays
|
||||
await expect(page.getByTestId('league-stats-section')).toBeVisible();
|
||||
|
||||
// Verify Next Race countdown displays correctly
|
||||
await expect(page.getByTestId('next-race-countdown')).toBeVisible();
|
||||
|
||||
// Verify Season progress bar shows correct percentage
|
||||
await expect(page.getByTestId('season-progress-bar')).toBeVisible();
|
||||
|
||||
// Verify Activity feed shows recent activity
|
||||
await expect(page.getByTestId('activity-feed')).toBeVisible();
|
||||
|
||||
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Admin user can view admin widgets', async ({ browser, request }) => {
|
||||
const { context, page } = await WebsiteAuthManager.createAuthContext(browser, request, 'admin');
|
||||
const capture = new ConsoleErrorCapture(page);
|
||||
capture.setAllowlist(CONSOLE_ALLOWLIST);
|
||||
|
||||
try {
|
||||
await page.goto(leagueId, { waitUntil: 'commit', timeout: 15000 });
|
||||
|
||||
// Verify page loads successfully
|
||||
expect(page.url()).toContain('/leagues/');
|
||||
|
||||
// Verify admin widgets are visible for authorized users
|
||||
await expect(page.getByTestId('admin-widgets')).toBeVisible();
|
||||
|
||||
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Stats match API values', async ({ page, request }) => {
|
||||
const capture = new ConsoleErrorCapture(page);
|
||||
capture.setAllowlist(CONSOLE_ALLOWLIST);
|
||||
|
||||
// Fetch API data
|
||||
const apiResponse = await request.get(`${process.env.API_BASE_URL || 'http://api:3000'}/leagues/${WebsiteRouteManager.IDs.LEAGUE}`);
|
||||
const apiData = await apiResponse.json();
|
||||
|
||||
// Navigate to league overview
|
||||
await page.goto(leagueId, { waitUntil: 'commit', timeout: 15000 });
|
||||
|
||||
// Verify stats match API values
|
||||
const membersStat = page.getByTestId('stat-members');
|
||||
const racesStat = page.getByTestId('stat-races');
|
||||
const avgSofStat = page.getByTestId('stat-avg-sof');
|
||||
|
||||
await expect(membersStat).toBeVisible();
|
||||
await expect(racesStat).toBeVisible();
|
||||
await expect(avgSofStat).toBeVisible();
|
||||
|
||||
// Verify the stats contain expected values from API
|
||||
const membersText = await membersStat.textContent();
|
||||
const racesText = await racesStat.textContent();
|
||||
const avgSofText = await avgSofStat.textContent();
|
||||
|
||||
// Basic validation - stats should not be empty
|
||||
expect(membersText).toBeTruthy();
|
||||
expect(racesText).toBeTruthy();
|
||||
expect(avgSofText).toBeTruthy();
|
||||
|
||||
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('3. /leagues/[id]/schedule (Schedule Page)', () => {
|
||||
const schedulePath = routeManager.resolvePathTemplate('/leagues/[id]/schedule', { id: WebsiteRouteManager.IDs.LEAGUE });
|
||||
|
||||
test('Unauthenticated user can view schedule page', async ({ page }) => {
|
||||
const capture = new ConsoleErrorCapture(page);
|
||||
capture.setAllowlist(CONSOLE_ALLOWLIST);
|
||||
|
||||
await page.goto(schedulePath, { waitUntil: 'commit', timeout: 15000 });
|
||||
|
||||
// Verify page loads successfully
|
||||
expect(page.url()).toContain('/schedule');
|
||||
|
||||
// Verify races are grouped by month
|
||||
await expect(page.getByTestId('schedule-month-group')).toBeVisible();
|
||||
|
||||
// Verify race list is present
|
||||
const raceItems = page.getByTestId('race-item');
|
||||
await expect(raceItems.first()).toBeVisible();
|
||||
|
||||
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('Authenticated user can view schedule page', async ({ browser, request }) => {
|
||||
const { context, page } = await WebsiteAuthManager.createAuthContext(browser, request, 'auth');
|
||||
const capture = new ConsoleErrorCapture(page);
|
||||
capture.setAllowlist(CONSOLE_ALLOWLIST);
|
||||
|
||||
try {
|
||||
await page.goto(schedulePath, { waitUntil: 'commit', timeout: 15000 });
|
||||
|
||||
// Verify page loads successfully
|
||||
expect(page.url()).toContain('/schedule');
|
||||
|
||||
// Verify races are grouped by month
|
||||
await expect(page.getByTestId('schedule-month-group')).toBeVisible();
|
||||
|
||||
// Verify race list is present
|
||||
const raceItems = page.getByTestId('race-item');
|
||||
await expect(raceItems.first()).toBeVisible();
|
||||
|
||||
// Verify Register/Withdraw buttons are present
|
||||
await expect(page.getByTestId('register-button')).toBeVisible();
|
||||
|
||||
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Admin user can view admin controls', async ({ browser, request }) => {
|
||||
const { context, page } = await WebsiteAuthManager.createAuthContext(browser, request, 'admin');
|
||||
const capture = new ConsoleErrorCapture(page);
|
||||
capture.setAllowlist(CONSOLE_ALLOWLIST);
|
||||
|
||||
try {
|
||||
await page.goto(schedulePath, { waitUntil: 'commit', timeout: 15000 });
|
||||
|
||||
// Verify page loads successfully
|
||||
expect(page.url()).toContain('/schedule');
|
||||
|
||||
// Verify admin controls are visible for authorized users
|
||||
await expect(page.getByTestId('admin-controls')).toBeVisible();
|
||||
|
||||
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Race detail modal shows correct data', async ({ page, request }) => {
|
||||
const capture = new ConsoleErrorCapture(page);
|
||||
capture.setAllowlist(CONSOLE_ALLOWLIST);
|
||||
|
||||
// Fetch API data
|
||||
const apiResponse = await request.get(`${process.env.API_BASE_URL || 'http://api:3000'}/leagues/${WebsiteRouteManager.IDs.LEAGUE}/schedule`);
|
||||
const apiData = await apiResponse.json();
|
||||
|
||||
// Navigate to schedule page
|
||||
await page.goto(schedulePath, { waitUntil: 'commit', timeout: 15000 });
|
||||
|
||||
// Click on a race item to open modal
|
||||
const raceItem = page.getByTestId('race-item').first();
|
||||
await raceItem.click();
|
||||
|
||||
// Verify modal is visible
|
||||
await expect(page.getByTestId('race-detail-modal')).toBeVisible();
|
||||
|
||||
// Verify modal contains race data
|
||||
const modalContent = page.getByTestId('race-detail-modal');
|
||||
await expect(modalContent.getByTestId('race-track')).toBeVisible();
|
||||
await expect(modalContent.getByTestId('race-car')).toBeVisible();
|
||||
await expect(modalContent.getByTestId('race-date')).toBeVisible();
|
||||
|
||||
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('4. /leagues/[id]/standings (Standings Page)', () => {
|
||||
const standingsPath = routeManager.resolvePathTemplate('/leagues/[id]/standings', { id: WebsiteRouteManager.IDs.LEAGUE });
|
||||
|
||||
test('Unauthenticated user can view standings page', async ({ page }) => {
|
||||
const capture = new ConsoleErrorCapture(page);
|
||||
capture.setAllowlist(CONSOLE_ALLOWLIST);
|
||||
|
||||
await page.goto(standingsPath, { waitUntil: 'commit', timeout: 15000 });
|
||||
|
||||
// Verify page loads successfully
|
||||
expect(page.url()).toContain('/standings');
|
||||
|
||||
// Verify standings table is present
|
||||
await expect(page.getByTestId('standings-table')).toBeVisible();
|
||||
|
||||
// Verify trend indicators display correctly
|
||||
await expect(page.getByTestId('trend-indicator')).toBeVisible();
|
||||
|
||||
// Verify championship stats show correct data
|
||||
await expect(page.getByTestId('championship-stats')).toBeVisible();
|
||||
|
||||
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('Authenticated user can view standings page', async ({ browser, request }) => {
|
||||
const { context, page } = await WebsiteAuthManager.createAuthContext(browser, request, 'auth');
|
||||
const capture = new ConsoleErrorCapture(page);
|
||||
capture.setAllowlist(CONSOLE_ALLOWLIST);
|
||||
|
||||
try {
|
||||
await page.goto(standingsPath, { waitUntil: 'commit', timeout: 15000 });
|
||||
|
||||
// Verify page loads successfully
|
||||
expect(page.url()).toContain('/standings');
|
||||
|
||||
// Verify standings table is present
|
||||
await expect(page.getByTestId('standings-table')).toBeVisible();
|
||||
|
||||
// Verify trend indicators display correctly
|
||||
await expect(page.getByTestId('trend-indicator')).toBeVisible();
|
||||
|
||||
// Verify championship stats show correct data
|
||||
await expect(page.getByTestId('championship-stats')).toBeVisible();
|
||||
|
||||
// Verify team standings toggle is present
|
||||
await expect(page.getByTestId('team-standings-toggle')).toBeVisible();
|
||||
|
||||
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Team standings toggle works correctly', async ({ page }) => {
|
||||
const capture = new ConsoleErrorCapture(page);
|
||||
capture.setAllowlist(CONSOLE_ALLOWLIST);
|
||||
|
||||
await page.goto(standingsPath, { waitUntil: 'commit', timeout: 15000 });
|
||||
|
||||
// Verify team standings toggle is present
|
||||
await expect(page.getByTestId('team-standings-toggle')).toBeVisible();
|
||||
|
||||
// Click on team standings toggle
|
||||
const toggle = page.getByTestId('team-standings-toggle');
|
||||
await toggle.click();
|
||||
|
||||
// Wait for toggle to apply
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Verify team standings are visible
|
||||
await expect(page.getByTestId('team-standings-table')).toBeVisible();
|
||||
|
||||
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('Drop weeks are marked correctly', async ({ page }) => {
|
||||
const capture = new ConsoleErrorCapture(page);
|
||||
capture.setAllowlist(CONSOLE_ALLOWLIST);
|
||||
|
||||
await page.goto(standingsPath, { waitUntil: 'commit', timeout: 15000 });
|
||||
|
||||
// Verify drop weeks are marked
|
||||
const dropWeeks = page.getByTestId('drop-week-marker');
|
||||
await expect(dropWeeks.first()).toBeVisible();
|
||||
|
||||
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('Standings data matches API values', async ({ page, request }) => {
|
||||
const capture = new ConsoleErrorCapture(page);
|
||||
capture.setAllowlist(CONSOLE_ALLOWLIST);
|
||||
|
||||
// Fetch API data
|
||||
const apiResponse = await request.get(`${process.env.API_BASE_URL || 'http://api:3000'}/leagues/${WebsiteRouteManager.IDs.LEAGUE}/standings`);
|
||||
const apiData = await apiResponse.json();
|
||||
|
||||
// Navigate to standings page
|
||||
await page.goto(standingsPath, { waitUntil: 'commit', timeout: 15000 });
|
||||
|
||||
// Verify standings table is present
|
||||
await expect(page.getByTestId('standings-table')).toBeVisible();
|
||||
|
||||
// Verify table rows match API data
|
||||
const tableRows = page.getByTestId('standings-row');
|
||||
const rowCount = await tableRows.count();
|
||||
|
||||
// Basic validation - should have at least one row
|
||||
expect(rowCount).toBeGreaterThan(0);
|
||||
|
||||
// Verify first row contains expected data
|
||||
const firstRow = tableRows.first();
|
||||
await expect(firstRow.getByTestId('standing-position')).toBeVisible();
|
||||
await expect(firstRow.getByTestId('standing-driver')).toBeVisible();
|
||||
await expect(firstRow.getByTestId('standing-points')).toBeVisible();
|
||||
|
||||
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('5. /leagues/[id]/roster (Roster Page)', () => {
|
||||
const rosterPath = routeManager.resolvePathTemplate('/leagues/[id]/roster', { id: WebsiteRouteManager.IDs.LEAGUE });
|
||||
|
||||
test('Unauthenticated user can view roster page', async ({ page }) => {
|
||||
const capture = new ConsoleErrorCapture(page);
|
||||
capture.setAllowlist(CONSOLE_ALLOWLIST);
|
||||
|
||||
await page.goto(rosterPath, { waitUntil: 'commit', timeout: 15000 });
|
||||
|
||||
// Verify page loads successfully
|
||||
expect(page.url()).toContain('/roster');
|
||||
|
||||
// Verify driver cards are present
|
||||
const driverCards = page.getByTestId('driver-card');
|
||||
await expect(driverCards.first()).toBeVisible();
|
||||
|
||||
// Verify driver cards show correct stats
|
||||
const firstCard = driverCards.first();
|
||||
await expect(firstCard.getByTestId('driver-card-name')).toBeVisible();
|
||||
await expect(firstCard.getByTestId('driver-card-stats')).toBeVisible();
|
||||
|
||||
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('Authenticated user can view roster page', async ({ browser, request }) => {
|
||||
const { context, page } = await WebsiteAuthManager.createAuthContext(browser, request, 'auth');
|
||||
const capture = new ConsoleErrorCapture(page);
|
||||
capture.setAllowlist(CONSOLE_ALLOWLIST);
|
||||
|
||||
try {
|
||||
await page.goto(rosterPath, { waitUntil: 'commit', timeout: 15000 });
|
||||
|
||||
// Verify page loads successfully
|
||||
expect(page.url()).toContain('/roster');
|
||||
|
||||
// Verify driver cards are present
|
||||
const driverCards = page.getByTestId('driver-card');
|
||||
await expect(driverCards.first()).toBeVisible();
|
||||
|
||||
// Verify driver cards show correct stats
|
||||
const firstCard = driverCards.first();
|
||||
await expect(firstCard.getByTestId('driver-card-name')).toBeVisible();
|
||||
await expect(firstCard.getByTestId('driver-card-stats')).toBeVisible();
|
||||
|
||||
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Admin user can view admin actions', async ({ browser, request }) => {
|
||||
const { context, page } = await WebsiteAuthManager.createAuthContext(browser, request, 'admin');
|
||||
const capture = new ConsoleErrorCapture(page);
|
||||
capture.setAllowlist(CONSOLE_ALLOWLIST);
|
||||
|
||||
try {
|
||||
await page.goto(rosterPath, { waitUntil: 'commit', timeout: 15000 });
|
||||
|
||||
// Verify page loads successfully
|
||||
expect(page.url()).toContain('/roster');
|
||||
|
||||
// Verify admin actions are visible for authorized users
|
||||
await expect(page.getByTestId('admin-actions')).toBeVisible();
|
||||
|
||||
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Roster data matches API values', async ({ page, request }) => {
|
||||
const capture = new ConsoleErrorCapture(page);
|
||||
capture.setAllowlist(CONSOLE_ALLOWLIST);
|
||||
|
||||
// Fetch API data
|
||||
const apiResponse = await request.get(`${process.env.API_BASE_URL || 'http://api:3000'}/leagues/${WebsiteRouteManager.IDs.LEAGUE}/memberships`);
|
||||
const apiData = await apiResponse.json();
|
||||
|
||||
// Navigate to roster page
|
||||
await page.goto(rosterPath, { waitUntil: 'commit', timeout: 15000 });
|
||||
|
||||
// Verify driver cards are present
|
||||
const driverCards = page.getByTestId('driver-card');
|
||||
const cardCount = await driverCards.count();
|
||||
|
||||
// Basic validation - should have at least one driver
|
||||
expect(cardCount).toBeGreaterThan(0);
|
||||
|
||||
// Verify first card contains expected data
|
||||
const firstCard = driverCards.first();
|
||||
await expect(firstCard.getByTestId('driver-card-name')).toBeVisible();
|
||||
await expect(firstCard.getByTestId('driver-card-stats')).toBeVisible();
|
||||
|
||||
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('6. Navigation Between League Pages', () => {
|
||||
test('User can navigate from discovery to league overview', async ({ page }) => {
|
||||
const capture = new ConsoleErrorCapture(page);
|
||||
capture.setAllowlist(CONSOLE_ALLOWLIST);
|
||||
|
||||
// Navigate to leagues discovery page
|
||||
await page.goto('/leagues', { waitUntil: 'commit', timeout: 15000 });
|
||||
|
||||
// Click on a league card
|
||||
const leagueCard = page.getByTestId('league-card').first();
|
||||
await leagueCard.click();
|
||||
|
||||
// Verify navigation to league overview
|
||||
await page.waitForURL(/\/leagues\/[^/]+$/, { timeout: 15000 });
|
||||
expect(page.url()).toMatch(/\/leagues\/[^/]+$/);
|
||||
|
||||
// Verify league overview content is visible
|
||||
await expect(page.getByTestId('league-detail-title')).toBeVisible();
|
||||
|
||||
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('User can navigate between league sub-pages', async ({ page }) => {
|
||||
const capture = new ConsoleErrorCapture(page);
|
||||
capture.setAllowlist(CONSOLE_ALLOWLIST);
|
||||
|
||||
// Navigate to league overview
|
||||
await page.goto(leagueId, { waitUntil: 'commit', timeout: 15000 });
|
||||
|
||||
// Click on Schedule tab
|
||||
const scheduleTab = page.getByTestId('schedule-tab');
|
||||
await scheduleTab.click();
|
||||
|
||||
// Verify navigation to schedule page
|
||||
await page.waitForURL(/\/leagues\/[^/]+\/schedule$/, { timeout: 15000 });
|
||||
expect(page.url()).toMatch(/\/leagues\/[^/]+\/schedule$/);
|
||||
|
||||
// Click on Standings tab
|
||||
const standingsTab = page.getByTestId('standings-tab');
|
||||
await standingsTab.click();
|
||||
|
||||
// Verify navigation to standings page
|
||||
await page.waitForURL(/\/leagues\/[^/]+\/standings$/, { timeout: 15000 });
|
||||
expect(page.url()).toMatch(/\/leagues\/[^/]+\/standings$/);
|
||||
|
||||
// Click on Roster tab
|
||||
const rosterTab = page.getByTestId('roster-tab');
|
||||
await rosterTab.click();
|
||||
|
||||
// Verify navigation to roster page
|
||||
await page.waitForURL(/\/leagues\/[^/]+\/roster$/, { timeout: 15000 });
|
||||
expect(page.url()).toMatch(/\/leagues\/[^/]+\/roster$/);
|
||||
|
||||
expect(capture.getUnexpectedErrors(), capture.format()).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user