Files
Michał Pierzchała a3e967526a refactor: rename ios-runner -> apple-runner (#981) (#996)
Finish the cosmetic ios-runner -> apple-runner rename now that the
top-level XCTest runner is the OS-agnostic Apple engine
(iOS/iPadOS/tvOS/macOS/visionOS from one Xcode project).

Cosmetic only, no behavior change:
- git mv ios-runner/ -> apple-runner/ (AgentDeviceRunner, README, RUNNER_PROTOCOL)
- Update repo project-path consumers: build-xcuitest-apple.sh, package.json
  files globs, .fallowrc.json, write-xcuitest-cache-metadata.mjs,
  runner-xctestrun.ts fingerprint/project paths, recording overlay + test,
  daemon-client-timeout kill pattern, setup-apple-replay hashFiles glob,
  ci.yml swift-compat scan, AGENTS.md.
- Rename runtime home cache/derived/lease dir default
  ~/.agent-device/ios-runner -> ~/.agent-device/apple-runner (build script,
  package/clean scripts, runner-xctestrun RUNNER_DERIVED_ROOT, runner-lease,
  runner-contract hint, cli-help/commands.md docs) and the tests asserting it.
- Rename OS-agnostic runner symbols: runIosRunnerCommand ->
  runAppleRunnerCommand, prewarmIosRunnerCache -> prewarmAppleRunnerCache,
  createIosRunnerCachePrewarmOnColdBoot / createIosRunnerCacheColdBootPrewarmForOpen
  -> createAppleRunner* (+ call sites, type aliases, test mocks).

Intentionally left as ios-runner (out of scope / would change behavior):
- prepare ios-runner CLI subcommand (user-facing command name)
- AGENT_DEVICE_IOS_RUNNER_* env var names and .tmp/ios-runner-derived CI values
- ios-runner-prebuilt cache-key-prefix, ci.yml job id, workflow/ADR filenames
- agent-device-ios-runner-<version> release artifact basenames

Part of #972 (Phase 3 - Apple PlatformPlugin).
2026-07-01 14:24:48 +02:00

221 lines
6.9 KiB
Swift

import XCTest
// Swift port of buildScrollGesturePlan from src/core/scroll-gesture.ts.
//
// This is a deliberate two-place invariant: the daemon keeps the TS implementation (for Android,
// recording, and reported-pixels), and the runner places the gesture with this Swift copy. The
// parity test vectors at the bottom of this file mirror src/core/__tests__/scroll-gesture.test.ts —
// if you change the math in either language, update the other and both vector sets.
//
// All inputs here are positive (reference dims, travel, center), so Swift's `.rounded()`
// (half away from zero) matches JS `Math.round` (half up) on every value computed below.
struct RunnerScrollGesturePlan {
let x1: Double
let y1: Double
let x2: Double
let y2: Double
let travelPixels: Double
}
private let runnerDefaultScrollAmount = 0.6
private let runnerDefaultEdgePaddingFraction = 0.05
func runnerScrollGesturePlan(
direction: String,
amount: Double?,
pixels: Double?,
referenceWidth: Double,
referenceHeight: Double
) -> RunnerScrollGesturePlan? {
// Mirror the TS INVALID_ARGS contract: non-positive or non-finite amount/pixels are rejected
// rather than clamped into a journaled 1px scroll. The daemon validates before sending, so
// this only triggers for non-daemon wire clients.
if let amount, !(amount.isFinite && amount > 0) { return nil }
if let pixels, !(pixels.isFinite && pixels > 0) { return nil }
let axisLength = (direction == "up" || direction == "down") ? referenceHeight : referenceWidth
let requestedAmount = amount ?? runnerDefaultScrollAmount
let requestedPixels: Double =
pixels.map { max(1, $0.rounded()) } ?? (axisLength * requestedAmount).rounded()
let edgePadding = max(1, (axisLength * runnerDefaultEdgePaddingFraction).rounded())
let maxTravelPixels = max(1, axisLength - edgePadding * 2)
let travelPixels = max(1, min(requestedPixels, maxTravelPixels))
let halfTravel = (travelPixels / 2).rounded()
let centerX = (referenceWidth / 2).rounded()
let centerY = (referenceHeight / 2).rounded()
func plan(_ x1: Double, _ y1: Double, _ x2: Double, _ y2: Double) -> RunnerScrollGesturePlan {
RunnerScrollGesturePlan(x1: x1, y1: y1, x2: x2, y2: y2, travelPixels: travelPixels)
}
switch direction {
case "up":
return plan(centerX, centerY - halfTravel, centerX, centerY + halfTravel)
case "down":
return plan(centerX, centerY + halfTravel, centerX, centerY - halfTravel)
case "left":
return plan(centerX - halfTravel, centerY, centerX + halfTravel, centerY)
case "right":
return plan(centerX + halfTravel, centerY, centerX - halfTravel, centerY)
default:
return nil
}
}
#if AGENT_DEVICE_RUNNER_UNIT_TESTS
extension RunnerTests {
// Cross-language parity vectors mirroring src/core/__tests__/scroll-gesture.test.ts. Keep these
// in sync with the vitest vectors so the two buildScrollGesturePlan implementations cannot drift.
func testRunnerScrollGesturePlanMapsRelativeAmount() throws {
let plan = try XCTUnwrap(
runnerScrollGesturePlan(
direction: "down",
amount: 0.5,
pixels: nil,
referenceWidth: 400,
referenceHeight: 800
)
)
XCTAssertEqual(plan.x1, 200)
XCTAssertEqual(plan.y1, 600)
XCTAssertEqual(plan.x2, 200)
XCTAssertEqual(plan.y2, 200)
XCTAssertEqual(plan.travelPixels, 400)
}
func testRunnerScrollGesturePlanPixelsDown() throws {
// 300x600, down, pixels 120 -> (150,360)->(150,240), travel 120.
let plan = try XCTUnwrap(
runnerScrollGesturePlan(
direction: "down",
amount: nil,
pixels: 120,
referenceWidth: 300,
referenceHeight: 600
)
)
XCTAssertEqual(plan.x1, 150)
XCTAssertEqual(plan.y1, 360)
XCTAssertEqual(plan.x2, 150)
XCTAssertEqual(plan.y2, 240)
XCTAssertEqual(plan.travelPixels, 120)
}
func testRunnerScrollGesturePlanClampsAmountAboveOne() throws {
// 400x800, down, amount 2 -> requested 1600 clamps to the safe band (720): (200,760)->(200,40).
let plan = try XCTUnwrap(
runnerScrollGesturePlan(
direction: "down",
amount: 2,
pixels: nil,
referenceWidth: 400,
referenceHeight: 800
)
)
XCTAssertEqual(plan.x1, 200)
XCTAssertEqual(plan.y1, 760)
XCTAssertEqual(plan.x2, 200)
XCTAssertEqual(plan.y2, 40)
XCTAssertEqual(plan.travelPixels, 720)
}
func testRunnerScrollGesturePlanClampsExplicitPixelsVertically() throws {
// 400x800, down, pixels 1000 clamps travel to the safe band (720): (200,760)->(200,40).
let plan = try XCTUnwrap(
runnerScrollGesturePlan(
direction: "down",
amount: nil,
pixels: 1000,
referenceWidth: 400,
referenceHeight: 800
)
)
XCTAssertEqual(plan.x1, 200)
XCTAssertEqual(plan.y1, 760)
XCTAssertEqual(plan.x2, 200)
XCTAssertEqual(plan.y2, 40)
XCTAssertEqual(plan.travelPixels, 720)
}
func testRunnerScrollGesturePlanFloorsTinyFrames() throws {
// 2x2, down, pixels 10 engages every max(1, ...) floor and the .5 rounding cases the two
// ports must agree on (halfTravel 0.5 -> 1, center 1 from 2/2): (1,2)->(1,0), travel 1.
let plan = try XCTUnwrap(
runnerScrollGesturePlan(
direction: "down",
amount: nil,
pixels: 10,
referenceWidth: 2,
referenceHeight: 2
)
)
XCTAssertEqual(plan.x1, 1)
XCTAssertEqual(plan.y1, 2)
XCTAssertEqual(plan.x2, 1)
XCTAssertEqual(plan.y2, 0)
XCTAssertEqual(plan.travelPixels, 1)
}
func testRunnerScrollGesturePlanClampsToSafeBand() throws {
// 300x600, right, pixels 500 clamps travel to the safe band (270).
let plan = try XCTUnwrap(
runnerScrollGesturePlan(
direction: "right",
amount: nil,
pixels: 500,
referenceWidth: 300,
referenceHeight: 600
)
)
XCTAssertEqual(plan.x1, 285)
XCTAssertEqual(plan.x2, 15)
XCTAssertEqual(plan.y1, 300)
XCTAssertEqual(plan.y2, 300)
XCTAssertEqual(plan.travelPixels, 270)
}
func testRunnerScrollGesturePlanRejectsUnknownDirection() {
XCTAssertNil(
runnerScrollGesturePlan(
direction: "sideways",
amount: nil,
pixels: 100,
referenceWidth: 300,
referenceHeight: 600
)
)
}
func testRunnerScrollGesturePlanRejectsInvalidAmountAndPixels() {
XCTAssertNil(
runnerScrollGesturePlan(
direction: "down",
amount: 0,
pixels: nil,
referenceWidth: 300,
referenceHeight: 600
)
)
XCTAssertNil(
runnerScrollGesturePlan(
direction: "down",
amount: nil,
pixels: -10,
referenceWidth: 300,
referenceHeight: 600
)
)
XCTAssertNil(
runnerScrollGesturePlan(
direction: "down",
amount: .infinity,
pixels: nil,
referenceWidth: 300,
referenceHeight: 600
)
)
}
}
#endif