Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/agents/write-tests-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,18 @@ After the skill completes, report:
- Test verification result (FAIL = success)
- Failure message (proof tests catch the bug)

## Best Practices

**For detailed UI test best practices, see:**
- `.github/instructions/uitests.instructions.md` - Auto-loaded when working on test files

Key topics covered in the instructions:
- Prefer C# over XAML for most tests
- Use test helper base classes (TestShell, TestContentPage, etc.)
- Avoid obsolete APIs (Application.MainPage → Window.Page)
- Use UITest optimized controls (UITestEntry, UITestEditor, UITestSearchBar)
- Check similar tests for patterns

## Quick Reference

```bash
Expand Down
261 changes: 250 additions & 11 deletions .github/instructions/uitests.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,17 @@ This document provides specific guidance for GitHub Copilot when writing UI test

1. **HostApp UI Test Page** (`src/Controls/tests/TestCases.HostApp/Issues/`)
- Create the actual UI page that demonstrates the feature or reproduces the issue
- Use XAML with proper `AutomationId` attributes on interactive controls for test automation
- Follow naming convention: `IssueXXXXX.xaml` and `IssueXXXXX.xaml.cs`
- **Prefer C# only** (`.cs` file) unless testing XAML-specific features (bindings, templates, styles)
- Add `AutomationId` attributes on interactive controls for test automation
- Follow naming convention: `IssueXXXXX.cs` (C# only) or `IssueXXXXX.xaml` + `IssueXXXXX.xaml.cs` (when XAML required)
- XXXXX should correspond to a GitHub issue number when applicable
- Ensure the UI provides clear visual feedback for the behavior being tested
- Code-behind must include `[Issue()]` attribute with tracker, number, description, and platform
- Class must include `[Issue()]` attribute with tracker, number, description, and platform

2. **NUnit Test Implementation** (`src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/`)
- Create corresponding Appium-based NUnit tests that inherit from `_IssuesUITest`
- Use the `AutomationId` values to locate and interact with UI elements
- Follow naming convention: `IssueXXXXX.cs` (matches the HostApp XAML file)
- Follow naming convention: `IssueXXXXX.cs` (matches the HostApp page file)
- Include appropriate `[Category(UITestCategories.XYZ)]` attributes (only ONE per test)
- Test should validate expected behavior through UI interactions and assertions

Expand All @@ -46,7 +47,7 @@ This document provides specific guidance for GitHub Copilot when writing UI test

**Test Files:**
- Pattern: `IssueXXXXX.cs` where XXXXX corresponds to a GitHub issue number
- Must match the corresponding XAML file in TestCases.HostApp
- Must match the corresponding HostApp page file name in TestCases.HostApp (either `.cs` only or `.xaml`)

**Test Methods:**
- Use descriptive names that clearly explain what behavior is being verified
Expand All @@ -55,26 +56,86 @@ This document provides specific guidance for GitHub Copilot when writing UI test

**AutomationId Values:**
- Always use unique, descriptive `AutomationId` values
- Reference the same `AutomationId` in both XAML and test code
- Reference the same `AutomationId` in both C# code (or XAML if used) and test code
- Use PascalCase for AutomationId values

## Complete Test Example

**HostApp Code-Behind** (`TestCases.HostApp/Issues/Issue12345.xaml.cs`):
### Example 1: C# Only (Preferred for Most Tests)

**HostApp Page** (`TestCases.HostApp/Issues/Issue12345.cs`):
```csharp
namespace Maui.Controls.Sample.Issues;

[Issue(IssueTracker.Github, 12345, "Description of the issue being tested", PlatformAffected.All)]
public partial class Issue12345 : ContentPage
[Issue(IssueTracker.Github, 12345, "Button click updates label text", PlatformAffected.All)]
public class Issue12345 : ContentPage
{
public Issue12345()
{
var resultLabel = new Label
{
Text = "Initial Text",
AutomationId = "ResultLabel"
};

Content = new VerticalStackLayout
{
Children =
{
new Button
{
Text = "Click Me",
AutomationId = "TestButton",
Command = new Command(() => resultLabel.Text = "Expected Text")
},
resultLabel
}
};
}
}
```

### Example 2: XAML (When Testing XAML-Specific Features)

**HostApp XAML** (`TestCases.HostApp/Issues/Issue12346.xaml`):
```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.Issue12346">
<VerticalStackLayout>
<Button Text="Click Me"
AutomationId="TestButton"
Clicked="OnButtonClicked" />
<Label Text="Initial Text"
x:Name="ResultLabel"
AutomationId="ResultLabel" />
</VerticalStackLayout>
</ContentPage>
```

**HostApp Code-Behind** (`TestCases.HostApp/Issues/Issue12346.xaml.cs`):
```csharp
namespace Maui.Controls.Sample.Issues;

