diff options
| author | Aram Drevekenin <aram@poor.dev> | 2026-07-27 18:24:37 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-07-27 18:24:37 +0200 |
| commit | ea07e2d5b6bbc9ea6f9c765b0838b3e91156a58d (patch) | |
| tree | 4a8fac8a9b27f2a8b32b3af61a3b5419b9937c81 /zellij-server/src/panes/unit | |
| parent | Switch from the unmaintained daemonize crate to a maintained fork (#4512) (diff) | |
| download | sl0ppij-ea07e2d5b6bbc9ea6f9c765b0838b3e91156a58d.tar.gz sl0ppij-ea07e2d5b6bbc9ea6f9c765b0838b3e91156a58d.tar.xz sl0ppij-ea07e2d5b6bbc9ea6f9c765b0838b3e91156a58d.zip | |
Nested session handling (#5417)
* fix: theme query and updated pixel dimensions
* add no-ui-fullscreen key/action/capability
* protocol core
* integration test harness
* fix integration tests
* fix flaky tests
* nested ui (dimming) and directional focus initial implementation
* integration tests for nested ui dimming and directional focus
* initial full screen nesteed implementation
* fix: keep hidden floating panes hidden across nested no-ui fullscreen
* fix integration tests
* no ui fullscreen integration tests
* hardening behavior/tests
* fix(nested): re-derive descend/dim on host tab switch and new-tab so clicking dimmed chrome ascends
* initial modal implementation
* initial ui improvements
* fix integration tests
* improve nested selection ui
* nested session modal refactoring and tests
* improve integration test speed somewhat
* some ui adjustments
* simplify full/auto nested session handling
* hints in compact-bar
* guest stdin leak fix
* allow floating panes to be fullscreen
* make nested session handling configurable
* protocol reannounce
* refactor: naming confusion
* improve ux somewhat
* rustfmt
* merge fixes
* improve integration test rate limiting (fix ci)
* add pr
Diffstat (limited to 'zellij-server/src/panes/unit')
| -rw-r--r-- | zellij-server/src/panes/unit/grid_tests.rs | 182 | ||||
| -rw-r--r-- | zellij-server/src/panes/unit/nested_session_modal_tests.rs | 360 | ||||
| -rw-r--r-- | zellij-server/src/panes/unit/terminal_pane_tests.rs | 147 |
3 files changed, 689 insertions, 0 deletions
diff --git a/zellij-server/src/panes/unit/grid_tests.rs b/zellij-server/src/panes/unit/grid_tests.rs index e21ea867..2029a4b4 100644 --- a/zellij-server/src/panes/unit/grid_tests.rs +++ b/zellij-server/src/panes/unit/grid_tests.rs @@ -6289,3 +6289,185 @@ fn csi_5n_status_query_still_handled_locally() { "DSR 5 must still produce its local 'all good' reply" ); } + +fn new_grid_for_nested_frames() -> Grid { + let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default())); + let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new())); + Grid::new( + 41, + 110, + Rc::new(RefCell::new(Palette::default())), + terminal_emulator_color_codes, + Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), + sixel_image_store, + Style::default(), + false, + true, + true, + true, + false, + ) +} + +fn nested_announce_message() -> zellij_utils::nested_session::NestedSessionMessage { + zellij_utils::nested_session::NestedSessionMessage::Announce { + session_name: "guest-session".to_owned(), + capabilities: vec![zellij_utils::nested_session::NestedSessionCapability::NestedControl], + } +} + +#[test] +fn nested_session_frame_is_staged_for_dispatch() { + let mut vte_parser = vte::Parser::new(); + let mut grid = new_grid_for_nested_frames(); + let message = nested_announce_message(); + for byte in zellij_utils::nested_session::encode_frame(&message) { + vte_parser.advance(&mut grid, byte); + } + assert_eq!(grid.pending_nested_session_messages, vec![message]); +} + +#[test] +fn nested_session_frame_split_across_feeds_is_staged() { + let mut vte_parser = vte::Parser::new(); + let mut grid = new_grid_for_nested_frames(); + let message = nested_announce_message(); + let frame = zellij_utils::nested_session::encode_frame(&message); + let (first_half, second_half) = frame.split_at(frame.len() / 2); + for byte in first_half { + vte_parser.advance(&mut grid, *byte); + } + assert!(grid.pending_nested_session_messages.is_empty()); + for byte in second_half { + vte_parser.advance(&mut grid, *byte); + } + assert_eq!(grid.pending_nested_session_messages, vec![message]); +} + +#[test] +fn dcs_with_wrong_magic_param_is_not_staged() { + let mut vte_parser = vte::Parser::new(); + let mut grid = new_grid_for_nested_frames(); + for byte in b"\x1bP26662nAAAA\x1b\\" { + vte_parser.advance(&mut grid, *byte); + } + assert!(grid.pending_nested_session_messages.is_empty()); +} + +#[test] +fn dcs_with_intermediates_is_not_staged() { + let mut vte_parser = vte::Parser::new(); + let mut grid = new_grid_for_nested_frames(); + for byte in b"\x1bP26661$nAAAA\x1b\\" { + vte_parser.advance(&mut grid, *byte); + } + assert!(grid.pending_nested_session_messages.is_empty()); +} + +#[test] +fn dcs_n_without_params_is_not_staged() { + let mut vte_parser = vte::Parser::new(); + let mut grid = new_grid_for_nested_frames(); + for byte in b"\x1bPnAAAA\x1b\\" { + vte_parser.advance(&mut grid, *byte); + } + assert!(grid.pending_nested_session_messages.is_empty()); +} + +#[test] +fn nested_session_frame_with_garbage_payload_is_dropped() { + let mut vte_parser = vte::Parser::new(); + let mut grid = new_grid_for_nested_frames(); + for byte in b"\x1bP26661n!!!not-base64!!!\x1b\\" { + vte_parser.advance(&mut grid, *byte); + } + assert!(grid.pending_nested_session_messages.is_empty()); +} + +fn serialize_text_bytes(text: &str) -> String { + text.as_bytes() + .iter() + .map(|byte| byte.to_string()) + .collect::<Vec<_>>() + .join(",") +} + +fn render_ui_component(component_name: &str, serialized_params: &str) -> Grid { + let mut vte_parser = vte::Parser::new(); + let mut grid = new_grid_for_nested_frames(); + let dcs = format!("\u{1b}Pz{};{}\u{1b}\\", component_name, serialized_params); + for byte in dcs.bytes() { + vte_parser.advance(&mut grid, byte); + } + grid +} + +fn styled_cells_of( + grid: &Grid, + needle: char, +) -> Vec<crate::panes::terminal_character::CharacterStyles> { + let mut styles = vec![]; + for line in grid.as_character_lines() { + for terminal_character in line { + if terminal_character.character == needle { + styles.push(*terminal_character.styles); + } + } + } + styles +} + +#[test] +fn disabled_ribbon_renders_italic_non_bold_unselected() { + use crate::panes::terminal_character::AnsiCode; + let grid = render_ui_component("ribbon", &format!("d{}", serialize_text_bytes("RES"))); + let letter_styles = styled_cells_of(&grid, 'R'); + assert!(!letter_styles.is_empty()); + for style in letter_styles { + assert_eq!(style.italic, Some(AnsiCode::On)); + assert_ne!(style.bold, Some(AnsiCode::On)); + } +} + +#[test] +fn disabled_text_with_opaque_flag_keeps_a_background() { + use crate::panes::terminal_character::AnsiCode; + let grid = render_ui_component("text", &format!("zd{}", serialize_text_bytes("RES"))); + let letter_styles = styled_cells_of(&grid, 'R'); + assert!(!letter_styles.is_empty()); + for style in letter_styles { + assert_eq!(style.italic, Some(AnsiCode::On)); + assert_ne!(style.bold, Some(AnsiCode::On)); + assert!(style.background.is_some()); + } +} + +#[test] +fn disabled_text_renders_italic_non_bold() { + use crate::panes::terminal_character::AnsiCode; + let grid = render_ui_component("text", &format!("d{}", serialize_text_bytes("RES"))); + let letter_styles = styled_cells_of(&grid, 'R'); + assert!(!letter_styles.is_empty()); + for style in letter_styles { + assert_eq!(style.italic, Some(AnsiCode::On)); + assert_ne!(style.bold, Some(AnsiCode::On)); + } +} + +#[test] +fn ui_component_flag_prefixes_parse_order_independently() { + let baseline = render_ui_component("text", &format!("xzd{}", serialize_text_bytes("RES"))); + let baseline_styles = styled_cells_of(&baseline, 'R'); + for prefix in ["xdz", "zxd", "zdx", "dxz", "dzx"] { + let grid = render_ui_component( + "text", + &format!("{}{}", prefix, serialize_text_bytes("RES")), + ); + assert_eq!( + styled_cells_of(&grid, 'R'), + baseline_styles, + "prefix {prefix} should parse the same flags as xzd" + ); + } +} diff --git a/zellij-server/src/panes/unit/nested_session_modal_tests.rs b/zellij-server/src/panes/unit/nested_session_modal_tests.rs new file mode 100644 index 00000000..4d0a14ee --- /dev/null +++ b/zellij-server/src/panes/unit/nested_session_modal_tests.rs @@ -0,0 +1,360 @@ +use super::*; +use zellij_utils::data::{PaletteColor, Style}; + +fn style_with_distinct_selected_background() -> Style { + let mut style = Style::default(); + style.colors.text_selected.background = PaletteColor::EightBit(200); + style.colors.text_selected.base = PaletteColor::EightBit(201); + style.colors.text_selected.emphasis_1 = PaletteColor::EightBit(202); + style.colors.text_selected.emphasis_3 = PaletteColor::EightBit(203); + style +} + +fn sample_shortcuts() -> GuestModalShortcuts { + GuestModalShortcuts { + zoom: vec!["Ctrl g".to_string(), "o".to_string(), "f".to_string()], + ascend: vec!["Ctrl o".to_string()], + descend: vec!["Ctrl o".to_string(), "d".to_string()], + } +} + +fn render_lines(chunks: &[CharacterChunk]) -> Vec<String> { + let mut lines: Vec<String> = Vec::new(); + for chunk in chunks { + let text: String = chunk + .terminal_characters + .iter() + .map(|c| c.character) + .collect(); + lines.push(text); + } + lines +} + +fn rendered_text(chunks: &[CharacterChunk]) -> String { + chunks + .iter() + .flat_map(|chunk| chunk.terminal_characters.iter().map(|c| c.character)) + .collect() +} + +#[test] +fn renders_all_expected_content() { + let style = Style::default(); + let shortcuts = sample_shortcuts(); + let chunks = guest_modal_chunks(80, 30, 0, 0, &style, "my-session", 1, &shortcuts); + assert_eq!(chunks.len(), 30); + let rendered = rendered_text(&chunks); + assert!(rendered.contains("Nested Zellij session detected: my-session")); + assert!(rendered.contains("What would you like to do?")); + assert!(rendered.contains("Zoom in and control this session")); + assert!(rendered.contains("Control this session on focus")); + assert!(rendered.contains("<↓↑> select")); + assert!(rendered.contains("<Enter> confirm")); + assert!(rendered.contains("<Esc> dismiss")); +} + +#[test] +fn renders_resolved_keybindings_as_joined_tokens() { + let style = Style::default(); + let shortcuts = sample_shortcuts(); + let chunks = guest_modal_chunks(80, 30, 0, 0, &style, "s", 0, &shortcuts); + let rendered = rendered_text(&chunks); + assert!(rendered.contains("<Ctrl g> + <o> + <f>")); + assert!(rendered.contains("<Ctrl o>")); +} + +#[test] +fn renders_unbound_when_no_keys() { + let style = Style::default(); + let shortcuts = GuestModalShortcuts::default(); + let chunks = guest_modal_chunks(80, 30, 0, 0, &style, "s", 0, &shortcuts); + let rendered = rendered_text(&chunks); + assert!(rendered.contains("<unbound>")); +} + +#[test] +fn selection_marker_moves_with_selected_index() { + let style = Style::default(); + let shortcuts = sample_shortcuts(); + + let first = render_lines(&guest_modal_chunks( + 80, 30, 0, 0, &style, "s", 0, &shortcuts, + )); + assert!(first + .iter() + .any(|line| line.trim_start().starts_with("> 1."))); + assert!(first.iter().any(|line| line.trim_start().starts_with("2."))); + assert!(!first + .iter() + .any(|line| line.trim_start().starts_with("> 2."))); + + let second = render_lines(&guest_modal_chunks( + 80, 30, 0, 0, &style, "s", 1, &shortcuts, + )); + assert!(second + .iter() + .any(|line| line.trim_start().starts_with("> 2."))); + assert!(!second + .iter() + .any(|line| line.trim_start().starts_with("> 1."))); +} + +fn number_column(line: &str, number: &str) -> usize { + line.find(number).expect("option number present") +} + +#[test] +fn options_are_left_justified_within_the_centered_block() { + let style = Style::default(); + let shortcuts = sample_shortcuts(); + let chunks = guest_modal_chunks(80, 30, 0, 0, &style, "s", 0, &shortcuts); + let lines = render_lines(&chunks); + let selected_line = lines + .iter() + .find(|line| line.trim_start().starts_with("> 1.")) + .expect("selected option line present"); + let selected_number_column = number_column(selected_line, "1."); + assert!( + selected_number_column > 2, + "block must be centered with a left pad before the marker" + ); + + for (number, needle) in [("2.", "2.")] { + let line = lines + .iter() + .find(|line| line.trim_start().starts_with(needle)) + .expect("unselected option line present"); + assert_eq!( + number_column(line, number), + selected_number_column, + "the option number column is stable across selection" + ); + } +} + +#[test] +fn small_columns_render_the_fallback() { + let style = Style::default(); + let shortcuts = GuestModalShortcuts::default(); + let chunks = guest_modal_chunks(18, 20, 0, 0, &style, "s", 0, &shortcuts); + assert_eq!(chunks.len(), 20); + let rendered = rendered_text(&chunks); + assert!(rendered.contains("Nested")); + assert!(!rendered.contains("What would you like to do?")); +} + +#[test] +fn small_rows_render_the_fallback() { + let style = Style::default(); + let shortcuts = sample_shortcuts(); + let chunks = guest_modal_chunks(80, 5, 0, 0, &style, "s", 0, &shortcuts); + assert_eq!(chunks.len(), 5); + let rendered = rendered_text(&chunks); + assert!(rendered.contains("Nested Zellij session")); + assert!(!rendered.contains("What would you like to do?")); +} + +#[test] +fn tiny_geometry_does_not_panic() { + let style = Style::default(); + let shortcuts = GuestModalShortcuts::default(); + let chunks = guest_modal_chunks(3, 2, 0, 0, &style, "session", 0, &shortcuts); + assert_eq!(chunks.len(), 2); + let empty = guest_modal_chunks(0, 0, 0, 0, &style, "session", 0, &shortcuts); + assert!(empty.is_empty()); +} + +#[test] +fn every_rendered_row_spans_the_full_width() { + let style = Style::default(); + let shortcuts = sample_shortcuts(); + let columns = 80; + let chunks = guest_modal_chunks(columns, 30, 0, 0, &style, "s", 0, &shortcuts); + for chunk in &chunks { + let width: usize = chunk + .terminal_characters + .iter() + .map(|c| c.character.width().unwrap_or(0).max(1)) + .sum(); + assert_eq!(width, columns); + } +} + +#[test] +fn hit_test_round_trips_the_options() { + let style = Style::default(); + let shortcuts = sample_shortcuts(); + let rows = 40; + let columns = 80; + let mut hit_rows: Vec<usize> = Vec::new(); + for row in 0..rows { + if let Some(option) = + guest_modal_option_at_content_row(rows, columns, row, &style, "session", 0, &shortcuts) + { + assert_eq!(hit_rows.len(), option); + hit_rows.push(row); + } + } + assert_eq!(hit_rows.len(), 2); +} + +#[test] +fn hit_test_returns_none_off_option_rows_and_in_fallback() { + let style = Style::default(); + let shortcuts = sample_shortcuts(); + assert_eq!( + guest_modal_option_at_content_row(40, 10, 0, &style, "session", 0, &shortcuts), + None + ); + assert_eq!( + guest_modal_option_at_content_row(4, 10, 2, &style, "session", 0, &shortcuts), + None + ); + assert_eq!( + guest_modal_option_at_content_row(40, 80, 0, &style, "session", 0, &shortcuts), + None + ); +} + +#[test] +fn hit_test_matches_rendered_marker_rows() { + let style = Style::default(); + let shortcuts = sample_shortcuts(); + let rows = 40; + let columns = 80; + let selected = 1; + let lines = render_lines(&guest_modal_chunks( + columns, rows, 0, 0, &style, "session", selected, &shortcuts, + )); + for (row, line) in lines.iter().enumerate() { + let hit = guest_modal_option_at_content_row( + rows, columns, row, &style, "session", selected, &shortcuts, + ); + let trimmed = line.trim_start(); + let is_option_line = + trimmed.starts_with("> ") || trimmed.starts_with("1.") || trimmed.starts_with("2."); + assert_eq!(hit.is_some(), is_option_line, "row {} mismatch", row); + } +} + +#[test] +fn selected_rows_carry_the_selected_background_rectangle() { + let style = style_with_distinct_selected_background(); + let shortcuts = sample_shortcuts(); + let selected = 0; + let columns = 80; + let rows = 40; + let styles = guest_modal_styles(&style); + let layout = guest_modal_layout(columns, rows, &styles, "session", selected, &shortcuts); + let selected_option_offset = layout.option_rows[selected]; + + let chunks = guest_modal_chunks(columns, rows, 0, 0, &style, "session", selected, &shortcuts); + let selected_row_index = layout.start_row + selected_option_offset; + let selected_chunk = &chunks[selected_row_index]; + + let selected_background = styles.selected_fill.background; + let base_background = styles.fill.background; + assert_ne!( + selected_background, base_background, + "the test style must give the selected background a distinct value" + ); + + let block_start = layout.left_pad; + let block_end = layout.left_pad + layout.block_width; + let mut block_cells = 0; + for (column, character) in selected_chunk.terminal_characters.iter().enumerate() { + if column >= block_start && column < block_end { + assert_eq!( + character.styles.background, selected_background, + "block cell {} on the selected row must carry the selected background", + column + ); + block_cells += 1; + } else { + assert_eq!( + character.styles.background, base_background, + "outer padding cell {} must keep the base background", + column + ); + } + } + assert_eq!( + block_cells, layout.block_width, + "the selected rectangle spans the whole block width" + ); +} + +#[test] +fn unselected_rows_do_not_use_the_selected_background() { + let style = style_with_distinct_selected_background(); + let shortcuts = sample_shortcuts(); + let selected = 0; + let columns = 80; + let rows = 40; + let styles = guest_modal_styles(&style); + let layout = guest_modal_layout(columns, rows, &styles, "session", selected, &shortcuts); + let other_option_offset = layout.option_rows[1]; + + let selected_background = styles.selected_fill.background; + let chunks = guest_modal_chunks(columns, rows, 0, 0, &style, "session", selected, &shortcuts); + let other_row = &chunks[layout.start_row + other_option_offset]; + for character in &other_row.terminal_characters { + assert_ne!( + character.styles.background, selected_background, + "unselected option row must not carry the selected background" + ); + } +} + +#[test] +fn keycode_words_join_with_plus_separators() { + let style = Style::default(); + let styles = guest_modal_styles(&style); + let keys = vec!["Ctrl g".to_string(), "o".to_string()]; + let words = guest_modal_keycode_words(&keys, styles.fill.clone(), styles.keycode.clone()); + let joined: String = words.iter().map(|(text, _)| text.as_str()).collect(); + assert_eq!(joined, "<Ctrl g>+<o>"); + assert_eq!(words[0].1, styles.keycode); + assert_eq!(words[1].1, styles.fill); + assert_eq!(words[2].1, styles.keycode); +} + +#[test] +fn keycode_words_render_unbound_when_empty() { + let style = Style::default(); + let styles = guest_modal_styles(&style); + let words = guest_modal_keycode_words(&[], styles.fill.clone(), styles.keycode.clone()); + assert_eq!(words.len(), 1); + assert_eq!(words[0].0, "<unbound>"); + assert_eq!(words[0].1, styles.fill); +} + +#[test] +fn wrap_words_respects_width_and_indent() { + let style = Style::default(); + let styles = guest_modal_styles(&style); + let words = guest_modal_text_words("one two three four five", styles.fill.clone()); + let indent = 4; + let width = 14; + let lines = guest_modal_wrap_words(&words, indent, styles.fill.clone(), width); + assert!(lines.len() > 1, "text wider than the width must wrap"); + for line in &lines { + let indent_segment = &line[0]; + assert_eq!(indent_segment.0, " ".repeat(indent)); + let rendered_width: usize = line.iter().map(|(text, _)| text.width()).sum(); + assert!( + rendered_width <= width, + "wrapped line width {} exceeds {}", + rendered_width, + width + ); + } +} + +#[test] +fn center_pad_centers_and_clamps() { + assert_eq!(center_modal_pad(10, 30), 10); + assert_eq!(center_modal_pad(30, 30), 0); + assert_eq!(center_modal_pad(40, 30), 0); +} diff --git a/zellij-server/src/panes/unit/terminal_pane_tests.rs b/zellij-server/src/panes/unit/terminal_pane_tests.rs index 2e34b273..a6234844 100644 --- a/zellij-server/src/panes/unit/terminal_pane_tests.rs +++ b/zellij-server/src/panes/unit/terminal_pane_tests.rs @@ -873,3 +873,150 @@ pub fn frameless_pane_position_is_on_frame() { assert!(!terminal_pane.position_is_on_frame(&Position::new(30, 130))); assert!(!terminal_pane.position_is_on_frame(&Position::new(30, 131))); } + +fn create_guest_modal_pane() -> TerminalPane { + let mut fake_win_size = PaneGeom::default(); + fake_win_size.cols.set_inner(80); + fake_win_size.rows.set_inner(24); + let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default())); + let terminal_emulator_colors = Rc::new(RefCell::new(Palette::default())); + let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new())); + TerminalPane::new( + 1, + fake_win_size, + Style::default(), + 0, + String::new(), + Rc::new(RefCell::new(LinkHandler::new())), + Rc::new(RefCell::new(None)), + sixel_image_store, + terminal_emulator_colors, + terminal_emulator_color_codes, + None, + None, + false, + true, + true, + true, + false, + None, + ) +} + +fn press_key( + pane: &mut TerminalPane, + bare_key: zellij_utils::data::BareKey, + client_id: u16, +) -> Option<crate::tab::AdjustedInput> { + let key = zellij_utils::data::KeyWithModifier::new(bare_key); + pane.adjust_input_to_terminal(&Some(key), vec![], false, Some(client_id)) +} + +#[test] +pub fn guest_modal_navigation_wraps() { + use zellij_utils::data::BareKey; + let mut pane = create_guest_modal_pane(); + let client_id = 1; + pane.set_guest_modal(&[client_id]); + assert_eq!(pane.guest_modal_selection(client_id), Some(0)); + press_key(&mut pane, BareKey::Up, client_id); + assert_eq!(pane.guest_modal_selection(client_id), Some(1)); + press_key(&mut pane, BareKey::Down, client_id); + assert_eq!(pane.guest_modal_selection(client_id), Some(0)); + press_key(&mut pane, BareKey::Down, client_id); + assert_eq!(pane.guest_modal_selection(client_id), Some(1)); + press_key(&mut pane, BareKey::Up, client_id); + assert_eq!(pane.guest_modal_selection(client_id), Some(0)); +} + +#[test] +pub fn guest_modal_tab_is_ignored() { + use zellij_utils::data::{BareKey, KeyWithModifier}; + let mut pane = create_guest_modal_pane(); + let client_id = 1; + pane.set_guest_modal(&[client_id]); + press_key(&mut pane, BareKey::Tab, client_id); + assert_eq!(pane.guest_modal_selection(client_id), Some(0)); + let key = KeyWithModifier::new(BareKey::Tab).with_shift_modifier(); + pane.adjust_input_to_terminal(&Some(key), vec![], false, Some(client_id)); + assert_eq!(pane.guest_modal_selection(client_id), Some(0)); +} + +#[test] +pub fn guest_modal_enter_confirms_selection() { + use crate::tab::AdjustedInput; + use zellij_utils::data::BareKey; + let mut pane = create_guest_modal_pane(); + let client_id = 1; + + pane.set_guest_modal(&[client_id]); + let outcome = press_key(&mut pane, BareKey::Enter, client_id); + assert!(matches!(outcome, Some(AdjustedInput::GuestModalZoom))); + + pane.set_guest_modal(&[client_id]); + press_key(&mut pane, BareKey::Down, client_id); + let outcome = press_key(&mut pane, BareKey::Enter, client_id); + assert!(matches!(outcome, Some(AdjustedInput::GuestModalDescend))); +} + +#[test] +pub fn guest_modal_digit_shortcuts() { + use crate::tab::AdjustedInput; + use zellij_utils::data::BareKey; + let mut pane = create_guest_modal_pane(); + let client_id = 1; + + pane.set_guest_modal(&[client_id]); + let outcome = press_key(&mut pane, BareKey::Char('1'), client_id); + assert!(matches!(outcome, Some(AdjustedInput::GuestModalZoom))); + + pane.set_guest_modal(&[client_id]); + let outcome = press_key(&mut pane, BareKey::Char('2'), client_id); + assert!(matches!(outcome, Some(AdjustedInput::GuestModalDescend))); +} + +#[test] +pub fn guest_modal_esc_descends() { + use crate::tab::AdjustedInput; + use zellij_utils::data::BareKey; + let mut pane = create_guest_modal_pane(); + let client_id = 1; + pane.set_guest_modal(&[client_id]); + let outcome = press_key(&mut pane, BareKey::Esc, client_id); + assert!(matches!(outcome, Some(AdjustedInput::GuestModalDescend))); +} + +#[test] +pub fn guest_modal_swallows_other_input() { + use zellij_utils::data::BareKey; + let mut pane = create_guest_modal_pane(); + let client_id = 1; + pane.set_guest_modal(&[client_id]); + let outcome = press_key(&mut pane, BareKey::Char('x'), client_id); + assert!(outcome.is_none()); + assert_eq!(pane.guest_modal_selection(client_id), Some(0)); +} + +#[test] +pub fn guest_modal_is_per_client() { + use zellij_utils::data::BareKey; + let mut pane = create_guest_modal_pane(); + let client_a = 1; + let client_b = 2; + pane.set_guest_modal(&[client_a, client_b]); + press_key(&mut pane, BareKey::Down, client_a); + assert_eq!(pane.guest_modal_selection(client_a), Some(1)); + assert_eq!(pane.guest_modal_selection(client_b), Some(0)); +} + +#[test] +pub fn guest_modal_no_modal_passes_input_through() { + use zellij_utils::data::BareKey; + let mut pane = create_guest_modal_pane(); + let client_id = 1; + let outcome = press_key(&mut pane, BareKey::Char('x'), client_id); + assert!(matches!( + outcome, + Some(crate::tab::AdjustedInput::WriteBytesToTerminal(_)) + )); +} |
