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)}`)); }); });