FocusIntegrationTests.swift•8.63 kB
import Foundation
import Testing
@testable import PeekabooCLI
#if !PEEKABOO_SKIP_AUTOMATION
@Suite(
"Focus Integration Tests",
.serialized,
.tags(.automation),
.enabled(if: CLITestEnvironment.runAutomationActions)
)
struct FocusIntegrationTests {
// Helper function to run peekaboo commands
private func runPeekabooCommand(
_ arguments: [String],
allowedExitStatuses: Set<Int32> = [0]
) async throws -> String {
let result = try await InProcessCommandRunner.runShared(
arguments,
allowedExitCodes: allowedExitStatuses
)
return result.combinedOutput
}
// MARK: - Session-based Focus Tests
@Test("click with session auto-focuses window")
func clickWithSessionAutoFocus() async throws {
// Create a session with Finder
let seeOutput = try await runPeekabooCommand([
"see",
"--app", "Finder",
"--json-output"
])
let seeData = try JSONDecoder().decode(SeeResponse.self, from: Data(seeOutput.utf8))
guard seeData.success,
let sessionId = seeData.data?.session_id else {
Issue.record("Failed to create session")
throw ProcessError.message("Failed to create session")
}
// Click should auto-focus the Finder window
let clickOutput = try await runPeekabooCommand([
"click", "button",
"--session", sessionId,
"--json-output"
])
let clickData = try JSONDecoder().decode(ClickResponse.self, from: Data(clickOutput.utf8))
// Should either click successfully (with auto-focus) or fail gracefully
#expect(clickData.success == true || clickData.error != nil)
}
@Test("type with session auto-focuses window")
func typeWithSessionAutoFocus() async throws {
// Create a session with a text editor if available
let apps = ["TextEdit", "Notes", "Stickies"]
var sessionId: String?
for app in apps {
let seeOutput = try await runPeekabooCommand([
"see",
"--app", app,
"--json-output"
])
let seeData = try JSONDecoder().decode(SeeResponse.self, from: Data(seeOutput.utf8))
if seeData.success, let id = seeData.data?.session_id {
sessionId = id
break
}
}
guard let session = sessionId else {
// Skip test if no text editor is available
return
}
// Type should auto-focus the window
let typeOutput = try await runPeekabooCommand([
"type", "test",
"--session", session,
"--json-output"
])
let typeData = try JSONDecoder().decode(TypeResponse.self, from: Data(typeOutput.utf8))
#expect(typeData.success == true || typeData.error != nil)
}
// MARK: - Application-based Focus Tests
@Test("menu command auto-focuses application")
func menuCommandAutoFocus() async throws {
// Menu command should auto-focus the app
let output = try await runPeekabooCommand([
"menu", "View",
"--app", "Finder",
"--json-output"
])
let data = try JSONDecoder().decode(MenuResponse.self, from: Data(output.utf8))
// Should either show menu (with auto-focus) or fail gracefully
#expect(data.success == true || data.error != nil)
}
// MARK: - Focus Options Integration Tests
@Test("click respects no-auto-focus flag")
func clickNoAutoFocus() async throws {
// Create session
let seeOutput = try await runPeekabooCommand([
"see",
"--app", "Finder",
"--json-output"
])
let seeData = try JSONDecoder().decode(SeeResponse.self, from: Data(seeOutput.utf8))
guard seeData.success,
let sessionId = seeData.data?.session_id else {
Issue.record("Failed to create session")
throw ProcessError.message("Failed to create session")
}
// Click with auto-focus disabled
let clickOutput = try await runPeekabooCommand([
"click", "button",
"--session", sessionId,
"--no-auto-focus",
"--json-output"
])
let clickData = try JSONDecoder().decode(ClickResponse.self, from: Data(clickOutput.utf8))
// Command should be accepted (may fail if window not focused)
#expect(clickData.success == true || clickData.error != nil)
}
@Test("type with custom focus timeout")
func typeCustomTimeout() async throws {
// Type with very short timeout
let output = try await runPeekabooCommand([
"type", "test",
"--focus-timeout", "0.1",
"--json-output"
])
let data = try JSONDecoder().decode(TypeResponse.self, from: Data(output.utf8))
// Should handle timeout gracefully
#expect(data.success == true || data.error != nil)
}
@Test("menu with high retry count")
func menuHighRetryCount() async throws {
let output = try await runPeekabooCommand([
"menu", "File",
"--app", "TextEdit",
"--focus-retry-count", "10",
"--json-output"
])
let data = try JSONDecoder().decode(MenuResponse.self, from: Data(output.utf8))
// Should respect retry count
#expect(data.success == true || data.error != nil)
}
// MARK: - Window Focus with Space Integration
@Test("window focus switches Space if needed")
func windowFocusSpaceSwitch() async throws {
// This test would ideally create a window on another Space
// For now, test that the option is accepted
let output = try await runPeekabooCommand([
"window", "focus",
"--app", "Safari",
"--space-switch",
"--json-output"
])
let data = try JSONDecoder().decode(WindowActionResponse.self, from: Data(output.utf8))
#expect(data.success == true || data.error != nil)
}
@Test("window focus moves window to current Space")
func windowFocusMoveHere() async throws {
let output = try await runPeekabooCommand([
"window", "focus",
"--app", "TextEdit",
"--move-here",
"--json-output"
])
let data = try JSONDecoder().decode(WindowActionResponse.self, from: Data(output.utf8))
#expect(data.success == true || data.error != nil)
}
// MARK: - Error Handling Tests
@Test("focus non-existent application")
func focusNonExistentApp() async throws {
let output = try await runPeekabooCommand([
"window", "focus",
"--app", "NonExistentApp12345",
"--json-output"
])
let data = try JSONDecoder().decode(WindowActionResponse.self, from: Data(output.utf8))
#expect(data.success == false)
#expect(data.error != nil)
#expect(data.error?.contains("not found") == true ||
data.error?.contains("not running") == true
)
}
@Test("focus window with invalid title")
func focusInvalidWindowTitle() async throws {
let output = try await runPeekabooCommand([
"window", "focus",
"--app", "Finder",
"--window-title", "ThisWindowDoesNotExist12345",
"--json-output"
])
let data = try JSONDecoder().decode(WindowActionResponse.self, from: Data(output.utf8))
// Should either find no match or use frontmost window
#expect(data.success == true || data.error != nil)
}
}
// MARK: - Response Types
private struct SeeResponse: Codable {
let success: Bool
let data: SeeData?
let error: String?
}
private struct SeeData: Codable {
let session_id: String
}
private struct ClickResponse: Codable {
let success: Bool
let data: ClickData?
let error: String?
}
private struct ClickData: Codable {
let action: String
}
private struct TypeResponse: Codable {
let success: Bool
let data: TypeData?
let error: String?
}
private struct TypeData: Codable {
let action: String
let text: String
}
private struct MenuResponse: Codable {
let success: Bool
let error: String?
}
private struct WindowActionResponse: Codable {
let success: Bool
let error: String?
}
private enum ProcessError: Error {
case message(String)
case binaryMissing
}
#endif