Mobile
MobileEngine drives an iOS simulator or an Android emulator through agent-device. The tests use the same Agent, Screen, and Expect as web tests. For a .NET MAUI app, see .NET MAUI.
Prerequisites
Section titled “Prerequisites”- macOS with Xcode and an iOS simulator runtime, or the Android SDK with an emulator.
- Node 22.12 or newer.
MobileEnginestarts agent-device throughnpx. - Check the setup once:
npx --yes agent-device@0.21.29 doctor. - An app build: an
.appbundle for the simulator, or an.apk.
1. Create the test project
Section titled “1. Create the test project”dotnet new nunit -n MyApp.MobileTestscd MyApp.MobileTestsdotnet add package E2E.Mobile.NUnit # or E2E.Mobile.XUnit.V32. Point at your app
Section titled “2. Point at your app”{ "targets": [ { "name": "ios", "platform": "ios", "app": { "bundleId": "com.example.myapp", "appPath": "../MyApp/build/MyApp.app" } } ], "actionTimeout": 15000, "assertionTimeout": 10000, "agents": { "default": { "provider": "copilot", "model": "claude-sonnet-5.5", "context": "MyApp is a small demo app with a greeting form and a counter." } }, "cache": { "dir": ".e2e/cache" }}appPath is relative to the folder of e2e.config.json. A device target has no URL. For Android, set "platform": "android" and appPath to the .apk, or run with E2E_MOBILE_PLATFORM=android and E2E_MOBILE_APP_PATH.
3. Write a test
Section titled “3. Write a test”Derive from MobileAppTest. It builds a MobileEngine from the config, installs the build once per run, launches the app fresh before each test, and gives the agent the open_app, swipe, and alert tools.
using E2E.Mobile.NUnit;
public sealed class GreetingTests : MobileAppTest{ [Test] public async Task Greets_the_name_you_type() { await Screen.GetByTestId("name").FillAsync("Ada"); await Screen.GetByTestId("greet").TapAsync();
await Expect.That(Screen.GetByTestId("greeting")).ToHaveTextAsync("Hello, Ada!"); }
[Test] public async Task The_agent_gets_a_greeting() { await Agent.ActAsync("get the app to greet {name}", new ActOptions { Params = new Dictionary<string, object?> { ["name"] = "Grace" }, });
await Expect.That(Screen.GetByTestId("greeting")).ToHaveTextAsync("Hello, Grace!"); }}GetByTestId finds a control by its accessibility identifier. Device is the device fixture: HomeAsync, OpenAppAsync, ForegroundAppAsync, and SetAppearanceAsync(Appearance.Dark).
await Device.HomeAsync();await Device.OpenAppAsync(AppId, new OpenAppOptions { Relaunch = true });Device.Locator takes an agent-device selector, for controls the Screen queries cannot name. It polls and is strict like any locator:
await Expect.That(Device.Locator("role=navigation-bar id=About")).ToBeVisibleAsync();await Device.Locator("id=SW_VERSION_SPECIFIER role=button").TapAsync();4. Run
Section titled “4. Run”Boot exactly one simulator, then run:
xcrun simctl boot "iPhone 17"dotnet test --project .To pick a device, set E2E_MOBILE_DEVICE to a UDID (xcrun simctl list devices available). With two booted or matching devices the run fails with AMBIGUOUS_MATCH.
Tag device tests, for example [assembly: Category("Device")], so a machine without a device can skip them.
Hosted devices
Section titled “Hosted devices”Set E2E_MOBILE_PROVIDER=limrun with LIMRUN_API_KEY to run on a hosted Limrun simulator. E2E.Eas and E2E.TestMu.Mobile add other providers. A provider whose service records its devices records the video of an attempt in place of agent-device (IDeviceProvider.RecordAsync). See the mobile sample README for the full variable list.