Skip to content

UI Test Coding Agent

Shane Neuville edited this page Dec 13, 2025 · 2 revisions

The UI Test Coding Agent is a specialized AI assistant for writing new UI tests for .NET MAUI with proper syntax, style, and conventions.

How to Use This Agent


What It Does

The UI Test Coding Agent:

  • ✅ Creates new UI test pages in TestCases.HostApp
  • ✅ Writes NUnit tests in TestCases.Shared.Tests
  • ✅ Follows MAUI UI testing conventions and patterns
  • ✅ Uses proper AutomationIds and Appium interactions
  • ✅ Applies correct test categories
  • ✅ Matches repository testing standards

When to Use

Use the UI Test Coding Agent when:

  • Creating new UI tests for a bug fix or feature
  • Adding test coverage for existing functionality
  • Converting manual test scenarios into automated tests
  • Updating existing tests with new test cases

Example Prompts

write UI test for issue #12345
write UI test for CollectionView item selection
create UI test that verifies Button click updates a Label
write UI test for Android SafeArea issue #12345
add a test case to Issue12345.cs that verifies the bug doesn't happen after rotation
write UI test that:
1. Navigates to a new page
2. Fills out a form
3. Submits it
4. Verifies success message appears
write UI test for issue #12345 that measures the Grid layout and verifies child element positions
write UI test for https://github.com/dotnet/maui/issues/12345
write UI test for Layout issue #12345 - make sure to use UITestCategories.Layout

Additional Example Prompts

Provide more context for better results:

write UI test for issue #12345 which is about CollectionView crashing on item removal
write UI test for https://github.com/dotnet/maui/issues/12345
write UI test for Android-specific SafeArea bug in issue #12345
write UI test that verifies:
- Button is visible on load
- Tapping button changes label text
- Label text persists after navigation
write UI test for Layout issue #12345 - make sure to use UITestCategories.Layout
write test that taps a Button and verifies a Label updates
write test that enters text in Entry and submits a form
write test that navigates to a new page and verifies page content
write test for Shell navigation with multiple tabs
write test that measures Grid row heights after layout
write test that verifies SafeArea padding on iOS
write test that verifies button VisualState changes on tap
write test that checks element visibility after property change
the test you created has a build error: [paste error]
the test can't find element "TestButton" - can you check the AutomationIds?
change the test category to UITestCategories.Layout instead of UITestCategories.Button

How the UITest-Coding-Agent Works: Complete Guide

Overview: Two-Project Architecture

The uitest-coding-agent creates automated UI tests for .NET MAUI using a two-project structure. This is fundamentally different from the sandbox-agent:

Aspect Sandbox Agent UITest-Coding Agent
Purpose Manual testing & validation Automated regression testing
Projects 1 (Sandbox app only) 2 (HostApp + Test project)
Test Framework Custom Appium script NUnit with Appium infrastructure
Execution BuildAndRunSandbox.ps1 BuildAndRunHostApp.ps1 + dotnet test
Persistence App stays running for manual validation Tests run and complete automatically
Output Device logs, screenshots NUnit test results, device logs

The Two-Project Structure

flowchart LR
    A[TestCases.HostApp] -->|contains| B[Issue12345.xaml]
    B -->|UI page with AutomationIds| C[Deployed to device]
    D[TestCases.Shared.Tests] -->|contains| E[Issue12345.cs]
    E -->|NUnit test using Appium| F[Runs dotnet test]
    F -->|connects via Appium| C
Loading

Project 1: TestCases.HostApp 📱

Location: src/Controls/tests/TestCases.HostApp/

Purpose: Contains the actual MAUI app with test pages

Structure:

TestCases.HostApp/
├── Controls.TestCases.HostApp.csproj
└── Issues/
    ├── Issue12345.xaml          # UI page
    └── Issue12345.xaml.cs        # Code-behind with [Issue] attribute

Example XAML (Issues/Issue12345.xaml):

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="Microsoft.Maui.Controls.ControlGallery.Issue12345">
    
    <VerticalStackLayout>
        <Label Text="Tap the button below"
               AutomationId="InstructionLabel" />
        
        <Button Text="Click Me"
                AutomationId="TestButton"
                Clicked="OnButtonClicked" />
        
        <Label x:Name="ResultLabel"
               AutomationId="ResultLabel"
               Text="Not clicked yet" />
    </VerticalStackLayout>
    
