diff --git a/.github/instructions/uitests.instructions.md b/.github/instructions/uitests.instructions.md
index f69bcb4f4aee..b8eb01bab8f3 100644
--- a/.github/instructions/uitests.instructions.md
+++ b/.github/instructions/uitests.instructions.md
@@ -523,6 +523,17 @@ cat /tmp/ios_crash.log | grep -A 20 -B 5 "Exception"
5. **Check for platform-specific issues** - iOS version compatibility, permissions, etc.
6. If you can't determine the fix, **ask for guidance** with the full exception details
+### Dangerous System Commands (Never Run)
+
+**🚨 NEVER run these commands — they cause destructive system-wide side effects:**
+
+- **`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.
+- **`csrutil disable`** — Disables System Integrity Protection
+- **`networksetup`** — Modifies network configuration
+- **`defaults delete`** on system domains — Resets system preferences
+
+**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.
+
## Before Committing
Verify the following checklist before committing UI tests:
diff --git a/.github/scripts/BuildAndRunHostApp.ps1 b/.github/scripts/BuildAndRunHostApp.ps1
index c6103609de46..60d15963eb66 100644
--- a/.github/scripts/BuildAndRunHostApp.ps1
+++ b/.github/scripts/BuildAndRunHostApp.ps1
@@ -260,7 +260,9 @@ if ($Platform -eq "catalyst") {
& chmod +x $executablePath
}
- Write-Success "MacCatalyst app prepared (Appium will launch with test name)"
+ # Set MAC_APP_PATH so Appium mac2 driver can launch the app directly
+ $env:MAC_APP_PATH = $appPath
+ Write-Success "MacCatalyst app prepared (MAC_APP_PATH=$appPath)"
} else {
Write-Warn "MacCatalyst app not found at: $appPath"
Write-Warn "Test may use wrong app bundle if another version is registered"
diff --git a/.github/scripts/shared/Start-Emulator.ps1 b/.github/scripts/shared/Start-Emulator.ps1
index 9fd9d227331a..b842d840a6cc 100644
--- a/.github/scripts/shared/Start-Emulator.ps1
+++ b/.github/scripts/shared/Start-Emulator.ps1
@@ -6,7 +6,7 @@
.DESCRIPTION
Handles device detection and startup for both Android and iOS platforms.
- Android: Automatically selects and starts emulator with priority: API 30 Nexus > API 30 > Nexus > First available
- - iOS: Automatically selects iPhone Xs with iOS 18.5 by default
+ - iOS: Automatically selects iPhone Xs with iOS 18.x (or iPhone 11 Pro with iOS 26.x) to match CI
.PARAMETER Platform
Target platform: "android" or "ios"
@@ -344,10 +344,17 @@ if ($Platform -eq "android") {
Write-Info "Auto-detecting iOS simulator..."
$simList = xcrun simctl list devices available --json | ConvertFrom-Json
- # Preferred devices in order of priority
- $preferredDevices = @("iPhone 16 Pro", "iPhone 15 Pro", "iPhone 14 Pro", "iPhone Xs")
# Preferred iOS versions in order (stable preferred, beta fallback)
$preferredVersions = @("iOS-18", "iOS-17", "iOS-26")
+ # Preferred devices per iOS version to match CI configuration:
+ # iOS 18.x → iPhone Xs (matches CI default in UITest.cs)
+ # iOS 26.x → iPhone 11 Pro (matches CI visual test requirement)
+ # iOS 17.x → iPhone Xs (fallback)
+ $preferredDevicesPerVersion = @{
+ "iOS-18" = @("iPhone Xs", "iPhone 16 Pro", "iPhone 15 Pro", "iPhone 14 Pro")
+ "iOS-17" = @("iPhone Xs", "iPhone 15 Pro", "iPhone 14 Pro")
+ "iOS-26" = @("iPhone 11 Pro", "iPhone 16 Pro", "iPhone 15 Pro")
+ }
$selectedDevice = $null
$selectedVersion = $null
@@ -356,13 +363,16 @@ if ($Platform -eq "android") {
foreach ($version in $preferredVersions) {
if ($selectedDevice) { break }
- # Get all runtimes matching this version prefix
+ # Get all runtimes matching this version prefix, sorted by version descending
+ # so the latest minor version is preferred (e.g., iOS-18-5 before iOS-18-3)
$matchingRuntimes = $simList.devices.PSObject.Properties |
- Where-Object { $_.Name -match $version }
+ Where-Object { $_.Name -match $version } |
+ Sort-Object { $_.Name } -Descending
if ($matchingRuntimes) {
- # Try each preferred device
- foreach ($deviceName in $preferredDevices) {
+ # Try each preferred device for this version
+ $devicesForVersion = if ($preferredDevicesPerVersion.ContainsKey($version)) { $preferredDevicesPerVersion[$version] } else { @("iPhone Xs", "iPhone 16 Pro") }
+ foreach ($deviceName in $devicesForVersion) {
$device = $null
$deviceRuntime = $null
foreach ($rt in $matchingRuntimes) {
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue16910.xaml b/src/Controls/tests/TestCases.HostApp/Issues/Issue16910.xaml
index 0e44acbe1f7e..b790faef3092 100644
--- a/src/Controls/tests/TestCases.HostApp/Issues/Issue16910.xaml
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue16910.xaml
@@ -2,20 +2,35 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue16910.xaml.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue16910.xaml.cs
index 5cb7595561d3..101a753825d9 100644
--- a/src/Controls/tests/TestCases.HostApp/Issues/Issue16910.xaml.cs
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue16910.xaml.cs
@@ -1,6 +1,4 @@
-using System.Collections;
-
-namespace Maui.Controls.Sample.Issues
+namespace Maui.Controls.Sample.Issues
{
[Issue(IssueTracker.Github, 16910, "IsRefreshing binding works", PlatformAffected.All)]
public partial class Issue16910 : ContentPage
@@ -10,8 +8,6 @@ public partial class Issue16910 : ContentPage
bool _isRefreshing;
- public IEnumerable ItemSource { get; set; }
-
public bool IsRefreshing
{
get => _isRefreshing;
@@ -45,15 +41,9 @@ public Issue16910()
{
InitializeComponent();
UpdateRefreshingLabels();
- ItemSource =
- Enumerable.Range(0, 100)
- .Select(x => new { Text = $"Item {x}", AutomationId = $"Item{x}" })
- .ToList();
-
- this.BindingContext = this;
+ BindingContext = this;
}
-
void OnStopRefreshClicked(object sender, EventArgs e)
{
refreshView.IsRefreshing = false;
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue1905.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue1905.cs
index 3f1865c68c71..561f338634f6 100644
--- a/src/Controls/tests/TestCases.HostApp/Issues/Issue1905.cs
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue1905.cs
@@ -1,50 +1,81 @@
-using Microsoft.Maui.Controls.PlatformConfiguration;
+using Microsoft.Maui.Controls.PlatformConfiguration;
using Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;
using ListView = Microsoft.Maui.Controls.ListView;
namespace Maui.Controls.Sample.Issues
{
+[Issue(IssueTracker.Github, 1905, "Pull to refresh doesn't work if iOS 11 large titles is enabled", PlatformAffected.iOS)]
+public class Issue1905LargeTitles : TestNavigationPage
+{
+protected override void Init()
+{
+// Large titles is the specific trigger for this bug
+On().SetPrefersLargeTitles(true);
+
+var statusLabel = new Label
+{
+Text = "Ready",
+AutomationId = "TestResult",
+FontSize = 20,
+};
+
+bool refreshCompleted = false;
+
+var items = new List();
+for (int i = 0; i < 20; i++)
+{
+items.Add($"pull to {i}");
+}
+
+var lst = new ListView
+{
+IsPullToRefreshEnabled = true,
+ItemsSource = items,
+};
+
+lst.RefreshCommand = new Command(async () =>
+{
+await Task.Delay(500);
+lst.ItemsSource = new List { "data refreshed" };
+lst.EndRefresh();
+refreshCompleted = true;
+});
+
+var runButton = new Button
+{
+Text = "Run Test",
+AutomationId = "RunTest",
+};
+
+runButton.Clicked += async (s, e) =>
+{
+statusLabel.Text = "Running...";
+refreshCompleted = false;
+
+// BeginRefresh programmatically — this is what fails with large titles
+lst.BeginRefresh();
+
+// Wait for refresh to complete (max 10s)
+for (int i = 0; i < 20; i++)
+{
+await Task.Delay(500);
+if (refreshCompleted)
+break;
+}
+
+statusLabel.Text = refreshCompleted ? "SUCCESS" : "FAIL: refresh did not complete";
+};
+
+var page = new ContentPage
+{
+Title = "Pull Large Titles",
+Content = new VerticalStackLayout
+{
+Children = { runButton, statusLabel, lst }
+}
+};
- [Issue(IssueTracker.Github, 1905, "Pull to refresh doesn't work if iOS 11 large titles is enabled", PlatformAffected.iOS)]
- public class Issue1905LargeTitles : TestNavigationPage
- {
- protected override void Init()
- {
- On().SetPrefersLargeTitles(true);
- var items = new List();
- for (int i = 0; i < 1000; i++)
- {
- items.Add($"pull to {DateTime.Now.Ticks}");
- }
- var page = new ContentPage
- {
- Title = "Pull Large Titles"
- };
-
- var lst = new ListView();
- lst.IsPullToRefreshEnabled = true;
- lst.ItemsSource = items;
- lst.RefreshCommand = new Command(async () =>
- {
- var newitems = new List();
- newitems.Add("data refreshed");
- await Task.Delay(5000);
- for (int i = 0; i < 1000; i++)
- {
- newitems.Add($"data {DateTime.Now.Ticks} refreshed");
- }
- lst.ItemsSource = newitems;
- lst.EndRefresh();
- });
- page.Content = new StackLayout { Children = { lst } };
- page.Appearing += async (sender, e) =>
- {
- await Task.Delay(500);
- lst.BeginRefresh();
- };
- page.ToolbarItems.Add(new ToolbarItem { Text = "Refresh", Command = new Command((obj) => lst.BeginRefresh()), AutomationId = "btnRefresh" });
- Navigation.PushAsync(page);
-
- }
- }
-}
\ No newline at end of file
+Navigation.PushAsync(page);
+}
+}
+}
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue3001.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue3001.cs
index a8ce7368285e..3a4d07e8a565 100644
--- a/src/Controls/tests/TestCases.HostApp/Issues/Issue3001.cs
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue3001.cs
@@ -1,66 +1,67 @@
-namespace Maui.Controls.Sample.Issues
+namespace Maui.Controls.Sample.Issues
{
+[Issue(IssueTracker.Github, 3001, "[macOS] Navigating back from a complex page is highly inefficient", PlatformAffected.macOS)]
+public class Issue3001 : TestContentPage
+{
+const string ButtonId = "ClearButton";
+const string ReadyId = "ReadyLabel";
- [Issue(IssueTracker.Github, 3001, "[macOS] Navigating back from a complex page is highly inefficient", PlatformAffected.macOS)]
- public class Issue3001 : TestContentPage
- {
- const string ButtonId = "ClearButton";
- const string ReadyId = "ReadyLabel";
-
- int _counter = 0;
- int _level = 0;
- const int maxLevel = 5;
+int _counter = 0;
+int _level = 0;
+// Reduced from 5 to 4 levels: 4^4 = 256 labels instead of 4^5 = 1024
+// Still deep enough to exercise nested disposal, but avoids catalyst timeout
+const int maxLevel = 4;
- public View BuildLevel()
- {
- if (_level == maxLevel)
- {
- _counter++;
- return new Label { Text = _counter.ToString(), FontSize = 10 };
- }
+public View BuildLevel()
+{
+if (_level == maxLevel)
+{
+_counter++;
+return new Label { Text = _counter.ToString(), FontSize = 10 };
+}
- _level++;
- var g1 = new Grid { RowSpacing = 0, ColumnSpacing = 0 };
+_level++;
+var g1 = new Grid { RowSpacing = 0, ColumnSpacing = 0 };
- var g2 = new Grid { RowSpacing = 0, ColumnSpacing = 0 };
- g1.Children.Add(g2);
+var g2 = new Grid { RowSpacing = 0, ColumnSpacing = 0 };
+g1.Children.Add(g2);
- var g = new Grid { RowSpacing = 0, ColumnSpacing = 0 };
- g2.Children.Add(g);
+var g = new Grid { RowSpacing = 0, ColumnSpacing = 0 };
+g2.Children.Add(g);
- g.RowDefinitions.Add(new RowDefinition { Height = GridLength.Star });
- g.RowDefinitions.Add(new RowDefinition { Height = GridLength.Star });
- g.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Star });
- g.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Star });
+g.RowDefinitions.Add(new RowDefinition { Height = GridLength.Star });
+g.RowDefinitions.Add(new RowDefinition { Height = GridLength.Star });
+g.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Star });
+g.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Star });
- g.Add(BuildLevel(), 0, 0);
- g.Add(BuildLevel(), 0, 1);
- g.Add(BuildLevel(), 1, 0);
- g.Add(BuildLevel(), 1, 1);
+g.Add(BuildLevel(), 0, 0);
+g.Add(BuildLevel(), 0, 1);
+g.Add(BuildLevel(), 1, 0);
+g.Add(BuildLevel(), 1, 1);
- _level--;
+_level--;
- return g1;
- }
+return g1;
+}
- protected override void Init()
- {
- var sp = new StackLayout();
- sp.Children.Add(new Button
- {
- Text = "Start",
- AutomationId = ButtonId,
- Command = new Command(() =>
- {
- Content = new Label
- {
- Text = "Ready",
- AutomationId = ReadyId
- };
- })
- });
- sp.Children.Add(BuildLevel());
- Content = sp;
- }
- }
+protected override void Init()
+{
+var sp = new StackLayout();
+sp.Children.Add(new Button
+{
+Text = "Start",
+AutomationId = ButtonId,
+Command = new Command(() =>
+{
+Content = new Label
+{
+Text = "Ready",
+AutomationId = ReadyId
+};
+})
+});
+sp.Children.Add(BuildLevel());
+Content = sp;
+}
+}
}
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue3275.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue3275.cs
index 479a766af525..7cd7c65bfa28 100644
--- a/src/Controls/tests/TestCases.HostApp/Issues/Issue3275.cs
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue3275.cs
@@ -1,9 +1,8 @@
-using System.Collections.ObjectModel;
+using System.Collections.ObjectModel;
using System.Windows.Input;
namespace Maui.Controls.Sample.Issues
{
-
[Issue(IssueTracker.Github, 3275, "For ListView in Recycle mode ScrollTo causes cell leak and in some cases NRE", PlatformAffected.iOS)]
public class Issue3275 : NavigationPage
{
@@ -14,11 +13,16 @@ public Issue3275() : base(new MainPage())
public class MainPage : ContentPage
{
static readonly string BtnLeakId = "btnLeak";
- static readonly string BtnScrollToId = "btnScrollTo";
+ Label _statusLabel;
public MainPage()
{
- var layout = new StackLayout();
+ _statusLabel = new Label
+ {
+ Text = "Ready",
+ AutomationId = "TestResult",
+ FontSize = 20,
+ };
var btn = new Button
{
@@ -26,189 +30,109 @@ public MainPage()
AutomationId = BtnLeakId,
Command = new Command(() =>
{
- Navigation.PushAsync(new Issue3275TransactionsPage1());
+ _statusLabel.Text = "Navigated";
+ Navigation.PushAsync(new TransactionsPage());
})
};
- layout.Children.Add(btn);
- Content = layout;
- }
-
-
- public class Issue3275TransactionsPage1 : ContentPage
- {
- private readonly TransactionsViewModel _viewModel = new TransactionsViewModel();
- FastListView _transactionsListView;
-
- public Issue3275TransactionsPage1()
+ Content = new VerticalStackLayout
{
- var grd = new Grid();
- grd.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
- grd.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
- grd.RowDefinitions.Add(new RowDefinition());
- _transactionsListView = new FastListView
- {
- HasUnevenRows = true,
- ItemTemplate = new DataTemplate(() =>
- {
- var viewCell = new ViewCell();
- var item = new MenuItem
- {
- Text = "test"
- };
- item.SetBinding(MenuItem.CommandProperty, new Binding("BindingContext.RepeatCommand", source: _transactionsListView));
- item.SetBinding(MenuItem.CommandParameterProperty, new Binding("."));
- viewCell.ContextActions.Add(item);
- var lbl = new Label();
- lbl.SetBinding(Label.TextProperty, "Name");
- viewCell.View = lbl;
- return viewCell;
- })
- };
- _transactionsListView.SetBinding(ListView.ItemsSourceProperty, "Items");
-
- grd.Children.Add(new Label
- {
- Text = "Click 'Scroll To' and go back"
- });
-
- var btn = new Button
- {
- Text = "Scroll to",
- AutomationId = BtnScrollToId,
- Command = new Command(() =>
- {
- var item = _viewModel.Items.Skip(250).First();
- _transactionsListView.ScrollTo(item, ScrollToPosition.MakeVisible, false);
- })
- };
-
- Grid.SetRow(btn, 1);
- grd.Children.Add(btn);
- Grid.SetRow(_transactionsListView, 2);
- grd.Children.Add(_transactionsListView);
-
- Content = grd;
-
-
- BindingContext = _viewModel;
- }
-
- protected override void OnDisappearing()
- {
- BindingContext = null; // IMPORTANT!!! Prism.Forms does this under the hood
- }
+ Spacing = 10,
+ Padding = 20,
+ Children = { btn, _statusLabel }
+ };
}
- public sealed class FastListView : ListView
+ protected override void OnAppearing()
{
- public FastListView() : base(ListViewCachingStrategy.RecycleElement)
+ base.OnAppearing();
+ // If we returned from the TransactionsPage without crashing, the test passed.
+ // The original bug causes an NRE crash during back navigation when
+ // OnDisappearing nulls BindingContext on recycled cells with ContextActions.
+ if (_statusLabel.Text == "Navigated")
{
+ _statusLabel.Text = "SUCCESS";
}
}
+ }
- public class TransactionsViewModel
- {
- public TransactionsViewModel()
- {
- var items = Enumerable.Range(1, 500).Select(i => new Item { Name = i.ToString() });
-
- Items = new ObservableCollection- (items);
-
- RepeatCommand = new AsyncDelegateCommand