#!/bin/bash # Starts the desktop app on a virtual display and runs the scripted UI cases with the ui helper: onboarding to the # chat list, a local group, then the chat list with the toolbar at the bottom (the default) and at the top. # Usage: scripts/ci/desktop-linux/smoke-test.sh [start|onboarding|layouts|setup|backend] # With "start" it stops at the first screen, for an agent to go on from; with "onboarding", at the chat list. # "setup" creates the group, makes the test SMP server (backend.sh) the one that resolves SimpleX names, and stops; # "backend" does the same, connects to the name that backend.sh registered, and goes on to both layouts. # Both need NAMES_SERVER, the test SMP server's address. # Each step saves a numbered screenshot and the text read from it to the output dir, prints the text to the log, # and adds a line to steps.log there. What the screens show is checked without stopping: every check is listed in # checks.txt and the job summary, and the test fails at the end if any failed. The app keeps running for later # steps of the job, which get UI_OUT. set -euo pipefail app_dir=$1 export UI_OUT=$2 cases=${3:-layouts} case "$cases" in start | onboarding | layouts | setup | backend) ;; *) echo "error: unknown case \"$cases\": start, onboarding, layouts (the default), setup or backend" exit 1 ;; esac # backend.sh registered seeded.testing for ci_contact name_label=seeded local_chat="seeded notes" ui=$(dirname "$0")/ui checks=$UI_OUT/checks.txt # switches are at the right of the left pane, where Settings open switch_x=335 mkdir -p "$UI_OUT" : > "$checks" if [ -n "${GITHUB_ENV:-}" ]; then echo "UI_OUT=$UI_OUT" >> "$GITHUB_ENV" fi report_checks() { [ -s "$checks" ] || return 0 echo "::group::checks" cat "$checks" echo "::endgroup::" if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then { echo "### Smoke test checks" echo sed 's/^/- /' "$checks" } >> "$GITHUB_STEP_SUMMARY" fi } finish() { local status=$? if [ "$status" -ne 0 ]; then "$ui" shot final || true fi echo "::group::app log, last 300 lines" tail -n 300 "$UI_OUT/app.log" || true echo "::endgroup::" report_checks exit "$status" } trap finish EXIT fail() { echo "error: $*" exit 1 } check() { local what=$1 shift if "$@"; then echo "PASS $what" | tee -a "$checks" else echo "FAIL $what" | tee -a "$checks" echo "::error::$what" fi } # text compared as ui compares it: letters and digits, lowercase, a lowercase l read as i normalise() { tr -cd '[:alnum:]' <<< "$1" | tr '[:upper:]' '[:lower:]' | tr l i } # Prints "x y" of the text in the chat list pane, left of x 390: a phrase equal to it, else, unless the second argument # is "exact", the first containing it. The chats, results and Settings are there; the right pane shows the open chat, # which can have the same names. left_pane_position() { local target line x y found="" target=$(normalise "$1") while IFS= read -r line; do x=${line%%,*} y=${line#*,} y=${y%% *} [ "$x" -lt 390 ] || continue case "$(normalise "${line#* }")" in "$target") echo "$x $y" return 0 ;; *"$target"*) found=${found:-"$x $y"} ;; esac done < <("$ui" text) [ -n "$found" ] && [ "${2:-}" != exact ] || return 1 echo "$found" } on_screen() { left_pane_position "$1" > /dev/null } not_on_screen() { ! left_pane_position "$1" > /dev/null } y_of() { local pos pos=$(left_pane_position "$1") || return 1 cut -d ' ' -f 2 <<< "$pos" } above() { local a b a=$(y_of "$1") || return 1 b=$(y_of "$2") || return 1 [ "$a" -lt "$b" ] } # the value is on the same line as its setting's title in_line_with() { local value title value=$(y_of "$1") || return 1 title=$(y_of "$2") || return 1 [ $((value - title)) -lt 10 ] && [ $((title - value)) -lt 10 ] } # Scrolls the left pane down until the text shows above its bottom toolbar. scroll_to() { local y for _ in 1 2 3 4 5 6 7 8; do if y=$(y_of "$1") && [ "$y" -lt 640 ]; then return 0 fi "$ui" scroll 195 400 down done return 1 } # Escape closes one screen at a time, and clears the search field back_to_chat_list() { for _ in 1 2 3 4 5 6; do on_chat_list && return 0 "$ui" key Escape done on_chat_list } # New chat has the same search field on_chat_list() { not_on_screen "Settings" && not_on_screen "Chat preferences" && not_on_screen "Network" && not_on_screen "New chat" && on_screen "Search or paste SimpleX link" } # Opens Settings from the user menu, behind the profile picture at the left of the bottom toolbar. open_settings() { "$ui" click 27 730 "$ui" click-text "Settings" 10 } # Turns "Reachable app toolbars" off in Settings > Appearance, which moves the toolbar and the search field to the top. use_top_toolbar() { local y "$ui" key Escape open_settings "$ui" click-text "Appearance" 10 "$ui" wait "Reachable app toolbars" 10 "$ui" shot appearance y=$(y_of "Reachable app toolbars") "$ui" click "$switch_x" "$y" "$ui" shot appearance-toolbar-switched back_to_chat_list || true "$ui" shot top-chat-list y=$(y_of "Search or paste SimpleX link") || fail "no search field after closing Settings" [ "$y" -lt 200 ] || fail "the search field is still at the bottom (y $y) after switching Reachable app toolbars" } # The search field and "Add list" next to it: at the bottom of the chat list, "Add list" above the field; at the top, below it. check_layout() { local layout=$1 field field=$(y_of "Search or paste SimpleX link") || field="" "$ui" shot "$layout-layout" check "$layout: \"$local_chat\" listed" on_screen "$local_chat" if [ "$layout" = bottom ]; then check "bottom: the search field is at the bottom" [ "${field:-0}" -gt 600 ] check "bottom: \"Add list\" above the search field" above "Add list" "Search or paste SimpleX link" else check "top: the search field is at the top" [ "${field:-900}" -lt 200 ] check "top: \"Add list\" below the search field" above "Search or paste SimpleX link" "Add list" fi } # A group of the user's own: without chats the chat list shows no search field. create_local_chat() { local y # the pencil at the right of the bottom toolbar opens New chat "$ui" click 363 730 "$ui" click-text "Create group" 10 "$ui" wait "Enter group name" 10 y=$(y_of "Enter group name") "$ui" click 195 $((y + 40)) "$ui" type "$local_chat" "$ui" shot new-group "$ui" click-text "Create group" 10 "$ui" click-text "Skip inviting members" 10 || true "$ui" shot group-created "$ui" key Escape back_to_chat_list || fail "no chat list after creating the group" # the group stays open in the right pane: Escape doesn't close a chat on desktop on_screen "$local_chat" || fail "\"$local_chat\" is not in the chat list" "$ui" shot local-chat } # Clicks a server role's value until its dropdown shows. The menu is open when an item reads exactly "yes", which no # role's value does ("default (yes)", "default (no)", "no"); it can take longer to show than OCR takes to read the screen. open_role_menu() { for _ in 1 2 3; do "$ui" click-text "$1" 10 || return 1 for _ in 1 2 3; do left_pane_position yes exact > /dev/null && return 0 sleep 1 done done return 1 } # Settings > Network & servers: the test SMP server added with the names role, and names off for the preset # operators, so that only the test server resolves names. use_names_server() { local x y operator open_settings # OCR reads "Network & servers" as two phrases "$ui" click-text "Network" 10 "$ui" click-text "Your servers" 10 "$ui" click-text "Add server" 10 "$ui" click-text "Enter server manually" 10 "$ui" wait "Your server address" 10 y=$(y_of "Your server address") "$ui" click 195 $((y + 50)) "$ui" type "$NAMES_SERVER" y=$(y_of "Use for new connections") "$ui" click "$switch_x" "$y" "$ui" shot new-server "$ui" key Escape # the New server screen shows the address with "127.0.0.1" too, until it closes "$ui" wait "Add server" 10 "$ui" wait "Test servers" 10 "$ui" click-text "127.0.0.1" 10 "$ui" wait "SMP server" 10 scroll_to "To resolve names" || fail "no \"To resolve names\" in the test server's settings" # A dropdown opens from its value; a custom server's roles default to yes, yes and no, so the first "default (yes)" # is the receiving one. A directory service can't reply to queues on this server, so it only resolves names. open_role_menu "default (yes)" || fail "no menu for the test server's \"To receive\"" # OCR misreads the short "no", the item after "yes" read -r x y < <(left_pane_position yes exact) || fail "the \"To receive\" menu closed" "$ui" click "$x" $((y + 48)) open_role_menu "default (no)" || fail "no menu for the test server's \"To resolve names\"" read -r x y < <(left_pane_position yes exact) || fail "the \"To resolve names\" menu closed" "$ui" click "$x" "$y" "$ui" shot names-server-roles # the first "default (yes)" and the first "yes" are on the lines after "To receive", so it is no in_line_with "default (yes)" "For private routing" || fail "the test server's \"To receive\" is still its default" in_line_with "yes" "To resolve names" || fail "the test server's \"To resolve names\" is not yes, or \"To receive\" is" "$ui" key Escape "$ui" wait "Add server" 10 "$ui" key Escape for operator in "SimpleX Chat" Flux; do "$ui" click-text "$operator" 10 # the operator's own toggle: the title " servers" can wrap "$ui" wait "Use servers" 10 scroll_to "To resolve names" || fail "no \"To resolve names\" in the $operator settings" y=$(y_of "To resolve names") "$ui" click "$switch_x" "$y" "$ui" shot "names-off-$(tr -cd 'a-zA-Z' <<< "$operator")" "$ui" key Escape "$ui" wait "Your servers" 10 done scroll_to "Save servers" || fail "no \"Save servers\" in Network & servers" "$ui" click-text "Save servers" 10 sleep 2 "$ui" shot servers-saved back_to_chat_list || fail "no chat list after saving the servers" } # A bare name in the search field is offered with the .testing domain; the test SMP server resolves it to ci_contact's # address, and the app shows that profile before connecting. connect_by_name() { "$ui" click-text "Search or paste SimpleX link" "$ui" type "$name_label" "$ui" wait "Connect to $name_label.testing" 15 "$ui" shot name-typed "$ui" click-text "Connect to $name_label.testing" check "$name_label.testing resolves to ci_contact" "$ui" wait ci_contact 60 check "the app offers to open a new chat with ci_contact" "$ui" wait "Open new chat" 10 "$ui" shot name-resolved "$ui" click-text Cancel 10 || "$ui" key Escape back_to_chat_list || fail "no chat list after the name's alert" } # private routing to unknown servers only: the preset servers that would proxy name lookups can't reach the test SMP # server on 127.0.0.1; developer tools at the debug level put the app's debug log in app.log "$ui" start "$app_dir" NetworkSMPProxyMode=Unknown DeveloperTools=true LogLevel=DEBUG # onboarding to the chat list "$ui" wait "Create your profile" 180 "$ui" shot first-screen if [ "$cases" = start ]; then echo "smoke test: the app shows its first screen" exit 0 fi "$ui" click-text "Create your profile" "$ui" wait "Create profile" 30 "$ui" click-text "Enter profile name" "$ui" type citester "$ui" shot profile-name "$ui" click-text "Create profile" # a new desktop database gets a random passphrase, which onboarding offers to replace "$ui" wait "Use random passphrase" 60 "$ui" shot database-passphrase "$ui" click-text "Use random passphrase" "$ui" wait "OK" 10 "$ui" shot random-passphrase-alert "$ui" click-text "OK" "$ui" wait "Continue" 60 "$ui" shot your-network "$ui" click-text "Continue" "$ui" wait "Accept" 30 "$ui" shot network-commitments "$ui" click-text "Accept" "$ui" wait "Talk to someone" 60 sleep 3 "$ui" shot chat-list if [ "$cases" = onboarding ]; then echo "smoke test passed: onboarding reached the chat list" exit 0 fi create_local_chat if [ "$cases" = setup ] || [ "$cases" = backend ]; then : "${NAMES_SERVER:?NAMES_SERVER must be the test SMP server address}" use_names_server if [ "$cases" = setup ]; then echo "smoke test setup done: \"$local_chat\" created, $NAMES_SERVER resolves names" exit 0 fi connect_by_name fi check_layout bottom use_top_toolbar check_layout top if grep -q '^FAIL' "$checks"; then fail "the smoke test failed checks: $(grep -c '^FAIL' "$checks") of $(grep -cE '^(PASS|FAIL)' "$checks")" fi echo "smoke test passed: onboarding reached the chat list, which shows \"$local_chat\" with the toolbar at the bottom and at the top"