diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 000000000..a81d4ae61 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,49 @@ +name: End to End tests + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +env: + CARGO_TERM_COLOR: always + +jobs: + test: + name: Bulild generic binary and run tests on it + runs-on: ubuntu-latest + + services: + ssh: + image: ghcr.io/linuxserver/openssh-server + env: + PUID: 1000 + PGID: 1000 + TZ: Europe/Vienna + PASSWORD_ACCESS: true + USER_PASSWORD: test + USER_NAME: test + ports: + - 2222:2222 + options: -v ${{ github.workspace }}/target:/usr/src/zellij --name ssh + steps: + - uses: actions/checkout@v2 + - name: Add WASM target + run: rustup target add wasm32-wasi + - name: Install musl-tools + run: sudo apt-get install -y --no-install-recommends musl-tools + - name: Add musl target + run: rustup target add x86_64-unknown-linux-musl + - name: Install cargo-make + run: cargo install --debug cargo-make + - name: Build asset + run: cargo make build-e2e + - name: Restart ssh container + # we need to do this because otherwise the volume will not be mounted + # on the docker container, since it was created before the folder existed + uses: docker://docker + with: + args: docker restart ssh + - name: Test + run: cargo make e2e-test diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 95ac05ec4..d34367f33 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,6 +44,18 @@ cargo make manpage To run `install` or `publish`, you'll need the package `binaryen` in the version `wasm-opt --version` > 97, for it's command `wasm-opt`. +## Running the end-to-end tests +Zellij includes some end to end tests which test the whole application as a black-box from the outside. +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. + +To run these tests locally, you'll need to have both `docker` and `docker-compose` installed. +Once you do, in the repository root: +1. `docker-compose up -d` will start up the docker container +2. `cargo make build-e2e` will build the generic linux executable of Zellij in the target folder, which is shared with the container +3. `cargo make 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. + ## Looking for something to work on? If you are new contributor to `Zellij` going through diff --git a/Cargo.lock b/Cargo.lock index 2996a8182..a2c525d65 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -312,6 +312,15 @@ dependencies = [ "vec_map", ] +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags", +] + [[package]] name = "colored" version = "2.0.0" @@ -504,7 +513,7 @@ dependencies = [ "lazy_static", "libc", "mio", - "parking_lot", + "parking_lot 0.11.1", "signal-hook 0.1.17", "winapi", ] @@ -902,6 +911,7 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a1b21a2971cea49ca4613c0e9fe8225ecaf5de64090fddc6002284726e9244" dependencies = [ + "backtrace", "console", "lazy_static", "serde", @@ -1017,12 +1027,47 @@ dependencies = [ "winapi", ] +[[package]] +name = "libssh2-sys" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0186af0d8f171ae6b9c4c90ec51898bad5d08a2d5e470903a50d9ad8959cbee" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de5435b8549c16d423ed0c03dbaafe57cf6c3344744f1242520d59c9d8ecec66" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linked-hash-map" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" +[[package]] +name = "lock_api" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" +dependencies = [ + "scopeguard", +] + [[package]] name = "lock_api" version = "0.4.4" @@ -1184,12 +1229,35 @@ version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" +[[package]] +name = "openssl-sys" +version = "0.9.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b0d6fb7d80f877617dfcb014e605e2b5ab2fb0afdf27935219bb6bd984cb98" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "parking" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" +[[package]] +name = "parking_lot" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e" +dependencies = [ + "lock_api 0.3.4", + "parking_lot_core 0.7.2", +] + [[package]] name = "parking_lot" version = "0.11.1" @@ -1197,8 +1265,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" dependencies = [ "instant", - "lock_api", - "parking_lot_core", + "lock_api 0.4.4", + "parking_lot_core 0.8.3", +] + +[[package]] +name = "parking_lot_core" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d58c7c768d4ba344e3e8d72518ac13e259d7c7ade24167003b8488e10b6740a3" +dependencies = [ + "cfg-if 0.1.10", + "cloudabi", + "libc", + "redox_syscall 0.1.57", + "smallvec", + "winapi", ] [[package]] @@ -1210,7 +1292,7 @@ dependencies = [ "cfg-if 1.0.0", "instant", "libc", - "redox_syscall", + "redox_syscall 0.2.8", "smallvec", "winapi", ] @@ -1227,6 +1309,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" + [[package]] name = "polling" version = "2.0.3" @@ -1422,6 +1510,12 @@ dependencies = [ "rand_core 0.3.1", ] +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + [[package]] name = "redox_syscall" version = "0.2.8" @@ -1437,7 +1531,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f" dependencies = [ - "redox_syscall", + "redox_syscall 0.2.8", ] [[package]] @@ -1447,7 +1541,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" dependencies = [ "getrandom", - "redox_syscall", + "redox_syscall 0.2.8", ] [[package]] @@ -1622,7 +1716,19 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d4f0e86297cad2658d92a707320d87bf4e6ae1050287f51d19b67ef3f153a7b" dependencies = [ - "lock_api", + "lock_api 0.4.4", +] + +[[package]] +name = "ssh2" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d876d4d57f6bbf2245d43f7ec53759461f801a446d3693704aa6d27b257844d7" +dependencies = [ + "bitflags", + "libc", + "libssh2-sys", + "parking_lot 0.10.2", ] [[package]] @@ -1749,7 +1855,7 @@ dependencies = [ "cfg-if 1.0.0", "libc", "rand 0.8.3", - "redox_syscall", + "redox_syscall 0.2.8", "remove_dir_all", "winapi", ] @@ -1783,7 +1889,7 @@ checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e" dependencies = [ "libc", "numtoa", - "redox_syscall", + "redox_syscall 0.2.8", "redox_termios", ] @@ -1929,6 +2035,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "vcpkg" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "025ce40a007e1907e58d5bc1a594def78e5573bb0b1160bc389634e8f12e4faa" + [[package]] name = "vec_map" version = "0.8.2" @@ -2333,6 +2445,8 @@ version = "0.14.0" dependencies = [ "insta", "names", + "rand 0.8.3", + "ssh2", "zellij-client", "zellij-server", "zellij-utils", diff --git a/Cargo.toml b/Cargo.toml index 7e483940b..165254525 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,9 @@ zellij-server = { path = "zellij-server/", version = "0.14.0" } zellij-utils = { path = "zellij-utils/", version = "0.14.0" } [dev-dependencies] -insta = "1.6.0" +insta = { version = "1.6.0", features = ["backtrace"] } +ssh2 = "0.9.1" +rand = "0.8.0" zellij-utils = { path = "zellij-utils/", version = "0.14.0", features = ["test"] } zellij-client = { path = "zellij-client/", version = "0.14.0", features = ["test"] } zellij-server = { path = "zellij-server/", version = "0.14.0", features = ["test"] } diff --git a/Makefile.toml b/Makefile.toml index 16b7695ba..e0043ab71 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -84,6 +84,10 @@ end env = { "CARGO_MAKE_WORKSPACE_INCLUDE_MEMBERS" = ["default-plugins/status-bar", "default-plugins/strider", "default-plugins/tab-bar"] } run_task = { name = "build-release", fork = true } +[tasks.build-plugins] +env = { "CARGO_MAKE_WORKSPACE_INCLUDE_MEMBERS" = ["default-plugins/status-bar", "default-plugins/strider", "default-plugins/tab-bar"] } +run_task = { name = "build", fork = true } + [tasks.wasm-opt-plugins] script_runner = "@duckscript" script = ''' @@ -120,6 +124,19 @@ dependencies = ["setup-cross-compilation", "build-plugins-release", "wasm-opt-pl command = "cross" args = ["build", "--verbose", "--release", "--target", "${CARGO_MAKE_TASK_ARGS}"] +# Build e2e asset +[tasks.build-e2e] +workspace = false +dependencies = ["build-plugins"] +command = "cargo" +args = ["build", "--verbose", "--target", "x86_64-unknown-linux-musl"] + +# Run e2e tests - we mark the e2e tests as "ignored" so they will not be run with the normal ones +[tasks.e2e-test] +workspace = false +command = "cargo" +args = ["test", "--", "--ignored", "--nocapture", "--test-threads", "1", "@@split(CARGO_MAKE_TASK_ARGS,;)"] + [tasks.setup-cross-compilation] command = "cargo" args = ["install", "cross"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..ec52486ee --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,21 @@ +--- +version: "2.1" +services: + zellij-e2e: + image: ghcr.io/linuxserver/openssh-server + container_name: zellij-e2e + hostname: zellij-e2e + environment: + - PUID=1000 + - PGID=1000 + - TZ=Europe/Vienna + - PASSWORD_ACCESS=true + - USER_PASSWORD=test + - USER_NAME=test + volumes: + - type: bind + source: ./target + target: /usr/src/zellij + ports: + - 2222:2222 + restart: unless-stopped diff --git a/src/tests/e2e/cases.rs b/src/tests/e2e/cases.rs new file mode 100644 index 000000000..5af67074c --- /dev/null +++ b/src/tests/e2e/cases.rs @@ -0,0 +1,716 @@ +use ::insta::assert_snapshot; +use zellij_utils::pane_size::PositionAndSize; + +use rand::Rng; + +use super::remote_runner::{RemoteRunner, RemoteTerminal, Step}; +use crate::tests::utils::commands::{ + CLOSE_PANE_IN_PANE_MODE, DETACH_IN_SESSION_MODE, ENTER, LOCK_MODE, NEW_TAB_IN_TAB_MODE, + PANE_MODE, QUIT, RESIZE_LEFT_IN_RESIZE_MODE, RESIZE_MODE, SCROLL_MODE, + SCROLL_UP_IN_SCROLL_MODE, SESSION_MODE, SPLIT_RIGHT_IN_PANE_MODE, TAB_MODE, + TOGGLE_ACTIVE_TERMINAL_FULLSCREEN_IN_PANE_MODE, +}; + +// All the E2E tests are marked as "ignored" so that they can be run separately from the normal +// tests + +#[test] +#[ignore] +pub fn starts_with_one_terminal() { + let fake_win_size = PositionAndSize { + cols: 120, + rows: 24, + x: 0, + y: 0, + ..Default::default() + }; + let last_snapshot = RemoteRunner::new("starts_with_one_terminal", fake_win_size, None) + .add_step(Step { + name: "Wait for app to load", + instruction: |remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(2, 2) + { + step_is_complete = true; + } + step_is_complete + }, + }) + .run_all_steps(); + assert_snapshot!(last_snapshot); +} + +#[test] +#[ignore] +pub fn split_terminals_vertically() { + let fake_win_size = PositionAndSize { + cols: 120, + rows: 24, + x: 0, + y: 0, + ..Default::default() + }; + + let last_snapshot = RemoteRunner::new("split_terminals_vertically", fake_win_size, None) + .add_step(Step { + name: "Split pane to the right", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(2, 2) + { + remote_terminal.send_key(&PANE_MODE); + remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); + // back to normal mode after split + remote_terminal.send_key(&ENTER); + step_is_complete = true; + } + step_is_complete + }, + }) + .add_step(Step { + name: "Wait for new pane to appear", + instruction: |remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(63, 2) && remote_terminal.tip_appears() { + // cursor is in the newly opened second pane + step_is_complete = true; + } + step_is_complete + }, + }) + .run_all_steps(); + assert_snapshot!(last_snapshot); +} + +#[test] +#[ignore] +pub fn cannot_split_terminals_vertically_when_active_terminal_is_too_small() { + let fake_win_size = PositionAndSize { + cols: 8, + rows: 20, + x: 0, + y: 0, + ..Default::default() + }; + let last_snapshot = RemoteRunner::new( + "cannot_split_terminals_vertically_when_active_terminal_is_too_small", + fake_win_size, + None, + ) + .add_step(Step { + name: "Split pane to the right", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(2, 2) { + remote_terminal.send_key(&PANE_MODE); + remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); + // back to normal mode after split + remote_terminal.send_key(&ENTER); + step_is_complete = true; + } + step_is_complete + }, + }) + .add_step(Step { + name: "Send text to terminal", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + // this is just normal input that should be sent into the one terminal so that we can make + // sure we silently failed to split in the previous step + remote_terminal.send_key(&"Hi!".as_bytes()); + true + }, + }) + .add_step(Step { + name: "Wait for text to appear", + instruction: |remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(5, 2) && remote_terminal.snapshot_contains("Hi!") + { + step_is_complete = true; + } + step_is_complete + }, + }) + .run_all_steps(); + assert_snapshot!(last_snapshot); +} + +#[test] +#[ignore] +pub fn scrolling_inside_a_pane() { + let fake_win_size = PositionAndSize { + cols: 120, + rows: 24, + x: 0, + y: 0, + ..Default::default() + }; + let last_snapshot = RemoteRunner::new("scrolling_inside_a_pane", fake_win_size, None) + .add_step(Step { + name: "Split pane to the right", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(2, 2) + { + remote_terminal.send_key(&PANE_MODE); + remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); + // back to normal mode after split + remote_terminal.send_key(&ENTER); + step_is_complete = true; + } + step_is_complete + }, + }) + .add_step(Step { + name: "Fill terminal with text", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(63, 2) && remote_terminal.tip_appears() { + // cursor is in the newly opened second pane + remote_terminal.send_key(&format!("{:0<57}", "line1 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line2 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line3 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line4 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line5 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line6 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line7 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line8 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line9 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line10 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line11 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line12 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line13 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line14 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line15 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line16 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line17 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line18 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<59}", "line19 ").as_bytes()); + remote_terminal.send_key(&format!("{:0<58}", "line20 ").as_bytes()); + step_is_complete = true; + } + step_is_complete + }, + }) + .add_step(Step { + name: "Scroll up inside pane", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(119, 20) { + // all lines have been written to the pane + remote_terminal.send_key(&SCROLL_MODE); + remote_terminal.send_key(&SCROLL_UP_IN_SCROLL_MODE); + step_is_complete = true; + } + step_is_complete + }, + }) + .add_step(Step { + name: "Wait for scroll to finish", + instruction: |remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(119, 20) + && remote_terminal.snapshot_contains("line1 ") + { + // scrolled up one line + step_is_complete = true; + } + step_is_complete + }, + }) + .run_all_steps(); + assert_snapshot!(last_snapshot); +} + +#[test] +#[ignore] +pub fn toggle_pane_fullscreen() { + let fake_win_size = PositionAndSize { + cols: 120, + rows: 24, + x: 0, + y: 0, + ..Default::default() + }; + let last_snapshot = RemoteRunner::new("toggle_pane_fullscreen", fake_win_size, None) + .add_step(Step { + name: "Split pane to the right", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(2, 2) + { + remote_terminal.send_key(&PANE_MODE); + remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); + // back to normal mode after split + remote_terminal.send_key(&ENTER); + step_is_complete = true; + } + step_is_complete + }, + }) + .add_step(Step { + name: "Change newly opened pane to be fullscreen", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(63, 2) && remote_terminal.tip_appears() { + // cursor is in the newly opened second pane + remote_terminal.send_key(&PANE_MODE); + remote_terminal.send_key(&TOGGLE_ACTIVE_TERMINAL_FULLSCREEN_IN_PANE_MODE); + // back to normal mode after toggling fullscreen + remote_terminal.send_key(&ENTER); + step_is_complete = true; + } + step_is_complete + }, + }) + .add_step(Step { + name: "Wait for pane to become fullscreen", + instruction: |remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(2, 0) { + // cursor is in full screen pane now + step_is_complete = true; + } + step_is_complete + }, + }) + .run_all_steps(); + assert_snapshot!(last_snapshot); +} + +#[test] +#[ignore] +pub fn open_new_tab() { + let fake_win_size = PositionAndSize { + cols: 120, + rows: 24, + x: 0, + y: 0, + ..Default::default() + }; + let last_snapshot = RemoteRunner::new("open_new_tab", fake_win_size, None) + .add_step(Step { + name: "Split pane to the right", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(2, 2) + { + remote_terminal.send_key(&PANE_MODE); + remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); + // back to normal mode after split + remote_terminal.send_key(&ENTER); + step_is_complete = true; + } + step_is_complete + }, + }) + .add_step(Step { + name: "Open new tab", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(63, 2) && remote_terminal.tip_appears() { + // cursor is in the newly opened second pane + remote_terminal.send_key(&TAB_MODE); + remote_terminal.send_key(&NEW_TAB_IN_TAB_MODE); + // back to normal mode after split + remote_terminal.send_key(&ENTER); + step_is_complete = true; + } + step_is_complete + }, + }) + .add_step(Step { + name: "Wait for new tab to open", + instruction: |remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(2, 2) + && remote_terminal.tip_appears() + && remote_terminal.snapshot_contains("Tab #2") + && remote_terminal.status_bar_appears() + { + // cursor is in the newly opened second tab + step_is_complete = true; + } + step_is_complete + }, + }) + .run_all_steps(); + assert_snapshot!(last_snapshot); +} + +#[test] +#[ignore] +pub fn close_pane() { + let fake_win_size = PositionAndSize { + cols: 120, + rows: 24, + x: 0, + y: 0, + ..Default::default() + }; + let last_snapshot = RemoteRunner::new("close_pane", fake_win_size, None) + .add_step(Step { + name: "Split pane to the right", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(2, 2) + { + remote_terminal.send_key(&PANE_MODE); + remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); + // back to normal mode after split + remote_terminal.send_key(&ENTER); + step_is_complete = true; + } + step_is_complete + }, + }) + .add_step(Step { + name: "Close pane", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(63, 2) && remote_terminal.tip_appears() { + // cursor is in the newly opened second pane + remote_terminal.send_key(&PANE_MODE); + remote_terminal.send_key(&CLOSE_PANE_IN_PANE_MODE); + // back to normal mode after close + remote_terminal.send_key(&ENTER); + step_is_complete = true; + } + step_is_complete + }, + }) + .add_step(Step { + name: "Wait for pane to close", + instruction: |remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(2, 2) && remote_terminal.tip_appears() { + // cursor is in the original pane + step_is_complete = true; + } + step_is_complete + }, + }) + .run_all_steps(); + assert_snapshot!(last_snapshot); +} + +#[test] +#[ignore] +pub fn exit_zellij() { + let fake_win_size = PositionAndSize { + cols: 120, + rows: 24, + x: 0, + y: 0, + ..Default::default() + }; + let last_snapshot = RemoteRunner::new("exit_zellij", fake_win_size, None) + .add_step(Step { + name: "Wait for app to load", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(2, 2) + { + remote_terminal.send_key(&QUIT); + step_is_complete = true; + } + step_is_complete + }, + }) + .add_step(Step { + name: "Wait for app to exit", + instruction: |remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if !remote_terminal.status_bar_appears() + && remote_terminal.snapshot_contains("Bye from Zellij!") + { + step_is_complete = true; + } + step_is_complete + }, + }) + .run_all_steps(); + assert!(last_snapshot.contains("Bye from Zellij!")); +} + +#[test] +#[ignore] +pub fn closing_last_pane_exits_zellij() { + let fake_win_size = PositionAndSize { + cols: 120, + rows: 24, + x: 0, + y: 0, + ..Default::default() + }; + let last_snapshot = RemoteRunner::new("closing_last_pane_exits_zellij", fake_win_size, None) + .add_step(Step { + name: "Close pane", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(2, 2) + { + remote_terminal.send_key(&PANE_MODE); + remote_terminal.send_key(&CLOSE_PANE_IN_PANE_MODE); + step_is_complete = true; + } + step_is_complete + }, + }) + .add_step(Step { + name: "Wait for app to exit", + instruction: |remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.snapshot_contains("Bye from Zellij!") { + step_is_complete = true; + } + step_is_complete + }, + }) + .run_all_steps(); + assert!(last_snapshot.contains("Bye from Zellij!")); +} + +#[test] +#[ignore] +pub fn resize_pane() { + let fake_win_size = PositionAndSize { + cols: 120, + rows: 24, + x: 0, + y: 0, + ..Default::default() + }; + let last_snapshot = RemoteRunner::new("resize_pane", fake_win_size, None) + .add_step(Step { + name: "Split pane to the right", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(2, 2) + { + remote_terminal.send_key(&PANE_MODE); + remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); + // back to normal mode after split + remote_terminal.send_key(&ENTER); + step_is_complete = true; + } + step_is_complete + }, + }) + .add_step(Step { + name: "Resize pane", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(63, 2) && remote_terminal.tip_appears() { + // cursor is in the newly opened second pane + remote_terminal.send_key(&RESIZE_MODE); + remote_terminal.send_key(&RESIZE_LEFT_IN_RESIZE_MODE); + // back to normal mode after resizing + remote_terminal.send_key(&ENTER); + step_is_complete = true; + } + step_is_complete + }, + }) + .add_step(Step { + name: "Wait for pane to be resized", + instruction: |remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(53, 2) && remote_terminal.tip_appears() { + // pane has been resized + step_is_complete = true; + } + step_is_complete + }, + }) + .run_all_steps(); + assert_snapshot!(last_snapshot); +} + +#[test] +#[ignore] +pub fn lock_mode() { + let fake_win_size = PositionAndSize { + cols: 120, + rows: 24, + x: 0, + y: 0, + ..Default::default() + }; + let last_snapshot = RemoteRunner::new("lock_mode", fake_win_size, None) + .add_step(Step { + name: "Enter lock mode", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(2, 2) + { + remote_terminal.send_key(&LOCK_MODE); + step_is_complete = true; + } + step_is_complete + }, + }) + .add_step(Step { + name: "Send keys that should not be intercepted by the app", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.snapshot_contains("INTERFACE LOCKED") { + remote_terminal.send_key(&TAB_MODE); + remote_terminal.send_key(&NEW_TAB_IN_TAB_MODE); + remote_terminal.send_key(&"abc".as_bytes()); + step_is_complete = true; + } + step_is_complete + }, + }) + .add_step(Step { + name: "Wait for terminal to render sent keys", + instruction: |remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(6, 2) { + // text has been entered into the only terminal pane + step_is_complete = true; + } + step_is_complete + }, + }) + .run_all_steps(); + assert_snapshot!(last_snapshot); +} + +#[test] +#[ignore] +pub fn resize_terminal_window() { + // this checks the resizing of the whole terminal window (reaction to SIGWINCH) and not just one pane + let fake_win_size = PositionAndSize { + cols: 120, + rows: 24, + x: 0, + y: 0, + ..Default::default() + }; + let last_snapshot = RemoteRunner::new("resize_terminal_window", fake_win_size, None) + .add_step(Step { + name: "Split pane to the right", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(2, 2) + { + remote_terminal.send_key(&PANE_MODE); + remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); + // back to normal mode after split + remote_terminal.send_key(&ENTER); + step_is_complete = true; + } + step_is_complete + }, + }) + .add_step(Step { + name: "Change terminal window size", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(63, 2) && remote_terminal.tip_appears() { + // new pane has been opened and focused + remote_terminal.change_size(100, 24); + step_is_complete = true; + } + step_is_complete + }, + }) + .add_step(Step { + name: "wait for terminal to be resized and app to be re-rendered", + instruction: |remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(43, 2) && remote_terminal.tip_appears() { + // size has been changed + step_is_complete = true; + } + step_is_complete + }, + }) + .run_all_steps(); + assert_snapshot!(last_snapshot); +} + +#[test] +#[ignore] +pub fn detach_and_attach_session() { + let fake_win_size = PositionAndSize { + cols: 120, + rows: 24, + x: 0, + y: 0, + ..Default::default() + }; + let session_id = rand::thread_rng().gen_range(0..10000); + let session_name = format!("session_{}", session_id); + let last_snapshot = RemoteRunner::new( + "detach_and_attach_session", + fake_win_size, + Some(session_name), + ) + .add_step(Step { + name: "Split pane to the right", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(2, 2) { + remote_terminal.send_key(&PANE_MODE); + remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); + // back to normal mode after split + remote_terminal.send_key(&ENTER); + step_is_complete = true; + } + step_is_complete + }, + }) + .add_step(Step { + name: "Send some text to the active pane", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(63, 2) && remote_terminal.tip_appears() { + // new pane has been opened and focused + remote_terminal.send_key(&"I am some text".as_bytes()); + step_is_complete = true; + } + step_is_complete + }, + }) + .add_step(Step { + name: "Detach session", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(77, 2) { + remote_terminal.send_key(&SESSION_MODE); + remote_terminal.send_key(&DETACH_IN_SESSION_MODE); + // text has been entered + step_is_complete = true; + } + step_is_complete + }, + }) + .add_step(Step { + name: "Reattach session", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if !remote_terminal.status_bar_appears() { + // we don't see the toolbar, so we can assume we've already detached + remote_terminal.attach_to_original_session(); + step_is_complete = true; + } + step_is_complete + }, + }) + .add_step(Step { + name: "Wait for session to be attached", + instruction: |remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(77, 2) { + // we're back inside the session + step_is_complete = true; + } + step_is_complete + }, + }) + .run_all_steps(); + assert_snapshot!(last_snapshot); +} diff --git a/src/tests/e2e/mod.rs b/src/tests/e2e/mod.rs new file mode 100644 index 000000000..3cf635425 --- /dev/null +++ b/src/tests/e2e/mod.rs @@ -0,0 +1,2 @@ +pub mod cases; +mod remote_runner; diff --git a/src/tests/e2e/remote_runner.rs b/src/tests/e2e/remote_runner.rs new file mode 100644 index 000000000..b53c3129d --- /dev/null +++ b/src/tests/e2e/remote_runner.rs @@ -0,0 +1,284 @@ +use zellij_tile::data::Palette; +use zellij_utils::pane_size::PositionAndSize; + +use zellij_server::panes::TerminalPane; +use zellij_utils::{vte, zellij_tile}; + +use ssh2::Session; +use std::io::prelude::*; +use std::net::TcpStream; + +const ZELLIJ_EXECUTABLE_LOCATION: &str = "/usr/src/zellij/x86_64-unknown-linux-musl/debug/zellij"; +const CONNECTION_STRING: &str = "127.0.0.1:2222"; +const CONNECTION_USERNAME: &str = "test"; +const CONNECTION_PASSWORD: &str = "test"; + +fn ssh_connect() -> 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.set_timeout(20000); + sess +} + +fn setup_remote_environment(channel: &mut ssh2::Channel, win_size: PositionAndSize) { + let (columns, rows) = (win_size.cols as u32, win_size.rows as u32); + channel + .request_pty("xterm", None, Some((columns, rows, 0, 0))) + .unwrap(); + channel.shell().unwrap(); + channel + .write_all(format!("export PS1=\"$ \"\n").as_bytes()) + .unwrap(); + channel.flush().unwrap(); +} + +fn start_zellij(channel: &mut ssh2::Channel, session_name: Option<&String>) { + match session_name.as_ref() { + Some(name) => { + channel + .write_all( + format!("{} --session {}\n", ZELLIJ_EXECUTABLE_LOCATION, name).as_bytes(), + ) + .unwrap(); + } + None => { + channel + .write_all(format!("{}\n", ZELLIJ_EXECUTABLE_LOCATION).as_bytes()) + .unwrap(); + } + }; + channel.flush().unwrap(); +} + +pub fn take_snapshot(terminal_output: &mut TerminalPane) -> String { + let output_lines = terminal_output.read_buffer_as_lines(); + let cursor_coordinates = terminal_output.cursor_coordinates(); + let mut snapshot = String::new(); + for (line_index, line) in output_lines.iter().enumerate() { + for (character_index, terminal_character) in line.iter().enumerate() { + if let Some((cursor_x, cursor_y)) = cursor_coordinates { + if line_index == cursor_y && character_index == cursor_x { + snapshot.push('█'); + continue; + } + } + snapshot.push(terminal_character.character); + } + if line_index != output_lines.len() - 1 { + snapshot.push('\n'); + } + } + snapshot +} + +pub struct RemoteTerminal<'a> { + channel: &'a mut ssh2::Channel, + session_name: Option<&'a String>, + cursor_x: usize, + cursor_y: usize, + current_snapshot: String, +} + +impl<'a> std::fmt::Debug for RemoteTerminal<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "cursor x: {}\ncursor_y: {}\ncurrent_snapshot:\n{}", + self.cursor_x, self.cursor_y, self.current_snapshot + ) + } +} + +impl<'a> RemoteTerminal<'a> { + pub fn cursor_position_is(&self, x: usize, y: usize) -> bool { + x == self.cursor_x && y == self.cursor_y + } + pub fn tip_appears(&self) -> bool { + self.current_snapshot.contains("Tip:") + } + pub fn status_bar_appears(&self) -> bool { + self.current_snapshot.contains("Ctrl +") && !self.current_snapshot.contains("─────") + // this is a bug that happens because the app draws borders around the status bar momentarily on first render + } + pub fn snapshot_contains(&self, text: &str) -> bool { + self.current_snapshot.contains(text) + } + pub fn send_key(&mut self, key: &[u8]) { + self.channel.write(key).unwrap(); + self.channel.flush().unwrap(); + } + pub fn change_size(&mut self, cols: u32, rows: u32) { + self.channel + .request_pty_size(cols, rows, Some(cols), Some(rows)) + .unwrap(); + } + pub fn attach_to_original_session(&mut self) { + self.channel + .write_all( + format!( + "{} attach {}\n", + ZELLIJ_EXECUTABLE_LOCATION, + self.session_name.unwrap() + ) + .as_bytes(), + ) + .unwrap(); + self.channel.flush().unwrap(); + } +} + +#[derive(Clone)] +pub struct Step { + pub instruction: fn(RemoteTerminal) -> bool, + pub name: &'static str, +} + +pub struct RemoteRunner { + steps: Vec, + current_step_index: usize, + vte_parser: vte::Parser, + terminal_output: TerminalPane, + channel: ssh2::Channel, + session_name: Option, + test_name: &'static str, + currently_running_step: Option, + retries_left: usize, + win_size: PositionAndSize, +} + +impl RemoteRunner { + pub fn new( + test_name: &'static str, + win_size: PositionAndSize, + session_name: Option, + ) -> Self { + let sess = ssh_connect(); + let mut channel = sess.channel_session().unwrap(); + let vte_parser = vte::Parser::new(); + let terminal_output = TerminalPane::new(0, win_size, Palette::default()); + setup_remote_environment(&mut channel, win_size); + start_zellij(&mut channel, session_name.as_ref()); + RemoteRunner { + steps: vec![], + channel, + terminal_output, + vte_parser, + session_name, + test_name, + currently_running_step: None, + current_step_index: 0, + retries_left: 3, + win_size, + } + } + pub fn add_step(mut self, step: Step) -> Self { + self.steps.push(step); + self + } + pub fn replace_steps(&mut self, steps: Vec) { + self.steps = steps; + } + fn current_remote_terminal_state(&mut self) -> RemoteTerminal { + let current_snapshot = self.get_current_snapshot(); + let (cursor_x, cursor_y) = self.terminal_output.cursor_coordinates().unwrap_or((0, 0)); + RemoteTerminal { + cursor_x, + cursor_y, + current_snapshot, + channel: &mut self.channel, + session_name: self.session_name.as_ref(), + } + } + pub fn run_next_step(&mut self) { + if let Some(next_step) = self.steps.get(self.current_step_index) { + let current_snapshot = take_snapshot(&mut self.terminal_output); + let (cursor_x, cursor_y) = self.terminal_output.cursor_coordinates().unwrap_or((0, 0)); + let remote_terminal = RemoteTerminal { + cursor_x, + cursor_y, + current_snapshot, + channel: &mut self.channel, + session_name: self.session_name.as_ref(), + }; + let instruction = next_step.instruction; + self.currently_running_step = Some(String::from(next_step.name)); + if instruction(remote_terminal) { + self.current_step_index += 1; + } + } + } + pub fn steps_left(&self) -> bool { + self.steps.get(self.current_step_index).is_some() + } + fn restart_test(&mut self) -> String { + let session_name = self.session_name.as_ref().map(|name| { + // this is so that we don't try to connect to the previous session if it's still stuck + // inside the container + format!("{}_{}", name, self.retries_left) + }); + let mut new_runner = RemoteRunner::new(self.test_name, self.win_size, session_name); + new_runner.retries_left = self.retries_left - 1; + new_runner.replace_steps(self.steps.clone()); + drop(std::mem::replace(self, new_runner)); + self.run_all_steps() + } + fn display_informative_error(&mut self) { + let test_name = self.test_name; + let current_step_name = self.currently_running_step.as_ref().cloned(); + match current_step_name { + Some(current_step) => { + let remote_terminal = self.current_remote_terminal_state(); + eprintln!("Timed out waiting for data on the SSH channel for test {}. Was waiting for step: {}", test_name, current_step); + eprintln!("{:?}", remote_terminal); + } + None => { + let remote_terminal = self.current_remote_terminal_state(); + eprintln!("Timed out waiting for data on the SSH channel for test {}. Haven't begun running steps yet.", test_name); + eprintln!("{:?}", remote_terminal); + } + } + } + pub fn run_all_steps(&mut self) -> String { + // returns the last snapshot + loop { + let mut buf = [0u8; 1024]; + match self.channel.read(&mut buf) { + Ok(0) => break, + Ok(_count) => { + for byte in buf.iter() { + self.vte_parser + .advance(&mut self.terminal_output.grid, *byte); + } + self.run_next_step(); + if !self.steps_left() { + break; + } + } + Err(e) => { + if e.kind() == std::io::ErrorKind::TimedOut { + if self.retries_left > 0 { + return self.restart_test(); + } + self.display_informative_error(); + panic!("Timed out waiting for test"); + } + panic!("Error while reading remote session: {}", e); + } + } + } + take_snapshot(&mut self.terminal_output) + } + pub fn get_current_snapshot(&mut self) -> String { + take_snapshot(&mut self.terminal_output) + } +} + +impl Drop for RemoteRunner { + fn drop(&mut self) { + self.channel.close().unwrap(); + } +} diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__cannot_split_terminals_vertically_when_active_terminal_is_too_small.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__cannot_split_terminals_vertically_when_active_terminal_is_too_small.snap new file mode 100644 index 000000000..23ffc097d --- /dev/null +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__cannot_split_terminals_vertically_when_active_terminal_is_too_small.snap @@ -0,0 +1,25 @@ +--- +source: src/tests/integration/e2e.rs +expression: last_snapshot + +--- + + +$ Hi!█ + + + + + + + + + + + + + + + + Ctrl + + diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__close_pane.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__close_pane.snap new file mode 100644 index 000000000..32f6d26f2 --- /dev/null +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__close_pane.snap @@ -0,0 +1,29 @@ +--- +source: src/tests/integration/e2e.rs +expression: last_snapshot + +--- + Zellij  Tab #1  + +$ █ + + + + + + + + + + + + + + + + + + + + Ctrl + LOCK 

