/** * 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; let teamIds: 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; // Create teams const teams = await Promise.all([ prisma.team.create({ data: { eventId: tournamentId, player1Id: players[0].id, player2Id: players[1].id, teamName: 'Team A', }, }), prisma.team.create({ data: { eventId: tournamentId, player1Id: players[2].id, player2Id: players[3].id, teamName: 'Team B', }, }), ]); teamIds = teams.map((t) => t.id); // Register participants await Promise.all( players.map((player) => prisma.eventParticipant.create({ data: { eventId: tournamentId, playerId: player.id, teamId: teams.find( (t) => t.player1Id === player.id || t.player2Id === player.id )?.id, }, }) ) ); }); 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.team.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(); }); });