import AppKit
import CoreGraphics
import Foundation
@testable import PeekabooCLI
@testable import PeekabooCore
import PeekabooFoundation
enum TestStubError: Error {
case unimplemented(String)
}
@MainActor
func stubUnimplemented(_ function: StaticString = #function) -> Never {
fatalError("Test stub method not implemented: \(function)")
}
// MARK: - Stub Services
@MainActor
final class StubScreenCaptureService: ScreenCaptureServiceProtocol {
var permissionGranted: Bool
init(permissionGranted: Bool = true) {
self.permissionGranted = permissionGranted
}
func captureScreen(displayIndex: Int?) async throws -> CaptureResult {
throw TestStubError.unimplemented(#function)
}
func captureWindow(appIdentifier: String, windowIndex: Int?) async throws -> CaptureResult {
throw TestStubError.unimplemented(#function)
}
func captureFrontmost() async throws -> CaptureResult {
throw TestStubError.unimplemented(#function)
}
func captureArea(_ rect: CGRect) async throws -> CaptureResult {
throw TestStubError.unimplemented(#function)
}
func hasScreenRecordingPermission() async -> Bool {
self.permissionGranted
}
}
@MainActor
final class StubAutomationService: UIAutomationServiceProtocol {
struct ClickCall: Sendable {
let target: ClickTarget
let clickType: ClickType
let sessionId: String?
}
struct TypeTextCall: Sendable {
let text: String
let target: String?
let clearExisting: Bool
let typingDelay: Int
let sessionId: String?
}
struct TypeActionsCall: Sendable {
let actions: [TypeAction]
let typingDelay: Int
let sessionId: String?
}
struct ScrollCall: Sendable {
let direction: ScrollDirection
let amount: Int
let target: String?
let smooth: Bool
let delay: Int
let sessionId: String?
}
struct SwipeCall: Sendable {
let from: CGPoint
let to: CGPoint
let duration: Int
let steps: Int
}
struct DragCall: Sendable {
let from: CGPoint
let to: CGPoint
let duration: Int
let steps: Int
let modifiers: String?
}
struct MoveMouseCall: Sendable {
let destination: CGPoint
let duration: Int
let steps: Int
}
struct HotkeyCall: Sendable {
let keys: String
let holdDuration: Int
}
struct WaitForElementCall: Sendable {
let target: ClickTarget
let timeout: TimeInterval
let sessionId: String?
}
private enum WaitTargetKey: Hashable {
case elementId(String)
case query(String)
case coordinates(x: Double, y: Double)
}
var clickCalls: [ClickCall] = []
var typeTextCalls: [TypeTextCall] = []
var typeActionsCalls: [TypeActionsCall] = []
var scrollCalls: [ScrollCall] = []
var swipeCalls: [SwipeCall] = []
var dragCalls: [DragCall] = []
var moveMouseCalls: [MoveMouseCall] = []
var hotkeyCalls: [HotkeyCall] = []
var waitForElementCalls: [WaitForElementCall] = []
var nextTypeActionsResult: TypeResult?
var typeActionsResultProvider: (([TypeAction], Int, String?) -> TypeResult)?
var waitForElementProvider: ((ClickTarget, TimeInterval, String?) -> WaitForElementResult)?
private var waitForElementResults: [WaitTargetKey: WaitForElementResult] = [:]
func setWaitForElementResult(_ result: WaitForElementResult, for target: ClickTarget) {
self.waitForElementResults[self.key(for: target)] = result
}
func detectElements(
in imageData: Data,
sessionId: String?,
windowContext: WindowContext?
) async throws -> ElementDetectionResult {
throw TestStubError.unimplemented(#function)
}
func click(target: ClickTarget, clickType: ClickType, sessionId: String?) async throws {
self.clickCalls.append(ClickCall(target: target, clickType: clickType, sessionId: sessionId))
}
func type(
text: String,
target: String?,
clearExisting: Bool,
typingDelay: Int,
sessionId: String?
) async throws {
self.typeTextCalls.append(
TypeTextCall(
text: text,
target: target,
clearExisting: clearExisting,
typingDelay: typingDelay,
sessionId: sessionId))
}
func typeActions(
_ actions: [TypeAction],
typingDelay: Int,
sessionId: String?
) async throws -> TypeResult {
self.typeActionsCalls.append(
TypeActionsCall(actions: actions, typingDelay: typingDelay, sessionId: sessionId)
)
if let provider = self.typeActionsResultProvider {
return provider(actions, typingDelay, sessionId)
}
if let nextResult = self.nextTypeActionsResult {
return nextResult
}
let totals = actions.reduce(into: (characters: 0, keyPresses: 0)) { partial, action in
switch action {
case let .text(text):
partial.characters += text.count
case .key:
partial.keyPresses += 1
case .clear:
break
}
}
return TypeResult(totalCharacters: totals.characters, keyPresses: totals.keyPresses)
}
func scroll(
direction: ScrollDirection,
amount: Int,
target: String?,
smooth: Bool,
delay: Int,
sessionId: String?
) async throws {
self.scrollCalls.append(
ScrollCall(
direction: direction,
amount: amount,
target: target,
smooth: smooth,
delay: delay,
sessionId: sessionId)
)
}
func hotkey(keys: String, holdDuration: Int) async throws {
self.hotkeyCalls.append(HotkeyCall(keys: keys, holdDuration: holdDuration))
}
func swipe(from: CGPoint, to: CGPoint, duration: Int, steps: Int) async throws {
self.swipeCalls.append(SwipeCall(from: from, to: to, duration: duration, steps: steps))
}
func hasAccessibilityPermission() async -> Bool {
true
}
func waitForElement(
target: ClickTarget,
timeout: TimeInterval,
sessionId: String?
) async throws -> WaitForElementResult {
self.waitForElementCalls.append(
WaitForElementCall(target: target, timeout: timeout, sessionId: sessionId)
)
if let provider = self.waitForElementProvider {
return provider(target, timeout, sessionId)
}
if let stored = self.waitForElementResults[self.key(for: target)] {
return stored
}
return WaitForElementResult(found: false, element: nil, waitTime: 0)
}
func drag(from: CGPoint, to: CGPoint, duration: Int, steps: Int, modifiers: String?) async throws {
self.dragCalls.append(
DragCall(from: from, to: to, duration: duration, steps: steps, modifiers: modifiers)
)
}
func moveMouse(to: CGPoint, duration: Int, steps: Int) async throws {
self.moveMouseCalls.append(
MoveMouseCall(destination: to, duration: duration, steps: steps)
)
}
func getFocusedElement() -> UIFocusInfo? {
nil
}
func findElement(
matching criteria: UIElementSearchCriteria,
in appName: String?
) async throws -> DetectedElement {
throw TestStubError.unimplemented(#function)
}
private func key(for target: ClickTarget) -> WaitTargetKey {
switch target {
case let .elementId(identifier):
.elementId(identifier)
case let .query(query):
.query(query)
case let .coordinates(point):
.coordinates(x: point.x, y: point.y)
}
}
}
@MainActor
final class StubApplicationService: ApplicationServiceProtocol {
var applications: [ServiceApplicationInfo]
var windowsByApp: [String: [ServiceWindowInfo]]
var launchResults: [String: ServiceApplicationInfo]
var launchCalls: [String] = []
var activateCalls: [String] = []
var quitCalls: [(identifier: String, force: Bool)] = []
var quitShouldSucceed = true
var hideCalls: [String] = []
var unhideCalls: [String] = []
var hideOtherCalls: [String] = []
var showAllCallCount = 0
init(applications: [ServiceApplicationInfo], windowsByApp: [String: [ServiceWindowInfo]] = [:]) {
self.applications = applications
self.windowsByApp = windowsByApp
self.launchResults = [:]
}
func listApplications() async throws -> UnifiedToolOutput<ServiceApplicationListData> {
let data = ServiceApplicationListData(applications: self.applications)
let summary = UnifiedToolOutput<ServiceApplicationListData>.Summary(
brief: "Stub application list",
status: .success,
counts: ["applications": self.applications.count]
)
return UnifiedToolOutput(
data: data,
summary: summary,
metadata: .init(duration: 0)
)
}
func findApplication(identifier: String) async throws -> ServiceApplicationInfo {
if let match = self.applications.first(where: { $0.name == identifier || $0.bundleIdentifier == identifier }) {
return match
}
throw PeekabooError.appNotFound(identifier)
}
func listWindows(for appIdentifier: String, timeout: Float?) async throws -> UnifiedToolOutput<ServiceWindowListData> {
let windows = self.windowsByApp[appIdentifier] ?? []
let targetApp = self.applications.first(where: { $0.name == appIdentifier })
let data = ServiceWindowListData(windows: windows, targetApplication: targetApp)
let summary = UnifiedToolOutput<ServiceWindowListData>.Summary(
brief: "Stub window list",
status: .success,
counts: ["windows": windows.count]
)
return UnifiedToolOutput(
data: data,
summary: summary,
metadata: .init(duration: 0)
)
}
func getFrontmostApplication() async throws -> ServiceApplicationInfo {
guard let first = self.applications.first else {
throw PeekabooError.appNotFound("frontmost")
}
return first
}
func isApplicationRunning(identifier: String) async -> Bool {
self.applications.contains { $0.name == identifier || $0.bundleIdentifier == identifier }
}
func launchApplication(identifier: String) async throws -> ServiceApplicationInfo {
self.launchCalls.append(identifier)
if let result = self.launchResults[identifier] {
return result
}
if let existing = self.applications.first(where: { $0.name == identifier || $0.bundleIdentifier == identifier }) {
return existing
}
return ServiceApplicationInfo(
processIdentifier: Int32.random(in: 1000...2000),
bundleIdentifier: "launched.\(identifier)",
name: identifier)
}
func activateApplication(identifier: String) async throws {
self.activateCalls.append(identifier)
}
func quitApplication(identifier: String, force: Bool) async throws -> Bool {
self.quitCalls.append((identifier: identifier, force: force))
return self.quitShouldSucceed
}
func hideApplication(identifier: String) async throws {
self.hideCalls.append(identifier)
}
func unhideApplication(identifier: String) async throws {
self.unhideCalls.append(identifier)
}
func hideOtherApplications(identifier: String) async throws {
self.hideOtherCalls.append(identifier)
}
func showAllApplications() async throws {
self.showAllCallCount += 1
}
}
final class StubSessionManager: SessionManagerProtocol, @unchecked Sendable {
private(set) var detectionResults: [String: ElementDetectionResult] = [:]
private(set) var sessionInfos: [String: SessionInfo] = [:]
private(set) var storedElements: [String: [String: PeekabooCore.UIElement]] = [:]
var mostRecentSessionId: String?
func createSession() async throws -> String {
let sessionId = UUID().uuidString
let now = Date()
self.sessionInfos[sessionId] = SessionInfo(
id: sessionId,
processId: 0,
createdAt: now,
lastAccessedAt: now,
sizeInBytes: 0,
screenshotCount: 0,
isActive: true
)
self.mostRecentSessionId = sessionId
return sessionId
}
func storeDetectionResult(sessionId: String, result: ElementDetectionResult) async throws {
self.detectionResults[sessionId] = result
self.mostRecentSessionId = sessionId
let existingInfo = self.sessionInfos[sessionId]
let createdAt = existingInfo?.createdAt ?? Date()
self.sessionInfos[sessionId] = SessionInfo(
id: sessionId,
processId: existingInfo?.processId ?? 0,
createdAt: createdAt,
lastAccessedAt: Date(),
sizeInBytes: existingInfo?.sizeInBytes ?? 0,
screenshotCount: (existingInfo?.screenshotCount ?? 0) + 1,
isActive: true
)
self.storedElements[sessionId] = result.elements.all
.reduce(into: [String: PeekabooCore.UIElement]()) { partial, element in
partial[element.id] = PeekabooCore.UIElement(
id: element.id,
elementId: element.id,
role: element.type.rawValue,
title: element.label,
label: element.label,
value: element.value,
description: nil,
help: nil,
roleDescription: nil,
identifier: element.attributes["identifier"],
frame: element.bounds,
isActionable: true,
parentId: nil,
children: [],
keyboardShortcut: nil
)
}
}
func getDetectionResult(sessionId: String) async throws -> ElementDetectionResult? {
self.detectionResults[sessionId]
}
func getMostRecentSession() async -> String? {
self.mostRecentSessionId
}
func listSessions() async throws -> [SessionInfo] {
Array(self.sessionInfos.values)
}
func cleanSession(sessionId: String) async throws {
self.detectionResults.removeValue(forKey: sessionId)
self.sessionInfos.removeValue(forKey: sessionId)
self.storedElements.removeValue(forKey: sessionId)
if self.mostRecentSessionId == sessionId {
self.mostRecentSessionId = nil
}
}
func cleanSessionsOlderThan(days: Int) async throws -> Int {
let threshold = Date().addingTimeInterval(TimeInterval(-days * 24 * 60 * 60))
let ids = self.sessionInfos.values
.filter { $0.lastAccessedAt < threshold }
.map { $0.id }
for id in ids {
try await self.cleanSession(sessionId: id)
}
return ids.count
}
func cleanAllSessions() async throws -> Int {
let count = self.sessionInfos.count
self.detectionResults.removeAll()
self.sessionInfos.removeAll()
self.storedElements.removeAll()
self.mostRecentSessionId = nil
return count
}
func getSessionStoragePath() -> String {
"/tmp/peekaboo-sessions"
}
func storeScreenshot(
sessionId: String,
screenshotPath: String,
applicationName: String?,
windowTitle: String?,
windowBounds: CGRect?
) async throws {
let existingInfo = self.sessionInfos[sessionId]
let createdAt = existingInfo?.createdAt ?? Date()
let screenshotCount = (existingInfo?.screenshotCount ?? 0) + 1
self.sessionInfos[sessionId] = SessionInfo(
id: sessionId,
processId: existingInfo?.processId ?? 0,
createdAt: createdAt,
lastAccessedAt: Date(),
sizeInBytes: existingInfo?.sizeInBytes ?? 0,
screenshotCount: screenshotCount,
isActive: existingInfo?.isActive ?? true
)
}
func getElement(sessionId: String, elementId: String) async throws -> PeekabooCore.UIElement? {
self.storedElements[sessionId]?[elementId]
}
func findElements(sessionId: String, matching query: String) async throws -> [PeekabooCore.UIElement] {
self.storedElements[sessionId]?.values.filter {
$0.label?.localizedCaseInsensitiveContains(query) == true ||
$0.title?.localizedCaseInsensitiveContains(query) == true
} ?? []
}
func getUIAutomationSession(sessionId: String) async throws -> UIAutomationSession? {
nil
}
}
final class StubFileService: FileServiceProtocol {
func cleanAllSessions(dryRun: Bool) async throws -> CleanResult {
CleanResult(sessionsRemoved: 0, bytesFreed: 0, sessionDetails: [], dryRun: dryRun)
}
func cleanOldSessions(hours: Int, dryRun: Bool) async throws -> CleanResult {
CleanResult(sessionsRemoved: 0, bytesFreed: 0, sessionDetails: [], dryRun: dryRun)
}
func cleanSpecificSession(sessionId: String, dryRun: Bool) async throws -> CleanResult {
CleanResult(sessionsRemoved: 0, bytesFreed: 0, sessionDetails: [], dryRun: dryRun)
}
func getSessionCacheDirectory() -> URL {
URL(fileURLWithPath: "/tmp/peekaboo-sessions")
}
func calculateDirectorySize(_ directory: URL) async throws -> Int64 {
0
}
func listSessions() async throws -> [FileSessionInfo] {
[]
}
}
@available(macOS 14.0, *)
final class StubProcessService: ProcessServiceProtocol, @unchecked Sendable {
struct LoadScriptCall {
let path: String
}
struct ExecuteScriptCall {
let script: PeekabooScript
let failFast: Bool
let verbose: Bool
}
struct ExecuteStepCall {
let step: ScriptStep
let sessionId: String?
}
var loadScriptCalls: [LoadScriptCall] = []
var executeScriptCalls: [ExecuteScriptCall] = []
var executeStepCalls: [ExecuteStepCall] = []
var scriptsByPath: [String: PeekabooScript] = [:]
var loadScriptProvider: ((String) async throws -> PeekabooScript)?
var executeScriptProvider: ((PeekabooScript, Bool, Bool) async throws -> [StepResult])?
var executeStepProvider: ((ScriptStep, String?) async throws -> StepExecutionResult)?
var nextScript: PeekabooScript?
var nextExecuteScriptResults: [StepResult]?
var nextStepResult: StepExecutionResult?
func loadScript(from path: String) async throws -> PeekabooScript {
self.loadScriptCalls.append(LoadScriptCall(path: path))
if let provider = self.loadScriptProvider {
return try await provider(path)
}
if let script = self.scriptsByPath[path] ?? self.scriptsByPath["*"] {
return script
}
if let script = self.nextScript {
return script
}
throw TestStubError.unimplemented(#function)
}
func executeScript(
_ script: PeekabooScript,
failFast: Bool,
verbose: Bool
) async throws -> [StepResult] {
self.executeScriptCalls.append(ExecuteScriptCall(script: script, failFast: failFast, verbose: verbose))
if let provider = self.executeScriptProvider {
return try await provider(script, failFast, verbose)
}
if let results = self.nextExecuteScriptResults {
return results
}
return []
}
func executeStep(
_ step: ScriptStep,
sessionId: String?
) async throws -> StepExecutionResult {
self.executeStepCalls.append(ExecuteStepCall(step: step, sessionId: sessionId))
if let provider = self.executeStepProvider {
return try await provider(step, sessionId)
}
if let result = self.nextStepResult {
return result
}
throw TestStubError.unimplemented(#function)
}
}
@MainActor
final class StubDockService: DockServiceProtocol {
var items: [DockItem]
var autoHidden: Bool
init(items: [DockItem] = [], autoHidden: Bool = false) {
self.items = items
self.autoHidden = autoHidden
}
func listDockItems(includeAll: Bool) async throws -> [DockItem] {
self.items
}
func launchFromDock(appName: String) async throws {
throw TestStubError.unimplemented(#function)
}
func addToDock(path: String, persistent: Bool) async throws {
throw TestStubError.unimplemented(#function)
}
func removeFromDock(appName: String) async throws {
throw TestStubError.unimplemented(#function)
}
func rightClickDockItem(appName: String, menuItem: String?) async throws {
throw TestStubError.unimplemented(#function)
}
func hideDock() async throws {
throw TestStubError.unimplemented(#function)
}
func showDock() async throws {
throw TestStubError.unimplemented(#function)
}
func isDockAutoHidden() async -> Bool {
self.autoHidden
}
func findDockItem(name: String) async throws -> DockItem {
guard let match = self.items.first(where: { $0.title == name }) else {
throw PeekabooError.elementNotFound(name)
}
return match
}
}
@MainActor
final class StubScreenService: ScreenServiceProtocol {
var screens: [ScreenInfo]
init(screens: [ScreenInfo] = []) {
self.screens = screens
}
func listScreens() -> [ScreenInfo] {
self.screens
}
func screenContainingWindow(bounds: CGRect) -> ScreenInfo? {
self.screens.first
}
func screen(at index: Int) -> ScreenInfo? {
guard index >= 0, index < self.screens.count else { return nil }
return self.screens[index]
}
var primaryScreen: ScreenInfo? {
self.screens.first
}
}
@MainActor
final class StubMenuService: MenuServiceProtocol {
var menusByApp: [String: MenuStructure]
var frontmostMenus: MenuStructure?
var menuExtras: [MenuExtraInfo]
var clickPathCalls: [(app: String, path: String)] = []
var clickItemCalls: [(app: String, item: String)] = []
var clickExtraCalls: [String] = []
var listMenusRequests: [String] = []
init(
menusByApp: [String: MenuStructure],
frontmostMenus: MenuStructure? = nil,
menuExtras: [MenuExtraInfo] = []
) {
self.menusByApp = menusByApp
self.frontmostMenus = frontmostMenus
self.menuExtras = menuExtras
}
func listMenus(for appIdentifier: String) async throws -> MenuStructure {
self.listMenusRequests.append(appIdentifier)
guard let structure = self.menusByApp[appIdentifier] else {
throw PeekabooError.menuNotFound(appIdentifier)
}
return structure
}
func listFrontmostMenus() async throws -> MenuStructure {
guard let menus = self.frontmostMenus else {
throw PeekabooError.menuNotFound("frontmost")
}
return menus
}
func clickMenuItem(app: String, itemPath: String) async throws {
guard self.menusByApp[app] != nil else {
throw PeekabooError.menuNotFound(app)
}
self.clickPathCalls.append((app, itemPath))
}
func clickMenuItemByName(app: String, itemName: String) async throws {
guard self.menusByApp[app] != nil else {
throw PeekabooError.menuNotFound(app)
}
self.clickItemCalls.append((app, itemName))
}
func clickMenuExtra(title: String) async throws {
guard self.menuExtras.contains(where: { $0.title == title }) else {
throw PeekabooError.menuNotFound(title)
}
self.clickExtraCalls.append(title)
}
func listMenuExtras() async throws -> [MenuExtraInfo] {
self.menuExtras
}
func listMenuBarItems() async throws -> [MenuBarItemInfo] {
[]
}
func clickMenuBarItem(named name: String) async throws -> PeekabooCore.ClickResult {
throw TestStubError.unimplemented(#function)
}
func clickMenuBarItem(at index: Int) async throws -> PeekabooCore.ClickResult {
throw TestStubError.unimplemented(#function)
}
}
@MainActor
final class StubDialogService: DialogServiceProtocol {
var dialogElements: DialogElements?
init(elements: DialogElements? = nil) {
self.dialogElements = elements
}
func findActiveDialog(windowTitle: String?) async throws -> DialogInfo {
guard let elements = self.dialogElements else {
throw PeekabooError.elementNotFound(windowTitle ?? "dialog")
}
return elements.dialogInfo
}
func clickButton(buttonText: String, windowTitle: String?) async throws -> DialogActionResult {
throw TestStubError.unimplemented(#function)
}
func enterText(
text: String,
fieldIdentifier: String?,
clearExisting: Bool,
windowTitle: String?
) async throws -> DialogActionResult {
throw TestStubError.unimplemented(#function)
}
func handleFileDialog(path: String?, filename: String?, actionButton: String) async throws -> DialogActionResult {
throw TestStubError.unimplemented(#function)
}
func dismissDialog(force: Bool, windowTitle: String?) async throws -> DialogActionResult {
throw TestStubError.unimplemented(#function)
}
func listDialogElements(windowTitle: String?) async throws -> DialogElements {
guard let elements = self.dialogElements else {
throw PeekabooError.elementNotFound(windowTitle ?? "dialog")
}
return elements
}
}
@MainActor
final class StubWindowService: WindowManagementServiceProtocol {
var windowsByApp: [String: [ServiceWindowInfo]]
init(windowsByApp: [String: [ServiceWindowInfo]]) {
self.windowsByApp = windowsByApp
}
func closeWindow(target: WindowTarget) async throws {
throw TestStubError.unimplemented(#function)
}
func minimizeWindow(target: WindowTarget) async throws {
throw TestStubError.unimplemented(#function)
}
func maximizeWindow(target: WindowTarget) async throws {
throw TestStubError.unimplemented(#function)
}
func moveWindow(target: WindowTarget, to position: CGPoint) async throws {
throw TestStubError.unimplemented(#function)
}
func resizeWindow(target: WindowTarget, to size: CGSize) async throws {
throw TestStubError.unimplemented(#function)
}
func setWindowBounds(target: WindowTarget, bounds: CGRect) async throws {
throw TestStubError.unimplemented(#function)
}
func focusWindow(target: WindowTarget) async throws {
throw TestStubError.unimplemented(#function)
}
func listWindows(target: WindowTarget) async throws -> [ServiceWindowInfo] {
switch target {
case let .application(app):
return self.windowsByApp[app] ?? []
case let .applicationAndTitle(app, title):
return self.windowsByApp[app]?.filter { $0.title.contains(title) } ?? []
case .frontmost:
return self.windowsByApp.values.first ?? []
case let .windowId(id):
return self.windowsByApp.values.flatMap { $0 }.filter { $0.windowID == id }
case let .title(title):
return self.windowsByApp.values.flatMap { $0 }.filter { $0.title.contains(title) }
case let .index(app, index):
guard let windows = self.windowsByApp[app], index < windows.count else { return [] }
return [windows[index]]
}
}
func getFocusedWindow() async throws -> ServiceWindowInfo? {
nil
}
}
@MainActor
final class StubSpaceService: SpaceCommandSpaceService {
var spaces: [SpaceInfo]
var windowSpaces: [Int: [SpaceInfo]]
var switchCalls: [CGSSpaceID] = []
var moveWindowCalls: [(windowID: CGWindowID, spaceID: CGSSpaceID?)] = []
var moveToCurrentCalls: [CGWindowID] = []
init(spaces: [SpaceInfo], windowSpaces: [Int: [SpaceInfo]] = [:]) {
self.spaces = spaces
self.windowSpaces = windowSpaces
}
func getAllSpaces() -> [SpaceInfo] {
self.spaces
}
func getSpacesForWindow(windowID: CGWindowID) -> [SpaceInfo] {
self.windowSpaces[Int(windowID)] ?? []
}
func moveWindowToCurrentSpace(windowID: CGWindowID) throws {
self.moveToCurrentCalls.append(windowID)
}
func moveWindowToSpace(windowID: CGWindowID, spaceID: CGSSpaceID) throws {
self.moveWindowCalls.append((windowID, spaceID))
}
func switchToSpace(_ spaceID: CGSSpaceID) async throws {
self.switchCalls.append(spaceID)
}
}
// MARK: - Aggregator
@MainActor
enum TestServicesFactory {
static func makePeekabooServices(
applications: ApplicationServiceProtocol = StubApplicationService(applications: []),
windows: WindowManagementServiceProtocol = StubWindowService(windowsByApp: [:]),
menu: MenuServiceProtocol = StubMenuService(menusByApp: [:]),
dialogs: DialogServiceProtocol = StubDialogService(),
dock: DockServiceProtocol = StubDockService(),
sessions: SessionManagerProtocol = StubSessionManager(),
files: FileServiceProtocol = StubFileService(),
process: ProcessServiceProtocol = StubProcessService(),
screens: [ScreenInfo] = [],
automation: UIAutomationServiceProtocol = StubAutomationService(),
screenCapture: ScreenCaptureServiceProtocol = StubScreenCaptureService()
) -> PeekabooServices {
let screenService = StubScreenService(screens: screens)
let services = PeekabooServices(
logging: LoggingService(),
screenCapture: screenCapture,
applications: applications,
automation: automation,
windows: windows,
menu: menu,
dock: dock,
dialogs: dialogs,
sessions: sessions,
files: files,
process: process,
permissions: PermissionsService(),
audioInput: AudioInputService(aiService: PeekabooAIService()),
agent: nil,
configuration: ConfigurationManager.shared,
screens: screenService)
return services
}
@MainActor
struct AutomationTestContext {
let services: PeekabooServices
let automation: StubAutomationService
let sessions: StubSessionManager
}
static func makeAutomationTestContext(
automation: StubAutomationService = StubAutomationService(),
sessions: StubSessionManager = StubSessionManager(),
applications: ApplicationServiceProtocol = StubApplicationService(applications: []),
windows: WindowManagementServiceProtocol = StubWindowService(windowsByApp: [:]),
menu: MenuServiceProtocol = StubMenuService(menusByApp: [:]),
dialogs: DialogServiceProtocol = StubDialogService(),
dock: DockServiceProtocol = StubDockService(),
files: FileServiceProtocol = StubFileService(),
process: ProcessServiceProtocol = StubProcessService(),
screens: [ScreenInfo] = [],
screenCapture: ScreenCaptureServiceProtocol = StubScreenCaptureService()
) -> AutomationTestContext {
let services = self.makePeekabooServices(
applications: applications,
windows: windows,
menu: menu,
dialogs: dialogs,
dock: dock,
sessions: sessions,
files: files,
process: process,
screens: screens,
automation: automation,
screenCapture: screenCapture
)
return AutomationTestContext(services: services, automation: automation, sessions: sessions)
}
}