aboutsummaryrefslogtreecommitdiff
path: root/src/client/shell/tests
diff options
context:
space:
mode:
authorakbash <akbash@herdr.dev>2026-09-08 17:51:59 +0300
committerGitHub <noreply@github.com>2026-09-08 17:51:59 +0300
commit68c7b78ec237034cbb0e21c8666842ed7991641d (patch)
tree3d5a63866fa785a3307959301f3022fa21bff5ed /src/client/shell/tests
parentffa0892ef87d927eef78fe8068de4d90201fb20b (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.rs264
-rw-r--r--src/client/shell/tests/keybindings_settings.rs20
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");
}