#!/usr/bin/env bash # Phone is the continuity tab of the Home category -- the half of the old Home # & Phone page that did not go to My Home. Three things carried over or arrived # with it, and each has already been got wrong once: # # the vitals strip reads plugin data that only exists while a phone is paired # and nearby. A missing battery or signal reading is the ordinary case, not an # error, so it renders as an em-dash # # the Reach it buttons mirror Control Center exactly: nearby, not mid-transfer, # and the plugin present. Dropping any one of those three offers an action # that silently does nothing # # Messages is not a KDE Connect action. It needs BlueBubbles installed and # nothing else, and it must stay that way -- coupling it to phone reachability # is the regression phone-messages-contract was written for, and this page # inherited the same row # # The live half boots an isolated shell with a fake Flatpak to prove the # installed-state probe runs and that nothing here launches the app. set -euo pipefail repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" settings_dir="$repo_dir/config/dot/quickshell/modules/settings" phone_page="$settings_dir/PhonePage.qml" settings_qmldir="$settings_dir/qmldir" system_settings="$repo_dir/config/dot/quickshell/services/SystemSettings.qml" fail() { printf 'phone page contract: %s\n' "$1" >&2 exit 1 } assert_contains() { local needle="$1" local file="$2" rg -Fq "$needle" "$file" || fail "$file is missing: $needle" } [[ -f "$phone_page" ]] || fail 'PhonePage.qml is missing' assert_contains 'SettingsPage {' "$phone_page" assert_contains 'objectName: "phone-page"' "$phone_page" assert_contains 'title: "Phone"' "$phone_page" assert_contains 'PhonePage 1.0 PhonePage.qml' "$settings_qmldir" if rg -q '^\s*Flickable \{' "$phone_page"; then fail 'PhonePage.qml still owns a copied Flickable scaffold' fi # ── Vitals ─────────────────────────────────────────────────────────────────── # KdeConnect.phoneBattery and phoneSignal are null whenever the plugin objects # are absent, which is most of the time on an unpaired or away phone. vitals_block="$(sed -n '/id: vitals/,/── Reach it/p' "$phone_page")" [[ -n "$vitals_block" ]] || fail 'PhonePage.qml has no vitals strip' rg -Fq 'KdeConnect.phoneBattery' <<<"$vitals_block" \ || fail 'the vitals strip does not read the phone battery' rg -Fq 'KdeConnect.phoneSignal' <<<"$vitals_block" \ || fail 'the vitals strip does not read the phone signal' [[ "$(rg -Fc -- '"—"' <<<"$vitals_block")" -ge 2 ]] \ || fail 'a null battery or signal does not render as an em-dash' # ── Reach it ───────────────────────────────────────────────────────────────── assert_contains 'readonly property bool actionsReady: KdeConnect.phoneReachable && !KdeConnect.transferActive' \ "$phone_page" for action in ring clipboard share; do rg -Fq "enabled: root.actionsReady && KdeConnect.supports(\"$action\")" "$phone_page" \ || fail "the $action action does not gate on nearby, idle, and plugin support together" done assert_contains 'KdeConnect.ring()' "$phone_page" assert_contains 'KdeConnect.sendClipboard()' "$phone_page" assert_contains 'KdeConnect.sendFile(path)' "$phone_page" assert_contains 'KdeConnect.cancelTransfer()' "$phone_page" # ── Messages ───────────────────────────────────────────────────────────────── assert_contains 'Opens BlueBubbles' "$phone_page" assert_contains 'SystemSettings.openApplication("bluebubbles")' "$phone_page" messages_card="$(sed -n '/title: "Messages"/,/title: "Device"/p' "$phone_page")" [[ -n "$messages_card" ]] || fail 'PhonePage.qml has no Messages card' rg -Fq 'SystemSettings.bluebubblesAvailable' <<<"$messages_card" \ || fail 'Messages enablement does not read BlueBubbles availability' if rg -Fq 'KdeConnect.' <<<"$messages_card"; then fail 'Messages is coupled to KDE Connect' fi assert_contains 'readonly property bool bluebubblesAvailable: root.bluebubblesDetected' "$system_settings" assert_contains 'command: ["flatpak", "info", "app.bluebubbles.BlueBubbles"]' "$system_settings" assert_contains '"bluebubbles": ["flatpak", "run", "app.bluebubbles.BlueBubbles"]' "$system_settings" # ── The live half ──────────────────────────────────────────────────────────── state_home="$(mktemp -d /tmp/panama-phone-page-state.XXXXXX)" config_path="$state_home/quickshell" test_bin="$state_home/bin" shell_log="$state_home/quickshell.log" flatpak_log="$state_home/flatpak.log" cleanup_bootstrap() { rm -rf "$state_home" } trap cleanup_bootstrap EXIT mkdir -p "$test_bin" cp -a "$repo_dir/config/dot/quickshell" "$config_path" : >"$flatpak_log" cat >"$config_path/scripts/panama-home-assistant" <<'EOF' #!/usr/bin/env bash set -euo pipefail case "${1:-}" in catalog|areas) printf '%s\n' '{"ok":false,"error":"test-helper"}' ;; esac EOF chmod +x "$config_path/scripts/panama-home-assistant" cat >"$test_bin/hyprctl" <<'EOF' #!/usr/bin/env bash set -euo pipefail if [[ "${1:-}" == "-j" && "${2:-}" == "monitors" ]]; then printf '%s\n' '[{"focused":true,"name":"TEST-1","description":"Phone contract","width":1920,"height":1080,"refreshRate":60,"scale":1,"currentFormat":"XRGB8888","colorManagementPreset":"srgb","vrr":false}]' exit 0 fi if [[ "${1:-}" == "keyword" ]]; then exit 0 fi exec /usr/sbin/hyprctl "$@" EOF chmod +x "$test_bin/hyprctl" cat >"$test_bin/flatpak" <<'EOF' #!/usr/bin/env bash set -euo pipefail printf '%s\n' "$*" >>"$PANAMA_FLATPAK_LOG" if [[ "${1:-}" == "info" && "${2:-}" == "app.bluebubbles.BlueBubbles" ]]; then exit 0 fi exit 97 EOF chmod +x "$test_bin/flatpak" qs_for_test() { PATH="$test_bin:$PATH" QS_CONFIG_PATH="$config_path" XDG_STATE_HOME="$state_home" \ PANAMA_FLATPAK_LOG="$flatpak_log" qs -p "$config_path" "$@" } stop_test_shell() { qs_for_test kill >/dev/null 2>&1 || true for _ in $(seq 1 80); do if ! qs_for_test list 2>/dev/null | rg '^Instance ' >/dev/null \ && ! qs_for_test ipc show >/dev/null 2>&1; then return 0 fi sleep 0.1 done return 1 } cleanup() { qs_for_test ipc call settings close >/dev/null 2>&1 || true if stop_test_shell; then rm -rf "$state_home" else printf 'phone page contract: branch shell did not stop; retained %s\n' "$state_home" >&2 fi } trap cleanup EXIT start_test_shell() { stop_test_shell || fail 'pre-existing branch shell did not stop cleanly' for _attempt in 1 2; do qs_for_test --daemonize >"$shell_log" 2>&1 for _ in $(seq 1 80); do if qs_for_test ipc show 2>/dev/null | rg '^target settings-system$' >/dev/null; then return fi sleep 0.1 done stop_test_shell || fail 'failed branch-shell attempt did not stop cleanly' done sed -n '1,240p' "$shell_log" >&2 fail 'isolated branch shell did not start' } start_test_shell qs_for_test ipc call settings page phone >/dev/null for _ in $(seq 1 40); do [[ "$(qs_for_test ipc call settings status | jq -r .page)" == "phone" ]] && break sleep 0.1 done status="$(qs_for_test ipc call settings status | jq -c .)" jq -e '.open == true and .page == "phone"' <<<"$status" >/dev/null \ || fail "the Phone tab did not render: $status" system_status="$(qs_for_test ipc call settings-system status | jq -c .)" jq -e '.bluebubblesAvailable == true' <<<"$system_status" >/dev/null \ || fail "BlueBubbles availability was not exposed: $system_status" shell_pid="$(qs_for_test list | awk '/Process ID:/ { print $3; exit }')" [[ "$shell_pid" =~ ^[0-9]+$ ]] || fail 'could not identify the branch shell process' for _ in $(seq 1 40); do if /usr/sbin/hyprctl -j clients | jq -e --argjson pid "$shell_pid" \ '[.[] | select(.pid == $pid and .title == "Settings" and .floating == false)] | length == 1' >/dev/null; then break fi sleep 0.1 done /usr/sbin/hyprctl -j clients | jq -e --argjson pid "$shell_pid" \ '[.[] | select(.pid == $pid and .title == "Settings" and .floating == false)] | length == 1' >/dev/null \ || fail 'the branch shell did not own exactly one tiled Panama Settings client' if [[ -n "${PANAMA_TEST_SCREENSHOT_PATH:-}" ]]; then geometry="$(/usr/sbin/hyprctl -j clients | jq -r --argjson pid "$shell_pid" \ '.[] | select(.pid == $pid and .title == "Settings") | "\(.at[0]),\(.at[1]) \(.size[0])x\(.size[1])"')" [[ -n "$geometry" ]] || fail 'could not resolve the Settings client geometry' grim -g "$geometry" "$PANAMA_TEST_SCREENSHOT_PATH" fi rg -Fxq 'info app.bluebubbles.BlueBubbles' "$flatpak_log" \ || fail 'the fixed BlueBubbles availability probe did not run' if rg -q '^run ' "$flatpak_log"; then fail 'the contract started BlueBubbles' fi trap - EXIT cleanup [[ ! -e "$state_home" ]] || fail 'temporary Phone state was not removed after shell exit' printf 'phone page contract: PASS\n'