diff --git a/e2e/schedule-tab.test.ts b/e2e/schedule-tab.test.ts new file mode 100644 index 0000000..f8c497a --- /dev/null +++ b/e2e/schedule-tab.test.ts @@ -0,0 +1,257 @@ +/** + * 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(); + }); +});