PANE  TAB  RESIZE  SCROLL  SESSION  QUIT  + Tip: Alt + n => open new pane. Alt + [] or hjkl => navigate between panes. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__close_tab.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__close_tab.snap new file mode 100644 index 000000000..bbb5b068a --- /dev/null +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__close_tab.snap @@ -0,0 +1,29 @@ +--- +source: src/tests/integration/e2e.rs +expression: last_snapshot + +--- + Zellij  Tab #1  + +$ │$ █ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + + Ctrl + LOCK 

PANE  TAB  RESIZE  SCROLL  SESSION  QUIT  + Tip: Alt + n => open new pane. Alt + [] or hjkl => navigate between panes. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__detach_and_attach_session.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__detach_and_attach_session.snap new file mode 100644 index 000000000..983331aa2 --- /dev/null +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__detach_and_attach_session.snap @@ -0,0 +1,29 @@ +--- +source: src/tests/integration/e2e.rs +expression: last_snapshot + +--- + Zellij  Tab #1  + +$ │$ I am some text█ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + + Ctrl + LOCK 

PANE  TAB  RESIZE  SCROLL  SESSION  QUIT  + Tip: Alt + n => open new pane. Alt + [] or hjkl => navigate between panes. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__lock_mode.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__lock_mode.snap new file mode 100644 index 000000000..bc54e6753 --- /dev/null +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__lock_mode.snap @@ -0,0 +1,29 @@ +--- +source: src/tests/integration/e2e.rs +expression: last_snapshot + +--- + Zellij  Tab #1  + +$ nabc█ + + + + + + + + + + + + + + + + + + + + Ctrl + LOCK 

