Fix command palette focus after terminal find (#2089)
* test: cover command palette focus guard * fix: block terminal find from stealing palette focus * test: cover text view focus-stealer fallback * Add regression for hidden DevTools sync republish loop * Avoid redundant DevTools visibility publishes * test: cover browser find focus after workspace round-trip * fix: restore browser find focus after workspace round-trip * fix: keep browser find caret on workspace return * Add workspace round-trip split find regressions * Keep inactive find overlays from stealing focus --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
This commit is contained in:
parent
b42f64fbe3
commit
8a3ab6b3f0
10 changed files with 402 additions and 59 deletions
|
|
@ -37,6 +37,38 @@ private enum CmuxThemeNotifications {
|
||||||
static let reloadConfig = Notification.Name("com.cmuxterm.themes.reload-config")
|
static let reloadConfig = Notification.Name("com.cmuxterm.themes.reload-config")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isCommandPaletteFocusStealingTerminalOrBrowserResponder(_ responder: NSResponder) -> Bool {
|
||||||
|
if responder is GhosttyNSView || responder is WKWebView {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if let textView = responder as? NSTextView,
|
||||||
|
!textView.isFieldEditor,
|
||||||
|
let delegateView = textView.delegate as? NSView {
|
||||||
|
return isCommandPaletteFocusStealingTerminalOrBrowserView(delegateView)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let view = responder as? NSView {
|
||||||
|
return isCommandPaletteFocusStealingTerminalOrBrowserView(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isCommandPaletteFocusStealingTerminalOrBrowserView(_ view: NSView) -> Bool {
|
||||||
|
if view is GhosttyNSView || view is GhosttySurfaceScrollView || view is WKWebView {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
var current: NSView? = view.superview
|
||||||
|
while let candidate = current {
|
||||||
|
if candidate is GhosttyNSView || candidate is GhosttySurfaceScrollView || candidate is WKWebView {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
current = candidate.superview
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
enum CmuxTypingTiming {
|
enum CmuxTypingTiming {
|
||||||
static let isEnabled: Bool = {
|
static let isEnabled: Bool = {
|
||||||
|
|
@ -4656,35 +4688,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
||||||
}
|
}
|
||||||
|
|
||||||
private func isFocusStealingResponderWhileCommandPaletteVisible(_ responder: NSResponder) -> Bool {
|
private func isFocusStealingResponderWhileCommandPaletteVisible(_ responder: NSResponder) -> Bool {
|
||||||
if responder is GhosttyNSView || responder is WKWebView {
|
isCommandPaletteFocusStealingTerminalOrBrowserResponder(responder)
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if let textView = responder as? NSTextView,
|
|
||||||
!textView.isFieldEditor,
|
|
||||||
let delegateView = textView.delegate as? NSView {
|
|
||||||
return isTerminalOrBrowserView(delegateView)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let view = responder as? NSView {
|
|
||||||
return isTerminalOrBrowserView(view)
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
private func isTerminalOrBrowserView(_ view: NSView) -> Bool {
|
|
||||||
if view is GhosttyNSView || view is WKWebView {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
var current: NSView? = view.superview
|
|
||||||
while let candidate = current {
|
|
||||||
if candidate is GhosttyNSView || candidate is WKWebView {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
current = candidate.superview
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func isInsideCommandPaletteOverlay(_ view: NSView) -> Bool {
|
private func isInsideCommandPaletteOverlay(_ view: NSView) -> Bool {
|
||||||
|
|
|
||||||
|
|
@ -222,6 +222,16 @@ private struct BrowserSearchTextFieldRepresentable: NSViewRepresentable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func focusField(_ field: BrowserSearchNativeTextField, in window: NSWindow) {
|
||||||
|
guard window.makeFirstResponder(field) else { return }
|
||||||
|
DispatchQueue.main.async { [weak field] in
|
||||||
|
guard let field,
|
||||||
|
let editor = field.currentEditor() as? NSTextView else { return }
|
||||||
|
let end = field.stringValue.utf16.count
|
||||||
|
editor.setSelectedRange(NSRange(location: end, length: 0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func controlTextDidChange(_ obj: Notification) {
|
func controlTextDidChange(_ obj: Notification) {
|
||||||
guard !isProgrammaticMutation else { return }
|
guard !isProgrammaticMutation else { return }
|
||||||
guard let field = obj.object as? NSTextField else { return }
|
guard let field = obj.object as? NSTextField else { return }
|
||||||
|
|
@ -294,7 +304,7 @@ private struct BrowserSearchTextFieldRepresentable: NSViewRepresentable {
|
||||||
field.currentEditor() != nil ||
|
field.currentEditor() != nil ||
|
||||||
((fr as? NSTextView)?.delegate as? NSTextField) === field
|
((fr as? NSTextView)?.delegate as? NSTextField) === field
|
||||||
guard !alreadyFocused else { return }
|
guard !alreadyFocused else { return }
|
||||||
window.makeFirstResponder(field)
|
coordinator.focusField(field, in: window)
|
||||||
}
|
}
|
||||||
return field
|
return field
|
||||||
}
|
}
|
||||||
|
|
@ -337,7 +347,7 @@ private struct BrowserSearchTextFieldRepresentable: NSViewRepresentable {
|
||||||
nsView.currentEditor() != nil ||
|
nsView.currentEditor() != nil ||
|
||||||
((fr as? NSTextView)?.delegate as? NSTextField) === nsView
|
((fr as? NSTextView)?.delegate as? NSTextField) === nsView
|
||||||
guard !alreadyFocused else { return }
|
guard !alreadyFocused else { return }
|
||||||
window.makeFirstResponder(nsView)
|
coordinator.focusField(nsView, in: window)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ struct SurfaceSearchOverlay: View {
|
||||||
let tabId: UUID
|
let tabId: UUID
|
||||||
let surfaceId: UUID
|
let surfaceId: UUID
|
||||||
@ObservedObject var searchState: TerminalSurface.SearchState
|
@ObservedObject var searchState: TerminalSurface.SearchState
|
||||||
|
let canApplyFocusRequest: () -> Bool
|
||||||
let onMoveFocusToTerminal: () -> Void
|
let onMoveFocusToTerminal: () -> Void
|
||||||
let onNavigateSearch: (_ action: String) -> Void
|
let onNavigateSearch: (_ action: String) -> Void
|
||||||
let onFieldDidFocus: () -> Void
|
let onFieldDidFocus: () -> Void
|
||||||
|
|
@ -37,6 +38,7 @@ struct SurfaceSearchOverlay: View {
|
||||||
text: $searchState.needle,
|
text: $searchState.needle,
|
||||||
isFocused: $isSearchFieldFocused,
|
isFocused: $isSearchFieldFocused,
|
||||||
surfaceId: surfaceId,
|
surfaceId: surfaceId,
|
||||||
|
canApplyFocusRequest: canApplyFocusRequest,
|
||||||
onFieldDidFocus: onFieldDidFocus,
|
onFieldDidFocus: onFieldDidFocus,
|
||||||
onEscape: {
|
onEscape: {
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
|
|
@ -227,6 +229,7 @@ private struct SearchTextFieldRepresentable: NSViewRepresentable {
|
||||||
@Binding var text: String
|
@Binding var text: String
|
||||||
@Binding var isFocused: Bool
|
@Binding var isFocused: Bool
|
||||||
let surfaceId: UUID
|
let surfaceId: UUID
|
||||||
|
let canApplyFocusRequest: () -> Bool
|
||||||
let onFieldDidFocus: () -> Void
|
let onFieldDidFocus: () -> Void
|
||||||
let onEscape: () -> Void
|
let onEscape: () -> Void
|
||||||
let onReturn: (_ isShift: Bool) -> Void
|
let onReturn: (_ isShift: Bool) -> Void
|
||||||
|
|
@ -319,6 +322,7 @@ private struct SearchTextFieldRepresentable: NSViewRepresentable {
|
||||||
guard let field, let coordinator else { return }
|
guard let field, let coordinator else { return }
|
||||||
guard let surface = notification.object as? TerminalSurface,
|
guard let surface = notification.object as? TerminalSurface,
|
||||||
surface.id == coordinator.parent.surfaceId else { return }
|
surface.id == coordinator.parent.surfaceId else { return }
|
||||||
|
guard coordinator.parent.canApplyFocusRequest() else { return }
|
||||||
guard let window = field.window else { return }
|
guard let window = field.window else { return }
|
||||||
// Don't re-focus if already first responder. makeFirstResponder on an
|
// Don't re-focus if already first responder. makeFirstResponder on an
|
||||||
// already-editing NSTextField ends the editing session and restarts it
|
// already-editing NSTextField ends the editing session and restarts it
|
||||||
|
|
@ -370,11 +374,16 @@ private struct SearchTextFieldRepresentable: NSViewRepresentable {
|
||||||
nsView.currentEditor() != nil ||
|
nsView.currentEditor() != nil ||
|
||||||
((fr as? NSTextView)?.delegate as? NSTextField) === nsView
|
((fr as? NSTextView)?.delegate as? NSTextField) === nsView
|
||||||
|
|
||||||
if isFocused, !isFirstResponder, context.coordinator.pendingFocusRequest != true {
|
if isFocused,
|
||||||
|
canApplyFocusRequest(),
|
||||||
|
!isFirstResponder,
|
||||||
|
context.coordinator.pendingFocusRequest != true {
|
||||||
context.coordinator.pendingFocusRequest = true
|
context.coordinator.pendingFocusRequest = true
|
||||||
DispatchQueue.main.async { [weak nsView, weak coordinator = context.coordinator] in
|
DispatchQueue.main.async { [weak nsView, weak coordinator = context.coordinator] in
|
||||||
coordinator?.pendingFocusRequest = nil
|
coordinator?.pendingFocusRequest = nil
|
||||||
guard let coordinator, coordinator.parent.isFocused else { return }
|
guard let coordinator,
|
||||||
|
coordinator.parent.isFocused,
|
||||||
|
coordinator.parent.canApplyFocusRequest() else { return }
|
||||||
guard let nsView, let window = nsView.window else { return }
|
guard let nsView, let window = nsView.window else { return }
|
||||||
let fr = window.firstResponder
|
let fr = window.firstResponder
|
||||||
let alreadyFocused = fr === nsView ||
|
let alreadyFocused = fr === nsView ||
|
||||||
|
|
|
||||||
|
|
@ -7868,6 +7868,9 @@ final class GhosttySurfaceScrollView: NSView {
|
||||||
tabId: terminalSurface.tabId,
|
tabId: terminalSurface.tabId,
|
||||||
surfaceId: terminalSurface.id,
|
surfaceId: terminalSurface.id,
|
||||||
searchState: searchState,
|
searchState: searchState,
|
||||||
|
canApplyFocusRequest: { [weak self] in
|
||||||
|
self?.canApplyMountedSearchFieldFocusRequest() ?? false
|
||||||
|
},
|
||||||
onMoveFocusToTerminal: { [weak self] in
|
onMoveFocusToTerminal: { [weak self] in
|
||||||
self?.searchFocusTarget = .terminal
|
self?.searchFocusTarget = .terminal
|
||||||
self?.moveFocus()
|
self?.moveFocus()
|
||||||
|
|
@ -7899,6 +7902,17 @@ final class GhosttySurfaceScrollView: NSView {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func canApplyMountedSearchFieldFocusRequest() -> Bool {
|
||||||
|
guard let terminalSurface = surfaceView.terminalSurface,
|
||||||
|
let app = AppDelegate.shared,
|
||||||
|
let manager = app.tabManagerFor(tabId: terminalSurface.tabId),
|
||||||
|
manager.selectedTabId == terminalSurface.tabId,
|
||||||
|
let workspace = manager.tabs.first(where: { $0.id == terminalSurface.tabId }) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return workspace.focusedPanelId == terminalSurface.id
|
||||||
|
}
|
||||||
|
|
||||||
private func requestMountedSearchFieldFocus(
|
private func requestMountedSearchFieldFocus(
|
||||||
generation: UInt64,
|
generation: UInt64,
|
||||||
force: Bool,
|
force: Bool,
|
||||||
|
|
@ -7906,6 +7920,7 @@ final class GhosttySurfaceScrollView: NSView {
|
||||||
) {
|
) {
|
||||||
guard searchOverlayMutationGeneration == generation else { return }
|
guard searchOverlayMutationGeneration == generation else { return }
|
||||||
guard force || searchFocusTarget == .searchField else { return }
|
guard force || searchFocusTarget == .searchField else { return }
|
||||||
|
guard canApplyMountedSearchFieldFocusRequest() else { return }
|
||||||
guard let overlay = searchOverlayHostingView,
|
guard let overlay = searchOverlayHostingView,
|
||||||
overlay.superview === self,
|
overlay.superview === self,
|
||||||
let window,
|
let window,
|
||||||
|
|
|
||||||
|
|
@ -3939,7 +3939,7 @@ extension BrowserPanel {
|
||||||
|
|
||||||
_ = hideDeveloperTools()
|
_ = hideDeveloperTools()
|
||||||
cancelDeveloperToolsRestoreRetry()
|
cancelDeveloperToolsRestoreRetry()
|
||||||
preferredDeveloperToolsVisible = false
|
setPreferredDeveloperToolsVisible(false)
|
||||||
preferredDeveloperToolsPresentation = .unknown
|
preferredDeveloperToolsPresentation = .unknown
|
||||||
forceDeveloperToolsRefreshOnNextAttach = false
|
forceDeveloperToolsRefreshOnNextAttach = false
|
||||||
developerToolsDetachedOpenGraceDeadline = nil
|
developerToolsDetachedOpenGraceDeadline = nil
|
||||||
|
|
@ -4219,6 +4219,11 @@ extension BrowserPanel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func setPreferredDeveloperToolsVisible(_ next: Bool) {
|
||||||
|
guard preferredDeveloperToolsVisible != next else { return }
|
||||||
|
preferredDeveloperToolsVisible = next
|
||||||
|
}
|
||||||
|
|
||||||
private func syncDeveloperToolsPresentationPreferenceFromUI() {
|
private func syncDeveloperToolsPresentationPreferenceFromUI() {
|
||||||
if !detachedDeveloperToolsWindows().isEmpty {
|
if !detachedDeveloperToolsWindows().isEmpty {
|
||||||
setPreferredDeveloperToolsPresentation(.detached)
|
setPreferredDeveloperToolsPresentation(.detached)
|
||||||
|
|
@ -4247,7 +4252,7 @@ extension BrowserPanel {
|
||||||
guard self.preferredDeveloperToolsVisible else { return }
|
guard self.preferredDeveloperToolsVisible else { return }
|
||||||
guard !self.isDeveloperToolsVisible() else { return }
|
guard !self.isDeveloperToolsVisible() else { return }
|
||||||
self.developerToolsDetachedOpenGraceDeadline = nil
|
self.developerToolsDetachedOpenGraceDeadline = nil
|
||||||
self.preferredDeveloperToolsVisible = false
|
self.setPreferredDeveloperToolsVisible(false)
|
||||||
self.cancelDeveloperToolsRestoreRetry()
|
self.cancelDeveloperToolsRestoreRetry()
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
dlog(
|
dlog(
|
||||||
|
|
@ -4383,7 +4388,7 @@ extension BrowserPanel {
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
if isDeveloperToolsTransitionInFlight {
|
if isDeveloperToolsTransitionInFlight {
|
||||||
pendingDeveloperToolsTransitionTargetVisible = targetVisible
|
pendingDeveloperToolsTransitionTargetVisible = targetVisible
|
||||||
preferredDeveloperToolsVisible = targetVisible
|
setPreferredDeveloperToolsVisible(targetVisible)
|
||||||
if !targetVisible {
|
if !targetVisible {
|
||||||
developerToolsDetachedOpenGraceDeadline = nil
|
developerToolsDetachedOpenGraceDeadline = nil
|
||||||
forceDeveloperToolsRefreshOnNextAttach = false
|
forceDeveloperToolsRefreshOnNextAttach = false
|
||||||
|
|
@ -4410,7 +4415,7 @@ extension BrowserPanel {
|
||||||
|
|
||||||
let isVisibleSelector = NSSelectorFromString("isVisible")
|
let isVisibleSelector = NSSelectorFromString("isVisible")
|
||||||
let visible = inspector.cmuxCallBool(selector: isVisibleSelector) ?? false
|
let visible = inspector.cmuxCallBool(selector: isVisibleSelector) ?? false
|
||||||
preferredDeveloperToolsVisible = targetVisible
|
setPreferredDeveloperToolsVisible(targetVisible)
|
||||||
developerToolsTransitionTargetVisible = targetVisible
|
developerToolsTransitionTargetVisible = targetVisible
|
||||||
|
|
||||||
if targetVisible {
|
if targetVisible {
|
||||||
|
|
@ -4512,7 +4517,7 @@ extension BrowserPanel {
|
||||||
guard let visible = inspector.cmuxCallBool(selector: NSSelectorFromString("isVisible")) else { return }
|
guard let visible = inspector.cmuxCallBool(selector: NSSelectorFromString("isVisible")) else { return }
|
||||||
if isDeveloperToolsTransitionInFlight {
|
if isDeveloperToolsTransitionInFlight {
|
||||||
let targetVisible = pendingDeveloperToolsTransitionTargetVisible ?? developerToolsTransitionTargetVisible ?? visible
|
let targetVisible = pendingDeveloperToolsTransitionTargetVisible ?? developerToolsTransitionTargetVisible ?? visible
|
||||||
preferredDeveloperToolsVisible = targetVisible
|
setPreferredDeveloperToolsVisible(targetVisible)
|
||||||
if targetVisible, visible {
|
if targetVisible, visible {
|
||||||
developerToolsDetachedOpenGraceDeadline = nil
|
developerToolsDetachedOpenGraceDeadline = nil
|
||||||
syncDeveloperToolsPresentationPreferenceFromUI()
|
syncDeveloperToolsPresentationPreferenceFromUI()
|
||||||
|
|
@ -4527,7 +4532,7 @@ extension BrowserPanel {
|
||||||
if visible {
|
if visible {
|
||||||
developerToolsDetachedOpenGraceDeadline = nil
|
developerToolsDetachedOpenGraceDeadline = nil
|
||||||
syncDeveloperToolsPresentationPreferenceFromUI()
|
syncDeveloperToolsPresentationPreferenceFromUI()
|
||||||
preferredDeveloperToolsVisible = true
|
setPreferredDeveloperToolsVisible(true)
|
||||||
developerToolsLastKnownVisibleAt = Date()
|
developerToolsLastKnownVisibleAt = Date()
|
||||||
cancelDeveloperToolsRestoreRetry()
|
cancelDeveloperToolsRestoreRetry()
|
||||||
return
|
return
|
||||||
|
|
@ -4535,7 +4540,7 @@ extension BrowserPanel {
|
||||||
if preserveVisibleIntent && preferredDeveloperToolsVisible {
|
if preserveVisibleIntent && preferredDeveloperToolsVisible {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
preferredDeveloperToolsVisible = false
|
setPreferredDeveloperToolsVisible(false)
|
||||||
developerToolsLastKnownVisibleAt = nil
|
developerToolsLastKnownVisibleAt = nil
|
||||||
cancelDeveloperToolsRestoreRetry()
|
cancelDeveloperToolsRestoreRetry()
|
||||||
}
|
}
|
||||||
|
|
@ -4590,7 +4595,7 @@ extension BrowserPanel {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
preferredDeveloperToolsVisible = false
|
setPreferredDeveloperToolsVisible(false)
|
||||||
developerToolsDetachedOpenGraceDeadline = nil
|
developerToolsDetachedOpenGraceDeadline = nil
|
||||||
developerToolsLastKnownVisibleAt = nil
|
developerToolsLastKnownVisibleAt = nil
|
||||||
forceDeveloperToolsRefreshOnNextAttach = false
|
forceDeveloperToolsRefreshOnNextAttach = false
|
||||||
|
|
@ -4636,7 +4641,7 @@ extension BrowserPanel {
|
||||||
|
|
||||||
let detachedOpenStillSettling = developerToolsDetachedOpenGraceDeadline.map { $0 > Date() } ?? false
|
let detachedOpenStillSettling = developerToolsDetachedOpenGraceDeadline.map { $0 > Date() } ?? false
|
||||||
if preferredDeveloperToolsPresentation == .detached && !detachedOpenStillSettling {
|
if preferredDeveloperToolsPresentation == .detached && !detachedOpenStillSettling {
|
||||||
preferredDeveloperToolsVisible = false
|
setPreferredDeveloperToolsVisible(false)
|
||||||
developerToolsDetachedOpenGraceDeadline = nil
|
developerToolsDetachedOpenGraceDeadline = nil
|
||||||
cancelDeveloperToolsRestoreRetry()
|
cancelDeveloperToolsRestoreRetry()
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
|
|
@ -4663,7 +4668,7 @@ extension BrowserPanel {
|
||||||
cmuxWithWindowFirstResponderBypass {
|
cmuxWithWindowFirstResponderBypass {
|
||||||
_ = revealDeveloperTools(inspector)
|
_ = revealDeveloperTools(inspector)
|
||||||
}
|
}
|
||||||
preferredDeveloperToolsVisible = true
|
setPreferredDeveloperToolsVisible(true)
|
||||||
let visibleAfterShow = inspector.cmuxCallBool(selector: NSSelectorFromString("isVisible")) ?? false
|
let visibleAfterShow = inspector.cmuxCallBool(selector: NSSelectorFromString("isVisible")) ?? false
|
||||||
if visibleAfterShow {
|
if visibleAfterShow {
|
||||||
syncDeveloperToolsPresentationPreferenceFromUI()
|
syncDeveloperToolsPresentationPreferenceFromUI()
|
||||||
|
|
|
||||||
|
|
@ -460,7 +460,7 @@ struct BrowserPanelView: View {
|
||||||
searchState: searchState,
|
searchState: searchState,
|
||||||
focusRequestGeneration: panel.searchFocusRequestGeneration,
|
focusRequestGeneration: panel.searchFocusRequestGeneration,
|
||||||
canApplyFocusRequest: { generation in
|
canApplyFocusRequest: { generation in
|
||||||
panel.canApplySearchFocusRequest(generation)
|
canApplyBrowserFindFieldFocusRequest(generation)
|
||||||
},
|
},
|
||||||
onNext: { panel.findNext() },
|
onNext: { panel.findNext() },
|
||||||
onPrevious: { panel.findPrevious() },
|
onPrevious: { panel.findPrevious() },
|
||||||
|
|
@ -1133,7 +1133,7 @@ struct BrowserPanelView: View {
|
||||||
searchState: searchState,
|
searchState: searchState,
|
||||||
focusRequestGeneration: panel.searchFocusRequestGeneration,
|
focusRequestGeneration: panel.searchFocusRequestGeneration,
|
||||||
canApplyFocusRequest: { generation in
|
canApplyFocusRequest: { generation in
|
||||||
panel.canApplySearchFocusRequest(generation)
|
canApplyBrowserFindFieldFocusRequest(generation)
|
||||||
},
|
},
|
||||||
onNext: { panel.findNext() },
|
onNext: { panel.findNext() },
|
||||||
onPrevious: { panel.findPrevious() },
|
onPrevious: { panel.findPrevious() },
|
||||||
|
|
@ -1299,6 +1299,10 @@ struct BrowserPanelView: View {
|
||||||
return workspace.focusedPanelId == panel.id
|
return workspace.focusedPanelId == panel.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func canApplyBrowserFindFieldFocusRequest(_ generation: UInt64) -> Bool {
|
||||||
|
isPanelFocusedInModel() && panel.canApplySearchFocusRequest(generation)
|
||||||
|
}
|
||||||
|
|
||||||
private func shouldApplyAddressBarExitFallback(in window: NSWindow) -> Bool {
|
private func shouldApplyAddressBarExitFallback(in window: NSWindow) -> Bool {
|
||||||
// Navigation-triggered omnibar blur can still be unwinding when Cmd+F opens
|
// Navigation-triggered omnibar blur can still be unwinding when Cmd+F opens
|
||||||
// the browser find bar. Once find is visible, any delayed omnibar-exit
|
// the browser find bar. Once find is visible, any delayed omnibar-exit
|
||||||
|
|
|
||||||
|
|
@ -3079,17 +3079,17 @@ class TabManager: ObservableObject {
|
||||||
guard let selectedTabId,
|
guard let selectedTabId,
|
||||||
let tab = tabs.first(where: { $0.id == selectedTabId }) else { return }
|
let tab = tabs.first(where: { $0.id == selectedTabId }) else { return }
|
||||||
|
|
||||||
// Try to restore previous focus
|
let panelId: UUID
|
||||||
if let restoredPanelId = lastFocusedPanelByTab[selectedTabId],
|
if let restoredPanelId = lastFocusedPanelByTab[selectedTabId],
|
||||||
tab.panels[restoredPanelId] != nil,
|
tab.panels[restoredPanelId] != nil {
|
||||||
tab.focusedPanelId != restoredPanelId {
|
panelId = restoredPanelId
|
||||||
tab.focusPanel(restoredPanelId)
|
} else if let focusedPanelId = tab.focusedPanelId,
|
||||||
|
tab.panels[focusedPanelId] != nil {
|
||||||
|
panelId = focusedPanelId
|
||||||
|
} else {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Focus the panel
|
|
||||||
guard let panelId = tab.focusedPanelId,
|
|
||||||
let panel = tab.panels[panelId] else { return }
|
|
||||||
|
|
||||||
// Defer unfocusing the previous workspace's panel until ContentView confirms handoff
|
// Defer unfocusing the previous workspace's panel until ContentView confirms handoff
|
||||||
// completion (new workspace has focus or timeout fallback), to avoid a visible freeze gap.
|
// completion (new workspace has focus or timeout fallback), to avoid a visible freeze gap.
|
||||||
if let previousTabId,
|
if let previousTabId,
|
||||||
|
|
@ -3101,12 +3101,9 @@ class TabManager: ObservableObject {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
panel.focus()
|
// Route workspace reactivation through the normal focus machinery so panel-local
|
||||||
|
// activation intents like browser find-field focus are restored on return.
|
||||||
// For terminal panels, ensure proper focus handling
|
tab.focusPanel(panelId)
|
||||||
if let terminalPanel = panel as? TerminalPanel {
|
|
||||||
terminalPanel.hostedView.ensureFocus(for: selectedTabId, surfaceId: panelId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func completePendingWorkspaceUnfocus(reason: String) {
|
func completePendingWorkspaceUnfocus(reason: String) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import XCTest
|
import XCTest
|
||||||
|
import Combine
|
||||||
import AppKit
|
import AppKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
|
|
@ -1941,6 +1942,34 @@ final class BrowserDeveloperToolsVisibilityPersistenceTests: XCTestCase {
|
||||||
XCTAssertEqual(inspector.showCount, 2)
|
XCTAssertEqual(inspector.showCount, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testSyncDoesNotRepublishHiddenDeveloperToolsIntentWhenInspectorAlreadyHidden() {
|
||||||
|
let (panel, inspector) = makePanelWithInspector(hideBehavior: .hides)
|
||||||
|
|
||||||
|
XCTAssertTrue(panel.showDeveloperTools())
|
||||||
|
waitForDeveloperToolsTransitions()
|
||||||
|
XCTAssertTrue(panel.isDeveloperToolsVisible())
|
||||||
|
|
||||||
|
inspector.hide()
|
||||||
|
XCTAssertFalse(panel.isDeveloperToolsVisible())
|
||||||
|
|
||||||
|
panel.syncDeveloperToolsPreferenceFromInspector()
|
||||||
|
waitForDeveloperToolsTransitions()
|
||||||
|
|
||||||
|
var publishCount = 0
|
||||||
|
let cancellable = panel.objectWillChange.sink {
|
||||||
|
publishCount += 1
|
||||||
|
}
|
||||||
|
defer { _ = cancellable }
|
||||||
|
|
||||||
|
panel.syncDeveloperToolsPreferenceFromInspector()
|
||||||
|
|
||||||
|
XCTAssertEqual(
|
||||||
|
publishCount,
|
||||||
|
0,
|
||||||
|
"Repeated hidden-inspector syncs should not republish the same hidden DevTools intent"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func testForcedRefreshAfterAttachKeepsVisibleInspectorState() {
|
func testForcedRefreshAfterAttachKeepsVisibleInspectorState() {
|
||||||
let (panel, inspector) = makePanelWithInspector()
|
let (panel, inspector) = makePanelWithInspector()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -404,6 +404,51 @@ final class CommandPaletteOpenShortcutConsumptionTests: XCTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
final class CommandPaletteFocusStealerClassificationTests: XCTestCase {
|
||||||
|
private final class NonViewTextDelegate: NSObject, NSTextViewDelegate {}
|
||||||
|
|
||||||
|
func testTreatsGhosttySurfaceViewAsFocusStealer() {
|
||||||
|
let surfaceView = GhosttyNSView(frame: NSRect(x: 0, y: 0, width: 120, height: 80))
|
||||||
|
|
||||||
|
XCTAssertTrue(isCommandPaletteFocusStealingTerminalOrBrowserResponder(surfaceView))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTreatsTextFieldInsideTerminalHostedViewAsFocusStealer() {
|
||||||
|
let hostedView = GhosttySurfaceScrollView(
|
||||||
|
surfaceView: GhosttyNSView(frame: NSRect(x: 0, y: 0, width: 120, height: 80))
|
||||||
|
)
|
||||||
|
let textField = NSTextField(frame: NSRect(x: 0, y: 0, width: 120, height: 24))
|
||||||
|
hostedView.addSubview(textField)
|
||||||
|
|
||||||
|
XCTAssertTrue(
|
||||||
|
isCommandPaletteFocusStealingTerminalOrBrowserResponder(textField),
|
||||||
|
"Terminal-owned overlay text inputs should not be allowed to reclaim focus from the command palette"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDoesNotTreatUnrelatedTextFieldAsFocusStealer() {
|
||||||
|
let textField = NSTextField(frame: NSRect(x: 0, y: 0, width: 120, height: 24))
|
||||||
|
|
||||||
|
XCTAssertFalse(isCommandPaletteFocusStealingTerminalOrBrowserResponder(textField))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTreatsTextViewInsideTerminalHostedViewAsFocusStealerWhenDelegateIsNotAView() {
|
||||||
|
let hostedView = GhosttySurfaceScrollView(
|
||||||
|
surfaceView: GhosttyNSView(frame: NSRect(x: 0, y: 0, width: 120, height: 80))
|
||||||
|
)
|
||||||
|
let textView = NSTextView(frame: NSRect(x: 0, y: 0, width: 120, height: 24))
|
||||||
|
let delegate = NonViewTextDelegate()
|
||||||
|
textView.delegate = delegate
|
||||||
|
hostedView.addSubview(textView)
|
||||||
|
|
||||||
|
XCTAssertTrue(
|
||||||
|
isCommandPaletteFocusStealingTerminalOrBrowserResponder(textView),
|
||||||
|
"NSTextView responders should still be blocked via the NSView hierarchy walk when the delegate is not a view"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
final class CommandPaletteRestoreFocusStateMachineTests: XCTestCase {
|
final class CommandPaletteRestoreFocusStateMachineTests: XCTestCase {
|
||||||
func testRestoresBrowserAddressBarWhenPaletteOpenedFromFocusedAddressBar() {
|
func testRestoresBrowserAddressBarWhenPaletteOpenedFromFocusedAddressBar() {
|
||||||
let panelId = UUID()
|
let panelId = UUID()
|
||||||
|
|
|
||||||
|
|
@ -853,11 +853,118 @@ final class BrowserPaneNavigationKeybindUITests: XCTestCase {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testBrowserFindFieldKeepsFocusAfterNewWorkspaceRoundTrip() {
|
||||||
|
let app = XCUIApplication()
|
||||||
|
app.launchEnvironment["CMUX_SOCKET_PATH"] = socketPath
|
||||||
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_RECORD_ONLY"] = "1"
|
||||||
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_PATH"] = dataPath
|
||||||
|
launchAndEnsureForeground(app)
|
||||||
|
|
||||||
|
let window = app.windows.firstMatch
|
||||||
|
_ = window.waitForExistence(timeout: 2.0)
|
||||||
|
|
||||||
|
app.typeKey("d", modifierFlags: [.command])
|
||||||
|
XCTAssertTrue(
|
||||||
|
waitForDataMatch(timeout: 6.0) { data in
|
||||||
|
guard data["lastSplitDirection"] == "right" else { return false }
|
||||||
|
guard let paneCountAfterSplit = Int(data["paneCountAfterSplit"] ?? "") else { return false }
|
||||||
|
return paneCountAfterSplit >= 2
|
||||||
|
},
|
||||||
|
"Expected Cmd+D to create a split before opening the browser. data=\(String(describing: loadData()))"
|
||||||
|
)
|
||||||
|
|
||||||
|
app.typeKey("l", modifierFlags: [.command])
|
||||||
|
|
||||||
|
let omnibar = app.textFields["BrowserOmnibarTextField"].firstMatch
|
||||||
|
XCTAssertTrue(omnibar.waitForExistence(timeout: 8.0), "Expected browser omnibar after Cmd+L")
|
||||||
|
|
||||||
|
app.typeKey("a", modifierFlags: [.command])
|
||||||
|
app.typeKey(XCUIKeyboardKey.delete.rawValue, modifierFlags: [])
|
||||||
|
app.typeText("example.com")
|
||||||
|
app.typeKey(XCUIKeyboardKey.return.rawValue, modifierFlags: [])
|
||||||
|
|
||||||
|
XCTAssertTrue(
|
||||||
|
waitForOmnibarToContainExampleDomain(omnibar, timeout: 8.0),
|
||||||
|
"Expected browser navigation to example domain before opening find. value=\(String(describing: omnibar.value))"
|
||||||
|
)
|
||||||
|
|
||||||
|
app.typeKey("f", modifierFlags: [.command])
|
||||||
|
|
||||||
|
let findField = app.textFields["BrowserFindSearchTextField"].firstMatch
|
||||||
|
XCTAssertTrue(findField.waitForExistence(timeout: 6.0), "Expected browser find field after Cmd+F")
|
||||||
|
|
||||||
|
app.typeText("seed")
|
||||||
|
XCTAssertTrue(
|
||||||
|
waitForCondition(timeout: 4.0) {
|
||||||
|
((findField.value as? String) ?? "") == "seed"
|
||||||
|
},
|
||||||
|
"Expected browser find field to capture initial typing. value=\(String(describing: findField.value))"
|
||||||
|
)
|
||||||
|
|
||||||
|
app.typeKey("p", modifierFlags: [.command, .shift])
|
||||||
|
|
||||||
|
let paletteSearchField = app.textFields["CommandPaletteSearchField"].firstMatch
|
||||||
|
XCTAssertTrue(paletteSearchField.waitForExistence(timeout: 5.0), "Expected command palette search field")
|
||||||
|
paletteSearchField.click()
|
||||||
|
paletteSearchField.typeText("New Workspace")
|
||||||
|
|
||||||
|
let firstResultRow = app.descendants(matching: .any).matching(identifier: "CommandPaletteResultRow.0").firstMatch
|
||||||
|
XCTAssertTrue(firstResultRow.waitForExistence(timeout: 5.0), "Expected command palette results for New Workspace")
|
||||||
|
app.typeKey(XCUIKeyboardKey.return.rawValue, modifierFlags: [])
|
||||||
|
|
||||||
|
XCTAssertTrue(
|
||||||
|
waitForNonExistence(paletteSearchField, timeout: 5.0),
|
||||||
|
"Expected command palette to dismiss after creating a workspace"
|
||||||
|
)
|
||||||
|
|
||||||
|
app.typeKey("1", modifierFlags: [.command])
|
||||||
|
|
||||||
|
let restoredFindField = app.textFields["BrowserFindSearchTextField"].firstMatch
|
||||||
|
XCTAssertTrue(restoredFindField.waitForExistence(timeout: 6.0), "Expected browser find field after returning to workspace 1")
|
||||||
|
XCTAssertTrue(
|
||||||
|
waitForCondition(timeout: 4.0) {
|
||||||
|
((restoredFindField.value as? String) ?? "") == "seed"
|
||||||
|
},
|
||||||
|
"Expected existing browser find query to persist after returning. value=\(String(describing: restoredFindField.value))"
|
||||||
|
)
|
||||||
|
|
||||||
|
app.typeText("x")
|
||||||
|
XCTAssertTrue(
|
||||||
|
waitForCondition(timeout: 4.0) {
|
||||||
|
((restoredFindField.value as? String) ?? "") == "seedx"
|
||||||
|
},
|
||||||
|
"Expected typing after returning from a new workspace to stay in the browser find field. " +
|
||||||
|
"findValue=\(String(describing: restoredFindField.value)) omnibarValue=\(String(describing: omnibar.value))"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testWorkspaceRoundTripPreservesFocusedTerminalFindWhenBrowserFindIsAlsoOpen() {
|
||||||
|
runSplitFindWorkspaceRoundTripScenario(restoredOwner: .terminal)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testWorkspaceRoundTripPreservesFocusedBrowserFindWhenTerminalFindIsAlsoOpen() {
|
||||||
|
runSplitFindWorkspaceRoundTripScenario(restoredOwner: .browser)
|
||||||
|
}
|
||||||
|
|
||||||
private enum FindFocusRoute {
|
private enum FindFocusRoute {
|
||||||
case cmdOptionArrows
|
case cmdOptionArrows
|
||||||
case cmdCtrlLetters
|
case cmdCtrlLetters
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enum SplitFindOwner {
|
||||||
|
case terminal
|
||||||
|
case browser
|
||||||
|
|
||||||
|
var focusedPanelKind: String {
|
||||||
|
switch self {
|
||||||
|
case .terminal:
|
||||||
|
return "terminal"
|
||||||
|
case .browser:
|
||||||
|
return "browser"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func runFindFocusPersistenceScenario(route: FindFocusRoute, useAutofocusRacePage: Bool) {
|
private func runFindFocusPersistenceScenario(route: FindFocusRoute, useAutofocusRacePage: Bool) {
|
||||||
let app = XCUIApplication()
|
let app = XCUIApplication()
|
||||||
app.launchEnvironment["CMUX_SOCKET_PATH"] = socketPath
|
app.launchEnvironment["CMUX_SOCKET_PATH"] = socketPath
|
||||||
|
|
@ -967,6 +1074,124 @@ final class BrowserPaneNavigationKeybindUITests: XCTestCase {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func runSplitFindWorkspaceRoundTripScenario(restoredOwner: SplitFindOwner) {
|
||||||
|
let app = XCUIApplication()
|
||||||
|
app.launchEnvironment["CMUX_SOCKET_PATH"] = socketPath
|
||||||
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_RECORD_ONLY"] = "1"
|
||||||
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_PATH"] = dataPath
|
||||||
|
launchAndEnsureForeground(app)
|
||||||
|
|
||||||
|
let window = app.windows.firstMatch
|
||||||
|
XCTAssertTrue(window.waitForExistence(timeout: 10.0), "Expected main window to exist")
|
||||||
|
|
||||||
|
app.typeKey("d", modifierFlags: [.command])
|
||||||
|
focusRightPaneForFindScenario(app, route: .cmdOptionArrows)
|
||||||
|
|
||||||
|
app.typeKey("l", modifierFlags: [.command, .shift])
|
||||||
|
let omnibar = app.textFields["BrowserOmnibarTextField"].firstMatch
|
||||||
|
XCTAssertTrue(omnibar.waitForExistence(timeout: 8.0), "Expected browser omnibar after Cmd+Shift+L")
|
||||||
|
|
||||||
|
app.typeKey("a", modifierFlags: [.command])
|
||||||
|
app.typeKey(XCUIKeyboardKey.delete.rawValue, modifierFlags: [])
|
||||||
|
app.typeText("example.com")
|
||||||
|
app.typeKey(XCUIKeyboardKey.return.rawValue, modifierFlags: [])
|
||||||
|
|
||||||
|
XCTAssertTrue(
|
||||||
|
waitForOmnibarToContainExampleDomain(omnibar, timeout: 8.0),
|
||||||
|
"Expected browser navigation to example domain before running workspace round trip. value=\(String(describing: omnibar.value))"
|
||||||
|
)
|
||||||
|
|
||||||
|
focusLeftPaneForFindScenario(app, route: .cmdOptionArrows)
|
||||||
|
XCTAssertTrue(
|
||||||
|
waitForDataMatch(timeout: 6.0) { data in
|
||||||
|
data["focusedPanelKind"] == "terminal"
|
||||||
|
},
|
||||||
|
"Expected left terminal pane to be focused before opening terminal find. data=\(String(describing: loadData()))"
|
||||||
|
)
|
||||||
|
app.typeKey("f", modifierFlags: [.command])
|
||||||
|
app.typeText("la")
|
||||||
|
|
||||||
|
focusRightPaneForFindScenario(app, route: .cmdOptionArrows)
|
||||||
|
XCTAssertTrue(
|
||||||
|
waitForDataMatch(timeout: 6.0) { data in
|
||||||
|
data["focusedPanelKind"] == "browser"
|
||||||
|
&& data["terminalFindNeedle"] == "la"
|
||||||
|
},
|
||||||
|
"Expected terminal find query to persist before opening browser find. data=\(String(describing: loadData()))"
|
||||||
|
)
|
||||||
|
app.typeKey("f", modifierFlags: [.command])
|
||||||
|
app.typeText("am")
|
||||||
|
|
||||||
|
switch restoredOwner {
|
||||||
|
case .terminal:
|
||||||
|
focusLeftPaneForFindScenario(app, route: .cmdOptionArrows)
|
||||||
|
case .browser:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertTrue(
|
||||||
|
waitForDataMatch(timeout: 6.0) { data in
|
||||||
|
data["focusedPanelKind"] == restoredOwner.focusedPanelKind
|
||||||
|
&& data["terminalFindNeedle"] == "la"
|
||||||
|
&& data["browserFindNeedle"] == "am"
|
||||||
|
},
|
||||||
|
"Expected the intended find owner before leaving workspace 1. data=\(String(describing: loadData()))"
|
||||||
|
)
|
||||||
|
|
||||||
|
openCommandPaletteForNewWorkspace(app)
|
||||||
|
app.typeKey("1", modifierFlags: [.command])
|
||||||
|
|
||||||
|
XCTAssertTrue(
|
||||||
|
waitForDataMatch(timeout: 6.0) { data in
|
||||||
|
data["focusedPanelKind"] == restoredOwner.focusedPanelKind
|
||||||
|
&& data["terminalFindNeedle"] == "la"
|
||||||
|
&& data["browserFindNeedle"] == "am"
|
||||||
|
},
|
||||||
|
"Expected the previously focused find owner to be restored after the workspace round trip. data=\(String(describing: loadData()))"
|
||||||
|
)
|
||||||
|
|
||||||
|
switch restoredOwner {
|
||||||
|
case .terminal:
|
||||||
|
app.typeText("foo")
|
||||||
|
XCTAssertTrue(
|
||||||
|
waitForDataMatch(timeout: 6.0) { data in
|
||||||
|
data["focusedPanelKind"] == "terminal"
|
||||||
|
&& data["terminalFindNeedle"] == "lafoo"
|
||||||
|
&& data["browserFindNeedle"] == "am"
|
||||||
|
},
|
||||||
|
"Expected typing after returning to stay in terminal find. data=\(String(describing: loadData()))"
|
||||||
|
)
|
||||||
|
case .browser:
|
||||||
|
app.typeText("do")
|
||||||
|
XCTAssertTrue(
|
||||||
|
waitForDataMatch(timeout: 6.0) { data in
|
||||||
|
data["focusedPanelKind"] == "browser"
|
||||||
|
&& data["terminalFindNeedle"] == "la"
|
||||||
|
&& data["browserFindNeedle"] == "amdo"
|
||||||
|
},
|
||||||
|
"Expected typing after returning to stay in browser find. data=\(String(describing: loadData()))"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func openCommandPaletteForNewWorkspace(_ app: XCUIApplication) {
|
||||||
|
app.typeKey("p", modifierFlags: [.command, .shift])
|
||||||
|
|
||||||
|
let paletteSearchField = app.textFields["CommandPaletteSearchField"].firstMatch
|
||||||
|
XCTAssertTrue(paletteSearchField.waitForExistence(timeout: 5.0), "Expected command palette search field")
|
||||||
|
paletteSearchField.click()
|
||||||
|
paletteSearchField.typeText("New Workspace")
|
||||||
|
|
||||||
|
let firstResultRow = app.descendants(matching: .any).matching(identifier: "CommandPaletteResultRow.0").firstMatch
|
||||||
|
XCTAssertTrue(firstResultRow.waitForExistence(timeout: 5.0), "Expected command palette results for New Workspace")
|
||||||
|
app.typeKey(XCUIKeyboardKey.return.rawValue, modifierFlags: [])
|
||||||
|
|
||||||
|
XCTAssertTrue(
|
||||||
|
waitForNonExistence(paletteSearchField, timeout: 5.0),
|
||||||
|
"Expected command palette to dismiss after creating a workspace"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private func focusLeftPaneForFindScenario(_ app: XCUIApplication, route: FindFocusRoute) {
|
private func focusLeftPaneForFindScenario(_ app: XCUIApplication, route: FindFocusRoute) {
|
||||||
switch route {
|
switch route {
|
||||||
case .cmdOptionArrows:
|
case .cmdOptionArrows:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue