Skip to content
Open
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
cda19c5
Add browser omnibar stale-focus regression test
lawrencecchen Mar 19, 2026
2a11cfe
Clear stale browser omnibar focus on web view focus
lawrencecchen Mar 19, 2026
70221f4
Add browser arrow key XCUITest after Cmd+L
lawrencecchen Mar 21, 2026
d1a1d91
Fix test-e2e workflow shell quoting
lawrencecchen Mar 21, 2026
0a1342b
Stabilize browser arrow key XCUITest setup
lawrencecchen Mar 21, 2026
e52458a
Broaden UI test cmux CLI discovery
lawrencecchen Mar 21, 2026
c1a5dab
Use browser web area as XCUITest click target
lawrencecchen Mar 21, 2026
04c0ac5
Wait for browser readiness before DOM harness injection
lawrencecchen Mar 21, 2026
ec96bdc
Stabilize browser keybind UI test launch
lawrencecchen Mar 21, 2026
0fb8150
Log browser UI test launch diagnostics
lawrencecchen Mar 21, 2026
ddb0159
Retry UI test startup activation
lawrencecchen Mar 21, 2026
ccce76a
Harden UI test startup activation
lawrencecchen Mar 21, 2026
b75a69c
Retry alternate UI test CLI paths
lawrencecchen Mar 21, 2026
6a479d1
Use control socket for browser arrow XCUITest
lawrencecchen Mar 21, 2026
aeddd83
Allow arrow XCUITest socket access
lawrencecchen Mar 21, 2026
4003142
Wait longer for browser socket replies
lawrencecchen Mar 21, 2026
d59f6e8
Poll browser eval for DOM readiness
lawrencecchen Mar 21, 2026
4029cf0
Add browser arrow omnibar XCUITest path
lawrencecchen Mar 21, 2026
d04bbf4
Retry browser arrow UI test page-input setup
lawrencecchen Mar 21, 2026
0c409de
Use raw HID arrow keys in browser XCUITest
lawrencecchen Mar 21, 2026
b3cb3b5
Route browser XCUITest arrows via simulate_shortcut
lawrencecchen Mar 21, 2026
b936e9c
Use in-process socket for browser XCUITest shortcuts
lawrencecchen Mar 21, 2026
73b29cd
Use raw socket shortcut command in browser XCUITest
lawrencecchen Mar 21, 2026
7f23a1e
Allow browser XCUITests to use the control socket
lawrencecchen Mar 21, 2026
0c92a89
Use notifications for browser XCUITest shortcuts
lawrencecchen Mar 21, 2026
de529f3
Use native key events in browser XCUITest
lawrencecchen Mar 21, 2026
5b805b0
Record real primary click for browser XCUITest
lawrencecchen Mar 21, 2026
61822eb
Record browser responder shape in arrow XCUITest
lawrencecchen Mar 21, 2026
b902e4d
Route browser arrows before AppKit key handling
lawrencecchen Mar 21, 2026
393da6c
Record omnibar focus after actual focus
lawrencecchen Mar 21, 2026
e286479
Generalize browser key routing
lawrencecchen Mar 22, 2026
a0b1e9c
Stabilize browser arrow UI test
lawrencecchen Mar 22, 2026
bbdcf5e
Add contenteditable browser arrow regression test
lawrencecchen Mar 22, 2026
106dc36
Stabilize browser contenteditable XCUITest launch
lawrencecchen Mar 23, 2026
e03ac68
Keep browser XCUITest socket override with launch tag
lawrencecchen Mar 23, 2026
0fd8c45
Wait for contenteditable fixture prerequisites
lawrencecchen Mar 23, 2026
23d793b
Capture browser eval failures in contenteditable XCUITest
lawrencecchen Mar 23, 2026
010edb8
Move contenteditable arrow XCUITest to app-side harness
lawrencecchen Mar 23, 2026
94c59af
Merge remote-tracking branch 'origin/main' into task-browser-up-down-…
lawrencecchen Mar 24, 2026
d0d4298
Add browser textarea and omnibar arrow regressions
lawrencecchen Mar 24, 2026
eca313f
Clear stale omnibar focus on browser clicks
lawrencecchen Mar 24, 2026
cbe375c
Route browser content keys around performKeyEquivalent
lawrencecchen Mar 24, 2026
e259eae
Preserve webview click blur intent for omnibar
lawrencecchen Mar 24, 2026
233a91c
Add browser webview click focus regressions
lawrencecchen Mar 24, 2026
c45e7ea
Use authoritative omnibar state for webview click handoff
lawrencecchen Mar 24, 2026
499729f
Fix browser arrow XCUITest counter baselines
lawrencecchen Mar 24, 2026
6d4d57f
Merge remote-tracking branch 'origin/main' into task-browser-up-down-…
lawrencecchen Mar 24, 2026
07b36b9
Fix modified browser arrow XCUITest expectations
lawrencecchen Mar 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,054 changes: 896 additions & 158 deletions Sources/AppDelegate.swift

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions Sources/Panels/BrowserPanel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4908,6 +4908,28 @@ extension BrowserPanel {
#endif
}

