Add e2e tests (#582)

* feature(tests): e2e tests

* chore(build): github action

* chore(build): fix workflow

* chore(build): fix workflow

* work

* work

* work

* work

* work

* work

* work

* work

* work

* work

* work

* work

* work

* work

* work

* work

* work

* work

* work

* work

* work

* work

* work

* work

* work

* work

* work

* work

* work

* work

* work

* work

* work

* work

* work

* work

* work

* work

* work

* work

* work

* work

* work

* work

* work

* work

* work

* work

* work

* work

* working

* working

* working

* bring back the proper errors

* make e2e flow run properly

* style(fmt): make rustfmt happy

* style(fmt): make rustfmt happy

* run on everything just to test the workflow

* bring back running behaviour on workflow
This commit is contained in:
Aram Drevekenin 2021-06-21 10:45:18 +02:00 committed by GitHub
parent 07ad2f54ea
commit 3313634fe9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1576 additions and 10 deletions

49
.github/workflows/e2e.yml vendored Normal file
View File

@ -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

View File

@ -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

132
Cargo.lock generated
View File

@ -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",

View File

@ -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"] }

View File

@ -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"]

21
docker-compose.yml Normal file
View File

@ -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

716
src/tests/e2e/cases.rs Normal file
View File

@ -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);
}

2
src/tests/e2e/mod.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod cases;
mod remote_runner;

View File

@ -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<Step>,
current_step_index: usize,
vte_parser: vte::Parser,
terminal_output: TerminalPane,
channel: ssh2::Channel,
session_name: Option<String>,
test_name: &'static str,
currently_running_step: Option<String>,
retries_left: usize,
win_size: PositionAndSize,
}
impl RemoteRunner {
pub fn new(
test_name: &'static str,
win_size: PositionAndSize,
session_name: Option<String>,
) -> 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<Step>) {
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();
}
}

View File

@ -0,0 +1,25 @@
---
source: src/tests/integration/e2e.rs
expression: last_snapshot
---
$ Hi!█
Ctrl +

View File

@ -0,0 +1,29 @@
---
source: src/tests/integration/e2e.rs
expression: last_snapshot
---
Zellij  Tab #1 
$ █
Ctrl + <g> LOCK  <p> PANE  <t> TAB  <r> RESIZE  <s> SCROLL  <o> SESSION  <q> QUIT 
Tip: Alt + n => open new pane. Alt + [] or hjkl => navigate between panes.

View File

@ -0,0 +1,29 @@
---
source: src/tests/integration/e2e.rs
expression: last_snapshot
---
Zellij  Tab #1 
$ │$ █
Ctrl + <g> LOCK  <p> PANE  <t> TAB  <r> RESIZE  <s> SCROLL  <o> SESSION  <q> QUIT 
Tip: Alt + n => open new pane. Alt + [] or hjkl => navigate between panes.

View File

@ -0,0 +1,29 @@
---
source: src/tests/integration/e2e.rs
expression: last_snapshot
---
Zellij  Tab #1 
$ │$ I am some text█
Ctrl + <g> LOCK  <p> PANE  <t> TAB  <r> RESIZE  <s> SCROLL  <o> SESSION  <q> QUIT 
Tip: Alt + n => open new pane. Alt + [] or hjkl => navigate between panes.

View File

@ -0,0 +1,29 @@
---
source: src/tests/integration/e2e.rs
expression: last_snapshot
---
Zellij  Tab #1 
$ nabc█
Ctrl + <g> LOCK  <p> PANE  <t> TAB  <r> RESIZE  <s> SCROLL  <o> SESSION  <q> QUIT 
-- INTERFACE LOCKED --

View File

@ -0,0 +1,29 @@
---
source: src/tests/integration/e2e.rs
expression: last_snapshot
---
Zellij  Tab #1  Tab #2 
$ █
Ctrl + <g> LOCK  <p> PANE  <t> TAB  <r> RESIZE  <s> SCROLL  <o> SESSION  <q> QUIT 
Tip: Alt + n => open new pane. Alt + [] or hjkl => navigate between panes.

View File

@ -0,0 +1,29 @@
---
source: src/tests/integration/e2e.rs
expression: last_snapshot
---
Zellij  Tab #1 
$ │$ █
Ctrl + <g> LOCK  <p> PANE  <t> TAB  <r> RESIZE  <s> SCROLL  <o> SESSION  <q> QUIT 
Tip: Alt + n => open new pane. Alt + [] or hjkl => navigate between panes.

View File

@ -0,0 +1,29 @@
---
source: src/tests/integration/e2e.rs
expression: last_snapshot
---
Zellij  Tab #1 
$ │$ █
Ctrl + <g> LOCK  <p> PANE  <t> TAB  <r> RESIZE  <s> SCROLL  <o> SESSION  <q> QUIT 
Tip: Alt + n => open new pane. Alt + [] or hjkl => navigate between panes.

View File

@ -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 + <g> LOCK  <p> PANE  <t> TAB  <r> RESIZE  <s> SCROLL  <o> SESSION  <q> QUIT 
<↓↑> Scroll / <PgUp/PgDn> Scroll Page / <ENTER> Select pane

View File

@ -0,0 +1,29 @@
---
source: src/tests/integration/e2e.rs
expression: last_snapshot
---
Zellij  Tab #1 
$ │$ █
Ctrl + <g> LOCK  <p> PANE  <t> TAB  <r> RESIZE  <s> SCROLL  <o> SESSION  <q> QUIT 
Tip: Alt + n => open new pane. Alt + [] or hjkl => navigate between panes.

View File

@ -0,0 +1,29 @@
---
source: src/tests/integration/e2e.rs
expression: last_snapshot
---
Zellij  Tab #1 
$ █
Ctrl + <g> LOCK  <p> PANE  <t> TAB  <r> RESIZE  <s> SCROLL  <o> SESSION  <q> QUIT 
Tip: Alt + n => open new pane. Alt + [] or hjkl => navigate between panes.

View File

@ -0,0 +1,29 @@
---
source: src/tests/integration/e2e.rs
expression: last_snapshot
---
$ █

View File

@ -1,3 +1,4 @@
pub mod e2e;
pub mod fakes;
pub mod integration;
pub mod possible_tty_inputs;

View File

@ -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] = [];