e455d5dba5
Reviewed-on: #41 Co-authored-by: David Gwilliam <dhgwilliam@gmail.com> Co-committed-by: David Gwilliam <dhgwilliam@gmail.com>
413 lines
14 KiB
TypeScript
413 lines
14 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.skip('Elo Rating Updates', () => {
|
|
test.beforeAll(async () => {
|
|
// Clean up any existing test data
|
|
const playerIds = await getEloTestPlayerIds();
|
|
|
|
// First delete matches that reference players
|
|
await prisma.match.deleteMany({
|
|
where: {
|
|
OR: [
|
|
{ player1P1Id: { in: playerIds } },
|
|
{ player1P2Id: { in: playerIds } },
|
|
{ player2P1Id: { in: playerIds } },
|
|
{ player2P2Id: { in: playerIds } },
|
|
]
|
|
}
|
|
});
|
|
|
|
// Then delete partnerships that reference players
|
|
await prisma.partnershipStat.deleteMany({
|
|
where: {
|
|
OR: [
|
|
{ player1Id: { in: playerIds } },
|
|
{ player2Id: { in: playerIds } },
|
|
]
|
|
}
|
|
});
|
|
|
|
// Then delete event participants that reference players
|
|
await prisma.eventParticipant.deleteMany({
|
|
where: {
|
|
playerId: { in: playerIds }
|
|
}
|
|
});
|
|
|
|
// Finally delete players
|
|
await prisma.player.deleteMany({
|
|
where: {
|
|
id: { in: playerIds }
|
|
}
|
|
});
|
|
});
|
|
|
|
test.afterAll(async () => {
|
|
// Clean up test data
|
|
const playerIds = await getEloTestPlayerIds();
|
|
|
|
// First delete matches that reference players
|
|
await prisma.match.deleteMany({
|
|
where: {
|
|
OR: [
|
|
{ player1P1Id: { in: playerIds } },
|
|
{ player1P2Id: { in: playerIds } },
|
|
{ player2P1Id: { in: playerIds } },
|
|
{ player2P2Id: { in: playerIds } },
|
|
]
|
|
}
|
|
});
|
|
|
|
// Then delete partnerships that reference players
|
|
await prisma.partnershipStat.deleteMany({
|
|
where: {
|
|
OR: [
|
|
{ player1Id: { in: playerIds } },
|
|
{ player2Id: { in: playerIds } },
|
|
]
|
|
}
|
|
});
|
|
|
|
// Then delete event participants that reference players
|
|
await prisma.eventParticipant.deleteMany({
|
|
where: {
|
|
playerId: { in: playerIds }
|
|
}
|
|
});
|
|
|
|
// Finally delete players
|
|
await prisma.player.deleteMany({
|
|
where: {
|
|
id: { in: playerIds }
|
|
}
|
|
});
|
|
});
|
|
|
|
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 ts = Date.now();
|
|
const player1 = await prisma.player.create({
|
|
data: {
|
|
name: `Elo Test Player 1 ${ts}`,
|
|
normalizedName: `elo_test_player_1_${ts}`,
|
|
currentElo: 1500,
|
|
gamesPlayed: 0,
|
|
wins: 0,
|
|
losses: 0,
|
|
},
|
|
});
|
|
|
|
const player2 = await prisma.player.create({
|
|
data: {
|
|
name: `Elo Test Player 2 ${ts}`,
|
|
normalizedName: `elo_test_player_2_${ts}`,
|
|
currentElo: 1500,
|
|
gamesPlayed: 0,
|
|
wins: 0,
|
|
losses: 0,
|
|
},
|
|
});
|
|
|
|
const player3 = await prisma.player.create({
|
|
data: {
|
|
name: `Elo Test Player 3 ${ts}`,
|
|
normalizedName: `elo_test_player_3_${ts}`,
|
|
currentElo: 1500,
|
|
gamesPlayed: 0,
|
|
wins: 0,
|
|
losses: 0,
|
|
},
|
|
});
|
|
|
|
const player4 = await prisma.player.create({
|
|
data: {
|
|
name: `Elo Test Player 4 ${ts}`,
|
|
normalizedName: `elo_test_player_4_${ts}`,
|
|
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(500);
|
|
|
|
// Wait for tournament dropdown to be ready
|
|
await page.waitForSelector('select#tournament', { timeout: 3000 });
|
|
|
|
// Wait a bit for tournaments to load
|
|
await page.waitForTimeout(500);
|
|
|
|
// 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(1000);
|
|
|
|
// 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(500);
|
|
|
|
// Re-select the tournament for the second upload
|
|
await page.waitForSelector('select#tournament', { timeout: 3000 });
|
|
await page.waitForTimeout(200); // 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(500);
|
|
fs.unlinkSync(tmpFile2);
|
|
|
|
await page.waitForTimeout(500);
|
|
|
|
// 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 ts = Date.now();
|
|
const player = await prisma.player.create({
|
|
data: {
|
|
name: `Elo Test Profile Player ${ts}`,
|
|
normalizedName: `elo_test_profile_player_${ts}`,
|
|
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();
|
|
});
|
|
});
|