import XCTest private func sidebarHelpPollUntil( timeout: TimeInterval, pollInterval: TimeInterval = 0.05, condition: () -> Bool ) -> Bool { let start = ProcessInfo.processInfo.systemUptime while true { if condition() { return true } if (ProcessInfo.processInfo.systemUptime - start) >= timeout { return false } RunLoop.current.run(until: Date().addingTimeInterval(pollInterval)) } } final class SidebarHelpMenuUITests: XCTestCase { override func setUp() { super.setUp() continueAfterFailure = false } func testHelpMenuOpensKeyboardShortcutsSection() { let app = XCUIApplication() app.launchEnvironment["CMUX_UI_TEST_MODE"] = "1" launchAndActivate(app) XCTAssertTrue(waitForWindowCount(atLeast: 1, app: app, timeout: 6.0)) let helpButton = requireElement( candidates: helpButtonCandidates(in: app), timeout: 6.0, description: "sidebar help button" ) helpButton.click() let keyboardShortcutsItem = requireElement( candidates: helpMenuItemCandidates(in: app, identifier: "SidebarHelpMenuOptionKeyboardShortcuts", title: "Keyboard Shortcuts"), timeout: 3.0, description: "Keyboard Shortcuts help menu item" ) keyboardShortcutsItem.click() XCTAssertTrue(app.staticTexts["ShortcutRecordingHint"].waitForExistence(timeout: 6.0)) } func testHelpMenuCheckForUpdatesTriggersSidebarUpdatePill() { let app = XCUIApplication() app.launchEnvironment["CMUX_UI_TEST_MODE"] = "1" app.launchEnvironment["CMUX_UI_TEST_FEED_URL"] = "https://cmux.test/appcast.xml" app.launchEnvironment["CMUX_UI_TEST_FEED_MODE"] = "available" app.launchEnvironment["CMUX_UI_TEST_UPDATE_VERSION"] = "9.9.9" app.launchEnvironment["CMUX_UI_TEST_AUTO_ALLOW_PERMISSION"] = "1" launchAndActivate(app) XCTAssertTrue(waitForWindowCount(atLeast: 1, app: app, timeout: 6.0)) let helpButton = requireElement( candidates: helpButtonCandidates(in: app), timeout: 6.0, description: "sidebar help button" ) helpButton.click() let checkForUpdatesItem = requireElement( candidates: helpMenuItemCandidates(in: app, identifier: "SidebarHelpMenuOptionCheckForUpdates", title: "Check for Updates"), timeout: 3.0, description: "Check for Updates help menu item" ) checkForUpdatesItem.click() let updatePill = app.buttons["UpdatePill"] XCTAssertTrue(updatePill.waitForExistence(timeout: 6.0)) XCTAssertEqual(updatePill.label, "Update Available: 9.9.9") } func testHelpMenuSendFeedbackOpensComposerSheet() { let app = XCUIApplication() app.launchEnvironment["CMUX_UI_TEST_MODE"] = "1" launchAndActivate(app) XCTAssertTrue(waitForWindowCount(atLeast: 1, app: app, timeout: 6.0)) let helpButton = requireElement( candidates: helpButtonCandidates(in: app), timeout: 6.0, description: "sidebar help button" ) helpButton.click() let sendFeedbackItem = requireElement( candidates: helpMenuItemCandidates(in: app, identifier: "SidebarHelpMenuOptionSendFeedback", title: "Send Feedback"), timeout: 3.0, description: "Send Feedback help menu item" ) sendFeedbackItem.click() XCTAssertTrue(app.staticTexts["Send Feedback"].waitForExistence(timeout: 3.0)) XCTAssertTrue( firstExistingElement( candidates: [ app.textFields["SidebarFeedbackEmailField"], app.textFields["Your Email"], ], timeout: 2.0 ) != nil ) XCTAssertTrue( firstExistingElement( candidates: [ app.buttons["SidebarFeedbackAttachButton"], app.buttons["Attach Images"], ], timeout: 2.0 ) != nil ) XCTAssertTrue( firstExistingElement( candidates: [ app.buttons["SidebarFeedbackSendButton"], app.buttons["Send"], ], timeout: 2.0 ) != nil ) XCTAssertTrue( app.staticTexts[ "A human will read this! You can also reach us at founders@manaflow.com." ].waitForExistence(timeout: 2.0) ) let messageEditor = requireElement( candidates: [ app.textViews["SidebarFeedbackMessageEditor"], app.scrollViews["SidebarFeedbackMessageEditor"], app.otherElements["SidebarFeedbackMessageEditor"], app.textViews["Message"], ], timeout: 2.0, description: "feedback message editor" ) messageEditor.click() app.typeText("hello") XCTAssertTrue(app.staticTexts["5/4000"].waitForExistence(timeout: 2.0)) } private func waitForWindowCount(atLeast count: Int, app: XCUIApplication, timeout: TimeInterval) -> Bool { sidebarHelpPollUntil(timeout: timeout) { app.windows.count >= count } } private func helpButtonCandidates(in app: XCUIApplication) -> [XCUIElement] { let sidebar = app.otherElements["Sidebar"] return [ app.buttons["SidebarHelpMenuButton"], app.buttons["Help"], sidebar.buttons["SidebarHelpMenuButton"], sidebar.buttons["Help"], ] } private func helpMenuItemCandidates( in app: XCUIApplication, identifier: String, title: String ) -> [XCUIElement] { [ app.buttons[identifier], app.buttons[title], ] } private func firstExistingElement( candidates: [XCUIElement], timeout: TimeInterval ) -> XCUIElement? { var match: XCUIElement? let found = sidebarHelpPollUntil(timeout: timeout) { for candidate in candidates where candidate.exists { match = candidate return true } return false } return found ? match : nil } private func requireElement( candidates: [XCUIElement], timeout: TimeInterval, description: String ) -> XCUIElement { guard let element = firstExistingElement(candidates: candidates, timeout: timeout) else { XCTFail("Expected \(description) to exist") return candidates[0] } return element } private func launchAndActivate(_ app: XCUIApplication, activateTimeout: TimeInterval = 2.0) { app.launch() let activated = sidebarHelpPollUntil(timeout: activateTimeout) { guard app.state != .runningForeground else { return true } app.activate() return app.state == .runningForeground } if !activated { app.activate() } XCTAssertTrue( sidebarHelpPollUntil(timeout: 2.0) { app.state == .runningForeground }, "App did not reach runningForeground before UI interactions" ) } } final class FeedbackComposerShortcutUITests: XCTestCase { override func setUp() { super.setUp() continueAfterFailure = false } func testCmdOptionFOpensFeedbackComposer() { let app = XCUIApplication() app.launchEnvironment["CMUX_UI_TEST_MODE"] = "1" app.launch() app.activate() XCTAssertTrue( sidebarHelpPollUntil(timeout: 6.0) { app.windows.count >= 1 } ) app.typeKey("f", modifierFlags: [.command, .option]) XCTAssertTrue(app.staticTexts["Send Feedback"].waitForExistence(timeout: 3.0)) XCTAssertTrue( app.textFields["SidebarFeedbackEmailField"].waitForExistence(timeout: 2.0) || app.textFields["Your Email"].waitForExistence(timeout: 2.0) ) } func testCmdOptionFWorksWithHiddenSidebar() { let app = XCUIApplication() app.launchEnvironment["CMUX_UI_TEST_MODE"] = "1" app.launch() app.activate() XCTAssertTrue( sidebarHelpPollUntil(timeout: 6.0) { app.windows.count >= 1 } ) app.typeKey("b", modifierFlags: [.command]) XCTAssertTrue( sidebarHelpPollUntil(timeout: 3.0) { !app.buttons["SidebarHelpMenuButton"].exists && !app.buttons["Help"].exists } ) app.typeKey("f", modifierFlags: [.command, .option]) XCTAssertTrue(app.staticTexts["Send Feedback"].waitForExistence(timeout: 3.0)) } func testCmdOptionFWorksFromSettingsWindow() { let app = XCUIApplication() app.launchEnvironment["CMUX_UI_TEST_MODE"] = "1" app.launchEnvironment["CMUX_UI_TEST_SHOW_SETTINGS"] = "1" app.launch() app.activate() XCTAssertTrue( sidebarHelpPollUntil(timeout: 6.0) { app.windows.count >= 2 } ) app.typeKey("f", modifierFlags: [.command, .option]) XCTAssertTrue(app.staticTexts["Send Feedback"].waitForExistence(timeout: 3.0)) XCTAssertTrue( app.textFields["SidebarFeedbackEmailField"].waitForExistence(timeout: 2.0) || app.textFields["Your Email"].waitForExistence(timeout: 2.0) ) } } final class CommandPaletteAllSurfacesUITests: XCTestCase { private var socketPath = "" private let hiddenSurfaceToken = "cmux-command-palette-hidden-surface" private let visibleSurfaceToken = "cmux-command-palette-visible-surface" override func setUp() { super.setUp() continueAfterFailure = false socketPath = "/tmp/cmux-ui-test-command-palette-\(UUID().uuidString).sock" try? FileManager.default.removeItem(atPath: socketPath) } override func tearDown() { try? FileManager.default.removeItem(atPath: socketPath) super.tearDown() } func testCmdShiftPBackspaceReturnsToWorkspaceResults() throws { let app = XCUIApplication() app.launchArguments += ["-AppleLanguages", "(en)", "-AppleLocale", "en_US"] app.launchEnvironment["CMUX_UI_TEST_MODE"] = "1" app.launchEnvironment["CMUX_SOCKET_PATH"] = socketPath launchAndActivate(app) XCTAssertTrue( sidebarHelpPollUntil(timeout: 8.0) { app.windows.count >= 1 }, "Expected the main window to be visible" ) XCTAssertTrue(waitForSocketPong(timeout: 12.0), "Expected control socket at \(socketPath)") let mainWindowId = try XCTUnwrap( socketCommand("current_window")?.trimmingCharacters(in: .whitespacesAndNewlines) ) openCommandPaletteCommands(app: app) _ = try XCTUnwrap( waitForCommandPaletteSnapshot(windowId: mainWindowId, mode: "commands", query: "", timeout: 5.0) { snapshot in self.commandPaletteResultRows(from: snapshot).contains { row in let commandId = row["command_id"] as? String ?? "" return !commandId.hasPrefix("switcher.") } } ) app.typeKey(XCUIKeyboardKey.delete.rawValue, modifierFlags: []) let switcherSnapshot = try XCTUnwrap( waitForCommandPaletteSnapshot(windowId: mainWindowId, mode: "switcher", query: "", timeout: 5.0) { snapshot in self.commandPaletteResultRows(from: snapshot).contains { row in let commandId = row["command_id"] as? String ?? "" return commandId.hasPrefix("switcher.workspace.") } } ) XCTAssertTrue( commandPaletteResultRows(from: switcherSnapshot).contains { row in let commandId = row["command_id"] as? String ?? "" return commandId.hasPrefix("switcher.workspace.") }, "Expected deleting the command prefix to restore workspace rows. snapshot=\(switcherSnapshot)" ) let rows = commandPaletteResultRows(from: switcherSnapshot) let firstRowCommandId = rows.first?["command_id"] as? String ?? "" XCTAssertTrue( firstRowCommandId.hasPrefix("switcher.workspace."), "Expected the first restored row to be a workspace. snapshot=\(switcherSnapshot)" ) let firstWorkspaceRow = try XCTUnwrap( rows.first(where: { row in let commandId = row["command_id"] as? String ?? "" return commandId.hasPrefix("switcher.workspace.") }), "Expected a workspace row in the restored switcher results. snapshot=\(switcherSnapshot)" ) let workspaceTitle = try XCTUnwrap( firstWorkspaceRow["title"] as? String, "Expected the restored workspace row to include a title. snapshot=\(switcherSnapshot)" ) let workspaceLabel = app.staticTexts[workspaceTitle].firstMatch XCTAssertTrue( sidebarHelpPollUntil(timeout: 2.0) { workspaceLabel.exists && workspaceLabel.isHittable }, "Expected the restored workspace row to be visibly rendered. title=\(workspaceTitle) snapshot=\(switcherSnapshot)" ) let staleCommandLabel = app.staticTexts["Close Other Workspaces"].firstMatch XCTAssertTrue( sidebarHelpPollUntil(timeout: 2.0) { !staleCommandLabel.exists || !staleCommandLabel.isHittable }, "Expected the stale command row to disappear after deleting the command prefix. snapshot=\(switcherSnapshot)" ) } func testCmdPSearchCanIncludeSurfacesFromOtherWorkspacesWhenEnabled() throws { let app = XCUIApplication() app.launchArguments += ["-AppleLanguages", "(en)", "-AppleLocale", "en_US"] app.launchEnvironment["CMUX_UI_TEST_MODE"] = "1" app.launchEnvironment["CMUX_UI_TEST_SHOW_SETTINGS"] = "1" app.launchEnvironment["CMUX_SOCKET_PATH"] = socketPath launchAndActivate(app) XCTAssertTrue( sidebarHelpPollUntil(timeout: 8.0) { app.windows.count >= 2 }, "Expected the main window and Settings window to be visible" ) XCTAssertTrue(waitForSocketPong(timeout: 12.0), "Expected control socket at \(socketPath)") let mainWindowId = try XCTUnwrap(socketCommand("current_window")?.trimmingCharacters(in: .whitespacesAndNewlines)) let secondaryWorkspaceId = try XCTUnwrap(okUUID(from: socketCommand("new_workspace"))) let initialSurfaceId = try XCTUnwrap(waitForSurfaceIDs(minimumCount: 1, timeout: 5.0).first) let hiddenSurfaceId = try XCTUnwrap(okUUID(from: socketCommand("new_surface --type=terminal"))) XCTAssertEqual( socketCommand("report_pwd /tmp/\(hiddenSurfaceToken) --tab=\(secondaryWorkspaceId) --panel=\(hiddenSurfaceId)"), "OK" ) XCTAssertEqual(socketCommand("focus_surface \(initialSurfaceId)"), "OK") XCTAssertEqual( socketCommand("report_pwd /tmp/\(visibleSurfaceToken) --tab=\(secondaryWorkspaceId) --panel=\(initialSurfaceId)"), "OK" ) XCTAssertEqual(socketCommand("select_workspace 0"), "OK") XCTAssertEqual(socketCommand("focus_window \(mainWindowId)"), "OK") RunLoop.current.run(until: Date().addingTimeInterval(0.4)) openCommandPalette(app: app, query: hiddenSurfaceToken) let disabledSnapshot = try XCTUnwrap( waitForCommandPaletteSnapshot(windowId: mainWindowId, query: hiddenSurfaceToken, timeout: 5.0) { snapshot in self.commandPaletteResultRows(from: snapshot).isEmpty } ) XCTAssertEqual(commandPaletteResultRows(from: disabledSnapshot).count, 0) dismissCommandPalette(app: app) focusSettingsWindow(app: app) let toggle = try requireSearchAllSurfacesToggle(app: app) if !toggleIsOn(toggle) { toggle.click() } XCTAssertTrue( sidebarHelpPollUntil(timeout: 3.0) { toggle.exists && toggleIsOn(toggle) }, "Expected the all-surfaces search setting to be enabled" ) XCTAssertEqual(socketCommand("focus_window \(mainWindowId)"), "OK") openCommandPalette(app: app, query: hiddenSurfaceToken) let enabledSnapshot = try XCTUnwrap( waitForCommandPaletteSnapshot(windowId: mainWindowId, query: hiddenSurfaceToken, timeout: 5.0) { snapshot in self.commandPaletteResultRows(from: snapshot).contains { row in let commandId = row["command_id"] as? String ?? "" let trailingLabel = row["trailing_label"] as? String ?? "" return commandId.hasPrefix("switcher.surface.") && trailingLabel == "Terminal" } } ) XCTAssertTrue( commandPaletteResultRows(from: enabledSnapshot).contains { row in let commandId = row["command_id"] as? String ?? "" let trailingLabel = row["trailing_label"] as? String ?? "" return commandId.hasPrefix("switcher.surface.") && trailingLabel == "Terminal" }, "Expected Cmd+P to surface the hidden terminal when all-surfaces search is enabled. snapshot=\(enabledSnapshot)" ) } private func launchAndActivate(_ app: XCUIApplication) { app.launch() XCTAssertTrue( sidebarHelpPollUntil(timeout: 4.0) { guard app.state != .runningForeground else { return true } app.activate() return app.state == .runningForeground }, "App did not reach runningForeground before UI interactions" ) } private func openCommandPalette(app: XCUIApplication, query: String) { let searchField = app.textFields["CommandPaletteSearchField"] app.typeKey("p", modifierFlags: [.command]) XCTAssertTrue(searchField.waitForExistence(timeout: 5.0), "Expected command palette search field") searchField.click() searchField.typeText(query) } private func openCommandPaletteCommands(app: XCUIApplication) { let searchField = app.textFields["CommandPaletteSearchField"] app.typeKey("p", modifierFlags: [.command, .shift]) XCTAssertTrue(searchField.waitForExistence(timeout: 5.0), "Expected command palette search field") searchField.click() } private func dismissCommandPalette(app: XCUIApplication) { let searchField = app.textFields["CommandPaletteSearchField"] for _ in 0..<2 { app.typeKey(XCUIKeyboardKey.escape.rawValue, modifierFlags: []) if sidebarHelpPollUntil(timeout: 1.0) { !searchField.exists } { return } } XCTAssertFalse(searchField.exists, "Expected command palette to dismiss") } private func focusSettingsWindow(app: XCUIApplication) { app.typeKey(",", modifierFlags: [.command]) } private func requireSearchAllSurfacesToggle(app: XCUIApplication) throws -> XCUIElement { let toggleId = "CommandPaletteSearchAllSurfacesToggle" let scrollView = app.scrollViews.firstMatch let candidates = [ app.switches[toggleId], app.checkBoxes[toggleId], app.buttons[toggleId], app.otherElements[toggleId], ] for _ in 0..<8 { if let element = firstExistingElement(candidates: candidates, timeout: 0.4), element.isHittable { return element } if scrollView.exists { scrollView.swipeUp() } } throw XCTSkip("Could not find the command palette all-surfaces toggle") } private func toggleIsOn(_ element: XCUIElement) -> Bool { let value = String(describing: element.value ?? "").trimmingCharacters(in: .whitespacesAndNewlines).lowercased() return value == "1" || value == "true" || value == "on" } private func firstExistingElement( candidates: [XCUIElement], timeout: TimeInterval ) -> XCUIElement? { var match: XCUIElement? let found = sidebarHelpPollUntil(timeout: timeout) { for candidate in candidates where candidate.exists { match = candidate return true } return false } return found ? match : nil } private func waitForSocketPong(timeout: TimeInterval) -> Bool { sidebarHelpPollUntil(timeout: timeout) { socketCommand("ping") == "PONG" } } private func waitForSurfaceIDs(minimumCount: Int, timeout: TimeInterval) -> [String] { var ids: [String] = [] let found = sidebarHelpPollUntil(timeout: timeout) { ids = surfaceIDs() return ids.count >= minimumCount } return found ? ids : surfaceIDs() } private func surfaceIDs() -> [String] { guard let response = socketCommand("list_surfaces"), !response.isEmpty, !response.hasPrefix("No surfaces") else { return [] } return response .split(separator: "\n") .compactMap { line in guard let range = line.range(of: ": ") else { return nil } return String(line[range.upperBound...]).trimmingCharacters(in: .whitespacesAndNewlines) } } private func okUUID(from response: String?) -> String? { guard let response, response.hasPrefix("OK ") else { return nil } let value = String(response.dropFirst(3)).trimmingCharacters(in: .whitespacesAndNewlines) return UUID(uuidString: value) != nil ? value : nil } private func socketCommand(_ command: String) -> String? { ControlSocketClient(path: socketPath, responseTimeout: 2.0).sendLine(command) } private func commandPaletteResultRows(from snapshot: [String: Any]) -> [[String: Any]] { snapshot["results"] as? [[String: Any]] ?? [] } private func waitForCommandPaletteSnapshot( windowId: String, mode: String = "switcher", query: String, timeout: TimeInterval, predicate: (([String: Any]) -> Bool)? = nil ) -> [String: Any]? { var latest: [String: Any]? let matched = sidebarHelpPollUntil(timeout: timeout) { guard let snapshot = commandPaletteSnapshot(windowId: windowId) else { return false } latest = snapshot guard (snapshot["visible"] as? Bool) == true else { return false } guard (snapshot["mode"] as? String) == mode else { return false } guard (snapshot["query"] as? String) == query else { return false } return predicate?(snapshot) ?? true } return matched ? latest : latest } private func commandPaletteSnapshot(windowId: String) -> [String: Any]? { let envelope = socketJSON( method: "debug.command_palette.results", params: [ "window_id": windowId, "limit": 20, ] ) guard let ok = envelope?["ok"] as? Bool, ok else { return nil } return envelope?["result"] as? [String: Any] } private func socketJSON(method: String, params: [String: Any]) -> [String: Any]? { let request: [String: Any] = [ "id": UUID().uuidString, "method": method, "params": params, ] return ControlSocketClient(path: socketPath, responseTimeout: 2.0).sendJSON(request) } private final class ControlSocketClient { private let path: String private let responseTimeout: TimeInterval init(path: String, responseTimeout: TimeInterval) { self.path = path self.responseTimeout = responseTimeout } func sendJSON(_ object: [String: Any]) -> [String: Any]? { guard JSONSerialization.isValidJSONObject(object), let data = try? JSONSerialization.data(withJSONObject: object), let line = String(data: data, encoding: .utf8), let response = sendLine(line), let responseData = response.data(using: .utf8), let parsed = try? JSONSerialization.jsonObject(with: responseData) as? [String: Any] else { return nil } return parsed } func sendLine(_ line: String) -> String? { let fd = socket(AF_UNIX, SOCK_STREAM, 0) guard fd >= 0 else { return nil } defer { close(fd) } #if os(macOS) var noSigPipe: Int32 = 1 _ = withUnsafePointer(to: &noSigPipe) { ptr in setsockopt( fd, SOL_SOCKET, SO_NOSIGPIPE, ptr, socklen_t(MemoryLayout.size) ) } #endif var addr = sockaddr_un() memset(&addr, 0, MemoryLayout.size) addr.sun_family = sa_family_t(AF_UNIX) let maxLen = MemoryLayout.size(ofValue: addr.sun_path) let bytes = Array(path.utf8CString) guard bytes.count <= maxLen else { return nil } withUnsafeMutablePointer(to: &addr.sun_path) { ptr in let raw = UnsafeMutableRawPointer(ptr).assumingMemoryBound(to: CChar.self) memset(raw, 0, maxLen) for index in 0...offset(of: \.sun_path) ?? 0 let addrLen = socklen_t(pathOffset + bytes.count) #if os(macOS) addr.sun_len = UInt8(min(Int(addrLen), 255)) #endif let connected = withUnsafePointer(to: &addr) { ptr in ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { sa in connect(fd, sa, addrLen) } } guard connected == 0 else { return nil } let payload = line + "\n" let wrote: Bool = payload.withCString { cString in var remaining = strlen(cString) var pointer = UnsafeRawPointer(cString) while remaining > 0 { let written = write(fd, pointer, remaining) if written <= 0 { return false } remaining -= written pointer = pointer.advanced(by: written) } return true } guard wrote else { return nil } let deadline = Date().addingTimeInterval(responseTimeout) var buffer = [UInt8](repeating: 0, count: 4096) var accumulator = "" while Date() < deadline { var pollDescriptor = pollfd(fd: fd, events: Int16(POLLIN), revents: 0) let ready = poll(&pollDescriptor, 1, 100) if ready < 0 { return nil } if ready == 0 { continue } let count = read(fd, &buffer, buffer.count) if count <= 0 { break } if let chunk = String(bytes: buffer[0..