On WarpBuild runners without a GUI session, XCUIApplication.launch() blocks ~60s then fails with "Failed to activate application (current state: Running Background)". Wrap launch() in XCTExpectFailure so the test can continue — keyboard and element APIs work via accessibility even when the app is in .runningBackground. Increase test execution time allowance from 120s to 180s to account for the 60s activation timeout on headless runners. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1074 lines
48 KiB
Swift
1074 lines
48 KiB
Swift
import XCTest
|
|
import Foundation
|
|
|
|
final class BrowserPaneNavigationKeybindUITests: XCTestCase {
|
|
private var dataPath = ""
|
|
private var socketPath = ""
|
|
|
|
override func setUp() {
|
|
super.setUp()
|
|
continueAfterFailure = false
|
|
dataPath = "/tmp/cmux-ui-test-goto-split-\(UUID().uuidString).json"
|
|
try? FileManager.default.removeItem(atPath: dataPath)
|
|
socketPath = "/tmp/cmux-ui-test-socket-\(UUID().uuidString).sock"
|
|
try? FileManager.default.removeItem(atPath: socketPath)
|
|
}
|
|
|
|
func testCmdCtrlHMovesLeftWhenWebViewFocused() {
|
|
let app = XCUIApplication()
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_SETUP"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_PATH"] = dataPath
|
|
app.launchEnvironment["CMUX_UI_TEST_FOCUS_SHORTCUTS"] = "1"
|
|
app.launchEnvironment["CMUX_SOCKET_PATH"] = socketPath
|
|
launchAndEnsureForeground(app)
|
|
|
|
XCTAssertTrue(
|
|
waitForData(keys: ["terminalPaneId", "browserPaneId", "webViewFocused"], timeout: 10.0),
|
|
"Expected goto_split setup data to be written"
|
|
)
|
|
|
|
guard let setup = loadData() else {
|
|
XCTFail("Missing goto_split setup data")
|
|
return
|
|
}
|
|
|
|
XCTAssertEqual(setup["webViewFocused"], "true", "Expected WKWebView to be first responder for this test")
|
|
|
|
guard let expectedTerminalPaneId = setup["terminalPaneId"] else {
|
|
XCTFail("Missing terminalPaneId in goto_split setup data")
|
|
return
|
|
}
|
|
|
|
// Trigger pane navigation via the actual key event path (while WebKit is first responder).
|
|
app.typeKey("h", modifierFlags: [.command, .control])
|
|
|
|
XCTAssertTrue(
|
|
waitForDataMatch(timeout: 5.0) { data in
|
|
data["lastMoveDirection"] == "left" && data["focusedPaneId"] == expectedTerminalPaneId
|
|
},
|
|
"Expected Cmd+Ctrl+H to move focus to left pane (terminal)"
|
|
)
|
|
}
|
|
|
|
func testCmdCtrlHMovesLeftWhenWebViewFocusedUsingGhosttyConfigKeybind() {
|
|
// Write a test Ghostty config in the preferred macOS location so GhosttyKit loads it at app startup.
|
|
let fileManager = FileManager.default
|
|
guard let appSupport = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else {
|
|
XCTFail("Missing Application Support directory")
|
|
return
|
|
}
|
|
|
|
let ghosttyDir = appSupport.appendingPathComponent("com.mitchellh.ghostty", isDirectory: true)
|
|
let configURL = ghosttyDir.appendingPathComponent("config.ghostty", isDirectory: false)
|
|
|
|
do {
|
|
try fileManager.createDirectory(at: ghosttyDir, withIntermediateDirectories: true)
|
|
} catch {
|
|
XCTFail("Failed to create Ghostty app support dir: \(error)")
|
|
return
|
|
}
|
|
|
|
let originalConfigData = try? Data(contentsOf: configURL)
|
|
addTeardownBlock {
|
|
if let originalConfigData {
|
|
try? originalConfigData.write(to: configURL, options: .atomic)
|
|
} else {
|
|
try? fileManager.removeItem(at: configURL)
|
|
}
|
|
}
|
|
|
|
let home = fileManager.homeDirectoryForCurrentUser
|
|
let configContents = """
|
|
# cmux ui test
|
|
working-directory = \(home.path)
|
|
keybind = cmd+ctrl+h=goto_split:left
|
|
"""
|
|
do {
|
|
try configContents.write(to: configURL, atomically: true, encoding: .utf8)
|
|
} catch {
|
|
XCTFail("Failed to write Ghostty config: \(error)")
|
|
return
|
|
}
|
|
|
|
let app = XCUIApplication()
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_SETUP"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_PATH"] = dataPath
|
|
app.launchEnvironment["CMUX_UI_TEST_FOCUS_SHORTCUTS"] = "1"
|
|
app.launchEnvironment["CMUX_SOCKET_PATH"] = socketPath
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_USE_GHOSTTY_CONFIG"] = "1"
|
|
launchAndEnsureForeground(app)
|
|
|
|
XCTAssertTrue(
|
|
waitForData(keys: ["terminalPaneId", "browserPaneId", "webViewFocused", "ghosttyGotoSplitLeftShortcut"], timeout: 10.0),
|
|
"Expected goto_split setup data to be written"
|
|
)
|
|
|
|
guard let setup = loadData() else {
|
|
XCTFail("Missing goto_split setup data")
|
|
return
|
|
}
|
|
|
|
XCTAssertEqual(setup["webViewFocused"], "true", "Expected WKWebView to be first responder for this test")
|
|
XCTAssertFalse((setup["ghosttyGotoSplitLeftShortcut"] ?? "").isEmpty, "Expected Ghostty trigger metadata to be present")
|
|
|
|
guard let expectedTerminalPaneId = setup["terminalPaneId"] else {
|
|
XCTFail("Missing terminalPaneId in goto_split setup data")
|
|
return
|
|
}
|
|
|
|
// Trigger pane navigation via the actual key event path (while WebKit is first responder).
|
|
app.typeKey("h", modifierFlags: [.command, .control])
|
|
|
|
XCTAssertTrue(
|
|
waitForDataMatch(timeout: 5.0) { data in
|
|
data["lastMoveDirection"] == "left" && data["focusedPaneId"] == expectedTerminalPaneId
|
|
},
|
|
"Expected Cmd+Ctrl+H to move focus to left pane (terminal) via Ghostty config trigger"
|
|
)
|
|
}
|
|
|
|
func testEscapeLeavesOmnibarAndFocusesWebView() {
|
|
let app = XCUIApplication()
|
|
app.launchEnvironment["CMUX_SOCKET_PATH"] = socketPath
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_SETUP"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_PATH"] = dataPath
|
|
app.launchEnvironment["CMUX_UI_TEST_FOCUS_SHORTCUTS"] = "1"
|
|
launchAndEnsureForeground(app)
|
|
|
|
XCTAssertTrue(
|
|
waitForData(keys: ["browserPanelId", "webViewFocused"], timeout: 10.0),
|
|
"Expected goto_split setup data to be written"
|
|
)
|
|
|
|
guard let setup = loadData() else {
|
|
XCTFail("Missing goto_split setup data")
|
|
return
|
|
}
|
|
|
|
XCTAssertEqual(setup["webViewFocused"], "true", "Expected WKWebView to be first responder for this test")
|
|
|
|
// Cmd+L focuses the omnibar (so WebKit is no longer first responder).
|
|
app.typeKey("l", modifierFlags: [.command])
|
|
XCTAssertTrue(
|
|
waitForDataMatch(timeout: 5.0) { data in
|
|
data["webViewFocusedAfterAddressBarFocus"] == "false"
|
|
},
|
|
"Expected Cmd+L to focus omnibar (WebKit not first responder)"
|
|
)
|
|
|
|
// 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"
|
|
},
|
|
"Expected Escape to return focus to WebKit"
|
|
)
|
|
}
|
|
|
|
func testEscapeRestoresFocusedPageInputAfterCmdL() {
|
|
let app = XCUIApplication()
|
|
app.launchEnvironment["CMUX_SOCKET_PATH"] = socketPath
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_SETUP"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_PATH"] = dataPath
|
|
app.launchEnvironment["CMUX_UI_TEST_FOCUS_SHORTCUTS"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_INPUT_SETUP"] = "1"
|
|
launchAndEnsureForeground(app)
|
|
|
|
XCTAssertTrue(
|
|
waitForData(
|
|
keys: [
|
|
"browserPanelId",
|
|
"webViewFocused",
|
|
"webInputFocusSeeded",
|
|
"webInputFocusElementId",
|
|
"webInputFocusSecondaryElementId",
|
|
"webInputFocusSecondaryClickOffsetX",
|
|
"webInputFocusSecondaryClickOffsetY"
|
|
],
|
|
timeout: 12.0
|
|
),
|
|
"Expected setup data including focused page input to be written"
|
|
)
|
|
|
|
guard let setup = loadData() else {
|
|
XCTFail("Missing goto_split setup data")
|
|
return
|
|
}
|
|
|
|
XCTAssertEqual(setup["webViewFocused"], "true", "Expected WKWebView to be first responder for this test")
|
|
XCTAssertEqual(setup["webInputFocusSeeded"], "true", "Expected test page input to be focused before Cmd+L")
|
|
|
|
guard let expectedInputId = setup["webInputFocusElementId"], !expectedInputId.isEmpty else {
|
|
XCTFail("Missing webInputFocusElementId in setup data")
|
|
return
|
|
}
|
|
guard let expectedSecondaryInputId = setup["webInputFocusSecondaryElementId"], !expectedSecondaryInputId.isEmpty else {
|
|
XCTFail("Missing webInputFocusSecondaryElementId in setup data")
|
|
return
|
|
}
|
|
guard let secondaryClickOffsetXRaw = setup["webInputFocusSecondaryClickOffsetX"],
|
|
let secondaryClickOffsetYRaw = setup["webInputFocusSecondaryClickOffsetY"],
|
|
let secondaryClickOffsetX = Double(secondaryClickOffsetXRaw),
|
|
let secondaryClickOffsetY = Double(secondaryClickOffsetYRaw) else {
|
|
XCTFail(
|
|
"Missing or invalid secondary input click offsets in setup data. " +
|
|
"webInputFocusSecondaryClickOffsetX=\(setup["webInputFocusSecondaryClickOffsetX"] ?? "nil") " +
|
|
"webInputFocusSecondaryClickOffsetY=\(setup["webInputFocusSecondaryClickOffsetY"] ?? "nil")"
|
|
)
|
|
return
|
|
}
|
|
|
|
app.typeKey("l", modifierFlags: [.command])
|
|
XCTAssertTrue(
|
|
waitForDataMatch(timeout: 5.0) { data in
|
|
data["webViewFocusedAfterAddressBarFocus"] == "false"
|
|
},
|
|
"Expected Cmd+L to focus omnibar"
|
|
)
|
|
|
|
app.typeKey(XCUIKeyboardKey.escape.rawValue, modifierFlags: [])
|
|
if !waitForDataMatch(timeout: 2.0, predicate: { data in
|
|
data["webViewFocusedAfterAddressBarExit"] == "true" &&
|
|
data["addressBarExitActiveElementId"] == expectedInputId &&
|
|
data["addressBarExitActiveElementEditable"] == "true"
|
|
}) {
|
|
app.typeKey(XCUIKeyboardKey.escape.rawValue, modifierFlags: [])
|
|
}
|
|
|
|
let restoredExpectedInput = waitForDataMatch(timeout: 6.0) { data in
|
|
data["webViewFocusedAfterAddressBarExit"] == "true" &&
|
|
data["addressBarExitActiveElementId"] == expectedInputId &&
|
|
data["addressBarExitActiveElementEditable"] == "true"
|
|
}
|
|
if !restoredExpectedInput {
|
|
let snapshot = loadData() ?? [:]
|
|
XCTFail(
|
|
"Expected Escape to restore focus to the previously focused page input. " +
|
|
"expectedInputId=\(expectedInputId) " +
|
|
"webViewFocusedAfterAddressBarExit=\(snapshot["webViewFocusedAfterAddressBarExit"] ?? "nil") " +
|
|
"addressBarExitActiveElementId=\(snapshot["addressBarExitActiveElementId"] ?? "nil") " +
|
|
"addressBarExitActiveElementTag=\(snapshot["addressBarExitActiveElementTag"] ?? "nil") " +
|
|
"addressBarExitActiveElementType=\(snapshot["addressBarExitActiveElementType"] ?? "nil") " +
|
|
"addressBarExitActiveElementEditable=\(snapshot["addressBarExitActiveElementEditable"] ?? "nil") " +
|
|
"addressBarExitTrackedFocusStateId=\(snapshot["addressBarExitTrackedFocusStateId"] ?? "nil") " +
|
|
"addressBarExitFocusTrackerInstalled=\(snapshot["addressBarExitFocusTrackerInstalled"] ?? "nil") " +
|
|
"addressBarFocusActiveElementId=\(snapshot["addressBarFocusActiveElementId"] ?? "nil") " +
|
|
"addressBarFocusTrackedFocusStateId=\(snapshot["addressBarFocusTrackedFocusStateId"] ?? "nil") " +
|
|
"addressBarFocusFocusTrackerInstalled=\(snapshot["addressBarFocusFocusTrackerInstalled"] ?? "nil") " +
|
|
"webInputFocusElementId=\(snapshot["webInputFocusElementId"] ?? "nil") " +
|
|
"webInputFocusTrackerInstalled=\(snapshot["webInputFocusTrackerInstalled"] ?? "nil") " +
|
|
"webInputFocusTrackedStateId=\(snapshot["webInputFocusTrackedStateId"] ?? "nil")"
|
|
)
|
|
}
|
|
|
|
let window = app.windows.firstMatch
|
|
XCTAssertTrue(
|
|
window.waitForExistence(timeout: 6.0),
|
|
"Expected app window for post-escape click regression check"
|
|
)
|
|
|
|
RunLoop.current.run(until: Date().addingTimeInterval(0.15))
|
|
window
|
|
.coordinate(withNormalizedOffset: CGVector(dx: 0.0, dy: 0.0))
|
|
.withOffset(CGVector(dx: secondaryClickOffsetX, dy: secondaryClickOffsetY))
|
|
.click()
|
|
RunLoop.current.run(until: Date().addingTimeInterval(0.15))
|
|
|
|
app.typeKey("l", modifierFlags: [.command])
|
|
let clickMovedFocusToSecondary = waitForDataMatch(timeout: 6.0) { data in
|
|
data["webViewFocusedAfterAddressBarFocus"] == "false" &&
|
|
data["addressBarFocusActiveElementId"] == expectedSecondaryInputId &&
|
|
data["addressBarFocusActiveElementEditable"] == "true"
|
|
}
|
|
if !clickMovedFocusToSecondary {
|
|
let snapshot = loadData() ?? [:]
|
|
XCTFail(
|
|
"Expected post-escape click to focus secondary page input before Cmd+L. " +
|
|
"secondaryInputId=\(expectedSecondaryInputId) " +
|
|
"addressBarFocusActiveElementId=\(snapshot["addressBarFocusActiveElementId"] ?? "nil") " +
|
|
"addressBarFocusActiveElementTag=\(snapshot["addressBarFocusActiveElementTag"] ?? "nil") " +
|
|
"addressBarFocusActiveElementType=\(snapshot["addressBarFocusActiveElementType"] ?? "nil") " +
|
|
"addressBarFocusActiveElementEditable=\(snapshot["addressBarFocusActiveElementEditable"] ?? "nil") " +
|
|
"addressBarFocusTrackedFocusStateId=\(snapshot["addressBarFocusTrackedFocusStateId"] ?? "nil") " +
|
|
"addressBarFocusFocusTrackerInstalled=\(snapshot["addressBarFocusFocusTrackerInstalled"] ?? "nil")"
|
|
)
|
|
}
|
|
|
|
app.typeKey(XCUIKeyboardKey.escape.rawValue, modifierFlags: [])
|
|
if !waitForDataMatch(timeout: 2.0, predicate: { data in
|
|
data["webViewFocusedAfterAddressBarExit"] == "true" &&
|
|
data["addressBarExitActiveElementId"] == expectedSecondaryInputId &&
|
|
data["addressBarExitActiveElementEditable"] == "true"
|
|
}) {
|
|
app.typeKey(XCUIKeyboardKey.escape.rawValue, modifierFlags: [])
|
|
}
|
|
|
|
XCTAssertTrue(
|
|
waitForDataMatch(timeout: 6.0) { data in
|
|
data["webViewFocusedAfterAddressBarExit"] == "true" &&
|
|
data["addressBarExitActiveElementId"] == expectedSecondaryInputId &&
|
|
data["addressBarExitActiveElementEditable"] == "true"
|
|
},
|
|
"Expected Escape to restore focus to the clicked secondary page input"
|
|
)
|
|
}
|
|
|
|
func testCmdLOpensBrowserWhenTerminalFocused() {
|
|
let app = XCUIApplication()
|
|
app.launchEnvironment["CMUX_SOCKET_PATH"] = socketPath
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_SETUP"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_PATH"] = dataPath
|
|
app.launchEnvironment["CMUX_UI_TEST_FOCUS_SHORTCUTS"] = "1"
|
|
launchAndEnsureForeground(app)
|
|
|
|
XCTAssertTrue(
|
|
waitForData(keys: ["browserPanelId", "terminalPaneId", "webViewFocused"], timeout: 10.0),
|
|
"Expected goto_split setup data to be written"
|
|
)
|
|
|
|
guard let setup = loadData() else {
|
|
XCTFail("Missing goto_split setup data")
|
|
return
|
|
}
|
|
|
|
guard let originalBrowserPanelId = setup["browserPanelId"] else {
|
|
XCTFail("Missing browserPanelId in goto_split setup data")
|
|
return
|
|
}
|
|
|
|
guard let expectedTerminalPaneId = setup["terminalPaneId"] else {
|
|
XCTFail("Missing terminalPaneId in goto_split setup data")
|
|
return
|
|
}
|
|
|
|
// Move focus to the terminal pane first.
|
|
app.typeKey("h", modifierFlags: [.command, .control])
|
|
XCTAssertTrue(
|
|
waitForDataMatch(timeout: 5.0) { data in
|
|
data["lastMoveDirection"] == "left" && data["focusedPaneId"] == expectedTerminalPaneId
|
|
},
|
|
"Expected Cmd+Ctrl+H to move focus to left pane (terminal)"
|
|
)
|
|
|
|
// Cmd+L should open a browser in the focused pane, then focus omnibar.
|
|
app.typeKey("l", modifierFlags: [.command])
|
|
XCTAssertTrue(
|
|
waitForDataMatch(timeout: 5.0) { data in
|
|
guard data["webViewFocusedAfterAddressBarFocus"] == "false" else { return false }
|
|
guard let focusedAddressPanelId = data["webViewFocusedAfterAddressBarFocusPanelId"] else { return false }
|
|
return focusedAddressPanelId != originalBrowserPanelId
|
|
},
|
|
"Expected Cmd+L on terminal focus to open a new browser and focus omnibar"
|
|
)
|
|
}
|
|
|
|
func testClickingOmnibarFocusesBrowserPane() {
|
|
let app = XCUIApplication()
|
|
app.launchEnvironment["CMUX_SOCKET_PATH"] = socketPath
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_SETUP"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_PATH"] = dataPath
|
|
app.launchEnvironment["CMUX_UI_TEST_FOCUS_SHORTCUTS"] = "1"
|
|
launchAndEnsureForeground(app)
|
|
|
|
XCTAssertTrue(
|
|
waitForData(keys: ["browserPanelId", "terminalPaneId", "webViewFocused"], timeout: 10.0),
|
|
"Expected goto_split setup data to be written"
|
|
)
|
|
|
|
guard let setup = loadData() else {
|
|
XCTFail("Missing goto_split setup data")
|
|
return
|
|
}
|
|
|
|
guard let expectedBrowserPanelId = setup["browserPanelId"] else {
|
|
XCTFail("Missing browserPanelId in goto_split setup data")
|
|
return
|
|
}
|
|
|
|
guard let expectedTerminalPaneId = setup["terminalPaneId"] else {
|
|
XCTFail("Missing terminalPaneId in goto_split setup data")
|
|
return
|
|
}
|
|
|
|
// Move focus away from browser to terminal first.
|
|
app.typeKey("h", modifierFlags: [.command, .control])
|
|
XCTAssertTrue(
|
|
waitForDataMatch(timeout: 5.0) { data in
|
|
data["lastMoveDirection"] == "left" && data["focusedPaneId"] == expectedTerminalPaneId
|
|
},
|
|
"Expected Cmd+Ctrl+H to move focus to left pane (terminal)"
|
|
)
|
|
|
|
let omnibar = app.textFields["BrowserOmnibarTextField"].firstMatch
|
|
XCTAssertTrue(omnibar.waitForExistence(timeout: 6.0), "Expected browser omnibar text field")
|
|
omnibar.click()
|
|
|
|
// Cmd+L behavior is context-aware:
|
|
// - If terminal is focused: opens a new browser and focuses that new omnibar.
|
|
// - If browser is focused: focuses current browser omnibar.
|
|
// After clicking the omnibar, Cmd+L should stay on the existing browser panel.
|
|
app.typeKey("l", modifierFlags: [.command])
|
|
XCTAssertTrue(
|
|
waitForDataMatch(timeout: 5.0) { data in
|
|
guard data["webViewFocusedAfterAddressBarFocus"] == "false" else { return false }
|
|
return data["webViewFocusedAfterAddressBarFocusPanelId"] == expectedBrowserPanelId
|
|
},
|
|
"Expected omnibar click to focus browser panel so Cmd+L stays on that browser"
|
|
)
|
|
}
|
|
|
|
func testClickingBrowserDismissesCommandPaletteAndKeepsBrowserFocus() {
|
|
let app = XCUIApplication()
|
|
app.launchEnvironment["CMUX_SOCKET_PATH"] = socketPath
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_SETUP"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_PATH"] = dataPath
|
|
app.launchEnvironment["CMUX_UI_TEST_FOCUS_SHORTCUTS"] = "1"
|
|
launchAndEnsureForeground(app)
|
|
|
|
XCTAssertTrue(
|
|
waitForData(keys: ["browserPanelId", "terminalPaneId", "webViewFocused"], timeout: 10.0),
|
|
"Expected goto_split setup data to be written"
|
|
)
|
|
|
|
guard let setup = loadData() else {
|
|
XCTFail("Missing goto_split setup data")
|
|
return
|
|
}
|
|
|
|
guard let expectedBrowserPanelId = setup["browserPanelId"] else {
|
|
XCTFail("Missing browserPanelId in goto_split setup data")
|
|
return
|
|
}
|
|
|
|
guard let expectedTerminalPaneId = setup["terminalPaneId"] else {
|
|
XCTFail("Missing terminalPaneId in goto_split setup data")
|
|
return
|
|
}
|
|
|
|
// Move focus away from browser to terminal first so Cmd+R opens the rename overlay.
|
|
app.typeKey("h", modifierFlags: [.command, .control])
|
|
XCTAssertTrue(
|
|
waitForDataMatch(timeout: 5.0) { data in
|
|
data["lastMoveDirection"] == "left" && data["focusedPaneId"] == expectedTerminalPaneId
|
|
},
|
|
"Expected Cmd+Ctrl+H to move focus to left pane (terminal)"
|
|
)
|
|
|
|
let renameField = app.textFields["CommandPaletteRenameField"].firstMatch
|
|
app.typeKey("r", modifierFlags: [.command])
|
|
XCTAssertTrue(
|
|
renameField.waitForExistence(timeout: 5.0),
|
|
"Expected Cmd+R to open the rename command palette while terminal is focused"
|
|
)
|
|
|
|
let browserPane = app.otherElements["BrowserPanelContent.\(expectedBrowserPanelId)"].firstMatch
|
|
XCTAssertTrue(browserPane.waitForExistence(timeout: 5.0), "Expected browser pane content for click target")
|
|
browserPane.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).click()
|
|
XCTAssertTrue(
|
|
waitForNonExistence(renameField, timeout: 5.0),
|
|
"Expected clicking the browser pane to dismiss the command palette"
|
|
)
|
|
|
|
// Cmd+L behavior is context-aware:
|
|
// - If terminal is still focused: opens a new browser in that pane.
|
|
// - If the original browser took focus: focuses that existing browser's omnibar.
|
|
app.typeKey("l", modifierFlags: [.command])
|
|
XCTAssertTrue(
|
|
waitForDataMatch(timeout: 5.0) { data in
|
|
guard data["webViewFocusedAfterAddressBarFocus"] == "false" else { return false }
|
|
return data["webViewFocusedAfterAddressBarFocusPanelId"] == expectedBrowserPanelId
|
|
},
|
|
"Expected clicking browser content to dismiss the palette and keep focus on the existing browser pane"
|
|
)
|
|
}
|
|
|
|
func testCmdDSplitsRightWhenWebViewFocused() {
|
|
let app = XCUIApplication()
|
|
app.launchEnvironment["CMUX_SOCKET_PATH"] = socketPath
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_SETUP"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_PATH"] = dataPath
|
|
launchAndEnsureForeground(app)
|
|
|
|
XCTAssertTrue(
|
|
waitForData(keys: ["webViewFocused", "initialPaneCount"], timeout: 10.0),
|
|
"Expected goto_split setup data to be written"
|
|
)
|
|
|
|
guard let setup = loadData() else {
|
|
XCTFail("Missing goto_split setup data")
|
|
return
|
|
}
|
|
|
|
XCTAssertEqual(setup["webViewFocused"], "true", "Expected WKWebView to be first responder for this test")
|
|
let initialPaneCount = Int(setup["initialPaneCount"] ?? "") ?? 0
|
|
XCTAssertGreaterThanOrEqual(initialPaneCount, 2, "Expected at least two panes before split. data=\(setup)")
|
|
|
|
app.typeKey("d", modifierFlags: [.command])
|
|
|
|
XCTAssertTrue(
|
|
waitForDataMatch(timeout: 5.0) { data in
|
|
guard data["lastSplitDirection"] == "right" else { return false }
|
|
guard let paneCountAfter = Int(data["paneCountAfterSplit"] ?? "") else { return false }
|
|
return paneCountAfter == initialPaneCount + 1
|
|
},
|
|
"Expected Cmd+D to split right while WKWebView is first responder"
|
|
)
|
|
}
|
|
|
|
func testCmdShiftDSplitsDownWhenWebViewFocused() {
|
|
let app = XCUIApplication()
|
|
app.launchEnvironment["CMUX_SOCKET_PATH"] = socketPath
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_SETUP"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_PATH"] = dataPath
|
|
launchAndEnsureForeground(app)
|
|
|
|
XCTAssertTrue(
|
|
waitForData(keys: ["webViewFocused", "initialPaneCount"], timeout: 10.0),
|
|
"Expected goto_split setup data to be written"
|
|
)
|
|
|
|
guard let setup = loadData() else {
|
|
XCTFail("Missing goto_split setup data")
|
|
return
|
|
}
|
|
|
|
XCTAssertEqual(setup["webViewFocused"], "true", "Expected WKWebView to be first responder for this test")
|
|
let initialPaneCount = Int(setup["initialPaneCount"] ?? "") ?? 0
|
|
XCTAssertGreaterThanOrEqual(initialPaneCount, 2, "Expected at least two panes before split. data=\(setup)")
|
|
|
|
app.typeKey("d", modifierFlags: [.command, .shift])
|
|
|
|
XCTAssertTrue(
|
|
waitForDataMatch(timeout: 5.0) { data in
|
|
guard data["lastSplitDirection"] == "down" else { return false }
|
|
guard let paneCountAfter = Int(data["paneCountAfterSplit"] ?? "") else { return false }
|
|
return paneCountAfter == initialPaneCount + 1
|
|
},
|
|
"Expected Cmd+Shift+D to split down while WKWebView is first responder"
|
|
)
|
|
}
|
|
|
|
func testCmdShiftEnterKeepsBrowserOmnibarHittableAcrossZoomRoundTripWhenWebViewFocused() {
|
|
let app = XCUIApplication()
|
|
app.launchEnvironment["CMUX_SOCKET_PATH"] = socketPath
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_SETUP"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_PATH"] = dataPath
|
|
launchAndEnsureForeground(app)
|
|
|
|
XCTAssertTrue(
|
|
waitForData(keys: ["browserPanelId", "webViewFocused"], timeout: 10.0),
|
|
"Expected goto_split setup data to be written"
|
|
)
|
|
|
|
guard let setup = loadData() else {
|
|
XCTFail("Missing goto_split setup data")
|
|
return
|
|
}
|
|
|
|
guard let browserPanelId = setup["browserPanelId"] else {
|
|
XCTFail("Missing browserPanelId in goto_split setup data")
|
|
return
|
|
}
|
|
|
|
XCTAssertEqual(setup["webViewFocused"], "true", "Expected WKWebView to be first responder for this test")
|
|
|
|
let omnibar = app.textFields["BrowserOmnibarTextField"].firstMatch
|
|
let pill = app.descendants(matching: .any).matching(identifier: "BrowserOmnibarPill").firstMatch
|
|
XCTAssertTrue(omnibar.waitForExistence(timeout: 6.0), "Expected browser omnibar text field before zoom")
|
|
XCTAssertTrue(pill.waitForExistence(timeout: 6.0), "Expected browser omnibar pill before zoom")
|
|
|
|
// Reproduce the loaded-page state from the bug report before toggling zoom.
|
|
app.typeKey("l", modifierFlags: [.command])
|
|
XCTAssertTrue(waitForElementToBecomeHittable(pill, timeout: 6.0), "Expected browser omnibar pill before navigation")
|
|
pill.click()
|
|
app.typeKey("a", modifierFlags: [.command])
|
|
app.typeKey(XCUIKeyboardKey.delete.rawValue, modifierFlags: [])
|
|
app.typeText(zoomRoundTripPageURL)
|
|
app.typeKey(XCUIKeyboardKey.return.rawValue, modifierFlags: [])
|
|
|
|
XCTAssertTrue(
|
|
waitForOmnibarToContain(omnibar, value: "data:text/html", timeout: 8.0),
|
|
"Expected browser to finish navigating to the regression page before zoom. value=\(String(describing: omnibar.value))"
|
|
)
|
|
|
|
let browserPane = app.otherElements["BrowserPanelContent.\(browserPanelId)"].firstMatch
|
|
XCTAssertTrue(browserPane.waitForExistence(timeout: 6.0), "Expected browser pane content before zoom")
|
|
browserPane.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).click()
|
|
|
|
app.typeKey(XCUIKeyboardKey.return.rawValue, modifierFlags: [.command, .shift])
|
|
XCTAssertTrue(
|
|
waitForDataMatch(timeout: 8.0) { data in
|
|
data["splitZoomedAfterToggle"] == "true" &&
|
|
data["otherTerminalHostHiddenAfterToggle"] == "true" &&
|
|
data["otherTerminalVisibleFlagAfterToggle"] == "false"
|
|
},
|
|
"Expected Cmd+Shift+Enter zoom-in to hide the non-browser terminal portal. data=\(loadData() ?? [:])"
|
|
)
|
|
app.typeKey(XCUIKeyboardKey.return.rawValue, modifierFlags: [.command, .shift])
|
|
XCTAssertTrue(
|
|
waitForDataMatch(timeout: 8.0) { data in
|
|
data["splitZoomedAfterToggle"] == "false" &&
|
|
data["otherTerminalHostHiddenAfterToggle"] == "false" &&
|
|
data["otherTerminalVisibleFlagAfterToggle"] == "true"
|
|
},
|
|
"Expected Cmd+Shift+Enter zoom-out to restore the non-browser terminal portal. data=\(loadData() ?? [:])"
|
|
)
|
|
|
|
XCTAssertTrue(omnibar.waitForExistence(timeout: 6.0), "Expected browser omnibar text field after Cmd+Shift+Enter zoom round-trip")
|
|
XCTAssertTrue(pill.waitForExistence(timeout: 6.0), "Expected browser omnibar pill after Cmd+Shift+Enter zoom round-trip")
|
|
XCTAssertTrue(
|
|
waitForElementToBecomeHittable(pill, timeout: 6.0),
|
|
"Expected browser omnibar to stay hittable after Cmd+Shift+Enter zoom round-trip"
|
|
)
|
|
let page = app.webViews.firstMatch
|
|
XCTAssertTrue(page.waitForExistence(timeout: 6.0), "Expected browser web area after Cmd+Shift+Enter")
|
|
XCTAssertLessThanOrEqual(
|
|
pill.frame.maxY,
|
|
page.frame.minY + 12,
|
|
"Expected browser omnibar to remain above the web content after Cmd+Shift+Enter. pill=\(pill.frame) page=\(page.frame)"
|
|
)
|
|
|
|
pill.click()
|
|
app.typeKey("a", modifierFlags: [.command])
|
|
app.typeKey(XCUIKeyboardKey.delete.rawValue, modifierFlags: [])
|
|
app.typeText("issue1144")
|
|
|
|
XCTAssertTrue(
|
|
waitForOmnibarToContain(omnibar, value: "issue1144", timeout: 4.0),
|
|
"Expected browser omnibar to stay editable after Cmd+Shift+Enter. value=\(String(describing: omnibar.value))"
|
|
)
|
|
}
|
|
|
|
func testCmdShiftEnterHidesBrowserPortalWhenTerminalPaneZooms() {
|
|
let app = XCUIApplication()
|
|
app.launchEnvironment["CMUX_SOCKET_PATH"] = socketPath
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_SETUP"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_PATH"] = dataPath
|
|
app.launchEnvironment["CMUX_UI_TEST_FOCUS_SHORTCUTS"] = "1"
|
|
launchAndEnsureForeground(app)
|
|
|
|
XCTAssertTrue(
|
|
waitForData(keys: ["terminalPaneId", "browserPanelId", "webViewFocused"], timeout: 10.0),
|
|
"Expected goto_split setup data to be written"
|
|
)
|
|
|
|
guard let setup = loadData() else {
|
|
XCTFail("Missing goto_split setup data")
|
|
return
|
|
}
|
|
|
|
guard let expectedTerminalPaneId = setup["terminalPaneId"] else {
|
|
XCTFail("Missing terminalPaneId in goto_split setup data")
|
|
return
|
|
}
|
|
|
|
app.typeKey("h", modifierFlags: [.command, .control])
|
|
|
|
XCTAssertTrue(
|
|
waitForDataMatch(timeout: 5.0) { data in
|
|
data["focusedPaneId"] == expectedTerminalPaneId && data["focusedPanelKind"] == "terminal"
|
|
},
|
|
"Expected Cmd+Ctrl+H to focus the terminal pane before zoom. data=\(loadData() ?? [:])"
|
|
)
|
|
|
|
app.typeKey(XCUIKeyboardKey.return.rawValue, modifierFlags: [.command, .shift])
|
|
XCTAssertTrue(
|
|
waitForDataMatch(timeout: 8.0) { data in
|
|
data["splitZoomedAfterToggle"] == "true" &&
|
|
data["browserContainerHiddenAfterToggle"] == "true" &&
|
|
data["browserVisibleFlagAfterToggle"] == "false"
|
|
},
|
|
"Expected Cmd+Shift+Enter zoom-in on the terminal pane to hide the browser portal. data=\(loadData() ?? [:])"
|
|
)
|
|
|
|
app.typeKey(XCUIKeyboardKey.return.rawValue, modifierFlags: [.command, .shift])
|
|
XCTAssertTrue(
|
|
waitForDataMatch(timeout: 8.0) { data in
|
|
data["splitZoomedAfterToggle"] == "false" &&
|
|
data["browserContainerHiddenAfterToggle"] == "false" &&
|
|
data["browserVisibleFlagAfterToggle"] == "true"
|
|
},
|
|
"Expected Cmd+Shift+Enter zoom-out from the terminal pane to restore the browser portal. data=\(loadData() ?? [:])"
|
|
)
|
|
}
|
|
|
|
func testCmdDSplitsRightWhenOmnibarFocused() {
|
|
let app = XCUIApplication()
|
|
app.launchEnvironment["CMUX_SOCKET_PATH"] = socketPath
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_SETUP"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_PATH"] = dataPath
|
|
launchAndEnsureForeground(app)
|
|
|
|
XCTAssertTrue(
|
|
waitForData(keys: ["webViewFocused", "initialPaneCount"], timeout: 10.0),
|
|
"Expected goto_split setup data to be written"
|
|
)
|
|
|
|
guard let setup = loadData() else {
|
|
XCTFail("Missing goto_split setup data")
|
|
return
|
|
}
|
|
|
|
let initialPaneCount = Int(setup["initialPaneCount"] ?? "") ?? 0
|
|
XCTAssertGreaterThanOrEqual(initialPaneCount, 2, "Expected at least two panes before split. data=\(setup)")
|
|
|
|
// Focus browser omnibar (WebKit no longer first responder).
|
|
app.typeKey("l", modifierFlags: [.command])
|
|
XCTAssertTrue(
|
|
waitForDataMatch(timeout: 5.0) { data in
|
|
data["webViewFocusedAfterAddressBarFocus"] == "false"
|
|
},
|
|
"Expected Cmd+L to focus omnibar before split"
|
|
)
|
|
|
|
app.typeKey("d", modifierFlags: [.command])
|
|
|
|
XCTAssertTrue(
|
|
waitForDataMatch(timeout: 5.0) { data in
|
|
guard data["lastSplitDirection"] == "right" else { return false }
|
|
guard let paneCountAfter = Int(data["paneCountAfterSplit"] ?? "") else { return false }
|
|
return paneCountAfter == initialPaneCount + 1
|
|
},
|
|
"Expected Cmd+D to split right while omnibar is first responder"
|
|
)
|
|
}
|
|
|
|
func testCmdShiftDSplitsDownWhenOmnibarFocused() {
|
|
let app = XCUIApplication()
|
|
app.launchEnvironment["CMUX_SOCKET_PATH"] = socketPath
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_SETUP"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_PATH"] = dataPath
|
|
launchAndEnsureForeground(app)
|
|
|
|
XCTAssertTrue(
|
|
waitForData(keys: ["webViewFocused", "initialPaneCount"], timeout: 10.0),
|
|
"Expected goto_split setup data to be written"
|
|
)
|
|
|
|
guard let setup = loadData() else {
|
|
XCTFail("Missing goto_split setup data")
|
|
return
|
|
}
|
|
|
|
let initialPaneCount = Int(setup["initialPaneCount"] ?? "") ?? 0
|
|
XCTAssertGreaterThanOrEqual(initialPaneCount, 2, "Expected at least two panes before split. data=\(setup)")
|
|
|
|
// Focus browser omnibar (WebKit no longer first responder).
|
|
app.typeKey("l", modifierFlags: [.command])
|
|
XCTAssertTrue(
|
|
waitForDataMatch(timeout: 5.0) { data in
|
|
data["webViewFocusedAfterAddressBarFocus"] == "false"
|
|
},
|
|
"Expected Cmd+L to focus omnibar before split"
|
|
)
|
|
|
|
app.typeKey("d", modifierFlags: [.command, .shift])
|
|
|
|
XCTAssertTrue(
|
|
waitForDataMatch(timeout: 5.0) { data in
|
|
guard data["lastSplitDirection"] == "down" else { return false }
|
|
guard let paneCountAfter = Int(data["paneCountAfterSplit"] ?? "") else { return false }
|
|
return paneCountAfter == initialPaneCount + 1
|
|
},
|
|
"Expected Cmd+Shift+D to split down while omnibar is first responder"
|
|
)
|
|
}
|
|
|
|
func testCmdOptionPaneSwitchPreservesFindFieldFocus() {
|
|
runFindFocusPersistenceScenario(route: .cmdOptionArrows, useAutofocusRacePage: false)
|
|
}
|
|
|
|
func testCmdCtrlPaneSwitchPreservesFindFieldFocus() {
|
|
runFindFocusPersistenceScenario(route: .cmdCtrlLetters, useAutofocusRacePage: false)
|
|
}
|
|
|
|
func testCmdOptionPaneSwitchPreservesFindFieldFocusDuringPageAutofocusRace() {
|
|
runFindFocusPersistenceScenario(route: .cmdOptionArrows, useAutofocusRacePage: true)
|
|
}
|
|
|
|
func testCmdFFocusesBrowserFindFieldAfterCmdDCmdLNavigation() {
|
|
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
|
|
// On some CI runners the app accepts key events before XCUI exposes the window tree.
|
|
_ = 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")
|
|
|
|
let omnibarValueBeforeFindTyping = (omnibar.value as? String) ?? ""
|
|
app.typeText("needle")
|
|
|
|
XCTAssertTrue(
|
|
waitForCondition(timeout: 4.0) {
|
|
((findField.value as? String) ?? "") == "needle"
|
|
},
|
|
"Expected Cmd+F to focus browser find after Cmd+D, Cmd+L, and navigation. " +
|
|
"findValue=\(String(describing: findField.value)) omnibarValue=\(String(describing: omnibar.value))"
|
|
)
|
|
let omnibarValueAfterFindTyping = (omnibar.value as? String) ?? ""
|
|
XCTAssertFalse(
|
|
omnibarValueAfterFindTyping.contains("needle"),
|
|
"Expected typing after Cmd+F to stay out of the omnibar. " +
|
|
"omnibarValueBefore=\(omnibarValueBeforeFindTyping) " +
|
|
"omnibarValueAfter=\(String(describing: omnibar.value)) " +
|
|
"findValue=\(String(describing: findField.value))"
|
|
)
|
|
}
|
|
|
|
private enum FindFocusRoute {
|
|
case cmdOptionArrows
|
|
case cmdCtrlLetters
|
|
}
|
|
|
|
private func runFindFocusPersistenceScenario(route: FindFocusRoute, useAutofocusRacePage: Bool) {
|
|
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
|
|
if route == .cmdCtrlLetters {
|
|
app.launchEnvironment["CMUX_UI_TEST_FOCUS_SHORTCUTS"] = "1"
|
|
}
|
|
launchAndEnsureForeground(app)
|
|
|
|
let window = app.windows.firstMatch
|
|
XCTAssertTrue(window.waitForExistence(timeout: 10.0), "Expected main window to exist")
|
|
|
|
// Repro setup: split, open browser split, navigate to example.com.
|
|
app.typeKey("d", modifierFlags: [.command])
|
|
focusRightPaneForFindScenario(app, route: route)
|
|
|
|
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: [])
|
|
if useAutofocusRacePage {
|
|
app.typeText(autofocusRacePageURL)
|
|
} else {
|
|
app.typeText("example.com")
|
|
}
|
|
app.typeKey(XCUIKeyboardKey.return.rawValue, modifierFlags: [])
|
|
|
|
if useAutofocusRacePage {
|
|
XCTAssertTrue(
|
|
waitForOmnibarToContain(omnibar, value: "data:text/html", timeout: 8.0),
|
|
"Expected browser navigation to data URL before running find flow. value=\(String(describing: omnibar.value))"
|
|
)
|
|
} else {
|
|
XCTAssertTrue(
|
|
waitForOmnibarToContainExampleDomain(omnibar, timeout: 8.0),
|
|
"Expected browser navigation to example domain before running find flow. value=\(String(describing: omnibar.value))"
|
|
)
|
|
}
|
|
|
|
// Left terminal: Cmd+F then type "la".
|
|
focusLeftPaneForFindScenario(app, route: route)
|
|
XCTAssertTrue(
|
|
waitForDataMatch(timeout: 6.0) { data in
|
|
data["focusedPanelKind"] == "terminal"
|
|
},
|
|
"Expected left terminal pane to be focused before terminal find. data=\(String(describing: loadData()))"
|
|
)
|
|
app.typeKey("f", modifierFlags: [.command])
|
|
app.typeText("la")
|
|
|
|
// Right browser: Cmd+F then type "am".
|
|
focusRightPaneForFindScenario(app, route: route)
|
|
XCTAssertTrue(
|
|
waitForDataMatch(timeout: 6.0) { data in
|
|
data["lastMoveDirection"] == "right"
|
|
&& data["focusedPanelKind"] == "browser"
|
|
&& data["terminalFindNeedle"] == "la"
|
|
},
|
|
"Expected terminal find query to persist as 'la' after focusing browser pane. data=\(String(describing: loadData()))"
|
|
)
|
|
app.typeKey("f", modifierFlags: [.command])
|
|
app.typeText("am")
|
|
|
|
if useAutofocusRacePage {
|
|
XCTAssertTrue(
|
|
waitForOmnibarToContain(omnibar, value: "#focused", timeout: 5.0),
|
|
"Expected autofocus race page to signal focus handoff via URL hash. value=\(String(describing: omnibar.value))"
|
|
)
|
|
}
|
|
|
|
// Left terminal: typing should keep going into terminal find field.
|
|
focusLeftPaneForFindScenario(app, route: route)
|
|
XCTAssertTrue(
|
|
waitForDataMatch(timeout: 6.0) { data in
|
|
data["lastMoveDirection"] == "left"
|
|
&& data["focusedPanelKind"] == "terminal"
|
|
&& data["browserFindNeedle"] == "am"
|
|
},
|
|
"Expected browser find query to persist as 'am' after returning left. data=\(String(describing: loadData()))"
|
|
)
|
|
app.typeText("foo")
|
|
|
|
// Right browser: typing should keep going into browser find field.
|
|
focusRightPaneForFindScenario(app, route: route)
|
|
XCTAssertTrue(
|
|
waitForDataMatch(timeout: 6.0) { data in
|
|
data["lastMoveDirection"] == "right"
|
|
&& data["focusedPanelKind"] == "browser"
|
|
&& data["terminalFindNeedle"] == "lafoo"
|
|
},
|
|
"Expected terminal find query to stay focused and become 'lafoo'. data=\(String(describing: loadData()))"
|
|
)
|
|
app.typeText("do")
|
|
|
|
// Move left once more so the recorder captures browser find state after typing.
|
|
focusLeftPaneForFindScenario(app, route: route)
|
|
XCTAssertTrue(
|
|
waitForDataMatch(timeout: 6.0) { data in
|
|
data["lastMoveDirection"] == "left"
|
|
&& data["focusedPanelKind"] == "terminal"
|
|
&& data["browserFindNeedle"] == "amdo"
|
|
},
|
|
"Expected browser find query to stay focused and become 'amdo'. data=\(String(describing: loadData()))"
|
|
)
|
|
}
|
|
|
|
private func focusLeftPaneForFindScenario(_ app: XCUIApplication, route: FindFocusRoute) {
|
|
switch route {
|
|
case .cmdOptionArrows:
|
|
app.typeKey(XCUIKeyboardKey.leftArrow.rawValue, modifierFlags: [.command, .option])
|
|
case .cmdCtrlLetters:
|
|
app.typeKey("h", modifierFlags: [.command, .control])
|
|
}
|
|
}
|
|
|
|
private func focusRightPaneForFindScenario(_ app: XCUIApplication, route: FindFocusRoute) {
|
|
switch route {
|
|
case .cmdOptionArrows:
|
|
app.typeKey(XCUIKeyboardKey.rightArrow.rawValue, modifierFlags: [.command, .option])
|
|
case .cmdCtrlLetters:
|
|
app.typeKey("l", modifierFlags: [.command, .control])
|
|
}
|
|
}
|
|
|
|
private func waitForOmnibarToContainExampleDomain(_ omnibar: XCUIElement, timeout: TimeInterval) -> Bool {
|
|
waitForCondition(timeout: timeout) {
|
|
let value = (omnibar.value as? String) ?? ""
|
|
return value.contains("example.com") || value.contains("example.org")
|
|
}
|
|
}
|
|
|
|
private func waitForOmnibarToContain(_ omnibar: XCUIElement, value expectedSubstring: String, timeout: TimeInterval) -> Bool {
|
|
waitForCondition(timeout: timeout) {
|
|
let value = (omnibar.value as? String) ?? ""
|
|
return value.contains(expectedSubstring)
|
|
}
|
|
}
|
|
|
|
private func waitForElementToBecomeHittable(_ element: XCUIElement, timeout: TimeInterval) -> Bool {
|
|
waitForCondition(timeout: timeout) {
|
|
element.exists && element.isHittable
|
|
}
|
|
}
|
|
|
|
private var autofocusRacePageURL: String {
|
|
"data:text/html,%3Cinput%20id%3D%22q%22%3E%3Cscript%3EsetTimeout%28function%28%29%7Bdocument.getElementById%28%22q%22%29.focus%28%29%3Blocation.hash%3D%22focused%22%3B%7D%2C700%29%3B%3C%2Fscript%3E"
|
|
}
|
|
|
|
private var zoomRoundTripPageURL: String {
|
|
"data:text/html,%3Ctitle%3EIssue%201144%3C/title%3E%3Cbody%20style%3D%22margin:0;background:%231d1f24;color:white;font-family:system-ui;height:2200px%22%3E%3Cmain%20style%3D%22padding:32px%22%3E%3Ch1%3EIssue%201144%20Regression%20Page%3C/h1%3E%3Cp%3EZoom%20should%20not%20leave%20stale%20split%20chrome%20above%20the%20browser%20omnibar.%3C/p%3E%3C/main%3E%3C/body%3E"
|
|
}
|
|
|
|
private func launchAndEnsureForeground(_ app: XCUIApplication, timeout: TimeInterval = 12.0) {
|
|
// On headless CI runners (no GUI session), XCUIApplication.launch()
|
|
// blocks ~60s then fails with "Failed to activate application
|
|
// (current state: Running Background)". Mark this as an expected
|
|
// failure so the test can continue — keyboard and element APIs work
|
|
// via accessibility even when the app is in .runningBackground.
|
|
let options = XCTExpectedFailure.Options()
|
|
options.isStrict = false
|
|
XCTExpectFailure("App activation may fail on headless CI runners", options: options) {
|
|
app.launch()
|
|
}
|
|
|
|
if app.state == .runningForeground { return }
|
|
|
|
if app.state == .runningBackground {
|
|
// App launched but couldn't activate — continue in background.
|
|
// XCUIElement queries and keyboard input work through the
|
|
// accessibility framework regardless of activation state.
|
|
return
|
|
}
|
|
|
|
XCTFail("App failed to start. state=\(app.state.rawValue)")
|
|
}
|
|
|
|
private func waitForData(keys: [String], timeout: TimeInterval) -> Bool {
|
|
waitForCondition(timeout: timeout) {
|
|
guard let data = self.loadData() else { return false }
|
|
return keys.allSatisfy { data[$0] != nil }
|
|
}
|
|
}
|
|
|
|
private func waitForDataMatch(timeout: TimeInterval, predicate: @escaping ([String: String]) -> Bool) -> Bool {
|
|
waitForCondition(timeout: timeout) {
|
|
guard let data = self.loadData() else { return false }
|
|
return predicate(data)
|
|
}
|
|
}
|
|
|
|
private func waitForNonExistence(_ element: XCUIElement, timeout: TimeInterval) -> Bool {
|
|
let predicate = NSPredicate(format: "exists == false")
|
|
let expectation = XCTNSPredicateExpectation(predicate: predicate, object: element)
|
|
return XCTWaiter().wait(for: [expectation], timeout: timeout) == .completed
|
|
}
|
|
|
|
private func loadData() -> [String: String]? {
|
|
guard let data = try? Data(contentsOf: URL(fileURLWithPath: dataPath)) else {
|
|
return nil
|
|
}
|
|
return (try? JSONSerialization.jsonObject(with: data)) as? [String: String]
|
|
}
|
|
|
|
private func waitForCondition(timeout: TimeInterval, predicate: @escaping () -> Bool) -> Bool {
|
|
let expectation = XCTNSPredicateExpectation(
|
|
predicate: NSPredicate { _, _ in predicate() },
|
|
object: nil
|
|
)
|
|
return XCTWaiter().wait(for: [expectation], timeout: timeout) == .completed
|
|
}
|
|
}
|