bb6be245b7
## Summary This PR implements the tournament schedule tab functionality and fixes all remaining E2E test failures. ### Changes Included 1. **Tournament Schedule Feature** - Added tournament schedule page at `/admin/tournaments/[id]/schedule` - Implemented "Generate Schedule" button functionality - Added schedule generation logic for round-robin tournaments 2. **E2E Test Fixes** - Fixed database connection issues in production builds - Improved test reliability with better error handling and debugging - Updated test infrastructure to use environment variables instead of hardcoded values 3. **CI/CD Updates** - Added E2E test job to PR workflow - Configured tests to run against development database - Moved database password to Gitea secrets 4. **Code Quality** - Removed hardcoded passwords from codebase - Improved Prisma client configuration - Enhanced authentication and navigation components ### Test Results All 16 E2E test scenarios are now passing: - Authentication tests: ✅ - Registration tests: ✅ - Tournament schedule tests: ✅ - Player schedule tests: ✅ - Admin navigation tests: ✅ ### Database Configuration - Tests run against `euchre_camp_dev` database - Production builds use environment variables for database configuration - Database password stored in Gitea secrets as `DB_PASSWORD` ### CI Pipeline The PR workflow now includes: 1. Unit tests 2. E2E tests (using production build) 3. Version bump analysis E2E tests must pass before PR can be merged. Reviewed-on: #27 Co-authored-by: David Gwilliam <dhgwilliam@gmail.com> Co-committed-by: David Gwilliam <dhgwilliam@gmail.com>
234 lines
7.8 KiB
TypeScript
234 lines
7.8 KiB
TypeScript
/**
|
|
* Issue #7: Schedule Tab
|
|
* Acceptance Test: Schedule Generation and Display
|
|
*
|
|
* User Story: As a tournament admin, I want a Schedule tab to view round matchups
|
|
*
|
|
* Acceptance Criteria:
|
|
* - Schedule tab added to tournament detail page
|
|
* - Displays round-robin schedule with round numbers
|
|
* - Round-robin schedule can be generated from teams
|
|
* - Matches are linkable to result entry
|
|
*/
|
|
|
|
import { test, expect } from '@playwright/test';
|
|
import { prisma } from '@/lib/prisma';
|
|
|
|
function getTestCredentials() {
|
|
const timestamp = Date.now();
|
|
return {
|
|
email: `schedule-admin-${timestamp}@example.com`,
|
|
password: 'AdminPassword123!',
|
|
name: `Schedule Admin ${timestamp}`,
|
|
};
|
|
}
|
|
|
|
test.describe.serial('Issue #7: Schedule Tab', () => {
|
|
let testEmail: string;
|
|
let testPassword: string;
|
|
let tournamentId: number;
|
|
|
|
test.beforeAll(async () => {
|
|
const credentials = getTestCredentials();
|
|
testEmail = credentials.email;
|
|
testPassword = credentials.password;
|
|
|
|
// Create admin user via API
|
|
const response = await fetch('http://localhost:3000/api/auth/sign-up/email', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
Origin: 'http://localhost:3000',
|
|
},
|
|
body: JSON.stringify({
|
|
email: testEmail,
|
|
password: testPassword,
|
|
name: credentials.name,
|
|
}),
|
|
});
|
|
|
|
console.log('Schedule test user creation response:', response.status);
|
|
|
|
// Update user to club_admin role
|
|
const user = await prisma.user.findUnique({
|
|
where: { email: testEmail },
|
|
});
|
|
if (user) {
|
|
await prisma.user.update({
|
|
where: { id: user.id },
|
|
data: { role: 'club_admin' },
|
|
});
|
|
}
|
|
|
|
// Create players for teams
|
|
const players = await Promise.all([
|
|
prisma.player.create({
|
|
data: { name: 'Alice', normalizedName: 'alice' },
|
|
}),
|
|
prisma.player.create({
|
|
data: { name: 'Bob', normalizedName: 'bob' },
|
|
}),
|
|
prisma.player.create({
|
|
data: { name: 'Charlie', normalizedName: 'charlie' },
|
|
}),
|
|
prisma.player.create({
|
|
data: { name: 'Diana', normalizedName: 'diana' },
|
|
}),
|
|
]);
|
|
|
|
// Create tournament
|
|
const tournament = await prisma.event.create({
|
|
data: {
|
|
name: `Schedule Test Tournament ${Date.now()}`,
|
|
format: 'round_robin',
|
|
ownerId: user?.id,
|
|
},
|
|
});
|
|
tournamentId = tournament.id;
|
|
|
|
// Register participants (teams are now ephemeral and generated during schedule creation)
|
|
await Promise.all(
|
|
players.map((player) =>
|
|
prisma.eventParticipant.create({
|
|
data: {
|
|
eventId: tournamentId,
|
|
playerId: player.id,
|
|
status: 'registered',
|
|
registrationDate: new Date(),
|
|
},
|
|
})
|
|
)
|
|
);
|
|
});
|
|
|
|
test.afterAll(async () => {
|
|
try {
|
|
// Clean up schedule data
|
|
if (tournamentId) {
|
|
await prisma.bracketMatchup.deleteMany({ where: { eventId: tournamentId } });
|
|
await prisma.tournamentRound.deleteMany({ where: { eventId: tournamentId } });
|
|
await prisma.eventParticipant.deleteMany({ where: { eventId: tournamentId } });
|
|
await prisma.event.delete({ where: { id: tournamentId } }).catch(() => {});
|
|
}
|
|
|
|
// Clean up user
|
|
const user = await prisma.user.findUnique({ where: { email: testEmail } });
|
|
if (user) {
|
|
await prisma.user.delete({ where: { id: user.id } });
|
|
}
|
|
|
|
// Clean up players
|
|
await prisma.player.deleteMany({
|
|
where: { normalizedName: { in: ['alice', 'bob', 'charlie', 'diana'] } },
|
|
});
|
|
} catch (error) {
|
|
console.error('Cleanup error:', error);
|
|
}
|
|
});
|
|
|
|
test('Schedule tab link exists on tournament detail page', async ({ page }) => {
|
|
// Login
|
|
await page.goto('http://localhost:3000/auth/login');
|
|
await page.fill('input[name="email"]', testEmail);
|
|
await page.fill('input[name="password"]', testPassword);
|
|
await page.click('button[type="submit"]');
|
|
await page.waitForURL(/\/(admin|players)/, { timeout: 10000 });
|
|
|
|
// Navigate to tournament detail
|
|
await page.goto(`http://localhost:3000/admin/tournaments/${tournamentId}`);
|
|
|
|
// Check Schedule tab link exists
|
|
const scheduleLink = page.locator('a', { hasText: 'Schedule' });
|
|
await expect(scheduleLink).toBeVisible();
|
|
});
|
|
|
|
test('Schedule page loads with no schedule message', async ({ page }) => {
|
|
// Login
|
|
await page.goto('http://localhost:3000/auth/login');
|
|
await page.fill('input[name="email"]', testEmail);
|
|
await page.fill('input[name="password"]', testPassword);
|
|
await page.click('button[type="submit"]');
|
|
await page.waitForURL(/\/(admin|players)/, { timeout: 10000 });
|
|
|
|
// Navigate to schedule page
|
|
await page.goto(`http://localhost:3000/admin/tournaments/${tournamentId}/schedule`);
|
|
|
|
// Check page content
|
|
await expect(page.locator('h1')).toContainText('Tournament Schedule');
|
|
await expect(page.locator('text=No Schedule Generated')).toBeVisible();
|
|
await expect(page.locator('button', { hasText: 'Generate Schedule' })).toBeVisible();
|
|
});
|
|
|
|
test('Generate schedule creates rounds and matchups', async ({ page }) => {
|
|
// Login
|
|
await page.goto('http://localhost:3000/auth/login');
|
|
await page.fill('input[name="email"]', testEmail);
|
|
await page.fill('input[name="password"]', testPassword);
|
|
await page.click('button[type="submit"]');
|
|
await page.waitForURL(/\/(admin|players)/, { timeout: 10000 });
|
|
|
|
// Navigate to schedule page
|
|
await page.goto(`http://localhost:3000/admin/tournaments/${tournamentId}/schedule`);
|
|
|
|
// Click generate schedule
|
|
await page.click('button:has-text("Generate Schedule")');
|
|
|
|
// Wait for success message or page reload
|
|
await page.waitForTimeout(3000);
|
|
|
|
// Verify rounds were created in database
|
|
const rounds = await prisma.tournamentRound.findMany({
|
|
where: { eventId: tournamentId },
|
|
});
|
|
expect(rounds.length).toBeGreaterThan(0);
|
|
|
|
// Verify matchups were created
|
|
const matchups = await prisma.bracketMatchup.findMany({
|
|
where: { eventId: tournamentId },
|
|
});
|
|
expect(matchups.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test('Schedule page displays generated rounds and matchups', async ({ page }) => {
|
|
// Login
|
|
await page.goto('http://localhost:3000/auth/login');
|
|
await page.fill('input[name="email"]', testEmail);
|
|
await page.fill('input[name="password"]', testPassword);
|
|
await page.click('button[type="submit"]');
|
|
await page.waitForURL(/\/(admin|players)/, { timeout: 10000 });
|
|
|
|
// Navigate to schedule page
|
|
await page.goto(`http://localhost:3000/admin/tournaments/${tournamentId}/schedule`);
|
|
|
|
// Check that rounds are displayed
|
|
await expect(page.locator('text=Round 1')).toBeVisible();
|
|
|
|
// Check that team names are displayed
|
|
await expect(page.locator('text=Alice + Bob')).toBeVisible();
|
|
await expect(page.locator('text=Charlie + Diana')).toBeVisible();
|
|
|
|
// Check that "Enter Result" link exists for pending matchups
|
|
await expect(page.locator('a:has-text("Enter Result")')).toBeVisible();
|
|
});
|
|
|
|
test('Schedule API returns rounds with matchups', async ({ page }) => {
|
|
// Login
|
|
await page.goto('http://localhost:3000/auth/login');
|
|
await page.fill('input[name="email"]', testEmail);
|
|
await page.fill('input[name="password"]', testPassword);
|
|
await page.click('button[type="submit"]');
|
|
await page.waitForURL(/\/(admin|players)/, { timeout: 10000 });
|
|
|
|
// Call the schedule API
|
|
const response = await page.request.get(
|
|
`http://localhost:3000/api/tournaments/${tournamentId}/schedule`
|
|
);
|
|
expect(response.ok()).toBe(true);
|
|
|
|
const data = await response.json();
|
|
expect(data.rounds).toBeDefined();
|
|
expect(data.rounds.length).toBeGreaterThan(0);
|
|
expect(data.rounds[0].bracketMatchups).toBeDefined();
|
|
});
|
|
});
|