43. Appium fundamentals for Avalonia apps

Goal

Why this matters

Prerequisites

1. Anatomy of the Avalonia Appium harness

Avalonia ships an Appium test suite in external/Avalonia/tests/Avalonia.IntegrationTests.Appium. Key parts:

Reuse this structure in your own project: create a fixture that launches your app (packaged exe/bundle), expose the AppiumDriver, and derive page-specific test classes from a TestBase that performs navigation.

2. Configure sessions per platform

DefaultAppFixture populates capability sets tailored to each OS:

var options = new AppiumOptions();
if (OperatingSystem.IsWindows())
{
    options.AddAdditionalCapability(MobileCapabilityType.App, TestAppPath);
    options.AddAdditionalCapability(MobileCapabilityType.PlatformName, MobilePlatform.Windows);
    options.AddAdditionalCapability(MobileCapabilityType.DeviceName, "WindowsPC");
    Session = new WindowsDriver(new Uri("http://127.0.0.1:4723"), options);
}
else if (OperatingSystem.IsMacOS())
{
    options.AddAdditionalCapability("appium:bundleId", "net.avaloniaui.avalonia.integrationtestapp");
    options.AddAdditionalCapability(MobileCapabilityType.PlatformName, MobilePlatform.MacOS);
    options.AddAdditionalCapability(MobileCapabilityType.AutomationName, "mac2");
    Session = new MacDriver(new Uri("http://127.0.0.1:4723/wd/hub"), options);
}

The fixture also foregrounds the window on Windows via SetForegroundWindow to avoid focus issues. Always close the session in Dispose even if Appium errors—macOS’ mac2 driver may throw on shutdown, so wrap in try/catch like the sample.

TIP: keep Appium/WAD endpoints configurable via environment variables so your CI scripts can point to remote device clouds.

TestBase selects a page by finding the pager control and clicking the relevant button. The same pattern applies to your app:

public class WindowTests : TestBase
{
    public WindowTests(DefaultAppFixture fixture) : base(fixture, "Window") { }

    [Fact]
    public void Can_toggle_window_state()
    {
        var windowStateCombo = Session.FindElementByAccessibilityId("CurrentWindowState");
        windowStateCombo.Click();
        Session.FindElementByAccessibilityId("WindowStateMaximized").SendClick();
        Assert.Equal("Maximized", windowStateCombo.GetComboBoxValue());
    }
}

The pageName passed to TestBase must match the accessible name exposed by the pager button. Avalonia’s sample ControlCatalog sets these via AutomationProperties.Name, so always annotate navigation controls in your app for consistent selectors.

4. Element discovery and helper APIs

Selectors vary subtly across platforms. Avalonia’s helpers hide those differences:

When building your suite, add similar extension methods instead of hard-coding XPath per test. Keep selectors rooted in AutomationId or names you control via AutomationProperties.AutomationId and Name to minimize brittleness.

5. Synchronization and retries

Appium commands are asynchronous relative to the app. Avalonia tests mix explicit waits, retries, and timeouts:

Wrap these patterns in helper methods so timing tweaks stay centralized. For more resilient waits, use Appium’s WebDriverWait with conditions such as driver.FindElementByAccessibilityId(...) or element.Displayed.

6. Cross-platform control with attributes and collections

Automation suites often need OS-specific assertions. Avalonia uses:

Follow suit by tagging tests with custom attributes that read environment variables or capability flags. This keeps your suite from failing on agents lacking certain features (e.g., Win32-only APIs).

7. Exposing automation IDs in Avalonia

Appium relies on the accessibility tree. Avalonia maps these properties as follows:

Ensure the controls you interact with set both AutomationId and Name; for templated controls expose IDs through x:Name or Automation.Id. Without these properties, selectors fall back to fragile XPath queries.

8. Running the suite

Windows

  1. Install WinAppDriver (ships with Visual Studio workloads) and start it on port 4723.
  2. Build your Avalonia app for net8.0-windows with UseWindowsForms disabled (the sample uses IntegrationTestApp).
  3. Launch Appium tests: dotnet test tests/Avalonia.IntegrationTests.Appium --logger "trx;LogFileName=appium.trx".

macOS

  1. Install Appium 2 with the mac2 driver and run appium --base-path /wd/hub.
  2. Ensure the test runner has accessibility permissions; the pipeline script resets them via pkill and osascript (external/Avalonia/azure-pipelines-integrationtests.yml:17).
  3. Bundle the app (samples/IntegrationTestApp/bundle.sh) so Appium can reference it by bundle ID.

Use the provided macos-clean-build-test.sh as a reference for orchestrating builds locally or in CI.

9. Troubleshooting

Practice lab

  1. Custom fixture – Implement a fixture that launches your app under test, parameterized by environment variables for executable path and Appium endpoint.
  2. Navigation helper – Create a TestBase that navigates your shell’s menu/pager via automation IDs, then write a smoke test asserting window title, version label, or status bar text.
  3. Selector audit – Add AutomationId attributes to controls in a sample page, write tests that locate them by accessibility ID, and verify they remain stable after theme changes.
  4. Cross-platform skip logic – Introduce [PlatformFact]-style attributes that read from RuntimeInformation and feature flags (e.g., skip tray icon tests on macOS), then apply them to OS-specific suites.
  5. Wait strategy – Replace any Thread.Sleep in your tests with a reusable wait helper that polls for element state using Appium’s WebDriverWait, ensuring the helper raises descriptive timeout errors.

What's next