import Testing import CoreGraphics @testable import OnyxNotch // Bildschirmkoordinaten auf macOS haben ihren Ursprung unten links, y wächst nach // oben. Die Notch sitzt also bei y = frame.maxY − höhe, nicht bei y = 0. @Suite("Notch-Geometrie") struct NotchGeometryTests { /// Gemessen auf dem Zielgerät (MacBook Pro Mac17,9, M5 Pro) am 10.08.2026. /// Dient als Referenzfall — wenn dieser Test bricht, hat Apple die /// Notch-Metrik geändert und nicht wir. static let builtIn = ScreenMetrics( frame: CGRect(x: 0, y: 0, width: 1800, height: 1169), safeAreaTop: 38, auxiliaryTopLeft: CGRect(x: 0, y: 1131, width: 790, height: 38), auxiliaryTopRight: CGRect(x: 1010, y: 1131, width: 790, height: 38) ) /// Typischer externer Monitor, rechts neben dem eingebauten angeordnet. static let external = ScreenMetrics( frame: CGRect(x: 1800, y: 0, width: 2560, height: 1440), safeAreaTop: 0, auxiliaryTopLeft: nil, auxiliaryTopRight: nil ) static let virtualSize = CGSize(width: 220, height: 32) // MARK: - Erkennung @Test("Eingebautes Display: echte Notch aus den Hilfsflächen berechnet") func physicalNotchFromRealDevice() { let g = NotchGeometry.resolve(Self.builtIn, virtualSize: Self.virtualSize) #expect(g.kind == .physical) #expect(g.rect == CGRect(x: 790, y: 1131, width: 220, height: 38)) } @Test("Externes Display ohne Notch: virtueller Ersatz, oben mittig") func virtualNotchOnExternal() { let g = NotchGeometry.resolve(Self.external, virtualSize: Self.virtualSize) #expect(g.kind == .virtual) #expect(g.isDetached) // Mittig: 1800 + (2560 − 220)/2 = 2970. // Und **nicht** an der Oberkante: ohne Hardware gibt es nichts zum // Verschmelzen, also schwebt die Insel. Dieser Bildschirm meldet keine // Menüleistenhöhe, deshalb bleibt es beim vorgegebenen Maß und der // kleinen Luft: 1440 − 3 − 32 = 1405. #expect(g.rect == CGRect(x: 2970, y: 1405, width: 220, height: 32)) } @Test("Die Insel liegt in der Menüleiste, nicht darunter") func islandFitsIntoTheMenuBar() { // Ein Bildschirm, der seine Menüleiste meldet — der Normalfall. let screen = ScreenMetrics( frame: CGRect(x: 1800, y: 0, width: 2560, height: 1440), safeAreaTop: 0, auxiliaryTopLeft: nil, auxiliaryTopRight: nil, isBuiltIn: false, menuBarHeight: 24, displayID: 2) let g = NotchGeometry.resolve(screen, virtualSize: CGSize(width: 72, height: 26)) #expect(g.isDetached) // 24 minus je 3 Punkt Luft: die vorgegebenen 26 wären zu hoch gewesen // und hätten unten aus der Leiste herausgeragt. #expect(g.rect.height == 18) // Oben und unten gleich viel Luft. #expect(screen.frame.maxY - g.rect.maxY == 3) #expect(g.rect.minY - (screen.frame.maxY - screen.menuBarHeight) == 3) } @Test("Eine sehr flache Menüleiste drückt die Insel nicht zum Strich") func islandKeepsAMinimumHeight() { let screen = ScreenMetrics( frame: CGRect(x: 0, y: 0, width: 2560, height: 1440), safeAreaTop: 0, auxiliaryTopLeft: nil, auxiliaryTopRight: nil, isBuiltIn: false, menuBarHeight: 12, displayID: 9) let g = NotchGeometry.resolve(screen, virtualSize: CGSize(width: 72, height: 26)) #expect(g.rect.height == NotchGeometry.minimumIslandHeight) } @Test("Echte Notch verschmilzt und schwebt nicht") func physicalNotchStaysMerged() { let g = NotchGeometry.resolve(Self.builtIn, virtualSize: Self.virtualSize) #expect(g.kind == .physical) #expect(!g.isDetached) // Kein Spalt: die Fläche *ist* die Kante. #expect(g.topMargin == 0) } @Test("Stil „immer schwebend\": auch mit Notch hängt die Insel darunter") func islandStyleDetachesEvenWithHardware() { let g = NotchGeometry.resolve(Self.builtIn, virtualSize: Self.virtualSize, style: .island) #expect(g.isDetached) // Unter der Hardware, nicht in ihr: sonst wäre die Insel zur Hälfte // schwarz auf schwarzem Gehäuse. #expect(g.topMargin > Self.builtIn.safeAreaTop) } @Test("Stil „immer verschmolzen\": ohne Hardware klebt die Fläche an der Kante") func notchStyleStaysAttachedWithoutHardware() { let g = NotchGeometry.resolve(Self.external, virtualSize: Self.virtualSize, style: .notch) #expect(!g.isDetached) #expect(g.topMargin == 0) #expect(g.rect.maxY == Self.external.frame.maxY) } @Test("safeAreaTop ohne Hilfsflächen gilt nicht als Notch") func safeAreaWithoutAuxIsVirtual() { // Kommt bei manchen Spiegelungs- und Fernanzeige-Konfigurationen vor: // ein Inset ist gesetzt, aber es gibt keine Notch, um die man herumlegen // könnte. Ein Panel an dieser Stelle säße im Nichts. let odd = ScreenMetrics( frame: CGRect(x: 0, y: 0, width: 1800, height: 1169), safeAreaTop: 38, auxiliaryTopLeft: nil, auxiliaryTopRight: nil ) #expect(NotchGeometry.resolve(odd, virtualSize: Self.virtualSize).kind == .virtual) } @Test("Notch-Mitte liegt auf der Bildschirmmitte") func notchIsCentred() { let g = NotchGeometry.resolve(Self.builtIn, virtualSize: Self.virtualSize) #expect(g.rect.midX == Self.builtIn.frame.midX) } // MARK: - Auslösefläche @Test("Auslösefläche ist breiter und tiefer als die Notch") func triggerIsLargerThanNotch() { let g = NotchGeometry.resolve(Self.builtIn, virtualSize: Self.virtualSize) let t = g.triggerRect #expect(t.width > g.rect.width) #expect(t.height > g.rect.height) #expect(t.midX == g.rect.midX) } @Test("Auslösefläche ragt nie über den oberen Bildschirmrand hinaus") func triggerStaysOnScreen() { let g = NotchGeometry.resolve(Self.builtIn, virtualSize: Self.virtualSize) #expect(g.triggerRect.maxY <= Self.builtIn.frame.maxY) #expect(g.triggerRect.minX >= Self.builtIn.frame.minX) #expect(g.triggerRect.maxX <= Self.builtIn.frame.maxX) } @Test("Auslösefläche eines schmalen Bildschirms bleibt innerhalb") func triggerClampsOnNarrowScreen() { let narrow = ScreenMetrics( frame: CGRect(x: 0, y: 0, width: 240, height: 400), safeAreaTop: 0, auxiliaryTopLeft: nil, auxiliaryTopRight: nil ) let g = NotchGeometry.resolve(narrow, virtualSize: Self.virtualSize) #expect(g.triggerRect.minX >= 0) #expect(g.triggerRect.maxX <= 240) } // MARK: - Panelfläche @Test("Panel hängt unter der Notch und ist auf sie zentriert") func panelHangsBelowNotch() { let g = NotchGeometry.resolve(Self.builtIn, virtualSize: Self.virtualSize) let panel = g.panelRect(for: CGSize(width: 640, height: 320)) #expect(panel.midX == g.rect.midX) // Oberkante bündig mit dem Bildschirmrand — das Panel wächst aus der // Notch heraus, es schwebt nicht darunter. #expect(panel.maxY == Self.builtIn.frame.maxY) #expect(panel.height == 320) } @Test("Panel breiter als der Bildschirm wird eingepasst statt abgeschnitten") func panelClampsToScreen() { let g = NotchGeometry.resolve(Self.builtIn, virtualSize: Self.virtualSize) let panel = g.panelRect(for: CGSize(width: 5000, height: 320)) #expect(panel.minX >= Self.builtIn.frame.minX) #expect(panel.maxX <= Self.builtIn.frame.maxX) } @Test("Panel auf dem externen Display liegt in dessen Koordinaten, nicht bei null") func panelUsesScreenOrigin() { let g = NotchGeometry.resolve(Self.external, virtualSize: Self.virtualSize) let panel = g.panelRect(for: CGSize(width: 640, height: 320)) #expect(panel.minX >= 1800) // Die Oberkante des Fensters folgt der schwebenden Insel, nicht der // Bildschirmkante — sonst klebte das Panel oben fest, während die // Insel darunter hängt. #expect(panel.maxY == 1440 - NotchGeometry.detachedMargin) } // MARK: - Sichtbarer Inhaltsbereich // // Die Notch ist undurchsichtig. Ein Panel, dessen Oberkante am Bildschirmrand // liegt, verliert dort alles, was gezeichnet wird — genau der Fehler, der in // der ersten lauffähigen Fassung sichtbar wurde. @Test("Der Inhaltsbereich liegt vollständig unterhalb der Notch") func contentClearsTheNotch() { let g = NotchGeometry.resolve(Self.builtIn, virtualSize: Self.virtualSize) let content = g.contentRect(for: CGSize(width: 446, height: 340)) #expect(!content.intersects(g.rect)) #expect(content.maxY <= g.rect.minY) } @Test("Der Inhaltsbereich behält die angeforderte Höhe — nichts geht verloren") func contentKeepsRequestedHeight() { let g = NotchGeometry.resolve(Self.builtIn, virtualSize: Self.virtualSize) let content = g.contentRect(for: CGSize(width: 446, height: 340)) #expect(content.height == 340) #expect(content.width == 446) } @Test("Das Fenster darüber reicht bis an die Bildschirmkante, damit es mit der Notch verschmilzt") func panelStillReachesScreenTop() { let g = NotchGeometry.resolve(Self.builtIn, virtualSize: Self.virtualSize) let panel = g.panelRect(for: CGSize(width: 446, height: 340 + g.rect.height)) #expect(panel.maxY == Self.builtIn.frame.maxY) } @Test("Auf einem externen Display gilt dasselbe für den virtuellen Balken") func contentClearsVirtualNotch() { let g = NotchGeometry.resolve(Self.external, virtualSize: Self.virtualSize) let content = g.contentRect(for: CGSize(width: 446, height: 340)) #expect(!content.intersects(g.rect)) #expect(content.height == 340) } @Test("Zu hohes Panel wird auf die Bildschirmhöhe begrenzt") func panelClampsHeight() { let g = NotchGeometry.resolve(Self.builtIn, virtualSize: Self.virtualSize) let panel = g.panelRect(for: CGSize(width: 640, height: 99_999)) #expect(panel.height <= Self.builtIn.frame.height) #expect(panel.minY >= Self.builtIn.frame.minY) } } @Suite("Hardware-Erkennung") struct HardwareNotchTests { @Test("Ein Bildschirm mit Hilfsflächen hat eine echte Notch") func builtInHasHardware() { let metrics = ScreenMetrics( frame: CGRect(x: 0, y: 0, width: 1800, height: 1169), safeAreaTop: 38, auxiliaryTopLeft: CGRect(x: 0, y: 1131, width: 790, height: 38), auxiliaryTopRight: CGRect(x: 1010, y: 1131, width: 790, height: 38), isBuiltIn: true, displayID: 1) #expect(NotchGeometry.hardwareRect(metrics) != nil) } @Test("Ein externer Bildschirm hat keine") func externalHasNone() { let metrics = ScreenMetrics( frame: CGRect(x: 1800, y: 0, width: 2560, height: 1440), safeAreaTop: 0, auxiliaryTopLeft: nil, auxiliaryTopRight: nil, isBuiltIn: false, displayID: 2) #expect(NotchGeometry.hardwareRect(metrics) == nil) } @Test("safeAreaTop allein genügt nicht") func insetAloneIsNotANotch() { // Bei Spiegelung und Fernanzeige ist das Inset gesetzt, obwohl es // nichts gibt, um das man herumlegen könnte. let metrics = ScreenMetrics( frame: CGRect(x: 0, y: 0, width: 1800, height: 1169), safeAreaTop: 38, auxiliaryTopLeft: nil, auxiliaryTopRight: nil, isBuiltIn: true, displayID: 3) #expect(NotchGeometry.hardwareRect(metrics) == nil) } @Test("Die Einstellungen und die Geometrie kommen zum selben Schluss") func settingsAgreeWithGeometry() { // Genau dafür ist `hardwareRect` öffentlich: würden beide die Frage // getrennt beantworten, zeigten die Einstellungen irgendwann einen // Regler für eine Breite, die gar nicht gilt. let builtIn = ScreenMetrics( frame: CGRect(x: 0, y: 0, width: 1800, height: 1169), safeAreaTop: 38, auxiliaryTopLeft: CGRect(x: 0, y: 1131, width: 790, height: 38), auxiliaryTopRight: CGRect(x: 1010, y: 1131, width: 790, height: 38), isBuiltIn: true, displayID: 1) let hasHardware = NotchGeometry.hardwareRect(builtIn) != nil let geometry = NotchGeometry.resolve(builtIn, virtualSize: CGSize(width: 72, height: 26)) #expect(NotchStyle.automatic.merges(withHardware: hasHardware)) #expect(geometry.kind == .physical) } }