Files
simplex-chat/scripts/ci/ios-sim/ui
T

238 lines
8.5 KiB
Bash
Executable File

#!/bin/bash
# Drives the app in the simulator SIM_UDID with AXe, for the scripted cases and for an agent on the runner.
# Screenshots and accessibility trees go to UI_OUT.
#
# ui tap <label> [secs] tap the element with this exact accessibility label, waiting up to secs (10) for it
# ui tap-value <text> [secs] tap the element with this exact value: text fields have no label, only their placeholder or text
# ui tap-xy <x> <y> tap this point (points, as in the tree)
# ui swipe <x1> <y1> <x2> <y2> swipe from one point to the other, e.g. to scroll a list
# ui type <text> type into the focused field (US keyboard characters only)
# ui clipboard <text> put the text on the clipboard, to paste it with the text field's Paste menu
# ui key <return|delete> [count] press a keyboard key: return submits the field (the Search key), delete erases a character
# ui wait <text> [secs] wait up to secs (30) until a label or value contains the text (case-sensitive)
# ui find <text> print the elements whose label or value contains the text (any case)
# ui shot <name> save UI_OUT/NN-<name>.png, a smaller NN-<name>-view.jpg and NN-<name>.json, and print the tree
# ui tree print the tree, one element per line: type, label, value, top-left corner and size in points,
# and "disabled" for a disabled element
# ui point <x> <y> print the element at this point, also when it belongs to a system alert that the tree misses
# ui layout <reachable|top> restart the app with the one-hand UI (reachable toolbar) or the top bar
# ui relaunch restart the app
# ui text-size <category> set Dynamic Type: medium (the default), large, extra-large, extra-extra-large,
# extra-extra-extra-large, accessibility-medium ... accessibility-extra-extra-extra-large
# ui directory <stop|start> stop or start the test directory (searches fail while it is stopped)
# ui listings the groups and channels listed in the test directory
# ui requests how many directory searches the app sent and how many were answered (needs APP_LOG)
set -euo pipefail
# nothing this helper runs needs the token of the Claude session that calls it
unset CLAUDE_CODE_OAUTH_TOKEN
: "${SIM_UDID:?SIM_UDID must be the simulator UDID}"
bundle_id=chat.simplex.app
app_group=group.chat.simplex.app
# the toolbar's new chat button: shown with and without conversations, unlike the search field
toolbar_button=Compose
# Elements below the screen, such as the keyboard's keys, are counted rather than listed; the screen is the first
# element's frame, or the given height.
compact_tree() {
jq -r --argjson screen "${1:-null}" '($screen // .[0].frame.height // 100000) as $h
| [.. | objects | select(.AXLabel or .AXValue)]
| (.[] | select((.frame.y // 0) < $h)
| "\(.type) \(.AXLabel | tojson) value=\(.AXValue | tojson) at \(.frame.x // 0 | round),\(.frame.y // 0 | round) size \(.frame.width // 0 | round)x\(.frame.height // 0 | round)\(if .enabled == false then " disabled" else "" end)"),
(map(select((.frame.y // 0) >= $h)) | length | select(. > 0) | "(\(.) elements below the screen not listed, such as keyboard keys)")'
}
# describe-ui failed once while a system alert was appearing, and read the tree a few seconds later (run 38895987656)
describe() {
local attempt
for attempt in 1 2 3 4; do
if axe describe-ui --udid "$SIM_UDID" "$@"; then
return
fi
sleep 2
done
axe describe-ui --udid "$SIM_UDID" "$@"
}
tree() {
describe | compact_tree
}
point() {
number "$1"
number "$2"
describe --point "$1,$2" | compact_tree 100000
}
wait_for() {
local text=$1 secs=${2:-30}
local deadline=$((SECONDS + secs)) t
until t=$(tree 2> /dev/null) && grep -qF -- "$text" <<< "$t"; do
if [ "$SECONDS" -ge "$deadline" ]; then
echo "error: \"$text\" not shown after $secs s"
return 1
fi
sleep 2
done
}
find_text() {
local t
t=$(tree)
if ! grep -iF -- "$1" <<< "$t"; then
echo "no element contains \"$1\""
return 1
fi
}
shot() {
: "${UI_OUT:?UI_OUT must be the output directory}"
if ! [[ "$1" =~ ^[A-Za-z0-9_-]+$ ]]; then
echo "error: a shot name has only letters, digits, - and _"
return 1
fi
mkdir -p "$UI_OUT"
local n f
n=$(find "$UI_OUT" -maxdepth 1 -name '*.png' | wc -l)
f=$(printf '%s/%02d-%s' "$UI_OUT" $((n + 1)) "$1")
xcrun simctl io "$SIM_UDID" screenshot "$f.png" > /dev/null
sips -Z 1000 -s format jpeg -s formatOptions 70 "$f.png" --out "$f-view.jpg" > /dev/null
describe > "$f.json"
echo "::group::accessibility tree: $1"
echo "saved $f.png, and $f-view.jpg to look at"
compact_tree < "$f.json"
echo "::endgroup::"
}
number() {
if ! [[ "$1" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
echo "error: \"$1\" is not a number"
exit 1
fi
}
key() {
local code count=${2:-1} i
case "$1" in
return) code=40 ;;
delete) code=42 ;;
*)
echo "error: key is return or delete"
return 1
;;
esac
number "$count"
for ((i = 0; i < count; i++)); do
axe key "$code" --udid "$SIM_UDID"
done
sleep 1
}
launch() {
xcrun simctl terminate "$SIM_UDID" "$bundle_id" 2> /dev/null || true
"$@"
xcrun simctl launch "$SIM_UDID" "$bundle_id" > /dev/null
wait_for "$toolbar_button" 90
}
toolbar_y() {
tree | grep -F "Button \"$toolbar_button\" " | sed -E 's/.* at [0-9-]+,([0-9-]+) .*/\1/' | head -n 1
}
# The preference is set in the app group's defaults while the app is not running; Settings shows the same switch.
layout() {
local one_hand
case "$1" in
reachable) one_hand=true ;;
top) one_hand=false ;;
*)
echo "error: layout is reachable or top"
return 1
;;
esac
local prefs
prefs=$(xcrun simctl get_app_container "$SIM_UDID" "$bundle_id" "$app_group")/Library/Preferences/$app_group
launch xcrun simctl spawn "$SIM_UDID" defaults write "$prefs" oneHandUI -bool "$one_hand"
local y
y=$(toolbar_y)
if { [ "$one_hand" = true ] && [ "$y" -lt 437 ]; } || { [ "$one_hand" = false ] && [ "$y" -ge 437 ]; }; then
echo "error: the toolbar is at y=$y, not where the $1 layout puts it"
return 1
fi
echo "layout $1: the toolbar is at y=$y"
}
text_size() {
case "$1" in
medium | large | extra-large | extra-extra-large | extra-extra-extra-large | accessibility-medium | accessibility-large | accessibility-extra-large | accessibility-extra-extra-large | accessibility-extra-extra-extra-large) ;;
*)
echo "error: unknown text size \"$1\""
return 1
;;
esac
xcrun simctl ui "$SIM_UDID" content_size "$1"
sleep 2
}
backend() {
: "${BACKEND_DIR:?BACKEND_DIR must be the work dir of the test backend}"
"$(dirname "$0")/backend.sh" "$@" "$BACKEND_DIR"
}
directory() {
case "$1" in
stop) backend directory-stop ;;
start) backend directory-start ;;
*)
echo "error: directory stop or directory start"
return 1
;;
esac
echo "test directory: $1"
}
requests() {
: "${APP_LOG:?APP_LOG must be the app log}"
local sent answered results
sent=$(grep -c 'chatSendCmd apiSendServiceRequest$' "$APP_LOG" || true)
results=$(grep -c 'chatSendCmd apiSendServiceRequest: ' "$APP_LOG" || true)
answered=$(grep -c 'chatSendCmd apiSendServiceRequest: serviceResponse' "$APP_LOG" || true)
echo "directory searches sent: $sent, answered: $answered, failed: $((results - answered)), waiting: $((sent - results))"
}
case "${1:-}" in
tap) axe tap --label "$2" --wait-timeout "${3:-10}" --post-delay 2 --udid "$SIM_UDID" ;;
tap-value) axe tap --value "$2" --wait-timeout "${3:-10}" --post-delay 2 --udid "$SIM_UDID" ;;
tap-xy)
number "$2"
number "$3"
axe tap -x "$2" -y "$3" --post-delay 2 --udid "$SIM_UDID"
;;
swipe)
for n in "$2" "$3" "$4" "$5"; do number "$n"; done
axe swipe --start-x "$2" --start-y "$3" --end-x "$4" --end-y "$5" --duration 0.5 --post-delay 1 --udid "$SIM_UDID"
;;
type) axe type "$2" --udid "$SIM_UDID" ;;
clipboard) printf '%s' "$2" | xcrun simctl pbcopy "$SIM_UDID" ;;
key) key "$2" "${3:-1}" ;;
wait) wait_for "$2" "${3:-30}" ;;
find) find_text "$2" ;;
shot) shot "$2" ;;
tree) tree ;;
point) point "$2" "$3" ;;
layout) layout "$2" ;;
relaunch) launch true ;;
text-size) text_size "$2" ;;
directory) directory "$2" ;;
listings) backend listings ;;
requests) requests ;;
*)
sed -n '3,26s/^# \{0,1\}//p' "$0"
exit 1
;;
esac