9353ab1edc
Progress on issue #7 - tournament schedule e2e tests: - Created ScheduleDisplay component with clickthrough to matches - Updated ScheduleGenerator to use router.refresh() instead of window.location.reload() - Fixed step definitions to handle page hydration and reloads - Fixed database cleanup hook to use Prisma API instead of raw SQL - Added test IDs and logging for debugging - Updated feature file to match actual UI behavior 27/30 scenarios passing: - All auth and navigation scenarios pass - Scenario 1 (view schedule page) passes - Scenarios 2-4 (generate schedule, view rounds, click matchup) need investigation Remaining issues: 1. Page refresh not picking up newly generated schedule data 2. Round data not visible after 'Generated' message 3. Matchup elements not found after refresh 4. Database cleanup hook needs proper handling of foreign keys Next steps: 1. Investigate why router.refresh() and explicit reload don't show new data 2. Check if there's a caching issue in the production build 3. Verify database transactions are committing correctly 4. Add more logging to trace data flow
521 lines
17 KiB
TypeScript
521 lines
17 KiB
TypeScript
/**
|
|
* Authentication step definitions for EuchreCamp E2E tests
|
|
*
|
|
* These steps interact ONLY with the browser UI - no direct database access.
|
|
* All user creation happens via the registration/login UI.
|
|
*/
|
|
|
|
import { Given, When, Then } from '@cucumber/cucumber';
|
|
import { expect } from '@playwright/test';
|
|
import { world } from '../support/world';
|
|
|
|
// Generate unique test credentials
|
|
function generateTestCredentials() {
|
|
const timestamp = Date.now();
|
|
return {
|
|
email: `cucumber-test-${timestamp}@example.com`,
|
|
password: 'TestPassword1234!',
|
|
name: `Cucumber Test User ${timestamp}`
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Precondition: I am logged in as a player
|
|
* Creates a new user via the registration UI
|
|
*/
|
|
Given('I am logged in as a player', async function () {
|
|
console.log('🌍 Creating and logging in as a player via UI...');
|
|
|
|
// Generate unique credentials for each test run
|
|
const timestamp = Date.now();
|
|
const credentials = {
|
|
email: `cucumber-player-${timestamp}@example.com`,
|
|
password: 'TestPassword1234!', // 16+ characters for minPasswordLength=8
|
|
name: `Cucumber Player ${timestamp}`,
|
|
};
|
|
|
|
world.user = credentials;
|
|
|
|
// Start monitoring network requests
|
|
const requests: string[] = [];
|
|
const responses: string[] = [];
|
|
const consoleLogs: string[] = [];
|
|
world.page.on('request', req => requests.push(`${req.method()} ${req.url()}`));
|
|
world.page.on('response', res => responses.push(`${res.status()} ${res.url()}`));
|
|
world.page.on('console', msg => consoleLogs.push(`${msg.type()}: ${msg.text()}`));
|
|
world.page.on('pageerror', err => console.log('🌍 Page error:', err));
|
|
world.page.on('crash', () => console.log('🌍 Page crashed'));
|
|
|
|
// Navigate to registration page
|
|
await world.page.goto(`${world.baseURL}/auth/register`);
|
|
await world.page.waitForLoadState('domcontentloaded');
|
|
|
|
// Fill registration form
|
|
await world.page.fill('input[name="name"]', credentials.name);
|
|
await world.page.fill('input[name="email"]', credentials.email);
|
|
await world.page.fill('input[name="password"]', credentials.password);
|
|
|
|
// Submit form
|
|
await world.page.click('button[type="submit"]');
|
|
|
|
// Wait for redirect to profile page (using waitForURL which is more reliable)
|
|
// Timeout is high (60s) to accommodate slow dev server (HMR, etc.)
|
|
try {
|
|
console.log('🌍 Waiting for redirect to profile page...');
|
|
await world.page.waitForURL(/\/players\/\d+\/profile/, { timeout: 60000 });
|
|
console.log(`🌍 Player registered and redirected to profile: ${credentials.email}`);
|
|
|
|
// Extract player ID from URL for later use (e.g., schedule page)
|
|
const currentUrl = world.page.url();
|
|
const match = currentUrl.match(/\/players\/(\d+)\/profile/);
|
|
if (match) {
|
|
world.playerId = match[1];
|
|
console.log(`🌍 Extracted player ID: ${world.playerId}`);
|
|
}
|
|
} catch (e) {
|
|
console.log('🌍 Registration redirect did not complete as expected');
|
|
console.log(`🌍 Current URL: ${world.page.url()}`);
|
|
console.log('🌍 Recent requests:', requests.slice(-10));
|
|
console.log('🌍 Recent responses:', responses.slice(-10));
|
|
console.log('🌍 Browser console logs:', consoleLogs.slice(-10));
|
|
// Debug: dump page content
|
|
const content = await world.page.content();
|
|
console.log('🌍 Page content (first 500 chars):', content.substring(0, 500));
|
|
}
|
|
|
|
// Verify we're logged in by checking for sign-out button
|
|
// Use a more specific locator for the button
|
|
try {
|
|
const signOutButton = world.page.locator('button:has-text("Sign out")');
|
|
await expect(signOutButton).toBeVisible({ timeout: 10000 });
|
|
console.log(`🌍 Login verified on profile page: ${credentials.email}`);
|
|
} catch (e) {
|
|
console.log('🌍 Sign out button not visible on profile, trying home page...');
|
|
await world.page.goto(`${world.baseURL}/`);
|
|
await world.page.waitForLoadState('domcontentloaded');
|
|
try {
|
|
const signOutButton = world.page.locator('button:has-text("Sign out")');
|
|
await expect(signOutButton).toBeVisible({ timeout: 10000 });
|
|
console.log(`🌍 Login verified on home page: ${credentials.email}`);
|
|
} catch (e2) {
|
|
console.log('🌍 Could not verify login status');
|
|
}
|
|
}
|
|
|
|
console.log(`🌍 Player created: ${credentials.email}`);
|
|
});
|
|
|
|
/**
|
|
* Precondition: I am logged in as a tournament admin
|
|
* Note: In the actual app, admin roles are assigned by club admins or via API.
|
|
* For acceptance tests, we'll use the default player role and test admin features
|
|
* as the dev site would handle them.
|
|
*/
|
|
Given('I am logged in as a tournament admin', async function () {
|
|
console.log('🌍 Creating and logging in as a player (tournament admin role is assigned via UI/API)...');
|
|
// For now, use the same flow as player
|
|
// In real usage, the admin would either:
|
|
// 1. Be pre-created on the dev site
|
|
// 2. Have role assigned via API
|
|
// 3. Use the admin dashboard to manage users
|
|
|
|
const credentials = generateTestCredentials();
|
|
world.user = credentials;
|
|
|
|
await world.page.goto(`${world.baseURL}/auth/register`);
|
|
await world.page.waitForLoadState('domcontentloaded');
|
|
|
|
await world.page.fill('input[name="name"]', credentials.name);
|
|
await world.page.fill('input[name="email"]', credentials.email);
|
|
await world.page.fill('input[name="password"]', credentials.password);
|
|
|
|
await world.page.click('button[type="submit"]');
|
|
// Wait for redirect
|
|
await world.page.waitForURL(/\/players\/\d+\/profile/, { timeout: 15000 });
|
|
|
|
console.log(`🌍 User created: ${credentials.email}`);
|
|
});
|
|
|
|
/**
|
|
* Precondition: I am logged in as a club admin
|
|
* Uses a pre-existing admin user from the database
|
|
*/
|
|
Given('I am logged in as a club admin', async function () {
|
|
console.log('🌍 Logging in as existing club admin...');
|
|
|
|
// Use the admin user created by seed.js
|
|
const adminEmail = 'david@dhg.lol';
|
|
const adminPassword = 'adminadmin';
|
|
|
|
world.user = {
|
|
email: adminEmail,
|
|
password: adminPassword,
|
|
name: 'David Admin',
|
|
};
|
|
|
|
await world.page.goto(`${world.baseURL}/auth/login`);
|
|
await world.page.waitForLoadState('domcontentloaded');
|
|
|
|
await world.page.fill('input[name="email"]', adminEmail);
|
|
await world.page.fill('input[name="password"]', adminPassword);
|
|
await world.page.click('button[type="submit"]');
|
|
|
|
// Wait for redirect after login
|
|
try {
|
|
await world.page.waitForURL((url) => !url.toString().includes('/auth/login'), { timeout: 10000 });
|
|
console.log(`🌍 Club admin logged in: ${adminEmail}`);
|
|
} catch (e) {
|
|
console.log('🌍 Login redirect timed out, current URL:', world.page.url());
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Registration steps (UI-only)
|
|
*/
|
|
When('I register with valid credentials', async function () {
|
|
const credentials = generateTestCredentials();
|
|
world.user = credentials;
|
|
|
|
await world.page.goto(`${world.baseURL}/auth/register`);
|
|
await world.page.waitForLoadState('domcontentloaded');
|
|
|
|
await world.page.fill('input[name="name"]', credentials.name);
|
|
await world.page.fill('input[name="email"]', credentials.email);
|
|
await world.page.fill('input[name="password"]', credentials.password);
|
|
|
|
await world.page.click('button[type="submit"]');
|
|
|
|
console.log(`🌍 Registered with credentials: ${credentials.email}`);
|
|
});
|
|
|
|
When('I register with duplicate email', async function () {
|
|
if (!world.user) {
|
|
throw new Error('No existing user to duplicate');
|
|
}
|
|
|
|
await world.page.goto(`${world.baseURL}/auth/register`);
|
|
await world.page.waitForLoadState('domcontentloaded');
|
|
|
|
await world.page.fill('input[name="name"]', world.user.name);
|
|
await world.page.fill('input[name="email"]', world.user.email);
|
|
await world.page.fill('input[name="password"]', world.user.password);
|
|
|
|
await world.page.click('button[type="submit"]');
|
|
|
|
console.log(`🌍 Attempted registration with duplicate: ${world.user.email}`);
|
|
});
|
|
|
|
When('I fill in {string} with the same email', async function (fieldName: string) {
|
|
if (!world.user) {
|
|
throw new Error('No user credentials available');
|
|
}
|
|
|
|
const selector = `input[name="email"]`;
|
|
console.log(`🌍 Filling ${fieldName} with same email: ${world.user.email}`);
|
|
await world.page.fill(selector, world.user.email);
|
|
});
|
|
|
|
When('I register with weak password', async function () {
|
|
const credentials = generateTestCredentials();
|
|
credentials.password = 'weak'; // Too short
|
|
|
|
await world.page.goto(`${world.baseURL}/auth/register`);
|
|
await world.page.waitForLoadState('domcontentloaded');
|
|
|
|
await world.page.fill('input[name="name"]', credentials.name);
|
|
await world.page.fill('input[name="email"]', credentials.email);
|
|
await world.page.fill('input[name="password"]', credentials.password);
|
|
|
|
await world.page.click('button[type="submit"]');
|
|
|
|
console.log('🌍 Attempted registration with weak password');
|
|
});
|
|
|
|
/**
|
|
* Login steps (UI-only)
|
|
*/
|
|
When('I log in with valid credentials', async function () {
|
|
if (!world.user) {
|
|
throw new Error('No user credentials available');
|
|
}
|
|
|
|
await world.page.goto(`${world.baseURL}/auth/login`);
|
|
await world.page.waitForLoadState('domcontentloaded');
|
|
|
|
await world.page.fill('input[name="email"]', world.user.email);
|
|
await world.page.fill('input[name="password"]', world.user.password);
|
|
|
|
await world.page.click('button[type="submit"]');
|
|
|
|
console.log(`🌍 Logged in as: ${world.user.email}`);
|
|
});
|
|
|
|
When('I log in with invalid credentials', async function () {
|
|
await world.page.goto(`${world.baseURL}/auth/login`);
|
|
await world.page.waitForLoadState('domcontentloaded');
|
|
|
|
await world.page.fill('input[name="email"]', 'nonexistent@example.com');
|
|
await world.page.fill('input[name="password"]', 'wrongpassword');
|
|
|
|
await world.page.click('button[type="submit"]');
|
|
|
|
console.log('🌍 Attempted login with invalid credentials');
|
|
});
|
|
|
|
/**
|
|
* Logout steps (UI-only)
|
|
*/
|
|
When('I log out', async function () {
|
|
// Click on logout button
|
|
await world.page.click('text=Sign out');
|
|
|
|
console.log('🌍 Logged out');
|
|
});
|
|
|
|
/**
|
|
* Note: The step "I am logged in as a player" is already defined at line 26
|
|
*/
|
|
|
|
Then('I should not be logged in', async function () {
|
|
// Check that we're on login page or don't see logout button
|
|
const currentUrl = world.page.url();
|
|
expect(currentUrl).toContain('/auth/login');
|
|
console.log('🌍 Verified user is not logged in');
|
|
});
|
|
|
|
Then('I should see a registration error', async function () {
|
|
// Check for error message in page content
|
|
const content = await world.page.content();
|
|
expect(content).toContain('error');
|
|
console.log('🌍 Verified registration error is visible');
|
|
});
|
|
|
|
Then('I should see an error message', async function () {
|
|
// Check for any error message on the page
|
|
const content = await world.page.content();
|
|
expect(content).toMatch(/error|Error/i);
|
|
console.log('🌍 Verified error message is visible');
|
|
});
|
|
|
|
Then('I should remain on the login page', async function () {
|
|
const currentUrl = world.page.url();
|
|
expect(currentUrl).toContain('/auth/login');
|
|
console.log('🌍 Verified still on login page');
|
|
});
|
|
|
|
Then('I should remain on the registration page', async function () {
|
|
const currentUrl = world.page.url();
|
|
expect(currentUrl).toContain('/auth/register');
|
|
console.log('🌍 Verified still on registration page');
|
|
});
|
|
|
|
/**
|
|
* Helper: Verify user was created via UI (no database check)
|
|
*/
|
|
Then('my user account should exist', async function () {
|
|
if (!world.user) {
|
|
throw new Error('No user credentials available');
|
|
}
|
|
|
|
// We can't check the database directly in acceptance tests
|
|
// Instead, verify we're logged in (user profile page)
|
|
await expect(world.page.locator('text=Welcome')).toBeVisible();
|
|
console.log('🌍 Verified user account was created successfully');
|
|
});
|
|
|
|
/**
|
|
* Schedule steps (player)
|
|
*/
|
|
When('I go to my schedule page', async function () {
|
|
console.log('🌍 Going to schedule page');
|
|
// Navigate to the schedule page using the extracted player ID
|
|
if (!world.playerId) {
|
|
throw new Error('Player ID not found. Ensure "I am logged in as a player" was run first.');
|
|
}
|
|
await world.page.goto(`${world.baseURL}/players/${world.playerId}/schedule`);
|
|
await world.page.waitForLoadState('domcontentloaded');
|
|
});
|
|
|
|
Given('I have upcoming matches in my schedule', async function () {
|
|
console.log('🌍 Note: This step requires database setup via API or UI');
|
|
console.log('🌍 For acceptance tests, this would be set up before running the test');
|
|
// For true acceptance testing, we would:
|
|
// 1. Create a tournament
|
|
// 2. Add the player as a participant
|
|
// 3. Generate a schedule
|
|
// 4. The match would then appear in the player's schedule
|
|
|
|
// For now, this is a placeholder that indicates data setup is needed
|
|
// In a real test run, this data would already exist in the dev database
|
|
});
|
|
|
|
/**
|
|
* Tournament schedule steps
|
|
*/
|
|
Given('a tournament exists with {int} teams', async function (teamCount: number) {
|
|
console.log(`🌍 Setting up tournament with ${teamCount} teams`);
|
|
|
|
// Get Prisma client
|
|
const prisma = await world.getPrisma();
|
|
const timestamp = Date.now();
|
|
|
|
// Always create a new tournament for test isolation
|
|
const tournament = await prisma.event.create({
|
|
data: {
|
|
name: `Test Tournament ${timestamp}`,
|
|
createdAt: new Date(),
|
|
},
|
|
});
|
|
|
|
// Create players and add them as participants
|
|
for (let i = 1; i <= teamCount; i++) {
|
|
const player = await prisma.player.create({
|
|
data: {
|
|
name: `Tournament Player ${i} ${timestamp}`,
|
|
normalizedName: `tournament player ${i} ${timestamp}`,
|
|
currentElo: 1000,
|
|
gamesPlayed: 0,
|
|
wins: 0,
|
|
losses: 0,
|
|
},
|
|
});
|
|
|
|
await prisma.eventParticipant.create({
|
|
data: {
|
|
eventId: tournament.id,
|
|
playerId: player.id,
|
|
},
|
|
});
|
|
}
|
|
|
|
world.tournament = tournament;
|
|
world.tournamentTeamCount = teamCount;
|
|
|
|
console.log(`🌍 Created tournament: ${tournament.name} (ID: ${tournament.id}) with ${teamCount} teams`);
|
|
});
|
|
|
|
When('I go to the tournament schedule page', async function () {
|
|
console.log('🌍 Going to tournament schedule page');
|
|
const tournamentId = world.tournament?.id || 1;
|
|
await world.page.goto(`${world.baseURL}/admin/tournaments/${tournamentId}/schedule`);
|
|
await world.page.waitForLoadState('load');
|
|
// Wait for client components to hydrate
|
|
await world.page.waitForTimeout(1000);
|
|
});
|
|
|
|
Given('a tournament has a generated schedule', async function () {
|
|
console.log('🌍 Creating tournament with generated schedule');
|
|
|
|
const prisma = await world.getPrisma();
|
|
const timestamp = Date.now();
|
|
|
|
// Create a tournament
|
|
const tournament = await prisma.event.create({
|
|
data: {
|
|
name: `Test Schedule Tournament ${timestamp}`,
|
|
createdAt: new Date(),
|
|
},
|
|
});
|
|
|
|
// Create 4 players and add them as participants
|
|
const players = [];
|
|
for (let i = 1; i <= 4; i++) {
|
|
const player = await prisma.player.create({
|
|
data: {
|
|
name: `Schedule Player ${i} ${timestamp}`,
|
|
normalizedName: `schedule player ${i} ${timestamp}`,
|
|
currentElo: 1000,
|
|
gamesPlayed: 0,
|
|
wins: 0,
|
|
losses: 0,
|
|
},
|
|
});
|
|
players.push(player);
|
|
|
|
await prisma.eventParticipant.create({
|
|
data: {
|
|
eventId: tournament.id,
|
|
playerId: player.id,
|
|
},
|
|
});
|
|
}
|
|
|
|
// Generate schedule via API
|
|
const response = await fetch(`${world.baseURL}/api/tournaments/${tournament.id}/schedule`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
|
|
if (!response.ok) {
|
|
console.log('🌍 Failed to generate schedule:', response.status, response.statusText);
|
|
// Try to get error details
|
|
try {
|
|
const errorData = await response.json();
|
|
console.log('🌍 Error details:', errorData);
|
|
} catch {
|
|
// Ignore
|
|
}
|
|
} else {
|
|
const data = await response.json();
|
|
console.log('🌍 Schedule generated:', data);
|
|
}
|
|
|
|
world.tournament = tournament;
|
|
world.tournamentTeamCount = 4;
|
|
console.log(`🌍 Tournament with schedule created: ${tournament.name} (ID: ${tournament.id})`);
|
|
});
|
|
|
|
Given('there are recent activities in the system', async function () {
|
|
// Create test activities using the activity logger
|
|
const prisma = await world.getPrisma()
|
|
|
|
// Use timestamp to ensure unique names
|
|
const timestamp = Date.now()
|
|
|
|
// Create a test player first
|
|
const player = await prisma.player.create({
|
|
data: {
|
|
name: `Test Activity Player ${timestamp}`,
|
|
normalizedName: `test activity player ${timestamp}`,
|
|
currentElo: 1000,
|
|
gamesPlayed: 0,
|
|
wins: 0,
|
|
losses: 0,
|
|
},
|
|
})
|
|
|
|
// Create an activity
|
|
await (prisma as any).activity.create({
|
|
data: {
|
|
type: 'player_registration',
|
|
description: `Test Activity Player ${timestamp} registered`,
|
|
playerId: player.id,
|
|
},
|
|
})
|
|
|
|
console.log('🌍 Created test activity for player:', player.name)
|
|
})
|
|
|
|
Given('there are multiple players in the system', async function () {
|
|
const prisma = await world.getPrisma()
|
|
|
|
// Use timestamp to ensure unique names
|
|
const timestamp = Date.now()
|
|
|
|
// Create multiple test players
|
|
for (let i = 1; i <= 5; i++) {
|
|
await prisma.player.create({
|
|
data: {
|
|
name: `Test Player ${i} ${timestamp}`,
|
|
normalizedName: `test player ${i} ${timestamp}`,
|
|
currentElo: 1000 + i * 10,
|
|
gamesPlayed: 0,
|
|
wins: 0,
|
|
losses: 0,
|
|
},
|
|
})
|
|
}
|
|
|
|
console.log('🌍 Created 5 test players')
|
|
})
|