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>
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) SSHTestprofile saved with stored password (sshtest@duero)SSHTest NoPwprofile saved with blank password (for test 04)claudeCLI available in PATH (for AI verification)htopinstalled 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 logmanifest.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 avoidwait_for_logmatching 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. SendKEYCODE_BACKbefore tapping buttons. - Test isolation: Each test should
force_stop()at the start for a clean state.