diff --git a/cmuxTests/WorkspaceTerminalFocusRecoveryTests.swift b/cmuxTests/WorkspaceTerminalFocusRecoveryTests.swift index fa26c2cc931a..8c77bd49e0c3 100644 --- a/cmuxTests/WorkspaceTerminalFocusRecoveryTests.swift +++ b/cmuxTests/WorkspaceTerminalFocusRecoveryTests.swift @@ -316,13 +316,21 @@ struct WorkspaceTerminalFocusRecoverySwiftTests { window.makeFirstResponder(nil) panel.surface.setFocus(false) + // Only a hidden-to-visible transition schedules the automatic apply. The panel can be + // visible again after setup, which would turn the reveal below into a no-op. + panel.hostedView.setVisibleInUI(false) surfaceView.frame = NSRect(x: 0, y: 0, width: 0, height: 0) + try #require(!panel.hostedView.debugPortalVisibleInUI, "The reveal below must start from a hidden panel") panel.hostedView.setVisibleInUI(true) + try #require(panel.hostedView.debugPortalVisibleInUI, "Portal authority should let the selected workspace's panel reveal") await AppKitTestEventPump().drain() _ = await AppKitTestEventPump().waitUntil { panel.hostedView.isSurfaceViewFirstResponder() } - #expect(panel.hostedView.isSurfaceViewFirstResponder()) + #expect( + panel.hostedView.isSurfaceViewFirstResponder(), + "First responder after the reveal: \(String(describing: window.firstResponder))" + ) #expect(panel.hostedView.debugRenderStats().desiredFocus) #expect( !panel.surface.debugDesiredFocusState(), @@ -402,15 +410,47 @@ struct WorkspaceTerminalFocusRecoverySwiftTests { let surfaceView = try #require(findSurfaceView(in: panel.hostedView), "Expected terminal surface view") + // Put the panel on screen so the find overlay mounts its search field. + panel.hostedView.setVisibleInUI(true) + var mountedSearchField: NSTextField? + let searchFieldMounted = await AppKitTestEventPump().waitUntil(timeout: .seconds(3)) { + mountedSearchField = findMountedSearchField(in: panel.hostedView) + return mountedSearchField != nil + } + try #require(searchFieldMounted, "Expected the find overlay to mount its search field") + let searchField = try #require(mountedSearchField) + // Re-applying the mounted state bumps the overlay generation, which cancels the + // mount's remaining forced field-focus retries. + panel.hostedView.setSearchOverlay(searchState: searchState) + // The overlay's focus binding starts out set, and only the field's end-editing callback + // clears it. Let the field hold focus once and then resign it, so the overlay has no + // pending claim on focus when the panel reveals. + if !cmuxTextFieldIsFirstResponder(searchField, in: window) { + window.makeFirstResponder(searchField) + } + await AppKitTestEventPump().drain() + try #require(cmuxTextFieldIsFirstResponder(searchField, in: window), "Expected the find field to take focus") + window.makeFirstResponder(nil) + // The restore under test: the terminal, not the find field, is the panel's focus intent. + panel.hostedView.preparePanelFocusIntentForActivation(.surface) panel.surface.setFocus(false) + await AppKitTestEventPump().drain() + + // Only a hidden-to-visible transition schedules the automatic apply. + panel.hostedView.setVisibleInUI(false) surfaceView.frame = NSRect(x: 0, y: 0, width: 0, height: 0) + try #require(!panel.hostedView.debugPortalVisibleInUI, "The reveal below must start from a hidden panel") panel.hostedView.setVisibleInUI(true) + try #require(panel.hostedView.debugPortalVisibleInUI, "Portal authority should let the selected workspace's panel reveal") await AppKitTestEventPump().drain() _ = await AppKitTestEventPump().waitUntil { panel.hostedView.isSurfaceViewFirstResponder() } - #expect(panel.hostedView.isSurfaceViewFirstResponder()) + #expect( + panel.hostedView.isSurfaceViewFirstResponder(), + "First responder after the reveal: \(String(describing: window.firstResponder))" + ) #expect( !panel.surface.debugDesiredFocusState(), "Find terminal restore must not drop hidden/tiny focus recovery before Ghostty focus is reapplied" @@ -566,4 +606,18 @@ struct WorkspaceTerminalFocusRecoverySwiftTests { } return nil } + +#if DEBUG + private func findMountedSearchField(in hostedView: GhosttySurfaceScrollView) -> NSTextField? { + guard let overlay = hostedView.debugSearchOverlayHostingViewForTesting() else { return nil } + var stack: [NSView] = [overlay] + while let current = stack.popLast() { + if let field = current as? NSTextField, field.isEditable { + return field + } + stack.append(contentsOf: current.subviews) + } + return nil + } +#endif }