diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index b8f6e9a890a0..d0d99c2b8373 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -138,6 +138,26 @@ When working with public API changes: - **Use `dotnet format analyzers`** if having trouble - **If files are incorrect**: Revert all changes, then add only the necessary new API entries +**🚨 CRITICAL: `#nullable enable` must be line 1** + +Every `PublicAPI.Unshipped.txt` file starts with `#nullable enable` (often BOM-prefixed: `ο»Ώ#nullable enable`) on the **first line**. If this line is moved or removed, the analyzer treats it as a declared API symbol and emits **RS0017** errors. + +**Never sort these files with plain `sort`** β€” the BOM bytes (`0xEF 0xBB 0xBF`) sort after ASCII characters under `LC_ALL=C`, pushing `#nullable enable` to the bottom of the file. + +When resolving merge conflicts or adding entries, use this safe pattern that preserves line 1: +```bash +for f in $(git diff --name-only --diff-filter=U | grep "PublicAPI.Unshipped.txt"); do + # Extract and preserve the #nullable enable line (with or without BOM) + HEADER=$(head -1 "$f" | grep -o '.*#nullable enable' || echo '#nullable enable') + # Strip conflict markers, remove all #nullable lines, sort+dedup the API entries + grep -v '^<<<<<<\|^======\|^>>>>>>\|#nullable enable' "$f" | LC_ALL=C sort -u | sed '/^$/d' > /tmp/api_fix.txt + # Reassemble: header first, then sorted entries + printf '%s\n' "$HEADER" > "$f" + cat /tmp/api_fix.txt >> "$f" + git add "$f" +done +``` + ### Branching - `main` - For bug fixes without API changes - `net10.0` - For new features and API changes @@ -181,10 +201,17 @@ git commit -m "Fix: Description of the change" 2. Exception: If the user's instructions explicitly include pushing, proceed without asking. ### Documentation + - Update XML documentation for public APIs - Follow existing code documentation patterns - Update relevant docs in `docs/` folder when needed +**Platform-Specific Documentation:** +- `.github/instructions/safe-area-ios.instructions.md` - Safe area investigation (iOS/macCatalyst) +- `.github/instructions/uitests.instructions.md` - UI test guidelines (includes safe area testing section) +- `.github/instructions/android.instructions.md` - Android handler implementation +- `.github/instructions/xaml-unittests.instructions.md` - XAML unit test guidelines + ### Opening PRs All PRs are required to have this at the top of the description: diff --git a/.github/instructions/safe-area-ios.instructions.md b/.github/instructions/safe-area-ios.instructions.md new file mode 100644 index 000000000000..0ee81f2c462f --- /dev/null +++ b/.github/instructions/safe-area-ios.instructions.md @@ -0,0 +1,34 @@ +--- +applyTo: + - "**/Platform/iOS/MauiView.cs" + - "**/Platform/iOS/MauiScrollView.cs" + - "**/Platform/iOS/*SafeArea*" +--- + +# Safe Area Guidelines (iOS/macCatalyst) + +## Platform Differences + +| | macOS 14/15 | macOS 26+ | +|-|-------------|-----------| +| Title bar inset | ~28px | ~0px | +| Used in CI | βœ… | ❌ | + +Local macOS 26+ testing does NOT validate CI behavior. Fixes must pass CI on macOS 14/15. + +| Platform | `UseSafeArea` default | +|----------|-----------------------| +| iOS | `false` | +| macCatalyst | `true` | + +## Architecture (PR #34024) + +**`IsParentHandlingSafeArea`** β€” before applying adjustments, `MauiView`/`MauiScrollView` walk ancestors to check if any ancestor handles the **same edges**. If so, descendant skips (avoids double-padding). Edge-aware: parent handling `Top` does not block child handling `Bottom`. Result cached in `bool? _parentHandlesSafeArea`; cleared on `SafeAreaInsetsDidChange`, `InvalidateSafeArea`, `MovedToWindow`. `AppliesSafeAreaAdjustments` is `internal` for cross-type ancestor checks. + +**`EqualsAtPixelLevel`** β€” safe area compared at device-pixel resolution to absorb sub-pixel animation noise (`0.0000001pt` during `TranslateToAsync`), preventing oscillation loops (#32586, #33934). + +## Anti-Patterns + +**❌ Window Guard** β€” comparing `Window.SafeAreaInsets` to filter callbacks blocks legitimate updates. On macCatalyst + custom TitleBar, `WindowViewController` pushes content down, changing the **view's** `SafeAreaInsets` without changing the **window's**. Caused 28px CI shift (macOS 14/15 only). Never gate per-view callbacks on window-level insets. + +**❌ Semantic mismatch** β€” `_safeArea` is filtered by `GetSafeAreaForEdge` (zeroes edges per `SafeAreaRegions`); raw `UIView.SafeAreaInsets` includes all edges. Never compare them β€” compare raw-to-raw or adjusted-to-adjusted. diff --git a/.github/instructions/uitests.instructions.md b/.github/instructions/uitests.instructions.md index b8eb01bab8f3..3b0e5c7747d7 100644 --- a/.github/instructions/uitests.instructions.md +++ b/.github/instructions/uitests.instructions.md @@ -731,3 +731,45 @@ grep -r "UITestEntry\|UITestEditor\|UITestSearchBar" src/Controls/tests/TestCase - Common helper methods - Platform-specific workarounds - UITest optimized control usage + +### Safe Area Testing (iOS/MacCatalyst) + +**⚠️ CRITICAL for macCatalyst safe area tests:** + +Safe area behavior differs significantly between macOS versions. Tests must account for this variability. + +| macOS Version | Title Bar Safe Area | CI Environment | +|---------------|---------------------|----------------| +| **macOS 14/15** | ~28px top inset | βœ… Used by CI | +| **macOS 26 (Liquid Glass)** | ~0px top inset | ❌ Local dev only | + +**Rules for safe area tests:** + +1. **Use tolerances for safe area measurements** - Exact pixel values vary by macOS version +2. **Test behavior, not exact values** - Verify content is NOT obscured, rather than checking exact padding pixels +3. **Use `GetRect()` for child content position** - Measure where content actually appears, not parent size +4. **Never hardcode safe area expectations** - Tests should pass on macOS 14/15 AND macOS 26 + +**Example patterns:** + +```csharp +// ❌ BAD: Hardcoded safe area value (breaks across macOS versions) +var safeArea = element.GetRect(); +Assert.That(safeArea.Y, Is.EqualTo(28)); // Fails on macOS 26 + +// βœ… GOOD: Test that content is not obscured by title bar +var contentRect = App.WaitForElement("MyContent").GetRect(); +var titleBarRect = App.WaitForElement("TitleBar").GetRect(); +Assert.That(contentRect.Y, Is.GreaterThanOrEqualTo(titleBarRect.Height), + "Content should not be obscured by title bar"); + +// βœ… GOOD: Use tolerance for safe area (accounts for OS differences) +Assert.That(contentRect.Y, Is.GreaterThan(0).And.LessThan(50), + "Content should have some top padding but not excessive"); +``` + +**Test category**: Use `UITestCategories.SafeAreaEdges` for safe area tests. + +**Platform scope**: Safe area tests should typically run on iOS and MacCatalyst (not just one). + +**See also**: `.github/instructions/safe-area-debugging.instructions.md` for investigation guidelines diff --git a/eng/pipelines/ci-device-tests.yml b/eng/pipelines/ci-device-tests.yml index 1547831fb642..d33cf5b4e7e4 100644 --- a/eng/pipelines/ci-device-tests.yml +++ b/eng/pipelines/ci-device-tests.yml @@ -5,6 +5,7 @@ trigger: - release/* - net*.0 - inflight/* + - darc-* tags: include: - '*' diff --git a/eng/pipelines/ci-uitests.yml b/eng/pipelines/ci-uitests.yml index c3b6ace38f43..7e16e1733d75 100644 --- a/eng/pipelines/ci-uitests.yml +++ b/eng/pipelines/ci-uitests.yml @@ -5,6 +5,7 @@ trigger: - release/* - net*.0 - inflight/* + - darc-* tags: include: - '*' diff --git a/eng/pipelines/ci.yml b/eng/pipelines/ci.yml index 58b1ba5dbb67..bc962be9a192 100644 --- a/eng/pipelines/ci.yml +++ b/eng/pipelines/ci.yml @@ -281,7 +281,25 @@ stages: timeout: 120 testCategory: MultiProject - # TODO: macOSTemplates and AOT template categories + # TODO: macOSTemplates category + + - name: win_aot_tests + ${{ if eq(variables['Build.DefinitionName'], 'maui-pr') }}: + pool: ${{ parameters.WindowsPool.public }} + runAsPublic: true + ${{ else }}: + pool: ${{ parameters.WindowsPool.internal }} + runAsPublic: false + timeout: 120 + testCategory: AOT + - name: mac_aot_tests + ${{ if eq(variables['Build.DefinitionName'], 'maui-pr') }}: + pool: ${{ parameters.MacOSPool.public }} + ${{ else }}: + pool: ${{ parameters.MacOSPool.internal }} + timeout: 240 + testCategory: AOT + - name: mac_runandroid_tests ${{ if eq(variables['Build.DefinitionName'], 'maui-pr') }}: pool: ${{ parameters.AndroidPoolLinux }} diff --git a/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.targets b/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.targets index 6eee58090437..0059c276f1a6 100644 --- a/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.targets +++ b/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.targets @@ -358,6 +358,20 @@ + + + + + + + false diff --git a/src/Controls/src/Core/Platform/Android/MultiPageFragmentStateAdapter.cs b/src/Controls/src/Core/Platform/Android/MultiPageFragmentStateAdapter.cs index d2d1ce913f74..9d93a163817f 100644 --- a/src/Controls/src/Core/Platform/Android/MultiPageFragmentStateAdapter.cs +++ b/src/Controls/src/Core/Platform/Android/MultiPageFragmentStateAdapter.cs @@ -7,11 +7,7 @@ namespace Microsoft.Maui.Controls.Platform { - internal class MultiPageFragmentStateAdapter<[DynamicallyAccessedMembers(BindableProperty.DeclaringTypeMembers -#if NET8_0 // IL2091 - | BindableProperty.ReturnTypeMembers -#endif - )] T> : FragmentStateAdapter where T : Page + internal class MultiPageFragmentStateAdapter<[DynamicallyAccessedMembers(BindableProperty.DeclaringTypeMembers | BindableProperty.ReturnTypeMembers)] T> : FragmentStateAdapter where T : Page { MultiPage _page; readonly IMauiContext _context; diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/LabelNotTruncatedWithMaxLines.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/LabelNotTruncatedWithMaxLines.png new file mode 100644 index 000000000000..dbeca9afe4c7 Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/LabelNotTruncatedWithMaxLines.png differ diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue28986_ParentChildTest.xaml b/src/Controls/tests/TestCases.HostApp/Issues/Issue28986_ParentChildTest.xaml new file mode 100644 index 000000000000..f3bfdc2b3a4e --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue28986_ParentChildTest.xaml @@ -0,0 +1,84 @@ + + + + + + + + + + + + +