Files
euchre_camp/e2e/csv-upload-deduplication.test.ts
T
david e08c2bbdfd
Pull Request / unit-tests (pull_request) Successful in 2m9s
Pull Request / analyze-bump-type (pull_request) Successful in 11s
Pull Request / build-and-deploy-ci (pull_request) Failing after 42m45s
fix: add debug logging to CSV upload and cucumber tests
2026-05-18 19:41:51 -07:00

232 lines
7.6 KiB
TypeScript

/**
* 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,
});
if (!response.ok()) {
console.log('CSV upload failed:', response.status(), await response.text());
}
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,
});
if (!response.ok()) {
console.log('CSV upload failed:', response.status(), await response.text());
}
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,
});
if (!response.ok()) {
console.log('CSV upload failed:', response.status(), await response.text());
}
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);
}
}
});
});