PANE  TAB  RESIZE  SCROLL  SESSION  QUIT  + -- INTERFACE LOCKED -- diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__open_new_tab.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__open_new_tab.snap new file mode 100644 index 000000000..9b8267835 --- /dev/null +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__open_new_tab.snap @@ -0,0 +1,29 @@ +--- +source: src/tests/integration/e2e.rs +expression: last_snapshot + +--- + Zellij  Tab #1  Tab #2  + +$ █ + + + + + + + + + + + + + + + + + + + + Ctrl + LOCK 

PANE  TAB  RESIZE  SCROLL  SESSION  QUIT  + Tip: Alt + n => open new pane. Alt + [] or hjkl => navigate between panes. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__resize_pane.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__resize_pane.snap new file mode 100644 index 000000000..5f1ed7f64 --- /dev/null +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__resize_pane.snap @@ -0,0 +1,29 @@ +--- +source: src/tests/integration/e2e.rs +expression: last_snapshot + +--- + Zellij  Tab #1  + +$ │$ █ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + + Ctrl + LOCK 

PANE  TAB  RESIZE  SCROLL  SESSION  QUIT  + Tip: Alt + n => open new pane. Alt + [] or hjkl => navigate between panes. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__resize_terminal_window.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__resize_terminal_window.snap new file mode 100644 index 000000000..a57d4849a --- /dev/null +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__resize_terminal_window.snap @@ -0,0 +1,29 @@ +--- +source: src/tests/integration/e2e.rs +expression: last_snapshot + +--- + Zellij  Tab #1  + +$ │$ █ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + + Ctrl + LOCK 

PANE  TAB  RESIZE  SCROLL  SESSION  QUIT  + Tip: Alt + n => open new pane. Alt + [] or hjkl => navigate between panes. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__scrolling_inside_a_pane.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__scrolling_inside_a_pane.snap new file mode 100644 index 000000000..a609be4f2 --- /dev/null +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__scrolling_inside_a_pane.snap @@ -0,0 +1,29 @@ +--- +source: src/tests/integration/e2e.rs +expression: last_snapshot + +--- + Zellij  Tab #1  + +$ │$ line1 000000000000000000000000000000000000000000000000000 + │line2 00000000000000000000000000000000000000000000000000000 + │line3 00000000000000000000000000000000000000000000000000000 + │line4 00000000000000000000000000000000000000000000000000000 + │line5 00000000000000000000000000000000000000000000000000000 + │line6 00000000000000000000000000000000000000000000000000000 + │line7 00000000000000000000000000000000000000000000000000000 + │line8 00000000000000000000000000000000000000000000000000000 + │line9 00000000000000000000000000000000000000000000000000000 + │line10 0000000000000000000000000000000000000000000000000000 + │line11 0000000000000000000000000000000000000000000000000000 + │line12 0000000000000000000000000000000000000000000000000000 + │line13 0000000000000000000000000000000000000000000000000000 + │line14 0000000000000000000000000000000000000000000000000000 + │line15 0000000000000000000000000000000000000000000000000000 + │line16 0000000000000000000000000000000000000000000000000000 + │line17 0000000000000000000000000000000000000000000000000000 + │line18 0000000000000000000000000000000000000000000000000000 + │line19 000000000000000000000000000000000000000000000000000█ + + Ctrl + LOCK 

