Files
callstack__agent-device/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedGesturePolicy.swift
T
Michał Pierzchała e58cbcdb5f refactor: colocate native platform sources under android/, apple/, linux/ (#1273)
Move the scattered root-level native projects into per-platform folders and drop
the now-redundant platform prefix:

- android-ime-helper/        -> android/ime-helper/
- android-multitouch-helper/ -> android/multitouch-helper/
- android-snapshot-helper/   -> android/snapshot-helper/
- apple-runner/              -> apple/runner/
- macos-helper/              -> apple/macos-helper/
- src/platforms/linux/atspi-dump.py -> linux/atspi-dump.py

Only repo source paths move. Identity surfaces stay frozen so no user's runner
cache is invalidated on upgrade: the derived-cache key hashes source paths
relative to AgentDeviceRunner and excludes packageVersion, and the
~/.agent-device/{apple-runner,macos-helper} namespaces, the
agent-device-android-*-helper artifact/manifest/protocol names, the
AgentDeviceRunner Xcode project, and the `prepare ios-runner` CLI command are
unchanged. Updates build/package scripts, CI, package.json files+scripts,
ignore/attr/fallow configs, runtime path resolvers, and test fixtures.

Also: re-base repo-root-relative refs inside the moved apple/runner for the
added nesting level (gated XCUITest fixture walk + two doc links), and clean the
legacy dist/apple-runner packaged output so the relocated runner can't
double-ship into the wholesale-included dist (with a regression test).
2026-07-15 21:47:38 +02:00

216 lines
6.6 KiB
Swift

import XCTest
// Runner-local policy for AX-free/private synthesized iOS gestures.
//
// This policy is intentionally separate from the TS interaction guarantee matrix:
// ADR 0011 models element-targeting guarantees, while this module models command
// paths that must keep scroll/drag/sequence usable when XCTest AX is unhealthy.
enum RunnerAccessibilityHealth: String, Equatable {
case unknown
case healthy
case unavailable
}
enum SynthesizedKeyboardPolicy: String, Equatable, Hashable {
case never
case whenAccessibilityHealthy
case requiredWhenAvailable
func allowsProbe(accessibilityHealth: RunnerAccessibilityHealth) -> Bool {
switch self {
case .never:
return false
case .whenAccessibilityHealthy:
return accessibilityHealth == .healthy
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 scroll
case synthesizedDrag
}
struct SynthesizedGesturePolicy: Equatable, Hashable {
let keyboardPolicy: SynthesizedKeyboardPolicy
let fallbackPolicy: SynthesizedFallbackPolicy
}
struct SynthesizedCoordinateContext {
let referenceFrame: CGRect
let keyboardPolicy: SynthesizedKeyboardPolicy
let fallbackPolicy: SynthesizedFallbackPolicy
let accessibilityHealth: RunnerAccessibilityHealth
func withReferenceFrame(_ frame: CGRect) -> SynthesizedCoordinateContext {
SynthesizedCoordinateContext(
referenceFrame: frame,
keyboardPolicy: keyboardPolicy,
fallbackPolicy: fallbackPolicy,
accessibilityHealth: accessibilityHealth
)
}
var allowsXCTestCoordinateFallback: Bool {
fallbackPolicy.allowsXCTestCoordinateFallback(accessibilityHealth: accessibilityHealth)
}
var allowsKeyboardProbe: Bool {
keyboardPolicy.allowsProbe(accessibilityHealth: accessibilityHealth)
}
}
func synthesizedGesturePolicy(_ kind: SynthesizedGesturePolicyKind) -> SynthesizedGesturePolicy {
switch kind {
case .coordinateTap:
return SynthesizedGesturePolicy(
keyboardPolicy: .never,
fallbackPolicy: .xctestCoordinateAllowed
)
case .scroll:
return SynthesizedGesturePolicy(
keyboardPolicy: .whenAccessibilityHealthy,
fallbackPolicy: .privateSynthesisRequired
)
case .synthesizedDrag:
return SynthesizedGesturePolicy(
keyboardPolicy: .requiredWhenAvailable,
fallbackPolicy: .xctestCoordinateWhenAccessibilityAvailable
)
}
}
func sequenceHasSynthesizedCoordinateStep(_ steps: [SequenceStep]) -> Bool {
steps.contains { step in
step.synthesized == true && step.kind == "tap"
}
}
extension RunnerTests {
func synthesizedSequenceCoordinateContext(steps: [SequenceStep]) -> SynthesizedCoordinateContext? {
guard sequenceHasSynthesizedCoordinateStep(steps) else { return nil }
return synthesizedCoordinateContext(policy: synthesizedGesturePolicy(.synthesizedDrag))
}
func logSynthesizedGesturePolicyDecision(
kind: SynthesizedGesturePolicyKind,
context: SynthesizedCoordinateContext?,
fallbackAttempted: Bool
) {
#if os(iOS)
guard let context else {
NSLog(
"AGENT_DEVICE_RUNNER_SYNTHESIZED_GESTURE_POLICY kind=%@ context=unavailable fallbackAttempted=%@",
kind.rawValue,
fallbackAttempted.description
)
return
}
NSLog(
"AGENT_DEVICE_RUNNER_SYNTHESIZED_GESTURE_POLICY kind=%@ axHealth=%@ frameSource=screenshot keyboardPolicy=%@ fallbackPolicy=%@ fallbackAllowed=%@ fallbackAttempted=%@",
kind.rawValue,
context.accessibilityHealth.rawValue,
context.keyboardPolicy.rawValue,
context.fallbackPolicy.rawValue,
context.allowsXCTestCoordinateFallback.description,
fallbackAttempted.description
)
#endif
}
}
#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)
)
}
func testSynthesizedKeyboardPolicyKeepsUnknownDragProbeButNotUnknownScrollProbe() {
XCTAssertFalse(
SynthesizedKeyboardPolicy.whenAccessibilityHealthy
.allowsProbe(accessibilityHealth: .unknown)
)
XCTAssertTrue(
SynthesizedKeyboardPolicy.requiredWhenAvailable
.allowsProbe(accessibilityHealth: .unknown)
)
XCTAssertFalse(
SynthesizedKeyboardPolicy.requiredWhenAvailable
.allowsProbe(accessibilityHealth: .unavailable)
)
}
func testSynthesizedGesturePoliciesMatchCommandContracts() {
XCTAssertEqual(
synthesizedGesturePolicy(.coordinateTap),
SynthesizedGesturePolicy(
keyboardPolicy: .never,
fallbackPolicy: .xctestCoordinateAllowed
)
)
XCTAssertEqual(
synthesizedGesturePolicy(.scroll),
SynthesizedGesturePolicy(
keyboardPolicy: .whenAccessibilityHealthy,
fallbackPolicy: .privateSynthesisRequired
)
)
XCTAssertEqual(
synthesizedGesturePolicy(.synthesizedDrag),
SynthesizedGesturePolicy(
keyboardPolicy: .requiredWhenAvailable,
fallbackPolicy: .xctestCoordinateWhenAccessibilityAvailable
)
)
}
}
#endif