feat: Implement tournament schedule tab and fix E2E tests #27

Merged
david merged 43 commits from feature/7-schedule-tab into main 2026-04-27 01:59:17 +00:00
7 changed files with 76 additions and 85 deletions
Showing only changes of commit eeb0f7970a - Show all commits
+3 -27
View File
@@ -27,7 +27,6 @@ test.describe.serial('Issue #7: Schedule Tab', () => {
let testEmail: string; let testEmail: string;
let testPassword: string; let testPassword: string;
let tournamentId: number; let tournamentId: number;
let teamIds: number[] = [];
test.beforeAll(async () => { test.beforeAll(async () => {
const credentials = getTestCredentials(); const credentials = getTestCredentials();
@@ -87,37 +86,15 @@ test.describe.serial('Issue #7: Schedule Tab', () => {
}); });
tournamentId = tournament.id; tournamentId = tournament.id;
// Create teams // Register participants (teams are now ephemeral and generated during schedule creation)
const teams = await Promise.all([
prisma.team.create({
data: {
eventId: tournamentId,
player1Id: players[0].id,
player2Id: players[1].id,
teamName: 'Team A',
},
}),
prisma.team.create({
data: {
eventId: tournamentId,
player1Id: players[2].id,
player2Id: players[3].id,
teamName: 'Team B',
},
}),
]);
teamIds = teams.map((t) => t.id);
// Register participants
await Promise.all( await Promise.all(
players.map((player) => players.map((player) =>
prisma.eventParticipant.create({ prisma.eventParticipant.create({
data: { data: {
eventId: tournamentId, eventId: tournamentId,
playerId: player.id, playerId: player.id,
teamId: teams.find( status: 'registered',
(t) => t.player1Id === player.id || t.player2Id === player.id registrationDate: new Date(),
)?.id,
}, },
}) })
) )
@@ -131,7 +108,6 @@ test.describe.serial('Issue #7: Schedule Tab', () => {
await prisma.bracketMatchup.deleteMany({ where: { eventId: tournamentId } }); await prisma.bracketMatchup.deleteMany({ where: { eventId: tournamentId } });
await prisma.tournamentRound.deleteMany({ where: { eventId: tournamentId } }); await prisma.tournamentRound.deleteMany({ where: { eventId: tournamentId } });
await prisma.eventParticipant.deleteMany({ where: { eventId: tournamentId } }); await prisma.eventParticipant.deleteMany({ where: { eventId: tournamentId } });
await prisma.team.deleteMany({ where: { eventId: tournamentId } });
await prisma.event.delete({ where: { id: tournamentId } }).catch(() => {}); await prisma.event.delete({ where: { id: tournamentId } }).catch(() => {});
} }
@@ -28,6 +28,7 @@ const mockTournament = {
description: 'A test tournament', description: 'A test tournament',
eventDate: new Date('2024-01-15'), eventDate: new Date('2024-01-15'),
eventType: 'tournament', eventType: 'tournament',
tournamentType: 'individual',
format: 'round_robin', format: 'round_robin',
status: 'planned', status: 'planned',
maxParticipants: 16, maxParticipants: 16,
@@ -36,6 +37,12 @@ const mockTournament = {
allowTies: false, allowTies: false,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
teamDurability: 'permanent',
partnerRotation: 'none',
allowByes: true,
teamConfiguration: null,
maxRosterChanges: null,
requireAdminVerify: false,
} }
describe('EditTournamentForm', () => { describe('EditTournamentForm', () => {
@@ -8,8 +8,8 @@ import { describe, test, expect, mock, beforeEach,} from 'bun:test';
import { prisma } from '@/lib/prisma'; import { prisma } from '@/lib/prisma';
// Create mock functions at module level // Create mock functions at module level
const playerFindFirstMock = mock(() => {}); const playerFindFirstMock = mock(async (_args: any): Promise<any> => null);
const playerCreateMock = mock(() => {}); const playerCreateMock = mock(async (_args: any): Promise<any> => ({}));
// Mock the prisma module // Mock the prisma module
mock.module('@/lib/prisma', () => ({ mock.module('@/lib/prisma', () => ({
@@ -326,10 +326,16 @@ describe('Player Deduplication', () => {
rating: 0, rating: 0,
}; };
playerFindFirstMock // Configure mock to return existing player for these specific calls
.mockResolvedValueOnce(existingPlayer) // First call for "Emily" playerFindFirstMock.mockImplementation(async (args: any) => {
.mockResolvedValueOnce(existingPlayer) // Second call for "EMILY" if (args.where.normalizedName === 'emily') {
.mockResolvedValueOnce(existingPlayer); // Third call for " Emily " return existingPlayer;
}
if (args.where.normalizedName === 'emily') {
return existingPlayer;
}
return null;
});
const result1 = await findOrCreatePlayer('Emily'); const result1 = await findOrCreatePlayer('Emily');
const result2 = await findOrCreatePlayer('EMILY'); const result2 = await findOrCreatePlayer('EMILY');
+9 -9
View File
@@ -11,21 +11,21 @@ import { recalculateAllElo } from '@/lib/elo-utils';
// Mock Prisma client // Mock Prisma client
const mockPrisma = { const mockPrisma = {
player: { player: {
updateMany: mock(() => {}).mockResolvedValue({ count: 0 }), updateMany: mock(async () => ({ count: 0 })),
update: mock(() => {}).mockResolvedValue({}), update: mock(async () => ({})),
}, },
eloSnapshot: { eloSnapshot: {
deleteMany: mock(() => {}).mockResolvedValue({ count: 0 }), deleteMany: mock(async () => ({ count: 0 })),
create: mock(() => {}).mockResolvedValue({}), create: mock(async () => ({})),
}, },
partnershipStat: { partnershipStat: {
deleteMany: mock(() => {}).mockResolvedValue({ count: 0 }), deleteMany: mock(async () => ({ count: 0 })),
findFirst: mock(() => {}).mockResolvedValue(null), // No existing stats initially findFirst: mock(async () => null), // No existing stats initially
update: mock(() => {}).mockResolvedValue({}), update: mock(async () => ({})),
create: mock(() => {}).mockResolvedValue({}), create: mock(async () => ({})),
}, },
match: { match: {
findMany: mock(() => {}).mockResolvedValue([]), findMany: mock(async () => []),
}, },
}; };
@@ -55,6 +55,7 @@ const createMockTournament = (id: number, ownerId: string | null): Event => ({
description: null, description: null,
eventDate: new Date(), eventDate: new Date(),
eventType: 'tournament', eventType: 'tournament',
tournamentType: 'individual',
format: 'round_robin', format: 'round_robin',
status: 'planned', status: 'planned',
maxParticipants: null, maxParticipants: null,
@@ -63,6 +64,12 @@ const createMockTournament = (id: number, ownerId: string | null): Event => ({
allowTies: false, allowTies: false,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
teamDurability: 'permanent',
partnerRotation: 'none',
allowByes: true,
teamConfiguration: null,
maxRosterChanges: null,
requireAdminVerify: false,
}); });
describe('Tournament Permissions', () => { describe('Tournament Permissions', () => {
@@ -185,9 +192,9 @@ describe('Tournament Permissions', () => {
); );
const mockTournaments = [ const mockTournaments = [
createMockTournament(1, 'user-1'), { ...createMockTournament(1, 'user-1'), participants: [] },
createMockTournament(2, 'user-2'), { ...createMockTournament(2, 'user-2'), participants: [] },
createMockTournament(3, 'user-3'), { ...createMockTournament(3, 'user-3'), participants: [] },
]; ];
eventFindManyMock.mockImplementation(async () => mockTournaments); eventFindManyMock.mockImplementation(async () => mockTournaments);
@@ -210,8 +217,8 @@ describe('Tournament Permissions', () => {
); );
const mockTournaments = [ const mockTournaments = [
createMockTournament(1, 'tour-admin-1'), { ...createMockTournament(1, 'tour-admin-1'), participants: [] },
createMockTournament(2, 'tour-admin-1'), { ...createMockTournament(2, 'tour-admin-1'), participants: [] },
]; ];
eventFindManyMock.mockImplementation(async () => mockTournaments); eventFindManyMock.mockImplementation(async () => mockTournaments);
@@ -237,8 +244,8 @@ describe('Tournament Permissions', () => {
); );
const mockTournaments = [ const mockTournaments = [
createMockTournament(1, 'user-1'), { ...createMockTournament(1, 'user-1'), participants: [] },
createMockTournament(2, 'user-2'), { ...createMockTournament(2, 'user-2'), participants: [] },
]; ];
eventFindManyMock.mockImplementation(async () => mockTournaments); eventFindManyMock.mockImplementation(async () => mockTournaments);
+23 -28
View File
@@ -7,29 +7,15 @@ import { describe, it, expect, mock, beforeEach,} from 'bun:test';
import { prisma } from '@/lib/prisma'; import { prisma } from '@/lib/prisma';
// Create mock functions at module level // Create mock functions at module level
const eventFindUniqueMock = mock(() => {}); const eventFindUniqueMock = mock(async () => ({}));
const eventUpdateMock = mock(() => {}); const eventUpdateMock = mock(async () => ({}));
const canManageTournamentMock = mock(() => {}); const canManageTournamentMock = mock(async () => ({ allowed: true }));
const canDeleteTournamentMock = mock(() => {}); const canDeleteTournamentMock = mock(async () => ({ allowed: true }));
// Store default implementations
const defaultCanManageTournament = canManageTournamentMock.mockResolvedValue({ allowed: true });
const defaultCanDeleteTournament = canDeleteTournamentMock.mockResolvedValue({ allowed: true });
// Mock the prisma client
mock.module('@/lib/prisma', () => ({
prisma: {
event: {
findUnique: eventFindUniqueMock,
update: eventUpdateMock,
},
},
}));
// Mock the permissions module // Mock the permissions module
mock.module('@/lib/permissions', () => ({ mock.module('@/lib/permissions', () => ({
canManageTournament: defaultCanManageTournament, canManageTournament: canManageTournamentMock,
canDeleteTournament: defaultCanDeleteTournament, canDeleteTournament: canDeleteTournamentMock,
})); }));
// Import the route handler after mocking // Import the route handler after mocking
@@ -151,10 +137,15 @@ describe('Tournament Update API', () => {
expect(response.status).toBe(200); expect(response.status).toBe(200);
// When allowTies is not provided, it should NOT be in the update data // When allowTies is not provided, it should NOT be in the update data
// (it will keep its existing value in the database) // (it will keep its existing value in the database)
const updateCall = eventUpdateMock.mock.calls[0][0]; expect(eventUpdateMock.mock.calls.length).toBeGreaterThan(0);
expect(updateCall.data.allowTies).toBeUndefined(); const updateCallArgs = (eventUpdateMock.mock.calls as any[][])[0];
expect(updateCall.data.name).toBe('Test Tournament'); expect(updateCallArgs).toBeDefined();
expect(updateCall.data.targetScore).toBe(5); if (updateCallArgs && updateCallArgs[0]) {
const updateData = updateCallArgs[0];
expect((updateData as any).data.allowTies).toBeUndefined();
expect((updateData as any).data.name).toBe('Test Tournament');
expect((updateData as any).data.targetScore).toBe(5);
}
}); });
it('should preserve allowTies value when updating other fields', async () => { it('should preserve allowTies value when updating other fields', async () => {
@@ -205,9 +196,13 @@ describe('Tournament Update API', () => {
const response = await PUT(request, { params }); const response = await PUT(request, { params });
expect(response.status).toBe(200); expect(response.status).toBe(200);
const updateCall = eventUpdateMock.mock.calls[0][0]; const updateCallArgs = (eventUpdateMock.mock.calls as any[][])[0];
expect(updateCall.data.allowTies).toBe(true); expect(updateCallArgs).toBeDefined();
expect(updateCall.data.name).toBe('Updated Tournament Name'); if (updateCallArgs && updateCallArgs[0]) {
expect(updateCall.data.targetScore).toBe(10); const updateData = updateCallArgs[0];
expect((updateData as any).data.allowTies).toBe(true);
expect((updateData as any).data.name).toBe('Updated Tournament Name');
expect((updateData as any).data.targetScore).toBe(10);
}
}); });
}); });
+8 -8
View File
@@ -11,10 +11,10 @@ import { prisma } from '@/lib/prisma';
import type { User, Player } from '@prisma/client'; import type { User, Player } from '@prisma/client';
// Create mock functions at module level // Create mock functions at module level
const getSessionMock = mock(() => {}); const getSessionMock = mock(async (): Promise<any> => null);
const userFindUniqueMock = mock(() => {}); const userFindUniqueMock = mock(async (): Promise<any> => null);
const userUpdateMock = mock(() => {}); const userUpdateMock = mock(async (): Promise<any> => ({}));
const playerFindUniqueMock = mock(() => {}); const playerFindUniqueMock = mock(async (): Promise<any> => null);
// Mock the getSession and prisma functions // Mock the getSession and prisma functions
mock.module('@/lib/auth-simple', () => ({ mock.module('@/lib/auth-simple', () => ({
@@ -63,10 +63,10 @@ const createMockPlayer = (id: number, name: string): Player => ({
describe('User Management', () => { describe('User Management', () => {
beforeEach(() => { beforeEach(() => {
// Reset mock implementations to default (no-op) before each test // Reset mock implementations to default (no-op) before each test
getSessionMock.mockImplementation(() => undefined); getSessionMock.mockImplementation(async () => undefined);
userFindUniqueMock.mockImplementation(() => undefined); userFindUniqueMock.mockImplementation(async () => undefined);
userUpdateMock.mockImplementation(() => undefined); userUpdateMock.mockImplementation(async () => undefined);
playerFindUniqueMock.mockImplementation(() => undefined); playerFindUniqueMock.mockImplementation(async () => undefined);
}); });
describe('User Name Editing', () => { describe('User Name Editing', () => {