mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
a3e967526a
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).
221 lines
6.9 KiB
Swift
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
|