ssh-workbench/docs/TESTING_ADB.md
jima 2a87fb58d1 100% Compose migration, AQB/CQB separation, password prompt, ADB test framework
Compose migration (4 files, 2059 lines of View code → Compose):
- ThemePickerDialog: Compose Canvas preview, LazyColumn theme list
- TerminalDialogs: 6 dead dialogs deleted, 2 migrated (hostKey, authPrompt)
  with TerminalDialogRequest sealed interface on MainViewModel for
  imperative→declarative bridge. Added PasswordDialog for no-stored-pw case.
- SnippetDialogs: full-screen Compose dialog with LazyColumn, search,
  inline create form, context menu
- KeyboardSettingsDialog: TabRow, Slider, Canvas preview, color picker.
  Data classes extracted to KeyboardModels.kt

AQB/CQB separation:
- Independent Quick Bar preferences for AKB vs CKB modes
- Settings UI mode-aware: CKB shows full keyboard dialog, AKB shows QB-only
- AQB positions filtered (no above/below keyboard)
- New docs: KEYBOARD.md, GLOSSARY.md (TV, AKB, CKB, AQB, CQB)

Password prompt:
- TerminalService prompts for password when no stored auth (SSHAuth.None)
- Compose PasswordDialog with remember checkbox
- clearPassword ADB broadcast for test cleanup

ADB test framework (Python):
- test.py runner with menu, --all, single test modes
- AI visual verification via claude -p reading screenshots
- 5 tests, 52 checks: connect, htop, vim, password prompt, multi-session
- Timestamped results with manifest.json for cross-run comparison

Coding conventions updated: 100% Compose mandated, no programmatic Views.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 15:00:35 +02:00

7.6 KiB

ADB Automated Test Framework

Last updated: 2026-04-03


Overview

Python-based test framework that drives the app via ADB on a Zebra TC21, captures screenshots, checks logs, and uses claude -p for AI-powered visual verification of each screenshot.

Structure

scripts/
├── test.py                      ← runner: menu, --all, or by number
├── test_lib/
│   └── common.py                ← TestRunner class with all helpers
├── tests/
│   ├── 01_connect_sshtest.py    ← SSH connect, echo, whoami, ls --color
│   ├── 02_htop.py               ← htop TUI: sort, tree, search, help
│   ├── 03_vim_edit.py           ← vim: create, edit, search, undo, save, :q!
│   ├── 04_password_prompt.py    ← no-password connect, type pw, save pw, auto-connect
│   └── 05_multi_session.py      ← multi-tab, SFTP, background/resume, close
└── test_results/
    └── 2026-04-03_HHMMSS/       ← timestamped per run
        ├── <test>_01_<label>.png
        ├── <test>_verdicts.txt
        ├── <test>_log.txt
        └── manifest.json         ← structured results for cross-run comparison

Usage

# Interactive menu
python3 scripts/test.py

# Run all tests
python3 scripts/test.py --all

# Run a single test by number or name prefix
python3 scripts/test.py 01
python3 scripts/test.py 03

Prerequisites

  • App installed on Zebra TC21 (pro debug APK)
  • Zebra connected via ADB (serial 22160523026079)
  • SSHTest profile saved with stored password (sshtest@duero)
  • SSHTest NoPw profile saved with blank password (for test 04)
  • claude CLI available in PATH (for AI verification)
  • htop installed on duero (for test 02)

TestRunner API (common.py)

App Control

Method Description
force_stop() Kill the app process
launch(*extras) Launch MainActivity with extras
launch_profile(name, clear_log=True) Launch with --es profile "name"

Terminal Input (via ADB broadcast)

Method Description
send_text(text) Type text into active terminal session
send_enter() Send Enter (0x0D)
send_esc(seq) Send ESC + sequence (e.g. "[D" for left arrow)
send_bytes(hex) Send raw hex bytes (e.g. "1b 62" for Alt+Left)
set_font_size(sp) Set terminal font size (6-34 sp)
clear_password(name) Delete saved password for a connection by name

Screenshots & Logs

Method Description
screenshot(label) Capture screenshot, save to results dir, return path
pull_log() Pull app debug log to results dir
read_log() Read current app debug log content
wait_for_log(pattern, timeout=15) Poll log for pattern (seconds)
log_contains(pattern) Check if pattern exists in log

Verification

Method Description
verify(desc, img_path, question) AI visual check — pipes screenshot to claude -p which reads the image and answers PASS/FAIL
assert_log(desc, pattern) Automated log pattern check — PASS/FAIL

