Platform support
Android is the fullest surface. iOS reaches parity for the loop that matters — inspect, act, assert — and where it cannot, it says so: a named error and an exit code, never a command that quietly does nothing.
This page is the canonical matrix. Other pages link here rather than restating it.
How to read the tables
Section titled “How to read the tables”| Meaning | |
|---|---|
| ✅ | Works. |
| ⚠️ | Works, with a caveat that changes how you write the test. |
| ⊘ | Accepted and exits 0, but does nothing, or answers with a placeholder. The intent is already satisfied, or there is nothing to report. |
| ❌ | Refused with a named reason and a non-zero exit — 3 for an environment/capability gap, 2 for a usage error. Never a silent no-op. |
A ⊘ is only used where the intent is already true (a simulator never sleeps, so stay-awake
has nothing to do). Everything else refuses rather than reporting a success that did not happen.
The two Android columns differ on exactly one row: a phone and an emulator run the same code. What varies on Android is the device, and three differences bite in practice:
- OEM skins silently ignore
settings put/svc/cmd. Every device-state write is therefore verified by reading it back. - Effective font scale depends on the API level.
font-scale=1.3lands at1.30on API 31 and about1.26on API 34, which scales non-linearly. Assert that a scale grew and was restored, never that it equals a literal. - Some skins break individual commands. Known cases are noted beside the command below.
Commands
Section titled “Commands”| Command | Android | iOS | ||
|---|---|---|---|---|
| Physical | Emulator | Simulator | Physical | |
| Inspect | ||||
ui / dump | ✅ | ✅ | ⚠️ --tree renders flat | ⚠️ --tree renders flat |
find | ✅ | ✅ | ✅ | ✅ |
assert | ✅ | ✅ | ✅ | ✅ |
wait | ✅ | ✅ | ✅ | ✅ |
current | ✅ | ✅ | ⊘ always (unknown) | ⊘ always (unknown) |
log / logs | ✅ logcat | ✅ | ✅ via log show | ❌ 3 |
| Act | ||||
tap / click | ✅ | ✅ | ✅ | ✅ |
text | ✅ | ✅ | ⚠️ --clear unreliable | ⚠️ --clear unreliable |
type | ✅ | ✅ | ✅ | ✅ |
key | ✅ | ✅ | ⚠️ different key set | ⚠️ different key set |
back | ✅ | ✅ | ❌ 2 — no hardware Back | ❌ 2 — no hardware Back |
home / enter | ✅ | ✅ | ✅ | ✅ |
swipe / scroll | ✅ | ✅ | ⚠️ --duration ignored | ⚠️ --duration ignored |
screenshot / shot | ✅ | ✅ | ✅ | ✅ |
launch / open | ✅ | ✅ | ⚠️ --clear exits 3 | ⚠️ --clear exits 3 |
stop | ✅ | ✅ | ✅ | ✅ |
clear | ✅ pm clear | ✅ | ❌ 3 — no per-app reset | ❌ 3 — no per-app reset |
install | ✅ .apk — replaces a differently-signed build, allows a downgrade | ✅ .apk — replaces a differently-signed build, allows a downgrade | ⚠️ .ipa or .app — no replace | ⚠️ .ipa or .app — no replace |
| Device state | ||||
device set | ✅ all eight keys | ✅ all eight keys | ⚠️ four of eight | ❌ 3 — none |
device get | ✅ | ✅ | ⚠️ four of eight | ⊘ n/a for every key |
device reset | ✅ | ✅ | ⚠️ restores what it could read | ⊘ nothing was captured |
device prep | ⚠️ needs an explicit --device | ✅ | ⊘ every knob is a no-op or unsupported | ⊘ same |
device caps | ✅ | ✅ | ✅ | ⚠️ reports the simulator table |
device release | ✅ | ✅ | ✅ | ✅ |
| Run a test | ||||
batch | ✅ | ✅ | ✅ | ✅ |
ai | ✅ | ✅ | ⚠️ --reset-app force-stops only | ⚠️ --reset-app force-stops only |
suite | ✅ | ✅ | ⚠️ --app force-stops only | ⚠️ --app force-stops only |
suite --devices / --servers (parallel) | ⚠️ all prefers emulators; name a serial to pool a phone | ✅ | ⚠️ all prefers simulators; name a UDID to pool a phone | ✅ |
server --devices (device pool) | ⚠️ all prefers emulators; name a serial to pool a phone | ✅ | ⚠️ all prefers simulators; name a UDID to pool a phone | ✅ |
run (start/status/archive/clear) | ✅ | ✅ | ✅ | ⚠️ archive carries no device log |
| Environment | ||||
devices | ✅ | ✅ | ✅ | ✅ |
devices start / stop / restart | ❌ 2 — never power-cycled | ✅ via emulator / adb emu kill | ✅ via simctl | ❌ 2 — never power-cycled |
devices start|restart --wipe | ❌ 2 | ✅ -wipe-data | ✅ simctl erase | ❌ 2 |
doctor | ✅ | ✅ | ⚠️ --fix is Android-only | ⚠️ --fix is Android-only |
companion | ✅ | ✅ | ❌ Android-only | ❌ Android-only |
server | ✅ | ✅ | ✅ | ✅ |
help / version | ✅ | ✅ | ✅ | ✅ |
Notes on the rows that carry a caveat:
--treerenders flat on iOS.idb’s accessibility list carries no nesting depth.uiandfindare unaffected; only the indentation is lost.text --clearis Android in practice. It sizes the deletion from the element’stext, and on iOS that is the accessibility label, not the field’s contents, so the deletion is the wrong length. There is also no way to read back what was typed.keycovers a different set per platform. Android-only:back,menu,search,center,app_switch/recents,volume_up,volume_down,mute. iOS-only:lock,side_button,siri,apple_pay. An unknown key exits2listing what is available. Reach the back control by label instead of by key: that is portable, and on Android it also avoids the soft keyboard swallowing the press.swipe --durationis ignored on iOS.idbpaces a swipe in pixels per step, so verikun passes coordinates only. Auto-scroll still works; it cannot pace the gesture there.suite --appdoes not reset app data on iOS. It degrades to a force-stop, so a test that depends on starting logged-out does not hold there — see Suites.installreplaces a differently-signed build on Android only. Android refuses to update a package across signing keys, which is routine on a shared device. verikun removes the installed build and installs again, warning on stderr that its app data is gone; a same-key install keeps its data. On iOS the install simply fails.installallows a version downgrade on Android when the build is debuggable. verikun always installs withadb install -d; Android only honors that flag for a debuggable build (the common CI/test case), so a release-signed build still fails withINSTALL_FAILED_VERSION_DOWNGRADE.- A device pool is one platform.
vk server --devicesserves one platform per server, andvk suite --servers a,bexits2when the servers report different ones. Run a suite on both platforms by running it twice — see Suites. doctor --fixis Android-only and an alias fordevice prep, so on a physical device it needs the serial named with--device.vk doctor --iosstill checks the toolchain.device prepneeds an explicit--deviceon a physical phone. Naming the serial is the opt-in. An emulator is auto-selected, as it is fordevices start|stop|restart, which likewise never power-cycles a physical device. On Android 9 thedndknob is not scriptable and prep fails on it (#103).companionis Android-only and exits3on iOS. It is on by default (VERIKUN_COMPANION=0opts out) and makes the hierarchy read roughly ten times faster; iOS reads are already fast. On HyperOS (Android 15) it currently fails to start and every read silently takes the slow path (#87). See The Android companion.
Selectors and state modifiers
Section titled “Selectors and state modifiers”| Modifier | Android | iOS | ||
|---|---|---|---|---|
| Physical | Emulator | Simulator | Physical | |
--enabled / --not-enabled | ✅ | ✅ | ✅ | ✅ |
--checked / --not-checked | ✅ | ✅ | ✅ derived | ✅ derived |
--selected / --not-selected | ✅ | ✅ | ❌ 3 | ❌ 3 |
--focused / --not-focused | ✅ | ✅ | ❌ 3 | ❌ 3 |
idb reports no selected or focused state at all, so a filter on either could only ever
match nothing; verikun refuses it with exit 3 instead of burning the wait window and
reporting “no element matched”. checked is derived from the element type and value.
Which kind of selector to reach for is the same on both platforms: @id first, text:
second, desc: never. See
Selectors for what each kind
maps to per platform.
Device settings
Section titled “Device settings”This is the canonical per-key matrix. Device state covers how the snapshot-and-restore works and what each value domain accepts.
| Key | Android | iOS | ||
|---|---|---|---|---|
| Physical | Emulator | Simulator | Physical | |
animations | ✅ | ✅ | ❌ 3 — nothing disables UIKit animation | ❌ 3 |
airplane | ⚠️ refused over wireless adb | ✅ | ❌ 3 — no radio | ❌ 3 |
dark | ✅ | ✅ | ✅ | ❌ 3 |
font-scale | ✅ | ✅ | ⚠️ nearest Dynamic Type category | ❌ 3 |
rotation | ✅ | ✅ | ❌ 3 — nothing rotates it | ❌ 3 |
stay-awake | ✅ | ✅ | ⊘ no-op — simulators do not sleep | ❌ 3 |
screen-timeout | ✅ | ✅ | ⊘ no-op — simulators do not sleep | ❌ 3 |
dnd | ✅ | ✅ | ❌ 3 — Focus is not scriptable | ❌ 3 |
doze | ✅ | ✅ | ⊘ no-op — no Doze equivalent | ❌ 3 |
- A physical iOS device supports none of them.
simctldrives simulators only andidbcovers interaction, not preferences. Each key refuses with the manual equivalent named, sovk device prepis Android-only in practice. vk device caps --iosreports the simulator answer either way. The capability table is static, so on a physical devicecapssaysdarkis supported andsetexits3. Trust this page, orset, overcapsthere.font-scaleon iOS maps to the nearest Dynamic Type category. The category applied is printed to stderr;1.3can land at an effective ratio near1.35.
An unsupported key exits 3 before any device I/O. For vk ai and vk suite it is
caught when the plan is validated, so a suite asking for rotation on iOS fails before the
first tap rather than half-way through a half-modified device.
Behaviour and reporting
Section titled “Behaviour and reporting”| Feature | Android | iOS | ||
|---|---|---|---|---|
| Physical | Emulator | Simulator | Physical | |
| Auto-wait on selectors | ✅ | ✅ | ✅ | ✅ |
| Modal-barrier settle on reads | ✅ | ✅ | ⊘ not needed | ⊘ not needed |
| Auto-scroll into view | ✅ | ✅ | ⚠️ orientation-blind | ⚠️ orientation-blind |
offscreen marker | ⚠️ rarely fires | ⚠️ rarely fires | ✅ | ✅ |
| Screenshot downscaling | ✅ | ✅ | ✅ | ✅ |
| JUnit + HTML reports | ✅ | ✅ | ✅ | ✅ |
| Device log in the archive | ✅ | ✅ | ✅ | ❌ log capture refuses |
| Password redaction | ✅ | ✅ | ❌ flag never set | ❌ flag never set |
| Failure screenshot + hierarchy | ✅ | ✅ | ✅ | ✅ |
Failure screenshot over --server | ❌ hierarchy only | ❌ hierarchy only | ❌ hierarchy only | ❌ hierarchy only |
| Device claims (auto-pick a free device) | ✅ | ✅ | ✅ | ✅ |
vk server failover — unreachable device | ✅ | ✅ | ✅ | ✅ |
vk server failover — device cannot serve an install | ✅ | ✅ | ❌ probe only | ❌ probe only |
vk server pool degrade / rejoin sweep | ✅ | ✅ | ✅ | ✅ |
vk server adb-server recycle | ✅ macOS host only | ✅ macOS host only | ⊘ no adb | ⊘ no adb |
vk server --log-file | ✅ | ✅ | ✅ | ✅ |
- The adb-server recycle needs a macOS host. A long-running adb server leaks USB handles
until devices drop mid-run, and the server restarts it while idle. Detecting it reads the
macOS kernel’s guard-violation log, so on a Linux host the server never recycles and
vk doctorstays quiet. iOS has no adb to recycle. See Remote devices & CI. - Failover on iOS moves only for an unreachable device. Telling “this device cannot take
the build” from “this build is broken” relies on
adb’sINSTALL_FAILED_*vocabulary, whichidbdoes not share, so a full simulator does not trigger a move: the install fails. See When the bound device fails. - Device claims are host-side and identical everywhere. Over
--serverthe claim is held by the server process on the host where the devices are. See Device claims. - The modal-barrier settle is Android-only. Its dumper skips a sheet’s contents until they are on screen; iOS has them in the first read. See Auto-wait.
offscreenis mostly an iOS signal. Android’s dumper drops nodes it considers invisible and clips the rest to the display, so a fully off-screen element is usually not in the tree at all. Do not write an Android test that expectsoffscreento fire.- Auto-scroll is orientation-blind on iOS.
idbgives no orientation signal, so the viewport is treated as a square of the longest edge: exact along the axis a list scrolls, permissive across it. - Password redaction does not fire on iOS. Redaction keys off the element’s
passwordflag, andidbreports a FlutterobscureTextfield as plain text, so the typed value lands in the report unredacted (#44). Do not rely on redaction cross-platform — see Reports & test runs.
Toolchain
Section titled “Toolchain”| Android | iOS | |
|---|---|---|
| Required | adb (platform-tools) |
xcrun and idb and idb_companion |
| Install | Android SDK platform-tools | brew install idb-companion + pip install fb-idb |
| Override the binary path | ADB |
IDB |
| Extra for a physical device | USB debugging | Developer mode, plus a reachable idb_companion |
| Check it | vk doctor |
vk doctor --ios |
idb is required to drive iOS at all, simulator or not. simctl covers screenshots,
launch, stop and logs on a simulator; the hierarchy and every interaction come from idb.
Full setup: iOS setup.
Where these tables come from
Section titled “Where these tables come from”The Android columns and the iOS simulator column are measured, by running the built CLI
against the repository’s Flutter fixture app on real hardware and simulators. The iOS
physical device column is read from the source rather than measured, so treat it as
reliable for what is refused and
report anything that disagrees. The measured
findings, with the hardware each was observed on, live in
example/flutter-app/README.md.
Where to go next
Section titled “Where to go next”- iOS setup — install idb and pick a target
- Troubleshooting — what a given iOS failure means
- Device state — how snapshot and restore work
- Selectors — the complete grammar
- Exit codes — what each refusal returns