Files
euchre_camp/e2e/elo-ratings.test.ts
T

398 lines
13 KiB
TypeScript

/**
* 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<number[]> {
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('networkidle');
// 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('networkidle');
}
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('networkidle');
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('networkidle');
}
// 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('networkidle');
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('networkidle');
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('networkidle');
// 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();
});
});