import AppKit import CoreGraphics import Foundation import Testing @testable import phbar // MARK: - PHThemeWindowDimension private struct DimensionWrapper: Decodable { let v: PHThemeWindowDimension } @Test func dimensionDecodesPoints() throws { let decoder = JSONDecoder() #expect(try decoder.decode(DimensionWrapper.self, from: Data(#"{"v":400}"#.utf8)).v == .points(400)) #expect(try decoder.decode(DimensionWrapper.self, from: Data(#"{"v":400.5}"#.utf8)).v == .points(400.5)) } @Test func dimensionDecodesPercentage() throws { let decoder = JSONDecoder() #expect(try decoder.decode(DimensionWrapper.self, from: Data(#"{"v":"100%"}"#.utf8)).v == .percentage(1.0)) #expect(try decoder.decode(DimensionWrapper.self, from: Data(#"{"v":"50%"}"#.utf8)).v == .percentage(0.5)) } @Test func dimensionRejectsGarbage() { let decoder = JSONDecoder() #expect(throws: (any Error).self) { try decoder.decode(DimensionWrapper.self, from: Data(#"{"v":"wat"}"#.utf8)) } } @Test func dimensionResolvesAgainstLength() { #expect(PHThemeWindowDimension.points(250).resolve(against: 1000) == 250) #expect(PHThemeWindowDimension.percentage(0.25).resolve(against: 1000) == 250) } // MARK: - PHThemeWindowAnchor geometry @Test func anchorPlacesAtTopEdge() { let screen = CGRect(x: 0, y: 0, width: 1000, height: 800) let size = CGSize(width: 1000, height: 30) let origin = PHThemeWindowAnchor.top.origin(in: screen, size: size) #expect(origin == CGPoint(x: 0, y: 770)) } @Test func anchorRespectsTopMargin() { let screen = CGRect(x: 0, y: 0, width: 1000, height: 800) let size = CGSize(width: 1000, height: 30) let rect = PHThemeWindowMargin(top: 10).inset(of: screen) let origin = PHThemeWindowAnchor.top.origin(in: rect, size: size) #expect(origin == CGPoint(x: 0, y: 760)) } @Test func anchorPlacesBottomLeadingCorner() { let screen = CGRect(x: 0, y: 0, width: 1000, height: 800) let size = CGSize(width: 200, height: 40) let rect = PHThemeWindowMargin(bottom: 8, leading: 12).inset(of: screen) let origin = PHThemeWindowAnchor.bottomLeading.origin(in: rect, size: size) #expect(origin == CGPoint(x: 12, y: 8)) } @Test func anchorTrailingCentersVertically() { let screen = CGRect(x: 0, y: 0, width: 1000, height: 800) let size = CGSize(width: 200, height: 40) let rect = PHThemeWindowMargin(trailing: 20).inset(of: screen) let origin = PHThemeWindowAnchor.trailing.origin(in: rect, size: size) #expect(origin == CGPoint(x: 780, y: 380)) } @Test func anchorCentersWithinContentRect() { let screen = CGRect(x: 0, y: 0, width: 1000, height: 800) let size = CGSize(width: 200, height: 40) // `.center` centers within the inset rect: a top margin lifts the center, // it doesn't ignore the margin. let rect = PHThemeWindowMargin(top: 100).inset(of: screen) let origin = PHThemeWindowAnchor.center.origin(in: rect, size: size) // rect: y ∈ [0, 700], midY = 350 → 350 − 20 = 330 #expect(origin == CGPoint(x: 400, y: 330)) } // MARK: - PHThemeWindowMargin.inset @Test func marginInsetAppliesAllEdges() { let rect = CGRect(x: 0, y: 0, width: 1000, height: 800) let inset = PHThemeWindowMargin(top: 10, bottom: 20, leading: 30, trailing: 40).inset(of: rect) #expect(inset == CGRect(x: 30, y: 20, width: 930, height: 770)) } @Test func marginInsetLeavesUnspecifiedEdgesUntouched() { let rect = CGRect(x: 0, y: 0, width: 1000, height: 800) // Only leading set; top/bottom/trailing are left as-is (treated as 0). let inset = PHThemeWindowMargin(leading: 50).inset(of: rect) #expect(inset == CGRect(x: 50, y: 0, width: 950, height: 800)) } // MARK: - BarWindow.computeFrame @MainActor @Test func computeFrameDefaultsToFullScreenTopBar() throws { let screen = try #require(NSScreen.main) // A window that omits geometry exercises computeFrame's defaults: // width 100%, top anchor, no margin → a full-screen top bar. Built locally // (rather than loading the bundled theme) so the test tracks the engine's // defaults, not the theme file's editorial margins. let theme = PHTheme( windows: [PHThemeWindow(name: "default")], texts: nil, styles: nil ) let config = PHConfig(theme: "default", window: "default", blocks: nil, env: nil, monitors: nil) let blocks = try PHBlock.load(from: "[[block]]\ncommand = \"echo x\"") let controller = BarController(config: config, screen: screen, theme: theme, blocks: blocks, debug: false) let frame = BarWindow.computeFrame(from: controller) #expect(frame.width == screen.frame.width) #expect(frame.minX == screen.frame.minX) #expect(frame.minY == screen.frame.maxY - 30) } @MainActor @Test func computeFrameUsesAbsoluteOriginOverAnchor() throws { let screen = try #require(NSScreen.main) let theme = PHTheme( windows: [PHThemeWindow(name: "abs", anchor: .bottom, origin: PHThemeWindowPoint(x: 100, y: 200))], texts: nil, styles: nil ) let config = PHConfig(theme: "default", window: "abs", blocks: nil, env: nil, monitors: nil) let blocks = try PHBlock.load(from: "[[block]]\ncommand = \"echo x\"") let controller = BarController(config: config, screen: screen, theme: theme, blocks: blocks, debug: false) let frame = BarWindow.computeFrame(from: controller) #expect(frame.origin == CGPoint(x: 100, y: 200)) #expect(frame.height == 30) } @MainActor @Test func computeFrameInsetsFullWidthBarByMargins() throws { // Regression: `width = "100%"` previously ignored leading/trailing margins // and spanned edge to edge. Margins now inset the content rectangle, so the // bar spans only between them. let screen = try #require(NSScreen.main) let sf = screen.frame let theme = PHTheme( windows: [PHThemeWindow( name: "inset", width: .percentage(1.0), anchor: .top, margin: PHThemeWindowMargin(leading: 20, trailing: 20) )], texts: nil, styles: nil ) let config = PHConfig(theme: "default", window: "inset", blocks: nil, env: nil, monitors: nil) let blocks = try PHBlock.load(from: "[[block]]\ncommand = \"echo x\"") let controller = BarController(config: config, screen: screen, theme: theme, blocks: blocks, debug: false) let frame = BarWindow.computeFrame(from: controller) #expect(frame.width == sf.width - 40) #expect(frame.minX == sf.minX + 20) #expect(frame.maxX == sf.maxX - 20) }