diff --git a/GhosttyTabsUITests/BrowserPaneNavigationKeybindUITests.swift b/GhosttyTabsUITests/BrowserPaneNavigationKeybindUITests.swift index 7694b5e3..6027832a 100644 --- a/GhosttyTabsUITests/BrowserPaneNavigationKeybindUITests.swift +++ b/GhosttyTabsUITests/BrowserPaneNavigationKeybindUITests.swift @@ -157,7 +157,12 @@ final class BrowserPaneNavigationKeybindUITests: XCTestCase { ) // Escape should leave the omnibar and focus WebKit again. + // Send Escape twice: the first may only clear suggestions/editing state + // (Chrome-like two-stage escape), the second triggers blur to WebView. app.typeKey(XCUIKeyboardKey.escape.rawValue, modifierFlags: []) + if !waitForDataMatch(timeout: 2.0, predicate: { $0["webViewFocusedAfterAddressBarExit"] == "true" }) { + app.typeKey(XCUIKeyboardKey.escape.rawValue, modifierFlags: []) + } XCTAssertTrue( waitForDataMatch(timeout: 5.0) { data in data["webViewFocusedAfterAddressBarExit"] == "true" diff --git a/GhosttyTabsUITests/MenuKeyEquivalentRoutingUITests.swift b/GhosttyTabsUITests/MenuKeyEquivalentRoutingUITests.swift index 8709e8e7..ef2a79f1 100644 --- a/GhosttyTabsUITests/MenuKeyEquivalentRoutingUITests.swift +++ b/GhosttyTabsUITests/MenuKeyEquivalentRoutingUITests.swift @@ -111,7 +111,12 @@ final class MenuKeyEquivalentRoutingUITests: XCTestCase { ) // Escape should leave the omnibar and focus WebKit again. + // Send Escape twice: the first may only clear suggestions/editing state + // (Chrome-like two-stage escape), the second triggers blur to WebView. app.typeKey(XCUIKeyboardKey.escape.rawValue, modifierFlags: []) + if !waitForGotoSplitMatch(timeout: 2.0, predicate: { $0["webViewFocusedAfterAddressBarExit"] == "true" }) { + app.typeKey(XCUIKeyboardKey.escape.rawValue, modifierFlags: []) + } XCTAssertTrue( waitForGotoSplitMatch(timeout: 5.0) { data in data["webViewFocusedAfterAddressBarExit"] == "true" diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index cdeba617..8f0c4bc2 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -1188,11 +1188,22 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent } private func recordGotoSplitUITestWebViewFocus(panelId: UUID, key: String) { - // Give the responder chain time to settle. - DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in + // Give the responder chain time to settle, retrying for slow environments (e.g. VM). + recordGotoSplitUITestWebViewFocusRetry(panelId: panelId, key: key, attempt: 0) + } + + private func recordGotoSplitUITestWebViewFocusRetry(panelId: UUID, key: String, attempt: Int) { + let delays: [Double] = [0.05, 0.1, 0.25, 0.5] + let delay = attempt < delays.count ? delays[attempt] : delays.last! + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in guard let self, let tabManager, let tab = tabManager.selectedWorkspace, let panel = tab.browserPanel(for: panelId) else { return } let focused = self.isWebViewFocused(panel) + // If focus hasn't settled yet and we have retries left, try again. + if !focused && key.contains("Exit") && attempt < delays.count - 1 { + self.recordGotoSplitUITestWebViewFocusRetry(panelId: panelId, key: key, attempt: attempt + 1) + return + } self.writeGotoSplitTestData([ key: focused ? "true" : "false", "\(key)PanelId": panelId.uuidString