feat: Implement tournament schedule tab and fix E2E tests #27
@@ -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');
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user