</ContentPage>

Example Code-Behind (Issues/Issue12345.xaml.cs):

using Microsoft.Maui.Controls;

namespace Maui.Controls.Sample.Issues;

[Issue(IssueTracker.Github, 12345, "Button click should update label", PlatformAffected.All)]
public partial class Issue12345 : ContentPage
{
    public Issue12345()
    {
        InitializeComponent();
    }
    
    private void OnButtonClicked(object sender, EventArgs e)
    {
        ResultLabel.Text = "Button was clicked!";
    }
}

Key Points:

  • ✅ Every element that tests interact with needs an AutomationId
  • ✅ Must include [Issue()] attribute with tracker, number, description, platform
  • ✅ File naming: IssueXXXXX.xaml and IssueXXXXX.xaml.cs

Project 2: TestCases.Shared.Tests 🧪

Location: src/Controls/tests/TestCases.Shared.Tests/

Purpose: Contains NUnit tests that use Appium to interact with HostApp

Structure:

TestCases.Shared.Tests/
├── Controls.TestCases.Shared.Tests.csproj
└── Tests/
    └── Issues/
        └── Issue12345.cs         # NUnit test

Example Test (Tests/Issues/Issue12345.cs):

using NUnit.Framework;
using UITest.Appium;
using UITest.Core;

namespace Microsoft.Maui.TestCases.Tests.Issues;

public class Issue12345 : _IssuesUITest
{
    public override string Issue => "Button click should update label";

    public Issue12345(TestDevice device) : base(device) { }

    [Test]
    [Category(UITestCategories.Button)]
    public void ButtonClickShouldUpdateLabel()
    {
        // Wait for UI to be ready
        App.WaitForElement("TestButton");
        
        // Verify initial state
        var initialText = App.FindElement("ResultLabel").GetText();
        Assert.That(initialText, Is.EqualTo("Not clicked yet"));
        
        // Perform action
        App.Tap("TestButton");
        
        // Verify result
        var updatedText = App.FindElement("ResultLabel").GetText();
        Assert.That(updatedText, Is.EqualTo("Button was clicked!"));
    }
}

Key Points:

  • ✅ Inherits from _IssuesUITest (provides App property and infrastructure)
  • ✅ Constructor takes TestDevice parameter
  • ✅ Must have [Test] attribute and exactly ONE [Category] attribute
  • ✅ Use descriptive test method names

Complete Workflow

flowchart TD
    A[Agent Creates HostApp Page] --> B[Agent Creates NUnit Test]
    B --> C[Run BuildAndRunHostApp.ps1]
    C --> D[Build HostApp]
    D --> E[Deploy to Device]
    E --> F[Run dotnet test]
    F --> G[NUnit starts test]
    G --> H[Test connects via Appium]
    H --> I[Test interacts with UI]
    I --> J[Assertions validate behavior]
    J --> K[Test completes with Pass/Fail]
    K --> L[Logs captured]
Loading

Step-by-Step: How BuildAndRunHostApp.ps1 Works

Phase 1: Prerequisites ✅

# 1. Create log directory
CustomAgentLogsTmp/UITests/
├── android-device.log       # Android logcat
├── ios-device.log           # iOS device logs
└── test-output.log          # dotnet test output

# 2. Validate projects exist
✅ TestCases.HostApp.csproj exists
✅ TestCases.Shared.Tests.csproj exists
✅ dotnet is available

Phase 2: Device Management 📱

Same as sandbox-agent - uses shared/Start-Emulator.ps1

# Android
$DeviceUdid = "emulator-5554"

# iOS
$DeviceUdid = "AC8BCB28-..."  # iPhone Xs simulator

Phase 3: Build HostApp 🏗️

