diff options
| author | Aram Drevekenin <aram@poor.dev> | 2026-06-16 15:44:22 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-06-16 15:44:22 +0200 |
| commit | 8c7e41c9aaafc44520380a2c86db517fde54ad31 (patch) | |
| tree | d54bb40aef913c07f2142bd835dfa47d09fae5af | |
| parent | Mobile layout (#5241) (diff) | |
| download | sl0ppij-8c7e41c9aaafc44520380a2c86db517fde54ad31.tar.gz sl0ppij-8c7e41c9aaafc44520380a2c86db517fde54ad31.tar.xz sl0ppij-8c7e41c9aaafc44520380a2c86db517fde54ad31.zip | |
chore: new test infra (#5269)
* initial implementation
* refactoring and cleanups
* port basic e2e tests
* fix tab rotation
* migrate client and mouse tests
* migrate the rest of the tests
* remove old e2e tests
* rustfmt
* add e2e note
* fix stale test
* fix: cross platform locking/cleanup
* fix: change mode race discovered by tests
* rustfmt
* fix: attach/detach race in tests
* fix plugin test assertion
* add pr
Diffstat (limited to '')
87 files changed, 4912 insertions, 3270 deletions
diff --git a/.config/nextest.toml b/.config/nextest.toml new file mode 100644 index 00000000..7c915027 --- /dev/null +++ b/.config/nextest.toml @@ -0,0 +1,3 @@ +[profile.default] +slow-timeout = { period = "30s", terminate-after = 2 } +fail-fast = false diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index cd05448b..8ad46cfc 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -113,6 +113,37 @@ jobs: - name: Test zellij-client run: cargo test --no-default-features -p zellij-client --lib + integration-test: + name: Integration Test + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v3 + + - name: Install Protoc + uses: arduino/setup-protoc@v3 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup toolchain + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + rustflags: "" + + - name: Install cargo-nextest + uses: taiki-e/install-action@v2 + with: + tool: cargo-nextest + + - name: Run whole-app integration tests + run: cargo xtask integration-test + test-no-web: name: Test (No Web) runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 2698a730..bcc9c33b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) * feat: allow tabs to have different sizes if clients aren't focused on the same one (https://github.com/zellij-org/zellij/pull/5133) * feat: PWA support for the web client (manifest + icons + iOS meta tags) so the page can be installed as a standalone app (https://github.com/zellij-org/zellij/pull/5184) * feat: mobile UI (https://github.com/zellij-org/zellij/pull/5241) +* chore: new integration testing infra (https://github.com/zellij-org/zellij/pull/5269) ## [0.44.3] - 2026-05-13 * fix(windows): bump windows-sys to 0.59 to align manifest with code, fixing source builds via `cargo install`/`cargo binstall` (https://github.com/zellij-org/zellij/pull/5139) @@ -4871,6 +4871,24 @@ dependencies = [ ] [[package]] +name = "zellij-integration-tests" +version = "0.45.0" +dependencies = [ + "anyhow", + "async-trait", + "crossbeam", + "insta", + "interprocess", + "log", + "regex", + "tokio", + "vte 0.11.0", + "zellij-client", + "zellij-server", + "zellij-utils", +] + +[[package]] name = "zellij-server" version = "0.45.0" dependencies = [ @@ -60,6 +60,7 @@ members = [ "default-plugins/link", "default-plugins/mobile", "zellij-client", + "zellij-integration-tests", "zellij-server", "zellij-utils", "zellij-tile", diff --git a/src/commands.rs b/src/commands.rs index 03151275..4d3b83d8 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -589,8 +589,12 @@ fn attach_with_cli_client( let get_current_dir = || std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); match Action::actions_from_cli(cli_action, Box::new(get_current_dir), config) { Ok(actions) => { - zellij_client::cli_client::start_cli_client(Box::new(os_input), session_name, actions); - std::process::exit(0); + let exit_status = zellij_client::cli_client::start_cli_client( + Box::new(os_input), + session_name, + actions, + ); + std::process::exit(exit_status); }, Err(e) => { eprintln!("{}", e); diff --git a/src/tests/e2e/cases.rs b/src/tests/e2e/cases.rs index 854e1c93..c20eb2c8 100644 --- a/src/tests/e2e/cases.rs +++ b/src/tests/e2e/cases.rs @@ -1,3 +1,10 @@ +/* + * + * NOTE: These tests are very heavy and are used as smoke tests just to verify the app is working + * end-to-end. Avoid adding new ones, preferring instead to use the zellij-integration-tests module + * it tests the app as a whole, only mocking the OS interaction parts + * +*/ #![allow(unused)] use insta::assert_snapshot; @@ -12,13 +19,6 @@ use regex::Regex; use std::fmt::Write; use std::path::Path; -use crate::tests::e2e::steps::{ - check_focus_on_second_tab, check_second_tab_opened, check_third_tab_is_left_wrapped, - check_third_tab_is_right_wrapped, check_third_tab_moved_left, - check_third_tab_moved_to_beginning, check_third_tab_opened, move_tab_left, move_tab_right, - new_tab, switch_focus_to_left_tab, type_second_tab_content, -}; - use super::remote_runner::{RemoteRunner, RemoteTerminal, Step}; pub const QUIT: [u8; 1] = [17]; // ctrl-q @@ -79,18 +79,6 @@ pub const SLEEP: [u8; 0] = []; pub const SECOND_TAB_CONTENT: [u8; 14] = [84, 97, 98, 32, 35, 50, 32, 99, 111, 110, 116, 101, 110, 116]; // Tab #2 content -pub fn sgr_mouse_report(position: Position, button: u8) -> Vec<u8> { - // button: (release is with lower case m, not supported here yet) - // 0 => left click - // 2 => right click - // 64 => scroll up - // 65 => scroll down - let Position { line, column } = position; - format!("\u{1b}[<{};{};{}M", button, column.0, line.0) - .as_bytes() - .to_vec() -} - // what we do here is adjust snapshots for various race conditions that should hopefully be // temporary until we can fix them - when adding stuff here, please add a detailed comment // explaining the race condition and what needs to be done to solve it @@ -158,662 +146,6 @@ pub fn starts_with_one_terminal() { #[test] #[ignore] -pub fn split_terminals_vertically() { - let fake_win_size = Size { - cols: 120, - rows: 24, - }; - - let mut test_attempts = 10; - let last_snapshot = loop { - RemoteRunner::kill_running_sessions(fake_win_size); - let mut runner = RemoteRunner::new(fake_win_size).add_step(Step { - name: "Split pane to the right", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(3, 2) - { - remote_terminal.send_key(&PANE_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); - // back to normal mode after split - step_is_complete = true; - } - step_is_complete - }, - }); - runner.run_all_steps(); - let last_snapshot = runner.take_snapshot_after(Step { - name: "Wait for new pane to appear", - instruction: |remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.cursor_position_is(63, 2) && remote_terminal.status_bar_appears() - { - // cursor is in the newly opened second pane - step_is_complete = true; - } - step_is_complete - }, - }); - if runner.test_timed_out && test_attempts > 0 { - test_attempts -= 1; - continue; - } else { - break last_snapshot; - } - }; - let last_snapshot = account_for_races_in_snapshot(last_snapshot); - assert_snapshot!(last_snapshot); -} - -#[test] -#[ignore] -pub fn cannot_split_terminals_vertically_when_active_terminal_is_too_small() { - let fake_win_size = Size { cols: 8, rows: 20 }; - let mut test_attempts = 10; - let last_snapshot = loop { - RemoteRunner::kill_running_sessions(fake_win_size); - let mut runner = RemoteRunner::new(fake_win_size).add_step(Step { - name: "Split pane to the right", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.cursor_position_is(3, 2) { - remote_terminal.send_key(&PANE_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); - // back to normal mode after split - step_is_complete = true; - } - step_is_complete - }, - }); - runner.run_all_steps(); - let last_snapshot = runner.take_snapshot_after(Step { - name: "Make sure only one pane appears", - instruction: |remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.cursor_position_is(3, 2) { - // ... is the truncated tip line - step_is_complete = true; - } - step_is_complete - }, - }); - if runner.test_timed_out && test_attempts > 0 { - test_attempts -= 1; - continue; - } else { - break last_snapshot; - } - }; - let last_snapshot = account_for_races_in_snapshot(last_snapshot); - assert_snapshot!(last_snapshot); -} - -#[test] -#[ignore] -pub fn scrolling_inside_a_pane() { - let fake_win_size = Size { - cols: 120, - rows: 24, - }; - let mut test_attempts = 10; - let last_snapshot = loop { - RemoteRunner::kill_running_sessions(fake_win_size); - let mut runner = RemoteRunner::new(fake_win_size) - .add_step(Step { - name: "Split pane to the right", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.status_bar_appears() - && remote_terminal.cursor_position_is(3, 2) - { - remote_terminal.send_key(&PANE_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); - step_is_complete = true; - } - step_is_complete - }, - }) - .add_step(Step { - name: "Fill terminal with text", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.cursor_position_is(63, 2) - && remote_terminal.status_bar_appears() - { - // cursor is in the newly opened second pane - remote_terminal.load_fixture("e2e/scrolling_inside_a_pane"); - step_is_complete = true; - } - step_is_complete - }, - }) - .add_step(Step { - name: "Scroll up inside pane", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.cursor_position_is(63, 21) - && remote_terminal.snapshot_contains("line21") - && remote_terminal.status_bar_appears() - { - // all lines have been written to the pane - remote_terminal.send_key(&SCROLL_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&SCROLL_UP_IN_SCROLL_MODE); - step_is_complete = true; - } - step_is_complete - }, - }); - runner.run_all_steps(); - let last_snapshot = runner.take_snapshot_after(Step { - name: "Wait for scroll to finish", - instruction: |remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.cursor_position_is(63, 21) - && remote_terminal.snapshot_contains("line3 ") - && remote_terminal.snapshot_contains("SCROLL: 1/3") - && remote_terminal.snapshot_contains("PgDn|PgUp") - { - // keyboard scrolls up 1 line, scrollback is 4 lines: cat command + 2 extra lines from fixture + prompt - // PgDn|PgUp only appears in the scroll mode status bar, confirming we're still in scroll mode - step_is_complete = true; - } - step_is_complete - }, - }); - if runner.test_timed_out && test_attempts > 0 { - test_attempts -= 1; - continue; - } else { - break last_snapshot; - } - }; - let last_snapshot = account_for_races_in_snapshot(last_snapshot); - assert_snapshot!(last_snapshot); -} - -#[test] -#[ignore] -pub fn toggle_pane_fullscreen() { - let fake_win_size = Size { - cols: 120, - rows: 24, - }; - let mut test_attempts = 10; - let last_snapshot = loop { - RemoteRunner::kill_running_sessions(fake_win_size); - let mut runner = RemoteRunner::new(fake_win_size) - .add_step(Step { - name: "Split pane to the right", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.status_bar_appears() - && remote_terminal.cursor_position_is(3, 2) - { - remote_terminal.send_key(&PANE_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); - step_is_complete = true; - } - step_is_complete - }, - }) - .add_step(Step { - name: "Change newly opened pane to be fullscreen", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.cursor_position_is(63, 2) - && remote_terminal.status_bar_appears() - { - // cursor is in the newly opened second pane - remote_terminal.send_key(&PANE_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&TOGGLE_ACTIVE_TERMINAL_FULLSCREEN_IN_PANE_MODE); - step_is_complete = true; - } - step_is_complete - }, - }); - runner.run_all_steps(); - let last_snapshot = runner.take_snapshot_after(Step { - name: "Wait for pane to become fullscreen", - instruction: |remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.cursor_position_is(3, 2) - && remote_terminal.snapshot_contains("LOCK") - { - // cursor is in full screen pane now - step_is_complete = true; - } - step_is_complete - }, - }); - if runner.test_timed_out && test_attempts > 0 { - test_attempts -= 1; - continue; - } else { - break last_snapshot; - } - }; - let last_snapshot = account_for_races_in_snapshot(last_snapshot); - assert_snapshot!(last_snapshot); -} - -#[test] -#[ignore] -pub fn open_new_tab() { - let fake_win_size = Size { - cols: 120, - rows: 24, - }; - let mut test_attempts = 10; - let last_snapshot = loop { - RemoteRunner::kill_running_sessions(fake_win_size); - let mut runner = RemoteRunner::new(fake_win_size) - .add_step(Step { - name: "Split pane to the right", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.status_bar_appears() - && remote_terminal.cursor_position_is(3, 2) - { - remote_terminal.send_key(&PANE_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); - step_is_complete = true; - } - step_is_complete - }, - }) - .add_step(Step { - name: "Open new tab", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.cursor_position_is(63, 2) - && remote_terminal.status_bar_appears() - { - // cursor is in the newly opened second pane - remote_terminal.send_key(&TAB_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&NEW_TAB_IN_TAB_MODE); - step_is_complete = true; - } - step_is_complete - }, - }); - runner.run_all_steps(); - let last_snapshot = runner.take_snapshot_after(Step { - name: "Wait for new tab to open", - instruction: |remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.cursor_position_is(3, 2) - && remote_terminal.snapshot_contains("Tab #2") - && remote_terminal.status_bar_appears() - { - // cursor is in the newly opened second tab - step_is_complete = true; - } - step_is_complete - }, - }); - if runner.test_timed_out && test_attempts > 0 { - test_attempts -= 1; - continue; - } else { - break last_snapshot; - } - }; - let last_snapshot = account_for_races_in_snapshot(last_snapshot); - assert_snapshot!(last_snapshot); -} - -#[test] -#[ignore] -pub fn close_tab() { - let fake_win_size = Size { - cols: 120, - rows: 24, - }; - let mut test_attempts = 10; - let last_snapshot = loop { - RemoteRunner::kill_running_sessions(fake_win_size); - let mut runner = RemoteRunner::new(fake_win_size) - .add_step(Step { - name: "Split pane to the right", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.status_bar_appears() - && remote_terminal.cursor_position_is(3, 2) - { - remote_terminal.send_key(&PANE_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); - step_is_complete = true; - } - step_is_complete - }, - }) - .add_step(Step { - name: "Open new tab", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.cursor_position_is(63, 2) - && remote_terminal.status_bar_appears() - { - // cursor is in the newly opened second pane - remote_terminal.send_key(&TAB_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&NEW_TAB_IN_TAB_MODE); - step_is_complete = true; - } - step_is_complete - }, - }) - .add_step(Step { - name: "Close tab", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.cursor_position_is(3, 2) - && remote_terminal.snapshot_contains("Tab #2") - && remote_terminal.status_bar_appears() - { - // cursor is in the newly opened second tab - remote_terminal.send_key(&TAB_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&CLOSE_TAB_IN_TAB_MODE); - step_is_complete = true; - } - step_is_complete - }, - }); - runner.run_all_steps(); - let last_snapshot = runner.take_snapshot_after(Step { - name: "Wait for tab to close", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.snapshot_contains("Tab #1") - && !remote_terminal.snapshot_contains("Tab #2") - { - // cursor is in the first tab again - step_is_complete = true; - } - step_is_complete - }, - }); - if runner.test_timed_out && test_attempts > 0 { - test_attempts -= 1; - continue; - } else { - break last_snapshot; - } - }; - assert!(last_snapshot.contains("Tab #1")); - assert!(!last_snapshot.contains("Tab #2")); -} - -#[test] -#[ignore] -pub fn move_tab_to_left() { - let mut test_attempts = 10; - let last_snapshot = loop { - RemoteRunner::kill_running_sessions(fake_win_size()); - let mut runner = RemoteRunner::new(fake_win_size()) - .add_step(new_tab()) - .add_step(check_second_tab_opened()) - .add_step(new_tab()) - .add_step(check_third_tab_opened()) // should have Tab#1 >> Tab#2 >> Tab#3 (focused on Tab#3) - .add_step(move_tab_left()); // now, it should be Tab#1 >> Tab#3 >> Tab#2 - - runner.run_all_steps(); - - let last_snapshot = runner.take_snapshot_after(check_third_tab_moved_left()); - if !runner.test_timed_out || test_attempts == 0 { - break last_snapshot; - } - test_attempts -= 1; - }; - assert_snapshot!(account_for_races_in_snapshot(last_snapshot)); -} - -fn fake_win_size() -> Size { - Size { - cols: 120, - rows: 24, - } -} - -#[test] -#[ignore] -pub fn move_tab_to_right() { - let mut test_attempts = 10; - let last_snapshot = loop { - RemoteRunner::kill_running_sessions(fake_win_size()); - let mut runner = RemoteRunner::new(fake_win_size()) - .add_step(new_tab()) - .add_step(check_second_tab_opened()) - .add_step(type_second_tab_content()) // allows verifying the focus later - .add_step(new_tab()) - .add_step(check_third_tab_opened()) - .add_step(switch_focus_to_left_tab()) - .add_step(check_focus_on_second_tab()) // should have Tab#1 >> Tab#2 >> Tab#3 (focused on Tab#2) - .add_step(move_tab_right()); // now, it should be Tab#1 >> Tab#3 >> Tab#2 - - runner.run_all_steps(); - - let last_snapshot = runner.take_snapshot_after(check_third_tab_moved_left()); - if !runner.test_timed_out || test_attempts == 0 { - break last_snapshot; - } - test_attempts -= 1; - }; - assert_snapshot!(account_for_races_in_snapshot(last_snapshot)); -} - -#[test] -#[ignore] -pub fn move_tab_to_left_until_it_wraps_around() { - let mut test_attempts = 10; - let last_snapshot = loop { - RemoteRunner::kill_running_sessions(fake_win_size()); - let mut runner = RemoteRunner::new(fake_win_size()) - .add_step(new_tab()) - .add_step(check_second_tab_opened()) - .add_step(new_tab()) - .add_step(check_third_tab_opened()) - .add_step(move_tab_left()) - .add_step(check_third_tab_moved_left()) - .add_step(move_tab_left()) - .add_step(check_third_tab_moved_to_beginning()) // should have Tab#3 >> Tab#1 >> Tab#2 (focused on Tab#3) - .add_step(move_tab_left()); // now, it should be Tab#2 >> Tab#1 >> Tab#3 - - runner.run_all_steps(); - - let last_snapshot = runner.take_snapshot_after(check_third_tab_is_left_wrapped()); - if !runner.test_timed_out || test_attempts == 0 { - break last_snapshot; - } - test_attempts -= 1; - }; - assert_snapshot!(account_for_races_in_snapshot(last_snapshot)); -} - -#[test] -#[ignore] -pub fn move_tab_to_right_until_it_wraps_around() { - let mut test_attempts = 10; - let last_snapshot = loop { - RemoteRunner::kill_running_sessions(fake_win_size()); - let mut runner = RemoteRunner::new(fake_win_size()) - .add_step(new_tab()) - .add_step(check_second_tab_opened()) - .add_step(new_tab()) - .add_step(check_third_tab_opened()) // should have Tab#1 >> Tab#2 >> Tab#3 (focused on Tab#3) - .add_step(move_tab_right()); // now, it should be Tab#3 >> Tab#2 >> Tab#1 - - runner.run_all_steps(); - - let last_snapshot = runner.take_snapshot_after(check_third_tab_is_right_wrapped()); - if !runner.test_timed_out || test_attempts == 0 { - break last_snapshot; - } - test_attempts -= 1; - }; - assert_snapshot!(account_for_races_in_snapshot(last_snapshot)); -} - -#[test] -#[ignore] -pub fn close_pane() { - let fake_win_size = Size { - cols: 120, - rows: 24, - }; - let mut test_attempts = 10; - let last_snapshot = loop { - RemoteRunner::kill_running_sessions(fake_win_size); - let mut runner = RemoteRunner::new(fake_win_size) - .add_step(Step { - name: "Split pane to the right", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.status_bar_appears() - && remote_terminal.cursor_position_is(3, 2) - { - remote_terminal.send_key(&PANE_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); - step_is_complete = true; - } - step_is_complete - }, - }) - .add_step(Step { - name: "Close pane", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.cursor_position_is(63, 2) - && remote_terminal.status_bar_appears() - { - // cursor is in the newly opened second pane - remote_terminal.send_key(&PANE_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&CLOSE_PANE_IN_PANE_MODE); - step_is_complete = true; - } - step_is_complete - }, - }); - runner.run_all_steps(); - let last_snapshot = runner.take_snapshot_after(Step { - name: "Wait for pane to close", - instruction: |remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.cursor_position_is(3, 2) && remote_terminal.status_bar_appears() - { - // cursor is in the original pane - step_is_complete = true; - } - step_is_complete - }, - }); - if runner.test_timed_out && test_attempts > 0 { - test_attempts -= 1; - continue; - } else { - break last_snapshot; - } - }; - let last_snapshot = account_for_races_in_snapshot(last_snapshot); - assert_snapshot!(last_snapshot); -} - -#[test] -#[ignore] -pub fn exit_zellij() { - let fake_win_size = Size { - cols: 120, - rows: 24, - }; - let mut test_attempts = 10; - let last_snapshot = { - RemoteRunner::kill_running_sessions(fake_win_size); - let mut runner = RemoteRunner::new(fake_win_size).add_step(Step { - name: "Wait for app to load", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(3, 2) - { - remote_terminal.send_key(&QUIT); - step_is_complete = true; - } - step_is_complete - }, - }); - runner.run_all_steps(); - runner.take_snapshot_after(Step { - name: "Wait for app to exit", - instruction: |remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if !remote_terminal.status_bar_appears() - && remote_terminal.snapshot_contains("Bye from Zellij!") - { - step_is_complete = true; - } - step_is_complete - }, - }) - }; - assert!(last_snapshot.contains("Bye from Zellij!")); -} - -#[test] -#[ignore] -pub fn closing_last_pane_exits_zellij() { - let fake_win_size = Size { - cols: 120, - rows: 24, - }; - let mut test_attempts = 10; - let last_snapshot = loop { - RemoteRunner::kill_running_sessions(fake_win_size); - let mut runner = RemoteRunner::new(fake_win_size).add_step(Step { - name: "Close pane", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(3, 2) - { - remote_terminal.send_key(&PANE_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&CLOSE_PANE_IN_PANE_MODE); - step_is_complete = true; - } - step_is_complete - }, - }); - runner.run_all_steps(); - if runner.test_timed_out && test_attempts > 0 { - test_attempts -= 1; - continue; - } - break runner.take_snapshot_after(Step { - name: "Wait for app to exit", - instruction: |remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.snapshot_contains("Bye from Zellij!") { - step_is_complete = true; - } - step_is_complete - }, - }); - }; - assert!(last_snapshot.contains("Bye from Zellij!")); -} - -#[test] -#[ignore] pub fn typing_exit_closes_pane() { let fake_win_size = Size { cols: 120, @@ -885,138 +217,6 @@ pub fn typing_exit_closes_pane() { #[test] #[ignore] -pub fn resize_pane() { - let fake_win_size = Size { - cols: 120, - rows: 24, - }; - let mut test_attempts = 10; - let last_snapshot = loop { - RemoteRunner::kill_running_sessions(fake_win_size); - let mut runner = RemoteRunner::new(fake_win_size) - .add_step(Step { - name: "Split pane to the right", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.status_bar_appears() - && remote_terminal.cursor_position_is(3, 2) - { - remote_terminal.send_key(&PANE_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); - step_is_complete = true; - } - step_is_complete - }, - }) - .add_step(Step { - name: "Resize pane", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.cursor_position_is(63, 2) - && remote_terminal.status_bar_appears() - { - // cursor is in the newly opened second pane - remote_terminal.send_key(&RESIZE_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&RESIZE_LEFT_IN_RESIZE_MODE); - // back to normal mode - remote_terminal.send_key(&ENTER); - step_is_complete = true; - } - step_is_complete - }, - }); - runner.run_all_steps(); - let last_snapshot = runner.take_snapshot_after(Step { - name: "Wait for pane to be resized", - instruction: |remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.cursor_position_is(57, 2) - && remote_terminal.status_bar_appears() - && remote_terminal.snapshot_contains("LOCK") - { - // pane has been resized - step_is_complete = true; - } - step_is_complete - }, - }); - if runner.test_timed_out && test_attempts > 0 { - test_attempts -= 1; - continue; - } else { - break last_snapshot; - } - }; - let last_snapshot = account_for_races_in_snapshot(last_snapshot); - assert_snapshot!(last_snapshot); -} - -#[test] -#[ignore] -pub fn lock_mode() { - let fake_win_size = Size { - cols: 120, - rows: 24, - }; - let mut test_attempts = 10; - let last_snapshot = loop { - RemoteRunner::kill_running_sessions(fake_win_size); - let mut runner = RemoteRunner::new(fake_win_size) - .add_step(Step { - name: "Enter lock mode", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.status_bar_appears() - && remote_terminal.cursor_position_is(3, 2) - { - remote_terminal.send_key(&LOCK_MODE); - step_is_complete = true; - } - step_is_complete - }, - }) - .add_step(Step { - name: "Send keys that should not be intercepted by the app", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if !remote_terminal.snapshot_contains("PANE") { - remote_terminal.send_key(&TAB_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&NEW_TAB_IN_TAB_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key("abc".as_bytes()); - step_is_complete = true; - } - step_is_complete - }, - }); - runner.run_all_steps(); - let last_snapshot = runner.take_snapshot_after(Step { - name: "Wait for terminal to render sent keys", - instruction: |remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.cursor_position_is(7, 2) { - // text has been entered into the only terminal pane - step_is_complete = true; - } - step_is_complete - }, - }); - if runner.test_timed_out && test_attempts > 0 { - test_attempts -= 1; - continue; - } else { - break last_snapshot; - } - }; - let last_snapshot = account_for_races_in_snapshot(last_snapshot); - assert_snapshot!(last_snapshot); -} - -#[test] -#[ignore] pub fn resize_terminal_window() { // this checks the resizing of the whole terminal window (reaction to SIGWINCH) and not just one pane let fake_win_size = Size { @@ -1082,101 +282,6 @@ pub fn resize_terminal_window() { #[test] #[ignore] -pub fn detach_and_attach_session() { - let fake_win_size = Size { - cols: 120, - rows: 24, - }; - let mut test_attempts = 10; - let last_snapshot = loop { - RemoteRunner::kill_running_sessions(fake_win_size); - let mut runner = RemoteRunner::new_mirrored_session(fake_win_size) - .add_step(Step { - name: "Split pane to the right", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.status_bar_appears() - && remote_terminal.cursor_position_is(3, 2) - { - remote_terminal.send_key(&PANE_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); - step_is_complete = true; - } - step_is_complete - }, - }) - .add_step(Step { - name: "Send some text to the active pane", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.cursor_position_is(63, 2) - && remote_terminal.status_bar_appears() - { - // new pane has been opened and focused - remote_terminal.send_key("I am some text".as_bytes()); - step_is_complete = true; - } - step_is_complete - }, - }) - .add_step(Step { - name: "Detach session", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.cursor_position_is(77, 2) { - remote_terminal.send_key(&SESSION_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&DETACH_IN_SESSION_MODE); - // text has been entered - step_is_complete = true; - } - step_is_complete - }, - }) - .add_step(Step { - name: "Reattach session", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if !remote_terminal.status_bar_appears() - && remote_terminal.snapshot_contains("Bye from Zellij!") - { - // we don't see the toolbar and Zellij's exit message is visible, - // so Zellij has fully exited and the server is ready to accept connections - remote_terminal.attach_to_original_session(); - step_is_complete = true; - } - step_is_complete - }, - }); - runner.run_all_steps(); - let last_snapshot = runner.take_snapshot_after(Step { - name: "Wait for session to be attached", - instruction: |remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.ctrl_plus_appears() - && remote_terminal.cursor_position_is(77, 2) - && !remote_terminal.snapshot_contains("Detach") - { - // we're back inside the session - step_is_complete = true; - } - step_is_complete - }, - }); - if runner.test_timed_out && test_attempts > 0 { - test_attempts -= 1; - continue; - } else { - break last_snapshot; - } - }; - let last_snapshot = account_for_races_in_snapshot(last_snapshot); - assert_snapshot!(last_snapshot); -} - -#[test] -#[ignore] pub fn quit_and_resurrect_session() { let fake_win_size = Size { cols: 120, @@ -1233,1175 +338,6 @@ pub fn quit_and_resurrect_session() { #[test] #[ignore] -pub fn quit_and_resurrect_session_with_viewport_serialization() { - let fake_win_size = Size { - cols: 120, - rows: 24, - }; - let mut test_attempts = 10; - let layout_name = "layout_for_resurrection.kdl"; - let last_snapshot = loop { - RemoteRunner::kill_running_sessions(fake_win_size); - let mut runner = RemoteRunner::new_mirrored_session_with_layout_and_viewport_serialization( - fake_win_size, - layout_name, - ) - .add_step(Step { - name: "Wait for session to be serialized", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.snapshot_contains("Waiting to run: top") { - std::thread::sleep(std::time::Duration::from_millis(5000)); // wait for - // serialization - remote_terminal.send_key(&QUIT); - step_is_complete = true; - } - step_is_complete - }, - }) - .add_step(Step { - name: "Resurrect session by attaching", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.snapshot_contains("Bye from Zellij!") { - remote_terminal.attach_to_original_session(); - step_is_complete = true; - } - step_is_complete - }, - }); - runner.run_all_steps(); - let last_snapshot = runner.take_snapshot_after(Step { - name: "Wait for session to be resurrected", - instruction: |remote_terminal: RemoteTerminal| -> bool { - remote_terminal.snapshot_contains("(FLOATING PANES VISIBLE)") - && remote_terminal.status_bar_appears() - && remote_terminal.tab_bar_appears() - }, - }); - if runner.test_timed_out && test_attempts > 0 { - test_attempts -= 1; - continue; - } else { - break last_snapshot; - } - }; - let last_snapshot = account_for_races_in_snapshot(last_snapshot); - assert_snapshot!(last_snapshot); -} - -#[test] -#[ignore] -pub fn status_bar_loads_custom_keybindings() { - let fake_win_size = Size { - cols: 120, - rows: 24, - }; - let config_file_name = "changed_keys.kdl"; - let mut test_attempts = 10; - let last_snapshot = loop { - RemoteRunner::kill_running_sessions(fake_win_size); - let mut runner = RemoteRunner::new_with_config(fake_win_size, config_file_name); - runner.run_all_steps(); - let last_snapshot = runner.take_snapshot_after(Step { - name: "Wait for app to load", - instruction: |remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.cursor_position_is(3, 2) - && remote_terminal.snapshot_contains("LOCK") - { - step_is_complete = true; - } - step_is_complete - }, - }); - if runner.test_timed_out && test_attempts > 0 { - test_attempts -= 1; - continue; - } else { - break last_snapshot; - } - }; - let last_snapshot = account_for_races_in_snapshot(last_snapshot); - assert_snapshot!(last_snapshot); -} - -#[test] -#[ignore] -fn focus_pane_with_mouse() { - let fake_win_size = Size { - cols: 120, - rows: 24, - }; - - let mut test_attempts = 10; - let last_snapshot = loop { - RemoteRunner::kill_running_sessions(fake_win_size); - let mut runner = RemoteRunner::new(fake_win_size) - .add_step(Step { - name: "Split pane to the right", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.status_bar_appears() - && remote_terminal.cursor_position_is(3, 2) - { - remote_terminal.send_key(&PANE_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); - step_is_complete = true; - } - step_is_complete - }, - }) - .add_step(Step { - name: "Click left pane", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.cursor_position_is(63, 2) - && remote_terminal.status_bar_appears() - { - remote_terminal.send_key(&sgr_mouse_report(Position::new(5, 2), 0)); - step_is_complete = true; - } - step_is_complete - }, - }); - runner.run_all_steps(); - let last_snapshot = runner.take_snapshot_after(Step { - name: "Wait for left pane to be focused", - instruction: |remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.cursor_position_is(3, 2) - && remote_terminal.status_bar_appears() - && remote_terminal.snapshot_contains("LOCK") - { - // cursor is in the newly opened second pane - step_is_complete = true; - } - step_is_complete - }, - }); - if runner.test_timed_out && test_attempts > 0 { - test_attempts -= 1; - continue; - } else { - break last_snapshot; - } - }; - let last_snapshot = account_for_races_in_snapshot(last_snapshot); - assert_snapshot!(last_snapshot); -} - -#[test] -#[ignore] -pub fn scrolling_inside_a_pane_with_mouse() { - let fake_win_size = Size { - cols: 120, - rows: 24, - }; - let mut test_attempts = 10; - let last_snapshot = loop { - RemoteRunner::kill_running_sessions(fake_win_size); - let mut runner = RemoteRunner::new(fake_win_size) - .add_step(Step { - name: "Split pane to the right", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.status_bar_appears() - && remote_terminal.cursor_position_is(3, 2) - { - remote_terminal.send_key(&PANE_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); - step_is_complete = true; - } - step_is_complete - }, - }) - .add_step(Step { - name: "Fill terminal with text", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.cursor_position_is(63, 2) - && remote_terminal.status_bar_appears() - && remote_terminal.snapshot_contains("LOCK") - { - remote_terminal.load_fixture("e2e/scrolling_inside_a_pane"); - step_is_complete = true; - } - step_is_complete - }, - }) - .add_step(Step { - name: "Scroll up inside pane", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.cursor_position_is(63, 21) - && remote_terminal.snapshot_contains("line21") - { - // all lines have been written to the pane - remote_terminal.send_key(&sgr_mouse_report(Position::new(2, 64), 64)); - step_is_complete = true; - } - step_is_complete - }, - }); - runner.run_all_steps(); - let last_snapshot = runner.take_snapshot_after(Step { - name: "Wait for scroll to finish", - instruction: |remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.cursor_position_is(63, 21) - && remote_terminal.snapshot_contains("line1 ") - && remote_terminal.snapshot_contains("SCROLL: 3/3") - && remote_terminal.snapshot_contains("LOCK") - { - step_is_complete = true; - } - step_is_complete - }, - }); - if runner.test_timed_out && test_attempts > 0 { - test_attempts -= 1; - continue; - } else { - break last_snapshot; - } - }; - let last_snapshot = account_for_races_in_snapshot(last_snapshot); - assert_snapshot!(last_snapshot); -} - -#[test] -#[ignore] -pub fn start_without_pane_frames() { - let fake_win_size = Size { - cols: 120, - rows: 24, - }; - - let mut test_attempts = 10; - let last_snapshot = loop { - RemoteRunner::kill_running_sessions(fake_win_size); - let mut runner = RemoteRunner::new_without_frames(fake_win_size).add_step(Step { - name: "Split pane to the right", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(2, 1) - { - remote_terminal.send_key(&PANE_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); - step_is_complete = true; - } - step_is_complete - }, - }); - runner.run_all_steps(); - let last_snapshot = runner.take_snapshot_after(Step { - name: "Wait for new pane to appear", - instruction: |remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.cursor_position_is(62, 1) && remote_terminal.status_bar_appears() - { - // cursor is in the newly opened second pane - step_is_complete = true; - } - step_is_complete - }, - }); - if runner.test_timed_out && test_attempts > 0 { - test_attempts -= 1; - continue; - } else { - break last_snapshot; - } - }; - let last_snapshot = account_for_races_in_snapshot(last_snapshot); - assert_snapshot!(last_snapshot); -} - -#[test] -#[ignore] -pub fn mirrored_sessions() { - let fake_win_size = Size { - cols: 120, - rows: 24, - }; - let mut test_attempts = 10; - let session_name = "mirrored_sessions"; - let (first_runner_snapshot, second_runner_snapshot) = loop { - // here we connect with one runner, then connect with another, perform some actions and - // then make sure they were also reflected (mirrored) in the first runner afterwards - RemoteRunner::kill_running_sessions(fake_win_size); - let mut first_runner = - RemoteRunner::new_with_session_name(fake_win_size, session_name, true) - .dont_panic() - .add_step(Step { - name: "Wait for app to load", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.status_bar_appears() - && remote_terminal.cursor_position_is(3, 2) - { - step_is_complete = true; - } - step_is_complete - }, - }); - first_runner.run_all_steps(); - - let mut second_runner = RemoteRunner::new_existing_session(fake_win_size, session_name) - .dont_panic() - .add_step(Step { - name: "Split pane to the right", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.status_bar_appears() - && remote_terminal.cursor_position_is(3, 2) - { - remote_terminal.send_key(&PANE_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); - step_is_complete = true; - } - step_is_complete - }, - }) - .add_step(Step { - name: "Open new tab (second user)", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.cursor_position_is(63, 2) - && remote_terminal.status_bar_appears() - { - // cursor is in the newly opened second pane - remote_terminal.send_key(&TAB_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&NEW_TAB_IN_TAB_MODE); - step_is_complete = true; - } - step_is_complete - }, - }) - .add_step(Step { - name: "Wait for new tab to open", - instruction: |remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.cursor_position_is(3, 2) - && remote_terminal.snapshot_contains("Tab #2") - && remote_terminal.status_bar_appears() - { - // cursor is in the newly opened second tab - step_is_complete = true; - } - step_is_complete - }, - }) - .add_step(Step { - name: "Switch to previous tab", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.cursor_position_is(3, 2) - && remote_terminal.status_bar_appears() - && remote_terminal.snapshot_contains("Tab #2") - { - // cursor is in the newly opened second pane - remote_terminal.send_key("some text".as_bytes()); - step_is_complete = true; - } - step_is_complete - }, - }) - .add_step(Step { - name: "Wait for text to appear on screen", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.snapshot_contains("some text") { - remote_terminal.send_key(&TAB_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&MOVE_FOCUS_LEFT_IN_PANE_MODE); // same key as tab mode - step_is_complete = true; - } - step_is_complete - }, - }); - second_runner.run_all_steps(); - - if (first_runner.test_timed_out || second_runner.test_timed_out) && test_attempts >= 0 { - test_attempts -= 1; - continue; - } - let second_runner_snapshot = second_runner.take_snapshot_after(Step { - name: "take snapshot after", - instruction: |remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.cursor_position_is(63, 2) - && remote_terminal.snapshot_contains("┐┌") - { - // cursor is back in the first tab - step_is_complete = true; - } - step_is_complete - }, - }); - let first_runner_snapshot = first_runner.take_snapshot_after(Step { - name: "take snapshot after", - instruction: |remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.cursor_position_is(63, 2) - && remote_terminal.snapshot_contains("┐┌") - { - // cursor is back in the first tab - step_is_complete = true; - } - step_is_complete - }, - }); - - if (first_runner.test_timed_out || second_runner.test_timed_out) && test_attempts >= 0 { - test_attempts -= 1; - continue; - } else { - break (first_runner_snapshot, second_runner_snapshot); - } - }; - let first_runner_snapshot = account_for_races_in_snapshot(first_runner_snapshot); - let second_runner_snapshot = account_for_races_in_snapshot(second_runner_snapshot); - assert_snapshot!(first_runner_snapshot); - assert_snapshot!(second_runner_snapshot); -} - -#[test] -#[ignore] -pub fn multiple_users_in_same_pane_and_tab() { - let fake_win_size = Size { - cols: 120, - rows: 24, - }; - let mut test_attempts = 10; - let session_name = "multiple_users_in_same_pane_and_tab"; - let (first_runner_snapshot, second_runner_snapshot) = loop { - // here we connect with one runner, then connect with another, perform some actions and - // then make sure they were also reflected (mirrored) in the first runner afterwards - RemoteRunner::kill_running_sessions(fake_win_size); - let mut first_runner = - RemoteRunner::new_with_session_name(fake_win_size, session_name, false) - .dont_panic() - .add_step(Step { - name: "Wait for app to load", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.status_bar_appears() - && remote_terminal.cursor_position_is(3, 2) - { - step_is_complete = true; - } - step_is_complete - }, - }); - first_runner.run_all_steps(); - - let mut second_runner = RemoteRunner::new_existing_session(fake_win_size, session_name) - .dont_panic() - .add_step(Step { - name: "Wait for app to load", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.status_bar_appears() - && remote_terminal.cursor_position_is(3, 2) - { - step_is_complete = true; - } - step_is_complete - }, - }); - second_runner.run_all_steps(); - - if (first_runner.test_timed_out || second_runner.test_timed_out) && test_attempts >= 0 { - test_attempts -= 1; - continue; - } - let second_runner_snapshot = second_runner.take_snapshot_after(Step { - name: "take snapshot after", - instruction: |remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.cursor_position_is(3, 2) - && remote_terminal.snapshot_contains("MY FOCUS") - { - // cursor is back in the first tab - step_is_complete = true; - } - step_is_complete - }, - }); - let first_runner_snapshot = first_runner.take_snapshot_after(Step { - name: "take snapshot after", - instruction: |remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.cursor_position_is(3, 2) - && remote_terminal.snapshot_contains("MY FOCUS") - { - // cursor is back in the first tab - step_is_complete = true; - } - step_is_complete - }, - }); - - if (first_runner.test_timed_out || second_runner.test_timed_out) && test_attempts >= 0 { - test_attempts -= 1; - continue; - } else { - break (first_runner_snapshot, second_runner_snapshot); - } - }; - let first_runner_snapshot = account_for_races_in_snapshot(first_runner_snapshot); - let second_runner_snapshot = account_for_races_in_snapshot(second_runner_snapshot); - assert_snapshot!(first_runner_snapshot); - assert_snapshot!(second_runner_snapshot); -} - -#[test] -#[ignore] -pub fn multiple_users_in_different_panes_and_same_tab() { - let fake_win_size = Size { - cols: 120, - rows: 24, - }; - let mut test_attempts = 10; - let session_name = "multiple_users_in_same_pane_and_tab"; - let (first_runner_snapshot, second_runner_snapshot) = loop { - // here we connect with one runner, then connect with another, perform some actions and - // then make sure they were also reflected (mirrored) in the first runner afterwards - RemoteRunner::kill_running_sessions(fake_win_size); - let mut first_runner = - RemoteRunner::new_with_session_name(fake_win_size, session_name, false) - .dont_panic() - .add_step(Step { - name: "Wait for app to load", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.status_bar_appears() - && remote_terminal.cursor_position_is(3, 2) - { - step_is_complete = true; - } - step_is_complete - }, - }); - first_runner.run_all_steps(); - - let mut second_runner = RemoteRunner::new_existing_session(fake_win_size, session_name) - .dont_panic() - .add_step(Step { - name: "Split pane to the right", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.status_bar_appears() - && remote_terminal.cursor_position_is(3, 2) - { - remote_terminal.send_key(&PANE_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); - step_is_complete = true; - } - step_is_complete - }, - }); - second_runner.run_all_steps(); - - if (first_runner.test_timed_out || second_runner.test_timed_out) && test_attempts >= 0 { - test_attempts -= 1; - continue; - } - - let second_runner_snapshot = second_runner.take_snapshot_after(Step { - name: "take snapshot after", - instruction: |remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.cursor_position_is(63, 2) - && remote_terminal.status_bar_appears() - && remote_terminal.snapshot_contains("LOCK") - { - // cursor is in the newly opened second pane - step_is_complete = true; - } - step_is_complete - }, - }); - - let first_runner_snapshot = first_runner.take_snapshot_after(Step { - name: "take snapshot after", - instruction: |remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.cursor_position_is(3, 2) - && remote_terminal.snapshot_contains("││$") - && remote_terminal.tab_bar_appears() - { - // cursor is back in the first tab - step_is_complete = true; - } - step_is_complete - }, - }); - - if (first_runner.test_timed_out || second_runner.test_timed_out) && test_attempts >= 0 { - test_attempts -= 1; - continue; - } else { - break (first_runner_snapshot, second_runner_snapshot); - } - }; - let first_runner_snapshot = account_for_races_in_snapshot(first_runner_snapshot); - let second_runner_snapshot = account_for_races_in_snapshot(second_runner_snapshot); - assert_snapshot!(first_runner_snapshot); - assert_snapshot!(second_runner_snapshot); -} - -#[test] -#[ignore] -pub fn multiple_users_in_different_tabs() { - let fake_win_size = Size { - cols: 120, - rows: 24, - }; - let mut test_attempts = 10; - let session_name = "multiple_users_in_different_tabs"; - let (first_runner_snapshot, second_runner_snapshot) = loop { - // here we connect with one runner, then connect with another, perform some actions and - // then make sure they were also reflected (mirrored) in the first runner afterwards - RemoteRunner::kill_running_sessions(fake_win_size); - let mut first_runner = - RemoteRunner::new_with_session_name(fake_win_size, session_name, false) - .dont_panic() - .add_step(Step { - name: "Wait for app to load", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.status_bar_appears() - && remote_terminal.cursor_position_is(3, 2) - { - step_is_complete = true; - } - step_is_complete - }, - }); - first_runner.run_all_steps(); - - let mut second_runner = RemoteRunner::new_existing_session(fake_win_size, session_name) - .dont_panic() - .add_step(Step { - name: "Open new tab", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.cursor_position_is(3, 2) - && remote_terminal.status_bar_appears() - { - // cursor is in the newly opened second pane - remote_terminal.send_key(&TAB_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&NEW_TAB_IN_TAB_MODE); - step_is_complete = true; - } - step_is_complete - }, - }); - second_runner.run_all_steps(); - - if (first_runner.test_timed_out || second_runner.test_timed_out) && test_attempts >= 0 { - test_attempts -= 1; - continue; - } - - let second_runner_snapshot = second_runner.take_snapshot_after(Step { - name: "Wait for new tab to open", - instruction: |remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.cursor_position_is(3, 2) - && remote_terminal.snapshot_contains("Tab #1 [ ]") - && remote_terminal.snapshot_contains("Tab #2") - && remote_terminal.status_bar_appears() - && !remote_terminal.snapshot_contains("AND:") - { - // cursor is in the newly opened second tab - step_is_complete = true; - } - step_is_complete - }, - }); - - let first_runner_snapshot = first_runner.take_snapshot_after(Step { - name: "Wait for new tab to open", - instruction: |remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.cursor_position_is(3, 2) - && remote_terminal.snapshot_contains("Tab #2 [ ]") - && remote_terminal.status_bar_appears() - { - // cursor is in the newly opened second tab - step_is_complete = true; - } - step_is_complete - }, - }); - - if (first_runner.test_timed_out || second_runner.test_timed_out) && test_attempts >= 0 { - test_attempts -= 1; - continue; - } else { - break (first_runner_snapshot, second_runner_snapshot); - } - }; - let first_runner_snapshot = account_for_races_in_snapshot(first_runner_snapshot); - let second_runner_snapshot = account_for_races_in_snapshot(second_runner_snapshot); - assert_snapshot!(first_runner_snapshot); - assert_snapshot!(second_runner_snapshot); -} - -#[test] -#[ignore] -pub fn bracketed_paste() { - let fake_win_size = Size { - cols: 120, - rows: 24, - }; - // here we enter some text, before which we invoke "bracketed paste mode" - // we make sure the text in bracketed paste mode is sent directly to the terminal and not - // interpreted by us (in this case it will send ^T to the terminal), then we exit bracketed - // paste, send some more text and make sure it's also sent to the terminal - let mut test_attempts = 10; - let last_snapshot = loop { - RemoteRunner::kill_running_sessions(fake_win_size); - let mut runner = RemoteRunner::new(fake_win_size).add_step(Step { - name: "Send pasted text followed by normal text", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.status_bar_appears() - && remote_terminal.tab_bar_appears() - && remote_terminal.cursor_position_is(3, 2) - { - remote_terminal.send_key(&BRACKETED_PASTE_START); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&TAB_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&NEW_TAB_IN_TAB_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key("a".as_bytes()); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key("b".as_bytes()); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key("c".as_bytes()); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&BRACKETED_PASTE_END); - step_is_complete = true; - } - step_is_complete - }, - }); - runner.run_all_steps(); - - let last_snapshot = runner.take_snapshot_after(Step { - name: "Wait for terminal to render sent keys", - instruction: |remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.snapshot_contains("abc") { - // text has been entered into the only terminal pane - step_is_complete = true; - } - step_is_complete - }, - }); - if runner.test_timed_out && test_attempts > 0 { - test_attempts -= 1; - continue; - } else { - break last_snapshot; - } - }; - let last_snapshot = account_for_races_in_snapshot(last_snapshot); - assert_snapshot!(last_snapshot); -} - -#[test] -#[ignore] -pub fn toggle_floating_panes() { - let fake_win_size = Size { - cols: 120, - rows: 24, - }; - - let mut test_attempts = 10; - let last_snapshot = loop { - RemoteRunner::kill_running_sessions(fake_win_size); - let mut runner = RemoteRunner::new(fake_win_size).add_step(Step { - name: "Toggle floating panes", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(3, 2) - { - remote_terminal.send_key(&PANE_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&TOGGLE_FLOATING_PANES); - // back to normal mode after split - step_is_complete = true; - } - step_is_complete - }, - }); - runner.run_all_steps(); - let last_snapshot = runner.take_snapshot_after(Step { - name: "Wait for new pane to appear", - instruction: |remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.cursor_position_is(33, 8) - && remote_terminal.snapshot_contains("STAGGERED") - && remote_terminal.snapshot_contains("LOCK") - { - // cursor is in the newly opened second pane - step_is_complete = true; - } - step_is_complete - }, - }); - if runner.test_timed_out && test_attempts > 0 { - test_attempts -= 1; - continue; - } else { - break last_snapshot; - } - }; - let last_snapshot = account_for_races_in_snapshot(last_snapshot); - assert_snapshot!(last_snapshot); -} - -#[test] -#[ignore] -pub fn tmux_mode() { - let fake_win_size = Size { - cols: 120, - rows: 24, - }; - - let mut test_attempts = 10; - let last_snapshot = loop { - RemoteRunner::kill_running_sessions(fake_win_size); - let mut runner = RemoteRunner::new(fake_win_size).add_step(Step { - name: "Split pane to the right", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(3, 2) - { - remote_terminal.send_key(&TMUX_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&SPLIT_RIGHT_IN_TMUX_MODE); - // back to normal mode after split - step_is_complete = true; - } - step_is_complete - }, - }); - runner.run_all_steps(); - let last_snapshot = runner.take_snapshot_after(Step { - name: "Wait for new pane to appear", - instruction: |remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.cursor_position_is(63, 2) && remote_terminal.status_bar_appears() - { - // cursor is in the newly opened second pane - step_is_complete = true; - } - step_is_complete - }, - }); - if runner.test_timed_out && test_attempts > 0 { - test_attempts -= 1; - continue; - } else { - break last_snapshot; - } - }; - let last_snapshot = account_for_races_in_snapshot(last_snapshot); - assert_snapshot!(last_snapshot); -} - -#[test] -#[ignore] -pub fn edit_scrollback() { - let fake_win_size = Size { - cols: 120, - rows: 24, - }; - - let mut test_attempts = 10; - let last_snapshot = loop { - RemoteRunner::kill_running_sessions(fake_win_size); - let mut runner = RemoteRunner::new(fake_win_size).add_step(Step { - name: "Split pane to the right", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(3, 2) - { - remote_terminal.send_key(&SCROLL_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&EDIT_SCROLLBACK); - step_is_complete = true; - } - step_is_complete - }, - }); - runner.run_all_steps(); - let last_snapshot = runner.take_snapshot_after(Step { - name: "Wait for editor to appear", - instruction: |remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.snapshot_contains(".dump") { - // the .dump is an indication we get on the bottom line of vi when editing a - // file - // the temp file name is randomly generated, so we don't assert the whole snapshot - step_is_complete = true; - } - step_is_complete - }, - }); - if runner.test_timed_out && test_attempts > 0 { - test_attempts -= 1; - continue; - } else { - break last_snapshot; - } - }; - assert!(last_snapshot.contains(".dump")); -} - -#[test] -#[ignore] -pub fn undo_rename_tab() { - let fake_win_size = Size { - cols: 120, - rows: 24, - }; - - let mut test_attempts = 10; - let last_snapshot = loop { - RemoteRunner::kill_running_sessions(fake_win_size); - let mut runner = RemoteRunner::new(fake_win_size).add_step(Step { - name: "Undo tab name change", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.status_bar_appears() - && remote_terminal.snapshot_contains("Tab #1") - { - remote_terminal.send_key(&TAB_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&RENAME_TAB_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&[97, 97]); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&ESC); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&ESC); - step_is_complete = true; - } - step_is_complete - }, - }); - runner.run_all_steps(); - - let last_snapshot = runner.take_snapshot_after(Step { - name: "Wait for tab name to apper on screen", - instruction: |remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.snapshot_contains("Tab #1") - && remote_terminal.snapshot_contains("LOCK") - { - step_is_complete = true - } - step_is_complete - }, - }); - - if runner.test_timed_out && test_attempts > 0 { - test_attempts -= 1; - continue; - } else { - break last_snapshot; - } - }; - let last_snapshot = account_for_races_in_snapshot(last_snapshot); - assert_snapshot!(last_snapshot); -} - -#[test] -#[ignore] -pub fn undo_rename_pane() { - let fake_win_size = Size { - cols: 120, - rows: 24, - }; - - let mut test_attempts = 10; - let last_snapshot = loop { - RemoteRunner::kill_running_sessions(fake_win_size); - let mut runner = RemoteRunner::new(fake_win_size).add_step(Step { - name: "Undo pane name change", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(3, 2) - { - remote_terminal.send_key(&PANE_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&RENAME_PANE_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&[97, 97]); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&ESC); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&ESC); - step_is_complete = true; - } - step_is_complete - }, - }); - runner.run_all_steps(); - - let last_snapshot = runner.take_snapshot_after(Step { - name: "Wait for pane name to apper on screen", - instruction: |remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.snapshot_contains("Pane #1") - && remote_terminal.snapshot_contains("LOCK") - { - step_is_complete = true - } - step_is_complete - }, - }); - - if runner.test_timed_out && test_attempts > 0 { - test_attempts -= 1; - continue; - } else { - break last_snapshot; - } - }; - let last_snapshot = account_for_races_in_snapshot(last_snapshot); - assert_snapshot!(last_snapshot); -} - -#[test] -#[ignore] -pub fn send_command_through_the_cli() { - // here we test the following flow: - // - send a command through the cli to run a bash script in a temporary folder - // - have it open a "command pane" that can be re-run with Enter - // - press Enter in the command pane to re-run the script - // - // the script appends the word "foo" to a temporary file and then `cat`s that file, - // so when we press "Enter", it will run again and we'll see two "foo"s one after the other, - // that's how we know the whole flow is working - let fake_win_size = Size { - cols: 150, - rows: 24, - }; - let mut test_attempts = 10; - let last_snapshot = loop { - RemoteRunner::kill_running_sessions(fake_win_size); - let mut runner = RemoteRunner::new(fake_win_size) - .add_step(Step { - name: "Run command through the cli", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.status_bar_appears() - && remote_terminal.cursor_position_is(3, 2) - { - let fixture_folder = remote_terminal.path_to_fixture_folder(); - remote_terminal.send_command_through_the_cli(&format!( - "{}/append-echo-script.sh", - fixture_folder - )); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&ENTER); - std::thread::sleep(std::time::Duration::from_millis(100)); - step_is_complete = true; - } - step_is_complete - }, - }) - .add_step(Step { - name: "Initial run of suspended command", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.snapshot_contains("<Ctrl-c>") { - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&SPACE); // run script - here we use SPACE - // instead of the default ENTER because - // sending ENTER over SSH can be a little - // problematic (read: I couldn't get it - // to pass consistently) - step_is_complete = true - } - step_is_complete - }, - }) - .add_step(Step { - name: "Wait for command to run", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.snapshot_contains("<Ctrl-c>") - && remote_terminal.cursor_position_is(76, 3) - { - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&SPACE); // re-run script - here we use SPACE - // instead of the default ENTER because - // sending ENTER over SSH can be a little - // problematic (read: I couldn't get it - // to pass consistently) - step_is_complete = true - } - step_is_complete - }, - }) - .add_step(Step { - name: "Wait for script to run again", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.snapshot_contains("<Ctrl-c>") - && remote_terminal.cursor_position_is(76, 4) - { - step_is_complete = true - } - step_is_complete - }, - }); - runner.run_all_steps(); - - let last_snapshot = runner.take_snapshot_after(Step { - name: "Wait for script to run twice", - instruction: |remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.snapshot_contains("foo") - && remote_terminal.cursor_position_is(76, 4) - { - step_is_complete = true - } - step_is_complete - }, - }); - - if runner.test_timed_out && test_attempts > 0 { - test_attempts -= 1; - continue; - } else { - break last_snapshot; - } - }; - let last_snapshot = account_for_races_in_snapshot(last_snapshot); - assert_snapshot!(last_snapshot); -} - -#[test] -#[ignore] pub fn send_blocking_command_through_the_cli() { // here we test the following flow: // - send a blocking command through the cli with --blocking --floating --close-on-exit @@ -2499,456 +435,3 @@ pub fn send_blocking_command_through_the_cli() { let last_snapshot = account_for_races_in_snapshot(last_snapshot); assert_snapshot!(last_snapshot); } - -#[test] -#[ignore] -pub fn load_plugins_in_background_on_startup() { - let fake_win_size = Size { - cols: 120, - rows: 24, - }; - let config_file_name = "load_background_plugins.kdl"; - let mut test_attempts = 10; - let mut test_timed_out = false; - let last_snapshot = loop { - RemoteRunner::kill_running_sessions(fake_win_size); - let mut runner = - RemoteRunner::new_with_config(fake_win_size, config_file_name).add_step(Step { - name: "Wait for plugin to load and request permissions", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.snapshot_contains("Allow? (y/n)") { - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key("y".as_bytes()); - step_is_complete = true; - } - step_is_complete - }, - }); - runner.run_all_steps(); - let last_snapshot = runner.take_snapshot_after(Step { - name: "Wait for plugin to disappear after permissions were granted", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if !remote_terminal.snapshot_contains("Allow? (y/n)") { - step_is_complete = true; - } - step_is_complete - }, - }); - if runner.test_timed_out && test_attempts > 0 { - test_attempts -= 1; - continue; - } else { - test_timed_out = runner.test_timed_out; - break last_snapshot; - } - }; - let last_snapshot = account_for_races_in_snapshot(last_snapshot); - assert!( - !test_timed_out, - "Test timed out, possibly waiting for permission request" - ); - assert_snapshot!(last_snapshot); -} - -#[test] -#[ignore] -pub fn pin_floating_panes() { - let fake_win_size = Size { - cols: 120, - rows: 24, - }; - let mut test_attempts = 10; - let last_snapshot = loop { - RemoteRunner::kill_running_sessions(fake_win_size); - let mut runner = RemoteRunner::new(fake_win_size) - .add_step(Step { - name: "Toggle floating panes", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.status_bar_appears() - && remote_terminal.cursor_position_is(3, 2) - { - remote_terminal.send_key(&PANE_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&TOGGLE_FLOATING_PANES); - step_is_complete = true; - } - step_is_complete - }, - }) - .add_step(Step { - name: "Pin floating pane", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.snapshot_contains("PIN [ ]") { - remote_terminal.send_key(&sgr_mouse_report(Position::new(8, 87), 0)); - step_is_complete = true; - } - step_is_complete - }, - }) - .add_step(Step { - name: "Focus underlying pane", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.snapshot_contains("PIN [+]") { - remote_terminal.send_key(&PANE_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&TOGGLE_FLOATING_PANES); - std::thread::sleep(std::time::Duration::from_millis(100)); - step_is_complete = true; - } - step_is_complete - }, - }) - .add_step(Step { - name: "Fill tiled pane with text", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.cursor_position_is(3, 2) { - remote_terminal.load_fixture("e2e/fill_for_pinned_pane"); - step_is_complete = true; - } - step_is_complete - }, - }) - .add_step(Step { - name: "Move cursor behind pinned pane", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.snapshot_contains("line") { - remote_terminal - .send_key(&format!(" hide_me").as_bytes().to_vec()); - step_is_complete = true; - } - step_is_complete - }, - }); - - runner.run_all_steps(); - let last_snapshot = runner.take_snapshot_after(Step { - name: "Wait for cursor to be behind pinned pane", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if !remote_terminal.snapshot_contains("LOCK") { - remote_terminal.send_key(&PANE_MODE); - } - if remote_terminal.snapshot_contains("hide") - && remote_terminal.snapshot_contains("LOCK") - { - // terminal has been filled with fixture text - step_is_complete = true; - } - step_is_complete - }, - }); - if runner.test_timed_out && test_attempts > 0 { - test_attempts -= 1; - continue; - } else { - break last_snapshot; - } - }; - let last_snapshot = account_for_races_in_snapshot(last_snapshot); - assert_snapshot!(last_snapshot); -} - -#[test] -#[ignore] -pub fn watcher_client_functionality() { - let fake_win_size = Size { - cols: 120, - rows: 24, - }; - let mut test_attempts = 10; - let session_name = "watcher_client_functionality"; - let (main_client_snapshot, watcher_snapshot) = loop { - RemoteRunner::kill_running_sessions(fake_win_size); - - // Step 1: Create main client and wait for it to load - let mut main_client = - RemoteRunner::new_with_session_name(fake_win_size, session_name, false) - .retry_pause_ms(200) - .dont_panic() - .add_step(Step { - name: "Wait for app to load", - instruction: |remote_terminal: RemoteTerminal| -> bool { - remote_terminal.status_bar_appears() - && remote_terminal.cursor_position_is(3, 2) - }, - }); - main_client.run_all_steps(); - - // Step 2: Start a foreground process that produces periodic output and then - // replaces the shell with a long sleep. Using exec means pane 1 never returns - // to an interactive shell prompt, so a SIGWINCH on re-attach cannot trigger a - // prompt redraw — the snapshot is deterministic. - main_client = main_client.add_step(Step { - name: "Start foreground output loop", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - remote_terminal.send_key( - b"for i in 1 2 3; do sleep 1; echo WATCHER_OUTPUT_$i; done; echo WATCHER_DONE; exec sleep 1000000", - ); - remote_terminal.send_key(&ENTER); - true - }, - }); - main_client.run_all_steps(); - - // Step 3: Wait for first output line to appear - main_client = main_client.add_step(Step { - name: "Wait for first output", - instruction: |remote_terminal: RemoteTerminal| -> bool { - let found = remote_terminal.snapshot_contains("WATCHER_OUTPUT_1"); - found - }, - }); - main_client.run_all_steps(); - - // Step 4: Attach watcher client - let mut watcher = RemoteRunner::new_watcher_session(fake_win_size, session_name) - .retry_pause_ms(200) - .dont_panic() - .add_step(Step { - name: "Wait for watcher to connect", - instruction: |remote_terminal: RemoteTerminal| -> bool { - remote_terminal.status_bar_appears() - // && remote_terminal.cursor_position_is(3, 2) - }, - }); - watcher.run_all_steps(); - - // Step 5: Main client splits pane right - main_client = main_client.add_step(Step { - name: "Split pane to the right", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - if remote_terminal.status_bar_appears() { - remote_terminal.send_key(&PANE_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - true - } else { - false - } - }, - }); - main_client.run_all_steps(); - - // Step 6: Verify watcher sees the split pane - watcher = watcher.add_step(Step { - name: "Watcher sees split pane", - instruction: |remote_terminal: RemoteTerminal| -> bool { - remote_terminal.status_bar_appears() && remote_terminal.snapshot_contains("┐┌") - // vertical split indicator - }, - }); - watcher.run_all_steps(); - - // Step 7: Watcher tries to open a new tab (should be ignored) - watcher = watcher.add_step(Step { - name: "Watcher attempts to open new tab", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - remote_terminal.send_key(&TAB_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&NEW_TAB_IN_TAB_MODE); - std::thread::sleep(std::time::Duration::from_millis(500)); - remote_terminal.send_key(b"some text that should not appear..."); - std::thread::sleep(std::time::Duration::from_millis(500)); - true - }, - }); - watcher.run_all_steps(); - - // Step 8: Verify main client didn't receive the tab command - main_client = main_client.add_step(Step { - name: "Verify no new tab was created", - instruction: |remote_terminal: RemoteTerminal| -> bool { - // Should still be in Tab #1, not Tab #2 - remote_terminal.snapshot_contains("Tab #1") - && !remote_terminal.snapshot_contains("Tab #2") - }, - }); - main_client.run_all_steps(); - - // Step 9: Watcher sends mouse click (should be ignored) - watcher = watcher.add_step(Step { - name: "Watcher attempts mouse click", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let click_position = Position { - line: Line(10), - column: Column(10), - }; - remote_terminal.send_key(&sgr_mouse_report(click_position, 0)); // left click - std::thread::sleep(std::time::Duration::from_millis(500)); - true - }, - }); - watcher.run_all_steps(); - - // Step 10: Main client detaches - main_client = main_client.add_step(Step { - name: "Main client detaches", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - remote_terminal.send_key(&SESSION_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&DETACH_IN_SESSION_MODE); - true - }, - }); - main_client.run_all_steps(); - - // Step 11: Verify watcher receives output even with no main client - watcher = watcher.add_step(Step { - name: "Watcher sees WATCHER_OUTPUT_2", - instruction: |remote_terminal: RemoteTerminal| -> bool { - remote_terminal.snapshot_contains("WATCHER_OUTPUT_2") - }, - }); - watcher.run_all_steps(); - - watcher = watcher.add_step(Step { - name: "Watcher sees WATCHER_OUTPUT_3", - instruction: |remote_terminal: RemoteTerminal| -> bool { - remote_terminal.snapshot_contains("WATCHER_OUTPUT_3") - }, - }); - watcher.run_all_steps(); - - // Step 12: Main client re-attaches - let mut main_client_reattached = - RemoteRunner::new_existing_session(fake_win_size, session_name) - .dont_panic() - .add_step(Step { - name: "Main client re-attaches", - instruction: |remote_terminal: RemoteTerminal| -> bool { - std::thread::sleep(std::time::Duration::from_millis(500)); // wait for watcher - // to fail running - // commands - remote_terminal.status_bar_appears() - && remote_terminal.snapshot_contains("┐┌") - }, - }); - main_client_reattached.run_all_steps(); - - watcher.run_all_steps(); - - // Take final snapshots — WATCHER_DONE means the loop finished and pane 1 is now - // running `sleep 1000000` (via exec), so no interactive shell and no prompt redraws. - let main_client_snapshot = main_client_reattached.take_snapshot_after(Step { - name: "Take main client final snapshot", - instruction: |remote_terminal: RemoteTerminal| -> bool { - remote_terminal.status_bar_appears() - && remote_terminal.snapshot_contains("WATCHER_DONE") - && remote_terminal.snapshot_contains("┐┌") - }, - }); - - let watcher_snapshot = watcher.take_snapshot_after(Step { - name: "Take watcher final snapshot", - instruction: |remote_terminal: RemoteTerminal| -> bool { - remote_terminal.status_bar_appears() - && remote_terminal.snapshot_contains("WATCHER_DONE") - && remote_terminal.snapshot_contains("┐┌") - }, - }); - - if (main_client_reattached.test_timed_out || watcher.test_timed_out) && test_attempts > 0 { - test_attempts -= 1; - continue; - } else { - break (main_client_snapshot, watcher_snapshot); - } - }; - - let main_client_snapshot = account_for_races_in_snapshot(main_client_snapshot); - let watcher_snapshot = account_for_races_in_snapshot(watcher_snapshot); - assert_snapshot!(main_client_snapshot); - assert_snapshot!(watcher_snapshot); -} - -#[test] -#[ignore] -pub fn override_layout_from_default_to_compact() { - let fake_win_size = Size { - cols: 120, - rows: 24, - }; - let mut test_attempts = 10; - let last_snapshot = loop { - RemoteRunner::kill_running_sessions(fake_win_size); - - let mut runner = RemoteRunner::new(fake_win_size) - .add_step(Step { - name: "Wait for default layout to load", - instruction: |remote_terminal: RemoteTerminal| -> bool { - remote_terminal.status_bar_appears() - }, - }) - .add_step(Step { - name: "Override to compact layout", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - remote_terminal.run_zellij_action("override-layout compact"); - std::thread::sleep(std::time::Duration::from_millis(500)); - remote_terminal.send_key(&ENTER); - true - }, - }); - - runner.run_all_steps(); - let last_snapshot = runner.take_snapshot_after(Step { - name: "Wait for compact layout to appear", - instruction: |remote_terminal: RemoteTerminal| -> bool { - // wait for both the layout indicator and the session name to render - remote_terminal.snapshot_contains("NORMAL") - && remote_terminal.snapshot_contains("e2e-test") - }, - }); - - if runner.test_timed_out && test_attempts > 0 { - test_attempts -= 1; - continue; - } else { - break last_snapshot; - } - }; - - let last_snapshot = account_for_races_in_snapshot(last_snapshot); - assert_snapshot!(last_snapshot); -} - -#[test] -#[ignore] -pub fn use_custom_layout_with_relative_path() { - let fake_win_size = Size { - cols: 120, - rows: 24, - }; - // Should resolve to $fixtures/layouts/upside-down.kdl - let config_dir_name = "e2e-upside-down"; - let mut test_attempts = 10; - let last_snapshot = loop { - RemoteRunner::kill_running_sessions(fake_win_size); - let mut runner = RemoteRunner::new_with_config_dir(fake_win_size, config_dir_name); - runner.run_all_steps(); - let last_snapshot = runner.take_snapshot_after(Step { - name: "Wait for app to start", - instruction: |remote_terminal: RemoteTerminal| -> bool { - let lines = remote_terminal.lines(); - lines.len() > 2 - && lines[0].trim().starts_with("Ctrl +") - && lines[1].trim().starts_with("Tip:") - }, - }); - if runner.test_timed_out && test_attempts > 0 { - test_attempts -= 1; - continue; - } else { - break last_snapshot; - } - }; - let last_snapshot = account_for_races_in_snapshot(last_snapshot); - assert_snapshot!(last_snapshot); -} diff --git a/src/tests/e2e/mod.rs b/src/tests/e2e/mod.rs index 05f2cc8c..3cf63542 100644 --- a/src/tests/e2e/mod.rs +++ b/src/tests/e2e/mod.rs @@ -1,3 +1,2 @@ pub mod cases; mod remote_runner; -mod steps; diff --git a/src/tests/e2e/remote_runner.rs b/src/tests/e2e/remote_runner.rs index 958b8f1e..9f6eec44 100644 --- a/src/tests/e2e/remote_runner.rs +++ b/src/tests/e2e/remote_runner.rs @@ -12,15 +12,11 @@ use ssh2::Session; use std::io::prelude::*; use std::net::TcpStream; -use std::path::Path; - use std::cell::RefCell; use std::rc::Rc; const ZELLIJ_EXECUTABLE_LOCATION: &str = "/usr/src/zellij/zellij"; const SET_ENV_VARIABLES: &str = "EDITOR=/usr/bin/vi"; -const ZELLIJ_CONFIG_PATH: &str = "/usr/src/zellij/fixtures/configs"; -const ZELLIJ_CONFIG_DIRS_PATH: &str = "/usr/src/zellij/fixtures/config-dirs"; const ZELLIJ_DATA_DIR: &str = "/usr/src/zellij/e2e-data"; const ZELLIJ_FIXTURE_PATH: &str = "/usr/src/zellij/fixtures"; const CONNECTION_STRING: &str = "127.0.0.1:2222"; @@ -40,16 +36,6 @@ fn ssh_connect() -> ssh2::Session { sess } -fn ssh_connect_without_timeout() -> ssh2::Session { - let tcp = TcpStream::connect(CONNECTION_STRING).unwrap(); - let mut sess = Session::new().unwrap(); - sess.set_tcp_stream(tcp); - sess.handshake().unwrap(); - sess.userauth_password(CONNECTION_USERNAME, CONNECTION_PASSWORD) - .unwrap(); - sess -} - fn setup_remote_environment(channel: &mut ssh2::Channel, win_size: Size) { let (columns, rows) = (win_size.cols as u32, win_size.rows as u32); channel @@ -99,34 +85,6 @@ fn start_zellij(channel: &mut ssh2::Channel) { channel.flush().unwrap(); } -fn start_zellij_with_config_dir(channel: &mut ssh2::Channel, config_dir: &str) { - stop_zellij(channel); - channel - .write_all( - format!( - "{} {} --session {} --data-dir {} --config-dir {} options --show-release-notes false --show-startup-tips false\n", - SET_ENV_VARIABLES, ZELLIJ_EXECUTABLE_LOCATION, SESSION_NAME, ZELLIJ_DATA_DIR, format!("{}/{}", ZELLIJ_CONFIG_DIRS_PATH, config_dir) - ) - .as_bytes(), - ) - .unwrap(); - channel.flush().unwrap(); -} - -fn start_zellij_mirrored_session(channel: &mut ssh2::Channel) { - stop_zellij(channel); - channel - .write_all( - format!( - "{} {} --session {} --data-dir {} options --show-release-notes false --show-startup-tips false --mirror-session true --serialization-interval 1\n", - SET_ENV_VARIABLES, ZELLIJ_EXECUTABLE_LOCATION, SESSION_NAME, ZELLIJ_DATA_DIR - ) - .as_bytes(), - ) - .unwrap(); - channel.flush().unwrap(); -} - fn start_zellij_mirrored_session_with_layout(channel: &mut ssh2::Channel, layout_file_name: &str) { stop_zellij(channel); channel @@ -145,103 +103,6 @@ fn start_zellij_mirrored_session_with_layout(channel: &mut ssh2::Channel, layout channel.flush().unwrap(); } -fn start_zellij_mirrored_session_with_layout_and_viewport_serialization( - channel: &mut ssh2::Channel, - layout_file_name: &str, -) { - stop_zellij(channel); - channel - .write_all( - format!( - "{} {} --session {} --data-dir {} --new-session-with-layout {} options --show-release-notes false --show-startup-tips false --mirror-session true --serialize-pane-viewport true --serialization-interval 1\n", - SET_ENV_VARIABLES, - ZELLIJ_EXECUTABLE_LOCATION, - SESSION_NAME, - ZELLIJ_DATA_DIR, - format!("{}/{}", ZELLIJ_FIXTURE_PATH, layout_file_name) - ) - .as_bytes(), - ) - .unwrap(); - channel.flush().unwrap(); -} - -fn start_zellij_in_session(channel: &mut ssh2::Channel, session_name: &str, mirrored: bool) { - stop_zellij(channel); - channel - .write_all( - format!( - "{} {} --session {} --data-dir {} options --show-release-notes false --show-startup-tips false --mirror-session {}\n", - SET_ENV_VARIABLES, - ZELLIJ_EXECUTABLE_LOCATION, - session_name, - ZELLIJ_DATA_DIR, - mirrored - ) - .as_bytes(), - ) - .unwrap(); - channel.flush().unwrap(); -} - -fn attach_to_existing_session(channel: &mut ssh2::Channel, session_name: &str) { - channel - .write_all( - format!( - "{} {} attach {}\n", - SET_ENV_VARIABLES, ZELLIJ_EXECUTABLE_LOCATION, session_name - ) - .as_bytes(), - ) - .unwrap(); - channel.flush().unwrap(); -} - -fn watch_existing_session(channel: &mut ssh2::Channel, session_name: &str) { - channel - .write_all( - format!( - "{} {} watch {}\n", - SET_ENV_VARIABLES, ZELLIJ_EXECUTABLE_LOCATION, session_name - ) - .as_bytes(), - ) - .unwrap(); - channel.flush().unwrap(); -} - -fn start_zellij_without_frames(channel: &mut ssh2::Channel) { - stop_zellij(channel); - channel - .write_all( - format!( - "{} {} --session {} --data-dir {} options --show-release-notes false --show-startup-tips false --pane-frames false\n", - SET_ENV_VARIABLES, ZELLIJ_EXECUTABLE_LOCATION, SESSION_NAME, ZELLIJ_DATA_DIR - ) - .as_bytes(), - ) - .unwrap(); - channel.flush().unwrap(); -} - -fn start_zellij_with_config(channel: &mut ssh2::Channel, config_path: &str) { - stop_zellij(channel); - channel - .write_all( - format!( - "{} {} --config {} --session {} --data-dir {} options --show-release-notes false --show-startup-tips false\n", - SET_ENV_VARIABLES, - ZELLIJ_EXECUTABLE_LOCATION, - config_path, - SESSION_NAME, - ZELLIJ_DATA_DIR - ) - .as_bytes(), - ) - .unwrap(); - channel.flush().unwrap(); -} - fn wait_for_startup(last_snapshot: &Arc<Mutex<String>>) { let start = std::time::Instant::now(); let timeout = std::time::Duration::from_secs(10); @@ -441,10 +302,6 @@ impl RemoteTerminal { pub fn snapshot_contains(&self, text: &str) -> bool { self.last_snapshot.lock().unwrap().contains(text) } - pub fn lines(&self) -> Vec<String> { - let s = self.last_snapshot.lock().unwrap(); - s.lines().map(|s| s.to_owned()).collect::<Vec<_>>() - } #[allow(unused)] pub fn current_snapshot(&self) -> String { // convenience method for writing tests, @@ -483,29 +340,6 @@ impl RemoteTerminal { } // release mutex before sleeping so the reader thread can process Zellij's startup output std::thread::sleep(std::time::Duration::from_secs(1)); // wait until Zellij stops parsing startup ANSI codes from the terminal STDIN } - pub fn run_zellij_action(&mut self, action_and_arguments: &str) { - let mut channel = self.channel.lock().unwrap(); - channel - .write_all( - format!( - "{} action {}", - ZELLIJ_EXECUTABLE_LOCATION, action_and_arguments - ) - .as_bytes(), - ) - .unwrap(); - channel.flush().unwrap(); - } - pub fn send_command_through_the_cli(&mut self, command: &str) { - let mut channel = self.channel.lock().unwrap(); - channel - .write_all( - // note that this is run with the -s flag that suspends the command on startup - format!("{} run -s -- \"{}\"", ZELLIJ_EXECUTABLE_LOCATION, command).as_bytes(), - ) - .unwrap(); - channel.flush().unwrap(); - } pub fn send_blocking_command_through_the_cli(&mut self, command: &str) { let mut channel = self.channel.lock().unwrap(); channel @@ -519,16 +353,6 @@ impl RemoteTerminal { .unwrap(); channel.flush().unwrap(); } - pub fn path_to_fixture_folder(&self) -> String { - ZELLIJ_FIXTURE_PATH.to_string() - } - pub fn load_fixture(&mut self, name: &str) { - let mut channel = self.channel.lock().unwrap(); - channel - .write_all(format!("cat {ZELLIJ_FIXTURE_PATH}/{name}\n").as_bytes()) - .unwrap(); - channel.flush().unwrap(); - } } #[derive(Clone)] @@ -544,7 +368,6 @@ pub struct RemoteRunner { currently_running_step: Option<String>, retries_left: usize, retry_pause_ms: usize, - panic_on_no_retries_left: bool, last_snapshot: Arc<Mutex<String>>, cursor_coordinates: Arc<Mutex<(usize, usize)>>, // x, y reader_thread: (Arc<AtomicBool>, std::thread::JoinHandle<()>), @@ -585,85 +408,6 @@ impl RemoteRunner { retries_left: RETRIES, retry_pause_ms: 100, test_timed_out: false, - panic_on_no_retries_left: true, - last_snapshot, - cursor_coordinates, - reader_thread, - } - } - pub fn new_with_config_dir(win_size: Size, config_dir_name: &str) -> Self { - let sess = ssh_connect(); - let mut channel = sess.channel_session().unwrap(); - let mut rows = Dimension::fixed(win_size.rows); - let mut cols = Dimension::fixed(win_size.cols); - rows.set_inner(win_size.rows); - cols.set_inner(win_size.cols); - let pane_geom = PaneGeom { - x: 0, - y: 0, - rows, - cols, - stacked: None, - is_pinned: false, - logical_position: None, - }; - setup_remote_environment(&mut channel, win_size); - start_zellij_with_config_dir(&mut channel, config_dir_name); - let channel = Arc::new(Mutex::new(channel)); - let last_snapshot = Arc::new(Mutex::new(String::new())); - let cursor_coordinates = Arc::new(Mutex::new((0, 0))); - sess.set_blocking(false); - let reader_thread = - read_from_channel(&channel, &last_snapshot, &cursor_coordinates, &pane_geom); - wait_for_startup(&last_snapshot); - RemoteRunner { - steps: vec![], - channel, - currently_running_step: None, - current_step_index: 0, - retries_left: RETRIES, - retry_pause_ms: 100, - test_timed_out: false, - panic_on_no_retries_left: true, - last_snapshot, - cursor_coordinates, - reader_thread, - } - } - pub fn new_mirrored_session(win_size: Size) -> Self { - let sess = ssh_connect(); - let mut channel = sess.channel_session().unwrap(); - let mut rows = Dimension::fixed(win_size.rows); - let mut cols = Dimension::fixed(win_size.cols); - rows.set_inner(win_size.rows); - cols.set_inner(win_size.cols); - let pane_geom = PaneGeom { - x: 0, - y: 0, - rows, - cols, - stacked: None, - is_pinned: false, - logical_position: None, - }; - setup_remote_environment(&mut channel, win_size); - start_zellij_mirrored_session(&mut channel); - let channel = Arc::new(Mutex::new(channel)); - let last_snapshot = Arc::new(Mutex::new(String::new())); - let cursor_coordinates = Arc::new(Mutex::new((0, 0))); - sess.set_blocking(false); - let reader_thread = - read_from_channel(&channel, &last_snapshot, &cursor_coordinates, &pane_geom); - wait_for_startup(&last_snapshot); - RemoteRunner { - steps: vec![], - channel, - currently_running_step: None, - current_step_index: 0, - retries_left: RETRIES, - retry_pause_ms: 100, - test_timed_out: false, - panic_on_no_retries_left: true, last_snapshot, cursor_coordinates, reader_thread, @@ -702,52 +446,6 @@ impl RemoteRunner { retries_left: RETRIES, retry_pause_ms: 100, test_timed_out: false, - panic_on_no_retries_left: true, - last_snapshot, - cursor_coordinates, - reader_thread, - } - } - pub fn new_mirrored_session_with_layout_and_viewport_serialization( - win_size: Size, - layout_file_name: &str, - ) -> Self { - let sess = ssh_connect(); - let mut channel = sess.channel_session().unwrap(); - let mut rows = Dimension::fixed(win_size.rows); - let mut cols = Dimension::fixed(win_size.cols); - rows.set_inner(win_size.rows); - cols.set_inner(win_size.cols); - let pane_geom = PaneGeom { - x: 0, - y: 0, - rows, - cols, - stacked: None, - is_pinned: false, - logical_position: None, - }; - setup_remote_environment(&mut channel, win_size); - start_zellij_mirrored_session_with_layout_and_viewport_serialization( - &mut channel, - layout_file_name, - ); - let channel = Arc::new(Mutex::new(channel)); - let last_snapshot = Arc::new(Mutex::new(String::new())); - let cursor_coordinates = Arc::new(Mutex::new((0, 0))); - sess.set_blocking(false); - let reader_thread = - read_from_channel(&channel, &last_snapshot, &cursor_coordinates, &pane_geom); - wait_for_startup(&last_snapshot); - RemoteRunner { - steps: vec![], - channel, - currently_running_step: None, - current_step_index: 0, - retries_left: RETRIES, - retry_pause_ms: 100, - test_timed_out: false, - panic_on_no_retries_left: true, last_snapshot, cursor_coordinates, reader_thread, @@ -765,207 +463,6 @@ impl RemoteRunner { sess.set_blocking(false); wait_for_shell_output(&mut channel, CLEANUP_DONE_MARKER); } - pub fn new_with_session_name(win_size: Size, session_name: &str, mirrored: bool) -> Self { - // notice that this method does not have a timeout, so use with caution! - let sess = ssh_connect_without_timeout(); - let mut channel = sess.channel_session().unwrap(); - let mut rows = Dimension::fixed(win_size.rows); - let mut cols = Dimension::fixed(win_size.cols); - rows.set_inner(win_size.rows); - cols.set_inner(win_size.cols); - let pane_geom = PaneGeom { - x: 0, - y: 0, - rows, - cols, - stacked: None, - is_pinned: false, - logical_position: None, - }; - setup_remote_environment(&mut channel, win_size); - start_zellij_in_session(&mut channel, session_name, mirrored); - let channel = Arc::new(Mutex::new(channel)); - let last_snapshot = Arc::new(Mutex::new(String::new())); - let cursor_coordinates = Arc::new(Mutex::new((0, 0))); - sess.set_blocking(false); - let reader_thread = - read_from_channel(&channel, &last_snapshot, &cursor_coordinates, &pane_geom); - wait_for_startup(&last_snapshot); - RemoteRunner { - steps: vec![], - channel, - currently_running_step: None, - current_step_index: 0, - retries_left: RETRIES, - retry_pause_ms: 100, - test_timed_out: false, - panic_on_no_retries_left: true, - last_snapshot, - cursor_coordinates, - reader_thread, - } - } - pub fn new_existing_session(win_size: Size, session_name: &str) -> Self { - let sess = ssh_connect_without_timeout(); - let mut channel = sess.channel_session().unwrap(); - let mut rows = Dimension::fixed(win_size.rows); - let mut cols = Dimension::fixed(win_size.cols); - rows.set_inner(win_size.rows); - cols.set_inner(win_size.cols); - let pane_geom = PaneGeom { - x: 0, - y: 0, - rows, - cols, - stacked: None, - is_pinned: false, - logical_position: None, - }; - setup_remote_environment(&mut channel, win_size); - attach_to_existing_session(&mut channel, session_name); - let channel = Arc::new(Mutex::new(channel)); - let last_snapshot = Arc::new(Mutex::new(String::new())); - let cursor_coordinates = Arc::new(Mutex::new((0, 0))); - sess.set_blocking(false); - let reader_thread = - read_from_channel(&channel, &last_snapshot, &cursor_coordinates, &pane_geom); - wait_for_startup(&last_snapshot); - RemoteRunner { - steps: vec![], - channel, - currently_running_step: None, - current_step_index: 0, - retries_left: RETRIES, - retry_pause_ms: 100, - test_timed_out: false, - panic_on_no_retries_left: true, - last_snapshot, - cursor_coordinates, - reader_thread, - } - } - pub fn new_watcher_session(win_size: Size, session_name: &str) -> Self { - let sess = ssh_connect_without_timeout(); - let mut channel = sess.channel_session().unwrap(); - let mut rows = Dimension::fixed(win_size.rows); - let mut cols = Dimension::fixed(win_size.cols); - rows.set_inner(win_size.rows); - cols.set_inner(win_size.cols); - let pane_geom = PaneGeom { - x: 0, - y: 0, - rows, - cols, - stacked: None, - is_pinned: false, - logical_position: None, - }; - setup_remote_environment(&mut channel, win_size); - watch_existing_session(&mut channel, session_name); - let channel = Arc::new(Mutex::new(channel)); - let last_snapshot = Arc::new(Mutex::new(String::new())); - let cursor_coordinates = Arc::new(Mutex::new((0, 0))); - sess.set_blocking(false); - let reader_thread = - read_from_channel(&channel, &last_snapshot, &cursor_coordinates, &pane_geom); - wait_for_startup(&last_snapshot); - RemoteRunner { - steps: vec![], - channel, - currently_running_step: None, - current_step_index: 0, - retries_left: RETRIES, - retry_pause_ms: 100, - test_timed_out: false, - panic_on_no_retries_left: true, - last_snapshot, - cursor_coordinates, - reader_thread, - } - } - pub fn new_without_frames(win_size: Size) -> Self { - let sess = ssh_connect(); - let mut channel = sess.channel_session().unwrap(); - let mut rows = Dimension::fixed(win_size.rows); - let mut cols = Dimension::fixed(win_size.cols); - rows.set_inner(win_size.rows); - cols.set_inner(win_size.cols); - let pane_geom = PaneGeom { - x: 0, - y: 0, - rows, - cols, - stacked: None, - is_pinned: false, - logical_position: None, - }; - setup_remote_environment(&mut channel, win_size); - start_zellij_without_frames(&mut channel); - let channel = Arc::new(Mutex::new(channel)); - let last_snapshot = Arc::new(Mutex::new(String::new())); - let cursor_coordinates = Arc::new(Mutex::new((0, 0))); - sess.set_blocking(false); - let reader_thread = - read_from_channel(&channel, &last_snapshot, &cursor_coordinates, &pane_geom); - wait_for_startup(&last_snapshot); - RemoteRunner { - steps: vec![], - channel, - currently_running_step: None, - current_step_index: 0, - retries_left: RETRIES, - retry_pause_ms: 100, - test_timed_out: false, - panic_on_no_retries_left: true, - last_snapshot, - cursor_coordinates, - reader_thread, - } - } - pub fn new_with_config(win_size: Size, config_file_name: &'static str) -> Self { - let remote_path = Path::new(ZELLIJ_CONFIG_PATH).join(config_file_name); - let sess = ssh_connect(); - let mut channel = sess.channel_session().unwrap(); - let mut rows = Dimension::fixed(win_size.rows); - let mut cols = Dimension::fixed(win_size.cols); - rows.set_inner(win_size.rows); - cols.set_inner(win_size.cols); - let pane_geom = PaneGeom { - x: 0, - y: 0, - rows, - cols, - stacked: None, - is_pinned: false, - logical_position: None, - }; - setup_remote_environment(&mut channel, win_size); - start_zellij_with_config(&mut channel, &remote_path.to_string_lossy()); - let channel = Arc::new(Mutex::new(channel)); - let last_snapshot = Arc::new(Mutex::new(String::new())); - let cursor_coordinates = Arc::new(Mutex::new((0, 0))); - sess.set_blocking(false); - let reader_thread = - read_from_channel(&channel, &last_snapshot, &cursor_coordinates, &pane_geom); - wait_for_startup(&last_snapshot); - RemoteRunner { - steps: vec![], - channel, - currently_running_step: None, - current_step_index: 0, - retries_left: RETRIES, - retry_pause_ms: 100, - test_timed_out: false, - panic_on_no_retries_left: true, - last_snapshot, - cursor_coordinates, - reader_thread, - } - } - pub fn dont_panic(mut self) -> Self { - self.panic_on_no_retries_left = false; - self - } #[allow(unused)] pub fn retry_pause_ms(mut self, retry_pause_ms: usize) -> Self { self.retry_pause_ms = retry_pause_ms; diff --git a/src/tests/e2e/steps.rs b/src/tests/e2e/steps.rs deleted file mode 100644 index cc8b5c5d..00000000 --- a/src/tests/e2e/steps.rs +++ /dev/null @@ -1,152 +0,0 @@ -use super::cases::{ - MOVE_FOCUS_LEFT_IN_NORMAL_MODE, MOVE_TAB_LEFT, MOVE_TAB_RIGHT, NEW_TAB_IN_TAB_MODE, - SECOND_TAB_CONTENT, TAB_MODE, -}; -use super::remote_runner::{RemoteTerminal, Step}; - -pub fn new_tab() -> Step { - Step { - name: "Open new tab", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.status_bar_appears() { - remote_terminal.send_key(&TAB_MODE); - std::thread::sleep(std::time::Duration::from_millis(100)); - remote_terminal.send_key(&NEW_TAB_IN_TAB_MODE); - step_is_complete = true; - } - step_is_complete - }, - } -} - -pub fn check_second_tab_opened() -> Step { - Step { - name: "Check second tab opened", - instruction: |remote_terminal: RemoteTerminal| -> bool { - remote_terminal.status_bar_appears() && remote_terminal.snapshot_contains("Tab #2") - }, - } -} - -pub fn move_tab_left() -> Step { - Step { - name: "Move tab left", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.status_bar_appears() { - remote_terminal.send_key(&MOVE_TAB_LEFT); - std::thread::sleep(std::time::Duration::from_millis(100)); - step_is_complete = true; - } - step_is_complete - }, - } -} - -pub fn check_third_tab_moved_left() -> Step { - Step { - name: "Check third tab is in the middle", - instruction: |remote_terminal: RemoteTerminal| -> bool { - remote_terminal.status_bar_appears() - && remote_terminal.snapshot_contains("Tab #1 Tab #3 Tab #2") - }, - } -} - -pub fn type_second_tab_content() -> Step { - Step { - name: "Type second tab content", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - if remote_terminal.snapshot_contains("Tab #2 content") { - return true; - } - // send (or re-send if the previous attempt arrived garbled) and wait for the next - // retry to confirm the content appeared correctly in the terminal - if remote_terminal.status_bar_appears() { - remote_terminal.send_key(&SECOND_TAB_CONTENT); - // sleep longer than the reader's idle poll (100ms) so the snapshot is updated - // before the next retry fires, preventing a spurious re-send - std::thread::sleep(std::time::Duration::from_millis(300)); - } - false - }, - } -} - -pub fn check_third_tab_opened() -> Step { - Step { - name: "Check third tab opened", - instruction: |remote_terminal: RemoteTerminal| -> bool { - remote_terminal.status_bar_appears() && remote_terminal.snapshot_contains("Tab #3") - }, - } -} - -pub fn switch_focus_to_left_tab() -> Step { - Step { - name: "Move focus to tab on the left", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.status_bar_appears() { - remote_terminal.send_key(&MOVE_FOCUS_LEFT_IN_NORMAL_MODE); - step_is_complete = true; - } - step_is_complete - }, - } -} - -pub fn check_focus_on_second_tab() -> Step { - Step { - name: "Check focus is on the second tab", - instruction: |remote_terminal: RemoteTerminal| -> bool { - remote_terminal.status_bar_appears() - && remote_terminal.snapshot_contains("Tab #2 content") - }, - } -} - -pub fn move_tab_right() -> Step { - Step { - name: "Move tab right", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - let mut step_is_complete = false; - if remote_terminal.status_bar_appears() { - remote_terminal.send_key(&MOVE_TAB_RIGHT); - step_is_complete = true; - } - step_is_complete - }, - } -} - -pub fn check_third_tab_moved_to_beginning() -> Step { - Step { - name: "Check third tab moved to beginning", - instruction: |remote_terminal: RemoteTerminal| -> bool { - remote_terminal.status_bar_appears() - && remote_terminal.snapshot_contains("Tab #3 Tab #1 Tab #2") - }, - } -} - -pub fn check_third_tab_is_left_wrapped() -> Step { - Step { - name: "Check third tab is in last position", - instruction: |remote_terminal: RemoteTerminal| -> bool { - remote_terminal.status_bar_appears() - && remote_terminal.snapshot_contains("Tab #2 Tab #1 Tab #3") - }, - } -} - -pub fn check_third_tab_is_right_wrapped() -> Step { - Step { - name: "Check third tab is in last position", - instruction: |remote_terminal: RemoteTerminal| -> bool { - remote_terminal.status_bar_appears() - && remote_terminal.snapshot_contains("Tab #3 Tab #2 Tab #1") - }, - } -} diff --git a/xtask/src/flags.rs b/xtask/src/flags.rs index 3814cd2c..82302e45 100644 --- a/xtask/src/flags.rs +++ b/xtask/src/flags.rs @@ -105,6 +105,17 @@ xflags::xflags! { repeated args: OsString } + /// Run the in-process whole-app integration tests + cmd integration-test { + /// Build with the default dev profile instead of dev-opt + /// (skips the one-time optimized dependency build, tests run ~7x slower) + optional --no-opt + /// Run the tests one at a time instead of in parallel + optional --serial + /// Arguments to pass to the test runner + repeated args: OsString + } + /// Build the application and all plugins cmd build { /// Build in release mode without debug symbols @@ -139,6 +150,7 @@ pub enum XtaskCmd { Run(Run), Format(Format), Test(Test), + IntegrationTest(IntegrationTest), Build(Build), } @@ -233,6 +245,14 @@ pub struct Test { } #[derive(Debug)] +pub struct IntegrationTest { + pub args: Vec<OsString>, + + pub no_opt: bool, + pub serial: bool, +} + +#[derive(Debug)] pub struct Build { pub release: bool, pub plugins_only: bool, diff --git a/xtask/src/integration_test.rs b/xtask/src/integration_test.rs new file mode 100644 index 00000000..88eb1154 --- /dev/null +++ b/xtask/src/integration_test.rs @@ -0,0 +1,47 @@ +use crate::flags; +use anyhow::Context; +use xshell::{cmd, Shell}; + +pub fn integration_test(sh: &Shell, flags: flags::IntegrationTest) -> anyhow::Result<()> { + let err_context = "failed to run task 'integration-test'"; + + let _pd = sh.push_dir(crate::project_root()); + let cargo = crate::cargo().context(err_context)?; + + let msg = ">> Running whole-app integration tests"; + crate::status(msg); + println!("{}", msg); + + let profile = if flags.no_opt { "dev" } else { "dev-opt" }; + + let nextest_available = cmd!(sh, "{cargo} nextest --version") + .quiet() + .ignore_stderr() + .read() + .is_ok(); + if nextest_available { + let serial_args: &[&str] = if flags.serial { + &["--test-threads=1"] + } else { + &[] + }; + cmd!( + sh, + "{cargo} nextest run --cargo-profile {profile} -p zellij-integration-tests" + ) + .args(serial_args) + .args(&flags.args) + .run() + .context(err_context)?; + } else { + println!(">> cargo-nextest not found, falling back to `cargo test -- --test-threads=1`"); + cmd!( + sh, + "{cargo} test --profile {profile} -p zellij-integration-tests -- --test-threads=1" + ) + .args(&flags.args) + .run() + .context(err_context)?; + } + Ok(()) +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 6262df1f..0e99d0b3 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -12,6 +12,7 @@ mod clippy; mod dist; mod flags; mod format; +mod integration_test; mod metadata; mod pipelines; mod test; @@ -131,6 +132,7 @@ fn main() -> anyhow::Result<()> { flags::XtaskCmd::Clippy(flags) => clippy::clippy(shell, flags), flags::XtaskCmd::Format(flags) => format::format(shell, flags), flags::XtaskCmd::Test(flags) => test::test(shell, flags), + flags::XtaskCmd::IntegrationTest(flags) => integration_test::integration_test(shell, flags), flags::XtaskCmd::Manpage(_flags) => build::manpage(shell), // Pipelines // These are composite commands, made up of multiple "stages" defined above. diff --git a/zellij-client/src/cli_client.rs b/zellij-client/src/cli_client.rs index 5887035b..faeb88c1 100644 --- a/zellij-client/src/cli_client.rs +++ b/zellij-client/src/cli_client.rs @@ -20,7 +20,7 @@ pub fn start_cli_client( mut os_input: Box<dyn ClientOsApi>, session_name: &str, actions: Vec<Action>, -) { +) -> i32 { let zellij_ipc_pipe: PathBuf = { let mut sock_dir = zellij_utils::consts::ZELLIJ_SOCK_DIR.clone(); fs::create_dir_all(&sock_dir).unwrap(); @@ -68,11 +68,16 @@ pub fn start_cli_client( ); }, action => { - individual_messages_client(&mut os_input, action, pane_id); + if let Some(exit_status) = + individual_messages_client(&mut os_input, action, pane_id) + { + return exit_status; + } }, } } os_input.send_to_server(ClientToServerMsg::ClientExited); + 0 } fn pipe_client( @@ -208,7 +213,7 @@ fn individual_messages_client( os_input: &mut Box<dyn ClientOsApi>, action: Action, pane_id: Option<u32>, -) { +) -> Option<i32> { let is_blocking = matches!( &action, Action::NewBlockingPane { @@ -226,26 +231,26 @@ fn individual_messages_client( loop { match os_input.recv_from_server() { Some((ServerToClientMsg::UnblockInputThread, _)) if !is_blocking => { - break; + return None; }, Some((ServerToClientMsg::Log { lines: log_lines }, _)) => { log_lines.iter().for_each(|line| println!("{line}")); - break; + return None; }, Some((ServerToClientMsg::LogError { lines: log_lines }, _)) => { log_lines.iter().for_each(|line| eprintln!("{line}")); - process::exit(2); + return Some(2); }, Some((ServerToClientMsg::Exit { exit_reason }, _)) => match exit_reason { ExitReason::Error(e) => { eprintln!("{}", e); - process::exit(2); + return Some(2); }, ExitReason::CustomExitStatus(exit_status) => { - process::exit(exit_status); + return Some(exit_status); }, _ => { - break; + return None; }, }, _ => {}, diff --git a/zellij-client/src/lib.rs b/zellij-client/src/lib.rs index d674e022..2c8cc950 100644 --- a/zellij-client/src/lib.rs +++ b/zellij-client/src/lib.rs @@ -879,7 +879,7 @@ pub fn start_client( os_input.update_session_name(name); let ipc_pipe = create_ipc_pipe(); - spawn_server(&*ipc_pipe, cli_args.debug).unwrap(); + os_input.spawn_server(&*ipc_pipe, cli_args.debug).unwrap(); if should_start_web_server { if let Err(e) = spawn_web_server(&cli_args) { log::error!("Failed to start web server: {}", e); @@ -933,7 +933,7 @@ pub fn start_client( os_input.update_session_name(name); let ipc_pipe = create_ipc_pipe(); - spawn_server(&*ipc_pipe, cli_args.debug).unwrap(); + os_input.spawn_server(&*ipc_pipe, cli_args.debug).unwrap(); if should_start_web_server { if let Err(e) = spawn_web_server(&cli_args) { log::error!("Failed to start web server: {}", e); @@ -971,18 +971,20 @@ pub fn start_client( > = channels::bounded(50); let send_input_instructions = SenderWithContext::new(send_input_instructions); - std::panic::set_hook({ - use zellij_utils::errors::handle_panic; - let send_client_instructions = send_client_instructions.clone(); - let os_input = os_input.clone(); - Box::new(move |info| { - os_input.disable_mouse().non_fatal(); - os_input.restore_console_mode(); - if let Ok(()) = os_input.unset_raw_mode() { - handle_panic(info, Some(&send_client_instructions)); - } - }) - }); + if os_input.should_install_panic_hook() { + std::panic::set_hook({ + use zellij_utils::errors::handle_panic; + let send_client_instructions = send_client_instructions.clone(); + let os_input = os_input.clone(); + Box::new(move |info| { + os_input.disable_mouse().non_fatal(); + os_input.restore_console_mode(); + if let Ok(()) = os_input.unset_raw_mode() { + handle_panic(info, Some(&send_client_instructions)); + } + }) + }); + } let on_force_close = config_options.on_force_close.unwrap_or_default(); let stdin_ansi_parser = Arc::new(Mutex::new(StdinAnsiParser::new())); @@ -1333,7 +1335,7 @@ pub fn start_server_detached( os_input.update_session_name(name); let ipc_pipe = create_ipc_pipe(); - spawn_server(&*ipc_pipe, cli_args.debug).unwrap(); + os_input.spawn_server(&*ipc_pipe, cli_args.debug).unwrap(); if should_start_web_server { if let Err(e) = spawn_web_server(&cli_args) { log::error!("Failed to start web server: {}", e); @@ -1388,7 +1390,7 @@ pub fn start_server_detached( os_input.update_session_name(name); let ipc_pipe = create_ipc_pipe(); - spawn_server(&*ipc_pipe, cli_args.debug).unwrap(); + os_input.spawn_server(&*ipc_pipe, cli_args.debug).unwrap(); if should_start_web_server { if let Err(e) = spawn_web_server(&cli_args) { log::error!("Failed to start web server: {}", e); diff --git a/zellij-client/src/os_input_output.rs b/zellij-client/src/os_input_output.rs index d22e5f7b..5d6e937b 100644 --- a/zellij-client/src/os_input_output.rs +++ b/zellij-client/src/os_input_output.rs @@ -137,6 +137,12 @@ pub trait ClientOsApi: Send + Sync + std::fmt::Debug { ); /// Establish a connection with the server socket. fn connect_to_server(&self, path: &Path); + fn spawn_server(&self, socket_path: &Path, debug: bool) -> Result<(), std::io::Error> { + crate::spawn_server(socket_path, debug) + } + fn should_install_panic_hook(&self) -> bool { + true + } fn load_palette(&self) -> Palette; fn enable_mouse(&self) -> Result<()>; fn disable_mouse(&self) -> Result<()>; diff --git a/zellij-integration-tests/Cargo.toml b/zellij-integration-tests/Cargo.toml new file mode 100644 index 00000000..84f62b78 --- /dev/null +++ b/zellij-integration-tests/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "zellij-integration-tests" +description = "In-process whole-app integration test harness for Zellij" +publish = false +license.workspace = true +repository.workspace = true +edition.workspace = true +version.workspace = true +rust-version.workspace = true + +[dependencies] +zellij-client = { path = "../zellij-client", version = "0.45.0" } +zellij-server = { path = "../zellij-server", version = "0.45.0" } +zellij-utils = { workspace = true } +anyhow = { workspace = true } +async-trait = { workspace = true } +crossbeam = { version = "0.8", default-features = false, features = ["crossbeam-channel", "std"] } +interprocess = { workspace = true } +insta = { version = "1.6.0", features = ["backtrace"] } +log = { workspace = true } +regex = { workspace = true } +tokio = { workspace = true } +vte = { workspace = true } diff --git a/zellij-integration-tests/preview_snapshots.sh b/zellij-integration-tests/preview_snapshots.sh new file mode 100755 index 00000000..0831c4ea --- /dev/null +++ b/zellij-integration-tests/preview_snapshots.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -lt 1 || $# -gt 2 ]]; then + echo "usage: $0 <test_name> [file]" >&2 + exit 1 +fi + +test_name=$1 +file=${2:-} +snapshot_dir=tests/snapshots + +if [[ ! -d $snapshot_dir ]]; then + echo "error: $snapshot_dir not found (run from the zellij-integration-tests folder)" >&2 + exit 1 +fi + +prefix=${file:-*} + +shopt -s nullglob +candidates=("$snapshot_dir"/${prefix}__"$test_name".snap "$snapshot_dir"/${prefix}__"$test_name"-*.snap) +shopt -u nullglob + +snapshots=() +for candidate in "${candidates[@]}"; do + [[ -f $candidate ]] && snapshots+=("$candidate") +done + +if [[ ${#snapshots[@]} -eq 0 ]]; then + echo "error: no snapshots found for test '$test_name'${file:+ in file '$file'}" >&2 + exit 1 +fi + +if [[ -z $file ]]; then + declare -A files_seen=() + for snapshot in "${snapshots[@]}"; do + base=${snapshot##*/} + files_seen[${base%%__*}]=1 + done + if [[ ${#files_seen[@]} -gt 1 ]]; then + files=("${!files_seen[@]}") + echo "error: test '$test_name' exists in multiple files: ${files[*]}" >&2 + echo "re-run with the file, e.g. $0 $test_name ${files[0]}" >&2 + exit 1 + fi +fi + +for snapshot in "${snapshots[@]}"; do + zellij action new-pane --stacked --close-on-exit -- vim "$(realpath "$snapshot")" +done diff --git a/zellij-integration-tests/src/client_screen.rs b/zellij-integration-tests/src/client_screen.rs new file mode 100644 index 00000000..3bddfa07 --- /dev/null +++ b/zellij-integration-tests/src/client_screen.rs @@ -0,0 +1,232 @@ +use std::cell::RefCell; +use std::collections::HashMap; +use std::rc::Rc; +use std::sync::{Arc, Condvar, Mutex}; +use std::time::Instant; + +use zellij_server::panes::{LinkHandler, TerminalPane}; +use zellij_utils::data::{Palette, Style}; +use zellij_utils::pane_size::{Dimension, PaneGeom, Size, SizeInPixels}; + +#[derive(Default)] +struct ReceivedBytes { + bytes: Vec<u8>, + generation: u64, +} + +#[derive(Default)] +struct ReceivedBytesWithChangeSignal { + received_bytes: Mutex<ReceivedBytes>, + change_signal: Condvar, +} + +#[derive(Clone)] +pub struct ClientScreen { + inner: Arc<ReceivedBytesWithChangeSignal>, + size: Arc<Mutex<Size>>, +} + +impl ClientScreen { + pub fn new(size: Arc<Mutex<Size>>) -> Self { + ClientScreen { + inner: Arc::new(ReceivedBytesWithChangeSignal::default()), + size, + } + } + + pub fn writer(&self) -> Box<dyn std::io::Write> { + Box::new(ClientScreenWriter { + inner: self.inner.clone(), + }) + } + + pub fn snapshot(&self) -> GridSnapshot { + let size = *self.size.lock().unwrap(); + let bytes = self.inner.received_bytes.lock().unwrap().bytes.clone(); + render_bytes(&bytes, size) + } + + pub fn wait_until( + &self, + what: &str, + predicate: impl Fn(&GridSnapshot) -> bool, + ) -> GridSnapshot { + let deadline = Instant::now() + crate::default_timeout(); + let mut received_bytes = self.inner.received_bytes.lock().unwrap(); + loop { + let size = *self.size.lock().unwrap(); + let grid_snapshot = render_bytes(&received_bytes.bytes, size); + if predicate(&grid_snapshot) { + return grid_snapshot; + } + let now = Instant::now(); + if now >= deadline { + panic!( + "timed out waiting for: {}\ncursor: {:?}\nlast rendered grid:\n{}\n=== (received {} stdout bytes, generation {}) ===\n=== zellij log tail ({}) ===\n{}", + what, + grid_snapshot.cursor, + grid_snapshot.text, + received_bytes.bytes.len(), + received_bytes.generation, + crate::test_env::log_file_path().display(), + crate::test_env::log_tail(40), + ); + } + let last_generation = received_bytes.generation; + let (guard, _) = self + .inner + .change_signal + .wait_timeout(received_bytes, deadline - now) + .unwrap(); + received_bytes = guard; + if received_bytes.generation == last_generation { + continue; + } + } + } +} + +struct ClientScreenWriter { + inner: Arc<ReceivedBytesWithChangeSignal>, +} + +impl std::io::Write for ClientScreenWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { + let mut received_bytes = self.inner.received_bytes.lock().unwrap(); + received_bytes.bytes.extend_from_slice(buf); + received_bytes.generation += 1; + self.inner.change_signal.notify_all(); + Ok(buf.len()) + } + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct CursorPosition { + pub x: usize, + pub y: usize, +} + +#[derive(Clone, Debug)] +pub struct GridSnapshot { + pub text: String, + pub cursor: Option<CursorPosition>, +} + +impl GridSnapshot { + pub fn contains(&self, needle: &str) -> bool { + self.text.contains(needle) + } + pub fn cursor_is_at(&self, x: usize, y: usize) -> bool { + self.cursor == Some(CursorPosition { x, y }) + } + pub fn status_bar_appears(&self) -> bool { + self.text.contains("Ctrl +") && self.text.contains("LOCK") + } + pub fn tab_bar_appears(&self) -> bool { + self.text.contains("Tab #1") + } + pub fn lines(&self) -> Vec<String> { + self.text.lines().map(|l| l.to_owned()).collect() + } +} + +impl std::fmt::Display for GridSnapshot { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.text) + } +} + +fn render_bytes(bytes: &[u8], win_size: Size) -> GridSnapshot { + let mut terminal_pane = build_terminal_pane(win_size); + let mut vte_parser = vte::Parser::new(); + for &byte in bytes { + vte_parser.advance(&mut terminal_pane.grid, byte); + } + + let cursor = terminal_pane + .cursor_coordinates() + .and_then(|(x, y, visible)| { + if visible { + Some(CursorPosition { x, y }) + } else { + None + } + }); + let mut text = String::new(); + let output_lines = terminal_pane.read_buffer_as_lines(); + for (line_index, line) in output_lines.iter().enumerate() { + for (character_index, terminal_character) in line.iter().enumerate() { + let character_position = CursorPosition { + x: character_index, + y: line_index, + }; + if cursor == Some(character_position) { + text.push('█'); + continue; + } + text.push(terminal_character.character); + } + if line_index != output_lines.len() - 1 { + text.push('\n'); + } + } + GridSnapshot { text, cursor } +} + +fn build_terminal_pane(win_size: Size) -> TerminalPane { + let mut rows = Dimension::fixed(win_size.rows); + let mut cols = Dimension::fixed(win_size.cols); + rows.set_inner(win_size.rows); + cols.set_inner(win_size.cols); + let position_and_size = PaneGeom { + x: 0, + y: 0, + rows, + cols, + stacked: None, + is_pinned: false, + logical_position: None, + }; + let pid = 0; + let pane_index = 0; + let pane_name = String::new(); + let link_handler = Rc::new(RefCell::new(LinkHandler::new())); + let character_cell_size = Rc::new(RefCell::new(Some(SizeInPixels { + height: 21, + width: 8, + }))); + let sixel_image_store = Rc::new(RefCell::new(Default::default())); + let terminal_emulator_colors = Rc::new(RefCell::new(Palette::default())); + let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new())); + let initial_pane_title = None; + let invoked_with = None; + let debug = false; + let arrow_fonts = true; + let styled_underlines = true; + let osc8_hyperlinks = true; + let explicitly_disable_keyboard_protocol = false; + let notification_end = None; + TerminalPane::new( + pid, + position_and_size, + Style::default(), + pane_index, + pane_name, + link_handler, + character_cell_size, + sixel_image_store, + terminal_emulator_colors, + terminal_emulator_color_codes, + initial_pane_title, + invoked_with, + debug, + arrow_fonts, + styled_underlines, + osc8_hyperlinks, + explicitly_disable_keyboard_protocol, + notification_end, + ) +} diff --git a/zellij-integration-tests/src/fake_client_os_api.rs b/zellij-integration-tests/src/fake_client_os_api.rs new file mode 100644 index 00000000..d341758a --- /dev/null +++ b/zellij-integration-tests/src/fake_client_os_api.rs @@ -0,0 +1,182 @@ +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex}; + +use zellij_client::os_input_output::{ClientOsApi, SignalEvent}; +use zellij_utils::data::Palette; +use zellij_utils::errors::ErrorContext; +use zellij_utils::ipc::{ + ClientToServerMsg, IpcReceiverWithContext, IpcSenderWithContext, ServerToClientMsg, +}; +use zellij_utils::pane_size::Size; +use zellij_utils::shared::default_palette; + +use crate::client_screen::ClientScreen; + +type ServerSpawner = Box<dyn FnOnce(PathBuf) + Send>; + +pub struct FakeClientOsApi { + client_screen: ClientScreen, + size: Arc<Mutex<Size>>, + stdin_rx: crossbeam::channel::Receiver<Vec<u8>>, + signal_rx: crossbeam::channel::Receiver<SignalEvent>, + send_instructions_to_server: Arc<Mutex<Option<IpcSenderWithContext<ClientToServerMsg>>>>, + receive_instructions_from_server: Arc<Mutex<Option<IpcReceiverWithContext<ServerToClientMsg>>>>, + session_name: Arc<Mutex<Option<String>>>, + server_spawner: Arc<Mutex<Option<ServerSpawner>>>, +} + +impl Clone for FakeClientOsApi { + fn clone(&self) -> Self { + FakeClientOsApi { + client_screen: self.client_screen.clone(), + size: self.size.clone(), + stdin_rx: self.stdin_rx.clone(), + signal_rx: self.signal_rx.clone(), + send_instructions_to_server: self.send_instructions_to_server.clone(), + receive_instructions_from_server: self.receive_instructions_from_server.clone(), + session_name: self.session_name.clone(), + server_spawner: self.server_spawner.clone(), + } + } +} + +impl std::fmt::Debug for FakeClientOsApi { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("FakeClientOsApi").finish() + } +} + +pub struct FakeClientHandle { + pub client_screen: ClientScreen, + pub size: Arc<Mutex<Size>>, + pub stdin_tx: crossbeam::channel::Sender<Vec<u8>>, + pub signal_tx: crossbeam::channel::Sender<SignalEvent>, +} + +impl FakeClientOsApi { + pub fn new( + initial_size: Size, + server_spawner: Option<ServerSpawner>, + ) -> (Self, FakeClientHandle) { + let size = Arc::new(Mutex::new(initial_size)); + let client_screen = ClientScreen::new(size.clone()); + let (stdin_tx, stdin_rx) = crossbeam::channel::unbounded(); + let (signal_tx, signal_rx) = crossbeam::channel::unbounded(); + let fake_client_os_api = FakeClientOsApi { + client_screen: client_screen.clone(), + size: size.clone(), + stdin_rx, + signal_rx, + send_instructions_to_server: Arc::new(Mutex::new(None)), + receive_instructions_from_server: Arc::new(Mutex::new(None)), + session_name: Arc::new(Mutex::new(None)), + server_spawner: Arc::new(Mutex::new(server_spawner)), + }; + let fake_client_handle = FakeClientHandle { + client_screen, + size, + stdin_tx, + signal_tx, + }; + (fake_client_os_api, fake_client_handle) + } +} + +impl ClientOsApi for FakeClientOsApi { + fn get_terminal_size(&self) -> Size { + *self.size.lock().unwrap() + } + fn set_raw_mode(&mut self) {} + fn unset_raw_mode(&self) -> Result<(), std::io::Error> { + Ok(()) + } + fn get_stdout_writer(&self) -> Box<dyn std::io::Write> { + self.client_screen.writer() + } + fn get_stdin_reader(&self) -> Box<dyn std::io::BufRead> { + Box::new(std::io::BufReader::new(std::io::empty())) + } + fn update_session_name(&mut self, new_session_name: String) { + *self.session_name.lock().unwrap() = Some(new_session_name); + } + fn read_from_stdin(&mut self) -> Result<Vec<u8>, &'static str> { + self.stdin_rx.recv().map_err(|_| "fake client stdin closed") + } + fn box_clone(&self) -> Box<dyn ClientOsApi> { + Box::new(self.clone()) + } + fn send_to_server(&self, msg: ClientToServerMsg) { + match self.send_instructions_to_server.lock().unwrap().as_mut() { + Some(ipc_sender) => { + let _ = ipc_sender.send_client_msg(msg); + }, + None => { + log::warn!("Server not ready, dropping message."); + }, + } + } + fn recv_from_server(&self) -> Option<(ServerToClientMsg, ErrorContext)> { + self.receive_instructions_from_server + .lock() + .unwrap() + .as_mut() + .unwrap() + .recv_server_msg() + } + fn handle_signals( + &self, + sigwinch_cb: Box<dyn Fn()>, + quit_cb: Box<dyn Fn()>, + _resize_receiver: Option<std::sync::mpsc::Receiver<()>>, + ) { + loop { + match self.signal_rx.recv() { + Ok(SignalEvent::Resize) => sigwinch_cb(), + Ok(SignalEvent::Quit) => { + quit_cb(); + break; + }, + Err(_) => break, + } + } + } + fn connect_to_server(&self, path: &Path) { + let socket; + loop { + match zellij_utils::consts::ipc_connect(path) { + Ok(sock) => { + socket = sock; + break; + }, + Err(_) => { + std::thread::sleep(std::time::Duration::from_millis(10)); + }, + } + } + let ipc_sender = IpcSenderWithContext::new(socket); + let ipc_receiver = ipc_sender.get_receiver(); + *self.send_instructions_to_server.lock().unwrap() = Some(ipc_sender); + *self.receive_instructions_from_server.lock().unwrap() = Some(ipc_receiver); + } + fn spawn_server(&self, socket_path: &Path, _debug: bool) -> Result<(), std::io::Error> { + if let Some(server_spawner) = self.server_spawner.lock().unwrap().take() { + server_spawner(socket_path.to_path_buf()); + } + Ok(()) + } + fn load_palette(&self) -> Palette { + default_palette() + } + fn enable_mouse(&self) -> anyhow::Result<()> { + Ok(()) + } + fn disable_mouse(&self) -> anyhow::Result<()> { + Ok(()) + } + fn env_variable(&self, _name: &str) -> Option<String> { + None + } + fn should_install_panic_hook(&self) -> bool { + false + } +} diff --git a/zellij-integration-tests/src/fake_pty.rs b/zellij-integration-tests/src/fake_pty.rs new file mode 100644 index 00000000..9191c077 --- /dev/null +++ b/zellij-integration-tests/src/fake_pty.rs @@ -0,0 +1,365 @@ +use std::collections::{HashMap, VecDeque}; +use std::sync::{Arc, Condvar, Mutex, MutexGuard}; +use std::time::Instant; + +use async_trait::async_trait; +use zellij_server::os_input_output::AsyncReader; +use zellij_server::panes::PaneId; +use zellij_utils::input::command::{RunCommand, TerminalAction}; + +pub(crate) type QuitCb = Box<dyn Fn(PaneId, Option<i32>, RunCommand) + Send>; + +pub(crate) const FAKE_PID_BASE: u32 = 100_000; + +pub(crate) struct FakePtyState { + pub terminal_action: Option<TerminalAction>, + pub output_tx: Option<tokio::sync::mpsc::UnboundedSender<Vec<u8>>>, + pub stdin: Vec<u8>, + pub size: Option<(u16, u16)>, + pub quit_cb: Option<QuitCb>, + pub exited: bool, + pub echo: bool, +} + +impl FakePtyState { + fn run_command(&self) -> RunCommand { + match &self.terminal_action { + Some(TerminalAction::RunCommand(run_command)) => run_command.clone(), + _ => RunCommand::default(), + } + } + + fn describe(&self) -> String { + format!( + "size={:?} exited={} stdin_bytes={}", + self.size, + self.exited, + self.stdin.len() + ) + } +} + +#[derive(Default)] +pub(crate) struct FakePtyRegistry { + next_terminal_id: u32, + pub fake_pty_states: HashMap<u32, FakePtyState>, + pub spawn_queue: VecDeque<u32>, +} + +impl FakePtyRegistry { + fn describe_all(&self) -> String { + let mut fake_pty_state_lines: Vec<String> = self + .fake_pty_states + .iter() + .map(|(terminal_id, fake_pty_state)| { + format!("terminal {}: {}", terminal_id, fake_pty_state.describe()) + }) + .collect(); + fake_pty_state_lines.sort(); + format!( + "{}\nspawn queue: {:?}", + fake_pty_state_lines.join("\n"), + self.spawn_queue + ) + } +} + +#[derive(Default)] +struct RegistryWithChangeSignal { + fake_pty_registry: Mutex<FakePtyRegistry>, + change_signal: Condvar, +} + +#[derive(Clone, Default)] +pub struct SharedPtys { + inner: Arc<RegistryWithChangeSignal>, +} + +impl SharedPtys { + fn lock_registry(&self) -> MutexGuard<'_, FakePtyRegistry> { + self.inner.fake_pty_registry.lock().unwrap() + } + + pub(crate) fn read<T>(&self, reader: impl FnOnce(&FakePtyRegistry) -> T) -> T { + reader(&self.lock_registry()) + } + + pub(crate) fn mutate<T>(&self, mutator: impl FnOnce(&mut FakePtyRegistry) -> T) -> T { + let result = mutator(&mut self.lock_registry()); + self.inner.change_signal.notify_all(); + result + } + + pub(crate) fn next_terminal_id(&self) -> u32 { + self.mutate(|fake_pty_registry| { + let terminal_id = fake_pty_registry.next_terminal_id; + fake_pty_registry.next_terminal_id += 1; + terminal_id + }) + } + + pub(crate) fn register( + &self, + terminal_id: u32, + terminal_action: Option<TerminalAction>, + quit_cb: Option<QuitCb>, + ) -> Box<dyn AsyncReader> { + let (output_tx, output_rx) = tokio::sync::mpsc::unbounded_channel(); + self.mutate(|fake_pty_registry| { + fake_pty_registry.fake_pty_states.insert( + terminal_id, + FakePtyState { + terminal_action, + output_tx: Some(output_tx), + stdin: Vec::new(), + size: None, + quit_cb, + exited: false, + echo: true, + }, + ); + fake_pty_registry.spawn_queue.push_back(terminal_id); + }); + Box::new(FakeAsyncReader { + output_rx, + pending: VecDeque::new(), + }) + } + + pub(crate) fn rerun(&self, terminal_id: u32, quit_cb: QuitCb) -> Box<dyn AsyncReader> { + let (output_tx, output_rx) = tokio::sync::mpsc::unbounded_channel(); + self.mutate(|fake_pty_registry| { + let fake_pty_state = fake_pty_registry + .fake_pty_states + .get_mut(&terminal_id) + .expect("re-run for unknown terminal id"); + fake_pty_state.output_tx = Some(output_tx); + fake_pty_state.quit_cb = Some(quit_cb); + fake_pty_state.exited = false; + }); + Box::new(FakeAsyncReader { + output_rx, + pending: VecDeque::new(), + }) + } + + pub(crate) fn exit_terminal(&self, terminal_id: u32, exit_status: Option<i32>) { + let newly_exited = self.mutate(|fake_pty_registry| { + let fake_pty_state = fake_pty_registry.fake_pty_states.get_mut(&terminal_id)?; + if fake_pty_state.exited { + return None; + } + fake_pty_state.exited = true; + fake_pty_state.output_tx.take(); + Some((fake_pty_state.quit_cb.take(), fake_pty_state.run_command())) + }); + if let Some((Some(quit_cb), run_command)) = newly_exited { + quit_cb(PaneId::Terminal(terminal_id), exit_status, run_command); + } + } + + pub(crate) fn remove(&self, terminal_id: u32) { + self.mutate(|fake_pty_registry| { + fake_pty_registry.fake_pty_states.remove(&terminal_id); + }); + } + + pub(crate) fn set_size(&self, terminal_id: u32, cols: u16, rows: u16) { + self.mutate(|fake_pty_registry| { + if let Some(fake_pty_state) = fake_pty_registry.fake_pty_states.get_mut(&terminal_id) { + fake_pty_state.size = Some((cols, rows)); + } + }); + } + + pub(crate) fn write_output(&self, terminal_id: u32, bytes: &[u8]) { + self.read(|fake_pty_registry| { + if let Some(fake_pty_state) = fake_pty_registry.fake_pty_states.get(&terminal_id) { + if let Some(output_tx) = fake_pty_state.output_tx.as_ref() { + let _ = output_tx.send(bytes.to_vec()); + } + } + }); + } + + pub(crate) fn append_stdin(&self, terminal_id: u32, bytes: &[u8]) -> bool { + self.mutate(|fake_pty_registry| { + match fake_pty_registry.fake_pty_states.get_mut(&terminal_id) { + Some(fake_pty_state) => { + fake_pty_state.stdin.extend_from_slice(bytes); + if fake_pty_state.echo { + if let Some(output_tx) = fake_pty_state.output_tx.as_ref() { + let _ = output_tx.send(bytes.to_vec()); + } + } + true + }, + None => false, + } + }) + } + + pub(crate) fn wait_for<T>( + &self, + what: &str, + mut condition: impl FnMut(&mut FakePtyRegistry) -> Option<T>, + ) -> T { + let deadline = Instant::now() + crate::default_timeout(); + let mut fake_pty_registry = self.lock_registry(); + loop { + if let Some(result) = condition(&mut fake_pty_registry) { + return result; + } + let now = Instant::now(); + if now >= deadline { + panic!( + "timed out waiting for: {}\n{}\n=== zellij log tail ({}) ===\n{}", + what, + fake_pty_registry.describe_all(), + crate::test_env::log_file_path().display(), + crate::test_env::log_tail(40), + ); + } + let (guard, _) = self + .inner + .change_signal + .wait_timeout(fake_pty_registry, deadline - now) + .unwrap(); + fake_pty_registry = guard; + } + } +} + +struct FakeAsyncReader { + output_rx: tokio::sync::mpsc::UnboundedReceiver<Vec<u8>>, + pending: VecDeque<u8>, +} + +#[async_trait] +impl AsyncReader for FakeAsyncReader { + async fn read(&mut self, buf: &mut [u8]) -> Result<usize, std::io::Error> { + if self.pending.is_empty() { + match self.output_rx.recv().await { + Some(bytes) => self.pending.extend(bytes), + None => return Ok(0), + } + } + let mut written = 0; + while written < buf.len() { + match self.pending.pop_front() { + Some(byte) => { + buf[written] = byte; + written += 1; + }, + None => break, + } + } + Ok(written) + } +} + +#[derive(Clone)] +pub struct FakePtyHandle { + pub(crate) terminal_id: u32, + pub(crate) shared_ptys: SharedPtys, +} + +impl FakePtyHandle { + pub fn terminal_id(&self) -> u32 { + self.terminal_id + } + + pub fn terminal_action(&self) -> Option<TerminalAction> { + self.shared_ptys.read(|fake_pty_registry| { + fake_pty_registry + .fake_pty_states + .get(&self.terminal_id) + .and_then(|fake_pty_state| fake_pty_state.terminal_action.clone()) + }) + } + + pub fn disable_echo(&self) { + self.shared_ptys.mutate(|fake_pty_registry| { + if let Some(fake_pty_state) = + fake_pty_registry.fake_pty_states.get_mut(&self.terminal_id) + { + fake_pty_state.echo = false; + } + }); + } + + pub fn output(&self, bytes: &[u8]) { + self.shared_ptys.read(|fake_pty_registry| { + let fake_pty_state = fake_pty_registry + .fake_pty_states + .get(&self.terminal_id) + .unwrap_or_else(|| panic!("unknown terminal id {}", self.terminal_id)); + let output_tx = fake_pty_state + .output_tx + .as_ref() + .unwrap_or_else(|| panic!("terminal {} already exited", self.terminal_id)); + let _ = output_tx.send(bytes.to_vec()); + }) + } + + pub fn exit(&self, exit_status: Option<i32>) { + self.shared_ptys + .exit_terminal(self.terminal_id, exit_status); + } + + pub fn is_exited(&self) -> bool { + self.shared_ptys.read(|fake_pty_registry| { + fake_pty_registry + .fake_pty_states + .get(&self.terminal_id) + .map(|fake_pty_state| fake_pty_state.exited) + .unwrap_or(true) + }) + } + + pub fn stdin_bytes(&self) -> Vec<u8> { + self.shared_ptys.read(|fake_pty_registry| { + fake_pty_registry + .fake_pty_states + .get(&self.terminal_id) + .map(|fake_pty_state| fake_pty_state.stdin.clone()) + .unwrap_or_default() + }) + } + + pub fn wait_for_stdin(&self, what: &str, predicate: impl Fn(&[u8]) -> bool) -> Vec<u8> { + let terminal_id = self.terminal_id; + self.shared_ptys.wait_for(what, move |fake_pty_registry| { + fake_pty_registry + .fake_pty_states + .get(&terminal_id) + .and_then(|fake_pty_state| { + if predicate(&fake_pty_state.stdin) { + Some(fake_pty_state.stdin.clone()) + } else { + None + } + }) + }) + } + + pub fn size(&self) -> Option<(u16, u16)> { + self.shared_ptys.read(|fake_pty_registry| { + fake_pty_registry + .fake_pty_states + .get(&self.terminal_id) + .and_then(|fake_pty_state| fake_pty_state.size) + }) + } + + pub fn wait_for_size(&self, what: &str, predicate: impl Fn(u16, u16) -> bool) -> (u16, u16) { + let terminal_id = self.terminal_id; + self.shared_ptys.wait_for(what, move |fake_pty_registry| { + fake_pty_registry + .fake_pty_states + .get(&terminal_id) + .and_then(|fake_pty_state| fake_pty_state.size) + .filter(|(cols, rows)| predicate(*cols, *rows)) + }) + } +} diff --git a/zellij-integration-tests/src/fake_server_os_api.rs b/zellij-integration-tests/src/fake_server_os_api.rs new file mode 100644 index 00000000..4db7842c --- /dev/null +++ b/zellij-integration-tests/src/fake_server_os_api.rs @@ -0,0 +1,201 @@ +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; + +use interprocess::local_socket::Stream as LocalSocketStream; +use zellij_server::os_input_output::{AsyncReader, ServerOsApi}; +use zellij_server::panes::PaneId; +use zellij_server::ClientId; +use zellij_utils::data::Palette; +use zellij_utils::errors::prelude::*; +use zellij_utils::input::command::{RunCommand, TerminalAction}; +use zellij_utils::ipc::{ + ClientToServerMsg, IpcReceiverWithContext, IpcSenderWithContext, ServerToClientMsg, +}; +use zellij_utils::shared::default_palette; + +use crate::fake_pty::{SharedPtys, FAKE_PID_BASE}; + +#[derive(Clone)] +struct NonBlockingClientSender { + buffer_tx: crossbeam::channel::Sender<ServerToClientMsg>, +} + +impl NonBlockingClientSender { + fn new(mut ipc_sender: IpcSenderWithContext<ServerToClientMsg>) -> Self { + let (buffer_tx, buffer_rx) = crossbeam::channel::bounded::<ServerToClientMsg>(5000); + std::thread::Builder::new() + .name("non_blocking_client_sender".to_string()) + .spawn(move || { + for msg in buffer_rx.iter() { + if ipc_sender.send_server_msg(msg).is_err() { + break; + } + } + }) + .unwrap(); + NonBlockingClientSender { buffer_tx } + } +} + +#[derive(Clone, Default)] +pub struct FakeServerOsApi { + pub shared_ptys: SharedPtys, + non_blocking_client_senders: Arc<Mutex<HashMap<ClientId, NonBlockingClientSender>>>, + fake_filesystem: Arc<Mutex<HashMap<String, String>>>, +} + +impl FakeServerOsApi { + pub fn written_files(&self) -> HashMap<String, String> { + self.fake_filesystem.lock().unwrap().clone() + } + + pub fn connected_client_count(&self) -> usize { + self.non_blocking_client_senders.lock().unwrap().len() + } +} + +impl ServerOsApi for FakeServerOsApi { + fn set_terminal_size_using_terminal_id( + &self, + id: u32, + cols: u16, + rows: u16, + _width_in_pixels: Option<u16>, + _height_in_pixels: Option<u16>, + ) -> Result<()> { + self.shared_ptys.set_size(id, cols, rows); + Ok(()) + } + fn spawn_terminal( + &self, + terminal_action: TerminalAction, + quit_cb: Box<dyn Fn(PaneId, Option<i32>, RunCommand) + Send>, + _default_editor: Option<PathBuf>, + ) -> Result<(u32, Box<dyn AsyncReader>, Option<u32>)> { + let terminal_id = self.shared_ptys.next_terminal_id(); + let opened_file_contents = match &terminal_action { + TerminalAction::OpenFile(payload) => self + .fake_filesystem + .lock() + .unwrap() + .get(&payload.path.to_string_lossy().to_string()) + .cloned(), + _ => None, + }; + let fake_async_reader = + self.shared_ptys + .register(terminal_id, Some(terminal_action), Some(quit_cb)); + if let Some(contents) = opened_file_contents { + let contents_with_carriage_returns = + contents.replace("\r\n", "\n").replace('\n', "\r\n"); + self.shared_ptys + .write_output(terminal_id, contents_with_carriage_returns.as_bytes()); + } + Ok(( + terminal_id, + fake_async_reader, + Some(FAKE_PID_BASE + terminal_id), + )) + } + fn reserve_terminal_id(&self) -> Result<u32> { + let terminal_id = self.shared_ptys.next_terminal_id(); + let _ = self.shared_ptys.register(terminal_id, None, None); + Ok(terminal_id) + } + fn write_to_tty_stdin(&self, terminal_id: u32, buf: &[u8]) -> Result<usize> { + if self.shared_ptys.append_stdin(terminal_id, buf) { + Ok(buf.len()) + } else { + Err(anyhow!("no fake pty for terminal id {terminal_id}")) + } + } + fn tcdrain(&self, _terminal_id: u32) -> Result<()> { + Ok(()) + } + fn kill(&self, pid: u32) -> Result<()> { + self.shared_ptys + .exit_terminal(pid.saturating_sub(FAKE_PID_BASE), None); + Ok(()) + } + fn force_kill(&self, pid: u32) -> Result<()> { + self.shared_ptys + .exit_terminal(pid.saturating_sub(FAKE_PID_BASE), None); + Ok(()) + } + fn send_sigint(&self, pid: u32) -> Result<()> { + self.shared_ptys + .exit_terminal(pid.saturating_sub(FAKE_PID_BASE), None); + Ok(()) + } + fn box_clone(&self) -> Box<dyn ServerOsApi> { + Box::new(self.clone()) + } + fn send_to_client(&self, client_id: ClientId, msg: ServerToClientMsg) -> Result<()> { + if let Some(non_blocking_client_sender) = self + .non_blocking_client_senders + .lock() + .unwrap() + .get(&client_id) + { + non_blocking_client_sender + .buffer_tx + .try_send(msg) + .with_context(|| format!("failed to buffer message for client {client_id}"))?; + } + Ok(()) + } + fn new_client( + &mut self, + client_id: ClientId, + stream: LocalSocketStream, + ) -> Result<IpcReceiverWithContext<ClientToServerMsg>> { + let ipc_receiver = IpcReceiverWithContext::new(stream); + let non_blocking_client_sender = NonBlockingClientSender::new(ipc_receiver.get_sender()); + self.non_blocking_client_senders + .lock() + .unwrap() + .insert(client_id, non_blocking_client_sender); + Ok(ipc_receiver) + } + fn new_client_with_reply( + &mut self, + _client_id: ClientId, + _stream: LocalSocketStream, + _reply_stream: LocalSocketStream, + ) -> Result<IpcReceiverWithContext<ClientToServerMsg>> { + unimplemented!("windows dual-pipe IPC is not used by the test harness") + } + fn remove_client(&mut self, client_id: ClientId) -> Result<()> { + self.non_blocking_client_senders + .lock() + .unwrap() + .remove(&client_id); + Ok(()) + } + fn load_palette(&self) -> Palette { + default_palette() + } + fn get_cwd(&self, _pid: u32) -> Option<PathBuf> { + None + } + fn write_to_file(&mut self, buf: String, file: Option<String>) -> Result<()> { + if let Some(file) = file { + self.fake_filesystem.lock().unwrap().insert(file, buf); + } + Ok(()) + } + fn re_run_command_in_terminal( + &self, + terminal_id: u32, + _run_command: RunCommand, + quit_cb: Box<dyn Fn(PaneId, Option<i32>, RunCommand) + Send>, + ) -> Result<(Box<dyn AsyncReader>, Option<u32>)> { + let fake_async_reader = self.shared_ptys.rerun(terminal_id, quit_cb); + Ok((fake_async_reader, Some(FAKE_PID_BASE + terminal_id))) + } + fn clear_terminal_id(&self, terminal_id: u32) -> Result<()> { + self.shared_ptys.remove(terminal_id); + Ok(()) + } +} diff --git a/zellij-integration-tests/src/keys.rs b/zellij-integration-tests/src/keys.rs new file mode 100644 index 00000000..fe54b42d --- /dev/null +++ b/zellij-integration-tests/src/keys.rs @@ -0,0 +1,63 @@ +const ESC_BYTE: u8 = 0x1b; + +const fn ctrl(character: u8) -> [u8; 1] { + [character & 0x1f] +} + +const fn alt(character: u8) -> [u8; 2] { + [ESC_BYTE, character] +} + +pub const QUIT: [u8; 1] = ctrl(b'q'); +pub const ESC: [u8; 1] = [ESC_BYTE]; +pub const ENTER: [u8; 1] = [b'\r']; +pub const SPACE: [u8; 1] = [b' ']; +pub const LOCK_MODE: [u8; 1] = ctrl(b'g'); + +pub const MOVE_FOCUS_LEFT_IN_NORMAL_MODE: [u8; 2] = alt(b'h'); +pub const MOVE_FOCUS_RIGHT_IN_NORMAL_MODE: [u8; 2] = alt(b'l'); + +pub const PANE_MODE: [u8; 1] = ctrl(b'p'); +pub const TMUX_MODE: [u8; 1] = ctrl(b'b'); +pub const SPAWN_TERMINAL_IN_PANE_MODE: [u8; 1] = [b'n']; +pub const MOVE_FOCUS_IN_PANE_MODE: [u8; 1] = [b'p']; +pub const SPLIT_DOWN_IN_PANE_MODE: [u8; 1] = [b'd']; +pub const SPLIT_RIGHT_IN_PANE_MODE: [u8; 1] = [b'r']; +pub const SPLIT_RIGHT_IN_TMUX_MODE: [u8; 1] = [b'%']; +pub const TOGGLE_ACTIVE_TERMINAL_FULLSCREEN_IN_PANE_MODE: [u8; 1] = [b'f']; +pub const TOGGLE_FLOATING_PANES: [u8; 1] = [b'w']; +pub const CLOSE_PANE_IN_PANE_MODE: [u8; 1] = [b'x']; +pub const MOVE_FOCUS_DOWN_IN_PANE_MODE: [u8; 1] = [b'j']; +pub const MOVE_FOCUS_UP_IN_PANE_MODE: [u8; 1] = [b'k']; +pub const MOVE_FOCUS_LEFT_IN_PANE_MODE: [u8; 1] = [b'h']; +pub const MOVE_FOCUS_RIGHT_IN_PANE_MODE: [u8; 1] = [b'l']; +pub const RENAME_PANE_MODE: [u8; 1] = [b'c']; + +pub const SCROLL_MODE: [u8; 1] = ctrl(b's'); +pub const SCROLL_UP_IN_SCROLL_MODE: [u8; 1] = [b'k']; +pub const SCROLL_DOWN_IN_SCROLL_MODE: [u8; 1] = [b'j']; +pub const SCROLL_PAGE_UP_IN_SCROLL_MODE: [u8; 1] = ctrl(b'b'); +pub const SCROLL_PAGE_DOWN_IN_SCROLL_MODE: [u8; 1] = ctrl(b'f'); +pub const EDIT_SCROLLBACK: [u8; 1] = [b'e']; + +pub const RESIZE_MODE: [u8; 1] = ctrl(b'n'); +pub const RESIZE_DOWN_IN_RESIZE_MODE: [u8; 1] = [b'j']; +pub const RESIZE_UP_IN_RESIZE_MODE: [u8; 1] = [b'k']; +pub const RESIZE_LEFT_IN_RESIZE_MODE: [u8; 1] = [b'h']; +pub const RESIZE_RIGHT_IN_RESIZE_MODE: [u8; 1] = [b'l']; + +pub const TAB_MODE: [u8; 1] = ctrl(b't'); +pub const NEW_TAB_IN_TAB_MODE: [u8; 1] = [b'n']; +pub const SWITCH_NEXT_TAB_IN_TAB_MODE: [u8; 1] = [b'l']; +pub const SWITCH_PREV_TAB_IN_TAB_MODE: [u8; 1] = [b'h']; +pub const CLOSE_TAB_IN_TAB_MODE: [u8; 1] = [b'x']; +pub const RENAME_TAB_MODE: [u8; 1] = [b'r']; + +pub const MOVE_TAB_LEFT: [u8; 2] = alt(b'i'); +pub const MOVE_TAB_RIGHT: [u8; 2] = alt(b'o'); + +pub const SESSION_MODE: [u8; 1] = ctrl(b'o'); +pub const DETACH_IN_SESSION_MODE: [u8; 1] = [b'd']; + +pub const BRACKETED_PASTE_START: [u8; 6] = *b"\x1b[200~"; +pub const BRACKETED_PASTE_END: [u8; 6] = *b"\x1b[201~"; diff --git a/zellij-integration-tests/src/lib.rs b/zellij-integration-tests/src/lib.rs new file mode 100644 index 00000000..15afe865 --- /dev/null +++ b/zellij-integration-tests/src/lib.rs @@ -0,0 +1,25 @@ +#![cfg(unix)] + +pub mod client_screen; +pub mod fake_client_os_api; +pub mod fake_pty; +pub mod fake_server_os_api; +pub mod keys; +pub mod runner; +pub mod test_env; + +pub use client_screen::{ClientScreen, CursorPosition, GridSnapshot}; +pub use fake_pty::FakePtyHandle; +pub use runner::{normalized, TestClient, TestRunner, TestSession}; +pub use zellij_utils::data::LayoutInfo; +pub use zellij_utils::pane_size::Size; + +use std::time::Duration; + +pub fn default_timeout() -> Duration { + std::env::var("ZELLIJ_TEST_TIMEOUT_MS") + .ok() + .and_then(|v| v.parse().ok()) + .map(Duration::from_millis) + .unwrap_or(Duration::from_secs(10)) +} diff --git a/zellij-integration-tests/src/runner.rs b/zellij-integration-tests/src/runner.rs new file mode 100644 index 00000000..26b59014 --- /dev/null +++ b/zellij-integration-tests/src/runner.rs @@ -0,0 +1,567 @@ +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; +use std::thread::JoinHandle; + +use zellij_client::os_input_output::SignalEvent; +use zellij_client::ClientInfo; +use zellij_utils::cli::{CliAction, CliArgs}; +use zellij_utils::data::{ConnectToSession, LayoutInfo}; +use zellij_utils::input::actions::Action; +use zellij_utils::input::options::Options; +use zellij_utils::pane_size::Size; +use zellij_utils::setup::Setup; + +use crate::client_screen::GridSnapshot; +use crate::fake_client_os_api::{FakeClientHandle, FakeClientOsApi}; +use crate::fake_pty::FakePtyHandle; +use crate::fake_server_os_api::FakeServerOsApi; +use crate::{keys, test_env}; + +pub struct TestRunner { + size: Size, + extra_config_kdl: String, + layout: Option<LayoutInfo>, +} + +impl TestRunner { + pub fn new(size: Size) -> Self { + TestRunner { + size, + extra_config_kdl: String::new(), + layout: None, + } + } + + pub fn with_config(mut self, extra_config_kdl: &str) -> Self { + self.extra_config_kdl.push('\n'); + self.extra_config_kdl.push_str(extra_config_kdl); + self + } + + pub fn with_layout(mut self, layout: LayoutInfo) -> Self { + self.layout = Some(layout); + self + } + + pub fn start(self) -> TestSession { + test_env::init(); + let session_name = test_env::unique_session_name(); + let config_path = test_env::write_config(&session_name, &self.extra_config_kdl); + let data_dir = test_env::init().join("data"); + + let cli_args = CliArgs { + config: Some(config_path), + data_dir: Some(data_dir), + ..Default::default() + }; + let (config, default_layout_info, config_options, _, _) = + Setup::from_cli_args(&cli_args).expect("failed to load harness config"); + + let concurrency_slot = test_env::acquire_concurrency_slot(); + + let fake_server_os_api = FakeServerOsApi::default(); + let server_thread: Arc<Mutex<Option<JoinHandle<()>>>> = Arc::new(Mutex::new(None)); + let server_spawner = in_process_server_spawner(fake_server_os_api.clone(), &server_thread); + + let (fake_client_os_api, fake_client_handle) = + FakeClientOsApi::new(self.size, Some(server_spawner)); + let layout_info = self.layout.or(default_layout_info); + let client_thread = spawn_client_thread( + fake_client_os_api, + cli_args.clone(), + config.clone(), + config_options.clone(), + ClientInfo::New(session_name.clone(), layout_info, None), + ); + + TestSession { + session_name, + size: self.size, + cli_args, + config, + config_options, + fake_server_os_api, + server_thread, + main_client: TestClient { + fake_client_handle, + thread: Some(client_thread), + }, + _concurrency_slot: concurrency_slot, + } + } +} + +fn in_process_server_spawner( + fake_server_os_api: FakeServerOsApi, + server_thread: &Arc<Mutex<Option<JoinHandle<()>>>>, +) -> Box<dyn FnOnce(std::path::PathBuf) + Send> { + let server_thread = server_thread.clone(); + Box::new(move |socket_path: std::path::PathBuf| { + let install_panic_hook = false; + let join_handle = std::thread::Builder::new() + .name("in_process_zellij_server".to_string()) + .spawn(move || { + zellij_server::start_server_impl( + Box::new(fake_server_os_api), + socket_path, + install_panic_hook, + ); + }) + .unwrap(); + *server_thread.lock().unwrap() = Some(join_handle); + }) +} + +fn new_pane_cli_action( + command: &[&str], + floating: bool, + close_on_exit: bool, + start_suspended: bool, + block_until_exit: bool, +) -> CliAction { + CliAction::NewPane { + direction: None, + command: command.iter().map(|part| part.to_string()).collect(), + plugin: None, + cwd: None, + floating, + in_place: false, + close_replaced_pane: false, + name: None, + close_on_exit, + start_suspended, + configuration: None, + skip_plugin_cache: false, + x: None, + y: None, + width: None, + height: None, + pinned: None, + stacked: false, + blocking: false, + block_until_exit_success: false, + block_until_exit_failure: false, + block_until_exit, + unblock_condition: None, + near_current_pane: false, + borderless: None, + tab_id: None, + } +} + +fn referenced_contents_files(layout: &str) -> impl Iterator<Item = &str> { + layout + .match_indices("contents_file=\"") + .filter_map(|(start, marker)| { + let rest = &layout[start + marker.len()..]; + rest.find('"').map(|end| &rest[..end]) + }) +} + +fn spawn_client_thread( + fake_client_os_api: FakeClientOsApi, + cli_args: CliArgs, + config: zellij_utils::input::config::Config, + config_options: Options, + client_info: ClientInfo, +) -> JoinHandle<Option<ConnectToSession>> { + std::thread::Builder::new() + .name("in_process_zellij_client".to_string()) + .spawn(move || { + let tab_position_to_focus = None; + let pane_id_to_focus = None; + let is_a_reconnect = false; + let start_detached_and_exit = false; + zellij_client::start_client( + Box::new(fake_client_os_api), + cli_args, + config, + config_options, + client_info, + tab_position_to_focus, + pane_id_to_focus, + is_a_reconnect, + start_detached_and_exit, + ) + }) + .unwrap() +} + +pub struct TestClient { + fake_client_handle: FakeClientHandle, + thread: Option<JoinHandle<Option<ConnectToSession>>>, +} + +impl TestClient { + pub fn send_stdin(&self, bytes: &[u8]) { + self.fake_client_handle + .stdin_tx + .send(bytes.to_vec()) + .expect("client stdin closed"); + } + + pub fn resize(&self, new_size: Size) { + *self.fake_client_handle.size.lock().unwrap() = new_size; + let _ = self.fake_client_handle.signal_tx.send(SignalEvent::Resize); + } + + pub fn wait_until( + &self, + what: &str, + predicate: impl Fn(&GridSnapshot) -> bool, + ) -> GridSnapshot { + self.fake_client_handle + .client_screen + .wait_until(what, predicate) + } + + pub fn snapshot(&self) -> GridSnapshot { + self.fake_client_handle.client_screen.snapshot() + } + + pub fn quit(mut self) { + self.send_stdin(&keys::QUIT); + self.join(); + } + + fn join(&mut self) -> Option<ConnectToSession> { + self.thread + .take() + .and_then(|join_handle: JoinHandle<Option<ConnectToSession>>| { + join_handle.join().expect("client thread panicked") + }) + } +} + +pub struct TestSession { + session_name: String, + size: Size, + cli_args: CliArgs, + config: zellij_utils::input::config::Config, + config_options: Options, + fake_server_os_api: FakeServerOsApi, + server_thread: Arc<Mutex<Option<JoinHandle<()>>>>, + main_client: TestClient, + _concurrency_slot: test_env::ConcurrencySlot, +} + +pub struct CliClientHandle { + thread: JoinHandle<i32>, +} + +impl CliClientHandle { + pub fn wait_for_exit(self) -> i32 { + self.thread.join().expect("cli client thread panicked") + } +} + +impl TestSession { + pub fn session_name(&self) -> &str { + &self.session_name + } + + pub fn expect_pty_spawn(&self) -> FakePtyHandle { + let terminal_id = self + .fake_server_os_api + .shared_ptys + .wait_for("a pty spawn", |fake_pty_registry| { + fake_pty_registry.spawn_queue.pop_front() + }); + FakePtyHandle { + terminal_id, + shared_ptys: self.fake_server_os_api.shared_ptys.clone(), + } + } + + pub fn send_stdin(&self, bytes: &[u8]) { + self.main_client.send_stdin(bytes); + } + + pub fn resize(&self, new_size: Size) { + self.main_client.resize(new_size); + } + + pub fn wait_until( + &self, + what: &str, + predicate: impl Fn(&GridSnapshot) -> bool, + ) -> GridSnapshot { + self.main_client.wait_until(what, predicate) + } + + pub fn snapshot(&self) -> GridSnapshot { + self.main_client.snapshot() + } + + pub fn wait_for_app_load(&self) -> GridSnapshot { + self.main_client.wait_until("app to load", |grid_snapshot| { + grid_snapshot.status_bar_appears() + && grid_snapshot.tab_bar_appears() + && grid_snapshot.cursor.is_some() + }) + } + + pub fn written_files(&self) -> std::collections::HashMap<String, String> { + self.fake_server_os_api.written_files() + } + + pub fn attach_client(&self, size: Size) -> TestClient { + let (fake_client_os_api, fake_client_handle) = FakeClientOsApi::new(size, None); + let thread = spawn_client_thread( + fake_client_os_api, + self.cli_args.clone(), + self.config.clone(), + self.config_options.clone(), + ClientInfo::Attach(self.session_name.clone(), self.config_options.clone()), + ); + TestClient { + fake_client_handle, + thread: Some(thread), + } + } + + pub fn attach_watcher(&self, size: Size) -> TestClient { + let (fake_client_os_api, fake_client_handle) = FakeClientOsApi::new(size, None); + let thread = spawn_client_thread( + fake_client_os_api, + self.cli_args.clone(), + self.config.clone(), + self.config_options.clone(), + ClientInfo::Watch(self.session_name.clone(), self.config_options.clone()), + ); + TestClient { + fake_client_handle, + thread: Some(thread), + } + } + + pub fn run_cli_action(&self, cli_action: CliAction) -> i32 { + self.spawn_cli_client(cli_action) + .join() + .expect("cli client thread panicked") + } + + fn spawn_cli_client(&self, cli_action: CliAction) -> JoinHandle<i32> { + let actions = Action::actions_from_cli( + cli_action, + Box::new(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/"))), + Some(self.config.clone()), + ) + .expect("failed to build cli actions"); + let (fake_client_os_api, _fake_client_handle) = FakeClientOsApi::new(self.size, None); + let session_name = self.session_name.clone(); + std::thread::Builder::new() + .name("in_process_cli_client".to_string()) + .spawn(move || { + zellij_client::cli_client::start_cli_client( + Box::new(fake_client_os_api), + &session_name, + actions, + ) + }) + .unwrap() + } + + pub fn save_session(&self) { + self.run_cli_action(CliAction::SaveSession); + } + + pub fn wait_for_serialized_session(&self) { + let layout_path = zellij_utils::consts::session_layout_cache_file_name(&self.session_name); + let session_dir = layout_path.parent().map(|parent| parent.to_path_buf()); + let deadline = std::time::Instant::now() + crate::default_timeout(); + loop { + if let Ok(layout) = std::fs::read_to_string(&layout_path) { + let referenced_contents_files_exist = session_dir.as_ref().map_or(true, |dir| { + referenced_contents_files(&layout).all(|name| dir.join(name).exists()) + }); + if referenced_contents_files_exist { + return; + } + } + if std::time::Instant::now() >= deadline { + panic!( + "timed out waiting for session serialization at {}", + layout_path.display() + ); + } + std::thread::sleep(std::time::Duration::from_millis(10)); + } + } + + pub fn resurrect(&mut self, size: Size) { + let layout_path = zellij_utils::consts::session_layout_cache_file_name(&self.session_name); + assert!( + layout_path.exists(), + "no serialized layout to resurrect from at {}", + layout_path.display() + ); + let mut cli_args = self.cli_args.clone(); + if cli_args.config_dir.is_none() { + cli_args.config_dir = cli_args + .config + .as_ref() + .and_then(|config| config.parent().map(|parent| parent.to_path_buf())); + } + let fake_server_os_api = FakeServerOsApi::default(); + let server_thread: Arc<Mutex<Option<JoinHandle<()>>>> = Arc::new(Mutex::new(None)); + let server_spawner = in_process_server_spawner(fake_server_os_api.clone(), &server_thread); + let (fake_client_os_api, fake_client_handle) = + FakeClientOsApi::new(size, Some(server_spawner)); + let client_thread = spawn_client_thread( + fake_client_os_api, + cli_args, + self.config.clone(), + self.config_options.clone(), + ClientInfo::Resurrect(self.session_name.clone(), layout_path, false, None), + ); + self.fake_server_os_api = fake_server_os_api; + self.server_thread = server_thread; + self.main_client = TestClient { + fake_client_handle, + thread: Some(client_thread), + }; + } + + pub fn override_layout(&self, layout_name: &str) { + self.run_cli_action(CliAction::OverrideLayout { + layout: Some(PathBuf::from(layout_name)), + layout_string: None, + layout_dir: None, + retain_existing_terminal_panes: false, + retain_existing_plugin_panes: false, + apply_only_to_active_tab: false, + }); + } + + pub fn run_suspended_command(&self, command: &[&str]) { + let start_suspended = true; + self.run_cli_action(new_pane_cli_action( + command, + false, + false, + start_suspended, + false, + )); + } + + pub fn run_blocking_command(&self, command: &[&str]) -> CliClientHandle { + let floating = false; + let close_on_exit = true; + let block_until_exit = true; + CliClientHandle { + thread: self.spawn_cli_client(new_pane_cli_action( + command, + floating, + close_on_exit, + false, + block_until_exit, + )), + } + } + + pub fn run_blocking_floating_command(&self, command: &[&str]) -> CliClientHandle { + let floating = true; + let close_on_exit = true; + let block_until_exit = true; + CliClientHandle { + thread: self.spawn_cli_client(new_pane_cli_action( + command, + floating, + close_on_exit, + false, + block_until_exit, + )), + } + } + + pub fn detach_main_client(&mut self) { + let connected_clients_before_detach = self.fake_server_os_api.connected_client_count(); + self.send_stdin(&keys::SESSION_MODE); + self.send_stdin(&keys::DETACH_IN_SESSION_MODE); + self.main_client.join(); + self.wait_for_server_to_release_a_client(connected_clients_before_detach); + } + + fn wait_for_server_to_release_a_client(&self, connected_clients_before_detach: usize) { + let deadline = std::time::Instant::now() + crate::default_timeout(); + loop { + if self.fake_server_os_api.connected_client_count() < connected_clients_before_detach { + return; + } + if std::time::Instant::now() >= deadline { + panic!("timed out waiting for server to release the detached client"); + } + std::thread::sleep(std::time::Duration::from_millis(10)); + } + } + + pub fn quit(&mut self) { + if self.main_client.thread.is_some() { + self.send_stdin(&keys::QUIT); + self.main_client.join(); + } + if let Some(server_thread) = self.server_thread.lock().unwrap().take() { + server_thread.join().expect("server thread panicked"); + } + } + + fn send_quit_without_joining(&self) { + let _ = self + .main_client + .fake_client_handle + .stdin_tx + .send(keys::QUIT.to_vec()); + } +} + +impl Drop for TestSession { + fn drop(&mut self) { + if self.main_client.thread.is_some() { + self.send_quit_without_joining(); + } + } +} + +pub fn normalized(grid_snapshot: &GridSnapshot) -> String { + let text = replace_unique_session_name(&grid_snapshot.text); + let text = strip_swap_layout_indication(&text); + strip_trailing_whitespace(&text) +} + +fn replace_unique_session_name(text: &str) -> String { + regex::Regex::new(r"Zellij \(test-\d+\)") + .unwrap() + .replace_all(text, "Zellij (test)") + .to_string() +} + +fn strip_swap_layout_indication(text: &str) -> String { + let swap_layout_name = "BASE|VERTICAL|HORIZONTAL|STACKED|STAGGERED|ENLARGED|SPREAD"; + let powerline_separated_or_plain = |prefix: &str| { + regex::Regex::new(&format!( + "{} [\u{e0b0}]? ?(?:{}) ?[\u{e0b0}]?[ ]*\n", + prefix, swap_layout_name + )) + .unwrap() + }; + let normal_mode = powerline_separated_or_plain("Alt <\\[\\]>"); + let tmux_mode_1 = powerline_separated_or_plain("Alt \\[\\|SPACE\\|Alt \\]"); + let tmux_mode_2 = powerline_separated_or_plain("Alt \\[\\|Alt \\]\\|SPACE"); + let tab_bar = regex::Regex::new(&format!( + " {{2,}}[\u{e0b0}]? ?(?:{}) ?[\u{e0b0}]?[ ]*\n", + swap_layout_name + )) + .unwrap(); + let text = normal_mode.replace_all(text, "\n"); + let text = tmux_mode_1.replace_all(&text, "\n"); + let text = tmux_mode_2.replace_all(&text, "\n"); + tab_bar.replace_all(&text, "\n").to_string() +} + +fn strip_trailing_whitespace(text: &str) -> String { + regex::Regex::new(r"\s*\n") + .unwrap() + .replace_all(text, "\n") + .to_string() +} diff --git a/zellij-integration-tests/src/test_env.rs b/zellij-integration-tests/src/test_env.rs new file mode 100644 index 00000000..a70c4e75 --- /dev/null +++ b/zellij-integration-tests/src/test_env.rs @@ -0,0 +1,202 @@ +use std::fs::{File, OpenOptions}; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::OnceLock; +use std::time::Duration; + +const TEMP_PARENT: &str = "/tmp/zellij-test"; +const TEST_ROOT_SUBDIRS: [&str; 8] = [ + "sock", "home", "cache", "data", "config", "runtime", "cwd", "tmp", +]; + +static TEST_ROOT: OnceLock<PathBuf> = OnceLock::new(); +static OWN_PROCESS_LOCK: OnceLock<File> = OnceLock::new(); +static SESSION_COUNTER: AtomicUsize = AtomicUsize::new(0); + +pub fn init() -> &'static Path { + TEST_ROOT + .get_or_init(|| { + let test_root = create_test_root(); + isolate_process_environment(&test_root); + zellij_utils::logging::configure_logger(); + test_root + }) + .as_path() +} + +fn create_test_root() -> PathBuf { + let parent = PathBuf::from(TEMP_PARENT); + std::fs::create_dir_all(&parent).unwrap(); + hold_own_process_lock(&parent); + remove_roots_of_dead_test_processes(&parent); + let test_root = parent.join(std::process::id().to_string()); + for subdir in TEST_ROOT_SUBDIRS { + std::fs::create_dir_all(test_root.join(subdir)).unwrap(); + } + test_root +} + +fn process_lock_path(parent: &Path, pid: u32) -> PathBuf { + parent.join(format!("{}.lock", pid)) +} + +fn hold_own_process_lock(parent: &Path) { + let lock_file = OpenOptions::new() + .create(true) + .write(true) + .open(process_lock_path(parent, std::process::id())) + .unwrap(); + lock_file.lock().unwrap(); + let _ = OWN_PROCESS_LOCK.set(lock_file); +} + +fn remove_roots_of_dead_test_processes(parent: &Path) { + let Ok(entries) = std::fs::read_dir(parent) else { + return; + }; + for entry in entries.flatten() { + let file_name = entry.file_name(); + let Some(owner_pid) = file_name + .to_str() + .and_then(|name| name.strip_suffix(".lock")) + .and_then(|pid| pid.parse::<u32>().ok()) + else { + continue; + }; + if owner_pid == std::process::id() { + continue; + } + let Ok(lock_file) = OpenOptions::new().write(true).open(entry.path()) else { + continue; + }; + if lock_file.try_lock().is_ok() { + let _ = std::fs::remove_dir_all(parent.join(owner_pid.to_string())); + let _ = std::fs::remove_file(entry.path()); + let _ = lock_file.unlock(); + } + } +} + +fn isolate_process_environment(test_root: &Path) { + std::env::set_var("TMPDIR", test_root.join("tmp")); + std::env::set_var("ZELLIJ_SOCKET_DIR", test_root.join("sock")); + std::env::set_var("HOME", test_root.join("home")); + std::env::set_var("XDG_CACHE_HOME", test_root.join("cache")); + std::env::set_var("XDG_DATA_HOME", test_root.join("data")); + std::env::set_var("XDG_CONFIG_HOME", test_root.join("config")); + std::env::set_var("XDG_RUNTIME_DIR", test_root.join("runtime")); + std::env::remove_var("ZELLIJ"); + std::env::remove_var("ZELLIJ_SESSION_NAME"); + std::env::remove_var("ZELLIJ_CONFIG_FILE"); + std::env::remove_var("ZELLIJ_CONFIG_DIR"); + std::env::set_current_dir(test_root.join("cwd")).unwrap(); +} + +pub fn unique_session_name() -> String { + let session_index = SESSION_COUNTER.fetch_add(1, Ordering::Relaxed); + format!("test-{}", session_index) +} + +pub struct ConcurrencySlot { + _slot_file: File, +} + +fn is_truthy(value: &str) -> bool { + matches!( + value.trim().to_ascii_lowercase().as_str(), + "1" | "true" | "yes" + ) +} + +fn slot_count() -> usize { + if std::env::var("ZELLIJ_TEST_SERIAL") + .map(|v| is_truthy(&v)) + .unwrap_or(false) + { + return 1; + } + if let Some(explicit) = std::env::var("ZELLIJ_TEST_CONCURRENCY") + .ok() + .and_then(|v| v.parse::<usize>().ok()) + { + return explicit.max(1); + } + let cpus = std::thread::available_parallelism() + .map(|n| n.get()) + .unwrap_or(4); + (cpus / 4).max(1) +} + +pub fn acquire_concurrency_slot() -> ConcurrencySlot { + let slots = slot_count(); + let slot_dir = PathBuf::from(TEMP_PARENT).join("concurrency-slots"); + std::fs::create_dir_all(&slot_dir).unwrap(); + loop { + for slot_index in 0..slots { + let slot_file = OpenOptions::new() + .create(true) + .write(true) + .open(slot_dir.join(format!("slot-{}", slot_index))) + .unwrap(); + if slot_file.try_lock().is_ok() { + return ConcurrencySlot { + _slot_file: slot_file, + }; + } + } + std::thread::sleep(Duration::from_millis(10)); + } +} + +pub const DEFAULT_TEST_CONFIG: &str = r#" +show_startup_tips false +show_release_notes false +session_serialization false +disable_session_metadata true +visual_bell false +mouse_mode false +advanced_mouse_actions false +theme "default" +"#; + +pub fn write_config(session_name: &str, extra_config_kdl: &str) -> PathBuf { + let test_root = init(); + let config_path = test_root + .join("config") + .join(format!("{}-config.kdl", session_name)); + let contents = format!( + "{}\n{}\n", + defaults_overridden_by(extra_config_kdl), + extra_config_kdl + ); + std::fs::write(&config_path, contents).unwrap(); + config_path +} + +fn defaults_overridden_by(extra_config_kdl: &str) -> String { + let overridden_keys: std::collections::HashSet<&str> = extra_config_kdl + .lines() + .filter_map(|line| line.split_whitespace().next()) + .collect(); + DEFAULT_TEST_CONFIG + .lines() + .filter(|line| { + line.split_whitespace() + .next() + .map_or(true, |key| !overridden_keys.contains(key)) + }) + .collect::<Vec<_>>() + .join("\n") +} + +pub fn log_file_path() -> PathBuf { + init(); + zellij_utils::consts::ZELLIJ_TMP_LOG_FILE.clone() +} + +pub fn log_tail(max_lines: usize) -> String { + let contents = std::fs::read_to_string(log_file_path()).unwrap_or_default(); + let lines: Vec<&str> = contents.lines().collect(); + let start = lines.len().saturating_sub(max_lines); + lines[start..].join("\n") +} diff --git a/zellij-integration-tests/tests/app.rs b/zellij-integration-tests/tests/app.rs new file mode 100644 index 00000000..70fd3a64 --- /dev/null +++ b/zellij-integration-tests/tests/app.rs @@ -0,0 +1,157 @@ +#![cfg(unix)] + +use insta::assert_snapshot; +use zellij_integration_tests::{keys, normalized, FakePtyHandle, Size, TestRunner, TestSession}; + +const TERMINAL_SIZE: Size = Size { + cols: 120, + rows: 24, +}; +const PROMPT: &[u8] = b"$ "; +const PROMPT_ROW: usize = 2; +const FIRST_PANE_PROMPT_X: usize = 3; +const RIGHT_PANE_PROMPT_X: usize = 63; + +const CHANGED_KEYS_CONFIG: &str = r#" +keybinds clear-defaults=true { + normal { + bind "F1" { SwitchToMode "Locked"; } + bind "F2" { SwitchToMode "Pane"; } + bind "F3" { SwitchToMode "Tab"; } + bind "F4" { SwitchToMode "Resize"; } + bind "F5" { SwitchToMode "Move"; } + bind "F6" { SwitchToMode "Scroll"; } + bind "Alt F7" { SwitchToMode "Session"; } + bind "Ctrl F8" { Quit; } + } +} +"#; + +fn claim_first_terminal_and_wait_for_prompt(zellij: &TestSession) -> FakePtyHandle { + let terminal = zellij.expect_pty_spawn(); + terminal.output(PROMPT); + zellij.wait_until( + "first terminal prompt rendered in loaded app", + |grid_snapshot| { + grid_snapshot.tab_bar_appears() + && grid_snapshot.status_bar_appears() + && grid_snapshot.cursor_is_at(FIRST_PANE_PROMPT_X, PROMPT_ROW) + }, + ); + terminal +} + +fn split_right_and_wait_for_prompt(zellij: &TestSession) -> FakePtyHandle { + zellij.send_stdin(&keys::PANE_MODE); + zellij.send_stdin(&keys::SPLIT_RIGHT_IN_PANE_MODE); + let terminal = zellij.expect_pty_spawn(); + terminal.output(PROMPT); + zellij.wait_until("right terminal prompt rendered", |grid_snapshot| { + grid_snapshot.status_bar_appears() + && grid_snapshot.cursor_is_at(RIGHT_PANE_PROMPT_X, PROMPT_ROW) + }); + terminal +} + +#[test] +fn exit_zellij() { + let zellij = TestRunner::new(TERMINAL_SIZE).start(); + claim_first_terminal_and_wait_for_prompt(&zellij); + + zellij.send_stdin(&keys::QUIT); + + zellij.wait_until("zellij said goodbye", |grid_snapshot| { + !grid_snapshot.status_bar_appears() && grid_snapshot.contains("Bye from Zellij!") + }); +} + +#[test] +fn resize_terminal_window() { + let mut zellij = TestRunner::new(TERMINAL_SIZE).start(); + let first_terminal = claim_first_terminal_and_wait_for_prompt(&zellij); + let right_terminal = split_right_and_wait_for_prompt(&zellij); + + zellij.resize(Size { + cols: 100, + rows: 24, + }); + right_terminal.wait_for_size("right terminal reacted to window resize", |cols, _| { + cols < (TERMINAL_SIZE.cols / 2) as u16 + }); + first_terminal.wait_for_size("left terminal reacted to window resize", |cols, _| { + cols < (TERMINAL_SIZE.cols / 2) as u16 + }); + + let grid_snapshot = + zellij.wait_until("app re-rendered at the new window size", |grid_snapshot| { + grid_snapshot.contains("Ctrl +") + && grid_snapshot.tab_bar_appears() + && grid_snapshot.cursor_is_at(53, PROMPT_ROW) + }); + assert_snapshot!(normalized(&grid_snapshot)); + zellij.quit(); +} + +#[test] +fn status_bar_loads_custom_keybindings() { + let zellij = TestRunner::new(TERMINAL_SIZE) + .with_config(CHANGED_KEYS_CONFIG) + .start(); + let terminal = zellij.expect_pty_spawn(); + terminal.output(PROMPT); + + let grid_snapshot = zellij.wait_until( + "status bar reflects the custom keybindings", + |grid_snapshot| { + grid_snapshot.cursor_is_at(FIRST_PANE_PROMPT_X, PROMPT_ROW) + && grid_snapshot.contains("LOCK") + }, + ); + assert_snapshot!(normalized(&grid_snapshot)); +} + +#[test] +fn use_custom_layout_with_relative_path() { + let layout_dir = format!( + "{}/../src/tests/fixtures/config-dirs/e2e-upside-down/layouts", + env!("CARGO_MANIFEST_DIR") + ); + let config = format!( + "layout_dir \"{}\"\ndefault_layout \"upside-down\"\n", + layout_dir + ); + let mut zellij = TestRunner::new(TERMINAL_SIZE).with_config(&config).start(); + let terminal = zellij.expect_pty_spawn(); + terminal.output(PROMPT); + + let grid_snapshot = zellij.wait_until("upside-down layout loaded from disk", |grid_snapshot| { + grid_snapshot.tab_bar_appears() + && grid_snapshot.contains("Zellij (test") + && grid_snapshot.cursor.is_some() + }); + assert_snapshot!(normalized(&grid_snapshot)); + zellij.quit(); +} + +#[test] +fn edit_scrollback() { + let mut zellij = TestRunner::new(TERMINAL_SIZE).start(); + let terminal = claim_first_terminal_and_wait_for_prompt(&zellij); + terminal.output(b"SCROLLBACK_MARKER_ONE\r\nSCROLLBACK_MARKER_TWO\r\n"); + zellij.wait_until("scrollback content rendered", |grid_snapshot| { + grid_snapshot.contains("SCROLLBACK_MARKER_TWO") + }); + + zellij.send_stdin(&keys::SCROLL_MODE); + zellij.send_stdin(&keys::EDIT_SCROLLBACK); + zellij.expect_pty_spawn(); + + let grid_snapshot = zellij.wait_until("scrollback editor pane open", |grid_snapshot| { + grid_snapshot.contains("EDITING SCROLLBACK") + && grid_snapshot.contains("SCROLLBACK_MARKER_ONE") + && grid_snapshot.contains("SCROLLBACK_MARKER_TWO") + && grid_snapshot.status_bar_appears() + }); + assert_snapshot!(normalized(&grid_snapshot)); + zellij.quit(); +} diff --git a/zellij-integration-tests/tests/basic.rs b/zellij-integration-tests/tests/basic.rs new file mode 100644 index 00000000..1b12316b --- /dev/null +++ b/zellij-integration-tests/tests/basic.rs @@ -0,0 +1,128 @@ +#![cfg(unix)] + +use insta::assert_snapshot; +use zellij_integration_tests::{keys, normalized, FakePtyHandle, Size, TestRunner, TestSession}; + +const TERMINAL_SIZE: Size = Size { + cols: 120, + rows: 24, +}; +const PROMPT: &[u8] = b"$ "; +const PROMPT_ROW: usize = 2; +const FIRST_PANE_PROMPT_X: usize = 3; +const RIGHT_PANE_PROMPT_X: usize = 63; + +fn start_zellij() -> TestSession { + TestRunner::new(TERMINAL_SIZE).start() +} + +fn claim_first_terminal_and_wait_for_prompt(zellij: &TestSession) -> FakePtyHandle { + let terminal = zellij.expect_pty_spawn(); + terminal.output(PROMPT); + zellij.wait_until( + "first terminal prompt rendered in loaded app", + |grid_snapshot| { + grid_snapshot.tab_bar_appears() + && grid_snapshot.status_bar_appears() + && grid_snapshot.cursor_is_at(FIRST_PANE_PROMPT_X, PROMPT_ROW) + }, + ); + terminal +} + +fn split_pane_right(zellij: &TestSession) { + zellij.send_stdin(&keys::PANE_MODE); + zellij.send_stdin(&keys::SPLIT_RIGHT_IN_PANE_MODE); +} + +fn split_right_and_wait_for_prompt(zellij: &TestSession) -> FakePtyHandle { + split_pane_right(zellij); + let terminal = zellij.expect_pty_spawn(); + terminal.output(PROMPT); + zellij.wait_until("right terminal prompt rendered", |grid_snapshot| { + grid_snapshot.status_bar_appears() + && grid_snapshot.cursor_is_at(RIGHT_PANE_PROMPT_X, PROMPT_ROW) + }); + terminal +} + +fn resize_focused_pane_left(zellij: &TestSession) { + zellij.send_stdin(&keys::RESIZE_MODE); + zellij.send_stdin(&keys::RESIZE_LEFT_IN_RESIZE_MODE); + zellij.send_stdin(&keys::ENTER); +} + +fn column_count(terminal: &FakePtyHandle, what: &str) -> u16 { + terminal.wait_for_size(what, |_, _| true).0 +} + +#[test] +fn starts_with_one_terminal() { + let mut zellij = start_zellij(); + claim_first_terminal_and_wait_for_prompt(&zellij); + let grid_snapshot = zellij.wait_until("steady loaded state", |grid_snapshot| { + grid_snapshot.tab_bar_appears() + && grid_snapshot.status_bar_appears() + && grid_snapshot.cursor_is_at(FIRST_PANE_PROMPT_X, PROMPT_ROW) + }); + assert_snapshot!(normalized(&grid_snapshot)); + zellij.quit(); +} + +#[test] +fn split_terminals_vertically() { + let mut zellij = start_zellij(); + claim_first_terminal_and_wait_for_prompt(&zellij); + split_right_and_wait_for_prompt(&zellij); + let grid_snapshot = zellij.wait_until("split rendered in normal mode", |grid_snapshot| { + grid_snapshot.status_bar_appears() + && grid_snapshot.cursor_is_at(RIGHT_PANE_PROMPT_X, PROMPT_ROW) + }); + assert_snapshot!(normalized(&grid_snapshot)); + zellij.quit(); +} + +#[test] +fn resize_pane() { + let mut zellij = start_zellij(); + let first_terminal = claim_first_terminal_and_wait_for_prompt(&zellij); + let right_terminal = split_right_and_wait_for_prompt(&zellij); + let initial_cols = column_count(&right_terminal, "right terminal initial width"); + + resize_focused_pane_left(&zellij); + + let resized_cols = right_terminal + .wait_for_size("right terminal grew", |cols, _| cols > initial_cols) + .0; + first_terminal.wait_for_size("left terminal shrank", |cols, _| cols < initial_cols); + + let cursor_shift_left = (resized_cols.saturating_sub(initial_cols)) as usize; + let grid_snapshot = zellij.wait_until("resized layout in normal mode", |grid_snapshot| { + grid_snapshot.status_bar_appears() + && grid_snapshot.cursor_is_at(RIGHT_PANE_PROMPT_X - cursor_shift_left, PROMPT_ROW) + }); + assert_snapshot!(normalized(&grid_snapshot)); + zellij.quit(); +} + +#[test] +fn command_pane_closes_on_exit() { + let mut zellij = start_zellij(); + claim_first_terminal_and_wait_for_prompt(&zellij); + let right_terminal = split_right_and_wait_for_prompt(&zellij); + + right_terminal.output(b"about to exit"); + zellij.wait_until("right terminal output rendered", |grid_snapshot| { + grid_snapshot.contains("about to exit") + }); + + right_terminal.exit(Some(0)); + zellij.wait_until( + "right terminal closed, focus back on first", + |grid_snapshot| { + !grid_snapshot.contains("about to exit") + && grid_snapshot.cursor_is_at(FIRST_PANE_PROMPT_X, PROMPT_ROW) + }, + ); + zellij.quit(); +} diff --git a/zellij-integration-tests/tests/cli.rs b/zellij-integration-tests/tests/cli.rs new file mode 100644 index 00000000..50440f2f --- /dev/null +++ b/zellij-integration-tests/tests/cli.rs @@ -0,0 +1,164 @@ +#![cfg(unix)] + +use insta::assert_snapshot; +use zellij_integration_tests::{keys, normalized, FakePtyHandle, Size, TestRunner, TestSession}; + +const TERMINAL_SIZE: Size = Size { + cols: 120, + rows: 24, +}; +const PROMPT: &[u8] = b"$ "; +const PROMPT_ROW: usize = 2; +const FIRST_PANE_PROMPT_X: usize = 3; + +fn start_zellij() -> TestSession { + TestRunner::new(TERMINAL_SIZE).start() +} + +fn claim_first_terminal_and_wait_for_prompt(zellij: &TestSession) -> FakePtyHandle { + let terminal = zellij.expect_pty_spawn(); + terminal.output(PROMPT); + zellij.wait_until("first terminal prompt rendered", |grid_snapshot| { + grid_snapshot.tab_bar_appears() + && grid_snapshot.status_bar_appears() + && grid_snapshot.cursor_is_at(FIRST_PANE_PROMPT_X, PROMPT_ROW) + }); + terminal +} + +#[test] +fn override_layout_from_default_to_compact() { + let mut zellij = start_zellij(); + claim_first_terminal_and_wait_for_prompt(&zellij); + + zellij.override_layout("compact"); + + let grid_snapshot = + zellij.wait_until("compact bar rendered with session name", |grid_snapshot| { + grid_snapshot.contains("Zellij (test") + && grid_snapshot.contains("NORMAL") + && grid_snapshot.contains("Tab #1") + && !grid_snapshot.status_bar_appears() + }); + assert_snapshot!(normalized(&grid_snapshot)); + zellij.quit(); +} + +#[test] +fn send_command_through_the_cli() { + let mut zellij = start_zellij(); + claim_first_terminal_and_wait_for_prompt(&zellij); + + zellij.run_suspended_command(&["suspended-command"]); + let command_terminal = zellij.expect_pty_spawn(); + zellij.wait_until("suspended command pane waiting to run", |grid_snapshot| { + grid_snapshot.contains("<Ctrl-c>") + }); + + zellij.send_stdin(&keys::ENTER); + zellij.wait_until("command running", |grid_snapshot| { + !grid_snapshot.contains("<Ctrl-c>") + }); + command_terminal.output(b"foo\r\n"); + zellij.wait_until("first run printed foo", |grid_snapshot| { + grid_snapshot.contains("foo") + }); + command_terminal.exit(Some(0)); + zellij.wait_until("command pane held again after exit", |grid_snapshot| { + grid_snapshot.contains("EXIT CODE") + }); + + zellij.send_stdin(&keys::ENTER); + zellij.wait_until("command re-running", |grid_snapshot| { + !grid_snapshot.contains("EXIT CODE") + }); + command_terminal.output(b"foo\r\nfoo\r\n"); + let grid_snapshot = zellij.wait_until("command re-ran", |grid_snapshot| { + grid_snapshot.text.matches("foo").count() >= 2 + }); + assert_snapshot!(normalized(&grid_snapshot)); + zellij.quit(); +} + +#[test] +fn send_blocking_command_through_the_cli() { + let mut zellij = start_zellij(); + claim_first_terminal_and_wait_for_prompt(&zellij); + + let blocking_command = zellij.run_blocking_floating_command(&["blocking-command"]); + let floating_terminal = zellij.expect_pty_spawn(); + zellij.wait_until("floating command pane appeared", |grid_snapshot| { + grid_snapshot.contains("PIN") + }); + + floating_terminal.exit(Some(42)); + assert_eq!(blocking_command.wait_for_exit(), 42); + + let grid_snapshot = zellij.wait_until("floating pane closed on exit", |grid_snapshot| { + !grid_snapshot.contains("PIN") + && grid_snapshot.cursor_is_at(FIRST_PANE_PROMPT_X, PROMPT_ROW) + }); + assert_snapshot!(normalized(&grid_snapshot)); + zellij.quit(); +} + +#[test] +fn watcher_client_functionality() { + let mut zellij = start_zellij(); + let first_terminal = claim_first_terminal_and_wait_for_prompt(&zellij); + first_terminal.output(b"\r\nWATCHER_OUTPUT_1\r\n"); + zellij.wait_until("first output line", |grid_snapshot| { + grid_snapshot.contains("WATCHER_OUTPUT_1") + }); + + let watcher = zellij.attach_watcher(TERMINAL_SIZE); + watcher.wait_until("watcher connected and sees the output", |grid_snapshot| { + grid_snapshot.status_bar_appears() && grid_snapshot.contains("WATCHER_OUTPUT_1") + }); + + zellij.send_stdin(&keys::PANE_MODE); + zellij.send_stdin(&keys::SPLIT_RIGHT_IN_PANE_MODE); + let right_terminal = zellij.expect_pty_spawn(); + right_terminal.output(PROMPT); + watcher.wait_until("watcher sees the split", |grid_snapshot| { + grid_snapshot.contains("┐┌") + }); + + let ignored_new_tab_from_watcher = [keys::TAB_MODE, keys::NEW_TAB_IN_TAB_MODE].concat(); + watcher.send_stdin(&ignored_new_tab_from_watcher); + + zellij.detach_main_client(); + first_terminal.output(b"WATCHER_OUTPUT_2\r\nWATCHER_OUTPUT_3\r\nWATCHER_DONE\r\n"); + watcher.wait_until( + "watcher keeps receiving output while no main client is attached", + |grid_snapshot| { + grid_snapshot.contains("WATCHER_DONE") + && grid_snapshot.contains("┐┌") + && !grid_snapshot.contains("Tab #2") + }, + ); + + let main = zellij.attach_client(TERMINAL_SIZE); + let main_snapshot = main.wait_until( + "re-attached main converges on the same state", + |grid_snapshot| { + grid_snapshot.status_bar_appears() + && grid_snapshot.contains("WATCHER_DONE") + && grid_snapshot.contains("┐┌") + }, + ); + assert_snapshot!(normalized(&main_snapshot)); + + let watcher_snapshot = + watcher.wait_until("watcher mirrors the re-attached session", |grid_snapshot| { + grid_snapshot.status_bar_appears() + && grid_snapshot.contains("WATCHER_DONE") + && grid_snapshot.contains("┐┌") + && !grid_snapshot.contains("Tab #2") + }); + assert_snapshot!(normalized(&watcher_snapshot)); + + main.quit(); + watcher.quit(); + zellij.quit(); +} diff --git a/zellij-integration-tests/tests/clients.rs b/zellij-integration-tests/tests/clients.rs new file mode 100644 index 00000000..09db9941 --- /dev/null +++ b/zellij-integration-tests/tests/clients.rs @@ -0,0 +1,217 @@ +#![cfg(unix)] + +use insta::assert_snapshot; +use zellij_integration_tests::{ + keys, normalized, FakePtyHandle, GridSnapshot, Size, TestRunner, TestSession, +}; + +const TERMINAL_SIZE: Size = Size { + cols: 120, + rows: 24, +}; +const PROMPT: &[u8] = b"$ "; +const PROMPT_ROW: usize = 2; +const FIRST_PANE_PROMPT_X: usize = 3; +const RIGHT_PANE_PROMPT_X: usize = 63; + +fn start_zellij() -> TestSession { + TestRunner::new(TERMINAL_SIZE).start() +} + +fn claim_first_terminal_and_wait_for_prompt(zellij: &TestSession) -> FakePtyHandle { + let terminal = zellij.expect_pty_spawn(); + terminal.output(PROMPT); + zellij.wait_until( + "first terminal prompt rendered in loaded app", + |grid_snapshot| { + grid_snapshot.tab_bar_appears() + && grid_snapshot.status_bar_appears() + && grid_snapshot.cursor_is_at(FIRST_PANE_PROMPT_X, PROMPT_ROW) + }, + ); + terminal +} + +#[test] +fn mirrored_sessions() { + let mut zellij = TestRunner::new(TERMINAL_SIZE) + .with_config("mirror_session true") + .start(); + claim_first_terminal_and_wait_for_prompt(&zellij); + + let second_client = zellij.attach_client(TERMINAL_SIZE); + second_client.wait_until("second client loaded on the first tab", |grid_snapshot| { + grid_snapshot.tab_bar_appears() + && grid_snapshot.status_bar_appears() + && grid_snapshot.cursor_is_at(FIRST_PANE_PROMPT_X, PROMPT_ROW) + }); + + second_client.send_stdin(&keys::PANE_MODE); + second_client.send_stdin(&keys::SPLIT_RIGHT_IN_PANE_MODE); + let right_terminal = zellij.expect_pty_spawn(); + right_terminal.output(PROMPT); + + let mirror_focused_right_pane = |grid_snapshot: &GridSnapshot| { + grid_snapshot.tab_bar_appears() + && grid_snapshot.status_bar_appears() + && grid_snapshot.contains("Pane #2") + && grid_snapshot.cursor_is_at(RIGHT_PANE_PROMPT_X, PROMPT_ROW) + }; + let main_grid = zellij.wait_until( + "main client follows the second client's focus into the split it never asked for", + mirror_focused_right_pane, + ); + let second_grid = second_client.wait_until( + "second client shows the split it created", + mirror_focused_right_pane, + ); + assert_eq!( + normalized(&main_grid), + normalized(&second_grid), + "mirrored clients must render an identical view" + ); + assert_snapshot!(normalized(&main_grid)); + second_client.quit(); + zellij.quit(); +} + +#[test] +fn multiple_users_in_same_pane_and_tab() { + let mut zellij = start_zellij(); + claim_first_terminal_and_wait_for_prompt(&zellij); + + let second_client = zellij.attach_client(TERMINAL_SIZE); + let second_grid = + second_client.wait_until("second client shares the focused pane", |grid_snapshot| { + grid_snapshot.tab_bar_appears() + && grid_snapshot.status_bar_appears() + && grid_snapshot.contains("MY FOCUS") + && grid_snapshot.cursor_is_at(FIRST_PANE_PROMPT_X, PROMPT_ROW) + }); + let main_grid = zellij.wait_until( + "main client shows the shared-focus indicator", + |grid_snapshot| { + grid_snapshot.tab_bar_appears() + && grid_snapshot.status_bar_appears() + && grid_snapshot.contains("MY FOCUS") + && grid_snapshot.cursor_is_at(FIRST_PANE_PROMPT_X, PROMPT_ROW) + }, + ); + assert_snapshot!(normalized(&main_grid)); + assert_snapshot!(normalized(&second_grid)); + second_client.quit(); + zellij.quit(); +} + +#[test] +fn multiple_users_in_different_panes_and_same_tab() { + let mut zellij = start_zellij(); + claim_first_terminal_and_wait_for_prompt(&zellij); + + let second_client = zellij.attach_client(TERMINAL_SIZE); + second_client.wait_until("second client loaded on the first tab", |grid_snapshot| { + grid_snapshot.status_bar_appears() + && grid_snapshot.cursor_is_at(FIRST_PANE_PROMPT_X, PROMPT_ROW) + }); + + second_client.send_stdin(&keys::PANE_MODE); + second_client.send_stdin(&keys::SPLIT_RIGHT_IN_PANE_MODE); + let right_terminal = zellij.expect_pty_spawn(); + right_terminal.output(PROMPT); + + let second_grid = second_client.wait_until( + "second client focused the new right pane", + |grid_snapshot| { + grid_snapshot.tab_bar_appears() + && grid_snapshot.status_bar_appears() + && grid_snapshot.cursor_is_at(RIGHT_PANE_PROMPT_X, PROMPT_ROW) + }, + ); + let main_grid = zellij.wait_until( + "main client sees the second client's split while staying on the left pane", + |grid_snapshot| { + grid_snapshot.tab_bar_appears() + && grid_snapshot.contains("Pane #2") + && grid_snapshot.status_bar_appears() + && grid_snapshot.cursor_is_at(FIRST_PANE_PROMPT_X, PROMPT_ROW) + }, + ); + assert_snapshot!(normalized(&main_grid)); + assert_snapshot!(normalized(&second_grid)); + second_client.quit(); + zellij.quit(); +} + +#[test] +fn multiple_users_in_different_tabs() { + let mut zellij = start_zellij(); + claim_first_terminal_and_wait_for_prompt(&zellij); + + let second_client = zellij.attach_client(TERMINAL_SIZE); + second_client.wait_until("second client loaded on the first tab", |grid_snapshot| { + grid_snapshot.status_bar_appears() + && grid_snapshot.cursor_is_at(FIRST_PANE_PROMPT_X, PROMPT_ROW) + }); + + second_client.send_stdin(&keys::TAB_MODE); + second_client.send_stdin(&keys::NEW_TAB_IN_TAB_MODE); + let second_tab_terminal = zellij.expect_pty_spawn(); + second_tab_terminal.output(PROMPT); + + let second_grid = + second_client.wait_until("second client moved to the new tab", |grid_snapshot| { + grid_snapshot.tab_bar_appears() + && grid_snapshot.status_bar_appears() + && grid_snapshot.contains("Tab #2") + && grid_snapshot.cursor_is_at(FIRST_PANE_PROMPT_X, PROMPT_ROW) + }); + let main_grid = zellij.wait_until( + "main client sees the new tab while staying on the first tab", + |grid_snapshot| { + grid_snapshot.tab_bar_appears() + && grid_snapshot.status_bar_appears() + && grid_snapshot.contains("Tab #2") + && grid_snapshot.cursor_is_at(FIRST_PANE_PROMPT_X, PROMPT_ROW) + }, + ); + assert_snapshot!(normalized(&main_grid)); + assert_snapshot!(normalized(&second_grid)); + second_client.quit(); + zellij.quit(); +} + +#[test] +fn detach_and_attach_session() { + let mut zellij = start_zellij(); + claim_first_terminal_and_wait_for_prompt(&zellij); + + zellij.send_stdin(&keys::PANE_MODE); + zellij.send_stdin(&keys::SPLIT_RIGHT_IN_PANE_MODE); + let right_terminal = zellij.expect_pty_spawn(); + right_terminal.output(PROMPT); + zellij.wait_until("right terminal prompt rendered", |grid_snapshot| { + grid_snapshot.status_bar_appears() + && grid_snapshot.cursor_is_at(RIGHT_PANE_PROMPT_X, PROMPT_ROW) + }); + + right_terminal.output(b"I am some text"); + zellij.wait_until("text rendered in the right terminal", |grid_snapshot| { + grid_snapshot.contains("I am some text") + }); + + zellij.detach_main_client(); + + let reattached_client = zellij.attach_client(TERMINAL_SIZE); + let grid_snapshot = reattached_client.wait_until( + "reattached client sees the restored split and text", + |grid_snapshot| { + grid_snapshot.tab_bar_appears() + && grid_snapshot.status_bar_appears() + && grid_snapshot.contains("Pane #2") + && grid_snapshot.contains("I am some text") + }, + ); + assert_snapshot!(normalized(&grid_snapshot)); + reattached_client.quit(); + zellij.quit(); +} diff --git a/zellij-integration-tests/tests/modes.rs b/zellij-integration-tests/tests/modes.rs new file mode 100644 index 00000000..c564be70 --- /dev/null +++ b/zellij-integration-tests/tests/modes.rs @@ -0,0 +1,137 @@ +#![cfg(unix)] + +use insta::assert_snapshot; +use zellij_integration_tests::{keys, normalized, FakePtyHandle, Size, TestRunner, TestSession}; + +const TERMINAL_SIZE: Size = Size { + cols: 120, + rows: 24, +}; +const PROMPT: &[u8] = b"$ "; +const PROMPT_ROW: usize = 2; +const FIRST_PANE_PROMPT_X: usize = 3; +const RIGHT_PANE_PROMPT_X: usize = 63; + +fn start_zellij() -> TestSession { + TestRunner::new(TERMINAL_SIZE).start() +} + +fn claim_first_terminal_and_wait_for_prompt(zellij: &TestSession) -> FakePtyHandle { + let terminal = zellij.expect_pty_spawn(); + terminal.output(PROMPT); + zellij.wait_until( + "first terminal prompt rendered in loaded app", + |grid_snapshot| { + grid_snapshot.tab_bar_appears() + && grid_snapshot.status_bar_appears() + && grid_snapshot.cursor_is_at(FIRST_PANE_PROMPT_X, PROMPT_ROW) + }, + ); + terminal +} + +#[test] +fn lock_mode() { + let mut zellij = start_zellij(); + let terminal = claim_first_terminal_and_wait_for_prompt(&zellij); + + zellij.send_stdin(&keys::LOCK_MODE); + zellij.wait_until("interface locked", |grid_snapshot| { + grid_snapshot.contains("LOCK") && !grid_snapshot.contains("PANE") + }); + + zellij.send_stdin(&keys::TAB_MODE); + zellij.send_stdin(&keys::NEW_TAB_IN_TAB_MODE); + zellij.send_stdin(b"abc"); + + terminal.wait_for_stdin("forwarded keys reached the pane", |stdin_bytes| { + stdin_bytes.windows(3).any(|window| window == b"abc") + }); + let grid_snapshot = + zellij.wait_until("forwarded keys rendered in locked pane", |grid_snapshot| { + grid_snapshot.contains("abc") + && !grid_snapshot.contains("PANE") + && !grid_snapshot.contains("Tab #2") + }); + assert_snapshot!(normalized(&grid_snapshot)); + + zellij.send_stdin(&keys::LOCK_MODE); + zellij.wait_until("interface unlocked", |grid_snapshot| { + grid_snapshot.contains("PANE") + }); + zellij.quit(); +} + +#[test] +fn tmux_mode() { + let mut zellij = start_zellij(); + claim_first_terminal_and_wait_for_prompt(&zellij); + + zellij.send_stdin(&keys::TMUX_MODE); + zellij.send_stdin(&keys::SPLIT_RIGHT_IN_TMUX_MODE); + let right_terminal = zellij.expect_pty_spawn(); + right_terminal.output(PROMPT); + + let grid_snapshot = zellij.wait_until("right terminal opened via tmux mode", |grid_snapshot| { + grid_snapshot.status_bar_appears() + && grid_snapshot.cursor_is_at(RIGHT_PANE_PROMPT_X, PROMPT_ROW) + }); + assert_snapshot!(normalized(&grid_snapshot)); + zellij.quit(); +} + +#[test] +fn bracketed_paste() { + let mut zellij = start_zellij(); + let terminal = claim_first_terminal_and_wait_for_prompt(&zellij); + + zellij.send_stdin(&keys::BRACKETED_PASTE_START); + zellij.send_stdin(&keys::TAB_MODE); + zellij.send_stdin(&keys::NEW_TAB_IN_TAB_MODE); + zellij.send_stdin(b"abc"); + zellij.send_stdin(&keys::BRACKETED_PASTE_END); + + terminal.wait_for_stdin("pasted text reached the pane", |stdin_bytes| { + stdin_bytes.windows(3).any(|window| window == b"abc") + }); + let grid_snapshot = zellij.wait_until("pasted text rendered, no new tab", |grid_snapshot| { + grid_snapshot.contains("abc") + && grid_snapshot.status_bar_appears() + && !grid_snapshot.contains("Tab #2") + }); + assert_snapshot!(normalized(&grid_snapshot)); + zellij.quit(); +} + +#[test] +fn scrolling_inside_a_pane() { + let mut zellij = start_zellij(); + claim_first_terminal_and_wait_for_prompt(&zellij); + + zellij.send_stdin(&keys::PANE_MODE); + zellij.send_stdin(&keys::SPLIT_RIGHT_IN_PANE_MODE); + let right_terminal = zellij.expect_pty_spawn(); + right_terminal.output(PROMPT); + zellij.wait_until("right terminal prompt rendered", |grid_snapshot| { + grid_snapshot.status_bar_appears() + && grid_snapshot.cursor_is_at(RIGHT_PANE_PROMPT_X, PROMPT_ROW) + }); + + for line in 1..=21 { + right_terminal.output(format!("line{}\r\n", line).as_bytes()); + } + zellij.wait_until("right terminal filled past its viewport", |grid_snapshot| { + grid_snapshot.contains("line21") + }); + + zellij.send_stdin(&keys::SCROLL_MODE); + zellij.send_stdin(&keys::SCROLL_UP_IN_SCROLL_MODE); + zellij.send_stdin(&keys::SCROLL_UP_IN_SCROLL_MODE); + + let grid_snapshot = + zellij.wait_until("scrolled up one line inside the pane", |grid_snapshot| { + grid_snapshot.contains("SCROLL: 2/2") && grid_snapshot.contains("PgDn|PgUp") + }); + assert_snapshot!(normalized(&grid_snapshot)); + zellij.quit(); +} diff --git a/zellij-integration-tests/tests/mouse.rs b/zellij-integration-tests/tests/mouse.rs new file mode 100644 index 00000000..40562a12 --- /dev/null +++ b/zellij-integration-tests/tests/mouse.rs @@ -0,0 +1,132 @@ +#![cfg(unix)] + +use insta::assert_snapshot; +use zellij_integration_tests::{keys, normalized, FakePtyHandle, Size, TestRunner, TestSession}; + +const TERMINAL_SIZE: Size = Size { + cols: 120, + rows: 24, +}; +const PROMPT: &[u8] = b"$ "; +const PROMPT_ROW: usize = 2; +const FIRST_PANE_PROMPT_X: usize = 3; +const RIGHT_PANE_PROMPT_X: usize = 63; + +fn start_zellij() -> TestSession { + TestRunner::new(TERMINAL_SIZE) + .with_config("mouse_mode true") + .start() +} + +fn sgr_mouse_report(column: usize, line: usize, button: u8) -> Vec<u8> { + format!("\u{1b}[<{};{};{}M", button, column, line).into_bytes() +} + +fn claim_first_terminal_and_wait_for_prompt(zellij: &TestSession) -> FakePtyHandle { + let terminal = zellij.expect_pty_spawn(); + terminal.output(PROMPT); + zellij.wait_until( + "first terminal prompt rendered in loaded app", + |grid_snapshot| { + grid_snapshot.tab_bar_appears() + && grid_snapshot.status_bar_appears() + && grid_snapshot.cursor_is_at(FIRST_PANE_PROMPT_X, PROMPT_ROW) + }, + ); + terminal +} + +fn split_right_and_wait_for_prompt(zellij: &TestSession) -> FakePtyHandle { + zellij.send_stdin(&keys::PANE_MODE); + zellij.send_stdin(&keys::SPLIT_RIGHT_IN_PANE_MODE); + let terminal = zellij.expect_pty_spawn(); + terminal.output(PROMPT); + zellij.wait_until("right terminal prompt rendered", |grid_snapshot| { + grid_snapshot.status_bar_appears() + && grid_snapshot.cursor_is_at(RIGHT_PANE_PROMPT_X, PROMPT_ROW) + }); + terminal +} + +#[test] +fn focus_pane_with_mouse() { + let mut zellij = start_zellij(); + claim_first_terminal_and_wait_for_prompt(&zellij); + split_right_and_wait_for_prompt(&zellij); + + zellij.send_stdin(&sgr_mouse_report(2, 5, 0)); + let grid_snapshot = zellij.wait_until("focus moved back to the left pane", |grid_snapshot| { + grid_snapshot.status_bar_appears() + && grid_snapshot.cursor_is_at(FIRST_PANE_PROMPT_X, PROMPT_ROW) + }); + assert_snapshot!(normalized(&grid_snapshot)); + zellij.quit(); +} + +#[test] +fn scrolling_inside_a_pane_with_mouse() { + let mut zellij = start_zellij(); + claim_first_terminal_and_wait_for_prompt(&zellij); + let right_terminal = split_right_and_wait_for_prompt(&zellij); + + for line in 1..=21 { + right_terminal.output(format!("line{}\r\n", line).as_bytes()); + } + zellij.wait_until("right pane filled past its viewport", |grid_snapshot| { + grid_snapshot.contains("line21") + }); + + zellij.send_stdin(&sgr_mouse_report(64, 2, 64)); + let grid_snapshot = zellij.wait_until("scrolled up inside the right pane", |grid_snapshot| { + grid_snapshot.status_bar_appears() + && grid_snapshot.contains("SCROLL:") + && grid_snapshot.contains("line1 ") + }); + assert_snapshot!(normalized(&grid_snapshot)); + zellij.quit(); +} + +#[test] +fn pin_floating_panes() { + let mut zellij = start_zellij(); + let tiled_terminal = claim_first_terminal_and_wait_for_prompt(&zellij); + + zellij.send_stdin(&keys::PANE_MODE); + zellij.send_stdin(&keys::TOGGLE_FLOATING_PANES); + let floating_terminal = zellij.expect_pty_spawn(); + floating_terminal.output(PROMPT); + zellij.wait_until("floating pane shows the unpinned button", |grid_snapshot| { + grid_snapshot.contains("PIN [ ]") + }); + + zellij.send_stdin(&sgr_mouse_report(87, 8, 0)); + zellij.wait_until("floating pane became pinned", |grid_snapshot| { + grid_snapshot.contains("PIN [+]") + }); + + zellij.send_stdin(&keys::PANE_MODE); + zellij.send_stdin(&keys::TOGGLE_FLOATING_PANES); + zellij.wait_until( + "focus settled on the tiled pane while the pinned pane stays up", + |grid_snapshot| { + grid_snapshot.status_bar_appears() + && grid_snapshot.contains("PIN [+]") + && grid_snapshot.cursor_is_at(FIRST_PANE_PROMPT_X, PROMPT_ROW) + }, + ); + + for line in 1..=13 { + tiled_terminal.output(format!("line{}{}\r\n", line, "a".repeat(112)).as_bytes()); + } + + let grid_snapshot = zellij.wait_until( + "pinned pane stays visible over the filled tiled pane", + |grid_snapshot| { + grid_snapshot.status_bar_appears() + && grid_snapshot.contains("PIN [+]") + && grid_snapshot.contains("line13") + }, + ); + assert_snapshot!(normalized(&grid_snapshot)); + zellij.quit(); +} diff --git a/zellij-integration-tests/tests/panes.rs b/zellij-integration-tests/tests/panes.rs new file mode 100644 index 00000000..5e14b103 --- /dev/null +++ b/zellij-integration-tests/tests/panes.rs @@ -0,0 +1,228 @@ +#![cfg(unix)] + +use insta::assert_snapshot; +use zellij_integration_tests::{keys, normalized, FakePtyHandle, Size, TestRunner, TestSession}; + +const TERMINAL_SIZE: Size = Size { + cols: 120, + rows: 24, +}; +const PROMPT: &[u8] = b"$ "; +const PROMPT_ROW: usize = 2; +const FIRST_PANE_PROMPT_X: usize = 3; +const RIGHT_PANE_PROMPT_X: usize = 63; + +fn start_zellij() -> TestSession { + TestRunner::new(TERMINAL_SIZE).start() +} + +fn claim_first_terminal_and_wait_for_prompt(zellij: &TestSession) -> FakePtyHandle { + let terminal = zellij.expect_pty_spawn(); + terminal.output(PROMPT); + zellij.wait_until( + "first terminal prompt rendered in loaded app", + |grid_snapshot| { + grid_snapshot.tab_bar_appears() + && grid_snapshot.status_bar_appears() + && grid_snapshot.cursor_is_at(FIRST_PANE_PROMPT_X, PROMPT_ROW) + }, + ); + terminal +} + +fn split_right_and_wait_for_prompt(zellij: &TestSession) -> FakePtyHandle { + zellij.send_stdin(&keys::PANE_MODE); + zellij.send_stdin(&keys::SPLIT_RIGHT_IN_PANE_MODE); + let terminal = zellij.expect_pty_spawn(); + terminal.output(PROMPT); + zellij.wait_until("right terminal prompt rendered", |grid_snapshot| { + grid_snapshot.status_bar_appears() + && grid_snapshot.cursor_is_at(RIGHT_PANE_PROMPT_X, PROMPT_ROW) + }); + terminal +} + +#[test] +fn cannot_split_terminals_vertically_when_active_terminal_is_too_small() { + let mut zellij = TestRunner::new(Size { cols: 8, rows: 20 }).start(); + let terminal = zellij.expect_pty_spawn(); + terminal.output(PROMPT); + zellij.wait_until("first terminal prompt rendered", |grid_snapshot| { + grid_snapshot.cursor_is_at(FIRST_PANE_PROMPT_X, PROMPT_ROW) + }); + let (width_before_split, _) = terminal.wait_for_size("first terminal sized", |_, _| true); + + zellij.send_stdin(&keys::PANE_MODE); + zellij.send_stdin(&keys::SPLIT_RIGHT_IN_PANE_MODE); + + terminal.output(b"done"); + let tab_bar_glyph = '\u{e0b0}'; + let grid_snapshot = zellij.wait_until( + "split attempt processed, chrome rendered", + |grid_snapshot| { + grid_snapshot.contains("done") + && grid_snapshot.contains("Ctrl +") + && grid_snapshot + .lines() + .first() + .is_some_and(|first_line| first_line.contains(tab_bar_glyph)) + }, + ); + + assert_eq!(terminal.size().unwrap().0, width_before_split); + assert_snapshot!(normalized(&grid_snapshot)); + zellij.quit(); +} + +#[test] +fn key_after_mode_switch_is_interpreted_in_new_mode_not_leaked_to_pane() { + let mut zellij = start_zellij(); + let first_terminal = claim_first_terminal_and_wait_for_prompt(&zellij); + + zellij.send_stdin(&keys::PANE_MODE); + zellij.send_stdin(&keys::SPLIT_RIGHT_IN_PANE_MODE); + + let second_terminal = zellij.expect_pty_spawn(); + second_terminal.output(PROMPT); + zellij.wait_until("split-right interpreted in pane mode", |grid_snapshot| { + grid_snapshot.cursor_is_at(RIGHT_PANE_PROMPT_X, PROMPT_ROW) + }); + + assert!(!first_terminal + .stdin_bytes() + .contains(&keys::SPLIT_RIGHT_IN_PANE_MODE[0])); + zellij.quit(); +} + +#[test] +fn toggle_pane_fullscreen() { + let mut zellij = start_zellij(); + claim_first_terminal_and_wait_for_prompt(&zellij); + split_right_and_wait_for_prompt(&zellij); + + zellij.send_stdin(&keys::PANE_MODE); + zellij.send_stdin(&keys::TOGGLE_ACTIVE_TERMINAL_FULLSCREEN_IN_PANE_MODE); + + let grid_snapshot = zellij.wait_until("focused pane is fullscreen", |grid_snapshot| { + grid_snapshot.cursor_is_at(FIRST_PANE_PROMPT_X, PROMPT_ROW) + && grid_snapshot.contains("LOCK") + && grid_snapshot.contains("(FULLSCREEN)") + }); + assert_snapshot!(normalized(&grid_snapshot)); + zellij.quit(); +} + +#[test] +fn close_pane() { + let mut zellij = start_zellij(); + claim_first_terminal_and_wait_for_prompt(&zellij); + split_right_and_wait_for_prompt(&zellij); + + zellij.send_stdin(&keys::PANE_MODE); + zellij.send_stdin(&keys::CLOSE_PANE_IN_PANE_MODE); + + let grid_snapshot = + zellij.wait_until("right pane closed, focus back on first", |grid_snapshot| { + grid_snapshot.status_bar_appears() + && grid_snapshot.cursor_is_at(FIRST_PANE_PROMPT_X, PROMPT_ROW) + }); + assert_snapshot!(normalized(&grid_snapshot)); + zellij.quit(); +} + +#[test] +fn closing_last_pane_exits_zellij() { + let zellij = start_zellij(); + claim_first_terminal_and_wait_for_prompt(&zellij); + + zellij.send_stdin(&keys::PANE_MODE); + zellij.send_stdin(&keys::CLOSE_PANE_IN_PANE_MODE); + + zellij.wait_until( + "zellij exited after closing the last pane", + |grid_snapshot| grid_snapshot.contains("Bye from Zellij!"), + ); +} + +#[test] +fn pane_closes_when_its_process_exits() { + let mut zellij = start_zellij(); + claim_first_terminal_and_wait_for_prompt(&zellij); + let right_terminal = split_right_and_wait_for_prompt(&zellij); + + right_terminal.exit(Some(0)); + + let grid_snapshot = + zellij.wait_until("right pane closed, focus back on first", |grid_snapshot| { + grid_snapshot.status_bar_appears() + && grid_snapshot.cursor_is_at(FIRST_PANE_PROMPT_X, PROMPT_ROW) + }); + assert_snapshot!(normalized(&grid_snapshot)); + zellij.quit(); +} + +#[test] +fn toggle_floating_panes() { + let mut zellij = start_zellij(); + claim_first_terminal_and_wait_for_prompt(&zellij); + + zellij.send_stdin(&keys::PANE_MODE); + zellij.send_stdin(&keys::TOGGLE_FLOATING_PANES); + let floating_terminal = zellij.expect_pty_spawn(); + floating_terminal.output(PROMPT); + + let grid_snapshot = zellij.wait_until("floating pane appeared", |grid_snapshot| { + grid_snapshot.cursor_is_at(33, 8) + && grid_snapshot.contains("STAGGERED") + && grid_snapshot.contains("LOCK") + }); + assert_snapshot!(normalized(&grid_snapshot)); + zellij.quit(); +} + +#[test] +fn undo_rename_pane() { + let mut zellij = start_zellij(); + claim_first_terminal_and_wait_for_prompt(&zellij); + + zellij.send_stdin(&keys::PANE_MODE); + zellij.send_stdin(&keys::RENAME_PANE_MODE); + zellij.send_stdin(b"aa"); + zellij.send_stdin(&keys::ESC); + zellij.send_stdin(&keys::ESC); + + let grid_snapshot = zellij.wait_until("pane name reverted to default", |grid_snapshot| { + grid_snapshot.contains("Pane #1") && grid_snapshot.contains("LOCK") + }); + assert_snapshot!(normalized(&grid_snapshot)); + zellij.quit(); +} + +#[test] +fn start_without_pane_frames() { + let mut zellij = TestRunner::new(TERMINAL_SIZE) + .with_config("pane_frames false") + .start(); + let first_terminal = zellij.expect_pty_spawn(); + first_terminal.output(PROMPT); + zellij.wait_until( + "first frameless terminal prompt rendered", + |grid_snapshot| { + grid_snapshot.tab_bar_appears() + && grid_snapshot.status_bar_appears() + && grid_snapshot.cursor_is_at(2, 1) + }, + ); + + zellij.send_stdin(&keys::PANE_MODE); + zellij.send_stdin(&keys::SPLIT_RIGHT_IN_PANE_MODE); + let right_terminal = zellij.expect_pty_spawn(); + right_terminal.output(PROMPT); + + let grid_snapshot = zellij.wait_until( + "right frameless terminal prompt rendered", + |grid_snapshot| grid_snapshot.status_bar_appears() && grid_snapshot.cursor_is_at(62, 1), + ); + assert_snapshot!(normalized(&grid_snapshot)); + zellij.quit(); +} diff --git a/zellij-integration-tests/tests/plugins.rs b/zellij-integration-tests/tests/plugins.rs new file mode 100644 index 00000000..e39c1af3 --- /dev/null +++ b/zellij-integration-tests/tests/plugins.rs @@ -0,0 +1,41 @@ +#![cfg(unix)] + +use insta::assert_snapshot; +use zellij_integration_tests::{normalized, Size, TestRunner}; + +const TERMINAL_SIZE: Size = Size { + cols: 120, + rows: 24, +}; + +fn load_background_plugin_config() -> String { + let plugin_path = format!( + "{}/../zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm", + env!("CARGO_MANIFEST_DIR") + ); + format!( + "plugins {{\n permission-requester location=\"file:{}\"\n}}\nload_plugins {{\n \"permission-requester\"\n}}\n", + plugin_path + ) +} + +#[test] +fn load_plugins_in_background_on_startup() { + let mut zellij = TestRunner::new(TERMINAL_SIZE) + .with_config(&load_background_plugin_config()) + .start(); + let terminal = zellij.expect_pty_spawn(); + terminal.output(b"$ "); + + let grid_snapshot = zellij + .wait_until("background plugin requests permissions", |grid_snapshot| { + grid_snapshot.contains("Allow? (y/n)") && grid_snapshot.tab_bar_appears() + }); + assert_snapshot!(normalized(&grid_snapshot)); + zellij.send_stdin(b"y"); + zellij.wait_until( + "permission prompt dismissed after granting", + |grid_snapshot| !grid_snapshot.contains("Allow? (y/n)"), + ); + zellij.quit(); +} diff --git a/zellij-integration-tests/tests/resurrection.rs b/zellij-integration-tests/tests/resurrection.rs new file mode 100644 index 00000000..55df5b32 --- /dev/null +++ b/zellij-integration-tests/tests/resurrection.rs @@ -0,0 +1,108 @@ +#![cfg(unix)] + +use insta::assert_snapshot; +use zellij_integration_tests::{keys, normalized, LayoutInfo, Size, TestRunner, TestSession}; + +const TERMINAL_SIZE: Size = Size { + cols: 120, + rows: 24, +}; +const PROMPT: &[u8] = b"$ "; + +const RESURRECT_LAYOUT: &str = r#" +layout { + default_tab_template { + pane size=1 borderless=true { + plugin location="tab-bar" + } + children + pane size=1 borderless=true { + plugin location="status-bar" + } + } + tab name="alpha" { + pane + } + tab name="beta" focus=true { + pane + } +} +"#; + +fn start_serializing_session(extra_config: &str) -> TestSession { + TestRunner::new(TERMINAL_SIZE) + .with_config(&format!("session_serialization true\n{}", extra_config)) + .with_layout(LayoutInfo::Stringified(RESURRECT_LAYOUT.to_string())) + .start() +} + +fn wait_for_layout_loaded(zellij: &TestSession) { + zellij.wait_until("layout loaded with both tabs", |grid_snapshot| { + grid_snapshot.contains("alpha") + && grid_snapshot.contains("beta") + && grid_snapshot.status_bar_appears() + }); +} + +#[test] +fn quit_and_resurrect_session() { + let mut zellij = start_serializing_session(""); + wait_for_layout_loaded(&zellij); + + zellij.send_stdin(&keys::PANE_MODE); + zellij.send_stdin(&keys::SPLIT_RIGHT_IN_PANE_MODE); + let new_pane = zellij.expect_pty_spawn(); + new_pane.output(PROMPT); + zellij.wait_until("new pane opened before serialization", |grid_snapshot| { + grid_snapshot.contains("┐┌") + }); + + zellij.save_session(); + zellij.wait_for_serialized_session(); + zellij.quit(); + + zellij.resurrect(TERMINAL_SIZE); + let grid_snapshot = zellij.wait_until( + "resurrected session restored the runtime pane", + |grid_snapshot| { + grid_snapshot.contains("Zellij (test") + && grid_snapshot.contains("alpha") + && grid_snapshot.contains("beta") + && grid_snapshot.contains("┐┌") + && grid_snapshot.status_bar_appears() + }, + ); + assert_snapshot!(normalized(&grid_snapshot)); + zellij.quit(); +} + +#[test] +fn quit_and_resurrect_session_with_viewport_serialization() { + let mut zellij = TestRunner::new(TERMINAL_SIZE) + .with_config("session_serialization true\nserialize_pane_viewport true") + .start(); + let terminal = zellij.expect_pty_spawn(); + terminal.output(b"VIEWPORT_CONTENT_TO_RESTORE\n"); + zellij.wait_until( + "pane content rendered before serialization", + |grid_snapshot| { + grid_snapshot.contains("VIEWPORT_CONTENT_TO_RESTORE") + && grid_snapshot.status_bar_appears() + }, + ); + + zellij.save_session(); + zellij.wait_for_serialized_session(); + zellij.quit(); + + zellij.resurrect(TERMINAL_SIZE); + let grid_snapshot = + zellij.wait_until("resurrected viewport and chrome settled", |grid_snapshot| { + grid_snapshot.contains("VIEWPORT_CONTENT_TO_RESTORE") + && grid_snapshot.contains("Zellij (test") + && grid_snapshot.tab_bar_appears() + && grid_snapshot.status_bar_appears() + }); + assert_snapshot!(normalized(&grid_snapshot)); + zellij.quit(); +} diff --git a/zellij-integration-tests/tests/snapshots/app__edit_scrollback.snap b/zellij-integration-tests/tests/snapshots/app__edit_scrollback.snap new file mode 100644 index 00000000..80505f55 --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/app__edit_scrollback.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/app.rs +expression: normalized(&grid_snapshot) +--- + Zellij (test) Tab #1 +┌ EDITING SCROLLBACK ──────────────────────────────────────────────────────────────────────────────────────────────────┐ +│$ SCROLLBACK_MARKER_ONE │ +│SCROLLBACK_MARKER_TWO │ +│█ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SEARCH <o> SESSION <q> QUIT ... diff --git a/zellij-integration-tests/tests/snapshots/app__resize_terminal_window.snap b/zellij-integration-tests/tests/snapshots/app__resize_terminal_window.snap new file mode 100644 index 00000000..2a285443 --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/app__resize_terminal_window.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/app.rs +expression: normalized(&grid_snapshot) +--- + Zellij (test) Tab #1 +┌ Pane #1 ───────────────────────────────────────┐┌ Pane #2 ───────────────────────────────────────┐ +│$ ││$ █ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +└────────────────────────────────────────────────┘└────────────────────────────────────────────────┘ + Ctrl + g p t n h s o q ... diff --git a/zellij-integration-tests/tests/snapshots/app__status_bar_loads_custom_keybindings.snap b/zellij-integration-tests/tests/snapshots/app__status_bar_loads_custom_keybindings.snap new file mode 100644 index 00000000..0670f288 --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/app__status_bar_loads_custom_keybindings.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/app.rs +expression: normalized(&grid_snapshot) +--- + Zellij (test) Tab #1 +┌ Pane #1 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│$ █ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + F1 LOCK F2 PANE F3 TAB F4 RESIZE F5 MOVE F6 SEARCH Alt F7 SESSION Ctrl F8 QUIT diff --git a/zellij-integration-tests/tests/snapshots/app__use_custom_layout_with_relative_path.snap b/zellij-integration-tests/tests/snapshots/app__use_custom_layout_with_relative_path.snap new file mode 100644 index 00000000..a60604ea --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/app__use_custom_layout_with_relative_path.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/app.rs +expression: normalized(&grid_snapshot) +--- + + Tip: UNBOUND => open new pane. UNBOUND => navigate between panes. UNBOUND => increase/decrease pane size. +┌ Pane #1 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│$ █ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + Zellij (test) Tab #1 diff --git a/zellij-integration-tests/tests/snapshots/basic__resize_pane.snap b/zellij-integration-tests/tests/snapshots/basic__resize_pane.snap new file mode 100644 index 00000000..ba160d8c --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/basic__resize_pane.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/basic.rs +expression: normalized(&snapshot) +--- + Zellij (test) Tab #1 +┌ Pane #1 ───────────────────────────────────────────┐┌ Pane #2 ───────────────────────────────────────────────────────┐ +│$ ││$ █ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +└────────────────────────────────────────────────────┘└────────────────────────────────────────────────────────────────┘ + Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SEARCH <o> SESSION <q> QUIT ... diff --git a/zellij-integration-tests/tests/snapshots/basic__split_terminals_vertically.snap b/zellij-integration-tests/tests/snapshots/basic__split_terminals_vertically.snap new file mode 100644 index 00000000..7f5f5a69 --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/basic__split_terminals_vertically.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/basic.rs +expression: normalized(&snapshot) +--- + Zellij (test) Tab #1 +┌ Pane #1 ─────────────────────────────────────────────────┐┌ Pane #2 ─────────────────────────────────────────────────┐ +│$ ││$ █ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +└──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘ + Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SEARCH <o> SESSION <q> QUIT ... diff --git a/zellij-integration-tests/tests/snapshots/basic__starts_with_one_terminal.snap b/zellij-integration-tests/tests/snapshots/basic__starts_with_one_terminal.snap new file mode 100644 index 00000000..7fff8f8f --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/basic__starts_with_one_terminal.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/basic.rs +expression: normalized(&snapshot) +--- + Zellij (test) Tab #1 +┌ Pane #1 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│$ █ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SEARCH <o> SESSION <q> QUIT ... diff --git a/zellij-integration-tests/tests/snapshots/cli__override_layout_from_default_to_compact.snap b/zellij-integration-tests/tests/snapshots/cli__override_layout_from_default_to_compact.snap new file mode 100644 index 00000000..386bd7e5 --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/cli__override_layout_from_default_to_compact.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/cli.rs +expression: normalized(&grid_snapshot) +--- +┌ Pane #1 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│$ █ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + Zellij (test) NORMAL Tab #1 diff --git a/zellij-integration-tests/tests/snapshots/cli__send_blocking_command_through_the_cli.snap b/zellij-integration-tests/tests/snapshots/cli__send_blocking_command_through_the_cli.snap new file mode 100644 index 00000000..6b2d58e7 --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/cli__send_blocking_command_through_the_cli.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/cli.rs +expression: normalized(&grid_snapshot) +--- + Zellij (test) Tab #1 +┌ Pane #1 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│$ █ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SEARCH <o> SESSION <q> QUIT ... diff --git a/zellij-integration-tests/tests/snapshots/cli__send_command_through_the_cli.snap b/zellij-integration-tests/tests/snapshots/cli__send_command_through_the_cli.snap new file mode 100644 index 00000000..42573137 --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/cli__send_command_through_the_cli.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/cli.rs +expression: normalized(&grid_snapshot) +--- + Zellij (test) Tab #1 +┌ Pane #1 ─────────────────────────────────────────────────┐┌ suspended-command ───────────────────────────────────────┐ +│$ ││foo │ +│ ││foo │ +│ ││█ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +└──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘ + Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SEARCH <o> SESSION <q> QUIT ... diff --git a/zellij-integration-tests/tests/snapshots/cli__watcher_client_functionality-2.snap b/zellij-integration-tests/tests/snapshots/cli__watcher_client_functionality-2.snap new file mode 100644 index 00000000..707da328 --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/cli__watcher_client_functionality-2.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/cli.rs +expression: normalized(&watcher_snapshot) +--- + Zellij (test) Tab #1 +┌ Pane #1 ─────────────────────────────────────────────────┐┌ Pane #2 ─────────────────────────────────────────────────┐ +│$ ││$ █ │ +│WATCHER_OUTPUT_1 ││ │ +│WATCHER_OUTPUT_2 ││ │ +│WATCHER_OUTPUT_3 ││ │ +│WATCHER_DONE ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +└──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘ + Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SEARCH <o> SESSION <q> QUIT ... diff --git a/zellij-integration-tests/tests/snapshots/cli__watcher_client_functionality.snap b/zellij-integration-tests/tests/snapshots/cli__watcher_client_functionality.snap new file mode 100644 index 00000000..633d8f3b --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/cli__watcher_client_functionality.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/cli.rs +expression: normalized(&main_snapshot) +--- + Zellij (test) Tab #1 +┌ Pane #1 ─────────────────────────────────────────────────┐┌ Pane #2 ─────────────────────────────────────────────────┐ +│$ ││$ █ │ +│WATCHER_OUTPUT_1 ││ │ +│WATCHER_OUTPUT_2 ││ │ +│WATCHER_OUTPUT_3 ││ │ +│WATCHER_DONE ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +└──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘ + Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SEARCH <o> SESSION <q> QUIT ... diff --git a/zellij-integration-tests/tests/snapshots/clients__detach_and_attach_session.snap b/zellij-integration-tests/tests/snapshots/clients__detach_and_attach_session.snap new file mode 100644 index 00000000..14b6ad52 --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/clients__detach_and_attach_session.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/clients.rs +expression: normalized(&grid_snapshot) +--- + Zellij (test) Tab #1 +┌ Pane #1 ─────────────────────────────────────────────────┐┌ Pane #2 ─────────────────────────────────────────────────┐ +│$ ││$ I am some text█ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +└──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘ + Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SEARCH <o> SESSION <q> QUIT ... diff --git a/zellij-integration-tests/tests/snapshots/clients__mirrored_sessions.snap b/zellij-integration-tests/tests/snapshots/clients__mirrored_sessions.snap new file mode 100644 index 00000000..0e6e31cb --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/clients__mirrored_sessions.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/clients.rs +expression: normalized(&main_grid) +--- + Zellij (test) Tab #1 +┌ Pane #1 ─────────────────────────────────────────────────┐┌ Pane #2 ─────────────────────────────────────────────────┐ +│$ ││$ █ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +└──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘ + Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SEARCH <o> SESSION <q> QUIT ... diff --git a/zellij-integration-tests/tests/snapshots/clients__multiple_users_in_different_panes_and_same_tab-2.snap b/zellij-integration-tests/tests/snapshots/clients__multiple_users_in_different_panes_and_same_tab-2.snap new file mode 100644 index 00000000..59e690c5 --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/clients__multiple_users_in_different_panes_and_same_tab-2.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/clients.rs +expression: normalized(&second_grid) +--- + Zellij (test) Tab #1 [ ] +┌ Pane #1 ───────────┤ FOCUSED USER: ├───────────────────┐┌ Pane #2 ──────────────┤ MY FOCUS ├───────────────────────┐ +│$ ││$ █ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +└──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘ + Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SEARCH <o> SESSION <q> QUIT ... diff --git a/zellij-integration-tests/tests/snapshots/clients__multiple_users_in_different_panes_and_same_tab.snap b/zellij-integration-tests/tests/snapshots/clients__multiple_users_in_different_panes_and_same_tab.snap new file mode 100644 index 00000000..6ad8e0cd --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/clients__multiple_users_in_different_panes_and_same_tab.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/clients.rs +expression: normalized(&main_grid) +--- + Zellij (test) Tab #1 [ ] +┌ Pane #1 ──────────────┤ MY FOCUS ├───────────────────────┐┌ Pane #2 ───────────┤ FOCUSED USER: ├───────────────────┐ +│$ █ ││$ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +└──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘ + Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SEARCH <o> SESSION <q> QUIT ... diff --git a/zellij-integration-tests/tests/snapshots/clients__multiple_users_in_different_tabs-2.snap b/zellij-integration-tests/tests/snapshots/clients__multiple_users_in_different_tabs-2.snap new file mode 100644 index 00000000..0806898d --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/clients__multiple_users_in_different_tabs-2.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/clients.rs +expression: normalized(&second_grid) +--- + Zellij (test) Tab #1 [ ] Tab #2 +┌ Pane #1 ────────────────────────────────────────────┤ MY FOCUS ├─────────────────────────────────────────────────────┐ +│$ █ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SEARCH <o> SESSION <q> QUIT ... diff --git a/zellij-integration-tests/tests/snapshots/clients__multiple_users_in_different_tabs.snap b/zellij-integration-tests/tests/snapshots/clients__multiple_users_in_different_tabs.snap new file mode 100644 index 00000000..fe0a78a9 --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/clients__multiple_users_in_different_tabs.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/clients.rs +expression: normalized(&main_grid) +--- + Zellij (test) Tab #1 Tab #2 [ ] +┌ Pane #1 ────────────────────────────────────────────┤ MY FOCUS ├─────────────────────────────────────────────────────┐ +│$ █ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SEARCH <o> SESSION <q> QUIT ... diff --git a/zellij-integration-tests/tests/snapshots/clients__multiple_users_in_same_pane_and_tab-2.snap b/zellij-integration-tests/tests/snapshots/clients__multiple_users_in_same_pane_and_tab-2.snap new file mode 100644 index 00000000..ea21dde0 --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/clients__multiple_users_in_same_pane_and_tab-2.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/clients.rs +expression: normalized(&second_grid) +--- + Zellij (test) Tab #1 [ ] +┌ Pane #1 ─────────────────────────────────────────┤ MY FOCUS AND: ├─────────────────────────────────────────────────┐ +│$ █ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SEARCH <o> SESSION <q> QUIT ... diff --git a/zellij-integration-tests/tests/snapshots/clients__multiple_users_in_same_pane_and_tab.snap b/zellij-integration-tests/tests/snapshots/clients__multiple_users_in_same_pane_and_tab.snap new file mode 100644 index 00000000..5e27178e --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/clients__multiple_users_in_same_pane_and_tab.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/clients.rs +expression: normalized(&main_grid) +--- + Zellij (test) Tab #1 [ ] +┌ Pane #1 ─────────────────────────────────────────┤ MY FOCUS AND: ├─────────────────────────────────────────────────┐ +│$ █ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SEARCH <o> SESSION <q> QUIT ... diff --git a/zellij-integration-tests/tests/snapshots/modes__bracketed_paste.snap b/zellij-integration-tests/tests/snapshots/modes__bracketed_paste.snap new file mode 100644 index 00000000..421627c3 --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/modes__bracketed_paste.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/modes.rs +expression: normalized(&grid_snapshot) +--- + Zellij (test) Tab #1 +┌ Pane #1 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│$ nabc█ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SEARCH <o> SESSION <q> QUIT ... diff --git a/zellij-integration-tests/tests/snapshots/modes__lock_mode.snap b/zellij-integration-tests/tests/snapshots/modes__lock_mode.snap new file mode 100644 index 00000000..8dde2b5b --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/modes__lock_mode.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/modes.rs +expression: normalized(&grid_snapshot) +--- + Zellij (test) Tab #1 +┌ Pane #1 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│$ nabc█ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + Ctrl + <g> LOCK diff --git a/zellij-integration-tests/tests/snapshots/modes__scrolling_inside_a_pane.snap b/zellij-integration-tests/tests/snapshots/modes__scrolling_inside_a_pane.snap new file mode 100644 index 00000000..0af68b35 --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/modes__scrolling_inside_a_pane.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/modes.rs +expression: normalized(&grid_snapshot) +--- + Zellij (test) Tab #1 +┌ Pane #1 ─────────────────────────────────────────────────┐┌ Pane #2 ─────────────────────────────────── SCROLL: 2/2 ┐ +│$ ││$ line1 │ +│ ││line2 │ +│ ││line3 │ +│ ││line4 │ +│ ││line5 │ +│ ││line6 │ +│ ││line7 │ +│ ││line8 │ +│ ││line9 │ +│ ││line10 │ +│ ││line11 │ +│ ││line12 │ +│ ││line13 │ +│ ││line14 │ +│ ││line15 │ +│ ││line16 │ +│ ││line17 │ +│ ││line18 │ +│ ││line19 │ +│ ││█ine20 │ +└──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘ + Ctrl + <s> SEARCH s Search ↓↑ Scroll PgDn|PgUp Scroll d|u Scroll e Edit ENTER Select diff --git a/zellij-integration-tests/tests/snapshots/modes__tmux_mode.snap b/zellij-integration-tests/tests/snapshots/modes__tmux_mode.snap new file mode 100644 index 00000000..42363818 --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/modes__tmux_mode.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/modes.rs +expression: normalized(&grid_snapshot) +--- + Zellij (test) Tab #1 +┌ Pane #1 ─────────────────────────────────────────────────┐┌ Pane #2 ─────────────────────────────────────────────────┐ +│$ ││$ █ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +└──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘ + Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SEARCH <o> SESSION <q> QUIT ... diff --git a/zellij-integration-tests/tests/snapshots/mouse__focus_pane_with_mouse.snap b/zellij-integration-tests/tests/snapshots/mouse__focus_pane_with_mouse.snap new file mode 100644 index 00000000..f007cf9d --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/mouse__focus_pane_with_mouse.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/mouse.rs +expression: normalized(&grid_snapshot) +--- + Zellij (test) Tab #1 +┌ Pane #1 ─────────────────────────────────────────────────┐┌ Pane #2 ─────────────────────────────────────────────────┐ +│$ █ ││$ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +└──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘ + Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SEARCH <o> SESSION <q> QUIT ... diff --git a/zellij-integration-tests/tests/snapshots/mouse__pin_floating_panes.snap b/zellij-integration-tests/tests/snapshots/mouse__pin_floating_panes.snap new file mode 100644 index 00000000..6bdb9ca6 --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/mouse__pin_floating_panes.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/mouse.rs +expression: normalized(&grid_snapshot) +--- + Zellij (test) Tab #1 +┌ Pane #1 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│$ line1aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa│ +│a │ +│line2aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa │ +│line3aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa │ +│line4aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa │ +│line5aaaaaaaaaaaaaaaaaaaaaaaa┌ Pane #2 ──────────────────────────────────────── PIN [+] ┐aaaaaaaaaaaaaaaaaaaaaaaaaaaa │ +│line6aaaaaaaaaaaaaaaaaaaaaaaa│$ │aaaaaaaaaaaaaaaaaaaaaaaaaaaa │ +│line7aaaaaaaaaaaaaaaaaaaaaaaa│ │aaaaaaaaaaaaaaaaaaaaaaaaaaaa │ +│line8aaaaaaaaaaaaaaaaaaaaaaaa│ │aaaaaaaaaaaaaaaaaaaaaaaaaaaa │ +│line9aaaaaaaaaaaaaaaaaaaaaaaa│ │aaaaaaaaaaaaaaaaaaaaaaaaaaaa │ +│line10aaaaaaaaaaaaaaaaaaaaaaa│ │aaaaaaaaaaaaaaaaaaaaaaaaaaaaa│ +│line11aaaaaaaaaaaaaaaaaaaaaaa│ │aaaaaaaaaaaaaaaaaaaaaaaaaaaaa│ +│line12aaaaaaaaaaaaaaaaaaaaaaa│ │aaaaaaaaaaaaaaaaaaaaaaaaaaaaa│ +│line13aaaaaaaaaaaaaaaaaaaaaaa│ │aaaaaaaaaaaaaaaaaaaaaaaaaaaaa│ +│█ │ │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SEARCH <o> SESSION <q> QUIT ... diff --git a/zellij-integration-tests/tests/snapshots/mouse__scrolling_inside_a_pane_with_mouse.snap b/zellij-integration-tests/tests/snapshots/mouse__scrolling_inside_a_pane_with_mouse.snap new file mode 100644 index 00000000..629cc513 --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/mouse__scrolling_inside_a_pane_with_mouse.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/mouse.rs +expression: normalized(&grid_snapshot) +--- + Zellij (test) Tab #1 +┌ Pane #1 ─────────────────────────────────────────────────┐┌ Pane #2 ─────────────────────────────────── SCROLL: 2/2 ┐ +│$ ││$ line1 │ +│ ││line2 │ +│ ││line3 │ +│ ││line4 │ +│ ││line5 │ +│ ││line6 │ +│ ││line7 │ +│ ││line8 │ +│ ││line9 │ +│ ││line10 │ +│ ││line11 │ +│ ││line12 │ +│ ││line13 │ +│ ││line14 │ +│ ││line15 │ +│ ││line16 │ +│ ││line17 │ +│ ││line18 │ +│ ││line19 │ +│ ││█ine20 │ +└──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘ + Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SEARCH <o> SESSION <q> QUIT ... diff --git a/zellij-integration-tests/tests/snapshots/panes__cannot_split_terminals_vertically_when_active_terminal_is_too_small.snap b/zellij-integration-tests/tests/snapshots/panes__cannot_split_terminals_vertically_when_active_terminal_is_too_small.snap new file mode 100644 index 00000000..0800ee6c --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/panes__cannot_split_terminals_vertically_when_active_terminal_is_too_small.snap @@ -0,0 +1,24 @@ +--- +source: zellij-integration-tests/tests/panes.rs +expression: normalized(&grid_snapshot) +--- + +┌──────┐ +│$ done│ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────┘ + Ctrl + diff --git a/zellij-integration-tests/tests/snapshots/panes__close_pane.snap b/zellij-integration-tests/tests/snapshots/panes__close_pane.snap new file mode 100644 index 00000000..b9d0bb9c --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/panes__close_pane.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/panes.rs +expression: normalized(&grid_snapshot) +--- + Zellij (test) Tab #1 +┌ Pane #1 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│$ █ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SEARCH <o> SESSION <q> QUIT ... diff --git a/zellij-integration-tests/tests/snapshots/panes__pane_closes_when_its_process_exits.snap b/zellij-integration-tests/tests/snapshots/panes__pane_closes_when_its_process_exits.snap new file mode 100644 index 00000000..b9d0bb9c --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/panes__pane_closes_when_its_process_exits.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/panes.rs +expression: normalized(&grid_snapshot) +--- + Zellij (test) Tab #1 +┌ Pane #1 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│$ █ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SEARCH <o> SESSION <q> QUIT ... diff --git a/zellij-integration-tests/tests/snapshots/panes__start_without_pane_frames.snap b/zellij-integration-tests/tests/snapshots/panes__start_without_pane_frames.snap new file mode 100644 index 00000000..e58b8c3e --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/panes__start_without_pane_frames.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/panes.rs +expression: normalized(&grid_snapshot) +--- + Zellij (test) Tab #1 +$ │$ █ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SEARCH <o> SESSION <q> QUIT ... diff --git a/zellij-integration-tests/tests/snapshots/panes__toggle_floating_panes.snap b/zellij-integration-tests/tests/snapshots/panes__toggle_floating_panes.snap new file mode 100644 index 00000000..3a4e1240 --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/panes__toggle_floating_panes.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/panes.rs +expression: normalized(&grid_snapshot) +--- + Zellij (test) Tab #1 +┌ Pane #1 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│$ │ +│ │ +│ │ +│ │ +│ │ +│ ┌ Pane #2 ──────────────────────────────────────── PIN [ ] ┐ │ +│ │$ █ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SEARCH <o> SESSION <q> QUIT ... diff --git a/zellij-integration-tests/tests/snapshots/panes__toggle_pane_fullscreen.snap b/zellij-integration-tests/tests/snapshots/panes__toggle_pane_fullscreen.snap new file mode 100644 index 00000000..fcb1eba9 --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/panes__toggle_pane_fullscreen.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/panes.rs +expression: normalized(&grid_snapshot) +--- + Zellij (test) Tab #1 (FULLSCREEN) +┌ Pane #2 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│$ █ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SEARCH <o> SESSION <q> QUIT ... diff --git a/zellij-integration-tests/tests/snapshots/panes__undo_rename_pane.snap b/zellij-integration-tests/tests/snapshots/panes__undo_rename_pane.snap new file mode 100644 index 00000000..b9d0bb9c --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/panes__undo_rename_pane.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/panes.rs +expression: normalized(&grid_snapshot) +--- + Zellij (test) Tab #1 +┌ Pane #1 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│$ █ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SEARCH <o> SESSION <q> QUIT ... diff --git a/zellij-integration-tests/tests/snapshots/plugins__load_plugins_in_background_on_startup.snap b/zellij-integration-tests/tests/snapshots/plugins__load_plugins_in_background_on_startup.snap new file mode 100644 index 00000000..7341d7a6 --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/plugins__load_plugins_in_background_on_startup.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/plugins.rs +expression: normalized(&grid_snapshot) +--- + Zellij (test) Tab #1 +┌ Pane #1 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│$ │ +│ │ +│ │ +│ │ +│ ┌ (.) - permission-requester ───────────────────── PIN [ ] ┐ │ +│ │This plugin asks permission to: ReadApplicationState, Chan│ │ +│ │geApplicationState, OpenFiles, RunCommands, OpenTerminalsO│ │ +│ │rPlugins, WriteToStdin, WebAccess, ReadCliPipes, MessageAn│ │ +│ │dLaunchOtherPlugins, Reconfigure, RunActionsAsUser, WriteT│ │ +│ │oClipboard, ReadSessionEnvironmentVariables. Allow? (y/n) │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-integration-tests/tests/snapshots/resurrection__quit_and_resurrect_session.snap b/zellij-integration-tests/tests/snapshots/resurrection__quit_and_resurrect_session.snap new file mode 100644 index 00000000..414fdd10 --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/resurrection__quit_and_resurrect_session.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/resurrection.rs +expression: normalized(&grid_snapshot) +--- + Zellij (test) alpha beta +┌ Pane #1 ─────────────────────────────────────────────────┐┌ Pane #2 ─────────────────────────────────────────────────┐ +│ ││█ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +└──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘ + Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SEARCH <o> SESSION <q> QUIT ... diff --git a/zellij-integration-tests/tests/snapshots/resurrection__quit_and_resurrect_session_with_viewport_serialization.snap b/zellij-integration-tests/tests/snapshots/resurrection__quit_and_resurrect_session_with_viewport_serialization.snap new file mode 100644 index 00000000..9cdf365b --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/resurrection__quit_and_resurrect_session_with_viewport_serialization.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/resurrection.rs +expression: normalized(&grid_snapshot) +--- + Zellij (test) Tab #1 +┌ Pane #1 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ │ +│VIEWPORT_CONTENT_TO_RESTORE │ +│ │ +│█ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SEARCH <o> SESSION <q> QUIT ... diff --git a/zellij-integration-tests/tests/snapshots/tabs__close_tab.snap b/zellij-integration-tests/tests/snapshots/tabs__close_tab.snap new file mode 100644 index 00000000..f1544d42 --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/tabs__close_tab.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/tabs.rs +expression: normalized(&grid_snapshot) +--- + Zellij (test) Tab #1 +┌ Pane #1 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│$ █ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SEARCH <o> SESSION <q> QUIT ... diff --git a/zellij-integration-tests/tests/snapshots/tabs__move_tab_to_left.snap b/zellij-integration-tests/tests/snapshots/tabs__move_tab_to_left.snap new file mode 100644 index 00000000..5afe4d28 --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/tabs__move_tab_to_left.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/tabs.rs +expression: normalized(&grid_snapshot) +--- + Zellij (test) Tab #1 Tab #3 Tab #2 +┌ Pane #1 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│$ █ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SEARCH <o> SESSION <q> QUIT ... diff --git a/zellij-integration-tests/tests/snapshots/tabs__move_tab_to_left_until_it_wraps_around.snap b/zellij-integration-tests/tests/snapshots/tabs__move_tab_to_left_until_it_wraps_around.snap new file mode 100644 index 00000000..d045b135 --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/tabs__move_tab_to_left_until_it_wraps_around.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/tabs.rs +expression: normalized(&grid_snapshot) +--- + Zellij (test) Tab #1 Tab #2 Tab #3 +┌ Pane #1 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│$ █ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SEARCH <o> SESSION <q> QUIT ... diff --git a/zellij-integration-tests/tests/snapshots/tabs__move_tab_to_right.snap b/zellij-integration-tests/tests/snapshots/tabs__move_tab_to_right.snap new file mode 100644 index 00000000..5afe4d28 --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/tabs__move_tab_to_right.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/tabs.rs +expression: normalized(&grid_snapshot) +--- + Zellij (test) Tab #1 Tab #3 Tab #2 +┌ Pane #1 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│$ █ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SEARCH <o> SESSION <q> QUIT ... diff --git a/zellij-integration-tests/tests/snapshots/tabs__move_tab_to_right_until_it_wraps_around.snap b/zellij-integration-tests/tests/snapshots/tabs__move_tab_to_right_until_it_wraps_around.snap new file mode 100644 index 00000000..2b4f6227 --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/tabs__move_tab_to_right_until_it_wraps_around.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/tabs.rs +expression: normalized(&grid_snapshot) +--- + Zellij (test) Tab #3 Tab #1 Tab #2 +┌ Pane #1 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│$ █ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SEARCH <o> SESSION <q> QUIT ... diff --git a/zellij-integration-tests/tests/snapshots/tabs__open_new_tab.snap b/zellij-integration-tests/tests/snapshots/tabs__open_new_tab.snap new file mode 100644 index 00000000..da0f2839 --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/tabs__open_new_tab.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/tabs.rs +expression: normalized(&zellij.snapshot()) +--- + Zellij (test) Tab #1 Tab #2 +┌ Pane #1 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│$ █ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SEARCH <o> SESSION <q> QUIT ... diff --git a/zellij-integration-tests/tests/snapshots/tabs__undo_rename_tab.snap b/zellij-integration-tests/tests/snapshots/tabs__undo_rename_tab.snap new file mode 100644 index 00000000..f1544d42 --- /dev/null +++ b/zellij-integration-tests/tests/snapshots/tabs__undo_rename_tab.snap @@ -0,0 +1,28 @@ +--- +source: zellij-integration-tests/tests/tabs.rs +expression: normalized(&grid_snapshot) +--- + Zellij (test) Tab #1 +┌ Pane #1 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│$ █ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SEARCH <o> SESSION <q> QUIT ... diff --git a/zellij-integration-tests/tests/tabs.rs b/zellij-integration-tests/tests/tabs.rs new file mode 100644 index 00000000..fedaf9f1 --- /dev/null +++ b/zellij-integration-tests/tests/tabs.rs @@ -0,0 +1,184 @@ +#![cfg(unix)] + +use insta::assert_snapshot; +use zellij_integration_tests::{ + keys, normalized, FakePtyHandle, GridSnapshot, Size, TestRunner, TestSession, +}; + +const TERMINAL_SIZE: Size = Size { + cols: 120, + rows: 24, +}; +const PROMPT: &[u8] = b"$ "; +const PROMPT_ROW: usize = 2; +const FIRST_PANE_PROMPT_X: usize = 3; + +fn start_zellij() -> TestSession { + TestRunner::new(TERMINAL_SIZE).start() +} + +fn claim_first_terminal_and_wait_for_prompt(zellij: &TestSession) -> FakePtyHandle { + let terminal = zellij.expect_pty_spawn(); + terminal.output(PROMPT); + zellij.wait_until( + "first terminal prompt rendered in loaded app", + |grid_snapshot| { + grid_snapshot.tab_bar_appears() + && grid_snapshot.status_bar_appears() + && grid_snapshot.cursor_is_at(FIRST_PANE_PROMPT_X, PROMPT_ROW) + }, + ); + terminal +} + +fn tabs_in_order(grid_snapshot: &GridSnapshot, labels: &[&str]) -> bool { + let mut search_from = 0; + for label in labels { + match grid_snapshot.text[search_from..].find(label) { + Some(offset) => search_from += offset + label.len(), + None => return false, + } + } + true +} + +fn open_new_tab_and_wait_for_prompt(zellij: &TestSession, expected_tab: &str) -> FakePtyHandle { + zellij.send_stdin(&keys::TAB_MODE); + zellij.send_stdin(&keys::NEW_TAB_IN_TAB_MODE); + let terminal = zellij.expect_pty_spawn(); + terminal.output(PROMPT); + let expected_tab = expected_tab.to_owned(); + zellij.wait_until("new tab opened with prompt", move |grid_snapshot| { + grid_snapshot.status_bar_appears() + && grid_snapshot.contains(&expected_tab) + && grid_snapshot.cursor_is_at(FIRST_PANE_PROMPT_X, PROMPT_ROW) + }); + terminal +} + +#[test] +fn open_new_tab() { + let mut zellij = start_zellij(); + claim_first_terminal_and_wait_for_prompt(&zellij); + open_new_tab_and_wait_for_prompt(&zellij, "Tab #2"); + let grid_snapshot = zellij.wait_until("second tab steady in normal mode", |grid_snapshot| { + grid_snapshot.status_bar_appears() + && grid_snapshot.contains("Tab #2") + && grid_snapshot.cursor_is_at(FIRST_PANE_PROMPT_X, PROMPT_ROW) + }); + assert_snapshot!(normalized(&grid_snapshot)); + zellij.quit(); +} + +#[test] +fn close_tab() { + let mut zellij = start_zellij(); + claim_first_terminal_and_wait_for_prompt(&zellij); + open_new_tab_and_wait_for_prompt(&zellij, "Tab #2"); + + zellij.send_stdin(&keys::TAB_MODE); + zellij.send_stdin(&keys::CLOSE_TAB_IN_TAB_MODE); + + let grid_snapshot = zellij.wait_until( + "second tab closed, only first tab remains", + |grid_snapshot| { + grid_snapshot.status_bar_appears() + && grid_snapshot.contains("Tab #1") + && !grid_snapshot.contains("Tab #2") + && grid_snapshot.cursor_is_at(FIRST_PANE_PROMPT_X, PROMPT_ROW) + }, + ); + assert_snapshot!(normalized(&grid_snapshot)); + zellij.quit(); +} + +#[test] +fn undo_rename_tab() { + let mut zellij = start_zellij(); + claim_first_terminal_and_wait_for_prompt(&zellij); + + zellij.send_stdin(&keys::TAB_MODE); + zellij.send_stdin(&keys::RENAME_TAB_MODE); + zellij.send_stdin(b"aa"); + zellij.send_stdin(&keys::ESC); + zellij.send_stdin(&keys::ESC); + + let grid_snapshot = zellij.wait_until("tab name reverted to default", |grid_snapshot| { + grid_snapshot.contains("Tab #1") && grid_snapshot.contains("LOCK") + }); + assert_snapshot!(normalized(&grid_snapshot)); + zellij.quit(); +} + +#[test] +fn move_tab_to_left() { + let mut zellij = start_zellij(); + claim_first_terminal_and_wait_for_prompt(&zellij); + open_new_tab_and_wait_for_prompt(&zellij, "Tab #2"); + open_new_tab_and_wait_for_prompt(&zellij, "Tab #3"); + + zellij.send_stdin(&keys::MOVE_TAB_LEFT); + + let grid_snapshot = zellij.wait_until("third tab moved one position left", |grid_snapshot| { + tabs_in_order(grid_snapshot, &["Tab #1", "Tab #3", "Tab #2"]) + }); + assert_snapshot!(normalized(&grid_snapshot)); + zellij.quit(); +} + +#[test] +fn move_tab_to_right() { + let mut zellij = start_zellij(); + claim_first_terminal_and_wait_for_prompt(&zellij); + open_new_tab_and_wait_for_prompt(&zellij, "Tab #2"); + open_new_tab_and_wait_for_prompt(&zellij, "Tab #3"); + + zellij.send_stdin(&keys::MOVE_FOCUS_LEFT_IN_NORMAL_MODE); + zellij.send_stdin(&keys::MOVE_TAB_RIGHT); + + let grid_snapshot = zellij.wait_until("second tab moved one position right", |grid_snapshot| { + tabs_in_order(grid_snapshot, &["Tab #1", "Tab #3", "Tab #2"]) + }); + assert_snapshot!(normalized(&grid_snapshot)); + zellij.quit(); +} + +#[test] +fn move_tab_to_left_until_it_wraps_around() { + let mut zellij = start_zellij(); + claim_first_terminal_and_wait_for_prompt(&zellij); + open_new_tab_and_wait_for_prompt(&zellij, "Tab #2"); + open_new_tab_and_wait_for_prompt(&zellij, "Tab #3"); + + zellij.send_stdin(&keys::MOVE_TAB_LEFT); + zellij.wait_until("third tab moved one position left", |grid_snapshot| { + tabs_in_order(grid_snapshot, &["Tab #1", "Tab #3", "Tab #2"]) + }); + zellij.send_stdin(&keys::MOVE_TAB_LEFT); + zellij.wait_until("third tab moved to the beginning", |grid_snapshot| { + tabs_in_order(grid_snapshot, &["Tab #3", "Tab #1", "Tab #2"]) + }); + zellij.send_stdin(&keys::MOVE_TAB_LEFT); + + let grid_snapshot = zellij.wait_until("third tab wrapped to the end", |grid_snapshot| { + tabs_in_order(grid_snapshot, &["Tab #1", "Tab #2", "Tab #3"]) + }); + assert_snapshot!(normalized(&grid_snapshot)); + zellij.quit(); +} + +#[test] +fn move_tab_to_right_until_it_wraps_around() { + let mut zellij = start_zellij(); + claim_first_terminal_and_wait_for_prompt(&zellij); + open_new_tab_and_wait_for_prompt(&zellij, "Tab #2"); + open_new_tab_and_wait_for_prompt(&zellij, "Tab #3"); + + zellij.send_stdin(&keys::MOVE_TAB_RIGHT); + + let grid_snapshot = zellij.wait_until("third tab wrapped to the beginning", |grid_snapshot| { + tabs_in_order(grid_snapshot, &["Tab #3", "Tab #1", "Tab #2"]) + }); + assert_snapshot!(normalized(&grid_snapshot)); + zellij.quit(); +} diff --git a/zellij-server/src/lib.rs b/zellij-server/src/lib.rs index 841fcce6..e9090f64 100644 --- a/zellij-server/src/lib.rs +++ b/zellij-server/src/lib.rs @@ -111,7 +111,7 @@ pub enum ServerInstruction { client_id: ClientId, }, DisconnectAllClientsExcept(ClientId), - ChangeMode(ClientId, InputMode), + ChangeMode(ClientId, InputMode, Option<NotificationEnd>), ChangeModeForAllClients(InputMode), Reconfigure { client_id: ClientId, @@ -795,7 +795,7 @@ mod session_state_tests { } } -pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) { +pub fn start_server(os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) { info!("Starting Zellij server!"); #[cfg(unix)] @@ -825,6 +825,14 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) { } } + start_server_impl(os_input, socket_path, true); +} + +pub fn start_server_impl( + mut os_input: Box<dyn ServerOsApi>, + socket_path: PathBuf, + install_panic_hook: bool, +) { envs::set_zellij("0".to_string()); let (to_server, server_receiver): ChannelWithContext<ServerInstruction> = channels::bounded(50); @@ -832,13 +840,15 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) { let session_data: Arc<RwLock<Option<SessionMetaData>>> = Arc::new(RwLock::new(None)); let session_state = Arc::new(RwLock::new(SessionState::new())); - std::panic::set_hook({ - use zellij_utils::errors::handle_panic; - let to_server = to_server.clone(); - Box::new(move |info| { - handle_panic(info, Some(&to_server)); - }) - }); + if install_panic_hook { + std::panic::set_hook({ + use zellij_utils::errors::handle_panic; + let to_server = to_server.clone(); + Box::new(move |info| { + handle_panic(info, Some(&to_server)); + }) + }); + } let _ = thread::Builder::new() .name("server_listener".to_string()) @@ -1628,14 +1638,24 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) { .unwrap() .associate_pipe_with_client(pipe_id, client_id); }, - ServerInstruction::ChangeMode(client_id, input_mode) => { + ServerInstruction::ChangeMode(client_id, input_mode, completion) => { + let mut session_data = session_data.write().unwrap(); + let session_data = session_data.as_mut().unwrap(); + let base_mode = session_data + .session_configuration + .get_client_default_input_mode(&client_id); session_data - .write() - .unwrap() - .as_mut() - .unwrap() .current_input_modes .insert(client_id, input_mode); + session_data + .senders + .send_to_screen(ScreenInstruction::ChangeMode( + input_mode, + Some(base_mode), + client_id, + completion, + )) + .unwrap(); }, ServerInstruction::ChangeModeForAllClients(input_mode) => { session_data diff --git a/zellij-server/src/plugins/unit/plugin_tests.rs b/zellij-server/src/plugins/unit/plugin_tests.rs index 0e3b0637..35d80ecf 100644 --- a/zellij-server/src/plugins/unit/plugin_tests.rs +++ b/zellij-server/src/plugins/unit/plugin_tests.rs @@ -1121,8 +1121,8 @@ pub fn switch_to_mode_plugin_command() { // destructor removes the directory let plugin_host_folder = PathBuf::from(temp_folder.path()); let cache_path = plugin_host_folder.join("permissions_test.kdl"); - let (plugin_thread_sender, screen_receiver, teardown) = - create_plugin_thread(Some(plugin_host_folder), None); + let (plugin_thread_sender, server_receiver, screen_receiver, teardown) = + create_plugin_thread_with_server_receiver(Some(plugin_host_folder), None); let plugin_should_float = Some(false); let plugin_title = Some("test_plugin".to_owned()); let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin { @@ -1137,10 +1137,17 @@ pub fn switch_to_mode_plugin_command() { cols: 121, rows: 20, }; + let received_server_instructions = Arc::new(Mutex::new(vec![])); + let server_thread = log_actions_in_thread!( + received_server_instructions, + ServerInstruction::ChangeMode, + server_receiver, + 1 + ); let received_screen_instructions = Arc::new(Mutex::new(vec![])); - let screen_thread = grant_permissions_and_log_actions_in_thread!( + let _screen_thread = grant_permissions_and_log_actions_in_thread_naked_variant!( received_screen_instructions, - ScreenInstruction::ChangeMode, + ScreenInstruction::Exit, screen_receiver, 1, &PermissionType::ChangeApplicationState, @@ -1174,14 +1181,14 @@ pub fn switch_to_mode_plugin_command() { Event::Key(KeyWithModifier::new(BareKey::Char('a'))), // this triggers a SwitchToMode(Tab) command in the fixture // plugin )])); - screen_thread.join().unwrap(); // this might take a while if the cache is cold + server_thread.join().unwrap(); // this might take a while if the cache is cold teardown(); - let switch_to_mode_event = received_screen_instructions + let switch_to_mode_event = received_server_instructions .lock() .unwrap() .iter() .find_map(|i| { - if let ScreenInstruction::ChangeMode(..) = i { + if let ServerInstruction::ChangeMode(..) = i { Some(i.clone()) } else { None diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__switch_to_mode_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__switch_to_mode_plugin_command.snap index feaaebd7..f8e48b96 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__switch_to_mode_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__switch_to_mode_plugin_command.snap @@ -4,11 +4,8 @@ expression: "format!(\"{:#?}\", switch_to_mode_event)" --- Some( ChangeMode( - Tab, - Some( - Normal, - ), 1, + Tab, Some( NotificationEnd { channel: None, diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index 4a3ffcc9..087a048e 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -322,19 +322,12 @@ pub(crate) fn route_action( }, Action::SwitchToMode { input_mode } => { senders - .send_to_server(ServerInstruction::ChangeMode(client_id, input_mode)) - .with_context(err_context)?; - senders - .send_to_screen(ScreenInstruction::ChangeMode( - input_mode, - Some(default_mode), + .send_to_server(ServerInstruction::ChangeMode( client_id, + input_mode, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; - senders - .send_to_screen(ScreenInstruction::Render) - .with_context(err_context)?; }, Action::Resize { resize, direction } => { let screen_instr = ScreenInstruction::Resize( diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index 95c47f5b..e287cae9 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -4036,13 +4036,11 @@ impl Screen { match self.get_active_tab(client_id) { Ok(active_tab) => { let active_tab_pos = active_tab.position; - let left_tab_pos = if active_tab_pos == 0 { - self.tabs.len() - 1 + if active_tab_pos == 0 { + self.rotate_tab_positions_left(); } else { - active_tab_pos - 1 - }; - - self.switch_tabs(active_tab_pos, left_tab_pos); + self.switch_tabs(active_tab_pos, active_tab_pos.saturating_sub(1)); + } self.log_and_report_session_state() .context("failed to move tab to left")?; }, @@ -4115,6 +4113,24 @@ impl Screen { self.tabs.insert(other_tab_id, other_tab); } + fn rotate_tab_positions_left(&mut self) { + let last_position = self.tabs.len().saturating_sub(1); + for tab in self.tabs.values_mut() { + tab.position = if tab.position == 0 { + last_position + } else { + tab.position.saturating_sub(1) + }; + } + } + + fn rotate_tab_positions_right(&mut self) { + let num_tabs = self.tabs.len(); + for tab in self.tabs.values_mut() { + tab.position = (tab.position + 1) % num_tabs; + } + } + pub fn move_active_tab_to_right(&mut self, client_id: ClientId) -> Result<()> { let err_context = || "Failed to move active tab right "; if self.tabs.len() < 2 { @@ -4128,9 +4144,11 @@ impl Screen { match self.get_active_tab(client_id) { Ok(active_tab) => { let active_tab_pos = active_tab.position; - let right_tab_pos = (active_tab_pos + 1) % self.tabs.len(); - - self.switch_tabs(active_tab_pos, right_tab_pos); + if active_tab_pos == self.tabs.len().saturating_sub(1) { + self.rotate_tab_positions_right(); + } else { + self.switch_tabs(active_tab_pos, active_tab_pos + 1); + } self.log_and_report_session_state() .context("failed to move tab to the right")?; }, @@ -4145,18 +4163,24 @@ impl Screen { } if let Some(tab) = self.tabs.get(&tab_id) { let tab_pos = tab.position; - let swap_pos = match direction { + let num_tabs = self.tabs.len(); + match direction { Direction::Left => { if tab_pos == 0 { - self.tabs.len() - 1 + self.rotate_tab_positions_left(); } else { - tab_pos - 1 + self.switch_tabs(tab_pos, tab_pos.saturating_sub(1)); + } + }, + Direction::Right => { + if tab_pos == num_tabs.saturating_sub(1) { + self.rotate_tab_positions_right(); + } else { + self.switch_tabs(tab_pos, tab_pos + 1); } }, - Direction::Right => (tab_pos + 1) % self.tabs.len(), _ => return Ok(()), - }; - self.switch_tabs(tab_pos, swap_pos); + } self.log_and_report_session_state()?; } else { log::error!("Tab with id {} not found", tab_id); diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs index 3e0a65db..1d33e8fd 100644 --- a/zellij-server/src/unit/screen_tests.rs +++ b/zellij-server/src/unit/screen_tests.rs @@ -1144,8 +1144,8 @@ fn tab_id_remains_stable_after_switch() { assert_eq!(screen.tabs.get(&2).unwrap().id, 2); assert_eq!(screen.tabs.get(&2).unwrap().position, 2); - // Move active tab (position 2, ID 2) to right, which wraps to position 0 - // This switches tabs at positions 2 and 0 (tab IDs 2 and 0) + // Move active tab (position 2, ID 2) to right, which wraps to position 0 and rotates the + // others right by one (IDs stay stable, only positions change) screen.move_active_tab_to_right(1).expect("TEST"); // Verify BTreeMap keys (IDs) remain unchanged @@ -1156,8 +1156,8 @@ fn tab_id_remains_stable_after_switch() { "Tab IDs in BTreeMap should remain 0, 1, 2 after switch" ); - // Verify IDs remain stable but positions are swapped - // Tab 0: was at position 0, now at position 2 (swapped with tab 2) + // Verify IDs remain stable but positions rotate + // Tab 0: was at position 0, rotated to position 1 assert_eq!( screen.tabs.get(&0).unwrap().id, 0, @@ -1165,11 +1165,11 @@ fn tab_id_remains_stable_after_switch() { ); assert_eq!( screen.tabs.get(&0).unwrap().position, - 2, - "Tab with ID 0 should now be at position 2" + 1, + "Tab with ID 0 should now be at position 1" ); - // Tab 1: remains unchanged at position 1 + // Tab 1: was at position 1, rotated to position 2 assert_eq!( screen.tabs.get(&1).unwrap().id, 1, @@ -1177,11 +1177,11 @@ fn tab_id_remains_stable_after_switch() { ); assert_eq!( screen.tabs.get(&1).unwrap().position, - 1, - "Tab with ID 1 should remain at position 1" + 2, + "Tab with ID 1 should now be at position 2" ); - // Tab 2: was at position 2, now at position 0 (swapped with tab 0) + // Tab 2: was at position 2, rotated over the edge to position 0 assert_eq!( screen.tabs.get(&2).unwrap().id, 2, @@ -1193,15 +1193,15 @@ fn tab_id_remains_stable_after_switch() { "Tab with ID 2 should now be at position 0" ); - // Verify that lookup by position works correctly after switch + // Verify that lookup by position works correctly after the rotation let tab_at_pos_0 = screen.tabs.values().find(|t| t.position == 0).unwrap(); assert_eq!(tab_at_pos_0.id, 2, "Tab at position 0 should have ID 2"); let tab_at_pos_1 = screen.tabs.values().find(|t| t.position == 1).unwrap(); - assert_eq!(tab_at_pos_1.id, 1, "Tab at position 1 should have ID 1"); + assert_eq!(tab_at_pos_1.id, 0, "Tab at position 1 should have ID 0"); let tab_at_pos_2 = screen.tabs.values().find(|t| t.position == 2).unwrap(); - assert_eq!(tab_at_pos_2.id, 0, "Tab at position 2 should have ID 0"); + assert_eq!(tab_at_pos_2.id, 1, "Tab at position 2 should have ID 1"); } #[test] diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_switch_mode_action.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_switch_mode_action.snap index 2ef0d483..208f2094 100644 --- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_switch_mode_action.snap +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_switch_mode_action.snap @@ -2,4 +2,4 @@ source: zellij-server/src/./unit/screen_tests.rs expression: "format!(\"{:?}\", switch_mode_action)" --- -Some(ChangeMode(10, Locked)) +Some(ChangeMode(10, Locked, Some(NotificationEnd { channel: None, exit_status: None, unblock_condition: None, affected_pane_id: None, affected_tab_id: None, error_message: None, stdout_message: None }))) |
