* Fix hand card rotation direction Cards now fan outward correctly instead of curling inward * Update StateRenderer to require MatchScene type for type safety - Change constructor parameter from Phaser.Scene to MatchScene - Update scene property type to MatchScene - Add import for MatchScene type - Update JSDoc example to reflect type-safe constructor * Defer Board creation to StateRenderer for correct rules config - Make board property nullable (Board | null instead of Board?) - Remove Board and createBoard imports (now handled by StateRenderer) - Update setupBoard() to skip Board creation - Add setBoard() method for StateRenderer to call - Update clearBoard() to use null instead of undefined - Add JSDoc explaining why Board creation is deferred * Create Board in StateRenderer with correct layout options - Add Board and createBoard imports - Add board property to StateRenderer - Create Board in render() on first call with correct rules_config - Add debug logging for Board creation and zone creation - Update clear() to destroy Board when clearing - Board now created after we have rules_config from first state * Add fatal error handling with toast notification and auto-redirect - Add 'fatal-error' event to GameBridgeEvents type - Import and initialize useToast in GamePage - Listen for 'fatal-error' event from Phaser - Show error toast that persists until redirect - Show full-screen fatal error overlay with countdown - Auto-redirect to /play after 3 seconds - Update StateRenderer to emit 'fatal-error' when Board creation fails * Gate debug logging with DEV flag - Add DEBUG_RENDERER constant gated by import.meta.env.DEV - Update all console.log statements in StateRenderer to only log in development - Keep console.error and console.warn as they are (always show errors) - Debug logs now only appear during development, not in production * Fix code audit issues - add missing imports and improve error UX Critical fixes: - Add missing gameBridge import to StateRenderer (fixes runtime error in fatal error handler) - Add missing Board type import to MatchScene (fixes TypeScript compilation error) UX improvements: - Replace fatal error auto-redirect with manual 'Return to Menu' button - Add toast notification when resignation fails - Give users unlimited time to read fatal errors before returning Addresses issues found in frontend code audit: - errors.missing-import (StateRenderer.ts:166) - errors.missing-type-import (MatchScene.ts:84) - errors.catch-only-console (GamePage.vue:145) - architecture.missing-fatal-error-handling (GamePage.vue:261) * Add CONTRIBUTING policy and fix pre-existing lint/test errors - Add CONTRIBUTING.md with strict policy: never use --no-verify without approval - Add comprehensive testing documentation (TESTING.md, VISUAL-TEST-GUIDE.md) - Add test-prize-fix.md quick test checklist and verify-fix.sh script Lint fixes (enables pre-commit hooks): - Remove unused imports in 9 files - Fix unused variables (underscore convention) - Replace 'as any' type assertions with proper VisibleGameState types - Add missing CARD_WIDTH_MEDIUM import in layout.spec.ts - All ESLint errors now resolved (only acceptable warnings remain) Test fixes (all 1000 tests now passing): - Fix layout.spec.ts: Add missing CARD_WIDTH_MEDIUM import - Fix PlayPage.spec.ts: Update test to use actual hardcoded UUIDs - Fix useAuth.spec.ts: Mock API profile fetch in initialization tests - Fix PhaserGame.spec.ts: Add scenes export to mock and update createGame call expectations This ensures pre-commit hooks work properly going forward and prevents bypassing TypeScript/lint checks that catch errors early. * Add comprehensive test coverage improvement plan - Create PROJECT_PLAN_TEST_COVERAGE.json with 25 structured tasks - Create TEST_COVERAGE_PLAN.md with executive summary and roadmap - Plan addresses critical gaps: game engine (0%), WebSocket (27%) - 6-week roadmap to reach 85% coverage from current 63% - Target: Phase 1 (weeks 1-3) - critical game engine and network tests - Includes quick wins, production blockers, and success metrics Based on coverage analysis showing: - Strong: Composables (84%), Components (90%), Stores (88%) - Critical gaps: Phaser game engine (~5,500 untested lines) - High priority: WebSocket/multiplayer reliability See TEST_COVERAGE_PLAN.md for overview and week-by-week breakdown. * Add coverage tooling and ignore coverage directory - Add @vitest/coverage-v8 package for coverage analysis - Add coverage/ directory to .gitignore - Used during test coverage analysis for PROJECT_PLAN_TEST_COVERAGE.json
227 lines
7.2 KiB
TypeScript
227 lines
7.2 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
import { setActivePinia, createPinia } from 'pinia'
|
|
|
|
import { useGameSocket } from './useGameSocket'
|
|
import { useAuthStore } from '@/stores/auth'
|
|
import { useGameStore } from '@/stores/game'
|
|
import { ConnectionStatus } from '@/types'
|
|
import type { VisibleGameState } from '@/types/game'
|
|
|
|
// Mock socket.io-client - simplified mocking
|
|
vi.mock('socket.io-client', () => ({
|
|
io: vi.fn(() => ({
|
|
connected: false,
|
|
on: vi.fn(),
|
|
once: vi.fn(),
|
|
off: vi.fn(),
|
|
emit: vi.fn(),
|
|
disconnect: vi.fn(),
|
|
io: { on: vi.fn() },
|
|
})),
|
|
}))
|
|
|
|
describe('useGameSocket', () => {
|
|
let authStore: ReturnType<typeof useAuthStore>
|
|
let gameStore: ReturnType<typeof useGameStore>
|
|
|
|
beforeEach(() => {
|
|
setActivePinia(createPinia())
|
|
authStore = useAuthStore()
|
|
gameStore = useGameStore()
|
|
|
|
// Setup valid auth token
|
|
authStore.setTokens({
|
|
accessToken: 'mock-token',
|
|
refreshToken: 'mock-refresh',
|
|
expiresAt: Date.now() + 3600000,
|
|
})
|
|
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Authentication
|
|
// ---------------------------------------------------------------------------
|
|
|
|
it('requires authentication to connect', async () => {
|
|
/**
|
|
* Test that connection fails when user is not authenticated.
|
|
*
|
|
* WebSocket connections require valid authentication. If the user
|
|
* is not logged in or the token is invalid, the connection should
|
|
* fail immediately with a clear error message.
|
|
*/
|
|
authStore.accessToken = null
|
|
|
|
const { connect } = useGameSocket()
|
|
|
|
await expect(connect('game-123')).rejects.toThrow('Not authenticated')
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Connection State
|
|
// ---------------------------------------------------------------------------
|
|
|
|
it('initializes with disconnected state', () => {
|
|
/**
|
|
* Test that the composable starts in a disconnected state.
|
|
*
|
|
* Before calling connect(), the composable should indicate that
|
|
* no connection is active. This ensures a clean initial state.
|
|
*/
|
|
const { isConnected, currentGameId } = useGameSocket()
|
|
|
|
expect(isConnected.value).toBe(false)
|
|
expect(currentGameId.value).toBeNull()
|
|
})
|
|
|
|
it('rejects duplicate connection attempts', async () => {
|
|
/**
|
|
* Test that attempting to connect while already connecting fails.
|
|
*
|
|
* If connect() is called while a connection is already in progress,
|
|
* it should reject immediately to prevent race conditions.
|
|
*/
|
|
const { connect } = useGameSocket()
|
|
|
|
// Start first connection
|
|
const promise1 = connect('game-123')
|
|
|
|
// Immediately try to connect again
|
|
await expect(connect('game-123')).rejects.toThrow('Connection already in progress')
|
|
|
|
// Clean up - Note: First promise will time out or fail, that's okay for this test
|
|
promise1.catch(() => {}) // Suppress unhandled rejection
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Action Queueing
|
|
// ---------------------------------------------------------------------------
|
|
|
|
it('queues actions when not connected', async () => {
|
|
/**
|
|
* Test that actions are queued when the connection is lost.
|
|
*
|
|
* If the user tries to perform an action while disconnected,
|
|
* we should queue it for retry when the connection is restored.
|
|
* This prevents losing actions during brief network interruptions.
|
|
*/
|
|
const { sendAction } = useGameSocket()
|
|
|
|
const mockAction = {
|
|
type: 'play_card' as const,
|
|
instance_id: 'card-1',
|
|
}
|
|
|
|
await expect(sendAction(mockAction)).rejects.toThrow('Not connected')
|
|
|
|
// Check that action was queued in the game store
|
|
expect(gameStore.pendingActions).toHaveLength(1)
|
|
expect(gameStore.pendingActions[0]).toEqual(mockAction)
|
|
})
|
|
|
|
it('requires game context for actions', async () => {
|
|
/**
|
|
* Test that actions fail when not in a game.
|
|
*
|
|
* sendAction() should only work when connected to a specific game.
|
|
* This prevents accidental action sends before joining a game.
|
|
*/
|
|
const { sendAction } = useGameSocket()
|
|
|
|
const mockAction = {
|
|
type: 'end_turn' as const,
|
|
}
|
|
|
|
await expect(sendAction(mockAction)).rejects.toThrow()
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Cleanup
|
|
// ---------------------------------------------------------------------------
|
|
|
|
it('clears state on disconnect', () => {
|
|
/**
|
|
* Test that disconnect() clears game state.
|
|
*
|
|
* When leaving a game, the composable should reset to initial state.
|
|
* This includes clearing the current game ID and game state.
|
|
*/
|
|
const { disconnect, currentGameId } = useGameSocket()
|
|
|
|
// Set up some state
|
|
gameStore.currentGameId = 'game-123'
|
|
gameStore.gameState = {} as VisibleGameState
|
|
|
|
disconnect()
|
|
|
|
expect(currentGameId.value).toBeNull()
|
|
expect(gameStore.gameState).toBeNull()
|
|
})
|
|
|
|
it('sets disconnected status on disconnect', () => {
|
|
/**
|
|
* Test that disconnect() updates connection status.
|
|
*
|
|
* The connection status should be set to DISCONNECTED so the UI
|
|
* can react appropriately (hide game UI, show lobby, etc.).
|
|
*/
|
|
const { disconnect } = useGameSocket()
|
|
|
|
// Simulate being connected
|
|
gameStore.setConnectionStatus(ConnectionStatus.CONNECTED)
|
|
|
|
disconnect()
|
|
|
|
expect(gameStore.connectionStatus).toBe(ConnectionStatus.DISCONNECTED)
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Integration with Stores
|
|
// ---------------------------------------------------------------------------
|
|
|
|
it('integrates with game store for state', () => {
|
|
/**
|
|
* Test that the composable updates the game store.
|
|
*
|
|
* All game state should flow through the game store as the single
|
|
* source of truth. This test verifies the integration is set up.
|
|
*/
|
|
const { disconnect } = useGameSocket()
|
|
|
|
// Verify we can interact with game store through the composable
|
|
disconnect()
|
|
|
|
// State should be cleared via the store
|
|
expect(gameStore.currentGameId).toBeNull()
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Return API
|
|
// ---------------------------------------------------------------------------
|
|
|
|
it('exposes the correct API', () => {
|
|
/**
|
|
* Test that the composable returns the expected interface.
|
|
*
|
|
* Components using this composable depend on a specific API.
|
|
* This test ensures the public interface is complete and correct.
|
|
*/
|
|
const api = useGameSocket()
|
|
|
|
expect(api).toHaveProperty('isConnected')
|
|
expect(api).toHaveProperty('isConnecting')
|
|
expect(api).toHaveProperty('currentGameId')
|
|
expect(api).toHaveProperty('lastHeartbeatAck')
|
|
expect(api).toHaveProperty('connect')
|
|
expect(api).toHaveProperty('disconnect')
|
|
expect(api).toHaveProperty('sendAction')
|
|
expect(api).toHaveProperty('sendResign')
|
|
|
|
expect(typeof api.connect).toBe('function')
|
|
expect(typeof api.disconnect).toBe('function')
|
|
expect(typeof api.sendAction).toBe('function')
|
|
expect(typeof api.sendResign).toBe('function')
|
|
})
|
|
})
|