# Build the HostApp
dotnet build src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj `
    -f net10.0-android `
    -c Debug `
    -p:TargetFramework=net10.0-android

Result: HostApp built with all test pages included


Phase 4: Deploy HostApp 📦

# Android
adb -s $DeviceUdid install -r bin/Debug/net10.0-android/*.apk

# iOS
xcrun simctl install $DeviceUdid bin/Debug/net10.0-ios/*.app

Result: HostApp installed on device with all Issue pages


Phase 5: Run dotnet test 🧪 ← KEY DIFFERENCE FROM SANDBOX

# Construct test filter
if ($TestFilter) {
    $filter = $TestFilter  # e.g., "FullyQualifiedName~Issue12345"
}
elseif ($Category) {
    $filter = "Category=$Category"  # e.g., "Category=Button"
}

# Set environment variables for the test
$env:DEVICE_UDID = $DeviceUdid
$env:PLATFORM = $Platform

# Run dotnet test
dotnet test src/Controls/tests/TestCases.Shared.Tests/Controls.TestCases.Shared.Tests.csproj `
    --filter $filter `
    --logger "console;verbosity=detailed" `
    --no-build `
    -v:n `
    > test-output.log 2>&1

What happens here:

  1. dotnet test starts NUnit test runner
  2. Test runner finds tests matching filter (e.g., Issue12345)
  3. NUnit creates instance of test class: new Issue12345(device)
  4. Test framework calls [Test] methods
  5. Test uses App property to interact with UI via Appium

What Happens During Test Execution

Step 1: Test Infrastructure Initialization

// _IssuesUITest base class does this automatically
public class Issue12345 : _IssuesUITest
{
    public Issue12345(TestDevice device) : base(device) 
    {
        // Base class:
        // 1. Connects to Appium at http://localhost:4723
        // 2. Configures platform-specific options (Android/iOS)
        // 3. Launches the HostApp
        // 4. Navigates to Issue12345 page
        // 5. Provides 'App' property for test to use
    }
}

Step 2: Test Method Execution

[Test]
[Category(UITestCategories.Button)]
public void ButtonClickShouldUpdateLabel()
{
    // This is YOUR test logic
    App.WaitForElement("TestButton");
    App.Tap("TestButton");
    var text = App.FindElement("ResultLabel").GetText();
    Assert.That(text, Is.EqualTo("Button was clicked!"));
}

Step 3: Appium Communication

Test Method
    ↓
App.WaitForElement("TestButton")
    ↓
AppiumDriver.FindElement(By.Id("TestButton"))
    ↓
HTTP POST to http://localhost:4723/session/{id}/element
    ↓
Appium Server
    ↓
UIAutomator2/XCUITest (on device)
    ↓
Finds element with resource-id="TestButton"
    ↓
Returns element to test

Phase 6: Log Capture 📝

Android:

# Get app package name
$packageName = "com.microsoft.maui.uitests"

# Get PID
$pid = & adb -s $DeviceUdid shell pidof -s $packageName

# Capture logs filtered by PID
& adb -s $DeviceUdid logcat -d --pid=$pid > android-device.log

iOS:

# Capture simulator logs
xcrun simctl spawn $DeviceUdid log stream `
    --predicate 'processImagePath contains "HostApp"' `
    > ios-device.log

Key Differences: Sandbox vs UITest

Feature Sandbox Agent UITest-Coding Agent
Test Framework Custom C# script NUnit with test infrastructure
Base Class None (standalone script) Inherits from _IssuesUITest
App Lifecycle App stays running App managed by test framework
Navigation Manual in script Automatic by _IssuesUITest
Test Discovery Single script Multiple tests via dotnet test --filter
Output Console output NUnit test results (Pass/Fail/Skip)
CI Integration Manual script execution Native dotnet test integration
Assertions Manual (Console.WriteLine) NUnit assertions (Assert.That)

The _IssuesUITest Base Class

What it provides:

public class _IssuesUITest
{
    // Automatic navigation to test page
    protected _IssuesUITest(TestDevice device)
    {
        // 1. Connects to Appium
        // 2. Launches HostApp
        // 3. Navigates to Issue page (e.g., Issue12345)
    }
    
    // App instance for UI interactions
    protected IApp App { get; }
    
    // Helper methods
    protected void VerifyScreenshot(string screenshotName = null);
    protected void WaitForElement(string automationId, int timeout = 30);
    // ... more helpers
}

