Skip to content

ios runner: navigation fallback helpers accept CGRect.infinite (tap point ≈ −9e307) #1812

Description

@thymikee

Two navigation helpers in the iOS runner accept CGRect.infinite as a valid frame. One of them turns it into a tap point at roughly -9e307; the other classifies it as sitting in the navigation header band.

Found by the first execution of the dark XCTest set (#1781 A7, PR #1789). Both tests live outside ios.yml's hand-written -only-testing: list, so nothing had ever run them.

Failures

Reproduced on Xcode 26.2 / iOS 26.2 Simulator (iPhone 16), full suite, 154 executed / 152 passed / 2 failed:

testTopLeadingNavigationFallbackPointRejectsInvalidFrame()
  XCTAssertNil failed: "(-8.988465674311579e+307, -8.988465674311579e+307)"

testTopNavigationControlFrameAcceptsOnlyHeaderBand()
  XCTAssertFalse failed

Both in apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Navigation.swift (tests at lines 185 and 205).

Root cause: the guards validate the size, never the origin

CGRect.infinite is { {-CGFLOAT_MAX/2, -CGFLOAT_MAX/2}, {CGFLOAT_MAX, CGFLOAT_MAX} }. Its width and height are CGFLOAT_MAXfinite and positive — so a size-only validity check waves it straight through. The absurd part of the rect is its origin, which neither helper looks at.

topLeadingNavigationFallbackPoint (line 102):

guard frame.width.isFinite, frame.height.isFinite, frame.width > 0, frame.height > 0 else {
  return nil
}
let xOffset = min(max(frame.width * 0.08, 28), 44)
return CGPoint(x: frame.minX + xOffset, y: frame.minY + yOffset)

With .infinite: the guard passes, xOffset clamps to 44, and frame.minX + 44 is -8.988465674311579e+307 — the 44 is entirely lost to floating-point precision. That is the exact value in the assertion output.

isTopNavigationControlFrame (line 84) has the same shape of guard, then:

let maxY = window.minY + min(max(window.height * 0.22, 96), 180)   // = 180 for a 430x932 window
return candidate.midY >= window.minY && candidate.midY <= maxY

CGRect.infinite.midY is -CGFLOAT_MAX/2 + CGFLOAT_MAX/2 = exactly 0.0, which lands inside [0, 180] → returns true.

The .zero half of the first test passes, because width > 0 does reject it. .infinite is the only invalid rect that slips through.

Impact

tapTopLeadingNavigationFallback feeds onScreenWindowFrame(app:) straight into topLeadingNavigationFallbackPoint. If that frame ever comes back as .infinite — an unresolved or not-yet-laid-out window — the runner synthesizes a tap at approximately (-9e307, -9e307) instead of declining to act. isTopNavigationControlFrame fails in the same direction: an unresolved element frame is treated as a top-navigation control.

Both are silent wrong-decision paths, not crashes.

Suggested fix

There is already a correct precedent in the same target — TapPointPolicy.isAllowed in apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTapPointPolicy.swift:

if windowFrame.isNull || windowFrame.isEmpty || windowFrame.isInfinite {

isInfinite is the API that detects exactly this rect. The navigation helpers should apply the same three-way check rather than a fourth hand-rolled spelling of "is this rect usable".

Worth extracting one shared validity predicate so there is a single definition, then checking whether any sibling geometry helper in the runner has the same size-only gap. Fix the helpers, not the tests — the assertions describe the intended contract, and the test names (RejectsInvalidFrame, AcceptsOnlyHeaderBand) say so.

Verifying

Needs a full-suite run; the PR lane's -only-testing: list does not include either test.

AGENT_DEVICE_XCUITEST_INCLUDE_UNIT_TESTS=1 \
  AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH=<scratch> \
  AGENT_DEVICE_XCUITEST_DESTINATION='generic/platform=iOS Simulator' \
  pnpm gate swift-runner-ios

xcodebuild test-without-building \
  -xctestrun <scratch>/Build/Products/*.xctestrun \
  -destination "platform=iOS Simulator,id=<udid>" \
  -skip-testing:AgentDeviceRunnerUITests/RunnerTests/testCommand \
  -resultBundlePath <scratch>/RunnerTests.xcresult

-skip-testing: on testCommand is required — it is the runner's server entry point and blocks for 24 hours.

Once #1789 lands, the XCTest Nightly lane runs this set every night and will report both failures until they are fixed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions