later in November we'll make agent-device leaner and move some of the functionality, such as remote providers, to plugins.
I guess every project will have its inflation and shrinking phase and we're approaching that now that core functionality is locked
github.com/callstack/agent-device/milestone/1 t.co
9 likes · 1 repost · 566 views
Test Automation with agent-device (Part 3)
---
title: "Test Automation with agent-device (Part 3)"
published: true
description: "Save test flows as agent-device .ad scripts and run them again."
tags: claude, ai, flutter, testing
canonical_url: krenin88.github.io
---
This post covers saving test flows as agent-device .ad scripts and running them again, using a Flutter app. It goes through writing an .ad file, getting the app into the right state before a replay, reading the report when a replay stops and fixing it, and the token cost of agent-driven QA. It is based on agent-device 0.21.31 (2026-10-25).
Installation and the basic loop are in Part 1, and real-device setup is in Part 2.
What an .ad file is
When an agent checks an app, it reads the screen, picks the next thing to tap, and reads the result, over and over. Exploring the same flow this way every time keeps costing time and tokens. An .ad file pins down the result of that exploration as a list of commands, and replay runs the list from top to bottom.
Each line has the form <command> [positional...] [flag...], the same as an agent-device command in the terminal. Quoting follows shell rules too, so a selector containing spaces is wrapped like 'label="..."'. Below is the start of a flow that opens the app fresh, taps a button on the first screen, types into an input, and checks the result screen.
# First-run flow — the app ID comes in via -e APP=
open ${APP} --relaunch
wait 'label="<first-screen button>"' 30000
press 'label="<first-screen button>"'
fill 'id="find-input"' "<text to type>"
press 'label="<search button>" role=button'
wait 'label="<result title>"' 60000
wait text "<text the result should show>" 10000
back
close
A few notes:
- # comments work. The docs do not say so, so the first line was a comment in a test replay, and it passed.
- Do not hard-code the app ID. Leave it as ${APP} and pass -e APP=<bundle id> at replay time, and the same file works for builds with different IDs, such as a dev build and a release build.
- The number after wait is a timeout in milliseconds. Give a long one to steps that wait on the network or on model inference.
- Device-selection flags stay out of the file. --platform, --serial, --device, and --settle are not part of a script step; they go on the replay command.
There are two ways to make an .ad file. You can start recording with open <bundle id> --relaunch --save-script=<file>.ad, go through the flow, and save with session save-script, or you can write it by hand. The 24-step flow in this post was written by hand, based on a flow the agent had already passed manually.
Getting to a "first launch" state before replaying
replay only runs the commands written down; it does not reset the app. If the flow starts by checking the first-launch consent dialog, an app that still remembers an earlier consent stops right at step 2. So a separate runner script deletes and reinstalls the app before each replay.
# Android emulator
adb -s <serial> uninstall <bundle id>
adb -s <serial> install -r <apk path>
adb -s <serial> shell cmd locale set-app-locales <bundle id> --locales en-US
agent-device replay <file>.ad --platform android --serial <serial> -e APP=<bundle id> --timeout 240000
# iOS simulator
xcrun simctl uninstall <UDID> <bundle id>
xcrun simctl install <UDID> <Runner.app path>
agent-device replay <file>.ad --platform ios --udid <UDID> -e APP=<bundle id> --timeout 300000
On Android, set-app-locales opens just this app in English while leaving the device language alone. On the iOS simulator no per-app language command was used, so the app follows the simulator's language setting. replay has a default work budget of 60 seconds, so raise it with --timeout for longer flows.
Deleting the app also deletes its data. So the runner script accepts only the designated test emulators (checked by name with adb -s <serial> emu avd name) and simulator UDIDs, and stops with exit code 2 if it gets any other device or an app ID outside your own. An emulator serial such as emulator-5554 is assigned in boot order and does not tell you which AVD it is, which is why the check uses the name.
With that in place, the 24-step flow finished with Replayed 24 steps on both runs on each platform: 8.8 and 9.4 seconds on the Android emulator, 13.9 and 14.0 seconds on the iOS simulator.
When a replay stops
When a replay gets stuck partway, it fails with REPLAY_DIVERGENCE. The report includes the step number and its line, the actionable elements on screen at that moment, and selector suggestions for that screen. So you do not have to start over after a fix, it also tells you how to resume with replay --from <step> --plan-digest <value>. When no automatic repair is possible, it says Repair hint: manual.
A first-launch system dialog. The flow that passed on Android stopped at step 2 the first time it ran on the iOS simulator.
Error (REPLAY_DIVERGENCE): Replay failed at step 2 (wait "label=\"<first-screen button>\"" 30000): wait timed out for selector: ...
Current surface: Allow "<app name>" to track your activity across other companies' apps ...
The report's Current surface showed the App Tracking Transparency (ATT) dialog. It is a system dialog that appears on first launch only on iOS, so the flow written against Android had no step for it. After closing it with agent-device alert dismiss and replaying again, the run got past this step. The runner script does not handle it, so on builds that show this dialog, close it before replaying.
Input role names that differ by platform. Next, the replay stopped at the fill in step 5. The first selector was role=text-field, which matched on Android, but the iOS simulator exposed the same input as an other element with a label. To find one selector that works on both platforms, these were tried in order:
1. editable=true — passed on Android, Selector did not match on iOS
2. role=textfield || role=text-field — || is the "or" syntax that appeared in the report's selector suggestions. It still did not match on iOS.
3. Adding Semantics(identifier:) to the Flutter widget — passed on both platforms
The last fix is one line of app code: identifier: 'find-input' added to the existing Semantics around the input, with no visible change on screen. The selector becomes id="find-input". As noted in Part 1, a Flutter Key does not show up in the accessibility tree, so adding an identifier is the reliable way to run the same script on both platforms.
Token cost and the rules for cutting it
Token cost was the reason for using .ad in the first place. Most of the cost of agent-driven QA came not from output but from cache reads. A cache read is the earlier conversation the agent re-reads on every response. The first real-device check of two apps took about 40 minutes and came to 121 responses, 77,985 output tokens, and 30,225,404 cache-read tokens, roughly 250,000 tokens re-read per response.
The QA agent was already on Sonnet, so the model was not the cause. In the same stretch, 11 screenshots were read into the conversation, and as the context grew, every response re-read more. So five rules went into the QA agent's instructions:
1. Save a flow as .ad the first time it passes, and use replay from then on. When it gets stuck, fix only that step using the divergence report.
2. Act with --settle and look only at the returned diff. Do not read the full screen tree.
3. Save screenshots to files only, and read at most one per item into the conversation.
4. End the session per item: one app, one thing to check. Past 40 responses, it is time to stop.
5. Read logs and build output with grep or tail, only the lines you need.
The batch after the rules (three emulator checks, an iPhone attempt, an Android real device, about 30 minutes) came to 105 responses, 44,113 output tokens, and 19,171,162 cache-read tokens, about 180,000 cache-read tokens per response. The items checked were different, so the two sets of numbers cannot be compared directly, and the 40-response limit was not met. Tokens after adopting .ad have not been measured separately yet, and neither replay on a real device nor running it in CI has been tried.
References
- agent-device repository: <callstack/agent-device>
- agent-device agent setup docs: <oss.callstack.com>
- Flutter SemanticsProperties.identifier: <api.flutter.dev>
Claude Code に React Native アプリを触らせるなら agent-device の CLI
はじめに
AI に React Native のバグを直させると、コードはそれらしく直してくれます。ただ、シミュレータで本当に直ったかを確かめさせようとすると、何かツールを入れる必要があります。React Native の有名どころの会社からも、Callstack が agent-device、Software Mansion が Argent を出していて、ほかに mobile-mcp もあります。
どれを入れればいいのか。比べた記事を探したら、英語でも日本語でも機能表の比較しか見つかりませんでした。Expo 自身もブログで「検証ツールの比較は今後の実験」と書いています。そこで、Bluesky の公式アプリで実際に直されたバグを使って、180回測りました。
!
検証環境は Claude Code 2.1.310、Opus 5.5、Xcode 27.1、iOS 26.5 のシミュレータ (iPhone 17) です。アプリは Bluesky の公式アプリ (コミット 61c7cad、Expo SDK 57) を使い、計測は2026年10月に行いました。ほかのアプリやエージェント、Android では試していません。
React Native なら agent-device の CLI を入れればいい
比べたのは、Callstack の agent-device、Software Mansion の Argent、Mobile Next の mobile-mcp の3つです。3つとも CLI と MCP の両方の入口があるので、6通りで測り、1回あたりの費用と、画面で再現から確認までやり切れたかを見ました。作業時間も参考に測っています。
早速結論ですが、自分のアプリに入れるなら agent-device の CLI を選びます。理由は2つです。
一番安い。1回あたり $0.70 で、mobilecli (mobile-mcp の CLI) と並んで最安。同じ agent-device の MCP より3割近く安い
React Native 向けの機能がある。コンポーネントの props や state を調べる React DevTools や、Metro の再読み込みをコマンドで使える。mobilecli には無い
費用だけなら mobilecli も同じくらい安く済みます。React Native のアプリなら、機能の分だけ agent-device の CLI が上だと判断しました。
MCP しか使えない環境なら、mobile-mcp を選びます。タップや入力をまとめて1回で送れ、画面を読んだ結果も短いので、MCP の3つの中で一番安く済みました。
速さを重視する場合は、Argent の MCP が選択肢になります。6通りの中で作業時間が一番短く済みました。ただし費用は一番高く、agent-device の CLI の1.6倍かかります。タップするたびにスクショが返ってくるので、やり取りは少なく済む一方、受け取る画像が多くなるためです。
Bluesky の実バグで180回測った
比べた3つのツール
どれも、エージェントにシミュレータの画面を読ませ、タップや入力をさせるためのツールです。違いは、画面をエージェントにどう渡すかにあります。
ツール
開発元
画面の渡し方
agent-device
Callstack
ボタンや文字を @e2 [button] "Add" のような参照の一覧で返す。操作のあとは、変わった部分だけを差分で返す
Argent
Software Mansion
タップなどの操作のたびに、結果のスクショを自動で返す。画面の要素の一覧も別に取れる
mobile-mcp
Mobile Next
画面の要素を1行1個の一覧で返し、座標を指定してタップする。複数の操作をまとめて1回で送れる
CLI はエージェントがシェルでコマンドを打って使い、MCP は Claude Code にツールとして登録して使います。mobile-mcp の CLI は、中で動いている mobilecli というエンジンをそのまま使う形です。この画面の渡し方の違いが、あとで見る費用の差になって表れます。
比べた6つの条件
3つのツールの CLI と MCP で6条件です。ツールはどれも公式ドキュメントが勧める入れ方にしました。
条件
入れたもの
agent-device CLI
CLI 0.21.31、公式の skill 4つ、公式が勧めるプロジェクトのルール文
agent-device MCP
agent-device mcp、同じルール文
Argent MCP
argent init が入れるもの一式 (MCP、skill 18個、rules) 0.30.1
Argent CLI
上から MCP の設定だけを外したもの
mobile-mcp MCP
@mobilenext/mobile-mcp 1.0.13
mobile-mcp CLI
中のエンジンの mobilecli 1.0.21 と公式の skill
xcrun simctl の起動・スクショ・URL を開く操作は、どの条件でも使えるようにしました。テレメトリは全ツールで切っています。
この6条件に、次のお題5問 × testID とアクセシビリティのラベルの有無 × 3回を掛けて、全部で180回測りました。
お題は Bluesky で実際に直された5件
Bluesky の公式アプリは Expo SDK 57 のアプリで、E2E テスト用のモックサーバーが付いています。毎回同じデータでアプリを立ち上げられるので、計測に向いています。
お題は、Bluesky で実際にマージされた PR を戻して作りました。選んだのは、モックサーバーで症状を出せて、直ったかを Maestro で判定できる PR です。比べるツールを一度も動かす前に決めたので、結果を見てお題を選び直してはいません。
お題
症状
症状を出すのに必要な操作
#10132
画像に ALT を付けても案内が消えない
画像の添付、ALT の入力と保存
#10163
モデレーションリストが開けない
URL でリストを開く
#11503
存在しないユーザーのプロフィールが読み込み中のまま止まる
URL でプロフィールを開く
#8502
引用を禁止した投稿でも、リポストの長押しで引用の作成画面が開く
別のアカウントで引用禁止の投稿を作り、リポストを長押し
#11820
(機能追加) 新規登録の生年月日を入力必須にする
新規登録のフォームに入力して Next を押す
エージェントには、バグの症状と環境を伝えたうえで「シミュレータで再現して、直して、直ったことを確かめて」と頼みました。条件ごとに変えたのは、使うツールを指定する1文だけです。ALT の案内のバグだけは、Bluesky の issue の英文をそのまま貼っています。「このバグを直して」とだけ頼んだときに、エージェントが自分から画面で確かめに行くかは測っていません。
実際に渡した指示の例このリポジトリは Bluesky の公式アプリです。次のバグに対応してください。
存在しないハンドルのプロフィール (例: bluesky://profile/nobody.test) を開くと、エラー画面にならず、読み込み中の表示のまま止まってしまう。
アプリは iOS シミュレータ「BlueskyLab iPhone 17」(UDID 2B6D50B9-95B8-4FD8-8EFB-37FFE605ACF2) で起動していて、Metro (localhost:8081) とローカルのテストサーバー (localhost) につながっています。alice でサインインしています。このシミュレータ以外は別の作業で使っているので、触らないでください。ファイルを保存すると、Metro がアプリを自動で再読み込みします。
シミュレータの操作には agent-device の CLI を使ってください。
シミュレータでバグを再現し、コードを直して、直ったことをシミュレータで確かめてください。Web 検索はしないでください。終わったら、原因と直した内容を短く報告してください。
バグが直ったかどうかは、エージェントの作業が終わったあとに、アプリが Bluesky の修正と同じ動きになるかを Maestro のフローで確かめました。エージェントが画面で確かめたかどうかは、ログを Sonnet 5.5 に読ませて判定し、食い違った箇所はコードとログを自分で読んで確認しています。それでも見落としはありえます。
エージェントに答えを見せない仕掛け
Bluesky は公開リポジトリなので、直し方がエージェントの手の届くところに残っています。答えを見て直せてしまうと、画面で再現する前に作業が終わり、ツールの差を測れません。比べたいツールを通らずに画面を操作できる道も同じです。そこで、次のように塞ぎました。
git の履歴と差分には、修正そのものが残っている。バグを入れた状態を1コミットだけのリポジトリにして、回ごとに作り直した
公式の E2E テストには、どの testID を押せばいいかが書いてある。テストのフォルダを外した
アプリには E2E テスト用に、ログインや画面の移動を1回で済ませる隠しボタンがある。描画しないようにした
Maestro が入っていると、比べたいツールを使わずに画面を操作できる。Maestro をコマンドとして呼べなくした
Web で検索すると、元の PR が見つかる。Web 検索を禁止した
実際の開発ではエージェントが git の履歴や E2E テストも読めるので、費用や時間はこの記事の数字より軽く済むかもしれません。外したものはどの条件でも同じなので、ツールどうしの順位には響かないと見ています。
agent-device の CLI を選ぶ2つの理由
再現から確認までは、6条件すべてが30回中30回やり切りました。差が出たのは費用です。agent-device の CLI を選ぶ理由は、一番安いことと、React Native 向けの機能があることの2つです。
CLI は画面を読む量が少ないので一番安い
条件
1回の費用 (平均)
作業時間 (平均)
再現・確認まで終えた
agent-device CLI
$0.70
2分05秒
30
mobile-mcp CLI (mobilecli)
$0.70
2分08秒
30
mobile-mcp MCP
$0.80
1分53秒
30
Argent CLI
$0.91
3分04秒
30
agent-device MCP
$0.95
1分53秒
30
Argent MCP
$1.11
1分41秒
30
agent-device の CLI は、mobilecli と並んで一番安く、Argent の MCP の6割ほどで済みました。同じお題・ラベル・回の組で比べると、agent-device の MCP には30組中29組、Argent の MCP には30組すべてで安くなっています。mobilecli とは30組中18組で、差はありません。
費用は API の料金に換算した値です。トークンの96%はキャッシュの読み込みで、1回平均142万トークンでも費用は $1 前後に収まりました。計測の順番によっては前の回のキャッシュを使えて安く出るので、どの回もキャッシュが空の状態から始めたとして計算し直しています。同じ組み合わせ3回の間のばらつきは、どの条件も6〜14%でした。
作業時間は参考にとどめてください。計測の途中に、同じ Mac で別の作業をしていた時間帯があります。
安さのわけは、画面を読む量にあります。CLI はシェルで動くので、エージェントが | grep や | head で必要な部分だけを読めます。同じツールの CLI と MCP を比べると、3つとも CLI の方が安くなりました。agent-device で3割近く、Argent で2割弱、mobile-mcp で1割強です。
画面を読む量の内訳
agent-device の2つをログで比べると、差は画面を読み取る量から来ていました。
条件
やり取り
画面を1回読んだ結果の長さ
受け取った画像
agent-device CLI
26回
約580文字
3.5枚
agent-device MCP
26回
約5,300文字
2.5枚
やり取りの回数は同じで、1回に読む画面の長さが9倍ほど違います。MCP は、ツールが返したものを丸ごと受け取るしかありません。mobile-mcp でも、CLI の約620文字に対して MCP は約2,300文字でした。
Argent の MCP が1回の作業で受け取った画像は平均9.5枚で、agent-device の CLI (3.5枚) の3倍近くでした。画像は文字よりトークンが重いので、そのぶん費用がかさみます。
なお、「画面を読み取った回」はツールの名前やコマンドの文字で拾う簡易な数え方です。傾向は見えますが、厳密な内訳ではありません。
React Native 向けの機能がある
費用で並んだ mobilecli との違いは、React Native 向けの機能です。agent-device には、React Native のアプリを調べるためのコマンドがあります。
できること
コマンドの例
コンポーネントの木、props、state、hooks を見る
agent-device react-devtools tree / inspect
遅い描画や再描画の原因を調べる
agent-device react-devtools profile start
JS だけの変更を Metro で読み込み直す
agent-device metro reload
LogBox や RedBox の表示を閉じる
agent-device react-native dismiss-overlay
React DevTools は、Metro につながった開発ビルド (React Native 0.74 以上) なら、アプリに手を入れずに使えます。mobilecli のコマンドは画面の操作と端末の管理が中心で、React Native に特化したものはありません。
ただし、今回の計測ではこの差は測れていません。お題はどれも画面に症状が出るバグで、agent-device の CLI の回でも、ログや通信を見るコマンドが数回使われただけでした。再描画が多すぎて重い、のように画面だけでは原因が分からないバグでどれだけ効くかは、試していません。
作りから予想できたことと、外れたこと
ここまでの結果のうち2つは、ツールの作りから予想がつきます。Argent の MCP はタップのたびにスクショを返すので、受け取る画像が増えて高くなります。CLI はエージェントが要る部分だけを読めるので、ツールの返すものを丸ごと受け取る MCP より安くなります。予想と違ったのは、次の2つでした。
testID やラベルを外しても、費用も時間も変わらない
testID とアクセシビリティのラベルをアプリから全部外した版でも測りました。アイコンだけのボタンが、名前の無いただの「Button」になる状態です。エージェントがボタンを探しにくくなり、費用がかさむと予想していました。
条件
1回の費用 (ラベルあり → 無し)
無しの方が安かった組
agent-device CLI
$0.71 → $0.69
8/15
mobile-mcp CLI
$0.71 → $0.69
8/15
mobile-mcp MCP
$0.83 → $0.76
8/15
Argent CLI
$1.01 → $0.82
11/15
agent-device MCP
$0.94 → $0.97
7/15
Argent MCP
$1.15 → $1.07
8/15
どの条件も、ラベル無しの方が安かった組は半分前後で、差は3回のばらつきの範囲に収まりました。Argent の CLI だけ2割安くなっていますが、ばらつきと区別しきれません。全条件を合わせた90組で見ても、トークンが少なかったのは41組、時間が短かったのは39組でした。
アイコンだけのリポストのボタンを長押しする #8502 でも、差は出ていません。エージェントは、名前の無いボタンをスクショや並び順から見つけていました。
なお、今回のお題はどれも、たどる画面が少ないものです。何画面もたどる操作では、結果が変わるかもしれません。また、Bluesky は testID とラベルが丁寧に付いたアプリで、ここで測ったのはそれを「外した」状態です。最初から付いていないアプリでは、画面の作りが違うかもしれません。
一番高い Argent の MCP が一番速い
作業時間は平均1分41秒で、6条件の中で一番短くなりました。同じお題どうしで並べると、30組中25組で agent-device の CLI より早く、平均で24秒の差です。
速さのわけは、やり取りの少なさです。タップのたびにスクショが返ってくるので、画面を見るためだけの呼び出しがほとんど要りません。やり取りは平均22回で、agent-device の CLI (26回) より4回少なく済みました。
その代わり、1回のやり取りの費用は agent-device の CLI の倍近く ($0.053 と $0.028) かかります。
まとめ
差が出たのは、バグを直せるかどうかではなく、画面をエージェントにどう渡すかでした。CLI で画面を短く読める agent-device と mobilecli は安く、操作のたびにスクショを返す Argent の MCP は高くつきました。新しいツールを入れるときも、CLI があるか、画面を何で返すか (文字の一覧か、スクショか) を見れば、費用の当たりはつけられます。testID やラベルは、少なくとも費用のためには足さなくて構いません。
まだエージェントの操作ツールを使ったことのない React Native の開発者は、まずは agent-device の CLI を試してみてください。
参考リンク
callstack/agent-device (GitHub)
software-mansion/argent (GitHub)
mobile-next/mobile-mcp (GitHub)
bluesky-social/social-app (GitHub)
Speedrun, an open-source mobile E2E test runner that runs your Maestro flows up to 8x faster
I've always been bothered by how slow mobile UI tests are. Most of a test run is spent waiting: fixed sleeps, polling, comparing screenshots to guess whether the app is done animating. Coming from a game dev background, where you fight for every millisecond of a frame, that felt like a solvable problem. So I spent the last while building my own runner from the ground up, and it turns out UI tests can be a lot faster.
Speedrun is a black-box E2E test runner for Android and iOS. It's open source (Apache 2.0) and it runs Maestro's YAML flow format so if you already have Maestro tests, trying it is one word:
maestro test flows/ # before
speedrun test flows/ # after
Numbers
One user journey (sign in with a code, search a shop, fill a cart, set a PIN under a loading overlay, scroll a long list), written for each tool the way its tests are usually written. Median of 3 runs after a warm-up, start-up included:
|Tool|Android emulator|iOS simulator|
|:-|:-|:-|
|Speedrun|20.4 s|27.2 s|
|maestro-runner|65.9 s|49.6 s|
|Appium|59.4 s|94.1 s|
|XCUITest|-|81.2 s|
|Maestro 2.11|126.5 s|113.1 s|
On the native Settings app benchmark it's 8x faster than Maestro on Android and 2.8x on iOS. On a 19-flow React-Native test suite it passes all 19 on both platforms, and is 6.1x faster on Android and 2.6x on iOS. Android is where it shines most, which is nice for CI: Android runs on cheap Linux runners.
Full disclosure: the main benchmark app is mine, built in React Native to be hard to test (endless animations, late popups, loading overlays, lists that load as you scroll). The flows, per-flow results and scripts to rerun everything are in the repo, and I'd love to see numbers on other apps.
Why it's fast
- It waits for the app, not the clock. A small on-device agent knows when the app has actually settled (accessibility events on Android, XCTest's idle state checked against the screen on iOS), so each step starts the moment the app is ready. No sleeps, no screenshot diffing.
- Reading the screen is cheap. The agent reads the UI tree in about 10 ms on Android and talks to the host over a compact binary protocol.
- Screens that never stop moving don't block it. Spinners, tickers and carousels are left out of the "is it done?" check.
- Nothing to warm up. One native binary, no JVM. The iOS agent stays running between runs, so later runs start in about 0.2 s.
It's also less flaky: it won't tap a popup that's still sliding in or a button under a loading overlay, and it scrolls an element out from under a sticky footer or the keyboard before tapping it.
It works with AI agents too
speedrun mcp exposes it as an MCP server (Claude Code, Cursor, etc.). Your agent can explore the app on its own: tap around, read each screen in a few lines (cheap on tokens, not a whole accessibility tree), figure out how things work, then save the steps that worked as a YAML flow you can drop into CI.
claude mcp add speedrun -- ~/.speedrun/speedrun mcp
||Android emulator|iOS simulator|
|:-|:-|:-|
|Speedrun (speedrun session)|4.2 s, ~1,560 tokens|9.4 s, ~1,230 tokens|
|agent-device 0.21|9.1 s, ~2,240 tokens|14.0 s, ~1,620 tokens|
|Argent 0.31|11.7 s, ~3,630 tokens|12.2 s, ~2,650 tokens|
Limitations (so you don't have to find them)
- Android emulators and devices, iOS **simulators** only (no real iOS devices yet)
- macOS (Apple silicon); the Linux x86_64 build is Android-only
- Maestro's AI commands (`assertWithAI` and co.) aren't supported; the few other differences are documented
- It's young (v0.1.10) and moving fast
Try it
Repo, docs and benchmark details: hubastard/speedrun
If you have a Maestro suite, I'd really like to hear how it does on yours, especially where it breaks or is slower. Happy to answer questions about how it works.
edit1: added maestro-runner to the benchmark.
edit2: added ai agent benchmark.
50 points · 9 comments
Speedrun, an open-source mobile E2E test runner (Android/iOS) that runs your Maestro flows up to 8x faster
I've always been bothered by how slow mobile UI tests are. Most of a test run is spent waiting: fixed sleeps, polling, comparing screenshots to guess whether the app is done animating. Coming from a game dev background, where you fight for every millisecond of a frame, that felt like a solvable problem. So I spent the last while building my own runner from the ground up, and it turns out UI tests can be a lot faster.
Speedrun is a black-box E2E test runner for Android and iOS. It's open source (Apache 2.0) and it runs Maestro's YAML flow format so if you already have Maestro tests, trying it is one word:
maestro test flows/ # before
speedrun test flows/ # after
Numbers
One user journey (sign in with a code, search a shop, fill a cart, set a PIN under a loading overlay, scroll a long list), written for each tool the way its tests are usually written. Median of 3 runs after a warm-up, start-up included:
|Tool|Android emulator|iOS simulator|
|:-|:-|:-|
|Speedrun|20.4 s|27.2 s|
|maestro-runner|65.9 s|49.6 s|
|Appium|59.4 s|94.1 s|
|XCUITest|-|81.2 s|
|Maestro 2.11|126.5 s|113.1 s|
On the native Settings app benchmark it's 8x faster than Maestro on Android and 2.8x on iOS. On a 19-flow React-Native test suite it passes all 19 on both platforms, and is 6.1x faster on Android and 2.6x on iOS. Android is where it shines most, which is nice for CI: Android runs on cheap Linux runners.
Full disclosure: the main benchmark app is mine, built in React Native to be hard to test (endless animations, late popups, loading overlays, lists that load as you scroll). The flows, per-flow results and scripts to rerun everything are in the repo, and I'd love to see numbers on other apps.
Why it's fast
- It waits for the app, not the clock. A small on-device agent knows when the app has actually settled (accessibility events on Android, XCTest's idle state checked against the screen on iOS), so each step starts the moment the app is ready. No sleeps, no screenshot diffing.
- Reading the screen is cheap. The agent reads the UI tree in about 10 ms on Android and talks to the host over a compact binary protocol.
- Screens that never stop moving don't block it. Spinners, tickers and carousels are left out of the "is it done?" check.
- Nothing to warm up. One native binary, no JVM. The iOS agent stays running between runs, so later runs start in about 0.2 s.
It's also less flaky: it won't tap a popup that's still sliding in or a button under a loading overlay, and it scrolls an element out from under a sticky footer or the keyboard before tapping it.
It works with AI agents too
speedrun mcp exposes it as an MCP server (Claude Code, Cursor, etc.). Your agent can explore the app on its own: tap around, read each screen in a few lines (cheap on tokens, not a whole accessibility tree), figure out how things work, then save the steps that worked as a YAML flow you can drop into CI.
claude mcp add speedrun -- ~/.speedrun/speedrun mcp
||Android emulator|iOS simulator|
|:-|:-|:-|
|Speedrun (speedrun session)|4.2 s, ~1,560 tokens|9.4 s, ~1,230 tokens|
|agent-device 0.21|9.1 s, ~2,240 tokens|14.0 s, ~1,620 tokens|
|Argent 0.31|11.7 s, ~3,630 tokens|12.2 s, ~2,650 tokens|
Limitations (so you don't have to find them)
- Android emulators and devices, iOS **simulators** only (no real iOS devices yet)
- macOS (Apple silicon); the Linux x86_64 build is Android-only
- Maestro's AI commands (`assertWithAI` and co.) aren't supported; the few other differences are documented
- It's young (v0.1.10) and moving fast
Try it
Repo, docs and benchmark details: hubastard/speedrun
If you have a Maestro suite, I'd really like to hear how it does on yours, especially where it breaks or is slower. Happy to answer questions about how it works.
edit1: added maestro-runner to the benchmark
edit2: added ai agent benchmark
61 points · 21 comments
Test Automation with agent-device (Part 2)
---
title: "Test Automation with agent-device (Part 2)"
published: true
description: "Signing the iPhone runner (automatic and manual) and wireless Android debugging: how to put agent-device on real devices, and the traps."
tags: claude, ai, flutter, testing
canonical_url: krenin88.github.io
---
agent-device drives real devices with the same commands it uses for simulators and emulators. A real iPhone, though, needs a signed test app installed on the device to drive the screen, and a real Android device has to be selected explicitly depending on how it is connected. This post covers why real devices were needed, how to prepare an iPhone and an Android device, and what to watch out for when handling apps on them. It is based on agent-device 0.21.31 (2026-10-24).
Installation and the basic loop are in Part 1.
Why real devices
Simulators and emulators are enough to check app screens, but they cannot reproduce how a build shipped through the store behaves on a real user's device. Three things had to be checked on real devices.
Store-build updates and reinstalls. Installing a TestFlight build over the existing App Store version (the update path), and deleting and reinstalling the app and then restoring a backup, can only be checked on a real device. Whether the backup file was overwritten was checked in the device's Files app under iCloud Drive, by comparing file size and modification time before and after the install.
System dialogs on first launch. The order in which system dialogs such as App Tracking Transparency (ATT) appear relative to the app's own screens has to be seen on a real install. A reinstall also resets that consent history, so first work out whether the state you want to test can actually be produced on the device.
Real ads. The Google Mobile Ads SDK automatically treats an emulator as a test device. logcat prints This request is sent from a test device., and even a release build with production ad unit IDs shows only test ads. That means a no-fill (the ad server finds no ad to serve) cannot be reproduced on an emulator either. On a real device that is not registered as a test device, logcat prints a setTestDeviceIds hint and real ads are requested. In that case, never tap the ad (invalid clicks), watch a production ad to the end at most once per session, and judge the rest from the ad-load results in logcat.
Preparing a real iPhone
On a real iPhone, agent-device installs an XCTest runner on the device. The XCTest runner is a test app that reads the screen and taps on your behalf, and it has to be signed with a development certificate to install. The first run takes a while because the runner gets built, and the cable has to stay connected throughout.
Automatic signing. According to the agent-device docs, if an Apple developer account is signed in to Xcode, the team ID is all you need. To change the runner's bundle ID, also set AGENT_DEVICE_IOS_BUNDLE_ID.
export AGENT_DEVICE_IOS_TEAM_ID=<team id>
Manual signing. If Xcode has no account, or the existing development profile is Xcode-managed, automatic signing will not work. In that case, create a development profile for the runner yourself with an App Store Connect API key. fastlane's sigh creates a profile for a wildcard app ID (*) containing the registered devices.
# key.json: key_id, issuer_id, key (contents of the .p8 file), in_house: false
fastlane sigh --development --app_identifier "*" --team_id <team id> \
--api_key_path key.json --provisioning_name "<profile name>" \
--output_path <folder> --skip_install --skip_certificate_verification
Putting the .p8 file path in key.json (key_filepath) failed; putting the file contents in key worked. Copy the generated profile to ~/Library/MobileDevice/Provisioning Profiles/<UUID>.mobileprovision, and delete the key file once you are done.
Then pass the signing details as environment variables. The signing identity is the SHA-1 value shown by security find-identity -v -p codesigning, and the profile was specified by the name given to sigh.
export AGENT_DEVICE_IOS_TEAM_ID=<team id>
export AGENT_DEVICE_IOS_SIGNING_IDENTITY=<certificate SHA-1>
export AGENT_DEVICE_IOS_PROVISIONING_PROFILE="<profile name>"
export AGENT_DEVICE_STATE_DIR=<separate state folder>
AGENT_DEVICE_STATE_DIR is the agent-device daemon's state folder. A separate one was set to keep this work apart from the default daemon used by other work. Bundling these variables and the working directory (cd) into one wrapper script runs every command in the same environment.
Mac and device settings. Besides signing, three things have to be right for the runner to work.
1. Developer tools security on the Mac must be enabled. If it is off, the runner stops at Developer mode is disabled for Apple development tools. This is a Mac-side setting, separate from Developer Mode on the device; run sudo DevToolsSecurity -enable with admin rights.
2. iPhone Settings → Developer → "Enable UI Automation" must be on. If it is off, the runner installs and the test starts, but nothing responds.
3. Keep the screen on and unlocked. When the runner timed out at "Starting XCTest runner", the device was locked. A retry after unlocking never happened, so the cause is only circumstantial, but checking the lock state first with xcrun devicectl device info lockState --device <UDID> is the safer habit.
Handling apps on an iPhone
Listing and deleting installed apps on a real device is done with Xcode's devicectl.
xcrun devicectl list devices
xcrun devicectl device info apps --device <UDID>
xcrun devicectl device uninstall app --device <UDID> <bundle-id>
The runner is removed the same way. Its bundle ID is com.callstack.agentdevice.runner.uitests.xctrunner, and running open after deleting it builds and installs the runner again.
When installing a test build from TestFlight while the App Store version of the same app is already installed, a warning says "You may lose the app's data." The Install button in that warning overwrites the app. It is safer to have the agent stop at this warning and ask a human.
Deleting an app wipes its on-device data. If the app has a backup, take a screenshot showing that backup is on in the app's settings and that the last backup time is just now before deleting; otherwise, stop instead of deleting.
One more screen-handling note: tapping a toggle in a settings screen by ref sometimes hits the middle of the row and does not flip it. In that case, use the element's rect from snapshot --json and tap the right edge of the toggle by coordinates.
Connecting a real Android device
A real Android device connected over USB debugging behaves exactly like an emulator. With wireless debugging, the device may not show up in adb devices right after connecting. It appears only after the _adb-tls-connect service shows up in adb mdns services, so do not conclude it is missing; check again a few seconds later.
A wireless serial is a long mDNS name and easy to mix up with an emulator. Add --serial to every adb and agent-device command. agent-device sessions are created per working directory, so if an emulator was already in use from the same directory, you get an "already bound to android device" error. In that case, open a separate session with --session <name>.
adb mdns services
agent-device open <bundle-id> --platform android --serial <serial> --session <name> --foreground
agent-device close --session <name>
adb disconnect <serial>
Three things commonly go wrong on Android:
- Quote adb shell commands as one string. Unless the whole thing is wrapped, as in adb shell "cmd notification post -t 'title' <tag> 'body'", adb splits the arguments on spaces.
- Buttons hidden by the keyboard are not found. Close the keyboard with adb shell input keyevent KEYCODE_BACK and snapshot again.
- Quote selectors that contain spaces. Use the label="..." form; passing flags as one variable blob gives "Unknown flag".
When something other than the app, such as an interstitial ad, is on top, a snapshot alone makes it hard to tell what state the app is in. Check the system state with adb as well.
adb -s <serial> shell dumpsys activity activities | grep topResumedActivity # what is in front right now
adb -s <serial> shell pidof <package> # is the app process alive
adb -s <serial> logcat -d | grep -E "FATAL|AndroidRuntime" # traces of a crash
Match the snapshot against whether the front screen is the ad or the app, whether the app is still alive behind it, and whether a crash left a record. Ad screens put many webview and link nodes into the accessibility tree, so filter those out when reading a snapshot.
References
- agent-device repository: <callstack/agent-device>
- agent-device agent setup docs: <oss.callstack.com>
- fastlane sigh docs: <docs.fastlane.tools>
- Android wireless debugging: <developer.android.com>
新時代のE2Eテスト、TesterArmyのe2eをReact Nativeアプリに導入してみた
こんにちは!テラーノベルでiOS/Android/Webとフロントエンド周りを担当している@kazutoyoです!
以前、Coding AgentにiOS/Androidアプリの「目と手」を与えるツールとしてagent-deviceを紹介しました。
zenn.dev
今回は、そのagent-deviceをエンジンに使っている、TesterArmyのE2Eテストフレームワーク e2e を紹介します。
AIキャラクターとチャットするアプリのめろ(Expo / React Native)に最近導入して、PRでシナリオテストを回しています。
mero.ist
e2eは、ゴールを自然言語で書くとエージェントがアプリを操作してくれる、いわゆるAIテストツールです。
ただ、全部をAIに任せるツールではなく、AIに任せる部分と、従来どおりロケーターで確かめる部分を1本のテストの中で書き分けられるのが特徴です。
この記事では、e2eの紹介と既存ツールとの比較、めろでの使い方をまとめます。
e2eとは
e2eはTesterArmyが開発している、Webとモバイルアプリ向けのE2Eテストフレームワークです。
npmのパッケージ名はそのまま e2e(SDK・ランナー・CLI)
エンジンは別パッケージになっている
@e2e-dev/web: Playwright経由でChromium/Firefox/WebKitを動かす
@e2e-dev/mobile: agent-device経由でiOS Simulator/Android Emulatorを動かす
ほかに、結果をPRにコメントするレポーター(@e2e-dev/github)や、クラウドの端末・ブラウザのプロバイダー(EAS Simulators、Limrun、Kernelなど)がある
テストはこのように書きます(公式のREADMEより、コメントは筆者が追加)。
import { test, expect } from 'e2e';
// テスト名: 「メンバーがProプランにアップグレードできる」
test('a member upgrades to Pro', async ({ app, agent, screen }) => {
// 支払い設定の画面を開く
await app.open('/settings/billing');
// 「ワークスペースをProプランにアップグレードする」をゴールとして渡し、操作はエージェントに任せる
await agent.act('upgrade the workspace to the Pro plan');
// 「請求のプレビューに日割りの金額が出ている」かをAIに判定させる
await agent.assert('the invoice preview shows a prorated amount');
// 最後はロケーターで、ステータスに「Pro」と出ていることを確かめる
await expect(screen.getByRole('status')).toContainText('Pro');
});
画面を開いたら、プランを変える操作はAIに任せ、結果をAIとロケーターの両方で確かめる、という流れです。
e2e.tester.army
3種類のステップ
e2eのテストは、次の3種類のステップを組み合わせて書きます。
ステップ
API
何をするか
モデル呼び出し
ゴール
agent.act
「ストーリーを1つ開く」のようなゴールを渡すと、エージェントが画面を読んでタップや入力をする
あり(キャッシュがあれば無し)
AIによる判定
agent.assert / agent.waitFor / agent.extract
「入力欄が画面の下端にある」のような画面の状態をAIが判定する
あり
ロケーター
screen.getByRole(...) / expect(...)
Playwrightと同じ感覚で、決まった仕様を確かめる
なし
エージェントは画面のアクセシビリティツリーを読んで操作します。
agent.assert に { vision: true } を付けると、画面のピクセルも見せて判定させられます。
e2e.tester.army
リプレイキャッシュ
agent.act の操作は、そのあとのロケーターの検証や agent.assert が通ったときだけ記録されます。
次の実行からはその記録を再生するだけなので、モデルを呼びません。
UIが変わって記録どおりに操作できなくなったときは、その場面からエージェントが引き継ぎます。
AI 4.1k tokens · 2 model calls · openai/gpt-6-luna-fast
Cache 4 replayed · 1 handed off · 1 missed
実行の最後には、このようにキャッシュの結果が出ます。
replayed は記録の再生だけで終わったステップ、handed off は再生の途中からエージェントが引き継いだステップ、missed は記録が無く最初からエージェントが操作したステップです。
引き継いだ場合も、エージェントがゴールを達成できれば、テストはそのまま続きます。
AIテストは毎回トークンを使い、毎回動きが変わるのが気になるところですが、検証が通った操作だけを記録しておく仕組みでそこを抑えています。
ただし、キャッシュされるのは agent.act の操作だけです。
agent.assert などのAIによる判定は毎回モデルを呼ぶので、その分のコストは実行のたびにかかります。
e2e.tester.army
ChatGPTなどのサブスクでも動かせる
エージェントのモデルには、AI SDKのプロバイダーをそのまま渡せます。
APIキーのほかに、ChatGPTやGitHub Copilotなどのサブスクに npx e2e login openai でログインして使うこともできます。
めろでは、OpenAIの低価格なモデルのGPT-6 Lunaを使っていて、手元ではChatGPTのサブスク、CIではOpenRouter経由のAPIキーで呼んでいます。
e2e.tester.army
他のE2Eテストツールとの比較
代表的なE2Eテストのツールと比べると、次のようになります。
Maestro
Detox
Appium
e2e
対象
iOS/Android/Web
React Native
iOS/Androidほか
Web + iOS/Android
書き方
YAML
JS/TS
各言語
TS + 自然言語
アプリへの組み込み
不要
必要
不要
不要
要素の指定
テキスト / id
testIDなど
セレクタ
ロケーター or 自然言語
AIによる操作
なし
なし
なし
あり(agent.act)
AIによる判定
あり(assertWithAI など)
なし
なし
あり(agent.assert など)
大きな違いは、アプリへの組み込みが要らないことと、判定だけでなく操作までAIに任せられることかなと思います。
Maestroとの違い
MaestroはYAMLのフローで手軽に書けます。
repeat や条件付きの runFlow があり、runScript / evalScript でJavaScript(HTTPクライアントも使えます)を実行できるので、ロジックも書けます。
AIについても、assertWithAI や extractTextWithAI で画面の判定や読み取りができます。
e2eとの違いは2つあります。
1つは、テスト全体をTypeScriptで書く点です。ヘルパー関数や型、Vitestでのユニットテストなど、アプリと同じ道具がそのまま使えます。
もう1つは、AIに判定だけでなく操作も任せられる点です。Maestroでは操作はコマンドで書きますが、e2eでは agent.act にゴールを渡すと、エージェントが操作を決めます。
Detoxとの違い
Detoxはアプリの内部と同期するグレーボックスのテストなので安定しますが、アプリ側にDetoxを組み込む必要があります。
e2eはアクセシビリティツリーを外から読むので、いつものDevClientのビルドをそのままテストできます。
React Nativeの testID は、iOS/Androidのどちらでも getByTestId で引けます。
自然言語だけで書くAIテストツールとの違い
自然言語だけでシナリオを書くツールは、書くのは楽な一方で、何を確かめたのかが曖昧になりやすいです。
e2eのドキュメントでは、各ゴールの結果を expect で確かめることや、最後の判定をロケーターで決定的に書くことを勧めていて、AIは変わりやすい操作の部分を受け持つ位置づけです。
e2eの弱いところ
AIのステップにはコストと待ち時間がかかるので、キャッシュが効く書き方にする必要がある
アクセシビリティツリーに出ないもの(OSのFace IDのダイアログや、Androidでツリーに出ない要素など)は、visionに頼るか工夫が要る
まだ0.xで、APIの変化が速い
めろでの活用例
ここからは、めろでの使い方を、どんなテストをしたいときにどう書いているかでまとめます。
めろは生成AIのチャットアプリなので、おすすめの一覧やサーバーが返すフォームなど、毎回中身が変わる画面が多いです。
そうした画面の操作や判定はAIに任せ、仕様として決まっていることはロケーターで確かめる、という書き分けを基本にしています。
アプリ固有の用語をエージェントに伝えたいとき
エージェントの設定の context に、アプリの説明や用語を書いておけます。
export default {
targets: [
{ name: "ios", engine: mobile({ platform: "ios" }), app: { bundleId: "..." } },
{ name: "android", engine: mobile({ platform: "android" }), app: { bundleId: "..." } },
],
agents: {
default: {
// 手元では ChatGPT のサブスク、CI は API キー
model: process.env.CI ? openrouter("openai/gpt-6-luna") : chatgpt("gpt-6-luna"),
system: "You are a thorough QA agent. Verify every outcome on screen.",
context:
"mero は AI キャラクターとチャットする日本語のアプリ。画面の文言は日本語。「ストーリー」はキャラクターとの会話の題材、「トーク」はキャラクターとのチャット。",
},
},
} satisfies E2EConfig;
アプリの中だけで通じる言葉をここで教えておくと、ゴールの文を短く書けるようになりました。
並びが毎回変わる一覧から、どれか1つを開きたいとき
おすすめの一覧のように並びが毎回変わる画面では、「何番目をタップする」とは書けません。
こういう操作は agent.act に任せて、開いた先の画面はロケーターで確かめています。
// どれを開くかはエージェントに任せる
await agent.act("ホームの一覧からストーリーを 1 つ開く");
// 開いた先の画面は、ボタンの表示で確かめる
await expect(screen.getByRole("button", /^トークを(はじめる|続ける)$/)).toBeVisible();
一覧からagent.actで1つ開き、会話を始める
レイアウトの崩れのように、ロケーターでは確かめにくい見た目を確かめたいとき
チャットの画面で、キーボードを開いたままAIの返信が届き、そのあとキーボードを閉じると、入力欄が浮いたまま、一覧の下が真っ黒、ヘッダーが隠れたまま、という不具合が以前ありました。
こうした見た目の崩れは、要素がツリーにあるかどうかでは分かりません。
入力欄が下に戻ったことは位置をロケーターで確かめ、画面全体の見た目は agent.assert に { vision: true } を付けて、画面を見て判定させています。
// 入力欄の位置を測る
const inputTop = async () => (await screen.getByTestId("talk-input").boundingBox())?.y ?? 0;
const openedTop = await inputTop();
// 一覧をタップしてキーボードを閉じると、入力欄が下に戻る
await screen.getByTestId("talk-message-list").tap();
await expect.poll(inputTop).toBeGreaterThan(openedTop);
// 画面全体が崩れていないかは、画面を見て AI に判定させる
await agent.assert(
"キーボードは閉じていて、上部に戻るボタンとストーリー名のヘッダーが見え、入力欄は画面の下端にあり、一覧に真っ黒な領域は無い",
{ vision: true },
);
サーバー側で中身が変わるフォームを、最後まで進めたいとき
めろには、質問に答えていくだけでAIキャラクターを作れる「かんたん作成」という機能があります。
相手の性別、キャラクター、言葉づかい、舞台といった質問に、候補から選んで答えていく形です。
この質問や選択肢はサーバーが返していて、運営側の設定で変わります。
そのため、質問ごとにテストを書くとすぐに壊れてしまいます。
回答はエージェントにまとめて任せて、確認画面に着いたことだけをロケーターで確かめています。
await agent.act(
"各ステップでは、すべての質問について候補のボタンを 1 つずつ押して答えてから「次へ」を押す。確認の画面(「これで作る」ボタンがある)まで進む。「これで作る」は押さない",
);
await expect(screen.getByRole("button", "これで作る")).toBeVisible({ timeout: 30_000 });
await agent.assert("確認の画面に、回答が表示されている項目が 1 つ以上ある", { vision: true });
そのあとの「編集して保存すると確認画面に戻る」「一時保存して閉じると、続きから再開できる」といった遷移は、ロケーターで書いています。
フォームの中身が変わってもテストは壊れず、確かめたい仕様は固定できます。
agent.actが質問に答えて辿り着いた確認画面
生体認証やバックグラウンドからの復帰を試したいとき
@e2e-dev/mobile の device フィクスチャで、生体認証や権限、ホームボタン、アプリの切り替えも操作できます。
Face IDでのアプリロックは、次の流れで確かめています。
device.enrollBiometrics("faceid", true) でFace IDを登録する
設定からロックを有効にする
device.home() から device.openApp() でアプリに戻り、ロックをかける
device.setBiometrics("faceid", "nonmatch") で認証に失敗させ、ロックが外れないことを確かめる
再試行で "match" を返して解除する
OSのダイアログはアプリのツリーに出ないので、Face IDの結果がアプリに反映されるまで送り直すヘルパーを用意しています。
送り直すのは20秒までで、それを過ぎても反映されなければテストを失敗にしているので、失敗を隠すことはありません。
アナリティクスのイベントが正しく送られたかも確かめたいとき
これまでは、アナリティクスのイベントが送られているかを、実装のたびに手で確かめていました。
e2eでテストを書くついでに、イベントも同じテストで確かめられないかと思って試したのがこの仕組みです。
e2eのテストは普通のTypeScriptなので、テストの外の仕組みとも組み合わせられます。
めろでは、開発中にJSのバンドルを配信しているReact NativeのMetroサーバーに、ミドルウェアで /__e2e/events という受け口を足して、開発ビルドのアプリがアナリティクスのイベント(Firebase Analyticsなどに送るユーザーの行動ログ)を送るたびにそこへPOSTするようにしています。
テストからはそれを読んで、操作のあとに期待したイベントが送られたか、イベントのパラメータが正しいかを確かめています。
Metroサーバーと同じポートを使うので、CIでポートを変えても、Androidエミュレータ(10.0.2.2)からでもそのまま届きます。
アプリを一通り見て回って、画面の一覧を作りたいとき
e2eには、ゴールだけを渡してアプリを探索させる e2e explore があります。
めろでは、この e2e explore を使って、画面マップを自作しています。
画面マップはe2eの機能ではなく、e2eの実行結果を元に自分たちで作っている社内のドキュメントです。
ホームや検索、詳細画面といった領域ごとに e2e explore で探索させて、画面ごとのスクリーンショットと送られたアナリティクスのイベントを集め、自作のスクリプトで画面と遷移をつないでいます。
社内のドキュメントサイトで画面を選ぶと、その画面の説明や仕様テストの内容、探索で見たことが横に出ます。
stagingのデータを変えないよう、探索には別のエージェントを定義して、context で行動を絞っています。
見るだけにすること。してよいのは、画面を開く・戻る・タブの切り替え・スクロール…
してはいけないこと: メッセージの送信、購入や課金のボタンを押す…
e2e.tester.army
PRごとに回して、結果をレビューで見たいとき
GitHub Actionsでは、次の流れで回しています。
@expo/fingerprint でネイティブ部分のハッシュを取り、同じハッシュのDevClientのビルドがキャッシュにあればダウンロード、なければビルドしてアップロードする
Metroサーバーを起動して pnpm test:e2e を実行する
@e2e-dev/github のレポーターが結果をPRに1件のコメントとして貼る(再実行すると同じコメントを更新する)
レポートやスクリーンショットをアーティファクトとして保存する
JSだけの変更ならネイティブのビルドを飛ばせるので、実行時間のほとんどはテストそのものです。
PRに付くコメントは次のようになります。
この回は全体で7分22秒かかり、AIを使ったのは5ステップ、モデルの呼び出しは12回、費用は$0.0041でした(モデルはGPT-6 Luna)。
agent.assert の判定は毎回モデルを呼ぶので、費用はキャッシュが効いても0にはならず、記録が使えない回はもう少し増えます。
e2e.tester.army
今はすべてのテストを回していますが、今後はe2eのテストフィルターも取り入れたいと考えています。
PRで --changed-since を付けて実行すると、Jevのような決定モデルがPRの差分とテストを読んで、その変更で影響を受けそうにないテストを外してくれます。
テストが増えても、PRで回すテストの数と時間を抑えられそうです。
e2e.tester.army
Coding Agent向けのスキル
e2eには、Claude CodeやCodex、CursorなどのCoding Agent向けのスキルが、現在3つ用意されています。
スキル
何をするか
e2e
テストを書く・実行する・失敗の原因を調べる。e2e mcp で起動中のアプリを操作して、画面を確かめたりロケーターを試したりもできる
create-verification-skill
プロジェクト専用の verify-<アプリ名> スキルと機能の一覧を作り、Coding Agentが自分の変更をユーザーと同じ操作で確かめられるようにする
migrate-to-e2e
Playwright・Cypress・Selenium・Detox・Maestroのテストを少しずつe2eに移す。進み具合は MIGRATION.md に残るので、次のセッションで続きから再開できる
e2e のスキルは npx e2e init で .agents/skills/ と .claude/skills/ に入ります。めろでもこれで入れています。
3つまとめて入れたい場合は、Claude Code・Codex・Cursor向けのプラグインがあります(Claude Codeの例)。
claude plugin marketplace add tester-army/e2e
claude plugin install e2e@e2e
e2e.tester.army
バグバッシュ
e2e のスキルには、バグバッシュの手順も入っています。
「このブランチのチェックアウトとアカウント設定をバグバッシュして」のように頼むと、Coding Agentが次の流れで進めてくれます。
アプリの画面とブランチの差分を読んで、探索の目標を5〜10個立てる
目標ごとに e2e explore を走らせる(4つずつ並列)
見つかった問題のうち、探索の限界や意図した仕様によるものを外す
残った問題ごとに再現テストを書いて実行し、テストが失敗したものだけをバグとして報告する
AIが見つけた問題をそのまま報告するのではなく、再現テストで確かめてから報告してくれるのが良いところです。
めろではまだ試せていませんが、リリース前の確認などで使ってみたいと思っています。
e2e.tester.army
導入してみて
よかった点
変わる部分と変わらない仕様を分けて書ける。おすすめの一覧やサーバー駆動のフォーム、ロケーターでは確かめにくい見た目など、めろには従来のE2Eと相性の悪い部分が多いですが、そこをAIに任せつつ、確かめたい仕様はロケーターで固定できます
普通のTypeScriptで書ける。ヘルパーをVitestでテストしたり、アナリティクスのイベントの検証や画面マップの生成を自分たちで足したりと、アプリの開発と同じ感覚で広げられます
アプリに何も組み込まなくていい。いつものDevClientのビルドをそのまま使えます
Coding Agentに書かせやすい。e2eはパッケージにドキュメントとエージェント向けのスキルを同梱しているので、Claude Codeに「この機能のe2eを書いて」と頼むと、規約に沿ったテストを書いてくれます
気をつけている点
1つのゴールに1つの結果。agent.act に複数のことを頼まず、ゴールのあとには必ず expect を置きます(キャッシュもこれが前提です)
状態を残さない。下書きやロックの設定など、前の実行の残りで次のテストが変わらないよう、テストの最初と最後で片付けます
プラットフォームの差は書いておく。Androidではツリーに出ない要素があるので、確かめられない部分はコメントに書いてiOSだけで確かめています
まとめ
e2eは、AIを使うところを人が選び、最後の判定はロケーターで決定的に行う設計なので、実際の運用に乗せやすいと感じています。
npx e2e init
このコマンドでWeb/モバイルのどちらかのエンジンとモデルを選ぶだけで導入でき、設定とサンプルのテストもできあがります。
Maestroなどからの移行用のスキル(migrate-to-e2e)も用意されているので、既存のMaestroのフローがあれば、それを元に試すこともできます。
e2e.tester.army
生成AIを使ったアプリのように、出力が毎回変わる画面のE2Eに悩んでいる方は、まずは触ってみることをおすすめします!
それでは良いReact Nativeライフを!
参考リンク
e2e(GitHub)
e2eドキュメント
Core concepts
Replay cache
Subscriptions
Test filters
Coding agents
Bug bash
Maestroからの移行ガイド
agent-device(GitHub)
Rozeniteとagent-deviceを紹介した過去記事
1 like
Test Automation with agent-device (Part 1)
---
title: "Test Automation with agent-device (Part 1)"
published: true
description: "Why agent-device for Flutter app testing, how to install it for Claude Code, the basic command loop, and Flutter gotchas."
tags: claude, ai, flutter, testing
canonical_url: krenin88.github.io
---
agent-device is an open-source tool from Callstack that lets an AI agent drive iOS and Android apps and check what is on screen. This post covers why it was picked for testing a Flutter app, how to install it for Claude Code, the basic command loop, and what to watch out for with Flutter. It is based on version 0.21.31 (2026-10-24).
Why agent-device
Flutter ships Android and iOS apps from one codebase, so the device-testing tool has to cover iOS too. Google Artemis, the first candidate, is a natural-language testing agent, but it supports Android only; iOS is still just a roadmap item.
The other candidates looked like this:
- mobile-mcp exposes taps, screenshots, and the accessibility tree on iOS and Android over MCP (the protocol an AI uses to call external tools). It has no sessions, replay, or evidence storage, so a verified flow is hard to hand off to CI.
- Maestro is an E2E framework that pins flows in YAML, and it has an MCP server. It suits regression runs of fixed flows better than an agent exploring the screen.
- Arbigent is an AI agent built on top of Maestro. Its author rates it 1/5 for speed and 3/5 for reliability, and its YAML format was still changing.
- Commercial services (Drizz, Minitap, and others) bundle a real-device farm with a natural-language agent, but the only public material was vendor material, so cost and quality could not be checked.
agent-device handles iOS and Android simulators, emulators, and real devices with one tool, and its docs state Flutter support explicitly. It ships both a CLI and an MCP server, can save a verified flow as an .ad script to replay later, and can export to Maestro YAML. It is MIT-licensed.
Installation
Install the CLI globally and check the environment.
npm i -g agent-device@latest
agent-device doctor
doctor checks connected devices, toolchains, and dev-server status. In this setup it passed with only warnings for unused platforms (HarmonyOS, Vega).
Register it with Claude Code as an MCP server. With -s user, the same tool is available in every project.
claude mcp add -s user agent-device -- agent-device mcp
The agent-device repository includes three agent skills: agent-device, ios-simulator, and android-emulator. The official instruction is npx skills add callstack/agent-device, but Claude Code's auto mode safety check blocked it. In that case, clone the repository and copy the three folders under skills/ into ~/.claude/skills/.
To undo everything, run claude mcp remove agent-device -s user and npm uninstall -g agent-device, then delete the three skill folders.
The basic loop
The basic loop is: prepare a device, open the app, act, verify, close. Pick the device with --platform and --device.
agent-device boot --platform android --device "<emulator name>" --headless
agent-device install /absolute/path/app-debug.apk --platform android --device "<emulator name>"
agent-device open <bundle-id> --platform android --device "<emulator name>" --foreground
agent-device press @e5 --settle
agent-device fill @e11 "text" --settle
agent-device wait text "<expected text>"
agent-device screenshot /absolute/path/home.png
agent-device close
open reads the screen and returns a ref such as @e5 for each element, like buttons and text fields. With --settle, press and fill wait until the screen goes quiet and return only what changed, so the agent can pick its next target without rereading the whole screen.
Three things commonly go wrong here:
- Refs go stale when the screen changes. Pick a new ref from the diff returned by each action.
- A screenshot is not verification. Verify with wait text or wait <selector>, and keep screenshots as evidence only.
- Give install an absolute path. A relative path resolves against the agent-device daemon's working directory and fails with "App binary not found".
For a Flutter app, build the iOS simulator app with flutter build ios --simulator --debug and the Android app with flutter build apk --debug.
Flutter gotchas
agent-device reads the screen through the accessibility tree. In Flutter, that means only widgets with text show up by default; icon buttons and containers do not. Add Semantics(identifier:) or semanticLabel to those widgets so a selector can find them. Flutter's Key does not appear in the accessibility tree.
Labels in the accessibility tree follow the language the device is set to, so the same app reads with different label text depending on that setting. A text-based selector changes with the device language, which makes identifier the more stable choice.
On iOS, AdMob test banners show up as platform_view, webview, and link nodes. Selectors in replay scripts should avoid those nodes.
References
- agent-device repository: <callstack/agent-device>
- agent-device agent setup docs: <oss.callstack.com>
- Google Artemis: <google/artemis>
- Maestro Flutter support docs: <docs.maestro.dev>
Agents that can see and tap your app are a big part of what we cover in The Agentic Rewind 👇
Join The Agentic Rewind: trnrw.com
agent-device: callstack/agent-device
22 views
use agent-device for agent verification locally or remotely, whatever, it just works callstack/agent-device
1 like · 93 views
Mentions
later in November we'll make agent-device leaner and move some of the functionality, such as remote providers, to plugins. I guess every project will have its inflation and shrinking phase and we're approaching that now that core functionality is locked github.com/callstack/agent-device/milestone/1 t.co
Test Automation with agent-device (Part 3) --- title: "Test Automation with agent-device (Part 3)" published: true description: "Save test flows as agent-device .ad scripts and run them again." tags: claude, ai, flutter, testing canonical_url: krenin88.github.io --- This post covers saving test flows as agent-device .ad scripts and running them again, using a Flutter app. It goes through writing an .ad file, getting the app into the right state before a replay, reading the report when a replay stops and fixing it, and the token cost of agent-driven QA. It is based on agent-device 0.21.31 (2026-10-25). Installation and the basic loop are in Part 1, and real-device setup is in Part 2. What an .ad file is When an agent checks an app, it reads the screen, picks the next thing to tap, and reads the result, over and over. Exploring the same flow this way every time keeps costing time and tokens. An .ad file pins down the result of that exploration as a list of commands, and replay runs the list from top to bottom. Each line has the form <command> [positional...] [flag...], the same as an agent-device command in the terminal. Quoting follows shell rules too, so a selector containing spaces is wrapped like 'label="..."'. Below is the start of a flow that opens the app fresh, taps a button on the first screen, types into an input, and checks the result screen. # First-run flow — the app ID comes in via -e APP= open ${APP} --relaunch wait 'label="<first-screen button>"' 30000 press 'label="<first-screen button>"' fill 'id="find-input"' "<text to type>" press 'label="<search button>" role=button' wait 'label="<result title>"' 60000 wait text "<text the result should show>" 10000 back close A few notes: - # comments work. The docs do not say so, so the first line was a comment in a test replay, and it passed. - Do not hard-code the app ID. Leave it as ${APP} and pass -e APP=<bundle id> at replay time, and the same file works for builds with different IDs, such as a dev build and a release build. - The number after wait is a timeout in milliseconds. Give a long one to steps that wait on the network or on model inference. - Device-selection flags stay out of the file. --platform, --serial, --device, and --settle are not part of a script step; they go on the replay command. There are two ways to make an .ad file. You can start recording with open <bundle id> --relaunch --save-script=<file>.ad, go through the flow, and save with session save-script, or you can write it by hand. The 24-step flow in this post was written by hand, based on a flow the agent had already passed manually. Getting to a "first launch" state before replaying replay only runs the commands written down; it does not reset the app. If the flow starts by checking the first-launch consent dialog, an app that still remembers an earlier consent stops right at step 2. So a separate runner script deletes and reinstalls the app before each replay. # Android emulator adb -s <serial> uninstall <bundle id> adb -s <serial> install -r <apk path> adb -s <serial> shell cmd locale set-app-locales <bundle id> --locales en-US agent-device replay <file>.ad --platform android --serial <serial> -e APP=<bundle id> --timeout 240000 # iOS simulator xcrun simctl uninstall <UDID> <bundle id> xcrun simctl install <UDID> <Runner.app path> agent-device replay <file>.ad --platform ios --udid <UDID> -e APP=<bundle id> --timeout 300000 On Android, set-app-locales opens just this app in English while leaving the device language alone. On the iOS simulator no per-app language command was used, so the app follows the simulator's language setting. replay has a default work budget of 60 seconds, so raise it with --timeout for longer flows. Deleting the app also deletes its data. So the runner script accepts only the designated test emulators (checked by name with adb -s <serial> emu avd name) and simulator UDIDs, and stops with exit code 2 if it gets any other device or an app ID outside your own. An emulator serial such as emulator-5554 is assigned in boot order and does not tell you which AVD it is, which is why the check uses the name. With that in place, the 24-step flow finished with Replayed 24 steps on both runs on each platform: 8.8 and 9.4 seconds on the Android emulator, 13.9 and 14.0 seconds on the iOS simulator. When a replay stops When a replay gets stuck partway, it fails with REPLAY_DIVERGENCE. The report includes the step number and its line, the actionable elements on screen at that moment, and selector suggestions for that screen. So you do not have to start over after a fix, it also tells you how to resume with replay --from <step> --plan-digest <value>. When no automatic repair is possible, it says Repair hint: manual. A first-launch system dialog. The flow that passed on Android stopped at step 2 the first time it ran on the iOS simulator. Error (REPLAY_DIVERGENCE): Replay failed at step 2 (wait "label=\"<first-screen button>\"" 30000): wait timed out for selector: ... Current surface: Allow "<app name>" to track your activity across other companies' apps ... The report's Current surface showed the App Tracking Transparency (ATT) dialog. It is a system dialog that appears on first launch only on iOS, so the flow written against Android had no step for it. After closing it with agent-device alert dismiss and replaying again, the run got past this step. The runner script does not handle it, so on builds that show this dialog, close it before replaying. Input role names that differ by platform. Next, the replay stopped at the fill in step 5. The first selector was role=text-field, which matched on Android, but the iOS simulator exposed the same input as an other element with a label. To find one selector that works on both platforms, these were tried in order: 1. editable=true — passed on Android, Selector did not match on iOS 2. role=textfield || role=text-field — || is the "or" syntax that appeared in the report's selector suggestions. It still did not match on iOS. 3. Adding Semantics(identifier:) to the Flutter widget — passed on both platforms The last fix is one line of app code: identifier: 'find-input' added to the existing Semantics around the input, with no visible change on screen. The selector becomes id="find-input". As noted in Part 1, a Flutter Key does not show up in the accessibility tree, so adding an identifier is the reliable way to run the same script on both platforms. Token cost and the rules for cutting it Token cost was the reason for using .ad in the first place. Most of the cost of agent-driven QA came not from output but from cache reads. A cache read is the earlier conversation the agent re-reads on every response. The first real-device check of two apps took about 40 minutes and came to 121 responses, 77,985 output tokens, and 30,225,404 cache-read tokens, roughly 250,000 tokens re-read per response. The QA agent was already on Sonnet, so the model was not the cause. In the same stretch, 11 screenshots were read into the conversation, and as the context grew, every response re-read more. So five rules went into the QA agent's instructions: 1. Save a flow as .ad the first time it passes, and use replay from then on. When it gets stuck, fix only that step using the divergence report. 2. Act with --settle and look only at the returned diff. Do not read the full screen tree. 3. Save screenshots to files only, and read at most one per item into the conversation. 4. End the session per item: one app, one thing to check. Past 40 responses, it is time to stop. 5. Read logs and build output with grep or tail, only the lines you need. The batch after the rules (three emulator checks, an iPhone attempt, an Android real device, about 30 minutes) came to 105 responses, 44,113 output tokens, and 19,171,162 cache-read tokens, about 180,000 cache-read tokens per response. The items checked were different, so the two sets of numbers cannot be compared directly, and the 40-response limit was not met. Tokens after adopting .ad have not been measured separately yet, and neither replay on a real device nor running it in CI has been tried. References - agent-device repository: <callstack/agent-device> - agent-device agent setup docs: <oss.callstack.com> - Flutter SemanticsProperties.identifier: <api.flutter.dev>
Claude Code に React Native アプリを触らせるなら agent-device の CLI はじめに AI に React Native のバグを直させると、コードはそれらしく直してくれます。ただ、シミュレータで本当に直ったかを確かめさせようとすると、何かツールを入れる必要があります。React Native の有名どころの会社からも、Callstack が agent-device、Software Mansion が Argent を出していて、ほかに mobile-mcp もあります。 どれを入れればいいのか。比べた記事を探したら、英語でも日本語でも機能表の比較しか見つかりませんでした。Expo 自身もブログで「検証ツールの比較は今後の実験」と書いています。そこで、Bluesky の公式アプリで実際に直されたバグを使って、180回測りました。 ! 検証環境は Claude Code 2.1.310、Opus 5.5、Xcode 27.1、iOS 26.5 のシミュレータ (iPhone 17) です。アプリは Bluesky の公式アプリ (コミット 61c7cad、Expo SDK 57) を使い、計測は2026年10月に行いました。ほかのアプリやエージェント、Android では試していません。 React Native なら agent-device の CLI を入れればいい 比べたのは、Callstack の agent-device、Software Mansion の Argent、Mobile Next の mobile-mcp の3つです。3つとも CLI と MCP の両方の入口があるので、6通りで測り、1回あたりの費用と、画面で再現から確認までやり切れたかを見ました。作業時間も参考に測っています。 早速結論ですが、自分のアプリに入れるなら agent-device の CLI を選びます。理由は2つです。 一番安い。1回あたり $0.70 で、mobilecli (mobile-mcp の CLI) と並んで最安。同じ agent-device の MCP より3割近く安い React Native 向けの機能がある。コンポーネントの props や state を調べる React DevTools や、Metro の再読み込みをコマンドで使える。mobilecli には無い 費用だけなら mobilecli も同じくらい安く済みます。React Native のアプリなら、機能の分だけ agent-device の CLI が上だと判断しました。 MCP しか使えない環境なら、mobile-mcp を選びます。タップや入力をまとめて1回で送れ、画面を読んだ結果も短いので、MCP の3つの中で一番安く済みました。 速さを重視する場合は、Argent の MCP が選択肢になります。6通りの中で作業時間が一番短く済みました。ただし費用は一番高く、agent-device の CLI の1.6倍かかります。タップするたびにスクショが返ってくるので、やり取りは少なく済む一方、受け取る画像が多くなるためです。 Bluesky の実バグで180回測った 比べた3つのツール どれも、エージェントにシミュレータの画面を読ませ、タップや入力をさせるためのツールです。違いは、画面をエージェントにどう渡すかにあります。 ツール 開発元 画面の渡し方 agent-device Callstack ボタンや文字を @e2 [button] "Add" のような参照の一覧で返す。操作のあとは、変わった部分だけを差分で返す Argent Software Mansion タップなどの操作のたびに、結果のスクショを自動で返す。画面の要素の一覧も別に取れる mobile-mcp Mobile Next 画面の要素を1行1個の一覧で返し、座標を指定してタップする。複数の操作をまとめて1回で送れる CLI はエージェントがシェルでコマンドを打って使い、MCP は Claude Code にツールとして登録して使います。mobile-mcp の CLI は、中で動いている mobilecli というエンジンをそのまま使う形です。この画面の渡し方の違いが、あとで見る費用の差になって表れます。 比べた6つの条件 3つのツールの CLI と MCP で6条件です。ツールはどれも公式ドキュメントが勧める入れ方にしました。 条件 入れたもの agent-device CLI CLI 0.21.31、公式の skill 4つ、公式が勧めるプロジェクトのルール文 agent-device MCP agent-device mcp、同じルール文 Argent MCP argent init が入れるもの一式 (MCP、skill 18個、rules) 0.30.1 Argent CLI 上から MCP の設定だけを外したもの mobile-mcp MCP @mobilenext/mobile-mcp 1.0.13 mobile-mcp CLI 中のエンジンの mobilecli 1.0.21 と公式の skill xcrun simctl の起動・スクショ・URL を開く操作は、どの条件でも使えるようにしました。テレメトリは全ツールで切っています。 この6条件に、次のお題5問 × testID とアクセシビリティのラベルの有無 × 3回を掛けて、全部で180回測りました。 お題は Bluesky で実際に直された5件 Bluesky の公式アプリは Expo SDK 57 のアプリで、E2E テスト用のモックサーバーが付いています。毎回同じデータでアプリを立ち上げられるので、計測に向いています。 お題は、Bluesky で実際にマージされた PR を戻して作りました。選んだのは、モックサーバーで症状を出せて、直ったかを Maestro で判定できる PR です。比べるツールを一度も動かす前に決めたので、結果を見てお題を選び直してはいません。 お題 症状 症状を出すのに必要な操作 #10132 画像に ALT を付けても案内が消えない 画像の添付、ALT の入力と保存 #10163 モデレーションリストが開けない URL でリストを開く #11503 存在しないユーザーのプロフィールが読み込み中のまま止まる URL でプロフィールを開く #8502 引用を禁止した投稿でも、リポストの長押しで引用の作成画面が開く 別のアカウントで引用禁止の投稿を作り、リポストを長押し #11820 (機能追加) 新規登録の生年月日を入力必須にする 新規登録のフォームに入力して Next を押す エージェントには、バグの症状と環境を伝えたうえで「シミュレータで再現して、直して、直ったことを確かめて」と頼みました。条件ごとに変えたのは、使うツールを指定する1文だけです。ALT の案内のバグだけは、Bluesky の issue の英文をそのまま貼っています。「このバグを直して」とだけ頼んだときに、エージェントが自分から画面で確かめに行くかは測っていません。 実際に渡した指示の例このリポジトリは Bluesky の公式アプリです。次のバグに対応してください。 存在しないハンドルのプロフィール (例: bluesky://profile/nobody.test) を開くと、エラー画面にならず、読み込み中の表示のまま止まってしまう。 アプリは iOS シミュレータ「BlueskyLab iPhone 17」(UDID 2B6D50B9-95B8-4FD8-8EFB-37FFE605ACF2) で起動していて、Metro (localhost:8081) とローカルのテストサーバー (localhost) につながっています。alice でサインインしています。このシミュレータ以外は別の作業で使っているので、触らないでください。ファイルを保存すると、Metro がアプリを自動で再読み込みします。 シミュレータの操作には agent-device の CLI を使ってください。 シミュレータでバグを再現し、コードを直して、直ったことをシミュレータで確かめてください。Web 検索はしないでください。終わったら、原因と直した内容を短く報告してください。 バグが直ったかどうかは、エージェントの作業が終わったあとに、アプリが Bluesky の修正と同じ動きになるかを Maestro のフローで確かめました。エージェントが画面で確かめたかどうかは、ログを Sonnet 5.5 に読ませて判定し、食い違った箇所はコードとログを自分で読んで確認しています。それでも見落としはありえます。 エージェントに答えを見せない仕掛け Bluesky は公開リポジトリなので、直し方がエージェントの手の届くところに残っています。答えを見て直せてしまうと、画面で再現する前に作業が終わり、ツールの差を測れません。比べたいツールを通らずに画面を操作できる道も同じです。そこで、次のように塞ぎました。 git の履歴と差分には、修正そのものが残っている。バグを入れた状態を1コミットだけのリポジトリにして、回ごとに作り直した 公式の E2E テストには、どの testID を押せばいいかが書いてある。テストのフォルダを外した アプリには E2E テスト用に、ログインや画面の移動を1回で済ませる隠しボタンがある。描画しないようにした Maestro が入っていると、比べたいツールを使わずに画面を操作できる。Maestro をコマンドとして呼べなくした Web で検索すると、元の PR が見つかる。Web 検索を禁止した 実際の開発ではエージェントが git の履歴や E2E テストも読めるので、費用や時間はこの記事の数字より軽く済むかもしれません。外したものはどの条件でも同じなので、ツールどうしの順位には響かないと見ています。 agent-device の CLI を選ぶ2つの理由 再現から確認までは、6条件すべてが30回中30回やり切りました。差が出たのは費用です。agent-device の CLI を選ぶ理由は、一番安いことと、React Native 向けの機能があることの2つです。 CLI は画面を読む量が少ないので一番安い 条件 1回の費用 (平均) 作業時間 (平均) 再現・確認まで終えた agent-device CLI $0.70 2分05秒 30 mobile-mcp CLI (mobilecli) $0.70 2分08秒 30 mobile-mcp MCP $0.80 1分53秒 30 Argent CLI $0.91 3分04秒 30 agent-device MCP $0.95 1分53秒 30 Argent MCP $1.11 1分41秒 30 agent-device の CLI は、mobilecli と並んで一番安く、Argent の MCP の6割ほどで済みました。同じお題・ラベル・回の組で比べると、agent-device の MCP には30組中29組、Argent の MCP には30組すべてで安くなっています。mobilecli とは30組中18組で、差はありません。 費用は API の料金に換算した値です。トークンの96%はキャッシュの読み込みで、1回平均142万トークンでも費用は $1 前後に収まりました。計測の順番によっては前の回のキャッシュを使えて安く出るので、どの回もキャッシュが空の状態から始めたとして計算し直しています。同じ組み合わせ3回の間のばらつきは、どの条件も6〜14%でした。 作業時間は参考にとどめてください。計測の途中に、同じ Mac で別の作業をしていた時間帯があります。 安さのわけは、画面を読む量にあります。CLI はシェルで動くので、エージェントが | grep や | head で必要な部分だけを読めます。同じツールの CLI と MCP を比べると、3つとも CLI の方が安くなりました。agent-device で3割近く、Argent で2割弱、mobile-mcp で1割強です。 画面を読む量の内訳 agent-device の2つをログで比べると、差は画面を読み取る量から来ていました。 条件 やり取り 画面を1回読んだ結果の長さ 受け取った画像 agent-device CLI 26回 約580文字 3.5枚 agent-device MCP 26回 約5,300文字 2.5枚 やり取りの回数は同じで、1回に読む画面の長さが9倍ほど違います。MCP は、ツールが返したものを丸ごと受け取るしかありません。mobile-mcp でも、CLI の約620文字に対して MCP は約2,300文字でした。 Argent の MCP が1回の作業で受け取った画像は平均9.5枚で、agent-device の CLI (3.5枚) の3倍近くでした。画像は文字よりトークンが重いので、そのぶん費用がかさみます。 なお、「画面を読み取った回」はツールの名前やコマンドの文字で拾う簡易な数え方です。傾向は見えますが、厳密な内訳ではありません。 React Native 向けの機能がある 費用で並んだ mobilecli との違いは、React Native 向けの機能です。agent-device には、React Native のアプリを調べるためのコマンドがあります。 できること コマンドの例 コンポーネントの木、props、state、hooks を見る agent-device react-devtools tree / inspect 遅い描画や再描画の原因を調べる agent-device react-devtools profile start JS だけの変更を Metro で読み込み直す agent-device metro reload LogBox や RedBox の表示を閉じる agent-device react-native dismiss-overlay React DevTools は、Metro につながった開発ビルド (React Native 0.74 以上) なら、アプリに手を入れずに使えます。mobilecli のコマンドは画面の操作と端末の管理が中心で、React Native に特化したものはありません。 ただし、今回の計測ではこの差は測れていません。お題はどれも画面に症状が出るバグで、agent-device の CLI の回でも、ログや通信を見るコマンドが数回使われただけでした。再描画が多すぎて重い、のように画面だけでは原因が分からないバグでどれだけ効くかは、試していません。 作りから予想できたことと、外れたこと ここまでの結果のうち2つは、ツールの作りから予想がつきます。Argent の MCP はタップのたびにスクショを返すので、受け取る画像が増えて高くなります。CLI はエージェントが要る部分だけを読めるので、ツールの返すものを丸ごと受け取る MCP より安くなります。予想と違ったのは、次の2つでした。 testID やラベルを外しても、費用も時間も変わらない testID とアクセシビリティのラベルをアプリから全部外した版でも測りました。アイコンだけのボタンが、名前の無いただの「Button」になる状態です。エージェントがボタンを探しにくくなり、費用がかさむと予想していました。 条件 1回の費用 (ラベルあり → 無し) 無しの方が安かった組 agent-device CLI $0.71 → $0.69 8/15 mobile-mcp CLI $0.71 → $0.69 8/15 mobile-mcp MCP $0.83 → $0.76 8/15 Argent CLI $1.01 → $0.82 11/15 agent-device MCP $0.94 → $0.97 7/15 Argent MCP $1.15 → $1.07 8/15 どの条件も、ラベル無しの方が安かった組は半分前後で、差は3回のばらつきの範囲に収まりました。Argent の CLI だけ2割安くなっていますが、ばらつきと区別しきれません。全条件を合わせた90組で見ても、トークンが少なかったのは41組、時間が短かったのは39組でした。 アイコンだけのリポストのボタンを長押しする #8502 でも、差は出ていません。エージェントは、名前の無いボタンをスクショや並び順から見つけていました。 なお、今回のお題はどれも、たどる画面が少ないものです。何画面もたどる操作では、結果が変わるかもしれません。また、Bluesky は testID とラベルが丁寧に付いたアプリで、ここで測ったのはそれを「外した」状態です。最初から付いていないアプリでは、画面の作りが違うかもしれません。 一番高い Argent の MCP が一番速い 作業時間は平均1分41秒で、6条件の中で一番短くなりました。同じお題どうしで並べると、30組中25組で agent-device の CLI より早く、平均で24秒の差です。 速さのわけは、やり取りの少なさです。タップのたびにスクショが返ってくるので、画面を見るためだけの呼び出しがほとんど要りません。やり取りは平均22回で、agent-device の CLI (26回) より4回少なく済みました。 その代わり、1回のやり取りの費用は agent-device の CLI の倍近く ($0.053 と $0.028) かかります。 まとめ 差が出たのは、バグを直せるかどうかではなく、画面をエージェントにどう渡すかでした。CLI で画面を短く読める agent-device と mobilecli は安く、操作のたびにスクショを返す Argent の MCP は高くつきました。新しいツールを入れるときも、CLI があるか、画面を何で返すか (文字の一覧か、スクショか) を見れば、費用の当たりはつけられます。testID やラベルは、少なくとも費用のためには足さなくて構いません。 まだエージェントの操作ツールを使ったことのない React Native の開発者は、まずは agent-device の CLI を試してみてください。 参考リンク callstack/agent-device (GitHub) software-mansion/argent (GitHub) mobile-next/mobile-mcp (GitHub) bluesky-social/social-app (GitHub)
Speedrun, an open-source mobile E2E test runner that runs your Maestro flows up to 8x faster I've always been bothered by how slow mobile UI tests are. Most of a test run is spent waiting: fixed sleeps, polling, comparing screenshots to guess whether the app is done animating. Coming from a game dev background, where you fight for every millisecond of a frame, that felt like a solvable problem. So I spent the last while building my own runner from the ground up, and it turns out UI tests can be a lot faster. Speedrun is a black-box E2E test runner for Android and iOS. It's open source (Apache 2.0) and it runs Maestro's YAML flow format so if you already have Maestro tests, trying it is one word: maestro test flows/ # before speedrun test flows/ # after Numbers One user journey (sign in with a code, search a shop, fill a cart, set a PIN under a loading overlay, scroll a long list), written for each tool the way its tests are usually written. Median of 3 runs after a warm-up, start-up included: |Tool|Android emulator|iOS simulator| |:-|:-|:-| |Speedrun|20.4 s|27.2 s| |maestro-runner|65.9 s|49.6 s| |Appium|59.4 s|94.1 s| |XCUITest|-|81.2 s| |Maestro 2.11|126.5 s|113.1 s| On the native Settings app benchmark it's 8x faster than Maestro on Android and 2.8x on iOS. On a 19-flow React-Native test suite it passes all 19 on both platforms, and is 6.1x faster on Android and 2.6x on iOS. Android is where it shines most, which is nice for CI: Android runs on cheap Linux runners. Full disclosure: the main benchmark app is mine, built in React Native to be hard to test (endless animations, late popups, loading overlays, lists that load as you scroll). The flows, per-flow results and scripts to rerun everything are in the repo, and I'd love to see numbers on other apps. Why it's fast - It waits for the app, not the clock. A small on-device agent knows when the app has actually settled (accessibility events on Android, XCTest's idle state checked against the screen on iOS), so each step starts the moment the app is ready. No sleeps, no screenshot diffing. - Reading the screen is cheap. The agent reads the UI tree in about 10 ms on Android and talks to the host over a compact binary protocol. - Screens that never stop moving don't block it. Spinners, tickers and carousels are left out of the "is it done?" check. - Nothing to warm up. One native binary, no JVM. The iOS agent stays running between runs, so later runs start in about 0.2 s. It's also less flaky: it won't tap a popup that's still sliding in or a button under a loading overlay, and it scrolls an element out from under a sticky footer or the keyboard before tapping it. It works with AI agents too speedrun mcp exposes it as an MCP server (Claude Code, Cursor, etc.). Your agent can explore the app on its own: tap around, read each screen in a few lines (cheap on tokens, not a whole accessibility tree), figure out how things work, then save the steps that worked as a YAML flow you can drop into CI. claude mcp add speedrun -- ~/.speedrun/speedrun mcp ||Android emulator|iOS simulator| |:-|:-|:-| |Speedrun (speedrun session)|4.2 s, ~1,560 tokens|9.4 s, ~1,230 tokens| |agent-device 0.21|9.1 s, ~2,240 tokens|14.0 s, ~1,620 tokens| |Argent 0.31|11.7 s, ~3,630 tokens|12.2 s, ~2,650 tokens| Limitations (so you don't have to find them) - Android emulators and devices, iOS **simulators** only (no real iOS devices yet) - macOS (Apple silicon); the Linux x86_64 build is Android-only - Maestro's AI commands (`assertWithAI` and co.) aren't supported; the few other differences are documented - It's young (v0.1.10) and moving fast Try it Repo, docs and benchmark details: hubastard/speedrun If you have a Maestro suite, I'd really like to hear how it does on yours, especially where it breaks or is slower. Happy to answer questions about how it works. edit1: added maestro-runner to the benchmark. edit2: added ai agent benchmark.
Speedrun, an open-source mobile E2E test runner (Android/iOS) that runs your Maestro flows up to 8x faster I've always been bothered by how slow mobile UI tests are. Most of a test run is spent waiting: fixed sleeps, polling, comparing screenshots to guess whether the app is done animating. Coming from a game dev background, where you fight for every millisecond of a frame, that felt like a solvable problem. So I spent the last while building my own runner from the ground up, and it turns out UI tests can be a lot faster. Speedrun is a black-box E2E test runner for Android and iOS. It's open source (Apache 2.0) and it runs Maestro's YAML flow format so if you already have Maestro tests, trying it is one word: maestro test flows/ # before speedrun test flows/ # after Numbers One user journey (sign in with a code, search a shop, fill a cart, set a PIN under a loading overlay, scroll a long list), written for each tool the way its tests are usually written. Median of 3 runs after a warm-up, start-up included: |Tool|Android emulator|iOS simulator| |:-|:-|:-| |Speedrun|20.4 s|27.2 s| |maestro-runner|65.9 s|49.6 s| |Appium|59.4 s|94.1 s| |XCUITest|-|81.2 s| |Maestro 2.11|126.5 s|113.1 s| On the native Settings app benchmark it's 8x faster than Maestro on Android and 2.8x on iOS. On a 19-flow React-Native test suite it passes all 19 on both platforms, and is 6.1x faster on Android and 2.6x on iOS. Android is where it shines most, which is nice for CI: Android runs on cheap Linux runners. Full disclosure: the main benchmark app is mine, built in React Native to be hard to test (endless animations, late popups, loading overlays, lists that load as you scroll). The flows, per-flow results and scripts to rerun everything are in the repo, and I'd love to see numbers on other apps. Why it's fast - It waits for the app, not the clock. A small on-device agent knows when the app has actually settled (accessibility events on Android, XCTest's idle state checked against the screen on iOS), so each step starts the moment the app is ready. No sleeps, no screenshot diffing. - Reading the screen is cheap. The agent reads the UI tree in about 10 ms on Android and talks to the host over a compact binary protocol. - Screens that never stop moving don't block it. Spinners, tickers and carousels are left out of the "is it done?" check. - Nothing to warm up. One native binary, no JVM. The iOS agent stays running between runs, so later runs start in about 0.2 s. It's also less flaky: it won't tap a popup that's still sliding in or a button under a loading overlay, and it scrolls an element out from under a sticky footer or the keyboard before tapping it. It works with AI agents too speedrun mcp exposes it as an MCP server (Claude Code, Cursor, etc.). Your agent can explore the app on its own: tap around, read each screen in a few lines (cheap on tokens, not a whole accessibility tree), figure out how things work, then save the steps that worked as a YAML flow you can drop into CI. claude mcp add speedrun -- ~/.speedrun/speedrun mcp ||Android emulator|iOS simulator| |:-|:-|:-| |Speedrun (speedrun session)|4.2 s, ~1,560 tokens|9.4 s, ~1,230 tokens| |agent-device 0.21|9.1 s, ~2,240 tokens|14.0 s, ~1,620 tokens| |Argent 0.31|11.7 s, ~3,630 tokens|12.2 s, ~2,650 tokens| Limitations (so you don't have to find them) - Android emulators and devices, iOS **simulators** only (no real iOS devices yet) - macOS (Apple silicon); the Linux x86_64 build is Android-only - Maestro's AI commands (`assertWithAI` and co.) aren't supported; the few other differences are documented - It's young (v0.1.10) and moving fast Try it Repo, docs and benchmark details: hubastard/speedrun If you have a Maestro suite, I'd really like to hear how it does on yours, especially where it breaks or is slower. Happy to answer questions about how it works. edit1: added maestro-runner to the benchmark edit2: added ai agent benchmark
Test Automation with agent-device (Part 2) --- title: "Test Automation with agent-device (Part 2)" published: true description: "Signing the iPhone runner (automatic and manual) and wireless Android debugging: how to put agent-device on real devices, and the traps." tags: claude, ai, flutter, testing canonical_url: krenin88.github.io --- agent-device drives real devices with the same commands it uses for simulators and emulators. A real iPhone, though, needs a signed test app installed on the device to drive the screen, and a real Android device has to be selected explicitly depending on how it is connected. This post covers why real devices were needed, how to prepare an iPhone and an Android device, and what to watch out for when handling apps on them. It is based on agent-device 0.21.31 (2026-10-24). Installation and the basic loop are in Part 1. Why real devices Simulators and emulators are enough to check app screens, but they cannot reproduce how a build shipped through the store behaves on a real user's device. Three things had to be checked on real devices. Store-build updates and reinstalls. Installing a TestFlight build over the existing App Store version (the update path), and deleting and reinstalling the app and then restoring a backup, can only be checked on a real device. Whether the backup file was overwritten was checked in the device's Files app under iCloud Drive, by comparing file size and modification time before and after the install. System dialogs on first launch. The order in which system dialogs such as App Tracking Transparency (ATT) appear relative to the app's own screens has to be seen on a real install. A reinstall also resets that consent history, so first work out whether the state you want to test can actually be produced on the device. Real ads. The Google Mobile Ads SDK automatically treats an emulator as a test device. logcat prints This request is sent from a test device., and even a release build with production ad unit IDs shows only test ads. That means a no-fill (the ad server finds no ad to serve) cannot be reproduced on an emulator either. On a real device that is not registered as a test device, logcat prints a setTestDeviceIds hint and real ads are requested. In that case, never tap the ad (invalid clicks), watch a production ad to the end at most once per session, and judge the rest from the ad-load results in logcat. Preparing a real iPhone On a real iPhone, agent-device installs an XCTest runner on the device. The XCTest runner is a test app that reads the screen and taps on your behalf, and it has to be signed with a development certificate to install. The first run takes a while because the runner gets built, and the cable has to stay connected throughout. Automatic signing. According to the agent-device docs, if an Apple developer account is signed in to Xcode, the team ID is all you need. To change the runner's bundle ID, also set AGENT_DEVICE_IOS_BUNDLE_ID. export AGENT_DEVICE_IOS_TEAM_ID=<team id> Manual signing. If Xcode has no account, or the existing development profile is Xcode-managed, automatic signing will not work. In that case, create a development profile for the runner yourself with an App Store Connect API key. fastlane's sigh creates a profile for a wildcard app ID (*) containing the registered devices. # key.json: key_id, issuer_id, key (contents of the .p8 file), in_house: false fastlane sigh --development --app_identifier "*" --team_id <team id> \ --api_key_path key.json --provisioning_name "<profile name>" \ --output_path <folder> --skip_install --skip_certificate_verification Putting the .p8 file path in key.json (key_filepath) failed; putting the file contents in key worked. Copy the generated profile to ~/Library/MobileDevice/Provisioning Profiles/<UUID>.mobileprovision, and delete the key file once you are done. Then pass the signing details as environment variables. The signing identity is the SHA-1 value shown by security find-identity -v -p codesigning, and the profile was specified by the name given to sigh. export AGENT_DEVICE_IOS_TEAM_ID=<team id> export AGENT_DEVICE_IOS_SIGNING_IDENTITY=<certificate SHA-1> export AGENT_DEVICE_IOS_PROVISIONING_PROFILE="<profile name>" export AGENT_DEVICE_STATE_DIR=<separate state folder> AGENT_DEVICE_STATE_DIR is the agent-device daemon's state folder. A separate one was set to keep this work apart from the default daemon used by other work. Bundling these variables and the working directory (cd) into one wrapper script runs every command in the same environment. Mac and device settings. Besides signing, three things have to be right for the runner to work. 1. Developer tools security on the Mac must be enabled. If it is off, the runner stops at Developer mode is disabled for Apple development tools. This is a Mac-side setting, separate from Developer Mode on the device; run sudo DevToolsSecurity -enable with admin rights. 2. iPhone Settings → Developer → "Enable UI Automation" must be on. If it is off, the runner installs and the test starts, but nothing responds. 3. Keep the screen on and unlocked. When the runner timed out at "Starting XCTest runner", the device was locked. A retry after unlocking never happened, so the cause is only circumstantial, but checking the lock state first with xcrun devicectl device info lockState --device <UDID> is the safer habit. Handling apps on an iPhone Listing and deleting installed apps on a real device is done with Xcode's devicectl. xcrun devicectl list devices xcrun devicectl device info apps --device <UDID> xcrun devicectl device uninstall app --device <UDID> <bundle-id> The runner is removed the same way. Its bundle ID is com.callstack.agentdevice.runner.uitests.xctrunner, and running open after deleting it builds and installs the runner again. When installing a test build from TestFlight while the App Store version of the same app is already installed, a warning says "You may lose the app's data." The Install button in that warning overwrites the app. It is safer to have the agent stop at this warning and ask a human. Deleting an app wipes its on-device data. If the app has a backup, take a screenshot showing that backup is on in the app's settings and that the last backup time is just now before deleting; otherwise, stop instead of deleting. One more screen-handling note: tapping a toggle in a settings screen by ref sometimes hits the middle of the row and does not flip it. In that case, use the element's rect from snapshot --json and tap the right edge of the toggle by coordinates. Connecting a real Android device A real Android device connected over USB debugging behaves exactly like an emulator. With wireless debugging, the device may not show up in adb devices right after connecting. It appears only after the _adb-tls-connect service shows up in adb mdns services, so do not conclude it is missing; check again a few seconds later. A wireless serial is a long mDNS name and easy to mix up with an emulator. Add --serial to every adb and agent-device command. agent-device sessions are created per working directory, so if an emulator was already in use from the same directory, you get an "already bound to android device" error. In that case, open a separate session with --session <name>. adb mdns services agent-device open <bundle-id> --platform android --serial <serial> --session <name> --foreground agent-device close --session <name> adb disconnect <serial> Three things commonly go wrong on Android: - Quote adb shell commands as one string. Unless the whole thing is wrapped, as in adb shell "cmd notification post -t 'title' <tag> 'body'", adb splits the arguments on spaces. - Buttons hidden by the keyboard are not found. Close the keyboard with adb shell input keyevent KEYCODE_BACK and snapshot again. - Quote selectors that contain spaces. Use the label="..." form; passing flags as one variable blob gives "Unknown flag". When something other than the app, such as an interstitial ad, is on top, a snapshot alone makes it hard to tell what state the app is in. Check the system state with adb as well. adb -s <serial> shell dumpsys activity activities | grep topResumedActivity # what is in front right now adb -s <serial> shell pidof <package> # is the app process alive adb -s <serial> logcat -d | grep -E "FATAL|AndroidRuntime" # traces of a crash Match the snapshot against whether the front screen is the ad or the app, whether the app is still alive behind it, and whether a crash left a record. Ad screens put many webview and link nodes into the accessibility tree, so filter those out when reading a snapshot. References - agent-device repository: <callstack/agent-device> - agent-device agent setup docs: <oss.callstack.com> - fastlane sigh docs: <docs.fastlane.tools> - Android wireless debugging: <developer.android.com>
新時代のE2Eテスト、TesterArmyのe2eをReact Nativeアプリに導入してみた こんにちは!テラーノベルでiOS/Android/Webとフロントエンド周りを担当している@kazutoyoです! 以前、Coding AgentにiOS/Androidアプリの「目と手」を与えるツールとしてagent-deviceを紹介しました。 zenn.dev 今回は、そのagent-deviceをエンジンに使っている、TesterArmyのE2Eテストフレームワーク e2e を紹介します。 AIキャラクターとチャットするアプリのめろ(Expo / React Native)に最近導入して、PRでシナリオテストを回しています。 mero.ist e2eは、ゴールを自然言語で書くとエージェントがアプリを操作してくれる、いわゆるAIテストツールです。 ただ、全部をAIに任せるツールではなく、AIに任せる部分と、従来どおりロケーターで確かめる部分を1本のテストの中で書き分けられるのが特徴です。 この記事では、e2eの紹介と既存ツールとの比較、めろでの使い方をまとめます。 e2eとは e2eはTesterArmyが開発している、Webとモバイルアプリ向けのE2Eテストフレームワークです。 npmのパッケージ名はそのまま e2e(SDK・ランナー・CLI) エンジンは別パッケージになっている @e2e-dev/web: Playwright経由でChromium/Firefox/WebKitを動かす @e2e-dev/mobile: agent-device経由でiOS Simulator/Android Emulatorを動かす ほかに、結果をPRにコメントするレポーター(@e2e-dev/github)や、クラウドの端末・ブラウザのプロバイダー(EAS Simulators、Limrun、Kernelなど)がある テストはこのように書きます(公式のREADMEより、コメントは筆者が追加)。 import { test, expect } from 'e2e'; // テスト名: 「メンバーがProプランにアップグレードできる」 test('a member upgrades to Pro', async ({ app, agent, screen }) => { // 支払い設定の画面を開く await app.open('/settings/billing'); // 「ワークスペースをProプランにアップグレードする」をゴールとして渡し、操作はエージェントに任せる await agent.act('upgrade the workspace to the Pro plan'); // 「請求のプレビューに日割りの金額が出ている」かをAIに判定させる await agent.assert('the invoice preview shows a prorated amount'); // 最後はロケーターで、ステータスに「Pro」と出ていることを確かめる await expect(screen.getByRole('status')).toContainText('Pro'); }); 画面を開いたら、プランを変える操作はAIに任せ、結果をAIとロケーターの両方で確かめる、という流れです。 e2e.tester.army 3種類のステップ e2eのテストは、次の3種類のステップを組み合わせて書きます。 ステップ API 何をするか モデル呼び出し ゴール agent.act 「ストーリーを1つ開く」のようなゴールを渡すと、エージェントが画面を読んでタップや入力をする あり(キャッシュがあれば無し) AIによる判定 agent.assert / agent.waitFor / agent.extract 「入力欄が画面の下端にある」のような画面の状態をAIが判定する あり ロケーター screen.getByRole(...) / expect(...) Playwrightと同じ感覚で、決まった仕様を確かめる なし エージェントは画面のアクセシビリティツリーを読んで操作します。 agent.assert に { vision: true } を付けると、画面のピクセルも見せて判定させられます。 e2e.tester.army リプレイキャッシュ agent.act の操作は、そのあとのロケーターの検証や agent.assert が通ったときだけ記録されます。 次の実行からはその記録を再生するだけなので、モデルを呼びません。 UIが変わって記録どおりに操作できなくなったときは、その場面からエージェントが引き継ぎます。 AI 4.1k tokens · 2 model calls · openai/gpt-6-luna-fast Cache 4 replayed · 1 handed off · 1 missed 実行の最後には、このようにキャッシュの結果が出ます。 replayed は記録の再生だけで終わったステップ、handed off は再生の途中からエージェントが引き継いだステップ、missed は記録が無く最初からエージェントが操作したステップです。 引き継いだ場合も、エージェントがゴールを達成できれば、テストはそのまま続きます。 AIテストは毎回トークンを使い、毎回動きが変わるのが気になるところですが、検証が通った操作だけを記録しておく仕組みでそこを抑えています。 ただし、キャッシュされるのは agent.act の操作だけです。 agent.assert などのAIによる判定は毎回モデルを呼ぶので、その分のコストは実行のたびにかかります。 e2e.tester.army ChatGPTなどのサブスクでも動かせる エージェントのモデルには、AI SDKのプロバイダーをそのまま渡せます。 APIキーのほかに、ChatGPTやGitHub Copilotなどのサブスクに npx e2e login openai でログインして使うこともできます。 めろでは、OpenAIの低価格なモデルのGPT-6 Lunaを使っていて、手元ではChatGPTのサブスク、CIではOpenRouter経由のAPIキーで呼んでいます。 e2e.tester.army 他のE2Eテストツールとの比較 代表的なE2Eテストのツールと比べると、次のようになります。 Maestro Detox Appium e2e 対象 iOS/Android/Web React Native iOS/Androidほか Web + iOS/Android 書き方 YAML JS/TS 各言語 TS + 自然言語 アプリへの組み込み 不要 必要 不要 不要 要素の指定 テキスト / id testIDなど セレクタ ロケーター or 自然言語 AIによる操作 なし なし なし あり(agent.act) AIによる判定 あり(assertWithAI など) なし なし あり(agent.assert など) 大きな違いは、アプリへの組み込みが要らないことと、判定だけでなく操作までAIに任せられることかなと思います。 Maestroとの違い MaestroはYAMLのフローで手軽に書けます。 repeat や条件付きの runFlow があり、runScript / evalScript でJavaScript(HTTPクライアントも使えます)を実行できるので、ロジックも書けます。 AIについても、assertWithAI や extractTextWithAI で画面の判定や読み取りができます。 e2eとの違いは2つあります。 1つは、テスト全体をTypeScriptで書く点です。ヘルパー関数や型、Vitestでのユニットテストなど、アプリと同じ道具がそのまま使えます。 もう1つは、AIに判定だけでなく操作も任せられる点です。Maestroでは操作はコマンドで書きますが、e2eでは agent.act にゴールを渡すと、エージェントが操作を決めます。 Detoxとの違い Detoxはアプリの内部と同期するグレーボックスのテストなので安定しますが、アプリ側にDetoxを組み込む必要があります。 e2eはアクセシビリティツリーを外から読むので、いつものDevClientのビルドをそのままテストできます。 React Nativeの testID は、iOS/Androidのどちらでも getByTestId で引けます。 自然言語だけで書くAIテストツールとの違い 自然言語だけでシナリオを書くツールは、書くのは楽な一方で、何を確かめたのかが曖昧になりやすいです。 e2eのドキュメントでは、各ゴールの結果を expect で確かめることや、最後の判定をロケーターで決定的に書くことを勧めていて、AIは変わりやすい操作の部分を受け持つ位置づけです。 e2eの弱いところ AIのステップにはコストと待ち時間がかかるので、キャッシュが効く書き方にする必要がある アクセシビリティツリーに出ないもの(OSのFace IDのダイアログや、Androidでツリーに出ない要素など)は、visionに頼るか工夫が要る まだ0.xで、APIの変化が速い めろでの活用例 ここからは、めろでの使い方を、どんなテストをしたいときにどう書いているかでまとめます。 めろは生成AIのチャットアプリなので、おすすめの一覧やサーバーが返すフォームなど、毎回中身が変わる画面が多いです。 そうした画面の操作や判定はAIに任せ、仕様として決まっていることはロケーターで確かめる、という書き分けを基本にしています。 アプリ固有の用語をエージェントに伝えたいとき エージェントの設定の context に、アプリの説明や用語を書いておけます。 export default { targets: [ { name: "ios", engine: mobile({ platform: "ios" }), app: { bundleId: "..." } }, { name: "android", engine: mobile({ platform: "android" }), app: { bundleId: "..." } }, ], agents: { default: { // 手元では ChatGPT のサブスク、CI は API キー model: process.env.CI ? openrouter("openai/gpt-6-luna") : chatgpt("gpt-6-luna"), system: "You are a thorough QA agent. Verify every outcome on screen.", context: "mero は AI キャラクターとチャットする日本語のアプリ。画面の文言は日本語。「ストーリー」はキャラクターとの会話の題材、「トーク」はキャラクターとのチャット。", }, }, } satisfies E2EConfig; アプリの中だけで通じる言葉をここで教えておくと、ゴールの文を短く書けるようになりました。 並びが毎回変わる一覧から、どれか1つを開きたいとき おすすめの一覧のように並びが毎回変わる画面では、「何番目をタップする」とは書けません。 こういう操作は agent.act に任せて、開いた先の画面はロケーターで確かめています。 // どれを開くかはエージェントに任せる await agent.act("ホームの一覧からストーリーを 1 つ開く"); // 開いた先の画面は、ボタンの表示で確かめる await expect(screen.getByRole("button", /^トークを(はじめる|続ける)$/)).toBeVisible(); 一覧からagent.actで1つ開き、会話を始める レイアウトの崩れのように、ロケーターでは確かめにくい見た目を確かめたいとき チャットの画面で、キーボードを開いたままAIの返信が届き、そのあとキーボードを閉じると、入力欄が浮いたまま、一覧の下が真っ黒、ヘッダーが隠れたまま、という不具合が以前ありました。 こうした見た目の崩れは、要素がツリーにあるかどうかでは分かりません。 入力欄が下に戻ったことは位置をロケーターで確かめ、画面全体の見た目は agent.assert に { vision: true } を付けて、画面を見て判定させています。 // 入力欄の位置を測る const inputTop = async () => (await screen.getByTestId("talk-input").boundingBox())?.y ?? 0; const openedTop = await inputTop(); // 一覧をタップしてキーボードを閉じると、入力欄が下に戻る await screen.getByTestId("talk-message-list").tap(); await expect.poll(inputTop).toBeGreaterThan(openedTop); // 画面全体が崩れていないかは、画面を見て AI に判定させる await agent.assert( "キーボードは閉じていて、上部に戻るボタンとストーリー名のヘッダーが見え、入力欄は画面の下端にあり、一覧に真っ黒な領域は無い", { vision: true }, ); サーバー側で中身が変わるフォームを、最後まで進めたいとき めろには、質問に答えていくだけでAIキャラクターを作れる「かんたん作成」という機能があります。 相手の性別、キャラクター、言葉づかい、舞台といった質問に、候補から選んで答えていく形です。 この質問や選択肢はサーバーが返していて、運営側の設定で変わります。 そのため、質問ごとにテストを書くとすぐに壊れてしまいます。 回答はエージェントにまとめて任せて、確認画面に着いたことだけをロケーターで確かめています。 await agent.act( "各ステップでは、すべての質問について候補のボタンを 1 つずつ押して答えてから「次へ」を押す。確認の画面(「これで作る」ボタンがある)まで進む。「これで作る」は押さない", ); await expect(screen.getByRole("button", "これで作る")).toBeVisible({ timeout: 30_000 }); await agent.assert("確認の画面に、回答が表示されている項目が 1 つ以上ある", { vision: true }); そのあとの「編集して保存すると確認画面に戻る」「一時保存して閉じると、続きから再開できる」といった遷移は、ロケーターで書いています。 フォームの中身が変わってもテストは壊れず、確かめたい仕様は固定できます。 agent.actが質問に答えて辿り着いた確認画面 生体認証やバックグラウンドからの復帰を試したいとき @e2e-dev/mobile の device フィクスチャで、生体認証や権限、ホームボタン、アプリの切り替えも操作できます。 Face IDでのアプリロックは、次の流れで確かめています。 device.enrollBiometrics("faceid", true) でFace IDを登録する 設定からロックを有効にする device.home() から device.openApp() でアプリに戻り、ロックをかける device.setBiometrics("faceid", "nonmatch") で認証に失敗させ、ロックが外れないことを確かめる 再試行で "match" を返して解除する OSのダイアログはアプリのツリーに出ないので、Face IDの結果がアプリに反映されるまで送り直すヘルパーを用意しています。 送り直すのは20秒までで、それを過ぎても反映されなければテストを失敗にしているので、失敗を隠すことはありません。 アナリティクスのイベントが正しく送られたかも確かめたいとき これまでは、アナリティクスのイベントが送られているかを、実装のたびに手で確かめていました。 e2eでテストを書くついでに、イベントも同じテストで確かめられないかと思って試したのがこの仕組みです。 e2eのテストは普通のTypeScriptなので、テストの外の仕組みとも組み合わせられます。 めろでは、開発中にJSのバンドルを配信しているReact NativeのMetroサーバーに、ミドルウェアで /__e2e/events という受け口を足して、開発ビルドのアプリがアナリティクスのイベント(Firebase Analyticsなどに送るユーザーの行動ログ)を送るたびにそこへPOSTするようにしています。 テストからはそれを読んで、操作のあとに期待したイベントが送られたか、イベントのパラメータが正しいかを確かめています。 Metroサーバーと同じポートを使うので、CIでポートを変えても、Androidエミュレータ(10.0.2.2)からでもそのまま届きます。 アプリを一通り見て回って、画面の一覧を作りたいとき e2eには、ゴールだけを渡してアプリを探索させる e2e explore があります。 めろでは、この e2e explore を使って、画面マップを自作しています。 画面マップはe2eの機能ではなく、e2eの実行結果を元に自分たちで作っている社内のドキュメントです。 ホームや検索、詳細画面といった領域ごとに e2e explore で探索させて、画面ごとのスクリーンショットと送られたアナリティクスのイベントを集め、自作のスクリプトで画面と遷移をつないでいます。 社内のドキュメントサイトで画面を選ぶと、その画面の説明や仕様テストの内容、探索で見たことが横に出ます。 stagingのデータを変えないよう、探索には別のエージェントを定義して、context で行動を絞っています。 見るだけにすること。してよいのは、画面を開く・戻る・タブの切り替え・スクロール… してはいけないこと: メッセージの送信、購入や課金のボタンを押す… e2e.tester.army PRごとに回して、結果をレビューで見たいとき GitHub Actionsでは、次の流れで回しています。 @expo/fingerprint でネイティブ部分のハッシュを取り、同じハッシュのDevClientのビルドがキャッシュにあればダウンロード、なければビルドしてアップロードする Metroサーバーを起動して pnpm test:e2e を実行する @e2e-dev/github のレポーターが結果をPRに1件のコメントとして貼る(再実行すると同じコメントを更新する) レポートやスクリーンショットをアーティファクトとして保存する JSだけの変更ならネイティブのビルドを飛ばせるので、実行時間のほとんどはテストそのものです。 PRに付くコメントは次のようになります。 この回は全体で7分22秒かかり、AIを使ったのは5ステップ、モデルの呼び出しは12回、費用は$0.0041でした(モデルはGPT-6 Luna)。 agent.assert の判定は毎回モデルを呼ぶので、費用はキャッシュが効いても0にはならず、記録が使えない回はもう少し増えます。 e2e.tester.army 今はすべてのテストを回していますが、今後はe2eのテストフィルターも取り入れたいと考えています。 PRで --changed-since を付けて実行すると、Jevのような決定モデルがPRの差分とテストを読んで、その変更で影響を受けそうにないテストを外してくれます。 テストが増えても、PRで回すテストの数と時間を抑えられそうです。 e2e.tester.army Coding Agent向けのスキル e2eには、Claude CodeやCodex、CursorなどのCoding Agent向けのスキルが、現在3つ用意されています。 スキル 何をするか e2e テストを書く・実行する・失敗の原因を調べる。e2e mcp で起動中のアプリを操作して、画面を確かめたりロケーターを試したりもできる create-verification-skill プロジェクト専用の verify-<アプリ名> スキルと機能の一覧を作り、Coding Agentが自分の変更をユーザーと同じ操作で確かめられるようにする migrate-to-e2e Playwright・Cypress・Selenium・Detox・Maestroのテストを少しずつe2eに移す。進み具合は MIGRATION.md に残るので、次のセッションで続きから再開できる e2e のスキルは npx e2e init で .agents/skills/ と .claude/skills/ に入ります。めろでもこれで入れています。 3つまとめて入れたい場合は、Claude Code・Codex・Cursor向けのプラグインがあります(Claude Codeの例)。 claude plugin marketplace add tester-army/e2e claude plugin install e2e@e2e e2e.tester.army バグバッシュ e2e のスキルには、バグバッシュの手順も入っています。 「このブランチのチェックアウトとアカウント設定をバグバッシュして」のように頼むと、Coding Agentが次の流れで進めてくれます。 アプリの画面とブランチの差分を読んで、探索の目標を5〜10個立てる 目標ごとに e2e explore を走らせる(4つずつ並列) 見つかった問題のうち、探索の限界や意図した仕様によるものを外す 残った問題ごとに再現テストを書いて実行し、テストが失敗したものだけをバグとして報告する AIが見つけた問題をそのまま報告するのではなく、再現テストで確かめてから報告してくれるのが良いところです。 めろではまだ試せていませんが、リリース前の確認などで使ってみたいと思っています。 e2e.tester.army 導入してみて よかった点 変わる部分と変わらない仕様を分けて書ける。おすすめの一覧やサーバー駆動のフォーム、ロケーターでは確かめにくい見た目など、めろには従来のE2Eと相性の悪い部分が多いですが、そこをAIに任せつつ、確かめたい仕様はロケーターで固定できます 普通のTypeScriptで書ける。ヘルパーをVitestでテストしたり、アナリティクスのイベントの検証や画面マップの生成を自分たちで足したりと、アプリの開発と同じ感覚で広げられます アプリに何も組み込まなくていい。いつものDevClientのビルドをそのまま使えます Coding Agentに書かせやすい。e2eはパッケージにドキュメントとエージェント向けのスキルを同梱しているので、Claude Codeに「この機能のe2eを書いて」と頼むと、規約に沿ったテストを書いてくれます 気をつけている点 1つのゴールに1つの結果。agent.act に複数のことを頼まず、ゴールのあとには必ず expect を置きます(キャッシュもこれが前提です) 状態を残さない。下書きやロックの設定など、前の実行の残りで次のテストが変わらないよう、テストの最初と最後で片付けます プラットフォームの差は書いておく。Androidではツリーに出ない要素があるので、確かめられない部分はコメントに書いてiOSだけで確かめています まとめ e2eは、AIを使うところを人が選び、最後の判定はロケーターで決定的に行う設計なので、実際の運用に乗せやすいと感じています。 npx e2e init このコマンドでWeb/モバイルのどちらかのエンジンとモデルを選ぶだけで導入でき、設定とサンプルのテストもできあがります。 Maestroなどからの移行用のスキル(migrate-to-e2e)も用意されているので、既存のMaestroのフローがあれば、それを元に試すこともできます。 e2e.tester.army 生成AIを使ったアプリのように、出力が毎回変わる画面のE2Eに悩んでいる方は、まずは触ってみることをおすすめします! それでは良いReact Nativeライフを! 参考リンク e2e(GitHub) e2eドキュメント Core concepts Replay cache Subscriptions Test filters Coding agents Bug bash Maestroからの移行ガイド agent-device(GitHub) Rozeniteとagent-deviceを紹介した過去記事
Test Automation with agent-device (Part 1) --- title: "Test Automation with agent-device (Part 1)" published: true description: "Why agent-device for Flutter app testing, how to install it for Claude Code, the basic command loop, and Flutter gotchas." tags: claude, ai, flutter, testing canonical_url: krenin88.github.io --- agent-device is an open-source tool from Callstack that lets an AI agent drive iOS and Android apps and check what is on screen. This post covers why it was picked for testing a Flutter app, how to install it for Claude Code, the basic command loop, and what to watch out for with Flutter. It is based on version 0.21.31 (2026-10-24). Why agent-device Flutter ships Android and iOS apps from one codebase, so the device-testing tool has to cover iOS too. Google Artemis, the first candidate, is a natural-language testing agent, but it supports Android only; iOS is still just a roadmap item. The other candidates looked like this: - mobile-mcp exposes taps, screenshots, and the accessibility tree on iOS and Android over MCP (the protocol an AI uses to call external tools). It has no sessions, replay, or evidence storage, so a verified flow is hard to hand off to CI. - Maestro is an E2E framework that pins flows in YAML, and it has an MCP server. It suits regression runs of fixed flows better than an agent exploring the screen. - Arbigent is an AI agent built on top of Maestro. Its author rates it 1/5 for speed and 3/5 for reliability, and its YAML format was still changing. - Commercial services (Drizz, Minitap, and others) bundle a real-device farm with a natural-language agent, but the only public material was vendor material, so cost and quality could not be checked. agent-device handles iOS and Android simulators, emulators, and real devices with one tool, and its docs state Flutter support explicitly. It ships both a CLI and an MCP server, can save a verified flow as an .ad script to replay later, and can export to Maestro YAML. It is MIT-licensed. Installation Install the CLI globally and check the environment. npm i -g agent-device@latest agent-device doctor doctor checks connected devices, toolchains, and dev-server status. In this setup it passed with only warnings for unused platforms (HarmonyOS, Vega). Register it with Claude Code as an MCP server. With -s user, the same tool is available in every project. claude mcp add -s user agent-device -- agent-device mcp The agent-device repository includes three agent skills: agent-device, ios-simulator, and android-emulator. The official instruction is npx skills add callstack/agent-device, but Claude Code's auto mode safety check blocked it. In that case, clone the repository and copy the three folders under skills/ into ~/.claude/skills/. To undo everything, run claude mcp remove agent-device -s user and npm uninstall -g agent-device, then delete the three skill folders. The basic loop The basic loop is: prepare a device, open the app, act, verify, close. Pick the device with --platform and --device. agent-device boot --platform android --device "<emulator name>" --headless agent-device install /absolute/path/app-debug.apk --platform android --device "<emulator name>" agent-device open <bundle-id> --platform android --device "<emulator name>" --foreground agent-device press @e5 --settle agent-device fill @e11 "text" --settle agent-device wait text "<expected text>" agent-device screenshot /absolute/path/home.png agent-device close open reads the screen and returns a ref such as @e5 for each element, like buttons and text fields. With --settle, press and fill wait until the screen goes quiet and return only what changed, so the agent can pick its next target without rereading the whole screen. Three things commonly go wrong here: - Refs go stale when the screen changes. Pick a new ref from the diff returned by each action. - A screenshot is not verification. Verify with wait text or wait <selector>, and keep screenshots as evidence only. - Give install an absolute path. A relative path resolves against the agent-device daemon's working directory and fails with "App binary not found". For a Flutter app, build the iOS simulator app with flutter build ios --simulator --debug and the Android app with flutter build apk --debug. Flutter gotchas agent-device reads the screen through the accessibility tree. In Flutter, that means only widgets with text show up by default; icon buttons and containers do not. Add Semantics(identifier:) or semanticLabel to those widgets so a selector can find them. Flutter's Key does not appear in the accessibility tree. Labels in the accessibility tree follow the language the device is set to, so the same app reads with different label text depending on that setting. A text-based selector changes with the device language, which makes identifier the more stable choice. On iOS, AdMob test banners show up as platform_view, webview, and link nodes. Selectors in replay scripts should avoid those nodes. References - agent-device repository: <callstack/agent-device> - agent-device agent setup docs: <oss.callstack.com> - Google Artemis: <google/artemis> - Maestro Flutter support docs: <docs.maestro.dev>
Agents that can see and tap your app are a big part of what we cover in The Agentic Rewind 👇 Join The Agentic Rewind: trnrw.com agent-device: callstack/agent-device
use agent-device for agent verification locally or remotely, whatever, it just works callstack/agent-device