Skip to main content
Glama
WindowCommandTests.swift16.8 kB
import CoreGraphics import Foundation import Testing @testable import PeekabooCLI @testable import PeekabooCore #if !PEEKABOO_SKIP_AUTOMATION @Suite("WindowCommand Tests", .serialized, .tags(.automation), .enabled(if: CLITestEnvironment.runAutomationRead)) struct WindowCommandTests { @Test func windowCommandHelp() async throws { let output = try await runPeekabooCommand(["window", "--help"]) #expect(output.contains("Manipulate application windows")) #expect(output.contains("close")) #expect(output.contains("minimize")) #expect(output.contains("maximize")) #expect(output.contains("move")) #expect(output.contains("resize")) #expect(output.contains("set-bounds")) #expect(output.contains("focus")) #expect(output.contains("list")) } @Test("window list hides non-shareable overlays") func windowListSkipsHiddenWindows() async throws { let appName = "OverlayApp" let appInfo = ServiceApplicationInfo( processIdentifier: 5555, bundleIdentifier: "dev.overlay", name: appName ) let overlay = ServiceWindowInfo( windowID: 10, title: "HUD", bounds: CGRect(x: 0, y: 0, width: 200, height: 120), layer: 8, sharingState: .none ) let mainWindow = ServiceWindowInfo( windowID: 11, title: "Document", bounds: CGRect(x: 50, y: 50, width: 1200, height: 800), index: 1, sharingState: .readWrite ) let context = await self.makeWindowContext( appInfo: appInfo, windows: [appName: [overlay, mainWindow]] ) let result = try await self.runWindowCommand([ "window", "list", "--app", appName, "--json-output", ], context: context) let output = result.stdout.isEmpty ? result.stderr : result.stdout let response = try JSONDecoder().decode( CodableJSONResponse<WindowListData>.self, from: Data(output.utf8) ) let windows = response.data.windows #expect(windows.count == 1) let window = try #require(windows.first) #expect(window.window_title == "Document") #expect(window.window_index == mainWindow.index) } @Test func windowCloseHelp() async throws { let output = try await runPeekabooCommand(["window", "close", "--help"]) #expect(output.contains("Close a window")) #expect(output.contains("--app")) #expect(output.contains("--window-title")) #expect(output.contains("--window-index")) } @Test func windowListCommand() async throws { // Test that window list delegates to list windows command let output = try await runPeekabooCommand(["window", "list", "--app", "Finder", "--json-output"]) let data = try JSONDecoder().decode(JSONResponse.self, from: Data(output.utf8)) #expect(data.success == true || data.error != nil) // Either succeeds or fails with proper error } @Test func windowCommandWithoutApp() async throws { // Test that window commands require --app let commands = ["close", "minimize", "maximize", "focus"] for command in commands { do { _ = try await self.runPeekabooCommand(["window", command]) Issue.record("Expected command to fail without --app") } catch { // Expected to fail #expect(error.localizedDescription.contains("--app must be specified") || error.localizedDescription.contains("Exit status: 1") ) } } } @Test func windowMoveRequiresCoordinates() async throws { do { _ = try await self.runPeekabooCommand(["window", "move", "--app", "Finder"]) Issue.record("Expected command to fail without coordinates") } catch { // Expected to fail - missing required x and y #expect(error.localizedDescription.contains("Missing expected argument") || error.localizedDescription.contains("Exit status: 64") ) } } @Test func windowResizeRequiresDimensions() async throws { do { _ = try await self.runPeekabooCommand(["window", "resize", "--app", "Finder"]) Issue.record("Expected command to fail without dimensions") } catch { // Expected to fail - missing required width and height #expect(error.localizedDescription.contains("Missing expected argument") || error.localizedDescription.contains("Exit status: 64") ) } } @Test func windowSetBoundsRequiresAllParameters() async throws { do { _ = try await self.runPeekabooCommand([ "window", "set-bounds", "--app", "Finder", "--x", "100", "--y", "100", ]) Issue.record("Expected command to fail without all parameters") } catch { // Expected to fail - missing required width and height #expect(error.localizedDescription.contains("Missing expected argument") || error.localizedDescription.contains("Exit status: 64") ) } } @Test("set-bounds reports refreshed bounds") func windowSetBoundsReportsFreshBounds() async throws { let appName = "TextEdit" let bundleID = "com.apple.TextEdit" let initialBounds = CGRect(x: 10, y: 20, width: 320, height: 240) let updatedBounds = CGRect(x: 400, y: 500, width: 640, height: 480) let context = await self.makeWindowContext( appInfo: ServiceApplicationInfo( processIdentifier: 42, bundleIdentifier: bundleID, name: appName ), windows: [ appName: [ ServiceWindowInfo( windowID: 101, title: "Untitled", bounds: initialBounds, isMinimized: false, isMainWindow: true, windowLevel: 0, alpha: 1.0, index: 0 ), ], ] ) let args = [ "window", "set-bounds", "--app", appName, "--x", String(Int(updatedBounds.origin.x)), "--y", String(Int(updatedBounds.origin.y)), "--width", String(Int(updatedBounds.size.width)), "--height", String(Int(updatedBounds.size.height)), "--json-output", ] let result = try await self.runWindowCommand(args, context: context) #expect(result.exitStatus == 0) let output = result.stdout.isEmpty ? result.stderr : result.stdout let response = try JSONDecoder().decode( CodableJSONResponse<WindowActionResult>.self, from: Data(output.utf8) ) #expect(response.success == true) let bounds = try #require(response.data.new_bounds) #expect(bounds.x == Int(updatedBounds.origin.x)) #expect(bounds.y == Int(updatedBounds.origin.y)) #expect(bounds.width == Int(updatedBounds.size.width)) #expect(bounds.height == Int(updatedBounds.size.height)) let storedBounds = await MainActor.run { context.windowService.windowsByApp[appName]?.first?.bounds } let refreshed = try #require(storedBounds) #expect(Int(refreshed.origin.x) == Int(updatedBounds.origin.x)) #expect(Int(refreshed.origin.y) == Int(updatedBounds.origin.y)) #expect(Int(refreshed.size.width) == Int(updatedBounds.size.width)) #expect(Int(refreshed.size.height) == Int(updatedBounds.size.height)) } @Test("resize reports refreshed bounds") func windowResizeReportsFreshBounds() async throws { let appName = "TextEdit" let bundleID = "com.apple.TextEdit" let initialBounds = CGRect(x: 50, y: 60, width: 200, height: 150) let updatedSize = CGSize(width: 880, height: 540) let context = await self.makeWindowContext( appInfo: ServiceApplicationInfo( processIdentifier: 99, bundleIdentifier: bundleID, name: appName ), windows: [ appName: [ ServiceWindowInfo( windowID: 202, title: "Draft", bounds: initialBounds, isMinimized: false, isMainWindow: true, windowLevel: 0, alpha: 1.0, index: 0 ), ], ] ) let args = [ "window", "resize", "--app", appName, "--width", String(Int(updatedSize.width)), "--height", String(Int(updatedSize.height)), "--json-output", ] let result = try await self.runWindowCommand(args, context: context) #expect(result.exitStatus == 0) let output = result.stdout.isEmpty ? result.stderr : result.stdout let response = try JSONDecoder().decode( CodableJSONResponse<WindowActionResult>.self, from: Data(output.utf8) ) #expect(response.success == true) let bounds = try #require(response.data.new_bounds) #expect(bounds.x == Int(initialBounds.origin.x)) #expect(bounds.y == Int(initialBounds.origin.y)) #expect(bounds.width == Int(updatedSize.width)) #expect(bounds.height == Int(updatedSize.height)) } // Helper function to run peekaboo commands private func runPeekabooCommand( _ arguments: [String], allowedExitStatuses: Set<Int32> = [0, 64] ) async throws -> String { do { let result = try await InProcessCommandRunner.runShared( arguments, allowedExitCodes: allowedExitStatuses ) return result.combinedOutput } catch let error as CommandExecutionError { let output = error.stdout.isEmpty ? error.stderr : error.stdout throw TestError.commandFailed(status: error.status, output: output) } } enum TestError: Error, LocalizedError { case commandFailed(status: Int32, output: String) case binaryMissing var errorDescription: String? { switch self { case let .commandFailed(status, output): "Command failed with exit status: \(status). Output: \(output)" case .binaryMissing: "Peekaboo binary missing" } } } private func runWindowCommand( _ arguments: [String], context: WindowHarnessContext, allowedExitStatuses: Set<Int32> = [0] ) async throws -> CommandRunResult { let result = try await InProcessCommandRunner.run(arguments, services: context.services) try result.validateExitStatus(allowedExitCodes: allowedExitStatuses, arguments: arguments) return result } @MainActor private func makeWindowContext( appInfo: ServiceApplicationInfo, windows: [String: [ServiceWindowInfo]] ) -> WindowHarnessContext { let applicationService = StubApplicationService(applications: [appInfo], windowsByApp: windows) let windowService = StubWindowService(windowsByApp: windows) let services = TestServicesFactory.makePeekabooServices( applications: applicationService, windows: windowService ) return WindowHarnessContext( services: services, windowService: windowService, applicationService: applicationService ) } private struct WindowHarnessContext { let services: PeekabooServices let windowService: StubWindowService let applicationService: StubApplicationService } } // MARK: - Local Integration Tests @Suite( "Window Command Local Integration Tests", .serialized, .tags(.automation), .enabled(if: CLITestEnvironment.runAutomationActions) ) struct WindowCommandLocalIntegrationTests { @Test(.enabled(if: ProcessInfo.processInfo.environment["RUN_LOCAL_TESTS"] == "true")) func windowMinimizeTextEdit() async throws { // This test requires TextEdit to be running and local permissions // First, ensure TextEdit is running and has a window let launchResult = try await runPeekabooCommand(["image", "--app", "TextEdit", "--json-output"]) let launchData = try JSONDecoder().decode(JSONResponse.self, from: Data(launchResult.utf8)) guard launchData.success else { Issue.record("TextEdit must be running for this test") return } // Try to minimize TextEdit window let result = try await runPeekabooCommand(["window", "minimize", "--app", "TextEdit", "--json-output"]) let data = try JSONDecoder().decode(JSONResponse.self, from: Data(result.utf8)) if let error = data.error { if error.code == "PERMISSION_ERROR_ACCESSIBILITY" { Issue.record("Accessibility permission required for window manipulation") return } } #expect(data.success == true) // Wait a bit for the animation try await Task.sleep(nanoseconds: 1_000_000_000) // 1 second } @Test(.enabled(if: ProcessInfo.processInfo.environment["RUN_LOCAL_TESTS"] == "true")) func windowMoveTextEdit() async throws { // This test requires TextEdit to be running and local permissions // Try to move TextEdit window let result = try await runPeekabooCommand([ "window", "move", "--app", "TextEdit", "--x", "200", "--y", "200", "--json-output", ]) let data = try JSONDecoder().decode(JSONResponse.self, from: Data(result.utf8)) if let error = data.error { if error.code == "PERMISSION_ERROR_ACCESSIBILITY" { Issue.record("Accessibility permission required for window manipulation") return } } #expect(data.success == true) if let responseData = data.data as? [String: Any], let newBounds = responseData["new_bounds"] as? [String: Any] { #expect(newBounds["x"] as? Int == 200) #expect(newBounds["y"] as? Int == 200) } } @Test(.enabled(if: ProcessInfo.processInfo.environment["RUN_LOCAL_TESTS"] == "true")) func windowFocusTextEdit() async throws { // This test requires TextEdit to be running // Try to focus TextEdit window let result = try await runPeekabooCommand([ "window", "focus", "--app", "TextEdit", "--json-output", ]) let data = try JSONDecoder().decode(JSONResponse.self, from: Data(result.utf8)) if let error = data.error { if error.code == "PERMISSION_ERROR_ACCESSIBILITY" { Issue.record("Accessibility permission required for window manipulation") return } } #expect(data.success == true) } // Helper function for local tests private func runPeekabooCommand( _ arguments: [String], allowedExitStatuses: Set<Int32> = [0, 1, 64] ) async throws -> String { do { let result = try await InProcessCommandRunner.runShared( arguments, allowedExitCodes: allowedExitStatuses ) return result.combinedOutput } catch let error as CommandExecutionError { let output = error.stdout.isEmpty ? error.stderr : error.stdout throw TestError.commandFailed(status: error.status, output: output) } } enum TestError: Error, LocalizedError { case commandFailed(status: Int32, output: String) case binaryMissing var errorDescription: String? { switch self { case let .commandFailed(status, output): "Exit status: \(status)" case .binaryMissing: "Peekaboo binary missing" } } } } #endif

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/steipete/Peekaboo'

If you have feedback or need assistance with the MCP directory API, please join our Discord server