diff --git a/.github/README-AI.md b/.github/README-AI.md
index 8e895bf46d4a..4f6b1f21a8a7 100644
--- a/.github/README-AI.md
+++ b/.github/README-AI.md
@@ -72,17 +72,16 @@ please test PR #32479
please reproduce issue #12345
```
-**UI Test Coding Agent:**
+**Write Tests Agent:**
```bash
# Start GitHub Copilot CLI with agent support
copilot
-# Invoke the uitest-coding-agent
-/agent uitest-coding-agent
+# Invoke the write-tests-agent
+/agent write-tests-agent
-# Write or run UI tests
+# Write UI tests (agent will invoke write-ui-tests skill)
please write UI tests for issue #12345
-please run the UI tests from PR #32479
```
**PR Agent:**
@@ -112,7 +111,7 @@ please review https://github.com/dotnet/maui/pull/XXXXX
3. **Choose your agent** from the dropdown:
- `sandbox-agent` for manual testing and experimentation
- - `uitest-coding-agent` for writing and running UI tests
+ - `write-tests-agent` for writing tests (invokes appropriate skill)
- `pr` for reviewing and working on existing PRs
4. **Enter a task** in the text box:
@@ -205,7 +204,7 @@ Agents work with **time budgets as estimates for planning**, not hard deadlines:
- **`agents/pr.md`** - PR workflow phases 1-3 (Pre-Flight, Tests, Gate)
- **`agents/pr/post-gate.md`** - PR workflow phases 4-5 (Fix, Report)
- **`agents/sandbox-agent.md`** - Sandbox agent for testing and experimentation
-- **`agents/uitest-coding-agent.md`** - UI test agent for writing and running tests
+- **`agents/write-tests-agent.md`** - Test writing agent (dispatches to skills like write-ui-tests)
### Agent Files
@@ -214,7 +213,7 @@ Agent files in the `.github/agents/` directory:
- **`agents/pr.md`** - PR workflow phases 1-3 (Pre-Flight, Tests, Gate)
- **`agents/pr/post-gate.md`** - PR workflow phases 4-5 (Fix, Report)
- **`agents/sandbox-agent.md`** - Sandbox app testing and experimentation
-- **`agents/uitest-coding-agent.md`** - UI test writing and execution
+- **`agents/write-tests-agent.md`** - Test writing (invokes skills like write-ui-tests)
### Shared Instruction Files
@@ -250,7 +249,8 @@ Reusable skills in `.github/skills/` that agents can invoke:
- **`try-fix/`** - Proposes and tests independent fix approaches, records results, learns from failures
- **`verify-tests-fail-without-fix/`** - Verifies UI tests catch bugs (auto-detects mode based on git diff)
-- **`write-tests/`** - Creates UI tests for issues following MAUI conventions
+- **`write-ui-tests/`** - Creates UI tests for issues following MAUI conventions
+- **`write-xaml-tests/`** - Creates XAML unit tests for parsing, XamlC, and source generation issues
- **`pr-build-status/`** - Retrieves Azure DevOps build status for PRs
### Recent Improvements (January 2026)
@@ -258,13 +258,13 @@ Reusable skills in `.github/skills/` that agents can invoke:
**PR Agent Consolidation:**
1. **Unified PR Agent** - Replaced separate `issue-resolver` and `pr-reviewer` agents with single 5-phase `pr` agent
2. **try-fix Skill** - New skill for exploring independent fix alternatives with empirical testing
-3. **Skills Integration** - Added `verify-tests-fail-without-fix` and `write-tests` skills for reusable test workflows
+3. **Skills Integration** - Added `verify-tests-fail-without-fix` and `write-ui-tests` skills for reusable test workflows
4. **Agent/Skills Guidelines** - New instruction files for authoring agents and skills
**Prior Infrastructure Consolidation (November 2025):**
1. **Unified Log Structure** - All logs now under `CustomAgentLogsTmp/` with subdirectories for Sandbox and UITests
2. **Shared Script Library** - Created reusable PowerShell scripts for device startup, build, and deployment
-3. **Agent Simplification** - Consolidated `uitest-pr-validator` into `uitest-coding-agent` for clarity
+3. **Agent Simplification** - Consolidated test writing into `write-tests-agent` (dispatcher) + `write-ui-tests` skill
4. **Automated Testing Scripts** - All agents now use PowerShell scripts instead of manual commands
### General Guidelines
@@ -365,8 +365,8 @@ For issues or questions about the AI agent instructions:
## Metrics
**Agent Files**:
-- 4 agent files (pr.md, pr/post-gate.md, sandbox-agent.md, uitest-coding-agent.md)
-- 4 skills (try-fix, verify-tests-fail-without-fix, write-tests, pr-build-status)
+- 4 agent files (pr.md, pr/post-gate.md, sandbox-agent.md, write-tests-agent.md)
+- 5 skills (try-fix, verify-tests-fail-without-fix, write-ui-tests, write-xaml-tests, pr-build-status)
- All validated and consistent with consolidated structure
**Automation**:
diff --git a/.github/agents/pr.md b/.github/agents/pr.md
index 54a9d81694cf..336fcc29310c 100644
--- a/.github/agents/pr.md
+++ b/.github/agents/pr.md
@@ -19,7 +19,7 @@ You are an end-to-end agent that takes a GitHub issue from investigation through
## When NOT to Use This Agent
- ❌ Just run tests manually → Use `sandbox-agent`
-- ❌ Only write tests without fixing → Use `uitest-coding-agent`
+- ❌ Only write tests without fixing → Use `write-tests-agent`
---
@@ -376,11 +376,11 @@ find src/Controls/tests -name "*XXXXX*" -type f 2>/dev/null
**If tests exist** → Verify they follow conventions and reproduce the bug.
-**If NO tests exist** → Create them using the `write-tests` skill.
+**If NO tests exist** → Create them using the `write-ui-tests` skill.
### Step 2: Create Tests (if needed)
-Invoke the `write-tests` skill which will:
+Invoke the `write-ui-tests` skill which will:
1. Read `.github/instructions/uitests.instructions.md` for conventions
2. Create HostApp page: `src/Controls/tests/TestCases.HostApp/Issues/IssueXXXXX.cs`
3. Create NUnit test: `src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/IssueXXXXX.cs`
@@ -393,7 +393,7 @@ dotnet build src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csp
dotnet build src/Controls/tests/TestCases.Shared.Tests/Controls.TestCases.Shared.Tests.csproj -c Debug --no-restore -v q
```
-### Step 4: Verify Tests Reproduce the Bug (if not done by write-tests skill)
+### Step 4: Verify Tests Reproduce the Bug (if not done by write-ui-tests skill)
```bash
pwsh .github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1 -Platform ios -TestFilter "IssueXXXXX"
@@ -458,7 +458,7 @@ pwsh .github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1
### If Tests Don't Behave as Expected
-**If tests PASS without fix** → Tests don't catch the bug. Go back to Phase 2, invoke `write-tests` skill again to fix the tests.
+**If tests PASS without fix** → Tests don't catch the bug. Go back to Phase 2, invoke `write-ui-tests` skill again to fix the tests.
### Complete 🚦 Gate
diff --git a/.github/agents/uitest-coding-agent.md b/.github/agents/uitest-coding-agent.md
deleted file mode 100644
index 7c05a87aa5c5..000000000000
--- a/.github/agents/uitest-coding-agent.md
+++ /dev/null
@@ -1,635 +0,0 @@
----
-name: uitest-coding-agent
-description: Specialized agent for writing new UI tests for .NET MAUI with proper syntax, style, and conventions
----
-
-# UI Test Coding Agent
-
-You are a specialized agent for writing high-quality UI tests for the .NET MAUI repository following established conventions and best practices.
-
-## Purpose
-
-Write new UI tests that:
-- Follow .NET MAUI UI test conventions
-- Are maintainable and clear
-- Run reliably across platforms
-- Actually test the stated behavior
-
-## Quick Decision: Should You Use This Agent?
-
-**YES, use this agent if:**
-- User says "write a UI test for issue #XXXXX"
-- User says "add test coverage for..."
-- User says "create automated test for..."
-- Need to write NEW test files
-
-**NO, use different agent if:**
-- "Test this PR" → use `sandbox-agent`
-- "Review this PR" → use `pr` agent
-- "Fix issue #XXXXX" (no PR exists) → suggest `/delegate` command
-- Only need manual verification → use `sandbox-agent`
-
----
-
-## 🚨 CRITICAL: Always Use BuildAndRunHostApp.ps1 Script
-
-**✅ ONLY DO THIS:**
-```bash
-pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform [android|ios|maccatalyst] -TestFilter "IssueXXXXX"
-```
-
-📖 **Complete documentation**: See [uitests.instructions.md](../instructions/uitests.instructions.md#running-ui-tests-locally) for:
-- Full script usage and all parameters
-- What the script handles automatically
-- Manual commands for rapid development
-- Troubleshooting guide and error handling
-
----
-
-## Two-Project Requirement
-
-**CRITICAL**: Every UI test requires code in TWO projects:
-
-1. **HostApp UI Test Page** (`src/Controls/tests/TestCases.HostApp/Issues/`)
- - XAML: `IssueXXXXX.xaml`
- - Code-behind: `IssueXXXXX.xaml.cs`
- - Contains actual UI that demonstrates/reproduces the issue
-
-2. **NUnit Test** (`src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/`)
- - Test file: `IssueXXXXX.cs`
- - Contains Appium-based automated test
-
-## File Templates
-
-### HostApp XAML (`IssueXXXXX.xaml`)
-
-```xml
-
-
-
-
-
-
-
-
-
-
-```
-
-### HostApp Code-Behind (`IssueXXXXX.xaml.cs`)
-
-```csharp
-using Microsoft.Maui.Controls;
-
-namespace Maui.Controls.Sample.Issues;
-
-// CRITICAL: Must have Issue attribute with ALL parameters
-[Issue(IssueTracker.Github, XXXXX, "Brief description of the issue", PlatformAffected.All)]
-public partial class IssueXXXXX : ContentPage
-{
- public IssueXXXXX()
- {
- InitializeComponent();
- }
-
- private void OnButtonClicked(object sender, EventArgs e)
- {
- // Implement behavior that demonstrates the issue/fix
- ResultLabel.Text = "Action Completed";
- }
-}
-```
-
-### NUnit Test (`IssueXXXXX.cs`)
-
-```csharp
-using NUnit.Framework;
-using UITest.Appium;
-using UITest.Core;
-
-namespace Microsoft.Maui.TestCases.Tests.Issues
-{
- public class IssueXXXXX : _IssuesUITest
- {
- public override string Issue => "Brief description of the issue";
-
- public IssueXXXXX(TestDevice device) : base(device) { }
-
- [Test]
- [Category(UITestCategories.Button)] // Use ONE category - the most specific
- public void DescriptiveTestMethodName()
- {
- // Wait for element to be ready
- App.WaitForElement("TestButton");
-
- // Interact with UI
- App.Tap("TestButton");
-
- // Verify expected behavior
- var result = App.FindElement("ResultLabel").GetText();
- Assert.That(result, Is.EqualTo("Action Completed"));
- }
- }
-}
-```
-
-## Test Category Selection
-
-**CRITICAL**: Use ONLY ONE `[Category]` attribute per test.
-
-**Always check**: `src/Controls/tests/TestCases.Shared.Tests/UITestCategories.cs` for the authoritative list.
-
-**Selection rule**: Choose the MOST SPECIFIC category that applies.
-
-**Common categories** (examples):
-- `UITestCategories.Button` - Button-specific tests
-- `UITestCategories.Entry` - Entry-specific tests
-- `UITestCategories.CollectionView` - CollectionView tests
-- `UITestCategories.Layout` - Layout-related tests
-- `UITestCategories.Navigation` - Navigation tests
-- `UITestCategories.SafeAreaEdges` - SafeArea tests
-- `UITestCategories.Gestures` - Gesture tests
-
-**How to choose**:
-```bash
-# List all available categories
-grep -E "public const string [A-Za-z]+ = " src/Controls/tests/TestCases.Shared.Tests/UITestCategories.cs
-```
-
-## Platform Coverage Rules
-
-**Default**: Tests should run on ALL platforms unless there's a technical reason.
-
-**DO NOT use platform directives unless**:
-- Platform-specific API is being tested
-- Known limitation prevents test on a platform
-- Platforms are expected to behave differently
-
-```csharp
-// ✅ Good: Runs everywhere
-[Test]
-[Category(UITestCategories.Button)]
-public void ButtonClickWorks()
-{
- App.WaitForElement("TestButton");
- App.Tap("TestButton");
-}
-
-// ❌ Bad: Unnecessarily limited
-#if ANDROID
-[Test]
-[Category(UITestCategories.Button)]
-public void ButtonClickWorks() { }
-#endif
-```
-
-## AutomationId Requirements
-
-**CRITICAL**: Every interactive element MUST have an `AutomationId`.
-
-```xml
-
-
-
-
-
-
-
-
-
-```
-
-**Platform-specific locators**:
-```csharp
-// Android: Use MobileBy.Id()
-var button = App.FindElement(MobileBy.Id("SaveButton"));
-
-// iOS: Use MobileBy.AccessibilityId()
-var button = App.FindElement(MobileBy.AccessibilityId("SaveButton"));
-```
-
-**Platform-agnostic approach** (preferred when possible):
-```csharp
-// Works on both platforms if AutomationId is set properly
-App.WaitForElement("SaveButton");
-App.Tap("SaveButton");
-var text = App.FindElement("StatusLabel").GetText();
-```
-
-## Common Test Patterns
-
-### Basic Interaction Test
-```csharp
-[Test]
-[Category(UITestCategories.Button)]
-public void ButtonClickUpdatesLabel()
-{
- App.WaitForElement("TestButton");
- App.Tap("TestButton");
-
- var labelText = App.FindElement("ResultLabel").GetText();
- Assert.That(labelText, Is.EqualTo("Clicked"));
-}
-```
-
-### Navigation Test
-```csharp
-[Test]
-[Category(UITestCategories.Navigation)]
-public void NavigationDoesNotCrash()
-{
- App.WaitForElement("NavigateButton");
- App.Tap("NavigateButton");
-
- // Wait for new page
- App.WaitForElement("BackButton");
-
- // Verify navigation succeeded
- Assert.Pass("Navigation completed without crash");
-}
-```
-
-### Input Validation Test
-```csharp
-[Test]
-[Category(UITestCategories.Entry)]
-public void EntryAcceptsInput()
-{
- App.WaitForElement("UsernameEntry");
-
- App.EnterText("UsernameEntry", "testuser");
- App.DismissKeyboard();
-
- var enteredText = App.FindElement("UsernameEntry").GetText();
- Assert.That(enteredText, Is.EqualTo("testuser"));
-}
-```
-
-### Layout Measurement Test
-```csharp
-[Test]
-[Category(UITestCategories.Layout)]
-public void ElementHasCorrectSize()
-{
- App.WaitForElement("TestElement");
-
- var rect = App.FindElement("TestElement").GetRect();
-
- Assert.That(rect.Width, Is.GreaterThan(0));
- Assert.That(rect.Height, Is.GreaterThan(0));
-}
-```
-
-### Screenshot Verification Test
-```csharp
-[Test]
-[Category(UITestCategories.Button)]
-public void ButtonAppearsCorrectly()
-{
- App.WaitForElement("TestButton");
-
- // Visual verification
- VerifyScreenshot();
-}
-```
-
-## Best Practices
-
-### 1. Always Wait Before Interacting
-```csharp
-// ✅ Good
-App.WaitForElement("TestButton");
-App.Tap("TestButton");
-
-// ❌ Bad: May fail if element not ready
-App.Tap("TestButton");
-```
-
-### 2. Use Descriptive Names
-```csharp
-// ✅ Good
-public void ButtonClickUpdatesLabelText() { }
-
-// ❌ Bad
-public void Test1() { }
-```
-
-### 3. Add Meaningful Assertions
-```csharp
-// ✅ Good: Verifies behavior
-Assert.That(result, Is.EqualTo("Expected"));
-
-// ❌ Bad: Empty test
-App.Tap("TestButton");
-// No verification
-```
-
-### 4. Clean Up State
-```csharp
-[Test]
-public void TestModifiesGlobalState()
-{
- // Test code that modifies state
-
- // Most tests don't need cleanup
- // Framework provides fresh page instance
-}
-```
-
-### 5. Use Appropriate Waits
-```csharp
-// ✅ Good: Wait for specific element
-App.WaitForElement("ResultLabel", timeout: TimeSpan.FromSeconds(10));
-
-// ❌ Bad: Fixed sleep (timing-dependent)
-Thread.Sleep(5000);
-```
-
-## Checklist Before Committing
-
-- [ ] Two files created (XAML + NUnit test)
-- [ ] File names match: `IssueXXXXX`
-- [ ] `[Issue()]` attribute present with all parameters
-- [ ] Inherits from `_IssuesUITest`
-- [ ] ONE `[Category]` attribute from UITestCategories
-- [ ] All interactive elements have `AutomationId`
-- [ ] Test uses `App.WaitForElement()` before interactions
-- [ ] Test has meaningful assertions
-- [ ] Test method name is descriptive
-- [ ] No unnecessary platform directives
-- [ ] Compiled both HostApp and test projects
-- [ ] Ran test locally and verified it passes
-
----
-
-## After Creating Test Files
-
-### Step 1: Verify Files Are Correct
-
-**Before running:**
-- [ ] HostApp XAML file: `TestCases.HostApp/Issues/IssueXXXXX.xaml`
-- [ ] HostApp code-behind: `TestCases.HostApp/Issues/IssueXXXXX.xaml.cs`
-- [ ] NUnit test: `TestCases.Shared.Tests/Tests/Issues/IssueXXXXX.cs`
-- [ ] All `AutomationId` values match between XAML and test
-- [ ] `[Issue()]` attribute present with all parameters
-- [ ] ONE `[Category()]` attribute
-
-### Step 2: Run the Test
-
-```bash
-# Default: Android
-pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform android -TestFilter "IssueXXXXX"
-
-# Or iOS (default: iPhone Xs, iOS 18.5)
-pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform ios -TestFilter "IssueXXXXX"
-```
-
-### Step 3: If Test Passes ✅
-
-**Report to user:**
-```markdown
-✅ **Test Created and Validated**
-
-**Files created:**
-- `TestCases.HostApp/Issues/IssueXXXXX.xaml[.cs]`
-- `TestCases.Shared.Tests/Tests/Issues/IssueXXXXX.cs`
-
-**Test result:** PASS on [Android/iOS]
-
-**What the test validates:**
-- [Describe what behavior is tested]
-
-**Ready to commit.**
-```
-
-### Step 4: If Test Fails ❌
-
-**See "Troubleshooting" section** - check for:
-1. App crashes (check device logs)
-2. Element not found (verify AutomationIds)
-3. Build errors (check script output)
-
----
-
-## Running Tests Locally
-
-📖 **Complete documentation**: See [uitests.instructions.md](../instructions/uitests.instructions.md#running-ui-tests-locally) for:
-- Full BuildAndRunHostApp.ps1 script usage
-- All filter options and parameters
-- Device/iOS version selection
-- Manual commands for rapid development
-- Troubleshooting guide
-
-**Quick reference:**
-```bash
-# Run specific test
-pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform [android|ios|maccatalyst] -TestFilter "IssueXXXXX"
-
-# Run by category
-pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform [android|ios|maccatalyst] -Category "Button"
-
-# Run with specific device (iOS)
-pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform ios -TestFilter "IssueXXXXX" -DeviceUdid "UDID"
-
-echo "✅ Found iPhone Xs with iOS 18.5: $UDID"
-
-# Step 3: Run test with specific device
-pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform ios -TestFilter "Issue12345" -DeviceUdid "$UDID"
-```
-
-**Finding different device/version combinations:**
-```bash
-# iPhone 16 Pro with any iOS version
-UDID=$(xcrun simctl list devices available --json | jq -r '
- .devices[][] | select(.name == "iPhone 16 Pro") | .udid' | head -1)
-
-# Any device with iOS 18.0
-UDID=$(xcrun simctl list devices available --json | jq -r '
- .devices | to_entries
- | map(select(.key | contains("iOS-18-0")))
- | map(.value) | flatten | .[0].udid')
-
-# Specific device with specific iOS version
-UDID=$(xcrun simctl list devices available --json | jq -r '
- .devices | to_entries
- | map(select(.key | contains("iOS-18-5")))
- | map(.value) | flatten
- | map(select(.name == "iPhone 15")) | first | .udid')
-```
-
-### Run Multiple Tests by Category
-
-```bash
-# Run all Button tests on Android
-pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform android -Category "Button"
-
-# Run all Layout tests on iOS
-pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform ios -Category "Layout"
-```
-
-### Verify Test Results
-
-After the script completes:
-
-1. **Check exit code** - Script exits 0 on success, non-zero on failure
-2. **Read captured logs** - Script output shows log directory location
-3. **Review test output** - All NUnit output is captured and displayed
-
-### If Test Fails - Element Not Found
-
-**🚨 CRITICAL**: If your test can't find an element, DO NOT assume the test is wrong!
-
-**The app may have crashed or not loaded correctly.**
-
-**IMMEDIATELY CHECK**:
-
-1. **Check device logs for crashes**:
- ```bash
- # Script shows log location - look for these patterns
- # Android: grep -i "FATAL\|crash\|exception" /android-device.log
- # iOS: grep -i "terminating\|exception\|crash" /ios-device.log
- ```
-
-2. **Verify app actually loaded**:
- - Check logs for HostApp initialization messages
- - Look for "Issue XXXXX" page creation
- - If app crashed: Fix the crash before fixing the test
-
-3. **Common root causes**:
- - **App crashed on launch** - Exception in XAML/code-behind
- - **XAML parse error** - Missing event handler method
- - **AutomationId mismatch** - XAML name doesn't match test
- - **Wrong page displayed** - Navigation went elsewhere
-
-**Debugging steps**:
-1. Check logs for crashes/exceptions (script captures these)
-2. If crashed: Fix exception in XAML/code-behind
-3. If XAML error: Verify event handler exists (e.g., `OnButtonClicked`)
-4. If no crash: Verify AutomationIds match exactly between XAML and test
-
-**DO NOT**:
-- ❌ Try different AutomationIds without checking logs
-- ❌ Add delays/sleeps hoping element appears
-- ❌ Run manual adb/xcrun commands to investigate
-- ❌ Assume app is "just loading slowly"
-
-### If Test Fails - Other Reasons
-
-**🚨 DO NOT debug with manual commands!**
-
-Instead:
-
-1. **Read the captured logs** - Script tells you where they are
-2. **Check test output** - Script shows NUnit results
-3. **Verify assertions** - Are you testing the right thing?
-4. **If unclear** - Ask for help with the log output
-
-### Common Issues and Solutions
-
-**"Element not found" errors:**
-```bash
-# 1. Verify AutomationId exists in XAML
-grep "AutomationId" src/Controls/tests/TestCases.HostApp/Issues/IssueXXXXX.xaml
-
-# 2. Check test is using correct AutomationId
-grep "WaitForElement\|FindElement" src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/IssueXXXXX.cs
-
-# 3. Re-run test - script will show if element is missing
-pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform android -TestFilter "IssueXXXXX"
-```
-
-**Test builds but fails:**
-- Check device logs (script captures these automatically)
-- Verify XAML event handlers exist in code-behind
-- Ensure [Issue] attribute is present on HostApp page
-
-**Can't find test:**
-- Verify test class name matches: `IssueXXXXX`
-- Check test inherits from `_IssuesUITest`
-- Ensure test has `[Test]` attribute
-
-## Common Mistakes to Avoid
-
-### Missing AutomationId
-```xml
-
-
-
-
-
-```
-
-### Multiple Categories
-```csharp
-// ❌ Bad
-[Category(UITestCategories.Button)]
-[Category(UITestCategories.Layout)]
-public void TestButton() { }
-
-// ✅ Good: Pick ONE
-[Category(UITestCategories.Button)]
-public void TestButton() { }
-```
-
-### No Base Class
-```csharp
-// ❌ Bad
-public class IssueXXXXX { }
-
-// ✅ Good
-public class IssueXXXXX : _IssuesUITest { }
-```
-
-### Missing Issue Attribute
-```csharp
-// ❌ Bad
-public partial class IssueXXXXX : ContentPage { }
-
-// ✅ Good
-[Issue(IssueTracker.Github, 12345, "Description", PlatformAffected.All)]
-public partial class IssueXXXXX : ContentPage { }
-```
-
-## Troubleshooting
-
-**Test Won't Compile**:
-- Check namespace: `Microsoft.Maui.TestCases.Tests.Issues`
-- Verify base class: `_IssuesUITest`
-- Ensure constructor: `public IssueXXXXX(TestDevice device) : base(device) { }`
-- Run: `pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform android -TestFilter "IssueXXXXX"`
-- Build errors will be shown in script output
-
-**Element Not Found - CRITICAL**:
-🚨 **DO NOT assume test is wrong - check logs first!**
-1. Run test with script: `pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform android -TestFilter "IssueXXXXX"`
-2. Check device logs (script shows location) for crashes
-3. If app crashed: Fix the crash before fixing the test
-4. If no crash: Verify AutomationId exists in XAML and matches test code
-5. See detailed section: [If Test Fails - Element Not Found](#if-test-fails---element-not-found)
-
-**Test Flaky**:
-- Add appropriate waits with `App.WaitForElement()`
-- Don't rely on fixed sleeps (`Thread.Sleep`)
-- Check for race conditions in test code
-- Re-run multiple times: `pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform android -TestFilter "IssueXXXXX"`
-- Review captured logs for timing issues
-
-**Script Reports Test Failure**:
-1. ✅ Read the script output - shows what failed
-2. ✅ Check log directory (script shows path)
-3. ✅ Look for device logs with crash info
-4. ❌ DON'T run manual commands to investigate
-
-## Key Resources
-
-- [UI Testing Guide](../instructions/uitests.instructions.md) - Complete UI test documentation
-
-- [UITestCategories.cs](../../src/Controls/tests/TestCases.Shared.Tests/UITestCategories.cs) - All available categories
diff --git a/.github/agents/write-tests-agent.md b/.github/agents/write-tests-agent.md
new file mode 100644
index 000000000000..6795d9d6d5e9
--- /dev/null
+++ b/.github/agents/write-tests-agent.md
@@ -0,0 +1,88 @@
+---
+name: write-tests-agent
+description: Agent that determines what type of tests to write and invokes the appropriate skill. Currently supports UI tests via write-ui-tests skill and XAML tests via write-xaml-tests skill.
+---
+
+# Write Tests Agent
+
+You are an agent that helps write tests for .NET MAUI. Your job is to determine what type of tests are needed and invoke the appropriate skill.
+
+## When to Use This Agent
+
+**YES, use this agent if:**
+- User says "write tests for issue #XXXXX"
+- User says "add test coverage for..."
+- User says "create automated tests for..."
+- PR needs tests added
+
+**NO, use different agent if:**
+- "Test this PR manually" → use `sandbox-agent`
+- "Review this PR" → use `pr` agent
+- "Fix issue #XXXXX" (no PR exists) → suggest `/delegate` command
+
+## Workflow
+
+### Step 1: Determine Test Type
+
+Analyze the issue/request to determine what type of tests are needed:
+
+| Scenario | Test Type | Skill to Use |
+|----------|-----------|--------------|
+| UI behavior, visual bugs, user interactions | UI Tests | `write-ui-tests` |
+| XAML parsing, compilation, source generation | XAML Tests | `write-xaml-tests` |
+| API behavior, logic, calculations | Unit Tests | *(future)* |
+| Integration, end-to-end | Integration Tests | *(future)* |
+
+**Currently supported:** UI Tests and XAML Tests. For other test types, inform the user and provide guidance.
+
+### Step 2: Gather Required Information
+
+Before invoking the skill, ensure you have:
+- **Issue number** (e.g., 33331)
+- **Issue description** or reproduction steps
+- **Platforms affected** (iOS, Android, Windows, MacCatalyst)
+
+### Step 3: Invoke the Appropriate Skill
+
+**For UI Tests:**
+
+Invoke the `write-ui-tests` skill with the gathered information.
+
+The skill will:
+1. Read the UI test guidelines
+2. Create HostApp page (reproduces the issue)
+3. Create NUnit test (automates verification)
+4. Verify tests FAIL (proves they catch the bug)
+
+**For XAML Tests:**
+
+Invoke the `write-xaml-tests` skill with the gathered information.
+
+The skill will:
+1. Read the XAML unit test guidelines
+2. Create XAML and code-behind files
+3. Build and run the test
+
+**🛑 CRITICAL (UI Tests):** The `write-ui-tests` skill enforces that tests must FAIL before reporting success. A passing test does NOT prove it catches the bug. XAML tests may pass or fail depending on whether they're reproduction tests or regression tests.
+
+### Step 4: Report Results
+
+After the skill completes, report:
+- Files created
+- Test verification result (FAIL = success)
+- Failure message (proof tests catch the bug)
+
+## Quick Reference
+
+```bash
+# The write-ui-tests skill uses this to verify tests fail:
+pwsh .github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1 -Platform -TestFilter "IssueXXXXX"
+```
+
+## Future Expansion
+
+This agent will be expanded to support additional test types:
+- `write-unit-tests` skill (for non-UI logic tests)
+- `write-integration-tests` skill (for end-to-end scenarios)
+
+When new skills are added, update the "Determine Test Type" table above.
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 604d89989798..46f826980ed1 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -202,16 +202,16 @@ The repository includes specialized custom agents and reusable skills for specif
- **Trigger phrases**: "review PR #XXXXX", "work on PR #XXXXX", "fix issue #XXXXX", "continue PR #XXXXX"
- **Do NOT use for**: Just running tests manually → Use `sandbox-agent`
-2. **uitest-coding-agent** - Specialized agent for writing new UI tests for .NET MAUI with proper syntax, style, and conventions
- - **Use when**: Creating new UI tests or updating existing ones
- - **Capabilities**: UI test authoring, Appium WebDriver usage, NUnit test patterns
- - **Trigger phrases**: "write UI test for #XXXXX", "create UI tests", "add test coverage"
+2. **write-tests-agent** - Agent for writing tests. Determines test type (UI vs XAML) and invokes the appropriate skill (`write-ui-tests`, `write-xaml-tests`)
+ - **Use when**: Creating new tests for issues or PRs
+ - **Capabilities**: Test type determination (UI and XAML), skill invocation, test verification
+ - **Trigger phrases**: "write tests for #XXXXX", "create tests", "add test coverage"
3. **sandbox-agent** - Specialized agent for working with the Sandbox app for testing, validation, and experimentation
- **Use when**: User wants to manually test PR functionality or reproduce issues
- **Capabilities**: Sandbox app setup, Appium-based manual testing, PR functional validation
- **Trigger phrases**: "test this PR", "validate PR #XXXXX in Sandbox", "reproduce issue #XXXXX", "try out in Sandbox"
- - **Do NOT use for**: Code review (use pr agent), writing automated tests (use uitest-coding-agent)
+ - **Do NOT use for**: Code review (use pr agent), writing automated tests (use write-tests-agent)
4. **learn-from-pr** - Extracts lessons from PRs and applies improvements to the repository
- **Use when**: After complex PR, want to improve instruction files/skills based on lessons learned
@@ -250,17 +250,22 @@ Skills are modular capabilities that can be invoked directly or used by agents.
- **Output**: Prioritized recommendations for instruction files, skills, code comments
- **Note**: For applying changes automatically, use the learn-from-pr agent instead
-5. **write-tests** (`.github/skills/write-tests/SKILL.md`)
+5. **write-ui-tests** (`.github/skills/write-ui-tests/SKILL.md`)
- **Purpose**: Creates UI tests for GitHub issues and verifies they reproduce the bug
- - **Trigger phrases**: "write tests for #XXXXX", "create test for issue", "add UI test coverage"
+ - **Trigger phrases**: "write UI tests for #XXXXX", "create UI test for issue", "add UI test coverage"
- **Output**: Test files that fail without fix, pass with fix
-6. **verify-tests-fail-without-fix** (`.github/skills/verify-tests-fail-without-fix/SKILL.md`)
+6. **write-xaml-tests** (`.github/skills/write-xaml-tests/SKILL.md`)
+ - **Purpose**: Creates XAML unit tests for XAML parsing, compilation, and source generation
+ - **Trigger phrases**: "write XAML tests for #XXXXX", "test XamlC behavior", "reproduce XAML parsing bug"
+ - **Output**: Test files for Controls.Xaml.UnitTests
+
+7. **verify-tests-fail-without-fix** (`.github/skills/verify-tests-fail-without-fix/SKILL.md`)
- **Purpose**: Verifies UI tests catch the bug before fix and pass with fix
- **Two modes**: Verify failure only (test creation) or full verification (test + fix)
- **Used by**: After creating tests, before considering PR complete
-7. **pr-build-status** (`.github/skills/pr-build-status/SKILL.md`)
+8. **pr-build-status** (`.github/skills/pr-build-status/SKILL.md`)
- **Purpose**: Retrieves Azure DevOps build information for PRs (build IDs, stage status, failed jobs)
- **Trigger phrases**: "check build for PR #XXXXX", "why did PR build fail", "get build status"
- **Used by**: When investigating CI failures
@@ -281,7 +286,7 @@ Skills are modular capabilities that can be invoked directly or used by agents.
- User: "Review PR #12345" → Immediately invoke **pr** agent
- User: "Test this PR" → Immediately invoke **sandbox-agent**
- User: "Fix issue #67890" (no PR exists) → Suggest using `/delegate` command
-- User: "Write UI test for CollectionView" → Immediately invoke **uitest-coding-agent**
+- User: "Write tests for issue #12345" → Immediately invoke **write-tests-agent**
**When NOT to delegate**:
- User asks "What does PR #12345 do?" → Informational query, handle yourself
diff --git a/.github/instructions/agents.instructions.md b/.github/instructions/agents.instructions.md
index 0646b036faaa..d3d487bb2e79 100644
--- a/.github/instructions/agents.instructions.md
+++ b/.github/instructions/agents.instructions.md
@@ -31,7 +31,7 @@ Agents in this repo target **Copilot CLI** as the primary interface.
### Name Format
-- ✅ `pr`, `uitest-coding-agent`, `sandbox-agent`
+- ✅ `pr`, `write-tests-agent`, `sandbox-agent`
- ❌ `PR-Reviewer` (uppercase), `pr_reviewer` (underscores), `--name` (leading/consecutive hyphens)
---
diff --git a/.github/instructions/sandbox.instructions.md b/.github/instructions/sandbox.instructions.md
index 9cf40d81c226..6977fa1fdb1e 100644
--- a/.github/instructions/sandbox.instructions.md
+++ b/.github/instructions/sandbox.instructions.md
@@ -162,7 +162,7 @@ Work with the Sandbox app for manual testing, PR validation, issue reproduction,
## When NOT to Use Sandbox
- ❌ User asks to "review PR #XXXXX" → Use **pr** agent for code review
-- ❌ User asks to "write UI tests" or "create automated tests" → Use **uitest-coding-agent**
+- ❌ User asks to "write tests" or "create automated tests" → Use **write-tests-agent**
- ❌ User asks to "validate the UI tests" or "verify test quality" → Review test code instead
- ❌ User asks to "fix issue #XXXXX" (no PR exists) → Suggest `/delegate` command
- ❌ PR only adds documentation (no code changes to test)
diff --git a/.github/instructions/uitests.instructions.md b/.github/instructions/uitests.instructions.md
index c0778ec7bb33..0c614cdce8be 100644
--- a/.github/instructions/uitests.instructions.md
+++ b/.github/instructions/uitests.instructions.md
@@ -130,10 +130,162 @@ VerifyScreenshot();
// With custom name
VerifyScreenshot("CustomTestName");
+// With tolerance (0.0-100.0 percentage) - use sparingly
+VerifyScreenshot(tolerance: 0.5); // Allow 0.5% difference for cross-machine rendering variance
+
+// PREFERRED: Keep retrying for up to 2 seconds (for animations)
+VerifyScreenshot(retryTimeout: TimeSpan.FromSeconds(2));
+
+// Combined: tolerance for rendering variance + retryTimeout for timing
+VerifyScreenshot(tolerance: 0.5, retryTimeout: TimeSpan.FromSeconds(2));
+
// Manual screenshot for debugging
App.Screenshot("TestStep1");
```
+**CRITICAL - VerifyScreenshot() Built-in Features:**
+
+`VerifyScreenshot()` **already includes** stability mechanisms. Do NOT add redundant delays:
+
+| Feature | Behavior | Parameter |
+|---------|----------|-----------|
+| **Android delay** | Automatic 350ms wait for animations | Built-in, cannot override |
+| **Retry logic** | Default: retries once; with retryTimeout: keeps retrying | Built-in |
+| **Retry delay** | 500ms delay between retry attempts | `retryDelay: TimeSpan` (customizable) |
+| **Retry timeout** | Total time to keep retrying | `retryTimeout: TimeSpan` (PREFERRED for flaky tests) |
+| **Tolerance** | Allow percentage difference (0-100) | `tolerance: double` (default: 0.0) |
+
+**When to customize:**
+- ✅ Use `retryTimeout` parameter for animations with variable timing (PREFERRED approach)
+- ✅ Use small `tolerance` (0.5%) for cross-machine rendering variance, NOT to hide timing issues
+- ✅ Use `retryDelay` if you need to change the delay between retry attempts
+- ❌ **DO NOT** add `Task.Delay()` or `Thread.Sleep()` before `VerifyScreenshot()` - use `retryTimeout` instead
+
+## Writing Robust UI Tests
+
+### Best Practices for Screenshot Tests
+
+When writing tests that use `VerifyScreenshot()`, follow these patterns to avoid flakiness:
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│ 1. UNDERSTAND TEST INFRASTRUCTURE │
+│ - Read UITest.cs base class implementation │
+│ - Understand built-in retry/delay/tolerance mechanisms │
+│ - Check what helpers/extensions already exist │
+├─────────────────────────────────────────────────────────────────┤
+│ 2. USE PROPER WAITING PATTERNS │
+│ - Use WaitForElement before interacting with elements │
+│ - Use retryTimeout for screenshots after animations │
+│ - Never use arbitrary Task.Delay() before VerifyScreenshot │
+├─────────────────────────────────────────────────────────────────┤
+│ 3. APPLY MINIMAL TOLERANCES │
+│ - Use retryTimeout for timing issues (preferred) │
+│ - Use small tolerance (0.5%) only for rendering variance │
+│ - Never use tolerance > 5% without justification │
+└─────────────────────────────────────────────────────────────────┘
+```
+
+### Common Flaky Test Patterns
+
+| Symptom | Root Cause | Fix Pattern | Anti-Pattern |
+|---------|------------|-------------|--------------|
+| **Visual diff in screenshot** | Animation not finished | `VerifyScreenshot(retryTimeout: TimeSpan.FromSeconds(2))` | ❌ Adding `Task.Delay()` before |
+| **Element not found** | Element not rendered yet | `App.WaitForElement("Id", timeout: TimeSpan.FromSeconds(10))` | ❌ `Thread.Sleep()` then `FindElement()` |
+| **Timeout on interaction** | Page not fully loaded | Wait for specific element that indicates ready state | ❌ Arbitrary 3-second delay |
+| **Inconsistent rect/position** | Layout not settled | Multiple `GetRect()` calls with comparison | ❌ Single `GetRect()` after delay |
+| **WebView failures** | External URL/network | Use mock URLs instead of external URLs | ❌ Adding longer timeouts |
+
+### Anti-Patterns (DO NOT DO)
+
+| Anti-Pattern | Why It's Wrong | Better Alternative |
+|--------------|----------------|-------------------|
+| ❌ `Task.Delay(500).Wait()` before `VerifyScreenshot()` | VerifyScreenshot already has built-in retry; use retryTimeout instead | Use `VerifyScreenshot(retryTimeout: TimeSpan.FromSeconds(2))` |
+| ❌ `Thread.Sleep(2000)` before element interaction | Arbitrary wait; doesn't guarantee element is ready | `App.WaitForElement("Id", timeout: ...)` |
+| ❌ Adding tolerance > 5% without justification | Hides real bugs; too permissive | Use `retryTimeout` for timing issues; small tolerance (0.5%) for rendering variance |
+| ❌ Using external URLs in WebView tests | External dependency; unreliable | Use mock URLs or local content |
+| ❌ Fixing symptoms without understanding infrastructure | Redundant fixes; doesn't address root cause | Read `UITest.cs` first (step 1 above) |
+
+### When to Use What
+
+**VerifyScreenshot() parameters (preferred):**
+```csharp
+// Animation timing issues - keep retrying for up to 2 seconds
+// This is the PREFERRED approach for flaky screenshot tests
+VerifyScreenshot(retryTimeout: TimeSpan.FromSeconds(2));
+
+// Small tolerance for cross-machine rendering variance + retryTimeout for timing
+// Use 0.5% tolerance as safety margin, NOT to hide timing issues
+VerifyScreenshot(tolerance: 0.5, retryTimeout: TimeSpan.FromSeconds(2));
+
+// Legacy: retryDelay only changes delay BETWEEN retries (default 500ms)
+// retryTimeout is preferred because it keeps trying until success
+VerifyScreenshot(retryDelay: TimeSpan.FromSeconds(1));
+```
+
+**Key difference: retryDelay vs retryTimeout:**
+- `retryDelay`: Delay between retry attempts (default 500ms). Only retries ONCE.
+- `retryTimeout`: Total time to keep retrying. Retries every `retryDelay` until timeout.
+- **Prefer `retryTimeout`** for animations with variable completion times.
+
+**WaitForElement (for element readiness):**
+```csharp
+// Wait up to 10 seconds for element to appear
+App.WaitForElement("ButtonId", timeout: TimeSpan.FromSeconds(10));
+
+// Then interact
+App.Tap("ButtonId");
+```
+
+**Task.Delay/Thread.Sleep (avoid if possible):**
+```csharp
+// AVOID: With retryTimeout, you rarely need explicit delays anymore
+//
+// Old pattern (before retryTimeout):
+// Task.Delay(300).Wait();
+// VerifyScreenshot(tolerance: 2.0);
+//
+// New pattern (preferred):
+VerifyScreenshot(tolerance: 0.5, retryTimeout: TimeSpan.FromSeconds(2));
+
+// ONLY use explicit delays when:
+// 1. Waiting for non-element state with no screenshot (rare)
+// 2. External system delay that can't be detected otherwise
+// 3. After exhausting other options AND documenting why
+```
+
+### Understanding Test Infrastructure
+
+**Key files to understand when writing UI tests:**
+
+1. **UITest.cs** - Base class with `VerifyScreenshot()` implementation
+ - Path: `src/Controls/tests/TestCases.Shared.Tests/UITest.cs`
+ - Contains: retry logic, tolerance parsing, platform-specific delays
+
+2. **_IssuesUITest.cs** - Issues test base class
+ - Path: `src/Controls/tests/TestCases.Shared.Tests/_IssuesUITest.cs`
+ - Contains: Navigation helpers, common patterns
+
+3. **Extension methods** - Platform-specific helpers
+ - Path: `src/Controls/tests/TestCases.Shared.Tests/` (various extension files)
+ - Contains: Existing helpers for common operations
+
+**Find existing patterns:**
+```bash
+# See VerifyScreenshot implementation (including retryTimeout)
+grep -A 30 "public void VerifyScreenshot" src/Controls/tests/TestCases.Shared.Tests/UITest.cs
+
+# Find existing tests using retryTimeout (preferred pattern)
+grep -r "retryTimeout" src/Controls/tests/TestCases.Shared.Tests/Tests/
+
+# Find existing tolerance patterns
+grep -r "tolerance:" src/Controls/tests/TestCases.Shared.Tests/Tests/
+```
+
+### Infrastructure Notes
+
+**Tolerance regex handles multiple locales:** The tolerance parsing uses regex pattern `\d+[.,]\d+` to match both `.` and `,` as decimal separators (e.g., "2.5%" or "2,5%"). If tolerance appears to not be applied, verify the regex patterns in `UITest.cs` `VerifyWithTolerance()` method.
+
## Test Categories
### Category Guidelines
diff --git a/.github/skills/pr-finalize/SKILL.md b/.github/skills/pr-finalize/SKILL.md
index 06dc93ef89cd..7310f356d465 100644
--- a/.github/skills/pr-finalize/SKILL.md
+++ b/.github/skills/pr-finalize/SKILL.md
@@ -1,6 +1,6 @@
---
name: pr-finalize
-description: Finalizes any PR for merge by verifying title and description match actual implementation. Reviews existing description quality before suggesting changes. Use when asked to "finalize PR", "check PR description", "review commit message", before merging any PR, or when PR implementation changed during review. Do NOT use for extracting lessons (use learn-from-pr), writing tests (use write-tests), or investigating build failures (use pr-build-status).
+description: Finalizes any PR for merge by verifying title and description match actual implementation. Reviews existing description quality before suggesting changes. Use when asked to "finalize PR", "check PR description", "review commit message", before merging any PR, or when PR implementation changed during review. Do NOT use for extracting lessons (use learn-from-pr), writing tests (use write-tests-agent), or investigating build failures (use pr-build-status).
---
# PR Finalize
diff --git a/.github/skills/write-tests/SKILL.md b/.github/skills/write-tests/SKILL.md
deleted file mode 100644
index 0149c1b82317..000000000000
--- a/.github/skills/write-tests/SKILL.md
+++ /dev/null
@@ -1,206 +0,0 @@
----
-name: write-tests
-description: Creates UI tests for a GitHub issue and verifies they reproduce the bug. Iterates until tests actually fail (proving they catch the issue). Use when PR lacks tests or tests need to be created for an issue.
-metadata:
- author: dotnet-maui
- version: "1.0"
-compatibility: Requires git, PowerShell, .NET SDK, and Appium for UI test execution.
----
-
-# Write Tests Skill
-
-Creates UI tests that reproduce a GitHub issue, following .NET MAUI conventions. **Verifies the tests actually fail before completing.**
-
-## When to Use
-
-- ✅ PR has no tests and needs them
-- ✅ Issue needs a reproduction test before fixing
-- ✅ Existing tests don't adequately cover the bug
-
-## Required Input
-
-Before invoking, ensure you have:
-- **Issue number** (e.g., 33331)
-- **Issue description** or reproduction steps
-- **Platforms affected** (iOS, Android, Windows, MacCatalyst)
-
-## Workflow
-
-### Step 1: Read the UI Test Guidelines
-
-```bash
-cat .github/instructions/uitests.instructions.md
-```
-
-This contains the authoritative conventions for:
-- File naming (`IssueXXXXX.xaml`, `IssueXXXXX.cs`)
-- File locations (`TestCases.HostApp/Issues/`, `TestCases.Shared.Tests/Tests/Issues/`)
-- Required attributes (`[Issue()]`, `[Category()]`)
-- Test patterns and assertions
-
-### Step 2: Create HostApp Page
-
-**Location:** `src/Controls/tests/TestCases.HostApp/Issues/IssueXXXXX.cs`
-
-```csharp
-namespace Maui.Controls.Sample.Issues;
-
-[Issue(IssueTracker.Github, XXXXX, "Brief description of issue", PlatformAffected.All)]
-public partial class IssueXXXXX : ContentPage
-{
- public IssueXXXXX()
- {
- // Create UI that reproduces the issue
- var button = new Button
- {
- Text = "Test Button",
- AutomationId = "TestButton" // Required for Appium
- };
-
- var resultLabel = new Label
- {
- Text = "Waiting...",
- AutomationId = "ResultLabel"
- };
-
- button.Clicked += (s, e) =>
- {
- resultLabel.Text = "Success";
- };
-
- Content = new VerticalStackLayout
- {
- Children = { button, resultLabel }
- };
- }
-}
-```
-
-**Key requirements:**
-- Add `AutomationId` to all interactive elements
-- Use `[Issue()]` attribute with tracker, number, description, platform
-- Keep UI minimal - just enough to reproduce the bug
-
-### Step 3: Create NUnit Test
-
-**Location:** `src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/IssueXXXXX.cs`
-
-```csharp
-namespace Microsoft.Maui.TestCases.Shared.Tests.Tests.Issues;
-
-public class IssueXXXXX : _IssuesUITest
-{
- public override string Issue => "Brief description matching HostApp";
-
- public IssueXXXXX(TestDevice device) : base(device) { }
-
- [Test]
- [Category(UITestCategories.Button)] // Pick ONE appropriate category
- public void ButtonClickUpdatesLabel()
- {
- // Wait for element to be ready
- App.WaitForElement("TestButton");
-
- // Interact with the UI
- App.Tap("TestButton");
-
- // Verify expected behavior
- var labelText = App.FindElement("ResultLabel").GetText();
- Assert.That(labelText, Is.EqualTo("Success"));
- }
-}
-```
-
-**Key requirements:**
-- Inherit from `_IssuesUITest`
-- Use same `AutomationId` values as HostApp
-- Add ONE `[Category()]` attribute (check `UITestCategories.cs` for options)
-- Use `App.WaitForElement()` before interactions
-
-### Step 4: Verify Files Compile
-
-```bash
-dotnet build src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj -c Debug -f net10.0-android --no-restore -v q
-dotnet build src/Controls/tests/TestCases.Shared.Tests/Controls.TestCases.Shared.Tests.csproj -c Debug --no-restore -v q
-```
-
-### Step 5: Verify Tests Reproduce the Bug ⚠️ CRITICAL
-
-**Tests must FAIL to prove they catch the bug.** Run verification:
-
-```bash
-pwsh .github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1 -Platform ios -TestFilter "IssueXXXXX"
-```
-
-The script auto-detects that only test files exist (no fix files) and runs in "verify failure only" mode.
-
-**If tests FAIL** → ✅ Success! Tests correctly reproduce the bug.
-
-**If tests PASS** → ❌ Your test is wrong. Go back to Step 2 and fix:
-- Review test scenario against issue description
-- Ensure test actions match reproduction steps
-- Update and rerun until tests FAIL
-
-**Do NOT mark this skill complete until tests FAIL.**
-
-## Output
-
-After completion (tests verified to fail), report:
-```markdown
-✅ Tests created and verified for Issue #XXXXX
-
-**Files:**
-- `src/Controls/tests/TestCases.HostApp/Issues/IssueXXXXX.cs`
-- `src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/IssueXXXXX.cs`
-
-**Test method:** `ButtonClickUpdatesLabel`
-**Category:** `UITestCategories.Button`
-**Verification:** Tests FAIL as expected (bug reproduced)
-```
-
-## Common Patterns
-
-### Testing Property Changes
-```csharp
-// HostApp: Add a way to trigger and observe the property
-var picker = new Picker { AutomationId = "TestPicker" };
-var statusLabel = new Label { AutomationId = "StatusLabel" };
-picker.PropertyChanged += (s, e) => {
- if (e.PropertyName == nameof(Picker.IsOpen))
- statusLabel.Text = $"IsOpen={picker.IsOpen}";
-};
-
-// Test: Verify the property changes correctly
-App.Tap("TestPicker");
-App.WaitForElement("StatusLabel");
-var status = App.FindElement("StatusLabel").GetText();
-Assert.That(status, Does.Contain("IsOpen=True"));
-```
-
-### Testing Layout/Positioning
-```csharp
-// Test: Use GetRect() for position/size assertions
-var rect = App.WaitForElement("TestElement").GetRect();
-Assert.That(rect.Height, Is.GreaterThan(0));
-Assert.That(rect.Y, Is.GreaterThanOrEqualTo(safeAreaTop));
-```
-
-### Testing Platform-Specific Behavior
-```csharp
-// Only limit platforms when NECESSARY
-[Test]
-[Category(UITestCategories.Picker)]
-public void PickerDismissResetsIsOpen()
-{
- // This test should run on all platforms unless there's
- // a specific technical reason it can't
- App.WaitForElement("TestPicker");
- // ...
-}
-```
-
-## References
-
-- **Full conventions:** `.github/instructions/uitests.instructions.md`
-- **Category list:** `src/Controls/tests/TestCases.Shared.Tests/UITestCategories.cs`
-- **Example tests:** `src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/`
diff --git a/.github/skills/write-ui-tests/SKILL.md b/.github/skills/write-ui-tests/SKILL.md
new file mode 100644
index 000000000000..11fd86858444
--- /dev/null
+++ b/.github/skills/write-ui-tests/SKILL.md
@@ -0,0 +1,325 @@
+---
+name: write-ui-tests
+description: Creates UI tests for a GitHub issue and verifies they reproduce the bug. Iterates until tests actually fail (proving they catch the issue). Use when PR lacks tests or tests need to be created for an issue.
+metadata:
+ author: dotnet-maui
+ version: "1.1"
+compatibility: Requires git, PowerShell, .NET SDK, and Appium for UI test execution.
+---
+
+# Write UI Tests Skill
+
+Creates UI tests that reproduce a GitHub issue, following .NET MAUI conventions. **Verifies the tests actually fail before completing.**
+
+## 🛑 BLOCKING REQUIREMENT
+
+**YOU CANNOT COMPLETE THIS SKILL UNTIL TESTS FAIL.**
+
+A test that passes does NOT prove it catches the bug. You MUST:
+1. Run tests and observe them **FAIL**
+2. If tests pass, **iterate on test code** until they fail
+3. Never report "done" with passing tests
+
+If tests keep passing after 3 iterations:
+- STOP and ask user: "Tests are passing but they should fail to prove they catch the bug. The test scenario may not correctly reproduce the issue. Should I try a different approach?"
+
+**Common mistakes that lead to passing tests:**
+- Test scenario doesn't match issue reproduction steps
+- Checking wrong element or property
+- Bug only manifests on specific platform (try different platform)
+- Bug requires specific timing or async behavior not captured
+- Issue description is incomplete - may need to ask user for clarification
+
+## When to Use
+
+- ✅ PR has no tests and needs them
+- ✅ Issue needs a reproduction test before fixing
+- ✅ Existing tests don't adequately cover the bug
+
+## Required Input
+
+Before invoking, ensure you have:
+- **Issue number** (e.g., 33331)
+- **Issue description** or reproduction steps
+- **Platforms affected** (iOS, Android, Windows, MacCatalyst)
+
+**Platform selection guidance:**
+- Start with the platform mentioned in the issue (often in title or labels)
+- If issue says "iOS" or has `platform/iOS` label → test on iOS first
+- If issue says "Android" or has `platform/Android` label → test on Android first
+- If issue affects "All" platforms → start with Android (faster emulator boot)
+- If test passes on one platform, try another before concluding test is wrong
+
+## Workflow
+
+### Step 1: Read the UI Test Guidelines
+
+```bash
+cat .github/instructions/uitests.instructions.md
+```
+
+This contains the authoritative conventions for:
+- File naming (`IssueXXXXX.cs` for C#-only, or `IssueXXXXX.xaml`/`.xaml.cs` for XAML)
+- File locations (`TestCases.HostApp/Issues/`, `TestCases.Shared.Tests/Tests/Issues/`)
+- Required attributes (`[Issue()]`, `[Category()]`)
+- Test patterns and assertions
+
+### Step 2: Create HostApp Page
+
+**Location:** `src/Controls/tests/TestCases.HostApp/Issues/IssueXXXXX.cs`
+
+```csharp
+namespace Maui.Controls.Sample.Issues;
+
+[Issue(IssueTracker.Github, XXXXX, "Brief description of issue", PlatformAffected.All)]
+public partial class IssueXXXXX : ContentPage
+{
+ public IssueXXXXX()
+ {
+ // Create UI that reproduces the issue
+ var button = new Button
+ {
+ Text = "Test Button",
+ AutomationId = "TestButton" // Required for Appium
+ };
+
+ var resultLabel = new Label
+ {
+ Text = "Waiting...",
+ AutomationId = "ResultLabel"
+ };
+
+ button.Clicked += (s, e) =>
+ {
+ resultLabel.Text = "Success";
+ };
+
+ Content = new VerticalStackLayout
+ {
+ Children = { button, resultLabel }
+ };
+ }
+}
+```
+
+**Key requirements:**
+- Add `AutomationId` to all interactive elements
+- Use `[Issue()]` attribute with tracker, number, description, platform
+- Keep UI minimal - just enough to reproduce the bug
+
+**Note:** XAML is optional. C#-only pages (as shown above) are simpler and preferred for most test scenarios. Use XAML only when the bug specifically relates to XAML parsing or markup behavior.
+
+### Step 3: Create NUnit Test
+
+**Location:** `src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/IssueXXXXX.cs`
+
+```csharp
+namespace Microsoft.Maui.TestCases.Tests.Issues;
+
+public class IssueXXXXX : _IssuesUITest
+{
+ public override string Issue => "Brief description matching HostApp";
+
+ public IssueXXXXX(TestDevice device) : base(device) { }
+
+ [Test]
+ [Category(UITestCategories.Button)] // Pick ONE appropriate category
+ public void ButtonClickUpdatesLabel()
+ {
+ // Wait for element to be ready
+ App.WaitForElement("TestButton");
+
+ // Interact with the UI
+ App.Tap("TestButton");
+
+ // Verify expected behavior
+ var labelText = App.FindElement("ResultLabel").GetText();
+ Assert.That(labelText, Is.EqualTo("Success"));
+ }
+}
+```
+
+**Key requirements:**
+- Inherit from `_IssuesUITest`
+- Use same `AutomationId` values as HostApp
+- Add ONE `[Category()]` attribute (check `UITestCategories.cs` for options)
+- Use `App.WaitForElement()` before interactions
+
+### Step 4: Verify Files Compile
+
+```bash
+# For Android
+dotnet build src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj -c Debug -f net10.0-android --no-restore -v q
+
+# For iOS
+dotnet build src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj -c Debug -f net10.0-ios --no-restore -v q
+
+# Test project (platform-independent)
+dotnet build src/Controls/tests/TestCases.Shared.Tests/Controls.TestCases.Shared.Tests.csproj -c Debug --no-restore -v q
+```
+
+### Step 5: Verify Tests Reproduce the Bug ⚠️ CRITICAL
+
+**Tests must FAIL to prove they catch the bug.** Run verification:
+
+```bash
+pwsh .github/skills/verify-tests-fail-without-fix/scripts/verify-tests-fail.ps1 -Platform -TestFilter "IssueXXXXX"
+```
+
+Replace `` with `android`, `ios`, or `maccatalyst` based on the issue's affected platforms.
+
+The script auto-detects that only test files exist (no fix files) and runs in "verify failure only" mode.
+
+> **Why FAIL = success?** The test must fail NOW (before the fix) to prove it catches the bug. After the fix is applied, it should pass. A test that passes now proves nothing.
+
+**If tests FAIL** → ✅ Success! Tests correctly reproduce the bug. Proceed to Output.
+
+**If tests PASS** → ❌ **STOP.** Test doesn't catch the bug. Iterate:
+
+1. **Re-read the issue reproduction steps** - Is your test doing exactly what the issue describes?
+2. **Check if you're testing the right thing** - Are you asserting on the correct element/property?
+3. **Try a different platform** - Bug may only manifest on iOS vs Android
+4. **Add debug output** - Use `Console.WriteLine` in HostApp to trace execution
+5. **Simplify** - Remove complexity until you isolate the bug behavior
+6. **After 3 failed iterations, STOP and ask user:**
+ > "Tests are passing after 3 iterations. This means either: (a) my test scenario doesn't correctly reproduce the bug, (b) the bug may already be fixed on this branch, or (c) I'm missing something from the issue description. How would you like me to proceed?"
+
+**Common reasons tests pass when they shouldn't:**
+| Symptom | Likely Cause | Fix |
+|---------|--------------|-----|
+| Test passes on all attempts | Test scenario doesn't match bug | Re-read issue reproduction steps carefully |
+| Test asserts pass but bug exists | Asserting wrong property/element | Check what exactly the bug affects |
+| Works on Android, fails on iOS | Bug is platform-specific | Try both platforms |
+| Bug involves timing | Race condition not captured | Add delays or event handlers |
+| Bug involves navigation | Page lifecycle not exercised | Ensure pages are actually pushed/popped |
+
+**Do NOT mark this skill complete until tests FAIL.**
+
+## Output
+
+**⚠️ ONLY use this output format if tests FAIL.** If tests pass, you have not completed this skill.
+
+After completion (tests verified to fail), report:
+```markdown
+✅ Tests created and verified for Issue #XXXXX
+
+**Files:**
+- `src/Controls/tests/TestCases.HostApp/Issues/IssueXXXXX.cs`
+- `src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/IssueXXXXX.cs`
+
+**Test method:** `ButtonClickUpdatesLabel`
+**Category:** `UITestCategories.Button`
+**Verification:** Tests FAIL as expected (bug reproduced)
+**Failure message:** `Expected "X" but got "Y"` (include actual assertion failure)
+```
+
+**If tests PASS after multiple iterations**, report instead:
+```markdown
+⚠️ Tests created but NOT verified for Issue #XXXXX
+
+**Files:** [list files]
+**Status:** Tests PASS when they should FAIL
+**Iterations tried:** 3
+**Problem:** [describe why test may not be catching the bug]
+**Next steps:** Need guidance on reproduction steps
+```
+
+## Common Patterns
+
+### Testing Property Changes
+```csharp
+// HostApp: Add a way to trigger and observe the property
+var picker = new Picker { AutomationId = "TestPicker" };
+var statusLabel = new Label { AutomationId = "StatusLabel" };
+picker.PropertyChanged += (s, e) => {
+ if (e.PropertyName == nameof(Picker.IsOpen))
+ statusLabel.Text = $"IsOpen={picker.IsOpen}";
+};
+
+// Test: Verify the property changes correctly
+App.Tap("TestPicker");
+App.WaitForElement("StatusLabel");
+var status = App.FindElement("StatusLabel").GetText();
+Assert.That(status, Does.Contain("IsOpen=True"));
+```
+
+### Testing Layout/Positioning
+```csharp
+// Test: Use GetRect() for position/size assertions
+var rect = App.WaitForElement("TestElement").GetRect();
+Assert.That(rect.Height, Is.GreaterThan(0));
+Assert.That(rect.Y, Is.GreaterThanOrEqualTo(safeAreaTop));
+```
+
+### Testing Visual State (Screenshots)
+```csharp
+// Use retryTimeout for animations - keeps retrying until success
+App.Tap("AnimatedButton");
+VerifyScreenshot(retryTimeout: TimeSpan.FromSeconds(2));
+
+// retryTimeout handles timing variance, small tolerance for cross-machine rendering
+VerifyScreenshot(tolerance: 0.5, retryTimeout: TimeSpan.FromSeconds(2));
+```
+
+### Testing Platform-Specific Behavior
+```csharp
+// Only limit platforms when NECESSARY
+[Test]
+[Category(UITestCategories.Picker)]
+public void PickerDismissResetsIsOpen()
+{
+ // This test should run on all platforms unless there's
+ // a specific technical reason it can't
+ App.WaitForElement("TestPicker");
+ // ...
+}
+```
+
+## iOS Device Selection
+
+When running tests on iOS, you may need to target a specific device or iOS version:
+
+```bash
+# Default: iPhone Xs with iOS 18.5
+pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform ios -TestFilter "Issue12345"
+
+# Find iPhone Xs with iOS 18.5 and get its UDID
+UDID=$(xcrun simctl list devices available --json | jq -r '
+ .devices | to_entries
+ | map(select(.key | contains("iOS-18-5")))
+ | map(.value) | flatten
+ | map(select(.name == "iPhone Xs")) | first | .udid')
+
+# Run with specific device
+pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform ios -TestFilter "Issue12345" -DeviceUdid "$UDID"
+```
+
+**Finding different device/version combinations:**
+```bash
+# iPhone 16 Pro with any iOS version
+UDID=$(xcrun simctl list devices available --json | jq -r '
+ .devices[][] | select(.name == "iPhone 16 Pro") | .udid' | head -1)
+
+# Any device with iOS 18.0
+UDID=$(xcrun simctl list devices available --json | jq -r '
+ .devices | to_entries
+ | map(select(.key | contains("iOS-18-0")))
+ | map(.value) | flatten | .[0].udid')
+```
+
+## Pre-Run Checklist
+
+Before running `verify-tests-fail.ps1`, confirm:
+
+- [ ] HostApp file exists: `TestCases.HostApp/Issues/IssueXXXXX.cs`
+- [ ] NUnit test file exists: `TestCases.Shared.Tests/Tests/Issues/IssueXXXXX.cs`
+- [ ] `[Issue()]` attribute present with all parameters
+- [ ] All `AutomationId` values match between HostApp and test
+- [ ] Test inherits from `_IssuesUITest`
+- [ ] ONE `[Category()]` attribute from `UITestCategories.cs`
+
+## References
+
+- **Full conventions:** `.github/instructions/uitests.instructions.md`
+- **Category list:** `src/Controls/tests/TestCases.Shared.Tests/UITestCategories.cs`
+- **Example tests:** `src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/`
diff --git a/.github/skills/write-xaml-tests/SKILL.md b/.github/skills/write-xaml-tests/SKILL.md
new file mode 100644
index 000000000000..3fff9b304287
--- /dev/null
+++ b/.github/skills/write-xaml-tests/SKILL.md
@@ -0,0 +1,91 @@
+---
+name: write-xaml-tests
+description: Creates XAML unit tests for GitHub issues in the Controls.Xaml.UnitTests project. Tests XAML parsing, compilation (XamlC), and source generation. Use when testing XAML-specific behavior, not UI interactions.
+metadata:
+ author: Stephane Delcroix
+ version: "1.0"
+compatibility: Requires .NET SDK for building and running xUnit tests.
+---
+
+# Write XAML Tests Skill
+
+Creates XAML unit tests that verify XAML parsing, XamlC compilation, and source generation behavior.
+
+## When to Use
+
+- ✅ Testing XAML parsing/inflation behavior
+- ✅ Testing XamlC (IL generation) correctness
+- ✅ Testing Source Generator output
+- ✅ XAML-specific bugs (bindings, markup extensions, x:Name, etc.)
+
+## When NOT to Use
+
+- ❌ Testing UI interactions or visual behavior → Use `write-ui-tests` skill
+- ❌ Testing runtime behavior after page loads → Use `write-ui-tests` skill
+- ❌ Testing platform-specific rendering → Use `write-ui-tests` skill
+
+## Required Input
+
+Before invoking, ensure you have:
+- **Issue number** (e.g., 12345)
+- **Issue description** - what XAML behavior is broken
+- **Expected vs actual behavior**
+
+## Workflow
+
+### Step 1: Read the XAML Unit Test Guidelines
+
+```bash
+cat .github/instructions/xaml-unittests.instructions.md
+```
+
+This contains the authoritative conventions for:
+- File naming (`MauiXXXXX.xaml` and `MauiXXXXX.xaml.cs`)
+- File location (`src/Controls/tests/Xaml.UnitTests/Issues/`)
+- Test patterns with `[Values] XamlInflator`
+- XamlC testing with `MockCompiler`
+- Source Generator testing with `MockSourceGenerator`
+- Special file extensions for invalid codegen (`.rt.xaml`, `.rtsg.xaml`, `.rtxc.xaml`)
+
+### Step 2: Create Test Files
+
+Following the conventions from Step 1, create:
+- `src/Controls/tests/Xaml.UnitTests/Issues/MauiXXXXX.xaml`
+- `src/Controls/tests/Xaml.UnitTests/Issues/MauiXXXXX.xaml.cs`
+
+### Step 3: Verify Tests Compile and Run
+
+```bash
+# Build the test project
+dotnet build src/Controls/tests/Xaml.UnitTests/Controls.Xaml.UnitTests.csproj -c Debug --no-restore -v q
+
+# Run specific test
+dotnet test src/Controls/tests/Xaml.UnitTests/Controls.Xaml.UnitTests.csproj --filter "FullyQualifiedName~MauiXXXXX" --no-build
+```
+
+### Step 4: Verify Test Behavior
+
+- **For bug reproduction tests**: Tests should FAIL before fix, PASS after fix
+- **For regression tests**: Tests should PASS to confirm behavior works
+
+## Output
+
+After completion, report:
+
+```markdown
+✅ XAML unit test created for Issue #XXXXX
+
+**Files:**
+- `src/Controls/tests/Xaml.UnitTests/Issues/MauiXXXXX.xaml`
+- `src/Controls/tests/Xaml.UnitTests/Issues/MauiXXXXX.xaml.cs`
+
+**Test method:** `DescriptiveTestName`
+**Inflators tested:** Runtime, XamlC, SourceGen
+**Verification:** Tests [PASS/FAIL] as expected
+```
+
+## References
+
+- **Full conventions:** `.github/instructions/xaml-unittests.instructions.md`
+- **Test project:** `src/Controls/tests/Xaml.UnitTests/`
+- **Existing issue tests:** `src/Controls/tests/Xaml.UnitTests/Issues/`
diff --git a/src/Controls/tests/TestCases.Shared.Tests/UITest.cs b/src/Controls/tests/TestCases.Shared.Tests/UITest.cs
index 68a0a7f6b708..44980598dc9d 100644
--- a/src/Controls/tests/TestCases.Shared.Tests/UITest.cs
+++ b/src/Controls/tests/TestCases.Shared.Tests/UITest.cs
@@ -153,6 +153,7 @@ public void VerifyScreenshotOrSetException(
ref Exception? exception,
string? name = null,
TimeSpan? retryDelay = null,
+ TimeSpan? retryTimeout = null,
int cropLeft = 0,
int cropRight = 0,
int cropTop = 0,
@@ -165,7 +166,7 @@ public void VerifyScreenshotOrSetException(
{
try
{
- VerifyScreenshot(name, retryDelay, cropLeft, cropRight, cropTop, cropBottom, tolerance
+ VerifyScreenshot(name, retryDelay, retryTimeout, cropLeft, cropRight, cropTop, cropBottom, tolerance
#if MACUITEST || WINTEST
, includeTitleBar
#endif
@@ -181,7 +182,9 @@ public void VerifyScreenshotOrSetException(
/// Verifies a screenshot by comparing it against a baseline image and throws an exception if verification fails.
///
/// Optional name for the screenshot. If not provided, a default name will be used.
- /// Optional delay between retry attempts when verification fails.
+ /// Optional delay between retry attempts when verification fails. Default is 500ms.
+ /// Optional total time to keep retrying before giving up. If not specified, only one retry is attempted.
+ /// Use this for animations with variable completion times (e.g., retryTimeout: TimeSpan.FromSeconds(2)).
/// Number of pixels to crop from the left of the screenshot.
/// Number of pixels to crop from the right of the screenshot.
/// Number of pixels to crop from the top of the screenshot.
@@ -205,6 +208,9 @@ public void VerifyScreenshotOrSetException(
/// // Allow 5% difference for animations or slight rendering variations
/// VerifyScreenshot("ButtonHoverState", tolerance: 5.0);
///
+ /// // Keep retrying for up to 2 seconds (useful for animations)
+ /// VerifyScreenshot("AnimatedElement", retryTimeout: TimeSpan.FromSeconds(2));
+ ///
/// // Combined with cropping and tolerance
/// VerifyScreenshot("HeaderSection", cropTop: 50, cropBottom: 100, tolerance: 3.0);
///
@@ -212,6 +218,7 @@ public void VerifyScreenshotOrSetException(
public void VerifyScreenshot(
string? name = null,
TimeSpan? retryDelay = null,
+ TimeSpan? retryTimeout = null,
int cropLeft = 0,
int cropRight = 0,
int cropTop = 0,
@@ -223,15 +230,53 @@ public void VerifyScreenshot(
)
{
retryDelay ??= TimeSpan.FromMilliseconds(500);
- // Retry the verification once in case the app is in a transient state
- try
+
+ // If retryTimeout is specified, keep retrying until timeout expires
+ // Otherwise, just retry once (backward compatible behavior)
+ if (retryTimeout.HasValue)
{
- Verify(name);
+ var stopwatch = System.Diagnostics.Stopwatch.StartNew();
+ Exception? lastException = null;
+
+ while (stopwatch.Elapsed < retryTimeout.Value)
+ {
+ try
+ {
+ Verify(name);
+ return; // Success
+ }
+ catch (Exception ex)
+ {
+ lastException = ex;
+ if (stopwatch.Elapsed + retryDelay.Value < retryTimeout.Value)
+ {
+ Thread.Sleep(retryDelay.Value);
+ }
+ }
+ }
+
+ // Final attempt after timeout
+ try
+ {
+ Verify(name);
+ }
+ catch
+ {
+ throw lastException ?? new InvalidOperationException("Screenshot verification failed");
+ }
}
- catch
+ else
{
- Thread.Sleep(retryDelay.Value);
- Verify(name);
+ // Original behavior: retry once
+ try
+ {
+ Verify(name);
+ }
+ catch
+ {
+ Thread.Sleep(retryDelay.Value);
+ Verify(name);
+ }
}
void Verify(string? name)
@@ -451,8 +496,8 @@ double ExtractDifferencePercentage(Exception ex)
{
var message = ex.Message;
- // Extract percentage from pattern: "X,XX% difference"
- var match = Regex.Match(message, @"(\d+,\d+)%\s*difference", RegexOptions.IgnoreCase);
+ // Extract percentage from pattern: "X.XX% difference" or "X,XX% difference"
+ var match = Regex.Match(message, @"(\d+[.,]\d+)%\s*difference", RegexOptions.IgnoreCase);
if (match.Success)
{
var percentageString = match.Groups[1].Value.Replace(',', '.');