Apply SwiftFormat and fix all SwiftLint violations

- Run SwiftFormat on all Swift files for consistent formatting
- Fix all critical SwiftLint violations:
  * Replace count > 0 with \!isEmpty
  * Use descriptive variable names instead of i, x, y
  * Replace % operator with isMultiple(of:)
  * Fix force try violations
  * Use trailing closure syntax
  * Replace for-if patterns with for-where
  * Fix line length violations
  * Use Data(_:) instead of .data(using:)\!
- Ensure zero SwiftLint errors for clean code quality

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Peter Steinberger 2025-06-08 00:18:23 +01:00
parent 45f087496a
commit e894210dbd
14 changed files with 865 additions and 832 deletions

View file

@ -41,22 +41,20 @@ struct AppsSubcommand: ParsableCommand {
} }
private func handleError(_ error: Error) { private func handleError(_ error: Error) {
let captureError: CaptureError let captureError: CaptureError = if let err = error as? CaptureError {
if let err = error as? CaptureError { err
captureError = err
} else { } else {
captureError = .unknownError(error.localizedDescription) .unknownError(error.localizedDescription)
} }
if jsonOutput { if jsonOutput {
let code: ErrorCode let code: ErrorCode = switch captureError {
switch captureError {
case .screenRecordingPermissionDenied: case .screenRecordingPermissionDenied:
code = .PERMISSION_ERROR_SCREEN_RECORDING .PERMISSION_ERROR_SCREEN_RECORDING
case .accessibilityPermissionDenied: case .accessibilityPermissionDenied:
code = .PERMISSION_ERROR_ACCESSIBILITY .PERMISSION_ERROR_ACCESSIBILITY
default: default:
code = .INTERNAL_SWIFT_ERROR .INTERNAL_SWIFT_ERROR
} }
outputError( outputError(
message: captureError.localizedDescription, message: captureError.localizedDescription,
@ -142,24 +140,22 @@ struct WindowsSubcommand: ParsableCommand {
} }
private func handleError(_ error: Error) { private func handleError(_ error: Error) {
let captureError: CaptureError let captureError: CaptureError = if let err = error as? CaptureError {
if let err = error as? CaptureError { err
captureError = err
} else { } else {
captureError = .unknownError(error.localizedDescription) .unknownError(error.localizedDescription)
} }
if jsonOutput { if jsonOutput {
let code: ErrorCode let code: ErrorCode = switch captureError {
switch captureError {
case .screenRecordingPermissionDenied: case .screenRecordingPermissionDenied:
code = .PERMISSION_ERROR_SCREEN_RECORDING .PERMISSION_ERROR_SCREEN_RECORDING
case .accessibilityPermissionDenied: case .accessibilityPermissionDenied:
code = .PERMISSION_ERROR_ACCESSIBILITY .PERMISSION_ERROR_ACCESSIBILITY
case .appNotFound: case .appNotFound:
code = .APP_NOT_FOUND .APP_NOT_FOUND
default: default:
code = .INTERNAL_SWIFT_ERROR .INTERNAL_SWIFT_ERROR
} }
outputError( outputError(
message: captureError.localizedDescription, message: captureError.localizedDescription,

View file

@ -56,8 +56,8 @@ class Logger {
} }
func getDebugLogs() -> [String] { func getDebugLogs() -> [String] {
return queue.sync { queue.sync {
return self.debugLogs self.debugLogs
} }
} }

View file

@ -118,9 +118,11 @@ enum CaptureError: Error, LocalizedError {
case .noDisplaysAvailable: case .noDisplaysAvailable:
"No displays available for capture." "No displays available for capture."
case .screenRecordingPermissionDenied: case .screenRecordingPermissionDenied:
"Screen recording permission is required. Please grant it in System Settings > Privacy & Security > Screen Recording." "Screen recording permission is required. " +
"Please grant it in System Settings > Privacy & Security > Screen Recording."
case .accessibilityPermissionDenied: case .accessibilityPermissionDenied:
"Accessibility permission is required for some operations. Please grant it in System Settings > Privacy & Security > Accessibility." "Accessibility permission is required for some operations. " +
"Please grant it in System Settings > Privacy & Security > Accessibility."
case .invalidDisplayID: case .invalidDisplayID:
"Invalid display ID provided." "Invalid display ID provided."
case .captureCreationFailed: case .captureCreationFailed:

View file

@ -3,6 +3,6 @@
// To use this file for development, copy it to Version.swift: // To use this file for development, copy it to Version.swift:
// cp Version.swift.development Version.swift // cp Version.swift.development Version.swift
struct Version { enum Version {
static let current = "dev" static let current = "dev"
} }

View file

@ -1,47 +1,47 @@
import AppKit
@testable import peekaboo @testable import peekaboo
import Testing import Testing
import AppKit
@Suite("ApplicationFinder Tests", .tags(.applicationFinder, .unit)) @Suite("ApplicationFinder Tests", .tags(.applicationFinder, .unit))
struct ApplicationFinderTests { struct ApplicationFinderTests {
// MARK: - Test Data // MARK: - Test Data
private static let testIdentifiers = [ private static let testIdentifiers = [
"Finder", "finder", "FINDER", "Find", "com.apple.finder" "Finder", "finder", "FINDER", "Find", "com.apple.finder"
] ]
private static let invalidIdentifiers = [ private static let invalidIdentifiers = [
"", " ", "NonExistentApp12345", "invalid.bundle.id", "", " ", "NonExistentApp12345", "invalid.bundle.id",
String(repeating: "a", count: 1000) String(repeating: "a", count: 1000)
] ]
// MARK: - Find Application Tests // MARK: - Find Application Tests
@Test("Finding an app by exact name match", .tags(.fast)) @Test("Finding an app by exact name match", .tags(.fast))
func findApplicationExactMatch() throws { func findApplicationExactMatch() throws {
// Test finding an app that should always be running on macOS // Test finding an app that should always be running on macOS
let result = try ApplicationFinder.findApplication(identifier: "Finder") let result = try ApplicationFinder.findApplication(identifier: "Finder")
#expect(result.localizedName == "Finder") #expect(result.localizedName == "Finder")
#expect(result.bundleIdentifier == "com.apple.finder") #expect(result.bundleIdentifier == "com.apple.finder")
} }
@Test("Finding an app is case-insensitive", .tags(.fast)) @Test("Finding an app is case-insensitive", .tags(.fast))
func findApplicationCaseInsensitive() throws { func findApplicationCaseInsensitive() throws {
// Test case-insensitive matching // Test case-insensitive matching
let result = try ApplicationFinder.findApplication(identifier: "finder") let result = try ApplicationFinder.findApplication(identifier: "finder")
#expect(result.localizedName == "Finder") #expect(result.localizedName == "Finder")
} }
@Test("Finding an app by bundle identifier", .tags(.fast)) @Test("Finding an app by bundle identifier", .tags(.fast))
func findApplicationByBundleIdentifier() throws { func findApplicationByBundleIdentifier() throws {
// Test finding by bundle identifier // Test finding by bundle identifier
let result = try ApplicationFinder.findApplication(identifier: "com.apple.finder") let result = try ApplicationFinder.findApplication(identifier: "com.apple.finder")
#expect(result.bundleIdentifier == "com.apple.finder") #expect(result.bundleIdentifier == "com.apple.finder")
} }
@Test("Throws error when app is not found", .tags(.fast)) @Test("Throws error when app is not found", .tags(.fast))
func findApplicationNotFound() throws { func findApplicationNotFound() throws {
// Test app not found error // Test app not found error
@ -49,49 +49,51 @@ struct ApplicationFinderTests {
try ApplicationFinder.findApplication(identifier: "NonExistentApp12345") try ApplicationFinder.findApplication(identifier: "NonExistentApp12345")
} }
} }
@Test("Finding an app by partial name match", .tags(.fast)) @Test("Finding an app by partial name match", .tags(.fast))
func findApplicationPartialMatch() throws { func findApplicationPartialMatch() throws {
// Test partial name matching // Test partial name matching
let result = try ApplicationFinder.findApplication(identifier: "Find") let result = try ApplicationFinder.findApplication(identifier: "Find")
// Should find Finder as closest match // Should find Finder as closest match
#expect(result.localizedName == "Finder") #expect(result.localizedName == "Finder")
} }
// MARK: - Parameterized Tests // MARK: - Parameterized Tests
@Test("Finding apps with various identifiers", @Test(
arguments: [ "Finding apps with various identifiers",
("Finder", "com.apple.finder"), arguments: [
("finder", "com.apple.finder"), ("Finder", "com.apple.finder"),
("FINDER", "com.apple.finder"), ("finder", "com.apple.finder"),
("com.apple.finder", "com.apple.finder") ("FINDER", "com.apple.finder"),
]) ("com.apple.finder", "com.apple.finder")
]
)
func findApplicationVariousIdentifiers(identifier: String, expectedBundleId: String) throws { func findApplicationVariousIdentifiers(identifier: String, expectedBundleId: String) throws {
let result = try ApplicationFinder.findApplication(identifier: identifier) let result = try ApplicationFinder.findApplication(identifier: identifier)
#expect(result.bundleIdentifier == expectedBundleId) #expect(result.bundleIdentifier == expectedBundleId)
} }
// MARK: - Get All Running Applications Tests // MARK: - Get All Running Applications Tests
@Test("Getting all running applications returns non-empty list", .tags(.fast)) @Test("Getting all running applications returns non-empty list", .tags(.fast))
func getAllRunningApplications() { func getAllRunningApplications() {
// Test getting all running applications // Test getting all running applications
let apps = ApplicationFinder.getAllRunningApplications() let apps = ApplicationFinder.getAllRunningApplications()
// Should have at least some apps running // Should have at least some apps running
#expect(apps.count > 0) #expect(!apps.isEmpty)
// Should include Finder // Should include Finder
let hasFinder = apps.contains { $0.app_name == "Finder" } let hasFinder = apps.contains { $0.app_name == "Finder" }
#expect(hasFinder == true) #expect(hasFinder == true)
} }
@Test("All running applications have required properties", .tags(.fast)) @Test("All running applications have required properties", .tags(.fast))
func allApplicationsHaveRequiredProperties() { func allApplicationsHaveRequiredProperties() {
let apps = ApplicationFinder.getAllRunningApplications() let apps = ApplicationFinder.getAllRunningApplications()
for app in apps { for app in apps {
#expect(!app.app_name.isEmpty) #expect(!app.app_name.isEmpty)
#expect(!app.bundle_id.isEmpty) #expect(!app.bundle_id.isEmpty)
@ -99,14 +101,14 @@ struct ApplicationFinderTests {
#expect(app.window_count >= 0) #expect(app.window_count >= 0)
} }
} }
// MARK: - Edge Cases and Advanced Tests // MARK: - Edge Cases and Advanced Tests
@Test("Finding app with special characters in name", .tags(.fast)) @Test("Finding app with special characters in name", .tags(.fast))
func findApplicationSpecialCharacters() throws { func findApplicationSpecialCharacters() throws {
// Test apps with special characters (if available) // Test apps with special characters (if available)
let specialApps = ["1Password", "CleanMyMac", "MacBook Pro"] let specialApps = ["1Password", "CleanMyMac", "MacBook Pro"]
for appName in specialApps { for appName in specialApps {
do { do {
let result = try ApplicationFinder.findApplication(identifier: appName) let result = try ApplicationFinder.findApplication(identifier: appName)
@ -118,25 +120,27 @@ struct ApplicationFinderTests {
} }
} }
} }
@Test("Fuzzy matching algorithm scoring", .tags(.fast)) @Test("Fuzzy matching algorithm scoring", .tags(.fast))
func fuzzyMatchingScoring() throws { func fuzzyMatchingScoring() throws {
// Test that exact matches get highest scores // Test that exact matches get highest scores
let finder = try ApplicationFinder.findApplication(identifier: "Finder") let finder = try ApplicationFinder.findApplication(identifier: "Finder")
#expect(finder.localizedName == "Finder") #expect(finder.localizedName == "Finder")
// Test prefix matching works // Test prefix matching works
let findResult = try ApplicationFinder.findApplication(identifier: "Find") let findResult = try ApplicationFinder.findApplication(identifier: "Find")
#expect(findResult.localizedName == "Finder") #expect(findResult.localizedName == "Finder")
} }
@Test("Bundle identifier parsing edge cases", @Test(
arguments: [ "Bundle identifier parsing edge cases",
"com.apple", arguments: [
"apple.finder", "com.apple",
"finder", "apple.finder",
"com.apple.finder.extra" "finder",
]) "com.apple.finder.extra"
]
)
func bundleIdentifierEdgeCases(partialBundleId: String) throws { func bundleIdentifierEdgeCases(partialBundleId: String) throws {
// Should either find Finder or throw appropriate error // Should either find Finder or throw appropriate error
do { do {
@ -147,7 +151,7 @@ struct ApplicationFinderTests {
#expect(Bool(true)) #expect(Bool(true))
} }
} }
@Test("Fuzzy matching prefers exact matches", .tags(.fast)) @Test("Fuzzy matching prefers exact matches", .tags(.fast))
func fuzzyMatchingPrefersExact() throws { func fuzzyMatchingPrefersExact() throws {
// If we have multiple matches, exact should win // If we have multiple matches, exact should win
@ -155,21 +159,23 @@ struct ApplicationFinderTests {
#expect(result.localizedName == "Finder") #expect(result.localizedName == "Finder")
#expect(result.bundleIdentifier == "com.apple.finder") #expect(result.bundleIdentifier == "com.apple.finder")
} }
@Test("Performance: Finding apps multiple times", @Test(
arguments: 1...10) "Performance: Finding apps multiple times",
arguments: 1...10
)
func findApplicationPerformance(iteration: Int) throws { func findApplicationPerformance(iteration: Int) throws {
// Test that finding an app completes quickly even when called multiple times // Test that finding an app completes quickly even when called multiple times
let result = try ApplicationFinder.findApplication(identifier: "Finder") let result = try ApplicationFinder.findApplication(identifier: "Finder")
#expect(result.localizedName == "Finder") #expect(result.localizedName == "Finder")
} }
@Test("Stress test: Search with many running apps", .tags(.performance)) @Test("Stress test: Search with many running apps", .tags(.performance))
func stressTestManyApps() { func stressTestManyApps() {
// Get current app count for baseline // Get current app count for baseline
let apps = ApplicationFinder.getAllRunningApplications() let apps = ApplicationFinder.getAllRunningApplications()
#expect(apps.count > 0) #expect(!apps.isEmpty)
// Test search performance doesn't degrade with app list size // Test search performance doesn't degrade with app list size
let startTime = CFAbsoluteTimeGetCurrent() let startTime = CFAbsoluteTimeGetCurrent()
do { do {
@ -180,20 +186,22 @@ struct ApplicationFinderTests {
Issue.record("Finder should always be found in performance test") Issue.record("Finder should always be found in performance test")
} }
} }
// MARK: - Integration Tests // MARK: - Integration Tests
@Test("Find and verify running state of system apps", @Test(
arguments: [ "Find and verify running state of system apps",
("Finder", true), arguments: [
("Dock", true), ("Finder", true),
("SystemUIServer", true) ("Dock", true),
]) ("SystemUIServer", true)
]
)
func verifySystemAppsRunning(appName: String, shouldBeRunning: Bool) throws { func verifySystemAppsRunning(appName: String, shouldBeRunning: Bool) throws {
do { do {
let result = try ApplicationFinder.findApplication(identifier: appName) let result = try ApplicationFinder.findApplication(identifier: appName)
#expect(result.localizedName != nil) #expect(result.localizedName != nil)
// Verify the app is in the running list // Verify the app is in the running list
let runningApps = ApplicationFinder.getAllRunningApplications() let runningApps = ApplicationFinder.getAllRunningApplications()
let isInList = runningApps.contains { $0.bundle_id == result.bundleIdentifier } let isInList = runningApps.contains { $0.bundle_id == result.bundleIdentifier }
@ -204,17 +212,17 @@ struct ApplicationFinderTests {
} }
} }
} }
@Test("Verify frontmost application detection", .tags(.integration)) @Test("Verify frontmost application detection", .tags(.integration))
func verifyFrontmostApp() throws { func verifyFrontmostApp() throws {
// Get the frontmost app using NSWorkspace // Get the frontmost app using NSWorkspace
let frontmostApp = NSWorkspace.shared.frontmostApplication let frontmostApp = NSWorkspace.shared.frontmostApplication
// Try to find it using our ApplicationFinder // Try to find it using our ApplicationFinder
if let bundleId = frontmostApp?.bundleIdentifier { if let bundleId = frontmostApp?.bundleIdentifier {
let result = try ApplicationFinder.findApplication(identifier: bundleId) let result = try ApplicationFinder.findApplication(identifier: bundleId)
#expect(result.bundleIdentifier == bundleId) #expect(result.bundleIdentifier == bundleId)
// Verify it's marked as active in our list // Verify it's marked as active in our list
let runningApps = ApplicationFinder.getAllRunningApplications() let runningApps = ApplicationFinder.getAllRunningApplications()
let appInfo = runningApps.first { $0.bundle_id == bundleId } let appInfo = runningApps.first { $0.bundle_id == bundleId }
@ -227,21 +235,20 @@ struct ApplicationFinderTests {
@Suite("ApplicationFinder Edge Cases", .tags(.applicationFinder, .unit)) @Suite("ApplicationFinder Edge Cases", .tags(.applicationFinder, .unit))
struct ApplicationFinderEdgeCaseTests { struct ApplicationFinderEdgeCaseTests {
@Test("Empty identifier throws appropriate error", .tags(.fast)) @Test("Empty identifier throws appropriate error", .tags(.fast))
func emptyIdentifierError() { func emptyIdentifierError() {
#expect(throws: (any Error).self) { #expect(throws: (any Error).self) {
try ApplicationFinder.findApplication(identifier: "") try ApplicationFinder.findApplication(identifier: "")
} }
} }
@Test("Whitespace-only identifier throws appropriate error", .tags(.fast)) @Test("Whitespace-only identifier throws appropriate error", .tags(.fast))
func whitespaceIdentifierError() { func whitespaceIdentifierError() {
#expect(throws: (any Error).self) { #expect(throws: (any Error).self) {
try ApplicationFinder.findApplication(identifier: " ") try ApplicationFinder.findApplication(identifier: " ")
} }
} }
@Test("Very long identifier doesn't crash", .tags(.fast)) @Test("Very long identifier doesn't crash", .tags(.fast))
func veryLongIdentifier() { func veryLongIdentifier() {
let longIdentifier = String(repeating: "a", count: 1000) let longIdentifier = String(repeating: "a", count: 1000)
@ -249,9 +256,11 @@ struct ApplicationFinderEdgeCaseTests {
try ApplicationFinder.findApplication(identifier: longIdentifier) try ApplicationFinder.findApplication(identifier: longIdentifier)
} }
} }
@Test("Unicode identifiers are handled correctly", @Test(
arguments: ["😀App", "App™", "Приложение", "アプリ"]) "Unicode identifiers are handled correctly",
arguments: ["😀App", "App™", "Приложение", "アプリ"]
)
func unicodeIdentifiers(identifier: String) { func unicodeIdentifiers(identifier: String) {
// Should not crash, either finds or throws appropriate error // Should not crash, either finds or throws appropriate error
do { do {
@ -262,19 +271,19 @@ struct ApplicationFinderEdgeCaseTests {
#expect(Bool(true)) #expect(Bool(true))
} }
} }
@Test("Case sensitivity in matching", .tags(.fast)) @Test("Case sensitivity in matching", .tags(.fast))
func caseSensitivityMatching() throws { func caseSensitivityMatching() throws {
// Test various case combinations // Test various case combinations
let caseVariations = ["finder", "FINDER", "Finder", "fInDeR"] let caseVariations = ["finder", "FINDER", "Finder", "fInDeR"]
for variation in caseVariations { for variation in caseVariations {
let result = try ApplicationFinder.findApplication(identifier: variation) let result = try ApplicationFinder.findApplication(identifier: variation)
#expect(result.localizedName == "Finder") #expect(result.localizedName == "Finder")
#expect(result.bundleIdentifier == "com.apple.finder") #expect(result.bundleIdentifier == "com.apple.finder")
} }
} }
@Test("Concurrent application searches", .tags(.concurrency)) @Test("Concurrent application searches", .tags(.concurrency))
func concurrentSearches() async { func concurrentSearches() async {
// Test thread safety of application finder // Test thread safety of application finder
@ -289,35 +298,33 @@ struct ApplicationFinderEdgeCaseTests {
} }
} }
} }
var successCount = 0 var successCount = 0
for await success in group { for await success in group where success {
if success { successCount += 1
successCount += 1
}
} }
// All searches should succeed for Finder // All searches should succeed for Finder
#expect(successCount == 10) #expect(successCount == 10)
} }
} }
@Test("Memory usage with large app lists", .tags(.performance)) @Test("Memory usage with large app lists", .tags(.performance))
func memoryUsageTest() { func memoryUsageTest() {
// Test memory doesn't grow excessively with repeated calls // Test memory doesn't grow excessively with repeated calls
for _ in 1...5 { for _ in 1...5 {
let apps = ApplicationFinder.getAllRunningApplications() let apps = ApplicationFinder.getAllRunningApplications()
#expect(apps.count > 0) #expect(!apps.isEmpty)
} }
// If we get here without crashing, memory management is working // If we get here without crashing, memory management is working
#expect(Bool(true)) #expect(Bool(true))
} }
@Test("Application list sorting consistency", .tags(.fast)) @Test("Application list sorting consistency", .tags(.fast))
func applicationListSorting() { func applicationListSorting() {
let apps = ApplicationFinder.getAllRunningApplications() let apps = ApplicationFinder.getAllRunningApplications()
// Verify list is sorted by name (case-insensitive) // Verify list is sorted by name (case-insensitive)
for index in 1..<apps.count { for index in 1..<apps.count {
let current = apps[index].app_name.lowercased() let current = apps[index].app_name.lowercased()
@ -325,19 +332,19 @@ struct ApplicationFinderEdgeCaseTests {
#expect(current >= previous) #expect(current >= previous)
} }
} }
@Test("Window count accuracy", .tags(.integration)) @Test("Window count accuracy", .tags(.integration))
func windowCountAccuracy() { func windowCountAccuracy() {
let apps = ApplicationFinder.getAllRunningApplications() let apps = ApplicationFinder.getAllRunningApplications()
for app in apps { for app in apps {
// Window count should be non-negative // Window count should be non-negative
#expect(app.window_count >= 0) #expect(app.window_count >= 0)
// Finder should typically have at least one window // Finder should typically have at least one window
if app.app_name == "Finder" { if app.app_name == "Finder" {
#expect(app.window_count >= 0) // Could be 0 if all windows minimized #expect(app.window_count >= 0) // Could be 0 if all windows minimized
} }
} }
} }
} }

View file

@ -1,28 +1,27 @@
import AppKit
import Foundation
@testable import peekaboo @testable import peekaboo
import Testing import Testing
import Foundation
import AppKit
@Suite("Image Capture Logic Tests", .tags(.imageCapture, .unit)) @Suite("Image Capture Logic Tests", .tags(.imageCapture, .unit))
struct ImageCaptureLogicTests { struct ImageCaptureLogicTests {
// MARK: - File Name Generation Tests // MARK: - File Name Generation Tests
@Test("File name generation for displays", .tags(.fast)) @Test("File name generation for displays", .tags(.fast))
func fileNameGenerationDisplays() throws { func fileNameGenerationDisplays() throws {
// We can't directly test private methods, but we can test the logic // We can't directly test private methods, but we can test the logic
// through public interfaces and verify the expected patterns // through public interfaces and verify the expected patterns
// Test that different screen indices would generate different names // Test that different screen indices would generate different names
let command1 = try ImageCommand.parse(["--screen-index", "0", "--format", "png"]) let command1 = try ImageCommand.parse(["--screen-index", "0", "--format", "png"])
let command2 = try ImageCommand.parse(["--screen-index", "1", "--format", "png"]) let command2 = try ImageCommand.parse(["--screen-index", "1", "--format", "png"])
#expect(command1.screenIndex == 0) #expect(command1.screenIndex == 0)
#expect(command2.screenIndex == 1) #expect(command2.screenIndex == 1)
#expect(command1.format == .png) #expect(command1.format == .png)
#expect(command2.format == .png) #expect(command2.format == .png)
} }
@Test("File name generation for applications", .tags(.fast)) @Test("File name generation for applications", .tags(.fast))
func fileNameGenerationApplications() throws { func fileNameGenerationApplications() throws {
let command = try ImageCommand.parse([ let command = try ImageCommand.parse([
@ -31,55 +30,55 @@ struct ImageCaptureLogicTests {
"--window-title", "Main Window", "--window-title", "Main Window",
"--format", "jpg" "--format", "jpg"
]) ])
#expect(command.app == "Test App") #expect(command.app == "Test App")
#expect(command.windowTitle == "Main Window") #expect(command.windowTitle == "Main Window")
#expect(command.format == .jpg) #expect(command.format == .jpg)
} }
@Test("Output path generation", .tags(.fast)) @Test("Output path generation", .tags(.fast))
func outputPathGeneration() throws { func outputPathGeneration() throws {
// Test default path behavior // Test default path behavior
let defaultCommand = try ImageCommand.parse([]) let defaultCommand = try ImageCommand.parse([])
#expect(defaultCommand.path == nil) #expect(defaultCommand.path == nil)
// Test custom path // Test custom path
let customCommand = try ImageCommand.parse(["--path", "/tmp/screenshots"]) let customCommand = try ImageCommand.parse(["--path", "/tmp/screenshots"])
#expect(customCommand.path == "/tmp/screenshots") #expect(customCommand.path == "/tmp/screenshots")
// Test path with filename // Test path with filename
let fileCommand = try ImageCommand.parse(["--path", "/tmp/test.png"]) let fileCommand = try ImageCommand.parse(["--path", "/tmp/test.png"])
#expect(fileCommand.path == "/tmp/test.png") #expect(fileCommand.path == "/tmp/test.png")
} }
// MARK: - Mode Determination Tests // MARK: - Mode Determination Tests
@Test("Mode determination comprehensive", .tags(.fast)) @Test("Mode determination comprehensive", .tags(.fast))
func modeDeterminationComprehensive() throws { func modeDeterminationComprehensive() throws {
// Screen mode (default when no app specified) // Screen mode (default when no app specified)
let screenCmd = try ImageCommand.parse([]) let screenCmd = try ImageCommand.parse([])
#expect(screenCmd.mode == nil) #expect(screenCmd.mode == nil)
#expect(screenCmd.app == nil) #expect(screenCmd.app == nil)
// Window mode (when app specified but no explicit mode) // Window mode (when app specified but no explicit mode)
let windowCmd = try ImageCommand.parse(["--app", "Finder"]) let windowCmd = try ImageCommand.parse(["--app", "Finder"])
#expect(windowCmd.mode == nil) // Will be determined as window during execution #expect(windowCmd.mode == nil) // Will be determined as window during execution
#expect(windowCmd.app == "Finder") #expect(windowCmd.app == "Finder")
// Explicit modes // Explicit modes
let explicitScreen = try ImageCommand.parse(["--mode", "screen"]) let explicitScreen = try ImageCommand.parse(["--mode", "screen"])
#expect(explicitScreen.mode == .screen) #expect(explicitScreen.mode == .screen)
let explicitWindow = try ImageCommand.parse(["--mode", "window", "--app", "Safari"]) let explicitWindow = try ImageCommand.parse(["--mode", "window", "--app", "Safari"])
#expect(explicitWindow.mode == .window) #expect(explicitWindow.mode == .window)
#expect(explicitWindow.app == "Safari") #expect(explicitWindow.app == "Safari")
let explicitMulti = try ImageCommand.parse(["--mode", "multi"]) let explicitMulti = try ImageCommand.parse(["--mode", "multi"])
#expect(explicitMulti.mode == .multi) #expect(explicitMulti.mode == .multi)
} }
// MARK: - Window Targeting Tests // MARK: - Window Targeting Tests
@Test("Window targeting by title", .tags(.fast)) @Test("Window targeting by title", .tags(.fast))
func windowTargetingByTitle() throws { func windowTargetingByTitle() throws {
let command = try ImageCommand.parse([ let command = try ImageCommand.parse([
@ -87,13 +86,13 @@ struct ImageCaptureLogicTests {
"--app", "Safari", "--app", "Safari",
"--window-title", "Main Window" "--window-title", "Main Window"
]) ])
#expect(command.mode == .window) #expect(command.mode == .window)
#expect(command.app == "Safari") #expect(command.app == "Safari")
#expect(command.windowTitle == "Main Window") #expect(command.windowTitle == "Main Window")
#expect(command.windowIndex == nil) #expect(command.windowIndex == nil)
} }
@Test("Window targeting by index", .tags(.fast)) @Test("Window targeting by index", .tags(.fast))
func windowTargetingByIndex() throws { func windowTargetingByIndex() throws {
let command = try ImageCommand.parse([ let command = try ImageCommand.parse([
@ -101,13 +100,13 @@ struct ImageCaptureLogicTests {
"--app", "Terminal", "--app", "Terminal",
"--window-index", "0" "--window-index", "0"
]) ])
#expect(command.mode == .window) #expect(command.mode == .window)
#expect(command.app == "Terminal") #expect(command.app == "Terminal")
#expect(command.windowIndex == 0) #expect(command.windowIndex == 0)
#expect(command.windowTitle == nil) #expect(command.windowTitle == nil)
} }
@Test("Window targeting priority - title vs index", .tags(.fast)) @Test("Window targeting priority - title vs index", .tags(.fast))
func windowTargetingPriority() throws { func windowTargetingPriority() throws {
// When both title and index are specified, both are preserved // When both title and index are specified, both are preserved
@ -117,83 +116,85 @@ struct ImageCaptureLogicTests {
"--window-title", "Main", "--window-title", "Main",
"--window-index", "1" "--window-index", "1"
]) ])
#expect(command.windowTitle == "Main") #expect(command.windowTitle == "Main")
#expect(command.windowIndex == 1) #expect(command.windowIndex == 1)
// In actual execution, title matching would take precedence // In actual execution, title matching would take precedence
} }
// MARK: - Screen Targeting Tests // MARK: - Screen Targeting Tests
@Test("Screen targeting by index", .tags(.fast)) @Test("Screen targeting by index", .tags(.fast))
func screenTargetingByIndex() throws { func screenTargetingByIndex() throws {
let command = try ImageCommand.parse([ let command = try ImageCommand.parse([
"--mode", "screen", "--mode", "screen",
"--screen-index", "1" "--screen-index", "1"
]) ])
#expect(command.mode == .screen) #expect(command.mode == .screen)
#expect(command.screenIndex == 1) #expect(command.screenIndex == 1)
} }
@Test("Screen index edge cases", @Test(
arguments: [-1, 0, 1, 5, 99, Int.max]) "Screen index edge cases",
arguments: [-1, 0, 1, 5, 99, Int.max]
)
func screenIndexEdgeCases(index: Int) throws { func screenIndexEdgeCases(index: Int) throws {
let command = try ImageCommand.parse([ let command = try ImageCommand.parse([
"--mode", "screen", "--mode", "screen",
"--screen-index", String(index) "--screen-index", String(index)
]) ])
#expect(command.screenIndex == index) #expect(command.screenIndex == index)
// Validation happens during execution, not parsing // Validation happens during execution, not parsing
} }
// MARK: - Capture Focus Tests // MARK: - Capture Focus Tests
@Test("Capture focus modes", .tags(.fast)) @Test("Capture focus modes", .tags(.fast))
func captureFocusModes() throws { func captureFocusModes() throws {
// Default background mode // Default background mode
let defaultCmd = try ImageCommand.parse([]) let defaultCmd = try ImageCommand.parse([])
#expect(defaultCmd.captureFocus == .background) #expect(defaultCmd.captureFocus == .background)
// Explicit background mode // Explicit background mode
let backgroundCmd = try ImageCommand.parse(["--capture-focus", "background"]) let backgroundCmd = try ImageCommand.parse(["--capture-focus", "background"])
#expect(backgroundCmd.captureFocus == .background) #expect(backgroundCmd.captureFocus == .background)
// Foreground mode // Foreground mode
let foregroundCmd = try ImageCommand.parse(["--capture-focus", "foreground"]) let foregroundCmd = try ImageCommand.parse(["--capture-focus", "foreground"])
#expect(foregroundCmd.captureFocus == .foreground) #expect(foregroundCmd.captureFocus == .foreground)
} }
// MARK: - Image Format Tests // MARK: - Image Format Tests
@Test("Image format handling", .tags(.fast)) @Test("Image format handling", .tags(.fast))
func imageFormatHandling() throws { func imageFormatHandling() throws {
// Default PNG format // Default PNG format
let defaultCmd = try ImageCommand.parse([]) let defaultCmd = try ImageCommand.parse([])
#expect(defaultCmd.format == .png) #expect(defaultCmd.format == .png)
// Explicit PNG format // Explicit PNG format
let pngCmd = try ImageCommand.parse(["--format", "png"]) let pngCmd = try ImageCommand.parse(["--format", "png"])
#expect(pngCmd.format == .png) #expect(pngCmd.format == .png)
// JPEG format // JPEG format
let jpgCmd = try ImageCommand.parse(["--format", "jpg"]) let jpgCmd = try ImageCommand.parse(["--format", "jpg"])
#expect(jpgCmd.format == .jpg) #expect(jpgCmd.format == .jpg)
} }
@Test("MIME type mapping", .tags(.fast)) @Test("MIME type mapping", .tags(.fast))
func mimeTypeMapping() { func mimeTypeMapping() {
// Test MIME type logic (as used in SavedFile creation) // Test MIME type logic (as used in SavedFile creation)
let pngMime = ImageFormat.png == .png ? "image/png" : "image/jpeg" let pngMime = ImageFormat.png == .png ? "image/png" : "image/jpeg"
let jpgMime = ImageFormat.jpg == .jpg ? "image/jpeg" : "image/png" let jpgMime = ImageFormat.jpg == .jpg ? "image/jpeg" : "image/png"
#expect(pngMime == "image/png") #expect(pngMime == "image/png")
#expect(jpgMime == "image/jpeg") #expect(jpgMime == "image/jpeg")
} }
// MARK: - Error Handling Tests // MARK: - Error Handling Tests
@Test("Error code mapping", .tags(.fast)) @Test("Error code mapping", .tags(.fast))
func errorCodeMapping() { func errorCodeMapping() {
// Test error code mapping logic used in handleError // Test error code mapping logic used in handleError
@ -206,18 +207,18 @@ struct ImageCaptureLogicTests {
(.invalidArgument("test"), .INVALID_ARGUMENT), (.invalidArgument("test"), .INVALID_ARGUMENT),
(.unknownError("test"), .UNKNOWN_ERROR) (.unknownError("test"), .UNKNOWN_ERROR)
] ]
// Verify error mapping logic exists // Verify error mapping logic exists
for (_, expectedCode) in testCases { for (_, expectedCode) in testCases {
// We can't directly test the private method, but verify the errors exist // We can't directly test the private method, but verify the errors exist
// Verify the error exists (non-nil check not needed for value types) // Verify the error exists (non-nil check not needed for value types)
#expect(Bool(true)) #expect(Bool(true))
#expect(expectedCode.rawValue.count > 0) #expect(!expectedCode.rawValue.isEmpty)
} }
} }
// MARK: - SavedFile Creation Tests // MARK: - SavedFile Creation Tests
@Test("SavedFile creation for screen capture", .tags(.fast)) @Test("SavedFile creation for screen capture", .tags(.fast))
func savedFileCreationScreenCapture() { func savedFileCreationScreenCapture() {
let savedFile = SavedFile( let savedFile = SavedFile(
@ -228,7 +229,7 @@ struct ImageCaptureLogicTests {
window_index: nil, window_index: nil,
mime_type: "image/png" mime_type: "image/png"
) )
#expect(savedFile.path == "/tmp/screen-0.png") #expect(savedFile.path == "/tmp/screen-0.png")
#expect(savedFile.item_label == "Display 1 (Index 0)") #expect(savedFile.item_label == "Display 1 (Index 0)")
#expect(savedFile.window_title == nil) #expect(savedFile.window_title == nil)
@ -236,7 +237,7 @@ struct ImageCaptureLogicTests {
#expect(savedFile.window_index == nil) #expect(savedFile.window_index == nil)
#expect(savedFile.mime_type == "image/png") #expect(savedFile.mime_type == "image/png")
} }
@Test("SavedFile creation for window capture", .tags(.fast)) @Test("SavedFile creation for window capture", .tags(.fast))
func savedFileCreationWindowCapture() { func savedFileCreationWindowCapture() {
let savedFile = SavedFile( let savedFile = SavedFile(
@ -247,7 +248,7 @@ struct ImageCaptureLogicTests {
window_index: 0, window_index: 0,
mime_type: "image/jpeg" mime_type: "image/jpeg"
) )
#expect(savedFile.path == "/tmp/safari-main.jpg") #expect(savedFile.path == "/tmp/safari-main.jpg")
#expect(savedFile.item_label == "Safari") #expect(savedFile.item_label == "Safari")
#expect(savedFile.window_title == "Main Window") #expect(savedFile.window_title == "Main Window")
@ -255,9 +256,9 @@ struct ImageCaptureLogicTests {
#expect(savedFile.window_index == 0) #expect(savedFile.window_index == 0)
#expect(savedFile.mime_type == "image/jpeg") #expect(savedFile.mime_type == "image/jpeg")
} }
// MARK: - Complex Configuration Tests // MARK: - Complex Configuration Tests
@Test("Complex multi-window capture configuration", .tags(.fast)) @Test("Complex multi-window capture configuration", .tags(.fast))
func complexMultiWindowConfiguration() throws { func complexMultiWindowConfiguration() throws {
let command = try ImageCommand.parse([ let command = try ImageCommand.parse([
@ -268,7 +269,7 @@ struct ImageCaptureLogicTests {
"--capture-focus", "foreground", "--capture-focus", "foreground",
"--json-output" "--json-output"
]) ])
#expect(command.mode == .multi) #expect(command.mode == .multi)
#expect(command.app == "Visual Studio Code") #expect(command.app == "Visual Studio Code")
#expect(command.format == .png) #expect(command.format == .png)
@ -276,7 +277,7 @@ struct ImageCaptureLogicTests {
#expect(command.captureFocus == .foreground) #expect(command.captureFocus == .foreground)
#expect(command.jsonOutput == true) #expect(command.jsonOutput == true)
} }
@Test("Complex screen capture configuration", .tags(.fast)) @Test("Complex screen capture configuration", .tags(.fast))
func complexScreenCaptureConfiguration() throws { func complexScreenCaptureConfiguration() throws {
let command = try ImageCommand.parse([ let command = try ImageCommand.parse([
@ -286,16 +287,16 @@ struct ImageCaptureLogicTests {
"--path", "/Users/test/screenshots/display-1.jpg", "--path", "/Users/test/screenshots/display-1.jpg",
"--json-output" "--json-output"
]) ])
#expect(command.mode == .screen) #expect(command.mode == .screen)
#expect(command.screenIndex == 1) #expect(command.screenIndex == 1)
#expect(command.format == .jpg) #expect(command.format == .jpg)
#expect(command.path == "/Users/test/screenshots/display-1.jpg") #expect(command.path == "/Users/test/screenshots/display-1.jpg")
#expect(command.jsonOutput == true) #expect(command.jsonOutput == true)
} }
// MARK: - Performance Tests // MARK: - Performance Tests
@Test("Configuration parsing performance", .tags(.performance)) @Test("Configuration parsing performance", .tags(.performance))
func configurationParsingPerformance() { func configurationParsingPerformance() {
let complexArgs = [ let complexArgs = [
@ -309,9 +310,9 @@ struct ImageCaptureLogicTests {
"--capture-focus", "foreground", "--capture-focus", "foreground",
"--json-output" "--json-output"
] ]
let startTime = CFAbsoluteTimeGetCurrent() let startTime = CFAbsoluteTimeGetCurrent()
// Parse many times to test performance // Parse many times to test performance
for _ in 1...100 { for _ in 1...100 {
do { do {
@ -321,40 +322,40 @@ struct ImageCaptureLogicTests {
Issue.record("Parsing should not fail: \(error)") Issue.record("Parsing should not fail: \(error)")
} }
} }
let duration = CFAbsoluteTimeGetCurrent() - startTime let duration = CFAbsoluteTimeGetCurrent() - startTime
#expect(duration < 1.0) // Should parse 1000 configs within 1 second #expect(duration < 1.0) // Should parse 1000 configs within 1 second
} }
// MARK: - Integration Readiness Tests // MARK: - Integration Readiness Tests
@Test("Command readiness for screen capture", .tags(.fast)) @Test("Command readiness for screen capture", .tags(.fast))
func commandReadinessScreenCapture() throws { func commandReadinessScreenCapture() throws {
let command = try ImageCommand.parse(["--mode", "screen"]) let command = try ImageCommand.parse(["--mode", "screen"])
// Verify command is properly configured for screen capture // Verify command is properly configured for screen capture
#expect(command.mode == .screen) #expect(command.mode == .screen)
#expect(command.app == nil) // No app needed for screen capture #expect(command.app == nil) // No app needed for screen capture
#expect(command.format == .png) // Has default format #expect(command.format == .png) // Has default format
} }
@Test("Command readiness for window capture", .tags(.fast)) @Test("Command readiness for window capture", .tags(.fast))
func commandReadinessWindowCapture() throws { func commandReadinessWindowCapture() throws {
let command = try ImageCommand.parse([ let command = try ImageCommand.parse([
"--mode", "window", "--mode", "window",
"--app", "Finder" "--app", "Finder"
]) ])
// Verify command is properly configured for window capture // Verify command is properly configured for window capture
#expect(command.mode == .window) #expect(command.mode == .window)
#expect(command.app == "Finder") // App is required #expect(command.app == "Finder") // App is required
#expect(command.format == .png) // Has default format #expect(command.format == .png) // Has default format
} }
@Test("Command validation for invalid configurations", .tags(.fast)) @Test("Command validation for invalid configurations", .tags(.fast))
func commandValidationInvalidConfigurations() { func commandValidationInvalidConfigurations() {
// These should parse successfully but would fail during execution // These should parse successfully but would fail during execution
// Window mode without app (would fail during execution) // Window mode without app (would fail during execution)
do { do {
let command = try ImageCommand.parse(["--mode", "window"]) let command = try ImageCommand.parse(["--mode", "window"])
@ -363,7 +364,7 @@ struct ImageCaptureLogicTests {
} catch { } catch {
Issue.record("Should parse successfully") Issue.record("Should parse successfully")
} }
// Invalid screen index (would fail during execution) // Invalid screen index (would fail during execution)
do { do {
let command = try ImageCommand.parse(["--screen-index", "-1"]) let command = try ImageCommand.parse(["--screen-index", "-1"])
@ -378,7 +379,6 @@ struct ImageCaptureLogicTests {
@Suite("Advanced Image Capture Logic", .tags(.imageCapture, .integration)) @Suite("Advanced Image Capture Logic", .tags(.imageCapture, .integration))
struct AdvancedImageCaptureLogicTests { struct AdvancedImageCaptureLogicTests {
@Test("Multi-mode capture scenarios", .tags(.fast)) @Test("Multi-mode capture scenarios", .tags(.fast))
func multiModeCaptureScenarios() throws { func multiModeCaptureScenarios() throws {
// Multi mode with app (should capture all windows) // Multi mode with app (should capture all windows)
@ -388,13 +388,13 @@ struct AdvancedImageCaptureLogicTests {
]) ])
#expect(multiWithApp.mode == .multi) #expect(multiWithApp.mode == .multi)
#expect(multiWithApp.app == "Safari") #expect(multiWithApp.app == "Safari")
// Multi mode without app (should capture all screens) // Multi mode without app (should capture all screens)
let multiWithoutApp = try ImageCommand.parse(["--mode", "multi"]) let multiWithoutApp = try ImageCommand.parse(["--mode", "multi"])
#expect(multiWithoutApp.mode == .multi) #expect(multiWithoutApp.mode == .multi)
#expect(multiWithoutApp.app == nil) #expect(multiWithoutApp.app == nil)
} }
@Test("Focus mode implications", .tags(.fast)) @Test("Focus mode implications", .tags(.fast))
func focusModeImplications() throws { func focusModeImplications() throws {
// Foreground focus should work with any capture mode // Foreground focus should work with any capture mode
@ -403,14 +403,14 @@ struct AdvancedImageCaptureLogicTests {
"--capture-focus", "foreground" "--capture-focus", "foreground"
]) ])
#expect(foregroundScreen.captureFocus == .foreground) #expect(foregroundScreen.captureFocus == .foreground)
let foregroundWindow = try ImageCommand.parse([ let foregroundWindow = try ImageCommand.parse([
"--mode", "window", "--mode", "window",
"--app", "Terminal", "--app", "Terminal",
"--capture-focus", "foreground" "--capture-focus", "foreground"
]) ])
#expect(foregroundWindow.captureFocus == .foreground) #expect(foregroundWindow.captureFocus == .foreground)
// Background focus (default) should work without additional permissions // Background focus (default) should work without additional permissions
let backgroundCapture = try ImageCommand.parse([ let backgroundCapture = try ImageCommand.parse([
"--mode", "window", "--mode", "window",
@ -418,30 +418,30 @@ struct AdvancedImageCaptureLogicTests {
]) ])
#expect(backgroundCapture.captureFocus == .background) #expect(backgroundCapture.captureFocus == .background)
} }
@Test("Path handling edge cases", .tags(.fast)) @Test("Path handling edge cases", .tags(.fast))
func pathHandlingEdgeCases() throws { func pathHandlingEdgeCases() throws {
// Relative paths // Relative paths
let relativePath = try ImageCommand.parse(["--path", "./screenshots/test.png"]) let relativePath = try ImageCommand.parse(["--path", "./screenshots/test.png"])
#expect(relativePath.path == "./screenshots/test.png") #expect(relativePath.path == "./screenshots/test.png")
// Home directory expansion // Home directory expansion
let homePath = try ImageCommand.parse(["--path", "~/Desktop/capture.jpg"]) let homePath = try ImageCommand.parse(["--path", "~/Desktop/capture.jpg"])
#expect(homePath.path == "~/Desktop/capture.jpg") #expect(homePath.path == "~/Desktop/capture.jpg")
// Absolute paths // Absolute paths
let absolutePath = try ImageCommand.parse(["--path", "/tmp/absolute/path.png"]) let absolutePath = try ImageCommand.parse(["--path", "/tmp/absolute/path.png"])
#expect(absolutePath.path == "/tmp/absolute/path.png") #expect(absolutePath.path == "/tmp/absolute/path.png")
// Paths with spaces // Paths with spaces
let spacePath = try ImageCommand.parse(["--path", "/path with spaces/image.png"]) let spacePath = try ImageCommand.parse(["--path", "/path with spaces/image.png"])
#expect(spacePath.path == "/path with spaces/image.png") #expect(spacePath.path == "/path with spaces/image.png")
// Unicode paths // Unicode paths
let unicodePath = try ImageCommand.parse(["--path", "/tmp/测试/スクリーン.png"]) let unicodePath = try ImageCommand.parse(["--path", "/tmp/测试/スクリーン.png"])
#expect(unicodePath.path == "/tmp/测试/スクリーン.png") #expect(unicodePath.path == "/tmp/测试/スクリーン.png")
} }
@Test("Command execution readiness matrix", .tags(.fast)) @Test("Command execution readiness matrix", .tags(.fast))
func commandExecutionReadinessMatrix() { func commandExecutionReadinessMatrix() {
// Define test scenarios // Define test scenarios
@ -456,7 +456,7 @@ struct AdvancedImageCaptureLogicTests {
(["--app", "Finder"], true, "Implicit window mode"), (["--app", "Finder"], true, "Implicit window mode"),
([], true, "Default screen capture") ([], true, "Default screen capture")
] ]
for scenario in scenarios { for scenario in scenarios {
do { do {
let command = try ImageCommand.parse(scenario.args) let command = try ImageCommand.parse(scenario.args)
@ -472,7 +472,7 @@ struct AdvancedImageCaptureLogicTests {
} }
} }
} }
@Test("Error propagation scenarios", .tags(.fast)) @Test("Error propagation scenarios", .tags(.fast))
func errorPropagationScenarios() { func errorPropagationScenarios() {
// Test that invalid arguments are properly handled // Test that invalid arguments are properly handled
@ -483,14 +483,14 @@ struct AdvancedImageCaptureLogicTests {
["--screen-index", "abc"], ["--screen-index", "abc"],
["--window-index", "xyz"] ["--window-index", "xyz"]
] ]
for args in invalidArgs { for args in invalidArgs {
#expect(throws: (any Error).self) { #expect(throws: (any Error).self) {
_ = try ImageCommand.parse(args) _ = try ImageCommand.parse(args)
} }
} }
} }
@Test("Memory efficiency with complex configurations", .tags(.memory)) @Test("Memory efficiency with complex configurations", .tags(.memory))
func memoryEfficiencyComplexConfigurations() { func memoryEfficiencyComplexConfigurations() {
// Test that complex configurations don't cause excessive memory usage // Test that complex configurations don't cause excessive memory usage
@ -498,12 +498,12 @@ struct AdvancedImageCaptureLogicTests {
["--mode", "multi", "--app", String(repeating: "LongAppName", count: 100)], ["--mode", "multi", "--app", String(repeating: "LongAppName", count: 100)],
["--window-title", String(repeating: "VeryLongTitle", count: 200)], ["--window-title", String(repeating: "VeryLongTitle", count: 200)],
["--path", String(repeating: "/very/long/path", count: 50)], ["--path", String(repeating: "/very/long/path", count: 50)],
Array(repeating: ["--mode", "screen"], count: 100).flatMap { $0 } Array(repeating: ["--mode", "screen"], count: 100).flatMap(\.self)
] ]
for config in complexConfigs { for config in complexConfigs {
do { do {
let _ = try ImageCommand.parse(config) _ = try ImageCommand.parse(config)
#expect(Bool(true)) // Command parsed successfully #expect(Bool(true)) // Command parsed successfully
} catch { } catch {
// Some may fail due to argument parsing limits, which is expected // Some may fail due to argument parsing limits, which is expected
@ -511,4 +511,4 @@ struct AdvancedImageCaptureLogicTests {
} }
} }
} }
} }

View file

@ -1,28 +1,27 @@
import ArgumentParser import ArgumentParser
import Foundation
@testable import peekaboo @testable import peekaboo
import Testing import Testing
import Foundation
@Suite("ImageCommand Tests", .tags(.imageCapture, .unit)) @Suite("ImageCommand Tests", .tags(.imageCapture, .unit))
struct ImageCommandTests { struct ImageCommandTests {
// MARK: - Test Data & Helpers // MARK: - Test Data & Helpers
private static let validFormats: [ImageFormat] = [.png, .jpg] private static let validFormats: [ImageFormat] = [.png, .jpg]
private static let validCaptureModes: [CaptureMode] = [.screen, .window, .multi] private static let validCaptureModes: [CaptureMode] = [.screen, .window, .multi]
private static let validCaptureFocus: [CaptureFocus] = [.background, .foreground] private static let validCaptureFocus: [CaptureFocus] = [.background, .foreground]
private static func createTestCommand(_ args: [String] = []) throws -> ImageCommand { private static func createTestCommand(_ args: [String] = []) throws -> ImageCommand {
return try ImageCommand.parse(args) try ImageCommand.parse(args)
} }
// MARK: - Command Parsing Tests // MARK: - Command Parsing Tests
@Test("Basic command parsing with defaults", .tags(.fast)) @Test("Basic command parsing with defaults", .tags(.fast))
func imageCommandParsing() throws { func imageCommandParsing() throws {
// Test basic command parsing // Test basic command parsing
let command = try ImageCommand.parse([]) let command = try ImageCommand.parse([])
// Verify defaults // Verify defaults
#expect(command.mode == nil) #expect(command.mode == nil)
#expect(command.format == .png) #expect(command.format == .png)
@ -31,36 +30,36 @@ struct ImageCommandTests {
#expect(command.captureFocus == .background) #expect(command.captureFocus == .background)
#expect(command.jsonOutput == false) #expect(command.jsonOutput == false)
} }
@Test("Command with screen mode", .tags(.fast)) @Test("Command with screen mode", .tags(.fast))
func imageCommandWithScreenMode() throws { func imageCommandWithScreenMode() throws {
// Test screen capture mode // Test screen capture mode
let command = try ImageCommand.parse(["--mode", "screen"]) let command = try ImageCommand.parse(["--mode", "screen"])
#expect(command.mode == .screen) #expect(command.mode == .screen)
} }
@Test("Command with app specifier", .tags(.fast)) @Test("Command with app specifier", .tags(.fast))
func imageCommandWithAppSpecifier() throws { func imageCommandWithAppSpecifier() throws {
// Test app-specific capture // Test app-specific capture
let command = try ImageCommand.parse([ let command = try ImageCommand.parse([
"--app", "Finder" "--app", "Finder"
]) ])
#expect(command.mode == nil) // mode is optional #expect(command.mode == nil) // mode is optional
#expect(command.app == "Finder") #expect(command.app == "Finder")
} }
@Test("Command with window title", .tags(.fast)) @Test("Command with window title", .tags(.fast))
func imageCommandWithWindowTitle() throws { func imageCommandWithWindowTitle() throws {
// Test window title capture // Test window title capture
let command = try ImageCommand.parse([ let command = try ImageCommand.parse([
"--window-title", "Documents" "--window-title", "Documents"
]) ])
#expect(command.windowTitle == "Documents") #expect(command.windowTitle == "Documents")
} }
@Test("Command with output path", .tags(.fast)) @Test("Command with output path", .tags(.fast))
func imageCommandWithOutput() throws { func imageCommandWithOutput() throws {
// Test output path specification // Test output path specification
@ -68,89 +67,93 @@ struct ImageCommandTests {
let command = try ImageCommand.parse([ let command = try ImageCommand.parse([
"--path", outputPath "--path", outputPath
]) ])
#expect(command.path == outputPath) #expect(command.path == outputPath)
} }
@Test("Command with format option", .tags(.fast)) @Test("Command with format option", .tags(.fast))
func imageCommandWithFormat() throws { func imageCommandWithFormat() throws {
// Test format specification // Test format specification
let command = try ImageCommand.parse([ let command = try ImageCommand.parse([
"--format", "jpg" "--format", "jpg"
]) ])
#expect(command.format == .jpg) #expect(command.format == .jpg)
} }
@Test("Command with focus option", .tags(.fast)) @Test("Command with focus option", .tags(.fast))
func imageCommandWithFocus() throws { func imageCommandWithFocus() throws {
// Test focus option // Test focus option
let command = try ImageCommand.parse([ let command = try ImageCommand.parse([
"--capture-focus", "foreground" "--capture-focus", "foreground"
]) ])
#expect(command.captureFocus == .foreground) #expect(command.captureFocus == .foreground)
} }
@Test("Command with JSON output", .tags(.fast)) @Test("Command with JSON output", .tags(.fast))
func imageCommandWithJSONOutput() throws { func imageCommandWithJSONOutput() throws {
// Test JSON output flag // Test JSON output flag
let command = try ImageCommand.parse([ let command = try ImageCommand.parse([
"--json-output" "--json-output"
]) ])
#expect(command.jsonOutput == true) #expect(command.jsonOutput == true)
} }
@Test("Command with multi mode", .tags(.fast)) @Test("Command with multi mode", .tags(.fast))
func imageCommandWithMultiMode() throws { func imageCommandWithMultiMode() throws {
// Test multi capture mode // Test multi capture mode
let command = try ImageCommand.parse([ let command = try ImageCommand.parse([
"--mode", "multi" "--mode", "multi"
]) ])
#expect(command.mode == .multi) #expect(command.mode == .multi)
} }
@Test("Command with screen index", .tags(.fast)) @Test("Command with screen index", .tags(.fast))
func imageCommandWithScreenIndex() throws { func imageCommandWithScreenIndex() throws {
// Test screen index specification // Test screen index specification
let command = try ImageCommand.parse([ let command = try ImageCommand.parse([
"--screen-index", "1" "--screen-index", "1"
]) ])
#expect(command.screenIndex == 1) #expect(command.screenIndex == 1)
} }
// MARK: - Parameterized Command Tests // MARK: - Parameterized Command Tests
@Test("Various command combinations", @Test(
arguments: [ "Various command combinations",
(args: ["--mode", "screen", "--format", "png"], mode: CaptureMode.screen, format: ImageFormat.png), arguments: [
(args: ["--mode", "window", "--format", "jpg"], mode: CaptureMode.window, format: ImageFormat.jpg), (args: ["--mode", "screen", "--format", "png"], mode: CaptureMode.screen, format: ImageFormat.png),
(args: ["--mode", "multi", "--json-output"], mode: CaptureMode.multi, format: ImageFormat.png) (args: ["--mode", "window", "--format", "jpg"], mode: CaptureMode.window, format: ImageFormat.jpg),
]) (args: ["--mode", "multi", "--json-output"], mode: CaptureMode.multi, format: ImageFormat.png)
]
)
func commandCombinations(args: [String], mode: CaptureMode, format: ImageFormat) throws { func commandCombinations(args: [String], mode: CaptureMode, format: ImageFormat) throws {
let command = try ImageCommand.parse(args) let command = try ImageCommand.parse(args)
#expect(command.mode == mode) #expect(command.mode == mode)
#expect(command.format == format) #expect(command.format == format)
} }
@Test("Invalid arguments throw errors", @Test(
arguments: [ "Invalid arguments throw errors",
["--mode", "invalid"], arguments: [
["--format", "bmp"], ["--mode", "invalid"],
["--capture-focus", "neither"], ["--format", "bmp"],
["--screen-index", "abc"] ["--capture-focus", "neither"],
]) ["--screen-index", "abc"]
]
)
func invalidArguments(args: [String]) { func invalidArguments(args: [String]) {
#expect(throws: (any Error).self) { #expect(throws: (any Error).self) {
_ = try ImageCommand.parse(args) _ = try ImageCommand.parse(args)
} }
} }
// MARK: - Model Tests // MARK: - Model Tests
@Test("SavedFile model creation", .tags(.fast)) @Test("SavedFile model creation", .tags(.fast))
func savedFileModel() { func savedFileModel() {
let savedFile = SavedFile( let savedFile = SavedFile(
@ -161,12 +164,12 @@ struct ImageCommandTests {
window_index: nil, window_index: nil,
mime_type: "image/png" mime_type: "image/png"
) )
#expect(savedFile.path == "/tmp/screenshot.png") #expect(savedFile.path == "/tmp/screenshot.png")
#expect(savedFile.item_label == "Screen 1") #expect(savedFile.item_label == "Screen 1")
#expect(savedFile.mime_type == "image/png") #expect(savedFile.mime_type == "image/png")
} }
@Test("ImageCaptureData encoding", .tags(.fast)) @Test("ImageCaptureData encoding", .tags(.fast))
func imageCaptureDataEncoding() throws { func imageCaptureDataEncoding() throws {
let savedFile = SavedFile( let savedFile = SavedFile(
@ -177,69 +180,69 @@ struct ImageCommandTests {
window_index: nil, window_index: nil,
mime_type: "image/png" mime_type: "image/png"
) )
let captureData = ImageCaptureData(saved_files: [savedFile]) let captureData = ImageCaptureData(saved_files: [savedFile])
// Test JSON encoding // Test JSON encoding
let encoder = JSONEncoder() let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase encoder.keyEncodingStrategy = .convertToSnakeCase
let data = try encoder.encode(captureData) let data = try encoder.encode(captureData)
#expect(data.count > 0) #expect(!data.isEmpty)
// Test decoding // Test decoding
let decoder = JSONDecoder() let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase decoder.keyDecodingStrategy = .convertFromSnakeCase
let decoded = try decoder.decode(ImageCaptureData.self, from: data) let decoded = try decoder.decode(ImageCaptureData.self, from: data)
#expect(decoded.saved_files.count == 1) #expect(decoded.saved_files.count == 1)
#expect(decoded.saved_files[0].path == "/tmp/test.png") #expect(decoded.saved_files[0].path == "/tmp/test.png")
} }
// MARK: - Enum Raw Value Tests // MARK: - Enum Raw Value Tests
@Test("CaptureMode raw values", .tags(.fast)) @Test("CaptureMode raw values", .tags(.fast))
func captureModeRawValues() { func captureModeRawValues() {
#expect(CaptureMode.screen.rawValue == "screen") #expect(CaptureMode.screen.rawValue == "screen")
#expect(CaptureMode.window.rawValue == "window") #expect(CaptureMode.window.rawValue == "window")
#expect(CaptureMode.multi.rawValue == "multi") #expect(CaptureMode.multi.rawValue == "multi")
} }
@Test("ImageFormat raw values", .tags(.fast)) @Test("ImageFormat raw values", .tags(.fast))
func imageFormatRawValues() { func imageFormatRawValues() {
#expect(ImageFormat.png.rawValue == "png") #expect(ImageFormat.png.rawValue == "png")
#expect(ImageFormat.jpg.rawValue == "jpg") #expect(ImageFormat.jpg.rawValue == "jpg")
} }
@Test("CaptureFocus raw values", .tags(.fast)) @Test("CaptureFocus raw values", .tags(.fast))
func captureFocusRawValues() { func captureFocusRawValues() {
#expect(CaptureFocus.background.rawValue == "background") #expect(CaptureFocus.background.rawValue == "background")
#expect(CaptureFocus.foreground.rawValue == "foreground") #expect(CaptureFocus.foreground.rawValue == "foreground")
} }
// MARK: - Mode Determination & Logic Tests // MARK: - Mode Determination & Logic Tests
@Test("Mode determination logic", .tags(.fast)) @Test("Mode determination logic", .tags(.fast))
func modeDeterminationLogic() throws { func modeDeterminationLogic() throws {
// No mode, no app -> should default to screen // No mode, no app -> should default to screen
let screenCommand = try ImageCommand.parse([]) let screenCommand = try ImageCommand.parse([])
#expect(screenCommand.mode == nil) #expect(screenCommand.mode == nil)
#expect(screenCommand.app == nil) #expect(screenCommand.app == nil)
// No mode, with app -> should infer window mode in actual execution // No mode, with app -> should infer window mode in actual execution
let windowCommand = try ImageCommand.parse(["--app", "Finder"]) let windowCommand = try ImageCommand.parse(["--app", "Finder"])
#expect(windowCommand.mode == nil) #expect(windowCommand.mode == nil)
#expect(windowCommand.app == "Finder") #expect(windowCommand.app == "Finder")
// Explicit mode should be preserved // Explicit mode should be preserved
let explicitCommand = try ImageCommand.parse(["--mode", "multi"]) let explicitCommand = try ImageCommand.parse(["--mode", "multi"])
#expect(explicitCommand.mode == .multi) #expect(explicitCommand.mode == .multi)
} }
@Test("Default values verification", .tags(.fast)) @Test("Default values verification", .tags(.fast))
func defaultValues() throws { func defaultValues() throws {
let command = try ImageCommand.parse([]) let command = try ImageCommand.parse([])
#expect(command.mode == nil) #expect(command.mode == nil)
#expect(command.format == .png) #expect(command.format == .png)
#expect(command.path == nil) #expect(command.path == nil)
@ -250,21 +253,25 @@ struct ImageCommandTests {
#expect(command.captureFocus == .background) #expect(command.captureFocus == .background)
#expect(command.jsonOutput == false) #expect(command.jsonOutput == false)
} }
@Test("Screen index boundary values", @Test(
arguments: [-1, 0, 1, 99, Int.max]) "Screen index boundary values",
arguments: [-1, 0, 1, 99, Int.max]
)
func screenIndexBoundaries(index: Int) throws { func screenIndexBoundaries(index: Int) throws {
let command = try ImageCommand.parse(["--screen-index", String(index)]) let command = try ImageCommand.parse(["--screen-index", String(index)])
#expect(command.screenIndex == index) #expect(command.screenIndex == index)
} }
@Test("Window index boundary values", @Test(
arguments: [-1, 0, 1, 10, Int.max]) "Window index boundary values",
arguments: [-1, 0, 1, 10, Int.max]
)
func windowIndexBoundaries(index: Int) throws { func windowIndexBoundaries(index: Int) throws {
let command = try ImageCommand.parse(["--window-index", String(index)]) let command = try ImageCommand.parse(["--window-index", String(index)])
#expect(command.windowIndex == index) #expect(command.windowIndex == index)
} }
@Test("Error handling for invalid combinations", .tags(.fast)) @Test("Error handling for invalid combinations", .tags(.fast))
func invalidCombinations() { func invalidCombinations() {
// Window capture without app should fail in execution // Window capture without app should fail in execution
@ -283,9 +290,8 @@ struct ImageCommandTests {
@Suite("ImageCommand Advanced Tests", .tags(.imageCapture, .integration)) @Suite("ImageCommand Advanced Tests", .tags(.imageCapture, .integration))
struct ImageCommandAdvancedTests { struct ImageCommandAdvancedTests {
// MARK: - Complex Scenario Tests // MARK: - Complex Scenario Tests
@Test("Complex command with multiple options", .tags(.fast)) @Test("Complex command with multiple options", .tags(.fast))
func complexCommand() throws { func complexCommand() throws {
let command = try ImageCommand.parse([ let command = try ImageCommand.parse([
@ -298,7 +304,7 @@ struct ImageCommandAdvancedTests {
"--capture-focus", "foreground", "--capture-focus", "foreground",
"--json-output" "--json-output"
]) ])
#expect(command.mode == .window) #expect(command.mode == .window)
#expect(command.app == "Safari") #expect(command.app == "Safari")
#expect(command.windowTitle == "Home") #expect(command.windowTitle == "Home")
@ -308,11 +314,11 @@ struct ImageCommandAdvancedTests {
#expect(command.captureFocus == .foreground) #expect(command.captureFocus == .foreground)
#expect(command.jsonOutput == true) #expect(command.jsonOutput == true)
} }
@Test("Command help text contains all options", .tags(.fast)) @Test("Command help text contains all options", .tags(.fast))
func commandHelpText() { func commandHelpText() {
let helpText = ImageCommand.helpMessage() let helpText = ImageCommand.helpMessage()
// Verify key options are documented // Verify key options are documented
#expect(helpText.contains("--mode")) #expect(helpText.contains("--mode"))
#expect(helpText.contains("--app")) #expect(helpText.contains("--app"))
@ -322,67 +328,71 @@ struct ImageCommandAdvancedTests {
#expect(helpText.contains("--capture-focus")) #expect(helpText.contains("--capture-focus"))
#expect(helpText.contains("--json-output")) #expect(helpText.contains("--json-output"))
} }
@Test("Command configuration", .tags(.fast)) @Test("Command configuration", .tags(.fast))
func commandConfiguration() { func commandConfiguration() {
let config = ImageCommand.configuration let config = ImageCommand.configuration
#expect(config.commandName == "image") #expect(config.commandName == "image")
#expect(config.abstract.contains("Capture")) #expect(config.abstract.contains("Capture"))
} }
@Test("Window specifier combinations", @Test(
arguments: [ "Window specifier combinations",
(app: "Safari", title: "Home", index: nil), arguments: [
(app: "Finder", title: nil, index: 0), (app: "Safari", title: "Home", index: nil),
(app: "Terminal", title: nil, index: nil) (app: "Finder", title: nil, index: 0),
]) (app: "Terminal", title: nil, index: nil)
]
)
func windowSpecifierCombinations(app: String, title: String?, index: Int?) throws { func windowSpecifierCombinations(app: String, title: String?, index: Int?) throws {
var args = ["--app", app] var args = ["--app", app]
if let title = title { if let title {
args.append(contentsOf: ["--window-title", title]) args.append(contentsOf: ["--window-title", title])
} }
if let index = index { if let index {
args.append(contentsOf: ["--window-index", String(index)]) args.append(contentsOf: ["--window-index", String(index)])
} }
let command = try ImageCommand.parse(args) let command = try ImageCommand.parse(args)
#expect(command.app == app) #expect(command.app == app)
#expect(command.windowTitle == title) #expect(command.windowTitle == title)
#expect(command.windowIndex == index) #expect(command.windowIndex == index)
} }
@Test("Path expansion handling", @Test(
arguments: [ "Path expansion handling",
"~/Desktop/screenshot.png", arguments: [
"/tmp/test.png", "~/Desktop/screenshot.png",
"./relative/path.png", "/tmp/test.png",
"/path with spaces/image.png" "./relative/path.png",
]) "/path with spaces/image.png"
]
)
func pathExpansion(path: String) throws { func pathExpansion(path: String) throws {
let command = try ImageCommand.parse(["--path", path]) let command = try ImageCommand.parse(["--path", path])
#expect(command.path == path) #expect(command.path == path)
} }
@Test("FileHandleTextOutputStream functionality", .tags(.fast)) @Test("FileHandleTextOutputStream functionality", .tags(.fast))
func fileHandleTextOutputStream() { func fileHandleTextOutputStream() {
// Test the custom text output stream // Test the custom text output stream
let pipe = Pipe() let pipe = Pipe()
var stream = FileHandleTextOutputStream(pipe.fileHandleForWriting) var stream = FileHandleTextOutputStream(pipe.fileHandleForWriting)
let testString = "Test output" let testString = "Test output"
stream.write(testString) stream.write(testString)
pipe.fileHandleForWriting.closeFile() pipe.fileHandleForWriting.closeFile()
let data = pipe.fileHandleForReading.readDataToEndOfFile() let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8) let output = String(data: data, encoding: .utf8)
#expect(output == testString) #expect(output == testString)
} }
@Test("Command validation edge cases", .tags(.fast)) @Test("Command validation edge cases", .tags(.fast))
func commandValidationEdgeCases() { func commandValidationEdgeCases() {
// Test very long paths // Test very long paths
@ -393,7 +403,7 @@ struct ImageCommandAdvancedTests {
} catch { } catch {
Issue.record("Should handle long paths gracefully") Issue.record("Should handle long paths gracefully")
} }
// Test unicode in paths // Test unicode in paths
let unicodePath = "/tmp/测试/スクリーン.png" let unicodePath = "/tmp/测试/スクリーン.png"
do { do {
@ -403,20 +413,20 @@ struct ImageCommandAdvancedTests {
Issue.record("Should handle unicode paths") Issue.record("Should handle unicode paths")
} }
} }
@Test("MIME type assignment logic", .tags(.fast)) @Test("MIME type assignment logic", .tags(.fast))
func mimeTypeAssignment() { func mimeTypeAssignment() throws {
// Test MIME type logic for different formats // Test MIME type logic for different formats
let pngCommand = try! ImageCommand.parse(["--format", "png"]) let pngCommand = try ImageCommand.parse(["--format", "png"])
#expect(pngCommand.format == .png) #expect(pngCommand.format == .png)
let jpgCommand = try! ImageCommand.parse(["--format", "jpg"]) let jpgCommand = try ImageCommand.parse(["--format", "jpg"])
#expect(jpgCommand.format == .jpg) #expect(jpgCommand.format == .jpg)
// Verify MIME types would be assigned correctly // Verify MIME types would be assigned correctly
// (This logic is in the SavedFile creation during actual capture) // (This logic is in the SavedFile creation during actual capture)
} }
@Test("Argument parsing stress test", .tags(.performance)) @Test("Argument parsing stress test", .tags(.performance))
func argumentParsingStressTest() { func argumentParsingStressTest() {
// Test parsing performance with many arguments // Test parsing performance with many arguments
@ -429,7 +439,7 @@ struct ImageCommandAdvancedTests {
"--capture-focus", "foreground", "--capture-focus", "foreground",
"--json-output" "--json-output"
] ]
do { do {
let command = try ImageCommand.parse(args) let command = try ImageCommand.parse(args)
#expect(command.mode == .multi) #expect(command.mode == .multi)
@ -438,17 +448,19 @@ struct ImageCommandAdvancedTests {
Issue.record("Should handle complex argument parsing") Issue.record("Should handle complex argument parsing")
} }
} }
@Test("Command option combinations validation", @Test(
arguments: [ "Command option combinations validation",
(["--mode", "screen"], true), arguments: [
(["--mode", "window", "--app", "Finder"], true), (["--mode", "screen"], true),
(["--mode", "multi"], true), (["--mode", "window", "--app", "Finder"], true),
(["--app", "Safari"], true), (["--mode", "multi"], true),
(["--window-title", "Test"], true), (["--app", "Safari"], true),
(["--screen-index", "0"], true), (["--window-title", "Test"], true),
(["--window-index", "0"], true) (["--screen-index", "0"], true),
]) (["--window-index", "0"], true)
]
)
func commandOptionCombinations(args: [String], shouldParse: Bool) { func commandOptionCombinations(args: [String], shouldParse: Bool) {
do { do {
let command = try ImageCommand.parse(args) let command = try ImageCommand.parse(args)
@ -458,4 +470,4 @@ struct ImageCommandAdvancedTests {
#expect(shouldParse == false) #expect(shouldParse == false)
} }
} }
} }

View file

@ -1,12 +1,11 @@
import Foundation
@testable import peekaboo @testable import peekaboo
import Testing import Testing
import Foundation
@Suite("JSONOutput Tests", .tags(.jsonOutput, .unit)) @Suite("JSONOutput Tests", .tags(.jsonOutput, .unit))
struct JSONOutputTests { struct JSONOutputTests {
// MARK: - AnyCodable Tests // MARK: - AnyCodable Tests
@Test("AnyCodable encoding with various types", .tags(.fast)) @Test("AnyCodable encoding with various types", .tags(.fast))
func anyCodableEncodingVariousTypes() throws { func anyCodableEncodingVariousTypes() throws {
// Test string // Test string
@ -14,19 +13,19 @@ struct JSONOutputTests {
let stringData = try JSONEncoder().encode(stringValue) let stringData = try JSONEncoder().encode(stringValue)
let stringResult = try JSONSerialization.jsonObject(with: stringData) as? String let stringResult = try JSONSerialization.jsonObject(with: stringData) as? String
#expect(stringResult == "test string") #expect(stringResult == "test string")
// Test number // Test number
let numberValue = AnyCodable(42) let numberValue = AnyCodable(42)
let numberData = try JSONEncoder().encode(numberValue) let numberData = try JSONEncoder().encode(numberValue)
let numberResult = try JSONSerialization.jsonObject(with: numberData) as? Int let numberResult = try JSONSerialization.jsonObject(with: numberData) as? Int
#expect(numberResult == 42) #expect(numberResult == 42)
// Test boolean // Test boolean
let boolValue = AnyCodable(true) let boolValue = AnyCodable(true)
let boolData = try JSONEncoder().encode(boolValue) let boolData = try JSONEncoder().encode(boolValue)
let boolResult = try JSONSerialization.jsonObject(with: boolData) as? Bool let boolResult = try JSONSerialization.jsonObject(with: boolData) as? Bool
#expect(boolResult == true) #expect(boolResult == true)
// Test null (using optional nil) // Test null (using optional nil)
let nilValue: String? = nil let nilValue: String? = nil
let nilAnyCodable = AnyCodable(nilValue as Any) let nilAnyCodable = AnyCodable(nilValue as Any)
@ -34,7 +33,7 @@ struct JSONOutputTests {
let nilString = String(data: nilData, encoding: .utf8) let nilString = String(data: nilData, encoding: .utf8)
#expect(nilString == "null") #expect(nilString == "null")
} }
@Test("AnyCodable with nested structures", .tags(.fast)) @Test("AnyCodable with nested structures", .tags(.fast))
func anyCodableNestedStructures() throws { func anyCodableNestedStructures() throws {
// Test array // Test array
@ -42,7 +41,7 @@ struct JSONOutputTests {
let arrayData = try JSONEncoder().encode(arrayValue) let arrayData = try JSONEncoder().encode(arrayValue)
let arrayResult = try JSONSerialization.jsonObject(with: arrayData) as? [Int] let arrayResult = try JSONSerialization.jsonObject(with: arrayData) as? [Int]
#expect(arrayResult == [1, 2, 3]) #expect(arrayResult == [1, 2, 3])
// Test dictionary // Test dictionary
let dictValue = AnyCodable(["key": "value", "number": 42]) let dictValue = AnyCodable(["key": "value", "number": 42])
let dictData = try JSONEncoder().encode(dictValue) let dictData = try JSONEncoder().encode(dictValue)
@ -50,21 +49,22 @@ struct JSONOutputTests {
#expect(dictResult?["key"] as? String == "value") #expect(dictResult?["key"] as? String == "value")
#expect(dictResult?["number"] as? Int == 42) #expect(dictResult?["number"] as? Int == 42)
} }
@Test("AnyCodable decoding", .tags(.fast)) @Test("AnyCodable decoding", .tags(.fast))
func anyCodableDecoding() throws { func anyCodableDecoding() throws {
// Test decoding from JSON // Test decoding from JSON
let jsonData = #"{"string": "test", "number": 42, "bool": true, "null": null}"#.data(using: .utf8)! let jsonString = #"{"string": "test", "number": 42, "bool": true, "null": null}"#
let jsonData = Data(jsonString.utf8)
let decoded = try JSONDecoder().decode([String: AnyCodable].self, from: jsonData) let decoded = try JSONDecoder().decode([String: AnyCodable].self, from: jsonData)
#expect(decoded["string"]?.value as? String == "test") #expect(decoded["string"]?.value as? String == "test")
#expect(decoded["number"]?.value as? Int == 42) #expect(decoded["number"]?.value as? Int == 42)
#expect(decoded["bool"]?.value as? Bool == true) #expect(decoded["bool"]?.value as? Bool == true)
#expect(decoded["null"]?.value == nil) #expect(decoded["null"]?.value == nil)
} }
// MARK: - AnyEncodable Tests // MARK: - AnyEncodable Tests
@Test("AnyEncodable with custom types", .tags(.fast)) @Test("AnyEncodable with custom types", .tags(.fast))
func anyEncodableCustomTypes() throws { func anyEncodableCustomTypes() throws {
// Test with ApplicationInfo // Test with ApplicationInfo
@ -75,21 +75,21 @@ struct JSONOutputTests {
is_active: true, is_active: true,
window_count: 2 window_count: 2
) )
// Test encoding through AnyCodable instead // Test encoding through AnyCodable instead
let anyCodable = AnyCodable(appInfo) let anyCodable = AnyCodable(appInfo)
let data = try JSONEncoder().encode(anyCodable) let data = try JSONEncoder().encode(anyCodable)
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
#expect(json?["app_name"] as? String == "Test App") #expect(json?["app_name"] as? String == "Test App")
#expect(json?["bundle_id"] as? String == "com.test.app") #expect(json?["bundle_id"] as? String == "com.test.app")
#expect(json?["pid"] as? Int32 == 1234) #expect(json?["pid"] as? Int32 == 1234)
#expect(json?["is_active"] as? Bool == true) #expect(json?["is_active"] as? Bool == true)
#expect(json?["window_count"] as? Int == 2) #expect(json?["window_count"] as? Int == 2)
} }
// MARK: - JSON Output Function Tests // MARK: - JSON Output Function Tests
@Test("outputJSON function with success data", .tags(.fast)) @Test("outputJSON function with success data", .tags(.fast))
func outputJSONSuccess() throws { func outputJSONSuccess() throws {
// Test data // Test data
@ -102,37 +102,37 @@ struct JSONOutputTests {
window_count: 1 window_count: 1
) )
]) ])
// Test JSON serialization directly without capturing stdout // Test JSON serialization directly without capturing stdout
let encoder = JSONEncoder() let encoder = JSONEncoder()
let data = try encoder.encode(testData) let data = try encoder.encode(testData)
let jsonString = String(data: data, encoding: .utf8) ?? "" let jsonString = String(data: data, encoding: .utf8) ?? ""
// Verify JSON structure // Verify JSON structure
#expect(jsonString.contains("Finder")) #expect(jsonString.contains("Finder"))
#expect(jsonString.contains("com.apple.finder")) #expect(jsonString.contains("com.apple.finder"))
#expect(!jsonString.isEmpty) #expect(!jsonString.isEmpty)
} }
@Test("CodableJSONResponse structure", .tags(.fast)) @Test("CodableJSONResponse structure", .tags(.fast))
func codableJSONResponseStructure() throws { func codableJSONResponseStructure() throws {
let testData = ["test": "value"] let testData = ["test": "value"]
let response = CodableJSONResponse( let response = CodableJSONResponse(
success: true, success: true,
data: testData, data: testData,
messages: nil, messages: nil,
debug_logs: [] debug_logs: []
) )
let encoder = JSONEncoder() let encoder = JSONEncoder()
let data = try encoder.encode(response) let data = try encoder.encode(response)
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
#expect(json?["success"] as? Bool == true) #expect(json?["success"] as? Bool == true)
#expect((json?["data"] as? [String: Any])?["test"] as? String == "value") #expect((json?["data"] as? [String: Any])?["test"] as? String == "value")
#expect(json?["error"] == nil) #expect(json?["error"] == nil)
} }
@Test("Error output JSON formatting", .tags(.fast)) @Test("Error output JSON formatting", .tags(.fast))
func errorOutputJSONFormatting() throws { func errorOutputJSONFormatting() throws {
// Test error JSON structure directly // Test error JSON structure directly
@ -141,27 +141,27 @@ struct JSONOutputTests {
code: .APP_NOT_FOUND, code: .APP_NOT_FOUND,
details: "Additional error details" details: "Additional error details"
) )
let response = JSONResponse( let response = JSONResponse(
success: false, success: false,
data: nil, data: nil,
messages: nil, messages: nil,
error: errorInfo error: errorInfo
) )
let encoder = JSONEncoder() let encoder = JSONEncoder()
let data = try encoder.encode(response) let data = try encoder.encode(response)
let jsonString = String(data: data, encoding: .utf8) ?? "" let jsonString = String(data: data, encoding: .utf8) ?? ""
// Verify error JSON structure // Verify error JSON structure
#expect(jsonString.contains("\"success\":false") || jsonString.contains("\"success\": false")) #expect(jsonString.contains("\"success\":false") || jsonString.contains("\"success\": false"))
#expect(jsonString.contains("\"error\"")) #expect(jsonString.contains("\"error\""))
#expect(jsonString.contains("Test error message")) #expect(jsonString.contains("Test error message"))
#expect(jsonString.contains("APP_NOT_FOUND")) #expect(jsonString.contains("APP_NOT_FOUND"))
} }
// MARK: - Edge Cases and Error Handling // MARK: - Edge Cases and Error Handling
@Test("AnyCodable with complex nested data", .tags(.fast)) @Test("AnyCodable with complex nested data", .tags(.fast))
func anyCodableComplexNestedData() throws { func anyCodableComplexNestedData() throws {
let complexData: [String: Any] = [ let complexData: [String: Any] = [
@ -177,15 +177,15 @@ struct JSONOutputTests {
] ]
] ]
] ]
let anyCodable = AnyCodable(complexData) let anyCodable = AnyCodable(complexData)
let encoded = try JSONEncoder().encode(anyCodable) let encoded = try JSONEncoder().encode(anyCodable)
let decoded = try JSONSerialization.jsonObject(with: encoded) as? [String: Any] let decoded = try JSONSerialization.jsonObject(with: encoded) as? [String: Any]
#expect(decoded?["simple"] as? String == "string") #expect(decoded?["simple"] as? String == "string")
#expect((decoded?["nested"] as? [String: Any]) != nil) #expect((decoded?["nested"] as? [String: Any]) != nil)
} }
@Test("JSON encoding performance with large data", .tags(.performance)) @Test("JSON encoding performance with large data", .tags(.performance))
func jsonEncodingPerformance() throws { func jsonEncodingPerformance() throws {
// Create large dataset // Create large dataset
@ -195,58 +195,56 @@ struct JSONOutputTests {
app_name: "App \(index)", app_name: "App \(index)",
bundle_id: "com.test.app\(index)", bundle_id: "com.test.app\(index)",
pid: Int32(1000 + index), pid: Int32(1000 + index),
is_active: index % 2 == 0, is_active: index.isMultiple(of: 2),
window_count: index % 10 window_count: index % 10
) )
largeAppList.append(appInfo) largeAppList.append(appInfo)
} }
let data = ApplicationListData(applications: largeAppList) let data = ApplicationListData(applications: largeAppList)
// Measure encoding performance // Measure encoding performance
let startTime = CFAbsoluteTimeGetCurrent() let startTime = CFAbsoluteTimeGetCurrent()
let encoded = try JSONEncoder().encode(data) let encoded = try JSONEncoder().encode(data)
let encodingTime = CFAbsoluteTimeGetCurrent() - startTime let encodingTime = CFAbsoluteTimeGetCurrent() - startTime
#expect(encoded.count > 0) #expect(!encoded.isEmpty)
#expect(encodingTime < 1.0) // Should encode within 1 second #expect(encodingTime < 1.0) // Should encode within 1 second
} }
@Test("Thread safety of JSON operations", .tags(.concurrency)) @Test("Thread safety of JSON operations", .tags(.concurrency))
func threadSafetyJSONOperations() async { func threadSafetyJSONOperations() async {
await withTaskGroup(of: Bool.self) { group in await withTaskGroup(of: Bool.self) { group in
for i in 0..<10 { for index in 0..<10 {
group.addTask { group.addTask {
do { do {
let appInfo = ApplicationInfo( let appInfo = ApplicationInfo(
app_name: "App \(i)", app_name: "App \(index)",
bundle_id: "com.test.app\(i)", bundle_id: "com.test.app\(index)",
pid: Int32(1000 + i), pid: Int32(1000 + index),
is_active: true, is_active: true,
window_count: 1 window_count: 1
) )
// Test encoding through AnyCodable instead // Test encoding through AnyCodable instead
let anyCodable = AnyCodable(appInfo) let anyCodable = AnyCodable(appInfo)
let _ = try JSONEncoder().encode(anyCodable) _ = try JSONEncoder().encode(anyCodable)
return true return true
} catch { } catch {
return false return false
} }
} }
} }
var successCount = 0 var successCount = 0
for await success in group { for await success in group where success {
if success { successCount += 1
successCount += 1
}
} }
#expect(successCount == 10) #expect(successCount == 10)
} }
} }
@Test("Memory usage with repeated JSON operations", .tags(.memory)) @Test("Memory usage with repeated JSON operations", .tags(.memory))
func memoryUsageJSONOperations() { func memoryUsageJSONOperations() {
// Test memory doesn't grow excessively with repeated JSON operations // Test memory doesn't grow excessively with repeated JSON operations
@ -258,16 +256,16 @@ struct JSONOutputTests {
is_active: true, is_active: true,
window_count: 1 window_count: 1
) )
do { do {
let encoded = try JSONEncoder().encode(data) let encoded = try JSONEncoder().encode(data)
#expect(encoded.count > 0) #expect(!encoded.isEmpty)
} catch { } catch {
Issue.record("JSON encoding should not fail: \(error)") Issue.record("JSON encoding should not fail: \(error)")
} }
} }
} }
@Test("Error code enum completeness", .tags(.fast)) @Test("Error code enum completeness", .tags(.fast))
func errorCodeEnumCompleteness() { func errorCodeEnumCompleteness() {
// Test that all error codes have proper raw values // Test that all error codes have proper raw values
@ -282,10 +280,10 @@ struct JSONOutputTests {
.INVALID_ARGUMENT, .INVALID_ARGUMENT,
.UNKNOWN_ERROR .UNKNOWN_ERROR
] ]
for errorCode in errorCodes { for errorCode in errorCodes {
#expect(!errorCode.rawValue.isEmpty) #expect(!errorCode.rawValue.isEmpty)
#expect(errorCode.rawValue.allSatisfy { $0.isASCII }) #expect(errorCode.rawValue.allSatisfy(\.isASCII))
} }
} }
} }
@ -294,31 +292,30 @@ struct JSONOutputTests {
@Suite("JSON Output Format Validation", .tags(.jsonOutput, .integration)) @Suite("JSON Output Format Validation", .tags(.jsonOutput, .integration))
struct JSONOutputFormatValidationTests { struct JSONOutputFormatValidationTests {
@Test("MCP protocol compliance", .tags(.integration)) @Test("MCP protocol compliance", .tags(.integration))
func mcpProtocolCompliance() throws { func mcpProtocolCompliance() throws {
// Test that JSON output follows MCP protocol format // Test that JSON output follows MCP protocol format
let testData = ApplicationListData(applications: []) let testData = ApplicationListData(applications: [])
let response = CodableJSONResponse( let response = CodableJSONResponse(
success: true, success: true,
data: testData, data: testData,
messages: nil, messages: nil,
debug_logs: [] debug_logs: []
) )
let encoder = JSONEncoder() let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase encoder.keyEncodingStrategy = .convertToSnakeCase
let data = try encoder.encode(response) let data = try encoder.encode(response)
// Verify it's valid JSON // Verify it's valid JSON
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
#expect(json != nil) // JSON was successfully created #expect(json != nil) // JSON was successfully created
// Verify required MCP fields // Verify required MCP fields
#expect(json?["success"] != nil) #expect(json?["success"] != nil)
#expect(json?["data"] != nil) #expect(json?["data"] != nil)
} }
@Test("Snake case conversion consistency", .tags(.fast)) @Test("Snake case conversion consistency", .tags(.fast))
func snakeCaseConversionConsistency() throws { func snakeCaseConversionConsistency() throws {
let appInfo = ApplicationInfo( let appInfo = ApplicationInfo(
@ -328,25 +325,25 @@ struct JSONOutputFormatValidationTests {
is_active: true, is_active: true,
window_count: 2 window_count: 2
) )
let encoder = JSONEncoder() let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase encoder.keyEncodingStrategy = .convertToSnakeCase
let data = try encoder.encode(appInfo) let data = try encoder.encode(appInfo)
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
// Verify snake_case conversion // Verify snake_case conversion
#expect(json?["app_name"] != nil) #expect(json?["app_name"] != nil)
#expect(json?["bundle_id"] != nil) #expect(json?["bundle_id"] != nil)
#expect(json?["is_active"] != nil) #expect(json?["is_active"] != nil)
#expect(json?["window_count"] != nil) #expect(json?["window_count"] != nil)
// Verify no camelCase keys exist // Verify no camelCase keys exist
#expect(json?["appName"] == nil) #expect(json?["appName"] == nil)
#expect(json?["bundleId"] == nil) #expect(json?["bundleId"] == nil)
#expect(json?["isActive"] == nil) #expect(json?["isActive"] == nil)
#expect(json?["windowCount"] == nil) #expect(json?["windowCount"] == nil)
} }
@Test("Large data structure serialization", .tags(.performance)) @Test("Large data structure serialization", .tags(.performance))
func largeDataStructureSerialization() throws { func largeDataStructureSerialization() throws {
// Create a complex data structure // Create a complex data structure
@ -357,11 +354,11 @@ struct JSONOutputFormatValidationTests {
window_id: UInt32(1000 + index), window_id: UInt32(1000 + index),
window_index: index, window_index: index,
bounds: WindowBounds(xCoordinate: index * 10, yCoordinate: index * 10, width: 800, height: 600), bounds: WindowBounds(xCoordinate: index * 10, yCoordinate: index * 10, width: 800, height: 600),
is_on_screen: index % 2 == 0 is_on_screen: index.isMultiple(of: 2)
) )
windows.append(window) windows.append(window)
} }
let windowData = WindowListData( let windowData = WindowListData(
windows: windows, windows: windows,
target_application_info: TargetApplicationInfo( target_application_info: TargetApplicationInfo(
@ -370,16 +367,16 @@ struct JSONOutputFormatValidationTests {
pid: 1234 pid: 1234
) )
) )
let startTime = CFAbsoluteTimeGetCurrent() let startTime = CFAbsoluteTimeGetCurrent()
let encoded = try JSONEncoder().encode(windowData) let encoded = try JSONEncoder().encode(windowData)
let duration = CFAbsoluteTimeGetCurrent() - startTime let duration = CFAbsoluteTimeGetCurrent() - startTime
#expect(encoded.count > 0) #expect(!encoded.isEmpty)
#expect(duration < 0.5) // Should complete within 500ms #expect(duration < 0.5) // Should complete within 500ms
// Verify the JSON is valid // Verify the JSON is valid
let _ = try JSONSerialization.jsonObject(with: encoded) _ = try JSONSerialization.jsonObject(with: encoded)
#expect(Bool(true)) // JSON was successfully created #expect(Bool(true)) // JSON was successfully created
} }
} }

View file

@ -1,12 +1,12 @@
import ArgumentParser import ArgumentParser
import Foundation
@testable import peekaboo @testable import peekaboo
import Testing import Testing
import Foundation
@Suite("ListCommand Tests", .tags(.unit)) @Suite("ListCommand Tests", .tags(.unit))
struct ListCommandTests { struct ListCommandTests {
// MARK: - Command Parsing Tests // MARK: - Command Parsing Tests
@Test("ListCommand has correct subcommands", .tags(.fast)) @Test("ListCommand has correct subcommands", .tags(.fast))
func listCommandSubcommands() throws { func listCommandSubcommands() throws {
// Test that ListCommand has the expected subcommands // Test that ListCommand has the expected subcommands
@ -15,31 +15,31 @@ struct ListCommandTests {
#expect(ListCommand.configuration.subcommands.contains { $0 == WindowsSubcommand.self }) #expect(ListCommand.configuration.subcommands.contains { $0 == WindowsSubcommand.self })
#expect(ListCommand.configuration.subcommands.contains { $0 == ServerStatusSubcommand.self }) #expect(ListCommand.configuration.subcommands.contains { $0 == ServerStatusSubcommand.self })
} }
@Test("AppsSubcommand parsing with defaults", .tags(.fast)) @Test("AppsSubcommand parsing with defaults", .tags(.fast))
func appsSubcommandParsing() throws { func appsSubcommandParsing() throws {
// Test parsing apps subcommand // Test parsing apps subcommand
let command = try AppsSubcommand.parse([]) let command = try AppsSubcommand.parse([])
#expect(command.jsonOutput == false) #expect(command.jsonOutput == false)
} }
@Test("AppsSubcommand with JSON output flag", .tags(.fast)) @Test("AppsSubcommand with JSON output flag", .tags(.fast))
func appsSubcommandWithJSONOutput() throws { func appsSubcommandWithJSONOutput() throws {
// Test apps subcommand with JSON flag // Test apps subcommand with JSON flag
let command = try AppsSubcommand.parse(["--json-output"]) let command = try AppsSubcommand.parse(["--json-output"])
#expect(command.jsonOutput == true) #expect(command.jsonOutput == true)
} }
@Test("WindowsSubcommand parsing with required app", .tags(.fast)) @Test("WindowsSubcommand parsing with required app", .tags(.fast))
func windowsSubcommandParsing() throws { func windowsSubcommandParsing() throws {
// Test parsing windows subcommand with required app // Test parsing windows subcommand with required app
let command = try WindowsSubcommand.parse(["--app", "Finder"]) let command = try WindowsSubcommand.parse(["--app", "Finder"])
#expect(command.app == "Finder") #expect(command.app == "Finder")
#expect(command.jsonOutput == false) #expect(command.jsonOutput == false)
#expect(command.includeDetails == nil) #expect(command.includeDetails == nil)
} }
@Test("WindowsSubcommand with detail options", .tags(.fast)) @Test("WindowsSubcommand with detail options", .tags(.fast))
func windowsSubcommandWithDetails() throws { func windowsSubcommandWithDetails() throws {
// Test windows subcommand with detail options // Test windows subcommand with detail options
@ -47,11 +47,11 @@ struct ListCommandTests {
"--app", "Finder", "--app", "Finder",
"--include-details", "bounds,ids" "--include-details", "bounds,ids"
]) ])
#expect(command.app == "Finder") #expect(command.app == "Finder")
#expect(command.includeDetails == "bounds,ids") #expect(command.includeDetails == "bounds,ids")
} }
@Test("WindowsSubcommand requires app parameter", .tags(.fast)) @Test("WindowsSubcommand requires app parameter", .tags(.fast))
func windowsSubcommandMissingApp() { func windowsSubcommandMissingApp() {
// Test that windows subcommand requires app // Test that windows subcommand requires app
@ -59,29 +59,31 @@ struct ListCommandTests {
try WindowsSubcommand.parse([]) try WindowsSubcommand.parse([])
} }
} }
// MARK: - Parameterized Command Tests // MARK: - Parameterized Command Tests
@Test("WindowsSubcommand detail parsing", @Test(
arguments: [ "WindowsSubcommand detail parsing",
"off_screen", arguments: [
"bounds", "off_screen",
"ids", "bounds",
"off_screen,bounds", "ids",
"bounds,ids", "off_screen,bounds",
"off_screen,bounds,ids" "bounds,ids",
]) "off_screen,bounds,ids"
]
)
func windowsDetailParsing(details: String) throws { func windowsDetailParsing(details: String) throws {
let command = try WindowsSubcommand.parse([ let command = try WindowsSubcommand.parse([
"--app", "Safari", "--app", "Safari",
"--include-details", details "--include-details", details
]) ])
#expect(command.includeDetails == details) #expect(command.includeDetails == details)
} }
// MARK: - Data Structure Tests // MARK: - Data Structure Tests
@Test("ApplicationInfo JSON encoding", .tags(.fast)) @Test("ApplicationInfo JSON encoding", .tags(.fast))
func applicationInfoEncoding() throws { func applicationInfoEncoding() throws {
// Test ApplicationInfo JSON encoding // Test ApplicationInfo JSON encoding
@ -92,13 +94,13 @@ struct ListCommandTests {
is_active: true, is_active: true,
window_count: 5 window_count: 5
) )
let encoder = JSONEncoder() let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase encoder.keyEncodingStrategy = .convertToSnakeCase
let data = try encoder.encode(appInfo) let data = try encoder.encode(appInfo)
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
#expect(json != nil) #expect(json != nil)
#expect(json?["app_name"] as? String == "Finder") #expect(json?["app_name"] as? String == "Finder")
#expect(json?["bundle_id"] as? String == "com.apple.finder") #expect(json?["bundle_id"] as? String == "com.apple.finder")
@ -106,7 +108,7 @@ struct ListCommandTests {
#expect(json?["is_active"] as? Bool == true) #expect(json?["is_active"] as? Bool == true)
#expect(json?["window_count"] as? Int == 5) #expect(json?["window_count"] as? Int == 5)
} }
@Test("ApplicationListData JSON encoding", .tags(.fast)) @Test("ApplicationListData JSON encoding", .tags(.fast))
func applicationListDataEncoding() throws { func applicationListDataEncoding() throws {
// Test ApplicationListData JSON encoding // Test ApplicationListData JSON encoding
@ -128,18 +130,18 @@ struct ListCommandTests {
) )
] ]
) )
let encoder = JSONEncoder() let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase encoder.keyEncodingStrategy = .convertToSnakeCase
let data = try encoder.encode(appData) let data = try encoder.encode(appData)
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
#expect(json != nil) #expect(json != nil)
let apps = json?["applications"] as? [[String: Any]] let apps = json?["applications"] as? [[String: Any]]
#expect(apps?.count == 2) #expect(apps?.count == 2)
} }
@Test("WindowInfo JSON encoding", .tags(.fast)) @Test("WindowInfo JSON encoding", .tags(.fast))
func windowInfoEncoding() throws { func windowInfoEncoding() throws {
// Test WindowInfo JSON encoding // Test WindowInfo JSON encoding
@ -150,25 +152,25 @@ struct ListCommandTests {
bounds: WindowBounds(xCoordinate: 100, yCoordinate: 200, width: 800, height: 600), bounds: WindowBounds(xCoordinate: 100, yCoordinate: 200, width: 800, height: 600),
is_on_screen: true is_on_screen: true
) )
let encoder = JSONEncoder() let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase encoder.keyEncodingStrategy = .convertToSnakeCase
let data = try encoder.encode(windowInfo) let data = try encoder.encode(windowInfo)
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
#expect(json != nil) #expect(json != nil)
#expect(json?["window_title"] as? String == "Documents") #expect(json?["window_title"] as? String == "Documents")
#expect(json?["window_id"] as? UInt32 == 1001) #expect(json?["window_id"] as? UInt32 == 1001)
#expect(json?["is_on_screen"] as? Bool == true) #expect(json?["is_on_screen"] as? Bool == true)
let bounds = json?["bounds"] as? [String: Any] let bounds = json?["bounds"] as? [String: Any]
#expect(bounds?["x_coordinate"] as? Int == 100) #expect(bounds?["x_coordinate"] as? Int == 100)
#expect(bounds?["y_coordinate"] as? Int == 200) #expect(bounds?["y_coordinate"] as? Int == 200)
#expect(bounds?["width"] as? Int == 800) #expect(bounds?["width"] as? Int == 800)
#expect(bounds?["height"] as? Int == 600) #expect(bounds?["height"] as? Int == 600)
} }
@Test("WindowListData JSON encoding", .tags(.fast)) @Test("WindowListData JSON encoding", .tags(.fast))
func windowListDataEncoding() throws { func windowListDataEncoding() throws {
// Test WindowListData JSON encoding // Test WindowListData JSON encoding
@ -188,25 +190,25 @@ struct ListCommandTests {
pid: 123 pid: 123
) )
) )
let encoder = JSONEncoder() let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase encoder.keyEncodingStrategy = .convertToSnakeCase
let data = try encoder.encode(windowData) let data = try encoder.encode(windowData)
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
#expect(json != nil) #expect(json != nil)
let windows = json?["windows"] as? [[String: Any]] let windows = json?["windows"] as? [[String: Any]]
#expect(windows?.count == 1) #expect(windows?.count == 1)
let targetApp = json?["target_application_info"] as? [String: Any] let targetApp = json?["target_application_info"] as? [String: Any]
#expect(targetApp?["app_name"] as? String == "Finder") #expect(targetApp?["app_name"] as? String == "Finder")
#expect(targetApp?["bundle_id"] as? String == "com.apple.finder") #expect(targetApp?["bundle_id"] as? String == "com.apple.finder")
} }
// MARK: - Window Detail Option Tests // MARK: - Window Detail Option Tests
@Test("WindowDetailOption raw values", .tags(.fast)) @Test("WindowDetailOption raw values", .tags(.fast))
func windowDetailOptionRawValues() { func windowDetailOptionRawValues() {
// Test window detail option values // Test window detail option values
@ -214,14 +216,14 @@ struct ListCommandTests {
#expect(WindowDetailOption.bounds.rawValue == "bounds") #expect(WindowDetailOption.bounds.rawValue == "bounds")
#expect(WindowDetailOption.ids.rawValue == "ids") #expect(WindowDetailOption.ids.rawValue == "ids")
} }
// MARK: - Window Specifier Tests // MARK: - Window Specifier Tests
@Test("WindowSpecifier with title", .tags(.fast)) @Test("WindowSpecifier with title", .tags(.fast))
func windowSpecifierTitle() { func windowSpecifierTitle() {
// Test window specifier with title // Test window specifier with title
let specifier = WindowSpecifier.title("Documents") let specifier = WindowSpecifier.title("Documents")
switch specifier { switch specifier {
case let .title(title): case let .title(title):
#expect(title == "Documents") #expect(title == "Documents")
@ -229,12 +231,12 @@ struct ListCommandTests {
Issue.record("Expected title specifier") Issue.record("Expected title specifier")
} }
} }
@Test("WindowSpecifier with index", .tags(.fast)) @Test("WindowSpecifier with index", .tags(.fast))
func windowSpecifierIndex() { func windowSpecifierIndex() {
// Test window specifier with index // Test window specifier with index
let specifier = WindowSpecifier.index(0) let specifier = WindowSpecifier.index(0)
switch specifier { switch specifier {
case let .index(index): case let .index(index):
#expect(index == 0) #expect(index == 0)
@ -242,11 +244,13 @@ struct ListCommandTests {
Issue.record("Expected index specifier") Issue.record("Expected index specifier")
} }
} }
// MARK: - Performance Tests // MARK: - Performance Tests
@Test("ApplicationListData encoding performance", @Test(
arguments: [10, 50, 100, 200]) "ApplicationListData encoding performance",
arguments: [10, 50, 100, 200]
)
func applicationListEncodingPerformance(appCount: Int) throws { func applicationListEncodingPerformance(appCount: Int) throws {
// Test performance of encoding many applications // Test performance of encoding many applications
let apps = (0..<appCount).map { index in let apps = (0..<appCount).map { index in
@ -258,14 +262,14 @@ struct ListCommandTests {
window_count: index % 5 window_count: index % 5
) )
} }
let appData = ApplicationListData(applications: apps) let appData = ApplicationListData(applications: apps)
let encoder = JSONEncoder() let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase encoder.keyEncodingStrategy = .convertToSnakeCase
// Ensure encoding works correctly // Ensure encoding works correctly
let data = try encoder.encode(appData) let data = try encoder.encode(appData)
#expect(data.count > 0) #expect(!data.isEmpty)
} }
} }
@ -273,37 +277,38 @@ struct ListCommandTests {
@Suite("ListCommand Advanced Tests", .tags(.integration)) @Suite("ListCommand Advanced Tests", .tags(.integration))
struct ListCommandAdvancedTests { struct ListCommandAdvancedTests {
@Test("ServerStatusSubcommand parsing", .tags(.fast)) @Test("ServerStatusSubcommand parsing", .tags(.fast))
func serverStatusSubcommandParsing() throws { func serverStatusSubcommandParsing() throws {
let command = try ServerStatusSubcommand.parse([]) let command = try ServerStatusSubcommand.parse([])
#expect(command.jsonOutput == false) #expect(command.jsonOutput == false)
let commandWithJSON = try ServerStatusSubcommand.parse(["--json-output"]) let commandWithJSON = try ServerStatusSubcommand.parse(["--json-output"])
#expect(commandWithJSON.jsonOutput == true) #expect(commandWithJSON.jsonOutput == true)
} }
@Test("Command help messages", .tags(.fast)) @Test("Command help messages", .tags(.fast))
func commandHelpMessages() { func commandHelpMessages() {
let listHelp = ListCommand.helpMessage() let listHelp = ListCommand.helpMessage()
#expect(listHelp.contains("List")) #expect(listHelp.contains("List"))
let appsHelp = AppsSubcommand.helpMessage() let appsHelp = AppsSubcommand.helpMessage()
#expect(appsHelp.contains("running applications")) #expect(appsHelp.contains("running applications"))
let windowsHelp = WindowsSubcommand.helpMessage() let windowsHelp = WindowsSubcommand.helpMessage()
#expect(windowsHelp.contains("windows")) #expect(windowsHelp.contains("windows"))
let statusHelp = ServerStatusSubcommand.helpMessage() let statusHelp = ServerStatusSubcommand.helpMessage()
#expect(statusHelp.contains("status")) #expect(statusHelp.contains("status"))
} }
@Test("Complex window info structures", @Test(
arguments: [ "Complex window info structures",
(title: "Main Window", id: 1001, onScreen: true), arguments: [
(title: "Hidden Window", id: 2001, onScreen: false), (title: "Main Window", id: 1001, onScreen: true),
(title: "Minimized", id: 3001, onScreen: false) (title: "Hidden Window", id: 2001, onScreen: false),
]) (title: "Minimized", id: 3001, onScreen: false)
]
)
func complexWindowInfo(title: String, id: UInt32, onScreen: Bool) throws { func complexWindowInfo(title: String, id: UInt32, onScreen: Bool) throws {
let windowInfo = WindowInfo( let windowInfo = WindowInfo(
window_title: title, window_title: title,
@ -312,27 +317,29 @@ struct ListCommandAdvancedTests {
bounds: nil, bounds: nil,
is_on_screen: onScreen is_on_screen: onScreen
) )
let encoder = JSONEncoder() let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase encoder.keyEncodingStrategy = .convertToSnakeCase
let data = try encoder.encode(windowInfo) let data = try encoder.encode(windowInfo)
let decoder = JSONDecoder() let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase decoder.keyDecodingStrategy = .convertFromSnakeCase
let decoded = try decoder.decode(WindowInfo.self, from: data) let decoded = try decoder.decode(WindowInfo.self, from: data)
#expect(decoded.window_title == title) #expect(decoded.window_title == title)
#expect(decoded.window_id == id) #expect(decoded.window_id == id)
#expect(decoded.is_on_screen == onScreen) #expect(decoded.is_on_screen == onScreen)
} }
@Test("Application state combinations", @Test(
arguments: [ "Application state combinations",
(active: true, windowCount: 5), arguments: [
(active: false, windowCount: 0), (active: true, windowCount: 5),
(active: true, windowCount: 0), (active: false, windowCount: 0),
(active: false, windowCount: 10) (active: true, windowCount: 0),
]) (active: false, windowCount: 10)
]
)
func applicationStates(active: Bool, windowCount: Int) { func applicationStates(active: Bool, windowCount: Int) {
let appInfo = ApplicationInfo( let appInfo = ApplicationInfo(
app_name: "TestApp", app_name: "TestApp",
@ -341,34 +348,34 @@ struct ListCommandAdvancedTests {
is_active: active, is_active: active,
window_count: windowCount window_count: windowCount
) )
#expect(appInfo.is_active == active) #expect(appInfo.is_active == active)
#expect(appInfo.window_count == windowCount) #expect(appInfo.window_count == windowCount)
// Logical consistency checks // Logical consistency checks
if windowCount > 0 { if windowCount > 0 {
// Apps with windows can be active or inactive // Apps with windows can be active or inactive
#expect(appInfo.window_count > 0) #expect(appInfo.window_count > 0)
} }
} }
@Test("Server permissions data encoding", .tags(.fast)) @Test("Server permissions data encoding", .tags(.fast))
func serverPermissionsEncoding() throws { func serverPermissionsEncoding() throws {
let permissions = ServerPermissions( let permissions = ServerPermissions(
screen_recording: true, screen_recording: true,
accessibility: false accessibility: false
) )
let statusData = ServerStatusData(permissions: permissions) let statusData = ServerStatusData(permissions: permissions)
let encoder = JSONEncoder() let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase encoder.keyEncodingStrategy = .convertToSnakeCase
let data = try encoder.encode(statusData) let data = try encoder.encode(statusData)
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
let permsJson = json?["permissions"] as? [String: Any] let permsJson = json?["permissions"] as? [String: Any]
#expect(permsJson?["screen_recording"] as? Bool == true) #expect(permsJson?["screen_recording"] as? Bool == true)
#expect(permsJson?["accessibility"] as? Bool == false) #expect(permsJson?["accessibility"] as? Bool == false)
} }
} }

View file

@ -1,322 +1,319 @@
import Foundation
@testable import peekaboo @testable import peekaboo
import Testing import Testing
import Foundation
@Suite("Logger Tests", .tags(.logger, .unit), .serialized) @Suite("Logger Tests", .tags(.logger, .unit), .serialized)
struct LoggerTests { struct LoggerTests {
// MARK: - Basic Functionality Tests // MARK: - Basic Functionality Tests
@Test("Logger singleton instance", .tags(.fast)) @Test("Logger singleton instance", .tags(.fast))
func loggerSingletonInstance() { func loggerSingletonInstance() {
let logger1 = Logger.shared let logger1 = Logger.shared
let logger2 = Logger.shared let logger2 = Logger.shared
// Should be the same instance // Should be the same instance
#expect(logger1 === logger2) #expect(logger1 === logger2)
} }
@Test("JSON output mode switching", .tags(.fast)) @Test("JSON output mode switching", .tags(.fast))
func jsonOutputModeSwitching() { func jsonOutputModeSwitching() {
let logger = Logger.shared let logger = Logger.shared
// Test setting JSON mode // Test setting JSON mode
logger.setJsonOutputMode(true) logger.setJsonOutputMode(true)
// Cannot directly test internal state, but verify no crash // Cannot directly test internal state, but verify no crash
logger.setJsonOutputMode(false) logger.setJsonOutputMode(false)
// Cannot directly test internal state, but verify no crash // Cannot directly test internal state, but verify no crash
// Test multiple switches // Test multiple switches
for _ in 1...10 { for _ in 1...10 {
logger.setJsonOutputMode(true) logger.setJsonOutputMode(true)
logger.setJsonOutputMode(false) logger.setJsonOutputMode(false)
} }
} }
@Test("Debug log message recording", .tags(.fast)) @Test("Debug log message recording", .tags(.fast))
func debugLogMessageRecording() async { func debugLogMessageRecording() async {
let logger = Logger.shared let logger = Logger.shared
// Enable JSON mode and clear logs // Enable JSON mode and clear logs
logger.setJsonOutputMode(true) logger.setJsonOutputMode(true)
logger.clearDebugLogs() logger.clearDebugLogs()
// Wait for mode setting to complete // Wait for mode setting to complete
try? await Task.sleep(nanoseconds: 10_000_000) // 10ms try? await Task.sleep(nanoseconds: 10_000_000) // 10ms
// Record some debug messages // Record some debug messages
logger.debug("Test debug message 1") logger.debug("Test debug message 1")
logger.debug("Test debug message 2") logger.debug("Test debug message 2")
logger.info("Test info message") logger.info("Test info message")
logger.error("Test error message") logger.error("Test error message")
// Wait for logging to complete // Wait for logging to complete
try? await Task.sleep(nanoseconds: 10_000_000) // 10ms try? await Task.sleep(nanoseconds: 10_000_000) // 10ms
let logs = logger.getDebugLogs() let logs = logger.getDebugLogs()
// Should have exactly the messages we added // Should have exactly the messages we added
#expect(logs.count == 4) #expect(logs.count == 4)
// Verify messages are stored // Verify messages are stored
#expect(logs.contains { $0.contains("Test debug message 1") }) #expect(logs.contains { $0.contains("Test debug message 1") })
#expect(logs.contains { $0.contains("Test debug message 2") }) #expect(logs.contains { $0.contains("Test debug message 2") })
#expect(logs.contains { $0.contains("Test info message") }) #expect(logs.contains { $0.contains("Test info message") })
#expect(logs.contains { $0.contains("Test error message") }) #expect(logs.contains { $0.contains("Test error message") })
// Reset for other tests // Reset for other tests
logger.setJsonOutputMode(false) logger.setJsonOutputMode(false)
} }
@Test("Debug logs retrieval and format", .tags(.fast)) @Test("Debug logs retrieval and format", .tags(.fast))
func debugLogsRetrievalAndFormat() async { func debugLogsRetrievalAndFormat() async {
let logger = Logger.shared let logger = Logger.shared
// Enable JSON mode and clear logs // Enable JSON mode and clear logs
logger.setJsonOutputMode(true) logger.setJsonOutputMode(true)
logger.clearDebugLogs() logger.clearDebugLogs()
// Wait for setup to complete // Wait for setup to complete
try? await Task.sleep(nanoseconds: 10_000_000) // 10ms try? await Task.sleep(nanoseconds: 10_000_000) // 10ms
// Add test messages // Add test messages
logger.debug("Debug test") logger.debug("Debug test")
logger.info("Info test") logger.info("Info test")
logger.warn("Warning test") logger.warn("Warning test")
logger.error("Error test") logger.error("Error test")
// Wait for logging to complete // Wait for logging to complete
try? await Task.sleep(nanoseconds: 10_000_000) // 10ms try? await Task.sleep(nanoseconds: 10_000_000) // 10ms
let logs = logger.getDebugLogs() let logs = logger.getDebugLogs()
// Should have exactly our messages // Should have exactly our messages
#expect(logs.count == 4) #expect(logs.count == 4)
// Verify log format includes level prefixes // Verify log format includes level prefixes
#expect(logs.contains { $0.contains("Debug test") }) #expect(logs.contains { $0.contains("Debug test") })
#expect(logs.contains { $0.contains("INFO: Info test") }) #expect(logs.contains { $0.contains("INFO: Info test") })
#expect(logs.contains { $0.contains("WARN: Warning test") }) #expect(logs.contains { $0.contains("WARN: Warning test") })
#expect(logs.contains { $0.contains("ERROR: Error test") }) #expect(logs.contains { $0.contains("ERROR: Error test") })
// Reset for other tests // Reset for other tests
logger.setJsonOutputMode(false) logger.setJsonOutputMode(false)
} }
// MARK: - Thread Safety Tests // MARK: - Thread Safety Tests
@Test("Concurrent logging operations", .tags(.concurrency)) @Test("Concurrent logging operations", .tags(.concurrency))
func concurrentLoggingOperations() async { func concurrentLoggingOperations() async {
let logger = Logger.shared let logger = Logger.shared
// Enable JSON mode and clear logs // Enable JSON mode and clear logs
logger.setJsonOutputMode(true) logger.setJsonOutputMode(true)
logger.clearDebugLogs() logger.clearDebugLogs()
// Wait for setup // Wait for setup
try? await Task.sleep(nanoseconds: 10_000_000) // 10ms try? await Task.sleep(nanoseconds: 10_000_000) // 10ms
let initialCount = logger.getDebugLogs().count let initialCount = logger.getDebugLogs().count
await withTaskGroup(of: Void.self) { group in await withTaskGroup(of: Void.self) { group in
// Create multiple concurrent logging tasks // Create multiple concurrent logging tasks
for i in 0..<10 { for index in 0..<10 {
group.addTask { group.addTask {
logger.debug("Concurrent message \(i)") logger.debug("Concurrent message \(index)")
logger.info("Concurrent info \(i)") logger.info("Concurrent info \(index)")
logger.error("Concurrent error \(i)") logger.error("Concurrent error \(index)")
} }
} }
} }
// Wait for logging to complete // Wait for logging to complete
try? await Task.sleep(nanoseconds: 50_000_000) // 50ms try? await Task.sleep(nanoseconds: 50_000_000) // 50ms
let finalLogs = logger.getDebugLogs() let finalLogs = logger.getDebugLogs()
// Should have all messages (30 new messages) // Should have all messages (30 new messages)
#expect(finalLogs.count >= initialCount + 30) #expect(finalLogs.count >= initialCount + 30)
// Verify no corruption by checking for our messages // Verify no corruption by checking for our messages
let recentLogs = finalLogs.suffix(30) let recentLogs = finalLogs.suffix(30)
var foundMessages = 0 var foundMessages = 0
for i in 0..<10 { for index in 0..<10 where recentLogs.contains(where: { $0.contains("Concurrent message \(index)") }) {
if recentLogs.contains(where: { $0.contains("Concurrent message \(i)") }) { foundMessages += 1
foundMessages += 1
}
} }
// Should find most or all messages (allowing for some timing issues) // Should find most or all messages (allowing for some timing issues)
#expect(foundMessages >= 7) #expect(foundMessages >= 7)
// Reset // Reset
logger.setJsonOutputMode(false) logger.setJsonOutputMode(false)
} }
@Test("Concurrent mode switching and logging", .tags(.concurrency)) @Test("Concurrent mode switching and logging", .tags(.concurrency))
func concurrentModeSwitchingAndLogging() async { func concurrentModeSwitchingAndLogging() async {
let logger = Logger.shared let logger = Logger.shared
await withTaskGroup(of: Void.self) { group in await withTaskGroup(of: Void.self) { group in
// Task 1: Rapid mode switching // Task 1: Rapid mode switching
group.addTask { group.addTask {
for i in 0..<50 { for index in 0..<50 {
logger.setJsonOutputMode(i % 2 == 0) logger.setJsonOutputMode(index.isMultiple(of: 2))
} }
} }
// Task 2: Continuous logging during mode switches // Task 2: Continuous logging during mode switches
group.addTask { group.addTask {
for i in 0..<100 { for index in 0..<100 {
logger.debug("Mode switch test \(i)") logger.debug("Mode switch test \(index)")
} }
} }
// Task 3: Log retrieval during operations // Task 3: Log retrieval during operations
group.addTask { group.addTask {
for _ in 0..<10 { for _ in 0..<10 {
let logs = logger.getDebugLogs() let logs = logger.getDebugLogs()
#expect(logs.count >= 0) // Should not crash // Logs count is always non-negative
} }
} }
} }
// Should complete without crashes // Should complete without crashes
#expect(Bool(true)) #expect(Bool(true))
} }
// MARK: - Memory Management Tests // MARK: - Memory Management Tests
@Test("Memory usage with extensive logging", .tags(.memory)) @Test("Memory usage with extensive logging", .tags(.memory))
func memoryUsageExtensiveLogging() async { func memoryUsageExtensiveLogging() async {
let logger = Logger.shared let logger = Logger.shared
// Enable JSON mode and clear logs // Enable JSON mode and clear logs
logger.setJsonOutputMode(true) logger.setJsonOutputMode(true)
logger.clearDebugLogs() logger.clearDebugLogs()
// Wait for setup // Wait for setup
try? await Task.sleep(nanoseconds: 10_000_000) // 10ms try? await Task.sleep(nanoseconds: 10_000_000) // 10ms
let initialCount = logger.getDebugLogs().count let initialCount = logger.getDebugLogs().count
// Generate many log messages // Generate many log messages
for i in 1...100 { for index in 1...100 {
logger.debug("Memory test message \(i)") logger.debug("Memory test message \(index)")
logger.info("Memory test info \(i)") logger.info("Memory test info \(index)")
logger.error("Memory test error \(i)") logger.error("Memory test error \(index)")
} }
// Wait for logging // Wait for logging
try? await Task.sleep(nanoseconds: 100_000_000) // 100ms try? await Task.sleep(nanoseconds: 100_000_000) // 100ms
let finalLogs = logger.getDebugLogs() let finalLogs = logger.getDebugLogs()
// Should have accumulated messages // Should have accumulated messages
#expect(finalLogs.count >= initialCount + 300) #expect(finalLogs.count >= initialCount + 300)
// Verify memory doesn't grow unbounded by checking we can still log // Verify memory doesn't grow unbounded by checking we can still log
logger.debug("Final test message") logger.debug("Final test message")
// Wait for final log // Wait for final log
try? await Task.sleep(nanoseconds: 10_000_000) // 10ms try? await Task.sleep(nanoseconds: 10_000_000) // 10ms
let postTestLogs = logger.getDebugLogs() let postTestLogs = logger.getDebugLogs()
#expect(postTestLogs.count > finalLogs.count) #expect(postTestLogs.count > finalLogs.count)
// Reset // Reset
logger.setJsonOutputMode(false) logger.setJsonOutputMode(false)
} }
@Test("Debug logs array management", .tags(.fast)) @Test("Debug logs array management", .tags(.fast))
func debugLogsArrayManagement() { func debugLogsArrayManagement() {
let logger = Logger.shared let logger = Logger.shared
// Test that logs are properly maintained // Test that logs are properly maintained
let initialLogs = logger.getDebugLogs() let initialLogs = logger.getDebugLogs()
// Add known messages // Add known messages
logger.debug("Management test 1") logger.debug("Management test 1")
logger.debug("Management test 2") logger.debug("Management test 2")
let middleLogs = logger.getDebugLogs() let middleLogs = logger.getDebugLogs()
#expect(middleLogs.count > initialLogs.count) #expect(middleLogs.count > initialLogs.count)
// Add more messages // Add more messages
logger.debug("Management test 3") logger.debug("Management test 3")
logger.debug("Management test 4") logger.debug("Management test 4")
let finalLogs = logger.getDebugLogs() let finalLogs = logger.getDebugLogs()
#expect(finalLogs.count > middleLogs.count) #expect(finalLogs.count > middleLogs.count)
// Verify recent messages are present // Verify recent messages are present
#expect(finalLogs.last?.contains("Management test 4") == true) #expect(finalLogs.last?.contains("Management test 4") == true)
} }
// MARK: - Performance Tests // MARK: - Performance Tests
@Test("Logging performance benchmark", .tags(.performance)) @Test("Logging performance benchmark", .tags(.performance))
func loggingPerformanceBenchmark() { func loggingPerformanceBenchmark() {
let logger = Logger.shared let logger = Logger.shared
// Measure logging performance // Measure logging performance
let messageCount = 1000 let messageCount = 1000
let startTime = CFAbsoluteTimeGetCurrent() let startTime = CFAbsoluteTimeGetCurrent()
for i in 1...messageCount { for index in 1...messageCount {
logger.debug("Performance test message \(i)") logger.debug("Performance test message \(index)")
} }
let duration = CFAbsoluteTimeGetCurrent() - startTime let duration = CFAbsoluteTimeGetCurrent() - startTime
// Should be able to log 1000 messages quickly // Should be able to log 1000 messages quickly
#expect(duration < 1.0) // Within 1 second #expect(duration < 1.0) // Within 1 second
// Verify all messages were logged // Verify all messages were logged
let logs = logger.getDebugLogs() let logs = logger.getDebugLogs()
let performanceMessages = logs.filter { $0.contains("Performance test message") } let performanceMessages = logs.filter { $0.contains("Performance test message") }
#expect(performanceMessages.count >= messageCount) #expect(performanceMessages.count >= messageCount)
} }
@Test("Debug log retrieval performance", .tags(.performance)) @Test("Debug log retrieval performance", .tags(.performance))
func debugLogRetrievalPerformance() { func debugLogRetrievalPerformance() {
let logger = Logger.shared let logger = Logger.shared
// Add many messages first // Add many messages first
for i in 1...100 { for index in 1...100 {
logger.debug("Retrieval test \(i)") logger.debug("Retrieval test \(index)")
} }
// Measure retrieval performance // Measure retrieval performance
let startTime = CFAbsoluteTimeGetCurrent() let startTime = CFAbsoluteTimeGetCurrent()
for _ in 1...10 { for _ in 1...10 {
let logs = logger.getDebugLogs() let logs = logger.getDebugLogs()
#expect(logs.count > 0) #expect(!logs.isEmpty)
} }
let duration = CFAbsoluteTimeGetCurrent() - startTime let duration = CFAbsoluteTimeGetCurrent() - startTime
// Should be able to retrieve logs quickly even with many messages // Should be able to retrieve logs quickly even with many messages
#expect(duration < 1.0) // Within 1 second for 10 retrievals #expect(duration < 1.0) // Within 1 second for 10 retrievals
} }
// MARK: - Edge Cases and Error Handling // MARK: - Edge Cases and Error Handling
@Test("Logging with special characters", .tags(.fast)) @Test("Logging with special characters", .tags(.fast))
func loggingWithSpecialCharacters() async { func loggingWithSpecialCharacters() async {
let logger = Logger.shared let logger = Logger.shared
// Enable JSON mode and clear logs // Enable JSON mode and clear logs
logger.setJsonOutputMode(true) logger.setJsonOutputMode(true)
logger.clearDebugLogs() logger.clearDebugLogs()
// Wait for setup // Wait for setup
try? await Task.sleep(nanoseconds: 10_000_000) // 10ms try? await Task.sleep(nanoseconds: 10_000_000) // 10ms
let initialCount = logger.getDebugLogs().count let initialCount = logger.getDebugLogs().count
// Test various special characters and unicode // Test various special characters and unicode
let specialMessages = [ let specialMessages = [
"Test with emoji: 🚀 🎉 ✅", "Test with emoji: 🚀 🎉 ✅",
@ -326,161 +323,161 @@ struct LoggerTests {
"Test with JSON: {\"key\": \"value\", \"number\": 42}", "Test with JSON: {\"key\": \"value\", \"number\": 42}",
"Test with special chars: @#$%^&*()_+-=[]{}|;':\",./<>?" "Test with special chars: @#$%^&*()_+-=[]{}|;':\",./<>?"
] ]
for message in specialMessages { for message in specialMessages {
logger.debug(message) logger.debug(message)
logger.info(message) logger.info(message)
logger.error(message) logger.error(message)
} }
// Wait for logging // Wait for logging
try? await Task.sleep(nanoseconds: 50_000_000) // 50ms try? await Task.sleep(nanoseconds: 50_000_000) // 50ms
let logs = logger.getDebugLogs() let logs = logger.getDebugLogs()
// Should have all messages // Should have all messages
#expect(logs.count >= initialCount + specialMessages.count * 3) #expect(logs.count >= initialCount + specialMessages.count * 3)
// Verify special characters are preserved // Verify special characters are preserved
let recentLogs = logs.suffix(specialMessages.count * 3) let recentLogs = logs.suffix(specialMessages.count * 3)
for message in specialMessages { for message in specialMessages {
#expect(recentLogs.contains { $0.contains(message) }) #expect(recentLogs.contains { $0.contains(message) })
} }
// Reset // Reset
logger.setJsonOutputMode(false) logger.setJsonOutputMode(false)
} }
@Test("Logging with very long messages", .tags(.fast)) @Test("Logging with very long messages", .tags(.fast))
func loggingWithVeryLongMessages() async { func loggingWithVeryLongMessages() async {
let logger = Logger.shared let logger = Logger.shared
// Enable JSON mode and clear logs for consistent testing // Enable JSON mode and clear logs for consistent testing
logger.setJsonOutputMode(true) logger.setJsonOutputMode(true)
logger.clearDebugLogs() logger.clearDebugLogs()
// Wait for setup // Wait for setup
try? await Task.sleep(nanoseconds: 10_000_000) // 10ms try? await Task.sleep(nanoseconds: 10_000_000) // 10ms
let initialCount = logger.getDebugLogs().count let initialCount = logger.getDebugLogs().count
// Test very long messages // Test very long messages
let longMessage = String(repeating: "A", count: 1000) let longMessage = String(repeating: "A", count: 1000)
let veryLongMessage = String(repeating: "B", count: 10000) let veryLongMessage = String(repeating: "B", count: 10000)
logger.debug(longMessage) logger.debug(longMessage)
logger.info(veryLongMessage) logger.info(veryLongMessage)
// Wait for logging // Wait for logging
try? await Task.sleep(nanoseconds: 10_000_000) // 10ms try? await Task.sleep(nanoseconds: 10_000_000) // 10ms
let logs = logger.getDebugLogs() let logs = logger.getDebugLogs()
// Should handle long messages without crashing // Should handle long messages without crashing
#expect(logs.count >= initialCount + 2) #expect(logs.count >= initialCount + 2)
// Verify long messages are stored (possibly truncated, but stored) // Verify long messages are stored (possibly truncated, but stored)
let recentLogs = logs.suffix(2) let recentLogs = logs.suffix(2)
#expect(recentLogs.contains { $0.contains("AAA") }) #expect(recentLogs.contains { $0.contains("AAA") })
#expect(recentLogs.contains { $0.contains("BBB") }) #expect(recentLogs.contains { $0.contains("BBB") })
// Reset // Reset
logger.setJsonOutputMode(false) logger.setJsonOutputMode(false)
} }
@Test("Logging with nil and empty strings", .tags(.fast)) @Test("Logging with nil and empty strings", .tags(.fast))
func loggingWithNilAndEmptyStrings() async { func loggingWithNilAndEmptyStrings() async {
let logger = Logger.shared let logger = Logger.shared
// Enable JSON mode and clear logs for consistent testing // Enable JSON mode and clear logs for consistent testing
logger.setJsonOutputMode(true) logger.setJsonOutputMode(true)
logger.clearDebugLogs() logger.clearDebugLogs()
// Wait for setup // Wait for setup
try? await Task.sleep(nanoseconds: 10_000_000) // 10ms try? await Task.sleep(nanoseconds: 10_000_000) // 10ms
let initialCount = logger.getDebugLogs().count let initialCount = logger.getDebugLogs().count
// Test empty messages // Test empty messages
logger.debug("") logger.debug("")
logger.info("") logger.info("")
logger.error("") logger.error("")
// Test whitespace-only messages // Test whitespace-only messages
logger.debug(" ") logger.debug(" ")
logger.info("\\t\\n\\r") logger.info("\\t\\n\\r")
// Wait for logging // Wait for logging
try? await Task.sleep(nanoseconds: 10_000_000) // 10ms try? await Task.sleep(nanoseconds: 10_000_000) // 10ms
let logs = logger.getDebugLogs() let logs = logger.getDebugLogs()
// Should handle empty/whitespace messages gracefully // Should handle empty/whitespace messages gracefully
#expect(logs.count >= initialCount + 5) #expect(logs.count >= initialCount + 5)
// Reset // Reset
logger.setJsonOutputMode(false) logger.setJsonOutputMode(false)
} }
// MARK: - Integration Tests // MARK: - Integration Tests
@Test("Logger integration with JSON output mode", .tags(.integration)) @Test("Logger integration with JSON output mode", .tags(.integration))
func loggerIntegrationWithJSONMode() async { func loggerIntegrationWithJSONMode() async {
let logger = Logger.shared let logger = Logger.shared
// Clear logs first // Clear logs first
logger.clearDebugLogs() logger.clearDebugLogs()
// Test logging in JSON mode only (since non-JSON mode goes to stderr) // Test logging in JSON mode only (since non-JSON mode goes to stderr)
logger.setJsonOutputMode(true) logger.setJsonOutputMode(true)
// Wait for mode setting // Wait for mode setting
try? await Task.sleep(nanoseconds: 10_000_000) // 10ms try? await Task.sleep(nanoseconds: 10_000_000) // 10ms
logger.debug("JSON mode message 1") logger.debug("JSON mode message 1")
logger.debug("JSON mode message 2") logger.debug("JSON mode message 2")
logger.debug("JSON mode message 3") logger.debug("JSON mode message 3")
// Wait for logging // Wait for logging
try? await Task.sleep(nanoseconds: 10_000_000) // 10ms try? await Task.sleep(nanoseconds: 10_000_000) // 10ms
let logs = logger.getDebugLogs() let logs = logger.getDebugLogs()
// Should have messages from JSON mode // Should have messages from JSON mode
#expect(logs.contains { $0.contains("JSON mode message 1") }) #expect(logs.contains { $0.contains("JSON mode message 1") })
#expect(logs.contains { $0.contains("JSON mode message 2") }) #expect(logs.contains { $0.contains("JSON mode message 2") })
#expect(logs.contains { $0.contains("JSON mode message 3") }) #expect(logs.contains { $0.contains("JSON mode message 3") })
// Reset // Reset
logger.setJsonOutputMode(false) logger.setJsonOutputMode(false)
} }
@Test("Logger state consistency", .tags(.fast)) @Test("Logger state consistency", .tags(.fast))
func loggerStateConsistency() async { func loggerStateConsistency() async {
let logger = Logger.shared let logger = Logger.shared
// Clear logs and set JSON mode // Clear logs and set JSON mode
logger.setJsonOutputMode(true) logger.setJsonOutputMode(true)
logger.clearDebugLogs() logger.clearDebugLogs()
// Wait for setup // Wait for setup
try? await Task.sleep(nanoseconds: 10_000_000) // 10ms try? await Task.sleep(nanoseconds: 10_000_000) // 10ms
// Test consistent JSON mode logging // Test consistent JSON mode logging
for i in 1...10 { for index in 1...10 {
logger.debug("State test \(i)") logger.debug("State test \(index)")
} }
// Wait for logging // Wait for logging
try? await Task.sleep(nanoseconds: 50_000_000) // 50ms try? await Task.sleep(nanoseconds: 50_000_000) // 50ms
let logs = logger.getDebugLogs() let logs = logger.getDebugLogs()
// Should maintain consistency // Should maintain consistency
let stateTestLogs = logs.filter { $0.contains("State test") } let stateTestLogs = logs.filter { $0.contains("State test") }
#expect(stateTestLogs.count >= 10) #expect(stateTestLogs.count >= 10)
// Reset // Reset
logger.setJsonOutputMode(false) logger.setJsonOutputMode(false)
} }
} }

View file

@ -1,65 +1,65 @@
import CoreGraphics
@testable import peekaboo @testable import peekaboo
import Testing import Testing
import CoreGraphics
@Suite("Models Tests", .tags(.models, .unit)) @Suite("Models Tests", .tags(.models, .unit))
struct ModelsTests { struct ModelsTests {
// MARK: - Enum Tests // MARK: - Enum Tests
@Test("CaptureMode enum values and parsing", .tags(.fast)) @Test("CaptureMode enum values and parsing", .tags(.fast))
func captureMode() { func captureMode() {
// Test CaptureMode enum values // Test CaptureMode enum values
#expect(CaptureMode.screen.rawValue == "screen") #expect(CaptureMode.screen.rawValue == "screen")
#expect(CaptureMode.window.rawValue == "window") #expect(CaptureMode.window.rawValue == "window")
#expect(CaptureMode.multi.rawValue == "multi") #expect(CaptureMode.multi.rawValue == "multi")
// Test CaptureMode from string // Test CaptureMode from string
#expect(CaptureMode(rawValue: "screen") == .screen) #expect(CaptureMode(rawValue: "screen") == .screen)
#expect(CaptureMode(rawValue: "window") == .window) #expect(CaptureMode(rawValue: "window") == .window)
#expect(CaptureMode(rawValue: "multi") == .multi) #expect(CaptureMode(rawValue: "multi") == .multi)
#expect(CaptureMode(rawValue: "invalid") == nil) #expect(CaptureMode(rawValue: "invalid") == nil)
} }
@Test("ImageFormat enum values and parsing", .tags(.fast)) @Test("ImageFormat enum values and parsing", .tags(.fast))
func imageFormat() { func imageFormat() {
// Test ImageFormat enum values // Test ImageFormat enum values
#expect(ImageFormat.png.rawValue == "png") #expect(ImageFormat.png.rawValue == "png")
#expect(ImageFormat.jpg.rawValue == "jpg") #expect(ImageFormat.jpg.rawValue == "jpg")
// Test ImageFormat from string // Test ImageFormat from string
#expect(ImageFormat(rawValue: "png") == .png) #expect(ImageFormat(rawValue: "png") == .png)
#expect(ImageFormat(rawValue: "jpg") == .jpg) #expect(ImageFormat(rawValue: "jpg") == .jpg)
#expect(ImageFormat(rawValue: "invalid") == nil) #expect(ImageFormat(rawValue: "invalid") == nil)
} }
@Test("CaptureFocus enum values and parsing", .tags(.fast)) @Test("CaptureFocus enum values and parsing", .tags(.fast))
func captureFocus() { func captureFocus() {
// Test CaptureFocus enum values // Test CaptureFocus enum values
#expect(CaptureFocus.background.rawValue == "background") #expect(CaptureFocus.background.rawValue == "background")
#expect(CaptureFocus.foreground.rawValue == "foreground") #expect(CaptureFocus.foreground.rawValue == "foreground")
// Test CaptureFocus from string // Test CaptureFocus from string
#expect(CaptureFocus(rawValue: "background") == .background) #expect(CaptureFocus(rawValue: "background") == .background)
#expect(CaptureFocus(rawValue: "foreground") == .foreground) #expect(CaptureFocus(rawValue: "foreground") == .foreground)
#expect(CaptureFocus(rawValue: "invalid") == nil) #expect(CaptureFocus(rawValue: "invalid") == nil)
} }
@Test("WindowDetailOption enum values and parsing", .tags(.fast)) @Test("WindowDetailOption enum values and parsing", .tags(.fast))
func windowDetailOption() { func windowDetailOption() {
// Test WindowDetailOption enum values // Test WindowDetailOption enum values
#expect(WindowDetailOption.off_screen.rawValue == "off_screen") #expect(WindowDetailOption.off_screen.rawValue == "off_screen")
#expect(WindowDetailOption.bounds.rawValue == "bounds") #expect(WindowDetailOption.bounds.rawValue == "bounds")
#expect(WindowDetailOption.ids.rawValue == "ids") #expect(WindowDetailOption.ids.rawValue == "ids")
// Test WindowDetailOption from string // Test WindowDetailOption from string
#expect(WindowDetailOption(rawValue: "off_screen") == .off_screen) #expect(WindowDetailOption(rawValue: "off_screen") == .off_screen)
#expect(WindowDetailOption(rawValue: "bounds") == .bounds) #expect(WindowDetailOption(rawValue: "bounds") == .bounds)
#expect(WindowDetailOption(rawValue: "ids") == .ids) #expect(WindowDetailOption(rawValue: "ids") == .ids)
#expect(WindowDetailOption(rawValue: "invalid") == nil) #expect(WindowDetailOption(rawValue: "invalid") == nil)
} }
// MARK: - Parameterized Enum Tests // MARK: - Parameterized Enum Tests
@Test("CaptureMode raw values are valid", .tags(.fast)) @Test("CaptureMode raw values are valid", .tags(.fast))
func captureModeRawValuesValid() { func captureModeRawValuesValid() {
let validValues = ["screen", "window", "multi"] let validValues = ["screen", "window", "multi"]
@ -67,7 +67,7 @@ struct ModelsTests {
#expect(CaptureMode(rawValue: rawValue) != nil) #expect(CaptureMode(rawValue: rawValue) != nil)
} }
} }
@Test("ImageFormat raw values are valid", .tags(.fast)) @Test("ImageFormat raw values are valid", .tags(.fast))
func imageFormatRawValuesValid() { func imageFormatRawValuesValid() {
let validValues = ["png", "jpg"] let validValues = ["png", "jpg"]
@ -75,7 +75,7 @@ struct ModelsTests {
#expect(ImageFormat(rawValue: rawValue) != nil) #expect(ImageFormat(rawValue: rawValue) != nil)
} }
} }
@Test("CaptureFocus raw values are valid", .tags(.fast)) @Test("CaptureFocus raw values are valid", .tags(.fast))
func captureFocusRawValuesValid() { func captureFocusRawValuesValid() {
let validValues = ["background", "foreground"] let validValues = ["background", "foreground"]
@ -83,19 +83,19 @@ struct ModelsTests {
#expect(CaptureFocus(rawValue: rawValue) != nil) #expect(CaptureFocus(rawValue: rawValue) != nil)
} }
} }
// MARK: - Model Structure Tests // MARK: - Model Structure Tests
@Test("WindowBounds initialization and properties", .tags(.fast)) @Test("WindowBounds initialization and properties", .tags(.fast))
func windowBounds() { func windowBounds() {
let bounds = WindowBounds(xCoordinate: 100, yCoordinate: 200, width: 1200, height: 800) let bounds = WindowBounds(xCoordinate: 100, yCoordinate: 200, width: 1200, height: 800)
#expect(bounds.xCoordinate == 100) #expect(bounds.xCoordinate == 100)
#expect(bounds.yCoordinate == 200) #expect(bounds.yCoordinate == 200)
#expect(bounds.width == 1200) #expect(bounds.width == 1200)
#expect(bounds.height == 800) #expect(bounds.height == 800)
} }
@Test("SavedFile with all properties", .tags(.fast)) @Test("SavedFile with all properties", .tags(.fast))
func savedFile() { func savedFile() {
let savedFile = SavedFile( let savedFile = SavedFile(
@ -106,7 +106,7 @@ struct ModelsTests {
window_index: 0, window_index: 0,
mime_type: "image/png" mime_type: "image/png"
) )
#expect(savedFile.path == "/tmp/test.png") #expect(savedFile.path == "/tmp/test.png")
#expect(savedFile.item_label == "Screen 1") #expect(savedFile.item_label == "Screen 1")
#expect(savedFile.window_title == "Safari - Main Window") #expect(savedFile.window_title == "Safari - Main Window")
@ -114,7 +114,7 @@ struct ModelsTests {
#expect(savedFile.window_index == 0) #expect(savedFile.window_index == 0)
#expect(savedFile.mime_type == "image/png") #expect(savedFile.mime_type == "image/png")
} }
@Test("SavedFile with nil optional values", .tags(.fast)) @Test("SavedFile with nil optional values", .tags(.fast))
func savedFileWithNilValues() { func savedFileWithNilValues() {
let savedFile = SavedFile( let savedFile = SavedFile(
@ -125,7 +125,7 @@ struct ModelsTests {
window_index: nil, window_index: nil,
mime_type: "image/png" mime_type: "image/png"
) )
#expect(savedFile.path == "/tmp/screen.png") #expect(savedFile.path == "/tmp/screen.png")
#expect(savedFile.item_label == nil) #expect(savedFile.item_label == nil)
#expect(savedFile.window_title == nil) #expect(savedFile.window_title == nil)
@ -133,7 +133,7 @@ struct ModelsTests {
#expect(savedFile.window_index == nil) #expect(savedFile.window_index == nil)
#expect(savedFile.mime_type == "image/png") #expect(savedFile.mime_type == "image/png")
} }
@Test("ApplicationInfo initialization", .tags(.fast)) @Test("ApplicationInfo initialization", .tags(.fast))
func applicationInfo() { func applicationInfo() {
let appInfo = ApplicationInfo( let appInfo = ApplicationInfo(
@ -143,14 +143,14 @@ struct ModelsTests {
is_active: true, is_active: true,
window_count: 2 window_count: 2
) )
#expect(appInfo.app_name == "Safari") #expect(appInfo.app_name == "Safari")
#expect(appInfo.bundle_id == "com.apple.Safari") #expect(appInfo.bundle_id == "com.apple.Safari")
#expect(appInfo.pid == 1234) #expect(appInfo.pid == 1234)
#expect(appInfo.is_active == true) #expect(appInfo.is_active == true)
#expect(appInfo.window_count == 2) #expect(appInfo.window_count == 2)
} }
@Test("WindowInfo with bounds", .tags(.fast)) @Test("WindowInfo with bounds", .tags(.fast))
func windowInfo() { func windowInfo() {
let bounds = WindowBounds(xCoordinate: 100, yCoordinate: 100, width: 1200, height: 800) let bounds = WindowBounds(xCoordinate: 100, yCoordinate: 100, width: 1200, height: 800)
@ -161,7 +161,7 @@ struct ModelsTests {
bounds: bounds, bounds: bounds,
is_on_screen: true is_on_screen: true
) )
#expect(windowInfo.window_title == "Safari - Main Window") #expect(windowInfo.window_title == "Safari - Main Window")
#expect(windowInfo.window_id == 12345) #expect(windowInfo.window_id == 12345)
#expect(windowInfo.window_index == 0) #expect(windowInfo.window_index == 0)
@ -172,7 +172,7 @@ struct ModelsTests {
#expect(windowInfo.bounds?.height == 800) #expect(windowInfo.bounds?.height == 800)
#expect(windowInfo.is_on_screen == true) #expect(windowInfo.is_on_screen == true)
} }
@Test("TargetApplicationInfo", .tags(.fast)) @Test("TargetApplicationInfo", .tags(.fast))
func targetApplicationInfo() { func targetApplicationInfo() {
let targetApp = TargetApplicationInfo( let targetApp = TargetApplicationInfo(
@ -180,14 +180,14 @@ struct ModelsTests {
bundle_id: "com.apple.Safari", bundle_id: "com.apple.Safari",
pid: 1234 pid: 1234
) )
#expect(targetApp.app_name == "Safari") #expect(targetApp.app_name == "Safari")
#expect(targetApp.bundle_id == "com.apple.Safari") #expect(targetApp.bundle_id == "com.apple.Safari")
#expect(targetApp.pid == 1234) #expect(targetApp.pid == 1234)
} }
// MARK: - Collection Data Tests // MARK: - Collection Data Tests
@Test("ApplicationListData contains applications", .tags(.fast)) @Test("ApplicationListData contains applications", .tags(.fast))
func applicationListData() { func applicationListData() {
let app1 = ApplicationInfo( let app1 = ApplicationInfo(
@ -197,7 +197,7 @@ struct ModelsTests {
is_active: true, is_active: true,
window_count: 2 window_count: 2
) )
let app2 = ApplicationInfo( let app2 = ApplicationInfo(
app_name: "Terminal", app_name: "Terminal",
bundle_id: "com.apple.Terminal", bundle_id: "com.apple.Terminal",
@ -205,14 +205,14 @@ struct ModelsTests {
is_active: false, is_active: false,
window_count: 1 window_count: 1
) )
let appListData = ApplicationListData(applications: [app1, app2]) let appListData = ApplicationListData(applications: [app1, app2])
#expect(appListData.applications.count == 2) #expect(appListData.applications.count == 2)
#expect(appListData.applications[0].app_name == "Safari") #expect(appListData.applications[0].app_name == "Safari")
#expect(appListData.applications[1].app_name == "Terminal") #expect(appListData.applications[1].app_name == "Terminal")
} }
@Test("WindowListData with target application", .tags(.fast)) @Test("WindowListData with target application", .tags(.fast))
func windowListData() { func windowListData() {
let bounds = WindowBounds(xCoordinate: 100, yCoordinate: 100, width: 1200, height: 800) let bounds = WindowBounds(xCoordinate: 100, yCoordinate: 100, width: 1200, height: 800)
@ -223,25 +223,25 @@ struct ModelsTests {
bounds: bounds, bounds: bounds,
is_on_screen: true is_on_screen: true
) )
let targetApp = TargetApplicationInfo( let targetApp = TargetApplicationInfo(
app_name: "Safari", app_name: "Safari",
bundle_id: "com.apple.Safari", bundle_id: "com.apple.Safari",
pid: 1234 pid: 1234
) )
let windowListData = WindowListData( let windowListData = WindowListData(
windows: [window], windows: [window],
target_application_info: targetApp target_application_info: targetApp
) )
#expect(windowListData.windows.count == 1) #expect(windowListData.windows.count == 1)
#expect(windowListData.windows[0].window_title == "Safari - Main Window") #expect(windowListData.windows[0].window_title == "Safari - Main Window")
#expect(windowListData.target_application_info.app_name == "Safari") #expect(windowListData.target_application_info.app_name == "Safari")
#expect(windowListData.target_application_info.bundle_id == "com.apple.Safari") #expect(windowListData.target_application_info.bundle_id == "com.apple.Safari")
#expect(windowListData.target_application_info.pid == 1234) #expect(windowListData.target_application_info.pid == 1234)
} }
@Test("ImageCaptureData with saved files", .tags(.fast)) @Test("ImageCaptureData with saved files", .tags(.fast))
func imageCaptureData() { func imageCaptureData() {
let savedFile = SavedFile( let savedFile = SavedFile(
@ -252,30 +252,36 @@ struct ModelsTests {
window_index: nil, window_index: nil,
mime_type: "image/png" mime_type: "image/png"
) )
let imageData = ImageCaptureData(saved_files: [savedFile]) let imageData = ImageCaptureData(saved_files: [savedFile])
#expect(imageData.saved_files.count == 1) #expect(imageData.saved_files.count == 1)
#expect(imageData.saved_files[0].path == "/tmp/test.png") #expect(imageData.saved_files[0].path == "/tmp/test.png")
#expect(imageData.saved_files[0].item_label == "Screen 1") #expect(imageData.saved_files[0].item_label == "Screen 1")
#expect(imageData.saved_files[0].mime_type == "image/png") #expect(imageData.saved_files[0].mime_type == "image/png")
} }
// MARK: - Error Tests // MARK: - Error Tests
@Test("CaptureError descriptions are user-friendly", .tags(.fast)) @Test("CaptureError descriptions are user-friendly", .tags(.fast))
func captureErrorDescriptions() { func captureErrorDescriptions() {
#expect(CaptureError.noDisplaysAvailable.errorDescription == "No displays available for capture.") #expect(CaptureError.noDisplaysAvailable.errorDescription == "No displays available for capture.")
#expect(CaptureError.screenRecordingPermissionDenied.errorDescription!.contains("Screen recording permission is required")) #expect(CaptureError.screenRecordingPermissionDenied.errorDescription!
.contains("Screen recording permission is required")
)
#expect(CaptureError.invalidDisplayID.errorDescription == "Invalid display ID provided.") #expect(CaptureError.invalidDisplayID.errorDescription == "Invalid display ID provided.")
#expect(CaptureError.captureCreationFailed.errorDescription == "Failed to create the screen capture.") #expect(CaptureError.captureCreationFailed.errorDescription == "Failed to create the screen capture.")
#expect(CaptureError.windowNotFound.errorDescription == "The specified window could not be found.") #expect(CaptureError.windowNotFound.errorDescription == "The specified window could not be found.")
#expect(CaptureError.windowCaptureFailed.errorDescription == "Failed to capture the specified window.") #expect(CaptureError.windowCaptureFailed.errorDescription == "Failed to capture the specified window.")
#expect(CaptureError.fileWriteError("/tmp/test.png").errorDescription == "Failed to write capture file to path: /tmp/test.png.") #expect(CaptureError.fileWriteError("/tmp/test.png")
#expect(CaptureError.appNotFound("Safari").errorDescription == "Application with identifier 'Safari' not found or is not running.") .errorDescription == "Failed to write capture file to path: /tmp/test.png."
)
#expect(CaptureError.appNotFound("Safari")
.errorDescription == "Application with identifier 'Safari' not found or is not running."
)
#expect(CaptureError.invalidWindowIndex(5).errorDescription == "Invalid window index: 5.") #expect(CaptureError.invalidWindowIndex(5).errorDescription == "Invalid window index: 5.")
} }
@Test("CaptureError exit codes", .tags(.fast)) @Test("CaptureError exit codes", .tags(.fast))
func captureErrorExitCodes() { func captureErrorExitCodes() {
let testCases: [(CaptureError, Int32)] = [ let testCases: [(CaptureError, Int32)] = [
@ -292,14 +298,14 @@ struct ModelsTests {
(.invalidArgument("test"), 20), (.invalidArgument("test"), 20),
(.unknownError("test"), 1) (.unknownError("test"), 1)
] ]
for (error, expectedCode) in testCases { for (error, expectedCode) in testCases {
#expect(error.exitCode == expectedCode) #expect(error.exitCode == expectedCode)
} }
} }
// MARK: - WindowData Tests // MARK: - WindowData Tests
@Test("WindowData initialization from CGRect", .tags(.fast)) @Test("WindowData initialization from CGRect", .tags(.fast))
func windowData() { func windowData() {
let bounds = CGRect(x: 100, y: 200, width: 1200, height: 800) let bounds = CGRect(x: 100, y: 200, width: 1200, height: 800)
@ -310,7 +316,7 @@ struct ModelsTests {
isOnScreen: true, isOnScreen: true,
windowIndex: 0 windowIndex: 0
) )
#expect(windowData.windowId == 12345) #expect(windowData.windowId == 12345)
#expect(windowData.title == "Safari - Main Window") #expect(windowData.title == "Safari - Main Window")
#expect(windowData.bounds.origin.x == 100) #expect(windowData.bounds.origin.x == 100)
@ -320,19 +326,19 @@ struct ModelsTests {
#expect(windowData.isOnScreen == true) #expect(windowData.isOnScreen == true)
#expect(windowData.windowIndex == 0) #expect(windowData.windowIndex == 0)
} }
@Test("WindowSpecifier variants", .tags(.fast)) @Test("WindowSpecifier variants", .tags(.fast))
func windowSpecifier() { func windowSpecifier() {
let titleSpecifier = WindowSpecifier.title("Main Window") let titleSpecifier = WindowSpecifier.title("Main Window")
let indexSpecifier = WindowSpecifier.index(0) let indexSpecifier = WindowSpecifier.index(0)
switch titleSpecifier { switch titleSpecifier {
case let .title(title): case let .title(title):
#expect(title == "Main Window") #expect(title == "Main Window")
case .index: case .index:
Issue.record("Expected title specifier") Issue.record("Expected title specifier")
} }
switch indexSpecifier { switch indexSpecifier {
case .title: case .title:
Issue.record("Expected index specifier") Issue.record("Expected index specifier")
@ -346,21 +352,22 @@ struct ModelsTests {
@Suite("Model Edge Cases", .tags(.models, .unit)) @Suite("Model Edge Cases", .tags(.models, .unit))
struct ModelEdgeCaseTests { struct ModelEdgeCaseTests {
@Test(
@Test("WindowBounds with edge values", "WindowBounds with edge values",
arguments: [ arguments: [
(x: 0, y: 0, width: 0, height: 0), (x: 0, y: 0, width: 0, height: 0),
(x: -100, y: -100, width: 100, height: 100), (x: -100, y: -100, width: 100, height: 100),
(x: Int.max, y: Int.max, width: 1, height: 1) (x: Int.max, y: Int.max, width: 1, height: 1)
]) ]
func windowBoundsEdgeCases(x: Int, y: Int, width: Int, height: Int) { )
let bounds = WindowBounds(xCoordinate: x, yCoordinate: y, width: width, height: height) func windowBoundsEdgeCases(x xCoordinate: Int, y yCoordinate: Int, width: Int, height: Int) {
#expect(bounds.xCoordinate == x) let bounds = WindowBounds(xCoordinate: xCoordinate, yCoordinate: yCoordinate, width: width, height: height)
#expect(bounds.yCoordinate == y) #expect(bounds.xCoordinate == xCoordinate)
#expect(bounds.yCoordinate == yCoordinate)
#expect(bounds.width == width) #expect(bounds.width == width)
#expect(bounds.height == height) #expect(bounds.height == height)
} }
@Test("ApplicationInfo with extreme values", .tags(.fast)) @Test("ApplicationInfo with extreme values", .tags(.fast))
func applicationInfoExtremeValues() { func applicationInfoExtremeValues() {
let appInfo = ApplicationInfo( let appInfo = ApplicationInfo(
@ -370,22 +377,24 @@ struct ModelEdgeCaseTests {
is_active: true, is_active: true,
window_count: Int.max window_count: Int.max
) )
#expect(appInfo.app_name.count == 1000) #expect(appInfo.app_name.count == 1000)
#expect(appInfo.bundle_id.contains("com.test.")) #expect(appInfo.bundle_id.contains("com.test."))
#expect(appInfo.pid == Int32.max) #expect(appInfo.pid == Int32.max)
#expect(appInfo.window_count == Int.max) #expect(appInfo.window_count == Int.max)
} }
@Test("SavedFile path validation", @Test(
arguments: [ "SavedFile path validation",
"/tmp/test.png", arguments: [
"/Users/test/Desktop/screenshot.jpg", "/tmp/test.png",
"~/Documents/capture.png", "/Users/test/Desktop/screenshot.jpg",
"./relative/path/image.png", "~/Documents/capture.png",
"/path with spaces/image.png", "./relative/path/image.png",
"/path/with/特殊文字.png" "/path with spaces/image.png",
]) "/path/with/特殊文字.png"
]
)
func savedFilePathValidation(path: String) { func savedFilePathValidation(path: String) {
let savedFile = SavedFile( let savedFile = SavedFile(
path: path, path: path,
@ -395,13 +404,15 @@ struct ModelEdgeCaseTests {
window_index: nil, window_index: nil,
mime_type: "image/png" mime_type: "image/png"
) )
#expect(savedFile.path == path) #expect(savedFile.path == path)
#expect(!savedFile.path.isEmpty) #expect(!savedFile.path.isEmpty)
} }
@Test("MIME type validation", @Test(
arguments: ["image/png", "image/jpeg", "image/jpg"]) "MIME type validation",
arguments: ["image/png", "image/jpeg", "image/jpg"]
)
func mimeTypeValidation(mimeType: String) { func mimeTypeValidation(mimeType: String) {
let savedFile = SavedFile( let savedFile = SavedFile(
path: "/tmp/test", path: "/tmp/test",
@ -411,8 +422,8 @@ struct ModelEdgeCaseTests {
window_index: nil, window_index: nil,
mime_type: mimeType mime_type: mimeType
) )
#expect(savedFile.mime_type == mimeType) #expect(savedFile.mime_type == mimeType)
#expect(savedFile.mime_type.starts(with: "image/")) #expect(savedFile.mime_type.starts(with: "image/"))
} }
} }

View file

@ -1,78 +1,78 @@
import AppKit
@testable import peekaboo @testable import peekaboo
import Testing import Testing
import AppKit
@Suite("PermissionsChecker Tests", .tags(.permissions, .unit)) @Suite("PermissionsChecker Tests", .tags(.permissions, .unit))
struct PermissionsCheckerTests { struct PermissionsCheckerTests {
// MARK: - Screen Recording Permission Tests // MARK: - Screen Recording Permission Tests
@Test("Screen recording permission check returns boolean", .tags(.fast)) @Test("Screen recording permission check returns boolean", .tags(.fast))
func checkScreenRecordingPermission() { func checkScreenRecordingPermission() {
// Test screen recording permission check // Test screen recording permission check
let hasPermission = PermissionsChecker.checkScreenRecordingPermission() let hasPermission = PermissionsChecker.checkScreenRecordingPermission()
// This test will pass or fail based on actual system permissions // This test will pass or fail based on actual system permissions
// The result should be a valid boolean // The result should be a valid boolean
#expect(hasPermission == true || hasPermission == false) #expect(hasPermission == true || hasPermission == false)
} }
@Test("Screen recording permission check is consistent", .tags(.fast)) @Test("Screen recording permission check is consistent", .tags(.fast))
func screenRecordingPermissionConsistency() { func screenRecordingPermissionConsistency() {
// Test that multiple calls return consistent results // Test that multiple calls return consistent results
let firstCheck = PermissionsChecker.checkScreenRecordingPermission() let firstCheck = PermissionsChecker.checkScreenRecordingPermission()
let secondCheck = PermissionsChecker.checkScreenRecordingPermission() let secondCheck = PermissionsChecker.checkScreenRecordingPermission()
#expect(firstCheck == secondCheck) #expect(firstCheck == secondCheck)
} }
@Test("Screen recording permission check performance", arguments: 1...5) @Test("Screen recording permission check performance", arguments: 1...5)
func screenRecordingPermissionPerformance(iteration: Int) { func screenRecordingPermissionPerformance(iteration: Int) {
// Permission checks should be fast // Permission checks should be fast
let hasPermission = PermissionsChecker.checkScreenRecordingPermission() let hasPermission = PermissionsChecker.checkScreenRecordingPermission()
#expect(hasPermission == true || hasPermission == false) #expect(hasPermission == true || hasPermission == false)
} }
// MARK: - Accessibility Permission Tests // MARK: - Accessibility Permission Tests
@Test("Accessibility permission check returns boolean", .tags(.fast)) @Test("Accessibility permission check returns boolean", .tags(.fast))
func checkAccessibilityPermission() { func checkAccessibilityPermission() {
// Test accessibility permission check // Test accessibility permission check
let hasPermission = PermissionsChecker.checkAccessibilityPermission() let hasPermission = PermissionsChecker.checkAccessibilityPermission()
// This will return the actual system state // This will return the actual system state
#expect(hasPermission == true || hasPermission == false) #expect(hasPermission == true || hasPermission == false)
} }
@Test("Accessibility permission matches AXIsProcessTrusted", .tags(.fast)) @Test("Accessibility permission matches AXIsProcessTrusted", .tags(.fast))
func accessibilityPermissionWithTrustedCheck() { func accessibilityPermissionWithTrustedCheck() {
// Test the AXIsProcessTrusted check // Test the AXIsProcessTrusted check
let options = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: false] let options = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: false]
let isTrusted = AXIsProcessTrustedWithOptions(options as CFDictionary) let isTrusted = AXIsProcessTrustedWithOptions(options as CFDictionary)
let hasPermission = PermissionsChecker.checkAccessibilityPermission() let hasPermission = PermissionsChecker.checkAccessibilityPermission()
// These should match // These should match
#expect(isTrusted == hasPermission) #expect(isTrusted == hasPermission)
} }
// MARK: - Combined Permission Tests // MARK: - Combined Permission Tests
@Test("Both permissions can be checked independently", .tags(.fast)) @Test("Both permissions can be checked independently", .tags(.fast))
func bothPermissions() { func bothPermissions() {
// Test both permission checks // Test both permission checks
let screenRecording = PermissionsChecker.checkScreenRecordingPermission() let screenRecording = PermissionsChecker.checkScreenRecordingPermission()
let accessibility = PermissionsChecker.checkAccessibilityPermission() let accessibility = PermissionsChecker.checkAccessibilityPermission()
// Both should return valid boolean values // Both should return valid boolean values
#expect(screenRecording == true || screenRecording == false) #expect(screenRecording == true || screenRecording == false)
#expect(accessibility == true || accessibility == false) #expect(accessibility == true || accessibility == false)
} }
// MARK: - Require Permission Tests // MARK: - Require Permission Tests
@Test("Require screen recording permission throws when denied", .tags(.fast)) @Test("Require screen recording permission throws when denied", .tags(.fast))
func requireScreenRecordingPermission() { func requireScreenRecordingPermission() {
let hasPermission = PermissionsChecker.checkScreenRecordingPermission() let hasPermission = PermissionsChecker.checkScreenRecordingPermission()
if hasPermission { if hasPermission {
// Should not throw when permission is granted // Should not throw when permission is granted
#expect(throws: Never.self) { #expect(throws: Never.self) {
@ -85,11 +85,11 @@ struct PermissionsCheckerTests {
} }
} }
} }
@Test("Require accessibility permission throws when denied", .tags(.fast)) @Test("Require accessibility permission throws when denied", .tags(.fast))
func requireAccessibilityPermission() { func requireAccessibilityPermission() {
let hasPermission = PermissionsChecker.checkAccessibilityPermission() let hasPermission = PermissionsChecker.checkAccessibilityPermission()
if hasPermission { if hasPermission {
// Should not throw when permission is granted // Should not throw when permission is granted
#expect(throws: Never.self) { #expect(throws: Never.self) {
@ -102,26 +102,26 @@ struct PermissionsCheckerTests {
} }
} }
} }
// MARK: - Error Message Tests // MARK: - Error Message Tests
@Test("Permission errors have descriptive messages", .tags(.fast)) @Test("Permission errors have descriptive messages", .tags(.fast))
func permissionErrorMessages() { func permissionErrorMessages() {
let screenError = CaptureError.screenRecordingPermissionDenied let screenError = CaptureError.screenRecordingPermissionDenied
let accessError = CaptureError.accessibilityPermissionDenied let accessError = CaptureError.accessibilityPermissionDenied
// CaptureError conforms to LocalizedError, so it has errorDescription // CaptureError conforms to LocalizedError, so it has errorDescription
#expect(screenError.errorDescription != nil) #expect(screenError.errorDescription != nil)
#expect(accessError.errorDescription != nil) #expect(accessError.errorDescription != nil)
#expect(screenError.errorDescription!.contains("Screen recording permission")) #expect(screenError.errorDescription!.contains("Screen recording permission"))
#expect(accessError.errorDescription!.contains("Accessibility permission")) #expect(accessError.errorDescription!.contains("Accessibility permission"))
} }
@Test("Permission errors have correct exit codes", .tags(.fast)) @Test("Permission errors have correct exit codes", .tags(.fast))
func permissionErrorExitCodes() { func permissionErrorExitCodes() {
let screenError = CaptureError.screenRecordingPermissionDenied let screenError = CaptureError.screenRecordingPermissionDenied
let accessError = CaptureError.accessibilityPermissionDenied let accessError = CaptureError.accessibilityPermissionDenied
#expect(screenError.exitCode == 11) #expect(screenError.exitCode == 11)
#expect(accessError.exitCode == 12) #expect(accessError.exitCode == 12)
} }
@ -131,7 +131,6 @@ struct PermissionsCheckerTests {
@Suite("Permission Edge Cases", .tags(.permissions, .unit)) @Suite("Permission Edge Cases", .tags(.permissions, .unit))
struct PermissionEdgeCaseTests { struct PermissionEdgeCaseTests {
@Test("Permission checks are thread-safe", .tags(.integration)) @Test("Permission checks are thread-safe", .tags(.integration))
func threadSafePermissionChecks() async { func threadSafePermissionChecks() async {
// Test concurrent permission checks // Test concurrent permission checks
@ -144,12 +143,12 @@ struct PermissionEdgeCaseTests {
PermissionsChecker.checkAccessibilityPermission() PermissionsChecker.checkAccessibilityPermission()
} }
} }
var results: [Bool] = [] var results: [Bool] = []
for await result in group { for await result in group {
results.append(result) results.append(result)
} }
// All results should be valid booleans // All results should be valid booleans
#expect(results.count == 20) #expect(results.count == 20)
for result in results { for result in results {
@ -157,7 +156,7 @@ struct PermissionEdgeCaseTests {
} }
} }
} }
@Test("ScreenCaptureKit availability check", .tags(.fast)) @Test("ScreenCaptureKit availability check", .tags(.fast))
func screenCaptureKitAvailable() { func screenCaptureKitAvailable() {
// Verify that we can at least access ScreenCaptureKit APIs // Verify that we can at least access ScreenCaptureKit APIs
@ -165,25 +164,25 @@ struct PermissionEdgeCaseTests {
let isAvailable = NSClassFromString("SCShareableContent") != nil let isAvailable = NSClassFromString("SCShareableContent") != nil
#expect(isAvailable == true) #expect(isAvailable == true)
} }
@Test("Permission state changes are detected", .tags(.integration)) @Test("Permission state changes are detected", .tags(.integration))
func permissionStateChanges() { func permissionStateChanges() {
// This test verifies that permission checks reflect current state // This test verifies that permission checks reflect current state
// Note: This test cannot actually change permissions, but verifies // Note: This test cannot actually change permissions, but verifies
// that repeated checks could detect changes if they occurred // that repeated checks could detect changes if they occurred
let initialScreen = PermissionsChecker.checkScreenRecordingPermission() let initialScreen = PermissionsChecker.checkScreenRecordingPermission()
let initialAccess = PermissionsChecker.checkAccessibilityPermission() let initialAccess = PermissionsChecker.checkAccessibilityPermission()
// Sleep briefly to allow for potential state changes // Sleep briefly to allow for potential state changes
Thread.sleep(forTimeInterval: 0.1) Thread.sleep(forTimeInterval: 0.1)
let finalScreen = PermissionsChecker.checkScreenRecordingPermission() let finalScreen = PermissionsChecker.checkScreenRecordingPermission()
let finalAccess = PermissionsChecker.checkAccessibilityPermission() let finalAccess = PermissionsChecker.checkAccessibilityPermission()
// In normal operation, these should be the same // In normal operation, these should be the same
// but the important thing is they reflect current state // but the important thing is they reflect current state
#expect(initialScreen == finalScreen) #expect(initialScreen == finalScreen)
#expect(initialAccess == finalAccess) #expect(initialAccess == finalAccess)
} }
} }

View file

@ -14,4 +14,4 @@ extension Tag {
@Tag static var performance: Self @Tag static var performance: Self
@Tag static var concurrency: Self @Tag static var concurrency: Self
@Tag static var memory: Self @Tag static var memory: Self
} }

View file

@ -5,19 +5,19 @@ import Testing
@Suite("WindowManager Tests", .tags(.windowManager, .unit)) @Suite("WindowManager Tests", .tags(.windowManager, .unit))
struct WindowManagerTests { struct WindowManagerTests {
// MARK: - Get Windows For App Tests // MARK: - Get Windows For App Tests
@Test("Getting windows for Finder app", .tags(.integration)) @Test("Getting windows for Finder app", .tags(.integration))
func getWindowsForFinderApp() throws { func getWindowsForFinderApp() throws {
// Get Finder's PID // Get Finder's PID
let apps = NSWorkspace.shared.runningApplications let apps = NSWorkspace.shared.runningApplications
let finder = try #require(apps.first(where: { $0.bundleIdentifier == "com.apple.finder" })) let finder = try #require(apps.first { $0.bundleIdentifier == "com.apple.finder" })
// Test getting windows for Finder // Test getting windows for Finder
let windows = try WindowManager.getWindowsForApp(pid: finder.processIdentifier) let windows = try WindowManager.getWindowsForApp(pid: finder.processIdentifier)
// Finder usually has at least one window // Finder usually has at least one window
#expect(windows.count >= 0) // Windows count is always non-negative
// If there are windows, verify they're sorted by index // If there are windows, verify they're sorted by index
if windows.count > 1 { if windows.count > 1 {
for index in 1..<windows.count { for index in 1..<windows.count {
@ -25,45 +25,45 @@ struct WindowManagerTests {
} }
} }
} }
@Test("Getting windows for non-existent app returns empty array", .tags(.fast)) @Test("Getting windows for non-existent app returns empty array", .tags(.fast))
func getWindowsForNonExistentApp() throws { func getWindowsForNonExistentApp() throws {
// Test with non-existent PID // Test with non-existent PID
let windows = try WindowManager.getWindowsForApp(pid: 99999) let windows = try WindowManager.getWindowsForApp(pid: 99999)
// Should return empty array, not throw // Should return empty array, not throw
#expect(windows.count == 0) #expect(windows.isEmpty)
} }
@Test("Off-screen window filtering works correctly", .tags(.integration)) @Test("Off-screen window filtering works correctly", .tags(.integration))
func getWindowsWithOffScreenOption() throws { func getWindowsWithOffScreenOption() throws {
// Get Finder's PID for testing // Get Finder's PID for testing
let apps = NSWorkspace.shared.runningApplications let apps = NSWorkspace.shared.runningApplications
let finder = try #require(apps.first(where: { $0.bundleIdentifier == "com.apple.finder" })) let finder = try #require(apps.first { $0.bundleIdentifier == "com.apple.finder" })
// Test with includeOffScreen = true // Test with includeOffScreen = true
let allWindows = try WindowManager.getWindowsForApp(pid: finder.processIdentifier, includeOffScreen: true) let allWindows = try WindowManager.getWindowsForApp(pid: finder.processIdentifier, includeOffScreen: true)
// Test with includeOffScreen = false (default) // Test with includeOffScreen = false (default)
let onScreenWindows = try WindowManager.getWindowsForApp(pid: finder.processIdentifier, includeOffScreen: false) let onScreenWindows = try WindowManager.getWindowsForApp(pid: finder.processIdentifier, includeOffScreen: false)
// All windows should include off-screen ones, so count should be >= on-screen only // All windows should include off-screen ones, so count should be >= on-screen only
#expect(allWindows.count >= onScreenWindows.count) #expect(allWindows.count >= onScreenWindows.count)
} }
// MARK: - WindowData Structure Tests // MARK: - WindowData Structure Tests
@Test("WindowData has all required properties", .tags(.fast)) @Test("WindowData has all required properties", .tags(.fast))
func windowDataStructure() throws { func windowDataStructure() throws {
// Get any app's windows to test the structure // Get any app's windows to test the structure
let apps = NSWorkspace.shared.runningApplications.filter { $0.activationPolicy == .regular } let apps = NSWorkspace.shared.runningApplications.filter { $0.activationPolicy == .regular }
guard let app = apps.first else { guard let app = apps.first else {
return // Skip test if no regular apps running return // Skip test if no regular apps running
} }
let windows = try WindowManager.getWindowsForApp(pid: app.processIdentifier) let windows = try WindowManager.getWindowsForApp(pid: app.processIdentifier)
// If we have windows, verify WindowData properties // If we have windows, verify WindowData properties
if let firstWindow = windows.first { if let firstWindow = windows.first {
// Check required properties exist // Check required properties exist
@ -74,25 +74,25 @@ struct WindowManagerTests {
#expect(firstWindow.bounds.height >= 0) #expect(firstWindow.bounds.height >= 0)
} }
} }
// MARK: - Window Info Tests // MARK: - Window Info Tests
@Test("Getting window info with details", .tags(.integration)) @Test("Getting window info with details", .tags(.integration))
func getWindowsInfoForApp() throws { func getWindowsInfoForApp() throws {
// Test getting window info with details // Test getting window info with details
let apps = NSWorkspace.shared.runningApplications.filter { $0.activationPolicy == .regular } let apps = NSWorkspace.shared.runningApplications.filter { $0.activationPolicy == .regular }
guard let app = apps.first else { guard let app = apps.first else {
return // Skip test if no regular apps running return // Skip test if no regular apps running
} }
let windowInfos = try WindowManager.getWindowsInfoForApp( let windowInfos = try WindowManager.getWindowsInfoForApp(
pid: app.processIdentifier, pid: app.processIdentifier,
includeOffScreen: false, includeOffScreen: false,
includeBounds: true, includeBounds: true,
includeIDs: true includeIDs: true
) )
// Verify WindowInfo structure // Verify WindowInfo structure
if let firstInfo = windowInfos.first { if let firstInfo = windowInfos.first {
#expect(!firstInfo.window_title.isEmpty) #expect(!firstInfo.window_title.isEmpty)
@ -100,40 +100,42 @@ struct WindowManagerTests {
#expect(firstInfo.bounds != nil) #expect(firstInfo.bounds != nil)
} }
} }
// MARK: - Parameterized Tests // MARK: - Parameterized Tests
@Test("Window retrieval with various options", @Test(
arguments: [ "Window retrieval with various options",
(includeOffScreen: true, includeBounds: true, includeIDs: true), arguments: [
(includeOffScreen: false, includeBounds: true, includeIDs: true), (includeOffScreen: true, includeBounds: true, includeIDs: true),
(includeOffScreen: true, includeBounds: false, includeIDs: true), (includeOffScreen: false, includeBounds: true, includeIDs: true),
(includeOffScreen: true, includeBounds: true, includeIDs: false) (includeOffScreen: true, includeBounds: false, includeIDs: true),
]) (includeOffScreen: true, includeBounds: true, includeIDs: false)
]
)
func windowRetrievalOptions(includeOffScreen: Bool, includeBounds: Bool, includeIDs: Bool) throws { func windowRetrievalOptions(includeOffScreen: Bool, includeBounds: Bool, includeIDs: Bool) throws {
let apps = NSWorkspace.shared.runningApplications.filter { $0.activationPolicy == .regular } let apps = NSWorkspace.shared.runningApplications.filter { $0.activationPolicy == .regular }
guard let app = apps.first else { guard let app = apps.first else {
return // Skip test if no regular apps running return // Skip test if no regular apps running
} }
let windowInfos = try WindowManager.getWindowsInfoForApp( let windowInfos = try WindowManager.getWindowsInfoForApp(
pid: app.processIdentifier, pid: app.processIdentifier,
includeOffScreen: includeOffScreen, includeOffScreen: includeOffScreen,
includeBounds: includeBounds, includeBounds: includeBounds,
includeIDs: includeIDs includeIDs: includeIDs
) )
// Verify options are respected // Verify options are respected
for info in windowInfos { for info in windowInfos {
#expect(!info.window_title.isEmpty) #expect(!info.window_title.isEmpty)
if includeIDs { if includeIDs {
#expect(info.window_id != nil) #expect(info.window_id != nil)
} else { } else {
#expect(info.window_id == nil) #expect(info.window_id == nil)
} }
if includeBounds { if includeBounds {
#expect(info.bounds != nil) #expect(info.bounds != nil)
} else { } else {
@ -141,22 +143,24 @@ struct WindowManagerTests {
} }
} }
} }
// MARK: - Performance Tests // MARK: - Performance Tests
@Test("Window retrieval performance", @Test(
arguments: 1...5) "Window retrieval performance",
arguments: 1...5
)
func getWindowsPerformance(iteration: Int) throws { func getWindowsPerformance(iteration: Int) throws {
// Test performance of getting windows // Test performance of getting windows
let apps = NSWorkspace.shared.runningApplications let apps = NSWorkspace.shared.runningApplications
let finder = try #require(apps.first(where: { $0.bundleIdentifier == "com.apple.finder" })) let finder = try #require(apps.first { $0.bundleIdentifier == "com.apple.finder" })
let windows = try WindowManager.getWindowsForApp(pid: finder.processIdentifier) let windows = try WindowManager.getWindowsForApp(pid: finder.processIdentifier)
#expect(windows.count >= 0) // Windows count is always non-negative
} }
// MARK: - Error Handling Tests // MARK: - Error Handling Tests
@Test("WindowError types exist", .tags(.fast)) @Test("WindowError types exist", .tags(.fast))
func windowListError() { func windowListError() {
// We can't easily force CGWindowListCopyWindowInfo to fail, // We can't easily force CGWindowListCopyWindowInfo to fail,
@ -176,35 +180,34 @@ struct WindowManagerTests {
@Suite("WindowManager Advanced Tests", .tags(.windowManager, .integration)) @Suite("WindowManager Advanced Tests", .tags(.windowManager, .integration))
struct WindowManagerAdvancedTests { struct WindowManagerAdvancedTests {
@Test("Multiple apps window retrieval", .tags(.integration)) @Test("Multiple apps window retrieval", .tags(.integration))
func multipleAppsWindows() throws { func multipleAppsWindows() throws {
let apps = NSWorkspace.shared.runningApplications.filter { $0.activationPolicy == .regular } let apps = NSWorkspace.shared.runningApplications.filter { $0.activationPolicy == .regular }
let appsToTest = apps.prefix(3) // Test first 3 apps let appsToTest = apps.prefix(3) // Test first 3 apps
for app in appsToTest { for app in appsToTest {
let windows = try WindowManager.getWindowsForApp(pid: app.processIdentifier) let windows = try WindowManager.getWindowsForApp(pid: app.processIdentifier)
// Each app should successfully return a window list (even if empty) // Each app should successfully return a window list (even if empty)
#expect(windows.count >= 0) // Windows count is always non-negative
// Verify window indices are sequential // Verify window indices are sequential
for (index, window) in windows.enumerated() { for (index, window) in windows.enumerated() {
#expect(window.windowIndex == index) #expect(window.windowIndex == index)
} }
} }
} }
@Test("Window bounds validation", .tags(.integration)) @Test("Window bounds validation", .tags(.integration))
func windowBoundsValidation() throws { func windowBoundsValidation() throws {
let apps = NSWorkspace.shared.runningApplications.filter { $0.activationPolicy == .regular } let apps = NSWorkspace.shared.runningApplications.filter { $0.activationPolicy == .regular }
guard let app = apps.first else { guard let app = apps.first else {
return // Skip test if no regular apps running return // Skip test if no regular apps running
} }
let windows = try WindowManager.getWindowsForApp(pid: app.processIdentifier) let windows = try WindowManager.getWindowsForApp(pid: app.processIdentifier)
for window in windows { for window in windows {
// Window bounds should be reasonable // Window bounds should be reasonable
#expect(window.bounds.width > 0) #expect(window.bounds.width > 0)
@ -213,40 +216,42 @@ struct WindowManagerAdvancedTests {
#expect(window.bounds.height < 10000) // Reasonable maximum #expect(window.bounds.height < 10000) // Reasonable maximum
} }
} }
@Test("System apps window detection", @Test(
arguments: ["com.apple.finder", "com.apple.dock", "com.apple.systemuiserver"]) "System apps window detection",
arguments: ["com.apple.finder", "com.apple.dock", "com.apple.systemuiserver"]
)
func systemAppsWindows(bundleId: String) throws { func systemAppsWindows(bundleId: String) throws {
let apps = NSWorkspace.shared.runningApplications let apps = NSWorkspace.shared.runningApplications
guard let app = apps.first(where: { $0.bundleIdentifier == bundleId }) else { guard let app = apps.first(where: { $0.bundleIdentifier == bundleId }) else {
return // Skip test if app not running return // Skip test if app not running
} }
let windows = try WindowManager.getWindowsForApp(pid: app.processIdentifier) let windows = try WindowManager.getWindowsForApp(pid: app.processIdentifier)
// System apps might have 0 or more windows // System apps might have 0 or more windows
#expect(windows.count >= 0) // Windows count is always non-negative
// If windows exist, they should have valid properties // If windows exist, they should have valid properties
for window in windows { for window in windows {
#expect(window.windowId > 0) #expect(window.windowId > 0)
#expect(!window.title.isEmpty) #expect(!window.title.isEmpty)
} }
} }
@Test("Window title encoding", .tags(.fast)) @Test("Window title encoding", .tags(.fast))
func windowTitleEncoding() throws { func windowTitleEncoding() throws {
// Test that window titles with special characters are handled // Test that window titles with special characters are handled
let apps = NSWorkspace.shared.runningApplications.filter { $0.activationPolicy == .regular } let apps = NSWorkspace.shared.runningApplications.filter { $0.activationPolicy == .regular }
for app in apps.prefix(5) { for app in apps.prefix(5) {
let windows = try WindowManager.getWindowsForApp(pid: app.processIdentifier) let windows = try WindowManager.getWindowsForApp(pid: app.processIdentifier)
for window in windows { for window in windows {
// Title should be valid UTF-8 // Title should be valid UTF-8
#expect(window.title.utf8.count > 0) #expect(!window.title.utf8.isEmpty)
// Should handle common special characters // Should handle common special characters
let specialChars = ["", "", "©", "", ""] let specialChars = ["", "", "©", "", ""]
// Window titles might contain these, should not crash // Window titles might contain these, should not crash
@ -256,15 +261,15 @@ struct WindowManagerAdvancedTests {
} }
} }
} }
@Test("Concurrent window queries", .tags(.integration)) @Test("Concurrent window queries", .tags(.integration))
func concurrentWindowQueries() async throws { func concurrentWindowQueries() async throws {
let apps = NSWorkspace.shared.runningApplications.filter { $0.activationPolicy == .regular } let apps = NSWorkspace.shared.runningApplications.filter { $0.activationPolicy == .regular }
guard let app = apps.first else { guard let app = apps.first else {
return // Skip test if no regular apps running return // Skip test if no regular apps running
} }
// Test concurrent access to WindowManager // Test concurrent access to WindowManager
await withTaskGroup(of: Result<[WindowData], Error>.self) { group in await withTaskGroup(of: Result<[WindowData], Error>.self) { group in
for _ in 0..<5 { for _ in 0..<5 {
@ -277,22 +282,22 @@ struct WindowManagerAdvancedTests {
} }
} }
} }
var results: [Result<[WindowData], Error>] = [] var results: [Result<[WindowData], Error>] = []
for await result in group { for await result in group {
results.append(result) results.append(result)
} }
// All concurrent queries should succeed // All concurrent queries should succeed
#expect(results.count == 5) #expect(results.count == 5)
for result in results { for result in results {
switch result { switch result {
case .success(let windows): case let .success(windows):
#expect(windows.count >= 0) // Windows count is always non-negative
case .failure(let error): case let .failure(error):
Issue.record("Concurrent query failed: \(error)") Issue.record("Concurrent query failed: \(error)")
} }
} }
} }
} }
} }