Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,63 @@ 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() {
// 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: 356, width: 44, 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])),
Expand Down
Loading