Websocket fixes.
@ -11,6 +11,7 @@ from app.models import User, SportEvent, SpreadBet, Wallet, Transaction, AdminSe
|
||||
from app.models import EventStatus, SpreadBetStatus, TeamSide, TransactionType, TransactionStatus
|
||||
from app.schemas.spread_bet import SpreadBet as SpreadBetSchema, SpreadBetCreate, SpreadBetDetail
|
||||
from app.routers.auth import get_current_user
|
||||
from app.routers.websocket import broadcast_to_event
|
||||
|
||||
router = APIRouter(prefix="/api/v1/spread-bets", tags=["spread-bets"])
|
||||
|
||||
@ -69,6 +70,20 @@ async def create_spread_bet(
|
||||
db.add(bet)
|
||||
await db.commit()
|
||||
await db.refresh(bet)
|
||||
|
||||
# Broadcast bet created event to all subscribers of this event
|
||||
await broadcast_to_event(
|
||||
bet.event_id,
|
||||
"bet_created",
|
||||
{
|
||||
"bet_id": bet.id,
|
||||
"spread": float(bet.spread),
|
||||
"team": bet.team.value,
|
||||
"stake_amount": float(bet.stake_amount),
|
||||
"creator_username": current_user.username,
|
||||
}
|
||||
)
|
||||
|
||||
return bet
|
||||
|
||||
|
||||
@ -162,6 +177,20 @@ async def take_spread_bet(
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(bet)
|
||||
|
||||
# Broadcast bet taken event to all subscribers of this event
|
||||
await broadcast_to_event(
|
||||
bet.event_id,
|
||||
"bet_taken",
|
||||
{
|
||||
"bet_id": bet.id,
|
||||
"spread": float(bet.spread),
|
||||
"team": bet.team.value,
|
||||
"stake_amount": float(bet.stake_amount),
|
||||
"taker_username": current_user.username,
|
||||
}
|
||||
)
|
||||
|
||||
return bet
|
||||
|
||||
|
||||
@ -284,6 +313,19 @@ async def cancel_spread_bet(
|
||||
if bet.status != SpreadBetStatus.OPEN:
|
||||
raise HTTPException(status_code=400, detail="Can only cancel open bets")
|
||||
|
||||
event_id = bet.event_id
|
||||
bet.status = SpreadBetStatus.CANCELLED
|
||||
await db.commit()
|
||||
|
||||
# Broadcast bet cancelled event to all subscribers of this event
|
||||
await broadcast_to_event(
|
||||
event_id,
|
||||
"bet_cancelled",
|
||||
{
|
||||
"bet_id": bet_id,
|
||||
"spread": float(bet.spread),
|
||||
"team": bet.team.value,
|
||||
}
|
||||
)
|
||||
|
||||
return {"message": "Bet cancelled"}
|
||||
|
||||
@ -1,43 +1,138 @@
|
||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Query
|
||||
from typing import Dict
|
||||
from typing import Dict, Set, Optional
|
||||
from jose import JWTError
|
||||
import json
|
||||
import uuid
|
||||
|
||||
from app.utils.security import decode_token
|
||||
|
||||
router = APIRouter(tags=["websocket"])
|
||||
|
||||
# Store active connections
|
||||
active_connections: Dict[int, WebSocket] = {}
|
||||
# Store active connections by connection_id (unique per connection)
|
||||
active_connections: Dict[str, WebSocket] = {}
|
||||
|
||||
# Store connections subscribed to specific events
|
||||
event_subscriptions: Dict[int, Set[str]] = {} # event_id -> set of connection_ids
|
||||
|
||||
# Map connection_id to websocket
|
||||
connection_websockets: Dict[str, WebSocket] = {}
|
||||
|
||||
|
||||
@router.websocket("/api/v1/ws")
|
||||
async def websocket_endpoint(websocket: WebSocket, token: str = Query(...)):
|
||||
async def websocket_endpoint(
|
||||
websocket: WebSocket,
|
||||
token: str = Query(...),
|
||||
event_id: Optional[int] = Query(None)
|
||||
):
|
||||
await websocket.accept()
|
||||
|
||||
# In a real implementation, you would validate the token here
|
||||
# For MVP, we'll accept all connections
|
||||
user_id = 1 # Placeholder
|
||||
# Generate unique connection ID
|
||||
connection_id = str(uuid.uuid4())
|
||||
|
||||
active_connections[user_id] = websocket
|
||||
# Try to decode token to get user_id (for logging purposes)
|
||||
user_id = None
|
||||
if token and token != 'guest':
|
||||
try:
|
||||
payload = decode_token(token)
|
||||
user_id = payload.get("sub")
|
||||
except (JWTError, Exception):
|
||||
pass # Invalid token, treat as guest
|
||||
|
||||
print(f"[WebSocket] New connection: {connection_id}, user_id: {user_id}, event_id: {event_id}")
|
||||
|
||||
# Store connection
|
||||
active_connections[connection_id] = websocket
|
||||
connection_websockets[connection_id] = websocket
|
||||
|
||||
# Subscribe to event if specified
|
||||
if event_id:
|
||||
if event_id not in event_subscriptions:
|
||||
event_subscriptions[event_id] = set()
|
||||
event_subscriptions[event_id].add(connection_id)
|
||||
print(f"[WebSocket] Subscribed {connection_id} to event {event_id}. Total subscribers: {len(event_subscriptions[event_id])}")
|
||||
|
||||
try:
|
||||
while True:
|
||||
data = await websocket.receive_text()
|
||||
# Handle incoming messages if needed
|
||||
# Handle incoming messages - could be used to subscribe/unsubscribe
|
||||
try:
|
||||
msg = json.loads(data)
|
||||
if msg.get('action') == 'subscribe' and msg.get('event_id'):
|
||||
eid = msg['event_id']
|
||||
if eid not in event_subscriptions:
|
||||
event_subscriptions[eid] = set()
|
||||
event_subscriptions[eid].add(connection_id)
|
||||
print(f"[WebSocket] {connection_id} subscribed to event {eid}")
|
||||
elif msg.get('action') == 'unsubscribe' and msg.get('event_id'):
|
||||
eid = msg['event_id']
|
||||
if eid in event_subscriptions:
|
||||
event_subscriptions[eid].discard(connection_id)
|
||||
print(f"[WebSocket] {connection_id} unsubscribed from event {eid}")
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
except WebSocketDisconnect:
|
||||
if user_id in active_connections:
|
||||
del active_connections[user_id]
|
||||
print(f"[WebSocket] Disconnected: {connection_id}")
|
||||
# Clean up connection
|
||||
if connection_id in active_connections:
|
||||
del active_connections[connection_id]
|
||||
if connection_id in connection_websockets:
|
||||
del connection_websockets[connection_id]
|
||||
|
||||
# Clean up event subscriptions
|
||||
for eid, subs in event_subscriptions.items():
|
||||
if connection_id in subs:
|
||||
subs.discard(connection_id)
|
||||
print(f"[WebSocket] Removed {connection_id} from event {eid} subscriptions")
|
||||
|
||||
|
||||
async def broadcast_to_event(event_id: int, event_type: str, data: dict):
|
||||
"""Broadcast a message to all connections subscribed to an event"""
|
||||
message = json.dumps({
|
||||
"type": event_type,
|
||||
"data": {"event_id": event_id, **data}
|
||||
})
|
||||
|
||||
print(f"[WebSocket] Broadcasting {event_type} to event {event_id}")
|
||||
|
||||
if event_id not in event_subscriptions:
|
||||
print(f"[WebSocket] No subscribers for event {event_id}")
|
||||
return
|
||||
|
||||
subscribers = event_subscriptions[event_id].copy()
|
||||
print(f"[WebSocket] Found {len(subscribers)} subscribers for event {event_id}")
|
||||
|
||||
disconnected = set()
|
||||
for conn_id in subscribers:
|
||||
ws = connection_websockets.get(conn_id)
|
||||
if ws:
|
||||
try:
|
||||
await ws.send_text(message)
|
||||
print(f"[WebSocket] Sent message to {conn_id}")
|
||||
except Exception as e:
|
||||
print(f"[WebSocket] Failed to send to {conn_id}: {e}")
|
||||
disconnected.add(conn_id)
|
||||
else:
|
||||
print(f"[WebSocket] Connection {conn_id} not found in websockets map")
|
||||
disconnected.add(conn_id)
|
||||
|
||||
# Clean up disconnected connections
|
||||
for conn_id in disconnected:
|
||||
event_subscriptions[event_id].discard(conn_id)
|
||||
if conn_id in active_connections:
|
||||
del active_connections[conn_id]
|
||||
if conn_id in connection_websockets:
|
||||
del connection_websockets[conn_id]
|
||||
|
||||
|
||||
async def broadcast_event(event_type: str, data: dict, user_ids: list[int] = None):
|
||||
"""Broadcast an event to specific users or all connected users"""
|
||||
"""Broadcast an event to all connected users"""
|
||||
message = json.dumps({
|
||||
"type": event_type,
|
||||
"data": data
|
||||
})
|
||||
|
||||
if user_ids:
|
||||
for user_id in user_ids:
|
||||
if user_id in active_connections:
|
||||
await active_connections[user_id].send_text(message)
|
||||
else:
|
||||
for connection in active_connections.values():
|
||||
await connection.send_text(message)
|
||||
for conn_id, ws in list(connection_websockets.items()):
|
||||
try:
|
||||
await ws.send_text(message)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
241
frontend/QA_TEST_REPORT.md
Normal file
@ -0,0 +1,241 @@
|
||||
# QA Test Report: Bet Creation and Taking Flow
|
||||
|
||||
**Date**: 2026-01-10
|
||||
**Tester**: QAPro (Automated E2E Testing)
|
||||
**Environment**: Chrome/Chromium, Frontend: http://localhost:5173, Backend: http://localhost:8000
|
||||
**Test Suite**: Bet Creation and Taking Flow Investigation
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
| Status | Count |
|
||||
|--------|-------|
|
||||
| Passed | 10 |
|
||||
| Failed | 1 |
|
||||
| Total Tests | 11 |
|
||||
|
||||
**Overall Assessment**: The core bet creation and taking functionality IS WORKING at the API level. However, there is a **CRITICAL UI BUG** in the TakeBetModal component that displays corrupted data to users.
|
||||
|
||||
---
|
||||
|
||||
## Test Results Summary
|
||||
|
||||
### Passed Tests
|
||||
- TC-001: Home page event loading
|
||||
- TC-002: Alice login and navigation to Sport Events
|
||||
- TC-003: Event selection and spread grid viewing
|
||||
- INVESTIGATION-003: TradingPanel bet creation (API call confirmed)
|
||||
- TAKE-001: TradingPanel Take bet section check
|
||||
- TAKE-002: SpreadGrid modal take bet flow (API returned 200, bet status: matched)
|
||||
- TAKE-003: TakeBetModal data verification
|
||||
- TAKE-004: Direct API take bet test (SUCCESS)
|
||||
|
||||
### Test Observations
|
||||
1. **Bet Creation via TradingPanel**: WORKING - API calls are made successfully
|
||||
2. **Bet Creation via SpreadGrid Modal**: WORKING - Modal opens, bet is created
|
||||
3. **Bet Taking via SpreadGrid Modal**: WORKING at API level - Returns 200, bet becomes "matched"
|
||||
4. **TakeBetModal UI Display**: BROKEN - Shows undefined/NaN values
|
||||
|
||||
---
|
||||
|
||||
## Defect Reports
|
||||
|
||||
### DEF-001: TakeBetModal Displays Corrupted Data (NaN and undefined)
|
||||
|
||||
**Severity**: HIGH
|
||||
**Priority**: P1
|
||||
**Status**: New
|
||||
**Component**: `/frontend/src/components/bets/TakeBetModal.tsx`
|
||||
|
||||
#### Description
|
||||
The TakeBetModal component displays corrupted data including "NaN", "$undefined", and missing values because it incorrectly accesses bet data. The `betInfo` variable is assigned from `event.spread_grid[spread.toString()]` which returns an array of `SpreadGridBet[]`, but the code treats it as a single `SpreadGridBet` object.
|
||||
|
||||
#### Steps to Reproduce
|
||||
1. Login as any user (bob@example.com / password123)
|
||||
2. Navigate to /sport-events
|
||||
3. Click on an event (e.g., "Wake Forest vs MS State")
|
||||
4. Click on a spread cell that shows "X open" (indicating available bets)
|
||||
5. Click "Take Bet" button in the SpreadDetailModal
|
||||
6. Observe the TakeBetModal content
|
||||
|
||||
#### Expected Result
|
||||
- "Their Bet" section shows: "[creator_username] is betting $[amount] on [team] [spread]"
|
||||
- "Your stake: $[amount]"
|
||||
- "Total pot: $[calculated_value]"
|
||||
- "House commission (10%): $[calculated_value]"
|
||||
- "Winner receives: $[calculated_value]"
|
||||
- Button shows: "Take Bet ($[amount])"
|
||||
|
||||
#### Actual Result
|
||||
- "Their Bet" section shows: "is betting **$** on MS State -9.5" (missing username and amount)
|
||||
- "Your stake: $" (missing amount)
|
||||
- "Total pot: $NaN"
|
||||
- "House commission (10%): $NaN"
|
||||
- "Winner receives: $NaN"
|
||||
- Button shows: "Take Bet ($undefined)"
|
||||
|
||||
#### Evidence
|
||||
- Screenshot: `tests/screenshots/take002-take-modal.png`
|
||||
- Screenshot: `tests/screenshots/take004-take-modal.png`
|
||||
|
||||
#### Root Cause Analysis
|
||||
In `/frontend/src/components/bets/TakeBetModal.tsx`, line 24:
|
||||
```typescript
|
||||
const betInfo = event.spread_grid[spread.toString()]
|
||||
```
|
||||
|
||||
This returns `SpreadGridBet[]` (an array), but the code uses it as a single object:
|
||||
- Line 74: `betInfo.creator_username` - accessing array property (undefined)
|
||||
- Line 75: `betInfo.stake` - accessing array property (undefined)
|
||||
- Line 98: `(betInfo.stake * 2).toFixed(2)` - undefined * 2 = NaN
|
||||
|
||||
#### Suggested Fix
|
||||
The modal should find the specific bet by `betId` from the array:
|
||||
```typescript
|
||||
const bets = event.spread_grid[spread.toString()] || []
|
||||
const betInfo = bets.find(b => b.bet_id === betId)
|
||||
```
|
||||
|
||||
#### Impact
|
||||
- Users see corrupted data when attempting to take a bet
|
||||
- Users cannot confirm the stake amount they are committing
|
||||
- Poor user experience and potential trust issues
|
||||
- However, the API call still works correctly (bet is taken successfully)
|
||||
|
||||
---
|
||||
|
||||
### DEF-002: TradingPanel "Take Existing Bet" Section Not Visible
|
||||
|
||||
**Severity**: MEDIUM
|
||||
**Priority**: P2
|
||||
**Status**: New
|
||||
**Component**: `/frontend/src/components/bets/TradingPanel.tsx`
|
||||
|
||||
#### Description
|
||||
When navigating to an event detail page via the home page, the TradingPanel's "Take Existing Bet" section is not visible, even when there are open bets available. This may be due to filtering logic or the section not being displayed when there are no bets at the currently selected spread.
|
||||
|
||||
#### Steps to Reproduce
|
||||
1. Login as bob@example.com / password123
|
||||
2. Navigate to home page (/)
|
||||
3. Click on an event name (e.g., "Wake Forest vs MS State")
|
||||
4. Observe the TradingPanel on the right side
|
||||
5. Look for "Take Existing Bet" section
|
||||
|
||||
#### Expected Result
|
||||
If there are open bets at any spread, the "Take Existing Bet" section should show available bets or indicate how to find them.
|
||||
|
||||
#### Actual Result
|
||||
The "Take Existing Bet" section is not displayed, even though the Order Book shows open bets exist.
|
||||
|
||||
#### Evidence
|
||||
- Test output: `Found 0 Take buttons` from TAKE-001 test
|
||||
- Screenshot: `tests/screenshots/take001-event-page.png`
|
||||
|
||||
#### Notes
|
||||
This may be working as designed - the section only shows bets at the currently selected spread. However, this could be improved for user experience.
|
||||
|
||||
---
|
||||
|
||||
## Positive Findings
|
||||
|
||||
### Working Features
|
||||
|
||||
1. **Bet Creation via TradingPanel**
|
||||
- Team selection (Home/Away) works correctly
|
||||
- Spread adjustment (+/-) works correctly
|
||||
- Stake amount input works correctly
|
||||
- Quick stake buttons work correctly
|
||||
- "Create Bet" button triggers API call correctly
|
||||
- API returns success and bet is created
|
||||
|
||||
2. **Bet Creation via SpreadGrid Modal**
|
||||
- Clicking spread cells opens detail modal
|
||||
- "Create New Bet" button opens CreateSpreadBetModal
|
||||
- Stake input works correctly
|
||||
- Submit creates bet via API
|
||||
|
||||
3. **Bet Taking via SpreadGrid Modal**
|
||||
- SpreadGrid shows available bets with "X open" indicator
|
||||
- Clicking spread with open bets shows available bets to take
|
||||
- "Take Bet" button in detail modal works
|
||||
- TakeBetModal opens (with data display bug)
|
||||
- Clicking confirm button successfully calls API
|
||||
- API returns 200 and bet status changes to "matched"
|
||||
|
||||
4. **API Endpoints**
|
||||
- `POST /api/v1/spread-bets` - Working
|
||||
- `POST /api/v1/spread-bets/{id}/take` - Working
|
||||
- `GET /api/v1/sport-events` - Working
|
||||
- `GET /api/v1/sport-events/{id}` - Working
|
||||
|
||||
5. **No JavaScript Errors**
|
||||
- No console errors during bet creation
|
||||
- No console errors during bet taking
|
||||
- All network requests complete successfully
|
||||
|
||||
---
|
||||
|
||||
## Test Coverage
|
||||
|
||||
| Feature | Automated | Manual | Status |
|
||||
|---------|-----------|--------|--------|
|
||||
| Login Flow | Yes | - | Pass |
|
||||
| Event List Loading | Yes | - | Pass |
|
||||
| Event Selection | Yes | - | Pass |
|
||||
| SpreadGrid Display | Yes | - | Pass |
|
||||
| SpreadGrid Cell Click | Yes | - | Pass |
|
||||
| Create Bet Modal | Yes | - | Pass |
|
||||
| Create Bet API Call | Yes | - | Pass |
|
||||
| Take Bet Modal Open | Yes | - | Pass |
|
||||
| Take Bet Modal Data | Yes | - | FAIL |
|
||||
| Take Bet API Call | Yes | - | Pass |
|
||||
| TradingPanel Display | Yes | - | Pass |
|
||||
| TradingPanel Create Bet | Yes | - | Pass |
|
||||
|
||||
---
|
||||
|
||||
## Recommendations
|
||||
|
||||
### Immediate Actions (P1)
|
||||
1. **Fix TakeBetModal data access bug** - Change array access to find specific bet by ID
|
||||
|
||||
### Short-term Improvements (P2)
|
||||
1. Add data-testid attributes to key UI elements for more reliable testing
|
||||
2. Improve TradingPanel's "Take Existing Bet" visibility/discoverability
|
||||
3. Add loading states to indicate when API calls are in progress
|
||||
|
||||
### Testing Improvements
|
||||
1. Add unit tests for TakeBetModal component
|
||||
2. Add integration tests for bet state transitions
|
||||
3. Add visual regression tests for modal content
|
||||
|
||||
---
|
||||
|
||||
## Test Artifacts
|
||||
|
||||
### Screenshots
|
||||
- `tests/screenshots/inv001-after-login.png` - Home page after login
|
||||
- `tests/screenshots/inv002-event-selected.png` - SpreadGrid view
|
||||
- `tests/screenshots/inv002-spread-modal.png` - SpreadDetailModal
|
||||
- `tests/screenshots/inv003-after-click.png` - TradingPanel view
|
||||
- `tests/screenshots/inv003-after-create.png` - After bet creation
|
||||
- `tests/screenshots/take002-take-modal.png` - TakeBetModal with bug
|
||||
- `tests/screenshots/take004-take-modal.png` - TakeBetModal with bug
|
||||
|
||||
### Test Files
|
||||
- `tests/bet-creation-taking.spec.ts` - Comprehensive E2E tests
|
||||
- `tests/bet-flow-investigation.spec.ts` - Investigation tests
|
||||
- `tests/take-bet-flow.spec.ts` - Take bet specific tests
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
**The bet creation and taking functionality is WORKING at the API level.** Both creating and taking bets successfully call the backend API and receive successful responses. The critical issue is the **TakeBetModal UI displaying corrupted data** (NaN, undefined) due to incorrect data access patterns in the React component.
|
||||
|
||||
The user can still successfully take bets despite the visual bug because the API call uses the correct `betId` parameter. However, this creates a poor user experience and should be fixed immediately.
|
||||
|
||||
---
|
||||
|
||||
*Report generated by QAPro - Automated QA Testing*
|
||||
@ -21,7 +21,9 @@ export const TakeBetModal = ({
|
||||
event,
|
||||
}: TakeBetModalProps) => {
|
||||
const queryClient = useQueryClient()
|
||||
const betInfo = event.spread_grid[spread.toString()]
|
||||
// spread_grid returns an array of bets at this spread - find the specific bet by ID
|
||||
const betsAtSpread = event.spread_grid[spread.toString()] || []
|
||||
const betInfo = betsAtSpread.find(b => b.bet_id === betId)
|
||||
|
||||
const takeBetMutation = useMutation({
|
||||
mutationFn: () => spreadBetsApi.takeBet(betId),
|
||||
|
||||
142
frontend/src/hooks/useEventWebSocket.ts
Normal file
@ -0,0 +1,142 @@
|
||||
import { useEffect, useRef, useCallback } from 'react'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { WS_URL } from '@/utils/constants'
|
||||
import { useAuthStore } from '@/store'
|
||||
|
||||
export interface WebSocketMessage {
|
||||
type: 'bet_created' | 'bet_taken' | 'bet_cancelled' | 'event_updated'
|
||||
data: {
|
||||
event_id: number
|
||||
bet_id?: number
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
interface UseEventWebSocketOptions {
|
||||
eventId: number
|
||||
onBetCreated?: (data: WebSocketMessage['data']) => void
|
||||
onBetTaken?: (data: WebSocketMessage['data']) => void
|
||||
onBetCancelled?: (data: WebSocketMessage['data']) => void
|
||||
}
|
||||
|
||||
export function useEventWebSocket({ eventId, onBetCreated, onBetTaken, onBetCancelled }: UseEventWebSocketOptions) {
|
||||
const wsRef = useRef<WebSocket | null>(null)
|
||||
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const queryClient = useQueryClient()
|
||||
const { token } = useAuthStore()
|
||||
|
||||
// Use refs for callbacks to avoid reconnecting when they change
|
||||
const onBetCreatedRef = useRef(onBetCreated)
|
||||
const onBetTakenRef = useRef(onBetTaken)
|
||||
const onBetCancelledRef = useRef(onBetCancelled)
|
||||
|
||||
// Update refs when callbacks change
|
||||
useEffect(() => {
|
||||
onBetCreatedRef.current = onBetCreated
|
||||
onBetTakenRef.current = onBetTaken
|
||||
onBetCancelledRef.current = onBetCancelled
|
||||
}, [onBetCreated, onBetTaken, onBetCancelled])
|
||||
|
||||
const invalidateEventQueries = useCallback(() => {
|
||||
console.log('[WebSocket] Refetching queries for event', eventId)
|
||||
// Force refetch queries - use refetchQueries for immediate update
|
||||
queryClient.refetchQueries({
|
||||
predicate: (query) => {
|
||||
const key = query.queryKey
|
||||
const match = Array.isArray(key) && key[0] === 'sport-event' && key[1] === eventId
|
||||
if (match) console.log('[WebSocket] Refetching query:', key)
|
||||
return match
|
||||
},
|
||||
})
|
||||
queryClient.refetchQueries({ queryKey: ['my-active-bets'] })
|
||||
}, [queryClient, eventId])
|
||||
|
||||
useEffect(() => {
|
||||
// Build WebSocket URL with token and event subscription
|
||||
const wsToken = token || 'guest'
|
||||
const wsUrl = `${WS_URL}/api/v1/ws?token=${wsToken}&event_id=${eventId}`
|
||||
|
||||
const connect = () => {
|
||||
// Clean up existing connection
|
||||
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
|
||||
return // Already connected
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`[WebSocket] Connecting to ${wsUrl}`)
|
||||
const ws = new WebSocket(wsUrl)
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log(`[WebSocket] Connected to event ${eventId}`)
|
||||
}
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const message: WebSocketMessage = JSON.parse(event.data)
|
||||
console.log('[WebSocket] Message received:', message)
|
||||
|
||||
// Only process messages for this event
|
||||
if (message.data?.event_id !== eventId) return
|
||||
|
||||
switch (message.type) {
|
||||
case 'bet_created':
|
||||
onBetCreatedRef.current?.(message.data)
|
||||
invalidateEventQueries()
|
||||
break
|
||||
case 'bet_taken':
|
||||
onBetTakenRef.current?.(message.data)
|
||||
invalidateEventQueries()
|
||||
break
|
||||
case 'bet_cancelled':
|
||||
onBetCancelledRef.current?.(message.data)
|
||||
invalidateEventQueries()
|
||||
break
|
||||
case 'event_updated':
|
||||
invalidateEventQueries()
|
||||
break
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[WebSocket] Failed to parse message:', err)
|
||||
}
|
||||
}
|
||||
|
||||
ws.onclose = (event) => {
|
||||
console.log(`[WebSocket] Disconnected from event ${eventId}`, event.code, event.reason)
|
||||
wsRef.current = null
|
||||
|
||||
// Attempt to reconnect after 3 seconds (unless it was a normal closure)
|
||||
if (event.code !== 1000) {
|
||||
reconnectTimeoutRef.current = setTimeout(() => {
|
||||
console.log('[WebSocket] Attempting to reconnect...')
|
||||
connect()
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('[WebSocket] Error:', error)
|
||||
}
|
||||
|
||||
wsRef.current = ws
|
||||
} catch (err) {
|
||||
console.error('[WebSocket] Failed to connect:', err)
|
||||
}
|
||||
}
|
||||
|
||||
connect()
|
||||
|
||||
return () => {
|
||||
if (reconnectTimeoutRef.current) {
|
||||
clearTimeout(reconnectTimeoutRef.current)
|
||||
}
|
||||
if (wsRef.current) {
|
||||
wsRef.current.close(1000, 'Component unmounted')
|
||||
wsRef.current = null
|
||||
}
|
||||
}
|
||||
}, [eventId, token, invalidateEventQueries])
|
||||
|
||||
return {
|
||||
isConnected: wsRef.current?.readyState === WebSocket.OPEN,
|
||||
}
|
||||
}
|
||||
@ -9,6 +9,8 @@ import { Button } from '@/components/common/Button'
|
||||
import { Loading } from '@/components/common/Loading'
|
||||
import { Header } from '@/components/layout/Header'
|
||||
import { ChevronLeft } from 'lucide-react'
|
||||
import { useEventWebSocket } from '@/hooks/useEventWebSocket'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
export const EventDetail = () => {
|
||||
const { id } = useParams<{ id: string }>()
|
||||
@ -25,6 +27,29 @@ export const EventDetail = () => {
|
||||
enabled: eventId > 0,
|
||||
})
|
||||
|
||||
// Connect to WebSocket for live updates
|
||||
useEventWebSocket({
|
||||
eventId,
|
||||
onBetCreated: (data) => {
|
||||
toast.success(`New bet: $${data.stake_amount} on ${data.team} at ${data.spread}`, {
|
||||
icon: '📈',
|
||||
duration: 3000,
|
||||
})
|
||||
},
|
||||
onBetTaken: (data) => {
|
||||
toast.success(`Bet matched: $${data.stake_amount} at ${data.spread}`, {
|
||||
icon: '🤝',
|
||||
duration: 3000,
|
||||
})
|
||||
},
|
||||
onBetCancelled: (data) => {
|
||||
toast(`Bet cancelled at ${data.spread}`, {
|
||||
icon: '❌',
|
||||
duration: 3000,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const handleBetCreated = () => {
|
||||
// Refetch event data to show new bet
|
||||
queryClient.invalidateQueries({ queryKey: ['sport-event', eventId] })
|
||||
@ -42,12 +67,12 @@ export const EventDetail = () => {
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<Header />
|
||||
<div className="px-4 sm:px-6 lg:px-8 py-8">
|
||||
<Link to="/">
|
||||
{/* <Link to="/">
|
||||
<Button variant="secondary">
|
||||
|
||||
|
||||
Back to Events
|
||||
</Button>
|
||||
</Link>
|
||||
</Link> */}
|
||||
<div className="mt-8">
|
||||
<Loading />
|
||||
</div>
|
||||
@ -61,12 +86,12 @@ export const EventDetail = () => {
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<Header />
|
||||
<div className="px-4 sm:px-6 lg:px-8 py-8">
|
||||
<Link to="/">
|
||||
{/* <Link to="/">
|
||||
<Button variant="secondary">
|
||||
|
||||
|
||||
Back to Events
|
||||
</Button>
|
||||
</Link>
|
||||
</Link> */}
|
||||
<div className="mt-8 text-center py-12 bg-white rounded-lg shadow">
|
||||
<p className="text-gray-500">Event not found</p>
|
||||
</div>
|
||||
@ -79,12 +104,12 @@ export const EventDetail = () => {
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<Header />
|
||||
<div className="px-4 sm:px-6 lg:px-8 py-8">
|
||||
<Link to="/">
|
||||
{/* <Link to="/">
|
||||
<Button variant="secondary" className="mb-6">
|
||||
|
||||
|
||||
Back to Events
|
||||
</Button>
|
||||
</Link>
|
||||
</Link> */}
|
||||
|
||||
{/* Trading Panel - Exchange-style interface */}
|
||||
<div className="mb-8">
|
||||
@ -109,7 +134,7 @@ export const EventDetail = () => {
|
||||
onBetTaken={handleBetTaken}
|
||||
/>
|
||||
</div> */}
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
{
|
||||
"status": "passed",
|
||||
"failedTests": []
|
||||
"status": "failed",
|
||||
"failedTests": [
|
||||
"6d68a41ba2df42f4b38a-e2ca0c03202a119dcb00"
|
||||
]
|
||||
}
|
||||
|
Before Width: | Height: | Size: 188 KiB |
@ -0,0 +1,32 @@
|
||||
# Page snapshot
|
||||
|
||||
```yaml
|
||||
- banner [ref=e4]:
|
||||
- generic [ref=e6]:
|
||||
- link "H2H" [ref=e7] [cursor=pointer]:
|
||||
- /url: /
|
||||
- heading "H2H" [level=1] [ref=e8]
|
||||
- navigation [ref=e9]:
|
||||
- link "Markets" [ref=e10] [cursor=pointer]:
|
||||
- /url: /sports
|
||||
- link "Live" [ref=e11] [cursor=pointer]:
|
||||
- /url: /live
|
||||
- link "New Bets" [ref=e12] [cursor=pointer]:
|
||||
- /url: /new-bets
|
||||
- link "How It Works" [ref=e13] [cursor=pointer]:
|
||||
- /url: /how-it-works
|
||||
- button "More" [ref=e15] [cursor=pointer]:
|
||||
- text: More
|
||||
- img [ref=e16]
|
||||
- link "Reward Center" [ref=e18] [cursor=pointer]:
|
||||
- /url: /rewards
|
||||
- img [ref=e19]
|
||||
- text: Reward Center
|
||||
- generic [ref=e24]:
|
||||
- link "Log In" [ref=e25] [cursor=pointer]:
|
||||
- /url: /login
|
||||
- button "Log In" [ref=e26]
|
||||
- link "Sign Up" [ref=e27] [cursor=pointer]:
|
||||
- /url: /register
|
||||
- button "Sign Up" [ref=e28]
|
||||
```
|
||||
|
After Width: | Height: | Size: 17 KiB |
BIN
frontend/test-results/websocket-debug.png
Normal file
|
After Width: | Height: | Size: 186 KiB |
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);
|
||||
});
|
||||