/** * E2E Test: CSV Upload Player Deduplication * * Tests that CSV uploads correctly deduplicate players by name */ import { test, expect } from '@playwright/test'; import { prisma } from '@/lib/prisma'; import fs from 'fs'; import path from 'path'; test.describe('CSV Upload Player Deduplication', () => { let testTournamentId: number; const testPlayerIds: number[] = []; const ts = Date.now(); test.beforeAll(async () => { // Create a test tournament const tournament = await prisma.event.create({ data: { name: 'Deduplication Test Tournament', eventDate: new Date(), eventType: 'tournament', format: 'round_robin', status: 'planned', }, }); testTournamentId = tournament.id; }); test.afterAll(async () => { // Clean up test data if (testTournamentId) { // Delete matches first (they reference players) await prisma.match.deleteMany({ where: { eventId: testTournamentId }, }); // Delete event participants await prisma.eventParticipant.deleteMany({ where: { eventId: testTournamentId }, }); // Delete test tournament await prisma.event.delete({ where: { id: testTournamentId }, }); } // Delete test players (those with "Dedupe" or "Aggregate Test" in the name) await prisma.player.deleteMany({ where: { OR: [ { name: { contains: 'Dedupe' } }, { name: { contains: 'Aggregate Test' } }, { name: { contains: 'Whitespace' } }, ], }, }); await prisma.$disconnect(); }); test('should not create duplicate players with different cases', async ({ request }) => { // Create a CSV with the same player name in different cases const csvContent = `Event #,Round,Table,Seat 1,Seat 3,Odds Points,Seat 2,Seat 4,Evens Points 1,1,1,Dedupe Player,Dedupe Player2,5,Dedupe player,Dedupe PLAYER2,3`; // Create a temporary CSV file const csvPath = path.join(__dirname, 'temp-deduplication-test.csv'); fs.writeFileSync(csvPath, csvContent); try { // Upload the CSV const csvFileContent = fs.readFileSync(csvPath, 'utf-8'); const blob = new Blob([csvFileContent], { type: 'text/csv' }); const file = new File([blob], 'deduplication-test.csv', { type: 'text/csv' }); const formData = new FormData(); formData.append('csvFile', file); formData.append('eventId', testTournamentId.toString()); const response = await request.post('/api/matches/upload', { multipart: formData, }); expect(response.ok()).toBeTruthy(); // Check that only 4 unique players were created (not 8) const dedupePlayers = await prisma.player.findMany({ where: { name: { contains: 'Dedupe', }, }, }); // We expect 4 unique players: "Dedupe Player", "Dedupe Player2", "Dedupe player", "Dedupe PLAYER2" // But due to normalization, "Dedupe player" should match "Dedupe Player" (case-insensitive) // and "Dedupe PLAYER2" should match "Dedupe Player2" // So we should have exactly 2 unique players after deduplication expect(dedupePlayers.length).toBe(2); // Verify the players have the correct names (original case preserved) const playerNames = dedupePlayers.map((p: { name: string }) => p.name).sort(); expect(playerNames).toEqual(['Dedupe Player', 'Dedupe Player2']); // Verify each player has games played dedupePlayers.forEach((player: { gamesPlayed: number }) => { expect(player.gamesPlayed).toBeGreaterThan(0); }); } finally { // Clean up temp file if (fs.existsSync(csvPath)) { fs.unlinkSync(csvPath); } } }); test('should handle whitespace variations correctly', async ({ request }) => { // Create a CSV with whitespace variations const csvContent = `Event #,Round,Table,Seat 1,Seat 3,Odds Points,Seat 2,Seat 4,Evens Points 1,1,1, Whitespace Player , Whitespace Player2 ,5,Whitespace Player, WhitespacePlayer2,3`; const csvPath = path.join(__dirname, 'temp-whitespace-test.csv'); fs.writeFileSync(csvPath, csvContent); try { const csvFileContent = fs.readFileSync(csvPath, 'utf-8'); const blob = new Blob([csvFileContent], { type: 'text/csv' }); const file = new File([blob], 'whitespace-test.csv', { type: 'text/csv' }); const formData = new FormData(); formData.append('csvFile', file); formData.append('eventId', testTournamentId.toString()); const response = await request.post('/api/matches/upload', { multipart: formData, }); expect(response.ok()).toBeTruthy(); // Check that players were created without extra whitespace const whitespacePlayers = await prisma.player.findMany({ where: { name: { contains: 'Whitespace', }, }, }); // Verify no players have leading/trailing whitespace whitespacePlayers.forEach((player: { name: string, normalizedName: string }) => { expect(player.name).toBe(player.name.trim()); expect(player.normalizedName).toBe(player.name.trim().toLowerCase()); }); } finally { if (fs.existsSync(csvPath)) { fs.unlinkSync(csvPath); } } }); test('should aggregate stats for duplicate players', async ({ request }) => { // First, create some players manually to simulate previous uploads const player1 = await prisma.player.create({ data: { name: `Aggregate Test ${ts}`, normalizedName: `aggregate test ${ts}`, currentElo: 1050, gamesPlayed: 5, wins: 3, losses: 2, }, }); testPlayerIds.push(player1.id); // Upload a CSV with the same player name const csvContent = `Event #,Round,Table,Seat 1,Seat 3,Odds Points,Seat 2,Seat 4,Evens Points 1,1,1,Aggregate Test ${ts},Test Player 1,5,Test Player 2,Test Player 3,3`; const csvPath = path.join(__dirname, 'temp-aggregate-test.csv'); fs.writeFileSync(csvPath, csvContent); try { const csvFileContent = fs.readFileSync(csvPath, 'utf-8'); const blob = new Blob([csvFileContent], { type: 'text/csv' }); const file = new File([blob], 'aggregate-test.csv', { type: 'text/csv' }); const formData = new FormData(); formData.append('csvFile', file); formData.append('eventId', testTournamentId.toString()); const response = await request.post('/api/matches/upload', { multipart: formData, }); expect(response.ok()).toBeTruthy(); // Check that the existing player was updated (not duplicated) const aggregatePlayers = await prisma.player.findMany({ where: { name: `Aggregate Test ${ts}`, }, }); // Should still be only one player expect(aggregatePlayers.length).toBe(1); // Stats should be updated (1 additional game played, 1 additional win) const player = aggregatePlayers[0]; expect(player.gamesPlayed).toBe(6); // 5 + 1 expect(player.wins).toBe(4); // 3 + 1 expect(player.losses).toBe(2); // unchanged (team lost) } finally { if (fs.existsSync(csvPath)) { fs.unlinkSync(csvPath); } } }); });