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

View file

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

View file

@ -118,9 +118,11 @@ enum CaptureError: Error, LocalizedError {
case .noDisplaysAvailable:
"No displays available for capture."
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:
"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:
"Invalid display ID provided."
case .captureCreationFailed:

View file

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

View file

@ -1,47 +1,47 @@
import AppKit
@testable import peekaboo
import Testing
import AppKit
@Suite("ApplicationFinder Tests", .tags(.applicationFinder, .unit))
struct ApplicationFinderTests {
// MARK: - Test Data
private static let testIdentifiers = [
"Finder", "finder", "FINDER", "Find", "com.apple.finder"
]
private static let invalidIdentifiers = [
"", " ", "NonExistentApp12345", "invalid.bundle.id",
"", " ", "NonExistentApp12345", "invalid.bundle.id",
String(repeating: "a", count: 1000)
]
// MARK: - Find Application Tests
@Test("Finding an app by exact name match", .tags(.fast))
func findApplicationExactMatch() throws {
// Test finding an app that should always be running on macOS
let result = try ApplicationFinder.findApplication(identifier: "Finder")
#expect(result.localizedName == "Finder")
#expect(result.bundleIdentifier == "com.apple.finder")
}
@Test("Finding an app is case-insensitive", .tags(.fast))
func findApplicationCaseInsensitive() throws {
// Test case-insensitive matching
let result = try ApplicationFinder.findApplication(identifier: "finder")
#expect(result.localizedName == "Finder")
}
@Test("Finding an app by bundle identifier", .tags(.fast))
func findApplicationByBundleIdentifier() throws {
// Test finding by bundle identifier
let result = try ApplicationFinder.findApplication(identifier: "com.apple.finder")
#expect(result.bundleIdentifier == "com.apple.finder")
}
@Test("Throws error when app is not found", .tags(.fast))
func findApplicationNotFound() throws {
// Test app not found error
@ -49,49 +49,51 @@ struct ApplicationFinderTests {
try ApplicationFinder.findApplication(identifier: "NonExistentApp12345")
}
}
@Test("Finding an app by partial name match", .tags(.fast))
func findApplicationPartialMatch() throws {
// Test partial name matching
let result = try ApplicationFinder.findApplication(identifier: "Find")
// Should find Finder as closest match
#expect(result.localizedName == "Finder")
}
// MARK: - Parameterized Tests
@Test("Finding apps with various identifiers",
arguments: [
("Finder", "com.apple.finder"),
("finder", "com.apple.finder"),
("FINDER", "com.apple.finder"),
("com.apple.finder", "com.apple.finder")
])
@Test(
"Finding apps with various identifiers",
arguments: [
("Finder", "com.apple.finder"),
("finder", "com.apple.finder"),
("FINDER", "com.apple.finder"),
("com.apple.finder", "com.apple.finder")
]
)
func findApplicationVariousIdentifiers(identifier: String, expectedBundleId: String) throws {
let result = try ApplicationFinder.findApplication(identifier: identifier)
#expect(result.bundleIdentifier == expectedBundleId)
}
// MARK: - Get All Running Applications Tests
@Test("Getting all running applications returns non-empty list", .tags(.fast))
func getAllRunningApplications() {
// Test getting all running applications
let apps = ApplicationFinder.getAllRunningApplications()
// Should have at least some apps running
#expect(apps.count > 0)
#expect(!apps.isEmpty)
// Should include Finder
let hasFinder = apps.contains { $0.app_name == "Finder" }
#expect(hasFinder == true)
}
@Test("All running applications have required properties", .tags(.fast))
func allApplicationsHaveRequiredProperties() {
let apps = ApplicationFinder.getAllRunningApplications()
for app in apps {
#expect(!app.app_name.isEmpty)
#expect(!app.bundle_id.isEmpty)
@ -99,14 +101,14 @@ struct ApplicationFinderTests {
#expect(app.window_count >= 0)
}
}
// MARK: - Edge Cases and Advanced Tests
@Test("Finding app with special characters in name", .tags(.fast))
func findApplicationSpecialCharacters() throws {
// Test apps with special characters (if available)
let specialApps = ["1Password", "CleanMyMac", "MacBook Pro"]
for appName in specialApps {
do {
let result = try ApplicationFinder.findApplication(identifier: appName)
@ -118,25 +120,27 @@ struct ApplicationFinderTests {
}
}
}
@Test("Fuzzy matching algorithm scoring", .tags(.fast))
func fuzzyMatchingScoring() throws {
// Test that exact matches get highest scores
let finder = try ApplicationFinder.findApplication(identifier: "Finder")
#expect(finder.localizedName == "Finder")
// Test prefix matching works
let findResult = try ApplicationFinder.findApplication(identifier: "Find")
#expect(findResult.localizedName == "Finder")
}
@Test("Bundle identifier parsing edge cases",
arguments: [
"com.apple",
"apple.finder",
"finder",
"com.apple.finder.extra"
])
@Test(
"Bundle identifier parsing edge cases",
arguments: [
"com.apple",
"apple.finder",
"finder",
"com.apple.finder.extra"
]
)
func bundleIdentifierEdgeCases(partialBundleId: String) throws {
// Should either find Finder or throw appropriate error
do {
@ -147,7 +151,7 @@ struct ApplicationFinderTests {
#expect(Bool(true))
}
}
@Test("Fuzzy matching prefers exact matches", .tags(.fast))
func fuzzyMatchingPrefersExact() throws {
// If we have multiple matches, exact should win
@ -155,21 +159,23 @@ struct ApplicationFinderTests {
#expect(result.localizedName == "Finder")
#expect(result.bundleIdentifier == "com.apple.finder")
}
@Test("Performance: Finding apps multiple times",
arguments: 1...10)
@Test(
"Performance: Finding apps multiple times",
arguments: 1...10
)
func findApplicationPerformance(iteration: Int) throws {
// Test that finding an app completes quickly even when called multiple times
let result = try ApplicationFinder.findApplication(identifier: "Finder")
#expect(result.localizedName == "Finder")
}
@Test("Stress test: Search with many running apps", .tags(.performance))
func stressTestManyApps() {
// Get current app count for baseline
let apps = ApplicationFinder.getAllRunningApplications()
#expect(apps.count > 0)
#expect(!apps.isEmpty)
// Test search performance doesn't degrade with app list size
let startTime = CFAbsoluteTimeGetCurrent()
do {
@ -180,20 +186,22 @@ struct ApplicationFinderTests {
Issue.record("Finder should always be found in performance test")
}
}
// MARK: - Integration Tests
@Test("Find and verify running state of system apps",
arguments: [
("Finder", true),
("Dock", true),
("SystemUIServer", true)
])
@Test(
"Find and verify running state of system apps",
arguments: [
("Finder", true),
("Dock", true),
("SystemUIServer", true)
]
)
func verifySystemAppsRunning(appName: String, shouldBeRunning: Bool) throws {
do {
let result = try ApplicationFinder.findApplication(identifier: appName)
#expect(result.localizedName != nil)
// Verify the app is in the running list
let runningApps = ApplicationFinder.getAllRunningApplications()
let isInList = runningApps.contains { $0.bundle_id == result.bundleIdentifier }
@ -204,17 +212,17 @@ struct ApplicationFinderTests {
}
}
}
@Test("Verify frontmost application detection", .tags(.integration))
func verifyFrontmostApp() throws {
// Get the frontmost app using NSWorkspace
let frontmostApp = NSWorkspace.shared.frontmostApplication
// Try to find it using our ApplicationFinder
if let bundleId = frontmostApp?.bundleIdentifier {
let result = try ApplicationFinder.findApplication(identifier: bundleId)
#expect(result.bundleIdentifier == bundleId)
// Verify it's marked as active in our list
let runningApps = ApplicationFinder.getAllRunningApplications()
let appInfo = runningApps.first { $0.bundle_id == bundleId }
@ -227,21 +235,20 @@ struct ApplicationFinderTests {
@Suite("ApplicationFinder Edge Cases", .tags(.applicationFinder, .unit))
struct ApplicationFinderEdgeCaseTests {
@Test("Empty identifier throws appropriate error", .tags(.fast))
func emptyIdentifierError() {
#expect(throws: (any Error).self) {
try ApplicationFinder.findApplication(identifier: "")
}
}
@Test("Whitespace-only identifier throws appropriate error", .tags(.fast))
func whitespaceIdentifierError() {
#expect(throws: (any Error).self) {
try ApplicationFinder.findApplication(identifier: " ")
}
}
@Test("Very long identifier doesn't crash", .tags(.fast))
func veryLongIdentifier() {
let longIdentifier = String(repeating: "a", count: 1000)
@ -249,9 +256,11 @@ struct ApplicationFinderEdgeCaseTests {
try ApplicationFinder.findApplication(identifier: longIdentifier)
}
}
@Test("Unicode identifiers are handled correctly",
arguments: ["😀App", "App™", "Приложение", "アプリ"])
@Test(
"Unicode identifiers are handled correctly",
arguments: ["😀App", "App™", "Приложение", "アプリ"]
)
func unicodeIdentifiers(identifier: String) {
// Should not crash, either finds or throws appropriate error
do {
@ -262,19 +271,19 @@ struct ApplicationFinderEdgeCaseTests {
#expect(Bool(true))
}
}
@Test("Case sensitivity in matching", .tags(.fast))
func caseSensitivityMatching() throws {
// Test various case combinations
let caseVariations = ["finder", "FINDER", "Finder", "fInDeR"]
for variation in caseVariations {
let result = try ApplicationFinder.findApplication(identifier: variation)
#expect(result.localizedName == "Finder")
#expect(result.bundleIdentifier == "com.apple.finder")
}
}
@Test("Concurrent application searches", .tags(.concurrency))
func concurrentSearches() async {
// Test thread safety of application finder
@ -289,35 +298,33 @@ struct ApplicationFinderEdgeCaseTests {
}
}
}
var successCount = 0
for await success in group {
if success {
successCount += 1
}
for await success in group where success {
successCount += 1
}
// All searches should succeed for Finder
#expect(successCount == 10)
}
}
@Test("Memory usage with large app lists", .tags(.performance))
func memoryUsageTest() {
// Test memory doesn't grow excessively with repeated calls
for _ in 1...5 {
let apps = ApplicationFinder.getAllRunningApplications()
#expect(apps.count > 0)
#expect(!apps.isEmpty)
}
// If we get here without crashing, memory management is working
#expect(Bool(true))
}
@Test("Application list sorting consistency", .tags(.fast))
func applicationListSorting() {
let apps = ApplicationFinder.getAllRunningApplications()
// Verify list is sorted by name (case-insensitive)
for index in 1..<apps.count {
let current = apps[index].app_name.lowercased()
@ -325,19 +332,19 @@ struct ApplicationFinderEdgeCaseTests {
#expect(current >= previous)
}
}
@Test("Window count accuracy", .tags(.integration))
func windowCountAccuracy() {
let apps = ApplicationFinder.getAllRunningApplications()
for app in apps {
// Window count should be non-negative
#expect(app.window_count >= 0)
// Finder should typically have at least one window
if app.app_name == "Finder" {
#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
import Testing
import Foundation
import AppKit
@Suite("Image Capture Logic Tests", .tags(.imageCapture, .unit))
struct ImageCaptureLogicTests {
// MARK: - File Name Generation Tests
@Test("File name generation for displays", .tags(.fast))
func fileNameGenerationDisplays() throws {
// We can't directly test private methods, but we can test the logic
// through public interfaces and verify the expected patterns
// Test that different screen indices would generate different names
let command1 = try ImageCommand.parse(["--screen-index", "0", "--format", "png"])
let command2 = try ImageCommand.parse(["--screen-index", "1", "--format", "png"])
#expect(command1.screenIndex == 0)
#expect(command2.screenIndex == 1)
#expect(command1.format == .png)
#expect(command2.format == .png)
}
@Test("File name generation for applications", .tags(.fast))
func fileNameGenerationApplications() throws {
let command = try ImageCommand.parse([
@ -31,55 +30,55 @@ struct ImageCaptureLogicTests {
"--window-title", "Main Window",
"--format", "jpg"
])
#expect(command.app == "Test App")
#expect(command.windowTitle == "Main Window")
#expect(command.format == .jpg)
}
@Test("Output path generation", .tags(.fast))
func outputPathGeneration() throws {
// Test default path behavior
let defaultCommand = try ImageCommand.parse([])
#expect(defaultCommand.path == nil)
// Test custom path
let customCommand = try ImageCommand.parse(["--path", "/tmp/screenshots"])
#expect(customCommand.path == "/tmp/screenshots")
// Test path with filename
let fileCommand = try ImageCommand.parse(["--path", "/tmp/test.png"])
#expect(fileCommand.path == "/tmp/test.png")
}
// MARK: - Mode Determination Tests
@Test("Mode determination comprehensive", .tags(.fast))
func modeDeterminationComprehensive() throws {
// Screen mode (default when no app specified)
let screenCmd = try ImageCommand.parse([])
#expect(screenCmd.mode == nil)
#expect(screenCmd.app == nil)
// Window mode (when app specified but no explicit mode)
let windowCmd = try ImageCommand.parse(["--app", "Finder"])
#expect(windowCmd.mode == nil) // Will be determined as window during execution
#expect(windowCmd.app == "Finder")
// Explicit modes
let explicitScreen = try ImageCommand.parse(["--mode", "screen"])
#expect(explicitScreen.mode == .screen)
let explicitWindow = try ImageCommand.parse(["--mode", "window", "--app", "Safari"])
#expect(explicitWindow.mode == .window)
#expect(explicitWindow.app == "Safari")
let explicitMulti = try ImageCommand.parse(["--mode", "multi"])
#expect(explicitMulti.mode == .multi)
}
// MARK: - Window Targeting Tests
@Test("Window targeting by title", .tags(.fast))
func windowTargetingByTitle() throws {
let command = try ImageCommand.parse([
@ -87,13 +86,13 @@ struct ImageCaptureLogicTests {
"--app", "Safari",
"--window-title", "Main Window"
])
#expect(command.mode == .window)
#expect(command.app == "Safari")
#expect(command.windowTitle == "Main Window")
#expect(command.windowIndex == nil)
}
@Test("Window targeting by index", .tags(.fast))
func windowTargetingByIndex() throws {
let command = try ImageCommand.parse([
@ -101,13 +100,13 @@ struct ImageCaptureLogicTests {
"--app", "Terminal",
"--window-index", "0"
])
#expect(command.mode == .window)
#expect(command.app == "Terminal")
#expect(command.windowIndex == 0)
#expect(command.windowTitle == nil)
}
@Test("Window targeting priority - title vs index", .tags(.fast))
func windowTargetingPriority() throws {
// When both title and index are specified, both are preserved
@ -117,83 +116,85 @@ struct ImageCaptureLogicTests {
"--window-title", "Main",
"--window-index", "1"
])
#expect(command.windowTitle == "Main")
#expect(command.windowIndex == 1)
// In actual execution, title matching would take precedence
}
// MARK: - Screen Targeting Tests
@Test("Screen targeting by index", .tags(.fast))
func screenTargetingByIndex() throws {
let command = try ImageCommand.parse([
"--mode", "screen",
"--screen-index", "1"
])
#expect(command.mode == .screen)
#expect(command.screenIndex == 1)
}
@Test("Screen index edge cases",
arguments: [-1, 0, 1, 5, 99, Int.max])
@Test(
"Screen index edge cases",
arguments: [-1, 0, 1, 5, 99, Int.max]
)
func screenIndexEdgeCases(index: Int) throws {
let command = try ImageCommand.parse([
"--mode", "screen",
"--screen-index", String(index)
])
#expect(command.screenIndex == index)
// Validation happens during execution, not parsing
}
// MARK: - Capture Focus Tests
@Test("Capture focus modes", .tags(.fast))
func captureFocusModes() throws {
// Default background mode
let defaultCmd = try ImageCommand.parse([])
#expect(defaultCmd.captureFocus == .background)
// Explicit background mode
let backgroundCmd = try ImageCommand.parse(["--capture-focus", "background"])
#expect(backgroundCmd.captureFocus == .background)
// Foreground mode
let foregroundCmd = try ImageCommand.parse(["--capture-focus", "foreground"])
#expect(foregroundCmd.captureFocus == .foreground)
}
// MARK: - Image Format Tests
@Test("Image format handling", .tags(.fast))
func imageFormatHandling() throws {
// Default PNG format
let defaultCmd = try ImageCommand.parse([])
#expect(defaultCmd.format == .png)
// Explicit PNG format
let pngCmd = try ImageCommand.parse(["--format", "png"])
#expect(pngCmd.format == .png)
// JPEG format
let jpgCmd = try ImageCommand.parse(["--format", "jpg"])
#expect(jpgCmd.format == .jpg)
}
@Test("MIME type mapping", .tags(.fast))
func mimeTypeMapping() {
// Test MIME type logic (as used in SavedFile creation)
let pngMime = ImageFormat.png == .png ? "image/png" : "image/jpeg"
let jpgMime = ImageFormat.jpg == .jpg ? "image/jpeg" : "image/png"
#expect(pngMime == "image/png")
#expect(jpgMime == "image/jpeg")
}
// MARK: - Error Handling Tests
@Test("Error code mapping", .tags(.fast))
func errorCodeMapping() {
// Test error code mapping logic used in handleError
@ -206,18 +207,18 @@ struct ImageCaptureLogicTests {
(.invalidArgument("test"), .INVALID_ARGUMENT),
(.unknownError("test"), .UNKNOWN_ERROR)
]
// Verify error mapping logic exists
for (_, expectedCode) in testCases {
// 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)
#expect(Bool(true))
#expect(expectedCode.rawValue.count > 0)
#expect(!expectedCode.rawValue.isEmpty)
}
}
// MARK: - SavedFile Creation Tests
@Test("SavedFile creation for screen capture", .tags(.fast))
func savedFileCreationScreenCapture() {
let savedFile = SavedFile(
@ -228,7 +229,7 @@ struct ImageCaptureLogicTests {
window_index: nil,
mime_type: "image/png"
)
#expect(savedFile.path == "/tmp/screen-0.png")
#expect(savedFile.item_label == "Display 1 (Index 0)")
#expect(savedFile.window_title == nil)
@ -236,7 +237,7 @@ struct ImageCaptureLogicTests {
#expect(savedFile.window_index == nil)
#expect(savedFile.mime_type == "image/png")
}
@Test("SavedFile creation for window capture", .tags(.fast))
func savedFileCreationWindowCapture() {
let savedFile = SavedFile(
@ -247,7 +248,7 @@ struct ImageCaptureLogicTests {
window_index: 0,
mime_type: "image/jpeg"
)
#expect(savedFile.path == "/tmp/safari-main.jpg")
#expect(savedFile.item_label == "Safari")
#expect(savedFile.window_title == "Main Window")
@ -255,9 +256,9 @@ struct ImageCaptureLogicTests {
#expect(savedFile.window_index == 0)
#expect(savedFile.mime_type == "image/jpeg")
}
// MARK: - Complex Configuration Tests
@Test("Complex multi-window capture configuration", .tags(.fast))
func complexMultiWindowConfiguration() throws {
let command = try ImageCommand.parse([
@ -268,7 +269,7 @@ struct ImageCaptureLogicTests {
"--capture-focus", "foreground",
"--json-output"
])
#expect(command.mode == .multi)
#expect(command.app == "Visual Studio Code")
#expect(command.format == .png)
@ -276,7 +277,7 @@ struct ImageCaptureLogicTests {
#expect(command.captureFocus == .foreground)
#expect(command.jsonOutput == true)
}
@Test("Complex screen capture configuration", .tags(.fast))
func complexScreenCaptureConfiguration() throws {
let command = try ImageCommand.parse([
@ -286,16 +287,16 @@ struct ImageCaptureLogicTests {
"--path", "/Users/test/screenshots/display-1.jpg",
"--json-output"
])
#expect(command.mode == .screen)
#expect(command.screenIndex == 1)
#expect(command.format == .jpg)
#expect(command.path == "/Users/test/screenshots/display-1.jpg")
#expect(command.jsonOutput == true)
}
// MARK: - Performance Tests
@Test("Configuration parsing performance", .tags(.performance))
func configurationParsingPerformance() {
let complexArgs = [
@ -309,9 +310,9 @@ struct ImageCaptureLogicTests {
"--capture-focus", "foreground",
"--json-output"
]
let startTime = CFAbsoluteTimeGetCurrent()
// Parse many times to test performance
for _ in 1...100 {
do {
@ -321,40 +322,40 @@ struct ImageCaptureLogicTests {
Issue.record("Parsing should not fail: \(error)")
}
}
let duration = CFAbsoluteTimeGetCurrent() - startTime
#expect(duration < 1.0) // Should parse 1000 configs within 1 second
}
// MARK: - Integration Readiness Tests
@Test("Command readiness for screen capture", .tags(.fast))
func commandReadinessScreenCapture() throws {
let command = try ImageCommand.parse(["--mode", "screen"])
// Verify command is properly configured for screen capture
#expect(command.mode == .screen)
#expect(command.app == nil) // No app needed for screen capture
#expect(command.format == .png) // Has default format
}
@Test("Command readiness for window capture", .tags(.fast))
func commandReadinessWindowCapture() throws {
let command = try ImageCommand.parse([
"--mode", "window",
"--app", "Finder"
])
// Verify command is properly configured for window capture
#expect(command.mode == .window)
#expect(command.app == "Finder") // App is required
#expect(command.format == .png) // Has default format
}
@Test("Command validation for invalid configurations", .tags(.fast))
func commandValidationInvalidConfigurations() {
// These should parse successfully but would fail during execution
// Window mode without app (would fail during execution)
do {
let command = try ImageCommand.parse(["--mode", "window"])
@ -363,7 +364,7 @@ struct ImageCaptureLogicTests {
} catch {
Issue.record("Should parse successfully")
}
// Invalid screen index (would fail during execution)
do {
let command = try ImageCommand.parse(["--screen-index", "-1"])
@ -378,7 +379,6 @@ struct ImageCaptureLogicTests {
@Suite("Advanced Image Capture Logic", .tags(.imageCapture, .integration))
struct AdvancedImageCaptureLogicTests {
@Test("Multi-mode capture scenarios", .tags(.fast))
func multiModeCaptureScenarios() throws {
// Multi mode with app (should capture all windows)
@ -388,13 +388,13 @@ struct AdvancedImageCaptureLogicTests {
])
#expect(multiWithApp.mode == .multi)
#expect(multiWithApp.app == "Safari")
// Multi mode without app (should capture all screens)
let multiWithoutApp = try ImageCommand.parse(["--mode", "multi"])
#expect(multiWithoutApp.mode == .multi)
#expect(multiWithoutApp.app == nil)
}
@Test("Focus mode implications", .tags(.fast))
func focusModeImplications() throws {
// Foreground focus should work with any capture mode
@ -403,14 +403,14 @@ struct AdvancedImageCaptureLogicTests {
"--capture-focus", "foreground"
])
#expect(foregroundScreen.captureFocus == .foreground)
let foregroundWindow = try ImageCommand.parse([
"--mode", "window",
"--app", "Terminal",
"--capture-focus", "foreground"
])
#expect(foregroundWindow.captureFocus == .foreground)
// Background focus (default) should work without additional permissions
let backgroundCapture = try ImageCommand.parse([
"--mode", "window",
@ -418,30 +418,30 @@ struct AdvancedImageCaptureLogicTests {
])
#expect(backgroundCapture.captureFocus == .background)
}
@Test("Path handling edge cases", .tags(.fast))
func pathHandlingEdgeCases() throws {
// Relative paths
let relativePath = try ImageCommand.parse(["--path", "./screenshots/test.png"])
#expect(relativePath.path == "./screenshots/test.png")
// Home directory expansion
let homePath = try ImageCommand.parse(["--path", "~/Desktop/capture.jpg"])
#expect(homePath.path == "~/Desktop/capture.jpg")
// Absolute paths
let absolutePath = try ImageCommand.parse(["--path", "/tmp/absolute/path.png"])
#expect(absolutePath.path == "/tmp/absolute/path.png")
// Paths with spaces
let spacePath = try ImageCommand.parse(["--path", "/path with spaces/image.png"])
#expect(spacePath.path == "/path with spaces/image.png")
// Unicode paths
let unicodePath = try ImageCommand.parse(["--path", "/tmp/测试/スクリーン.png"])
#expect(unicodePath.path == "/tmp/测试/スクリーン.png")
}
@Test("Command execution readiness matrix", .tags(.fast))
func commandExecutionReadinessMatrix() {
// Define test scenarios
@ -456,7 +456,7 @@ struct AdvancedImageCaptureLogicTests {
(["--app", "Finder"], true, "Implicit window mode"),
([], true, "Default screen capture")
]
for scenario in scenarios {
do {
let command = try ImageCommand.parse(scenario.args)
@ -472,7 +472,7 @@ struct AdvancedImageCaptureLogicTests {
}
}
}
@Test("Error propagation scenarios", .tags(.fast))
func errorPropagationScenarios() {
// Test that invalid arguments are properly handled
@ -483,14 +483,14 @@ struct AdvancedImageCaptureLogicTests {
["--screen-index", "abc"],
["--window-index", "xyz"]
]
for args in invalidArgs {
#expect(throws: (any Error).self) {
_ = try ImageCommand.parse(args)
}
}
}
@Test("Memory efficiency with complex configurations", .tags(.memory))
func memoryEfficiencyComplexConfigurations() {
// Test that complex configurations don't cause excessive memory usage
@ -498,12 +498,12 @@ struct AdvancedImageCaptureLogicTests {
["--mode", "multi", "--app", String(repeating: "LongAppName", count: 100)],
["--window-title", String(repeating: "VeryLongTitle", count: 200)],
["--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 {
do {
let _ = try ImageCommand.parse(config)
_ = try ImageCommand.parse(config)
#expect(Bool(true)) // Command parsed successfully
} catch {
// 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 Foundation
@testable import peekaboo
import Testing
import Foundation
@Suite("ImageCommand Tests", .tags(.imageCapture, .unit))
struct ImageCommandTests {
// MARK: - Test Data & Helpers
private static let validFormats: [ImageFormat] = [.png, .jpg]
private static let validCaptureModes: [CaptureMode] = [.screen, .window, .multi]
private static let validCaptureFocus: [CaptureFocus] = [.background, .foreground]
private static func createTestCommand(_ args: [String] = []) throws -> ImageCommand {
return try ImageCommand.parse(args)
try ImageCommand.parse(args)
}
// MARK: - Command Parsing Tests
@Test("Basic command parsing with defaults", .tags(.fast))
func imageCommandParsing() throws {
// Test basic command parsing
let command = try ImageCommand.parse([])
// Verify defaults
#expect(command.mode == nil)
#expect(command.format == .png)
@ -31,36 +30,36 @@ struct ImageCommandTests {
#expect(command.captureFocus == .background)
#expect(command.jsonOutput == false)
}
@Test("Command with screen mode", .tags(.fast))
func imageCommandWithScreenMode() throws {
// Test screen capture mode
let command = try ImageCommand.parse(["--mode", "screen"])
#expect(command.mode == .screen)
}
@Test("Command with app specifier", .tags(.fast))
func imageCommandWithAppSpecifier() throws {
// Test app-specific capture
let command = try ImageCommand.parse([
"--app", "Finder"
])
#expect(command.mode == nil) // mode is optional
#expect(command.app == "Finder")
}
@Test("Command with window title", .tags(.fast))
func imageCommandWithWindowTitle() throws {
// Test window title capture
let command = try ImageCommand.parse([
"--window-title", "Documents"
])
#expect(command.windowTitle == "Documents")
}
@Test("Command with output path", .tags(.fast))
func imageCommandWithOutput() throws {
// Test output path specification
@ -68,89 +67,93 @@ struct ImageCommandTests {
let command = try ImageCommand.parse([
"--path", outputPath
])
#expect(command.path == outputPath)
}
@Test("Command with format option", .tags(.fast))
func imageCommandWithFormat() throws {
// Test format specification
let command = try ImageCommand.parse([
"--format", "jpg"
])
#expect(command.format == .jpg)
}
@Test("Command with focus option", .tags(.fast))
func imageCommandWithFocus() throws {
// Test focus option
let command = try ImageCommand.parse([
"--capture-focus", "foreground"
])
#expect(command.captureFocus == .foreground)
}
@Test("Command with JSON output", .tags(.fast))
func imageCommandWithJSONOutput() throws {
// Test JSON output flag
let command = try ImageCommand.parse([
"--json-output"
])
#expect(command.jsonOutput == true)
}
@Test("Command with multi mode", .tags(.fast))
func imageCommandWithMultiMode() throws {
// Test multi capture mode
let command = try ImageCommand.parse([
"--mode", "multi"
])
#expect(command.mode == .multi)
}
@Test("Command with screen index", .tags(.fast))
func imageCommandWithScreenIndex() throws {
// Test screen index specification
let command = try ImageCommand.parse([
"--screen-index", "1"
])
#expect(command.screenIndex == 1)
}
// MARK: - Parameterized Command Tests
@Test("Various command combinations",
arguments: [
(args: ["--mode", "screen", "--format", "png"], mode: CaptureMode.screen, 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)
])
@Test(
"Various command combinations",
arguments: [
(args: ["--mode", "screen", "--format", "png"], mode: CaptureMode.screen, 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 {
let command = try ImageCommand.parse(args)
#expect(command.mode == mode)
#expect(command.format == format)
}
@Test("Invalid arguments throw errors",
arguments: [
["--mode", "invalid"],
["--format", "bmp"],
["--capture-focus", "neither"],
["--screen-index", "abc"]
])
@Test(
"Invalid arguments throw errors",
arguments: [
["--mode", "invalid"],
["--format", "bmp"],
["--capture-focus", "neither"],
["--screen-index", "abc"]
]
)
func invalidArguments(args: [String]) {
#expect(throws: (any Error).self) {
_ = try ImageCommand.parse(args)
}
}
// MARK: - Model Tests
@Test("SavedFile model creation", .tags(.fast))
func savedFileModel() {
let savedFile = SavedFile(
@ -161,12 +164,12 @@ struct ImageCommandTests {
window_index: nil,
mime_type: "image/png"
)
#expect(savedFile.path == "/tmp/screenshot.png")
#expect(savedFile.item_label == "Screen 1")
#expect(savedFile.mime_type == "image/png")
}
@Test("ImageCaptureData encoding", .tags(.fast))
func imageCaptureDataEncoding() throws {
let savedFile = SavedFile(
@ -177,69 +180,69 @@ struct ImageCommandTests {
window_index: nil,
mime_type: "image/png"
)
let captureData = ImageCaptureData(saved_files: [savedFile])
// Test JSON encoding
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
let data = try encoder.encode(captureData)
#expect(data.count > 0)
#expect(!data.isEmpty)
// Test decoding
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let decoded = try decoder.decode(ImageCaptureData.self, from: data)
#expect(decoded.saved_files.count == 1)
#expect(decoded.saved_files[0].path == "/tmp/test.png")
}
// MARK: - Enum Raw Value Tests
@Test("CaptureMode raw values", .tags(.fast))
func captureModeRawValues() {
#expect(CaptureMode.screen.rawValue == "screen")
#expect(CaptureMode.window.rawValue == "window")
#expect(CaptureMode.multi.rawValue == "multi")
}
@Test("ImageFormat raw values", .tags(.fast))
func imageFormatRawValues() {
#expect(ImageFormat.png.rawValue == "png")
#expect(ImageFormat.jpg.rawValue == "jpg")
}
@Test("CaptureFocus raw values", .tags(.fast))
func captureFocusRawValues() {
#expect(CaptureFocus.background.rawValue == "background")
#expect(CaptureFocus.foreground.rawValue == "foreground")
}
// MARK: - Mode Determination & Logic Tests
@Test("Mode determination logic", .tags(.fast))
func modeDeterminationLogic() throws {
// No mode, no app -> should default to screen
let screenCommand = try ImageCommand.parse([])
#expect(screenCommand.mode == nil)
#expect(screenCommand.app == nil)
// No mode, with app -> should infer window mode in actual execution
let windowCommand = try ImageCommand.parse(["--app", "Finder"])
#expect(windowCommand.mode == nil)
#expect(windowCommand.app == "Finder")
// Explicit mode should be preserved
let explicitCommand = try ImageCommand.parse(["--mode", "multi"])
#expect(explicitCommand.mode == .multi)
}
@Test("Default values verification", .tags(.fast))
func defaultValues() throws {
let command = try ImageCommand.parse([])
#expect(command.mode == nil)
#expect(command.format == .png)
#expect(command.path == nil)
@ -250,21 +253,25 @@ struct ImageCommandTests {
#expect(command.captureFocus == .background)
#expect(command.jsonOutput == false)
}
@Test("Screen index boundary values",
arguments: [-1, 0, 1, 99, Int.max])
@Test(
"Screen index boundary values",
arguments: [-1, 0, 1, 99, Int.max]
)
func screenIndexBoundaries(index: Int) throws {
let command = try ImageCommand.parse(["--screen-index", String(index)])
#expect(command.screenIndex == index)
}
@Test("Window index boundary values",
arguments: [-1, 0, 1, 10, Int.max])
@Test(
"Window index boundary values",
arguments: [-1, 0, 1, 10, Int.max]
)
func windowIndexBoundaries(index: Int) throws {
let command = try ImageCommand.parse(["--window-index", String(index)])
#expect(command.windowIndex == index)
}
@Test("Error handling for invalid combinations", .tags(.fast))
func invalidCombinations() {
// Window capture without app should fail in execution
@ -283,9 +290,8 @@ struct ImageCommandTests {
@Suite("ImageCommand Advanced Tests", .tags(.imageCapture, .integration))
struct ImageCommandAdvancedTests {
// MARK: - Complex Scenario Tests
@Test("Complex command with multiple options", .tags(.fast))
func complexCommand() throws {
let command = try ImageCommand.parse([
@ -298,7 +304,7 @@ struct ImageCommandAdvancedTests {
"--capture-focus", "foreground",
"--json-output"
])
#expect(command.mode == .window)
#expect(command.app == "Safari")
#expect(command.windowTitle == "Home")
@ -308,11 +314,11 @@ struct ImageCommandAdvancedTests {
#expect(command.captureFocus == .foreground)
#expect(command.jsonOutput == true)
}
@Test("Command help text contains all options", .tags(.fast))
func commandHelpText() {
let helpText = ImageCommand.helpMessage()
// Verify key options are documented
#expect(helpText.contains("--mode"))
#expect(helpText.contains("--app"))
@ -322,67 +328,71 @@ struct ImageCommandAdvancedTests {
#expect(helpText.contains("--capture-focus"))
#expect(helpText.contains("--json-output"))
}
@Test("Command configuration", .tags(.fast))
func commandConfiguration() {
let config = ImageCommand.configuration
#expect(config.commandName == "image")
#expect(config.abstract.contains("Capture"))
}
@Test("Window specifier combinations",
arguments: [
(app: "Safari", title: "Home", index: nil),
(app: "Finder", title: nil, index: 0),
(app: "Terminal", title: nil, index: nil)
])
@Test(
"Window specifier combinations",
arguments: [
(app: "Safari", title: "Home", index: nil),
(app: "Finder", title: nil, index: 0),
(app: "Terminal", title: nil, index: nil)
]
)
func windowSpecifierCombinations(app: String, title: String?, index: Int?) throws {
var args = ["--app", app]
if let title = title {
if let title {
args.append(contentsOf: ["--window-title", title])
}
if let index = index {
if let index {
args.append(contentsOf: ["--window-index", String(index)])
}
let command = try ImageCommand.parse(args)
#expect(command.app == app)
#expect(command.windowTitle == title)
#expect(command.windowIndex == index)
}
@Test("Path expansion handling",
arguments: [
"~/Desktop/screenshot.png",
"/tmp/test.png",
"./relative/path.png",
"/path with spaces/image.png"
])
@Test(
"Path expansion handling",
arguments: [
"~/Desktop/screenshot.png",
"/tmp/test.png",
"./relative/path.png",
"/path with spaces/image.png"
]
)
func pathExpansion(path: String) throws {
let command = try ImageCommand.parse(["--path", path])
#expect(command.path == path)
}
@Test("FileHandleTextOutputStream functionality", .tags(.fast))
func fileHandleTextOutputStream() {
// Test the custom text output stream
let pipe = Pipe()
var stream = FileHandleTextOutputStream(pipe.fileHandleForWriting)
let testString = "Test output"
stream.write(testString)
pipe.fileHandleForWriting.closeFile()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8)
#expect(output == testString)
}
@Test("Command validation edge cases", .tags(.fast))
func commandValidationEdgeCases() {
// Test very long paths
@ -393,7 +403,7 @@ struct ImageCommandAdvancedTests {
} catch {
Issue.record("Should handle long paths gracefully")
}
// Test unicode in paths
let unicodePath = "/tmp/测试/スクリーン.png"
do {
@ -403,20 +413,20 @@ struct ImageCommandAdvancedTests {
Issue.record("Should handle unicode paths")
}
}
@Test("MIME type assignment logic", .tags(.fast))
func mimeTypeAssignment() {
func mimeTypeAssignment() throws {
// 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)
let jpgCommand = try! ImageCommand.parse(["--format", "jpg"])
let jpgCommand = try ImageCommand.parse(["--format", "jpg"])
#expect(jpgCommand.format == .jpg)
// Verify MIME types would be assigned correctly
// (This logic is in the SavedFile creation during actual capture)
}
@Test("Argument parsing stress test", .tags(.performance))
func argumentParsingStressTest() {
// Test parsing performance with many arguments
@ -429,7 +439,7 @@ struct ImageCommandAdvancedTests {
"--capture-focus", "foreground",
"--json-output"
]
do {
let command = try ImageCommand.parse(args)
#expect(command.mode == .multi)
@ -438,17 +448,19 @@ struct ImageCommandAdvancedTests {
Issue.record("Should handle complex argument parsing")
}
}
@Test("Command option combinations validation",
arguments: [
(["--mode", "screen"], true),
(["--mode", "window", "--app", "Finder"], true),
(["--mode", "multi"], true),
(["--app", "Safari"], true),
(["--window-title", "Test"], true),
(["--screen-index", "0"], true),
(["--window-index", "0"], true)
])
@Test(
"Command option combinations validation",
arguments: [
(["--mode", "screen"], true),
(["--mode", "window", "--app", "Finder"], true),
(["--mode", "multi"], true),
(["--app", "Safari"], true),
(["--window-title", "Test"], true),
(["--screen-index", "0"], true),
(["--window-index", "0"], true)
]
)
func commandOptionCombinations(args: [String], shouldParse: Bool) {
do {
let command = try ImageCommand.parse(args)
@ -458,4 +470,4 @@ struct ImageCommandAdvancedTests {
#expect(shouldParse == false)
}
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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