Add command palette follow-up regressions

This commit is contained in:
Lawrence Chen 2026-03-25 17:40:38 -07:00
parent 58dc932248
commit 9295a7f676
No known key found for this signature in database
2 changed files with 249 additions and 20 deletions

View file

@ -406,6 +406,7 @@ final class CommandPaletteOpenShortcutConsumptionTests: XCTestCase {
final class CommandPaletteFocusStealerClassificationTests: XCTestCase {
private final class NonViewTextDelegate: NSObject, NSTextViewDelegate {}
private final class UnrelatedViewTextDelegate: NSView, NSTextViewDelegate {}
func testTreatsGhosttySurfaceViewAsFocusStealer() {
let surfaceView = GhosttyNSView(frame: NSRect(x: 0, y: 0, width: 120, height: 80))
@ -446,6 +447,21 @@ final class CommandPaletteFocusStealerClassificationTests: XCTestCase {
"NSTextView responders should still be blocked via the NSView hierarchy walk when the delegate is not a view"
)
}
func testTreatsTextViewInsideTerminalHostedViewAsFocusStealerWhenDelegateViewIsUnrelated() {
let hostedView = GhosttySurfaceScrollView(
surfaceView: GhosttyNSView(frame: NSRect(x: 0, y: 0, width: 120, height: 80))
)
let textView = NSTextView(frame: NSRect(x: 0, y: 0, width: 120, height: 24))
let delegateView = UnrelatedViewTextDelegate(frame: .zero)
textView.delegate = delegateView
hostedView.addSubview(textView)
XCTAssertTrue(
isCommandPaletteFocusStealingTerminalOrBrowserResponder(textView),
"NSTextView responders should still be blocked via the NSView hierarchy walk when the delegate view is unrelated"
)
}
}

View file

@ -2,6 +2,11 @@ import XCTest
import Foundation
final class BrowserPaneNavigationKeybindUITests: XCTestCase {
private struct WorkspaceContext {
let workspaceId: String
let windowId: String
}
private var dataPath = ""
private var socketPath = ""
@ -862,6 +867,12 @@ final class BrowserPaneNavigationKeybindUITests: XCTestCase {
let window = app.windows.firstMatch
_ = window.waitForExistence(timeout: 2.0)
XCTAssertTrue(waitForSocketPong(timeout: 12.0), "Expected control socket at \(socketPath)")
guard let originalWorkspace = currentWorkspaceContext() else {
XCTFail("Expected current workspace context before leaving the original workspace")
return
}
app.typeKey("d", modifierFlags: [.command])
XCTAssertTrue(
@ -901,24 +912,12 @@ final class BrowserPaneNavigationKeybindUITests: XCTestCase {
"Expected browser find field to capture initial typing. value=\(String(describing: findField.value))"
)
app.typeKey("p", modifierFlags: [.command, .shift])
let paletteSearchField = app.textFields["CommandPaletteSearchField"].firstMatch
XCTAssertTrue(paletteSearchField.waitForExistence(timeout: 5.0), "Expected command palette search field")
paletteSearchField.click()
paletteSearchField.typeText("New Workspace")
let firstResultRow = app.descendants(matching: .any).matching(identifier: "CommandPaletteResultRow.0").firstMatch
XCTAssertTrue(firstResultRow.waitForExistence(timeout: 5.0), "Expected command palette results for New Workspace")
app.typeKey(XCUIKeyboardKey.return.rawValue, modifierFlags: [])
openCommandPaletteForNewWorkspace(app, windowId: originalWorkspace.windowId)
XCTAssertTrue(
waitForNonExistence(paletteSearchField, timeout: 5.0),
"Expected command palette to dismiss after creating a workspace"
selectWorkspace(originalWorkspace.workspaceId),
"Expected to return to the original workspace by identity"
)
app.typeKey("1", modifierFlags: [.command])
let restoredFindField = app.textFields["BrowserFindSearchTextField"].firstMatch
XCTAssertTrue(restoredFindField.waitForExistence(timeout: 6.0), "Expected browser find field after returning to workspace 1")
XCTAssertTrue(
@ -1083,6 +1082,12 @@ final class BrowserPaneNavigationKeybindUITests: XCTestCase {
let window = app.windows.firstMatch
XCTAssertTrue(window.waitForExistence(timeout: 10.0), "Expected main window to exist")
XCTAssertTrue(waitForSocketPong(timeout: 12.0), "Expected control socket at \(socketPath)")
guard let originalWorkspace = currentWorkspaceContext() else {
XCTFail("Expected current workspace context before leaving workspace 1")
return
}
app.typeKey("d", modifierFlags: [.command])
focusRightPaneForFindScenario(app, route: .cmdOptionArrows)
@ -1138,8 +1143,11 @@ final class BrowserPaneNavigationKeybindUITests: XCTestCase {
"Expected the intended find owner before leaving workspace 1. data=\(String(describing: loadData()))"
)
openCommandPaletteForNewWorkspace(app)
app.typeKey("1", modifierFlags: [.command])
openCommandPaletteForNewWorkspace(app, windowId: originalWorkspace.windowId)
XCTAssertTrue(
selectWorkspace(originalWorkspace.workspaceId),
"Expected to return to the original workspace by identity"
)
XCTAssertTrue(
waitForDataMatch(timeout: 6.0) { data in
@ -1174,7 +1182,7 @@ final class BrowserPaneNavigationKeybindUITests: XCTestCase {
}
}
private func openCommandPaletteForNewWorkspace(_ app: XCUIApplication) {
private func openCommandPaletteForNewWorkspace(_ app: XCUIApplication, windowId: String) {
app.typeKey("p", modifierFlags: [.command, .shift])
let paletteSearchField = app.textFields["CommandPaletteSearchField"].firstMatch
@ -1182,8 +1190,25 @@ final class BrowserPaneNavigationKeybindUITests: XCTestCase {
paletteSearchField.click()
paletteSearchField.typeText("New Workspace")
let firstResultRow = app.descendants(matching: .any).matching(identifier: "CommandPaletteResultRow.0").firstMatch
XCTAssertTrue(firstResultRow.waitForExistence(timeout: 5.0), "Expected command palette results for New Workspace")
guard let snapshot = waitForCommandPaletteSnapshot(
windowId: windowId,
mode: "commands",
query: "New Workspace",
timeout: 5.0,
predicate: { snapshot in
guard let firstRow = self.commandPaletteResultRows(from: snapshot).first else { return false }
return (firstRow["command_id"] as? String) == "palette.newWorkspace"
}
) else {
XCTFail("Expected palette.newWorkspace to be the selected command palette result")
return
}
XCTAssertEqual(
commandPaletteResultRows(from: snapshot).first?["command_id"] as? String,
"palette.newWorkspace",
"Expected palette.newWorkspace to be selected before pressing Return"
)
app.typeKey(XCUIKeyboardKey.return.rawValue, modifierFlags: [])
XCTAssertTrue(
@ -1230,6 +1255,87 @@ final class BrowserPaneNavigationKeybindUITests: XCTestCase {
}
}
private func waitForSocketPong(timeout: TimeInterval) -> Bool {
waitForCondition(timeout: timeout) {
socketCommand("ping") == "PONG"
}
}
private func currentWorkspaceContext() -> WorkspaceContext? {
guard let envelope = socketJSON(method: "workspace.current", params: [:]),
let ok = envelope["ok"] as? Bool,
ok,
let result = envelope["result"] as? [String: Any],
let workspaceId = result["workspace_id"] as? String,
let windowId = result["window_id"] as? String else {
return nil
}
return WorkspaceContext(workspaceId: workspaceId, windowId: windowId)
}
private func selectWorkspace(_ workspaceId: String) -> Bool {
guard let envelope = socketJSON(
method: "workspace.select",
params: ["workspace_id": workspaceId]
),
let ok = envelope["ok"] as? Bool,
ok else {
return false
}
return waitForCondition(timeout: 5.0) {
self.currentWorkspaceContext()?.workspaceId == workspaceId
}
}
private func socketCommand(_ command: String) -> String? {
ControlSocketClient(path: socketPath, responseTimeout: 2.0).sendLine(command)
}
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 func commandPaletteResultRows(from snapshot: [String: Any]) -> [[String: Any]] {
snapshot["results"] as? [[String: Any]] ?? []
}
private func waitForCommandPaletteSnapshot(
windowId: String,
mode: String,
query: String,
timeout: TimeInterval,
predicate: (([String: Any]) -> Bool)? = nil
) -> [String: Any]? {
var latest: [String: Any]?
let matched = waitForCondition(timeout: timeout) {
guard let snapshot = self.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 : nil
}
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 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"
}
@ -1296,4 +1402,111 @@ final class BrowserPaneNavigationKeybindUITests: XCTestCase {
)
return XCTWaiter().wait(for: [expectation], timeout: timeout) == .completed
}
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<Int32>.size)
)
}
#endif
var addr = sockaddr_un()
memset(&addr, 0, MemoryLayout<sockaddr_un>.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..<bytes.count {
raw[index] = bytes[index]
}
}
let pathOffset = MemoryLayout<sockaddr_un>.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..<count], encoding: .utf8) {
accumulator.append(chunk)
if let newline = accumulator.firstIndex(of: "\n") {
return String(accumulator[..<newline])
}
}
}
return accumulator.isEmpty ? nil : accumulator.trimmingCharacters(in: .whitespacesAndNewlines)
}
}
}