mirror of
https://github.com/zellij-org/zellij.git
synced 2026-10-11 22:50:07 +00:00
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
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
[profile.default]
|
||||
slow-timeout = { period = "30s", terminate-after = 2 }
|
||||
fail-fast = false
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Generated
+18
@@ -4870,6 +4870,24 @@ dependencies = [
|
||||
"zellij-utils",
|
||||
]
|
||||
|
||||
[[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"
|
||||
|
||||
@@ -60,6 +60,7 @@ members = [
|
||||
"default-plugins/link",
|
||||
"default-plugins/mobile",
|
||||
"zellij-client",
|
||||
"zellij-integration-tests",
|
||||
"zellij-server",
|
||||
"zellij-utils",
|
||||
"zellij-tile",
|
||||
|
||||
+6
-2
@@ -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);
|
||||
|
||||
+7
-2524
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,2 @@
|
||||
pub mod cases;
|
||||
mod remote_runner;
|
||||
mod steps;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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")
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -232,6 +244,14 @@ pub struct Test {
|
||||
pub no_web: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct IntegrationTest {
|
||||
pub args: Vec<OsString>,
|
||||
|
||||
pub no_opt: bool,
|
||||
pub serial: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Build {
|
||||
pub release: bool,
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
},
|
||||
_ => {},
|
||||
|
||||
+18
-16
@@ -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);
|
||||
|
||||
@@ -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<()>;
|
||||
|
||||
@@ -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 }
|
||||
+50
@@ -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
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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~";
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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 ...
|
||||
@@ -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 ...
|
||||
+28
@@ -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
|
||||
+28
@@ -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
|
||||
@@ -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 ...
|
||||
@@ -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 ...
|
||||
@@ -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 ...
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: zellij-integration-tests/tests/cli.rs
|
||||
expression: normalized(&grid_snapshot)
|
||||
---
|
||||
┌ Pane #1 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│$ █ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
Zellij (test) NORMAL Tab #1
|
||||
+28
@@ -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 ...
|
||||
@@ -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 ...
|
||||
@@ -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 ...
|
||||
@@ -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 ...
|
||||
@@ -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 ...
|
||||
@@ -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 ...
|
||||
+28
@@ -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 ...
|
||||
+28
@@ -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 ...
|
||||
+28
@@ -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 ...
|
||||
+28
@@ -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 ...
|
||||
+28
@@ -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 ...
|
||||
+28
@@ -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 ...
|
||||
@@ -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 ...
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 ...
|
||||
@@ -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 ...
|
||||
@@ -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 ...
|
||||
+28
@@ -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 ...
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
---
|
||||
source: zellij-integration-tests/tests/panes.rs
|
||||
expression: normalized(&grid_snapshot)
|
||||
---
|
||||
|
||||
┌──────┐
|
||||
│$ done│
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────┘
|
||||
Ctrl +
|
||||
@@ -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 ...
|
||||
+28
@@ -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 ...
|
||||
@@ -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 ...
|
||||
@@ -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 ...
|
||||
@@ -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 ...
|
||||
@@ -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 ...
|
||||
+28
@@ -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) │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
+28
@@ -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 ...
|
||||
+28
@@ -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 ...
|
||||
@@ -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 ...
|
||||
@@ -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 ...
|
||||
+28
@@ -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 ...
|
||||
@@ -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 ...
|
||||
+28
@@ -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 ...
|
||||
@@ -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 ...
|
||||
@@ -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 ...
|
||||
@@ -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();
|
||||
}
|
||||
+34
-14
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-4
@@ -4,11 +4,8 @@ expression: "format!(\"{:#?}\", switch_to_mode_event)"
|
||||
---
|
||||
Some(
|
||||
ChangeMode(
|
||||
Tab,
|
||||
Some(
|
||||
Normal,
|
||||
),
|
||||
1,
|
||||
Tab,
|
||||
Some(
|
||||
NotificationEnd {
|
||||
channel: None,
|
||||
|
||||
@@ -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(
|
||||
|
||||
+39
-15
@@ -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);
|
||||
|
||||
@@ -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]
|
||||
|
||||
+1
-1
@@ -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 })))
|
||||
|
||||
Reference in New Issue
Block a user