Why this matters:

  • ✅ No need to write Appium connection code
  • ✅ Automatic navigation to correct test page
  • ✅ Consistent test structure across all tests
  • ✅ Built-in helpers for common operations

Common Test Patterns

Pattern 1: Simple Interaction Test

[Test]
[Category(UITestCategories.Button)]
public void ButtonClickUpdatesLabel()
{
    App.WaitForElement("TestButton");
    App.Tap("TestButton");
    
    var text = App.FindElement("ResultLabel").GetText();
    Assert.That(text, Is.EqualTo("Expected"));
}

Pattern 2: Navigation Test

[Test]
[Category(UITestCategories.Navigation)]
public void NavigateToDetailPage()
{
    App.WaitForElement("NavigateButton");
    App.Tap("NavigateButton");
    
    // Verify we're on the new page
    App.WaitForElement("DetailPageLabel");
    
    VerifyScreenshot();
}

Pattern 3: Layout Measurement Test

[Test]
[Category(UITestCategories.Layout)]
public void SafeAreaPaddingApplied()
{
    App.WaitForElement("ContentGrid");
    
    var rect = App.FindElement("ContentGrid").GetRect();
    
    // Verify bottom padding
    Assert.That(rect.Y + rect.Height, Is.LessThan(screenHeight));
}

Running Tests

Run Specific Test

# By test name
pwsh .github/scripts/BuildAndRunHostApp.ps1 `
    -Platform android `
    -TestFilter "FullyQualifiedName~Issue12345"

# By partial name
pwsh .github/scripts/BuildAndRunHostApp.ps1 `
    -Platform android `
    -TestFilter "Issue12345"

Run by Category

# All button tests
pwsh .github/scripts/BuildAndRunHostApp.ps1 `
    -Platform android `
    -Category "Button"

# All layout tests
pwsh .github/scripts/BuildAndRunHostApp.ps1 `
    -Platform ios `
    -Category "Layout"

Run on Specific Device

