Skip to content

fix(ios-runner): measure the sole-screen frame on the XCTest screen object - #3949

Closed
strzhao wants to merge 1 commit into
callstack:mainfrom
strzhao:fix/ios-runner-sole-screen-frame
Closed

strzhao wants to merge 1 commit into
callstack:mainfrom
strzhao:fix/ios-runner-sole-screen-frame

Conversation

@strzhao

@strzhao strzhao commented Oct 25, 2026

Copy link
Copy Markdown

Root cause

testInAppBackVerifiesSoleScreenWhenXCTestCannotReadFramesOrEnumerate and testInAppBackStillAsksForWindowsWhereNoSoleScreenAnswers have failed the nightly since 10-23 (#3943), the night after #3894 landed — the full-suite lane is the only place those two execute.

The UI-test runner process hosts no scene, so UIKit's UIScreen answers a placeholder panel there. Probe on the nightly's device shape (iPhone 17 Pro, iOS 26.5 simulator):

  • UIScreen.main.bounds → 320×480 points (nativeBounds 960×1440 @3x)
  • XCUIScreen private bounds → 1206×2622 px with private scale 3
  • public XCUIScreen.screenshot() → {402, 874} @3x — the real panel

soleScreenReferenceFrame derived its frame from UIScreen.main, so the window-less back fallback aimed at (28, 56) — the status bar, above the 62–106 pt bar band the fallback exists to hit. The tap changed nothing; the --agent-device-back-regression status stayed "launched". The placeholder sole screen also "answered", so the windows route was skipped, which is why the windows-routed test failed too.

Nightly evidence: AGENT_DEVICE_RUNNER_SYNTHESIZED_SOLE_SCREEN_ROUTE resolved={{0, 0}, {320, 480}}, then IN_APP_BACK_VISUAL_VERIFICATION … changed=no before #3894's assertions failed.

Fix

The bridge grows RunnerResolveSoleScreenPanelGeometry: the same sole-screen rule as the routing resolver, measuring the panel on the XCTest screen object the route gate already trusts, fail-closed through the exception catcher with the shared APP_SCREEN_UNRESOLVED log — a future Xcode that drops either private key refuses the route instead of aiming blind.

  • soleScreenReferenceFrame now measures through that resolver; UIScreen.main is out of the gesture path.
  • testGestureViewportOnAnUnserializableTreeIsTheSoleScreen pins the route answer to the resolver instead of the placeholder panel.
  • New testSoleScreenPanelGeometryMatchesTheCapturedPanel cross-checks the resolver against the one public panel answer in the process (a capture of the same screen).

Verification

Red first, on this simulator against ed798f9cf: both #3894 tests failed with the same signature as the nightly (resolved={{0,0},{320,480}}, 2 failures / 2 tests). After the fix, 10/10 sole-screen and navigation tests pass, including the two previously-red ones (resolved={{0,0},{402,874}}, changed=yes on the regression fixture, status "Back performed").

Fixes #3943

…bject

The runner process hosts no scene, so UIKit's UIScreen answers a
placeholder panel there — 960x1440 @3x (320x480 points) on the nightly's
iPhone 17 Pro — while the route gate and the capture path read the real
XCUIScreen. A reference frame derived from UIScreen.main therefore aimed
the window-less back fallback at (28, 56): the status bar, above the
62-106pt bar band the fallback exists to hit. The tap changed nothing,
both callstack#3894 nightly tests failed, and the windows route was skipped
because the placeholder sole screen 'answered'.

Probe evidence on this simulator (logged in the PR description):
UIScreen.main.bounds = 320x480, XCUIScreen KVC bounds = 1206x2622 with
KVC scale 3, and a public XCUIScreen screenshot at {402,874} @3x.

The bridge grows RunnerResolveSoleScreenPanelGeometry: the same
sole-screen rule as the routing resolver, fail-closed through the
exception catcher with the shared APP_SCREEN_UNRESOLVED log, so a
future Xcode that drops either private key refuses the route instead of
aiming blind. The gestureViewport unit test now pins the route answer to
that resolver, and a new test cross-checks the resolver against the
public capture of the same screen.

@cubic-dev-ai cubic-dev-ai Bot 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.

3 issues found across 4 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerXCTestEventBridge.h">

<violation number="1" location="apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerXCTestEventBridge.h:101">
P3: The new out-parameter is named as points-per-pixel, but it contains pixels-per-point and callers divide by it. Rename it to `pixelsPerPointScale` and update the comment so future callers do not invert the conversion.</violation>
</file>

<file name="apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerXCTestEventBridge.m">

<violation number="1" location="apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerXCTestEventBridge.m:286">
P2: The resolver accepts NaN or infinite scale and dimensions because it checks only `<= 0`, then reports success with invalid geometry. Reject all non-finite values with `isfinite` before returning success.</violation>
</file>

<file name="apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+SynthesizedInteractionTests.swift">

<violation number="1" location="apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+SynthesizedInteractionTests.swift:133">
P2: These expectations ignore the orientation transformation applied by `soleScreenReferenceFrame`, so the test fails whenever the device is landscape even though the route returns the correctly oriented viewport. Build the expected frame with the app's interface orientation before comparing both dimensions.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

readSize = bounds.size;
readScale = [[screens.firstObject valueForKey:@"scale"] doubleValue];
}];
if (exception != nil || readScale <= 0 || readSize.width <= 0 || readSize.height <= 0) {

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.

P2: The resolver accepts NaN or infinite scale and dimensions because it checks only <= 0, then reports success with invalid geometry. Reject all non-finite values with isfinite before returning success.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerXCTestEventBridge.m, line 286:

<comment>The resolver accepts NaN or infinite scale and dimensions because it checks only `<= 0`, then reports success with invalid geometry. Reject all non-finite values with `isfinite` before returning success.</comment>

<file context>
@@ -259,6 +259,45 @@ BOOL RunnerResolveApplicationScreen(
+    readSize = bounds.size;
+    readScale = [[screens.firstObject valueForKey:@"scale"] doubleValue];
+  }];
+  if (exception != nil || readScale <= 0 || readSize.width <= 0 || readSize.height <= 0) {
+    NSLog(
+      @"AGENT_DEVICE_RUNNER_APP_SCREEN_UNRESOLVED stage=sole-screen-geometry size=(%g,%g) scale=%g raised=%@",
</file context>
Suggested change
if (exception != nil || readScale <= 0 || readSize.width <= 0 || readSize.height <= 0) {
if (exception != nil || !isfinite(readScale) || readScale <= 0 || !isfinite(readSize.width) || readSize.width <= 0 || !isfinite(readSize.height) || readSize.height <= 0) {

XCTAssertEqual(response.data?.y, 0)
XCTAssertEqual(response.data?.x2, Double(screen.width))
XCTAssertEqual(response.data?.y2, Double(screen.height))
XCTAssertEqual(response.data?.x2, pixelSize.width / scale)

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.

P2: These expectations ignore the orientation transformation applied by soleScreenReferenceFrame, so the test fails whenever the device is landscape even though the route returns the correctly oriented viewport. Build the expected frame with the app's interface orientation before comparing both dimensions.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+SynthesizedInteractionTests.swift, line 133:

<comment>These expectations ignore the orientation transformation applied by `soleScreenReferenceFrame`, so the test fails whenever the device is landscape even though the route returns the correctly oriented viewport. Build the expected frame with the app's interface orientation before comparing both dimensions.</comment>

<file context>
@@ -121,11 +121,17 @@ extension RunnerTests {
       XCTAssertEqual(response.data?.y, 0)
-      XCTAssertEqual(response.data?.x2, Double(screen.width))
-      XCTAssertEqual(response.data?.y2, Double(screen.height))
+      XCTAssertEqual(response.data?.x2, pixelSize.width / scale)
+      XCTAssertEqual(response.data?.y2, pixelSize.height / scale)
     } else {
</file context>

// are guarded here and surface as a message instead of an exception reaching the caller.
FOUNDATION_EXPORT NSString * _Nullable RunnerResolveSoleScreenPanelGeometry(
CGSize *pixelSize,
CGFloat *pointsPerPixelScale

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.

P3: The new out-parameter is named as points-per-pixel, but it contains pixels-per-point and callers divide by it. Rename it to pixelsPerPointScale and update the comment so future callers do not invert the conversion.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerXCTestEventBridge.h, line 101:

<comment>The new out-parameter is named as points-per-pixel, but it contains pixels-per-point and callers divide by it. Rename it to `pixelsPerPointScale` and update the comment so future callers do not invert the conversion.</comment>

<file context>
@@ -90,6 +90,17 @@ FOUNDATION_EXPORT NSString * _Nullable RunnerResolveSoleScreen(
+// are guarded here and surface as a message instead of an exception reaching the caller.
+FOUNDATION_EXPORT NSString * _Nullable RunnerResolveSoleScreenPanelGeometry(
+  CGSize *pixelSize,
+  CGFloat *pointsPerPixelScale
+);
+
</file context>

@strzhao strzhao closed this Oct 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

XCTest Nightly: two in-app back runner tests fail on main since 10-23

1 participant