diff options
| author | akbash <akbash@herdr.dev> | 2026-09-15 16:11:39 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-15 15:11:39 +0200 |
| commit | f236ba54193d0f474b98849400b0d3a77d2056bd (patch) | |
| tree | ee1c08a07bb1dff6f4e909553cf0f7ba50f04c1d /src/server/headless/tests | |
| parent | 9bd5f4ed37d9b087aa4e89d17f2d5ee74c69beae (diff) | |
fix: propagate focused pane moves to attached clients (#4159)
refs #4153
Co-authored-by: akbash-bot <300245827+akbash-bot@users.noreply.github.com>
Co-authored-by: JJ Liebig <jonathan.liebig@gmail.com>
Diffstat (limited to 'src/server/headless/tests')
| -rw-r--r-- | src/server/headless/tests/mod.rs | 2 | ||||
| -rw-r--r-- | src/server/headless/tests/pane_move.rs | 226 |
2 files changed, 228 insertions, 0 deletions
diff --git a/src/server/headless/tests/mod.rs b/src/server/headless/tests/mod.rs index 8e731211..093b5625 100644 --- a/src/server/headless/tests/mod.rs +++ b/src/server/headless/tests/mod.rs @@ -2,6 +2,8 @@ use super::*; #[path = "pane_graphics.rs"] mod pane_graphics_tests; +#[path = "pane_move.rs"] +mod pane_move_tests; #[path = "surface_interest.rs"] mod surface_interest_tests; diff --git a/src/server/headless/tests/pane_move.rs b/src/server/headless/tests/pane_move.rs new file mode 100644 index 00000000..93bff0c9 --- /dev/null +++ b/src/server/headless/tests/pane_move.rs @@ -0,0 +1,226 @@ +use super::*; +use crate::api::schema::{ + ErrorResponse, PaneMoveDestination, PaneMoveParams, PaneMoveResult, ResponseResult, + SuccessResponse, +}; + +fn pane_move_server() -> HeadlessServer { + let mut server = test_headless_server(); + let mut first = crate::workspace::Workspace::test_new("first"); + first.test_add_tab(Some("remaining")); + first.switch_tab(0); + server.app.state.workspaces = vec![first, crate::workspace::Workspace::test_new("second")]; + server.app.state.ensure_test_terminals(); + server.app.state.active = Some(0); + server.app.state.selected = 0; + server.app.state.mode = crate::app::Mode::Terminal; + server +} + +fn public_move( + server: &mut HeadlessServer, + params: PaneMoveParams, +) -> Result<PaneMoveResult, ErrorResponse> { + let (respond_to, response_rx) = std::sync::mpsc::channel(); + server.handle_api_request_with_shutdown_check(crate::api::ApiRequestMessage { + request: crate::api::schema::Request { + id: "move-pane".into(), + method: crate::api::schema::Method::PaneMove(params), + }, + respond_to, + response_write_complete: None, + stream_active: None, + }); + let response = response_rx.recv().expect("pane move response"); + match serde_json::from_str::<SuccessResponse>(&response) { + Ok(SuccessResponse { + result: ResponseResult::PaneMove { move_result }, + .. + }) => Ok(move_result), + Ok(other) => panic!("expected pane move response, got {other:?}"), + Err(_) => Err(serde_json::from_str(&response).expect("error response")), + } +} + +#[tokio::test] +async fn public_pane_move_focus_follows_the_moved_pane() { + let mut server = pane_move_server(); + let source = server.app.state.workspaces[0].tabs[0].root_pane; + let terminal_id = server.app.state.workspaces[0].tabs[0] + .terminal_id(source) + .unwrap() + .clone(); + let (runtime, mut input_rx) = crate::terminal::TerminalRuntime::test_with_channel(80, 24); + server.app.terminal_runtimes.insert(terminal_id, runtime); + let source_id = server.app.public_pane_id(0, source).unwrap(); + let destination_id = server.app.public_workspace_id(1); + let (control_rx, render_rx) = connect_test_shell(&mut server, 9, 80, 23); + let initial = client_shell_snapshot(read_server_message(control_rx.recv().unwrap())); + + let moved = public_move( + &mut server, + PaneMoveParams { + pane_id: source_id, + destination: PaneMoveDestination::NewTab { + workspace_id: Some(destination_id.clone()), + label: None, + }, + focus: true, + }, + ) + .unwrap(); + assert!(moved.changed); + assert_eq!(server.app.state.active, Some(1)); + assert!(moved.closed_tab_id.is_some()); + let location = server.clients[&9].shell_location.as_ref().unwrap(); + assert_eq!( + location.focused_workspace_id.as_deref(), + Some(destination_id.as_str()), + "successful public pane move with focus must move the attached client" + ); + assert_eq!(location.focused_tab_id(), Some(moved.pane.tab_id.as_str())); + + server.render_and_stream(); + let snapshot = client_shell_snapshot(read_server_message(control_rx.recv().unwrap())); + assert!(snapshot.revision > initial.revision); + assert_eq!( + snapshot.focused_workspace_id.as_deref(), + Some(destination_id.as_str()) + ); + assert_eq!( + snapshot.focused_tab_id.as_deref(), + Some(moved.pane.tab_id.as_str()) + ); + assert_eq!( + snapshot.focused_pane_id.as_deref(), + Some(moved.pane.pane_id.as_str()) + ); + let surface = recv_pane_surface(&render_rx, "moved pane surface"); + assert_eq!(surface.projection_revision, snapshot.revision); + + server.handle_server_event(ServerEvent::ClientShellPaneInput { + client_id: 9, + pane_id: moved.pane.pane_id, + events: vec![crate::protocol::ClientPaneInputEvent::TextCommit( + "x".into(), + )], + }); + assert_eq!( + input_rx.try_recv().expect("input reaches moved terminal"), + Bytes::from_static(b"x") + ); + assert_eq!(server.app.state.active, Some(1)); + assert_eq!(server.shell_tab_id_for_client(9), Some(moved.pane.tab_id)); + for workspace in &server.app.state.workspaces { + workspace.assert_invariants_for_test(); + } + shutdown_test_runtimes(&mut server); +} + +#[tokio::test] +async fn public_pane_move_focus_handles_source_removal_and_unchanged_server_target() { + // Both moves leave the server's numeric workspace/tab coordinates unchanged. + // One creates a workspace; the other moves into an already-focused split tab. + for new_workspace in [true, false] { + let mut server = pane_move_server(); + let source = server.app.state.workspaces[0].tabs[0].root_pane; + let source_id = server.app.public_pane_id(0, source).unwrap(); + let first_tab = server.app.public_tab_id(0, 0).unwrap(); + let second_tab = server.app.public_tab_id(1, 0).unwrap(); + let destination = if new_workspace { + server.app.state.workspaces.remove(1); + server.app.state.workspaces[0].tabs.remove(1); + PaneMoveDestination::NewWorkspace { + label: None, + tab_label: None, + } + } else { + server.app.state.switch_workspace_tab(1, 0); + PaneMoveDestination::Tab { + tab_id: second_tab, + target_pane_id: None, + split: crate::api::schema::SplitDirection::Right, + ratio: None, + } + }; + let target_before = server.default_shell_target(); + let (_first_control, _first_render) = connect_test_shell(&mut server, 9, 80, 23); + let (_second_control, _second_render) = connect_test_shell(&mut server, 10, 80, 23); + assert!(server.focus_shell_client_on_tab(9, &first_tab)); + assert!(server.focus_shell_client_on_tab(10, &first_tab)); + let moved = public_move( + &mut server, + PaneMoveParams { + pane_id: source_id, + destination, + focus: true, + }, + ) + .unwrap(); + assert!(moved.changed); + assert_eq!(server.default_shell_target(), target_before); + assert_eq!(moved.closed_workspace_id.is_some(), new_workspace); + for client_id in [9, 10] { + assert_eq!( + server.shell_tab_id_for_client(client_id).as_deref(), + Some(moved.pane.tab_id.as_str()) + ); + let target = server.shell_focus_target(client_id).unwrap(); + assert_eq!(target.pane_id, source); + if new_workspace { + let location = server.clients[&client_id].shell_location.as_ref().unwrap(); + assert_eq!(location.active_tab_ids.len(), 1); + } + } + shutdown_test_runtimes(&mut server); + } +} + +#[tokio::test] +async fn public_pane_move_without_effective_focus_preserves_client_views() { + for case in ["no-focus", "same-tab", "zoomed", "invalid"] { + let mut server = pane_move_server(); + let source = server.app.state.workspaces[0].tabs[0].root_pane; + let source_id = server.app.public_pane_id(0, source).unwrap(); + let remaining_tab = server.app.public_tab_id(0, 1).unwrap(); + let destination_tab = server.app.public_tab_id(1, 0).unwrap(); + let (_control, _render) = connect_test_shell(&mut server, 9, 80, 23); + // Keep a valid view distinct from the server default in every case. + assert!(server.focus_shell_client_on_tab(9, &remaining_tab)); + let location_before = server.clients[&9].shell_location.clone(); + let revision_before = server.clients[&9].shell_projection_revision; + if case == "zoomed" { + server.app.state.workspaces[0].tabs[0].zoomed = true; + } + let tab_id = match case { + "same-tab" => server.app.public_tab_id(0, 0).unwrap(), + "invalid" => "missing-tab".into(), + _ => destination_tab, + }; + let result = public_move( + &mut server, + PaneMoveParams { + pane_id: source_id, + destination: PaneMoveDestination::Tab { + tab_id, + target_pane_id: None, + split: crate::api::schema::SplitDirection::Right, + ratio: None, + }, + focus: case != "no-focus", + }, + ); + if case == "invalid" { + assert_eq!(result.unwrap_err().error.code, "tab_not_found"); + } else { + assert_eq!(result.unwrap().changed, case == "no-focus"); + } + assert_eq!(server.clients[&9].shell_location, location_before, "{case}"); + assert_eq!( + server.clients[&9].shell_projection_revision, revision_before, + "{case}" + ); + assert_eq!(server.app.state.active, Some(0)); + shutdown_test_runtimes(&mut server); + } +} |