# iOS with specific simulator
pwsh .github/scripts/BuildAndRunHostApp.ps1 `
    -Platform ios `
    -TestFilter "Issue12345" `
    -DeviceUdid "AC8BCB28-A72D-4A2D-90E7-E78FF0BA07EE"

Test Categories

CRITICAL: Every test must have exactly ONE category.

Common categories (see UITestCategories.cs for complete list):

  • Button - Button control tests
  • Label - Label control tests
  • Entry - Entry control tests
  • CollectionView - CollectionView tests
  • ListView - ListView tests
  • Layout - Layout-related tests
  • Navigation - Navigation tests
  • SafeAreaEdges - Safe area tests
  • Gestures - Gesture recognition tests

Check the full list:

grep "public const string" src/Controls/tests/TestCases.Shared.Tests/UITestCategories.cs

Output & Results

Test Output Format

Starting test execution, please wait...
A total of 1 test files matched the specified pattern.

Passed! - Failed: 0, Passed: 1, Skipped: 0, Total: 1, Duration: 15s

Log Files

CustomAgentLogsTmp/UITests/
├── test-output.log         # dotnet test output
├── android-device.log      # Android logcat
└── ios-device.log          # iOS device logs

Analyzing Results

# Check test results
cat CustomAgentLogsTmp/UITests/test-output.log | grep -E "Passed|Failed"

# Check device logs for app behavior
cat CustomAgentLogsTmp/UITests/android-device.log | grep "MAUI"

Complete Example: Issue12345

File 1: HostApp Page

TestCases.HostApp/Issues/Issue12345.xaml:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="Maui.Controls.Sample.Issues.Issue12345">
    
    <VerticalStackLayout Padding="20" Spacing="10">
        <Label Text="Test SafeArea Bottom Padding"
               AutomationId="InstructionLabel"
               FontSize="20" />
        
        <Entry Placeholder="Focus me to show keyboard"
               AutomationId="TestEntry" />
        
        <Label x:Name="StatusLabel"
               AutomationId="StatusLabel"
               Text="No keyboard" />
    </VerticalStackLayout>
    
</ContentPage>

TestCases.HostApp/Issues/Issue12345.xaml.cs:

namespace Maui.Controls.Sample.Issues;

[Issue(IssueTracker.Github, 12345, 
    "SafeArea bottom padding should adjust with keyboard", 
    PlatformAffected.Android | PlatformAffected.iOS)]
public partial class Issue12345 : ContentPage
{
    public Issue12345()
    {
        InitializeComponent();
        
        TestEntry.Focused += (s, e) => StatusLabel.Text = "Keyboard shown";
        TestEntry.Unfocused += (s, e) => StatusLabel.Text = "No keyboard";
    }
}

File 2: NUnit Test

TestCases.Shared.Tests/Tests/Issues/Issue12345.cs:

using NUnit.Framework;
using UITest.Appium;
using UITest.Core;

namespace Microsoft.Maui.TestCases.Tests.Issues;

public class Issue12345 : _IssuesUITest
{
    public override string Issue => "SafeArea bottom padding should adjust with keyboard";

    public Issue12345(TestDevice device) : base(device) { }

    [Test]
    [Category(UITestCategories.SafeAreaEdges)]
    public void SafeAreaAdjustsWithKeyboard()
    {
        // Wait for page to load
        App.WaitForElement("TestEntry");
        
        // Get initial bottom position
        var initialRect = App.FindElement("StatusLabel").GetRect();
        var initialBottom = initialRect.Y + initialRect.Height;
        
        // Tap entry to show keyboard
        App.Tap("TestEntry");
        Thread.Sleep(1000); // Wait for keyboard animation
        
        // Verify status changed
        var statusText = App.FindElement("StatusLabel").GetText();
        Assert.That(statusText, Is.EqualTo("Keyboard shown"));
        
        // Get position with keyboard
        var keyboardRect = App.FindElement("StatusLabel").GetRect();
        var keyboardBottom = keyboardRect.Y + keyboardRect.Height;
        
        // Verify content moved up (bottom position is less)
        Assert.That(keyboardBottom, Is.LessThan(initialBottom),
            "Content should move up when keyboard appears");
        
        // Optional: Visual verification
        VerifyScreenshot();
    }
}

Running the Test

# Android
pwsh .github/scripts/BuildAndRunHostApp.ps1 `
    -Platform android `
    -TestFilter "Issue12345"

# iOS
pwsh .github/scripts/BuildAndRunHostApp.ps1 `
    -Platform ios `
    -TestFilter "Issue12345"

Summary: Key Concepts

Concept Description
Two-Project HostApp contains UI pages, Tests project contains NUnit tests
_IssuesUITest Base class that provides App property and automatic navigation
AutomationId Required on all UI elements for Appium to find them
NUnit Test framework with [Test] and Assert.That()
dotnet test Standard .NET test execution (not custom script)
BuildAndRunHostApp.ps1 Orchestrates build, deploy, test execution
Test Discovery Tests found via --filter parameter
Categories One per test, used for grouping and filtering

Files Involved

File Purpose
TestCases.HostApp/Issues/IssueXXXXX.xaml UI page with test scenario
TestCases.HostApp/Issues/IssueXXXXX.xaml.cs Code-behind with [Issue] attribute
TestCases.Shared.Tests/Tests/Issues/IssueXXXXX.cs NUnit test
.github/scripts/BuildAndRunHostApp.ps1 Test orchestration script
CustomAgentLogsTmp/UITests/test-output.log Test execution results
CustomAgentLogsTmp/UITests/android-device.log Device logs

Next Steps

To create a new UI test:

  1. Create HostApp page:

    • TestCases.HostApp/Issues/IssueXXXXX.xaml
    • Add AutomationId to all interactive elements
    • Add [Issue()] attribute to code-behind
  2. Create NUnit test:

    • TestCases.Shared.Tests/Tests/Issues/IssueXXXXX.cs
    • Inherit from _IssuesUITest
    • Add [Test] and [Category] attributes
    • Use App property to interact with UI
  3. Run the test:

    pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform android -TestFilter "IssueXXXXX"
  4. Check results:

    cat CustomAgentLogsTmp/UITests/test-output.log

That's it! 🎉

Clone this wiki locally