Automation
Tests & automation
What worked with your agent runs on its own afterwards. Every command is a plain shell call with an exit code, so a
shell script or a CI job can run the same steps: as flow files that looktap test checks, or as plain scripts.
Test flows in CI
A flow file is a run script: one looktap command per line, # for comments. Use wait where the app takes
time and expect where the flow should check something:
# flows/sign-in.txt
launch com.example.myapp
wait "Sign in"
fill Email [email protected]
tap "Continue"
wait Welcome
expect "Sign in" --gone
$ looktap test flows/ -o results --junit results/junit.xml
✓ flows/browse.txt 9 steps · 6.1 s
✗ flows/checkout.txt 3.4 s
step 5 (expect Total = €4.20): expect failed: "Total" is "€0.00", expected "€4.20" (waited 3 s)
│ $ tap "Add to bag"
│ tapped "Add to bag" at 195,612
│ …
evidence: results/failures/flows-checkout
✓ flows/sign-in.txt 6 steps · 4.8 s
looktap: 3 flows: 2 passed, 1 failed · 14.3 s · results/junit.xml
Flows run in order on one device, each from wherever the last one left the app, so start each with launch (or
reset <app> and launch for a fresh install). Steps wait for their targets, but
a flow that depends on a backend can still be slow: raise --wait rather than adding sleeps.
A web page with no accessibility labels (many sign-in pages) gives wait nothing to match: wait --settle waits
until the screen, its picture included, holds still, and a blank page that's still loading doesn't count. Where a
person has to act (a consent screen, a second factor), handoff makes that a step: it tells them, then carries on
once the next screen shows. pause is the fixed wait for what's left. In a React Native dev build,
"dismissLogBox": true in looktap.config.json closes the LogBox toast before every tap:
# flows/google-sign-in.txt
tap "Continue with Google"
wait --settle
handoff "Sign in with Google and approve access" --until Home
expect Home
Flow files live in your repo, so never put real credentials in one. Sign in with a test-only account, and keep its
password out of the file: test replaces ${NAME} with that environment variable (a CI secret), e.g.
fill Password "${PASSWORD}".
In GitHub Actions, iOS runs on a macOS runner and Android on a Linux runner with KVM:
jobs:
ios:
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22 }
- run: npm install -g @looktap/cli
- run: xcodebuild -scheme MyApp -sdk iphonesimulator -derivedDataPath build build
- run: looktap boot "iPhone 16" --headless
- run: looktap install build/Build/Products/Debug-iphonesimulator/MyApp.app
- run: looktap test flows/ -o results --junit results/junit.xml
- uses: actions/upload-artifact@v4
if: always()
with: { name: ios-results, path: results }
android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22 }
- run: npm install -g @looktap/cli
- run: ./gradlew assembleDebug
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 34
arch: x86_64
script: |
looktap -p android install app/build/outputs/apk/debug/app-debug.apk --grant
looktap -p android test flows/ -o results --junit results/junit.xml
- uses: actions/upload-artifact@v4
if: always()
with: { name: android-results, path: results }
Upload results/ even when the job fails: that's where the failed flows' screenshots and logs are.
Record a flow (codegen)
On Android you can write a flow by using the app. codegen restarts the app, then turns each touch into the step
that replays it:
$ looktap codegen flows/about.txt com.android.settings
+ stop com.android.settings
+ launch com.android.settings
+ wait Settings
recording com.android.settings to flows/about.txt — use the phone. …
+ scrollto "About phone"
+ tap "About phone"
+ back
+ tap "Search settings"
+ fill Search wifi
expect "Wi-Fi"
ok: "Wi-Fi" at 107,148
+ expect Wi-Fi
saved 9 steps to flows/about.txt — run it: looktap test flows/about.txt
- Taps are saved by label: a tap anywhere on a row is a tap on the row's label, when
tapwith that label finds the same row again. Where no label does (an icon with no accessibility label), it's saved astapxy, with a note. - Typing is one
fill, with the field's name and its final text. Text typed into a password field isn't saved: the step reads it from an environment variable instead (fill Password "${PASSWORD}"). - Scrolling that brought a row into view becomes
scrollto, which replays on any screen size. Other swipes (a carousel, a map) stay swipes. The navigation bar's Back and Home, and gesture navigation, becomebackandhome. - Checks: type a command into the terminal while it records, e.g.
expect "Welcome". It's run, and saved if it passes.undodrops the last step.
It reads the touchscreen through adb shell getevent, so it records real fingers (not taps sent by looktap itself).
It isn't available on iOS yet: write the flow by hand there, or record it on Android if the app is the same.
A repeatable screenshot sweep
#!/bin/sh
set -e
looktap launch
for route in home search messages profile settings; do
looktap open "myapp://$route" --ls > /dev/null
looktap shot "$route"
looktap swipe 200 700 200 250
looktap shot "${route}__scrolled"
done
Same screen on several devices
looktap boot "iPhone 14" --headless && looktap boot "iPhone 16 Pro" --headless
looktap devices
# ios 42E41FA7-… iPhone 14 · simulator · iOS 26.0 · headless
# ios F9213EDC-… iPhone 16 Pro · simulator · iOS 26.0 · headless
# android emulator-5554 Pixel_9a · emulator · Android 16 · window
for d in 42E41FA7-EA40-45C1-8C6E-13A70BAF1FD6 F9213EDC-180D-412B-B24C-2FD69A37D3AC; do
looktap -d $d open myapp://home
looktap -d $d wait "Home"
looktap -d $d shot -o shots/$d home
done
A command that names no device uses the one the last command used, for 10 minutes and while it's still up, without
listing devices again (0.3–0.5 s saved on each command). boot and every command that names a device (-d,
LOOKTAP_DEVICE) set it. When two people or agents drive two devices at the same time, each should name its device
on every command.
Generated from README.md. Edit it there.