func syncWebViewFirstResponderPolicy(isPanelFocused: Bool, reason: String) {
guard let cmuxWebView = webView as? CmuxWebView else { return }
let next = isPanelFocused && !shouldSuppressWebViewFocus()
if cmuxWebView.allowsFirstResponderAcquisition != next {
#if DEBUG
dlog(
"browser.focus.policy.resync panel=\(id.uuidString.prefix(5)) " +
"web=\(ObjectIdentifier(cmuxWebView)) old=\(cmuxWebView.allowsFirstResponderAcquisition ? 1 : 0) " +
"new=\(next ? 1 : 0) reason=\(reason) " +
"panelFocusedUsed=\(isPanelFocused ? 1 : 0)"
)
#endif
}
cmuxWebView.allowsFirstResponderAcquisition = next
}

func prepareForExplicitWebViewFocus(isPanelFocused: Bool, reason: String) {
endSuppressWebViewFocusForAddressBar()
clearWebViewFocusSuppression()
syncWebViewFirstResponderPolicy(isPanelFocused: isPanelFocused, reason: reason)
}

func shouldSuppressOmnibarAutofocus() -> Bool {
if let until = suppressOmnibarAutofocusUntil {
return Date() < until
Expand Down
32 changes: 6 additions & 26 deletions Sources/Panels/BrowserPanelView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1239,20 +1239,8 @@ struct BrowserPanelView: View {
reason: String,
isPanelFocusedOverride: Bool? = nil
) {
guard let cmuxWebView = panel.webView as? CmuxWebView else { return }
let isPanelFocused = isPanelFocusedOverride ?? isFocused
let next = isPanelFocused && !panel.shouldSuppressWebViewFocus()
if cmuxWebView.allowsFirstResponderAcquisition != next {
#if DEBUG
dlog(
"browser.focus.policy.resync panel=\(panel.id.uuidString.prefix(5)) " +
"web=\(ObjectIdentifier(cmuxWebView)) old=\(cmuxWebView.allowsFirstResponderAcquisition ? 1 : 0) " +
"new=\(next ? 1 : 0) reason=\(reason) " +
"panelFocusedUsed=\(isPanelFocused ? 1 : 0)"
)
#endif
}
cmuxWebView.allowsFirstResponderAcquisition = next
panel.syncWebViewFirstResponderPolicy(isPanelFocused: isPanelFocused, reason: reason)
}

private func setAddressBarFocused(_ focused: Bool, reason: String) {
Expand Down Expand Up @@ -6336,19 +6324,11 @@ struct WebViewRepresentable: NSViewRepresentable {
webView: WKWebView,
isPanelFocused: Bool
) {
guard let cmuxWebView = webView as? CmuxWebView else { return }
let next = isPanelFocused && !panel.shouldSuppressWebViewFocus()
if cmuxWebView.allowsFirstResponderAcquisition != next {
#if DEBUG
dlog(
"browser.focus.policy panel=\(panel.id.uuidString.prefix(5)) " +
"web=\(ObjectIdentifier(cmuxWebView)) old=\(cmuxWebView.allowsFirstResponderAcquisition ? 1 : 0) " +
"new=\(next ? 1 : 0) isPanelFocused=\(isPanelFocused ? 1 : 0) " +
"suppress=\(panel.shouldSuppressWebViewFocus() ? 1 : 0)"
)
#endif
}
cmuxWebView.allowsFirstResponderAcquisition = next
guard webView is CmuxWebView else { return }
panel.syncWebViewFirstResponderPolicy(
isPanelFocused: isPanelFocused,
reason: "applyWebViewFirstResponderPolicy"
)
}

static func dismantleNSView(_ nsView: NSView, coordinator: Coordinator) {
Expand Down
20 changes: 14 additions & 6 deletions Sources/TerminalController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8635,6 +8635,12 @@ class TerminalController {
tabManager.selectWorkspace(ws)
}

browserPanel.prepareForExplicitWebViewFocus(
isPanelFocused: true,
reason: "socket.browser.focus_webview"
)
NotificationCenter.default.post(name: .browserDidBlurAddressBar, object: surfaceId)

// Prevent omnibar auto-focus from immediately stealing first responder back.
browserPanel.suppressOmnibarAutofocus(for: 1.0)

Expand Down Expand Up @@ -11804,19 +11810,19 @@ class TerminalController {
case "left":
storedKey = "←"
keyCode = 123
charactersIgnoringModifiers = storedKey
charactersIgnoringModifiers = String(UnicodeScalar(NSLeftArrowFunctionKey)!)
case "right":
storedKey = "→"
keyCode = 124
charactersIgnoringModifiers = storedKey
charactersIgnoringModifiers = String(UnicodeScalar(NSRightArrowFunctionKey)!)
case "down":
storedKey = "↓"
keyCode = 125
charactersIgnoringModifiers = storedKey
charactersIgnoringModifiers = String(UnicodeScalar(NSDownArrowFunctionKey)!)
case "up":
storedKey = "↑"
keyCode = 126
charactersIgnoringModifiers = storedKey
charactersIgnoringModifiers = String(UnicodeScalar(NSUpArrowFunctionKey)!)
case "enter", "return":
storedKey = "\r"
keyCode = UInt16(kVK_Return)
Expand Down Expand Up @@ -13587,8 +13593,10 @@ class TerminalController {

// Programmatic WebView focus should win over stale omnibar focus state, especially
// after workspace switches where the blank-page omnibar auto-focus can re-trigger.
browserPanel.endSuppressWebViewFocusForAddressBar()
browserPanel.clearWebViewFocusSuppression()
browserPanel.prepareForExplicitWebViewFocus(
isPanelFocused: true,
reason: "socket.focus_webview"
)
NotificationCenter.default.post(name: .browserDidBlurAddressBar, object: panelId)

// Prevent omnibar auto-focus from immediately stealing first responder back.
Expand Down
63 changes: 63 additions & 0 deletions cmuxTests/AppDelegateShortcutRoutingTests.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import XCTest
import WebKit

#if canImport(cmux_DEV)
@testable import cmux_DEV
Expand Down Expand Up @@ -2328,6 +2329,68 @@ final class AppDelegateShortcutRoutingTests: XCTestCase {
XCTAssertEqual(observedDelta, 1)
}

func testDownArrowIsNotConsumedAfterBrowserWebViewRetakesFocusFromStaleAddressBarState() {
guard let appDelegate = AppDelegate.shared else {
XCTFail("Expected AppDelegate.shared")
return
}

let windowId = appDelegate.createMainWindow()
defer {
closeWindow(withId: windowId)
}

guard let window = window(withId: windowId),
let contentView = window.contentView else {
XCTFail("Expected test window")
return
}

let webView = CmuxWebView(frame: contentView.bounds, configuration: WKWebViewConfiguration())
webView.autoresizingMask = [.width, .height]
contentView.addSubview(webView)

window.makeKeyAndOrderFront(nil)
RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.05))

XCTAssertTrue(window.makeFirstResponder(webView), "Expected test web view to become first responder")

let stalePanelId = UUID()
NotificationCenter.default.post(name: .browserDidFocusAddressBar, object: stalePanelId)
RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.05))
XCTAssertEqual(
appDelegate.focusedBrowserAddressBarPanelId(),
stalePanelId,
"Expected stale address-bar focus state to be installed for the regression setup"
)

NotificationCenter.default.post(name: .browserDidBecomeFirstResponderWebView, object: webView)
RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.05))

guard let downArrowEvent = makeKeyDownEvent(
key: String(UnicodeScalar(NSDownArrowFunctionKey)!),
modifiers: [],
keyCode: 125,
windowNumber: window.windowNumber
) else {
XCTFail("Failed to construct Down Arrow event")
return
}

#if DEBUG
XCTAssertFalse(
appDelegate.debugHandleCustomShortcut(event: downArrowEvent),
"Plain Down Arrow should reach WebKit once the page retakes focus from the omnibar"
)
#else
XCTFail("debugHandleCustomShortcut is only available in DEBUG")
#endif
XCTAssertNil(
appDelegate.focusedBrowserAddressBarPanelId(),
"Browser web view focus should clear stale omnibar focus state"
)
}

