fix(e2e): stabilize tests and support Apple Silicon local dev (#4952)

- cross-compile musl binary on macOS via cargo-zigbuild, with
arch-independent symlink so snapshots are stable across x86_64 and aarch64

- kill_running_sessions() now calls stop_zellij() instead of
start_zellij(), eliminating 3s startup per test

- prevent steps from firing prematurely by tightening completion conditions
and adding re-send logic for garbled keys
This commit is contained in:
Thomas Linford
2026-04-01 12:41:07 +02:00
committed by GitHub
parent 7c68623580
commit 6542afabd8
11 changed files with 192 additions and 86 deletions
+1
View File
@@ -18,6 +18,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
* fix: proper error on socket paths that are too long (https://github.com/zellij-org/zellij/pull/4957)
* fix: scrollback with partial scroll region (eg. Codex bug) (https://github.com/zellij-org/zellij/pull/4941)
* fix: remote attach issues on macOS (https://github.com/zellij-org/zellij/pull/4972)
* test: improve e2e test reliability and local macOS dev support (https://github.com/zellij-org/zellij/pull/4952)
## [0.44.0] - 2026-03-23
* fix: meta key handling in web client (https://github.com/zellij-org/zellij/pull/4376)
+8 -9
View File
@@ -59,21 +59,20 @@ Zellij includes some end-to-end tests which test the whole application as a blac
These tests work by running a docker container which contains the Zellij binary, connecting to it via ssh, sending some commands and comparing the output received against predefined snapshots.
<details>
<summary>Should you be a macOS (including m1) user, please follow these commands before. (expand here):</summary>
<summary>macOS prerequisites (expand here):</summary>
1. `rustup target add x86_64-unknown-linux-musl`
2. `brew install messense/macos-cross-toolchains/x86_64-unknown-linux-musl`
3. `export CC_x86_64_unknown_linux_musl=$(brew --prefix)/bin/x86_64-unknown-linux-musl-gcc`
4. `export AR_x86_64_unknown_linux_musl=$(brew --prefix)/bin/x86_64-unknown-linux-musl-ar`
5. `export CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER=$CC_x86_64_unknown_linux_musl`
The build uses `cargo-zigbuild` to cross-compile a native musl binary (arm64 on Apple Silicon, amd64 on Intel) so the Docker container runs without emulation on any Mac.
1. `cargo install cargo-zigbuild`
2. `brew install zig`
</details>
To run these tests locally, you'll need to have either `docker` or `podman` and also `docker-compose` installed.
To run these tests locally, you'll need to have either `docker` or `podman` and also `docker compose` installed.
Once you do, in the repository root:
1. `docker-compose up -d` will start up the docker container
2. `cargo xtask ci e2e --build` will build the generic linux executable of Zellij in the target folder, which is shared with the container
1. `docker compose up -d` will start up the docker container
2. `cargo xtask ci e2e --build` will build the Zellij binary in the target folder, which is shared with the container
3. `cargo xtask ci e2e --test` will run the tests
To re-run the tests after you've changed something in the code base, be sure to repeat steps 2 and 3.
+49 -31
View File
@@ -296,6 +296,7 @@ pub fn scrolling_inside_a_pane() {
let mut step_is_complete = false;
if remote_terminal.cursor_position_is(63, 21)
&& remote_terminal.snapshot_contains("line21")
&& remote_terminal.status_bar_appears()
{
// all lines have been written to the pane
remote_terminal.send_key(&SCROLL_MODE);
@@ -314,8 +315,10 @@ pub fn scrolling_inside_a_pane() {
if remote_terminal.cursor_position_is(63, 21)
&& remote_terminal.snapshot_contains("line3 ")
&& remote_terminal.snapshot_contains("SCROLL: 1/3")
&& remote_terminal.snapshot_contains("PgDn|PgUp")
{
// keyboard scrolls up 1 line, scrollback is 4 lines: cat command + 2 extra lines from fixture + prompt
// PgDn|PgUp only appears in the scroll mode status bar, confirming we're still in scroll mode
step_is_complete = true;
}
step_is_complete
@@ -1135,8 +1138,11 @@ pub fn detach_and_attach_session() {
name: "Reattach session",
instruction: |mut remote_terminal: RemoteTerminal| -> bool {
let mut step_is_complete = false;
if !remote_terminal.status_bar_appears() {
// we don't see the toolbar, so we can assume we've already detached
if !remote_terminal.status_bar_appears()
&& remote_terminal.snapshot_contains("Bye from Zellij!")
{
// we don't see the toolbar and Zellij's exit message is visible,
// so Zellij has fully exited and the server is ready to accept connections
remote_terminal.attach_to_original_session();
step_is_complete = true;
}
@@ -1209,11 +1215,9 @@ pub fn quit_and_resurrect_session() {
let last_snapshot = runner.take_snapshot_after(Step {
name: "Wait for session to be resurrected",
instruction: |remote_terminal: RemoteTerminal| -> bool {
let mut step_is_complete = false;
if remote_terminal.snapshot_contains("(FLOATING PANES VISIBLE)") {
step_is_complete = true;
}
step_is_complete
remote_terminal.snapshot_contains("(FLOATING PANES VISIBLE)")
&& remote_terminal.status_bar_appears()
&& remote_terminal.tab_bar_appears()
},
});
if runner.test_timed_out && test_attempts > 0 {
@@ -1270,11 +1274,9 @@ pub fn quit_and_resurrect_session_with_viewport_serialization() {
let last_snapshot = runner.take_snapshot_after(Step {
name: "Wait for session to be resurrected",
instruction: |remote_terminal: RemoteTerminal| -> bool {
let mut step_is_complete = false;
if remote_terminal.snapshot_contains("(FLOATING PANES VISIBLE)") {
step_is_complete = true;
}
step_is_complete
remote_terminal.snapshot_contains("(FLOATING PANES VISIBLE)")
&& remote_terminal.status_bar_appears()
&& remote_terminal.tab_bar_appears()
},
});
if runner.test_timed_out && test_attempts > 0 {
@@ -2422,14 +2424,9 @@ pub fn send_blocking_command_through_the_cli() {
if remote_terminal.status_bar_appears()
&& remote_terminal.cursor_position_is(3, 2)
{
// Send a command that sleeps for 2 seconds then exits with status 42
// Using bash -c with proper escaping
// remote_terminal.send_key("aaa\n".as_bytes());
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_blocking_command_through_the_cli(
// "lskdjlskdjf"
"bash -c 'sleep 2 && exit 42'",
);
remote_terminal
.send_blocking_command_through_the_cli("bash -c 'sleep 2 && exit 42'");
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&ENTER);
std::thread::sleep(std::time::Duration::from_millis(100));
@@ -2457,8 +2454,13 @@ pub fn send_blocking_command_through_the_cli() {
instruction: |mut remote_terminal: RemoteTerminal| -> bool {
let mut step_is_complete = false;
// After 2+ seconds, the command should complete and the floating pane should close
// then we do echo $? to make sure we got back its exit status
if !remote_terminal.snapshot_contains("PIN [ ]") {
// Wait until the floating pane is gone AND the shell prompt is back before
// asking for $?, otherwise we can race the blocking CLI process itself
// returning to the shell.
if !remote_terminal.snapshot_contains("PIN [ ]")
&& remote_terminal.snapshot_contains("$ \u{2588}")
&& remote_terminal.status_bar_appears()
{
remote_terminal.send_key("echo $?".as_bytes());
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&ENTER);
@@ -2474,7 +2476,11 @@ pub fn send_blocking_command_through_the_cli() {
name: "Verify CLI returned with proper exit status after command completed",
instruction: |remote_terminal: RemoteTerminal| -> bool {
let mut step_is_complete = false;
// wait until echo $? is visible, the exit status rendered, and the cursor is back
// at a blank prompt, which means the shell command actually executed
if remote_terminal.snapshot_contains("echo $?")
&& remote_terminal.snapshot_contains("│42")
&& remote_terminal.snapshot_contains("$ \u{2588}")
&& remote_terminal.status_bar_appears()
{
step_is_complete = true
@@ -2664,6 +2670,7 @@ pub fn watcher_client_functionality() {
// Step 1: Create main client and wait for it to load
let mut main_client =
RemoteRunner::new_with_session_name(fake_win_size, session_name, false)
.retry_pause_ms(200)
.dont_panic()
.add_step(Step {
name: "Wait for app to load",
@@ -2674,12 +2681,16 @@ pub fn watcher_client_functionality() {
});
main_client.run_all_steps();
// Step 2: Start a background process that produces periodic output
// Step 2: Start a foreground process that produces periodic output and then
// replaces the shell with a long sleep. Using exec means pane 1 never returns
// to an interactive shell prompt, so a SIGWINCH on re-attach cannot trigger a
// prompt redraw — the snapshot is deterministic.
main_client = main_client.add_step(Step {
name: "Start background output loop",
name: "Start foreground output loop",
instruction: |mut remote_terminal: RemoteTerminal| -> bool {
remote_terminal.send_key(b"{ for i in 1 2 3; do sleep 1; echo \"WATCHER_OUTPUT_$i\"; done & } 2>/dev/null");
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(
b"for i in 1 2 3; do sleep 1; echo WATCHER_OUTPUT_$i; done; echo WATCHER_DONE; exec sleep 1000000",
);
remote_terminal.send_key(&ENTER);
true
},
@@ -2688,7 +2699,7 @@ pub fn watcher_client_functionality() {
// Step 3: Wait for first output line to appear
main_client = main_client.add_step(Step {
name: "Wait for first background output",
name: "Wait for first output",
instruction: |remote_terminal: RemoteTerminal| -> bool {
let found = remote_terminal.snapshot_contains("WATCHER_OUTPUT_1");
found
@@ -2698,6 +2709,7 @@ pub fn watcher_client_functionality() {
// Step 4: Attach watcher client
let mut watcher = RemoteRunner::new_watcher_session(fake_win_size, session_name)
.retry_pause_ms(200)
.dont_panic()
.add_step(Step {
name: "Wait for watcher to connect",
@@ -2712,8 +2724,7 @@ pub fn watcher_client_functionality() {
main_client = main_client.add_step(Step {
name: "Split pane to the right",
instruction: |mut remote_terminal: RemoteTerminal| -> bool {
if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(1, 7)
{
if remote_terminal.status_bar_appears() {
remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE);
@@ -2730,7 +2741,7 @@ pub fn watcher_client_functionality() {
watcher = watcher.add_step(Step {
name: "Watcher sees split pane",
instruction: |remote_terminal: RemoteTerminal| -> bool {
remote_terminal.cursor_position_is(63, 2) && remote_terminal.snapshot_contains("┐┌")
remote_terminal.status_bar_appears() && remote_terminal.snapshot_contains("┐┌")
// vertical split indicator
},
});
@@ -2789,7 +2800,7 @@ pub fn watcher_client_functionality() {
});
main_client.run_all_steps();
// Step 11: Verify watcher receives background output even with no main client
// Step 11: Verify watcher receives output even with no main client
watcher = watcher.add_step(Step {
name: "Watcher sees WATCHER_OUTPUT_2",
instruction: |remote_terminal: RemoteTerminal| -> bool {
@@ -2824,11 +2835,14 @@ pub fn watcher_client_functionality() {
watcher.run_all_steps();
// Step 15: Take final snapshots
// Take final snapshots — WATCHER_DONE means the loop finished and pane 1 is now
// running `sleep 1000000` (via exec), so no interactive shell and no prompt redraws.
let main_client_snapshot = main_client_reattached.take_snapshot_after(Step {
name: "Take main client final snapshot",
instruction: |remote_terminal: RemoteTerminal| -> bool {
remote_terminal.status_bar_appears()
&& remote_terminal.snapshot_contains("WATCHER_DONE")
&& remote_terminal.snapshot_contains("┐┌")
},
});
@@ -2836,6 +2850,8 @@ pub fn watcher_client_functionality() {
name: "Take watcher final snapshot",
instruction: |remote_terminal: RemoteTerminal| -> bool {
remote_terminal.status_bar_appears()
&& remote_terminal.snapshot_contains("WATCHER_DONE")
&& remote_terminal.snapshot_contains("┐┌")
},
});
@@ -2885,7 +2901,9 @@ pub fn override_layout_from_default_to_compact() {
let last_snapshot = runner.take_snapshot_after(Step {
name: "Wait for compact layout to appear",
instruction: |remote_terminal: RemoteTerminal| -> bool {
// wait for both the layout indicator and the session name to render
remote_terminal.snapshot_contains("NORMAL")
&& remote_terminal.snapshot_contains("e2e-test")
},
});
+69 -19
View File
@@ -17,7 +17,7 @@ use std::path::Path;
use std::cell::RefCell;
use std::rc::Rc;
const ZELLIJ_EXECUTABLE_LOCATION: &str = "/usr/src/zellij/x86_64-unknown-linux-musl/release/zellij";
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";
@@ -28,6 +28,7 @@ const CONNECTION_USERNAME: &str = "test";
const CONNECTION_PASSWORD: &str = "test";
const SESSION_NAME: &str = "e2e-test";
const RETRIES: usize = 10;
const CLEANUP_DONE_MARKER: &str = "__ZELLIJ_CLEANUP_DONE__";
fn ssh_connect() -> ssh2::Session {
let tcp = TcpStream::connect(CONNECTION_STRING).unwrap();
@@ -75,6 +76,13 @@ fn stop_zellij(channel: &mut ssh2::Channel) {
channel
.write_all(b"rm -rf ~/.cache/zellij/permissions.kdl\n")
.unwrap();
// create an arch-independent symlink so the binary path in snapshots is stable across
// x86_64 (CI) and aarch64 (Apple Silicon local dev)
channel
.write_all(
b"ln -sf /usr/src/zellij/$(uname -m)-unknown-linux-musl/release/zellij /usr/src/zellij/zellij\n",
)
.unwrap();
}
fn start_zellij(channel: &mut ssh2::Channel) {
@@ -89,7 +97,6 @@ fn start_zellij(channel: &mut ssh2::Channel) {
)
.unwrap();
channel.flush().unwrap();
std::thread::sleep(std::time::Duration::from_secs(3)); // wait until Zellij stops parsing startup ANSI codes from the terminal STDIN
}
fn start_zellij_with_config_dir(channel: &mut ssh2::Channel, config_dir: &str) {
@@ -104,7 +111,6 @@ fn start_zellij_with_config_dir(channel: &mut ssh2::Channel, config_dir: &str) {
)
.unwrap();
channel.flush().unwrap();
std::thread::sleep(std::time::Duration::from_secs(3)); // wait until Zellij stops parsing startup ANSI codes from the terminal STDIN
}
fn start_zellij_mirrored_session(channel: &mut ssh2::Channel) {
@@ -119,7 +125,6 @@ fn start_zellij_mirrored_session(channel: &mut ssh2::Channel) {
)
.unwrap();
channel.flush().unwrap();
std::thread::sleep(std::time::Duration::from_secs(3)); // wait until Zellij stops parsing startup ANSI codes from the terminal STDIN
}
fn start_zellij_mirrored_session_with_layout(channel: &mut ssh2::Channel, layout_file_name: &str) {
@@ -138,7 +143,6 @@ fn start_zellij_mirrored_session_with_layout(channel: &mut ssh2::Channel, layout
)
.unwrap();
channel.flush().unwrap();
std::thread::sleep(std::time::Duration::from_secs(3)); // wait until Zellij stops parsing startup ANSI codes from the terminal STDIN
}
fn start_zellij_mirrored_session_with_layout_and_viewport_serialization(
@@ -160,7 +164,6 @@ fn start_zellij_mirrored_session_with_layout_and_viewport_serialization(
)
.unwrap();
channel.flush().unwrap();
std::thread::sleep(std::time::Duration::from_secs(3)); // wait until Zellij stops parsing startup ANSI codes from the terminal STDIN
}
fn start_zellij_in_session(channel: &mut ssh2::Channel, session_name: &str, mirrored: bool) {
@@ -179,7 +182,6 @@ fn start_zellij_in_session(channel: &mut ssh2::Channel, session_name: &str, mirr
)
.unwrap();
channel.flush().unwrap();
std::thread::sleep(std::time::Duration::from_secs(3)); // wait until Zellij stops parsing startup ANSI codes from the terminal STDIN
}
fn attach_to_existing_session(channel: &mut ssh2::Channel, session_name: &str) {
@@ -193,7 +195,6 @@ fn attach_to_existing_session(channel: &mut ssh2::Channel, session_name: &str) {
)
.unwrap();
channel.flush().unwrap();
std::thread::sleep(std::time::Duration::from_secs(3)); // wait until Zellij stops parsing startup ANSI codes from the terminal STDIN
}
fn watch_existing_session(channel: &mut ssh2::Channel, session_name: &str) {
@@ -207,7 +208,6 @@ fn watch_existing_session(channel: &mut ssh2::Channel, session_name: &str) {
)
.unwrap();
channel.flush().unwrap();
std::thread::sleep(std::time::Duration::from_secs(3)); // wait until Zellij stops parsing startup ANSI codes from the terminal STDIN
}
fn start_zellij_without_frames(channel: &mut ssh2::Channel) {
@@ -222,7 +222,6 @@ fn start_zellij_without_frames(channel: &mut ssh2::Channel) {
)
.unwrap();
channel.flush().unwrap();
std::thread::sleep(std::time::Duration::from_secs(3)); // wait until Zellij stops parsing startup ANSI codes from the terminal STDIN
}
fn start_zellij_with_config(channel: &mut ssh2::Channel, config_path: &str) {
@@ -241,7 +240,40 @@ fn start_zellij_with_config(channel: &mut ssh2::Channel, config_path: &str) {
)
.unwrap();
channel.flush().unwrap();
std::thread::sleep(std::time::Duration::from_secs(3)); // wait until Zellij stops parsing startup ANSI codes from the terminal STDIN
}
fn wait_for_startup(last_snapshot: &Arc<Mutex<String>>) {
let start = std::time::Instant::now();
let timeout = std::time::Duration::from_secs(10);
loop {
if last_snapshot.lock().unwrap().contains("Ctrl +") {
break;
}
if start.elapsed() > timeout {
break; // timed out — let the test proceed and fail naturally
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
}
fn wait_for_shell_output(channel: &mut ssh2::Channel, needle: &str) {
let start = std::time::Instant::now();
let timeout = std::time::Duration::from_secs(10);
let mut output = String::new();
let mut buf = [0u8; 4096];
loop {
if output.contains(needle) || start.elapsed() > timeout {
break;
}
match channel.read(&mut buf) {
Ok(0) => std::thread::sleep(std::time::Duration::from_millis(50)),
Ok(count) => output.push_str(&String::from_utf8_lossy(&buf[..count])),
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
std::thread::sleep(std::time::Duration::from_millis(50));
},
Err(_) => break,
}
}
}
fn read_from_channel(
@@ -431,13 +463,15 @@ impl RemoteTerminal {
.unwrap();
}
pub fn attach_to_original_session(&mut self) {
let mut channel = self.channel.lock().unwrap();
channel
.write_all(
format!("{} attach {}\n", ZELLIJ_EXECUTABLE_LOCATION, SESSION_NAME).as_bytes(),
)
.unwrap();
channel.flush().unwrap();
{
let mut channel = self.channel.lock().unwrap();
channel
.write_all(
format!("{} attach {}\n", ZELLIJ_EXECUTABLE_LOCATION, SESSION_NAME).as_bytes(),
)
.unwrap();
channel.flush().unwrap();
} // 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) {
@@ -533,6 +567,7 @@ impl RemoteRunner {
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,
@@ -571,6 +606,7 @@ impl RemoteRunner {
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,
@@ -609,6 +645,7 @@ impl RemoteRunner {
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,
@@ -647,6 +684,7 @@ impl RemoteRunner {
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,
@@ -691,6 +729,7 @@ impl RemoteRunner {
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,
@@ -709,7 +748,13 @@ impl RemoteRunner {
let sess = ssh_connect();
let mut channel = sess.channel_session().unwrap();
setup_remote_environment(&mut channel, win_size);
start_zellij(&mut channel);
stop_zellij(&mut channel);
channel
.write_all(format!("printf '{}\\n'\n", CLEANUP_DONE_MARKER).as_bytes())
.unwrap();
channel.flush().unwrap();
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!
@@ -736,6 +781,7 @@ impl RemoteRunner {
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,
@@ -774,6 +820,7 @@ impl RemoteRunner {
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,
@@ -812,6 +859,7 @@ impl RemoteRunner {
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,
@@ -850,6 +898,7 @@ impl RemoteRunner {
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,
@@ -889,6 +938,7 @@ impl RemoteRunner {
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,
@@ -3,7 +3,7 @@ source: src/tests/e2e/cases.rs
expression: last_snapshot
---
┌ Pane #1 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│$ /usr/src/zellij/x86_64-unknown-linux-musl/release/zellij action override-layout compact │
│$ /usr/src/zellij/zellij action override-layout compact │
│$ │
│$ █ │
│ │
@@ -25,4 +25,4 @@ expression: last_snapshot
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
Zellij (e2e-test) NORMAL  Tab #1 
Zellij (e2e-test) NORMAL  Tab #1 
@@ -4,7 +4,7 @@ expression: last_snapshot
---
Zellij (e2e-test)  Tab #1 
┌ Pane #1 ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│$ /usr/src/zellij/x86_64-unknown-linux-musl/release/zellij run --blocking --floating --close-on-exit -- bash -c 'sleep 2 && exit 42' │
│$ /usr/src/zellij/zellij run --blocking --floating --close-on-exit -- bash -c 'sleep 2 && exit 42' │
│$ │
│$ echo $? │
│42 │
@@ -25,4 +25,4 @@ expression: last_snapshot
│ │
│ │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SEARCH  <o> SESSION  <q> QUIT  Alt +  <n> New Pane  <f> Floating 
Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SEARCH  <o> SESSION  <q> QUIT  Alt +  <n> New Pane  <f> Floating 
@@ -4,8 +4,8 @@ expression: last_snapshot
---
Zellij (e2e-test)  Tab #1  Alt <[]>  VERTICAL 
┌ Pane #1 ────────────────────────────────────────────────────────────────┐┌ /usr/src/zellij/fixtures/append-echo-script.sh ─────────────────────────┐
│$ /usr/src/zellij/x86_64-unknown-linux-musl/release/zellij run -s -- "/us││foo │
│r/src/zellij/fixtures/append-echo-script.sh" ││foo │
│$ /usr/src/zellij/zellij run -s -- "/usr/src/zellij/fixtures/append-echo-││foo │
│script.sh" ││foo │
│terminal_1 ││█ │
│$ ││ │
│$ ││ │
@@ -4,13 +4,13 @@ expression: watcher_snapshot
---
Zellij (watcher_client_functionality)  Tab #1 
┌ Pane #1 ─────────────────────────────────────────────────┐┌ Pane #2 ─────────────────────────────────────────────────┐
│$ { for i in 1 2 3; do sleep 1; echo "WATCHER_OUTPUT_$i"; ││$ █ │
│done & } 2>/dev/null ││ │
│$ ││ │
│$ WATCHER_OUTPUT_1 ││ │
│$ for i in 1 2 3; do sleep 1; echo WATCHER_OUTPUT_$i; done││$ █ │
│; echo WATCHER_DONE; exec sleep 1000000 ││ │
│WATCHER_OUTPUT_1 ││ │
│WATCHER_OUTPUT_2 ││ │
│WATCHER_OUTPUT_3 ││ │
│$ ││ │
│WATCHER_DONE ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
@@ -4,13 +4,13 @@ expression: main_client_snapshot
---
Zellij (watcher_client_functionality)  Tab #1 
┌ Pane #1 ─────────────────────────────────────────────────┐┌ Pane #2 ─────────────────────────────────────────────────┐
│$ { for i in 1 2 3; do sleep 1; echo "WATCHER_OUTPUT_$i"; ││$ █ │
│done & } 2>/dev/null ││ │
│$ ││ │
│$ WATCHER_OUTPUT_1 ││ │
│$ for i in 1 2 3; do sleep 1; echo WATCHER_OUTPUT_$i; done││$ █ │
│; echo WATCHER_DONE; exec sleep 1000000 ││ │
│WATCHER_OUTPUT_1 ││ │
│WATCHER_OUTPUT_2 ││ │
│WATCHER_OUTPUT_3 ││ │
│$ ││ │
│WATCHER_DONE ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
+9 -3
View File
@@ -58,12 +58,18 @@ pub fn type_second_tab_content() -> Step {
Step {
name: "Type second tab content",
instruction: |mut remote_terminal: RemoteTerminal| -> bool {
let mut step_is_complete = false;
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);
step_is_complete = true;
// 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));
}
step_is_complete
false
},
}
}
+40 -8
View File
@@ -89,23 +89,55 @@ fn e2e_build(sh: &Shell) -> anyhow::Result<()> {
}
let _pd = sh.push_dir(project_root);
crate::cargo()
.and_then(|cargo| {
// On Windows, build natively (MSVC). On Unix, cross-compile for musl.
if cfg!(windows) {
// On Windows, build natively (MSVC).
// On macOS: use cargo-zigbuild which handles musl cross-compilation for any arch without
// needing a separate linker toolchain. The target arch matches the host so the Docker
// container runs natively without emulation (arm64 on Apple Silicon, amd64 on Intel).
// Install with: cargo install cargo-zigbuild && brew install zig
// On Linux: cross-compile for musl.
if cfg!(windows) {
crate::cargo()
.and_then(|cargo| {
cmd!(sh, "{cargo} build --release")
.run()
.map_err(anyhow::Error::new)
} else {
})
.context(err_context)
} else if cfg!(target_os = "macos") {
let target = if cfg!(target_arch = "aarch64") {
"aarch64-unknown-linux-musl"
} else {
"x86_64-unknown-linux-musl"
};
crate::cargo()
.and_then(|cargo| {
if which::which("cargo-zigbuild").is_err() {
eprintln!("!! 'cargo-zigbuild' wasn't found but is needed on macOS.");
eprintln!("!! Please install it with:");
eprintln!("!! cargo install cargo-zigbuild");
eprintln!("!! brew install zig");
return Err(anyhow::anyhow!("couldn't find 'cargo-zigbuild'"));
}
cmd!(sh, "rustup target add {target}")
.run()
.map_err(anyhow::Error::new)?;
cmd!(sh, "{cargo} zigbuild --release --target {target}")
.run()
.map_err(anyhow::Error::new)
})
.context(err_context)
} else {
crate::cargo()
.and_then(|cargo| {
cmd!(
sh,
"{cargo} build --release --target x86_64-unknown-linux-musl"
)
.run()
.map_err(anyhow::Error::new)
}
})
.context(err_context)
})
.context(err_context)
}
}
/// Native release build: builds plugins, generates protobufs, and runs