diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 6b4acc59..1b979eac 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -5776,6 +5776,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent "expectedLatestWindowId": window1.windowId.uuidString, "expectedLatestTabId": tabId1.uuidString, ], at: path) + self.prepareMultiWindowNotificationSourceTerminalIfNeeded( + at: path, + windowId: window1.windowId, + tabManager: window1.tabManager, + tabId: tabId1, + surfaceId: surfaceId1 + ) self.publishMultiWindowNotificationSocketStateIfNeeded(at: path) } } @@ -5783,6 +5790,71 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent } } + private func prepareMultiWindowNotificationSourceTerminalIfNeeded( + at path: String, + windowId: UUID, + tabManager: TabManager, + tabId: UUID, + surfaceId: UUID + ) { + let env = ProcessInfo.processInfo.environment + guard env["CMUX_UI_TEST_NOTIFY_SOURCE_TERMINAL_READY"] == "1" else { return } + + writeMultiWindowNotificationTestData([ + "sourceTerminalReady": "pending", + "sourceTerminalFocusFailure": "", + ], at: path) + + let deadline = Date().addingTimeInterval(8.0) + + func publish(ready: Bool, failure: String = "") { + writeMultiWindowNotificationTestData([ + "sourceTerminalReady": ready ? "1" : "0", + "sourceTerminalFocusFailure": failure, + ], at: path) + } + + func poll() { + guard let workspace = tabManager.tabs.first(where: { $0.id == tabId }) else { + publish(ready: false, failure: "workspace_missing") + return + } + guard let terminalPanel = workspace.terminalPanel(for: surfaceId) else { + publish(ready: false, failure: "terminal_missing") + return + } + + let isWindowFrontmost = { + guard let window = self.mainWindow(for: windowId) else { return false } + return NSApp.keyWindow === window || NSApp.mainWindow === window + }() + if isWindowFrontmost && terminalPanel.hostedView.isSurfaceViewFirstResponder() { + publish(ready: true) + return + } + + guard Date() < deadline else { + publish( + ready: false, + failure: isWindowFrontmost ? "terminal_not_first_responder" : "window_not_frontmost" + ) + return + } + + _ = self.focusMainWindow(windowId: windowId) + if let tab = tabManager.tabs.first(where: { $0.id == tabId }) { + tabManager.selectTab(tab) + tabManager.focusSurface(tabId: tabId, surfaceId: surfaceId) + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { + poll() + } + } + + poll() + } + private func publishMultiWindowNotificationSocketStateIfNeeded(at path: String) { let env = ProcessInfo.processInfo.environment guard env["CMUX_UI_TEST_SOCKET_SANITY"] == "1" else { return } diff --git a/cmuxUITests/MultiWindowNotificationsUITests.swift b/cmuxUITests/MultiWindowNotificationsUITests.swift index 43696454..632ad44d 100644 --- a/cmuxUITests/MultiWindowNotificationsUITests.swift +++ b/cmuxUITests/MultiWindowNotificationsUITests.swift @@ -199,6 +199,7 @@ final class MultiWindowNotificationsUITests: XCTestCase { app.launchEnvironment["CMUX_SOCKET_MODE"] = "allowAll" app.launchEnvironment["CMUX_SOCKET_ENABLE"] = "1" app.launchEnvironment["CMUX_UI_TEST_SOCKET_SANITY"] = "1" + app.launchEnvironment["CMUX_UI_TEST_NOTIFY_SOURCE_TERMINAL_READY"] = "1" app.launchEnvironment["CMUX_UI_TEST_ENABLE_DUPLICATE_LAUNCH_OBSERVER"] = "1" app.launchEnvironment["CMUX_TAG"] = launchTag app.launch() @@ -211,9 +212,15 @@ final class MultiWindowNotificationsUITests: XCTestCase { let tabId2 = data["tabId2"] ?? "" let surfaceId2 = data["surfaceId2"] ?? "" let socketReady = data["socketReady"] ?? "" - return !tabId2.isEmpty && !surfaceId2.isEmpty && !socketReady.isEmpty && socketReady != "pending" + let sourceTerminalReady = data["sourceTerminalReady"] ?? "" + return !tabId2.isEmpty && + !surfaceId2.isEmpty && + !socketReady.isEmpty && + socketReady != "pending" && + !sourceTerminalReady.isEmpty && + sourceTerminalReady != "pending" }, - "Expected multi-window notification setup data and socket readiness" + "Expected multi-window notification setup data, socket readiness, and source terminal focus" ) guard let setup = loadData() else { @@ -224,18 +231,6 @@ final class MultiWindowNotificationsUITests: XCTestCase { XCTFail("Missing setup workspace id") return } - guard let tabId1 = setup["tabId1"], !tabId1.isEmpty else { - XCTFail("Missing source workspace id") - return - } - guard let window1Id = setup["window1Id"], !window1Id.isEmpty else { - XCTFail("Missing source window id") - return - } - guard let sourceSurfaceId = setup["surfaceId1"], !sourceSurfaceId.isEmpty else { - XCTFail("Missing source surface id") - return - } if let expectedSocketPath = setup["socketExpectedPath"], !expectedSocketPath.isEmpty { socketPath = expectedSocketPath } @@ -257,6 +252,13 @@ final class MultiWindowNotificationsUITests: XCTestCase { XCTFail("Missing target surface id for workspace \(tabId2)") return } + guard setup["sourceTerminalReady"] == "1" else { + XCTFail( + "Expected source terminal to be focused before typing. " + + "failure=\(setup["sourceTerminalFocusFailure"] ?? "")" + ) + return + } XCTAssertTrue(waitForWindowCount(atLeast: 2, app: app, timeout: 6.0)) @@ -302,13 +304,6 @@ final class MultiWindowNotificationsUITests: XCTestCase { ) return } - XCTAssertEqual(socketCommand("focus_window \(window1Id)"), "OK", "Expected source window to be focusable") - XCTAssertEqual(socketCommand("select_workspace \(tabId1)"), "OK", "Expected source workspace to be selectable") - XCTAssertEqual(socketCommand("focus_surface \(sourceSurfaceId)"), "OK", "Expected source terminal to be focusable") - XCTAssertTrue( - waitForTerminalFocus(surfaceId: sourceSurfaceId, timeout: 4.0), - "Expected source terminal surface to own first responder before typing" - ) app.typeText("sh \(commandScriptPath)") app.typeKey(XCUIKeyboardKey.return.rawValue, modifierFlags: [])