aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2026-06-16 15:44:22 +0200
committerGitHub <noreply@github.com>2026-06-16 15:44:22 +0200
commit8c7e41c9aaafc44520380a2c86db517fde54ad31 (patch)
treed54bb40aef913c07f2142bd835dfa47d09fae5af
parentMobile layout (#5241) (diff)
downloadsl0ppij-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 '')
-rw-r--r--.config/nextest.toml3
-rw-r--r--.github/workflows/rust.yml31
-rw-r--r--CHANGELOG.md1
-rw-r--r--Cargo.lock18
-rw-r--r--Cargo.toml1
-rw-r--r--src/commands.rs8
-rw-r--r--src/tests/e2e/cases.rs2531
-rw-r--r--src/tests/e2e/mod.rs1
-rw-r--r--src/tests/e2e/remote_runner.rs503
-rw-r--r--src/tests/e2e/steps.rs152
-rw-r--r--xtask/src/flags.rs20
-rw-r--r--xtask/src/integration_test.rs47
-rw-r--r--xtask/src/main.rs2
-rw-r--r--zellij-client/src/cli_client.rs23
-rw-r--r--zellij-client/src/lib.rs34
-rw-r--r--zellij-client/src/os_input_output.rs6
-rw-r--r--zellij-integration-tests/Cargo.toml23
-rwxr-xr-xzellij-integration-tests/preview_snapshots.sh50
-rw-r--r--zellij-integration-tests/src/client_screen.rs232
-rw-r--r--zellij-integration-tests/src/fake_client_os_api.rs182
-rw-r--r--zellij-integration-tests/src/fake_pty.rs365
-rw-r--r--zellij-integration-tests/src/fake_server_os_api.rs201
-rw-r--r--zellij-integration-tests/src/keys.rs63
-rw-r--r--zellij-integration-tests/src/lib.rs25
-rw-r--r--zellij-integration-tests/src/runner.rs567
-rw-r--r--zellij-integration-tests/src/test_env.rs202
-rw-r--r--zellij-integration-tests/tests/app.rs157
-rw-r--r--zellij-integration-tests/tests/basic.rs128
-rw-r--r--zellij-integration-tests/tests/cli.rs164
-rw-r--r--zellij-integration-tests/tests/clients.rs217
-rw-r--r--zellij-integration-tests/tests/modes.rs137
-rw-r--r--zellij-integration-tests/tests/mouse.rs132
-rw-r--r--zellij-integration-tests/tests/panes.rs228
-rw-r--r--zellij-integration-tests/tests/plugins.rs41
-rw-r--r--zellij-integration-tests/tests/resurrection.rs108
-rw-r--r--zellij-integration-tests/tests/snapshots/app__edit_scrollback.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/app__resize_terminal_window.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/app__status_bar_loads_custom_keybindings.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/app__use_custom_layout_with_relative_path.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/basic__resize_pane.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/basic__split_terminals_vertically.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/basic__starts_with_one_terminal.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/cli__override_layout_from_default_to_compact.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/cli__send_blocking_command_through_the_cli.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/cli__send_command_through_the_cli.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/cli__watcher_client_functionality-2.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/cli__watcher_client_functionality.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/clients__detach_and_attach_session.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/clients__mirrored_sessions.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/clients__multiple_users_in_different_panes_and_same_tab-2.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/clients__multiple_users_in_different_panes_and_same_tab.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/clients__multiple_users_in_different_tabs-2.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/clients__multiple_users_in_different_tabs.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/clients__multiple_users_in_same_pane_and_tab-2.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/clients__multiple_users_in_same_pane_and_tab.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/modes__bracketed_paste.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/modes__lock_mode.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/modes__scrolling_inside_a_pane.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/modes__tmux_mode.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/mouse__focus_pane_with_mouse.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/mouse__pin_floating_panes.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/mouse__scrolling_inside_a_pane_with_mouse.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/panes__cannot_split_terminals_vertically_when_active_terminal_is_too_small.snap24
-rw-r--r--zellij-integration-tests/tests/snapshots/panes__close_pane.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/panes__pane_closes_when_its_process_exits.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/panes__start_without_pane_frames.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/panes__toggle_floating_panes.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/panes__toggle_pane_fullscreen.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/panes__undo_rename_pane.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/plugins__load_plugins_in_background_on_startup.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/resurrection__quit_and_resurrect_session.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/resurrection__quit_and_resurrect_session_with_viewport_serialization.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/tabs__close_tab.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/tabs__move_tab_to_left.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/tabs__move_tab_to_left_until_it_wraps_around.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/tabs__move_tab_to_right.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/tabs__move_tab_to_right_until_it_wraps_around.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/tabs__open_new_tab.snap28
-rw-r--r--zellij-integration-tests/tests/snapshots/tabs__undo_rename_tab.snap28
-rw-r--r--zellij-integration-tests/tests/tabs.rs184
-rw-r--r--zellij-server/src/lib.rs48
-rw-r--r--zellij-server/src/plugins/unit/plugin_tests.rs21
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__switch_to_mode_plugin_command.snap5
-rw-r--r--zellij-server/src/route.rs11
-rw-r--r--zellij-server/src/screen.rs54
-rw-r--r--zellij-server/src/unit/screen_tests.rs26
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_switch_mode_action.snap2
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)
diff --git a/Cargo.lock b/Cargo.lock
index 9f86bf89..b887fa6d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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 = [
diff --git a/Cargo.toml b/Cargo.toml
index 1a45e4cf..2c2100de 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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 })))