#![cfg(unix)] use insta::assert_snapshot; use zellij_integration_tests::{ claim_first_terminal_and_wait_for_prompt, col, host_descended_bar_with_ascend_keys_settled, keys, normalized, split_right_and_wait_for_prompt, start_zellij, GridSnapshot, NestedDepthThreeHarness, NestedHarness, TERMINAL_SIZE, }; use zellij_utils::cli::CliAction; use zellij_utils::nested_session::NestedSessionMessage; const ASCEND_KEY: &[u8] = b"]"; const DESCEND_KEY: &[u8] = b"["; const ARROW_LEFT: &[u8] = b"\x1b[D"; fn sgr_mouse_report(column: usize, line: usize, button: u8) -> Vec { format!("\u{1b}[<{};{};{}M", button, column, line).into_bytes() } fn last_line_contains(grid_snapshot: &GridSnapshot, needle: &str) -> bool { grid_snapshot .lines() .last() .map_or(false, |last_line| last_line.contains(needle)) } fn session_mode_bar_settled(grid_snapshot: &GridSnapshot) -> bool { last_line_contains(grid_snapshot, "SESSION") && last_line_contains(grid_snapshot, "Detach") } fn pane_mode_bar_settled(grid_snapshot: &GridSnapshot) -> bool { last_line_contains(grid_snapshot, "Fullscreen") } 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_fullscreen_breadcrumb_line(guest_grid: &GridSnapshot) -> bool { guest_grid.lines().first().map_or(false, |first_line| { first_line.contains('▸') && first_line.contains('(') }) } fn breadcrumb_for(ancestry: &[&str], name: &str) -> String { format!("({} ▸ {}) ", ancestry.join(" ▸ "), name) } fn ancestor_segment_range(first_line: &str) -> (usize, usize) { let start = first_line.find('(').expect("breadcrumb open paren present"); let after_arrow = first_line[start..] .rfind('▸') .expect("breadcrumb arrow present"); let end = start + after_arrow + '▸'.len_utf8(); ( first_line[..start].chars().count(), first_line[..end].chars().count() + 1, ) } 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 enter_fullscreen_from_descended_guest(nested: &NestedHarness) { 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')); } fn host_own_name_present(host_grid: &GridSnapshot, host_session_name: &str) -> bool { let host_own_slot = format!("({})", host_session_name); host_grid.contains(&host_own_slot) } fn depth_three_descend(nested: &NestedDepthThreeHarness) { nested.boot_and_descend_depth_three(); nested.inner.wait_for_app_load(); nested .inner .wait_until("inner settled in normal mode", guest_ui_settled); } fn enter_fullscreen_from_descended_inner(nested: &NestedDepthThreeHarness) { nested.outer.send_stdin(&keys::ctrl('o')); nested.inner.wait_until( "inner entered session mode before the fullscreen bind", session_mode_bar_settled, ); nested.outer.send_stdin(&keys::key('f')); } #[test] fn guest_fullscreen_fills_the_host_display_and_hides_chrome() { let nested = NestedHarness::start(TERMINAL_SIZE); let host_session_name = nested.host.session_name().to_string(); let guest_session_name = nested.guest.session_name().to_string(); boot_and_descend_on_first_load(&nested); let entered = nested.mark_guest_to_host(); enter_fullscreen_from_descended_guest(&nested); 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 expected_breadcrumb = breadcrumb_for(&[&host_session_name], &guest_session_name); let fullscreen_grid = nested.host.wait_until( "guest content fills the whole host display without host chrome", |host_grid| { host_grid.contains(&expected_breadcrumb) && !host_own_name_present(host_grid, &host_session_name) && host_grid.lines().first().map_or(false, |first_line| { first_line.contains(&expected_breadcrumb) && first_line.contains("Tab #1") }) && host_grid.cursor_is_at(col(0).row(1)) }, ); assert!( fullscreen_grid.row_of_line("Pane #1").is_none(), "no stray host pane-title line while the guest is fullscreened" ); assert_snapshot!( "depth2_guest_fullscreen_fills_display", normalized(&fullscreen_grid) ); } #[test] fn toggle_off_restores_the_host_layout_exactly() { let nested = NestedHarness::start(TERMINAL_SIZE); boot_and_descend_on_first_load(&nested); let host_session_name = nested.host.session_name().to_string(); let guest_session_name = nested.guest.session_name().to_string(); let expected_breadcrumb = breadcrumb_for(&[&host_session_name], &guest_session_name); let before_fullscreen = nested .host .wait_until("host chrome present before fullscreen", |host_grid| { host_grid.status_bar_appears() && host_grid.tab_bar_appears() && host_descended_bar_with_ascend_keys_settled(host_grid) && host_own_name_present(host_grid, &host_session_name) && !guest_fullscreen_breadcrumb_line(host_grid) }); let entered = nested.mark_guest_to_host(); enter_fullscreen_from_descended_guest(&nested); nested .guest_to_host() .wait_for_after(entered, "guest asks host to fullscreen", |message| { matches!( message, NestedSessionMessage::ToggleHostFullscreen { fullscreen: true } ) }); nested .host .wait_until("host chrome hidden while fullscreen", |host_grid| { host_grid.contains(&expected_breadcrumb) && !host_own_name_present(host_grid, &host_session_name) }); 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, "guest asks host to exit fullscreen", |message| { matches!( message, NestedSessionMessage::ToggleHostFullscreen { fullscreen: false } ) }, ); let after_fullscreen = nested.host.wait_until( "host chrome and layout restored after toggle-off", |host_grid| { host_grid.status_bar_appears() && host_grid.tab_bar_appears() && host_descended_bar_with_ascend_keys_settled(host_grid) && host_own_name_present(host_grid, &host_session_name) && !guest_fullscreen_breadcrumb_line(host_grid) }, ); assert_eq!( after_fullscreen.lines().last(), before_fullscreen.lines().last(), "host status bar restored exactly after fullscreen exit" ); assert!( after_fullscreen .lines() .first() .map_or(false, |first_line| !first_line.contains('▸')), "breadcrumb gone from the host tab-bar after fullscreen exit" ); } #[test] fn fullscreen_state_reaches_the_guest_on_enter_and_exit() { let nested = NestedHarness::start(TERMINAL_SIZE); boot_and_descend_on_first_load(&nested); let entered = nested.mark_host_to_guest(); enter_fullscreen_from_descended_guest(&nested); nested.host_to_guest().wait_for_after( entered, "the guest is told it entered host fullscreen", |message| { matches!( message, NestedSessionMessage::FullscreenState { fullscreen: true } ) }, ); nested .guest .wait_until("guest breadcrumb shown while fullscreen", |guest_grid| { guest_fullscreen_breadcrumb_line(guest_grid) }); let exited = nested.mark_host_to_guest(); 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.host_to_guest().wait_for_after( exited, "the guest is told it left host fullscreen", |message| { matches!( message, NestedSessionMessage::FullscreenState { fullscreen: false } ) }, ); nested .guest .wait_until("guest breadcrumb cleared after exit", |guest_grid| { !guest_fullscreen_breadcrumb_line(guest_grid) }); } fn column_of(first_line: &str, needle: &str) -> usize { let byte_index = first_line.find(needle).expect("needle present on line"); first_line[..byte_index].chars().count() } #[test] fn breadcrumb_is_present_only_while_fullscreened() { let nested = NestedHarness::start(TERMINAL_SIZE); let host_session_name = nested.host.session_name().to_string(); let guest_session_name = nested.guest.session_name().to_string(); boot_and_descend_on_first_load(&nested); let before = nested .guest .wait_until("guest tab-bar settled before fullscreen", guest_ui_settled); assert!( !guest_fullscreen_breadcrumb_line(&before) && before .lines() .first() .map_or(true, |first_line| !first_line.contains('▸')), "no breadcrumb on the guest tab-bar before fullscreen" ); enter_fullscreen_from_descended_guest(&nested); let expected_breadcrumb = breadcrumb_for(&[&host_session_name], &guest_session_name); let fullscreened = nested.guest.wait_until( "guest tab-bar shows the breadcrumb while fullscreened", |guest_grid| { guest_grid.lines().first().map_or(false, |first_line| { first_line.contains(&expected_breadcrumb) }) }, ); let first_line = fullscreened.lines().into_iter().next().unwrap(); assert!( first_line.contains(&format!("{})", guest_session_name)), "the guest's own name is present in the breadcrumb" ); let ancestor_column = column_of(&first_line, &host_session_name); assert!( fullscreened.char_is_italic(ancestor_column, 0), "the ancestor segment of the breadcrumb is styled italic" ); let name_column = column_of(&first_line, &guest_session_name); assert!( fullscreened.char_is_bold(name_column, 0), "the guest's own name in the breadcrumb is styled bold" ); assert!( !fullscreened.char_is_dim(ancestor_column, 0), "the ancestor segment is not dimmed" ); assert_snapshot!( "depth2_breadcrumb_tab_bar_line", normalized(&fullscreened).lines().next().unwrap() ); let exited = nested.mark_host_to_guest(); 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.host_to_guest().wait_for_after( exited, "guest told it left host fullscreen", |message| { matches!( message, NestedSessionMessage::FullscreenState { fullscreen: false } ) }, ); nested.guest.wait_until( "guest breadcrumb gone after fullscreen exit", |guest_grid| { guest_grid .lines() .first() .map_or(false, |first_line| !first_line.contains('▸')) }, ); } #[test] fn nested_but_tiled_guest_shows_no_breadcrumb() { let nested = NestedHarness::start(TERMINAL_SIZE); boot_and_descend_on_first_load(&nested); let tiled = nested .guest .wait_until("nested guest tab-bar settled while tiled", guest_ui_settled); assert!( tiled .lines() .first() .map_or(true, |first_line| { !first_line.contains('▸') }), "a nested but not fullscreened guest shows no breadcrumb" ); } #[test] fn standalone_no_ui_fullscreen_shows_no_breadcrumb() { let mut zellij = start_zellij(); claim_first_terminal_and_wait_for_prompt(&zellij); split_right_and_wait_for_prompt(&zellij); zellij.send_stdin(&keys::ctrl('p')); zellij.send_stdin(&keys::key('F')); let fullscreened = zellij.wait_until( "standalone pane covers the display without chrome", |grid| { !grid.tab_bar_appears() && !grid.status_bar_appears() && grid.cursor_is_at(col(2).row(0)) }, ); assert!( !fullscreened.contains("▸"), "a standalone (non-nested) NoUi fullscreen shows no breadcrumb" ); zellij.quit(); } #[test] fn innermost_fullscreen_fills_the_whole_outer_terminal_and_zooms_the_chain() { let nested = NestedDepthThreeHarness::start_depth_three(TERMINAL_SIZE); let outer_session_name = nested.outer.session_name().to_string(); let middle_session_name = nested.middle.session_name().to_string(); let inner_session_name = nested.inner.session_name().to_string(); depth_three_descend(&nested); let bubbled = nested.mark_inner_to_middle(); let outer_told = nested.mark_outer_to_middle(); let middle_told = nested.mark_middle_to_inner(); enter_fullscreen_from_descended_inner(&nested); nested.inner_to_middle_frames().wait_for_after( bubbled, "inner asks the middle host to fullscreen it", |message| { matches!( message, NestedSessionMessage::ToggleHostFullscreen { fullscreen: true } ) }, ); nested.outer_to_middle_frames().wait_for_after( outer_told, "outer tells the middle it is in host fullscreen", |message| { matches!( message, NestedSessionMessage::FullscreenState { fullscreen: true } ) }, ); nested.middle_to_inner_frames().wait_for_after( middle_told, "middle tells the inner it is in host fullscreen", |message| { matches!( message, NestedSessionMessage::FullscreenState { fullscreen: true } ) }, ); let expected_breadcrumb = breadcrumb_for( &[&outer_session_name, &middle_session_name], &inner_session_name, ); let fullscreen_grid = nested.outer.wait_until( "inner content fills the whole outer display with all three ancestry segments", |outer_grid| { outer_grid.contains(&expected_breadcrumb) && !host_own_name_present(outer_grid, &outer_session_name) && !host_own_name_present(outer_grid, &middle_session_name) && outer_grid.lines().first().map_or(false, |first_line| { first_line.contains(&expected_breadcrumb) && first_line.contains("Tab #1") }) && outer_grid.cursor_is_at(col(0).row(1)) }, ); assert_snapshot!( "depth3_inner_fullscreen_breadcrumb", normalized(&fullscreen_grid) ); } #[test] fn zoom_modal_while_outer_is_ascended_still_fullscreens_the_outer() { let nested = NestedDepthThreeHarness::start_depth_three(TERMINAL_SIZE); let outer_session_name = nested.outer.session_name().to_string(); let middle_session_name = nested.middle.session_name().to_string(); let inner_session_name = nested.inner.session_name().to_string(); nested.descend_outer_into_middle_via_modal(); nested.middle.wait_for_app_load(); let ascended = nested.mark_outer_to_middle(); nested.outer.send_stdin(&keys::ctrl('o')); nested.middle.wait_until( "middle entered session mode before the ascend key", session_mode_bar_settled, ); nested.outer.send_stdin(ASCEND_KEY); nested.wait_for_outer_to_ascend_from_middle_after(ascended); nested.outer.wait_until( "outer restored its own chrome after ascending out of the middle", |outer_grid| host_own_name_present(outer_grid, &outer_session_name), ); let outer_told = nested.mark_outer_to_middle(); nested.zoom_middle_into_inner_via_modal(); nested.outer_to_middle_frames().wait_for_after( outer_told, "outer honors the fullscreen chain even though it is ascended out of the middle", |message| { matches!( message, NestedSessionMessage::FullscreenState { fullscreen: true } ) }, ); let expected_breadcrumb = breadcrumb_for( &[&outer_session_name, &middle_session_name], &inner_session_name, ); nested.outer.wait_until( "the ascended outer drops its own chrome and shows the full nested fullscreen breadcrumb", |outer_grid| { outer_grid.contains(&expected_breadcrumb) && !host_own_name_present(outer_grid, &outer_session_name) && !host_own_name_present(outer_grid, &middle_session_name) }, ); } fn ascend_inner_to_middle(nested: &NestedDepthThreeHarness) { let requested = nested.mark_inner_to_middle(); let ascended = nested.mark_middle_to_inner(); nested.outer.send_stdin(&keys::ctrl('o')); nested.inner.wait_until( "inner entered session mode before ascending to the middle", session_mode_bar_settled, ); nested.outer.send_stdin(ASCEND_KEY); nested.wait_for_inner_to_request_host_focus_after(requested); nested.wait_for_middle_to_ascend_from_inner_after(ascended); } fn redescend_middle_into_inner(nested: &NestedDepthThreeHarness) { let descended = nested.mark_middle_to_inner(); nested.outer.send_stdin(&keys::ctrl('p')); nested.middle.wait_until( "middle entered pane mode before re-descending into the inner", |middle_grid| { last_line_contains(middle_grid, "PANE") && last_line_contains(middle_grid, "Move") }, ); nested.outer.send_stdin(ARROW_LEFT); nested.outer.send_stdin(&keys::ctrl('o')); nested.middle.wait_until( "middle entered session mode before the descend key", session_mode_bar_settled, ); nested.outer.send_stdin(DESCEND_KEY); nested.wait_for_middle_to_descend_into_inner_after(descended); } #[test] fn fullscreen_from_inner_while_middle_already_fullscreen_does_not_cancel() { let nested = NestedDepthThreeHarness::start_depth_three(TERMINAL_SIZE); let outer_session_name = nested.outer.session_name().to_string(); let middle_session_name = nested.middle.session_name().to_string(); let inner_session_name = nested.inner.session_name().to_string(); depth_three_descend(&nested); ascend_inner_to_middle(&nested); nested.outer.send_stdin(&keys::ctrl('p')); nested.middle.wait_until( "middle entered pane mode before splitting a sibling", |middle_grid| { last_line_contains(middle_grid, "PANE") && last_line_contains(middle_grid, "Move") }, ); nested.outer.send_stdin(&keys::key('r')); let middle_sibling = nested.middle.expect_pty_spawn(); middle_sibling.output(b"MIDDLE-SIBLING "); nested.middle.wait_until( "middle spawned a sibling pane next to the inner guest", |middle_grid| middle_grid.contains("MIDDLE-SIBLING"), ); let outer_told_true = nested.mark_outer_to_middle(); nested.outer.send_stdin(&keys::ctrl('o')); nested.middle.wait_until( "middle entered session mode before fullscreening itself", session_mode_bar_settled, ); nested.outer.send_stdin(&keys::key('f')); nested.outer_to_middle_frames().wait_for_after( outer_told_true, "outer fullscreens the middle pane and tells the middle it is fullscreen", |message| { matches!( message, NestedSessionMessage::FullscreenState { fullscreen: true } ) }, ); let middle_breadcrumb = breadcrumb_for(&[&outer_session_name], &middle_session_name); nested.outer.wait_until( "outer display fullscreened by the middle before the inner enters fullscreen", |outer_grid| { outer_grid.contains(&middle_breadcrumb) && !host_own_name_present(outer_grid, &outer_session_name) }, ); redescend_middle_into_inner(&nested); let cancels_before = nested.outer_to_middle_frames().count(|message| { matches!( message, NestedSessionMessage::FullscreenState { fullscreen: false } ) }); let bubbled = nested.mark_inner_to_middle(); enter_fullscreen_from_descended_inner(&nested); nested.inner_to_middle_frames().wait_for_after( bubbled, "inner asks the middle to fullscreen it", |message| { matches!( message, NestedSessionMessage::ToggleHostFullscreen { fullscreen: true } ) }, ); let inner_breadcrumb = breadcrumb_for( &[&outer_session_name, &middle_session_name], &inner_session_name, ); let final_grid = nested.outer.wait_until( "the whole chain ends fullscreen with the inner content covering the outer display", |outer_grid| { outer_grid.contains(&inner_breadcrumb) && !host_own_name_present(outer_grid, &outer_session_name) && !host_own_name_present(outer_grid, &middle_session_name) && outer_grid.cursor_is_at(col(0).row(1)) }, ); let cancels_after = nested.outer_to_middle_frames().count(|message| { matches!( message, NestedSessionMessage::FullscreenState { fullscreen: false } ) }); assert_eq!( cancels_before, cancels_after, "entering fullscreen at the inner while the middle is already fullscreen must not emit a spurious exit to the outer" ); assert_eq!( nested.inner_to_middle_frames().count(|message| matches!( message, NestedSessionMessage::ToggleHostFullscreen { fullscreen: false } )), 0, "no ToggleHostFullscreen{{false}} is emitted while entering fullscreen" ); assert!( final_grid.contains(&inner_breadcrumb), "the outer display is fullscreen by the inner content at the end" ); } const FLOATING_MARKER: &str = "HOST-FLOAT"; 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) { 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(b"HOST-SIBLING "); nested.host.wait_until( "host returned to normal mode after the split key", |host_grid| host_grid.contains("HOST-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); } #[test] fn host_floating_layer_is_hidden_during_nested_fullscreen_and_reappears_on_exit() { let nested = NestedHarness::start(TERMINAL_SIZE); boot_and_descend_on_first_load(&nested); ascend_via_focus_host_binding(&nested); split_host_sibling(&nested); let observer = nested.host.attach_client(TERMINAL_SIZE); observer.wait_until( "observer host client attached focused on the sibling host pane", |host_grid| host_grid.contains("HOST-SIBLING") && last_line_contains(host_grid, "LOCK"), ); observer.send_stdin(&keys::ctrl('p')); observer.wait_until( "observer entered pane mode before the floating pane", |host_grid| host_grid.contains("PANE") && host_grid.contains("Move"), ); observer.send_stdin(&keys::key('w')); let floating = nested.host.expect_pty_spawn(); floating.output(FLOATING_MARKER.as_bytes()); observer.wait_until( "the host floating pane spawned and is visible on the observer", |host_grid| host_grid.contains(FLOATING_MARKER), ); observer.send_stdin(&keys::alt('f')); observer.wait_until("observer hid the host floating pane", |host_grid| { !host_grid.contains(FLOATING_MARKER) }); descend_into_guest_on_the_left(&nested); nested .guest .wait_until("guest settled after descend", guest_ui_settled); let entered = nested.mark_guest_to_host(); enter_fullscreen_from_descended_guest(&nested); nested .guest_to_host() .wait_for_after(entered, "guest asks host to fullscreen", |message| { matches!( message, NestedSessionMessage::ToggleHostFullscreen { fullscreen: true } ) }); let host_session_name = nested.host.session_name().to_string(); let guest_session_name = nested.guest.session_name().to_string(); let expected_breadcrumb = breadcrumb_for(&[&host_session_name], &guest_session_name); nested .host .wait_until("guest fullscreened over the host display", |host_grid| { host_grid.contains(&expected_breadcrumb) && !host_own_name_present(host_grid, &host_session_name) }); observer.send_stdin(&keys::alt('f')); observer.wait_until( "the host floating layer stays hidden while the guest covers the display, even though show_panes was set", |host_grid| !host_grid.contains(FLOATING_MARKER) && !host_grid.contains("HOST-SIBLING"), ); 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, "guest asks host to exit fullscreen", |message| { matches!( message, NestedSessionMessage::ToggleHostFullscreen { fullscreen: false } ) }, ); observer.wait_until( "the host floating pane renders again on the observer after fullscreen exit", |host_grid| host_grid.contains(FLOATING_MARKER) && host_grid.status_bar_appears(), ); } #[test] fn restoring_the_innermost_fullscreen_unzooms_the_whole_chain() { let nested = NestedDepthThreeHarness::start_depth_three(TERMINAL_SIZE); let outer_session_name = nested.outer.session_name().to_string(); let middle_session_name = nested.middle.session_name().to_string(); let inner_session_name = nested.inner.session_name().to_string(); depth_three_descend(&nested); enter_fullscreen_from_descended_inner(&nested); let expected_breadcrumb = breadcrumb_for( &[&outer_session_name, &middle_session_name], &inner_session_name, ); nested.outer.wait_until( "outer display fullscreened by the inner before restore", |outer_grid| { outer_grid.contains(&expected_breadcrumb) && !host_own_name_present(outer_grid, &outer_session_name) }, ); let bubbled = nested.mark_inner_to_middle(); let outer_told = nested.mark_outer_to_middle(); let middle_told = nested.mark_middle_to_inner(); nested.outer.send_stdin(&keys::ctrl('o')); nested.inner.wait_until( "inner entered session mode before the toggle-off bind", session_mode_bar_settled, ); nested.outer.send_stdin(&keys::key('f')); nested.inner_to_middle_frames().wait_for_after( bubbled, "inner asks the middle host to exit fullscreen", |message| { matches!( message, NestedSessionMessage::ToggleHostFullscreen { fullscreen: false } ) }, ); nested.outer_to_middle_frames().wait_for_after( outer_told, "outer tells the middle it left host fullscreen", |message| { matches!( message, NestedSessionMessage::FullscreenState { fullscreen: false } ) }, ); nested.middle_to_inner_frames().wait_for_after( middle_told, "middle tells the inner it left host fullscreen", |message| { matches!( message, NestedSessionMessage::FullscreenState { fullscreen: false } ) }, ); nested.outer.wait_until( "outer chrome restored and breadcrumb gone across the whole chain", |outer_grid| { outer_grid.status_bar_appears() && host_own_name_present(outer_grid, &outer_session_name) && outer_grid .lines() .first() .map_or(false, |first_line| !first_line.contains('▸')) }, ); nested.middle.wait_until( "middle chrome restored after chain un-zoom", |middle_grid| { middle_grid.status_bar_appears() && middle_grid .lines() .first() .map_or(false, |first_line| !first_line.contains('▸')) }, ); } #[test] fn clicking_the_ancestor_breadcrumb_segment_ascends_out_of_the_guest() { let nested = NestedHarness::start_with_host_and_guest_config( TERMINAL_SIZE, "mouse_mode true", "mouse_mode true", ); let host_session_name = nested.host.session_name().to_string(); let guest_session_name = nested.guest.session_name().to_string(); boot_and_descend_on_first_load(&nested); enter_fullscreen_from_descended_guest(&nested); let expected_breadcrumb = breadcrumb_for(&[&host_session_name], &guest_session_name); let fullscreen_grid = nested.host.wait_until( "guest fullscreened with the breadcrumb visible before clicking", |host_grid| { host_grid.lines().first().map_or(false, |first_line| { first_line.contains(&expected_breadcrumb) }) }, ); let first_line = fullscreen_grid.lines().into_iter().next().unwrap(); let (ancestor_start, ancestor_end) = ancestor_segment_range(&first_line); let non_ascend_before = nested .guest_to_host() .count(|message| matches!(message, NestedSessionMessage::FocusHost { direction: None })); let name_column = column_of(&first_line, &guest_session_name) + 1; nested.host.send_stdin(&sgr_mouse_report(name_column, 1, 0)); nested .host .send_stdin(&format!("\u{1b}[<0;{};1m", name_column).into_bytes()); nested.guest.wait_until( "guest still fullscreened after a non-ancestor click", |guest_grid| guest_ui_settled(guest_grid), ); assert_eq!( nested.guest_to_host().count(|message| matches!( message, NestedSessionMessage::FocusHost { direction: None } )), non_ascend_before, "clicking the own-name part of the breadcrumb must not ascend" ); let requested = nested.mark_guest_to_host(); let ascended = nested.mark_host_to_guest(); let ancestor_click_column = (ancestor_start + ancestor_end) / 2 + 1; nested .host .send_stdin(&sgr_mouse_report(ancestor_click_column, 1, 0)); nested .host .send_stdin(&format!("\u{1b}[<0;{};1m", ancestor_click_column).into_bytes()); nested.guest_to_host().wait_for_after( requested, "clicking the ancestor breadcrumb segment asks the host to take focus back", |message| matches!(message, NestedSessionMessage::FocusHost { direction: None }), ); nested.wait_for_host_to_ascend_from_guest_after(ascended); nested.host.wait_until( "host regained control and restored its chrome after the breadcrumb click", |host_grid| { host_grid.status_bar_appears() && host_own_name_present(host_grid, &host_session_name) }, ); nested.host.send_stdin(&keys::alt('n')); let host_new_pane = nested.host.expect_pty_spawn(); host_new_pane.output(b"$ "); nested.host.wait_until( "host acts on its own key after the breadcrumb click ascended it", |host_grid| host_grid.contains("Pane #2"), ); } #[test] fn host_manually_breaking_fullscreen_drifts_the_guest_to_not_fullscreen() { let nested = NestedHarness::start(TERMINAL_SIZE); let host_session_name = nested.host.session_name().to_string(); let guest_session_name = nested.guest.session_name().to_string(); boot_and_descend_on_first_load(&nested); let entered = nested.mark_host_to_guest(); enter_fullscreen_from_descended_guest(&nested); nested.host_to_guest().wait_for_after( entered, "the guest is told it entered host fullscreen", |message| { matches!( message, NestedSessionMessage::FullscreenState { fullscreen: true } ) }, ); let expected_breadcrumb = breadcrumb_for(&[&host_session_name], &guest_session_name); nested .host .wait_until("guest fullscreened over the host display", |host_grid| { host_grid.contains(&expected_breadcrumb) && !host_own_name_present(host_grid, &host_session_name) }); let drifted = nested.mark_host_to_guest(); let exit_code = nested .host .run_cli_action(CliAction::ToggleNoUiFullscreen { pane_id: None }); assert_eq!( exit_code, 0, "host toggled its own no-ui fullscreen off cleanly" ); nested.host_to_guest().wait_for_after( drifted, "the host's drift detection tells the guest it is no longer fullscreen", |message| { matches!( message, NestedSessionMessage::FullscreenState { fullscreen: false } ) }, ); nested.guest.wait_until( "guest breadcrumb clears after the host manually broke the fullscreen", |guest_grid| { guest_grid .lines() .first() .map_or(false, |first_line| !first_line.contains('▸')) }, ); nested.host.wait_until( "host chrome restored after manually breaking the fullscreen", |host_grid| { host_grid.status_bar_appears() && host_own_name_present(host_grid, &host_session_name) }, ); } #[test] fn guest_killed_while_fullscreened_unzooms_the_host_after_liveness_timeout() { let nested = NestedHarness::start(TERMINAL_SIZE); let host_session_name = nested.host.session_name().to_string(); let guest_session_name = nested.guest.session_name().to_string(); boot_and_descend_on_first_load(&nested); let entered = nested.mark_host_to_guest(); enter_fullscreen_from_descended_guest(&nested); nested.host_to_guest().wait_for_after( entered, "the guest is told it entered host fullscreen", |message| { matches!( message, NestedSessionMessage::FullscreenState { fullscreen: true } ) }, ); let expected_breadcrumb = breadcrumb_for(&[&host_session_name], &guest_session_name); nested.host.wait_until( "guest fullscreen composite fully settled before freezing the guest", |host_grid| { host_grid.contains(&expected_breadcrumb) && !host_own_name_present(host_grid, &host_session_name) && host_grid.cursor_is_at(col(0).row(1)) }, ); nested.freeze_guest(); nested.host.wait_until( "host un-zooms and restores its chrome after the frozen guest times out", |host_grid| { host_grid.status_bar_appears() && host_own_name_present(host_grid, &host_session_name) && host_grid .lines() .first() .map_or(false, |first_line| !first_line.contains('▸')) }, ); } #[test] fn fullscreen_breadcrumb_has_no_nested_marker_and_an_emphasized_own_name() { let nested = NestedHarness::start(TERMINAL_SIZE); let host_session_name = nested.host.session_name().to_string(); let guest_session_name = nested.guest.session_name().to_string(); boot_and_descend_on_first_load(&nested); enter_fullscreen_from_descended_guest(&nested); let expected_breadcrumb = breadcrumb_for(&[&host_session_name], &guest_session_name); let fullscreened = nested.guest.wait_until( "guest tab-bar shows the breadcrumb while fullscreened", |guest_grid| { guest_grid.lines().first().map_or(false, |first_line| { first_line.contains(&expected_breadcrumb) }) }, ); let first_line = fullscreened.lines().into_iter().next().unwrap(); assert!( !first_line.contains("[NESTED]"), "the breadcrumb no longer contains the [NESTED] marker" ); assert!( first_line.contains(&format!("{})", guest_session_name)) && first_line.contains('▸'), "the breadcrumb shows the guest's own name closed by a paren after the arrow" ); let ancestor_column = column_of(&first_line, &host_session_name); assert!( fullscreened.char_is_italic(ancestor_column, 0), "the ancestor breadcrumb segment is italic while descended" ); assert!( !fullscreened.char_is_dim(ancestor_column, 0), "the descended guest breadcrumb ancestor segment is not dimmed" ); let name_column = column_of(&first_line, &guest_session_name); assert!( fullscreened.char_is_bold(name_column, 0), "the guest's own name in the breadcrumb is bold while descended" ); assert!( !fullscreened.char_is_dim(name_column, 0), "the descended guest breadcrumb own name is not dimmed" ); }