diff options
| author | Igor Soarez <igor@soarez.org> | 2026-10-05 11:27:00 +0300 |
|---|---|---|
| committer | Igor Soarez <igor@soarez.org> | 2026-10-05 11:27:00 +0300 |
| commit | a58874ca96a87fbf6a86e8df7821f29749985b58 (patch) | |
| tree | 655b3f52bf768a2757a07dd52944dce1e4518453 /tmuxrdr/scripts/test-e2e | |
| parent | 48cd6744f077a3d59e61bbdcdb1e0bff8da7a540 (diff) | |
tmuxrdr: install, end-to-end test and upstream sync scripts
install links ~/.config/herdr/config.toml and the plugin to this checkout
and registers the plugin. test-e2e presses each binding in a herdr client
running in a throwaway tmux and session, and checks the result through the
herdr CLI. sync-upstream rebases the tmuxrdr commits onto a newer herdr
release, keeping the old branch as a backup.
Diffstat (limited to 'tmuxrdr/scripts/test-e2e')
| -rwxr-xr-x | tmuxrdr/scripts/test-e2e | 249 |
1 files changed, 249 insertions, 0 deletions
diff --git a/tmuxrdr/scripts/test-e2e b/tmuxrdr/scripts/test-e2e new file mode 100755 index 00000000..5913afa8 --- /dev/null +++ b/tmuxrdr/scripts/test-e2e @@ -0,0 +1,249 @@ +#!/bin/bash + +# tmuxrdr end-to-end test: the tmux bindings pressed in a real herdr client, +# each effect checked through the herdr CLI. Run after every rebase onto a new +# herdr (scripts/sync-upstream) and after changing the config or plugin. +# +# The client runs in a throwaway tmux server -- its own socket, no tmux.conf, +# tmux's prefix switched off so ctrl+b reaches herdr -- against a throwaway +# herdr session and a copy of tmuxrdr/config/config.toml. Your sessions, your +# tmux and the config file itself are left alone; everything is removed at the +# end. The plugin is global to herdr, so it must be installed first +# (scripts/install yes). The prefix+] check pastes the clipboard into cat in +# the throwaway pane -- printed, never run -- and is skipped when the +# clipboard holds no text. +# +# HERDR=path/to/herdr test another binary, e.g. a tmuxrdr build +# TMUXRDR_E2E_DELAY=1.2 seconds after each key (default 0.8); raise it on +# a loaded machine before suspecting a failure +# +# Exits non-zero when any check fails. + +set -uo pipefail + +repo="$(cd "$(dirname "$0")/../.." && pwd -P)" +herdr="${HERDR:-herdr}" +delay="${TMUXRDR_E2E_DELAY:-0.8}" +session="tmuxrdr-e2e-$$" +T=(tmux -L "tmuxrdr-e2e-$$" -f /dev/null) +H=("$herdr" --session "$session") +plugin_id="tmuxrdr.tmux-mode" +work="$(mktemp -d)" +cfg="$work/config.toml" +state="${XDG_STATE_HOME:-$HOME/.local/state}/herdr/plugins/$plugin_id/$session" + +pass=0 fail=0 skip=0 + +cleanup() { + "${T[@]}" kill-server 2>/dev/null + # tmux leaves its socket behind when the server ends with the session. + rm -f "${TMUX_TMPDIR:-/tmp}/tmux-$(id -u)/tmuxrdr-e2e-$$" + "$herdr" session stop "$session" >/dev/null 2>&1 + "$herdr" session delete "$session" >/dev/null 2>&1 + rm -rf "$work" "$state" +} +trap cleanup EXIT + +die() { >&2 echo "ERROR: $*"; exit 2; } + +for tool in tmux jq "$herdr"; do + command -v "$tool" >/dev/null || die "$tool not found" +done +"$herdr" plugin list --json 2>/dev/null \ + | jq -e --arg id "$plugin_id" --arg root "$repo/tmuxrdr/plugin" \ + '[.. | objects | select(.plugin_id? == $id and .plugin_root? == $root)] | length > 0' >/dev/null \ + || die "$plugin_id is not registered from this checkout; run tmuxrdr/scripts/install yes" +[[ -e "${XDG_DATA_HOME:-$HOME/.local/share}/tmuxrdr/plugin/bin" ]] \ + || die "no ~/.local/share/tmuxrdr/plugin; run tmuxrdr/scripts/install yes" + +cp "$repo/tmuxrdr/config/config.toml" "$cfg" + +# --- helpers ----------------------------------------------------------------- + +k() { "${T[@]}" send-keys -t e2e "$@"; sleep "$delay"; } +type_line() { "${T[@]}" send-keys -t e2e -l "$1"; "${T[@]}" send-keys -t e2e Enter; sleep "$delay"; } + +check() { + if [[ -n "$2" && "$2" == "$3" ]]; then + pass=$((pass + 1)); echo "PASS $1" + else + fail=$((fail + 1)); echo "FAIL $1 -- got '${2}', want '${3}'" + fi +} + +# check_seen NAME REGEX: something matching REGEX shows on screen within ~3 s. +check_seen() { + local i + for i in $(seq 12); do + if "${T[@]}" capture-pane -p -t e2e | grep -q -E -- "$2"; then + pass=$((pass + 1)); echo "PASS $1"; return + fi + sleep 0.25 + done + fail=$((fail + 1)); echo "FAIL $1 -- nothing on screen matching /$2/" +} + +skip() { skip=$((skip + 1)); echo "SKIP $1 -- $2"; } + +focus() { "${H[@]}" pane current | jq -r '.result.pane.pane_id'; } +order() { "${H[@]}" pane layout --pane "$(focus)" | jq -r '[.result.layout.panes[].pane_id] | join(" ")'; } +width() { "${H[@]}" pane layout --pane "$1" | jq -r --arg p "$1" '.result.layout.panes[] | select(.pane_id == $p) | .rect.width'; } +ws() { "${H[@]}" workspace list | jq -r '.result.workspaces[] | select(.focused) | .workspace_id'; } +ws_label() { "${H[@]}" workspace list | jq -r '.result.workspaces[] | select(.focused) | .label'; } +tab() { "${H[@]}" tab list --workspace "$(ws)" | jq -r '.result.tabs[] | select(.focused) | .tab_id'; } +tab_pos() { "${H[@]}" tab list --workspace "$(ws)" | jq -r '[.result.tabs[] | .focused] | index(true) + 1'; } +tab_count() { "${H[@]}" tab list --workspace "$(ws)" | jq -r '.result.tabs | length'; } +tab_panes() { "${H[@]}" pane list --workspace "$(ws)" | jq -r --arg t "${1:-$(tab)}" '[.result.panes[] | select(.tab_id == $t)] | length'; } + +# --- start ------------------------------------------------------------------- + +"${T[@]}" new-session -d -s e2e -x 160 -y 45 -c /tmp \ + "env -u HERDR_ENV HERDR_CONFIG_PATH='$cfg' '$herdr' --session '$session'" \; \ + set -g prefix None \; set -g prefix2 None \; set -g status off +for _ in $(seq 40); do + [[ "$("${H[@]}" workspace list 2>/dev/null | jq -r '.result.workspaces | length' 2>/dev/null)" == 1 ]] && break + sleep 0.25 +done +[[ "$(focus 2>/dev/null)" == w1:p1 ]] || die "herdr client did not come up" +echo "herdr $("$herdr" --version | awk '{print $2}'), session $session, config copy of tmuxrdr/config/config.toml" + +# --- panes ------------------------------------------------------------------- + +k C-b %; check 'prefix+% split side by side' "$(tab_panes)" 2 +k C-b '"'; check 'prefix+" split stacked' "$(tab_panes)" 3 +k C-b h; check 'prefix+h select-pane -L' "$(focus)" w1:p1 +k C-b l; check 'prefix+l select-pane -R' "$(focus)" w1:p2 +k C-b j; check 'prefix+j select-pane -D' "$(focus)" w1:p3 +k C-b k; check 'prefix+k select-pane -U' "$(focus)" w1:p2 +k C-b Down; check 'prefix+Down' "$(focus)" w1:p3 +k C-b Up; check 'prefix+Up' "$(focus)" w1:p2 +k C-b Left; check 'prefix+Left' "$(focus)" w1:p1 +k C-b Right; check 'prefix+Right' "$(focus)" w1:p2 +k C-b o; check 'prefix+o next pane' "$(focus)" w1:p3 +k C-b '\;'; check 'prefix+; last pane' "$(focus)" w1:p2 +k C-b '*'; check 'prefix+* new-pane' "$(tab_panes)" 4 +k C-b z; check 'prefix+z zoom' "$("${H[@]}" pane layout --pane "$(focus)" | jq -r '.result.layout.zoomed')" true +k C-b z + +# Expected orders are worked out from the order the splits left, a b c d in +# pane-index order with the first one focused -- tmux 3.7c's behaviour, as +# measured: swap-pane moves the current pane and keeps it current; +# rotate-window moves every pane and keeps focus on the position. +k C-b Left +read -r a b c d <<<"$(order)" +check 'first pane focused for the pane-order checks' "$(focus)" "$a" +k C-b '}'; check 'prefix+} swap-pane -D' "$(order) / $(focus)" "$b $a $c $d / $a" +k C-b '{'; check 'prefix+{ swap-pane -U' "$(order) / $(focus)" "$a $b $c $d / $a" +k C-b C-o; check 'prefix+C-o rotate-window, focus keeps the position' "$(order) / $(focus)" "$b $c $d $a / $b" +k C-b M-o; check 'prefix+M-o rotate-window -D' "$(order) / $(focus)" "$a $b $c $d / $a" +k C-b m; check 'prefix+m mark' "$(cat "$state/marked" 2>/dev/null)" "$a" +k C-b M; check 'prefix+M clear mark' "$([[ -e "$state/marked" ]] && echo marked || echo cleared)" cleared + +w0="$(width "$a")" +k C-b C-Right; w1="$(width "$a")" +check 'prefix+C-Right resize by about a cell' "$(( w1 - w0 >= 1 && w1 - w0 <= 2 ))" 1 +k C-b M-Right; w2="$(width "$a")" +check 'prefix+M-Right resize by about 5 cells' "$(( w2 - w1 >= 4 && w2 - w1 <= 9 ))" 1 +k C-b E +check 'prefix+E select-layout -E evens the columns' "$(( $(width "$a") - $(width "$b") <= 1 && $(width "$b") - $(width "$a") <= 1 ))" 1 + +# --- messages ---------------------------------------------------------------- + +"${T[@]}" send-keys -t e2e C-b i +check_seen 'prefix+i display-message, tmux format' '\[tmp\] 1:[^,]*, current pane 0 - \([0-9]{2}:[0-9]{2} [0-9]{2}-[A-Z][a-z]{2}-[0-9]{2}\)' +sleep 3 +"${T[@]}" send-keys -t e2e C-b 0 +check_seen "prefix+0 nothing at index 0" "can't find window: 0" +sleep 3 + +# --- windows ----------------------------------------------------------------- + +k C-b c; k C-b c; check 'prefix+c new-window' "$(tab_count)" 3 +k C-b p; check 'prefix+p previous-window' "$(tab_pos)" 2 +k C-b n; check 'prefix+n next-window' "$(tab_pos)" 3 +k C-b 1; check 'prefix+1 select-window' "$(tab_pos)" 1 +k C-b 3; check 'prefix+3 select-window' "$(tab_pos)" 3 +k C-b 1 +k C-b '!' +check 'prefix+! break-pane' "$(tab_count) $(tab_panes) $(tab_panes w1:t1)" "4 1 3" +"${T[@]}" send-keys -t e2e C-b "'"; check_seen "prefix+' index prompt" '\(index\)' +type_line 1; check "prefix+' select-window 1" "$(tab_pos)" 1 +"${T[@]}" send-keys -t e2e C-b .; check_seen 'prefix+. target prompt' '\(target\)' +type_line 2; check 'prefix+. move-window 2' "$(tab_pos)" 2 + +"${T[@]}" send-keys -t e2e C-b x; check_seen 'prefix+x asks' 'kill-pane [0-9]+\? \(y/n\)' +n0="$(tab_panes)" +k n; check 'prefix+x n keeps the pane' "$(tab_panes)" "$n0" +k C-b x; k y; check 'prefix+x y kills it' "$(tab_panes)" "$(( n0 - 1 ))" +"${T[@]}" send-keys -t e2e C-b '&'; check_seen 'prefix+& asks' 'kill-window [^ ]+\? \(y/n\)' +k n; check 'prefix+& n keeps the window' "$(tab_count)" 4 + +# --- sessions ---------------------------------------------------------------- + +"${H[@]}" workspace create --cwd /tmp --label other >/dev/null; sleep 0.5 +k C-b ')'; check 'prefix+) next session' "$(ws_label)" other +k C-b '('; check 'prefix+( previous session' "$(ws_label)" tmp +k C-b L; check 'prefix+L last session' "$(ws_label)" other +k C-b L; check 'prefix+L toggles back' "$(ws_label)" tmp + +# --- prompts, choosers, modes ------------------------------------------------ + +"${T[@]}" send-keys -t e2e C-b :; check_seen 'prefix+: command prompt' '│:' +type_line 'rename-window e2e'; check ':rename-window' "$("${H[@]}" tab get "$(tab)" | jq -r '.result.tab.label')" e2e +"${T[@]}" send-keys -t e2e C-b :; sleep "$delay"; type_line 'display-message tmuxrdr-e2e-message' +check_seen ':display-message' 'tmuxrdr-e2e-message' +for key in s w f q; do + "${T[@]}" send-keys -t e2e C-b "$key" + case "$key" in + s) check_seen 'prefix+s choose a session' 'NAVIGATE' ;; + *) check_seen "prefix+$key choose a window" 'search agents and terminals' ;; + esac + k Escape +done +"${T[@]}" send-keys -t e2e C-b '?'; check_seen 'prefix+? list keys' 'keybinds'; k Escape +"${T[@]}" send-keys -t e2e C-b /; check_seen 'prefix+/ describe keys' 'keybinds'; k Escape +"${T[@]}" send-keys -t e2e C-b '['; check_seen 'prefix+[ copy mode' 'COPY'; k q +"${T[@]}" send-keys -t e2e C-b t; check_seen 'prefix+t clock' '██'; k q +"${T[@]}" send-keys -t e2e C-b '~'; check_seen 'prefix+~ messages' 'INFO|WARN|DEBUG|ERROR'; k q +"${T[@]}" send-keys -t e2e C-b '$'; check_seen 'prefix+$ rename-session' 'rename workspace'; k Escape +"${T[@]}" send-keys -t e2e C-b ,; check_seen 'prefix+, rename-window' 'rename tab'; k Escape +"${T[@]}" send-keys -t e2e C-b C; check_seen 'prefix+C customize' 'settings'; k Escape + +# --- input ------------------------------------------------------------------- + +p="$(focus)" +"${H[@]}" pane run "$p" 'cat -v' >/dev/null; sleep "$delay" +k C-b C-b; k Enter +check 'prefix twice sends a literal C-b (send-prefix)' \ + "$("${H[@]}" pane read "$p" --source visible | grep -q '\^B' && echo yes)" yes +k C-c + +# Pasted into cat, which prints it and runs nothing, whatever the clipboard +# holds; the check looks for its last line, the one sure to be on screen. +line="$( (pbpaste 2>/dev/null || true) | grep -v '^[[:space:]]*$' | tail -n 1 | cut -c1-40)" +if [[ -n "$line" ]]; then + "${H[@]}" pane run "$p" 'cat' >/dev/null; sleep "$delay" + k C-b ']'; sleep 0.5 + check 'prefix+] paste-buffer' \ + "$("${H[@]}" pane read "$p" --source visible | grep -q -F -- "$line" && echo yes)" yes + k C-c +else + skip 'prefix+] paste-buffer' 'no text on the clipboard' +fi + +# --- reload, then detach ----------------------------------------------------- + +k C-b ')' # "other": a workspace with a single tab +check_seen 'tab row before reload' '│ *1 +' +sed -i.bak 's/^hide_tab_bar_when_single_tab = false/hide_tab_bar_when_single_tab = true/' "$cfg" +k C-b r +check 'prefix+r reload hides the single tab row' \ + "$("${T[@]}" capture-pane -p -t e2e | sed -n 1p | grep -c -E '│ *1 +' | tr -d ' ')" 0 + +k C-b d; sleep 1 +check 'prefix+d detach ends the client' "$("${T[@]}" has-session -t e2e 2>/dev/null && echo attached || echo detached)" detached +check 'the server keeps running' "$("${H[@]}" status server 2>/dev/null | awk 'NR == 1 {print $2}')" running + +echo +echo "$pass passed, $fail failed, $skip skipped" +[[ "$fail" -eq 0 ]] |
