- makepad_test/runtime.rs: forward NIGIG_TEST_MODE from host env to the Android app via 'am start' intent extra; add wait_timeout (60s) used by wait_visible/wait_hidden/wait_count; make query_widgets tolerant of snapshot timeouts; grant READ_CONTACTS during adb setup - makepad-platform android_jni.rs: read makepad.NIGIG_TEST_MODE intent extra and surface it as the NIGIG_TEST_MODE env var via apply_studio_env - cargo_makepad compile.rs: support verbatim custom AndroidManifest.xml in addition to the templated variant - makepad-xr xr_root.rs: add ortho camera controls (ortho, ortho_height, min/max), derive Debug on XrCamera - docs: ANDROID.md and DESKTOP_VISIBLE.md for makepad_test
195 lines
8.6 KiB
Markdown
195 lines
8.6 KiB
Markdown
# makepad_test on Android
|
|
|
|
How `makepad_test` runs Makepad UI tests on real Android devices, and the
|
|
steps that were taken to get it working end-to-end. This is the Android
|
|
companion to [GUIDE.md](./GUIDE.md), which covers the desktop headless and
|
|
visible-Studio modes.
|
|
|
|
## What "Android mode" does
|
|
|
|
When `MAKEPAD_TEST_ANDROID=1` is set, the test runtime:
|
|
|
|
1. starts an in-process `StudioHub` that listens on `127.0.0.1:<port>` on the host
|
|
2. forwards that port to the device with `adb reverse tcp:<port> tcp:<port>`
|
|
3. builds the APK through `cargo-makepad` (`android build -p <package>`)
|
|
4. installs the APK with `adb install -r`
|
|
5. force-stops any previous instance of the app
|
|
6. launches the app with `am start`, passing the hub address, build id, and crate name as intent extras
|
|
7. waits for the app to connect to the hub (`AppStarted`)
|
|
8. settles until the app actually answers a request (see "Startup race fix")
|
|
9. drives the test through the normal `TestApp` / `Locator` / `Selector` APIs
|
|
|
|
The app dials `127.0.0.1:<port>` on the device; `adb reverse` maps that back
|
|
to the host listener, so no separate device-side network setup is needed.
|
|
|
|
## Two runtime modes
|
|
|
|
| Mode | Activity launched | Platform build | When |
|
|
|------|------------------|----------------|------|
|
|
| Legacy Java | `dev.makepad.<pkg>/.MakepadApp` | no `--cfg native_activity` | default |
|
|
| NativeActivity | `<pkg>/android.app.NativeActivity` | `--cfg native_activity` + `--native-activity` build flag | `MAKEPAD_TEST_NATIVE_ACTIVITY=1` |
|
|
|
|
- **Legacy Java** is the default. `cargo-makepad` generates a `MakepadApp`
|
|
Java `Activity` that bridges into `MakepadNative.activityOnCreate`.
|
|
- **NativeActivity** requires the flag on both sides: the platform crate must
|
|
be compiled with `--cfg native_activity` (so the `native_activity.rs`
|
|
module and the `ANativeActivity_onCreate` entry point are used instead of
|
|
the Java activity), and the APK must be built with
|
|
`cargo makepad android --native-activity build -p <pkg>`.
|
|
`build_android_apk` in `runtime.rs` adds the flag automatically when
|
|
`config.android_native_activity` is set.
|
|
|
|
The launch is wired up in `adb_launch` in `libs/makepad_test/src/runtime.rs`:
|
|
|
|
```text
|
|
am start -n <activity>
|
|
-e makepad.STUDIO_HOST 127.0.0.1:<port>
|
|
-e makepad.STUDIO_BUILD <build_id>
|
|
-e makepad.STUDIO_CRATE <package>
|
|
```
|
|
|
|
## Environment variables
|
|
|
|
| Variable | Purpose | Default |
|
|
|----------|---------|---------|
|
|
| `MAKEPAD_TEST_ANDROID` | enable Android mode (any truthy value) | unset (off) |
|
|
| `MAKEPAD_TEST_DEVICE` | adb device serial (`-s <serial>`) | unset (default adb device) |
|
|
| `MAKEPAD_TEST_ADB` | path to the adb binary | `adb` on `PATH` |
|
|
| `MAKEPAD_TEST_ANDROID_PORT` | host hub port / adb reverse port | `8001` |
|
|
| `MAKEPAD_TEST_NATIVE_ACTIVITY` | use NativeActivity instead of legacy Java | unset (legacy) |
|
|
| `MAKEPAD_WORKSPACE_ROOT` | workspace root holding `tools/cargo_makepad` | auto-detected by walking up from the manifest dir |
|
|
| `MAKEPAD_STUDIO_HUB_DEBUG` | print every hub child line (build + app output) | unset |
|
|
|
|
The hub binds `127.0.0.1:<port>` and may fall back to a different port if
|
|
`8001` is already taken (for example by a real Studio). `start_android_app`
|
|
reads the port the hub actually bound (`connection.studio_addr()`) and routes
|
|
`adb reverse` and the intent extras at that port, never a hardcoded one.
|
|
|
|
## Build system requirements
|
|
|
|
- Android builds use the **nightly** toolchain: the platform crate needs
|
|
`cargo +nightly` and the NDK target installed for `aarch64-linux-android`.
|
|
- The `cargo-makepad` binary must exist at `<workspace>/target/release/cargo-makepad`:
|
|
`cargo build --release -p cargo-makepad`.
|
|
- The APK lands at
|
|
`target/android/makepad-android-apk/<pkg_underscored>/apk/<pkg_underscored>.apk`.
|
|
- `tools/cargo_makepad/src/android/compile.rs` gained a `native_activity`
|
|
argument that flows into `rust_build` (alongside the profile-based
|
|
`prefer_dynamic` choice) and is plumbed through both APK build call sites.
|
|
|
|
Sanity-compile checks that must stay green:
|
|
|
|
```bash
|
|
# legacy Java cfg
|
|
cargo +nightly check -p makepad-platform --target aarch64-linux-android
|
|
|
|
# native-activity cfg
|
|
RUSTFLAGS="--cfg native_activity" cargo +nightly check -p makepad-platform --target aarch64-linux-android
|
|
```
|
|
|
|
## Device setup
|
|
|
|
```bash
|
|
export MAKEPAD_TEST_ADB="<repo>/tools/cargo_makepad/android_33_macos_x64/platform-tools/adb"
|
|
export MAKEPAD_TEST_DEVICE="RF8Y103NERA" # your device serial
|
|
$MAKEPAD_TEST_ADB devices # must list the device
|
|
$MAKEPAD_TEST_ADB -s "$MAKEPAD_TEST_DEVICE" wait-for-device
|
|
```
|
|
|
|
- The device must be **authorized** (accept the USB debugging prompt).
|
|
- Keep the screen on during the run:
|
|
`adb -s <serial> shell svc power stayon true`.
|
|
- On some devices a screen-off can still slow or starve the app; test with the
|
|
screen awake.
|
|
|
|
## Running the tests
|
|
|
|
Legacy Java mode (default):
|
|
|
|
```bash
|
|
MAKEPAD_TEST_ANDROID=1 \
|
|
MAKEPAD_TEST_DEVICE="RF8Y103NERA" \
|
|
MAKEPAD_TEST_ADB="<repo>/tools/cargo_makepad/android_33_macos_x64/platform-tools/adb" \
|
|
cargo test --release -p makepad-example-counter --test ui -- --test-threads=1
|
|
```
|
|
|
|
NativeActivity mode:
|
|
|
|
```bash
|
|
MAKEPAD_TEST_ANDROID=1 \
|
|
MAKEPAD_TEST_NATIVE_ACTIVITY=1 \
|
|
MAKEPAD_TEST_DEVICE="RF8Y103NERA" \
|
|
MAKEPAD_TEST_ADB="<repo>/tools/cargo_makepad/android_33_macos_x64/platform-tools/adb" \
|
|
cargo test --release -p makepad-example-counter --test ui -- --test-threads=1
|
|
```
|
|
|
|
`--test-threads=1` is required: each test owns the shared hub port and the
|
|
serialized app session.
|
|
|
|
Progress is printed to stderr with `[makepad-test] Android: ...` lines as each
|
|
phase completes (forward, build, install, force-stop, launch, connect,
|
|
responsive).
|
|
|
|
## Expected results
|
|
|
|
Recorded runs on the reconciled tree:
|
|
|
|
| Mode | Device | Result | Time |
|
|
|------|--------|--------|------|
|
|
| Native | RF8Y103NERA (SM-A165F) | 2 passed | 515.71s |
|
|
| Native | R28M52LJP2Y (SM-A6060), screen off | 2 passed | 179.65s |
|
|
| Legacy | R28M52LJP2Y (SM-A6060) | 2 passed | 361.76s |
|
|
| Legacy | RF8Y103NERA (SM-A165F) | 2 passed | 957.16s |
|
|
| Native | RF8Y103NERA (SM-A165F) | 2 passed | 520.70s |
|
|
|
|
The APK build dominates the wall time; app install and test execution are the
|
|
small remainder.
|
|
|
|
## The startup race fix (settle step)
|
|
|
|
**Symptom:** the app connected (`AppStarted`) but the first widget query or
|
|
click was lost, failing the test with a timeout on the first interaction.
|
|
|
|
**Root cause:** the websocket connects on a background thread before the app's
|
|
event loop is up. A cold start can answer the handshake well before it can
|
|
service hub requests. Legacy Java starts are the slowest: the first frame (and
|
|
with it the main loop that drains requests) only comes after the
|
|
`SurfaceView` surface materializes, which on a first launch after install can
|
|
exceed the per-request `ACTION_TIMEOUT` (10s).
|
|
|
|
**Fix:** after `wait_for_android_app_started`, call
|
|
`wait_for_android_app_responsive` (`runtime.rs:1754`). It repeatedly sends
|
|
`ClientToHub::WidgetTreeDump` and waits for a matching `WidgetTreeDump`
|
|
reply, with a per-attempt `ACTION_TIMEOUT` and an overall
|
|
`ANDROID_STARTUP_TIMEOUT` (120s) deadline. The test body's first query only
|
|
starts once the app has actually answered a request, closing the boot race.
|
|
|
|
## Debugging
|
|
|
|
Enable hub transport diagnostics, which echo every child stdout/stderr line
|
|
(APK build output, app logs, protocol messages):
|
|
|
|
```bash
|
|
MAKEPAD_STUDIO_HUB_DEBUG=1 ... cargo test --release -p makepad-example-counter --test ui -- --test-threads=1 --nocapture
|
|
```
|
|
|
|
Failure artifacts are written to `target/makepad_test/<package>/<test>/`
|
|
(`failure.txt`, `failure-screenshot.png`, logs), exactly like desktop mode.
|
|
|
|
## Troubleshooting
|
|
|
|
| Symptom | Likely cause / fix |
|
|
|---------|--------------------|
|
|
| `adb: error: device '<serial>' not found` | device disconnected; reconnect USB and `adb devices` |
|
|
| `device unauthorized` | accept the USB debugging prompt on the device |
|
|
| `timed out waiting for Android app to connect to hub` | wrong port / stale `adb reverse`; the hub binds a fallback port, so make sure adb and the app use the actually-bound port (already handled in code) |
|
|
| `timed out waiting for Android app to become responsive` | very slow cold start; raise `ANDROID_STARTUP_TIMEOUT`, keep the screen on, or rerun once warm |
|
|
| `cargo-makepad not found at ...` | `cargo build --release -p cargo-makepad` first |
|
|
| test fails only on the very first launch after install | known cold-start surface race; rerun warm, or run the legacy case twice |
|
|
|
|
## Current limitations
|
|
|
|
- one device at a time (`-s <serial>` targets a single device)
|
|
- one app session per test (serial suite with `--test-threads=1`)
|
|
- the APK build happens inside the test process, so the first test of a suite
|
|
is the slow one; subsequent tests reuse the built APK
|