[Issue(IssueTracker.Github, 12346, "Testing XAML binding behavior", PlatformAffected.All)]
public partial class Issue12346 : ContentPage
{
public Issue12346()
{
InitializeComponent();
}

void OnButtonClicked(object sender, EventArgs e)
{
ResultLabel.Text = "Expected Text";
}
}
```

**NUnit Test** (`TestCases.Shared.Tests/Tests/Issues/Issue12345.cs`):
### NUnit Test (Same for Both Examples)

**NUnit Test** (`TestCases.Shared.Tests/Tests/Issues/Issue12345.cs` or `Issue12346.cs`):
```csharp
public class Issue12345 : _IssuesUITest
{
Expand Down Expand Up @@ -467,7 +528,7 @@ cat /tmp/ios_crash.log | grep -A 20 -B 5 "Exception"
Verify the following checklist before committing UI tests:

- [ ] Compile both the HostApp project and TestCases.Shared.Tests project successfully
- [ ] Verify AutomationId references match between XAML and test code
- [ ] Verify AutomationId references match between HostApp UI (C# or XAML) and test code
- [ ] Ensure file names follow the `IssueXXXXX` pattern and match between projects
- [ ] Ensure test methods have descriptive names
- [ ] Verify test inherits from `_IssuesUITest`
Expand All @@ -481,3 +542,181 @@ Verify the following checklist before committing UI tests:
- The test infrastructure handles navigation to the test page and basic cleanup
- If your test modifies global app state, consider whether cleanup is needed
- Most tests don't require explicit cleanup as each test gets a fresh page instance

## Best Practices

### Default: C# Over XAML

**Use C# files (`.cs`) for UI tests. Only use XAML files (`.xaml`) when the test scenario requires XAML-specific features.**

**When to use C# only (`.cs` file):**
- ✅ Simple control tests (Button, Label, Entry, etc.)
- ✅ Layout tests (Grid, StackLayout, FlexLayout, etc.)
- ✅ Navigation tests
- ✅ Event handling tests
- ✅ Property tests
- ✅ Most UI behavior tests

**When XAML is required (`.xaml` + `.xaml.cs` files):**
- ✅ Testing XAML binding syntax
- ✅ Testing XAML templates (DataTemplate, ControlTemplate)
- ✅ Testing XAML styles and resources
- ✅ Testing XAML markup extensions
- ✅ Testing XamlC compilation behavior
- ✅ Testing XAML-specific parsing or compilation issues

**Examples:**

```csharp
// ✅ GOOD: C# only test (most common pattern)
public class Issue12345 : ContentPage
{
public Issue12345()
{
Content = new StackLayout
{
Children =
{
new Label { Text = "Hello", AutomationId = "MyLabel" },
new Button { Text = "Click Me", AutomationId = "MyButton" }
}
};
}
}
```

```xaml
<!-- ❌ AVOID unless testing XAML bindings/templates/styles -->
<ContentPage ...>
<StackLayout>
<Label Text="Hello" AutomationId="MyLabel" />
<Button Text="Click Me" AutomationId="MyButton" />
</StackLayout>
</ContentPage>
```

### Use Test Helper Base Classes

**ALWAYS check for and use existing test helper base classes instead of creating from scratch:**

| Base Class | Use For | Example |
|------------|---------|---------|
| `TestShell` | Shell-related tests | `public class Issue12345 : TestShell` |
| `TestContentPage` | ContentPage tests needing `Init()` pattern | `public class Issue12345 : TestContentPage` |
| `TestNavigationPage` | NavigationPage tests | `public class Issue12345 : TestNavigationPage` |
| `ContentPage` | Simple page tests (direct inheritance) | `public class Issue12345 : ContentPage` |

**TestShell provides:**
- Platform-specific automation IDs for flyout and back buttons
- Helper methods: `AddContentPage()`, `AddBottomTab()`, `AddTopTab()`, `AddFlyoutItem()`
- Abstract `Init()` method for setup
- `DisplayedPage` property for accessing current page

**TestContentPage/TestNavigationPage provide:**
- Abstract `Init()` method for deferred initialization
- Cleaner separation of setup logic

**Example:**

```csharp
// ✅ GOOD: Using TestShell for Shell tests
[Issue(IssueTracker.Github, 12345, "Shell navigation bug", PlatformAffected.All)]
public class Issue12345 : TestShell
{
protected override void Init()
{
AddContentPage(new ContentPage
{
Content = new Label { Text = "Test" }
});
}
}

// ❌ BAD: Creating Shell from scratch
public class Issue12345 : Shell
{
public Issue12345()
{
Items.Add(new ShellItem { ... }); // Verbose, error-prone
}
}
```

### Avoid Obsolete APIs

**NEVER use obsolete APIs in new tests. Use modern equivalents:**

| ❌ Obsolete API | ✅ Modern API | Notes |
|----------------|--------------|-------|
| `Application.MainPage` | `Window.Page` | Access via `this.Window.Page` in ContentPage |
| `Application.MainPage` | `Application.Current.Windows[0].Page` | When not in Page context |
| `Frame` | `Border` | Frame is deprecated, use Border instead |
| `Device.BeginInvokeOnMainThread` | `Dispatcher.Dispatch` or `MainThread.BeginInvokeOnMainThread` | Modern threading APIs |

**Examples:**

```csharp
// ✅ GOOD: Modern Window API
this.Window.Page = new NavigationPage(new MyPage());

// ❌ BAD: Obsolete Application.MainPage
Application.MainPage = new NavigationPage(new MyPage());

// ✅ GOOD: Border
new Border { Content = new Label { Text = "Hello" } }

// ❌ BAD: Frame (deprecated)
new Frame { Content = new Label { Text = "Hello" } }
```

### Use UITest Optimized Controls for Screenshot Tests

**For tests that use `VerifyScreenshot()`, use UITest optimized controls instead of standard text input controls.** These controls provide `IsCursorVisible` to prevent cursor blinking from causing flaky screenshot comparisons.

| Standard Control | UITest Control | Purpose |
|------------------|----------------|---------|
| `Entry` | `UITestEntry` | Text input without cursor blink |
| `Editor` | `UITestEditor` | Multi-line input without cursor blink |
| `SearchBar` | `UITestSearchBar` | Search input without cursor blink |

**Example:**

```csharp
// For screenshot tests, use UITest controls (UITestEntry, UITestEditor, UITestSearchBar)
var entry = new UITestEntry
{
Placeholder = "Enter text",
IsCursorVisible = false, // Prevents flaky screenshots
AutomationId = "TestEntry"
};

// For non-screenshot tests, standard Entry is fine
var entry = new Entry { Placeholder = "Enter text", AutomationId = "TestEntry" };
```

**Location:** `src/Controls/tests/TestCases.HostApp/Controls/UITest*.cs`

### Check Similar Tests for Patterns

**Before creating a new test, search for similar tests to reuse patterns:**

```bash
# Find similar control tests
grep -r "class.*Issue.*Button" src/Controls/tests/TestCases.HostApp/Issues/*.cs

# Find Shell tests
grep -r "TestShell" src/Controls/tests/TestCases.HostApp/Issues/*.cs

# Find tests for specific control
grep -r "CollectionView" src/Controls/tests/TestCases.HostApp/Issues/*.cs

# Find tests using UITest optimized controls
grep -r "UITestEntry\|UITestEditor\|UITestSearchBar" src/Controls/tests/TestCases.HostApp/Issues/*.cs
```

**Reuse established patterns:**
- AutomationId naming conventions
- Test structure and layout
- Common helper methods
- Platform-specific workarounds
- UITest optimized control usage