583 lines
21 KiB
TypeScript
583 lines
21 KiB
TypeScript
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)}`));
|
|
});
|
|
});
|