MoveCommandTests.swift•6.43 kB
import CoreGraphics
import Foundation
import PeekabooCore
import PeekabooFoundation
import Testing
@testable import PeekabooCLI
#if !PEEKABOO_SKIP_AUTOMATION
@Suite(
"MoveCommand Tests",
.serialized,
.tags(.safe),
.enabled(if: CLITestEnvironment.runAutomationRead)
)
struct MoveCommandTests {
@Test("move --help lists options")
func moveHelp() async throws {
let context = await self.makeContext()
let result = try await self.runMove(arguments: ["--help"], context: context)
#expect(result.exitStatus == 0)
#expect(self.output(from: result).contains("Move the mouse cursor"))
}
@Test("Coordinate moves call automation service")
func coordinateMove() async throws {
let context = await self.makeContext()
let result = try await self.runMove(
arguments: ["100,200", "--duration", "750", "--steps", "10"],
context: context
)
#expect(result.exitStatus == 0)
let moveCalls = await self.automationState(context) { $0.moveMouseCalls }
let call = try #require(moveCalls.first)
#expect(call.destination == CGPoint(x: 100, y: 200))
#expect(call.duration == 750)
#expect(call.steps == 10)
#expect(call.profile == .linear)
}
@Test("Move command requires a target")
func requiresTarget() async throws {
let context = await self.makeContext()
let result = try await self.runMove(arguments: [], context: context)
#expect(result.exitStatus != 0)
let moveCalls = await self.automationState(context) { $0.moveMouseCalls }
#expect(moveCalls.isEmpty)
}
@Test("Move by element ID resolves using stored detection results")
func moveByElementId() async throws {
let context = await self.makeContext()
let element = DetectedElement(
id: "B1",
type: .button,
label: "Submit",
bounds: CGRect(x: 50, y: 70, width: 120, height: 40)
)
let detection = ElementDetectionResult(
sessionId: "session-id",
screenshotPath: "/tmp/screenshot.png",
elements: DetectedElements(buttons: [element]),
metadata: DetectionMetadata(detectionTime: 0, elementCount: 1, method: "stub")
)
try await context.sessions.storeDetectionResult(sessionId: "session-id", result: detection)
let result = try await self.runMove(
arguments: ["--id", "B1", "--session", "session-id", "--json-output"],
context: context
)
#expect(result.exitStatus == 0)
let moveCalls = await self.automationState(context) { $0.moveMouseCalls }
let call = try #require(moveCalls.first)
#expect(call.destination.x == element.bounds.midX)
#expect(call.destination.y == element.bounds.midY)
#expect(call.profile == .linear)
}
@Test("Move by query waits for element using automation service")
func moveByQuery() async throws {
let context = await self.makeContext { automation, sessions in
sessions.mostRecentSessionId = "session-query"
let element = DetectedElement(
id: "B2",
type: .button,
label: "Continue",
bounds: CGRect(x: 200, y: 300, width: 80, height: 24)
)
automation.setWaitForElementResult(
WaitForElementResult(found: true, element: element, waitTime: 0.05),
for: .query("Continue")
)
}
let result = try await self.runMove(arguments: ["--to", "Continue"], context: context)
#expect(result.exitStatus == 0)
let waitCalls = await self.automationState(context) { $0.waitForElementCalls }
#expect(waitCalls.count == 1)
let moveCalls = await self.automationState(context) { $0.moveMouseCalls }
let call = try #require(moveCalls.first)
#expect(call.destination == CGPoint(x: 240, y: 312)) // mid-point of element bounds
#expect(call.profile == .linear)
}
@Test("JSON output contains expected shape")
func jsonOutput() async throws {
let context = await self.makeContext()
let result = try await self.runMove(arguments: ["150,250", "--json-output"], context: context)
#expect(result.exitStatus == 0)
let data = try #require(self.output(from: result).data(using: .utf8))
let payload = try JSONDecoder().decode(MoveResult.self, from: data)
#expect(payload.success)
#expect(payload.targetDescription.contains("Coordinates"))
#expect(payload.targetLocation["x"] == 150)
#expect(payload.targetLocation["y"] == 250)
#expect(payload.profile == "linear")
}
@Test("Human profile toggles movement mode")
func humanProfileSelection() async throws {
let context = await self.makeContext()
let result = try await self.runMove(arguments: ["100,200", "--profile", "human"], context: context)
#expect(result.exitStatus == 0)
let moveCalls = await self.automationState(context) { $0.moveMouseCalls }
let call = try #require(moveCalls.first)
#expect(call.profile == .human())
#expect(call.steps >= 30)
#expect(call.duration >= 280)
}
// MARK: - Helpers
private func runMove(
arguments: [String],
context: TestServicesFactory.AutomationTestContext
) async throws -> CommandRunResult {
try await InProcessCommandRunner.run(["move"] + arguments, services: context.services)
}
private func output(from result: CommandRunResult) -> String {
result.stdout.isEmpty ? result.stderr : result.stdout
}
private func makeContext(
configure: (@MainActor (StubAutomationService, StubSessionManager) -> Void)? = nil
) async -> TestServicesFactory.AutomationTestContext {
await MainActor.run {
let context = TestServicesFactory.makeAutomationTestContext()
configure?(context.automation, context.sessions)
return context
}
}
private func automationState<T: Sendable>(
_ context: TestServicesFactory.AutomationTestContext,
_ operation: @MainActor (StubAutomationService) -> T
) async -> T {
await MainActor.run {
operation(context.automation)
}
}
}
#endif