mirror of
https://github.com/samsonjs/Peekaboo.git
synced 2026-03-25 09:25:47 +00:00
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:
parent
45f087496a
commit
e894210dbd
14 changed files with 865 additions and 832 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -56,8 +56,8 @@ class Logger {
|
|||
}
|
||||
|
||||
func getDebugLogs() -> [String] {
|
||||
return queue.sync {
|
||||
return self.debugLogs
|
||||
queue.sync {
|
||||
self.debugLogs
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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/"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,4 +14,4 @@ extension Tag {
|
|||
@Tag static var performance: Self
|
||||
@Tag static var concurrency: Self
|
||||
@Tag static var memory: Self
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue