mirror of
https://github.com/samsonjs/Peekaboo.git
synced 2026-04-27 15:07:41 +00:00
Apply SwiftFormat changes for v1.0.0 release
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
1a2a817822
commit
0bec93e364
14 changed files with 95 additions and 77 deletions
|
|
@ -26,7 +26,7 @@ final class ApplicationFinder: Sendable {
|
||||||
|
|
||||||
// Find all possible matches
|
// Find all possible matches
|
||||||
let allMatches = findAllMatches(for: identifier, in: runningApps)
|
let allMatches = findAllMatches(for: identifier, in: runningApps)
|
||||||
|
|
||||||
// Filter out browser helpers for common browser searches
|
// Filter out browser helpers for common browser searches
|
||||||
let matches = filterBrowserHelpers(matches: allMatches, identifier: identifier)
|
let matches = filterBrowserHelpers(matches: allMatches, identifier: identifier)
|
||||||
|
|
||||||
|
|
@ -180,7 +180,7 @@ final class ApplicationFinder: Sendable {
|
||||||
// Provide browser-specific error messages
|
// Provide browser-specific error messages
|
||||||
let browserIdentifiers = ["chrome", "safari", "firefox", "edge", "brave", "arc", "opera"]
|
let browserIdentifiers = ["chrome", "safari", "firefox", "edge", "brave", "arc", "opera"]
|
||||||
let lowerIdentifier = identifier.lowercased()
|
let lowerIdentifier = identifier.lowercased()
|
||||||
|
|
||||||
if browserIdentifiers.contains(lowerIdentifier) {
|
if browserIdentifiers.contains(lowerIdentifier) {
|
||||||
// Logger.shared.error("\(identifier.capitalized) browser is not running or not found")
|
// Logger.shared.error("\(identifier.capitalized) browser is not running or not found")
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -319,48 +319,49 @@ final class ApplicationFinder: Sendable {
|
||||||
|
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func filterBrowserHelpers(matches: [AppMatch], identifier: String) -> [AppMatch] {
|
private static func filterBrowserHelpers(matches: [AppMatch], identifier: String) -> [AppMatch] {
|
||||||
// Define common browser identifiers that should filter out helpers
|
// Define common browser identifiers that should filter out helpers
|
||||||
let browserIdentifiers = ["chrome", "safari", "firefox", "edge", "brave", "arc", "opera"]
|
let browserIdentifiers = ["chrome", "safari", "firefox", "edge", "brave", "arc", "opera"]
|
||||||
let lowerIdentifier = identifier.lowercased()
|
let lowerIdentifier = identifier.lowercased()
|
||||||
|
|
||||||
// Check if the search is for a common browser
|
// Check if the search is for a common browser
|
||||||
guard browserIdentifiers.contains(lowerIdentifier) else {
|
guard browserIdentifiers.contains(lowerIdentifier) else {
|
||||||
return matches // No filtering for non-browser searches
|
return matches // No filtering for non-browser searches
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logger.shared.debug("Filtering browser helpers for '\(identifier)' search")
|
// Logger.shared.debug("Filtering browser helpers for '\(identifier)' search")
|
||||||
|
|
||||||
// Filter out helper processes for browser searches
|
// Filter out helper processes for browser searches
|
||||||
let filteredMatches = matches.filter { match in
|
let filteredMatches = matches.filter { match in
|
||||||
guard let appName = match.app.localizedName?.lowercased() else { return true }
|
guard let appName = match.app.localizedName?.lowercased() else { return true }
|
||||||
|
|
||||||
// Exclude obvious helper processes
|
// Exclude obvious helper processes
|
||||||
let isHelper = appName.contains("helper") ||
|
let isHelper = appName.contains("helper") ||
|
||||||
appName.contains("renderer") ||
|
appName.contains("renderer") ||
|
||||||
appName.contains("utility") ||
|
appName.contains("utility") ||
|
||||||
appName.contains("plugin") ||
|
appName.contains("plugin") ||
|
||||||
appName.contains("service") ||
|
appName.contains("service") ||
|
||||||
appName.contains("crashpad") ||
|
appName.contains("crashpad") ||
|
||||||
appName.contains("gpu") ||
|
appName.contains("gpu") ||
|
||||||
appName.contains("background")
|
appName.contains("background")
|
||||||
|
|
||||||
if isHelper {
|
if isHelper {
|
||||||
// Logger.shared.debug("Filtering out helper process: \(appName)")
|
// Logger.shared.debug("Filtering out helper process: \(appName)")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we filtered out all matches, return the original matches to avoid "not found" errors
|
// If we filtered out all matches, return the original matches to avoid "not found" errors
|
||||||
// But log a warning about this case
|
// But log a warning about this case
|
||||||
if filteredMatches.isEmpty && !matches.isEmpty {
|
if filteredMatches.isEmpty && !matches.isEmpty {
|
||||||
// Logger.shared.debug("All matches were filtered as helpers, returning original matches to avoid 'not found' error")
|
// Logger.shared.debug("All matches were filtered as helpers, returning original matches to avoid 'not
|
||||||
|
// found' error")
|
||||||
return matches
|
return matches
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logger.shared.debug("After browser helper filtering: \(filteredMatches.count) matches remaining")
|
// Logger.shared.debug("After browser helper filtering: \(filteredMatches.count) matches remaining")
|
||||||
return filteredMatches
|
return filteredMatches
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -164,10 +164,15 @@ struct ImageCommand: AsyncParsableCommand {
|
||||||
return savedFiles
|
return savedFiles
|
||||||
}
|
}
|
||||||
|
|
||||||
private func captureAllScreensWithFallback(displays: [CGDirectDisplayID]) async throws(CaptureError) -> [SavedFile] {
|
private func captureAllScreensWithFallback(displays: [CGDirectDisplayID]) async throws(CaptureError)
|
||||||
|
-> [SavedFile] {
|
||||||
var savedFiles: [SavedFile] = []
|
var savedFiles: [SavedFile] = []
|
||||||
for (index, displayID) in displays.enumerated() {
|
for (index, displayID) in displays.enumerated() {
|
||||||
let savedFile = try await captureSingleDisplayWithFallback(displayID: displayID, index: index, labelSuffix: "")
|
let savedFile = try await captureSingleDisplayWithFallback(
|
||||||
|
displayID: displayID,
|
||||||
|
index: index,
|
||||||
|
labelSuffix: ""
|
||||||
|
)
|
||||||
savedFiles.append(savedFile)
|
savedFiles.append(savedFile)
|
||||||
}
|
}
|
||||||
return savedFiles
|
return savedFiles
|
||||||
|
|
@ -243,12 +248,12 @@ struct ImageCommand: AsyncParsableCommand {
|
||||||
let availableTitles = windows.map { "\"\($0.title)\"" }.joined(separator: ", ")
|
let availableTitles = windows.map { "\"\($0.title)\"" }.joined(separator: ", ")
|
||||||
let searchTerm = windowTitle
|
let searchTerm = windowTitle
|
||||||
let appName = targetApp.localizedName ?? "Unknown"
|
let appName = targetApp.localizedName ?? "Unknown"
|
||||||
|
|
||||||
Logger.shared.debug(
|
Logger.shared.debug(
|
||||||
"Window not found. Searched for '\(searchTerm)' in \(appName). " +
|
"Window not found. Searched for '\(searchTerm)' in \(appName). " +
|
||||||
"Available windows: \(availableTitles)"
|
"Available windows: \(availableTitles)"
|
||||||
)
|
)
|
||||||
|
|
||||||
throw CaptureError.windowTitleNotFound(searchTerm, appName, availableTitles)
|
throw CaptureError.windowTitleNotFound(searchTerm, appName, availableTitles)
|
||||||
}
|
}
|
||||||
targetWindow = window
|
targetWindow = window
|
||||||
|
|
@ -410,38 +415,38 @@ struct ImageCommand: AsyncParsableCommand {
|
||||||
throw CaptureError.windowCaptureFailed(error)
|
throw CaptureError.windowCaptureFailed(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func captureFrontmostWindow() async throws -> [SavedFile] {
|
private func captureFrontmostWindow() async throws -> [SavedFile] {
|
||||||
Logger.shared.debug("Capturing frontmost window")
|
Logger.shared.debug("Capturing frontmost window")
|
||||||
|
|
||||||
// Get the frontmost (active) application
|
// Get the frontmost (active) application
|
||||||
guard let frontmostApp = NSWorkspace.shared.frontmostApplication else {
|
guard let frontmostApp = NSWorkspace.shared.frontmostApplication else {
|
||||||
throw CaptureError.appNotFound("No frontmost application found")
|
throw CaptureError.appNotFound("No frontmost application found")
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.shared.debug("Frontmost app: \(frontmostApp.localizedName ?? "Unknown")")
|
Logger.shared.debug("Frontmost app: \(frontmostApp.localizedName ?? "Unknown")")
|
||||||
|
|
||||||
// Get windows for the frontmost app
|
// Get windows for the frontmost app
|
||||||
let windows = try WindowManager.getWindowsForApp(pid: frontmostApp.processIdentifier)
|
let windows = try WindowManager.getWindowsForApp(pid: frontmostApp.processIdentifier)
|
||||||
guard !windows.isEmpty else {
|
guard !windows.isEmpty else {
|
||||||
throw CaptureError.noWindowsFound(frontmostApp.localizedName ?? "frontmost application")
|
throw CaptureError.noWindowsFound(frontmostApp.localizedName ?? "frontmost application")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the frontmost window (index 0)
|
// Get the frontmost window (index 0)
|
||||||
let frontmostWindow = windows[0]
|
let frontmostWindow = windows[0]
|
||||||
|
|
||||||
Logger.shared.debug("Capturing frontmost window: '\(frontmostWindow.title)'")
|
Logger.shared.debug("Capturing frontmost window: '\(frontmostWindow.title)'")
|
||||||
|
|
||||||
// Generate output path
|
// Generate output path
|
||||||
let timestamp = DateFormatter.timestamp.string(from: Date())
|
let timestamp = DateFormatter.timestamp.string(from: Date())
|
||||||
let appName = frontmostApp.localizedName ?? "UnknownApp"
|
let appName = frontmostApp.localizedName ?? "UnknownApp"
|
||||||
let safeName = appName.replacingOccurrences(of: " ", with: "_")
|
let safeName = appName.replacingOccurrences(of: " ", with: "_")
|
||||||
let fileName = "frontmost_\(safeName)_\(timestamp).\(format.rawValue)"
|
let fileName = "frontmost_\(safeName)_\(timestamp).\(format.rawValue)"
|
||||||
let filePath = OutputPathResolver.getOutputPathWithFallback(basePath: path, fileName: fileName)
|
let filePath = OutputPathResolver.getOutputPathWithFallback(basePath: path, fileName: fileName)
|
||||||
|
|
||||||
// Capture the window
|
// Capture the window
|
||||||
try await captureWindow(frontmostWindow, to: filePath)
|
try await captureWindow(frontmostWindow, to: filePath)
|
||||||
|
|
||||||
return [SavedFile(
|
return [SavedFile(
|
||||||
path: filePath,
|
path: filePath,
|
||||||
item_label: appName,
|
item_label: appName,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import Foundation
|
|
||||||
import AppKit
|
import AppKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
struct ImageErrorHandler {
|
enum ImageErrorHandler {
|
||||||
static func handleError(_ error: Error, jsonOutput: Bool) {
|
static func handleError(_ error: Error, jsonOutput: Bool) {
|
||||||
let captureError: CaptureError = if let err = error as? CaptureError {
|
let captureError: CaptureError = if let err = error as? CaptureError {
|
||||||
err
|
err
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import Foundation
|
|
||||||
import CoreGraphics
|
import CoreGraphics
|
||||||
|
import Foundation
|
||||||
import ImageIO
|
import ImageIO
|
||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,17 @@ struct JSONResponse: Codable {
|
||||||
let debug_logs: [String]
|
let debug_logs: [String]
|
||||||
let error: ErrorInfo?
|
let error: ErrorInfo?
|
||||||
|
|
||||||
init(success: Bool, data: Any? = nil, messages: [String]? = nil, debugLogs: [String] = [], error: ErrorInfo? = nil) {
|
init(
|
||||||
|
success: Bool,
|
||||||
|
data: Any? = nil,
|
||||||
|
messages: [String]? = nil,
|
||||||
|
debugLogs: [String] = [],
|
||||||
|
error: ErrorInfo? = nil
|
||||||
|
) {
|
||||||
self.success = success
|
self.success = success
|
||||||
self.data = data.map(AnyCodable.init)
|
self.data = data.map(AnyCodable.init)
|
||||||
self.messages = messages
|
self.messages = messages
|
||||||
self.debug_logs = debugLogs
|
debug_logs = debugLogs
|
||||||
self.error = error
|
self.error = error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ struct ListCommand: AsyncParsableCommand {
|
||||||
subcommands: [AppsSubcommand.self, WindowsSubcommand.self, ServerStatusSubcommand.self],
|
subcommands: [AppsSubcommand.self, WindowsSubcommand.self, ServerStatusSubcommand.self],
|
||||||
defaultSubcommand: AppsSubcommand.self
|
defaultSubcommand: AppsSubcommand.self
|
||||||
)
|
)
|
||||||
|
|
||||||
func run() async throws {
|
func run() async throws {
|
||||||
// Root command doesn't do anything, subcommands handle everything
|
// Root command doesn't do anything, subcommands handle everything
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,10 +61,10 @@ struct WindowBounds: Codable, Sendable {
|
||||||
let y_coordinate: Int
|
let y_coordinate: Int
|
||||||
let width: Int
|
let width: Int
|
||||||
let height: Int
|
let height: Int
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case x_coordinate = "x_coordinate"
|
case x_coordinate
|
||||||
case y_coordinate = "y_coordinate"
|
case y_coordinate
|
||||||
case width
|
case width
|
||||||
case height
|
case height
|
||||||
}
|
}
|
||||||
|
|
@ -149,7 +149,8 @@ enum CaptureError: Error, LocalizedError, Sendable {
|
||||||
if !availableTitles.isEmpty {
|
if !availableTitles.isEmpty {
|
||||||
message += " Available windows: \(availableTitles)."
|
message += " Available windows: \(availableTitles)."
|
||||||
}
|
}
|
||||||
message += " Note: For URLs, try without the protocol (e.g., 'example.com:8080' instead of 'http://example.com:8080')."
|
message +=
|
||||||
|
" Note: For URLs, try without the protocol (e.g., 'example.com:8080' instead of 'http://example.com:8080')."
|
||||||
return message
|
return message
|
||||||
case let .windowCaptureFailed(underlyingError):
|
case let .windowCaptureFailed(underlyingError):
|
||||||
var message = "Failed to capture the specified window."
|
var message = "Failed to capture the specified window."
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import Foundation
|
||||||
|
|
||||||
struct OutputPathResolver: Sendable {
|
struct OutputPathResolver: Sendable {
|
||||||
static func getOutputPath(basePath: String?, fileName: String, screenIndex: Int? = nil) -> String {
|
static func getOutputPath(basePath: String?, fileName: String, screenIndex: Int? = nil) -> String {
|
||||||
if let basePath = basePath {
|
if let basePath {
|
||||||
validatePath(basePath)
|
validatePath(basePath)
|
||||||
return determineOutputPath(basePath: basePath, fileName: fileName, screenIndex: screenIndex)
|
return determineOutputPath(basePath: basePath, fileName: fileName, screenIndex: screenIndex)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -11,7 +11,7 @@ struct OutputPathResolver: Sendable {
|
||||||
}
|
}
|
||||||
|
|
||||||
static func getOutputPathWithFallback(basePath: String?, fileName: String) -> String {
|
static func getOutputPathWithFallback(basePath: String?, fileName: String) -> String {
|
||||||
if let basePath = basePath {
|
if let basePath {
|
||||||
validatePath(basePath)
|
validatePath(basePath)
|
||||||
return determineOutputPathWithFallback(basePath: basePath, fileName: fileName)
|
return determineOutputPathWithFallback(basePath: basePath, fileName: fileName)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -118,17 +118,17 @@ struct OutputPathResolver: Sendable {
|
||||||
return "\(basePath)/\(fileName)"
|
return "\(basePath)/\(fileName)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func validatePath(_ path: String) {
|
private static func validatePath(_ path: String) {
|
||||||
// Check for path traversal attempts
|
// Check for path traversal attempts
|
||||||
if path.contains("../") || path.contains("..\\") {
|
if path.contains("../") || path.contains("..\\") {
|
||||||
// Logger.shared.debug("Potential path traversal detected in path: \(path)")
|
// Logger.shared.debug("Potential path traversal detected in path: \(path)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for system-sensitive paths
|
// Check for system-sensitive paths
|
||||||
let sensitivePathPrefixes = ["/etc/", "/usr/", "/bin/", "/sbin/", "/System/", "/Library/System/"]
|
let sensitivePathPrefixes = ["/etc/", "/usr/", "/bin/", "/sbin/", "/System/", "/Library/System/"]
|
||||||
let normalizedPath = (path as NSString).standardizingPath
|
let normalizedPath = (path as NSString).standardizingPath
|
||||||
|
|
||||||
for prefix in sensitivePathPrefixes where normalizedPath.hasPrefix(prefix) {
|
for prefix in sensitivePathPrefixes where normalizedPath.hasPrefix(prefix) {
|
||||||
// Logger.shared.debug("Path points to system directory: \(path) -> \(normalizedPath)")
|
// Logger.shared.debug("Path points to system directory: \(path) -> \(normalizedPath)")
|
||||||
break
|
break
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import Foundation
|
|
||||||
import CoreGraphics
|
import CoreGraphics
|
||||||
|
import Foundation
|
||||||
@preconcurrency import ScreenCaptureKit
|
@preconcurrency import ScreenCaptureKit
|
||||||
|
|
||||||
struct ScreenCapture: Sendable {
|
struct ScreenCapture: Sendable {
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@ struct PeekabooCommand: AsyncParsableCommand {
|
||||||
subcommands: [ImageCommand.self, ListCommand.self],
|
subcommands: [ImageCommand.self, ListCommand.self],
|
||||||
defaultSubcommand: ImageCommand.self
|
defaultSubcommand: ImageCommand.self
|
||||||
)
|
)
|
||||||
|
|
||||||
func run() async throws {
|
func run() async throws {
|
||||||
// Root command doesn't do anything, subcommands handle everything
|
// Root command doesn't do anything, subcommands handle everything
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -423,14 +423,14 @@ struct ApplicationFinderEdgeCaseTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Browser Helper Filtering Tests
|
// MARK: - Browser Helper Filtering Tests
|
||||||
|
|
||||||
@Test("Browser helper filtering for Chrome searches", .tags(.browserFiltering))
|
@Test("Browser helper filtering for Chrome searches", .tags(.browserFiltering))
|
||||||
func browserHelperFilteringChrome() {
|
func browserHelperFilteringChrome() {
|
||||||
// Test that Chrome helper processes are filtered out when searching for "chrome"
|
// Test that Chrome helper processes are filtered out when searching for "chrome"
|
||||||
// Note: This test documents expected behavior even when Chrome isn't running
|
// Note: This test documents expected behavior even when Chrome isn't running
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let result = try ApplicationFinder.findApplication(identifier: "chrome")
|
let result = try ApplicationFinder.findApplication(identifier: "chrome")
|
||||||
// If found, should be the main Chrome app, not a helper
|
// If found, should be the main Chrome app, not a helper
|
||||||
|
|
@ -446,11 +446,11 @@ struct ApplicationFinderEdgeCaseTests {
|
||||||
print("Chrome not found, which is acceptable for browser helper filtering test")
|
print("Chrome not found, which is acceptable for browser helper filtering test")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test("Browser helper filtering for Safari searches", .tags(.browserFiltering))
|
@Test("Browser helper filtering for Safari searches", .tags(.browserFiltering))
|
||||||
func browserHelperFilteringSafari() {
|
func browserHelperFilteringSafari() {
|
||||||
// Test that Safari helper processes are filtered out when searching for "safari"
|
// Test that Safari helper processes are filtered out when searching for "safari"
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let result = try ApplicationFinder.findApplication(identifier: "safari")
|
let result = try ApplicationFinder.findApplication(identifier: "safari")
|
||||||
// If found, should be the main Safari app, not a helper
|
// If found, should be the main Safari app, not a helper
|
||||||
|
|
@ -465,14 +465,14 @@ struct ApplicationFinderEdgeCaseTests {
|
||||||
print("Safari not found, which is acceptable for browser helper filtering test")
|
print("Safari not found, which is acceptable for browser helper filtering test")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test("Non-browser searches should not filter helpers", .tags(.browserFiltering))
|
@Test("Non-browser searches should not filter helpers", .tags(.browserFiltering))
|
||||||
func nonBrowserSearchesPreserveHelpers() {
|
func nonBrowserSearchesPreserveHelpers() {
|
||||||
// Test that non-browser searches still find helper processes if that's what's being searched for
|
// 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
|
// This tests that helper filtering only applies to browser identifiers
|
||||||
let nonBrowserIdentifiers = ["finder", "textedit", "calculator", "activity monitor"]
|
let nonBrowserIdentifiers = ["finder", "textedit", "calculator", "activity monitor"]
|
||||||
|
|
||||||
for identifier in nonBrowserIdentifiers {
|
for identifier in nonBrowserIdentifiers {
|
||||||
do {
|
do {
|
||||||
let result = try ApplicationFinder.findApplication(identifier: identifier)
|
let result = try ApplicationFinder.findApplication(identifier: identifier)
|
||||||
|
|
@ -484,13 +484,13 @@ struct ApplicationFinderEdgeCaseTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test("Browser error messages are more specific", .tags(.browserFiltering))
|
@Test("Browser error messages are more specific", .tags(.browserFiltering))
|
||||||
func browserSpecificErrorMessages() {
|
func browserSpecificErrorMessages() {
|
||||||
// Test that browser-specific error messages are provided when browsers aren't found
|
// Test that browser-specific error messages are provided when browsers aren't found
|
||||||
|
|
||||||
let browserIdentifiers = ["chrome", "firefox", "edge"]
|
let browserIdentifiers = ["chrome", "firefox", "edge"]
|
||||||
|
|
||||||
for browser in browserIdentifiers {
|
for browser in browserIdentifiers {
|
||||||
do {
|
do {
|
||||||
_ = try ApplicationFinder.findApplication(identifier: browser)
|
_ = try ApplicationFinder.findApplication(identifier: browser)
|
||||||
|
|
|
||||||
|
|
@ -263,7 +263,6 @@ struct ImageCommandTests {
|
||||||
#expect(command.screenIndex == index)
|
#expect(command.screenIndex == index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test(
|
@Test(
|
||||||
"Window index boundary values",
|
"Window index boundary values",
|
||||||
arguments: [0, 1, 10, 9999]
|
arguments: [0, 1, 10, 9999]
|
||||||
|
|
@ -273,7 +272,6 @@ struct ImageCommandTests {
|
||||||
#expect(command.windowIndex == index)
|
#expect(command.windowIndex == index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test("Error handling for invalid combinations", .tags(.fast))
|
@Test("Error handling for invalid combinations", .tags(.fast))
|
||||||
func invalidCombinations() {
|
func invalidCombinations() {
|
||||||
// Window capture without app should fail in execution
|
// Window capture without app should fail in execution
|
||||||
|
|
@ -340,7 +338,11 @@ struct ImageCommandPathHandlingTests {
|
||||||
func singleScreenFilePath() {
|
func singleScreenFilePath() {
|
||||||
// For single screen, should use exact path
|
// For single screen, should use exact path
|
||||||
let fileName = "screen_1_20250608_120000.png"
|
let fileName = "screen_1_20250608_120000.png"
|
||||||
let result = OutputPathResolver.determineOutputPath(basePath: "/tmp/my-screenshot.png", fileName: fileName, screenIndex: 0)
|
let result = OutputPathResolver.determineOutputPath(
|
||||||
|
basePath: "/tmp/my-screenshot.png",
|
||||||
|
fileName: fileName,
|
||||||
|
screenIndex: 0
|
||||||
|
)
|
||||||
|
|
||||||
#expect(result == "/tmp/my-screenshot.png")
|
#expect(result == "/tmp/my-screenshot.png")
|
||||||
}
|
}
|
||||||
|
|
@ -575,7 +577,10 @@ struct ImageCommandErrorHandlingTests {
|
||||||
// This test validates the logic without actually creating directories
|
// This test validates the logic without actually creating directories
|
||||||
|
|
||||||
let fileName = "screen_1_20250608_120001.png"
|
let fileName = "screen_1_20250608_120001.png"
|
||||||
let result = OutputPathResolver.determineOutputPath(basePath: "/tmp/test-path-creation/file.png", fileName: fileName)
|
let result = OutputPathResolver.determineOutputPath(
|
||||||
|
basePath: "/tmp/test-path-creation/file.png",
|
||||||
|
fileName: fileName
|
||||||
|
)
|
||||||
|
|
||||||
// Should return the intended path even if directory creation might fail
|
// Should return the intended path even if directory creation might fail
|
||||||
#expect(result == "/tmp/test-path-creation/file_1_20250608_120001.png")
|
#expect(result == "/tmp/test-path-creation/file_1_20250608_120001.png")
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ struct JSONOutputTests {
|
||||||
struct TestWrapper: Codable {
|
struct TestWrapper: Codable {
|
||||||
let value: AnyCodable
|
let value: AnyCodable
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test string
|
// Test string
|
||||||
let stringWrapper = TestWrapper(value: AnyCodable("test string"))
|
let stringWrapper = TestWrapper(value: AnyCodable("test string"))
|
||||||
let stringData = try JSONEncoder().encode(stringWrapper)
|
let stringData = try JSONEncoder().encode(stringWrapper)
|
||||||
|
|
|
||||||
|
|
@ -139,21 +139,21 @@ struct ScreenshotValidationTests {
|
||||||
// 1. The physical pixel dimensions of the display
|
// 1. The physical pixel dimensions of the display
|
||||||
// 2. How macOS reports display information
|
// 2. How macOS reports display information
|
||||||
// 3. Whether the display is Retina or not
|
// 3. Whether the display is Retina or not
|
||||||
//
|
//
|
||||||
// Instead of trying to match exact dimensions, verify:
|
// Instead of trying to match exact dimensions, verify:
|
||||||
// - The image has reasonable dimensions
|
// - The image has reasonable dimensions
|
||||||
// - The aspect ratio is preserved
|
// - The aspect ratio is preserved
|
||||||
|
|
||||||
#expect(image.size.width > 0)
|
#expect(image.size.width > 0)
|
||||||
#expect(image.size.height > 0)
|
#expect(image.size.height > 0)
|
||||||
#expect(image.size.width <= 8192) // Max reasonable display width
|
#expect(image.size.width <= 8192) // Max reasonable display width
|
||||||
#expect(image.size.height <= 8192) // Max reasonable display height
|
#expect(image.size.height <= 8192) // Max reasonable display height
|
||||||
|
|
||||||
// Verify aspect ratio is reasonable (between 1:3 and 3:1)
|
// Verify aspect ratio is reasonable (between 1:3 and 3:1)
|
||||||
let aspectRatio = image.size.width / image.size.height
|
let aspectRatio = image.size.width / image.size.height
|
||||||
#expect(aspectRatio > 0.33)
|
#expect(aspectRatio > 0.33)
|
||||||
#expect(aspectRatio < 3.0)
|
#expect(aspectRatio < 3.0)
|
||||||
|
|
||||||
print("Display \(index): captured \(image.size.width)x\(image.size.height)")
|
print("Display \(index): captured \(image.size.width)x\(image.size.height)")
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -204,7 +204,7 @@ struct ScreenshotValidationTests {
|
||||||
// - Whether screen recording permission dialogs appear
|
// - Whether screen recording permission dialogs appear
|
||||||
#expect(averageTime < 1.5) // Average should be under 1.5 seconds
|
#expect(averageTime < 1.5) // Average should be under 1.5 seconds
|
||||||
#expect(maxTime < 3.0) // Max should be under 3 seconds
|
#expect(maxTime < 3.0) // Max should be under 3 seconds
|
||||||
|
|
||||||
// Performance benchmarks on typical hardware:
|
// Performance benchmarks on typical hardware:
|
||||||
// - Single 1080p display: ~100-200ms
|
// - Single 1080p display: ~100-200ms
|
||||||
// - Single 4K display: ~300-500ms
|
// - Single 4K display: ~300-500ms
|
||||||
|
|
@ -315,18 +315,18 @@ struct ScreenshotValidationTests {
|
||||||
format: ImageFormat
|
format: ImageFormat
|
||||||
) async throws -> ImageCaptureData {
|
) async throws -> ImageCaptureData {
|
||||||
let availableContent = try await SCShareableContent.current
|
let availableContent = try await SCShareableContent.current
|
||||||
|
|
||||||
guard let scDisplay = availableContent.displays.first(where: { $0.displayID == displayID }) else {
|
guard let scDisplay = availableContent.displays.first(where: { $0.displayID == displayID }) else {
|
||||||
throw CaptureError.captureCreationFailed(nil)
|
throw CaptureError.captureCreationFailed(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
let filter = SCContentFilter(display: scDisplay, excludingWindows: [])
|
let filter = SCContentFilter(display: scDisplay, excludingWindows: [])
|
||||||
|
|
||||||
let configuration = SCStreamConfiguration()
|
let configuration = SCStreamConfiguration()
|
||||||
configuration.backgroundColor = .clear
|
configuration.backgroundColor = .clear
|
||||||
configuration.shouldBeOpaque = true
|
configuration.shouldBeOpaque = true
|
||||||
configuration.showsCursor = false
|
configuration.showsCursor = false
|
||||||
|
|
||||||
let image = try await SCScreenshotManager.captureImage(
|
let image = try await SCScreenshotManager.captureImage(
|
||||||
contentFilter: filter,
|
contentFilter: filter,
|
||||||
configuration: configuration
|
configuration: configuration
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue