aboutsummaryrefslogtreecommitdiff
path: root/tmuxrdr/scripts/test-e2e
blob: 0adaebc44a82a2e922133479b3c6129df80b6c4d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
#!/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/binary    test another tmuxrdr build than the installed
#                           tmuxrdr, e.g. target/release/herdr
#   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:-tmuxrdr}"
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=""  # set once the binary's directory name is known, below

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
# The binary's own directory name -- tmuxrdr for a tmuxrdr build -- from the
# config path it reports; plugin state lives under the same name.
app="$(basename "$(dirname "$("$herdr" --help | sed -n 's/^Config: *//p')")")"
[[ "$app" == tmuxrdr ]] || die "$herdr keeps its files under '$app', not tmuxrdr: not a tmuxrdr build"
state="${XDG_STATE_HOME:-$HOME/.local/state}/$app/plugins/$plugin_id/$session"
"$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'; }
ws_count() { "${H[@]}" workspace list | jq -r '.result.workspaces | length'; }
prompt() { "${T[@]}" send-keys -t e2e C-b :; sleep "$delay"; type_line "$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"

diagnostics="$(HERDR_CONFIG_PATH="$cfg" "$herdr" config check 2>&1)"
check 'config check: no unknown keys or bad bindings' "$(head -n 1 <<<"$diagnostics")" "config: ok"
[[ "$diagnostics" == "config: ok" ]] || sed 's/^/      /' <<<"$diagnostics"

# --- 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
# ui.tab_style = "tmux": tmux's status line, the window before last flagged "-"
# and unnamed tabs called after their directory (/private/tmp).
check_seen 'tab row in tmux format: [session] #I:#W#F' '\[tmp\] 1:tmp- 2:tmp  3:tmp\*'
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

# tmux's session commands, on workspaces, from the : prompt.
prompt 'new-session -s e2e-new'; check ':new-session -s  creates and switches' "$(ws_label) $(ws_count)" "e2e-new 3"
prompt 'switch-client -t tmp';   check ':switch-client -t' "$(ws_label)" tmp
prompt 'new -A -s e2e-new';      check ':new -A  switches to the existing one' "$(ws_label) $(ws_count)" "e2e-new 3"
prompt 'switch-client -p';       check ':switch-client -p' "$(ws_label)" other
prompt 'new-session -s tmp';     check_seen ':new-session on a taken name' 'duplicate session: tmp'; k x
prompt 'ls';                     check_seen ':ls  lists the sessions' 'e2e-new: [0-9]+ windows'; k x
prompt 'kill-session -t e2e-new'; check ':kill-session -t' "$(ws_count)" 2
prompt 'switch-client -t tmp';   check ':switch-client -t 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' '\[other\] 1:tmp\*'
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 | grep -c -E '\[other\] 1:tmp\*' | 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 ]]