mirror of
https://github.com/samsonjs/Peekaboo.git
synced 2026-04-27 15:07:41 +00:00
- Fixed all Swift test compilation errors and SwiftLint violations - Enhanced test host app with permission status display and CLI availability checking - Refactored ImageCommand.swift to improve readability and reduce function length - Updated all tests to use proper Swift Testing patterns - Added comprehensive local testing framework for screenshot functionality - Updated documentation with proper test execution instructions - Applied SwiftFormat to all Swift files and achieved zero serious linting issues 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
320 lines
11 KiB
Swift
320 lines
11 KiB
Swift
import AppKit
|
|
import CoreGraphics
|
|
@testable import peekaboo
|
|
import Testing
|
|
|
|
@Suite(
|
|
"Screenshot Validation Tests",
|
|
.tags(.localOnly, .screenshot, .integration),
|
|
.enabled(if: ProcessInfo.processInfo.environment["RUN_LOCAL_TESTS"] == "true")
|
|
)
|
|
struct ScreenshotValidationTests {
|
|
// MARK: - Image Analysis Tests
|
|
|
|
@Test("Validate screenshot contains expected content", .tags(.imageAnalysis))
|
|
func validateScreenshotContent() throws {
|
|
// Create a temporary test window with known content
|
|
let testWindow = createTestWindow(withContent: .text("PEEKABOO_TEST_12345"))
|
|
defer { testWindow.close() }
|
|
|
|
// Give window time to render
|
|
RunLoop.current.run(until: Date().addingTimeInterval(0.5))
|
|
|
|
// Capture the window
|
|
let windowID = CGWindowID(testWindow.windowNumber)
|
|
|
|
let outputPath = "/tmp/peekaboo-content-test.png"
|
|
defer { try? FileManager.default.removeItem(atPath: outputPath) }
|
|
|
|
let captureData = try captureWindowToFile(windowID: windowID, path: outputPath, format: .png)
|
|
|
|
// Load and analyze the image
|
|
guard let image = NSImage(contentsOfFile: outputPath) else {
|
|
Issue.record("Failed to load captured image")
|
|
return
|
|
}
|
|
|
|
// Verify image properties
|
|
#expect(image.size.width > 0)
|
|
#expect(image.size.height > 0)
|
|
|
|
// In a real test, we could use OCR or pixel analysis to verify content
|
|
print("Captured image size: \(image.size)")
|
|
}
|
|
|
|
@Test("Compare screenshots for visual regression", .tags(.regression))
|
|
func visualRegressionTest() throws {
|
|
// Create test window with specific visual pattern
|
|
let testWindow = createTestWindow(withContent: .grid)
|
|
defer { testWindow.close() }
|
|
|
|
RunLoop.current.run(until: Date().addingTimeInterval(0.5))
|
|
|
|
let windowID = CGWindowID(testWindow.windowNumber)
|
|
|
|
// Capture baseline
|
|
let baselinePath = "/tmp/peekaboo-baseline.png"
|
|
let currentPath = "/tmp/peekaboo-current.png"
|
|
defer {
|
|
try? FileManager.default.removeItem(atPath: baselinePath)
|
|
try? FileManager.default.removeItem(atPath: currentPath)
|
|
}
|
|
|
|
_ = try captureWindowToFile(windowID: windowID, path: baselinePath, format: .png)
|
|
|
|
// Make a small change (in real tests, this would be application state change)
|
|
Thread.sleep(forTimeInterval: 0.1)
|
|
|
|
// Capture current
|
|
_ = try captureWindowToFile(windowID: windowID, path: currentPath, format: .png)
|
|
|
|
// Compare images
|
|
let baselineImage = NSImage(contentsOfFile: baselinePath)
|
|
let currentImage = NSImage(contentsOfFile: currentPath)
|
|
|
|
#expect(baselineImage != nil)
|
|
#expect(currentImage != nil)
|
|
|
|
// In practice, we'd use image diff algorithms here
|
|
#expect(baselineImage!.size == currentImage!.size)
|
|
}
|
|
|
|
@Test("Test different image formats", .tags(.formats))
|
|
func imageFormats() throws {
|
|
let testWindow = createTestWindow(withContent: .gradient)
|
|
defer { testWindow.close() }
|
|
|
|
RunLoop.current.run(until: Date().addingTimeInterval(0.5))
|
|
|
|
let windowID = CGWindowID(testWindow.windowNumber)
|
|
|
|
let formats: [ImageFormat] = [.png, .jpg]
|
|
|
|
for format in formats {
|
|
let path = "/tmp/peekaboo-format-test.\(format.rawValue)"
|
|
defer { try? FileManager.default.removeItem(atPath: path) }
|
|
|
|
let captureData = try captureWindowToFile(windowID: windowID, path: path, format: format)
|
|
|
|
#expect(FileManager.default.fileExists(atPath: path))
|
|
|
|
// Verify file size makes sense for format
|
|
let attributes = try FileManager.default.attributesOfItem(atPath: path)
|
|
let fileSize = attributes[.size] as? Int64 ?? 0
|
|
|
|
print("Format \(format.rawValue): \(fileSize) bytes")
|
|
#expect(fileSize > 0)
|
|
|
|
// PNG should typically be larger than JPG for photos
|
|
if format == .jpg {
|
|
#expect(fileSize < 500_000) // JPG should be reasonably compressed
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Multi-Display Tests
|
|
|
|
@Test("Capture from multiple displays", .tags(.multiDisplay))
|
|
func multiDisplayCapture() throws {
|
|
let screens = NSScreen.screens
|
|
print("Found \(screens.count) display(s)")
|
|
|
|
for (index, screen) in screens.enumerated() {
|
|
let displayID = getDisplayID(for: screen)
|
|
let outputPath = "/tmp/peekaboo-display-\(index).png"
|
|
defer { try? FileManager.default.removeItem(atPath: outputPath) }
|
|
|
|
do {
|
|
_ = try captureDisplayToFile(displayID: displayID, path: outputPath, format: .png)
|
|
|
|
#expect(FileManager.default.fileExists(atPath: outputPath))
|
|
|
|
// Verify captured dimensions match screen
|
|
if let image = NSImage(contentsOfFile: outputPath) {
|
|
let screenSize = screen.frame.size
|
|
let scale = screen.backingScaleFactor
|
|
|
|
// Image size should match screen size * scale factor
|
|
#expect(abs(image.size.width - screenSize.width * scale) < 2)
|
|
#expect(abs(image.size.height - screenSize.height * scale) < 2)
|
|
}
|
|
} catch {
|
|
print("Failed to capture display \(index): \(error)")
|
|
if screens.count == 1 {
|
|
throw error // Re-throw if it's the only display
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Performance Tests
|
|
|
|
@Test("Screenshot capture performance", .tags(.performance))
|
|
func capturePerformance() throws {
|
|
let testWindow = createTestWindow(withContent: .solid(.white))
|
|
defer { testWindow.close() }
|
|
|
|
RunLoop.current.run(until: Date().addingTimeInterval(0.5))
|
|
|
|
let windowID = CGWindowID(testWindow.windowNumber)
|
|
|
|
let iterations = 10
|
|
var captureTimes: [TimeInterval] = []
|
|
|
|
for iteration in 0..<iterations {
|
|
let path = "/tmp/peekaboo-perf-\(iteration).png"
|
|
defer { try? FileManager.default.removeItem(atPath: path) }
|
|
|
|
let start = CFAbsoluteTimeGetCurrent()
|
|
_ = try captureWindowToFile(windowID: windowID, path: path, format: .png)
|
|
let duration = CFAbsoluteTimeGetCurrent() - start
|
|
|
|
captureTimes.append(duration)
|
|
}
|
|
|
|
let averageTime = captureTimes.reduce(0, +) / Double(iterations)
|
|
let maxTime = captureTimes.max() ?? 0
|
|
|
|
print("Capture performance: avg=\(averageTime * 1000)ms, max=\(maxTime * 1000)ms")
|
|
|
|
// Performance expectations
|
|
#expect(averageTime < 0.1) // Average should be under 100ms
|
|
#expect(maxTime < 0.2) // Max should be under 200ms
|
|
}
|
|
|
|
// MARK: - Helper Functions
|
|
|
|
private func createTestWindow(withContent content: TestContent) -> NSWindow {
|
|
let window = NSWindow(
|
|
contentRect: NSRect(x: 100, y: 100, width: 400, height: 300),
|
|
styleMask: [.titled, .closable],
|
|
backing: .buffered,
|
|
defer: false
|
|
)
|
|
|
|
window.title = "Peekaboo Test Window"
|
|
window.isReleasedWhenClosed = false
|
|
|
|
let contentView = NSView(frame: window.contentRect(forFrameRect: window.frame))
|
|
contentView.wantsLayer = true
|
|
|
|
switch content {
|
|
case let .solid(color):
|
|
contentView.layer?.backgroundColor = color.cgColor
|
|
case .gradient:
|
|
let gradient = CAGradientLayer()
|
|
gradient.frame = contentView.bounds
|
|
gradient.colors = [
|
|
NSColor.red.cgColor,
|
|
NSColor.yellow.cgColor,
|
|
NSColor.green.cgColor,
|
|
NSColor.blue.cgColor
|
|
]
|
|
contentView.layer?.addSublayer(gradient)
|
|
case let .text(string):
|
|
contentView.layer?.backgroundColor = NSColor.white.cgColor
|
|
let textField = NSTextField(labelWithString: string)
|
|
textField.font = NSFont.systemFont(ofSize: 24)
|
|
textField.frame = contentView.bounds
|
|
textField.alignment = .center
|
|
contentView.addSubview(textField)
|
|
case .grid:
|
|
contentView.layer?.backgroundColor = NSColor.white.cgColor
|
|
// Grid pattern would be drawn here
|
|
}
|
|
|
|
window.contentView = contentView
|
|
window.makeKeyAndOrderFront(nil)
|
|
|
|
return window
|
|
}
|
|
|
|
private func captureWindowToFile(
|
|
windowID: CGWindowID,
|
|
path: String,
|
|
format: ImageFormat
|
|
) throws -> ImageCaptureData {
|
|
// Create image from window
|
|
guard let image = CGWindowListCreateImage(
|
|
.null,
|
|
.optionIncludingWindow,
|
|
windowID,
|
|
[.boundsIgnoreFraming, .nominalResolution]
|
|
) else {
|
|
throw CaptureError.windowCaptureFailed
|
|
}
|
|
|
|
// Save to file
|
|
let nsImage = NSImage(cgImage: image, size: NSSize(width: image.width, height: image.height))
|
|
try saveImage(nsImage, to: path, format: format)
|
|
|
|
return ImageCaptureData(saved_files: [
|
|
SavedFile(
|
|
path: path,
|
|
item_label: "Window \(windowID)",
|
|
window_title: nil,
|
|
window_id: nil,
|
|
window_index: nil,
|
|
mime_type: format == .png ? "image/png" : "image/jpeg"
|
|
)
|
|
])
|
|
}
|
|
|
|
private func captureDisplayToFile(
|
|
displayID: CGDirectDisplayID,
|
|
path: String,
|
|
format: ImageFormat
|
|
) throws -> ImageCaptureData {
|
|
guard let image = CGDisplayCreateImage(displayID) else {
|
|
throw CaptureError.captureCreationFailed
|
|
}
|
|
|
|
let nsImage = NSImage(cgImage: image, size: NSSize(width: image.width, height: image.height))
|
|
try saveImage(nsImage, to: path, format: format)
|
|
|
|
return ImageCaptureData(saved_files: [
|
|
SavedFile(
|
|
path: path,
|
|
item_label: "Display \(displayID)",
|
|
window_title: nil,
|
|
window_id: nil,
|
|
window_index: nil,
|
|
mime_type: format == .png ? "image/png" : "image/jpeg"
|
|
)
|
|
])
|
|
}
|
|
|
|
private func saveImage(_ image: NSImage, to path: String, format: ImageFormat) throws {
|
|
guard let tiffData = image.tiffRepresentation,
|
|
let bitmap = NSBitmapImageRep(data: tiffData) else {
|
|
throw CaptureError.fileWriteError(path)
|
|
}
|
|
|
|
let data: Data? = switch format {
|
|
case .png:
|
|
bitmap.representation(using: .png, properties: [:])
|
|
case .jpg:
|
|
bitmap.representation(using: .jpeg, properties: [.compressionFactor: 0.9])
|
|
}
|
|
|
|
guard let imageData = data else {
|
|
throw CaptureError.fileWriteError(path)
|
|
}
|
|
|
|
try imageData.write(to: URL(fileURLWithPath: path))
|
|
}
|
|
|
|
private func getDisplayID(for screen: NSScreen) -> CGDirectDisplayID {
|
|
let key = NSDeviceDescriptionKey("NSScreenNumber")
|
|
return screen.deviceDescription[key] as? CGDirectDisplayID ?? 0
|
|
}
|
|
}
|
|
|
|
// MARK: - Test Content Types
|
|
|
|
enum TestContent {
|
|
case solid(NSColor)
|
|
case gradient
|
|
case text(String)
|
|
case grid
|
|
}
|