Skip to main content
Glama
DialogCommandTests.swift14 kB
import Foundation import PeekabooCore import PeekabooFoundation import Testing @testable import PeekabooCLI private struct DialogTextFieldPayload: Codable, Sendable { let title: String? let value: String? let placeholder: String? } private struct DialogListPayload: Codable, Sendable { let title: String let role: String let buttons: [String] let textFields: [DialogTextFieldPayload] let textElements: [String] } #if !PEEKABOO_SKIP_AUTOMATION @Suite( "Dialog Command Tests", .serialized, .tags(.automation), .enabled(if: CLITestEnvironment.runAutomationRead) ) struct DialogCommandTests { @Test("Dialog command exists") func dialogCommandExists() { let config = DialogCommand.commandDescription #expect(config.commandName == "dialog") #expect(config.abstract.contains("Interact with system dialogs and alerts")) } @Test("Dialog command has expected subcommands") func dialogSubcommands() { let subcommands = DialogCommand.commandDescription.subcommands #expect(subcommands.count == 5) var subcommandNames: [String] = [] subcommandNames.reserveCapacity(subcommands.count) for descriptor in subcommands { guard let name = descriptor.commandDescription.commandName else { continue } subcommandNames.append(name) } #expect(subcommandNames.contains("click")) #expect(subcommandNames.contains("input")) #expect(subcommandNames.contains("file")) #expect(subcommandNames.contains("dismiss")) #expect(subcommandNames.contains("list")) } @Test("Dialog click command help") func dialogClickHelp() async throws { let result = try await runCommand(["dialog", "click", "--help"]) #expect(result.status == 0) let output = result.output #expect(output.contains("OVERVIEW: Click a button in a dialog using DialogService")) #expect(output.contains("--button")) #expect(output.contains("--window")) #expect(output.contains("--json-output")) } @Test("Dialog input command help") func dialogInputHelp() async throws { let result = try await runCommand(["dialog", "input", "--help"]) #expect(result.status == 0) let output = result.output #expect(output.contains("OVERVIEW: Enter text in a dialog field using DialogService")) #expect(output.contains("--text")) #expect(output.contains("--field")) #expect(output.contains("--index")) #expect(output.contains("--clear")) } @Test("Dialog file command help") func dialogFileHelp() async throws { let result = try await runCommand(["dialog", "file", "--help"]) #expect(result.status == 0) let output = result.output #expect(output.contains("OVERVIEW: Handle file save/open dialogs using DialogService")) #expect(output.contains("--path")) #expect(output.contains("--name")) #expect(output.contains("--select")) } @Test("Dialog dismiss command help") func dialogDismissHelp() async throws { let result = try await runCommand(["dialog", "dismiss", "--help"]) #expect(result.status == 0) let output = result.output #expect(output.contains("OVERVIEW: Dismiss a dialog using DialogService")) #expect(output.contains("--force")) #expect(output.contains("--window")) } @Test("dialog dismiss uses force flag") func dialogDismissForce() async throws { let dialogService = StubDialogService() dialogService.dismissResult = DialogActionResult( success: true, action: .dismiss, details: ["method": "escape"] ) let services = self.makeTestServices(dialogs: dialogService) let (output, status) = try await self.runCommand( ["dialog", "dismiss", "--force", "--json-output"], services: services ) #expect(status == 0) struct Payload: Codable { let success: Bool let data: DialogDismissResult } struct DialogDismissResult: Codable { let method: String } let response = try JSONDecoder().decode(Payload.self, from: Data(output.utf8)) #expect(response.success == true) #expect(response.data.method == "escape") } @Test("Dialog list command help") func dialogListHelp() async throws { let result = try await runCommand(["dialog", "list", "--help"]) #expect(result.status == 0) let output = result.output #expect(output.contains("OVERVIEW: List elements in current dialog using DialogService")) #expect(output.contains("--json-output")) } @Test("Dialog error handling") func dialogErrorHandling() { // Test that DialogError enum values are properly mapped let errorCases: [(PeekabooError, StandardErrorCode, String)] = [ (.elementNotFound("OK"), .elementNotFound, "Element not found: OK"), (.invalidInput("Field index 5 out of range"), .invalidInput, "Invalid input: Field index 5 out of range"), ( .operationError(message: "No text fields found in dialog."), .unknownError, "No text fields found in dialog." ), ] for (error, code, message) in errorCases { #expect(error.code == code) #expect(error.errorDescription == message) } } @Test("Dialog service integration") @MainActor func dialogServiceIntegration() { // Verify that PeekabooServices includes the dialog service let services = self.makeTestServices() _ = services.dialogs // This should compile without errors } @Test("dialog list surfaces stubbed elements in JSON") func dialogListWithStubData() async throws { let elements = DialogElements( dialogInfo: DialogInfo( title: "Open", role: "AXWindow", subrole: "AXDialog", isFileDialog: true, bounds: .init(x: 0, y: 0, width: 400, height: 300) ), buttons: [ DialogButton(title: "New Document"), DialogButton(title: "Open"), ], textFields: [ DialogTextField(title: "Name", value: "", placeholder: "File name", index: 0, isEnabled: true), ], staticTexts: ["Choose a document to open"] ) let dialogService = StubDialogService(elements: elements) let services = self.makeTestServices(dialogs: dialogService) let (output, status) = try await self.runCommand( ["dialog", "list", "--json-output"], services: services ) #expect(status == 0) let data = try #require(output.data(using: .utf8)) let response = try JSONDecoder().decode(CodableJSONResponse<DialogListPayload>.self, from: data) #expect(response.data.title == "Open") #expect(response.data.buttons.contains("New Document")) #expect(response.data.textFields.first?.placeholder == "File name") } @Test("dialog click emits JSON success when stub succeeds") func dialogClickJSON() async throws { let dialogService = await MainActor.run { StubDialogService() } dialogService.clickButtonResult = DialogActionResult( success: true, action: .clickButton, details: ["button": "New Document", "window": "Open"] ) let services = self.makeTestServices(dialogs: dialogService) let (output, status) = try await self.runCommand( ["dialog", "click", "--button", "New Document", "--json-output"], services: services ) #expect(status == 0) let data = try #require(output.data(using: .utf8)) let response = try JSONDecoder().decode(JSONResponse.self, from: data) #expect(response.success == true) #expect(dialogService.recordedButtonClicks.count == 1) #expect(dialogService.recordedButtonClicks.first?.button == "New Document") } private struct CommandFailure: Error { let status: Int32 let stderr: String } private func runCommand(_ args: [String]) async throws -> (output: String, status: Int32) { let services = self.makeTestServices() return try await self.runCommand(args, services: services) } private func runCommand( _ args: [String], services: PeekabooServices ) async throws -> (output: String, status: Int32) { let result = try await InProcessCommandRunner.run(args, services: services) let output = result.stdout.isEmpty ? result.stderr : result.stdout if result.exitStatus != 0 { throw CommandFailure(status: result.exitStatus, stderr: output) } return (output, result.exitStatus) } @MainActor private func makeTestServices( dialogs: any DialogServiceProtocol = StubDialogService() ) -> PeekabooServices { TestServicesFactory.makePeekabooServices(dialogs: dialogs) } } // MARK: - Dialog Command Integration Tests @Suite( "Dialog Command Integration Tests", .serialized, .tags(.automation), .enabled(if: CLITestEnvironment.runAutomationActions) ) struct DialogCommandIntegrationTests { @Test("List active dialogs with ") func listActiveDialogs() async throws { let output = try await runAutomationCommand([ "dialog", "list", "--json-output", ]) struct TextField: Codable { let title: String let value: String let placeholder: String } struct DialogListResult: Codable { let title: String let role: String let buttons: [String] let textFields: [TextField] let textElements: [String] } // Try to decode as success response first if let response = try? JSONDecoder().decode( CodableJSONResponse<DialogListResult>.self, from: Data(output.utf8) ) { if response.success { #expect(!response.data.title.isEmpty) #expect(!response.data.buttons.isEmpty) } } else { // Otherwise it's an error response let errorResponse = try JSONDecoder().decode(JSONResponse.self, from: Data(output.utf8)) #expect(errorResponse.error?.code == "NO_ACTIVE_DIALOG") } } @Test("Dialog click workflow") func dialogClickWorkflow() async throws { // This would click a button if a dialog is present let output = try await runAutomationCommand([ "dialog", "click", "--button", "OK", "--json-output", ]) let data = try JSONDecoder().decode(JSONResponse.self, from: Data(output.utf8)) if !data.success { // Expected if no dialog is open #expect(data.error?.code == "NO_ACTIVE_DIALOG") } } @Test("Dialog input workflow") func dialogInputWorkflow() async throws { let output = try await runAutomationCommand([ "dialog", "input", "--text", "Test input", "--field", "Name", "--json-output", ]) let data = try JSONDecoder().decode(JSONResponse.self, from: Data(output.utf8)) if !data.success { // Expected if no dialog is open #expect(data.error?.code == "NO_ACTIVE_DIALOG") } } @Test("Dialog dismiss with escape") func dialogDismissEscape() async throws { let output = try await runAutomationCommand([ "dialog", "dismiss", "--force", "--json-output", ]) struct DialogDismissResult: Codable { let action: String let method: String let button: String? } if let response = try? JSONDecoder().decode( CodableJSONResponse<DialogDismissResult>.self, from: Data(output.utf8) ) { if response.success { #expect(response.data.method == "escape") } } } @Test("File dialog handling") func fileDialogHandling() async throws { let output = try await runAutomationCommand([ "dialog", "file", "--path", "/tmp", "--name", "test.txt", "--select", "Save", "--json-output", ]) struct FileDialogResult: Codable { let action: String let path: String? let name: String? let buttonClicked: String } // Try to decode as success response first if let response = try? JSONDecoder().decode( CodableJSONResponse<FileDialogResult>.self, from: Data(output.utf8) ) { if response.success { #expect(response.data.action == "file_dialog") #expect(response.data.path == "/tmp") #expect(response.data.name == "test.txt") } } else { // Otherwise it's an error response let errorResponse = try JSONDecoder().decode(JSONResponse.self, from: Data(output.utf8)) #expect(errorResponse.error?.code == "NO_ACTIVE_DIALOG" || errorResponse.error?.code == "NO_FILE_DIALOG") } } } // MARK: - Test Helpers private func runAutomationCommand( _ args: [String], allowedExitStatuses: Set<Int32> = [0, 1, 64] ) async throws -> String { let result = try await InProcessCommandRunner.runShared( args, allowedExitCodes: allowedExitStatuses ) return result.combinedOutput } #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