From 1ce1efe3b1e2e6dd30fec6007ac6623e7ba02291 Mon Sep 17 00:00:00 2001 From: Vally Fixture Date: Mon, 3 Aug 2026 19:02:50 +0200 Subject: [PATCH 01/27] De-flake DragEvents UI test event assertions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9be49656-7117-4235-9d96-404779ab6b16 --- .../Tests/DragAndDropUITests.cs | 55 ++++--------------- 1 file changed, 12 insertions(+), 43 deletions(-) diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/DragAndDropUITests.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/DragAndDropUITests.cs index 60a2ea803955..46a4b7717232 100644 --- a/src/Controls/tests/TestCases.Shared.Tests/Tests/DragAndDropUITests.cs +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/DragAndDropUITests.cs @@ -33,50 +33,19 @@ public void DragEvents() App.WaitForElement("LabelDragElement"); App.DragAndDrop("LabelDragElement", "DragTarget"); - App.WaitForElement("DragStartEventsLabel"); - var textAfterDragStart = App.FindElement("DragStartEventsLabel").GetText(); - - if (string.IsNullOrEmpty(textAfterDragStart)) - { - Assert.Fail("Text was expected: Drag start event"); - } - else - { - Assert.That(textAfterDragStart, Is.EqualTo("DragStarting")); - } - - App.WaitForElement("DragOverEventsLabel"); - var textAfterDragOver = App.FindElement("DragOverEventsLabel").GetText(); - if (string.IsNullOrEmpty(textAfterDragOver)) - { - Assert.Fail("Text was expected: Drag over event"); - } - else - { - Assert.That(textAfterDragOver, Is.EqualTo("DragOver")); - } - - App.WaitForElement("DragCompletedEventsLabel"); - var textAfterDragComplete = App.FindElement("DragCompletedEventsLabel").GetText(); - if (string.IsNullOrEmpty(textAfterDragComplete)) - { - Assert.Fail("Text was expected: Drag complete event"); - } - else - { - Assert.That(textAfterDragComplete, Is.EqualTo("DropCompleted")); - } + AssertEventText("DragStartEventsLabel", "DragStarting"); + AssertEventText("DragOverEventsLabel", "DragOver"); + AssertEventText("DragCompletedEventsLabel", "DropCompleted"); + AssertEventText("DropEventsLabel", "Drop"); + } - App.WaitForElement("DropEventsLabel"); - var textAfterDrop = App.FindElement("DropEventsLabel").GetText(); - if (string.IsNullOrEmpty(textAfterDrop)) - { - Assert.Fail("Text was expected: Drop event"); - } - else - { - Assert.That(textAfterDrop, Is.EqualTo("Drop")); - } + void AssertEventText(string automationId, string expectedText) + { + Assert.That( + App.WaitForTextToBePresentInElement(automationId, expectedText), + Is.True, + $"Timed out waiting for {automationId} to contain '{expectedText}'."); + Assert.That(App.FindElement(automationId).GetText(), Is.EqualTo(expectedText)); } [Test] From 937bab4617515235c15eef4bdb07289116b41b7d Mon Sep 17 00:00:00 2001 From: Vally Fixture Date: Mon, 3 Aug 2026 19:21:53 +0200 Subject: [PATCH 02/27] Wait for exact label text in DragEvents to avoid placeholder false-positive AssertEventText waited with a Contains-based substring match, but each event label's placeholder already contains the expected value (e.g. "DragOverEvents: " contains "DragOver", "DropEvents: " contains "Drop"), so the wait passed immediately on the placeholder and the following equality assertion could still flap. Add a WaitForTextEqualToElement helper that waits for the text to become exactly the expected value (ordinal) and use it here. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d00747b7-96f3-4e7a-8dfb-e3a48db04b2d --- .../Tests/DragAndDropUITests.cs | 8 +++-- .../src/UITest.Appium/HelperExtensions.cs | 34 +++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/DragAndDropUITests.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/DragAndDropUITests.cs index 46a4b7717232..9a7efd783428 100644 --- a/src/Controls/tests/TestCases.Shared.Tests/Tests/DragAndDropUITests.cs +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/DragAndDropUITests.cs @@ -41,10 +41,14 @@ public void DragEvents() void AssertEventText(string automationId, string expectedText) { + // Wait for the label's text to become EXACTLY the expected value. A substring wait is + // unreliable here because each label's placeholder (e.g. "DragOverEvents: ") already + // contains the expected event name (e.g. "DragOver"), so a Contains-based wait would pass + // immediately on the placeholder and the following equality assertion would still flap. Assert.That( - App.WaitForTextToBePresentInElement(automationId, expectedText), + App.WaitForTextEqualToElement(automationId, expectedText), Is.True, - $"Timed out waiting for {automationId} to contain '{expectedText}'."); + $"Timed out waiting for {automationId} to become '{expectedText}'."); Assert.That(App.FindElement(automationId).GetText(), Is.EqualTo(expectedText)); } diff --git a/src/TestUtils/src/UITest.Appium/HelperExtensions.cs b/src/TestUtils/src/UITest.Appium/HelperExtensions.cs index 6bbd5cfae4ec..930fc2663729 100644 --- a/src/TestUtils/src/UITest.Appium/HelperExtensions.cs +++ b/src/TestUtils/src/UITest.Appium/HelperExtensions.cs @@ -1081,6 +1081,40 @@ public static bool WaitForTextToBePresentInElement(this IApp app, string automat } } + /// + /// Waits until the element's text is exactly equal to (ordinal), rather + /// than merely containing it. Use this when the element's placeholder/initial text already + /// contains the expected value as a substring, which would make a Contains-based wait pass + /// prematurely on the placeholder. + /// + public static bool WaitForTextEqualToElement(this IApp app, string automationId, string text, TimeSpan? timeout = null) + { + timeout ??= DefaultTimeout; + TimeSpan retryFrequency = TimeSpan.FromMilliseconds(500); + + DateTime start = DateTime.Now; + + while (true) + { + var element = app.FindElements(automationId).FirstOrDefault(); + + if (element is not null && element.TryGetText(out var s) && string.Equals(s, text, StringComparison.Ordinal)) + { + return true; + } + + long elapsed = DateTime.Now.Subtract(start).Ticks; + if (elapsed >= timeout.Value.Ticks) + { + Debug.WriteLine($">>>>> {elapsed} ticks elapsed, timeout value is {timeout.Value.Ticks}"); + + return false; + } + + Task.Delay(retryFrequency.Milliseconds).Wait(); + } + } + /// /// Repeatedly executes a query until it returns a non-empty value or the specified retry count is reached. /// From 798f38ef133cb8a73b4281e0884c979c1b33c7ee Mon Sep 17 00:00:00 2001 From: Vally Fixture Date: Mon, 3 Aug 2026 20:04:36 +0200 Subject: [PATCH 03/27] Address review nits on DragEvents wait helper - Use the Task.Delay(TimeSpan) overload in WaitForTextEqualToElement instead of TimeSpan.Milliseconds (which is only the 0-999 component). - Drop the redundant GetText re-read in AssertEventText; the exact-text wait already asserts the value, and the extra read only re-opened a window for transient Appium flakiness. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d00747b7-96f3-4e7a-8dfb-e3a48db04b2d --- .../tests/TestCases.Shared.Tests/Tests/DragAndDropUITests.cs | 4 ++-- src/TestUtils/src/UITest.Appium/HelperExtensions.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/DragAndDropUITests.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/DragAndDropUITests.cs index 9a7efd783428..e8dfda4acfa9 100644 --- a/src/Controls/tests/TestCases.Shared.Tests/Tests/DragAndDropUITests.cs +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/DragAndDropUITests.cs @@ -44,12 +44,12 @@ void AssertEventText(string automationId, string expectedText) // Wait for the label's text to become EXACTLY the expected value. A substring wait is // unreliable here because each label's placeholder (e.g. "DragOverEvents: ") already // contains the expected event name (e.g. "DragOver"), so a Contains-based wait would pass - // immediately on the placeholder and the following equality assertion would still flap. + // immediately on the placeholder. The wait already asserts the exact text, so no separate + // GetText re-read is needed (it would only re-open a window for transient Appium flakiness). Assert.That( App.WaitForTextEqualToElement(automationId, expectedText), Is.True, $"Timed out waiting for {automationId} to become '{expectedText}'."); - Assert.That(App.FindElement(automationId).GetText(), Is.EqualTo(expectedText)); } [Test] diff --git a/src/TestUtils/src/UITest.Appium/HelperExtensions.cs b/src/TestUtils/src/UITest.Appium/HelperExtensions.cs index 930fc2663729..bc9386d4e918 100644 --- a/src/TestUtils/src/UITest.Appium/HelperExtensions.cs +++ b/src/TestUtils/src/UITest.Appium/HelperExtensions.cs @@ -1111,7 +1111,7 @@ public static bool WaitForTextEqualToElement(this IApp app, string automationId, return false; } - Task.Delay(retryFrequency.Milliseconds).Wait(); + Task.Delay(retryFrequency).Wait(); } } From 01008ebb27536ec2804220969f389982173e1e77 Mon Sep 17 00:00:00 2001 From: kubaflo <34349119+kubaflo@users.noreply.github.com> Date: Mon, 3 Aug 2026 20:37:01 +0200 Subject: [PATCH 04/27] Share text-wait polling loop and reuse exact-text wait in layout drag test Addresses reviewer feedback on the DragEvents wait helper: - Extract a shared predicate-based WaitForText core so WaitForTextToBePresentInElement (contains) and WaitForTextEqualToElement (exact) can no longer drift apart; this also fixes the older helper's Task.Delay(int) millisecond overload to use the TimeSpan overload. - Surface the last observed text (and expected value) in the timeout diagnostic so a placeholder-stuck/stalled label is distinguishable from a text-read failure. - Reuse the exact-text wait for the four event labels in DragAndDropBetweenLayouts, which previously waited only for element existence and could observe the placeholder on Windows. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d00747b7-96f3-4e7a-8dfb-e3a48db04b2d --- .../Tests/DragAndDropUITests.cs | 13 ++--- .../src/UITest.Appium/HelperExtensions.cs | 48 ++++++++----------- 2 files changed, 26 insertions(+), 35 deletions(-) diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/DragAndDropUITests.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/DragAndDropUITests.cs index e8dfda4acfa9..1b76ffe5dbbc 100644 --- a/src/Controls/tests/TestCases.Shared.Tests/Tests/DragAndDropUITests.cs +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/DragAndDropUITests.cs @@ -44,8 +44,9 @@ void AssertEventText(string automationId, string expectedText) // Wait for the label's text to become EXACTLY the expected value. A substring wait is // unreliable here because each label's placeholder (e.g. "DragOverEvents: ") already // contains the expected event name (e.g. "DragOver"), so a Contains-based wait would pass - // immediately on the placeholder. The wait already asserts the exact text, so no separate - // GetText re-read is needed (it would only re-open a window for transient Appium flakiness). + // immediately on the placeholder. WaitForTextEqualToElement polls until the text matches + // exactly, so the Assert below fails only on a genuine timeout; no separate GetText re-read + // is needed (it would only re-open a window for transient Appium flakiness). Assert.That( App.WaitForTextEqualToElement(automationId, expectedText), Is.True, @@ -67,7 +68,7 @@ public void DragAndDropBetweenLayouts() App.WaitForElement("Green"); App.DragAndDrop("Red", "Green"); - App.WaitForElement("DragStartEventsLabel"); + App.WaitForTextEqualToElement("DragStartEventsLabel", "DragStarting"); var textAfterDragStart = App.FindElement("DragStartEventsLabel").GetText(); if (string.IsNullOrEmpty(textAfterDragStart)) @@ -79,7 +80,7 @@ public void DragAndDropBetweenLayouts() Assert.That(textAfterDragStart, Is.EqualTo("DragStarting")); } - App.WaitForElement("DragOverEventsLabel"); + App.WaitForTextEqualToElement("DragOverEventsLabel", "DragOver"); var textAfterDragOver = App.FindElement("DragOverEventsLabel").GetText(); if (string.IsNullOrEmpty(textAfterDragOver)) { @@ -90,7 +91,7 @@ public void DragAndDropBetweenLayouts() Assert.That(textAfterDragOver, Is.EqualTo("DragOver")); } - App.WaitForElement("DragCompletedEventsLabel"); + App.WaitForTextEqualToElement("DragCompletedEventsLabel", "DropCompleted"); var textAfterDragComplete = App.FindElement("DragCompletedEventsLabel").GetText(); if (string.IsNullOrEmpty(textAfterDragComplete)) { @@ -112,7 +113,7 @@ public void DragAndDropBetweenLayouts() Assert.That(rainbowColorText, Is.EqualTo("RainbowColorsAdd:Red")); } - App.WaitForElement("DropEventsLabel"); + App.WaitForTextEqualToElement("DropEventsLabel", "Drop"); var textAfterDrop = App.FindElement("DropEventsLabel").GetText(); if (string.IsNullOrEmpty(textAfterDrop)) { diff --git a/src/TestUtils/src/UITest.Appium/HelperExtensions.cs b/src/TestUtils/src/UITest.Appium/HelperExtensions.cs index bc9386d4e918..19abd1b3a7e4 100644 --- a/src/TestUtils/src/UITest.Appium/HelperExtensions.cs +++ b/src/TestUtils/src/UITest.Appium/HelperExtensions.cs @@ -1054,32 +1054,7 @@ public static void WaitForNoElement( } public static bool WaitForTextToBePresentInElement(this IApp app, string automationId, string text, TimeSpan? timeout = null) - { - timeout ??= DefaultTimeout; - TimeSpan retryFrequency = TimeSpan.FromMilliseconds(500); - - DateTime start = DateTime.Now; - - while (true) - { - var element = app.FindElements(automationId).FirstOrDefault(); - - if (element is not null && element.TryGetText(out var s) && s.Contains(text, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - long elapsed = DateTime.Now.Subtract(start).Ticks; - if (elapsed >= timeout.Value.Ticks) - { - Debug.WriteLine($">>>>> {elapsed} ticks elapsed, timeout value is {timeout.Value.Ticks}"); - - return false; - } - - Task.Delay(retryFrequency.Milliseconds).Wait(); - } - } + => app.WaitForText(automationId, text, s => s.Contains(text, StringComparison.OrdinalIgnoreCase), timeout); /// /// Waits until the element's text is exactly equal to (ordinal), rather @@ -1088,25 +1063,40 @@ public static bool WaitForTextToBePresentInElement(this IApp app, string automat /// prematurely on the placeholder. /// public static bool WaitForTextEqualToElement(this IApp app, string automationId, string text, TimeSpan? timeout = null) + => app.WaitForText(automationId, text, s => string.Equals(s, text, StringComparison.Ordinal), timeout); + + /// + /// Shared polling loop for the text-wait helpers. Repeatedly reads the element's text and + /// returns as soon as is satisfied. On + /// timeout it logs the last observed text (and the expected value) so a stalled or + /// placeholder-stuck label is distinguishable from a text-read failure, then returns + /// . + /// + static bool WaitForText(this IApp app, string automationId, string expected, Func matches, TimeSpan? timeout) { timeout ??= DefaultTimeout; TimeSpan retryFrequency = TimeSpan.FromMilliseconds(500); DateTime start = DateTime.Now; + string? lastObservedText = null; while (true) { var element = app.FindElements(automationId).FirstOrDefault(); - if (element is not null && element.TryGetText(out var s) && string.Equals(s, text, StringComparison.Ordinal)) + if (element is not null && element.TryGetText(out var s)) { - return true; + lastObservedText = s; + if (matches(s)) + { + return true; + } } long elapsed = DateTime.Now.Subtract(start).Ticks; if (elapsed >= timeout.Value.Ticks) { - Debug.WriteLine($">>>>> {elapsed} ticks elapsed, timeout value is {timeout.Value.Ticks}"); + Debug.WriteLine($">>>>> {elapsed} ticks elapsed, timeout value is {timeout.Value.Ticks}; last observed text for '{automationId}' was '{lastObservedText ?? ""}', expected '{expected}'"); return false; } From 7ce61e3c08d99e59c873e4070de9379ffa16c8d1 Mon Sep 17 00:00:00 2001 From: Vally Fixture Date: Mon, 3 Aug 2026 20:52:18 +0200 Subject: [PATCH 05/27] Wait for iOS Entry keyboard before snapshot Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9be49656-7117-4235-9d96-404779ab6b16 --- .../Tests/FeatureMatrix/EntryFeatureTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/EntryFeatureTests.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/EntryFeatureTests.cs index 6b1d1405587e..479253b1755a 100644 --- a/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/EntryFeatureTests.cs +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/EntryFeatureTests.cs @@ -196,6 +196,7 @@ public void VerifyTextWhenClearButtonVisibleSetNever() App.Tap("Apply"); App.WaitForElement("TestEntry"); App.Tap("TestEntry"); + App.WaitForKeyboardToShow(); VerifyScreenshotWithKeyboardHandling(); } From 945e7afdf3e1e9955ae863c8aefa8c46207387db Mon Sep 17 00:00:00 2001 From: Vally Fixture Date: Mon, 3 Aug 2026 21:03:03 +0200 Subject: [PATCH 06/27] Limit Entry keyboard wait to iOS Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9be49656-7117-4235-9d96-404779ab6b16 --- .../Tests/FeatureMatrix/EntryFeatureTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/EntryFeatureTests.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/EntryFeatureTests.cs index 479253b1755a..cf80cf67fde5 100644 --- a/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/EntryFeatureTests.cs +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/EntryFeatureTests.cs @@ -196,7 +196,8 @@ public void VerifyTextWhenClearButtonVisibleSetNever() App.Tap("Apply"); App.WaitForElement("TestEntry"); App.Tap("TestEntry"); - App.WaitForKeyboardToShow(); + if (App is AppiumIOSApp) + App.WaitForKeyboardToShow(); VerifyScreenshotWithKeyboardHandling(); } From 45ca022239b0006f77c317910f37fcfe14c5de59 Mon Sep 17 00:00:00 2001 From: Vally Fixture Date: Mon, 3 Aug 2026 23:31:12 +0200 Subject: [PATCH 07/27] Require iOS keyboard before Entry snapshot Assert the synchronization wait succeeds so the test cannot continue to the snapshot without reaching its expected keyboard-visible state. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9be49656-7117-4235-9d96-404779ab6b16 --- .../Tests/FeatureMatrix/EntryFeatureTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/EntryFeatureTests.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/EntryFeatureTests.cs index cf80cf67fde5..3b457fd63b92 100644 --- a/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/EntryFeatureTests.cs +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/EntryFeatureTests.cs @@ -197,7 +197,7 @@ public void VerifyTextWhenClearButtonVisibleSetNever() App.WaitForElement("TestEntry"); App.Tap("TestEntry"); if (App is AppiumIOSApp) - App.WaitForKeyboardToShow(); + Assert.That(App.WaitForKeyboardToShow(), Is.True, "The iOS keyboard did not appear before the snapshot."); VerifyScreenshotWithKeyboardHandling(); } From 8dfc49383ea080616914da4e8370a9de7ea4a849 Mon Sep 17 00:00:00 2001 From: Vally Fixture Date: Mon, 3 Aug 2026 20:53:52 +0200 Subject: [PATCH 08/27] Extend ListView visual stabilization window Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9be49656-7117-4235-9d96-404779ab6b16 --- .../tests/TestCases.Shared.Tests/Tests/Issues/Issue18896.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue18896.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue18896.cs index 84776ca17834..2dc10481d3d3 100644 --- a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue18896.cs +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue18896.cs @@ -29,6 +29,6 @@ public void Issue18896Test() // ListView with HasUnevenRows may have variable height row rendering that requires // additional time for images to load and scrollbar to disappear. // Use retryTimeout to adaptively wait for the UI to stabilize. - VerifyScreenshot(retryTimeout: TimeSpan.FromSeconds(3)); + VerifyScreenshot(retryTimeout: TimeSpan.FromSeconds(5)); } } \ No newline at end of file From a2fc75cceefe85ce11723bf7215ed9be40b93dda Mon Sep 17 00:00:00 2001 From: Vally Fixture Date: Mon, 3 Aug 2026 21:58:06 +0200 Subject: [PATCH 09/27] Stabilize RefreshView interactive refresh UI test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9be49656-7117-4235-9d96-404779ab6b16 --- .../Tests/Issues/Issue16910.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue16910.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue16910.cs index e324286c8641..d7918b5e06ab 100644 --- a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue16910.cs +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue16910.cs @@ -38,8 +38,20 @@ public void BindingUpdatesFromProgrammaticRefresh() public void BindingUpdatesFromInteractiveRefresh() { var scrollViewRect = App.WaitForElement("RefreshScrollView", timeout: TimeSpan.FromSeconds(45)).GetRect(); - //In CI, using App.ScrollDown sometimes fails to trigger the refresh command, so here use DragCoordinates instead of the ScrollDown action in Appium. - App.DragCoordinates(scrollViewRect.CenterX(), scrollViewRect.Y + 50, scrollViewRect.CenterX(), scrollViewRect.Y + scrollViewRect.Height - 50); + void PullToRefresh() => + App.DragCoordinates(scrollViewRect.CenterX(), scrollViewRect.Y + 50, scrollViewRect.CenterX(), scrollViewRect.Y + scrollViewRect.Height - 50); + + // In CI, a single pull gesture occasionally does not trigger the refresh command. + PullToRefresh(); + try + { + App.WaitForElement("IsRefreshing", timeout: TimeSpan.FromSeconds(10)); + } + catch (TimeoutException) + { + PullToRefresh(); + } + App.WaitForElement("IsRefreshing", timeout: TimeSpan.FromSeconds(45)); App.Tap("StopRefreshing"); App.WaitForElement("IsNotRefreshing", timeout: TimeSpan.FromSeconds(45)); From 216d3bda5445d9de962d7a1d5b4d4ab8baea01fa Mon Sep 17 00:00:00 2001 From: Vally Fixture Date: Mon, 3 Aug 2026 22:06:50 +0200 Subject: [PATCH 10/27] Wait for SwipeView invoke result in UI test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9be49656-7117-4235-9d96-404779ab6b16 --- .../tests/TestCases.Shared.Tests/Tests/Issues/Issue36154.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue36154.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue36154.cs index e005d404709b..eb0420b5c5f1 100644 --- a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue36154.cs +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue36154.cs @@ -30,6 +30,6 @@ public void Issue36154SwipeViewShouldRevealItems() // Swipe left (finger moves left) → reveals RightItems App.DragCoordinates(centerX, centerY, centerX - 200, centerY); - Assert.That(App.WaitForElement("ResultLabel").GetText(), Is.EqualTo("RIGHT invoked!")); + App.WaitForTextToBePresentInElement("ResultLabel", "RIGHT invoked!"); } } From cadfb47d092681f1808fda560a0c57578f953a8f Mon Sep 17 00:00:00 2001 From: kubaflo <34349119+kubaflo@users.noreply.github.com> Date: Mon, 3 Aug 2026 22:36:22 +0200 Subject: [PATCH 11/27] Assert on SwipeView result wait so the test can actually fail WaitForTextToBePresentInElement returns false on timeout rather than throwing, so ignoring its result let the test pass even if the SwipeView invoke callback never updated ResultLabel. Assert the result to turn a missed callback back into a real failure. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d00747b7-96f3-4e7a-8dfb-e3a48db04b2d --- .../TestCases.Shared.Tests/Tests/Issues/Issue36154.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue36154.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue36154.cs index eb0420b5c5f1..acbe2cf8ac6a 100644 --- a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue36154.cs +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue36154.cs @@ -30,6 +30,11 @@ public void Issue36154SwipeViewShouldRevealItems() // Swipe left (finger moves left) → reveals RightItems App.DragCoordinates(centerX, centerY, centerX - 200, centerY); - App.WaitForTextToBePresentInElement("ResultLabel", "RIGHT invoked!"); + // WaitForTextToBePresentInElement returns false (rather than throwing) on timeout, so assert on + // it: otherwise the test would pass even if the SwipeView invoke callback never updated the label. + Assert.That( + App.WaitForTextToBePresentInElement("ResultLabel", "RIGHT invoked!"), + Is.True, + "Timed out waiting for ResultLabel to display 'RIGHT invoked!' after the swipe."); } } From d12539d0a3de83e7085ca28cb543a204984e75da Mon Sep 17 00:00:00 2001 From: Vally Fixture Date: Tue, 4 Aug 2026 00:57:56 +0200 Subject: [PATCH 12/27] Stabilize initial parent-child SafeArea layout check Retry the initial geometry assertions while iOS finishes laying out the issue page instead of accepting a transient off-screen rect. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9be49656-7117-4235-9d96-404779ab6b16 --- .../Issues/Issue28986_ParentChildTest.cs | 60 +++++++++---------- 1 file changed, 27 insertions(+), 33 deletions(-) diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue28986_ParentChildTest.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue28986_ParentChildTest.cs index 4b9997fc215d..1dfe7ba3f964 100644 --- a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue28986_ParentChildTest.cs +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue28986_ParentChildTest.cs @@ -46,43 +46,37 @@ public void VerifyInitialStateParentTopChildBottom() // 2. Bottom indicator is inset from screen bottom by safe area (child handles bottom) // 3. Both work independently without conflict - // Get screen dimensions - var parentGridRect = App.WaitForElement("ParentGrid").GetRect(); - var screenTop = parentGridRect.Y; - var screenBottom = parentGridRect.Y + parentGridRect.Height; - // Verify initial status WaitForText("StatusLabel", "Parent: Top=Container, Bottom=None | Child: Bottom=Container"); - // Measure top indicator position - var topIndicatorRect = App.WaitForElement("TopIndicator").GetRect(); - var topIndicatorTop = topIndicatorRect.Y; - - // Top indicator should be below the screen top (safe area applied) - var topInsetFromScreenTop = topIndicatorTop - screenTop; - Assert.That(topInsetFromScreenTop, Is.GreaterThan(5), - $"Top indicator should be inset from screen top by safe area. " + - $"Current inset: {topInsetFromScreenTop}pt (expected >5pt)"); - - // Measure bottom indicator position - var bottomIndicatorRect = App.WaitForElement("BottomIndicator").GetRect(); - var bottomIndicatorBottom = bottomIndicatorRect.Y + bottomIndicatorRect.Height; - - // Bottom indicator should be above the screen bottom (safe area applied) - var bottomInsetFromScreenBottom = screenBottom - bottomIndicatorBottom; - // On devices with bottom safe area (iOS home indicator, Android nav bar), verify meaningful inset. - // On gesture-nav Android devices, bottom safe area is correctly 0. - if (HasBottomSafeArea(bottomInsetFromScreenBottom)) - { - Assert.That(bottomInsetFromScreenBottom, Is.GreaterThan(5), - $"Bottom indicator should be inset from screen bottom by safe area. " + - $"Current inset: {bottomInsetFromScreenBottom}pt (expected >5pt)"); - } - else + App.RetryAssert(() => { - Assert.That(bottomInsetFromScreenBottom, Is.GreaterThanOrEqualTo(0), - $"Bottom indicator should not extend below screen bottom. Inset: {bottomInsetFromScreenBottom}pt"); - } + var parentGridRect = App.WaitForElement("ParentGrid").GetRect(); + var screenTop = parentGridRect.Y; + var screenBottom = parentGridRect.Y + parentGridRect.Height; + + var topIndicatorRect = App.WaitForElement("TopIndicator").GetRect(); + var topInsetFromScreenTop = topIndicatorRect.Y - screenTop; + Assert.That(topInsetFromScreenTop, Is.GreaterThan(5), + $"Top indicator should be inset from screen top by safe area. " + + $"Current inset: {topInsetFromScreenTop}pt (expected >5pt)"); + + var bottomIndicatorRect = App.WaitForElement("BottomIndicator").GetRect(); + var bottomIndicatorBottom = bottomIndicatorRect.Y + bottomIndicatorRect.Height; + var bottomInsetFromScreenBottom = screenBottom - bottomIndicatorBottom; + + if (HasBottomSafeArea(bottomInsetFromScreenBottom)) + { + Assert.That(bottomInsetFromScreenBottom, Is.GreaterThan(5), + $"Bottom indicator should be inset from screen bottom by safe area. " + + $"Current inset: {bottomInsetFromScreenBottom}pt (expected >5pt)"); + } + else + { + Assert.That(bottomInsetFromScreenBottom, Is.GreaterThanOrEqualTo(0), + $"Bottom indicator should not extend below screen bottom. Inset: {bottomInsetFromScreenBottom}pt"); + } + }); } [Test, Order(2)] From ac88ad49ac90a43dd663660262327655bfcfb43e Mon Sep 17 00:00:00 2001 From: Vally Fixture Date: Tue, 4 Aug 2026 01:36:45 +0200 Subject: [PATCH 13/27] Avoid nested waits in SafeArea layout retry Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9be49656-7117-4235-9d96-404779ab6b16 --- .../Tests/Issues/Issue28986_ParentChildTest.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue28986_ParentChildTest.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue28986_ParentChildTest.cs index 1dfe7ba3f964..b21ef5ba592f 100644 --- a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue28986_ParentChildTest.cs +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue28986_ParentChildTest.cs @@ -51,17 +51,17 @@ public void VerifyInitialStateParentTopChildBottom() App.RetryAssert(() => { - var parentGridRect = App.WaitForElement("ParentGrid").GetRect(); + var parentGridRect = App.FindElement("ParentGrid").GetRect(); var screenTop = parentGridRect.Y; var screenBottom = parentGridRect.Y + parentGridRect.Height; - var topIndicatorRect = App.WaitForElement("TopIndicator").GetRect(); + var topIndicatorRect = App.FindElement("TopIndicator").GetRect(); var topInsetFromScreenTop = topIndicatorRect.Y - screenTop; Assert.That(topInsetFromScreenTop, Is.GreaterThan(5), $"Top indicator should be inset from screen top by safe area. " + $"Current inset: {topInsetFromScreenTop}pt (expected >5pt)"); - var bottomIndicatorRect = App.WaitForElement("BottomIndicator").GetRect(); + var bottomIndicatorRect = App.FindElement("BottomIndicator").GetRect(); var bottomIndicatorBottom = bottomIndicatorRect.Y + bottomIndicatorRect.Height; var bottomInsetFromScreenBottom = screenBottom - bottomIndicatorBottom; From f1050c3370524f6412aeff15f51b93ae027e950c Mon Sep 17 00:00:00 2001 From: Vally Fixture Date: Tue, 4 Aug 2026 01:29:16 +0200 Subject: [PATCH 14/27] Stabilize picker keyboard transition UI test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9be49656-7117-4235-9d96-404779ab6b16 --- .../Tests/Issues/Issue24496.cs | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue24496.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue24496.cs index 42b5e46ab7bb..fb72e54d09d6 100644 --- a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue24496.cs +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue24496.cs @@ -15,19 +15,25 @@ public Issue24496(TestDevice testDevice) : base(testDevice) [Test] [Category(UITestCategories.Entry)] - public void PickerNewKeyboardIsAboveKeyboard() - { - App.WaitForElement("Picker6"); + public void PickerNewKeyboardIsAboveKeyboard() + { + App.WaitForElement("Picker6"); App.Tap("Picker6"); - VerifyScreenshot(TestContext.CurrentContext.Test.MethodName + "_Picker6"); + App.WaitForElement(AppiumQuery.ByXPath("//XCUIElementTypePickerWheel")); + VerifyScreenshot(TestContext.CurrentContext.Test.MethodName + "_Picker6"); if (App is AppiumIOSApp iosApp && HelperExtensions.IsIOS26OrHigher(iosApp)) { var rect = App.WaitForElement("ScrollViewId").GetRect(); App.DragCoordinates(rect.CenterX(), rect.CenterY(), rect.CenterX(), rect.CenterY() - 60); } - App.Tap("Entry7"); - VerifyScreenshot(TestContext.CurrentContext.Test.MethodName + "_Entry7", cropBottom: 1000); - } - } + App.RetryAssert(() => + { + App.Tap("Entry7"); + Assert.That(App.IsFocused("Entry7"), Is.True, "Entry7 did not receive focus after the picker scroll."); + }); + App.WaitForNoElement(AppiumQuery.ByXPath("//XCUIElementTypePickerWheel")); + VerifyScreenshot(TestContext.CurrentContext.Test.MethodName + "_Entry7", cropBottom: 1000, retryTimeout: TimeSpan.FromSeconds(2)); + } + } } #endif From f2b686f460163bbebc6577ed0b30cbded8f4d965 Mon Sep 17 00:00:00 2001 From: Vally Fixture Date: Tue, 4 Aug 2026 06:07:20 +0200 Subject: [PATCH 15/27] [release/11.0.1xx-preview7] Stabilize CollectionView footer screenshot Allow the empty CollectionView footer visual assertion to retry during the brief MacCatalyst layout race observed in Preview 7 CI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9be49656-7117-4235-9d96-404779ab6b16 --- .../tests/TestCases.Shared.Tests/Tests/Issues/Issue28604.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue28604.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue28604.cs index 9ca4dbf12484..dc107cc27a4f 100644 --- a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue28604.cs +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue28604.cs @@ -17,7 +17,7 @@ public Issue28604(TestDevice testDevice) : base(testDevice) public void FooterShouldDisplayAtBottomOfEmptyView() { App.WaitForElement("CollectionView"); - VerifyScreenshot(); + VerifyScreenshot(retryTimeout: TimeSpan.FromSeconds(3)); } } #endif \ No newline at end of file From 116c8f4c9b56356229de2962c25d1fff087eba63 Mon Sep 17 00:00:00 2001 From: Vally Fixture Date: Tue, 4 Aug 2026 13:32:57 +0200 Subject: [PATCH 16/27] Tune CollectionView screenshot retry Use the established two-second retry window with a 0.5 percent screenshot tolerance for the empty-footer timing test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9be49656-7117-4235-9d96-404779ab6b16 --- .../tests/TestCases.Shared.Tests/Tests/Issues/Issue28604.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue28604.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue28604.cs index dc107cc27a4f..757e1b3ea7ab 100644 --- a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue28604.cs +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue28604.cs @@ -17,7 +17,7 @@ public Issue28604(TestDevice testDevice) : base(testDevice) public void FooterShouldDisplayAtBottomOfEmptyView() { App.WaitForElement("CollectionView"); - VerifyScreenshot(retryTimeout: TimeSpan.FromSeconds(3)); + VerifyScreenshot(tolerance: 0.5, retryTimeout: TimeSpan.FromSeconds(2)); } } #endif \ No newline at end of file From 70956e8f9484df1e082d5af3982e44f4ad25f282 Mon Sep 17 00:00:00 2001 From: Vally Fixture Date: Mon, 3 Aug 2026 18:31:45 +0200 Subject: [PATCH 17/27] Allow screen resolution fallback after test failure Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9be49656-7117-4235-9d96-404779ab6b16 --- eng/scripts/Set-ScreenResolution.ps1 | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/eng/scripts/Set-ScreenResolution.ps1 b/eng/scripts/Set-ScreenResolution.ps1 index acefd73f8128..e3bd37d281ec 100644 --- a/eng/scripts/Set-ScreenResolution.ps1 +++ b/eng/scripts/Set-ScreenResolution.ps1 @@ -174,8 +174,7 @@ namespace DisplaySettings return $false } ([DisplaySettings.NativeMethods]::DISP_CHANGE_FAILED) { - Write-Error "The resolution change test failed (FAILED)" - return $false + Write-Warning "The resolution change test failed (FAILED). Attempting to apply the resolution anyway..." } default { Write-Warning "Unexpected test result, attempting to apply anyway..." From 2644776ba814a0324741d1498892a570a2f7f07b Mon Sep 17 00:00:00 2001 From: Vally Fixture Date: Mon, 3 Aug 2026 19:02:09 +0200 Subject: [PATCH 18/27] Make the CDS_TEST fallback warning actionable The DISP_CHANGE_FAILED branch logged a redundant "failed (FAILED)" message without the return code or the resolution being attempted. Log the specific CDS_TEST result and the target resolution instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d00747b7-96f3-4e7a-8dfb-e3a48db04b2d --- eng/scripts/Set-ScreenResolution.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/scripts/Set-ScreenResolution.ps1 b/eng/scripts/Set-ScreenResolution.ps1 index e3bd37d281ec..ada6ddccb947 100644 --- a/eng/scripts/Set-ScreenResolution.ps1 +++ b/eng/scripts/Set-ScreenResolution.ps1 @@ -174,7 +174,7 @@ namespace DisplaySettings return $false } ([DisplaySettings.NativeMethods]::DISP_CHANGE_FAILED) { - Write-Warning "The resolution change test failed (FAILED). Attempting to apply the resolution anyway..." + Write-Warning "CDS_TEST returned DISP_CHANGE_FAILED ($testResult) for target ${Width}x${Height}; attempting to apply the resolution anyway..." } default { Write-Warning "Unexpected test result, attempting to apply anyway..." From 4b9b9c23a29753f0093cef54dd09a033edeff5af Mon Sep 17 00:00:00 2001 From: Vally Fixture Date: Tue, 4 Aug 2026 13:50:10 +0200 Subject: [PATCH 19/27] Add deterministic screen resolution fallback tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9be49656-7117-4235-9d96-404779ab6b16 --- .../scripts/Set-ScreenResolution.Tests.ps1 | 76 +++++++++++++++++++ eng/scripts/Set-ScreenResolution.ps1 | 62 ++++++++++----- 2 files changed, 118 insertions(+), 20 deletions(-) create mode 100644 .github/scripts/Set-ScreenResolution.Tests.ps1 diff --git a/.github/scripts/Set-ScreenResolution.Tests.ps1 b/.github/scripts/Set-ScreenResolution.Tests.ps1 new file mode 100644 index 000000000000..f3762e7d5044 --- /dev/null +++ b/.github/scripts/Set-ScreenResolution.Tests.ps1 @@ -0,0 +1,76 @@ +#!/usr/bin/env pwsh +#Requires -Modules Pester + +BeforeAll { + $screenResolutionScript = Join-Path $PSScriptRoot '../../eng/scripts/Set-ScreenResolution.ps1' + $tokens = $null + $parseErrors = $null + $ast = [System.Management.Automation.Language.Parser]::ParseFile( + $screenResolutionScript, + [ref]$tokens, + [ref]$parseErrors) + + if ($parseErrors.Count -gt 0) { + throw "Set-ScreenResolution.ps1 has parse errors: $($parseErrors -join '; ')" + } + + $functionDefinitions = $ast.FindAll({ + param($node) + $node -is [System.Management.Automation.Language.FunctionDefinitionAst] + }, $true) + + foreach ($functionName in @( + 'Get-ScreenResolutionProbeAction', + 'Test-ScreenResolutionApplySucceeded' + )) { + $definition = $functionDefinitions | + Where-Object Name -EQ $functionName | + Select-Object -First 1 + + if ($null -eq $definition) { + throw "Function '$functionName' was not found in Set-ScreenResolution.ps1" + } + + Invoke-Expression $definition.Extent.Text + } + + $setResolutionDefinition = $functionDefinitions | + Where-Object Name -EQ 'Set-ScreenResolution' | + Select-Object -First 1 + + if ($null -eq $setResolutionDefinition) { + throw "Function 'Set-ScreenResolution' was not found in Set-ScreenResolution.ps1" + } + + $setResolutionBody = $setResolutionDefinition.Extent.Text +} + +Describe 'Set-ScreenResolution native result decisions' { + It 'returns for CDS_TEST result ' -ForEach @( + @{ Result = 0; Expected = 'Apply' } + @{ Result = -1; Expected = 'Apply' } + @{ Result = -2; Expected = 'Reject' } + @{ Result = -3; Expected = 'Apply' } + @{ Result = -4; Expected = 'Apply' } + @{ Result = -5; Expected = 'Apply' } + ) { + Get-ScreenResolutionProbeAction -Result $Result | Should -Be $Expected + } + + It 'returns for real apply result ' -ForEach @( + @{ Result = 0; Expected = $true } + @{ Result = 1; Expected = $true } + @{ Result = -1; Expected = $false } + @{ Result = -2; Expected = $false } + @{ Result = -3; Expected = $false } + @{ Result = -4; Expected = $false } + @{ Result = -5; Expected = $false } + ) { + Test-ScreenResolutionApplySucceeded -Result $Result | Should -Be $Expected + } + + It 'uses the pure decisions in the native flow' { + $setResolutionBody | Should -Match 'Get-ScreenResolutionProbeAction\s+-Result\s+\$testResult' + $setResolutionBody | Should -Match 'Test-ScreenResolutionApplySucceeded\s+-Result\s+\$changeResult' + } +} diff --git a/eng/scripts/Set-ScreenResolution.ps1 b/eng/scripts/Set-ScreenResolution.ps1 index ada6ddccb947..bf73a9c0a9cc 100644 --- a/eng/scripts/Set-ScreenResolution.ps1 +++ b/eng/scripts/Set-ScreenResolution.ps1 @@ -37,6 +37,26 @@ param ( Set-StrictMode -Version 2.0 $ErrorActionPreference = "Stop" +function Get-ScreenResolutionProbeAction { + param ( + [int]$Result + ) + + if ($Result -eq -2) { + return "Reject" + } + + return "Apply" +} + +function Test-ScreenResolutionApplySucceeded { + param ( + [int]$Result + ) + + return ($Result -eq 0 -or $Result -eq 1) +} + function Set-ScreenResolution { param ( [int]$Width, @@ -167,33 +187,35 @@ namespace DisplaySettings if ($testResult -ne [DisplaySettings.NativeMethods]::DISP_CHANGE_SUCCESSFUL) { Write-Warning "Resolution test returned code: $testResult" - - switch ($testResult) { - ([DisplaySettings.NativeMethods]::DISP_CHANGE_BADMODE) { - Write-Error "The resolution ${Width}x${Height} is not supported by this display (BADMODE)" - return $false - } - ([DisplaySettings.NativeMethods]::DISP_CHANGE_FAILED) { - Write-Warning "CDS_TEST returned DISP_CHANGE_FAILED ($testResult) for target ${Width}x${Height}; attempting to apply the resolution anyway..." - } - default { - Write-Warning "Unexpected test result, attempting to apply anyway..." - } + + if ((Get-ScreenResolutionProbeAction -Result $testResult) -eq "Reject") { + Write-Error "The resolution ${Width}x${Height} is not supported by this display (BADMODE)" + return $false + } + + if ($testResult -eq [DisplaySettings.NativeMethods]::DISP_CHANGE_FAILED) { + Write-Warning "CDS_TEST returned DISP_CHANGE_FAILED ($testResult) for target ${Width}x${Height}; attempting to apply the resolution anyway..." + } + else { + Write-Warning "Unexpected test result, attempting to apply anyway..." } } # Apply the resolution change $changeResult = [DisplaySettings.NativeMethods]::ChangeDisplaySettings([ref]$devMode, [DisplaySettings.NativeMethods]::CDS_UPDATEREGISTRY) - - switch ($changeResult) { - ([DisplaySettings.NativeMethods]::DISP_CHANGE_SUCCESSFUL) { - Write-Host "Successfully set screen resolution to ${Width}x${Height}" - return $true - } - ([DisplaySettings.NativeMethods]::DISP_CHANGE_RESTART) { + + if (Test-ScreenResolutionApplySucceeded -Result $changeResult) { + if ($changeResult -eq [DisplaySettings.NativeMethods]::DISP_CHANGE_RESTART) { Write-Host "Screen resolution set to ${Width}x${Height}. A restart may be required for some applications." - return $true } + else { + Write-Host "Successfully set screen resolution to ${Width}x${Height}" + } + + return $true + } + + switch ($changeResult) { ([DisplaySettings.NativeMethods]::DISP_CHANGE_BADMODE) { Write-Error "The resolution ${Width}x${Height} is not supported by this display" return $false From ddff22547acd581f1e6881bb4dced574883aa9f8 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Tue, 4 Aug 2026 15:13:06 +0200 Subject: [PATCH 20/27] Add independent Graphics device coverage Extract the image-scaling contract tests that pass against the unmodified Preview 7 implementation. Keep the nonpositive-size cases in the functional PR because those require its runtime fix. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9be49656-7117-4235-9d96-404779ab6b16 --- .../tests/DeviceTests/Tests/ImageTests.cs | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) diff --git a/src/Graphics/tests/DeviceTests/Tests/ImageTests.cs b/src/Graphics/tests/DeviceTests/Tests/ImageTests.cs index 283e25b760e3..d6ec122cd4e0 100644 --- a/src/Graphics/tests/DeviceTests/Tests/ImageTests.cs +++ b/src/Graphics/tests/DeviceTests/Tests/ImageTests.cs @@ -1,5 +1,11 @@ using System; using System.Threading.Tasks; +#if IOS || MACCATALYST +using CoreFoundation; +using CoreGraphics; +using Foundation; +using UIKit; +#endif using Microsoft.Maui.Graphics.Platform; using Microsoft.Maui.Storage; using Xunit; @@ -48,6 +54,171 @@ public async Task CanGetStreamFromImage(ImageFormat format, float quality) Assert.True(newStream.Length > 0, "Assert.True(newStream.Length > 0)"); } +#if IOS || MACCATALYST + [Theory] + [InlineData(1f)] + [InlineData(2f)] + [InlineData(3f)] + public void ScaleImageUsesOneXBackingScale(float sourceScale) + { + var sourceSize = new CGSize(30, 20); + using var sourceRenderer = new UIGraphicsImageRenderer(sourceSize, new UIGraphicsImageRendererFormat + { + Opaque = false, + Scale = sourceScale, + }); + + using var source = sourceRenderer.CreateImage(context => + { + UIColor.Red.SetFill(); + context.FillRect(new CGRect(CGPoint.Empty, sourceSize)); + }); + + using var scaled = source.ScaleImage(new CGSize(10, 5)); + + Assert.Equal(1, (double)scaled.CurrentScale); + Assert.Equal(10, (double)scaled.Size.Width); + Assert.Equal(5, (double)scaled.Size.Height); + Assert.NotNull(scaled.CGImage); + Assert.Equal(10, (int)scaled.CGImage.Width); + Assert.Equal(5, (int)scaled.CGImage.Height); + } + + [Fact] + public async Task ScaleImageCanRunOnBackgroundThread() + { + var sourceSize = new CGSize(30, 20); + using var sourceRenderer = new UIGraphicsImageRenderer(sourceSize, new UIGraphicsImageRendererFormat + { + Opaque = false, + Scale = 2, + }); + + using var source = sourceRenderer.CreateImage(context => + { + UIColor.Red.SetFill(); + context.FillRect(new CGRect(CGPoint.Empty, sourceSize)); + }); + + using var scaled = await Task.Run(() => source.ScaleImage(new CGSize(10, 5))); + + Assert.Equal(1, (double)scaled.CurrentScale); + Assert.Equal(10, (int)scaled.CGImage.Width); + Assert.Equal(5, (int)scaled.CGImage.Height); + } + + [Fact] + public async Task ScaleImageDoesNotRequireMainThreadProgress() + { + using var source = CreatePatternImage(UIImageOrientation.Up); + + Task scaleTask = null; + var completedWhileMainThreadWasBlocked = false; + + void ScaleAndWait() + { + scaleTask = Task.Run(() => source.ScaleImage(new CGSize(10, 5))); + completedWhileMainThreadWasBlocked = scaleTask.Wait(TimeSpan.FromSeconds(5)); + } + + if (NSThread.IsMain) + ScaleAndWait(); + else + DispatchQueue.MainQueue.DispatchSync(ScaleAndWait); + + using var scaled = await scaleTask; + + Assert.True(completedWhileMainThreadWasBlocked, "ScaleImage must not synchronously depend on main-thread progress."); + Assert.Equal(10, (int)scaled.CGImage.Width); + Assert.Equal(5, (int)scaled.CGImage.Height); + } + + [Theory] + [InlineData(UIImageOrientation.Up)] + [InlineData(UIImageOrientation.Down)] + [InlineData(UIImageOrientation.Left)] + [InlineData(UIImageOrientation.Right)] + [InlineData(UIImageOrientation.UpMirrored)] + [InlineData(UIImageOrientation.DownMirrored)] + [InlineData(UIImageOrientation.LeftMirrored)] + [InlineData(UIImageOrientation.RightMirrored)] + public void ScaleImageMatchesUIKitRendering(UIImageOrientation orientation) + { + var targetSize = new CGSize(12.25, 8.75); + using var source = CreatePatternImage(orientation); + using var expected = RunOnMainThread(() => + { + using var format = new UIGraphicsImageRendererFormat + { + Opaque = false, + PreferredRange = UIGraphicsImageRendererFormatRange.Standard, + Scale = 1, + }; + using var renderer = new UIGraphicsImageRenderer(targetSize, format); + return renderer.CreateImage(_ => source.Draw(new CGRect(CGPoint.Empty, targetSize))); + }); + using var actual = source.ScaleImage(targetSize); + + Assert.Equal(UIImageOrientation.Up, actual.Orientation); + Assert.Equal(GetPixelData(expected), GetPixelData(actual)); + } + + private static UIImage CreatePatternImage(UIImageOrientation orientation) + { + using var image = RunOnMainThread(() => + { + var sourceSize = new CGSize(30, 20); + using var renderer = new UIGraphicsImageRenderer(sourceSize); + return renderer.CreateImage(context => + { + UIColor.FromRGBA(1f, 0f, 0f, 0.5f).SetFill(); + context.FillRect(new CGRect(0, 0, 20, 10)); + UIColor.Blue.SetFill(); + context.FillRect(new CGRect(20, 0, 10, 20)); + UIColor.Green.SetFill(); + context.FillRect(new CGRect(0, 10, 10, 10)); + }); + }); + + return UIImage.FromImage(image.CGImage, 1, orientation); + } + + private static byte[] GetPixelData(UIImage image) + { + var cgImage = image.CGImage; + var width = checked((int)cgImage.Width); + var height = checked((int)cgImage.Height); + var bytesPerRow = checked(4 * width); + var pixels = new byte[checked(bytesPerRow * height)]; + + using var colorSpace = CGColorSpace.CreateDeviceRGB(); + using var context = new CGBitmapContext( + pixels, + width, + height, + 8, + bytesPerRow, + colorSpace, + CGBitmapFlags.ByteOrder32Little | CGBitmapFlags.PremultipliedFirst); + + context.TranslateCTM(0, height); + context.ScaleCTM(1, -1); + context.DrawImage(new CGRect(0, 0, width, height), cgImage); + + return pixels; + } + + private static T RunOnMainThread(Func action) + { + if (NSThread.IsMain) + return action(); + + T result = default!; + DispatchQueue.MainQueue.DispatchSync(() => result = action()); + return result; + } +#endif + [Theory] [InlineData(ImageFormat.Png, 2.0f)] [InlineData(ImageFormat.Png, 80f)] From 98ea8bba1a690af544b100b204d6e1083989bbfb Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Tue, 4 Aug 2026 15:59:43 +0200 Subject: [PATCH 21/27] Stabilize CarouselView leak test cleanup Allow UIKit deferred controller cleanup to complete before forcing managed garbage collection, avoiding false leak failures that race native teardown. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9be49656-7117-4235-9d96-404779ab6b16 --- .../DeviceTests/Elements/CarouselView/CarouselViewTests.iOS.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Controls/tests/DeviceTests/Elements/CarouselView/CarouselViewTests.iOS.cs b/src/Controls/tests/DeviceTests/Elements/CarouselView/CarouselViewTests.iOS.cs index 7675d1dd2fc8..10bfc4304586 100644 --- a/src/Controls/tests/DeviceTests/Elements/CarouselView/CarouselViewTests.iOS.cs +++ b/src/Controls/tests/DeviceTests/Elements/CarouselView/CarouselViewTests.iOS.cs @@ -41,6 +41,9 @@ await InvokeOnMainThreadAsync(async () => ((IElementHandler)handler).DisconnectHandler(); }); + // Allow UIKit to release controller-owned references before forcing managed GC. + await Task.Delay(100); + // Force garbage collection await AssertionExtensions.WaitForGC(weakCarouselView, weakHandler); From c824c4a991ac198654995ba78648b26d7de3d5ac Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Tue, 4 Aug 2026 17:32:49 +0200 Subject: [PATCH 22/27] Stabilize unloaded page alert UI test Replace fixed timing delays with the exact Window-detach transition and wait for an explicit completion signal. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9be49656-7117-4235-9d96-404779ab6b16 --- .../TestCases.HostApp/Issues/Issue33287.cs | 39 +++++++++++-------- .../Tests/Issues/Issue33287.cs | 16 +++++--- 2 files changed, 34 insertions(+), 21 deletions(-) diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue33287.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue33287.cs index 407bfb776c94..240407a2b17f 100644 --- a/src/Controls/tests/TestCases.HostApp/Issues/Issue33287.cs +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue33287.cs @@ -1,5 +1,5 @@ using System; -using System.Threading.Tasks; +using System.ComponentModel; namespace Maui.Controls.Sample.Issues; @@ -17,6 +17,12 @@ public Issue33287MainPage() { Title = "Issue 33287"; + var statusLabel = new Label + { + Text = "Waiting for alert request", + AutomationId = "AlertStatusLabel" + }; + Content = new VerticalStackLayout { Padding = 20, @@ -28,13 +34,15 @@ public Issue33287MainPage() Text = "Navigate to Second Page", AutomationId = "NavigateButton", Command = new Command(async () => - await Navigation.PushAsync(new Issue33287SecondPage())) + await Navigation.PushAsync(new Issue33287SecondPage(status => + statusLabel.Text = status))) }, new Label { Text = "MainPage", AutomationId = "MainPageLabel" - } + }, + statusLabel } }; } @@ -42,9 +50,10 @@ await Navigation.PushAsync(new Issue33287SecondPage())) public class Issue33287SecondPage : ContentPage { - public Issue33287SecondPage() + public Issue33287SecondPage(Action updateStatus) { Title = "Second Page"; + PropertyChanged += OnPropertyChanged; Content = new VerticalStackLayout { @@ -59,20 +68,18 @@ public Issue33287SecondPage() } } }; - } - protected override async void OnAppearing() - { - base.OnAppearing(); + async void OnPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName != nameof(Window) || Window is not null) + return; - // Wait long enough for the user/test to navigate back -#if MACCATALYST - await Task.Delay(4000); -#else - await Task.Delay(2000); -#endif + PropertyChanged -= OnPropertyChanged; + updateStatus("Page detached"); - // Without the fix this throws NullReferenceException and crashes the app - await DisplayAlertAsync("Test Alert", "This alert was delayed", "OK"); + // Request the alert before handler teardown changes IsPlatformEnabled. + await DisplayAlertAsync("Test Alert", "This alert was delayed", "OK"); + updateStatus("Alert request completed"); + } } } diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33287.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33287.cs index 5a5e166ad556..eab09a8c2a0c 100644 --- a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33287.cs +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33287.cs @@ -23,12 +23,18 @@ public void DisplayAlertAsyncShouldNotCrashWhenPageUnloaded() App.WaitForElement("GoBackButton"); App.Tap("GoBackButton"); - // Back on main page — wait for the delayed DisplayAlertAsync to fire. - // Without the fix the NRE crashes the app and this element becomes unreachable. + // Back on the main page, wait until the detached page's alert request completes. + // Without the fix the NRE crashes the app and this status is never updated. App.WaitForElement("MainPageLabel"); - System.Threading.Thread.Sleep(3000); - - // Verify the app is still alive and responsive after the alert fired on the detached page. + Assert.That( + App.WaitForTextToBePresentInElement( + "AlertStatusLabel", + "Alert request completed", + timeout: TimeSpan.FromSeconds(10)), + Is.True, + "The detached page's alert request should complete"); + + // Verify the app is still alive and responsive after the alert request on the detached page. // Without the fix the app process is dead and this call will throw/timeout. Assert.That(App.FindElement("MainPageLabel").GetText(), Is.EqualTo("MainPage"), "App should remain responsive after DisplayAlertAsync on an unloaded page"); From db10e413ef86039b0e94436b1b7c2465e631b7a4 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Tue, 4 Aug 2026 19:00:58 +0200 Subject: [PATCH 23/27] [release/11.0.1xx-preview7] Limit modal animation leak test to Android The test was introduced to cover Android ModalNavigationManager animation cleanup and is not applicable to Apple transition lifecycles. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9be49656-7117-4235-9d96-404779ab6b16 --- src/Controls/tests/DeviceTests/Elements/Modal/ModalTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Controls/tests/DeviceTests/Elements/Modal/ModalTests.cs b/src/Controls/tests/DeviceTests/Elements/Modal/ModalTests.cs index abac26a5bb5a..7dbab7954deb 100644 --- a/src/Controls/tests/DeviceTests/Elements/Modal/ModalTests.cs +++ b/src/Controls/tests/DeviceTests/Elements/Modal/ModalTests.cs @@ -722,6 +722,7 @@ await CreateHandlerAndAddToWindow(window, async handler => } #endif +#if ANDROID [Fact("Dont leak with Animation")] public async Task ModalPageDontLeakWithAnimation() { @@ -751,6 +752,7 @@ await CreateHandlerAndAddToWindow(window, async handler => await AssertionExtensions.WaitForGC(references.ToArray()); } +#endif class PageTypes : IEnumerable { From 405a994d17d4d7af4441e4d19b5385ea45e9a8d9 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Tue, 4 Aug 2026 19:14:45 +0200 Subject: [PATCH 24/27] [release/11.0.1xx-preview7] Stabilize Android button rotation screenshot Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9be49656-7117-4235-9d96-404779ab6b16 --- .../tests/TestCases.Shared.Tests/Tests/Issues/Issue22306.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue22306.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue22306.cs index a5e9bfeb3ea3..a95de598ed7f 100644 --- a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue22306.cs +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue22306.cs @@ -46,7 +46,12 @@ public void ButtonsLayoutResolveWhenParentSizeChanges() App.SetOrientationPortrait(); WaitForAllElements(); // Cannot use the original screenshot as the black bar on bottom is not as dark after rotation +#if ANDROID + // Android can consistently rerasterize the same post-rotation layout with minor antialiasing differences. + VerifyScreenshot(TestContext.CurrentContext.Test.MethodName + "Original2", tolerance: 2, retryTimeout: TimeSpan.FromSeconds(2)); +#else VerifyScreenshot(TestContext.CurrentContext.Test.MethodName + "Original2", tolerance: 0.5, retryTimeout: TimeSpan.FromSeconds(2)); +#endif } finally { From b7e949d1f6bda78eb4ac78f469451fc89670594f Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Tue, 4 Aug 2026 19:26:40 +0200 Subject: [PATCH 25/27] Make image deadlock test fail within timeout Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9be49656-7117-4235-9d96-404779ab6b16 --- src/Graphics/tests/DeviceTests/Tests/ImageTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Graphics/tests/DeviceTests/Tests/ImageTests.cs b/src/Graphics/tests/DeviceTests/Tests/ImageTests.cs index d6ec122cd4e0..4ec5597c04da 100644 --- a/src/Graphics/tests/DeviceTests/Tests/ImageTests.cs +++ b/src/Graphics/tests/DeviceTests/Tests/ImageTests.cs @@ -126,9 +126,10 @@ void ScaleAndWait() else DispatchQueue.MainQueue.DispatchSync(ScaleAndWait); + Assert.True(completedWhileMainThreadWasBlocked, "ScaleImage must not synchronously depend on main-thread progress."); + using var scaled = await scaleTask; - Assert.True(completedWhileMainThreadWasBlocked, "ScaleImage must not synchronously depend on main-thread progress."); Assert.Equal(10, (int)scaled.CGImage.Width); Assert.Equal(5, (int)scaled.CGImage.Height); } From 67525bdd55fcbc9012a4db2660d35661ed69a208 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Tue, 4 Aug 2026 19:26:39 +0200 Subject: [PATCH 26/27] Address Android screenshot tolerance review Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9be49656-7117-4235-9d96-404779ab6b16 --- .../TestCases.Shared.Tests/Tests/Issues/Issue22306.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue22306.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue22306.cs index a95de598ed7f..07edf36c00cf 100644 --- a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue22306.cs +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue22306.cs @@ -46,12 +46,9 @@ public void ButtonsLayoutResolveWhenParentSizeChanges() App.SetOrientationPortrait(); WaitForAllElements(); // Cannot use the original screenshot as the black bar on bottom is not as dark after rotation -#if ANDROID // Android can consistently rerasterize the same post-rotation layout with minor antialiasing differences. - VerifyScreenshot(TestContext.CurrentContext.Test.MethodName + "Original2", tolerance: 2, retryTimeout: TimeSpan.FromSeconds(2)); -#else - VerifyScreenshot(TestContext.CurrentContext.Test.MethodName + "Original2", tolerance: 0.5, retryTimeout: TimeSpan.FromSeconds(2)); -#endif + var finalPortraitTolerance = _testDevice == TestDevice.Android ? 2 : 0.5; + VerifyScreenshot(TestContext.CurrentContext.Test.MethodName + "Original2", tolerance: finalPortraitTolerance, retryTimeout: TimeSpan.FromSeconds(2)); } finally { From 83da465424b64351a923ecfbb01048a68cbffa14 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Tue, 4 Aug 2026 22:04:55 +0200 Subject: [PATCH 27/27] Harden alert test and Pester trigger Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9be49656-7117-4235-9d96-404779ab6b16 --- .github/workflows/powershell-script-tests.yml | 3 ++- .../tests/TestCases.HostApp/Issues/Issue33287.cs | 9 +++++---- .../TestCases.Shared.Tests/Tests/Issues/Issue33287.cs | 10 +++++----- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/.github/workflows/powershell-script-tests.yml b/.github/workflows/powershell-script-tests.yml index e44491fdb299..475806cddcd6 100644 --- a/.github/workflows/powershell-script-tests.yml +++ b/.github/workflows/powershell-script-tests.yml @@ -1,4 +1,4 @@ -# Pester regression gate for `.github/scripts/**`. +# Pester regression gate for `.github/scripts/**` and the scripts covered by those tests. # # Why this exists: the automation under `.github/scripts` ships a large Pester # suite (transport gating, safe-output expectation reconciliation, milestone @@ -19,6 +19,7 @@ on: pull_request: paths: - '.github/scripts/**' + - 'eng/scripts/Set-ScreenResolution.ps1' - '.github/workflows/powershell-script-tests.yml' workflow_dispatch: diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue33287.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue33287.cs index 240407a2b17f..e6843f219da5 100644 --- a/src/Controls/tests/TestCases.HostApp/Issues/Issue33287.cs +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue33287.cs @@ -69,7 +69,7 @@ public Issue33287SecondPage(Action updateStatus) } }; - async void OnPropertyChanged(object sender, PropertyChangedEventArgs e) + void OnPropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName != nameof(Window) || Window is not null) return; @@ -77,9 +77,10 @@ async void OnPropertyChanged(object sender, PropertyChangedEventArgs e) PropertyChanged -= OnPropertyChanged; updateStatus("Page detached"); - // Request the alert before handler teardown changes IsPlatformEnabled. - await DisplayAlertAsync("Test Alert", "This alert was delayed", "OK"); - updateStatus("Alert request completed"); + // The original NRE occurs synchronously while creating the alert request. + // A detached page may keep the returned task pending until it is reattached. + _ = DisplayAlertAsync("Test Alert", "This alert was delayed", "OK"); + updateStatus("Alert request returned"); } } } diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33287.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33287.cs index eab09a8c2a0c..15fa1959d19f 100644 --- a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33287.cs +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33287.cs @@ -16,23 +16,23 @@ public void DisplayAlertAsyncShouldNotCrashWhenPageUnloaded() { App.WaitForElement("NavigateButton"); - // Navigate to second page (starts a 2-second delayed DisplayAlertAsync) + // Navigate to the second page. App.Tap("NavigateButton"); // Wait for second page to appear, then go back immediately App.WaitForElement("GoBackButton"); App.Tap("GoBackButton"); - // Back on the main page, wait until the detached page's alert request completes. - // Without the fix the NRE crashes the app and this status is never updated. + // Back on the main page, wait until the detached page creates the alert request. + // Without the fix the synchronous NRE crashes the app before this status is updated. App.WaitForElement("MainPageLabel"); Assert.That( App.WaitForTextToBePresentInElement( "AlertStatusLabel", - "Alert request completed", + "Alert request returned", timeout: TimeSpan.FromSeconds(10)), Is.True, - "The detached page's alert request should complete"); + "The detached page should create the alert request without crashing"); // Verify the app is still alive and responsive after the alert request on the detached page. // Without the fix the app process is dead and this call will throw/timeout.