PANE  TAB  RESIZE  SCROLL  SESSION  QUIT  + <↓↑> Scroll / Scroll Page / Select pane diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__split_terminals_vertically.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__split_terminals_vertically.snap new file mode 100644 index 000000000..bbb5b068a --- /dev/null +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__split_terminals_vertically.snap @@ -0,0 +1,29 @@ +--- +source: src/tests/integration/e2e.rs +expression: last_snapshot + +--- + Zellij  Tab #1  + +$ │$ █ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + + Ctrl + LOCK 

PANE  TAB  RESIZE  SCROLL  SESSION  QUIT  + Tip: Alt + n => open new pane. Alt + [] or hjkl => navigate between panes. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__starts_with_one_terminal.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__starts_with_one_terminal.snap new file mode 100644 index 000000000..32f6d26f2 --- /dev/null +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__starts_with_one_terminal.snap @@ -0,0 +1,29 @@ +--- +source: src/tests/integration/e2e.rs +expression: last_snapshot + +--- + Zellij  Tab #1  + +$ █ + + + + + + + + + + + + + + + + + + + + Ctrl + LOCK 

PANE  TAB  RESIZE  SCROLL  SESSION  QUIT  + Tip: Alt + n => open new pane. Alt + [] or hjkl => navigate between panes. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__toggle_pane_fullscreen.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__toggle_pane_fullscreen.snap new file mode 100644 index 000000000..98ddffc76 --- /dev/null +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__toggle_pane_fullscreen.snap @@ -0,0 +1,29 @@ +--- +source: src/tests/integration/e2e.rs +expression: last_snapshot + +--- +$ █ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 014246d11..bd5907791 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -1,3 +1,4 @@ +pub mod e2e; pub mod fakes; pub mod integration; pub mod possible_tty_inputs; diff --git a/src/tests/utils.rs b/src/tests/utils.rs index 6267c883a..2058f9df2 100644 --- a/src/tests/utils.rs +++ b/src/tests/utils.rs @@ -51,6 +51,7 @@ pub mod commands { pub const QUIT: [u8; 1] = [17]; // ctrl-q pub const ESC: [u8; 1] = [27]; pub const ENTER: [u8; 1] = [10]; // char '\n' + pub const LOCK_MODE: [u8; 1] = [7]; // ctrl-g pub const MOVE_FOCUS_LEFT_IN_NORMAL_MODE: [u8; 2] = [27, 104]; // alt-h pub const MOVE_FOCUS_RIGHT_IN_NORMAL_MODE: [u8; 2] = [27, 108]; // alt-l @@ -85,6 +86,9 @@ pub mod commands { pub const SWITCH_PREV_TAB_IN_TAB_MODE: [u8; 1] = [104]; // h pub const CLOSE_TAB_IN_TAB_MODE: [u8; 1] = [120]; // x + pub const SESSION_MODE: [u8; 1] = [15]; // ctrl-o + pub const DETACH_IN_SESSION_MODE: [u8; 1] = [100]; // d + pub const BRACKETED_PASTE_START: [u8; 6] = [27, 91, 50, 48, 48, 126]; // \u{1b}[200~ pub const BRACKETED_PASTE_END: [u8; 6] = [27, 91, 50, 48, 49, 126]; // \u{1b}[201 pub const SLEEP: [u8; 0] = [];