func testEscapeDismissesCommandPaletteWhenVisibilityStateStaysStalePastInitialPendingWindow() {
guard let appDelegate = AppDelegate.shared else {
XCTFail("Expected AppDelegate.shared")
Expand Down
50 changes: 50 additions & 0 deletions cmuxTests/BrowserConfigTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2547,6 +2547,56 @@ final class BrowserReturnKeyDownRoutingTests: XCTestCase {
}
}

final class BrowserDirectKeyDownRoutingTests: XCTestCase {
func testRoutesWhenBrowserWebViewIsFirstResponder() {
let webView = CmuxWebView(frame: .zero, configuration: WKWebViewConfiguration())

XCTAssertTrue(
shouldDirectRouteBrowserFirstResponderKeyDown(
firstResponder: webView,
firstResponderIsBrowser: true,
focusedBrowserAddressBarPanelId: nil
)
)
}

func testRoutesWhenBrowserFieldEditorIsFirstResponder() {
let fieldEditor = NSTextView(frame: .zero)
fieldEditor.isFieldEditor = true

XCTAssertTrue(
shouldDirectRouteBrowserFirstResponderKeyDown(
firstResponder: fieldEditor,
firstResponderIsBrowser: true,
focusedBrowserAddressBarPanelId: nil
)
)
}

func testDoesNotRouteWhenBrowserAddressBarRemainsFocused() {
let webView = CmuxWebView(frame: .zero, configuration: WKWebViewConfiguration())

XCTAssertFalse(
shouldDirectRouteBrowserFirstResponderKeyDown(
firstResponder: webView,
firstResponderIsBrowser: true,
focusedBrowserAddressBarPanelId: UUID()
)
)
}

func testDoesNotRouteWhenFirstResponderIsNotBrowser() {
let view = NSView(frame: .zero)

XCTAssertFalse(
shouldDirectRouteBrowserFirstResponderKeyDown(
firstResponder: view,
firstResponderIsBrowser: false,
focusedBrowserAddressBarPanelId: nil
)
)
}
}

final class BrowserZoomShortcutActionTests: XCTestCase {
func testZoomInSupportsEqualsAndPlusVariants() {
Expand Down
Loading
Loading