@@ -2939,6 +2939,83 @@ final class TerminalWindowPortalLifecycleTests: XCTestCase {
29392939 )
29402940 }
29412941
2942+ func testBindQueuesExternalGeometrySyncForQueuedLayoutShift( ) {
2943+ let window = NSWindow (
2944+ contentRect: NSRect ( x: 0 , y: 0 , width: 700 , height: 420 ) ,
2945+ styleMask: [ . titled, . closable] ,
2946+ backing: . buffered,
2947+ defer: false
2948+ )
2949+ defer {
2950+ NotificationCenter . default. post ( name: NSWindow . willCloseNotification, object: window)
2951+ window. orderOut ( nil )
2952+ }
2953+
2954+ let surface = TerminalSurface (
2955+ tabId: UUID ( ) ,
2956+ context: GHOSTTY_SURFACE_CONTEXT_SPLIT,
2957+ configTemplate: nil ,
2958+ workingDirectory: nil
2959+ )
2960+ guard let contentView = window. contentView else {
2961+ XCTFail ( " Expected content view " )
2962+ return
2963+ }
2964+
2965+ let shiftedContainer = NSView ( frame: NSRect ( x: 40 , y: 60 , width: 260 , height: 180 ) )
2966+ contentView. addSubview ( shiftedContainer)
2967+ let anchor = NSView ( frame: NSRect ( x: 0 , y: 0 , width: 260 , height: 180 ) )
2968+ shiftedContainer. addSubview ( anchor)
2969+ let hosted = surface. hostedView
2970+ TerminalWindowPortalRegistry . bind (
2971+ hostedView: hosted,
2972+ to: anchor,
2973+ visibleInUI: true ,
2974+ expectedSurfaceId: surface. id,
2975+ expectedGeneration: surface. portalBindingGeneration ( )
2976+ )
2977+ TerminalWindowPortalRegistry . synchronizeForAnchor ( anchor)
2978+
2979+ let anchorCenter = NSPoint ( x: anchor. bounds. midX, y: anchor. bounds. midY)
2980+ let originalWindowPoint = anchor. convert ( anchorCenter, to: nil )
2981+ let originalAnchorFrameInWindow = anchor. convert ( anchor. bounds, to: nil )
2982+ XCTAssertNotNil (
2983+ TerminalWindowPortalRegistry . terminalViewAtWindowPoint ( originalWindowPoint, in: window) ,
2984+ " Initial hit-testing should resolve the portal-hosted terminal at its original window position "
2985+ )
2986+
2987+ DispatchQueue . main. async {
2988+ shiftedContainer. frame. origin. x += 72
2989+ contentView. layoutSubtreeIfNeeded ( )
2990+ window. displayIfNeeded ( )
2991+ }
2992+
2993+ RunLoop . current. run ( until: Date ( ) . addingTimeInterval ( 0.05 ) )
2994+
2995+ let shiftedAnchorFrameInWindow = anchor. convert ( anchor. bounds, to: nil )
2996+ XCTAssertGreaterThan (
2997+ shiftedAnchorFrameInWindow. minX,
2998+ originalAnchorFrameInWindow. minX + 1 ,
2999+ " The queued layout shift should move the anchor to the right "
3000+ )
3001+ let retiredStaleWindowPoint = NSPoint (
3002+ x: ( originalAnchorFrameInWindow. minX + shiftedAnchorFrameInWindow. minX) / 2 ,
3003+ y: shiftedAnchorFrameInWindow. midY
3004+ )
3005+ let shiftedWindowPoint = NSPoint (
3006+ x: ( originalAnchorFrameInWindow. maxX + shiftedAnchorFrameInWindow. maxX) / 2 ,
3007+ y: shiftedAnchorFrameInWindow. midY
3008+ )
3009+ XCTAssertNil (
3010+ TerminalWindowPortalRegistry . terminalViewAtWindowPoint ( retiredStaleWindowPoint, in: window) ,
3011+ " Bind should queue a later external sync so restore-like ancestor shifts do not leave a stale portal in the sidebar region "
3012+ )
3013+ XCTAssertNotNil (
3014+ TerminalWindowPortalRegistry . terminalViewAtWindowPoint ( shiftedWindowPoint, in: window) ,
3015+ " Bind should refresh the portal after queued ancestor layout settles "
3016+ )
3017+ }
3018+
29423019 func testScheduledExternalGeometrySyncKeepsDragDrivenResizeResponsive( ) {
29433020 let window = NSWindow (
29443021 contentRect: NSRect ( x: 0 , y: 0 , width: 700 , height: 420 ) ,
0 commit comments