Websocket fixes.
826
frontend/tests/bet-creation-taking.spec.ts
Normal file
@ -0,0 +1,826 @@
|
||||
import { test, expect, Page } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Comprehensive E2E Tests for Bet Creation and Taking Flows
|
||||
*
|
||||
* Test Plan:
|
||||
* 1. Test bet creation flow with Alice
|
||||
* 2. Test bet taking flow with Bob
|
||||
* 3. Verify JavaScript errors and network failures
|
||||
* 4. Document all issues found
|
||||
*/
|
||||
|
||||
// Test data
|
||||
const TEST_USERS = {
|
||||
alice: { email: 'alice@example.com', password: 'password123' },
|
||||
bob: { email: 'bob@example.com', password: 'password123' },
|
||||
};
|
||||
|
||||
// Helper function to login
|
||||
async function login(page: Page, user: { email: string; password: string }) {
|
||||
await page.goto('/login');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.fill('input[type="email"]', user.email);
|
||||
await page.fill('input[type="password"]', user.password);
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForLoadState('networkidle');
|
||||
// Wait for redirect after login
|
||||
await page.waitForTimeout(1000);
|
||||
}
|
||||
|
||||
// Helper function to logout
|
||||
async function logout(page: Page) {
|
||||
const logoutButton = page.getByRole('button', { name: /logout/i });
|
||||
if (await logoutButton.isVisible()) {
|
||||
await logoutButton.click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to collect errors
|
||||
interface TestErrors {
|
||||
jsErrors: string[];
|
||||
networkErrors: { url: string; status: number; statusText: string }[];
|
||||
consoleErrors: string[];
|
||||
}
|
||||
|
||||
function setupErrorTracking(page: Page): TestErrors {
|
||||
const errors: TestErrors = {
|
||||
jsErrors: [],
|
||||
networkErrors: [],
|
||||
consoleErrors: [],
|
||||
};
|
||||
|
||||
page.on('pageerror', (error) => {
|
||||
errors.jsErrors.push(error.message);
|
||||
});
|
||||
|
||||
page.on('response', (response) => {
|
||||
if (response.status() >= 400) {
|
||||
errors.networkErrors.push({
|
||||
url: response.url(),
|
||||
status: response.status(),
|
||||
statusText: response.statusText(),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
page.on('console', (msg) => {
|
||||
if (msg.type() === 'error') {
|
||||
errors.consoleErrors.push(msg.text());
|
||||
}
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
test.describe('Bet Creation and Taking - E2E Tests', () => {
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
|
||||
test('TC-001: Verify home page loads with sport events', async ({ page }) => {
|
||||
const errors = setupErrorTracking(page);
|
||||
|
||||
console.log('\n=== TC-001: Home Page Event Loading ===');
|
||||
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.screenshot({ path: 'tests/screenshots/tc001-home-page.png', fullPage: true });
|
||||
|
||||
// Check if events heading is visible
|
||||
const headingVisible = await page.getByRole('heading', { name: /events/i }).isVisible({ timeout: 5000 }).catch(() => false);
|
||||
console.log(`Events heading visible: ${headingVisible}`);
|
||||
|
||||
// Check for event cards
|
||||
const eventCards = page.locator('button').filter({ hasText: /vs/i });
|
||||
const eventCount = await eventCards.count();
|
||||
console.log(`Found ${eventCount} event cards`);
|
||||
|
||||
// Log any errors
|
||||
if (errors.jsErrors.length > 0) {
|
||||
console.log('JavaScript Errors:', errors.jsErrors);
|
||||
}
|
||||
if (errors.networkErrors.length > 0) {
|
||||
console.log('Network Errors:', errors.networkErrors);
|
||||
}
|
||||
|
||||
expect(eventCount).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('TC-002: Alice logs in and navigates to Sport Events', async ({ page }) => {
|
||||
const errors = setupErrorTracking(page);
|
||||
|
||||
console.log('\n=== TC-002: Alice Login and Navigate to Sport Events ===');
|
||||
|
||||
await login(page, TEST_USERS.alice);
|
||||
await page.screenshot({ path: 'tests/screenshots/tc002-alice-logged-in.png', fullPage: true });
|
||||
|
||||
// Navigate to Sport Events page
|
||||
const sportEventsLink = page.getByRole('link', { name: /sport.*events/i });
|
||||
if (await sportEventsLink.isVisible()) {
|
||||
await sportEventsLink.click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
console.log('Navigated to Sport Events via nav link');
|
||||
} else {
|
||||
// Try direct navigation
|
||||
await page.goto('/sport-events');
|
||||
await page.waitForLoadState('networkidle');
|
||||
console.log('Navigated to Sport Events via direct URL');
|
||||
}
|
||||
|
||||
await page.screenshot({ path: 'tests/screenshots/tc002-sport-events-page.png', fullPage: true });
|
||||
|
||||
// Check for Sport Events heading
|
||||
const pageHeading = await page.getByRole('heading', { name: /sport events/i }).isVisible({ timeout: 5000 }).catch(() => false);
|
||||
console.log(`Sport Events heading visible: ${pageHeading}`);
|
||||
|
||||
// Log any errors
|
||||
if (errors.jsErrors.length > 0) {
|
||||
console.log('JavaScript Errors:', errors.jsErrors);
|
||||
}
|
||||
|
||||
expect(pageHeading).toBe(true);
|
||||
});
|
||||
|
||||
test('TC-003: Alice selects an event and views the spread grid', async ({ page }) => {
|
||||
const errors = setupErrorTracking(page);
|
||||
|
||||
console.log('\n=== TC-003: Select Event and View Spread Grid ===');
|
||||
|
||||
await login(page, TEST_USERS.alice);
|
||||
await page.goto('/sport-events');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Find and click on an event
|
||||
const eventButtons = page.locator('.grid button, button.bg-white').filter({ hasText: /vs/i });
|
||||
const eventCount = await eventButtons.count();
|
||||
console.log(`Found ${eventCount} events to select`);
|
||||
|
||||
if (eventCount > 0) {
|
||||
await eventButtons.first().click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.screenshot({ path: 'tests/screenshots/tc003-event-selected.png', fullPage: true });
|
||||
|
||||
// Check if spread grid is visible
|
||||
const spreadGridVisible = await page.locator('.grid').first().isVisible();
|
||||
console.log(`Spread grid visible: ${spreadGridVisible}`);
|
||||
|
||||
// Check for spread buttons (they show spreads like +3.5, -2.5, etc.)
|
||||
const spreadButtons = page.locator('button').filter({ hasText: /^[+-]?\d+\.?\d*$/ });
|
||||
const spreadCount = await spreadButtons.count();
|
||||
console.log(`Found ${spreadCount} spread buttons`);
|
||||
|
||||
// Look for TradingPanel elements
|
||||
const tradingPanelVisible = await page.locator('text=Place Bet').isVisible({ timeout: 5000 }).catch(() => false);
|
||||
console.log(`TradingPanel "Place Bet" visible: ${tradingPanelVisible}`);
|
||||
}
|
||||
|
||||
// Log any errors
|
||||
if (errors.jsErrors.length > 0) {
|
||||
console.log('JavaScript Errors:', errors.jsErrors);
|
||||
}
|
||||
if (errors.networkErrors.length > 0) {
|
||||
console.log('Network Errors:', errors.networkErrors);
|
||||
}
|
||||
});
|
||||
|
||||
test('TC-004: Alice attempts to create a bet via TradingPanel', async ({ page }) => {
|
||||
const errors = setupErrorTracking(page);
|
||||
|
||||
console.log('\n=== TC-004: Alice Creates Bet via TradingPanel ===');
|
||||
|
||||
await login(page, TEST_USERS.alice);
|
||||
|
||||
// Go to home page where events are shown with TradingPanel
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Check for event cards
|
||||
const eventCards = page.locator('button').filter({ hasText: /vs/i });
|
||||
const eventCount = await eventCards.count();
|
||||
console.log(`Found ${eventCount} events on home page`);
|
||||
|
||||
if (eventCount > 0) {
|
||||
// Click on the first event
|
||||
await eventCards.first().click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.screenshot({ path: 'tests/screenshots/tc004-event-details.png', fullPage: true });
|
||||
|
||||
// Look for the TradingPanel's stake input
|
||||
const stakeInput = page.locator('input[type="number"]');
|
||||
const stakeInputVisible = await stakeInput.isVisible({ timeout: 5000 }).catch(() => false);
|
||||
console.log(`Stake input visible: ${stakeInputVisible}`);
|
||||
|
||||
if (stakeInputVisible) {
|
||||
// Enter stake amount
|
||||
await stakeInput.fill('50');
|
||||
console.log('Entered stake amount: 50');
|
||||
|
||||
await page.screenshot({ path: 'tests/screenshots/tc004-stake-entered.png', fullPage: true });
|
||||
|
||||
// Find the create bet button
|
||||
const createBetButton = page.locator('button').filter({ hasText: /create.*bet/i });
|
||||
const createButtonVisible = await createBetButton.isVisible({ timeout: 3000 }).catch(() => false);
|
||||
console.log(`Create Bet button visible: ${createButtonVisible}`);
|
||||
|
||||
if (createButtonVisible) {
|
||||
// Set up network request listener before clicking
|
||||
const createBetPromise = page.waitForResponse(
|
||||
(response) => response.url().includes('/api/v1/spread-bets') && response.request().method() === 'POST',
|
||||
{ timeout: 10000 }
|
||||
).catch(() => null);
|
||||
|
||||
// Click create bet button
|
||||
await createBetButton.click();
|
||||
console.log('Clicked Create Bet button');
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
await page.screenshot({ path: 'tests/screenshots/tc004-after-create-click.png', fullPage: true });
|
||||
|
||||
// Check for the API response
|
||||
const response = await createBetPromise;
|
||||
if (response) {
|
||||
console.log(`Create bet API response status: ${response.status()}`);
|
||||
if (response.status() >= 400) {
|
||||
const responseBody = await response.json().catch(() => ({}));
|
||||
console.log('API Error Response:', responseBody);
|
||||
} else {
|
||||
console.log('Bet created successfully via API');
|
||||
}
|
||||
} else {
|
||||
console.log('No API call detected for bet creation');
|
||||
}
|
||||
|
||||
// Check for success/error toast
|
||||
const successToast = await page.locator('text=Bet created successfully').isVisible({ timeout: 3000 }).catch(() => false);
|
||||
const errorToast = await page.locator('[role="alert"], .toast, text=Failed').isVisible({ timeout: 1000 }).catch(() => false);
|
||||
console.log(`Success toast visible: ${successToast}`);
|
||||
console.log(`Error toast visible: ${errorToast}`);
|
||||
}
|
||||
} else {
|
||||
console.log('Stake input not found - checking alternative UI...');
|
||||
|
||||
// Try SpreadGrid approach - click on a spread cell
|
||||
const spreadCells = page.locator('button').filter({ hasText: /^[+-]?\d+\.?\d*$/ });
|
||||
const spreadCellCount = await spreadCells.count();
|
||||
console.log(`Found ${spreadCellCount} spread cells`);
|
||||
|
||||
if (spreadCellCount > 0) {
|
||||
await spreadCells.first().click();
|
||||
await page.waitForTimeout(1000);
|
||||
await page.screenshot({ path: 'tests/screenshots/tc004-spread-cell-clicked.png', fullPage: true });
|
||||
|
||||
// Check for modal
|
||||
const modalVisible = await page.locator('[role="dialog"], .modal, .fixed.inset-0').isVisible({ timeout: 3000 }).catch(() => false);
|
||||
console.log(`Modal visible after clicking spread: ${modalVisible}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log all collected errors
|
||||
console.log('\n--- Error Summary ---');
|
||||
console.log('JavaScript Errors:', errors.jsErrors);
|
||||
console.log('Network Errors:', errors.networkErrors);
|
||||
console.log('Console Errors:', errors.consoleErrors);
|
||||
});
|
||||
|
||||
test('TC-005: Alice creates bet via SpreadGrid modal flow', async ({ page }) => {
|
||||
const errors = setupErrorTracking(page);
|
||||
|
||||
console.log('\n=== TC-005: Alice Creates Bet via SpreadGrid Modal ===');
|
||||
|
||||
await login(page, TEST_USERS.alice);
|
||||
await page.goto('/sport-events');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Select an event
|
||||
const eventButtons = page.locator('button').filter({ hasText: /vs/i });
|
||||
if (await eventButtons.count() > 0) {
|
||||
await eventButtons.first().click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.screenshot({ path: 'tests/screenshots/tc005-event-view.png', fullPage: true });
|
||||
|
||||
// Click on a spread cell to open modal
|
||||
const spreadCells = page.locator('button').filter({ hasText: /^[+-]?\d+\.?\d*$/ });
|
||||
const spreadCount = await spreadCells.count();
|
||||
console.log(`Found ${spreadCount} spread cells`);
|
||||
|
||||
if (spreadCount > 0) {
|
||||
await spreadCells.nth(Math.floor(spreadCount / 2)).click(); // Click middle spread
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.screenshot({ path: 'tests/screenshots/tc005-spread-modal-opened.png', fullPage: true });
|
||||
|
||||
// Look for "Create New Bet" button in modal
|
||||
const createNewBetButton = page.locator('button').filter({ hasText: /create new bet|create bet/i });
|
||||
const createButtonVisible = await createNewBetButton.isVisible({ timeout: 3000 }).catch(() => false);
|
||||
console.log(`"Create New Bet" button visible: ${createButtonVisible}`);
|
||||
|
||||
if (createButtonVisible) {
|
||||
await createNewBetButton.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.screenshot({ path: 'tests/screenshots/tc005-create-bet-modal.png', fullPage: true });
|
||||
|
||||
// Fill in the stake amount
|
||||
const stakeInput = page.locator('input[type="number"]');
|
||||
if (await stakeInput.isVisible()) {
|
||||
await stakeInput.fill('100');
|
||||
console.log('Entered stake amount: 100');
|
||||
|
||||
await page.screenshot({ path: 'tests/screenshots/tc005-stake-filled.png', fullPage: true });
|
||||
|
||||
// Click submit button
|
||||
const submitButton = page.locator('button[type="submit"], button').filter({ hasText: /create bet/i }).first();
|
||||
|
||||
// Set up API listener
|
||||
const apiPromise = page.waitForResponse(
|
||||
(response) => response.url().includes('/api/v1/spread-bets') && response.request().method() === 'POST',
|
||||
{ timeout: 10000 }
|
||||
).catch(() => null);
|
||||
|
||||
await submitButton.click();
|
||||
console.log('Clicked submit button');
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
await page.screenshot({ path: 'tests/screenshots/tc005-after-submit.png', fullPage: true });
|
||||
|
||||
const response = await apiPromise;
|
||||
if (response) {
|
||||
console.log(`API Response Status: ${response.status()}`);
|
||||
const body = await response.json().catch(() => ({}));
|
||||
console.log('API Response Body:', JSON.stringify(body, null, 2));
|
||||
|
||||
if (response.status() >= 400) {
|
||||
console.log('DEFECT: Bet creation API returned error');
|
||||
}
|
||||
} else {
|
||||
console.log('WARNING: No API call made for bet creation');
|
||||
}
|
||||
|
||||
// Check for success toast
|
||||
const successVisible = await page.locator('text=Bet created successfully').isVisible({ timeout: 3000 }).catch(() => false);
|
||||
console.log(`Success message visible: ${successVisible}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log errors
|
||||
console.log('\n--- Error Summary ---');
|
||||
console.log('JavaScript Errors:', errors.jsErrors);
|
||||
console.log('Network Errors:', errors.networkErrors);
|
||||
});
|
||||
|
||||
test('TC-006: Bob logs in and attempts to take a bet', async ({ page }) => {
|
||||
const errors = setupErrorTracking(page);
|
||||
|
||||
console.log('\n=== TC-006: Bob Takes a Bet ===');
|
||||
|
||||
await login(page, TEST_USERS.bob);
|
||||
await page.goto('/sport-events');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Select an event
|
||||
const eventButtons = page.locator('button').filter({ hasText: /vs/i });
|
||||
if (await eventButtons.count() > 0) {
|
||||
await eventButtons.first().click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.screenshot({ path: 'tests/screenshots/tc006-bob-event-view.png', fullPage: true });
|
||||
|
||||
// Look for spread cells with open bets (green background typically)
|
||||
const openBetCells = page.locator('button.bg-green-50, button').filter({ hasText: /open/i });
|
||||
const openBetCount = await openBetCells.count();
|
||||
console.log(`Found ${openBetCount} cells with open bets indicator`);
|
||||
|
||||
// Click on any spread cell
|
||||
const spreadCells = page.locator('button').filter({ hasText: /^[+-]?\d+\.?\d*$/ });
|
||||
if (await spreadCells.count() > 0) {
|
||||
await spreadCells.first().click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.screenshot({ path: 'tests/screenshots/tc006-spread-detail-modal.png', fullPage: true });
|
||||
|
||||
// Look for "Take Bet" button
|
||||
const takeBetButton = page.locator('button').filter({ hasText: /take.*bet/i });
|
||||
const takeBetVisible = await takeBetButton.isVisible({ timeout: 3000 }).catch(() => false);
|
||||
console.log(`Take Bet button visible: ${takeBetVisible}`);
|
||||
|
||||
if (takeBetVisible) {
|
||||
await takeBetButton.first().click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.screenshot({ path: 'tests/screenshots/tc006-take-bet-modal.png', fullPage: true });
|
||||
|
||||
// Check if TakeBetModal opened properly
|
||||
const modalTitle = await page.locator('text=Take Bet').isVisible({ timeout: 2000 }).catch(() => false);
|
||||
console.log(`TakeBetModal title visible: ${modalTitle}`);
|
||||
|
||||
// Look for confirm button in TakeBetModal
|
||||
const confirmButton = page.locator('button').filter({ hasText: /take bet|confirm/i }).last();
|
||||
|
||||
if (await confirmButton.isVisible()) {
|
||||
// Set up API listener
|
||||
const apiPromise = page.waitForResponse(
|
||||
(response) => response.url().includes('/take') && response.request().method() === 'POST',
|
||||
{ timeout: 10000 }
|
||||
).catch(() => null);
|
||||
|
||||
await confirmButton.click();
|
||||
console.log('Clicked confirm/take button');
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
await page.screenshot({ path: 'tests/screenshots/tc006-after-take.png', fullPage: true });
|
||||
|
||||
const response = await apiPromise;
|
||||
if (response) {
|
||||
console.log(`Take Bet API Response Status: ${response.status()}`);
|
||||
const body = await response.json().catch(() => ({}));
|
||||
console.log('API Response Body:', JSON.stringify(body, null, 2));
|
||||
} else {
|
||||
console.log('WARNING: No API call made for taking bet');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('No "Take Bet" button found - checking if there are available bets');
|
||||
|
||||
// Check modal content
|
||||
const modalContent = await page.locator('.fixed.inset-0').textContent().catch(() => '');
|
||||
console.log('Modal content:', modalContent?.substring(0, 500));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log errors
|
||||
console.log('\n--- Error Summary ---');
|
||||
console.log('JavaScript Errors:', errors.jsErrors);
|
||||
console.log('Network Errors:', errors.networkErrors);
|
||||
});
|
||||
|
||||
test('TC-007: Test TradingPanel Create Bet Button Functionality', async ({ page }) => {
|
||||
const errors = setupErrorTracking(page);
|
||||
|
||||
console.log('\n=== TC-007: TradingPanel Create Bet Button Test ===');
|
||||
|
||||
await login(page, TEST_USERS.alice);
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Click on an event to see TradingPanel
|
||||
const eventCards = page.locator('button').filter({ hasText: /vs/i });
|
||||
if (await eventCards.count() > 0) {
|
||||
await eventCards.first().click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
// Take screenshot of the full event view
|
||||
await page.screenshot({ path: 'tests/screenshots/tc007-full-event-view.png', fullPage: true });
|
||||
|
||||
// Check if we're seeing TradingPanel or SpreadGrid
|
||||
const hasPlaceBetSection = await page.locator('text=Place Bet').isVisible().catch(() => false);
|
||||
const hasStakeInput = await page.locator('input[type="number"]').isVisible().catch(() => false);
|
||||
const hasTeamButtons = await page.locator('button').filter({ hasText: /HOME|AWAY/i }).count() > 0;
|
||||
|
||||
console.log(`"Place Bet" section visible: ${hasPlaceBetSection}`);
|
||||
console.log(`Stake input visible: ${hasStakeInput}`);
|
||||
console.log(`Team selection buttons visible: ${hasTeamButtons}`);
|
||||
|
||||
if (hasPlaceBetSection && hasStakeInput) {
|
||||
console.log('TradingPanel detected - testing create bet flow');
|
||||
|
||||
// Enter stake
|
||||
await page.locator('input[type="number"]').fill('75');
|
||||
|
||||
// Find and click Create Bet button
|
||||
const createButton = page.locator('button').filter({ hasText: /create.*bet/i });
|
||||
if (await createButton.isVisible()) {
|
||||
// Monitor network
|
||||
const responsePromise = page.waitForResponse(
|
||||
(resp) => resp.url().includes('/api/v1/spread-bets') && resp.request().method() === 'POST',
|
||||
{ timeout: 10000 }
|
||||
).catch(() => null);
|
||||
|
||||
await createButton.click();
|
||||
console.log('Create Bet button clicked');
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
await page.screenshot({ path: 'tests/screenshots/tc007-after-create-click.png', fullPage: true });
|
||||
|
||||
const response = await responsePromise;
|
||||
if (response) {
|
||||
const status = response.status();
|
||||
console.log(`API Response Status: ${status}`);
|
||||
|
||||
if (status >= 200 && status < 300) {
|
||||
console.log('SUCCESS: Bet created via TradingPanel');
|
||||
} else {
|
||||
const body = await response.json().catch(() => ({}));
|
||||
console.log('FAILURE: API returned error:', body);
|
||||
}
|
||||
} else {
|
||||
console.log('ISSUE: No API request was made when clicking Create Bet');
|
||||
}
|
||||
} else {
|
||||
console.log('Create Bet button not found');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n--- Error Summary ---');
|
||||
console.log('JavaScript Errors:', errors.jsErrors);
|
||||
console.log('Network Errors:', errors.networkErrors);
|
||||
});
|
||||
|
||||
test('TC-008: Comprehensive Error Detection on Event Page', async ({ page }) => {
|
||||
const errors = setupErrorTracking(page);
|
||||
|
||||
console.log('\n=== TC-008: Comprehensive Error Detection ===');
|
||||
|
||||
// Test as authenticated user
|
||||
await login(page, TEST_USERS.alice);
|
||||
|
||||
// Navigate to sport events
|
||||
await page.goto('/sport-events');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Select first event
|
||||
const events = page.locator('button').filter({ hasText: /vs/i });
|
||||
if (await events.count() > 0) {
|
||||
await events.first().click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
await page.screenshot({ path: 'tests/screenshots/tc008-event-loaded.png', fullPage: true });
|
||||
|
||||
// Interact with various elements and collect errors
|
||||
|
||||
// 1. Click spread cells
|
||||
const spreadCells = page.locator('button').filter({ hasText: /^[+-]?\d+\.?\d*$/ });
|
||||
const spreadCount = await spreadCells.count();
|
||||
console.log(`Testing ${Math.min(spreadCount, 3)} spread cells for errors`);
|
||||
|
||||
for (let i = 0; i < Math.min(spreadCount, 3); i++) {
|
||||
await spreadCells.nth(i).click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Close any modal
|
||||
const closeButton = page.locator('button').filter({ hasText: /[x×]/ }).first();
|
||||
if (await closeButton.isVisible().catch(() => false)) {
|
||||
await closeButton.click();
|
||||
await page.waitForTimeout(300);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Try quick stake buttons if visible
|
||||
const quickStakes = page.locator('button').filter({ hasText: /^\$\d+$/ });
|
||||
if (await quickStakes.count() > 0) {
|
||||
await quickStakes.first().click();
|
||||
await page.waitForTimeout(300);
|
||||
console.log('Quick stake button clicked');
|
||||
}
|
||||
|
||||
// 3. Try team selection if visible
|
||||
const teamButtons = page.locator('button').filter({ hasText: /^(home|away)$/i });
|
||||
if (await teamButtons.count() > 0) {
|
||||
await teamButtons.first().click();
|
||||
await page.waitForTimeout(300);
|
||||
console.log('Team selection button clicked');
|
||||
}
|
||||
}
|
||||
|
||||
await page.screenshot({ path: 'tests/screenshots/tc008-final-state.png', fullPage: true });
|
||||
|
||||
// Report all collected errors
|
||||
console.log('\n========== ERROR REPORT ==========');
|
||||
console.log(`JavaScript Errors (${errors.jsErrors.length}):`);
|
||||
errors.jsErrors.forEach((e, i) => console.log(` ${i + 1}. ${e}`));
|
||||
|
||||
console.log(`\nNetwork Errors (${errors.networkErrors.length}):`);
|
||||
errors.networkErrors.forEach((e, i) => console.log(` ${i + 1}. ${e.status} ${e.url}`));
|
||||
|
||||
console.log(`\nConsole Errors (${errors.consoleErrors.length}):`);
|
||||
errors.consoleErrors.forEach((e, i) => console.log(` ${i + 1}. ${e}`));
|
||||
console.log('===================================');
|
||||
|
||||
// This test documents errors but doesn't fail - it's for diagnostic purposes
|
||||
if (errors.jsErrors.length > 0) {
|
||||
console.log('\nWARNING: JavaScript errors detected during testing');
|
||||
}
|
||||
});
|
||||
|
||||
test('TC-009: Test TakeBetModal Data Bug', async ({ page }) => {
|
||||
const errors = setupErrorTracking(page);
|
||||
|
||||
console.log('\n=== TC-009: TakeBetModal Data Bug Investigation ===');
|
||||
console.log('Known Issue: TakeBetModal uses array as object (betInfo)');
|
||||
|
||||
// First create a bet as Alice
|
||||
await login(page, TEST_USERS.alice);
|
||||
await page.goto('/sport-events');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const events = page.locator('button').filter({ hasText: /vs/i });
|
||||
if (await events.count() > 0) {
|
||||
await events.first().click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Click spread cell
|
||||
const spreadCells = page.locator('button').filter({ hasText: /^[+-]?\d+\.?\d*$/ });
|
||||
if (await spreadCells.count() > 0) {
|
||||
await spreadCells.first().click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Try to create a bet
|
||||
const createButton = page.locator('button').filter({ hasText: /create new bet/i });
|
||||
if (await createButton.isVisible().catch(() => false)) {
|
||||
await createButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const stakeInput = page.locator('input[type="number"]');
|
||||
if (await stakeInput.isVisible()) {
|
||||
await stakeInput.fill('50');
|
||||
|
||||
const submitButton = page.locator('button[type="submit"]').filter({ hasText: /create/i });
|
||||
await submitButton.click();
|
||||
await page.waitForTimeout(2000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Logout and login as Bob
|
||||
await logout(page);
|
||||
await login(page, TEST_USERS.bob);
|
||||
|
||||
await page.goto('/sport-events');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Select same event
|
||||
const bobEvents = page.locator('button').filter({ hasText: /vs/i });
|
||||
if (await bobEvents.count() > 0) {
|
||||
await bobEvents.first().click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.screenshot({ path: 'tests/screenshots/tc009-bob-event-view.png', fullPage: true });
|
||||
|
||||
// Find spread with open bets
|
||||
const openBetIndicator = page.locator('text=/\\d+ open/');
|
||||
const hasOpenBets = await openBetIndicator.count() > 0;
|
||||
console.log(`Found open bets indicator: ${hasOpenBets}`);
|
||||
|
||||
if (hasOpenBets) {
|
||||
// Click on that spread
|
||||
const spreadWithOpenBet = openBetIndicator.locator('..').locator('..');
|
||||
await spreadWithOpenBet.first().click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.screenshot({ path: 'tests/screenshots/tc009-spread-with-open-bet.png', fullPage: true });
|
||||
|
||||
// Find and click Take Bet
|
||||
const takeBetButton = page.locator('button').filter({ hasText: /take bet/i });
|
||||
if (await takeBetButton.isVisible().catch(() => false)) {
|
||||
console.log('Take Bet button found - clicking...');
|
||||
await takeBetButton.first().click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.screenshot({ path: 'tests/screenshots/tc009-take-bet-modal.png', fullPage: true });
|
||||
|
||||
// Check for errors in the modal - the bug would cause issues here
|
||||
const modalContent = await page.locator('[role="dialog"], .fixed.inset-0.bg-black').textContent().catch(() => '');
|
||||
console.log('TakeBetModal content sample:', modalContent?.substring(0, 300));
|
||||
|
||||
// Check if modal shows proper data or undefined
|
||||
const showsUndefined = modalContent?.includes('undefined') || modalContent?.includes('NaN');
|
||||
console.log(`Modal shows undefined/NaN data: ${showsUndefined}`);
|
||||
|
||||
if (showsUndefined) {
|
||||
console.log('DEFECT CONFIRMED: TakeBetModal displays undefined/NaN due to incorrect data access');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n--- Error Summary ---');
|
||||
console.log('JavaScript Errors:', errors.jsErrors);
|
||||
if (errors.jsErrors.some(e => e.includes('Cannot read') || e.includes('undefined'))) {
|
||||
console.log('DEFECT: Type error detected - likely the betInfo array/object bug');
|
||||
}
|
||||
});
|
||||
|
||||
test('TC-010: API Endpoint Verification', async ({ page }) => {
|
||||
console.log('\n=== TC-010: Direct API Endpoint Verification ===');
|
||||
|
||||
await login(page, TEST_USERS.alice);
|
||||
|
||||
// Get auth token from storage
|
||||
const token = await page.evaluate(() => localStorage.getItem('token'));
|
||||
console.log(`Auth token present: ${!!token}`);
|
||||
|
||||
// Test sport-events endpoint
|
||||
const eventsResponse = await page.request.get('http://localhost:8000/api/v1/sport-events', {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
console.log(`GET /sport-events status: ${eventsResponse.status()}`);
|
||||
|
||||
if (eventsResponse.ok()) {
|
||||
const events = await eventsResponse.json();
|
||||
console.log(`Found ${events.length} events`);
|
||||
|
||||
if (events.length > 0) {
|
||||
const eventId = events[0].id;
|
||||
|
||||
// Test event with grid endpoint
|
||||
const gridResponse = await page.request.get(`http://localhost:8000/api/v1/sport-events/${eventId}`, {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
console.log(`GET /sport-events/${eventId} status: ${gridResponse.status()}`);
|
||||
|
||||
if (gridResponse.ok()) {
|
||||
const eventWithGrid = await gridResponse.json();
|
||||
console.log(`Event: ${eventWithGrid.home_team} vs ${eventWithGrid.away_team}`);
|
||||
console.log(`Spread grid keys: ${Object.keys(eventWithGrid.spread_grid || {}).join(', ')}`);
|
||||
}
|
||||
|
||||
// Test create bet endpoint
|
||||
const createBetResponse = await page.request.post('http://localhost:8000/api/v1/spread-bets', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: {
|
||||
event_id: eventId,
|
||||
spread: events[0].official_spread,
|
||||
team: 'home',
|
||||
stake_amount: 100
|
||||
}
|
||||
});
|
||||
console.log(`POST /spread-bets status: ${createBetResponse.status()}`);
|
||||
|
||||
if (!createBetResponse.ok()) {
|
||||
const errorBody = await createBetResponse.json().catch(() => ({}));
|
||||
console.log('Create bet error:', errorBody);
|
||||
} else {
|
||||
const createdBet = await createBetResponse.json();
|
||||
console.log(`Created bet ID: ${createdBet.id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('TradingPanel Specific Tests', () => {
|
||||
test('TC-011: TradingPanel UI Elements Presence', async ({ page }) => {
|
||||
const errors = setupErrorTracking(page);
|
||||
|
||||
console.log('\n=== TC-011: TradingPanel UI Elements Check ===');
|
||||
|
||||
await login(page, TEST_USERS.alice);
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Click on event
|
||||
const events = page.locator('button').filter({ hasText: /vs/i });
|
||||
if (await events.count() > 0) {
|
||||
await events.first().click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
// Check for all expected TradingPanel elements
|
||||
const elements = {
|
||||
'Place Bet heading': await page.locator('text=Place Bet').isVisible().catch(() => false),
|
||||
'Team buttons': await page.locator('button').filter({ hasText: /HOME|AWAY/i }).count() > 0,
|
||||
'Spread selector': await page.locator('button').filter({ hasText: /[+-]/ }).count() > 0,
|
||||
'Stake input': await page.locator('input[type="number"]').isVisible().catch(() => false),
|
||||
'Quick stakes': await page.locator('button').filter({ hasText: /^\$\d+$/ }).count() > 0,
|
||||
'Create button': await page.locator('button').filter({ hasText: /create.*bet/i }).isVisible().catch(() => false),
|
||||
'Order book': await page.locator('text=Order Book').isVisible().catch(() => false),
|
||||
};
|
||||
|
||||
console.log('TradingPanel Elements:');
|
||||
Object.entries(elements).forEach(([name, present]) => {
|
||||
console.log(` ${present ? '[X]' : '[ ]'} ${name}`);
|
||||
});
|
||||
|
||||
await page.screenshot({ path: 'tests/screenshots/tc011-trading-panel-elements.png', fullPage: true });
|
||||
}
|
||||
|
||||
console.log('\nJS Errors:', errors.jsErrors);
|
||||
});
|
||||
});
|
||||
171
frontend/tests/bet-e2e-simple.spec.ts
Normal file
@ -0,0 +1,171 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Bet Creation and Taking E2E', () => {
|
||||
|
||||
test('Create bet as Alice via TradingPanel', async ({ page }) => {
|
||||
const errors: string[] = [];
|
||||
const networkErrors: string[] = [];
|
||||
|
||||
page.on('pageerror', error => {
|
||||
console.log('Page error:', error.message);
|
||||
errors.push(error.message);
|
||||
});
|
||||
|
||||
page.on('response', response => {
|
||||
if (response.status() >= 400) {
|
||||
networkErrors.push(`${response.status()} ${response.url()}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Login as Alice
|
||||
await page.goto('/login');
|
||||
await page.fill('input[type="email"]', 'alice@example.com');
|
||||
await page.fill('input[type="password"]', 'password123');
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
// Wait for login to complete
|
||||
await page.waitForURL('/', { timeout: 10000 });
|
||||
console.log('Alice logged in successfully');
|
||||
|
||||
// Navigate to event page
|
||||
await page.goto('/events/1', { waitUntil: 'networkidle' });
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Take screenshot before
|
||||
await page.screenshot({ path: 'test-results/bet-create-before.png', fullPage: true });
|
||||
|
||||
// Check if TradingPanel is visible
|
||||
const placeBetSection = await page.locator('text=Place Bet').first();
|
||||
expect(await placeBetSection.isVisible()).toBe(true);
|
||||
console.log('Place Bet section is visible');
|
||||
|
||||
// Get initial bet count from stats
|
||||
const betsText = await page.locator('text=/Bets:.*\\d+/').first().textContent();
|
||||
const initialBets = parseInt(betsText?.match(/\d+/)?.[0] || '0');
|
||||
console.log('Initial bet count:', initialBets);
|
||||
|
||||
// Select home team (should already be selected by default)
|
||||
const homeTeamBtn = page.locator('button').filter({ hasText: /Wake Forest|Home/i }).first();
|
||||
if (await homeTeamBtn.isVisible()) {
|
||||
await homeTeamBtn.click();
|
||||
}
|
||||
|
||||
// Enter stake amount
|
||||
const stakeInput = page.locator('input[type="number"]');
|
||||
await stakeInput.fill('50');
|
||||
console.log('Entered stake amount: 50');
|
||||
|
||||
// Click create bet button
|
||||
const createBetBtn = page.locator('button').filter({ hasText: /Create.*Bet/i }).first();
|
||||
expect(await createBetBtn.isVisible()).toBe(true);
|
||||
|
||||
// Listen for API response
|
||||
const responsePromise = page.waitForResponse(
|
||||
response => response.url().includes('/api/v1/spread-bets') && response.request().method() === 'POST',
|
||||
{ timeout: 10000 }
|
||||
);
|
||||
|
||||
await createBetBtn.click();
|
||||
console.log('Clicked Create Bet button');
|
||||
|
||||
// Wait for API response
|
||||
try {
|
||||
const response = await responsePromise;
|
||||
console.log('API Response status:', response.status());
|
||||
const body = await response.json();
|
||||
console.log('API Response body:', JSON.stringify(body, null, 2));
|
||||
|
||||
expect(response.status()).toBe(200);
|
||||
console.log('Bet created successfully via API!');
|
||||
} catch (e) {
|
||||
console.log('No API response received or error:', e);
|
||||
}
|
||||
|
||||
// Wait for UI update
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Take screenshot after
|
||||
await page.screenshot({ path: 'test-results/bet-create-after.png', fullPage: true });
|
||||
|
||||
// Check for success toast
|
||||
const toastVisible = await page.locator('text=successfully').isVisible({ timeout: 5000 }).catch(() => false);
|
||||
console.log('Success toast visible:', toastVisible);
|
||||
|
||||
// Log any errors
|
||||
console.log('Page errors:', errors);
|
||||
console.log('Network errors:', networkErrors);
|
||||
|
||||
expect(errors.length).toBe(0);
|
||||
});
|
||||
|
||||
test('Take bet as Bob', async ({ page }) => {
|
||||
const errors: string[] = [];
|
||||
const networkErrors: string[] = [];
|
||||
|
||||
page.on('pageerror', error => {
|
||||
console.log('Page error:', error.message);
|
||||
errors.push(error.message);
|
||||
});
|
||||
|
||||
page.on('response', response => {
|
||||
if (response.status() >= 400) {
|
||||
networkErrors.push(`${response.status()} ${response.url()}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Login as Bob
|
||||
await page.goto('/login');
|
||||
await page.fill('input[type="email"]', 'bob@example.com');
|
||||
await page.fill('input[type="password"]', 'password123');
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
await page.waitForURL('/', { timeout: 10000 });
|
||||
console.log('Bob logged in successfully');
|
||||
|
||||
// Navigate to event page
|
||||
await page.goto('/events/1', { waitUntil: 'networkidle' });
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
await page.screenshot({ path: 'test-results/bob-event-page.png', fullPage: true });
|
||||
|
||||
// Look for "Take" button in the TradingPanel
|
||||
const takeBtn = page.locator('button').filter({ hasText: 'Take' }).first();
|
||||
|
||||
if (await takeBtn.isVisible({ timeout: 5000 }).catch(() => false)) {
|
||||
console.log('Found Take button');
|
||||
|
||||
// Listen for API response
|
||||
const responsePromise = page.waitForResponse(
|
||||
response => response.url().includes('/api/v1/spread-bets') && response.url().includes('/take'),
|
||||
{ timeout: 10000 }
|
||||
);
|
||||
|
||||
await takeBtn.click();
|
||||
console.log('Clicked Take button');
|
||||
|
||||
try {
|
||||
const response = await responsePromise;
|
||||
console.log('Take bet API Response status:', response.status());
|
||||
const body = await response.json();
|
||||
console.log('Take bet API Response:', JSON.stringify(body, null, 2));
|
||||
|
||||
expect(response.status()).toBe(200);
|
||||
expect(body.status).toBe('matched');
|
||||
console.log('Bet taken successfully!');
|
||||
} catch (e) {
|
||||
console.log('Take bet API error:', e);
|
||||
}
|
||||
} else {
|
||||
console.log('No Take button found - checking available bets');
|
||||
// List what buttons are available
|
||||
const buttons = await page.locator('button').allTextContents();
|
||||
console.log('Available buttons:', buttons.filter(b => b.trim()));
|
||||
}
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
await page.screenshot({ path: 'test-results/bob-after-take.png', fullPage: true });
|
||||
|
||||
console.log('Page errors:', errors);
|
||||
console.log('Network errors:', networkErrors);
|
||||
});
|
||||
});
|
||||
582
frontend/tests/bet-flow-investigation.spec.ts
Normal file
@ -0,0 +1,582 @@
|
||||
import { test, expect, Page } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Focused Investigation Tests for Bet Creation and Taking Issues
|
||||
*
|
||||
* This test suite investigates why bet creation and taking are not working
|
||||
* on the events page.
|
||||
*/
|
||||
|
||||
const TEST_USERS = {
|
||||
alice: { email: 'alice@example.com', password: 'password123' },
|
||||
bob: { email: 'bob@example.com', password: 'password123' },
|
||||
};
|
||||
|
||||
interface TestErrors {
|
||||
jsErrors: string[];
|
||||
networkErrors: { url: string; status: number; method: string; body?: any }[];
|
||||
consoleErrors: string[];
|
||||
}
|
||||
|
||||
function setupErrorTracking(page: Page): TestErrors {
|
||||
const errors: TestErrors = {
|
||||
jsErrors: [],
|
||||
networkErrors: [],
|
||||
consoleErrors: [],
|
||||
};
|
||||
|
||||
page.on('pageerror', (error) => {
|
||||
errors.jsErrors.push(`${error.name}: ${error.message}`);
|
||||
});
|
||||
|
||||
page.on('response', async (response) => {
|
||||
if (response.status() >= 400) {
|
||||
let body = null;
|
||||
try {
|
||||
body = await response.json();
|
||||
} catch {}
|
||||
errors.networkErrors.push({
|
||||
url: response.url(),
|
||||
status: response.status(),
|
||||
method: response.request().method(),
|
||||
body,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
page.on('console', (msg) => {
|
||||
if (msg.type() === 'error') {
|
||||
errors.consoleErrors.push(msg.text());
|
||||
}
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
async function login(page: Page, user: { email: string; password: string }) {
|
||||
await page.goto('/login');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.fill('input[type="email"]', user.email);
|
||||
await page.fill('input[type="password"]', user.password);
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(1500);
|
||||
}
|
||||
|
||||
test.describe('Bet Flow Investigation', () => {
|
||||
test('INVESTIGATION-001: Check event click and TradingPanel on home page', async ({ page }) => {
|
||||
const errors = setupErrorTracking(page);
|
||||
|
||||
console.log('\n=== INVESTIGATION-001: Home Page Event Click ===');
|
||||
|
||||
await login(page, TEST_USERS.alice);
|
||||
console.log('Logged in as Alice');
|
||||
|
||||
await page.screenshot({ path: 'tests/screenshots/inv001-after-login.png', fullPage: true });
|
||||
|
||||
// The home page shows events in a table/list format
|
||||
// Look for event rows that contain "vs"
|
||||
const eventRows = page.locator('tr, [class*="divide-y"] > div, button').filter({ hasText: /vs/i });
|
||||
const eventCount = await eventRows.count();
|
||||
console.log(`Found ${eventCount} event elements`);
|
||||
|
||||
if (eventCount > 0) {
|
||||
// Click first event
|
||||
const firstEvent = eventRows.first();
|
||||
const eventText = await firstEvent.textContent();
|
||||
console.log(`Clicking event: ${eventText?.substring(0, 100)}...`);
|
||||
|
||||
await firstEvent.click();
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
await page.screenshot({ path: 'tests/screenshots/inv001-after-event-click.png', fullPage: true });
|
||||
|
||||
// Check what's visible now
|
||||
const pageContent = await page.content();
|
||||
|
||||
// Check for TradingPanel elements
|
||||
const tradingPanelElements = {
|
||||
'Order Book': await page.locator('text=Order Book').isVisible(),
|
||||
'Place Bet': await page.locator('text=Place Bet').isVisible(),
|
||||
'Create Bet button': await page.locator('button').filter({ hasText: /create.*bet/i }).isVisible(),
|
||||
'Stake input': await page.locator('input[type="number"]').first().isVisible(),
|
||||
'Team selection': await page.locator('button').filter({ hasText: /home|away/i }).first().isVisible(),
|
||||
};
|
||||
|
||||
console.log('\nTradingPanel Elements:');
|
||||
for (const [name, visible] of Object.entries(tradingPanelElements)) {
|
||||
console.log(` ${visible ? '[FOUND]' : '[MISSING]'} ${name}`);
|
||||
}
|
||||
|
||||
// Check for SpreadGrid elements
|
||||
const spreadGridElements = {
|
||||
'Spread cells': await page.locator('button').filter({ hasText: /^[+-]?\d+\.?\d*$/ }).count(),
|
||||
'Back button': await page.locator('text=Back').isVisible(),
|
||||
};
|
||||
|
||||
console.log('\nSpreadGrid Elements:');
|
||||
console.log(` Spread cells found: ${spreadGridElements['Spread cells']}`);
|
||||
console.log(` Back button visible: ${spreadGridElements['Back button']}`);
|
||||
}
|
||||
|
||||
console.log('\n--- Errors ---');
|
||||
console.log('JS Errors:', errors.jsErrors);
|
||||
console.log('Network Errors:', errors.networkErrors);
|
||||
console.log('Console Errors:', errors.consoleErrors);
|
||||
});
|
||||
|
||||
test('INVESTIGATION-002: Test bet creation flow step by step', async ({ page }) => {
|
||||
const errors = setupErrorTracking(page);
|
||||
|
||||
console.log('\n=== INVESTIGATION-002: Bet Creation Step by Step ===');
|
||||
|
||||
await login(page, TEST_USERS.alice);
|
||||
|
||||
// Navigate directly to sport events page
|
||||
await page.goto('/sport-events');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
console.log('On Sport Events page');
|
||||
await page.screenshot({ path: 'tests/screenshots/inv002-sport-events.png', fullPage: true });
|
||||
|
||||
// Find and click on an event
|
||||
const eventButtons = page.locator('button').filter({ hasText: /vs/i });
|
||||
const count = await eventButtons.count();
|
||||
console.log(`Found ${count} event buttons`);
|
||||
|
||||
if (count > 0) {
|
||||
await eventButtons.first().click();
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
console.log('Clicked on first event');
|
||||
await page.screenshot({ path: 'tests/screenshots/inv002-event-selected.png', fullPage: true });
|
||||
|
||||
// Look for spread cells and click one
|
||||
const spreadCells = page.locator('button').filter({ hasText: /^[+-]?\d+\.?\d*$/ });
|
||||
const spreadCount = await spreadCells.count();
|
||||
console.log(`Found ${spreadCount} spread cells`);
|
||||
|
||||
if (spreadCount > 0) {
|
||||
// Click on a spread cell near the middle
|
||||
const middleIndex = Math.floor(spreadCount / 2);
|
||||
const spreadCell = spreadCells.nth(middleIndex);
|
||||
const spreadValue = await spreadCell.textContent();
|
||||
console.log(`Clicking spread cell: ${spreadValue}`);
|
||||
|
||||
await spreadCell.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.screenshot({ path: 'tests/screenshots/inv002-spread-modal.png', fullPage: true });
|
||||
|
||||
// Check if modal opened
|
||||
const modalOverlay = page.locator('.fixed.inset-0.bg-black, [role="dialog"]');
|
||||
const modalVisible = await modalOverlay.isVisible();
|
||||
console.log(`Modal overlay visible: ${modalVisible}`);
|
||||
|
||||
if (modalVisible) {
|
||||
// Look for Create New Bet button
|
||||
const createButton = page.locator('button').filter({ hasText: /create new bet/i });
|
||||
const createVisible = await createButton.isVisible();
|
||||
console.log(`"Create New Bet" button visible: ${createVisible}`);
|
||||
|
||||
if (createVisible) {
|
||||
await createButton.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.screenshot({ path: 'tests/screenshots/inv002-create-modal.png', fullPage: true });
|
||||
|
||||
// Fill stake amount
|
||||
const stakeInput = page.locator('input[type="number"]');
|
||||
if (await stakeInput.isVisible()) {
|
||||
await stakeInput.fill('50');
|
||||
console.log('Entered stake amount: 50');
|
||||
|
||||
// Monitor API call
|
||||
const apiPromise = page.waitForResponse(
|
||||
resp => resp.url().includes('/api/v1/spread-bets') && resp.request().method() === 'POST',
|
||||
{ timeout: 15000 }
|
||||
).catch(e => {
|
||||
console.log('API wait error:', e.message);
|
||||
return null;
|
||||
});
|
||||
|
||||
// Click submit
|
||||
const submitButton = page.locator('button[type="submit"]');
|
||||
if (await submitButton.isVisible()) {
|
||||
console.log('Clicking submit button...');
|
||||
await submitButton.click();
|
||||
|
||||
await page.waitForTimeout(3000);
|
||||
await page.screenshot({ path: 'tests/screenshots/inv002-after-submit.png', fullPage: true });
|
||||
|
||||
const response = await apiPromise;
|
||||
if (response) {
|
||||
console.log(`API Response: ${response.status()} ${response.statusText()}`);
|
||||
const body = await response.json().catch(() => null);
|
||||
if (body) {
|
||||
console.log('Response body:', JSON.stringify(body, null, 2));
|
||||
}
|
||||
} else {
|
||||
console.log('WARNING: No API call was made!');
|
||||
}
|
||||
|
||||
// Check for success/error toast
|
||||
await page.waitForTimeout(500);
|
||||
const toastText = await page.locator('[class*="toast"], [role="alert"]').textContent().catch(() => '');
|
||||
console.log(`Toast message: ${toastText}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n--- Final Error Summary ---');
|
||||
console.log('JS Errors:', errors.jsErrors);
|
||||
console.log('Network Errors:', errors.networkErrors.map(e => `${e.method} ${e.url}: ${e.status}`));
|
||||
console.log('Console Errors:', errors.consoleErrors);
|
||||
});
|
||||
|
||||
test('INVESTIGATION-003: Test TradingPanel bet creation', async ({ page }) => {
|
||||
const errors = setupErrorTracking(page);
|
||||
|
||||
console.log('\n=== INVESTIGATION-003: TradingPanel Bet Creation ===');
|
||||
|
||||
await login(page, TEST_USERS.alice);
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Click on an event row/cell
|
||||
const eventElements = page.locator('tr:has-text("vs"), div:has-text("vs")').filter({ hasText: /football|basketball|hockey|soccer/i });
|
||||
const eventCount = await eventElements.count();
|
||||
console.log(`Found ${eventCount} event rows/divs`);
|
||||
|
||||
// Try clicking on the event name text directly
|
||||
const eventLinks = page.locator('text=/\\w+ vs \\w+/i');
|
||||
const linkCount = await eventLinks.count();
|
||||
console.log(`Found ${linkCount} event name links`);
|
||||
|
||||
if (linkCount > 0) {
|
||||
await eventLinks.first().click();
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
await page.screenshot({ path: 'tests/screenshots/inv003-after-click.png', fullPage: true });
|
||||
|
||||
// Check current URL
|
||||
const url = page.url();
|
||||
console.log(`Current URL: ${url}`);
|
||||
|
||||
// Check for TradingPanel
|
||||
const placeBetVisible = await page.locator('text=Place Bet').isVisible();
|
||||
console.log(`"Place Bet" section visible: ${placeBetVisible}`);
|
||||
|
||||
if (placeBetVisible) {
|
||||
// Enter stake
|
||||
const stakeInput = page.locator('input[type="number"]').first();
|
||||
if (await stakeInput.isVisible()) {
|
||||
await stakeInput.fill('100');
|
||||
console.log('Entered stake: 100');
|
||||
|
||||
// Find create button
|
||||
const createBtn = page.locator('button').filter({ hasText: /create.*bet/i });
|
||||
const btnText = await createBtn.textContent();
|
||||
console.log(`Create button text: ${btnText}`);
|
||||
|
||||
if (await createBtn.isEnabled()) {
|
||||
console.log('Create button is enabled, clicking...');
|
||||
|
||||
// Listen for network request
|
||||
let apiCalled = false;
|
||||
page.on('request', req => {
|
||||
if (req.url().includes('/spread-bets') && req.method() === 'POST') {
|
||||
apiCalled = true;
|
||||
console.log('API Request detected:', req.url());
|
||||
console.log('Request body:', req.postData());
|
||||
}
|
||||
});
|
||||
|
||||
await createBtn.click();
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
console.log(`API was called: ${apiCalled}`);
|
||||
await page.screenshot({ path: 'tests/screenshots/inv003-after-create.png', fullPage: true });
|
||||
} else {
|
||||
console.log('Create button is disabled');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n--- Errors ---');
|
||||
console.log('JS Errors:', errors.jsErrors);
|
||||
console.log('Network Errors:', errors.networkErrors);
|
||||
});
|
||||
|
||||
test('INVESTIGATION-004: Direct API test for bet creation', async ({ page, request }) => {
|
||||
console.log('\n=== INVESTIGATION-004: Direct API Test ===');
|
||||
|
||||
// Login via UI to get token
|
||||
await login(page, TEST_USERS.alice);
|
||||
|
||||
// Get token from localStorage
|
||||
const token = await page.evaluate(() => localStorage.getItem('token'));
|
||||
console.log(`Token retrieved: ${token ? 'Yes' : 'No'}`);
|
||||
|
||||
if (!token) {
|
||||
console.log('ERROR: No token found after login');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get events
|
||||
const eventsResponse = await request.get('http://localhost:8000/api/v1/sport-events', {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
console.log(`GET /sport-events: ${eventsResponse.status()}`);
|
||||
|
||||
const events = await eventsResponse.json();
|
||||
console.log(`Found ${events.length} events`);
|
||||
|
||||
if (events.length > 0) {
|
||||
const event = events[0];
|
||||
console.log(`Testing with event: ${event.home_team} vs ${event.away_team}`);
|
||||
console.log(`Event ID: ${event.id}, Official spread: ${event.official_spread}`);
|
||||
console.log(`Min bet: ${event.min_bet_amount}, Max bet: ${event.max_bet_amount}`);
|
||||
|
||||
// Create bet directly via API
|
||||
const createResponse = await request.post('http://localhost:8000/api/v1/spread-bets', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: {
|
||||
event_id: event.id,
|
||||
spread: event.official_spread,
|
||||
team: 'home',
|
||||
stake_amount: 50
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`POST /spread-bets: ${createResponse.status()}`);
|
||||
const responseBody = await createResponse.json();
|
||||
console.log('Response:', JSON.stringify(responseBody, null, 2));
|
||||
|
||||
if (createResponse.status() >= 400) {
|
||||
console.log('BET CREATION FAILED via direct API');
|
||||
} else {
|
||||
console.log('BET CREATED SUCCESSFULLY via direct API');
|
||||
console.log(`Bet ID: ${responseBody.id}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('INVESTIGATION-005: Check for TakeBetModal bug', async ({ page }) => {
|
||||
const errors = setupErrorTracking(page);
|
||||
|
||||
console.log('\n=== INVESTIGATION-005: TakeBetModal Data Bug ===');
|
||||
console.log('Investigating: betInfo is array but used as object');
|
||||
|
||||
// First, create a bet as Alice via API
|
||||
await login(page, TEST_USERS.alice);
|
||||
const aliceToken = await page.evaluate(() => localStorage.getItem('token'));
|
||||
|
||||
// Get event
|
||||
const eventsResp = await page.request.get('http://localhost:8000/api/v1/sport-events', {
|
||||
headers: { Authorization: `Bearer ${aliceToken}` }
|
||||
});
|
||||
const events = await eventsResp.json();
|
||||
|
||||
if (events.length > 0) {
|
||||
const event = events[0];
|
||||
|
||||
// Create bet as Alice
|
||||
const createResp = await page.request.post('http://localhost:8000/api/v1/spread-bets', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${aliceToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: {
|
||||
event_id: event.id,
|
||||
spread: event.official_spread,
|
||||
team: 'home',
|
||||
stake_amount: 75
|
||||
}
|
||||
});
|
||||
|
||||
if (createResp.ok()) {
|
||||
const createdBet = await createResp.json();
|
||||
console.log(`Created bet ${createdBet.id} as Alice`);
|
||||
|
||||
// Now logout and login as Bob
|
||||
await page.goto('/');
|
||||
await page.locator('button:has-text("Logout")').click().catch(() => {});
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await login(page, TEST_USERS.bob);
|
||||
console.log('Logged in as Bob');
|
||||
|
||||
// Navigate to sport events
|
||||
await page.goto('/sport-events');
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
// Select the event
|
||||
const eventBtn = page.locator('button').filter({ hasText: new RegExp(event.home_team, 'i') });
|
||||
if (await eventBtn.count() > 0) {
|
||||
await eventBtn.first().click();
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
await page.screenshot({ path: 'tests/screenshots/inv005-bob-event.png', fullPage: true });
|
||||
|
||||
// Find spread cell with the bet
|
||||
const spreadCell = page.locator('button').filter({ hasText: new RegExp(`^[+-]?${event.official_spread}$`) });
|
||||
if (await spreadCell.count() > 0) {
|
||||
await spreadCell.first().click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.screenshot({ path: 'tests/screenshots/inv005-spread-modal.png', fullPage: true });
|
||||
|
||||
// Look for Take Bet button
|
||||
const takeBetBtn = page.locator('button').filter({ hasText: /take bet/i });
|
||||
if (await takeBetBtn.count() > 0) {
|
||||
console.log('Take Bet button found');
|
||||
await takeBetBtn.first().click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.screenshot({ path: 'tests/screenshots/inv005-take-modal.png', fullPage: true });
|
||||
|
||||
// Check modal content for undefined/NaN
|
||||
const modalText = await page.locator('.fixed.inset-0').textContent().catch(() => '');
|
||||
console.log('Modal text sample:', modalText?.substring(0, 500));
|
||||
|
||||
const hasUndefined = modalText?.includes('undefined');
|
||||
const hasNaN = modalText?.includes('NaN');
|
||||
const hasNull = modalText?.includes('null');
|
||||
|
||||
console.log(`Contains "undefined": ${hasUndefined}`);
|
||||
console.log(`Contains "NaN": ${hasNaN}`);
|
||||
console.log(`Contains "null": ${hasNull}`);
|
||||
|
||||
if (hasUndefined || hasNaN) {
|
||||
console.log('\nDEFECT CONFIRMED: TakeBetModal displays corrupted data');
|
||||
console.log('Root cause: betInfo is assigned from spread_grid[spread] which returns SpreadGridBet[]');
|
||||
console.log('but is used as a single SpreadGridBet object (accessing .creator_username, .stake directly)');
|
||||
}
|
||||
} else {
|
||||
console.log('No Take Bet button found - bet may not be visible to Bob');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n--- Errors ---');
|
||||
console.log('JS Errors:', errors.jsErrors);
|
||||
console.log('Network Errors:', errors.networkErrors);
|
||||
});
|
||||
|
||||
test('INVESTIGATION-006: Full bet creation and taking workflow', async ({ page }) => {
|
||||
const errors = setupErrorTracking(page);
|
||||
|
||||
console.log('\n=== INVESTIGATION-006: Full Workflow Test ===');
|
||||
|
||||
// Step 1: Login as Alice and create bet
|
||||
await login(page, TEST_USERS.alice);
|
||||
const aliceToken = await page.evaluate(() => localStorage.getItem('token'));
|
||||
|
||||
// Get events
|
||||
const eventsResp = await page.request.get('http://localhost:8000/api/v1/sport-events', {
|
||||
headers: { Authorization: `Bearer ${aliceToken}` }
|
||||
});
|
||||
const events = await eventsResp.json();
|
||||
console.log(`Found ${events.length} events`);
|
||||
|
||||
if (events.length === 0) {
|
||||
console.log('ERROR: No events available');
|
||||
return;
|
||||
}
|
||||
|
||||
const event = events[0];
|
||||
console.log(`Using event: ${event.home_team} vs ${event.away_team}`);
|
||||
|
||||
// Create bet via API (to ensure it works)
|
||||
const createResp = await page.request.post('http://localhost:8000/api/v1/spread-bets', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${aliceToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: {
|
||||
event_id: event.id,
|
||||
spread: event.official_spread,
|
||||
team: 'home',
|
||||
stake_amount: 50
|
||||
}
|
||||
});
|
||||
|
||||
const createStatus = createResp.status();
|
||||
const createBody = await createResp.json();
|
||||
console.log(`Create bet API: ${createStatus}`);
|
||||
console.log('Create response:', createBody);
|
||||
|
||||
if (createStatus >= 400) {
|
||||
console.log('DEFECT: Cannot create bet via API');
|
||||
return;
|
||||
}
|
||||
|
||||
const betId = createBody.id;
|
||||
console.log(`Created bet ID: ${betId}`);
|
||||
|
||||
// Step 2: Logout Alice
|
||||
await page.goto('/');
|
||||
await page.getByRole('button', { name: /logout/i }).click().catch(() => {});
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Step 3: Login as Bob and try to take bet
|
||||
await login(page, TEST_USERS.bob);
|
||||
const bobToken = await page.evaluate(() => localStorage.getItem('token'));
|
||||
console.log('Logged in as Bob');
|
||||
|
||||
// Try to take bet via API
|
||||
const takeResp = await page.request.post(`http://localhost:8000/api/v1/spread-bets/${betId}/take`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${bobToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const takeStatus = takeResp.status();
|
||||
const takeBody = await takeResp.json();
|
||||
console.log(`Take bet API: ${takeStatus}`);
|
||||
console.log('Take response:', takeBody);
|
||||
|
||||
if (takeStatus >= 400) {
|
||||
console.log('DEFECT: Cannot take bet via API');
|
||||
} else {
|
||||
console.log('SUCCESS: Bet taken successfully via API');
|
||||
console.log(`Bet status: ${takeBody.status}`);
|
||||
}
|
||||
|
||||
// Now test via UI
|
||||
console.log('\n--- UI Test ---');
|
||||
await page.goto('/sport-events');
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
// Click on the event
|
||||
const eventBtn = page.locator('button').filter({ hasText: new RegExp(event.home_team, 'i') });
|
||||
if (await eventBtn.count() > 0) {
|
||||
await eventBtn.first().click();
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
await page.screenshot({ path: 'tests/screenshots/inv006-event-view.png', fullPage: true });
|
||||
|
||||
// Check what's displayed
|
||||
const pageText = await page.textContent('body');
|
||||
console.log(`Page shows "matched": ${pageText?.includes('matched')}`);
|
||||
console.log(`Page shows "open": ${pageText?.includes('open')}`);
|
||||
}
|
||||
|
||||
console.log('\n--- Final Error Summary ---');
|
||||
console.log('JS Errors:', errors.jsErrors);
|
||||
console.log('Network Errors:', errors.networkErrors.map(e => `${e.method} ${e.url}: ${e.status} ${JSON.stringify(e.body)}`));
|
||||
});
|
||||
});
|
||||
BIN
frontend/tests/screenshots/inv001-after-login.png
Normal file
|
After Width: | Height: | Size: 613 KiB |
BIN
frontend/tests/screenshots/inv002-event-selected.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
frontend/tests/screenshots/inv002-sport-events.png
Normal file
|
After Width: | Height: | Size: 390 KiB |
BIN
frontend/tests/screenshots/inv002-spread-modal.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
frontend/tests/screenshots/inv003-after-click.png
Normal file
|
After Width: | Height: | Size: 288 KiB |
BIN
frontend/tests/screenshots/inv003-after-create.png
Normal file
|
After Width: | Height: | Size: 289 KiB |
BIN
frontend/tests/screenshots/take001-event-page.png
Normal file
|
After Width: | Height: | Size: 363 KiB |
BIN
frontend/tests/screenshots/take002-after-confirm.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
frontend/tests/screenshots/take002-spread-grid.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
frontend/tests/screenshots/take002-spread-modal.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
frontend/tests/screenshots/take002-take-modal.png
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
frontend/tests/screenshots/take003-take-modal.png
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
frontend/tests/screenshots/take004-bob-grid.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
frontend/tests/screenshots/take004-detail-modal.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
frontend/tests/screenshots/take004-take-modal.png
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
frontend/tests/screenshots/tc001-home-page.png
Normal file
|
After Width: | Height: | Size: 684 KiB |
478
frontend/tests/take-bet-flow.spec.ts
Normal file
@ -0,0 +1,478 @@
|
||||
import { test, expect, Page } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Focused Tests for Take Bet Flow
|
||||
* Investigating why taking a bet doesn't work
|
||||
*/
|
||||
|
||||
const TEST_USERS = {
|
||||
alice: { email: 'alice@example.com', password: 'password123' },
|
||||
bob: { email: 'bob@example.com', password: 'password123' },
|
||||
};
|
||||
|
||||
interface TestErrors {
|
||||
jsErrors: string[];
|
||||
networkErrors: { url: string; status: number; method: string; body?: any }[];
|
||||
consoleErrors: string[];
|
||||
}
|
||||
|
||||
function setupErrorTracking(page: Page): TestErrors {
|
||||
const errors: TestErrors = {
|
||||
jsErrors: [],
|
||||
networkErrors: [],
|
||||
consoleErrors: [],
|
||||
};
|
||||
|
||||
page.on('pageerror', (error) => {
|
||||
errors.jsErrors.push(`${error.name}: ${error.message}`);
|
||||
});
|
||||
|
||||
page.on('response', async (response) => {
|
||||
if (response.status() >= 400) {
|
||||
let body = null;
|
||||
try {
|
||||
body = await response.json();
|
||||
} catch {}
|
||||
errors.networkErrors.push({
|
||||
url: response.url(),
|
||||
status: response.status(),
|
||||
method: response.request().method(),
|
||||
body,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
page.on('console', (msg) => {
|
||||
if (msg.type() === 'error') {
|
||||
errors.consoleErrors.push(msg.text());
|
||||
}
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
async function login(page: Page, user: { email: string; password: string }) {
|
||||
await page.goto('/login');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.fill('input[type="email"]', user.email);
|
||||
await page.fill('input[type="password"]', user.password);
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(1500);
|
||||
}
|
||||
|
||||
test.describe('Take Bet Flow Investigation', () => {
|
||||
test('TAKE-001: Bob takes bet via TradingPanel', async ({ page }) => {
|
||||
const errors = setupErrorTracking(page);
|
||||
|
||||
console.log('\n=== TAKE-001: Take Bet via TradingPanel ===');
|
||||
|
||||
// Login as Bob
|
||||
await login(page, TEST_USERS.bob);
|
||||
console.log('Logged in as Bob');
|
||||
|
||||
// Go to home page and click on an event
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Click on first event name to go to event detail page
|
||||
const eventLinks = page.locator('text=/\\w+ vs \\w+/i');
|
||||
const linkCount = await eventLinks.count();
|
||||
console.log(`Found ${linkCount} event links`);
|
||||
|
||||
if (linkCount > 0) {
|
||||
await eventLinks.first().click();
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
await page.screenshot({ path: 'tests/screenshots/take001-event-page.png', fullPage: true });
|
||||
|
||||
// Look for "Take Existing Bet" section in TradingPanel
|
||||
const takeExistingSection = page.locator('text=Take Existing Bet');
|
||||
const hasTakeSection = await takeExistingSection.isVisible().catch(() => false);
|
||||
console.log(`"Take Existing Bet" section visible: ${hasTakeSection}`);
|
||||
|
||||
// Look for Take buttons
|
||||
const takeButtons = page.locator('button').filter({ hasText: /^take$/i });
|
||||
const takeButtonCount = await takeButtons.count();
|
||||
console.log(`Found ${takeButtonCount} Take buttons`);
|
||||
|
||||
if (takeButtonCount > 0) {
|
||||
// Set up API listener
|
||||
let apiCalled = false;
|
||||
let apiUrl = '';
|
||||
let apiResponse: any = null;
|
||||
|
||||
page.on('request', req => {
|
||||
if (req.url().includes('/take') && req.method() === 'POST') {
|
||||
apiCalled = true;
|
||||
apiUrl = req.url();
|
||||
console.log('Take API Request:', req.url());
|
||||
}
|
||||
});
|
||||
|
||||
page.on('response', async resp => {
|
||||
if (resp.url().includes('/take')) {
|
||||
apiResponse = {
|
||||
status: resp.status(),
|
||||
body: await resp.json().catch(() => null)
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Click first Take button
|
||||
await takeButtons.first().click();
|
||||
console.log('Clicked Take button');
|
||||
|
||||
await page.waitForTimeout(3000);
|
||||
await page.screenshot({ path: 'tests/screenshots/take001-after-take.png', fullPage: true });
|
||||
|
||||
console.log(`API was called: ${apiCalled}`);
|
||||
if (apiCalled) {
|
||||
console.log(`API URL: ${apiUrl}`);
|
||||
}
|
||||
if (apiResponse) {
|
||||
console.log('API Response:', apiResponse);
|
||||
}
|
||||
|
||||
// Check for toast messages
|
||||
const successToast = await page.locator('text=Bet taken successfully').isVisible().catch(() => false);
|
||||
const errorToast = await page.locator('[role="status"]').textContent().catch(() => '');
|
||||
console.log(`Success toast visible: ${successToast}`);
|
||||
console.log(`Toast content: ${errorToast}`);
|
||||
} else {
|
||||
console.log('No Take buttons found in TradingPanel - checking if there are open bets');
|
||||
|
||||
// Check order book for open bets
|
||||
const openIndicators = await page.locator('text=/\\d+ open/i').count();
|
||||
console.log(`Open bet indicators found: ${openIndicators}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n--- Errors ---');
|
||||
console.log('JS Errors:', errors.jsErrors);
|
||||
console.log('Network Errors:', errors.networkErrors);
|
||||
});
|
||||
|
||||
test('TAKE-002: Bob takes bet via SpreadGrid modal', async ({ page }) => {
|
||||
const errors = setupErrorTracking(page);
|
||||
|
||||
console.log('\n=== TAKE-002: Take Bet via SpreadGrid Modal ===');
|
||||
|
||||
await login(page, TEST_USERS.bob);
|
||||
console.log('Logged in as Bob');
|
||||
|
||||
await page.goto('/sport-events');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Select first event
|
||||
const eventButtons = page.locator('button').filter({ hasText: /vs/i });
|
||||
if (await eventButtons.count() > 0) {
|
||||
await eventButtons.first().click();
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
await page.screenshot({ path: 'tests/screenshots/take002-spread-grid.png', fullPage: true });
|
||||
|
||||
// Find a spread with open bets (green background, shows "X open")
|
||||
const spreadsWithOpen = page.locator('button').filter({ hasText: /open/i });
|
||||
const openSpreadCount = await spreadsWithOpen.count();
|
||||
console.log(`Found ${openSpreadCount} spreads with open bets`);
|
||||
|
||||
if (openSpreadCount > 0) {
|
||||
// Click on spread with open bets
|
||||
await spreadsWithOpen.first().click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.screenshot({ path: 'tests/screenshots/take002-spread-modal.png', fullPage: true });
|
||||
|
||||
// Look for Take Bet button in modal
|
||||
const takeBetButtons = page.locator('button').filter({ hasText: /take bet/i });
|
||||
const takeBtnCount = await takeBetButtons.count();
|
||||
console.log(`Found ${takeBtnCount} "Take Bet" buttons in modal`);
|
||||
|
||||
if (takeBtnCount > 0) {
|
||||
// Log modal content before clicking
|
||||
const modalContent = await page.locator('.fixed.inset-0').textContent().catch(() => '');
|
||||
console.log('Modal content preview:', modalContent?.substring(0, 300));
|
||||
|
||||
// Set up API listener
|
||||
const apiPromise = page.waitForResponse(
|
||||
resp => resp.url().includes('/take') && resp.request().method() === 'POST',
|
||||
{ timeout: 10000 }
|
||||
).catch(() => null);
|
||||
|
||||
// Click Take Bet button
|
||||
await takeBetButtons.first().click();
|
||||
console.log('Clicked "Take Bet" button');
|
||||
|
||||
await page.waitForTimeout(1500);
|
||||
await page.screenshot({ path: 'tests/screenshots/take002-take-modal.png', fullPage: true });
|
||||
|
||||
// Check if TakeBetModal opened
|
||||
const takeModalTitle = await page.locator('h2, h3').filter({ hasText: /take bet/i }).isVisible().catch(() => false);
|
||||
console.log(`TakeBetModal title visible: ${takeModalTitle}`);
|
||||
|
||||
// Check for data issues in modal (the bug: betInfo is array used as object)
|
||||
const takeModalContent = await page.locator('.fixed.inset-0').last().textContent().catch(() => '');
|
||||
console.log('TakeBetModal content:', takeModalContent?.substring(0, 500));
|
||||
|
||||
const hasUndefined = takeModalContent?.includes('undefined');
|
||||
const hasNaN = takeModalContent?.includes('NaN');
|
||||
console.log(`Modal shows undefined: ${hasUndefined}`);
|
||||
console.log(`Modal shows NaN: ${hasNaN}`);
|
||||
|
||||
if (hasUndefined || hasNaN) {
|
||||
console.log('DEFECT CONFIRMED: TakeBetModal has data display issue');
|
||||
}
|
||||
|
||||
// Look for confirm button in TakeBetModal
|
||||
const confirmTakeBtn = page.locator('button').filter({ hasText: /take bet.*\$/i });
|
||||
if (await confirmTakeBtn.isVisible().catch(() => false)) {
|
||||
console.log('Clicking confirm button in TakeBetModal');
|
||||
await confirmTakeBtn.click();
|
||||
|
||||
await page.waitForTimeout(3000);
|
||||
await page.screenshot({ path: 'tests/screenshots/take002-after-confirm.png', fullPage: true });
|
||||
|
||||
const response = await apiPromise;
|
||||
if (response) {
|
||||
console.log(`Take API status: ${response.status()}`);
|
||||
const body = await response.json().catch(() => null);
|
||||
console.log('Take API response:', body);
|
||||
} else {
|
||||
console.log('No API call made');
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('No spreads with open bets found');
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n--- Errors ---');
|
||||
console.log('JS Errors:', errors.jsErrors);
|
||||
console.log('Network Errors:', errors.networkErrors);
|
||||
});
|
||||
|
||||
test('TAKE-003: Verify TakeBetModal receives correct data', async ({ page }) => {
|
||||
const errors = setupErrorTracking(page);
|
||||
|
||||
console.log('\n=== TAKE-003: TakeBetModal Data Verification ===');
|
||||
|
||||
await login(page, TEST_USERS.bob);
|
||||
await page.goto('/sport-events');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Select first event
|
||||
const eventButtons = page.locator('button').filter({ hasText: /vs/i });
|
||||
if (await eventButtons.count() > 0) {
|
||||
await eventButtons.first().click();
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
// Find spread with high number of open bets
|
||||
const openTexts = await page.locator('text=/\\d+ open/').all();
|
||||
console.log(`Found ${openTexts.length} open bet indicators`);
|
||||
|
||||
// Find the parent button of an open bet indicator
|
||||
for (const openText of openTexts.slice(0, 3)) {
|
||||
const parentButton = openText.locator('xpath=ancestor::button');
|
||||
if (await parentButton.count() > 0) {
|
||||
const spreadText = await parentButton.textContent();
|
||||
console.log(`Clicking spread: ${spreadText?.trim()}`);
|
||||
|
||||
await parentButton.click();
|
||||
await page.waitForTimeout(800);
|
||||
|
||||
// Check modal for available bets
|
||||
const availableBets = page.locator('.fixed.inset-0').locator('text=Available to Take');
|
||||
const hasAvailable = await availableBets.isVisible().catch(() => false);
|
||||
console.log(`"Available to Take" section visible: ${hasAvailable}`);
|
||||
|
||||
if (hasAvailable) {
|
||||
// Get bet details from modal
|
||||
const betDetails = await page.locator('.bg-green-50, .border-green-200').first().textContent().catch(() => '');
|
||||
console.log(`First available bet details: ${betDetails}`);
|
||||
|
||||
// Click Take Bet
|
||||
const takeBetBtn = page.locator('button').filter({ hasText: /^take bet$/i });
|
||||
if (await takeBetBtn.count() > 0) {
|
||||
await takeBetBtn.first().click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.screenshot({ path: 'tests/screenshots/take003-take-modal.png', fullPage: true });
|
||||
|
||||
// Analyze TakeBetModal content
|
||||
const modalText = await page.locator('.fixed.inset-0').last().textContent().catch(() => '');
|
||||
|
||||
// Check for proper values (should have $ amounts, usernames, etc.)
|
||||
const has$Amount = /\$\d+/.test(modalText || '');
|
||||
const hasCreatorInfo = /by \w+/.test(modalText || '');
|
||||
const hasPotInfo = /pot/i.test(modalText || '');
|
||||
|
||||
console.log('\nTakeBetModal Content Analysis:');
|
||||
console.log(` Has $ amount: ${has$Amount}`);
|
||||
console.log(` Has creator info: ${hasCreatorInfo}`);
|
||||
console.log(` Has pot info: ${hasPotInfo}`);
|
||||
console.log(` Modal text sample: ${modalText?.substring(0, 400)}`);
|
||||
|
||||
// Close modal and continue
|
||||
const closeBtn = page.locator('button').filter({ hasText: /cancel|×|close/i });
|
||||
if (await closeBtn.count() > 0) {
|
||||
await closeBtn.first().click();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close modal
|
||||
await page.keyboard.press('Escape');
|
||||
await page.waitForTimeout(300);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n--- Error Summary ---');
|
||||
console.log('JS Errors:', errors.jsErrors);
|
||||
if (errors.jsErrors.length > 0) {
|
||||
console.log('DEFECT: JavaScript errors occurred');
|
||||
}
|
||||
});
|
||||
|
||||
test('TAKE-004: Direct API test - take existing bet', async ({ page, request }) => {
|
||||
console.log('\n=== TAKE-004: Direct API Take Bet Test ===');
|
||||
|
||||
// First, login as Alice and create a bet
|
||||
await login(page, TEST_USERS.alice);
|
||||
|
||||
// Get token via evaluating the auth store
|
||||
const aliceAuth = await page.evaluate(() => {
|
||||
const authStr = localStorage.getItem('auth-storage');
|
||||
return authStr ? JSON.parse(authStr) : null;
|
||||
});
|
||||
console.log('Alice auth storage:', aliceAuth ? 'Found' : 'Not found');
|
||||
|
||||
let aliceToken = null;
|
||||
if (aliceAuth?.state?.token) {
|
||||
aliceToken = aliceAuth.state.token;
|
||||
}
|
||||
|
||||
if (!aliceToken) {
|
||||
// Try alternate storage location
|
||||
aliceToken = await page.evaluate(() => localStorage.getItem('token'));
|
||||
}
|
||||
|
||||
console.log(`Alice token: ${aliceToken ? 'Retrieved' : 'Not found'}`);
|
||||
|
||||
if (!aliceToken) {
|
||||
// Try to get token from cookies or session storage
|
||||
const cookies = await page.context().cookies();
|
||||
console.log('Cookies:', cookies.map(c => c.name));
|
||||
|
||||
const sessionData = await page.evaluate(() => sessionStorage.getItem('token'));
|
||||
console.log(`Session token: ${sessionData ? 'Found' : 'Not found'}`);
|
||||
}
|
||||
|
||||
// Get events via the page's network context
|
||||
await page.goto('/sport-events');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Create bet via UI since we can't reliably get token
|
||||
const eventButtons = page.locator('button').filter({ hasText: /vs/i });
|
||||
if (await eventButtons.count() > 0) {
|
||||
await eventButtons.first().click();
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
// Click on a spread
|
||||
const spreadCells = page.locator('button').filter({ hasText: /^[+-]?\d+\.?\d*$/ });
|
||||
await spreadCells.first().click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Create bet
|
||||
const createBtn = page.locator('button').filter({ hasText: /create new bet/i });
|
||||
if (await createBtn.isVisible()) {
|
||||
await createBtn.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Fill stake
|
||||
await page.locator('input[type="number"]').fill('25');
|
||||
await page.locator('button[type="submit"]').click();
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
console.log('Created bet as Alice');
|
||||
}
|
||||
}
|
||||
|
||||
// Logout Alice
|
||||
await page.goto('/');
|
||||
await page.waitForTimeout(500);
|
||||
const logoutBtn = page.locator('button').filter({ hasText: /logout/i });
|
||||
if (await logoutBtn.isVisible()) {
|
||||
await logoutBtn.click();
|
||||
await page.waitForTimeout(1000);
|
||||
}
|
||||
|
||||
// Login as Bob
|
||||
await login(page, TEST_USERS.bob);
|
||||
console.log('Logged in as Bob');
|
||||
|
||||
// Navigate and take bet
|
||||
await page.goto('/sport-events');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const bobEventBtns = page.locator('button').filter({ hasText: /vs/i });
|
||||
if (await bobEventBtns.count() > 0) {
|
||||
await bobEventBtns.first().click();
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
await page.screenshot({ path: 'tests/screenshots/take004-bob-grid.png', fullPage: true });
|
||||
|
||||
// Find and click spread with open bets
|
||||
const openSpread = page.locator('button').filter({ hasText: /open/i }).first();
|
||||
if (await openSpread.isVisible()) {
|
||||
await openSpread.click();
|
||||
await page.waitForTimeout(800);
|
||||
|
||||
await page.screenshot({ path: 'tests/screenshots/take004-detail-modal.png', fullPage: true });
|
||||
|
||||
// Click Take Bet
|
||||
const takeBtn = page.locator('button').filter({ hasText: /take bet/i }).first();
|
||||
if (await takeBtn.isVisible()) {
|
||||
// Monitor API call
|
||||
const apiPromise = page.waitForResponse(
|
||||
resp => resp.url().includes('/take'),
|
||||
{ timeout: 10000 }
|
||||
).catch(() => null);
|
||||
|
||||
await takeBtn.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.screenshot({ path: 'tests/screenshots/take004-take-modal.png', fullPage: true });
|
||||
|
||||
// Click confirm in TakeBetModal
|
||||
const confirmBtn = page.locator('button').filter({ hasText: /take bet.*\$|confirm/i }).last();
|
||||
if (await confirmBtn.isVisible()) {
|
||||
await confirmBtn.click();
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
const response = await apiPromise;
|
||||
if (response) {
|
||||
console.log(`API Response: ${response.status()}`);
|
||||
const body = await response.json().catch(() => null);
|
||||
console.log('Response body:', body);
|
||||
|
||||
if (response.status() >= 200 && response.status() < 300) {
|
||||
console.log('SUCCESS: Bet taken via UI');
|
||||
} else {
|
||||
console.log('FAILURE: Bet taking failed');
|
||||
}
|
||||
} else {
|
||||
console.log('WARNING: No API call detected');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
100
frontend/tests/websocket-debug.spec.ts
Normal file
@ -0,0 +1,100 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('Debug WebSocket connection', async ({ page }) => {
|
||||
const consoleMessages: string[] = [];
|
||||
const wsMessages: string[] = [];
|
||||
|
||||
// Capture all console messages
|
||||
page.on('console', msg => {
|
||||
const text = `[${msg.type()}] ${msg.text()}`;
|
||||
consoleMessages.push(text);
|
||||
if (msg.text().includes('WebSocket')) {
|
||||
console.log(text);
|
||||
}
|
||||
});
|
||||
|
||||
// Capture page errors
|
||||
page.on('pageerror', error => {
|
||||
console.log('Page error:', error.message);
|
||||
});
|
||||
|
||||
// Navigate to event page
|
||||
await page.goto('/events/1', { waitUntil: 'networkidle' });
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Check console for WebSocket messages
|
||||
console.log('\n=== All Console Messages ===');
|
||||
consoleMessages.forEach(msg => console.log(msg));
|
||||
|
||||
// Take screenshot
|
||||
await page.screenshot({ path: 'test-results/websocket-debug.png', fullPage: true });
|
||||
|
||||
// Check for WebSocket related logs
|
||||
const wsLogs = consoleMessages.filter(m => m.toLowerCase().includes('websocket') || m.toLowerCase().includes('ws://') || m.toLowerCase().includes('wss://'));
|
||||
console.log('\n=== WebSocket Related Logs ===');
|
||||
wsLogs.forEach(msg => console.log(msg));
|
||||
});
|
||||
|
||||
test('Check WebSocket URL configuration', async ({ page }) => {
|
||||
// Navigate and check what WS_URL is being used
|
||||
await page.goto('/events/1');
|
||||
|
||||
const wsUrl = await page.evaluate(() => {
|
||||
// @ts-ignore
|
||||
return window.__WS_URL__ || 'not exposed';
|
||||
});
|
||||
console.log('WS_URL from window:', wsUrl);
|
||||
|
||||
// Check environment variables
|
||||
const envCheck = await page.evaluate(() => {
|
||||
return {
|
||||
// @ts-ignore
|
||||
VITE_WS_URL: import.meta?.env?.VITE_WS_URL,
|
||||
// @ts-ignore
|
||||
VITE_API_URL: import.meta?.env?.VITE_API_URL,
|
||||
};
|
||||
});
|
||||
console.log('Environment variables:', envCheck);
|
||||
});
|
||||
|
||||
test('Test WebSocket endpoint directly', async ({ page, request }) => {
|
||||
// First check if backend is running
|
||||
try {
|
||||
const healthCheck = await request.get('http://localhost:8000/api/v1/sport-events');
|
||||
console.log('Backend health check status:', healthCheck.status());
|
||||
} catch (e) {
|
||||
console.log('Backend not reachable:', e);
|
||||
}
|
||||
|
||||
// Try to connect to WebSocket from browser
|
||||
await page.goto('/events/1');
|
||||
|
||||
const wsResult = await page.evaluate(async () => {
|
||||
return new Promise((resolve) => {
|
||||
const ws = new WebSocket('ws://localhost:8000/api/v1/ws?token=guest&event_id=1');
|
||||
|
||||
ws.onopen = () => {
|
||||
resolve({ status: 'connected', readyState: ws.readyState });
|
||||
ws.close();
|
||||
};
|
||||
|
||||
ws.onerror = (e) => {
|
||||
resolve({ status: 'error', error: 'Connection failed' });
|
||||
};
|
||||
|
||||
ws.onclose = (e) => {
|
||||
if (e.code !== 1000) {
|
||||
resolve({ status: 'closed', code: e.code, reason: e.reason });
|
||||
}
|
||||
};
|
||||
|
||||
// Timeout after 5 seconds
|
||||
setTimeout(() => {
|
||||
resolve({ status: 'timeout', readyState: ws.readyState });
|
||||
ws.close();
|
||||
}, 5000);
|
||||
});
|
||||
});
|
||||
|
||||
console.log('WebSocket connection result:', wsResult);
|
||||
});
|
||||