mirror of
https://github.com/samsonjs/Peekaboo.git
synced 2026-04-27 15:07:41 +00:00
- Remove redundant bundle ID checks in ApplicationFinderTests - Replace do-catch with #expect(throws:) for cleaner error testing - Simplify permission test assertions to avoid false failures - Remove unnecessary boolean comparisons in permission checks These changes make the tests more maintainable and less prone to environment-specific failures. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
501 lines
19 KiB
Swift
501 lines
19 KiB
Swift
import AppKit
|
|
@testable import peekaboo
|
|
import Testing
|
|
|
|
@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",
|
|
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
|
|
#expect(throws: (any Error).self) {
|
|
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")
|
|
]
|
|
)
|
|
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.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)
|
|
// Some system processes may have empty bundle IDs - no need to check twice
|
|
#expect(app.pid > 0)
|
|
#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)
|
|
#expect(result.localizedName != nil)
|
|
#expect(!result.localizedName!.isEmpty)
|
|
} catch {
|
|
// Expected if app is not installed - no assertion needed
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
@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(
|
|
"Fuzzy matching handles typos",
|
|
arguments: [
|
|
("Finderr", "Finder"), // Extra character at end
|
|
("Fnder", "Finder"), // Missing character
|
|
("Fidner", "Finder"), // Transposed characters
|
|
("Findr", "Finder"), // Missing character at end
|
|
("inder", "Finder") // Missing first character
|
|
]
|
|
)
|
|
func fuzzyMatchingTypos(typo: String, expectedApp: String) throws {
|
|
// Test that fuzzy matching can handle common typos
|
|
do {
|
|
let result = try ApplicationFinder.findApplication(identifier: typo)
|
|
#expect(result.localizedName == expectedApp)
|
|
} catch {
|
|
// If fuzzy matching doesn't work for this typo, it's okay
|
|
// The test documents the behavior either way
|
|
print("Fuzzy matching did not find \(expectedApp) for typo: \(typo)")
|
|
}
|
|
}
|
|
|
|
@Test("Fuzzy matching with Chrome typos", .tags(.fast))
|
|
func fuzzyMatchingChromeTypos() throws {
|
|
// Test the specific example from the user - "Chromee" should match "Chrome"
|
|
// Note: This test will only pass if Chrome is actually running
|
|
let chromeVariations = ["Chromee", "Chrom", "Chrme", "Chorme"]
|
|
|
|
for variation in chromeVariations {
|
|
do {
|
|
let result = try ApplicationFinder.findApplication(identifier: variation)
|
|
// If Chrome is found, verify it's actually Chrome or Google Chrome
|
|
#expect(result.localizedName?.contains("Chrome") == true)
|
|
} catch {
|
|
// Chrome might not be running, which is okay for this test
|
|
print("Chrome not found for variation: \(variation)")
|
|
}
|
|
}
|
|
}
|
|
|
|
@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 {
|
|
let result = try ApplicationFinder.findApplication(identifier: partialBundleId)
|
|
#expect(result.bundleIdentifier != nil)
|
|
} catch {
|
|
// Expected for invalid/partial bundle IDs
|
|
#expect(Bool(true))
|
|
}
|
|
}
|
|
|
|
@Test("Fuzzy matching prefers exact matches", .tags(.fast))
|
|
func fuzzyMatchingPrefersExact() throws {
|
|
// If we have multiple matches, exact should win
|
|
let result = try ApplicationFinder.findApplication(identifier: "Finder")
|
|
#expect(result.localizedName == "Finder")
|
|
#expect(result.bundleIdentifier == "com.apple.finder")
|
|
}
|
|
|
|
@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.isEmpty)
|
|
|
|
// Test search performance doesn't degrade with app list size
|
|
let startTime = CFAbsoluteTimeGetCurrent()
|
|
do {
|
|
_ = try ApplicationFinder.findApplication(identifier: "Finder")
|
|
let duration = CFAbsoluteTimeGetCurrent() - startTime
|
|
#expect(duration < 1.0) // Should complete within 1 second
|
|
} catch {
|
|
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)
|
|
]
|
|
)
|
|
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 }
|
|
#expect(isInList == shouldBeRunning)
|
|
} catch {
|
|
if shouldBeRunning {
|
|
Issue.record("System app \(appName) should be running but was not found")
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test("SystemUIServer detection (optional)", .tags(.unit))
|
|
func systemUIServerDetection() throws {
|
|
// SystemUIServer may not be running on all macOS configurations
|
|
// This test is more lenient and just checks if detection works when present
|
|
do {
|
|
let result = try ApplicationFinder.findApplication(identifier: "SystemUIServer")
|
|
#expect(result.localizedName != nil)
|
|
// Just verify we can find it - don't check list consistency since
|
|
// SystemUIServer might not be included in the filtered application list
|
|
} catch ApplicationError.notFound {
|
|
// SystemUIServer not running - this is acceptable on some configurations
|
|
Logger.shared.debug("SystemUIServer not found - acceptable on some macOS configurations")
|
|
}
|
|
}
|
|
|
|
@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 }
|
|
#expect(appInfo?.is_active == true)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Extended Test Suite for Edge Cases
|
|
|
|
@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)
|
|
#expect(throws: (any Error).self) {
|
|
try ApplicationFinder.findApplication(identifier: longIdentifier)
|
|
}
|
|
}
|
|
|
|
@Test(
|
|
"Unicode identifiers are handled correctly",
|
|
arguments: ["😀App", "App™", "Приложение", "アプリ"]
|
|
)
|
|
func unicodeIdentifiers(identifier: String) {
|
|
// Should not crash, either finds or throws appropriate error
|
|
do {
|
|
let result = try ApplicationFinder.findApplication(identifier: identifier)
|
|
#expect(result.localizedName != nil)
|
|
} catch {
|
|
// Test passes if an error is thrown for invalid identifier
|
|
#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
|
|
await withTaskGroup(of: Bool.self) { group in
|
|
for _ in 0..<10 {
|
|
group.addTask {
|
|
do {
|
|
let result = try ApplicationFinder.findApplication(identifier: "Finder")
|
|
return result.localizedName == "Finder"
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
var successCount = 0
|
|
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.isEmpty)
|
|
}
|
|
|
|
// If we get here without crashing, memory management is working
|
|
#expect(Bool(true))
|
|
}
|
|
|
|
@Test("Fuzzy matching finds similar apps", .tags(.fast))
|
|
func fuzzyMatchingFindsSimilarApps() throws {
|
|
// Test that fuzzy matching can find apps with typos
|
|
let result = try ApplicationFinder.findApplication(identifier: "Finderr")
|
|
// Should find "Finder" despite the typo
|
|
#expect(result.localizedName?.lowercased().contains("finder") == true)
|
|
}
|
|
|
|
@Test("Non-existent app throws error", .tags(.fast))
|
|
func nonExistentAppThrowsError() {
|
|
// Test with a completely non-existent app name
|
|
#expect(throws: ApplicationError.notFound("XyzNonExistentApp123")) {
|
|
_ = try ApplicationFinder.findApplication(identifier: "XyzNonExistentApp123")
|
|
}
|
|
}
|
|
|
|
@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()
|
|
let previous = apps[index - 1].app_name.lowercased()
|
|
#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
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Browser Helper Filtering Tests
|
|
|
|
@Test("Browser helper filtering for Chrome searches", .tags(.browserFiltering))
|
|
func browserHelperFilteringChrome() {
|
|
// Test that Chrome helper processes are filtered out when searching for "chrome"
|
|
// Note: This test documents expected behavior even when Chrome isn't running
|
|
|
|
do {
|
|
let result = try ApplicationFinder.findApplication(identifier: "chrome")
|
|
// If found, should be the main Chrome app, not a helper
|
|
if let appName = result.localizedName?.lowercased() {
|
|
#expect(!appName.contains("helper"))
|
|
#expect(!appName.contains("renderer"))
|
|
#expect(!appName.contains("utility"))
|
|
#expect(appName.contains("chrome"))
|
|
}
|
|
} catch {
|
|
// Chrome might not be running, which is okay for this test
|
|
// The important thing is that the filtering logic exists
|
|
print("Chrome not found, which is acceptable for browser helper filtering test")
|
|
}
|
|
}
|
|
|
|
@Test("Browser helper filtering for Safari searches", .tags(.browserFiltering))
|
|
func browserHelperFilteringSafari() {
|
|
// Test that Safari helper processes are filtered out when searching for "safari"
|
|
|
|
do {
|
|
let result = try ApplicationFinder.findApplication(identifier: "safari")
|
|
// If found, should be the main Safari app, not a helper
|
|
if let appName = result.localizedName?.lowercased() {
|
|
#expect(!appName.contains("helper"))
|
|
#expect(!appName.contains("renderer"))
|
|
#expect(!appName.contains("utility"))
|
|
#expect(appName.contains("safari"))
|
|
}
|
|
} catch {
|
|
// Safari might not be running, which is okay for this test
|
|
print("Safari not found, which is acceptable for browser helper filtering test")
|
|
}
|
|
}
|
|
|
|
@Test("Non-browser searches should not filter helpers", .tags(.browserFiltering))
|
|
func nonBrowserSearchesPreserveHelpers() {
|
|
// Test that non-browser searches still find helper processes if that's what's being searched for
|
|
|
|
// This tests that helper filtering only applies to browser identifiers
|
|
let nonBrowserIdentifiers = ["finder", "textedit", "calculator", "activity monitor"]
|
|
|
|
for identifier in nonBrowserIdentifiers {
|
|
do {
|
|
let result = try ApplicationFinder.findApplication(identifier: identifier)
|
|
// Should find the app regardless of whether it's a "helper" (for non-browsers)
|
|
#expect(result.localizedName != nil)
|
|
} catch {
|
|
// App might not be running, which is fine for this test
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test("Browser error messages are more specific", .tags(.browserFiltering))
|
|
func browserSpecificErrorMessages() {
|
|
// Test that browser-specific error messages are provided when browsers aren't found
|
|
|
|
let browserIdentifiers = ["chrome", "firefox", "edge"]
|
|
|
|
for browser in browserIdentifiers {
|
|
do {
|
|
_ = try ApplicationFinder.findApplication(identifier: browser)
|
|
// If browser is found, test passes
|
|
} catch let ApplicationError.notFound(identifier) {
|
|
// Should get a not found error with the identifier
|
|
#expect(identifier == browser)
|
|
// The error logging would contain browser-specific message, but we can't test that here
|
|
} catch {
|
|
Issue.record("Expected ApplicationError.notFound for browser '\(browser)', got \(error)")
|
|
}
|
|
}
|
|
}
|
|
}
|