diff --git a/ui/src/main.rs b/ui/src/main.rs index c516ea0..54b77d9 100644 --- a/ui/src/main.rs +++ b/ui/src/main.rs @@ -336,6 +336,9 @@ impl ChessApp { ServerMessage2::UIUpdate { fen } => { info!("Board updated with FEN: {}", fen); // UI will automatically redraw with new FEN + if let Ok(mut game_state) = self.game_state.lock() { + game_state.fen = fen; + } } } } @@ -543,3 +546,350 @@ impl eframe::App for ChessApp { ctx.request_repaint(); } } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_default_game_state() { + let game_state = GameState::default(); + assert_eq!( + game_state.fen, + "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" + ); + assert_eq!(game_state.player_color, None); + assert_eq!(game_state.opponent_name, None); + assert_eq!(game_state.match_id, None); + assert_eq!(game_state.game_over, None); + } + + #[test] + fn test_fen_to_board_starting_position() { + let app = ChessApp::default(); + let board = app.fen_to_board("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"); + + // Test black pieces on rank 0 + assert_eq!(board[0][0], 'r'); + assert_eq!(board[0][1], 'n'); + assert_eq!(board[0][2], 'b'); + assert_eq!(board[0][3], 'q'); + assert_eq!(board[0][4], 'k'); + assert_eq!(board[0][5], 'b'); + assert_eq!(board[0][6], 'n'); + assert_eq!(board[0][7], 'r'); + + // Test black pawns on rank 1 + for col in 0..8 { + assert_eq!(board[1][col], 'p'); + } + + // Test empty squares in the middle + for row in 2..6 { + for col in 0..8 { + assert_eq!(board[row][col], ' '); + } + } + + // Test white pawns on rank 6 + for col in 0..8 { + assert_eq!(board[6][col], 'P'); + } + + // Test white pieces on rank 7 + assert_eq!(board[7][0], 'R'); + assert_eq!(board[7][1], 'N'); + assert_eq!(board[7][2], 'B'); + assert_eq!(board[7][3], 'Q'); + assert_eq!(board[7][4], 'K'); + assert_eq!(board[7][5], 'B'); + assert_eq!(board[7][6], 'N'); + assert_eq!(board[7][7], 'R'); + } + + #[test] + fn test_fen_to_board_with_numbers() { + let app = ChessApp::default(); + let board = app.fen_to_board("4k3/8/8/8/8/8/8/4K3"); + + // Test empty squares around kings + for row in 0..8 { + for col in 0..8 { + if (row == 0 && col == 4) || (row == 7 && col == 4) { + continue; // Skip king positions + } + assert_eq!(board[row][col], ' '); + } + } + + // Test king positions + assert_eq!(board[0][4], 'k'); // black king + assert_eq!(board[7][4], 'K'); // white king + } + + #[test] + fn test_chess_char_to_piece() { + let app = ChessApp::default(); + + // Test white pieces + assert_eq!(app.chess_char_to_piece('K'), "♔"); + assert_eq!(app.chess_char_to_piece('Q'), "♕"); + assert_eq!(app.chess_char_to_piece('R'), "♖"); + assert_eq!(app.chess_char_to_piece('B'), "♗"); + assert_eq!(app.chess_char_to_piece('N'), "♘"); + assert_eq!(app.chess_char_to_piece('P'), "♙"); + + // Test black pieces + assert_eq!(app.chess_char_to_piece('k'), "♚"); + assert_eq!(app.chess_char_to_piece('q'), "♛"); + assert_eq!(app.chess_char_to_piece('r'), "♜"); + assert_eq!(app.chess_char_to_piece('b'), "♝"); + assert_eq!(app.chess_char_to_piece('n'), "♞"); + assert_eq!(app.chess_char_to_piece('p'), "♟︎"); + + // Test invalid piece + assert_eq!(app.chess_char_to_piece('X'), ""); + assert_eq!(app.chess_char_to_piece(' '), ""); + } + + #[test] + fn test_chess_app_default() { + let app = ChessApp::default(); + + assert_eq!(app.server_port, "9001"); + assert_eq!(app.username, "Player"); + assert!(app.tx_to_network.is_none()); + assert!(app.rx_from_network.is_none()); + assert!(app.selected_square.is_none()); + + // Verify initial state is MainMenu + match app.state { + AppState::MainMenu => (), + _ => panic!("Expected initial state to be MainMenu"), + } + } + + #[test] + fn test_game_state_clone() { + let mut original = GameState::default(); + original.player_color = Some("white".to_string()); + original.opponent_name = Some("Opponent".to_string()); + original.match_id = Some(Uuid::new_v4()); + original.game_over = Some("Checkmate".to_string()); + + let cloned = original.clone(); + + assert_eq!(original.fen, cloned.fen); + assert_eq!(original.player_color, cloned.player_color); + assert_eq!(original.opponent_name, cloned.opponent_name); + assert_eq!(original.match_id, cloned.match_id); + assert_eq!(original.game_over, cloned.game_over); + } + + #[test] + fn test_handle_click_selection() { + let mut app = ChessApp::default(); + + // Initially no square should be selected + assert_eq!(app.selected_square, None); + + // Click on a square should select it + app.handle_click(3, 4); + assert_eq!(app.selected_square, Some((3, 4))); + + // Click on another square should deselect and send move (if tx exists) + // Since we don't have a real tx in tests, we just verify the selection is cleared + app.handle_click(4, 4); + assert_eq!(app.selected_square, None); + } + + #[test] + fn test_process_network_messages_match_found() { + let mut app = ChessApp::default(); + let (tx, mut rx) = mpsc::unbounded_channel(); + app.rx_from_network = Some(rx); + + // Send a MatchFound message + let match_id = Uuid::new_v4(); + let message = ServerMessage2::MatchFound { + match_id, + color: "white".to_string(), + opponent_name: "TestOpponent".to_string(), + }; + + tx.send(message).unwrap(); + + // Process the message + app.process_network_messages(); + + // State should transition to InGame + match app.state { + AppState::InGame => (), + _ => panic!("Expected state to transition to InGame"), + } + } + + #[test] + fn test_process_network_messages_game_over() { + let mut app = ChessApp::default(); + let (tx, mut rx) = mpsc::unbounded_channel(); + app.rx_from_network = Some(rx); + + // Send a GameEnd message + let message = ServerMessage2::GameEnd { + winner: "White won by checkmate".to_string(), + }; + + tx.send(message).unwrap(); + + // Process the message + app.process_network_messages(); + + // State should transition to GameOver + match app.state { + AppState::GameOver => (), + _ => panic!("Expected state to transition to GameOver"), + } + } + + #[test] + fn test_process_network_messages_ui_update() { + let mut app = ChessApp::default(); + let (tx, mut rx) = mpsc::unbounded_channel(); + app.rx_from_network = Some(rx); + + // Send a UIUpdate message + let new_fen = "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1".to_string(); + let message = ServerMessage2::UIUpdate { + fen: new_fen.clone(), + }; + + tx.send(message).unwrap(); + + // Process the message + app.process_network_messages(); + + // Game state should be updated with new FEN + let game_state = app.game_state.lock().unwrap(); + assert_eq!(game_state.fen, new_fen); + } + + #[test] + fn test_process_network_messages_ok_response() { + let mut app = ChessApp::default(); + app.state = AppState::Connecting; + let (tx, mut rx) = mpsc::unbounded_channel(); + app.rx_from_network = Some(rx); + + // Send an Ok message + let message = ServerMessage2::Ok { response: Ok(()) }; + + tx.send(message).unwrap(); + + // Process the message + app.process_network_messages(); + + // State should transition to FindingMatch when in Connecting state + match app.state { + AppState::FindingMatch => (), + _ => panic!("Expected state to transition to FindingMatch"), + } + } + + #[test] + fn test_fen_edge_cases() { + let app = ChessApp::default(); + + // Test empty board + let empty_board = app.fen_to_board("8/8/8/8/8/8/8/8"); + for row in 0..8 { + for col in 0..8 { + assert_eq!(empty_board[row][col], ' '); + } + } + + // Test FEN with multiple digit numbers + let board = app.fen_to_board("k7/8/8/8/8/8/8/7K"); + assert_eq!(board[0][0], 'k'); + assert_eq!(board[7][7], 'K'); + + // Test FEN with mixed pieces and numbers + let board = app.fen_to_board("r3k2r/8/8/8/8/8/8/R3K2R"); + assert_eq!(board[0][0], 'r'); + assert_eq!(board[0][4], 'k'); + assert_eq!(board[0][7], 'r'); + assert_eq!(board[7][0], 'R'); + assert_eq!(board[7][4], 'K'); + assert_eq!(board[7][7], 'R'); + } + + #[tokio::test] + async fn test_client_event_serialization() { + // Test Join event + let join_event = ClientEvent::Join { + username: "test".to_string(), + }; + let serialized = serde_json::to_string(&join_event).unwrap(); + assert!(serialized.contains("Join")); + assert!(serialized.contains("test")); + + // Test FindMatch event + let find_match_event = ClientEvent::FindMatch; + let serialized = serde_json::to_string(&find_match_event).unwrap(); + assert!(serialized.contains("FindMatch")); + + // Test Move event + let chess_move = ChessMove::Quiet { + piece_type: engine::piecetype::PieceType::WhiteKing, + from_square: BoardSquare { x: 0, y: 1 }, + to_square: BoardSquare { x: 2, y: 2 }, + promotion_piece: None, + }; + let move_event = ClientEvent::Move { step: chess_move }; + let serialized = serde_json::to_string(&move_event).unwrap(); + assert!(serialized.contains("Move")); + + // Test Resign event + let resign_event = ClientEvent::Resign; + let serialized = serde_json::to_string(&resign_event).unwrap(); + assert!(serialized.contains("Resign")); + } + + #[test] + fn test_server_message_deserialization() { + // Test Ok message + let ok_json = r#"{"Ok":{"response":{"Ok":null}}}"#; + let message: ServerMessage2 = serde_json::from_str(ok_json).unwrap(); + match message { + ServerMessage2::Ok { response } => { + assert!(response.is_ok()); + } + _ => panic!("Expected Ok message"), + } + + // Test MatchFound message + let match_found_json = r#"{"MatchFound":{"match_id":"12345678-1234-1234-1234-123456789012","color":"white","opponent_name":"Test"}}"#; + let message: ServerMessage2 = serde_json::from_str(match_found_json).unwrap(); + match message { + ServerMessage2::MatchFound { + match_id, + color, + opponent_name, + } => { + assert_eq!(color, "white"); + assert_eq!(opponent_name, "Test"); + } + _ => panic!("Expected MatchFound message"), + } + + // Test UIUpdate message + let ui_update_json = + r#"{"UIUpdate":{"fen":"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"}}"#; + let message: ServerMessage2 = serde_json::from_str(ui_update_json).unwrap(); + match message { + ServerMessage2::UIUpdate { fen } => { + assert!(fen.contains("rnbqkbnr")); + } + _ => panic!("Expected UIUpdate message"), + } + } +}