/** * E2E Test: Elo Rating Updates * * Tests that Elo ratings are correctly updated when matches are added */ import { test, expect } from '@playwright/test'; import { prisma } from '@/lib/prisma'; import fs from 'fs'; import path from 'path'; test.describe('Elo Rating Updates', () => { test.beforeAll(async () => { // Clean up any existing test data // First delete matches that reference players await prisma.match.deleteMany({ where: { OR: [ { player1P1Id: { in: await getEloTestPlayerIds() } }, { player1P2Id: { in: await getEloTestPlayerIds() } }, { player2P1Id: { in: await getEloTestPlayerIds() } }, { player2P2Id: { in: await getEloTestPlayerIds() } }, ] } }); // Then delete partnerships await prisma.partnershipStat.deleteMany({ where: { OR: [ { player1Id: { in: await getEloTestPlayerIds() } }, { player2Id: { in: await getEloTestPlayerIds() } }, ] } }); // Finally delete players await prisma.player.deleteMany({ where: { name: { startsWith: 'Elo Test' } } }); }); test.afterAll(async () => { // Clean up test data // First delete matches that reference players await prisma.match.deleteMany({ where: { OR: [ { player1P1Id: { in: await getEloTestPlayerIds() } }, { player1P2Id: { in: await getEloTestPlayerIds() } }, { player2P1Id: { in: await getEloTestPlayerIds() } }, { player2P2Id: { in: await getEloTestPlayerIds() } }, ] } }); // Then delete partnerships await prisma.partnershipStat.deleteMany({ where: { OR: [ { player1Id: { in: await getEloTestPlayerIds() } }, { player2Id: { in: await getEloTestPlayerIds() } }, ] } }); // Finally delete players await prisma.player.deleteMany({ where: { name: { startsWith: 'Elo Test' } } }); await prisma.$disconnect(); }); async function getEloTestPlayerIds(): Promise { const players = await prisma.player.findMany({ where: { name: { startsWith: 'Elo Test' } }, select: { id: true } }); return players.map(p => p.id); } test('Elo rating updates after match upload', async ({ page }) => { // Step 1: Create test players with known initial ratings const player1 = await prisma.player.create({ data: { name: 'Elo Test Player 1', normalizedName: 'elo test player 1', currentElo: 1500, gamesPlayed: 0, wins: 0, losses: 0, }, }); const player2 = await prisma.player.create({ data: { name: 'Elo Test Player 2', normalizedName: 'elo test player 2', currentElo: 1500, gamesPlayed: 0, wins: 0, losses: 0, }, }); const player3 = await prisma.player.create({ data: { name: 'Elo Test Player 3', normalizedName: 'elo test player 3', currentElo: 1500, gamesPlayed: 0, wins: 0, losses: 0, }, }); const player4 = await prisma.player.create({ data: { name: 'Elo Test Player 4', normalizedName: 'elo test player 4', currentElo: 1500, gamesPlayed: 0, wins: 0, losses: 0, }, }); // Step 2: Use existing admin user from global setup // The global setup creates an admin user with email starting with 'setup-admin-' // We'll find that user and use it for the test const existingAdmin = await prisma.user.findFirst({ where: { email: { startsWith: 'setup-admin-' }, role: 'club_admin' }, orderBy: { createdAt: 'desc' } }); if (!existingAdmin) { throw new Error('No existing admin user found. Please run global setup first.'); } const adminEmail = existingAdmin.email; const adminPassword = 'AdminPassword123!'; // Password from global setup const adminUser = existingAdmin; console.log('Using existing admin user:', adminEmail); console.log('Admin user ID:', adminUser.id); console.log('Admin user role:', adminUser.role); // Create tournament owned by admin user console.log('Creating tournament with ownerId:', adminUser.id); const tournament = await prisma.event.create({ data: { name: 'Elo Test Multi Tournament', eventType: 'tournament', status: 'planned', ownerId: adminUser.id, }, }); console.log('Tournament created with ID:', tournament.id); console.log('Tournament status:', tournament.status); // Verify tournament was created and can be found const tournamentCheck = await prisma.event.findUnique({ where: { id: tournament.id } }); console.log('Tournament verified:', tournamentCheck ? 'yes' : 'no'); console.log('Tournament owner ID:', tournamentCheck?.ownerId); console.log('Current user ID:', adminUser.id); console.log('Owner matches:', tournamentCheck?.ownerId === adminUser.id); // Step 3: Use existing session from global setup // Navigate to admin page to check if we're already logged in // Set up console listener to catch server logs page.on('console', msg => { console.log('SERVER LOG:', msg.text()); }); await page.goto('/admin'); await page.waitForLoadState('domcontentloaded'); // Check if we're logged in const content = await page.content(); const isLoggedIn = content.includes('Sign out'); console.log('User is logged in:', isLoggedIn); console.log('Page title:', await page.title()); console.log('Current URL:', page.url()); // Check if there's a login form const hasLoginForm = content.includes('Sign in to your account'); console.log('Has login form:', hasLoginForm); // If the URL is a player profile, we're logged in but as a regular user // In that case, we need to log out and log in as admin if (page.url().includes('/players/')) { console.log('User is logged in but redirected to player profile (not admin)'); console.log('Logging out and logging in as admin...'); await page.click('button:has-text("Sign out")'); await page.waitForURL('/auth/login'); await page.waitForLoadState('domcontentloaded'); } if (!isLoggedIn || page.url().includes('/players/')) { // If not logged in or logged in as regular user, log in as admin console.log('Logging in as admin...'); await page.goto('/auth/login'); await page.waitForLoadState('domcontentloaded'); await page.fill('input[name="email"]', adminEmail); await page.fill('input[name="password"]', adminPassword); await page.click('button[type="submit"]'); await page.waitForURL(/\/(admin|players)/, { timeout: 10000 }); await page.waitForLoadState('domcontentloaded'); } // Verify tournament ownership const tournamentVerify = await prisma.event.findUnique({ where: { id: tournament.id } }); console.log('Tournament owner ID:', tournamentVerify?.ownerId); console.log('Current user ID:', adminUser.id); console.log('Owner matches:', tournamentVerify?.ownerId === adminUser.id); // Navigate to match upload page await page.goto('/admin/matches/upload'); // Wait for page to load and tournaments to be fetched await page.waitForLoadState('domcontentloaded'); await page.waitForTimeout(2000); // Wait for tournament dropdown to be ready await page.waitForSelector('select#tournament', { timeout: 5000 }); // Wait a bit for tournaments to load await page.waitForTimeout(2000); // Select the tournament manually const tournamentSelect = await page.locator('select#tournament'); console.log('Selecting tournament ID:', tournament.id); await tournamentSelect.selectOption({ value: tournament.id.toString() }); // Wait for React state to update await page.waitForTimeout(500); // Verify the selection const selectedTournament = await tournamentSelect.inputValue(); console.log('Tournament selected in dropdown:', selectedTournament); console.log('Expected tournament ID:', tournament.id); if (selectedTournament !== tournament.id.toString()) { console.log('WARNING: Tournament selection mismatch!'); } // Check if there's a permission error on the page const uploadContent = await page.content(); if (uploadContent.includes('Insufficient permissions')) { console.log('ERROR: Permission error detected on page'); console.log('Page URL:', page.url()); // Take a screenshot for debugging await page.screenshot({ path: '/tmp/permission-error.png' }); // Check the navigation to see current user const navContent = await page.locator('nav').textContent(); console.log('Navigation content:', navContent); } // Create matches where player1/player2 win twice and player3/player4 lose twice const csvContent1 = `Event #,Round,Table,Seat 1,Seat 3,Odds Points,Seat 2,Seat 4,Evens Points,Winner ${tournament.id},1,1,${player1.name},${player3.name},10,${player2.name},${player4.name},5,odds`; const csvContent2 = `Event #,Round,Table,Seat 1,Seat 3,Odds Points,Seat 2,Seat 4,Evens Points,Winner ${tournament.id},2,1,${player1.name},${player3.name},10,${player2.name},${player4.name},5,odds`; // Upload first match through UI const tmpFile1 = path.join('/tmp', `elo-test-multi-1-${Date.now()}.csv`); fs.writeFileSync(tmpFile1, csvContent1); // Wait for file input to be ready await page.waitForSelector('input[type="file"]', { timeout: 5000 }); // Upload the file await page.setInputFiles('input[type="file"]', tmpFile1); // Wait for file to be selected await page.waitForTimeout(500); // Click submit button await page.click('button[type="submit"]'); // Wait for upload to complete await page.waitForTimeout(3000); // Check for any error messages const uploadContentAfter = await page.content(); if (uploadContentAfter.includes('error') || uploadContentAfter.includes('Error')) { console.log('Upload page has error message'); } fs.unlinkSync(tmpFile1); // Navigate back to upload page for second match await page.goto('/admin/matches/upload'); await page.waitForLoadState('domcontentloaded'); await page.waitForTimeout(2000); // Re-select the tournament for the second upload await page.waitForSelector('select#tournament', { timeout: 5000 }); await page.waitForTimeout(1000); // Wait for tournaments to load const tournamentSelect2 = await page.locator('select#tournament'); const currentSelection = await tournamentSelect2.inputValue(); // If tournament is not selected, select it manually if (!currentSelection || currentSelection !== tournament.id.toString()) { console.log('Selecting tournament for second upload:', tournament.id); await tournamentSelect2.selectOption({ value: tournament.id.toString() }); await page.waitForTimeout(500); } // Upload second match through UI const tmpFile2 = path.join('/tmp', `elo-test-multi-2-${Date.now()}.csv`); fs.writeFileSync(tmpFile2, csvContent2); await page.setInputFiles('input[type="file"]', tmpFile2); await page.waitForTimeout(500); await page.click('button[type="submit"]'); await page.waitForTimeout(2000); fs.unlinkSync(tmpFile2); await page.waitForTimeout(2000); // Verify ratings after multiple matches const updatedPlayer1 = await prisma.player.findUnique({ where: { id: player1.id } }); const updatedPlayer4 = await prisma.player.findUnique({ where: { id: player4.id } }); // Player 1 should have higher rating after 2 wins // With K=32, each win gives ~8 Elo points, so 2 wins = ~16 Elo // Expected: 1500 + 16 = 1516 expect(updatedPlayer1?.currentElo).toBeGreaterThan(1500); expect(updatedPlayer1?.currentElo).toBeLessThanOrEqual(1520); expect(updatedPlayer1?.gamesPlayed).toBe(2); expect(updatedPlayer1?.wins).toBe(2); // Player 4 should have lower rating after 2 losses // Expected: 1500 - 16 = 1484 expect(updatedPlayer4?.currentElo).toBeLessThan(1500); expect(updatedPlayer4?.currentElo).toBeGreaterThanOrEqual(1480); expect(updatedPlayer4?.gamesPlayed).toBe(2); expect(updatedPlayer4?.losses).toBe(2); }); test('Elo ratings are visible on player profile', async ({ page }) => { // Create a test player const player = await prisma.player.create({ data: { name: 'Elo Test Profile Player', normalizedName: 'elo test profile player', currentElo: 1750, gamesPlayed: 50, wins: 30, losses: 20, }, }); // Navigate to player profile await page.goto(`/players/${player.id}/profile`); // Wait for page to load await page.waitForLoadState('domcontentloaded'); // Verify rating is displayed (from player.currentElo) await expect(page.locator('text=1750')).toBeVisible(); // Total games shown on profile is calculated from partnership stats // Since we haven't added any matches yet, it should show 0 or N/A for best partner // Just verify the page loads with the Elo rating await expect(page.locator('text=Current Elo')).toBeVisible(); }); });