Lifecycle

Method Description
begin(name) Start a named test
summary() Print results, save verdicts + manifest.json, return True if all passed

AI Verification

Each verify() call spawns a separate claude -p --no-session-persistence --allowedTools "Read" process. The prompt tells Claude to read the screenshot PNG and answer PASS or FAIL with a short reason.

t.verify("Terminal shows prompt", img,
    "Is this a connected SSH terminal showing a shell prompt?")

Output:

  [verify] Terminal shows prompt ... PASS

ADB Broadcast API

The app registers a broadcast receiver (debug builds only) at action com.roundingmobile.sshworkbench.INPUT:

Extra Type Description
text String Send text to active terminal
enter Boolean Send Enter (0x0D)
esc String Send ESC + string
bytes String Send raw hex bytes
fontsize String Set font size (e.g. "8.0")
clearPassword String Delete saved password for connection name
log Boolean Log cursor position
dump Boolean Dump full screen buffer to log

UI Interaction via ADB

For Compose dialogs and UI elements that can't be reached via broadcast (password dialog, menus, buttons), use adb shell input:

t.adb_shell("input text 'password'")     # type into focused field
t.adb_shell("input tap 550 840")          # tap at coordinates
t.adb_shell("input keyevent KEYCODE_BACK")  # dismiss keyboard/menu
t.adb_shell("input keyevent KEYCODE_HOME")  # background app
t.adb_shell("input keyevent KEYCODE_APP_SWITCH")  # open recents

Tab Bar Coordinates (720px wide, 2x density, 135dp tabs)

Target X Y
Tab 1 label 67 88
Tab 1 dots (⋮) 125 88
Tab 2 label 202 88
Tab 2 dots (⋮) 260 88
Tab 3 label 337 88
+ button 660 88

Overflow Menu Items (from tab ⋮)

Item Y
Duplicate 145
Connect via SFTP 240
Rename 335
Theme 430
Close 525

Test Descriptions

01_connect_sshtest (5 checks)

Connect to duero via SSHTest profile. Verify terminal prompt, echo command output, whoami, colored ls.

02_htop (8 checks)

Launch htop. Verify main screen, sort by memory (M), sort by CPU (P), tree view (t), search for sshd (/), help screen (h), clean quit (q).

03_vim_edit (11 checks)

Open vim, enter insert mode, type 7 lines, search (/FINDME), delete line (dd), undo (u), save (:wq), verify with cat + wc -l. Reopen, add line, force quit (:q!), verify unsaved changes discarded.

04_password_prompt (12 checks)

Clears any saved password first. Connect with blank-password profile → password dialog appears. Type password → Connect → terminal works. Exit → reconnect → prompted again (not saved). Type + check Remember → Connect. Exit → reconnect → auto-connects (saved). Cleanup: clears saved password at end.

05_multi_session (16 checks)

Open session 1, open session 2 via + button. Switch between tabs. Open SFTP via overflow menu. Switch back to terminal. Press Home → resume from recents. Verify both sessions survived. Close session 2 via overflow → Close. Verify remaining session works.

Results & Comparison

Each run saves to scripts/test_results/<timestamp>/ with:

  • Screenshots (PNG) for every verification step
  • <test>_verdicts.txt — PASS/FAIL verdicts with AI reasoning
  • <test>_log.txt — full app debug log
  • manifest.json — structured results for automated comparison
{
  "tests": [
    {
      "name": "connect_sshtest",
      "passed": 5, "failed": 0, "skipped": 0, "total": 5,
      "verdicts": ["Terminal shows shell prompt: PASS — ..."],
      "timestamp": "2026-04-03T12:16:47"
    }
  ],
  "summary": { "total_passed": 52, "total_failed": 0, "total_skipped": 0 }
}

To compare runs: diff the manifest.json files or visually compare screenshot pairs from different timestamps.

Tips

  • Stale log: Always delete the log file before launching (t.adb_shell(f"rm -f {t.LOG_PATH}")) to avoid wait_for_log matching old entries.
  • Font size: Set to 7sp early in each test for more visible content in screenshots.
  • Timing: Use wait_for_log("Connected visible=true") instead of fixed sleeps — it waits for the terminal pane to be visible AND connected.
  • Keyboard dismiss: After input text, the system keyboard pops up. Send KEYCODE_BACK before tapping buttons.
  • Test isolation: Each test should force_stop() at the start for a clean state.