Peekaboo/peekaboo-cli/Tests/peekabooTests/ScreenshotValidationTests.swift
Peter Steinberger fbf32f8e21 Prepare for beta.14 release: comprehensive test improvements and code cleanup
- 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>
2025-06-08 02:00:44 +01:00

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
}