From e4b1c9c15d4016c9a68091290422ce765ee437bc Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Tue, 22 Sep 2026 22:55:42 -0700 Subject: [PATCH] test(focus-recovery): start the hidden/tiny reveal from a hidden panel automaticApplyDoesNotBypassHiddenTinyFirstResponderDeferral and findTerminalRestorePreservesHiddenTinyFirstResponderDeferral hide the panel early in setup and count on their final setVisibleInUI(true) to schedule the automatic first-responder apply. setVisibleInUI only schedules that apply on a hidden-to-visible transition. CI diagnostics showed the first responder still on the window after the reveal, which fits a reveal that changed nothing because setup had left the panel visible. Both tests now hide the panel right before the reveal and require the hidden and visible states around it. The find test also left the overlay's own focus handling active. Mounting the overlay forces focus into the search field, and the overlay's focus binding starts set and clears only when the field ends editing. The test now waits for the field to mount, cancels the forced retries, lets the field take and resign focus, and then sets the terminal focus intent it exercises. Both first-responder expectations now print the actual first responder when they fail. Co-Authored-By: Claude Opus 5.5 --- .../WorkspaceTerminalFocusRecoveryTests.swift | 58 ++++++++++++++++++- 1 file changed, 56 insertions(+), 2 deletions(-) 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 }