SmartLabelPlacerTests.swift•5.38 kB
import AppKit
import Testing
@testable import PeekabooCLI
@testable import PeekabooCore
@Suite("SmartLabelPlacer Tests", .serialized, .tags(.fast))
@MainActor
struct SmartLabelPlacerTests {
@Test("Scoring expands candidate rects with padding")
func scoringExpandsRectForCalmerPlacement() {
let imageSize = NSSize(width: 200, height: 200)
let image = Self.makeImage(size: imageSize)
let detector = RecordingTextDetector()
let placer = SmartLabelPlacer(
image: image,
fontSize: 10,
debugMode: false,
logger: .shared,
textDetector: detector
)
let element = DetectedElement.make(id: "elem-top")
let elementRect = NSRect(x: 50, y: 50, width: 30, height: 20)
let labelSize = NSSize(width: 30, height: 10)
let result = placer.findBestLabelPosition(
for: element,
elementRect: elementRect,
labelSize: labelSize,
existingLabels: [],
allElements: [(element: element, rect: elementRect)]
)
#expect(result != nil)
let expected = Self.expectedScoringRect(
from: result!.labelRect,
imageSize: imageSize
)
#expect(detector.recordedRects.first != nil)
Self.expect(detector.recordedRects.first!, equals: expected)
}
@Test("Scoring rects clamp to image bounds")
func scoringRectsClampWithinImage() {
let imageSize = NSSize(width: 200, height: 200)
let image = Self.makeImage(size: imageSize)
let detector = RecordingTextDetector()
let placer = SmartLabelPlacer(
image: image,
fontSize: 10,
debugMode: false,
logger: .shared,
textDetector: detector
)
// Position element close enough to the bottom that the expanded sampling
// rect would extend beyond the image bounds.
let element = DetectedElement.make(id: "elem-bottom")
let elementRect = NSRect(x: 40, y: 95, width: 30, height: 15)
let labelSize = NSSize(width: 32, height: 12)
let result = placer.findBestLabelPosition(
for: element,
elementRect: elementRect,
labelSize: labelSize,
existingLabels: [],
allElements: [(element: element, rect: elementRect)]
)
#expect(result != nil)
let expected = Self.expectedScoringRect(
from: result!.labelRect,
imageSize: imageSize
)
#expect(detector.recordedRects.first != nil)
Self.expect(detector.recordedRects.first!, equals: expected)
#expect(detector.recordedRects.first!.minY >= 0)
}
}
// MARK: - Helpers
extension SmartLabelPlacerTests {
fileprivate static func makeImage(size: NSSize) -> NSImage {
let image = NSImage(size: size)
image.lockFocus()
NSColor.white.setFill()
NSRect(origin: .zero, size: size).fill()
image.unlockFocus()
return image
}
fileprivate static func expectedScoringRect(from labelRect: NSRect, imageSize: NSSize) -> NSRect {
let imageRect = NSRect(
x: labelRect.origin.x,
y: imageSize.height - labelRect.origin.y - labelRect.height,
width: labelRect.width,
height: labelRect.height
)
// Mirror the SmartLabelPlacer logic: expand by the padding and clamp to bounds.
let expanded = imageRect.insetBy(
dx: -SmartLabelPlacer.defaultScoreRegionPadding,
dy: -SmartLabelPlacer.defaultScoreRegionPadding
)
return self.clamp(expanded, within: NSRect(origin: .zero, size: imageSize))
}
fileprivate static func clamp(_ rect: NSRect, within bounds: NSRect) -> NSRect {
let minX = max(bounds.minX, rect.minX)
let maxX = min(bounds.maxX, rect.maxX)
let minY = max(bounds.minY, rect.minY)
let maxY = min(bounds.maxY, rect.maxY)
return NSRect(
x: minX,
y: minY,
width: max(0, maxX - minX),
height: max(0, maxY - minY)
)
}
fileprivate static func expect(_ lhs: NSRect, equals rhs: NSRect, accuracy: CGFloat = 0.001) {
#expect(abs(lhs.origin.x - rhs.origin.x) < accuracy)
#expect(abs(lhs.origin.y - rhs.origin.y) < accuracy)
#expect(abs(lhs.size.width - rhs.size.width) < accuracy)
#expect(abs(lhs.size.height - rhs.size.height) < accuracy)
}
}
// MARK: - Test Doubles
private final class RecordingTextDetector: SmartLabelPlacerTextDetecting {
var recordedRects: [NSRect] = []
func scoreRegionForLabelPlacement(_ rect: NSRect, in image: NSImage) -> Float {
self.recordedRects.append(rect)
return 0.5
}
func analyzeRegion(_ rect: NSRect, in image: NSImage) -> AcceleratedTextDetector.EdgeDensityResult {
AcceleratedTextDetector.EdgeDensityResult(density: 0, hasText: false)
}
}
extension DetectedElement {
fileprivate static func make(id: String) -> DetectedElement {
DetectedElement(
id: id,
type: .button,
label: id,
value: nil,
bounds: .zero,
isEnabled: true,
isSelected: nil,
attributes: [:]
)
}
}