diff --git a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerScrollViewportPolicy.swift b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerScrollViewportPolicy.swift index a2dad024e4..d6921c5269 100644 --- a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerScrollViewportPolicy.swift +++ b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerScrollViewportPolicy.swift @@ -193,7 +193,7 @@ extension RunnerTests { ) -> RunnerScrollViewport { #if os(iOS) // Every scroll reports its decision, including the two ways it avoids reading the keyboard at - // all: a policy that forbids the probe, and a probe that finds no keyboard. + // all: unavailable accessibility health, and a probe that finds no keyboard. guard context.allowsKeyboardProbe else { logScrollViewport(decision: "probeSkipped", keyboardMinY: nil, swipeHeight: context.referenceFrame.height, context: context) return ScrollViewportPolicy.frames(referenceFrame: context.referenceFrame, clip: .unobstructed) @@ -230,8 +230,8 @@ extension RunnerTests { #if os(iOS) /// The #2500 diagnostic for a scroll that reports no travel: whether the swipe was clipped, and - /// whether the keyboard probe was even permitted. `axHealth` is the first thing to read, because a - /// policy that skipped the probe looks exactly like a keyboard that was never found. + /// whether the keyboard probe was even permitted. `axHealth` is the first thing to read, because + /// a health that skips the probe looks exactly like a keyboard that was never found. private func logScrollViewport( decision: String, keyboardMinY: Double?, @@ -239,9 +239,8 @@ extension RunnerTests { context: SynthesizedCoordinateContext ) { NSLog( - "AGENT_DEVICE_RUNNER_SCROLL_VIEWPORT kind=scroll axHealth=%@ keyboardPolicy=%@ decision=%@ keyboardMinY=%@ swipeHeight=%.1f", + "AGENT_DEVICE_RUNNER_SCROLL_VIEWPORT kind=scroll axHealth=%@ decision=%@ keyboardMinY=%@ swipeHeight=%.1f", context.accessibilityHealth.rawValue, - context.keyboardPolicy.rawValue, decision, keyboardMinY.map { String(format: "%.1f", $0) } ?? "none", swipeHeight diff --git a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift index c8c622cfe6..56041533a4 100644 --- a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +++ b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift @@ -76,10 +76,7 @@ extension RunnerTests { var fallback: GestureFallback? if command.synthesized == true { let policyKind = SynthesizedGesturePolicyKind.coordinateTap - let context = synthesizedCoordinateContext( - app: activeApp, - policy: synthesizedGesturePolicy(policyKind) - ) + let context = synthesizedCoordinateContext(app: activeApp) switch performSynthesizedGesture(activeApp, kind: policyKind, context: context, synthesize: { synthesizedTapAt( app: activeApp, @@ -169,10 +166,7 @@ extension RunnerTests { var fallback: GestureFallback? if command.synthesized == true { let policyKind = SynthesizedGesturePolicyKind.coordinateTap - let context = synthesizedCoordinateContext( - app: activeApp, - policy: synthesizedGesturePolicy(policyKind) - ) + let context = synthesizedCoordinateContext(app: activeApp) switch performSynthesizedGesture(activeApp, kind: policyKind, context: context, synthesize: { synthesizedTapAt(app: activeApp, x: x, y: y, context: context) }) { @@ -233,10 +227,7 @@ extension RunnerTests { let fallback: GestureFallback? #if os(iOS) let policyKind = SynthesizedGesturePolicyKind.coordinateLongPress - let context = synthesizedCoordinateContext( - app: activeApp, - policy: synthesizedGesturePolicy(policyKind) - ) + let context = synthesizedCoordinateContext(app: activeApp) switch performSynthesizedGesture(activeApp, kind: policyKind, context: context, synthesize: { synthesizedLongPressAt(app: activeApp, x: x, y: y, durationMs: durationMs, context: context) }) { @@ -287,11 +278,7 @@ extension RunnerTests { else { return invalidScrollDirectionResponse(commandName: "scroll") } - let scrollPolicyKind = SynthesizedGesturePolicyKind.scroll - guard let scrollContext = synthesizedCoordinateContext( - app: activeApp, - policy: synthesizedGesturePolicy(scrollPolicyKind) - ) else { + guard let scrollContext = synthesizedCoordinateContext(app: activeApp) else { return Response( ok: false, error: ErrorPayload(message: "scroll could not resolve a usable interaction frame") diff --git a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Navigation.swift b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Navigation.swift index 075ace7bb7..4bb91cab25 100644 --- a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Navigation.swift +++ b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Navigation.swift @@ -81,10 +81,7 @@ extension RunnerTests { @MainActor private func tapResolvedNavigationBackControl(app: XCUIApplication, frame: CGRect) -> InAppBackOutcome { guard SnapshotGeometry.isPositiveFinite(frame) else { return .unavailable } - let context = synthesizedCoordinateContext( - app: app, - policy: synthesizedGesturePolicy(.coordinateTap) - ) + let context = synthesizedCoordinateContext(app: app) guard dispatchNavigationTap(app: app, point: CGPoint(x: frame.midX, y: frame.midY), context: context) else { return .unavailable } @@ -226,7 +223,6 @@ extension RunnerTests { #if os(iOS) guard let context = synthesizedCoordinateContext( app: app, - policy: synthesizedGesturePolicy(.coordinateTap), windowQueryRejected: windowQueryRejected ), let point = Self.topLeadingNavigationFallbackPoint( in: context.referenceFrame, diff --git a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScrollDragExecution.swift b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScrollDragExecution.swift index ea7c8761ba..43655770ac 100644 --- a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScrollDragExecution.swift +++ b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScrollDragExecution.swift @@ -141,8 +141,7 @@ extension RunnerTests { profile: SynthesizedDragProfile ) -> Response? { #if os(iOS) - let policy = synthesizedGesturePolicy(policyKind) - let context = context ?? synthesizedCoordinateContext(app: activeApp, policy: policy) + let context = context ?? synthesizedCoordinateContext(app: activeApp) guard let plan = axFreeSynthesizedDragPlan( app: activeApp, x: x, @@ -162,12 +161,13 @@ extension RunnerTests { ) } let durationMs = min(max(durationMs, 16), 10000) - let dragFrame = axFreeDragVisualizationFrame( + let dragFrame = DragVisualizationFrame( x: plan.points.x, y: plan.points.y, x2: plan.points.x2, y2: plan.points.y2, - referenceFrame: plan.referenceFrame + referenceWidth: Double(plan.referenceFrame.width), + referenceHeight: Double(plan.referenceFrame.height) ) switch performSynthesizedGesture(activeApp, kind: policyKind, context: plan.context, synthesize: { synthesizedDragAt( diff --git a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedGesturePolicy.swift b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedGesturePolicy.swift index 63ca4591f7..f78e61809c 100644 --- a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedGesturePolicy.swift +++ b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedGesturePolicy.swift @@ -13,47 +13,28 @@ enum RunnerAccessibilityHealth: String, Equatable { case unavailable } -enum SynthesizedKeyboardPolicy: String, Equatable, Hashable { - case never - case requiredWhenAvailable - - func allowsProbe(accessibilityHealth: RunnerAccessibilityHealth) -> Bool { - switch self { - case .never: - return false - case .requiredWhenAvailable: - return accessibilityHealth != .unavailable - } - } -} - -enum SynthesizedFallbackPolicy: String, Equatable, Hashable { - case privateSynthesisRequired - case xctestCoordinateWhenAccessibilityAvailable - case xctestCoordinateAllowed - - func allowsXCTestCoordinateFallback(accessibilityHealth: RunnerAccessibilityHealth) -> Bool { - switch self { - case .privateSynthesisRequired: - return false - case .xctestCoordinateWhenAccessibilityAvailable: - return accessibilityHealth != .unavailable - case .xctestCoordinateAllowed: - return true - } - } -} - enum SynthesizedGesturePolicyKind: String, Equatable, Hashable { case coordinateTap case coordinateLongPress case scroll case synthesizedDrag -} -struct SynthesizedGesturePolicy: Equatable, Hashable { - let keyboardPolicy: SynthesizedKeyboardPolicy - let fallbackPolicy: SynthesizedFallbackPolicy + /// Whether the call site's XCTest coordinate gesture may run after private synthesis fails. + /// Scroll's swipe is viewport-center-symmetric, so an unverified dispatch cannot be told apart + /// from one that never reached the view, and it keeps the refusal. A coordinate long press + /// resolves the app frame through the tree, so on a tree XCTest cannot serialize that read fails + /// after its retries and raises an exception that ends the runner test; its fallback needs a tree + /// that answers. + func allowsXCTestCoordinateFallback(accessibilityHealth: RunnerAccessibilityHealth) -> Bool { + switch self { + case .coordinateTap: + return true + case .coordinateLongPress, .synthesizedDrag: + return accessibilityHealth != .unavailable + case .scroll: + return false + } + } } struct SynthesizedCoordinateContext { @@ -63,54 +44,22 @@ struct SynthesizedCoordinateContext { /// sole-screen route: XCTest could not resolve the window, `referenceFrame` is the device's only /// screen, and the record is routed to that screen. let resolvedWindow: XCUIElement? - let keyboardPolicy: SynthesizedKeyboardPolicy let accessibilityHealth: RunnerAccessibilityHealth func withReferenceFrame(_ frame: CGRect) -> SynthesizedCoordinateContext { SynthesizedCoordinateContext( referenceFrame: frame, resolvedWindow: resolvedWindow, - keyboardPolicy: keyboardPolicy, accessibilityHealth: accessibilityHealth ) } + /// The keyboard probe is not free — `visibleKeyboardFrame` resolves `app.keyboards.firstMatch` + /// with a live AX fetch — so `.unavailable` skips it: there the fetch is known not to answer, + /// and a gesture that needs the frame fails open on a missing one. `.unknown` still probes; + /// skipping it there left the first scroll of a session swiping under the keys. var allowsKeyboardProbe: Bool { - keyboardPolicy.allowsProbe(accessibilityHealth: accessibilityHealth) - } -} - -func synthesizedGesturePolicy(_ kind: SynthesizedGesturePolicyKind) -> SynthesizedGesturePolicy { - switch kind { - case .coordinateTap: - return SynthesizedGesturePolicy( - keyboardPolicy: .never, - fallbackPolicy: .xctestCoordinateAllowed - ) - case .coordinateLongPress: - // XCTest's coordinate press resolves the app frame through the tree. On a tree XCTest cannot - // serialize, that read fails after its retries and the press raises an exception that ends the - // runner test, so the fallback needs a tree that answers. - return SynthesizedGesturePolicy( - keyboardPolicy: .never, - fallbackPolicy: .xctestCoordinateWhenAccessibilityAvailable - ) - case .scroll: - // Scroll places a viewport-center-symmetric swipe, so it cannot tell a keyboard-struck swipe - // from a scroll that reached the edge without reading the live keyboard frame (#2500). The - // probe is not free — `visibleKeyboardFrame` resolves `app.keyboards.firstMatch` with a live AX - // fetch — but skipping it on `.unknown` left the first scroll of a session swiping under the - // keys, which is the failure this command exists to avoid. `.unavailable` still skips it: there - // the fetch is known not to answer, and `ScrollViewportPolicy` fails open on a missing frame. - return SynthesizedGesturePolicy( - keyboardPolicy: .requiredWhenAvailable, - fallbackPolicy: .privateSynthesisRequired - ) - case .synthesizedDrag: - return SynthesizedGesturePolicy( - keyboardPolicy: .requiredWhenAvailable, - fallbackPolicy: .xctestCoordinateWhenAccessibilityAvailable - ) + accessibilityHealth != .unavailable } } @@ -167,13 +116,13 @@ extension RunnerTests { steps: [SequenceStep], app: XCUIApplication ) -> SynthesizedCoordinateContext? { - guard let kind = steps.lazy.compactMap(synthesizedPolicyKind(forSequenceStep:)).first else { + guard steps.lazy.contains(where: { synthesizedPolicyKind(forSequenceStep: $0) != nil }) else { return nil } - return synthesizedCoordinateContext(app: app, policy: synthesizedGesturePolicy(kind)) + return synthesizedCoordinateContext(app: app) } - /// `context` is nil when no window frame resolved; `kind`'s fallback policy then reads the + /// `context` is nil when no window frame resolved; `kind`'s fallback decision then reads the /// runner's current accessibility health. @MainActor func performSynthesizedGesture( @@ -192,7 +141,7 @@ extension RunnerTests { logSynthesizedGesturePolicyDecision(kind: kind, context: context, fallbackAttempted: false) return .performed(timing: timing) } - let fallbackAllowed = synthesizedGesturePolicy(kind).fallbackPolicy.allowsXCTestCoordinateFallback( + let fallbackAllowed = kind.allowsXCTestCoordinateFallback( accessibilityHealth: context?.accessibilityHealth ?? mainOwned.accessibilityHealth ) logSynthesizedGesturePolicyDecision( @@ -218,7 +167,7 @@ extension RunnerTests { fallbackAttempted: fallbackAttempted ) // The same decision for the same gesture kind on every command is one line; a changed policy - // (AX health, keyboard, fallback) is a new one. + // (AX health or the fallback decision) is a new one. if lastLoggedGesturePolicyLines[kind] != line { lastLoggedGesturePolicyLines[kind] = line runnerMarkerWriter(line) @@ -235,12 +184,9 @@ extension RunnerTests { return "AGENT_DEVICE_RUNNER_SYNTHESIZED_GESTURE_POLICY kind=\(kind.rawValue)" + " context=unavailable fallbackAttempted=\(fallbackAttempted)" } - let fallbackPolicy = synthesizedGesturePolicy(kind).fallbackPolicy return "AGENT_DEVICE_RUNNER_SYNTHESIZED_GESTURE_POLICY kind=\(kind.rawValue)" - + " axHealth=\(context.accessibilityHealth.rawValue) frameSource=window" - + " keyboardPolicy=\(context.keyboardPolicy.rawValue)" - + " fallbackPolicy=\(fallbackPolicy.rawValue)" - + " fallbackAllowed=\(fallbackPolicy.allowsXCTestCoordinateFallback(accessibilityHealth: context.accessibilityHealth))" + + " axHealth=\(context.accessibilityHealth.rawValue)" + + " fallbackAllowed=\(kind.allowsXCTestCoordinateFallback(accessibilityHealth: context.accessibilityHealth))" + " fallbackAttempted=\(fallbackAttempted)" } } diff --git a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedInteraction.swift b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedInteraction.swift index bcbaf43e14..703c9c22eb 100644 --- a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedInteraction.swift +++ b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedInteraction.swift @@ -21,10 +21,7 @@ extension RunnerTests { ) } let orientation = Int(RunnerSynthesizedGesture.interfaceOrientation(forApplication: app)) - guard let context = context ?? synthesizedCoordinateContext( - app: app, - policy: synthesizedGesturePolicy(.synthesizedDrag) - ) else { + guard let context = context ?? synthesizedCoordinateContext(app: app) else { return .unsupported( message: "synthesized coordinate drag could not resolve an app window with a finite screen frame", hint: "Retry after the app is foregrounded, or use a plain screenshot to choose coordinates." @@ -107,10 +104,7 @@ extension RunnerTests { ) } let orientation = Int(RunnerSynthesizedGesture.interfaceOrientation(forApplication: app)) - guard let context = context ?? synthesizedCoordinateContext( - app: app, - policy: synthesizedGesturePolicy(.coordinateTap) - ) else { + guard let context = context ?? synthesizedCoordinateContext(app: app) else { return .unsupported( message: "synthesized coordinate tap could not resolve an app window with a finite screen frame", hint: "Retry after the app is foregrounded, or use a plain screenshot to choose coordinates." @@ -348,10 +342,7 @@ extension RunnerTests { @MainActor func gestureViewportFrame(app: XCUIApplication) -> CGRect { #if os(iOS) - guard let context = synthesizedCoordinateContext( - app: app, - policy: synthesizedGesturePolicy(.synthesizedDrag) - ) else { + guard let context = synthesizedCoordinateContext(app: app) else { return frameAvoidingKeyboard(app: app, frame: app.frame) } // The sole-screen route means the windows query just failed, so a keyboard probe would too. @@ -396,23 +387,15 @@ extension RunnerTests { context: SynthesizedCoordinateContext? = nil ) -> SynthesizedDragPlan? { #if os(iOS) - let context = context ?? synthesizedCoordinateContext( - app: app, - policy: synthesizedGesturePolicy(.synthesizedDrag) - ) + let context = context ?? synthesizedCoordinateContext(app: app) guard x.isFinite, y.isFinite, x2.isFinite, y2.isFinite, let context else { return nil } - let points = keyboardAvoidingSynthesizedDragPoints( - app: app, - x: x, - y: y, - x2: x2, - y2: y2, - context: context - ) + let points = context.allowsKeyboardProbe + ? keyboardAvoidingDragPoints(app: app, x: x, y: y, x2: x2, y2: y2) + : DragPoints(x: x, y: y, x2: x2, y2: y2) return SynthesizedDragPlan( points: points, context: context @@ -422,23 +405,6 @@ extension RunnerTests { #endif } - func axFreeDragVisualizationFrame( - x: Double, - y: Double, - x2: Double, - y2: Double, - referenceFrame: CGRect - ) -> DragVisualizationFrame { - return DragVisualizationFrame( - x: x, - y: y, - x2: x2, - y2: y2, - referenceWidth: Double(referenceFrame.width), - referenceHeight: Double(referenceFrame.height) - ) - } - func logSynthesizedDispatch( kind: String, start: CGPoint, @@ -478,7 +444,6 @@ extension RunnerTests { @MainActor func synthesizedCoordinateContext( app: XCUIApplication, - policy: SynthesizedGesturePolicy, windowQueryRejected: Bool = false ) -> SynthesizedCoordinateContext? { #if os(iOS) @@ -508,7 +473,6 @@ extension RunnerTests { return SynthesizedCoordinateContext( referenceFrame: referenceFrame, resolvedWindow: window, - keyboardPolicy: policy.keyboardPolicy, accessibilityHealth: health ) #else @@ -581,24 +545,6 @@ extension RunnerTests { } } - func keyboardAvoidingSynthesizedDragPoints( - app: XCUIApplication, - x: Double, - y: Double, - x2: Double, - y2: Double, - context: SynthesizedCoordinateContext - ) -> DragPoints { -#if os(iOS) - guard context.allowsKeyboardProbe else { - return DragPoints(x: x, y: y, x2: x2, y2: y2) - } - return keyboardAvoidingDragPoints(app: app, x: x, y: y, x2: x2, y2: y2) -#else - return DragPoints(x: x, y: y, x2: x2, y2: y2) -#endif - } - func swipe(app: XCUIApplication, direction: String) -> DragVisualizationFrame? { if performTvRemoteSwipeIfAvailable(direction: direction) { let frame = resolvedTouchReferenceFrame(app: app, appFrame: app.frame) diff --git a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TypeExecution.swift b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TypeExecution.swift index 6017473904..be0efb4578 100644 --- a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TypeExecution.swift +++ b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TypeExecution.swift @@ -42,10 +42,7 @@ extension RunnerTests { return textInputProbeUnavailableResponse() } let policyKind = SynthesizedGesturePolicyKind.coordinateTap - let context = synthesizedCoordinateContext( - app: activeApp, - policy: synthesizedGesturePolicy(policyKind) - ) + let context = synthesizedCoordinateContext(app: activeApp) switch performSynthesizedGesture(activeApp, kind: policyKind, context: context, synthesize: { synthesizedTapAt(app: activeApp, x: x, y: y, context: context) }) { diff --git a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+SynthesizedGesturePolicyTests.swift b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+SynthesizedGesturePolicyTests.swift index 1a33f34ae2..e5cf577f63 100644 --- a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+SynthesizedGesturePolicyTests.swift +++ b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+SynthesizedGesturePolicyTests.swift @@ -26,79 +26,42 @@ extension RunnerTests { #if AGENT_DEVICE_RUNNER_UNIT_TESTS extension RunnerTests { - func testSynthesizedFallbackPolicyRequiresPrivateSynthesisForScrollWhenAxUnavailableOrUnknown() { - XCTAssertFalse( - SynthesizedFallbackPolicy.privateSynthesisRequired - .allowsXCTestCoordinateFallback(accessibilityHealth: .unavailable) - ) - XCTAssertFalse( - SynthesizedFallbackPolicy.privateSynthesisRequired - .allowsXCTestCoordinateFallback(accessibilityHealth: .unknown) - ) - XCTAssertFalse( - SynthesizedFallbackPolicy.privateSynthesisRequired - .allowsXCTestCoordinateFallback(accessibilityHealth: .healthy) - ) - } - - func testSynthesizedDragCoordinateFallbackAllowsUnknownButNotUnavailableAccessibility() { - XCTAssertTrue( - SynthesizedFallbackPolicy.xctestCoordinateWhenAccessibilityAvailable - .allowsXCTestCoordinateFallback(accessibilityHealth: .healthy) - ) - XCTAssertFalse( - SynthesizedFallbackPolicy.xctestCoordinateWhenAccessibilityAvailable - .allowsXCTestCoordinateFallback(accessibilityHealth: .unavailable) - ) - XCTAssertTrue( - SynthesizedFallbackPolicy.xctestCoordinateWhenAccessibilityAvailable - .allowsXCTestCoordinateFallback(accessibilityHealth: .unknown) - ) - } - - /// Keyboard-policy semantics only. Which command gets which policy is the table below; a probe - /// that is merely permitted still costs a live AX fetch, so the two questions stay separate. - func testSynthesizedKeyboardPolicyAllowsProbeOnlyWhenAccessibilityPermitsIt() { - XCTAssertFalse(SynthesizedKeyboardPolicy.never.allowsProbe(accessibilityHealth: .healthy)) - XCTAssertTrue( - SynthesizedKeyboardPolicy.requiredWhenAvailable - .allowsProbe(accessibilityHealth: .unknown) - ) - XCTAssertFalse( - SynthesizedKeyboardPolicy.requiredWhenAvailable - .allowsProbe(accessibilityHealth: .unavailable) - ) + /// The whole fallback decision: which gesture kind may take the call site's XCTest coordinate + /// gesture, at each accessibility health. + func testSynthesizedCoordinateFallbackFollowsItsKindAndAccessibilityHealth() { + let cases: [(kind: SynthesizedGesturePolicyKind, health: RunnerAccessibilityHealth, allowed: Bool)] = [ + (.coordinateTap, .unknown, true), + (.coordinateTap, .healthy, true), + (.coordinateTap, .unavailable, true), + (.coordinateLongPress, .unknown, true), + (.coordinateLongPress, .healthy, true), + (.coordinateLongPress, .unavailable, false), + (.scroll, .unknown, false), + (.scroll, .healthy, false), + (.scroll, .unavailable, false), + (.synthesizedDrag, .unknown, true), + (.synthesizedDrag, .healthy, true), + (.synthesizedDrag, .unavailable, false), + ] + for testCase in cases { + XCTAssertEqual( + testCase.kind.allowsXCTestCoordinateFallback(accessibilityHealth: testCase.health), + testCase.allowed, + "kind=\(testCase.kind.rawValue) axHealth=\(testCase.health.rawValue)" + ) + } } - func testSynthesizedGesturePoliciesMatchCommandContracts() { - XCTAssertEqual( - synthesizedGesturePolicy(.coordinateTap), - SynthesizedGesturePolicy( - keyboardPolicy: .never, - fallbackPolicy: .xctestCoordinateAllowed - ) - ) - XCTAssertEqual( - synthesizedGesturePolicy(.coordinateLongPress), - SynthesizedGesturePolicy( - keyboardPolicy: .never, - fallbackPolicy: .xctestCoordinateWhenAccessibilityAvailable - ) - ) - XCTAssertEqual( - synthesizedGesturePolicy(.scroll), - SynthesizedGesturePolicy( - keyboardPolicy: .requiredWhenAvailable, - fallbackPolicy: .privateSynthesisRequired - ) - ) - XCTAssertEqual( - synthesizedGesturePolicy(.synthesizedDrag), - SynthesizedGesturePolicy( - keyboardPolicy: .requiredWhenAvailable, - fallbackPolicy: .xctestCoordinateWhenAccessibilityAvailable - ) - ) + func testKeyboardProbeIsAllowedAtEveryAccessibilityHealthExceptUnavailable() { + let cases: [(health: RunnerAccessibilityHealth, allowsProbe: Bool)] = [ + (.unknown, true), + (.healthy, true), + (.unavailable, false), + ] + for testCase in cases { + let context = synthesizedGestureTestContext(accessibilityHealth: testCase.health) + XCTAssertEqual(context.allowsKeyboardProbe, testCase.allowsProbe, "axHealth=\(testCase.health.rawValue)") + } } func testCoordinateTapTextInputProbeSkipsPenalizedXCTestChannel() { @@ -199,7 +162,6 @@ extension RunnerTests { SynthesizedCoordinateContext( referenceFrame: CGRect(x: 0, y: 0, width: 390, height: 844), resolvedWindow: app.windows.firstMatch, - keyboardPolicy: .never, accessibilityHealth: accessibilityHealth ) } diff --git a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+SynthesizedInteractionTests.swift b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+SynthesizedInteractionTests.swift index 86ce04521b..7ee8056f4c 100644 --- a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+SynthesizedInteractionTests.swift +++ b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+SynthesizedInteractionTests.swift @@ -48,13 +48,6 @@ extension RunnerTests { } } - /// Whether the sole-screen route is open for the launched app: an iPhone with one screen always, - /// an iPad only while the app proves it fills that screen. `RunnerResolveSoleScreen` owns the rule; - /// a test reads its answer rather than restating a device list. - private var showsAppsOnlyFullScreen: Bool { - soleScreenRouteIsOpen(for: app) - } - /// On a tree XCTest cannot serialize, XCTest's coordinate press reads an infinite app frame and /// raises an exception that ends the runner test. A long press needs no tree: when the sole-screen /// route is open for the app it is synthesized on that screen, and anything else keeps the XCTest @@ -84,7 +77,7 @@ extension RunnerTests { XCTAssertTrue(longPressResponse.ok, String(describing: longPressResponse.error)) XCTAssertEqual(longPressResponse.data?.message, "long pressed") XCTAssertTrue(pressSeriesResponse.ok, String(describing: pressSeriesResponse.error)) - if showsAppsOnlyFullScreen { + if soleScreenRouteIsOpen(for: app) { XCTAssertEqual(longPressCoordinatePresses, 0) XCTAssertNil(longPressResponse.data?.gestureFallback) XCTAssertEqual(pressSeriesResponse.data?.completedSteps, 2) @@ -123,7 +116,7 @@ extension RunnerTests { let response = try executeOnMainPrepared(command: command, activeApp: app) - if showsAppsOnlyFullScreen { + if soleScreenRouteIsOpen(for: app) { XCTAssertTrue(response.ok, String(describing: response.error)) let screen = UIScreen.main.bounds XCTAssertEqual(response.data?.x, 0)