Goal
Why this matters
Prerequisites
http://127.0.0.1:4723.schtasks or a Windows Service wrapper so CI agents bring it up automatically.npm install -g appium). For Appium 1, the built-in mac driver is sufficient; for Appium 2 install the mac2 driver (appium driver install mac2).external/Avalonia/tests/Avalonia.IntegrationTests.Appium/readme.md).samples/IntegrationTestApp/bundle.sh builds and publishes the bundle.appium --base-path=/wd/hub.The harness toggles between Appium 1 and 2 using the IsRunningAppium2 property (external/Avalonia/tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj:5). Set the property to true in Directory.Build.props or via dotnet test -p:IsRunningAppium2=true when running against Appium 2.
Appium launches desktop apps by path (Windows) or bundle identifier (macOS). The Avalonia sample uses IntegrationTestApp and rebuilds it before each run:
external/Avalonia/tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh:1) cleans the repo, compiles native dependencies, bundles the app, and opens it once to register Launch Services.external/Avalonia/azure-pipelines-integrationtests.yml:42) builds IntegrationTestApp and the test project before running dotnet test.When testing your own app:
TEST_APP_PATH, TEST_APP_BUNDLE).DefaultAppFixture and override ConfigureWin32Options / ConfigureMacOptions to use those values. Example:protected override void ConfigureWin32Options(AppiumOptions options, string? app = null)
{
base.ConfigureWin32Options(options, Environment.GetEnvironmentVariable("TEST_APP_PATH"));
}
protected override void ConfigureMacOptions(AppiumOptions options, string? app = null)
{
base.ConfigureMacOptions(options, Environment.GetEnvironmentVariable("TEST_APP_BUNDLE"));
}
OverlayPopupsAppFixture adds --overlayPopups on both platforms (external/Avalonia/tests/Avalonia.IntegrationTests.Appium/OverlayPopupsAppFixture.cs:4).Automation servers must be running when tests start and shut down afterward. Avalonia’s pipelines demonstrate the sequence:
pkill node, pkill IntegrationTestApp), starts Appium in the background, bundles the app, launches it, runs dotnet test, then terminates Appium and the app again (external/Avalonia/tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh:6).external/Avalonia/azure-pipelines-integrationtests.yml:32). When scripting locally, run Start-Process "WinAppDriver.exe" before tests and Stop-Process -Name WinAppDriver afterward.General guidelines:
pkill, Stop-Process) on both success and failure to keep subsequent runs deterministic.appium > appium.out &). Publish them when the job fails for easier triage (see pipeline’s publish appium.out step).Device clouds (BrowserStack App Automate, Sauce Labs, Azure-hosted desktops) require the same capabilities plus authentication tokens:
options.AddAdditionalCapability("browserstack.user", Environment.GetEnvironmentVariable("BS_USER"));
options.AddAdditionalCapability("browserstack.key", Environment.GetEnvironmentVariable("BS_KEY"));
options.AddAdditionalCapability("appium:options", new Dictionary<string, object>
{
["osVersion"] = "11",
["deviceName"] = "Windows 11",
["appium:app"] = "bs://<uploaded-app-id>"
});
Upload your Avalonia app (packaged exe zipped, or macOS .app bundle) via the vendor’s CLI before tests run. On hosted Windows machines, ensure the automation provider exposes UI Automation trees—some locked-down images disable it.
When targeting clouds, keep these adjustments in fixtures:
protected override void ConfigureWin32Options(AppiumOptions options, string? app = null)
{
if (UseCloud)
{
options.AddAdditionalCapability("app", CloudAppId);
options.AddAdditionalCapability("bstack:options", new { osVersion = "11", sessionName = TestContext.CurrentContext.Test.Name });
}
else
{
base.ConfigureWin32Options(options, app);
}
}
Guard cloud-specific behavior using environment variables so local runs stay unchanged.
The harness conditionally compiles for Appium 1 vs. 2 via APPIUM1/APPIUM2 constants (AppiumDriverEx.cs). Checklist:
dotnet test -p:IsRunningAppium2=true when hitting Appium 2 endpoints. This updates DefineConstants and switches to the newer Appium.WebDriver 5.x client.If you see protocol errors, print the server log (appium.out) and compare capability names. Appium 2 requires appium: prefixes for vendor-specific entries (already shown in DefaultAppFixture.ConfigureMacOptions).
Desktop automation breaks when the app lacks accessibility permissions:
System Settings → Privacy & Security → Accessibility. The readme covers the exact steps.Automate these steps where possible—on macOS you can pre-provision a profile or run a script to enable permissions via tccutil. For Windows, prefer an image with WinAppDriver pre-installed.
Augment your harness to collect evidence:
appium --log-level info --log appium.log to write structured JSON logs.Session.Manage().Logs.GetLog("driver"); after a failure.HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\WinAppDriver\ConsoleLogging = 1).record-video.runsettings file when executing through VSTest (Chapter 42).SessionNotCreatedException – Check that the app path/bundle exists and the process isn’t already running. On macOS, run osascript cleanup like the sample script to delete stale bundles.Could not find app – Re-run your packaging script; the bundle path changes when switching architectures (osx-arm64 vs. osx-x64).IsRunningAppium2 with the server version. Appium 2 rejects legacy capability names like bundleId without the appium: prefix.Session accesses in try/finally or use IAsyncLifetime to guarantee cleanup after each class.scripts/run-appium-tests.ps1 and .sh) that build your app, start/stop automation servers, and invoke dotnet test. Validate they leave no background processes.DefaultAppFixture to read capabilities from JSON (local vs. cloud). Add tests that assert the chosen configuration by inspecting Session.Capabilities.IsRunningAppium2. Capture and compare server logs to understand protocol differences.What's next