752 lines
38 KiB
Swift
752 lines
38 KiB
Swift
import XCTest
|
|
import Foundation
|
|
|
|
final class CloseWorkspaceCmdDUITests: XCTestCase {
|
|
override func setUp() {
|
|
super.setUp()
|
|
continueAfterFailure = false
|
|
}
|
|
|
|
func testCmdDConfirmsCloseWhenClosingLastWorkspaceClosesWindow() {
|
|
let app = XCUIApplication()
|
|
// Force a confirmation alert when closing the current workspace so we can validate Cmd+D.
|
|
app.launchEnvironment["CMUX_UI_TEST_FORCE_CONFIRM_CLOSE_WORKSPACE"] = "1"
|
|
app.launch()
|
|
app.activate()
|
|
|
|
// Close current workspace. With a single workspace/window, this will close the window after confirmation.
|
|
app.typeKey("w", modifierFlags: [.command, .shift])
|
|
XCTAssertTrue(waitForCloseWorkspaceAlert(app: app, timeout: 5.0))
|
|
|
|
// Cmd+D should accept the destructive close and close the window.
|
|
app.typeKey("d", modifierFlags: [.command])
|
|
|
|
XCTAssertTrue(
|
|
waitForNoWindowsOrAppNotRunningForeground(app: app, timeout: 6.0),
|
|
"Expected Cmd+D to confirm close and close the last window"
|
|
)
|
|
}
|
|
|
|
func testCmdWClosingLastTabKeepsWorkspaceWindowOpen() {
|
|
let app = XCUIApplication()
|
|
let keyequivPath = "/tmp/cmux-ui-test-keyequiv-\(UUID().uuidString).json"
|
|
try? FileManager.default.removeItem(atPath: keyequivPath)
|
|
app.launchEnvironment["CMUX_UI_TEST_KEYEQUIV_PATH"] = keyequivPath
|
|
app.launch()
|
|
app.activate()
|
|
|
|
let baseline = loadJSON(atPath: keyequivPath)?["closePanelInvocations"].flatMap(Int.init) ?? 0
|
|
app.typeKey("w", modifierFlags: [.command])
|
|
XCTAssertTrue(
|
|
waitForKeyequivInt("closePanelInvocations", toBeAtLeast: baseline + 1, atPath: keyequivPath, timeout: 5.0),
|
|
"Expected Cmd+W to route through the close-current-tab action"
|
|
)
|
|
|
|
if waitForCloseTabAlert(app: app, timeout: 5.0) {
|
|
clickCloseOnCloseTabAlert(app: app)
|
|
XCTAssertFalse(
|
|
isCloseTabAlertPresent(app: app),
|
|
"Expected close tab confirmation to dismiss after confirming the close"
|
|
)
|
|
}
|
|
|
|
XCTAssertTrue(
|
|
waitForWindowCount(app: app, atLeast: 1, timeout: 6.0),
|
|
"Expected Cmd+W on the last tab to keep the workspace window open"
|
|
)
|
|
}
|
|
|
|
func testCmdNOpensNewWindowWhenNoWindowsOpen() {
|
|
let app = XCUIApplication()
|
|
app.launchEnvironment["CMUX_UI_TEST_FORCE_CONFIRM_CLOSE_WORKSPACE"] = "1"
|
|
app.launch()
|
|
app.activate()
|
|
|
|
// Close the only window.
|
|
app.typeKey("w", modifierFlags: [.command, .shift])
|
|
XCTAssertTrue(waitForCloseWorkspaceAlert(app: app, timeout: 5.0))
|
|
app.typeKey("d", modifierFlags: [.command])
|
|
|
|
XCTAssertTrue(
|
|
waitForWindowCount(app: app, toBe: 0, timeout: 6.0),
|
|
"Expected last window to close"
|
|
)
|
|
|
|
// Cmd+N should create a new window when there are no windows.
|
|
app.activate()
|
|
app.typeKey("n", modifierFlags: [.command])
|
|
|
|
XCTAssertTrue(
|
|
waitForWindowCount(app: app, atLeast: 1, timeout: 6.0),
|
|
"Expected Cmd+N to open a new window when no windows are open"
|
|
)
|
|
}
|
|
|
|
func testChildExitInHorizontalSplitClosesOnlyExitedPane() {
|
|
let attempts = 8
|
|
for attempt in 1...attempts {
|
|
let app = XCUIApplication()
|
|
let dataPath = "/tmp/cmux-ui-test-child-exit-split-\(UUID().uuidString).json"
|
|
try? FileManager.default.removeItem(atPath: dataPath)
|
|
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_SETUP"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_PATH"] = dataPath
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_LAYOUT"] = "lr"
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_EXPECTED_PANELS_AFTER"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_AUTO_TRIGGER"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_STRICT"] = "1"
|
|
app.launch()
|
|
app.activate()
|
|
defer { app.terminate() }
|
|
|
|
XCTAssertTrue(waitForAnyJSON(atPath: dataPath, timeout: 12.0), "Attempt \(attempt): expected child-exit test data at \(dataPath)")
|
|
guard let data = waitForJSONKey("done", equals: "1", atPath: dataPath, timeout: 12.0) else {
|
|
XCTFail("Attempt \(attempt): timed out waiting for done=1. data=\(loadJSON(atPath: dataPath) ?? [:])")
|
|
return
|
|
}
|
|
|
|
if let setupError = data["setupError"], !setupError.isEmpty {
|
|
XCTFail("Attempt \(attempt): setup failed: \(setupError)")
|
|
return
|
|
}
|
|
|
|
let workspaceCountAfter = Int(data["workspaceCountAfter"] ?? "") ?? -1
|
|
let panelCountAfter = Int(data["panelCountAfter"] ?? "") ?? -1
|
|
let closedWorkspace = (data["closedWorkspace"] ?? "") == "1"
|
|
let timedOut = (data["timedOut"] ?? "") == "1"
|
|
|
|
XCTAssertFalse(timedOut, "Attempt \(attempt): timed out waiting for child-exit close. data=\(data)")
|
|
XCTAssertEqual(workspaceCountAfter, 1, "Attempt \(attempt): expected workspace to remain open. data=\(data)")
|
|
XCTAssertEqual(panelCountAfter, 1, "Attempt \(attempt): expected only exited pane to close. data=\(data)")
|
|
XCTAssertFalse(closedWorkspace, "Attempt \(attempt): expected workspace/window to stay open. data=\(data)")
|
|
}
|
|
}
|
|
|
|
func testCtrlDFromKeyboardInHorizontalSplitClosesOnlyFocusedPane() {
|
|
let app = XCUIApplication()
|
|
let dataPath = "/tmp/cmux-ui-test-child-exit-keyboard-\(UUID().uuidString).json"
|
|
try? FileManager.default.removeItem(atPath: dataPath)
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_SETUP"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_PATH"] = dataPath
|
|
app.launch()
|
|
app.activate()
|
|
|
|
XCTAssertTrue(waitForAnyJSON(atPath: dataPath, timeout: 12.0), "Expected keyboard child-exit setup data at \(dataPath)")
|
|
guard let ready = waitForJSONKey("ready", equals: "1", atPath: dataPath, timeout: 12.0) else {
|
|
XCTFail("Timed out waiting for ready=1. data=\(loadJSON(atPath: dataPath) ?? [:])")
|
|
return
|
|
}
|
|
|
|
if let setupError = ready["setupError"], !setupError.isEmpty {
|
|
XCTFail("Setup failed: \(setupError)")
|
|
return
|
|
}
|
|
|
|
let rightPanelId = ready["rightPanelId"] ?? ""
|
|
guard !rightPanelId.isEmpty else {
|
|
XCTFail("Missing rightPanelId in setup data. data=\(ready)")
|
|
return
|
|
}
|
|
assertCtrlDPreconditionsBeforeTrigger(ready, expectedExitPanelId: rightPanelId, context: "Horizontal split")
|
|
|
|
// Exercise the real keyboard path (same path as user typing Ctrl+D), not an in-process helper.
|
|
app.activate()
|
|
app.typeKey("d", modifierFlags: [.control])
|
|
|
|
guard let done = waitForJSONKey("done", equals: "1", atPath: dataPath, timeout: 10.0) else {
|
|
XCTFail("Timed out waiting for done=1 after Ctrl+D. data=\(loadJSON(atPath: dataPath) ?? [:])")
|
|
return
|
|
}
|
|
|
|
let workspaceCountAfter = Int(done["workspaceCountAfter"] ?? "") ?? -1
|
|
let panelCountAfter = Int(done["panelCountAfter"] ?? "") ?? -1
|
|
let closedWorkspace = (done["closedWorkspace"] ?? "") == "1"
|
|
let timedOut = (done["timedOut"] ?? "") == "1"
|
|
let focusedPanelAfter = done["focusedPanelAfter"] ?? ""
|
|
let firstResponderPanelAfter = done["firstResponderPanelAfter"] ?? ""
|
|
|
|
XCTAssertFalse(timedOut, "Keyboard Ctrl+D test timed out. data=\(done)")
|
|
XCTAssertFalse(closedWorkspace, "Ctrl+D should not close workspace/window when another pane remains. data=\(done)")
|
|
XCTAssertEqual(workspaceCountAfter, 1, "Expected workspace to remain open after Ctrl+D in split. data=\(done)")
|
|
XCTAssertEqual(panelCountAfter, 1, "Expected only exited pane to close after Ctrl+D in split. data=\(done)")
|
|
if !focusedPanelAfter.isEmpty || !firstResponderPanelAfter.isEmpty {
|
|
XCTAssertEqual(
|
|
firstResponderPanelAfter,
|
|
focusedPanelAfter,
|
|
"Expected first responder and focused panel to converge after Ctrl+D. data=\(done)"
|
|
)
|
|
}
|
|
}
|
|
|
|
func testCtrlDFromKeyboardInThreePaneLayoutClosesOnlyFocusedPane() {
|
|
let app = XCUIApplication()
|
|
let dataPath = "/tmp/cmux-ui-test-child-exit-keyboard-tree-\(UUID().uuidString).json"
|
|
try? FileManager.default.removeItem(atPath: dataPath)
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_SETUP"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_PATH"] = dataPath
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_LAYOUT"] = "lr_left_vertical"
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_EXPECTED_PANELS_AFTER"] = "2"
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_AUTO_TRIGGER"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_STRICT"] = "1"
|
|
app.launch()
|
|
app.activate()
|
|
|
|
XCTAssertTrue(waitForAnyJSON(atPath: dataPath, timeout: 12.0), "Expected keyboard child-exit setup data at \(dataPath)")
|
|
guard let ready = waitForJSONKey("ready", equals: "1", atPath: dataPath, timeout: 12.0) else {
|
|
XCTFail("Timed out waiting for ready=1. data=\(loadJSON(atPath: dataPath) ?? [:])")
|
|
return
|
|
}
|
|
|
|
if let setupError = ready["setupError"], !setupError.isEmpty {
|
|
XCTFail("Setup failed: \(setupError)")
|
|
return
|
|
}
|
|
|
|
let rightPanelId = ready["rightPanelId"] ?? ""
|
|
guard !rightPanelId.isEmpty else {
|
|
XCTFail("Missing rightPanelId in setup data. data=\(ready)")
|
|
return
|
|
}
|
|
assertCtrlDPreconditionsBeforeTrigger(ready, expectedExitPanelId: rightPanelId, context: "Three-pane layout")
|
|
guard let done = waitForJSONKey("done", equals: "1", atPath: dataPath, timeout: 10.0) else {
|
|
XCTFail("Timed out waiting for done=1 after Ctrl+D. data=\(loadJSON(atPath: dataPath) ?? [:])")
|
|
return
|
|
}
|
|
|
|
let workspaceCountAfter = Int(done["workspaceCountAfter"] ?? "") ?? -1
|
|
let panelCountAfter = Int(done["panelCountAfter"] ?? "") ?? -1
|
|
let closedWorkspace = (done["closedWorkspace"] ?? "") == "1"
|
|
let timedOut = (done["timedOut"] ?? "") == "1"
|
|
let focusedPanelAfter = done["focusedPanelAfter"] ?? ""
|
|
let firstResponderPanelAfter = done["firstResponderPanelAfter"] ?? ""
|
|
|
|
XCTAssertFalse(timedOut, "Keyboard Ctrl+D test timed out. data=\(done)")
|
|
XCTAssertFalse(closedWorkspace, "Ctrl+D should not close workspace/window when multiple panes remain. data=\(done)")
|
|
XCTAssertEqual(workspaceCountAfter, 1, "Expected workspace to remain open after Ctrl+D in three-pane layout. data=\(done)")
|
|
XCTAssertEqual(panelCountAfter, 2, "Expected only focused exited pane to close in three-pane layout. data=\(done)")
|
|
if !focusedPanelAfter.isEmpty || !firstResponderPanelAfter.isEmpty {
|
|
XCTAssertEqual(
|
|
firstResponderPanelAfter,
|
|
focusedPanelAfter,
|
|
"Expected first responder and focused panel to converge after Ctrl+D in three-pane layout. data=\(done)"
|
|
)
|
|
}
|
|
}
|
|
|
|
func testCtrlDAfterClosingRightColumnIn2x2KeepsWorkspaceOpen() {
|
|
// This regression can be timing-sensitive; run several fresh launches to catch
|
|
// any single bad close routing/focus cycle.
|
|
let attempts = 8
|
|
for attempt in 1...attempts {
|
|
let app = XCUIApplication()
|
|
let dataPath = "/tmp/cmux-ui-test-child-exit-keyboard-2x2-\(UUID().uuidString).json"
|
|
try? FileManager.default.removeItem(atPath: dataPath)
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_SETUP"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_PATH"] = dataPath
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_LAYOUT"] = "lrtd_close_right_then_exit_top_left"
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_EXPECTED_PANELS_AFTER"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_AUTO_TRIGGER"] = "0"
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_STRICT"] = "1"
|
|
app.launch()
|
|
app.activate()
|
|
defer { app.terminate() }
|
|
|
|
XCTAssertTrue(
|
|
waitForAnyJSON(atPath: dataPath, timeout: 12.0),
|
|
"Attempt \(attempt): expected keyboard child-exit setup data at \(dataPath)"
|
|
)
|
|
guard let ready = waitForJSONKey("ready", equals: "1", atPath: dataPath, timeout: 12.0) else {
|
|
XCTFail("Attempt \(attempt): timed out waiting for ready=1. data=\(loadJSON(atPath: dataPath) ?? [:])")
|
|
return
|
|
}
|
|
|
|
if let setupError = ready["setupError"], !setupError.isEmpty {
|
|
XCTFail("Attempt \(attempt): setup failed: \(setupError)")
|
|
return
|
|
}
|
|
|
|
let panelCountBefore = Int(ready["panelCountBeforeCtrlD"] ?? "") ?? -1
|
|
let exitPanelId = ready["exitPanelId"] ?? ""
|
|
XCTAssertEqual(
|
|
panelCountBefore,
|
|
2,
|
|
"Attempt \(attempt): expected two panels before Ctrl+D in 2x2-right-close repro. data=\(ready)"
|
|
)
|
|
guard !exitPanelId.isEmpty else {
|
|
XCTFail("Attempt \(attempt): missing exitPanelId in setup data. data=\(ready)")
|
|
return
|
|
}
|
|
assertCtrlDPreconditionsBeforeTrigger(ready, expectedExitPanelId: exitPanelId, context: "Attempt \(attempt): 2x2-right-close")
|
|
|
|
app.typeKey("d", modifierFlags: [.control])
|
|
|
|
guard let done = waitForJSONKey("done", equals: "1", atPath: dataPath, timeout: 10.0) else {
|
|
XCTFail("Attempt \(attempt): timed out waiting for done=1 after Ctrl+D. data=\(loadJSON(atPath: dataPath) ?? [:])")
|
|
return
|
|
}
|
|
|
|
let workspaceCountAfter = Int(done["workspaceCountAfter"] ?? "") ?? -1
|
|
let panelCountAfter = Int(done["panelCountAfter"] ?? "") ?? -1
|
|
let closedWorkspace = (done["closedWorkspace"] ?? "") == "1"
|
|
let timedOut = (done["timedOut"] ?? "") == "1"
|
|
let focusedPanelAfter = done["focusedPanelAfter"] ?? ""
|
|
let firstResponderPanelAfter = done["firstResponderPanelAfter"] ?? ""
|
|
let triggerMode = done["autoTriggerMode"] ?? ""
|
|
|
|
XCTAssertFalse(timedOut, "Attempt \(attempt): keyboard Ctrl+D 2x2-right-close timed out. data=\(done)")
|
|
XCTAssertNotEqual(triggerMode, "runtime_close_callback", "Attempt \(attempt): expected real keyboard child-exit path, not runtime callback shortcut. data=\(done)")
|
|
XCTAssertFalse(closedWorkspace, "Attempt \(attempt): Ctrl+D should not close workspace/window when another pane remains. data=\(done)")
|
|
XCTAssertEqual(workspaceCountAfter, 1, "Attempt \(attempt): workspace should remain open after Ctrl+D. data=\(done)")
|
|
XCTAssertEqual(panelCountAfter, 1, "Attempt \(attempt): only focused pane should close after Ctrl+D. data=\(done)")
|
|
if !focusedPanelAfter.isEmpty || !firstResponderPanelAfter.isEmpty {
|
|
XCTAssertEqual(
|
|
firstResponderPanelAfter,
|
|
focusedPanelAfter,
|
|
"Attempt \(attempt): expected focus indicator and first responder to converge after Ctrl+D. data=\(done)"
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
func testCtrlDAfterClosingBottomRowIn2x2KeepsWorkspaceOpen() {
|
|
let attempts = 8
|
|
for attempt in 1...attempts {
|
|
let app = XCUIApplication()
|
|
let dataPath = "/tmp/cmux-ui-test-child-exit-keyboard-2x2-bottom-\(UUID().uuidString).json"
|
|
try? FileManager.default.removeItem(atPath: dataPath)
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_SETUP"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_PATH"] = dataPath
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_LAYOUT"] = "tdlr_close_bottom_then_exit_top_left"
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_EXPECTED_PANELS_AFTER"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_AUTO_TRIGGER"] = "0"
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_STRICT"] = "1"
|
|
app.launch()
|
|
app.activate()
|
|
defer { app.terminate() }
|
|
|
|
XCTAssertTrue(
|
|
waitForAnyJSON(atPath: dataPath, timeout: 12.0),
|
|
"Attempt \(attempt): expected keyboard child-exit setup data at \(dataPath)"
|
|
)
|
|
guard let ready = waitForJSONKey("ready", equals: "1", atPath: dataPath, timeout: 12.0) else {
|
|
XCTFail("Attempt \(attempt): timed out waiting for ready=1. data=\(loadJSON(atPath: dataPath) ?? [:])")
|
|
return
|
|
}
|
|
|
|
if let setupError = ready["setupError"], !setupError.isEmpty {
|
|
XCTFail("Attempt \(attempt): setup failed: \(setupError)")
|
|
return
|
|
}
|
|
|
|
let panelCountBefore = Int(ready["panelCountBeforeCtrlD"] ?? "") ?? -1
|
|
let exitPanelId = ready["exitPanelId"] ?? ""
|
|
XCTAssertEqual(
|
|
panelCountBefore,
|
|
2,
|
|
"Attempt \(attempt): expected two panels before Ctrl+D in 2x2-bottom-close repro. data=\(ready)"
|
|
)
|
|
guard !exitPanelId.isEmpty else {
|
|
XCTFail("Attempt \(attempt): missing exitPanelId in setup data. data=\(ready)")
|
|
return
|
|
}
|
|
assertCtrlDPreconditionsBeforeTrigger(ready, expectedExitPanelId: exitPanelId, context: "Attempt \(attempt): 2x2-bottom-close")
|
|
|
|
app.typeKey("d", modifierFlags: [.control])
|
|
|
|
guard let done = waitForJSONKey("done", equals: "1", atPath: dataPath, timeout: 10.0) else {
|
|
XCTFail("Attempt \(attempt): timed out waiting for done=1 after Ctrl+D. data=\(loadJSON(atPath: dataPath) ?? [:])")
|
|
return
|
|
}
|
|
|
|
let workspaceCountAfter = Int(done["workspaceCountAfter"] ?? "") ?? -1
|
|
let panelCountAfter = Int(done["panelCountAfter"] ?? "") ?? -1
|
|
let closedWorkspace = (done["closedWorkspace"] ?? "") == "1"
|
|
let timedOut = (done["timedOut"] ?? "") == "1"
|
|
let focusedPanelAfter = done["focusedPanelAfter"] ?? ""
|
|
let firstResponderPanelAfter = done["firstResponderPanelAfter"] ?? ""
|
|
let triggerMode = done["autoTriggerMode"] ?? ""
|
|
|
|
XCTAssertFalse(timedOut, "Attempt \(attempt): keyboard Ctrl+D 2x2-bottom-close timed out. data=\(done)")
|
|
XCTAssertNotEqual(triggerMode, "runtime_close_callback", "Attempt \(attempt): expected real keyboard child-exit path, not runtime callback shortcut. data=\(done)")
|
|
XCTAssertFalse(closedWorkspace, "Attempt \(attempt): Ctrl+D should not close workspace/window when another pane remains. data=\(done)")
|
|
XCTAssertEqual(workspaceCountAfter, 1, "Attempt \(attempt): workspace should remain open after Ctrl+D. data=\(done)")
|
|
XCTAssertEqual(panelCountAfter, 1, "Attempt \(attempt): only focused pane should close after Ctrl+D. data=\(done)")
|
|
if !focusedPanelAfter.isEmpty || !firstResponderPanelAfter.isEmpty {
|
|
XCTAssertEqual(
|
|
firstResponderPanelAfter,
|
|
focusedPanelAfter,
|
|
"Attempt \(attempt): expected focus indicator and first responder to converge after Ctrl+D. data=\(done)"
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
func testCtrlDFromRealKeyboardAfterClosingRightColumnIn2x2KeepsWorkspaceOpen() {
|
|
let attempts = 8
|
|
for attempt in 1...attempts {
|
|
let app = XCUIApplication()
|
|
let dataPath = "/tmp/cmux-ui-test-child-exit-keyboard-2x2-realkey-\(UUID().uuidString).json"
|
|
try? FileManager.default.removeItem(atPath: dataPath)
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_SETUP"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_PATH"] = dataPath
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_LAYOUT"] = "lrtd_close_right_then_exit_top_left"
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_EXPECTED_PANELS_AFTER"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_AUTO_TRIGGER"] = "0"
|
|
app.launch()
|
|
app.activate()
|
|
defer { app.terminate() }
|
|
|
|
XCTAssertTrue(
|
|
waitForAnyJSON(atPath: dataPath, timeout: 12.0),
|
|
"Attempt \(attempt): expected keyboard child-exit setup data at \(dataPath)"
|
|
)
|
|
guard let ready = waitForJSONKey("ready", equals: "1", atPath: dataPath, timeout: 12.0) else {
|
|
XCTFail("Attempt \(attempt): timed out waiting for ready=1. data=\(loadJSON(atPath: dataPath) ?? [:])")
|
|
return
|
|
}
|
|
|
|
if let setupError = ready["setupError"], !setupError.isEmpty {
|
|
XCTFail("Attempt \(attempt): setup failed: \(setupError)")
|
|
return
|
|
}
|
|
|
|
let panelCountBefore = Int(ready["panelCountBeforeCtrlD"] ?? "") ?? -1
|
|
let exitPanelId = ready["exitPanelId"] ?? ""
|
|
XCTAssertEqual(
|
|
panelCountBefore,
|
|
2,
|
|
"Attempt \(attempt): expected two panels before Ctrl+D in 2x2-right-close repro. data=\(ready)"
|
|
)
|
|
guard !exitPanelId.isEmpty else {
|
|
XCTFail("Attempt \(attempt): missing exitPanelId in setup data. data=\(ready)")
|
|
return
|
|
}
|
|
assertCtrlDPreconditionsBeforeTrigger(ready, expectedExitPanelId: exitPanelId, context: "Attempt \(attempt): 2x2-right-close real key")
|
|
|
|
app.typeKey("d", modifierFlags: [.control])
|
|
|
|
guard let done = waitForJSONKey("done", equals: "1", atPath: dataPath, timeout: 10.0) else {
|
|
XCTFail("Attempt \(attempt): timed out waiting for done=1 after real keyboard Ctrl+D. data=\(loadJSON(atPath: dataPath) ?? [:])")
|
|
return
|
|
}
|
|
|
|
let workspaceCountAfter = Int(done["workspaceCountAfter"] ?? "") ?? -1
|
|
let panelCountAfter = Int(done["panelCountAfter"] ?? "") ?? -1
|
|
let closedWorkspace = (done["closedWorkspace"] ?? "") == "1"
|
|
let timedOut = (done["timedOut"] ?? "") == "1"
|
|
let focusedPanelAfter = done["focusedPanelAfter"] ?? ""
|
|
let firstResponderPanelAfter = done["firstResponderPanelAfter"] ?? ""
|
|
|
|
XCTAssertFalse(timedOut, "Attempt \(attempt): real keyboard Ctrl+D timed out. data=\(done)")
|
|
XCTAssertFalse(closedWorkspace, "Attempt \(attempt): real keyboard Ctrl+D should not close workspace/window when another pane remains. data=\(done)")
|
|
XCTAssertEqual(workspaceCountAfter, 1, "Attempt \(attempt): workspace should remain open after real keyboard Ctrl+D. data=\(done)")
|
|
XCTAssertEqual(panelCountAfter, 1, "Attempt \(attempt): only focused pane should close after real keyboard Ctrl+D. data=\(done)")
|
|
XCTAssertTrue(
|
|
waitForWindowCount(app: app, atLeast: 1, timeout: 2.0),
|
|
"Attempt \(attempt): app window should remain open after Ctrl+D closes one split. data=\(done)"
|
|
)
|
|
if let showChildExitedCount = Int(done["probeShowChildExitedCount"] ?? "") {
|
|
XCTAssertEqual(showChildExitedCount, 1, "Attempt \(attempt): expected exactly one SHOW_CHILD_EXITED callback for one Ctrl+D. data=\(done)")
|
|
}
|
|
if let keyDownCount = Int(done["probeKeyDownCount"] ?? "") {
|
|
XCTAssertEqual(keyDownCount, 1, "Attempt \(attempt): expected exactly one keyDown for one Ctrl+D keypress. data=\(done)")
|
|
}
|
|
if !focusedPanelAfter.isEmpty || !firstResponderPanelAfter.isEmpty {
|
|
XCTAssertEqual(
|
|
firstResponderPanelAfter,
|
|
focusedPanelAfter,
|
|
"Attempt \(attempt): expected focus indicator and first responder to converge after real keyboard Ctrl+D. data=\(done)"
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
func testCtrlDFromRealKeyboardInHorizontalSplitKeepsWindowOpen() {
|
|
let attempts = 12
|
|
for attempt in 1...attempts {
|
|
let app = XCUIApplication()
|
|
let dataPath = "/tmp/cmux-ui-test-child-exit-keyboard-lr-realkey-\(UUID().uuidString).json"
|
|
try? FileManager.default.removeItem(atPath: dataPath)
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_SETUP"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_PATH"] = dataPath
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_LAYOUT"] = "lr"
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_EXPECTED_PANELS_AFTER"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_AUTO_TRIGGER"] = "0"
|
|
app.launch()
|
|
app.activate()
|
|
defer { app.terminate() }
|
|
|
|
XCTAssertTrue(
|
|
waitForAnyJSON(atPath: dataPath, timeout: 12.0),
|
|
"Attempt \(attempt): expected keyboard child-exit setup data at \(dataPath)"
|
|
)
|
|
guard let ready = waitForJSONKey("ready", equals: "1", atPath: dataPath, timeout: 12.0) else {
|
|
XCTFail("Attempt \(attempt): timed out waiting for ready=1. data=\(loadJSON(atPath: dataPath) ?? [:])")
|
|
return
|
|
}
|
|
|
|
if let setupError = ready["setupError"], !setupError.isEmpty {
|
|
XCTFail("Attempt \(attempt): setup failed: \(setupError)")
|
|
return
|
|
}
|
|
|
|
let panelCountBefore = Int(ready["panelCountBeforeCtrlD"] ?? "") ?? -1
|
|
let exitPanelId = ready["exitPanelId"] ?? ""
|
|
XCTAssertEqual(
|
|
panelCountBefore,
|
|
2,
|
|
"Attempt \(attempt): expected two panels before Ctrl+D in left/right repro. data=\(ready)"
|
|
)
|
|
guard !exitPanelId.isEmpty else {
|
|
XCTFail("Attempt \(attempt): missing exitPanelId in setup data. data=\(ready)")
|
|
return
|
|
}
|
|
assertCtrlDPreconditionsBeforeTrigger(ready, expectedExitPanelId: exitPanelId, context: "Attempt \(attempt): left/right real key")
|
|
|
|
app.typeKey("d", modifierFlags: [.control])
|
|
|
|
guard let done = waitForJSONKey("done", equals: "1", atPath: dataPath, timeout: 10.0) else {
|
|
XCTFail("Attempt \(attempt): timed out waiting for done=1 after real keyboard Ctrl+D. data=\(loadJSON(atPath: dataPath) ?? [:])")
|
|
return
|
|
}
|
|
|
|
let workspaceCountAfter = Int(done["workspaceCountAfter"] ?? "") ?? -1
|
|
let panelCountAfter = Int(done["panelCountAfter"] ?? "") ?? -1
|
|
let closedWorkspace = (done["closedWorkspace"] ?? "") == "1"
|
|
let timedOut = (done["timedOut"] ?? "") == "1"
|
|
let focusedPanelAfter = done["focusedPanelAfter"] ?? ""
|
|
let firstResponderPanelAfter = done["firstResponderPanelAfter"] ?? ""
|
|
|
|
XCTAssertFalse(timedOut, "Attempt \(attempt): real keyboard Ctrl+D timed out. data=\(done)")
|
|
XCTAssertFalse(closedWorkspace, "Attempt \(attempt): real keyboard Ctrl+D should not close workspace/window when another pane remains. data=\(done)")
|
|
XCTAssertEqual(workspaceCountAfter, 1, "Attempt \(attempt): workspace should remain open after real keyboard Ctrl+D. data=\(done)")
|
|
XCTAssertEqual(panelCountAfter, 1, "Attempt \(attempt): only focused pane should close after real keyboard Ctrl+D. data=\(done)")
|
|
XCTAssertTrue(
|
|
waitForWindowCount(app: app, atLeast: 1, timeout: 2.0),
|
|
"Attempt \(attempt): app window should remain open after Ctrl+D closes one split. data=\(done)"
|
|
)
|
|
if let showChildExitedCount = Int(done["probeShowChildExitedCount"] ?? "") {
|
|
XCTAssertEqual(showChildExitedCount, 1, "Attempt \(attempt): expected exactly one SHOW_CHILD_EXITED callback for one Ctrl+D. data=\(done)")
|
|
}
|
|
if let keyDownCount = Int(done["probeKeyDownCount"] ?? "") {
|
|
XCTAssertEqual(keyDownCount, 1, "Attempt \(attempt): expected exactly one keyDown for one Ctrl+D keypress. data=\(done)")
|
|
}
|
|
if !focusedPanelAfter.isEmpty || !firstResponderPanelAfter.isEmpty {
|
|
XCTAssertEqual(
|
|
firstResponderPanelAfter,
|
|
focusedPanelAfter,
|
|
"Attempt \(attempt): expected focus indicator and first responder to converge after real keyboard Ctrl+D. data=\(done)"
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
func testCtrlDEarlyDuringSplitStartupKeepsWindowOpen() {
|
|
let attempts = 12
|
|
for attempt in 1...attempts {
|
|
let app = XCUIApplication()
|
|
let dataPath = "/tmp/cmux-ui-test-child-exit-keyboard-lr-early-ctrl-\(UUID().uuidString).json"
|
|
try? FileManager.default.removeItem(atPath: dataPath)
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_SETUP"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_PATH"] = dataPath
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_LAYOUT"] = "lr"
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_EXPECTED_PANELS_AFTER"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_AUTO_TRIGGER"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_STRICT"] = "1"
|
|
app.launchEnvironment["CMUX_UI_TEST_CHILD_EXIT_KEYBOARD_TRIGGER_MODE"] = "early_ctrl_d"
|
|
app.launch()
|
|
app.activate()
|
|
defer { app.terminate() }
|
|
|
|
XCTAssertTrue(
|
|
waitForAnyJSON(atPath: dataPath, timeout: 12.0),
|
|
"Attempt \(attempt): expected early Ctrl+D setup data at \(dataPath)"
|
|
)
|
|
guard let done = waitForJSONKey("done", equals: "1", atPath: dataPath, timeout: 10.0) else {
|
|
XCTFail("Attempt \(attempt): timed out waiting for done=1 after early Ctrl+D. data=\(loadJSON(atPath: dataPath) ?? [:])")
|
|
return
|
|
}
|
|
|
|
if let setupError = done["setupError"], !setupError.isEmpty {
|
|
XCTFail("Attempt \(attempt): setup failed: \(setupError)")
|
|
return
|
|
}
|
|
|
|
let workspaceCountAfter = Int(done["workspaceCountAfter"] ?? "") ?? -1
|
|
let panelCountAfter = Int(done["panelCountAfter"] ?? "") ?? -1
|
|
let closedWorkspace = (done["closedWorkspace"] ?? "") == "1"
|
|
let timedOut = (done["timedOut"] ?? "") == "1"
|
|
let triggerMode = done["autoTriggerMode"] ?? ""
|
|
let exitPanelId = done["exitPanelId"] ?? ""
|
|
let workspaceId = done["workspaceId"] ?? ""
|
|
let probeSurfaceId = done["probeShowChildExitedSurfaceId"] ?? ""
|
|
let probeTabId = done["probeShowChildExitedTabId"] ?? ""
|
|
|
|
XCTAssertFalse(timedOut, "Attempt \(attempt): early Ctrl+D timed out. data=\(done)")
|
|
XCTAssertEqual(triggerMode, "strict_early_ctrl_d", "Attempt \(attempt): expected strict early Ctrl+D trigger mode. data=\(done)")
|
|
XCTAssertFalse(closedWorkspace, "Attempt \(attempt): workspace/window should stay open after early Ctrl+D. data=\(done)")
|
|
XCTAssertEqual(workspaceCountAfter, 1, "Attempt \(attempt): workspace should remain open after early Ctrl+D. data=\(done)")
|
|
XCTAssertEqual(panelCountAfter, 1, "Attempt \(attempt): only focused pane should close after early Ctrl+D. data=\(done)")
|
|
if let showChildExitedCount = Int(done["probeShowChildExitedCount"] ?? "") {
|
|
XCTAssertEqual(showChildExitedCount, 1, "Attempt \(attempt): expected exactly one SHOW_CHILD_EXITED callback for one early Ctrl+D. data=\(done)")
|
|
}
|
|
if !exitPanelId.isEmpty, !probeSurfaceId.isEmpty {
|
|
XCTAssertEqual(probeSurfaceId, exitPanelId, "Attempt \(attempt): SHOW_CHILD_EXITED should target the split opened by Cmd+D. data=\(done)")
|
|
}
|
|
if !workspaceId.isEmpty, !probeTabId.isEmpty {
|
|
XCTAssertEqual(probeTabId, workspaceId, "Attempt \(attempt): SHOW_CHILD_EXITED should resolve to the active workspace. data=\(done)")
|
|
}
|
|
XCTAssertTrue(
|
|
waitForWindowCount(app: app, atLeast: 1, timeout: 2.0),
|
|
"Attempt \(attempt): app window should remain open after early Ctrl+D. data=\(done)"
|
|
)
|
|
}
|
|
}
|
|
|
|
private func waitForCloseWorkspaceAlert(app: XCUIApplication, timeout: TimeInterval) -> Bool {
|
|
let expectation = XCTNSPredicateExpectation(
|
|
predicate: NSPredicate { _, _ in
|
|
app.dialogs.containing(.staticText, identifier: "Close workspace?").firstMatch.exists ||
|
|
app.alerts.containing(.staticText, identifier: "Close workspace?").firstMatch.exists ||
|
|
app.staticTexts["Close workspace?"].exists
|
|
},
|
|
object: NSObject()
|
|
)
|
|
return XCTWaiter().wait(for: [expectation], timeout: timeout) == .completed
|
|
}
|
|
|
|
private func waitForCloseTabAlert(app: XCUIApplication, timeout: TimeInterval) -> Bool {
|
|
let expectation = XCTNSPredicateExpectation(
|
|
predicate: NSPredicate { _, _ in
|
|
self.isCloseTabAlertPresent(app: app)
|
|
},
|
|
object: NSObject()
|
|
)
|
|
return XCTWaiter().wait(for: [expectation], timeout: timeout) == .completed
|
|
}
|
|
|
|
// Must match the defaultValue for dialog.closeTab.title in TabManager.
|
|
private func isCloseTabAlertPresent(app: XCUIApplication) -> Bool {
|
|
if app.dialogs.containing(.staticText, identifier: "Close tab?").firstMatch.exists { return true }
|
|
if app.alerts.containing(.staticText, identifier: "Close tab?").firstMatch.exists { return true }
|
|
return app.staticTexts["Close tab?"].exists
|
|
}
|
|
|
|
// Must match the defaultValue for dialog.closeTab.title in TabManager.
|
|
private func clickCloseOnCloseTabAlert(app: XCUIApplication) {
|
|
let dialog = app.dialogs.containing(.staticText, identifier: "Close tab?").firstMatch
|
|
if dialog.exists {
|
|
dialog.buttons["Close"].firstMatch.click()
|
|
return
|
|
}
|
|
|
|
let alert = app.alerts.containing(.staticText, identifier: "Close tab?").firstMatch
|
|
if alert.exists {
|
|
alert.buttons["Close"].firstMatch.click()
|
|
return
|
|
}
|
|
|
|
let anyDialog = app.dialogs.firstMatch
|
|
if anyDialog.exists, anyDialog.buttons["Close"].exists {
|
|
anyDialog.buttons["Close"].firstMatch.click()
|
|
}
|
|
}
|
|
|
|
private func waitForWindowCount(app: XCUIApplication, toBe count: Int, timeout: TimeInterval) -> Bool {
|
|
let expectation = XCTNSPredicateExpectation(
|
|
predicate: NSPredicate { _, _ in
|
|
app.windows.count == count
|
|
},
|
|
object: NSObject()
|
|
)
|
|
return XCTWaiter().wait(for: [expectation], timeout: timeout) == .completed
|
|
}
|
|
|
|
private func waitForWindowCount(app: XCUIApplication, atLeast count: Int, timeout: TimeInterval) -> Bool {
|
|
let expectation = XCTNSPredicateExpectation(
|
|
predicate: NSPredicate { _, _ in
|
|
app.windows.count >= count
|
|
},
|
|
object: NSObject()
|
|
)
|
|
return XCTWaiter().wait(for: [expectation], timeout: timeout) == .completed
|
|
}
|
|
|
|
private func waitForNoWindowsOrAppNotRunningForeground(app: XCUIApplication, timeout: TimeInterval) -> Bool {
|
|
let expectation = XCTNSPredicateExpectation(
|
|
predicate: NSPredicate { _, _ in
|
|
app.state != .runningForeground || app.windows.count == 0
|
|
},
|
|
object: NSObject()
|
|
)
|
|
return XCTWaiter().wait(for: [expectation], timeout: timeout) == .completed
|
|
}
|
|
|
|
private func waitForKeyequivInt(_ key: String, toBeAtLeast expected: Int, atPath path: String, timeout: TimeInterval) -> Bool {
|
|
let expectation = XCTNSPredicateExpectation(
|
|
predicate: NSPredicate { _, _ in
|
|
let value = self.loadJSON(atPath: path)?[key].flatMap(Int.init) ?? 0
|
|
return value >= expected
|
|
},
|
|
object: NSObject()
|
|
)
|
|
return XCTWaiter().wait(for: [expectation], timeout: timeout) == .completed
|
|
}
|
|
|
|
private func waitForAnyJSON(atPath path: String, timeout: TimeInterval) -> Bool {
|
|
let expectation = XCTNSPredicateExpectation(
|
|
predicate: NSPredicate { _, _ in
|
|
self.loadJSON(atPath: path) != nil
|
|
},
|
|
object: NSObject()
|
|
)
|
|
return XCTWaiter().wait(for: [expectation], timeout: timeout) == .completed
|
|
}
|
|
|
|
private func waitForJSONKey(_ key: String, equals expected: String, atPath path: String, timeout: TimeInterval) -> [String: String]? {
|
|
var matchedData: [String: String]?
|
|
let expectation = XCTNSPredicateExpectation(
|
|
predicate: NSPredicate { _, _ in
|
|
guard let data = self.loadJSON(atPath: path), data[key] == expected else {
|
|
return false
|
|
}
|
|
matchedData = data
|
|
return true
|
|
},
|
|
object: NSObject()
|
|
)
|
|
guard XCTWaiter().wait(for: [expectation], timeout: timeout) == .completed else {
|
|
return nil
|
|
}
|
|
return matchedData
|
|
}
|
|
|
|
private func assertCtrlDPreconditionsBeforeTrigger(
|
|
_ data: [String: String],
|
|
expectedExitPanelId: String,
|
|
context: String
|
|
) {
|
|
XCTAssertEqual(
|
|
data["focusedPanelBefore"],
|
|
expectedExitPanelId,
|
|
"\(context): expected target exit pane to be focused before Ctrl+D. data=\(data)"
|
|
)
|
|
let firstResponderPanelBefore = data["firstResponderPanelBefore"] ?? ""
|
|
if !firstResponderPanelBefore.isEmpty {
|
|
XCTAssertEqual(
|
|
firstResponderPanelBefore,
|
|
expectedExitPanelId,
|
|
"\(context): expected first responder to match target pane before Ctrl+D when present. data=\(data)"
|
|
)
|
|
}
|
|
}
|
|
|
|
private func loadJSON(atPath path: String) -> [String: String]? {
|
|
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)),
|
|
let object = try? JSONSerialization.jsonObject(with: data) as? [String: String] else {
|
|
return nil
|
|
}
|
|
return object
|
|
}
|
|
|
|
}
|