| Dimension | Shared | Only in A | Only in B | Overlap |
|---|---|---|---|---|
| Sections | 0 | 36 | 59 | 0% |
| Commands | 1 | 18 | 2 | 5% |
| Section tags | 7 | 3 | 2 | 58% |
What each file covers
Sections
0 shared · 36 only in A · 59 only in B- − GitHub Copilot Development Environment Instructions
- − Code Review Instructions
- − Repository Overview
- − Key Technologies
- − Development Environment Setup
- − Platform-Specific Requirements
- − Project Structure
- − Important Directories
- − Platform-Specific Code Organization
- − Platform-Specific File Extensions
- − Sample Projects
- − Development Workflow
- − Testing
- − CI Pipelines (Azure DevOps)
- − Investigating CI Failures
- − Gradle / Maven Dependency Failures (CFSClean)
- − Code Formatting
- − Contribution Guidelines
- − Handling Existing PRs for Assigned Issues
- − Auto-Generated Files (Never Commit)
- − PublicAPI.Unshipped.txt File Management
- − Branching
- − Git Workflow (Copilot CLI Rules)
- − Create a feature branch (NEVER work directly on main)
- − Make commits normally
- − Push to remote (for new branches)
- − For subsequent pushes on the same branch
- − Check out the PR branch directly (do NOT create a new branch off it)
- − Make fixes and commit to the PR branch
- − Documentation
- − Opening PRs
- − Custom Agents and Skills
- − Skills vs Agents
- − Available Custom Agents
- − Reusable Skills
- − Using Custom Agents
- + UI Testing Guidelines for .NET MAUI
- + Overview
- + UI Test Structure
- + Two-Project Requirement
- + Base Class and Infrastructure
- + Naming Conventions
- + Complete Test Example
- + Example 1: C# Only (Preferred for Most Tests)
- + Example 2: XAML (When Testing XAML-Specific Features)
- + NUnit Test (Same for Both Examples)
- + Common Patterns
- + Waiting for Elements
- + Interacting with Elements
- + Assertions
- + Screenshot Verification
- + Writing Robust UI Tests
- + Best Practices for Screenshot Tests
- + Common Flaky Test Patterns
- + Anti-Patterns (DO NOT DO)
- + When to Use What
- + Understanding Test Infrastructure
- + See VerifyScreenshot implementation (including retryTimeout)
- + Find existing tests using retryTimeout (preferred pattern)
- + Find existing tolerance patterns
- + Infrastructure Notes
- + Test Categories
- + Category Guidelines
- + Platform Coverage
- + Default Behavior
- + No Inline #if Directives in Test Methods
- + Running UI Tests Locally
- + BuildAndRunHostApp.ps1 Script (ONLY Way to Run Tests)
- + Run specific test on Android
- + Run specific test on iOS
- + Run specific test on MacCatalyst
- + Run tests by category
- + Run specific test with custom device (iOS only)
- + Prerequisites: Kill Existing Appium Processes
- + Kill any Appium processes on port 4723
- + Troubleshooting
- + Monitor logcat for the crash
- + Capture crash logs
- + Try to launch the app
- + Wait a moment for crash
- + Stop log capture
- + Review the crash log
- + Dangerous System Commands (Never Run)
- + Before Committing
- + Test State Management
- + Best Practices
- + Default: C# Over XAML
- + Use Test Helper Base Classes
- + Avoid Obsolete APIs
- + Use UITest Optimized Controls for Screenshot Tests
- + Check Similar Tests for Patterns
- + Find similar control tests
- + Find Shell tests
- + Find tests for specific control
- + Find tests using UITest optimized controls
Commands
1 shared · 18 only in A · 2 only in B- − dotnet format Microsoft.Maui.sln --no-restore --exclude Templates/src --exclude-diagnostics CA1822
- − git checkout -b feature/issue-12345
- − git add .
- − git commit -m "Fix: Description of the change"
- − git push -u origin feature/issue-12345
- − git push
- − gh pr checkout 12345
- − dotnet cake
- − dotnet tool restore
- − dotnet --version
- − gh pr checks
- − dotnet-public-maven
- − dotnet/android#10738
- − dotnet format analyzers
- − git fetch origin
- − git for-each-ref --sort=-version:refname --count=1 --format='%(refname:lstrip=3)' refs/remotes/origin/net*.0
- − gh pr checkout
- − dotnet-dnceng@dotnet-arcade-skills
- + dotnet build
- + dotnet
- dotnet test
Section tags
7 shared · 3 only in A · 2 only in B- − lint-format
- − agent-behaviour
- − docs
- + build
- + ui
- setup
- test
- code-style
- architecture
- testing-strategy
- git-pr
- do-not
Line diff
dotnet/maui · .github/copilot-instructions.md
@@ −1 @@
1---
2description: "Guidance for GitHub Copilot when working on the .NET MAUI repository."
3---
4
5# GitHub Copilot Development Environment Instructions
6
7This document provides specific guidance for GitHub Copilot when working on the .NET MAUI repository. It serves as context for understanding the project structure, development workflow, and best practices.
8
9## Code Review Instructions
10
11When performing a code review on PRs that change functional code, run the pr-finalize skill to verify that the PR title and description accurately match the actual implementation. This ensures proper documentation and helps maintain high-quality commit messages.
12
13## Repository Overview
14
15**.NET MAUI** is a cross-platform framework for creating mobile and desktop applications with C# and XAML. This repository contains the core framework code that enables development for Android, iOS, iPadOS, macOS, and Windows from a single shared codebase.
16
17### Key Technologies
18
19- **.NET SDK** - Version is **ALWAYS** defined in `global.json` at repository root
20 - **main branch**: Latest stable .NET version
21 - **Feature branches**: Each `netN.0` branch targets the .NET N SDK. By convention, the highest `netN.0` branch is the current development branch for new features and API changes.
22- **Cake build system** for compilation and packaging (`dotnet cake`)
23- **MSBuild** with custom build tasks (must build `Microsoft.Maui.BuildTasks.slnf` first)
24- **Testing frameworks**:
25 - **xUnit** - Unit tests (`*.UnitTests.csproj`)
26 - **NUnit** - UI tests (`TestCases.Shared.Tests`)
27 - **Appium WebDriver** - UI test automation
28
29## Development Environment Setup
30
31This guidance assumes:
32- Repository is already cloned and tools are restored (`dotnet tool restore` completed)
33- Build tasks are compiled (`Microsoft.Maui.BuildTasks.slnf` built successfully)
34- Correct .NET SDK version installed (verify with `dotnet --version` against `global.json`)
35
36### Platform-Specific Requirements
37
38- **Android**: OpenJDK 17 + Android SDK (install via `android` command after `dotnet tool restore`)
39- **iOS/macOS**: Xcode (current stable version)
40- **Windows**: Windows SDK
41
42## Project Structure
43
44### Important Directories
45- `src/Core/` - Core MAUI framework code
46- `src/Controls/` - UI controls and components
47- `src/Essentials/` - Platform APIs and essentials
48- `src/TestUtils/` - Testing utilities and infrastructure
49- `docs/` - Development documentation
50- `eng/` - Build engineering and tooling
51- `.github/` - GitHub workflows and configuration
52
53### Platform-Specific Code Organization
54- **Android** specific code is inside folders labeled `Android`
55- **iOS** specific code is inside folders labeled `iOS`
56- **MacCatalyst** specific code is inside folders named `MacCatalyst`
57- **Windows** specific code is inside folders named `Windows`
58
59### Platform-Specific File Extensions
60
61Platform-specific files use naming conventions to control compilation:
62
63**File extension patterns**:
64- `.windows.cs` - Windows TFM only
65- `.android.cs` - Android TFM only
66- `.ios.cs` - iOS and MacCatalyst TFMs (both)
67- `.maccatalyst.cs` - MacCatalyst TFM only (does NOT compile for iOS)
68
69**Important**: Both `.ios.cs` and `.maccatalyst.cs` files compile for MacCatalyst. There is no precedence mechanism that excludes one when the other exists.
70
71**Example**: If you have both `CollectionView.ios.cs` and `CollectionView.maccatalyst.cs`, both will compile for MacCatalyst builds. The `.maccatalyst.cs` file won't compile for iOS, but the `.ios.cs` file will compile for both iOS and MacCatalyst.
72
73### Sample Projects
74
75- `src/Controls/samples/Maui.Controls.Sample` - Full gallery sample with all controls and features
76- `src/Controls/samples/Maui.Controls.Sample.Sandbox` - Empty project for testing/reproduction
77- `src/Essentials/samples/Essentials.Sample` - Essentials API demonstrations (non-UI MAUI APIs)
78- `src/BlazorWebView/samples/` - BlazorWebView sample applications
79
80## Development Workflow
81
82### Testing
83
84Major test projects:
85- **Core**: `src/Core/tests/UnitTests/Core.UnitTests.csproj`
86- **Essentials**: `src/Essentials/test/UnitTests/Essentials.UnitTests.csproj`
87- **Controls**: `src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj`
88- **XAML**: `src/Controls/tests/Xaml.UnitTests/Controls.Xaml.UnitTests.csproj`
89
90Find all tests: `find . -name "*.UnitTests.csproj"`
91
92### CI Pipelines (Azure DevOps)
93
94When referencing or triggering CI pipelines, use these current pipeline names:
95
96| Pipeline | Name | Purpose |
97|----------|------|---------|
98| Overall CI | `maui-pr` | Full PR validation build |
99| Device Tests | `maui-pr-devicetests` | Helix-based device tests |
100| UI Tests | `maui-pr-uitests` | Appium-based UI tests |
101
102**⚠️ Old pipeline names** (e.g., `MAUI-UITests-public`, `MAUI-public`) are **outdated** and should NOT be used. Always use the names above.
103
104### Investigating CI Failures
105
106**🚨 ALWAYS use the `azdo-build-investigator` skill when investigating CI failures or assessing merge readiness.** Its instructions direct you to invoke the `ci-analysis` skill first for the core investigation workflow, then apply MAUI-specific corrections (correct pipeline names, XHarness quirks, binlog guidance).
107
108Do NOT default to manually querying AzDO APIs or rely solely on `gh pr checks` pass/fail counts.
109
110**When to use it:**
111- "How does CI look?" / "Is CI green?" / "Can we merge?"
112- "What's failing?" / "Are these known failures?"
113- "Is this PR safe to merge?" / "Any CI concerns?"
114- After any PR push to verify the build
115
116**Verifying specific tests:** When asked "did test X pass?" or "did the new test run?", query the **actual AzDO test results** — do NOT infer whether a test ran by inspecting code attributes. Class-level traits, base class categories, and assembly-level attributes can all cause a test to run even when the method itself has no visible category. Check the evidence, not the code.
117
118**Anti-pattern:** Writing ad-hoc scripts to parse AzDO build timelines. The skills handle Helix work item details, known issue cross-referencing, and test result aggregation that manual approaches miss.
119
120### Gradle / Maven Dependency Failures (CFSClean)
121
122The official CI build uses CFSClean network isolation which blocks `repo.maven.apache.org`. All Gradle/Maven dependencies resolve through the `dotnet-public-maven` Azure Artifacts feed.
123
124**If CI fails with Gradle 401 errors** like `"No local versions of package"` or `"Please provide authentication to save package from upstream"`, it means a Maven package hasn't been ingested into the feed yet. **Fix:** run `./eng/ingest-maven-deps.sh` locally to pre-populate the feed. See `src/Core/AndroidNative/settings.gradle` for details.
125
126**Do NOT upgrade Gradle past 8.x** — the Android SDK's `net.android.init.gradle.kts` is incompatible with Gradle 9.x (`dotnet/android#10738`).
127
128### Code Formatting
129
130Always format code before committing:
131
132```bash
133dotnet format Microsoft.Maui.sln --no-restore --exclude Templates/src --exclude-diagnostics CA1822
134```
135
136## Contribution Guidelines
137
138### Handling Existing PRs for Assigned Issues
139
140**🚨 CRITICAL REQUIREMENT: Always develop your own solution first, then compare with existing PRs.**
141
1421. **Develop your own solution first** - Analyze the issue independently and design your approach without looking at existing PRs
1432. **Search for existing PRs** - After developing your solution, search for open PRs addressing the same issue
1443. **Compare and evaluate** - Examine existing PR approaches and decide which solution better addresses the issue
1454. **Document your decision** - In your PR description, compare your solution to existing PRs and explain why you chose your approach, including concerns with alternatives
1465. **Improve either solution** - Whether using your solution or an existing one, enhance with better tests, code quality, error handling, or documentation
147
148### Auto-Generated Files (Never Commit)
149
150These files are auto-generated and must NOT be committed:
151- `cgmanifest.json` - Generated during CI builds
152- `templatestrings.json` - Auto-generated localization
153
154**For AI agents:** Always reset changes to these files before committing.
155
156### PublicAPI.Unshipped.txt File Management
157
158When working with public API changes:
159- **Never disable analyzers** to bypass PublicAPI.Unshipped.txt issues
160- **Always add correct API entries** to PublicAPI.Unshipped.txt files
161- **Use `dotnet format analyzers`** if having trouble
162- **If files are incorrect**: Revert all changes, then add only the necessary new API entries
163
164### Branching
165- `main` - For bug fixes without API changes
166- The highest `netN.0` branch (by convention) - For new features and API changes. To find it, run `git fetch origin` then: `git for-each-ref --sort=-version:refname --count=1 --format='%(refname:lstrip=3)' refs/remotes/origin/net*.0`
167
168### Git Workflow (Copilot CLI Rules)
169
170**🚨 CRITICAL Git Rules for Copilot CLI:**
171
1721. **NEVER commit directly to `main`** - Always create a feature branch for your work. Direct commits to `main` are strictly prohibited.
173
1742. **When amending an existing PR, work on the PR's branch directly** - Do NOT create a separate branch off a PR branch. The PR branch already IS a feature branch. Creating a new branch off it means CI won't run on the original PR, defeating the purpose. Use `gh pr checkout` to switch to the PR branch, make your changes, commit, **then** ask before pushing so the user can review locally first.
175
1763. **Do NOT rebase, squash, or force-push** unless explicitly requested by the user. These operations rewrite git history and can cause problems for other contributors. Default behavior should be regular commits and pushes.
177
178**Safe Git Workflow:**
179```bash
180# Create a feature branch (NEVER work directly on main)
181git checkout -b feature/issue-12345
182
183# Make commits normally
184git add .
185git commit -m "Fix: Description of the change"
186
187# Push to remote (for new branches)
188git push -u origin feature/issue-12345
189
190# For subsequent pushes on the same branch
191git push
192```
193
194**When asked to update an existing PR:**
195```bash
196# Check out the PR branch directly (do NOT create a new branch off it)
197gh pr checkout 12345
198
199# Make fixes and commit to the PR branch
200git add .
201git commit -m "Fix: Description of the change"
202```
2031. **STOP and ask the user** before pushing: "Changes are committed locally. Would you like me to push these changes to the PR?"
2042. Exception: If the user's instructions explicitly include pushing, proceed without asking.
205
206### Documentation
207- Update XML documentation for public APIs
208- Follow existing code documentation patterns
209- Update relevant docs in `docs/` folder when needed
210
211### Opening PRs
212
213All PRs are required to have this at the top of the description:
214
215```
216<!-- Please let the below note in for people that find this PR -->
217> [!NOTE]
218> Are you waiting for the changes in this PR to be merged?
219> It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you!
220```
221
222Always put that at the top, without the block quotes. Without it, users will NOT be able to try the PR and your work will have been in vain!
223
224
225
226## Custom Agents and Skills
227
228The repository includes specialized custom agents and reusable skills for specific tasks.
229
230### Skills vs Agents
231
232| Aspect | Skills | Agents |
233|--------|--------|--------|
234| **Invoke** | `/skill-name` or direct request | Delegate to agent |
235| **Output** | Analysis, recommendations | Actions, changes applied |
236| **Interaction** | Interactive discussion | Autonomous workflow |
237| **Example** | `/learn-from-pr` → recommendations | learn-from-pr agent → applies changes |
238
239### Available Custom Agents
240
2411. **pr** - Sequential 4-phase workflow for reviewing and working on PRs
242 - **Use when**: A PR already exists and needs review or work, OR an issue needs a fix
243 - **Capabilities**: PR review, test verification, fix exploration, alternative comparison
244 - **Trigger phrases**: "review PR #XXXXX", "work on PR #XXXXX", "fix issue #XXXXX", "continue PR #XXXXX"
245 - **Do NOT use for**: Just running tests manually → Use `sandbox-agent`
246
2472. **write-tests-agent** - Agent for writing tests. Determines test type (UI vs XAML) and invokes the appropriate skill (`write-ui-tests`, `write-xaml-tests`)
248 - **Use when**: Creating new tests for issues or PRs
249 - **Capabilities**: Test type determination (UI and XAML), skill invocation, test verification
250 - **Trigger phrases**: "write tests for #XXXXX", "create tests", "add test coverage"
251
2523. **sandbox-agent** - Specialized agent for working with the Sandbox app for testing, validation, and experimentation
253 - **Use when**: User wants to manually test PR functionality or reproduce issues
254 - **Capabilities**: Sandbox app setup, Appium-based manual testing, PR functional validation
255 - **Trigger phrases**: "test this PR", "validate PR #XXXXX in Sandbox", "reproduce issue #XXXXX", "try out in Sandbox"
256 - **Do NOT use for**: Code review (use pr agent), writing automated tests (use write-tests-agent)
257
2584. **learn-from-pr** - Extracts lessons from PRs and applies improvements to the repository
259 - **Use when**: After complex PR, want to improve instruction files/skills based on lessons learned
260 - **Capabilities**: Analyzes PR, identifies failure modes, applies improvements to instruction files, skills, code comments
261 - **Trigger phrases**: "learn from PR #XXXXX and apply improvements", "improve repo based on what we learned", "update skills based on PR"
262 - **Output**: Applied changes to instruction files, skills, architecture docs, code comments
263 - **Do NOT use for**: Analysis only without applying changes → Use `/learn-from-pr` skill instead
264
2655. **release-readiness-agent** - Assesses ship-readiness for a .NET MAUI release branch — both **SR** (`release/*-srN`) and **Preview** (`release/*-previewN`)
266 - **Use when**: A release (SR or Preview) is approaching ship date and you need a synthesized verdict with WorkIQ/MCP enrichment on top of the deterministic report — **or** for a portfolio question across all active releases ("status on releases", "what needs attention across releases") where the user may not know which releases exist
267 - **Capabilities**: Resolves the branch (SR or Preview) from natural language, picks the right script (`Get-ReleaseReadiness.ps1` for SR, `Get-PreviewReadiness.ps1` for Preview), enriches `rejected-from-sr` candidates with WorkIQ context (SR lane), patches `UNKNOWN` ship-check rows via MCP (`maestro_default_channels`, `maestro_builds`), presents an overall verdict
268 - **Trigger phrases**: "is SR7 ready to ship", "release readiness for release/10.0.1xx-sr7", "survey the SR8 branch", "how does net11 preview6 look", "is preview6 ready to cut", "release readiness for release/11.0.1xx-preview6" — **plus portfolio / cross-release questions with no specific release named**: "give me a status on releases", "release status overview", "what's the status across all releases", "what needs attention across releases", "what's next for MAUI releases"
269 - **Output**: Verdict (Ready / Conditionally Ready / Not Ready) + per-candidate classification (SR) or per-section table (Preview) + actionable next steps
270 - **Do NOT use for**: Programmatic / scripted consumers that just need the raw JSON — use the `release-readiness` skill directly. Reviewing a single PR (use **pr**). Running tests manually (use **sandbox-agent**).
271
272### Reusable Skills
273
274Skills are modular capabilities that can be invoked directly or used by agents. Located in `.github/skills/`:
275
276#### User-Facing Skills
277
2781. **pr-review** (`.github/skills/pr-review/SKILL.md`)
279 - **Purpose**: End-to-end PR review orchestrator — 3 phases: pr-preflight, try-fix, pr-report. Gate runs separately before this skill via Review-PR.ps1.
280 - **Trigger phrases**: "review PR #XXXXX", "work on PR #XXXXX", "fix issue #XXXXX", "continue PR #XXXXX"
281 - **Capabilities**: Multi-model fix exploration, alternative comparison, PR review recommendation
282 - **Do NOT use for**: Just running tests manually → Use `sandbox-agent`
283 - **Phase instructions** (in `.github/pr-review/`):
284 - `pr-preflight.md` — Context gathering from issue/PR
285 - `pr-report.md` — Final recommendation
286 - **Phase skill**: `try-fix` — Multi-model fix exploration
287 - **Note**: Gate (test verification) runs as a script step in `Review-PR.ps1` before this skill is invoked. Gate result is passed in the prompt.
288
2892. **issue-triage** (`.github/skills/issue-triage/SKILL.md`)
290 - **Purpose**: Query and triage open issues that need milestones, labels, or investigation
291 - **Trigger phrases**: "find issues to triage", "show me old Android issues", "what issues need attention"
292 - **Scripts**: `init-triage-session.ps1`, `query-issues.ps1`, `record-triage.ps1`
293
2942. **find-reviewable-pr** (`.github/skills/find-reviewable-pr/SKILL.md`)
295 - **Purpose**: Finds open PRs in dotnet/maui and dotnet/docs-maui that need review
296 - **Trigger phrases**: "find PRs to review", "show milestoned PRs", "find partner PRs"
297 - **Scripts**: `query-reviewable-prs.ps1`
298 - **Categories**: P/0, milestoned, partner, community, recent, docs-maui
299
3003. **pr-finalize** (`.github/skills/pr-finalize/SKILL.md`)
301 - **Purpose**: Verifies PR title and description match actual implementation, AND performs code review for best practices before merge.
302 - **Trigger phrases**: "finalize PR #XXXXX", "check PR description for #XXXXX", "review commit message"
303 - **Used by**: Before merging any PR, when description may be stale
304 - **Note**: Does NOT require agent involvement or session markdown - works on any PR
305 - **🚨 CRITICAL**: NEVER use `--approve` or `--request-changes` - only post comments. Approval is a human decision.
306
3074. **code-review** (`.github/skills/code-review/SKILL.md`)
308 - **Purpose**: Reviews PR code changes for correctness, safety, and consistency with MAUI conventions. Walks through a MAUI-specific checklist covering handler lifecycle, platform code, safe area, threading, public API, and test patterns.
309 - **Trigger phrases**: "review code for PR #XXXXX", "code review PR #XXXXX", "review this PR's code"
310 - **Note**: Standalone skill — uses independence-first assessment (reads code before PR description to avoid anchoring bias). Can be used by any agent or invoked directly.
311 - **🚨 CRITICAL**: NEVER use `--approve` or `--request-changes` — only post comments. Approval is a human decision.
312
3135. **learn-from-pr** (`.github/skills/learn-from-pr/SKILL.md`)
314 - **Purpose**: Analyzes completed PR to identify repository improvements (analysis only, no changes applied)
315 - **Trigger phrases**: "what can we learn from PR #XXXXX?", "how can we improve agents based on PR #XXXXX?"
316 - **Used by**: After complex PRs, when agent struggled to find solution
317 - **Output**: Prioritized recommendations for instruction files, skills, code comments
318 - **Note**: For applying changes automatically, use the learn-from-pr agent instead
319
3206. **write-ui-tests** (`.github/skills/write-ui-tests/SKILL.md`)
321 - **Purpose**: Creates UI tests for GitHub issues and verifies they reproduce the bug
322 - **Trigger phrases**: "write UI tests for #XXXXX", "create UI test for issue", "add UI test coverage"
323 - **Output**: Test files that fail without fix, pass with fix
324
3257. **write-xaml-tests** (`.github/skills/write-xaml-tests/SKILL.md`)
326 - **Purpose**: Creates XAML unit tests for XAML parsing, compilation, and source generation
327 - **Trigger phrases**: "write XAML tests for #XXXXX", "test XamlC behavior", "reproduce XAML parsing bug"
328 - **Output**: Test files for Controls.Xaml.UnitTests
329
3309. **verify-tests-fail-without-fix** (`.github/skills/verify-tests-fail-without-fix/SKILL.md`)
331 - **Purpose**: Verifies tests catch the bug before fix and pass with fix. Auto-detects test type (UI, device, unit, XAML) and dispatches to the appropriate runner.
332 - **Two modes**: Verify failure only (test creation) or full verification (test + fix)
333 - **Used by**: After creating tests, before considering PR complete
334
33510. **run-integration-tests** (`.github/skills/run-integration-tests/SKILL.md`)
336 - **Purpose**: Build, pack, and run .NET MAUI integration tests locally
337 - **Trigger phrases**: "run integration tests", "test templates locally", "run macOSTemplates tests", "run RunOniOS tests"
338 - **Categories**: Build, WindowsTemplates, macOSTemplates, Blazor, MultiProject, Samples, AOT, RunOnAndroid, RunOniOS
339 - **Note**: **ALWAYS use this skill** instead of manual `dotnet test` commands for integration tests
340
34111. **dependency-flow** (`.github/skills/dependency-flow/SKILL.md`)
342 - **Purpose**: MAUI-specific dependency flow rules, channel conventions, and feed lookup workflows
343 - **Trigger phrases**: "feeds for .NET MAUI X.Y.Z", "where is MAUI build", "promote build to public feed", "what channels is MAUI on", "subscription health for MAUI"
344 - **Wraps**: `maestro-cli` skill (from `dotnet-dnceng@dotnet-arcade-skills` plugin) and maestro MCP tools
345 - **Note**: Provides MAUI-specific guardrails on top of core Maestro/darc operations — channel naming, safety deny-list, input validation, and prompt injection defense
346
34712. **release-readiness** (`.github/skills/release-readiness/SKILL.md`)
348 - **Purpose**: Deterministic ship-readiness engine for .NET MAUI release branches — both **SR** (`release/*-srN`) and **Preview** (`release/*-previewN`). Surveys CI, computes what's actually shipping, classifies open regressions, identifies port candidates and rejected backports
349 - **Trigger phrases**: "release readiness for SRN", "is SR7 ready to ship", "survey the SR branch", "release readiness for preview6", "how does preview6 look (deterministic)", "status across all releases" (reads the live `[Release Readiness]` tracker issues by body marker — no survey re-run needed)
350 - **Scripts**: `Get-ReleaseReadiness.ps1` (SR lane), `Get-PreviewReadiness.ps1` (Preview lane), `Find-ReleaseReadinessTrackers.ps1` (tracker discovery)
351 - **Output**: JSON + Markdown report, list of source PRs, classification of regression issues (in-sr-active, rejected-from-sr, no-fix-yet, etc.)
352 - **Note**: Deterministic and reproducible — no MCP, no LLM judgment. Use **this skill directly** when you need raw output for a script, dashboard, cron job, or programmatic consumer. For natural-language verdict synthesis with WorkIQ enrichment, use the **`release-readiness-agent`** instead.
353
354#### Internal Skills (Used by Agents)
355
35613. **try-fix** (`.github/skills/try-fix/SKILL.md`)
357 - **Purpose**: Proposes ONE independent fix approach, applies it, tests, records result with failure analysis, then reverts
358 - **Used by**: pr agent Phase 3 (Fix phase) - rarely invoked directly by users
359 - **Behavior**: Reads prior attempts to learn from failures. Max 5 attempts per session.
360 - **Output**: Updates session markdown with attempt results and failure analysis
361
362### Using Custom Agents
363
364**Delegation Policy**: When user request matches agent trigger phrases, **ALWAYS delegate to the appropriate agent immediately**. Do not ask for permission or explain alternatives unless the request is ambiguous.
365
366**Examples of correct delegation**:
367- User: "Review PR #12345" → Immediately invoke **pr** agent
368- User: "Test this PR" → Immediately invoke **sandbox-agent**
369- User: "Fix issue #67890" (no PR exists) → Suggest using `/delegate` command
370- User: "Write tests for issue #12345" → Immediately invoke **write-tests-agent**
371- User: "Is SR7 ready to ship?" → Immediately invoke **release-readiness-agent**
372- User: "How does net11 preview6 look?" → Immediately invoke **release-readiness-agent**
373- User: "Give me a status on releases / what needs attention across releases?" → Immediately invoke **release-readiness-agent** (portfolio mode — it enumerates active releases by reading the `[Release Readiness]` tracker issues; don't ask "which release?")
374- User: "Give me the raw release-readiness JSON for SR8" → Use the **release-readiness** skill directly (no enrichment needed)
375
376**When NOT to delegate**:
377- User asks "What does PR #12345 do?" → Informational query, handle yourself
378- User asks "How do I test PRs?" → Documentation query, handle yourself
379- User has follow-up questions after agent completes → Continue the conversation yourself
dotnet/maui · .github/instructions/uitests.instructions.md
@@ +1 @@
1---
2applyTo: "src/Controls/tests/TestCases.Shared.Tests/**,src/Controls/tests/TestCases.HostApp/**"
3---
4
5# UI Testing Guidelines for .NET MAUI
6
7## Overview
8
9This document provides specific guidance for GitHub Copilot when writing UI tests for the .NET MAUI repository.
10
11
12
13**Critical Principle**: UI tests should run on all applicable platforms (iOS, Android, Windows, MacCatalyst) by default unless there is a specific technical limitation.
14
15## UI Test Structure
16
17### Two-Project Requirement
18
19**CRITICAL: Every UI test requires code in TWO separate projects:**
20
211. **HostApp UI Test Page** (`src/Controls/tests/TestCases.HostApp/Issues/`)
22 - Create the actual UI page that demonstrates the feature or reproduces the issue
23 - **Prefer C# only** (`.cs` file) unless testing XAML-specific features (bindings, templates, styles)
24 - Add `AutomationId` attributes on interactive controls for test automation
25 - Follow naming convention: `IssueXXXXX.cs` (C# only) or `IssueXXXXX.xaml` + `IssueXXXXX.xaml.cs` (when XAML required)
26 - XXXXX should correspond to a GitHub issue number when applicable
27 - Ensure the UI provides clear visual feedback for the behavior being tested
28 - Class must include `[Issue()]` attribute with tracker, number, description, and platform
29
302. **NUnit Test Implementation** (`src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/`)
31 - Create corresponding Appium-based NUnit tests that inherit from `_IssuesUITest`
32 - Use the `AutomationId` values to locate and interact with UI elements
33 - Follow naming convention: `IssueXXXXX.cs` (matches the HostApp page file)
34 - Include appropriate `[Category(UITestCategories.XYZ)]` attributes (only ONE per test)
35 - Test should validate expected behavior through UI interactions and assertions
36
37### Base Class and Infrastructure
38
39- Each test class must inherit from `_IssuesUITest`
40- The `_IssuesUITest` base class provides:
41 - The `App` property for interacting with UI elements
42 - Test initialization and setup
43 - Helper methods for common UI test operations
44- The test infrastructure automatically handles platform detection and page navigation
45
46### Naming Conventions
47
48**Test Files:**
49- Pattern: `IssueXXXXX.cs` where XXXXX corresponds to a GitHub issue number
50- Must match the corresponding HostApp page file name in TestCases.HostApp (either `.cs` only or `.xaml`)
51
52**Test Methods:**
53- Use descriptive names that clearly explain what behavior is being verified
54- ✅ Good: `VerifySafeAreaBottomPaddingWithKeyboard()`, `ButtonClickUpdatesLabel()`
55- ❌ Bad: `Test1()`, `TestMethod()`, `RunTest()`
56
57**AutomationId Values:**
58- Always use unique, descriptive `AutomationId` values
59- Reference the same `AutomationId` in both C# code (or XAML if used) and test code
60- Use PascalCase for AutomationId values
61
62## Complete Test Example
63
64### Example 1: C# Only (Preferred for Most Tests)
65
66**HostApp Page** (`TestCases.HostApp/Issues/Issue12345.cs`):
67```csharp
68namespace Maui.Controls.Sample.Issues;
69
70[Issue(IssueTracker.Github, 12345, "Button click updates label text", PlatformAffected.All)]
71public class Issue12345 : ContentPage
72{
73 public Issue12345()
74 {
75 var resultLabel = new Label
76 {
77 Text = "Initial Text",
78 AutomationId = "ResultLabel"
79 };
80
81 Content = new VerticalStackLayout
82 {
83 Children =
84 {
85 new Button
86 {
87 Text = "Click Me",
88 AutomationId = "TestButton",
89 Command = new Command(() => resultLabel.Text = "Expected Text")
90 },
91 resultLabel
92 }
93 };
94 }
95}
96```
97
98### Example 2: XAML (When Testing XAML-Specific Features)
99
100**HostApp XAML** (`TestCases.HostApp/Issues/Issue12346.xaml`):
101```xaml
102<?xml version="1.0" encoding="utf-8" ?>
103<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
104 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
105 x:Class="Maui.Controls.Sample.Issues.Issue12346">
106 <VerticalStackLayout>
107 <Button Text="Click Me"
108 AutomationId="TestButton"
109 Clicked="OnButtonClicked" />
110 <Label Text="Initial Text"
111 x:Name="ResultLabel"
112 AutomationId="ResultLabel" />
113 </VerticalStackLayout>
114</ContentPage>
115```
116
117**HostApp Code-Behind** (`TestCases.HostApp/Issues/Issue12346.xaml.cs`):
118```csharp
119namespace Maui.Controls.Sample.Issues;
120
121[Issue(IssueTracker.Github, 12346, "Testing XAML binding behavior", PlatformAffected.All)]
122public partial class Issue12346 : ContentPage
123{
124 public Issue12346()
125 {
126 InitializeComponent();
127 }
128
129 void OnButtonClicked(object sender, EventArgs e)
130 {
131 ResultLabel.Text = "Expected Text";
132 }
133}
134```
135
136### NUnit Test (Same for Both Examples)
137
138**NUnit Test** (`TestCases.Shared.Tests/Tests/Issues/Issue12345.cs` or `Issue12346.cs`):
139```csharp
140public class Issue12345 : _IssuesUITest
141{
142 public override string Issue => "Description of the issue being tested";
143
144 public Issue12345(TestDevice device) : base(device) { }
145
146 [Test]
147 [Category(UITestCategories.Layout)] // Pick the most appropriate category
148 public void ButtonClickUpdatesLabel()
149 {
150 // Wait for element to be ready
151 App.WaitForElement("TestButton");
152
153 // Interact with the UI
154 App.Tap("TestButton");
155
156 // Verify expected behavior
157 var labelText = App.FindElement("ResultLabel").GetText();
158 Assert.That(labelText, Is.EqualTo("Expected Text"));
159
160 // Optional: Visual verification
161 VerifyScreenshot();
162 }
163}
164```
165
166## Common Patterns
167
168### Waiting for Elements
169```csharp
170App.WaitForElement("AutomationId");
171```
172
173### Interacting with Elements
174```csharp
175App.Tap("AutomationId");
176App.FindElement("AutomationId").GetText();
177var rect = App.WaitForElement("AutomationId").GetRect();
178```
179
180### Assertions
181```csharp
182Assert.That(actualValue, Is.EqualTo(expectedValue).Within(tolerance));
183Assert.That(rect.Height, Is.LessThanOrEqualTo(maxHeight));
184```
185
186### Screenshot Verification
187```csharp
188// Verify visual appearance (automated comparison)
189VerifyScreenshot();
190
191// With custom name
192VerifyScreenshot("CustomTestName");
193
194// With tolerance (0.0-100.0 percentage) - use sparingly
195VerifyScreenshot(tolerance: 0.5); // Allow 0.5% difference for cross-machine rendering variance
196
197// PREFERRED: Keep retrying for up to 2 seconds (for animations)
198VerifyScreenshot(retryTimeout: TimeSpan.FromSeconds(2));
199
200// Combined: tolerance for rendering variance + retryTimeout for timing
201VerifyScreenshot(tolerance: 0.5, retryTimeout: TimeSpan.FromSeconds(2));
202
203// Manual screenshot for debugging
204App.Screenshot("TestStep1");
205```
206
207**CRITICAL - VerifyScreenshot() Built-in Features:**
208
209`VerifyScreenshot()` **already includes** stability mechanisms. Do NOT add redundant delays:
210
211| Feature | Behavior | Parameter |
212|---------|----------|-----------|
213| **Android delay** | Automatic 350ms wait for animations | Built-in, cannot override |
214| **Retry logic** | Default: retries once; with retryTimeout: keeps retrying | Built-in |
215| **Retry delay** | 500ms delay between retry attempts | `retryDelay: TimeSpan` (customizable) |
216| **Retry timeout** | Total time to keep retrying | `retryTimeout: TimeSpan` (PREFERRED for flaky tests) |
217| **Tolerance** | Allow percentage difference (0-100) | `tolerance: double` (default: 0.0) |
218
219**When to customize:**
220- ✅ Use `retryTimeout` parameter for animations with variable timing (PREFERRED approach)
221- ✅ Use small `tolerance` (0.5%) for cross-machine rendering variance, NOT to hide timing issues
222- ✅ Use `retryDelay` if you need to change the delay between retry attempts
223- ❌ **DO NOT** add `Task.Delay()` or `Thread.Sleep()` before `VerifyScreenshot()` - use `retryTimeout` instead
224
225## Writing Robust UI Tests
226
227### Best Practices for Screenshot Tests
228
229When writing tests that use `VerifyScreenshot()`, follow these patterns to avoid flakiness:
230
231```
232┌─────────────────────────────────────────────────────────────────┐
233│ 1. UNDERSTAND TEST INFRASTRUCTURE │
234│ - Read UITest.cs base class implementation │
235│ - Understand built-in retry/delay/tolerance mechanisms │
236│ - Check what helpers/extensions already exist │
237├─────────────────────────────────────────────────────────────────┤
238│ 2. USE PROPER WAITING PATTERNS │
239│ - Use WaitForElement before interacting with elements │
240│ - Use retryTimeout for screenshots after animations │
241│ - Never use arbitrary Task.Delay() before VerifyScreenshot │
242├─────────────────────────────────────────────────────────────────┤
243│ 3. APPLY MINIMAL TOLERANCES │
244│ - Use retryTimeout for timing issues (preferred) │
245│ - Use small tolerance (0.5%) only for rendering variance │
246│ - Never use tolerance > 5% without justification │
247└─────────────────────────────────────────────────────────────────┘
248```
249
250### Common Flaky Test Patterns
251
252| Symptom | Root Cause | Fix Pattern | Anti-Pattern |
253|---------|------------|-------------|--------------|
254| **Visual diff in screenshot** | Animation not finished | `VerifyScreenshot(retryTimeout: TimeSpan.FromSeconds(2))` | ❌ Adding `Task.Delay()` before |
255| **Element not found** | Element not rendered yet | `App.WaitForElement("Id", timeout: TimeSpan.FromSeconds(10))` | ❌ `Thread.Sleep()` then `FindElement()` |
256| **Timeout on interaction** | Page not fully loaded | Wait for specific element that indicates ready state | ❌ Arbitrary 3-second delay |
257| **Inconsistent rect/position** | Layout not settled | Multiple `GetRect()` calls with comparison | ❌ Single `GetRect()` after delay |
258| **WebView failures** | External URL/network | Use mock URLs instead of external URLs | ❌ Adding longer timeouts |
259
260### Anti-Patterns (DO NOT DO)
261
262| Anti-Pattern | Why It's Wrong | Better Alternative |
263|--------------|----------------|-------------------|
264| ❌ `Task.Delay(500).Wait()` before `VerifyScreenshot()` | VerifyScreenshot already has built-in retry; use retryTimeout instead | Use `VerifyScreenshot(retryTimeout: TimeSpan.FromSeconds(2))` |
265| ❌ `Thread.Sleep(2000)` before element interaction | Arbitrary wait; doesn't guarantee element is ready | `App.WaitForElement("Id", timeout: ...)` |
266| ❌ Adding tolerance > 5% without justification | Hides real bugs; too permissive | Use `retryTimeout` for timing issues; small tolerance (0.5%) for rendering variance |
267| ❌ Using external URLs in WebView tests | External dependency; unreliable | Use mock URLs or local content |
268| ❌ Fixing symptoms without understanding infrastructure | Redundant fixes; doesn't address root cause | Read `UITest.cs` first (step 1 above) |
269
270### When to Use What
271
272**VerifyScreenshot() parameters (preferred):**
273```csharp
274// Animation timing issues - keep retrying for up to 2 seconds
275// This is the PREFERRED approach for flaky screenshot tests
276VerifyScreenshot(retryTimeout: TimeSpan.FromSeconds(2));
277
278// Small tolerance for cross-machine rendering variance + retryTimeout for timing
279// Use 0.5% tolerance as safety margin, NOT to hide timing issues
280VerifyScreenshot(tolerance: 0.5, retryTimeout: TimeSpan.FromSeconds(2));
281
282// Legacy: retryDelay only changes delay BETWEEN retries (default 500ms)
283// retryTimeout is preferred because it keeps trying until success
284VerifyScreenshot(retryDelay: TimeSpan.FromSeconds(1));
285```
286
287**Key difference: retryDelay vs retryTimeout:**
288- `retryDelay`: Delay between retry attempts (default 500ms). Only retries ONCE.
289- `retryTimeout`: Total time to keep retrying. Retries every `retryDelay` until timeout.
290- **Prefer `retryTimeout`** for animations with variable completion times.
291
292**WaitForElement (for element readiness):**
293```csharp
294// Wait up to 10 seconds for element to appear
295App.WaitForElement("ButtonId", timeout: TimeSpan.FromSeconds(10));
296
297// Then interact
298App.Tap("ButtonId");
299```
300
301**Task.Delay/Thread.Sleep (avoid if possible):**
302```csharp
303// AVOID: With retryTimeout, you rarely need explicit delays anymore
304//
305// Old pattern (before retryTimeout):
306// Task.Delay(300).Wait();
307// VerifyScreenshot(tolerance: 2.0);
308//
309// New pattern (preferred):
310VerifyScreenshot(tolerance: 0.5, retryTimeout: TimeSpan.FromSeconds(2));
311
312// ONLY use explicit delays when:
313// 1. Waiting for non-element state with no screenshot (rare)
314// 2. External system delay that can't be detected otherwise
315// 3. After exhausting other options AND documenting why
316```
317
318### Understanding Test Infrastructure
319
320**Key files to understand when writing UI tests:**
321
3221. **UITest.cs** - Base class with `VerifyScreenshot()` implementation
323 - Path: `src/Controls/tests/TestCases.Shared.Tests/UITest.cs`
324 - Contains: retry logic, tolerance parsing, platform-specific delays
325
3262. **_IssuesUITest.cs** - Issues test base class
327 - Path: `src/Controls/tests/TestCases.Shared.Tests/_IssuesUITest.cs`
328 - Contains: Navigation helpers, common patterns
329
3303. **Extension methods** - Platform-specific helpers
331 - Path: `src/Controls/tests/TestCases.Shared.Tests/` (various extension files)
332 - Contains: Existing helpers for common operations
333
334**Find existing patterns:**
335```bash
336# See VerifyScreenshot implementation (including retryTimeout)
337grep -A 30 "public void VerifyScreenshot" src/Controls/tests/TestCases.Shared.Tests/UITest.cs
338
339# Find existing tests using retryTimeout (preferred pattern)
340grep -r "retryTimeout" src/Controls/tests/TestCases.Shared.Tests/Tests/
341
342# Find existing tolerance patterns
343grep -r "tolerance:" src/Controls/tests/TestCases.Shared.Tests/Tests/
344```
345
346### Infrastructure Notes
347
348**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.
349
350## Test Categories
351
352### Category Guidelines
353- Use appropriate categories from `UITestCategories`
354- **Only ONE** `[Category]` attribute per test
355- Pick the most specific category that applies
356
357### Test Categories
358
359**CRITICAL**: Always check [UITestCategories.cs](../../src/Controls/tests/TestCases.Shared.Tests/UITestCategories.cs) for the authoritative, complete list of categories.
360
361**Selection rule**: Choose the MOST SPECIFIC category that applies to your test. If multiple categories seem applicable, choose the one that best describes the primary focus of the test.
362
363**Common categories** (examples only - not exhaustive):
364- **SafeArea**: `SafeAreaEdges` - Safe area and padding tests
365- **Basic controls**: `Button`, `Label`, `Entry`, `Editor` - Specific control tests
366- **Collection controls**: `CollectionView`, `ListView`, `CarouselView` - Collection control tests
367- **Layout**: `Layout` - Layout-related tests
368- **Navigation**: `Shell`, `Navigation`, `TabbedPage` - Navigation tests
369- **Interaction**: `Gestures`, `Focus`, `Accessibility` - Interaction tests
370- **Lifecycle**: `Window`, `Page`, `LifeCycle` - Page lifecycle tests
371
372**List all categories programmatically**:
373```bash
374grep -E "public const string [A-Za-z]+ = " src/Controls/tests/TestCases.Shared.Tests/UITestCategories.cs
375```
376
377**Important**: When a new UI test category is added to `UITestCategories.cs`, also update `eng/pipelines/common/ui-tests.yml` to include the new category.
378
379## Platform Coverage
380
381### Default Behavior
382
383Tests should run on all applicable platforms by default. The test infrastructure handles platform detection automatically.
384
385### No Inline #if Directives in Test Methods
386
387**Do NOT use `#if ANDROID`, `#if IOS`, etc. directly in test methods.** Platform-specific behavior must be hidden behind extension methods for readability.
388
389**Note:** This rule is about **code cleanliness**, not platform scope. Using `#if ANDROID ... #else ...` still compiles for all platforms - the issue is that inline directives make test logic hard to read and maintain.
390
391```csharp
392// ❌ BAD - inline #if in test method (hard to read)
393[Test]
394public void MyTest()
395{
396#if ANDROID
397 App.TapCoordinates(100, 200);
398#else
399 App.Tap("MyElement");
400#endif
401}
402
403// ✅ GOOD - platform logic in extension method (clean test)
404[Test]
405public void MyTest()
406{
407 App.TapElementCrossPlatform("MyElement");
408}
409```
410
411Move platform-specific logic to extension methods to keep test code clean and readable.
412
413## Running UI Tests Locally
414
415**CRITICAL: ALWAYS use the BuildAndRunHostApp.ps1 script to run UI tests. NEVER run `dotnet test` or `dotnet build` commands manually.**
416
417### BuildAndRunHostApp.ps1 Script (ONLY Way to Run Tests)
418
419**Script location**: `.github/scripts/BuildAndRunHostApp.ps1`
420
421**Usage:**
422```powershell
423# Run specific test on Android
424pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform android -TestFilter "FullyQualifiedName~Issue12345"
425
426# Run specific test on iOS
427pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform ios -TestFilter "FullyQualifiedName~Issue12345"
428
429# Run specific test on MacCatalyst
430pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform maccatalyst -TestFilter "FullyQualifiedName~Issue12345"
431
432# Run tests by category
433pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform android -Category "SafeAreaEdges"
434
435# Run specific test with custom device (iOS only)
436pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform ios -TestFilter "Issue12345" -DeviceUdid "12345678-1234567890ABCDEF"
437```
438
439**What the script handles automatically:**
440- ✅ Automatic device detection and boot (iPhone Xs for iOS, first available for Android)
441- ✅ Building TestCases.HostApp (always fresh build)
442- ✅ App installation and deployment
443- ✅ Running your NUnit test via `dotnet test`
444- ✅ Complete log capture to `CustomAgentLogsTmp/UITests/` directory:
445 - `android-device.log` or `ios-device.log` - Device logs filtered to HostApp
446 - `test-output.log` - Test execution output
447
448**Why you must use the script:**
449- The script ensures correct device targeting and environment variables
450- It handles platform-specific quirks and setup requirements
451- It provides consistent test execution across all platforms
452- It captures logs automatically for debugging
453- Manual `dotnet` commands often fail due to missing environment setup
454
455### Prerequisites: Kill Existing Appium Processes
456
457**CRITICAL**: Before running UITests with BuildAndRunHostApp.ps1, always kill any existing Appium processes. The UITest framework needs to start its own Appium server, and having a stale process running will cause the tests to fail with an error like:
458
459```
460AppiumServerHasNotBeenStartedLocallyException: The local appium server has not been started.
461Time 120000 ms for the service starting has been expired!
462```
463
464**Solution: Always kill existing Appium processes before running tests:**
465
466```bash
467# Kill any Appium processes on port 4723
468lsof -i :4723 | grep LISTEN | awk '{print $2}' | xargs kill -9 2>/dev/null && echo "✅ Killed existing Appium processes" || echo "ℹ️ No Appium processes running on port 4723"
469```
470
471**Why this is needed:** The UITest framework automatically starts and manages its own Appium server. If there's already an Appium process running (from a previous test run or manual testing), the framework will timeout trying to start a new one.
472
473### Troubleshooting
474
475**Android App Crashes on Launch:**
476
477If you encounter navigation fragment errors or resource ID issues:
478```
479java.lang.IllegalArgumentException: No view found for id 0x7f0800f8 (com.microsoft.maui.uitests:id/inward) for fragment NavigationRootManager_ElementBasedFragment
480```
481
482**Solution:** Read the crash logs to find the actual exception:
483```bash
484# Monitor logcat for the crash
485adb logcat | grep -E "(FATAL|AndroidRuntime|Exception|Error|Crash)"
486```
487
488**Debugging steps:**
4891. **Find the exception** in logcat - look for the stack trace
4902. **Investigate the root cause** - What line of code is throwing? Why?
4913. **Check for null references** - Are required resources missing?
4924. **Verify resource IDs exist** - Check if the ID referenced actually exists in the app
4935. If you can't determine the fix, **ask for guidance** with the full exception details
494
495**iOS App Crashes on Launch or Won't Start with Appium:**
496
497If the iOS app crashes when launched by Appium or manually with `xcrun simctl launch`:
498
499**Solution:** Read the crash logs to find the actual exception:
500```bash
501# Capture crash logs
502xcrun simctl spawn booted log stream --predicate 'processImagePath contains "TestCases.HostApp"' --level=debug > /tmp/ios_crash.log 2>&1 &
503LOG_PID=$!
504
505# Try to launch the app
506xcrun simctl launch $UDID com.microsoft.maui.uitests
507
508# Wait a moment for crash
509sleep 3
510
511# Stop log capture
512kill $LOG_PID
513
514# Review the crash log
515cat /tmp/ios_crash.log | grep -A 20 -B 5 "Exception"
516```
517
518**Debugging steps:**
5191. **Find the exception** in the crash log - look for stack traces
5202. **Investigate the root cause** - What's causing the crash?
5213. **Check for missing resources** - Are all required files included in the bundle?
5224. **Verify Info.plist** - Are required keys present?
5235. **Check for platform-specific issues** - iOS version compatibility, permissions, etc.
5246. If you can't determine the fix, **ask for guidance** with the full exception details
525
526### Dangerous System Commands (Never Run)
527
528**🚨 NEVER run these commands — they cause destructive system-wide side effects:**
529
530- **`tccutil reset`** — Wipes ALL macOS permissions (Accessibility, Camera, etc.) system-wide. This breaks Appium/WebDriverAgent, Xcode, and other tools. Once reset, permissions must be manually re-granted through System Settings.
531- **`csrutil disable`** — Disables System Integrity Protection
532- **`networksetup`** — Modifies network configuration
533- **`defaults delete`** on system domains — Resets system preferences
534
535**General rule:** Do not run commands that modify macOS system-level privacy, security, or permission settings. If you need to check permissions, read them — never reset or modify them.
536
537## Before Committing
538
539Verify the following checklist before committing UI tests:
540
541- [ ] Compile both the HostApp project and TestCases.Shared.Tests project successfully
542- [ ] Verify AutomationId references match between HostApp UI (C# or XAML) and test code
543- [ ] Ensure file names follow the `IssueXXXXX` pattern and match between projects
544- [ ] Ensure test methods have descriptive names
545- [ ] Verify test inherits from `_IssuesUITest`
546- [ ] Confirm only ONE `[Category]` attribute per test
547- [ ] No inline `#if` directives in test code (use extension methods)
548- [ ] Test passes locally on at least one platform
549
550### Test State Management
551
552- Tests should be independent and not rely on state from other tests
553- The test infrastructure handles navigation to the test page and basic cleanup
554- If your test modifies global app state, consider whether cleanup is needed
555- Most tests don't require explicit cleanup as each test gets a fresh page instance
556
557## Best Practices
558
559### Default: C# Over XAML
560
561**Use C# files (`.cs`) for UI tests. Only use XAML files (`.xaml`) when the test scenario requires XAML-specific features.**
562
563**When to use C# only (`.cs` file):**
564- ✅ Simple control tests (Button, Label, Entry, etc.)
565- ✅ Layout tests (Grid, StackLayout, FlexLayout, etc.)
566- ✅ Navigation tests
567- ✅ Event handling tests
568- ✅ Property tests
569- ✅ Most UI behavior tests
570
571**When XAML is required (`.xaml` + `.xaml.cs` files):**
572- ✅ Testing XAML binding syntax
573- ✅ Testing XAML templates (DataTemplate, ControlTemplate)
574- ✅ Testing XAML styles and resources
575- ✅ Testing XAML markup extensions
576- ✅ Testing XamlC compilation behavior
577- ✅ Testing XAML-specific parsing or compilation issues
578
579**Examples:**
580
581```csharp
582// ✅ GOOD: C# only test (most common pattern)
583public class Issue12345 : ContentPage
584{
585 public Issue12345()
586 {
587 Content = new StackLayout
588 {
589 Children =
590 {
591 new Label { Text = "Hello", AutomationId = "MyLabel" },
592 new Button { Text = "Click Me", AutomationId = "MyButton" }
593 }
594 };
595 }
596}
597```
598
599```xaml
600<!-- ❌ AVOID unless testing XAML bindings/templates/styles -->
601<ContentPage ...>
602 <StackLayout>
603 <Label Text="Hello" AutomationId="MyLabel" />
604 <Button Text="Click Me" AutomationId="MyButton" />
605 </StackLayout>
606</ContentPage>
607```
608
609### Use Test Helper Base Classes
610
611**ALWAYS check for and use existing test helper base classes instead of creating from scratch:**
612
613| Base Class | Use For | Example |
614|------------|---------|---------|
615| `TestShell` | Shell-related tests | `public class Issue12345 : TestShell` |
616| `TestContentPage` | ContentPage tests needing `Init()` pattern | `public class Issue12345 : TestContentPage` |
617| `TestNavigationPage` | NavigationPage tests | `public class Issue12345 : TestNavigationPage` |
618| `ContentPage` | Simple page tests (direct inheritance) | `public class Issue12345 : ContentPage` |
619
620**TestShell provides:**
621- Platform-specific automation IDs for flyout and back buttons
622- Helper methods: `AddContentPage()`, `AddBottomTab()`, `AddTopTab()`, `AddFlyoutItem()`
623- Abstract `Init()` method for setup
624- `DisplayedPage` property for accessing current page
625
626**TestContentPage/TestNavigationPage provide:**
627- Abstract `Init()` method for deferred initialization
628- Cleaner separation of setup logic
629
630**Example:**
631
632```csharp
633// ✅ GOOD: Using TestShell for Shell tests
634[Issue(IssueTracker.Github, 12345, "Shell navigation bug", PlatformAffected.All)]
635public class Issue12345 : TestShell
636{
637 protected override void Init()
638 {
639 AddContentPage(new ContentPage
640 {
641 Content = new Label { Text = "Test" }
642 });
643 }
644}
645
646// ❌ BAD: Creating Shell from scratch
647public class Issue12345 : Shell
648{
649 public Issue12345()
650 {
651 Items.Add(new ShellItem { ... }); // Verbose, error-prone
652 }
653}
654```
655
656### Avoid Obsolete APIs
657
658**NEVER use obsolete APIs in new tests. Use modern equivalents:**
659
660| ❌ Obsolete API | ✅ Modern API | Notes |
661|----------------|--------------|-------|
662| `Application.MainPage` | `Window.Page` | Access via `this.Window.Page` in ContentPage |
663| `Application.MainPage` | `Application.Current.Windows[0].Page` | When not in Page context |
664| `Frame` | `Border` | Frame is deprecated, use Border instead |
665| `Device.BeginInvokeOnMainThread` | `Dispatcher.Dispatch` or `MainThread.BeginInvokeOnMainThread` | Modern threading APIs |
666
667**Examples:**
668
669```csharp
670// ✅ GOOD: Modern Window API
671this.Window.Page = new NavigationPage(new MyPage());
672
673// ❌ BAD: Obsolete Application.MainPage
674Application.MainPage = new NavigationPage(new MyPage());
675
676// ✅ GOOD: Border
677new Border { Content = new Label { Text = "Hello" } }
678
679// ❌ BAD: Frame (deprecated)
680new Frame { Content = new Label { Text = "Hello" } }
681```
682
683### Use UITest Optimized Controls for Screenshot Tests
684
685**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.
686
687| Standard Control | UITest Control | Purpose |
688|------------------|----------------|---------|
689| `Entry` | `UITestEntry` | Text input without cursor blink |
690| `Editor` | `UITestEditor` | Multi-line input without cursor blink |
691| `SearchBar` | `UITestSearchBar` | Search input without cursor blink |
692
693**Example:**
694
695```csharp
696// For screenshot tests, use UITest controls (UITestEntry, UITestEditor, UITestSearchBar)
697var entry = new UITestEntry
698{
699 Placeholder = "Enter text",
700 IsCursorVisible = false, // Prevents flaky screenshots
701 AutomationId = "TestEntry"
702};
703
704// For non-screenshot tests, standard Entry is fine
705var entry = new Entry { Placeholder = "Enter text", AutomationId = "TestEntry" };
706```
707
708**Location:** `src/Controls/tests/TestCases.HostApp/Controls/UITest*.cs`
709
710### Check Similar Tests for Patterns
711
712**Before creating a new test, search for similar tests to reuse patterns:**
713
714```bash
715# Find similar control tests
716grep -r "class.*Issue.*Button" src/Controls/tests/TestCases.HostApp/Issues/*.cs
717
718# Find Shell tests
719grep -r "TestShell" src/Controls/tests/TestCases.HostApp/Issues/*.cs
720
721# Find tests for specific control
722grep -r "CollectionView" src/Controls/tests/TestCases.HostApp/Issues/*.cs
723
724# Find tests using UITest optimized controls
725grep -r "UITestEntry\|UITestEditor\|UITestSearchBar" src/Controls/tests/TestCases.HostApp/Issues/*.cs
726```
727
728**Reuse established patterns:**
729- AutomationId naming conventions
730- Test structure and layout
731- Common helper methods
732- Platform-specific workarounds
733- UITest optimized control usage
734
735### Safe Area Testing (iOS/MacCatalyst)
736
737**⚠️ CRITICAL for macCatalyst safe area tests:**
738
739Safe area behavior differs significantly between macOS versions. Tests must account for this variability.
740
741| macOS Version | Title Bar Safe Area | CI Environment |
742|---------------|---------------------|----------------|
743| **macOS 14/15** | ~28px top inset | ✅ Used by CI |
744| **macOS 26 (Liquid Glass)** | ~0px top inset | ❌ Local dev only |
745
746**Rules for safe area tests:**
747
7481. **Use tolerances for safe area measurements** - Exact pixel values vary by macOS version
7492. **Test behavior, not exact values** - Verify content is NOT obscured, rather than checking exact padding pixels
7503. **Use `GetRect()` for child content position** - Measure where content actually appears, not parent size
7514. **Never hardcode safe area expectations** - Tests should pass on macOS 14/15 AND macOS 26
752
753**Example patterns:**
754
755```csharp
756// ❌ BAD: Hardcoded safe area value (breaks across macOS versions)
757var safeArea = element.GetRect();
758Assert.That(safeArea.Y, Is.EqualTo(28)); // Fails on macOS 26
759
760// ✅ GOOD: Test that content is not obscured by title bar
761var contentRect = App.WaitForElement("MyContent").GetRect();
762var titleBarRect = App.WaitForElement("TitleBar").GetRect();
763Assert.That(contentRect.Y, Is.GreaterThanOrEqualTo(titleBarRect.Height),
764 "Content should not be obscured by title bar");
765
766// ✅ GOOD: Use tolerance for safe area (accounts for OS differences)
767Assert.That(contentRect.Y, Is.GreaterThan(0).And.LessThan(50),
768 "Content should have some top padding but not excessive");
769```
770
771**Test category**: Use `UITestCategories.SafeAreaEdges` for safe area tests.
772
773**Platform scope**: Safe area tests should typically run on iOS and MacCatalyst (not just one).
774
775**See also**: `.github/instructions/safe-area-debugging.instructions.md` for investigation guidelines
776
@@ −1 +1 @@
11 ---
2−description: "Guidance for GitHub Copilot when working on the .NET MAUI repository."
2+applyTo: "src/Controls/tests/TestCases.Shared.Tests/**,src/Controls/tests/TestCases.HostApp/**"
33 ---
44
5−# GitHub Copilot Development Environment Instructions
5+# UI Testing Guidelines for .NET MAUI
66
7−This document provides specific guidance for GitHub Copilot when working on the .NET MAUI repository. It serves as context for understanding the project structure, development workflow, and best practices.
7+## Overview
88
9−## Code Review Instructions
9+This document provides specific guidance for GitHub Copilot when writing UI tests for the .NET MAUI repository.
1010
11−When performing a code review on PRs that change functional code, run the pr-finalize skill to verify that the PR title and description accurately match the actual implementation. This ensures proper documentation and helps maintain high-quality commit messages.
1211
13−## Repository Overview
1412
15−**.NET MAUI** is a cross-platform framework for creating mobile and desktop applications with C# and XAML. This repository contains the core framework code that enables development for Android, iOS, iPadOS, macOS, and Windows from a single shared codebase.
13+**Critical Principle**: UI tests should run on all applicable platforms (iOS, Android, Windows, MacCatalyst) by default unless there is a specific technical limitation.
1614
17−### Key Technologies
15+## UI Test Structure
1816
19−- **.NET SDK** - Version is **ALWAYS** defined in `global.json` at repository root
20− - **main branch**: Latest stable .NET version
21− - **Feature branches**: Each `netN.0` branch targets the .NET N SDK. By convention, the highest `netN.0` branch is the current development branch for new features and API changes.
22−- **Cake build system** for compilation and packaging (`dotnet cake`)
23−- **MSBuild** with custom build tasks (must build `Microsoft.Maui.BuildTasks.slnf` first)
24−- **Testing frameworks**:
25− - **xUnit** - Unit tests (`*.UnitTests.csproj`)
26− - **NUnit** - UI tests (`TestCases.Shared.Tests`)
27− - **Appium WebDriver** - UI test automation
17+### Two-Project Requirement
2818
29−## Development Environment Setup
19+**CRITICAL: Every UI test requires code in TWO separate projects:**
3020
31−This guidance assumes:
32−- Repository is already cloned and tools are restored (`dotnet tool restore` completed)
33−- Build tasks are compiled (`Microsoft.Maui.BuildTasks.slnf` built successfully)
34−- Correct .NET SDK version installed (verify with `dotnet --version` against `global.json`)
21+1. **HostApp UI Test Page** (`src/Controls/tests/TestCases.HostApp/Issues/`)
22+ - Create the actual UI page that demonstrates the feature or reproduces the issue
23+ - **Prefer C# only** (`.cs` file) unless testing XAML-specific features (bindings, templates, styles)
24+ - Add `AutomationId` attributes on interactive controls for test automation
25+ - Follow naming convention: `IssueXXXXX.cs` (C# only) or `IssueXXXXX.xaml` + `IssueXXXXX.xaml.cs` (when XAML required)
26+ - XXXXX should correspond to a GitHub issue number when applicable
27+ - Ensure the UI provides clear visual feedback for the behavior being tested
28+ - Class must include `[Issue()]` attribute with tracker, number, description, and platform
3529
36−### Platform-Specific Requirements
30+2. **NUnit Test Implementation** (`src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/`)
31+ - Create corresponding Appium-based NUnit tests that inherit from `_IssuesUITest`
32+ - Use the `AutomationId` values to locate and interact with UI elements
33+ - Follow naming convention: `IssueXXXXX.cs` (matches the HostApp page file)
34+ - Include appropriate `[Category(UITestCategories.XYZ)]` attributes (only ONE per test)
35+ - Test should validate expected behavior through UI interactions and assertions
3736
38−- **Android**: OpenJDK 17 + Android SDK (install via `android` command after `dotnet tool restore`)
39−- **iOS/macOS**: Xcode (current stable version)
40−- **Windows**: Windows SDK
37+### Base Class and Infrastructure
4138
42−## Project Structure
39+- Each test class must inherit from `_IssuesUITest`
40+- The `_IssuesUITest` base class provides:
41+ - The `App` property for interacting with UI elements
42+ - Test initialization and setup
43+ - Helper methods for common UI test operations
44+- The test infrastructure automatically handles platform detection and page navigation
4345
44−### Important Directories
45−- `src/Core/` - Core MAUI framework code
46−- `src/Controls/` - UI controls and components
47−- `src/Essentials/` - Platform APIs and essentials
48−- `src/TestUtils/` - Testing utilities and infrastructure
49−- `docs/` - Development documentation
50−- `eng/` - Build engineering and tooling
51−- `.github/` - GitHub workflows and configuration
46+### Naming Conventions
5247
53−### Platform-Specific Code Organization
54−- **Android** specific code is inside folders labeled `Android`
55−- **iOS** specific code is inside folders labeled `iOS`
56−- **MacCatalyst** specific code is inside folders named `MacCatalyst`
57−- **Windows** specific code is inside folders named `Windows`
48+**Test Files:**
49+- Pattern: `IssueXXXXX.cs` where XXXXX corresponds to a GitHub issue number
50+- Must match the corresponding HostApp page file name in TestCases.HostApp (either `.cs` only or `.xaml`)
5851
59−### Platform-Specific File Extensions
52+**Test Methods:**
53+- Use descriptive names that clearly explain what behavior is being verified
54+- ✅ Good: `VerifySafeAreaBottomPaddingWithKeyboard()`, `ButtonClickUpdatesLabel()`
55+- ❌ Bad: `Test1()`, `TestMethod()`, `RunTest()`
6056
61−Platform-specific files use naming conventions to control compilation:
57+**AutomationId Values:**
58+- Always use unique, descriptive `AutomationId` values
59+- Reference the same `AutomationId` in both C# code (or XAML if used) and test code
60+- Use PascalCase for AutomationId values
6261
63−**File extension patterns**:
64−- `.windows.cs` - Windows TFM only
65−- `.android.cs` - Android TFM only
66−- `.ios.cs` - iOS and MacCatalyst TFMs (both)
67−- `.maccatalyst.cs` - MacCatalyst TFM only (does NOT compile for iOS)
62+## Complete Test Example
6863
69−**Important**: Both `.ios.cs` and `.maccatalyst.cs` files compile for MacCatalyst. There is no precedence mechanism that excludes one when the other exists.
64+### Example 1: C# Only (Preferred for Most Tests)
7065
71−**Example**: If you have both `CollectionView.ios.cs` and `CollectionView.maccatalyst.cs`, both will compile for MacCatalyst builds. The `.maccatalyst.cs` file won't compile for iOS, but the `.ios.cs` file will compile for both iOS and MacCatalyst.
66+**HostApp Page** (`TestCases.HostApp/Issues/Issue12345.cs`):
67+```csharp
68+namespace Maui.Controls.Sample.Issues;
7269
73−### Sample Projects
70+[Issue(IssueTracker.Github, 12345, "Button click updates label text", PlatformAffected.All)]
71+public class Issue12345 : ContentPage
72+{
73+ public Issue12345()
74+ {
75+ var resultLabel = new Label
76+ {
77+ Text = "Initial Text",
78+ AutomationId = "ResultLabel"
79+ };
7480
75−- `src/Controls/samples/Maui.Controls.Sample` - Full gallery sample with all controls and features
76−- `src/Controls/samples/Maui.Controls.Sample.Sandbox` - Empty project for testing/reproduction
77−- `src/Essentials/samples/Essentials.Sample` - Essentials API demonstrations (non-UI MAUI APIs)
78−- `src/BlazorWebView/samples/` - BlazorWebView sample applications
81+ Content = new VerticalStackLayout
82+ {
83+ Children =
84+ {
85+ new Button
86+ {
87+ Text = "Click Me",
88+ AutomationId = "TestButton",
89+ Command = new Command(() => resultLabel.Text = "Expected Text")
90+ },
91+ resultLabel
92+ }
93+ };
94+ }
95+}
96+```
7997
80−## Development Workflow
98+### Example 2: XAML (When Testing XAML-Specific Features)
8199
82−### Testing
100+**HostApp XAML** (`TestCases.HostApp/Issues/Issue12346.xaml`):
101+```xaml
102+<?xml version="1.0" encoding="utf-8" ?>
103+<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
104+ xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
105+ x:Class="Maui.Controls.Sample.Issues.Issue12346">
106+ <VerticalStackLayout>
107+ <Button Text="Click Me"
108+ AutomationId="TestButton"
109+ Clicked="OnButtonClicked" />
110+ <Label Text="Initial Text"
111+ x:Name="ResultLabel"
112+ AutomationId="ResultLabel" />
113+ </VerticalStackLayout>
114+</ContentPage>
115+```
83116
84−Major test projects:
85−- **Core**: `src/Core/tests/UnitTests/Core.UnitTests.csproj`
86−- **Essentials**: `src/Essentials/test/UnitTests/Essentials.UnitTests.csproj`
87−- **Controls**: `src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj`
88−- **XAML**: `src/Controls/tests/Xaml.UnitTests/Controls.Xaml.UnitTests.csproj`
117+**HostApp Code-Behind** (`TestCases.HostApp/Issues/Issue12346.xaml.cs`):
118+```csharp
119+namespace Maui.Controls.Sample.Issues;
89120
90−Find all tests: `find . -name "*.UnitTests.csproj"`
121+[Issue(IssueTracker.Github, 12346, "Testing XAML binding behavior", PlatformAffected.All)]
122+public partial class Issue12346 : ContentPage
123+{
124+ public Issue12346()
125+ {
126+ InitializeComponent();
127+ }
91128
92−### CI Pipelines (Azure DevOps)
129+ void OnButtonClicked(object sender, EventArgs e)
130+ {
131+ ResultLabel.Text = "Expected Text";
132+ }
133+}
134+```
93135
94−When referencing or triggering CI pipelines, use these current pipeline names:
136+### NUnit Test (Same for Both Examples)
95137
96−| Pipeline | Name | Purpose |
97−|----------|------|---------|
98−| Overall CI | `maui-pr` | Full PR validation build |
99−| Device Tests | `maui-pr-devicetests` | Helix-based device tests |
100−| UI Tests | `maui-pr-uitests` | Appium-based UI tests |
138+**NUnit Test** (`TestCases.Shared.Tests/Tests/Issues/Issue12345.cs` or `Issue12346.cs`):
139+```csharp
140+public class Issue12345 : _IssuesUITest
141+{
142+ public override string Issue => "Description of the issue being tested";
101143
102−**⚠️ Old pipeline names** (e.g., `MAUI-UITests-public`, `MAUI-public`) are **outdated** and should NOT be used. Always use the names above.
144+ public Issue12345(TestDevice device) : base(device) { }
103145
104−### Investigating CI Failures
146+ [Test]
147+ [Category(UITestCategories.Layout)] // Pick the most appropriate category
148+ public void ButtonClickUpdatesLabel()
149+ {
150+ // Wait for element to be ready
151+ App.WaitForElement("TestButton");
105152
106−**🚨 ALWAYS use the `azdo-build-investigator` skill when investigating CI failures or assessing merge readiness.** Its instructions direct you to invoke the `ci-analysis` skill first for the core investigation workflow, then apply MAUI-specific corrections (correct pipeline names, XHarness quirks, binlog guidance).
153+ // Interact with the UI
154+ App.Tap("TestButton");
107155
108−Do NOT default to manually querying AzDO APIs or rely solely on `gh pr checks` pass/fail counts.
156+ // Verify expected behavior
157+ var labelText = App.FindElement("ResultLabel").GetText();
158+ Assert.That(labelText, Is.EqualTo("Expected Text"));
109159
110−**When to use it:**
111−- "How does CI look?" / "Is CI green?" / "Can we merge?"
112−- "What's failing?" / "Are these known failures?"
113−- "Is this PR safe to merge?" / "Any CI concerns?"
114−- After any PR push to verify the build
160+ // Optional: Visual verification
161+ VerifyScreenshot();
162+ }
163+}
164+```
115165
116−**Verifying specific tests:** When asked "did test X pass?" or "did the new test run?", query the **actual AzDO test results** — do NOT infer whether a test ran by inspecting code attributes. Class-level traits, base class categories, and assembly-level attributes can all cause a test to run even when the method itself has no visible category. Check the evidence, not the code.
166+## Common Patterns
117167
118−**Anti-pattern:** Writing ad-hoc scripts to parse AzDO build timelines. The skills handle Helix work item details, known issue cross-referencing, and test result aggregation that manual approaches miss.
168+### Waiting for Elements
169+```csharp
170+App.WaitForElement("AutomationId");
171+```
119172
120−### Gradle / Maven Dependency Failures (CFSClean)
173+### Interacting with Elements
174+```csharp
175+App.Tap("AutomationId");
176+App.FindElement("AutomationId").GetText();
177+var rect = App.WaitForElement("AutomationId").GetRect();
178+```
121179
122−The official CI build uses CFSClean network isolation which blocks `repo.maven.apache.org`. All Gradle/Maven dependencies resolve through the `dotnet-public-maven` Azure Artifacts feed.
180+### Assertions
181+```csharp
182+Assert.That(actualValue, Is.EqualTo(expectedValue).Within(tolerance));
183+Assert.That(rect.Height, Is.LessThanOrEqualTo(maxHeight));
184+```
123185
124−**If CI fails with Gradle 401 errors** like `"No local versions of package"` or `"Please provide authentication to save package from upstream"`, it means a Maven package hasn't been ingested into the feed yet. **Fix:** run `./eng/ingest-maven-deps.sh` locally to pre-populate the feed. See `src/Core/AndroidNative/settings.gradle` for details.
186+### Screenshot Verification
187+```csharp
188+// Verify visual appearance (automated comparison)
189+VerifyScreenshot();
125190
126−**Do NOT upgrade Gradle past 8.x** — the Android SDK's `net.android.init.gradle.kts` is incompatible with Gradle 9.x (`dotnet/android#10738`).
191+// With custom name
192+VerifyScreenshot("CustomTestName");
127193
128−### Code Formatting
194+// With tolerance (0.0-100.0 percentage) - use sparingly
195+VerifyScreenshot(tolerance: 0.5); // Allow 0.5% difference for cross-machine rendering variance
129196
130−Always format code before committing:
197+// PREFERRED: Keep retrying for up to 2 seconds (for animations)
198+VerifyScreenshot(retryTimeout: TimeSpan.FromSeconds(2));
131199
200+// Combined: tolerance for rendering variance + retryTimeout for timing
201+VerifyScreenshot(tolerance: 0.5, retryTimeout: TimeSpan.FromSeconds(2));
202+
203+// Manual screenshot for debugging
204+App.Screenshot("TestStep1");
205+```
206+
207+**CRITICAL - VerifyScreenshot() Built-in Features:**
208+
209+`VerifyScreenshot()` **already includes** stability mechanisms. Do NOT add redundant delays:
210+
211+| Feature | Behavior | Parameter |
212+|---------|----------|-----------|
213+| **Android delay** | Automatic 350ms wait for animations | Built-in, cannot override |
214+| **Retry logic** | Default: retries once; with retryTimeout: keeps retrying | Built-in |
215+| **Retry delay** | 500ms delay between retry attempts | `retryDelay: TimeSpan` (customizable) |
216+| **Retry timeout** | Total time to keep retrying | `retryTimeout: TimeSpan` (PREFERRED for flaky tests) |
217+| **Tolerance** | Allow percentage difference (0-100) | `tolerance: double` (default: 0.0) |
218+
219+**When to customize:**
220+- ✅ Use `retryTimeout` parameter for animations with variable timing (PREFERRED approach)
221+- ✅ Use small `tolerance` (0.5%) for cross-machine rendering variance, NOT to hide timing issues
222+- ✅ Use `retryDelay` if you need to change the delay between retry attempts
223+- ❌ **DO NOT** add `Task.Delay()` or `Thread.Sleep()` before `VerifyScreenshot()` - use `retryTimeout` instead
224+
225+## Writing Robust UI Tests
226+
227+### Best Practices for Screenshot Tests
228+
229+When writing tests that use `VerifyScreenshot()`, follow these patterns to avoid flakiness:
230+
231+```
232+┌─────────────────────────────────────────────────────────────────┐
233+│ 1. UNDERSTAND TEST INFRASTRUCTURE │
234+│ - Read UITest.cs base class implementation │
235+│ - Understand built-in retry/delay/tolerance mechanisms │
236+│ - Check what helpers/extensions already exist │
237+├─────────────────────────────────────────────────────────────────┤
238+│ 2. USE PROPER WAITING PATTERNS │
239+│ - Use WaitForElement before interacting with elements │
240+│ - Use retryTimeout for screenshots after animations │
241+│ - Never use arbitrary Task.Delay() before VerifyScreenshot │
242+├─────────────────────────────────────────────────────────────────┤
243+│ 3. APPLY MINIMAL TOLERANCES │
244+│ - Use retryTimeout for timing issues (preferred) │
245+│ - Use small tolerance (0.5%) only for rendering variance │
246+│ - Never use tolerance > 5% without justification │
247+└─────────────────────────────────────────────────────────────────┘
248+```
249+
250+### Common Flaky Test Patterns
251+
252+| Symptom | Root Cause | Fix Pattern | Anti-Pattern |
253+|---------|------------|-------------|--------------|
254+| **Visual diff in screenshot** | Animation not finished | `VerifyScreenshot(retryTimeout: TimeSpan.FromSeconds(2))` | ❌ Adding `Task.Delay()` before |
255+| **Element not found** | Element not rendered yet | `App.WaitForElement("Id", timeout: TimeSpan.FromSeconds(10))` | ❌ `Thread.Sleep()` then `FindElement()` |
256+| **Timeout on interaction** | Page not fully loaded | Wait for specific element that indicates ready state | ❌ Arbitrary 3-second delay |
257+| **Inconsistent rect/position** | Layout not settled | Multiple `GetRect()` calls with comparison | ❌ Single `GetRect()` after delay |
258+| **WebView failures** | External URL/network | Use mock URLs instead of external URLs | ❌ Adding longer timeouts |
259+
260+### Anti-Patterns (DO NOT DO)
261+
262+| Anti-Pattern | Why It's Wrong | Better Alternative |
263+|--------------|----------------|-------------------|
264+| ❌ `Task.Delay(500).Wait()` before `VerifyScreenshot()` | VerifyScreenshot already has built-in retry; use retryTimeout instead | Use `VerifyScreenshot(retryTimeout: TimeSpan.FromSeconds(2))` |
265+| ❌ `Thread.Sleep(2000)` before element interaction | Arbitrary wait; doesn't guarantee element is ready | `App.WaitForElement("Id", timeout: ...)` |
266+| ❌ Adding tolerance > 5% without justification | Hides real bugs; too permissive | Use `retryTimeout` for timing issues; small tolerance (0.5%) for rendering variance |
267+| ❌ Using external URLs in WebView tests | External dependency; unreliable | Use mock URLs or local content |
268+| ❌ Fixing symptoms without understanding infrastructure | Redundant fixes; doesn't address root cause | Read `UITest.cs` first (step 1 above) |
269+
270+### When to Use What
271+
272+**VerifyScreenshot() parameters (preferred):**
273+```csharp
274+// Animation timing issues - keep retrying for up to 2 seconds
275+// This is the PREFERRED approach for flaky screenshot tests
276+VerifyScreenshot(retryTimeout: TimeSpan.FromSeconds(2));
277+
278+// Small tolerance for cross-machine rendering variance + retryTimeout for timing
279+// Use 0.5% tolerance as safety margin, NOT to hide timing issues
280+VerifyScreenshot(tolerance: 0.5, retryTimeout: TimeSpan.FromSeconds(2));
281+
282+// Legacy: retryDelay only changes delay BETWEEN retries (default 500ms)
283+// retryTimeout is preferred because it keeps trying until success
284+VerifyScreenshot(retryDelay: TimeSpan.FromSeconds(1));
285+```
286+
287+**Key difference: retryDelay vs retryTimeout:**
288+- `retryDelay`: Delay between retry attempts (default 500ms). Only retries ONCE.
289+- `retryTimeout`: Total time to keep retrying. Retries every `retryDelay` until timeout.
290+- **Prefer `retryTimeout`** for animations with variable completion times.
291+
292+**WaitForElement (for element readiness):**
293+```csharp
294+// Wait up to 10 seconds for element to appear
295+App.WaitForElement("ButtonId", timeout: TimeSpan.FromSeconds(10));
296+
297+// Then interact
298+App.Tap("ButtonId");
299+```
300+
301+**Task.Delay/Thread.Sleep (avoid if possible):**
302+```csharp
303+// AVOID: With retryTimeout, you rarely need explicit delays anymore
304+//
305+// Old pattern (before retryTimeout):
306+// Task.Delay(300).Wait();
307+// VerifyScreenshot(tolerance: 2.0);
308+//
309+// New pattern (preferred):
310+VerifyScreenshot(tolerance: 0.5, retryTimeout: TimeSpan.FromSeconds(2));
311+
312+// ONLY use explicit delays when:
313+// 1. Waiting for non-element state with no screenshot (rare)
314+// 2. External system delay that can't be detected otherwise
315+// 3. After exhausting other options AND documenting why
316+```
317+
318+### Understanding Test Infrastructure
319+
320+**Key files to understand when writing UI tests:**
321+
322+1. **UITest.cs** - Base class with `VerifyScreenshot()` implementation
323+ - Path: `src/Controls/tests/TestCases.Shared.Tests/UITest.cs`
324+ - Contains: retry logic, tolerance parsing, platform-specific delays
325+
326+2. **_IssuesUITest.cs** - Issues test base class
327+ - Path: `src/Controls/tests/TestCases.Shared.Tests/_IssuesUITest.cs`
328+ - Contains: Navigation helpers, common patterns
329+
330+3. **Extension methods** - Platform-specific helpers
331+ - Path: `src/Controls/tests/TestCases.Shared.Tests/` (various extension files)
332+ - Contains: Existing helpers for common operations
333+
334+**Find existing patterns:**
132335 ```bash
133−dotnet format Microsoft.Maui.sln --no-restore --exclude Templates/src --exclude-diagnostics CA1822
336+# See VerifyScreenshot implementation (including retryTimeout)
337+grep -A 30 "public void VerifyScreenshot" src/Controls/tests/TestCases.Shared.Tests/UITest.cs
338+
339+# Find existing tests using retryTimeout (preferred pattern)
340+grep -r "retryTimeout" src/Controls/tests/TestCases.Shared.Tests/Tests/
341+
342+# Find existing tolerance patterns
343+grep -r "tolerance:" src/Controls/tests/TestCases.Shared.Tests/Tests/
134344 ```
135345
136−## Contribution Guidelines
346+### Infrastructure Notes
137347
138−### Handling Existing PRs for Assigned Issues
348+**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.
139349
140−**🚨 CRITICAL REQUIREMENT: Always develop your own solution first, then compare with existing PRs.**
350+## Test Categories
141351
142−1. **Develop your own solution first** - Analyze the issue independently and design your approach without looking at existing PRs
143−2. **Search for existing PRs** - After developing your solution, search for open PRs addressing the same issue
144−3. **Compare and evaluate** - Examine existing PR approaches and decide which solution better addresses the issue
145−4. **Document your decision** - In your PR description, compare your solution to existing PRs and explain why you chose your approach, including concerns with alternatives
146−5. **Improve either solution** - Whether using your solution or an existing one, enhance with better tests, code quality, error handling, or documentation
352+### Category Guidelines
353+- Use appropriate categories from `UITestCategories`
354+- **Only ONE** `[Category]` attribute per test
355+- Pick the most specific category that applies
147356
148−### Auto-Generated Files (Never Commit)
357+### Test Categories
149358
150−These files are auto-generated and must NOT be committed:
151−- `cgmanifest.json` - Generated during CI builds
152−- `templatestrings.json` - Auto-generated localization
359+**CRITICAL**: Always check [UITestCategories.cs](../../src/Controls/tests/TestCases.Shared.Tests/UITestCategories.cs) for the authoritative, complete list of categories.
153360
154−**For AI agents:** Always reset changes to these files before committing.
361+**Selection rule**: Choose the MOST SPECIFIC category that applies to your test. If multiple categories seem applicable, choose the one that best describes the primary focus of the test.
155362
156−### PublicAPI.Unshipped.txt File Management
363+**Common categories** (examples only - not exhaustive):
364+- **SafeArea**: `SafeAreaEdges` - Safe area and padding tests
365+- **Basic controls**: `Button`, `Label`, `Entry`, `Editor` - Specific control tests
366+- **Collection controls**: `CollectionView`, `ListView`, `CarouselView` - Collection control tests
367+- **Layout**: `Layout` - Layout-related tests
368+- **Navigation**: `Shell`, `Navigation`, `TabbedPage` - Navigation tests
369+- **Interaction**: `Gestures`, `Focus`, `Accessibility` - Interaction tests
370+- **Lifecycle**: `Window`, `Page`, `LifeCycle` - Page lifecycle tests
157371
158−When working with public API changes:
159−- **Never disable analyzers** to bypass PublicAPI.Unshipped.txt issues
160−- **Always add correct API entries** to PublicAPI.Unshipped.txt files
161−- **Use `dotnet format analyzers`** if having trouble
162−- **If files are incorrect**: Revert all changes, then add only the necessary new API entries
372+**List all categories programmatically**:
373+```bash
374+grep -E "public const string [A-Za-z]+ = " src/Controls/tests/TestCases.Shared.Tests/UITestCategories.cs
375+```
163376
164−### Branching
165−- `main` - For bug fixes without API changes
166−- The highest `netN.0` branch (by convention) - For new features and API changes. To find it, run `git fetch origin` then: `git for-each-ref --sort=-version:refname --count=1 --format='%(refname:lstrip=3)' refs/remotes/origin/net*.0`
377+**Important**: When a new UI test category is added to `UITestCategories.cs`, also update `eng/pipelines/common/ui-tests.yml` to include the new category.
167378
168−### Git Workflow (Copilot CLI Rules)
379+## Platform Coverage
169380
170−**🚨 CRITICAL Git Rules for Copilot CLI:**
381+### Default Behavior
171382
172−1. **NEVER commit directly to `main`** - Always create a feature branch for your work. Direct commits to `main` are strictly prohibited.
383+Tests should run on all applicable platforms by default. The test infrastructure handles platform detection automatically.
173384
174−2. **When amending an existing PR, work on the PR's branch directly** - Do NOT create a separate branch off a PR branch. The PR branch already IS a feature branch. Creating a new branch off it means CI won't run on the original PR, defeating the purpose. Use `gh pr checkout` to switch to the PR branch, make your changes, commit, **then** ask before pushing so the user can review locally first.
385+### No Inline #if Directives in Test Methods
175386
176−3. **Do NOT rebase, squash, or force-push** unless explicitly requested by the user. These operations rewrite git history and can cause problems for other contributors. Default behavior should be regular commits and pushes.
387+**Do NOT use `#if ANDROID`, `#if IOS`, etc. directly in test methods.** Platform-specific behavior must be hidden behind extension methods for readability.
177388
178−**Safe Git Workflow:**
389+**Note:** This rule is about **code cleanliness**, not platform scope. Using `#if ANDROID ... #else ...` still compiles for all platforms - the issue is that inline directives make test logic hard to read and maintain.
390+
391+```csharp
392+// ❌ BAD - inline #if in test method (hard to read)
393+[Test]
394+public void MyTest()
395+{
396+#if ANDROID
397+ App.TapCoordinates(100, 200);
398+#else
399+ App.Tap("MyElement");
400+#endif
401+}
402+
403+// ✅ GOOD - platform logic in extension method (clean test)
404+[Test]
405+public void MyTest()
406+{
407+ App.TapElementCrossPlatform("MyElement");
408+}
409+```
410+
411+Move platform-specific logic to extension methods to keep test code clean and readable.
412+
413+## Running UI Tests Locally
414+
415+**CRITICAL: ALWAYS use the BuildAndRunHostApp.ps1 script to run UI tests. NEVER run `dotnet test` or `dotnet build` commands manually.**
416+
417+### BuildAndRunHostApp.ps1 Script (ONLY Way to Run Tests)
418+
419+**Script location**: `.github/scripts/BuildAndRunHostApp.ps1`
420+
421+**Usage:**
422+```powershell
423+# Run specific test on Android
424+pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform android -TestFilter "FullyQualifiedName~Issue12345"
425+
426+# Run specific test on iOS
427+pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform ios -TestFilter "FullyQualifiedName~Issue12345"
428+
429+# Run specific test on MacCatalyst
430+pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform maccatalyst -TestFilter "FullyQualifiedName~Issue12345"
431+
432+# Run tests by category
433+pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform android -Category "SafeAreaEdges"
434+
435+# Run specific test with custom device (iOS only)
436+pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform ios -TestFilter "Issue12345" -DeviceUdid "12345678-1234567890ABCDEF"
437+```
438+
439+**What the script handles automatically:**
440+- ✅ Automatic device detection and boot (iPhone Xs for iOS, first available for Android)
441+- ✅ Building TestCases.HostApp (always fresh build)
442+- ✅ App installation and deployment
443+- ✅ Running your NUnit test via `dotnet test`
444+- ✅ Complete log capture to `CustomAgentLogsTmp/UITests/` directory:
445+ - `android-device.log` or `ios-device.log` - Device logs filtered to HostApp
446+ - `test-output.log` - Test execution output
447+
448+**Why you must use the script:**
449+- The script ensures correct device targeting and environment variables
450+- It handles platform-specific quirks and setup requirements
451+- It provides consistent test execution across all platforms
452+- It captures logs automatically for debugging
453+- Manual `dotnet` commands often fail due to missing environment setup
454+
455+### Prerequisites: Kill Existing Appium Processes
456+
457+**CRITICAL**: Before running UITests with BuildAndRunHostApp.ps1, always kill any existing Appium processes. The UITest framework needs to start its own Appium server, and having a stale process running will cause the tests to fail with an error like:
458+
459+```
460+AppiumServerHasNotBeenStartedLocallyException: The local appium server has not been started.
461+Time 120000 ms for the service starting has been expired!
462+```
463+
464+**Solution: Always kill existing Appium processes before running tests:**
465+
179466 ```bash
180−# Create a feature branch (NEVER work directly on main)
181−git checkout -b feature/issue-12345
467+# Kill any Appium processes on port 4723
468+lsof -i :4723 | grep LISTEN | awk '{print $2}' | xargs kill -9 2>/dev/null && echo "✅ Killed existing Appium processes" || echo "ℹ️ No Appium processes running on port 4723"
469+```
182470
183−# Make commits normally
184−git add .
185−git commit -m "Fix: Description of the change"
471+**Why this is needed:** The UITest framework automatically starts and manages its own Appium server. If there's already an Appium process running (from a previous test run or manual testing), the framework will timeout trying to start a new one.
186472
187−# Push to remote (for new branches)
188−git push -u origin feature/issue-12345
473+### Troubleshooting
189474
190−# For subsequent pushes on the same branch
191−git push
475+**Android App Crashes on Launch:**
476+
477+If you encounter navigation fragment errors or resource ID issues:
192478 ```
479+java.lang.IllegalArgumentException: No view found for id 0x7f0800f8 (com.microsoft.maui.uitests:id/inward) for fragment NavigationRootManager_ElementBasedFragment
480+```
193481
194−**When asked to update an existing PR:**
482+**Solution:** Read the crash logs to find the actual exception:
195483 ```bash
196−# Check out the PR branch directly (do NOT create a new branch off it)
197−gh pr checkout 12345
484+# Monitor logcat for the crash
485+adb logcat | grep -E "(FATAL|AndroidRuntime|Exception|Error|Crash)"
486+```
198487
199−# Make fixes and commit to the PR branch
200−git add .
201−git commit -m "Fix: Description of the change"
488+**Debugging steps:**
489+1. **Find the exception** in logcat - look for the stack trace
490+2. **Investigate the root cause** - What line of code is throwing? Why?
491+3. **Check for null references** - Are required resources missing?
492+4. **Verify resource IDs exist** - Check if the ID referenced actually exists in the app
493+5. If you can't determine the fix, **ask for guidance** with the full exception details
494+
495+**iOS App Crashes on Launch or Won't Start with Appium:**
496+
497+If the iOS app crashes when launched by Appium or manually with `xcrun simctl launch`:
498+
499+**Solution:** Read the crash logs to find the actual exception:
500+```bash
501+# Capture crash logs
502+xcrun simctl spawn booted log stream --predicate 'processImagePath contains "TestCases.HostApp"' --level=debug > /tmp/ios_crash.log 2>&1 &
503+LOG_PID=$!
504+
505+# Try to launch the app
506+xcrun simctl launch $UDID com.microsoft.maui.uitests
507+
508+# Wait a moment for crash
509+sleep 3
510+
511+# Stop log capture
512+kill $LOG_PID
513+
514+# Review the crash log
515+cat /tmp/ios_crash.log | grep -A 20 -B 5 "Exception"
202516 ```
203−1. **STOP and ask the user** before pushing: "Changes are committed locally. Would you like me to push these changes to the PR?"
204−2. Exception: If the user's instructions explicitly include pushing, proceed without asking.
205517
206−### Documentation
207−- Update XML documentation for public APIs
208−- Follow existing code documentation patterns
209−- Update relevant docs in `docs/` folder when needed
518+**Debugging steps:**
519+1. **Find the exception** in the crash log - look for stack traces
520+2. **Investigate the root cause** - What's causing the crash?
521+3. **Check for missing resources** - Are all required files included in the bundle?
522+4. **Verify Info.plist** - Are required keys present?
523+5. **Check for platform-specific issues** - iOS version compatibility, permissions, etc.
524+6. If you can't determine the fix, **ask for guidance** with the full exception details
210525
211−### Opening PRs
526+### Dangerous System Commands (Never Run)
212527
213−All PRs are required to have this at the top of the description:
528+**🚨 NEVER run these commands — they cause destructive system-wide side effects:**
214529
530+- **`tccutil reset`** — Wipes ALL macOS permissions (Accessibility, Camera, etc.) system-wide. This breaks Appium/WebDriverAgent, Xcode, and other tools. Once reset, permissions must be manually re-granted through System Settings.
531+- **`csrutil disable`** — Disables System Integrity Protection
532+- **`networksetup`** — Modifies network configuration
533+- **`defaults delete`** on system domains — Resets system preferences
534+
535+**General rule:** Do not run commands that modify macOS system-level privacy, security, or permission settings. If you need to check permissions, read them — never reset or modify them.
536+
537+## Before Committing
538+
539+Verify the following checklist before committing UI tests:
540+
541+- [ ] Compile both the HostApp project and TestCases.Shared.Tests project successfully
542+- [ ] Verify AutomationId references match between HostApp UI (C# or XAML) and test code
543+- [ ] Ensure file names follow the `IssueXXXXX` pattern and match between projects
544+- [ ] Ensure test methods have descriptive names
545+- [ ] Verify test inherits from `_IssuesUITest`
546+- [ ] Confirm only ONE `[Category]` attribute per test
547+- [ ] No inline `#if` directives in test code (use extension methods)
548+- [ ] Test passes locally on at least one platform
549+
550+### Test State Management
551+
552+- Tests should be independent and not rely on state from other tests
553+- The test infrastructure handles navigation to the test page and basic cleanup
554+- If your test modifies global app state, consider whether cleanup is needed
555+- Most tests don't require explicit cleanup as each test gets a fresh page instance
556+
557+## Best Practices
558+
559+### Default: C# Over XAML
560+
561+**Use C# files (`.cs`) for UI tests. Only use XAML files (`.xaml`) when the test scenario requires XAML-specific features.**
562+
563+**When to use C# only (`.cs` file):**
564+- ✅ Simple control tests (Button, Label, Entry, etc.)
565+- ✅ Layout tests (Grid, StackLayout, FlexLayout, etc.)
566+- ✅ Navigation tests
567+- ✅ Event handling tests
568+- ✅ Property tests
569+- ✅ Most UI behavior tests
570+
571+**When XAML is required (`.xaml` + `.xaml.cs` files):**
572+- ✅ Testing XAML binding syntax
573+- ✅ Testing XAML templates (DataTemplate, ControlTemplate)
574+- ✅ Testing XAML styles and resources
575+- ✅ Testing XAML markup extensions
576+- ✅ Testing XamlC compilation behavior
577+- ✅ Testing XAML-specific parsing or compilation issues
578+
579+**Examples:**
580+
581+```csharp
582+// ✅ GOOD: C# only test (most common pattern)
583+public class Issue12345 : ContentPage
584+{
585+ public Issue12345()
586+ {
587+ Content = new StackLayout
588+ {
589+ Children =
590+ {
591+ new Label { Text = "Hello", AutomationId = "MyLabel" },
592+ new Button { Text = "Click Me", AutomationId = "MyButton" }
593+ }
594+ };
595+ }
596+}
215597 ```
216−<!-- Please let the below note in for people that find this PR -->
217−> [!NOTE]
218−> Are you waiting for the changes in this PR to be merged?
219−> It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you!
598+
599+```xaml
600+<!-- ❌ AVOID unless testing XAML bindings/templates/styles -->
601+<ContentPage ...>
602+ <StackLayout>
603+ <Label Text="Hello" AutomationId="MyLabel" />
604+ <Button Text="Click Me" AutomationId="MyButton" />
605+ </StackLayout>
606+</ContentPage>
220607 ```
221608
222−Always put that at the top, without the block quotes. Without it, users will NOT be able to try the PR and your work will have been in vain!
609+### Use Test Helper Base Classes
223610
611+**ALWAYS check for and use existing test helper base classes instead of creating from scratch:**
224612
613+| Base Class | Use For | Example |
614+|------------|---------|---------|
615+| `TestShell` | Shell-related tests | `public class Issue12345 : TestShell` |
616+| `TestContentPage` | ContentPage tests needing `Init()` pattern | `public class Issue12345 : TestContentPage` |
617+| `TestNavigationPage` | NavigationPage tests | `public class Issue12345 : TestNavigationPage` |
618+| `ContentPage` | Simple page tests (direct inheritance) | `public class Issue12345 : ContentPage` |
225619
226−## Custom Agents and Skills
620+**TestShell provides:**
621+- Platform-specific automation IDs for flyout and back buttons
622+- Helper methods: `AddContentPage()`, `AddBottomTab()`, `AddTopTab()`, `AddFlyoutItem()`
623+- Abstract `Init()` method for setup
624+- `DisplayedPage` property for accessing current page
227625
228−The repository includes specialized custom agents and reusable skills for specific tasks.
626+**TestContentPage/TestNavigationPage provide:**
627+- Abstract `Init()` method for deferred initialization
628+- Cleaner separation of setup logic
229629
230−### Skills vs Agents
630+**Example:**
231631
232−| Aspect | Skills | Agents |
233−|--------|--------|--------|
234−| **Invoke** | `/skill-name` or direct request | Delegate to agent |
235−| **Output** | Analysis, recommendations | Actions, changes applied |
236−| **Interaction** | Interactive discussion | Autonomous workflow |
237−| **Example** | `/learn-from-pr` → recommendations | learn-from-pr agent → applies changes |
632+```csharp
633+// ✅ GOOD: Using TestShell for Shell tests
634+[Issue(IssueTracker.Github, 12345, "Shell navigation bug", PlatformAffected.All)]
635+public class Issue12345 : TestShell
636+{
637+ protected override void Init()
638+ {
639+ AddContentPage(new ContentPage
640+ {
641+ Content = new Label { Text = "Test" }
642+ });
643+ }
644+}
238645
239−### Available Custom Agents
646+// ❌ BAD: Creating Shell from scratch
647+public class Issue12345 : Shell
648+{
649+ public Issue12345()
650+ {
651+ Items.Add(new ShellItem { ... }); // Verbose, error-prone
652+ }
653+}
654+```
240655
241−1. **pr** - Sequential 4-phase workflow for reviewing and working on PRs
242− - **Use when**: A PR already exists and needs review or work, OR an issue needs a fix
243− - **Capabilities**: PR review, test verification, fix exploration, alternative comparison
244− - **Trigger phrases**: "review PR #XXXXX", "work on PR #XXXXX", "fix issue #XXXXX", "continue PR #XXXXX"
245− - **Do NOT use for**: Just running tests manually → Use `sandbox-agent`
656+### Avoid Obsolete APIs
246657
247−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`)
248− - **Use when**: Creating new tests for issues or PRs
249− - **Capabilities**: Test type determination (UI and XAML), skill invocation, test verification
250− - **Trigger phrases**: "write tests for #XXXXX", "create tests", "add test coverage"
658+**NEVER use obsolete APIs in new tests. Use modern equivalents:**
251659
252−3. **sandbox-agent** - Specialized agent for working with the Sandbox app for testing, validation, and experimentation
253− - **Use when**: User wants to manually test PR functionality or reproduce issues
254− - **Capabilities**: Sandbox app setup, Appium-based manual testing, PR functional validation
255− - **Trigger phrases**: "test this PR", "validate PR #XXXXX in Sandbox", "reproduce issue #XXXXX", "try out in Sandbox"
256− - **Do NOT use for**: Code review (use pr agent), writing automated tests (use write-tests-agent)
660+| ❌ Obsolete API | ✅ Modern API | Notes |
661+|----------------|--------------|-------|
662+| `Application.MainPage` | `Window.Page` | Access via `this.Window.Page` in ContentPage |
663+| `Application.MainPage` | `Application.Current.Windows[0].Page` | When not in Page context |
664+| `Frame` | `Border` | Frame is deprecated, use Border instead |
665+| `Device.BeginInvokeOnMainThread` | `Dispatcher.Dispatch` or `MainThread.BeginInvokeOnMainThread` | Modern threading APIs |
257666
258−4. **learn-from-pr** - Extracts lessons from PRs and applies improvements to the repository
259− - **Use when**: After complex PR, want to improve instruction files/skills based on lessons learned
260− - **Capabilities**: Analyzes PR, identifies failure modes, applies improvements to instruction files, skills, code comments
261− - **Trigger phrases**: "learn from PR #XXXXX and apply improvements", "improve repo based on what we learned", "update skills based on PR"
262− - **Output**: Applied changes to instruction files, skills, architecture docs, code comments
263− - **Do NOT use for**: Analysis only without applying changes → Use `/learn-from-pr` skill instead
667+**Examples:**
264668
265−5. **release-readiness-agent** - Assesses ship-readiness for a .NET MAUI release branch — both **SR** (`release/*-srN`) and **Preview** (`release/*-previewN`)
266− - **Use when**: A release (SR or Preview) is approaching ship date and you need a synthesized verdict with WorkIQ/MCP enrichment on top of the deterministic report — **or** for a portfolio question across all active releases ("status on releases", "what needs attention across releases") where the user may not know which releases exist
267− - **Capabilities**: Resolves the branch (SR or Preview) from natural language, picks the right script (`Get-ReleaseReadiness.ps1` for SR, `Get-PreviewReadiness.ps1` for Preview), enriches `rejected-from-sr` candidates with WorkIQ context (SR lane), patches `UNKNOWN` ship-check rows via MCP (`maestro_default_channels`, `maestro_builds`), presents an overall verdict
268− - **Trigger phrases**: "is SR7 ready to ship", "release readiness for release/10.0.1xx-sr7", "survey the SR8 branch", "how does net11 preview6 look", "is preview6 ready to cut", "release readiness for release/11.0.1xx-preview6" — **plus portfolio / cross-release questions with no specific release named**: "give me a status on releases", "release status overview", "what's the status across all releases", "what needs attention across releases", "what's next for MAUI releases"
269− - **Output**: Verdict (Ready / Conditionally Ready / Not Ready) + per-candidate classification (SR) or per-section table (Preview) + actionable next steps
270− - **Do NOT use for**: Programmatic / scripted consumers that just need the raw JSON — use the `release-readiness` skill directly. Reviewing a single PR (use **pr**). Running tests manually (use **sandbox-agent**).
669+```csharp
670+// ✅ GOOD: Modern Window API
671+this.Window.Page = new NavigationPage(new MyPage());
271672
272−### Reusable Skills
673+// ❌ BAD: Obsolete Application.MainPage
674+Application.MainPage = new NavigationPage(new MyPage());
273675
274−Skills are modular capabilities that can be invoked directly or used by agents. Located in `.github/skills/`:
676+// ✅ GOOD: Border
677+new Border { Content = new Label { Text = "Hello" } }
275678
276−#### User-Facing Skills
679+// ❌ BAD: Frame (deprecated)
680+new Frame { Content = new Label { Text = "Hello" } }
681+```
277682
278−1. **pr-review** (`.github/skills/pr-review/SKILL.md`)
279− - **Purpose**: End-to-end PR review orchestrator — 3 phases: pr-preflight, try-fix, pr-report. Gate runs separately before this skill via Review-PR.ps1.
280− - **Trigger phrases**: "review PR #XXXXX", "work on PR #XXXXX", "fix issue #XXXXX", "continue PR #XXXXX"
281− - **Capabilities**: Multi-model fix exploration, alternative comparison, PR review recommendation
282− - **Do NOT use for**: Just running tests manually → Use `sandbox-agent`
283− - **Phase instructions** (in `.github/pr-review/`):
284− - `pr-preflight.md` — Context gathering from issue/PR
285− - `pr-report.md` — Final recommendation
286− - **Phase skill**: `try-fix` — Multi-model fix exploration
287− - **Note**: Gate (test verification) runs as a script step in `Review-PR.ps1` before this skill is invoked. Gate result is passed in the prompt.
683+### Use UITest Optimized Controls for Screenshot Tests
288684
289−2. **issue-triage** (`.github/skills/issue-triage/SKILL.md`)
290− - **Purpose**: Query and triage open issues that need milestones, labels, or investigation
291− - **Trigger phrases**: "find issues to triage", "show me old Android issues", "what issues need attention"
292− - **Scripts**: `init-triage-session.ps1`, `query-issues.ps1`, `record-triage.ps1`
685+**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.
293686
294−2. **find-reviewable-pr** (`.github/skills/find-reviewable-pr/SKILL.md`)
295− - **Purpose**: Finds open PRs in dotnet/maui and dotnet/docs-maui that need review
296− - **Trigger phrases**: "find PRs to review", "show milestoned PRs", "find partner PRs"
297− - **Scripts**: `query-reviewable-prs.ps1`
298− - **Categories**: P/0, milestoned, partner, community, recent, docs-maui
687+| Standard Control | UITest Control | Purpose |
688+|------------------|----------------|---------|
689+| `Entry` | `UITestEntry` | Text input without cursor blink |
690+| `Editor` | `UITestEditor` | Multi-line input without cursor blink |
691+| `SearchBar` | `UITestSearchBar` | Search input without cursor blink |
299692
300−3. **pr-finalize** (`.github/skills/pr-finalize/SKILL.md`)
301− - **Purpose**: Verifies PR title and description match actual implementation, AND performs code review for best practices before merge.
302− - **Trigger phrases**: "finalize PR #XXXXX", "check PR description for #XXXXX", "review commit message"
303− - **Used by**: Before merging any PR, when description may be stale
304− - **Note**: Does NOT require agent involvement or session markdown - works on any PR
305− - **🚨 CRITICAL**: NEVER use `--approve` or `--request-changes` - only post comments. Approval is a human decision.
693+**Example:**
306694
307−4. **code-review** (`.github/skills/code-review/SKILL.md`)
308− - **Purpose**: Reviews PR code changes for correctness, safety, and consistency with MAUI conventions. Walks through a MAUI-specific checklist covering handler lifecycle, platform code, safe area, threading, public API, and test patterns.
309− - **Trigger phrases**: "review code for PR #XXXXX", "code review PR #XXXXX", "review this PR's code"
310− - **Note**: Standalone skill — uses independence-first assessment (reads code before PR description to avoid anchoring bias). Can be used by any agent or invoked directly.
311− - **🚨 CRITICAL**: NEVER use `--approve` or `--request-changes` — only post comments. Approval is a human decision.
695+```csharp
696+// For screenshot tests, use UITest controls (UITestEntry, UITestEditor, UITestSearchBar)
697+var entry = new UITestEntry
698+{
699+ Placeholder = "Enter text",
700+ IsCursorVisible = false, // Prevents flaky screenshots
701+ AutomationId = "TestEntry"
702+};
312703
313−5. **learn-from-pr** (`.github/skills/learn-from-pr/SKILL.md`)
314− - **Purpose**: Analyzes completed PR to identify repository improvements (analysis only, no changes applied)
315− - **Trigger phrases**: "what can we learn from PR #XXXXX?", "how can we improve agents based on PR #XXXXX?"
316− - **Used by**: After complex PRs, when agent struggled to find solution
317− - **Output**: Prioritized recommendations for instruction files, skills, code comments
318− - **Note**: For applying changes automatically, use the learn-from-pr agent instead
704+// For non-screenshot tests, standard Entry is fine
705+var entry = new Entry { Placeholder = "Enter text", AutomationId = "TestEntry" };
706+```
319707
320−6. **write-ui-tests** (`.github/skills/write-ui-tests/SKILL.md`)
321− - **Purpose**: Creates UI tests for GitHub issues and verifies they reproduce the bug
322− - **Trigger phrases**: "write UI tests for #XXXXX", "create UI test for issue", "add UI test coverage"
323− - **Output**: Test files that fail without fix, pass with fix
708+**Location:** `src/Controls/tests/TestCases.HostApp/Controls/UITest*.cs`
324709
325−7. **write-xaml-tests** (`.github/skills/write-xaml-tests/SKILL.md`)
326− - **Purpose**: Creates XAML unit tests for XAML parsing, compilation, and source generation
327− - **Trigger phrases**: "write XAML tests for #XXXXX", "test XamlC behavior", "reproduce XAML parsing bug"
328− - **Output**: Test files for Controls.Xaml.UnitTests
710+### Check Similar Tests for Patterns
329711
330−9. **verify-tests-fail-without-fix** (`.github/skills/verify-tests-fail-without-fix/SKILL.md`)
331− - **Purpose**: Verifies tests catch the bug before fix and pass with fix. Auto-detects test type (UI, device, unit, XAML) and dispatches to the appropriate runner.
332− - **Two modes**: Verify failure only (test creation) or full verification (test + fix)
333− - **Used by**: After creating tests, before considering PR complete
712+**Before creating a new test, search for similar tests to reuse patterns:**
334713
335−10. **run-integration-tests** (`.github/skills/run-integration-tests/SKILL.md`)
336− - **Purpose**: Build, pack, and run .NET MAUI integration tests locally
337− - **Trigger phrases**: "run integration tests", "test templates locally", "run macOSTemplates tests", "run RunOniOS tests"
338− - **Categories**: Build, WindowsTemplates, macOSTemplates, Blazor, MultiProject, Samples, AOT, RunOnAndroid, RunOniOS
339− - **Note**: **ALWAYS use this skill** instead of manual `dotnet test` commands for integration tests
714+```bash
715+# Find similar control tests
716+grep -r "class.*Issue.*Button" src/Controls/tests/TestCases.HostApp/Issues/*.cs
340717
341−11. **dependency-flow** (`.github/skills/dependency-flow/SKILL.md`)
342− - **Purpose**: MAUI-specific dependency flow rules, channel conventions, and feed lookup workflows
343− - **Trigger phrases**: "feeds for .NET MAUI X.Y.Z", "where is MAUI build", "promote build to public feed", "what channels is MAUI on", "subscription health for MAUI"
344− - **Wraps**: `maestro-cli` skill (from `dotnet-dnceng@dotnet-arcade-skills` plugin) and maestro MCP tools
345− - **Note**: Provides MAUI-specific guardrails on top of core Maestro/darc operations — channel naming, safety deny-list, input validation, and prompt injection defense
718+# Find Shell tests
719+grep -r "TestShell" src/Controls/tests/TestCases.HostApp/Issues/*.cs
346720
347−12. **release-readiness** (`.github/skills/release-readiness/SKILL.md`)
348− - **Purpose**: Deterministic ship-readiness engine for .NET MAUI release branches — both **SR** (`release/*-srN`) and **Preview** (`release/*-previewN`). Surveys CI, computes what's actually shipping, classifies open regressions, identifies port candidates and rejected backports
349− - **Trigger phrases**: "release readiness for SRN", "is SR7 ready to ship", "survey the SR branch", "release readiness for preview6", "how does preview6 look (deterministic)", "status across all releases" (reads the live `[Release Readiness]` tracker issues by body marker — no survey re-run needed)
350− - **Scripts**: `Get-ReleaseReadiness.ps1` (SR lane), `Get-PreviewReadiness.ps1` (Preview lane), `Find-ReleaseReadinessTrackers.ps1` (tracker discovery)
351− - **Output**: JSON + Markdown report, list of source PRs, classification of regression issues (in-sr-active, rejected-from-sr, no-fix-yet, etc.)
352− - **Note**: Deterministic and reproducible — no MCP, no LLM judgment. Use **this skill directly** when you need raw output for a script, dashboard, cron job, or programmatic consumer. For natural-language verdict synthesis with WorkIQ enrichment, use the **`release-readiness-agent`** instead.
721+# Find tests for specific control
722+grep -r "CollectionView" src/Controls/tests/TestCases.HostApp/Issues/*.cs
353723
354−#### Internal Skills (Used by Agents)
724+# Find tests using UITest optimized controls
725+grep -r "UITestEntry\|UITestEditor\|UITestSearchBar" src/Controls/tests/TestCases.HostApp/Issues/*.cs
726+```
355727
356−13. **try-fix** (`.github/skills/try-fix/SKILL.md`)
357− - **Purpose**: Proposes ONE independent fix approach, applies it, tests, records result with failure analysis, then reverts
358− - **Used by**: pr agent Phase 3 (Fix phase) - rarely invoked directly by users
359− - **Behavior**: Reads prior attempts to learn from failures. Max 5 attempts per session.
360− - **Output**: Updates session markdown with attempt results and failure analysis
728+**Reuse established patterns:**
729+- AutomationId naming conventions
730+- Test structure and layout
731+- Common helper methods
732+- Platform-specific workarounds
733+- UITest optimized control usage
361734
362−### Using Custom Agents
735+### Safe Area Testing (iOS/MacCatalyst)
363736
364−**Delegation Policy**: When user request matches agent trigger phrases, **ALWAYS delegate to the appropriate agent immediately**. Do not ask for permission or explain alternatives unless the request is ambiguous.
737+**⚠️ CRITICAL for macCatalyst safe area tests:**
365738
366−**Examples of correct delegation**:
367−- User: "Review PR #12345" → Immediately invoke **pr** agent
368−- User: "Test this PR" → Immediately invoke **sandbox-agent**
369−- User: "Fix issue #67890" (no PR exists) → Suggest using `/delegate` command
370−- User: "Write tests for issue #12345" → Immediately invoke **write-tests-agent**
371−- User: "Is SR7 ready to ship?" → Immediately invoke **release-readiness-agent**
372−- User: "How does net11 preview6 look?" → Immediately invoke **release-readiness-agent**
373−- User: "Give me a status on releases / what needs attention across releases?" → Immediately invoke **release-readiness-agent** (portfolio mode — it enumerates active releases by reading the `[Release Readiness]` tracker issues; don't ask "which release?")
374−- User: "Give me the raw release-readiness JSON for SR8" → Use the **release-readiness** skill directly (no enrichment needed)
739+Safe area behavior differs significantly between macOS versions. Tests must account for this variability.
375740
376−**When NOT to delegate**:
377−- User asks "What does PR #12345 do?" → Informational query, handle yourself
378−- User asks "How do I test PRs?" → Documentation query, handle yourself
379−- User has follow-up questions after agent completes → Continue the conversation yourself
741+| macOS Version | Title Bar Safe Area | CI Environment |
742+|---------------|---------------------|----------------|
743+| **macOS 14/15** | ~28px top inset | ✅ Used by CI |
744+| **macOS 26 (Liquid Glass)** | ~0px top inset | ❌ Local dev only |
745+
746+**Rules for safe area tests:**
747+
748+1. **Use tolerances for safe area measurements** - Exact pixel values vary by macOS version
749+2. **Test behavior, not exact values** - Verify content is NOT obscured, rather than checking exact padding pixels
750+3. **Use `GetRect()` for child content position** - Measure where content actually appears, not parent size
751+4. **Never hardcode safe area expectations** - Tests should pass on macOS 14/15 AND macOS 26
752+
753+**Example patterns:**
754+
755+```csharp
756+// ❌ BAD: Hardcoded safe area value (breaks across macOS versions)
757+var safeArea = element.GetRect();
758+Assert.That(safeArea.Y, Is.EqualTo(28)); // Fails on macOS 26
759+
760+// ✅ GOOD: Test that content is not obscured by title bar
761+var contentRect = App.WaitForElement("MyContent").GetRect();
762+var titleBarRect = App.WaitForElement("TitleBar").GetRect();
763+Assert.That(contentRect.Y, Is.GreaterThanOrEqualTo(titleBarRect.Height),
764+ "Content should not be obscured by title bar");
765+
766+// ✅ GOOD: Use tolerance for safe area (accounts for OS differences)
767+Assert.That(contentRect.Y, Is.GreaterThan(0).And.LessThan(50),
768+ "Content should have some top padding but not excessive");
769+```
770+
771+**Test category**: Use `UITestCategories.SafeAreaEdges` for safe area tests.
772+
773+**Platform scope**: Safe area tests should typically run on iOS and MacCatalyst (not just one).
774+
775+**See also**: `.github/instructions/safe-area-debugging.instructions.md` for investigation guidelines
776+
