From 143a558f8447801ef40c2d8f0733f512aa49529b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sat, 10 Oct 2026 17:06:55 +0200 Subject: [PATCH 1/2] fix(apple-runner): accept the iOS 27 floating-bar back control outside the classic header band (#3333) On iOS 27.1 the SwiftUI floating toolbar hosts the back control as a _UIButtonBarButton with identifier BackButton at midY 194 in a 678pt window, below the classic header band (maxY 149). topNavigationBackElement matched it by keyword but isTopNavigationControlFrame rejected it, and the leading fallback then tapped empty space. Add isTopNavigationBackCandidateFrame: the classic band still admits any keyword match, and an exact BackButton identifier (UIKit's system back identity, exposed by XCUI) is additionally admitted anywhere in the upper 40% of the window. It is bounded rather than a bypass, so a deep content control is never tapped, and keyword-only matches such as a "back-link" button at the same position stay rejected. Ranking, InAppBackOutcome, the unknown-vs-unavailable split (#2728), and the macOS/tvOS branches are untouched. Verified on iPhone Duo / iOS 27.1: 01-settings.ad passes 7/7 steps in 6/6 runs, and back from General navigates to the root. Co-Authored-By: Claude Sonnet 5.5 --- .../RunnerTests+Navigation.swift | 26 +++++++++- .../RunnerTests+NavigationTests.swift | 49 +++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Navigation.swift b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Navigation.swift index 9cd790aaa5..f52ddf2820 100644 --- a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Navigation.swift +++ b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Navigation.swift @@ -3,6 +3,8 @@ import XCTest extension RunnerTests { static let navigationBackKeywords = ["back", "close", "cancel"] + static let systemBackButtonIdentifier = "BackButton" + static let floatingBarBackControlMaxYFraction: CGFloat = 0.4 static let navigationFallbackVerificationDelay: TimeInterval = 0.25 /// What one in-app `back` attempt concluded. `.unverified` carries the typed capture failure the @@ -88,7 +90,11 @@ extension RunnerTests { let candidates = app.buttons.matching(Self.navigationBackPredicate()).allElementsBoundByIndex.compactMap { element -> (XCUIElement, Int)? in guard element.exists, element.isHittable else { return nil } - guard Self.isTopNavigationControlFrame(element.frame, in: frame) else { return nil } + guard Self.isTopNavigationBackCandidateFrame( + element.frame, + in: frame, + identifier: element.identifier + ) else { return nil } guard let rank = Self.navigationBackControlRank( label: element.label, identifier: element.identifier @@ -132,6 +138,24 @@ extension RunnerTests { return candidate.midY >= window.minY && candidate.midY <= maxY } + /// UIKit names the system back control `BackButton`. That identity is the evidence that lets it sit + /// below the classic header band, where the iOS 27 floating toolbar places it (#3333); keyword-only + /// matches stay inside the band so content controls that merely say "back" are not tapped. + static func isTopNavigationBackCandidateFrame( + _ candidate: CGRect, + in window: CGRect, + identifier: String + ) -> Bool { + if isTopNavigationControlFrame(candidate, in: window) { return true } + guard identifier == systemBackButtonIdentifier, + SnapshotGeometry.isPositiveFinite(candidate), SnapshotGeometry.isPositiveFinite(window) + else { + return false + } + let maxY = window.minY + window.height * floatingBarBackControlMaxYFraction + return candidate.midY >= window.minY && candidate.midY <= maxY + } + static func topLeadingNavigationFallbackPoint(in frame: CGRect) -> CGPoint? { guard SnapshotGeometry.isPositiveFinite(frame) else { return nil diff --git a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+NavigationTests.swift b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+NavigationTests.swift index e18e91241f..9c717869b8 100644 --- a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+NavigationTests.swift +++ b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+NavigationTests.swift @@ -51,6 +51,55 @@ extension RunnerTests { XCTAssertFalse(Self.isTopNavigationControlFrame(.infinite, in: window)) } + func testTopNavigationBackCandidateAcceptsIOS27FloatingBarSystemBackButton() { + let window = CGRect(x: 0, y: 0, width: 466, height: 678) + let floatingBarBack = CGRect(x: 399, y: 175, width: 38, height: 38) + + XCTAssertFalse(Self.isTopNavigationControlFrame(floatingBarBack, in: window)) + XCTAssertTrue( + Self.isTopNavigationBackCandidateFrame(floatingBarBack, in: window, identifier: "BackButton") + ) + } + + func testTopNavigationBackCandidateRejectsKeywordOnlyMatchBelowHeaderBand() { + let window = CGRect(x: 0, y: 0, width: 466, height: 678) + let floatingBarBack = CGRect(x: 399, y: 175, width: 38, height: 38) + + XCTAssertFalse( + Self.isTopNavigationBackCandidateFrame(floatingBarBack, in: window, identifier: "back-link") + ) + XCTAssertFalse( + Self.isTopNavigationBackCandidateFrame(floatingBarBack, in: window, identifier: "") + ) + } + + func testTopNavigationBackCandidateBoundsSystemBackButtonToUpperWindow() { + let window = CGRect(x: 0, y: 0, width: 430, height: 932) + + XCTAssertFalse( + Self.isTopNavigationBackCandidateFrame( + CGRect(x: 20, y: 760, width: 72, height: 44), + in: window, + identifier: "BackButton" + ) + ) + XCTAssertFalse( + Self.isTopNavigationBackCandidateFrame(.infinite, in: window, identifier: "BackButton") + ) + } + + func testTopNavigationBackCandidateKeepsClassicHeaderBandForKeywordMatches() { + let window = CGRect(x: 0, y: 0, width: 430, height: 932) + + XCTAssertTrue( + Self.isTopNavigationBackCandidateFrame( + CGRect(x: 340, y: 84, width: 72, height: 44), + in: window, + identifier: "back-link" + ) + ) + } + func testNavigationVisualVerificationSeparatesNoChangeFromNoSample() { XCTAssertEqual( Self.navigationVisualObservation(before: Data([1, 2, 3]), after: Data([1, 2, 4])), From c5b1714c09579ff634dd23ef996a17224730d724 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sat, 10 Oct 2026 17:56:23 +0200 Subject: [PATCH 2/2] test(apple-runner): pin both sides of the floating-bar back bound Co-Authored-By: Claude Opus 5.5 --- .../UnitTests/RunnerTests+NavigationTests.swift | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+NavigationTests.swift b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+NavigationTests.swift index 9c717869b8..8b0ddf743a 100644 --- a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+NavigationTests.swift +++ b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+NavigationTests.swift @@ -74,11 +74,19 @@ extension RunnerTests { } func testTopNavigationBackCandidateBoundsSystemBackButtonToUpperWindow() { + // 40% of 932 is 372.8; the classic band ends at 180, so only the floating-bar bound decides. let window = CGRect(x: 0, y: 0, width: 430, height: 932) + XCTAssertTrue( + Self.isTopNavigationBackCandidateFrame( + CGRect(x: 20, y: 348, width: 44, height: 44), + in: window, + identifier: "BackButton" + ) + ) XCTAssertFalse( Self.isTopNavigationBackCandidateFrame( - CGRect(x: 20, y: 760, width: 72, height: 44), + CGRect(x: 20, y: 356, width: 44, height: 44), in: window, identifier: "BackButton" )