#![cfg(unix)] use zellij_integration_tests::{ guest_ascended_bar_settled, keys, FakePtyHandle, GridSnapshot, NestedHarness, Size, PROMPT, TERMINAL_SIZE, }; use zellij_utils::nested_session::NestedSessionMessage; const ASCEND_KEY: &[u8] = b"]"; const DESCEND_KEY: &[u8] = b"["; const ARROW_LEFT: &[u8] = b"\x1b[D"; fn last_line_contains(grid_snapshot: &GridSnapshot, needle: &str) -> bool { grid_snapshot .lines() .last() .map_or(false, |last_line| last_line.contains(needle)) } fn has_breadcrumb(grid: &GridSnapshot) -> bool { grid.lines().first().map_or(false, |l| l.contains('▸')) } fn normal_mode_bar_settled(grid_snapshot: &GridSnapshot) -> bool { last_line_contains(grid_snapshot, "LOCK") } fn pane_mode_bar_settled(grid_snapshot: &GridSnapshot) -> bool { last_line_contains(grid_snapshot, "Fullscreen") } fn session_mode_bar_settled(grid_snapshot: &GridSnapshot) -> bool { last_line_contains(grid_snapshot, "SESSION") && last_line_contains(grid_snapshot, "Detach") } fn scroll_mode_bar_settled(grid_snapshot: &GridSnapshot) -> bool { last_line_contains(grid_snapshot, "Scroll") && last_line_contains(grid_snapshot, "Edit") && last_line_contains(grid_snapshot, "Select") } fn entering_search_bar_settled(grid_snapshot: &GridSnapshot) -> bool { last_line_contains(grid_snapshot, "ENTERING SEARCH TERM") } fn guest_normal_mode_bar_settled(guest_grid: &GridSnapshot) -> bool { guest_grid.lines().last().map_or(false, |last_line| { if last_line.contains("LOCK") { return true; } let tokens: Vec = last_line .replace('\u{e0b0}', " ") .split_whitespace() .map(|token| token.to_owned()) .collect(); ["g", "p", "t", "n", "h", "s", "o", "q"] .iter() .all(|mode_letter| tokens.iter().any(|token| token == mode_letter)) }) } fn guest_ui_settled(guest_grid: &GridSnapshot) -> bool { let lines = guest_grid.lines(); lines .first() .map_or(false, |first_line| first_line.contains("Tab #1")) && last_line_contains(guest_grid, "Ctrl +") && guest_normal_mode_bar_settled(guest_grid) } fn guest_ascended_ui_settled(guest_grid: &GridSnapshot) -> bool { guest_grid .lines() .first() .map_or(false, |first_line| first_line.contains("Tab #1")) && guest_ascended_bar_settled(guest_grid) } fn prompt_count(grid_snapshot: &GridSnapshot) -> usize { grid_snapshot.text.matches('$').count() } fn boot_and_descend_on_first_load(nested: &NestedHarness) { nested.wait_for_guest_to_announce(); nested.wait_for_host_to_acknowledge_guest(); nested.descend_into_guest_via_modal(); nested.guest.wait_for_app_load(); nested.wait_for_host_to_ping_guest(); nested.wait_for_guest_to_reply_to_ping(); nested .guest .wait_until("guest settled in normal mode", guest_ui_settled); } fn ascend_via_focus_host_binding(nested: &NestedHarness) { let requested = nested.mark_guest_to_host(); let ascended = nested.mark_host_to_guest(); nested.guest.wait_until( "guest settled in normal mode before the mode key is passed through", guest_ui_settled, ); nested.host.send_stdin(&keys::ctrl('o')); nested.guest.wait_until( "guest entered session mode after the passed-through mode key", session_mode_bar_settled, ); nested.host.send_stdin(ASCEND_KEY); nested.wait_for_guest_to_request_host_focus_after(requested); nested.wait_for_host_to_ascend_from_guest_after(ascended); } fn split_host_sibling(nested: &NestedHarness) -> FakePtyHandle { nested.host.send_stdin(&keys::ctrl('p')); nested.host.wait_until( "host entered pane mode before the split key", pane_mode_bar_settled, ); nested.host.send_stdin(&keys::key('r')); let sibling = nested.host.expect_pty_spawn(); sibling.output(PROMPT); nested.host.wait_until( "host returned to normal mode after the split key", |host_grid| normal_mode_bar_settled(host_grid) && prompt_count(host_grid) >= 1, ); sibling } fn descend_into_guest_on_the_left(nested: &NestedHarness) { let descended = nested.mark_host_to_guest(); nested.host.send_stdin(&keys::ctrl('p')); nested.host.wait_until( "host entered pane mode before the focus-left key", pane_mode_bar_settled, ); nested.host.send_stdin(ARROW_LEFT); nested.host.send_stdin(&keys::ctrl('o')); nested.host.wait_until( "host entered session mode before the descend key", session_mode_bar_settled, ); nested.host.send_stdin(DESCEND_KEY); nested.wait_for_host_to_descend_into_guest_after(descended); } fn build_descended_state_with_host_sibling(nested: &NestedHarness) -> FakePtyHandle { boot_and_descend_on_first_load(nested); ascend_via_focus_host_binding(nested); let sibling = split_host_sibling(nested); descend_into_guest_on_the_left(nested); sibling } fn quit_guest_then_host(mut nested: NestedHarness) { nested.guest.quit(); nested.wait_for_host_to_reclaim_focus_after_guest_exit(); nested.host.quit(); } const SMALLER_SIZE: Size = Size { cols: 90, rows: 18 }; const LARGER_SIZE: Size = Size { cols: 140, rows: 30, }; #[test] fn resizing_the_terminal_while_descended_keeps_the_guest_sized_to_its_pane() { let nested = NestedHarness::start(TERMINAL_SIZE); boot_and_descend_on_first_load(&nested); let guest_pane_before = nested.host_pane.size().expect("guest pane sized on load"); let guest_rows_before = nested.guest.snapshot().row_count(); nested.host.resize(SMALLER_SIZE); let guest_pane_smaller = nested.host_pane.wait_for_size( "the guest pane shrinks with the host terminal while descended", move |cols, rows| (cols, rows) != guest_pane_before, ); assert!( guest_pane_smaller.0 < guest_pane_before.0 || guest_pane_smaller.1 < guest_pane_before.1, "the guest pane must shrink when the host terminal shrinks \ (before={guest_pane_before:?}, after={guest_pane_smaller:?})", ); let guest_after_shrink = nested.guest.wait_until( "the guest re-renders at the smaller pane size while staying descended", |guest_grid| { guest_grid.row_count() == guest_pane_smaller.1 as usize && guest_grid.tab_bar_appears() }, ); assert!( guest_after_shrink.row_count() < guest_rows_before, "the descended guest must render fewer rows after the host shrinks", ); nested.host.resize(LARGER_SIZE); let guest_pane_larger = nested.host_pane.wait_for_size( "the guest pane grows with the host terminal while descended", move |cols, rows| (cols, rows) != guest_pane_smaller, ); nested.guest.wait_until( "the guest re-renders at the larger pane size while staying descended", |guest_grid| { guest_grid.row_count() == guest_pane_larger.1 as usize && guest_ui_settled(guest_grid) }, ); nested.host.send_stdin(&keys::alt('n')); let guest_new_pane = nested.guest.expect_pty_spawn(); guest_new_pane.output(PROMPT); nested.guest.wait_until( "a key after the resize still reaches the descended guest", |guest_grid| guest_grid.contains("Pane #2") && guest_grid.contains("Pane #1"), ); assert_eq!( nested.focus_lost_count(), 0, "resizing while descended must not ascend the host out of the guest", ); quit_guest_then_host(nested); } #[test] fn resizing_the_terminal_while_nested_fullscreen_keeps_the_guest_filling_the_display() { let nested = NestedHarness::start(TERMINAL_SIZE); boot_and_descend_on_first_load(&nested); let entered = nested.mark_guest_to_host(); nested.host.send_stdin(&keys::ctrl('o')); nested.guest.wait_until( "guest entered session mode before the fullscreen bind", session_mode_bar_settled, ); nested.host.send_stdin(&keys::key('f')); nested.guest_to_host().wait_for_after( entered, "the descended guest asks its host to fullscreen its pane", |message| { matches!( message, NestedSessionMessage::ToggleHostFullscreen { fullscreen: true } ) }, ); let fullscreen_pane_before = nested.host.wait_until( "the guest fills the whole host display before the resize", |host_grid| { has_breadcrumb(host_grid) && host_grid.contains("Tab #1") && host_grid.row_count() == TERMINAL_SIZE.rows }, ); let host_rows_before = fullscreen_pane_before.row_count(); nested.host.resize(LARGER_SIZE); let host_after_resize = nested.host.wait_until( "the fullscreened guest still fills the whole enlarged host display", |host_grid| { host_grid.row_count() == LARGER_SIZE.rows && has_breadcrumb(host_grid) && host_grid.contains("Tab #1") }, ); assert!( host_after_resize.row_count() > host_rows_before, "the host display must render more rows after the terminal grows", ); assert!( host_after_resize.row_of_line("Pane #1").is_none(), "no stray host pane-title line while the guest is fullscreened after resize", ); nested.guest.wait_until( "the guest re-renders filling the enlarged display while fullscreened", |guest_grid| guest_grid.row_count() == LARGER_SIZE.rows && has_breadcrumb(guest_grid), ); let exited = nested.mark_guest_to_host(); nested.host.send_stdin(&keys::ctrl('o')); nested.guest.wait_until( "guest entered session mode before the toggle-off bind", session_mode_bar_settled, ); nested.host.send_stdin(&keys::key('f')); nested.guest_to_host().wait_for_after( exited, "the guest asks its host to exit fullscreen", |message| { matches!( message, NestedSessionMessage::ToggleHostFullscreen { fullscreen: false } ) }, ); nested.host.wait_until( "the host chrome returns at the enlarged size after exiting fullscreen", |host_grid| { host_grid.row_count() == LARGER_SIZE.rows && !has_breadcrumb(host_grid) && host_grid.tab_bar_appears() && host_grid.status_bar_appears() }, ); quit_guest_then_host(nested); } #[test] fn host_scrollback_on_a_guest_pane_scrolls_the_host_not_the_guest() { let mut nested = NestedHarness::start(TERMINAL_SIZE); boot_and_descend_on_first_load(&nested); ascend_via_focus_host_binding(&nested); nested.host.send_stdin(&keys::ctrl('s')); nested.host.wait_until( "the host enters scroll mode over the guest pane", scroll_mode_bar_settled, ); let focus_gained_before_scroll = nested.focus_gained_count(); nested.host.send_stdin(&keys::key('k')); nested.host.send_stdin(&keys::key('k')); nested.host.wait_until( "the host stays in scroll mode after scrolling the guest pane", scroll_mode_bar_settled, ); assert_eq!( nested.focus_gained_count(), focus_gained_before_scroll, "scrolling the host over a guest pane must not descend into the guest", ); nested.host.send_stdin(&keys::ESC); let host_back_to_normal = nested.host.wait_until( "the host returns to normal mode after scrolling the guest pane", |host_grid| normal_mode_bar_settled(host_grid), ); nested.guest.wait_until( "the guest still shows its ascended UI undisturbed by the host scrolling its pane", guest_ascended_ui_settled, ); assert!(normal_mode_bar_settled(&host_back_to_normal)); nested.guest.quit(); nested.host.quit(); } #[test] fn host_search_over_a_guest_pane_stays_host_side() { let mut nested = NestedHarness::start(TERMINAL_SIZE); boot_and_descend_on_first_load(&nested); ascend_via_focus_host_binding(&nested); nested.host.send_stdin(&keys::ctrl('s')); nested.host.wait_until( "the host enters scroll mode over the guest pane", scroll_mode_bar_settled, ); nested.host.send_stdin(&keys::key('s')); nested.host.wait_until( "the host enters search-input over the guest pane", entering_search_bar_settled, ); let focus_gained_before_search = nested.focus_gained_count(); nested.host.send_stdin(b"Tab"); nested.host.send_stdin(&keys::ESC); nested.host.wait_until( "the host settles back in scroll mode after the search input", scroll_mode_bar_settled, ); assert_eq!( nested.focus_gained_count(), focus_gained_before_search, "searching the host over a guest pane must not descend into the guest", ); nested.host.send_stdin(&keys::ESC); nested.host.wait_until( "the host returns to normal mode after searching the guest pane", |host_grid| normal_mode_bar_settled(host_grid), ); nested.guest.wait_until( "the guest still shows its ascended UI undisturbed by the host searching its pane", guest_ascended_ui_settled, ); nested.guest.quit(); nested.host.quit(); } #[test] fn closing_a_host_sibling_pane_while_descended_keeps_the_guest_live() { let nested = NestedHarness::start(TERMINAL_SIZE); let sibling = build_descended_state_with_host_sibling(&nested); nested.host.wait_until( "the host shows its guest pane and its sibling before the sibling closes", |host_grid| host_grid.contains("Pane #2"), ); nested.guest.wait_until( "the guest is settled while the host is descended into it", guest_ui_settled, ); sibling.exit(Some(0)); nested.host.wait_until( "the host closed its exited sibling, leaving only the guest pane", |host_grid| !host_grid.contains("Pane #2"), ); nested.host.send_stdin(&keys::alt('n')); let guest_new_pane = nested.guest.expect_pty_spawn(); guest_new_pane.output(PROMPT); nested.guest.wait_until( "the guest is still live and reacts to keys after the host sibling closed", |guest_grid| guest_grid.contains("Pane #2") && guest_grid.contains("│"), ); quit_guest_then_host(nested); } #[test] fn closing_an_inner_guest_pane_while_descended_keeps_the_guest_live() { let nested = NestedHarness::start(TERMINAL_SIZE); boot_and_descend_on_first_load(&nested); nested.host.send_stdin(&keys::alt('n')); let guest_second_pane = nested.guest.expect_pty_spawn(); guest_second_pane.output(PROMPT); nested.guest.wait_until( "the guest opened a second inner pane from the passed-through key", |guest_grid| { guest_grid.contains("Pane #1") && guest_grid.contains("Pane #2") && guest_ui_settled(guest_grid) }, ); guest_second_pane.exit(Some(0)); nested.guest.wait_until( "the guest closed its inner pane and returned to a single pane", |guest_grid| { !guest_grid.contains("Pane #2") && guest_grid.tab_bar_appears() && guest_grid.cursor.is_some() }, ); nested.host.send_stdin(&keys::alt('n')); let guest_new_pane = nested.guest.expect_pty_spawn(); guest_new_pane.output(PROMPT); nested.guest.wait_until( "the guest is still descended-into and reacts to keys after closing an inner pane", |guest_grid| guest_grid.contains("Pane #2") && guest_grid.contains("│"), ); quit_guest_then_host(nested); } #[test] fn detaching_the_guest_client_while_descended_returns_control_to_the_host() { let mut nested = NestedHarness::start(TERMINAL_SIZE); boot_and_descend_on_first_load(&nested); nested.guest.detach_main_client(); nested.wait_for_host_to_reclaim_focus_after_guest_exit(); let sibling = split_host_sibling(&nested); sibling.output(PROMPT); nested.host.wait_until( "the host acts on its own keybindings again after the guest detached", |host_grid| host_grid.contains("Pane #2") && normal_mode_bar_settled(host_grid), ); nested.host.quit(); } #[test] fn opening_the_host_session_manager_with_a_guest_pane_does_not_misbehave() { let mut nested = NestedHarness::start(TERMINAL_SIZE); boot_and_descend_on_first_load(&nested); ascend_via_focus_host_binding(&nested); nested.host.send_stdin(&keys::ctrl('o')); nested.host.wait_until( "the host entered session mode before launching the session manager", session_mode_bar_settled, ); nested.host.send_stdin(&keys::key('w')); nested.host.wait_until( "the host session manager opened over its guest pane", |host_grid| host_grid.contains("Session Manager") && host_grid.status_bar_appears(), ); nested.host.send_stdin(&keys::ctrl('c')); nested.host.wait_until( "the host session manager closed and normal chrome returned", |host_grid| !host_grid.contains("Session Manager") && normal_mode_bar_settled(host_grid), ); nested.guest.wait_until( "the guest is undisturbed by the host session manager", guest_ascended_ui_settled, ); nested.host.send_stdin(&keys::alt('n')); let host_new_pane = nested.host.expect_pty_spawn(); host_new_pane.output(PROMPT); nested.host.wait_until( "the host still acts on its own keybindings after the session manager closed", |host_grid| host_grid.contains("Pane #2") && normal_mode_bar_settled(host_grid), ); nested.guest.quit(); nested.host.quit(); }