-
Notifications
You must be signed in to change notification settings - Fork 1.9k
UI Test Coding Agent
The UI Test Coding Agent is a specialized AI assistant for writing new UI tests for .NET MAUI with proper syntax, style, and conventions.
- VS Code: Custom Agents in VS Code
- Copilot CLI: GitHub Copilot CLI
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
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
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
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
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 |
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
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.xamlandIssueXXXXX.xaml.cs
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(providesAppproperty and infrastructure) - ✅ Constructor takes
TestDeviceparameter - ✅ Must have
[Test]attribute and exactly ONE[Category]attribute - ✅ Use descriptive test method names
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]
# 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 availableSame as sandbox-agent - uses shared/Start-Emulator.ps1
# Android
$DeviceUdid = "emulator-5554"
# iOS
$DeviceUdid = "AC8BCB28-..." # iPhone Xs simulator# Build the HostApp
dotnet build src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj `
-f net10.0-android `
-c Debug `
-p:TargetFramework=net10.0-androidResult: HostApp built with all test pages included
# Android
adb -s $DeviceUdid install -r bin/Debug/net10.0-android/*.apk
# iOS
xcrun simctl install $DeviceUdid bin/Debug/net10.0-ios/*.appResult: HostApp installed on device with all Issue pages
# 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>&1What happens here:
-
dotnet teststarts NUnit test runner - Test runner finds tests matching filter (e.g.,
Issue12345) - NUnit creates instance of test class:
new Issue12345(device) - Test framework calls
[Test]methods - Test uses
Appproperty to interact with UI via Appium
// _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
}
}[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!"));
}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
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.logiOS:
# Capture simulator logs
xcrun simctl spawn $DeviceUdid log stream `
--predicate 'processImagePath contains "HostApp"' `
> ios-device.log| 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) |
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
[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"));
}[Test]
[Category(UITestCategories.Navigation)]
public void NavigateToDetailPage()
{
App.WaitForElement("NavigateButton");
App.Tap("NavigateButton");
// Verify we're on the new page
App.WaitForElement("DetailPageLabel");
VerifyScreenshot();
}[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));
}# 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"# All button tests
pwsh .github/scripts/BuildAndRunHostApp.ps1 `
-Platform android `
-Category "Button"
# All layout tests
pwsh .github/scripts/BuildAndRunHostApp.ps1 `
-Platform ios `
-Category "Layout"# iOS with specific simulator
pwsh .github/scripts/BuildAndRunHostApp.ps1 `
-Platform ios `
-TestFilter "Issue12345" `
-DeviceUdid "AC8BCB28-A72D-4A2D-90E7-E78FF0BA07EE"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.csStarting 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
CustomAgentLogsTmp/UITests/
├── test-output.log # dotnet test output
├── android-device.log # Android logcat
└── ios-device.log # iOS device logs
# 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"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";
}
}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();
}
}# Android
pwsh .github/scripts/BuildAndRunHostApp.ps1 `
-Platform android `
-TestFilter "Issue12345"
# iOS
pwsh .github/scripts/BuildAndRunHostApp.ps1 `
-Platform ios `
-TestFilter "Issue12345"| 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 |
| 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 |
To create a new UI test:
-
Create HostApp page:
TestCases.HostApp/Issues/IssueXXXXX.xaml- Add
AutomationIdto all interactive elements - Add
[Issue()]attribute to code-behind
-
Create NUnit test:
TestCases.Shared.Tests/Tests/Issues/IssueXXXXX.cs- Inherit from
_IssuesUITest - Add
[Test]and[Category]attributes - Use
Appproperty to interact with UI
-
Run the test:
pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform android -TestFilter "IssueXXXXX"
-
Check results:
cat CustomAgentLogsTmp/UITests/test-output.log
That's it! 🎉