diff options
| author | akbash <akbash@herdr.dev> | 2026-09-08 17:51:59 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-08 17:51:59 +0300 |
| commit | 68c7b78ec237034cbb0e21c8666842ed7991641d (patch) | |
| tree | 3d5a63866fa785a3307959301f3022fa21bff5ed /src/client/shell/tests | |
| parent | ffa0892ef87d927eef78fe8068de4d90201fb20b (diff) | |
fix: collapse worktree groups with saved machines (#3781)
* fix: collapse worktree groups with saved machines
refs #3778
* fix: scope worktree collapse by machine
refs #3778
* fix: collapse machines without switching focus
refs #3778
---------
Co-authored-by: akbash-bot <300245827+akbash-bot@users.noreply.github.com>
Co-authored-by: Ogulcan Celik <ogulcancelik@gmail.com>
Diffstat (limited to 'src/client/shell/tests')
| -rw-r--r-- | src/client/shell/tests/endpoints.rs | 264 | ||||
| -rw-r--r-- | src/client/shell/tests/keybindings_settings.rs | 20 |
2 files changed, 282 insertions, 2 deletions
diff --git a/src/client/shell/tests/endpoints.rs b/src/client/shell/tests/endpoints.rs index 52c71923..515c9837 100644 --- a/src/client/shell/tests/endpoints.rs +++ b/src/client/shell/tests/endpoints.rs @@ -256,6 +256,185 @@ fn sidebar_renders_local_and_saved_ssh_endpoints_with_status() { } #[test] +fn saved_machine_preserves_endpoint_scoped_worktree_collapses() { + fn add_worktree_group(snapshot: &mut ClientShellSnapshot, parent_id: &str, child_id: &str) { + snapshot.workspaces[0].workspace_id = parent_id.into(); + snapshot.workspaces[0].worktree = Some(ClientShellWorktree { + key: "repo".into(), + label: "repo".into(), + is_linked_worktree: false, + }); + let mut child = snapshot.workspaces[0].clone(); + child.workspace_id = child_id.into(); + child.active_tab_id = format!("tab_{child_id}"); + child.number = 2; + child.label = "feature".into(); + child.focused = false; + child.agent_status = AgentStatus::Blocked; + child.worktree = Some(ClientShellWorktree { + key: "repo".into(), + label: "repo".into(), + is_linked_worktree: true, + }); + snapshot.workspaces.push(child); + } + + let (mut state, remote_id) = state_with_remote(); + let mut local = snapshot(); + add_worktree_group(&mut local, "ws_1", "ws_2"); + state.set_snapshot(Box::new(local)); + let mut remote = snapshot(); + remote.boot_id = "remote-boot".into(); + remote.focused_workspace_id = Some("remote_ws_1".into()); + add_worktree_group(&mut remote, "remote_ws_1", "remote_ws_2"); + state.set_endpoint_snapshot(&remote_id, Box::new(remote)); + + state.open_workspace_context_menu("ws_1".into(), 0, 0); + let toggle_index = match state.overlay.as_ref() { + Some(ClientShellOverlay::ContextMenu(menu)) => menu + .items() + .iter() + .position(|item| item.action == ClientContextMenuAction::ToggleGroup) + .expect("collapse menu item"), + _ => panic!("workspace context menu"), + }; + state.activate_context_menu_item(toggle_index, &mut ClientShellInput::default()); + + let frame = state + .compose(100, 28) + .expect("collapsed local worktree group"); + assert!(!state + .hits + .workspaces + .iter() + .any(|hit| { hit.endpoint_id == ClientEndpointId::Local && hit.workspace_id == "ws_2" })); + assert!(state + .hits + .workspaces + .iter() + .any(|hit| hit.endpoint_id == remote_id && hit.workspace_id == "remote_ws_2")); + let local_parent = state + .hits + .workspaces + .iter() + .find(|hit| hit.endpoint_id == ClientEndpointId::Local && hit.workspace_id == "ws_1") + .expect("local parent workspace"); + let (local_toggle, key) = local_parent + .group_toggle + .as_ref() + .expect("local worktree group marker"); + assert_eq!(key, "repo"); + let buffer = frame.to_ratatui_buffer().expect("frame should reconstruct"); + assert_eq!(buffer[(local_toggle.x, local_toggle.y)].symbol(), "▸"); + assert!((local_parent.rect.x..local_parent.rect.right()) + .any(|x| buffer[(x, local_parent.rect.y)].fg == state.config.palette.red)); + + assert!(state.activate_endpoint_projection(&remote_id)); + let mut remote_surface = surface(); + remote_surface.boot_id = "remote-boot".into(); + state.set_pane_surface(remote_surface); + state + .compose(100, 28) + .expect("active remote worktree group"); + let mut switch = ClientShellInput::default(); + state.record_binding( + crate::input::KeybindMatch::Action(crate::input::KeybindAction::SwitchWorkspace(1)), + &mut switch, + ); + assert!(matches!( + &switch.actions[..], + [ClientShellAction::Endpoint { request, .. }] + if matches!( + &request.method, + crate::api::schema::Method::WorkspaceFocus(target) + if target.workspace_id == "remote_ws_2" + ) + )); + let remote_parent = state + .hits + .workspaces + .iter() + .find(|hit| hit.endpoint_id == remote_id && hit.workspace_id == "remote_ws_1") + .expect("visible remote worktree parent") + .rect; + let remote_child = state + .hits + .workspaces + .iter() + .find(|hit| hit.endpoint_id == remote_id && hit.workspace_id == "remote_ws_2") + .expect("visible remote worktree child") + .rect; + state.handle_raw_events(vec![RawInputEvent::Mouse(crossterm::event::MouseEvent { + kind: MouseEventKind::Down(MouseButton::Left), + column: remote_parent.x + 3, + row: remote_parent.y, + modifiers: KeyModifiers::empty(), + })]); + state.handle_raw_events(vec![RawInputEvent::Mouse(crossterm::event::MouseEvent { + kind: MouseEventKind::Drag(MouseButton::Left), + column: remote_child.x + 3, + row: remote_child.bottom(), + modifiers: KeyModifiers::empty(), + })]); + assert!(matches!( + state.chrome_drag, + Some(ClientChromeDrag::Workspace { + target: Some(_), + .. + }) + )); + state.chrome_drag = None; + state.workspace_press = None; + + let remote_toggle = state + .hits + .workspaces + .iter() + .find(|hit| hit.endpoint_id == remote_id && hit.workspace_id == "remote_ws_1") + .and_then(|hit| hit.group_toggle.as_ref()) + .expect("remote worktree group marker") + .0; + state.handle_raw_events(vec![RawInputEvent::Mouse(crossterm::event::MouseEvent { + kind: MouseEventKind::Down(MouseButton::Left), + column: remote_toggle.x, + row: remote_toggle.y, + modifiers: KeyModifiers::empty(), + })]); + state.compose(100, 28).expect("both groups collapsed"); + assert!(!state + .hits + .workspaces + .iter() + .any(|hit| { hit.endpoint_id == remote_id && hit.workspace_id == "remote_ws_2" })); + + let local_toggle = state + .hits + .workspaces + .iter() + .find(|hit| hit.endpoint_id == ClientEndpointId::Local && hit.workspace_id == "ws_1") + .and_then(|hit| hit.group_toggle.as_ref()) + .expect("collapsed local group marker") + .0; + state.handle_raw_events(vec![RawInputEvent::Mouse(crossterm::event::MouseEvent { + kind: MouseEventKind::Down(MouseButton::Left), + column: local_toggle.x, + row: local_toggle.y, + modifiers: KeyModifiers::empty(), + })]); + state.compose(100, 28).expect("only remote group collapsed"); + assert!(state + .hits + .workspaces + .iter() + .any(|hit| { hit.endpoint_id == ClientEndpointId::Local && hit.workspace_id == "ws_2" })); + assert!(!state + .hits + .workspaces + .iter() + .any(|hit| { hit.endpoint_id == remote_id && hit.workspace_id == "remote_ws_2" })); +} + +#[test] fn active_workspace_is_the_only_highlight_when_machine_is_expanded() { let (mut state, endpoint_id) = state_with_remote(); assert!(state.activate_endpoint_projection(&endpoint_id)); @@ -468,7 +647,7 @@ fn unselected_endpoint_completion_projects_done_client_side() { } #[test] -fn clicking_remote_endpoint_requests_activation_without_mutating_projection() { +fn clicking_remote_machine_name_requests_activation_without_mutating_projection() { let (mut state, endpoint_id) = state_with_remote(); state.compose(100, 28).expect("combined endpoint frame"); let hit = state @@ -480,7 +659,7 @@ fn clicking_remote_endpoint_requests_activation_without_mutating_projection() { .rect; let outcome = state.handle_raw_events(vec![RawInputEvent::Mouse(MouseEvent { kind: MouseEventKind::Down(MouseButton::Left), - column: hit.x, + column: hit.x + 3, row: hit.y, modifiers: KeyModifiers::empty(), })]); @@ -502,6 +681,87 @@ fn clicking_remote_endpoint_requests_activation_without_mutating_projection() { } #[test] +fn machine_arrow_toggles_inactive_machine_without_switching() { + for sidebar_collapsed in [false, true] { + for status in [ + ClientEndpointStatus::Online, + ClientEndpointStatus::Reconnecting, + ] { + let (mut state, remote_id) = state_with_remote(); + let mut other_profile = remote_profile(); + other_profile.id = ProfileId::parse("1123456789abcdef0123456789abcdef").unwrap(); + let other_id = ClientEndpointId::Ssh(other_profile.id.clone()); + state.set_endpoint_catalog(&[remote_profile(), other_profile]); + state.set_endpoint_status(&other_id, ClientEndpointStatus::Online); + state.set_endpoint_snapshot(&other_id, Box::new(snapshot())); + state.set_endpoint_status(&remote_id, status); + state.sidebar_collapsed = sidebar_collapsed; + + for collapsed in [true, false] { + let frame = state.compose(100, 28).expect("three machine frame"); + let machine = state + .hits + .machines + .iter() + .find(|hit| hit.endpoint_id == remote_id) + .expect("remote machine") + .rect; + let column = machine.x + u16::from(!sidebar_collapsed); + let buffer = frame.to_ratatui_buffer().expect("frame buffer"); + assert_eq!( + buffer[(column, machine.y)].symbol(), + if collapsed { "▾" } else { "▸" } + ); + let outcome = state.handle_raw_events(vec![RawInputEvent::Mouse(MouseEvent { + kind: MouseEventKind::Down(MouseButton::Left), + column, + row: machine.y, + modifiers: KeyModifiers::empty(), + })]); + assert!( + outcome.actions.is_empty(), + "collapse must not switch machines" + ); + assert!(outcome.requests.is_empty()); + assert!(outcome.repaint); + assert_eq!(state.active_endpoint_id, ClientEndpointId::Local); + assert_eq!(state.snapshot.as_ref().unwrap().boot_id, "boot-1"); + assert_eq!( + state + .snapshot + .as_ref() + .unwrap() + .focused_workspace_id + .as_deref(), + Some("ws_1") + ); + assert_eq!(state.collapsed_endpoints.contains(&remote_id), collapsed); + assert!(!state.collapsed_endpoints.contains(&ClientEndpointId::Local)); + assert!(!state.collapsed_endpoints.contains(&other_id)); + assert!(state.endpoint_error.is_none()); + + state.compose(100, 28).expect("toggled machine frame"); + assert_eq!( + state + .hits + .workspaces + .iter() + .any(|hit| hit.endpoint_id == remote_id), + !collapsed + ); + for endpoint_id in [&ClientEndpointId::Local, &other_id] { + assert!(state + .hits + .workspaces + .iter() + .any(|hit| &hit.endpoint_id == endpoint_id)); + } + } + } + } +} + +#[test] fn context_menu_lookup_ignores_inactive_endpoint_workspaces() { let (mut state, endpoint_id) = state_with_remote(); state.compose(100, 28).expect("combined endpoint frame"); diff --git a/src/client/shell/tests/keybindings_settings.rs b/src/client/shell/tests/keybindings_settings.rs index b8f17e57..1654d0df 100644 --- a/src/client/shell/tests/keybindings_settings.rs +++ b/src/client/shell/tests/keybindings_settings.rs @@ -62,7 +62,18 @@ fn manual_client_chrome_preferences_round_trip_per_endpoint() { state.sidebar_collapsed_manual = true; state.collapsed_groups.insert("repo-two".into()); state.collapsed_groups.insert("repo-one".into()); + let profile = + SavedSshEndpoint::new("Build", "dev@build.example", "agents").expect("saved SSH profile"); + let remote_id = ClientEndpointId::Ssh(profile.id.clone()); + state + .remote_collapsed_groups + .insert(remote_id.clone(), HashSet::from(["/repo".to_owned()])); state.persist_chrome_preferences(&mut ClientShellInput::default()); + let stored = std::fs::read_to_string(&path).expect("stored client chrome preferences"); + assert!(stored.contains(profile.id.as_str())); + assert!(stored.contains("repo-one")); + assert!(!stored.contains(&profile.label)); + assert!(!stored.contains(&profile.target)); let reloaded_config = ClientShellConfig::from_config(&Config::default()).with_preferences_path(path.clone()); @@ -77,6 +88,15 @@ fn manual_client_chrome_preferences_round_trip_per_endpoint() { reloaded.collapsed_groups, HashSet::from(["repo-one".to_string(), "repo-two".to_string()]) ); + assert_eq!( + reloaded.remote_collapsed_groups.get(&remote_id), + Some(&HashSet::from(["/repo".to_owned()])) + ); + let mut reloaded = reloaded; + reloaded.persist_chrome_preferences(&mut ClientShellInput::default()); + let stored_again = std::fs::read_to_string(&path).expect("restored client chrome preferences"); + assert!(stored_again.contains(profile.id.as_str())); + assert!(stored_again.contains("/repo")); std::fs::remove_file(path).expect("remove client chrome preferences"); } |
