Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions GhosttyTabs.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@
B9000014A1B2C3D4E5F60719 /* JumpToUnreadUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000013A1B2C3D4E5F60719 /* JumpToUnreadUITests.swift */; };
B9000015A1B2C3D4E5F60719 /* MultiWindowNotificationsUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000016A1B2C3D4E5F60719 /* MultiWindowNotificationsUITests.swift */; };
B900001AA1B2C3D4E5F60719 /* CloseWorkspaceConfirmDialogUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000019A1B2C3D4E5F60719 /* CloseWorkspaceConfirmDialogUITests.swift */; };
D1A0B0C2A1B2C3D4E5F60719 /* CloseWorkspacesConfirmDialogUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A0B0C3A1B2C3D4E5F60719 /* CloseWorkspacesConfirmDialogUITests.swift */; };
B9000023A1B2C3D4E5F60719 /* CloseWorkspaceCmdDUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000022A1B2C3D4E5F60719 /* CloseWorkspaceCmdDUITests.swift */; };
BEEF50100000000000000003 /* WindowCloseConfirmCancelUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEEF50100000000000000004 /* WindowCloseConfirmCancelUITests.swift */; };
E1000000A1B2C3D4E5F60718 /* MenuKeyEquivalentRoutingUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1000001A1B2C3D4E5F60718 /* MenuKeyEquivalentRoutingUITests.swift */; };
Expand Down Expand Up @@ -297,6 +298,7 @@
063BC42CEE257D6213A2E30C /* WindowAndDragTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEE83F8394D90ACACD8E19DD /* WindowAndDragTests.swift */; };
1521D55DC63D5E5FC4955E31 /* ShortcutAndCommandPaletteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6083A7DAD962E287FC2FFE94 /* ShortcutAndCommandPaletteTests.swift */; };
CB23911D7E131E8FBC9B82B6 /* SidebarOrderingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC39DE4B96D1931C52AF7D68 /* SidebarOrderingTests.swift */; };
D1A0B0C0A1B2C3D4E5F60719 /* SidebarMarkdownRendererTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A0B0C1A1B2C3D4E5F60719 /* SidebarMarkdownRendererTests.swift */; };
4378399A7C0245EF8186F306 /* OmnibarAndToolsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B09C007F42697761B5F1A2AB /* OmnibarAndToolsTests.swift */; };
734F49D37E543DD01C2F4FEF /* NotificationAndMenuBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C075029771815DD5DA1332 /* NotificationAndMenuBarTests.swift */; };
B6BF3DC98DB1495E57900199 /* TabManagerUnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42092CDB2109E250F7F2A76E /* TabManagerUnitTests.swift */; };
Expand Down Expand Up @@ -645,6 +647,7 @@
B9000013A1B2C3D4E5F60719 /* JumpToUnreadUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JumpToUnreadUITests.swift; sourceTree = "<group>"; };
B9000016A1B2C3D4E5F60719 /* MultiWindowNotificationsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiWindowNotificationsUITests.swift; sourceTree = "<group>"; };
B9000019A1B2C3D4E5F60719 /* CloseWorkspaceConfirmDialogUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseWorkspaceConfirmDialogUITests.swift; sourceTree = "<group>"; };
D1A0B0C3A1B2C3D4E5F60719 /* CloseWorkspacesConfirmDialogUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseWorkspacesConfirmDialogUITests.swift; sourceTree = "<group>"; };
B9000022A1B2C3D4E5F60719 /* CloseWorkspaceCmdDUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseWorkspaceCmdDUITests.swift; sourceTree = "<group>"; };
BEEF50100000000000000004 /* WindowCloseConfirmCancelUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowCloseConfirmCancelUITests.swift; sourceTree = "<group>"; };
E1000001A1B2C3D4E5F60718 /* MenuKeyEquivalentRoutingUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuKeyEquivalentRoutingUITests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -683,6 +686,7 @@
BEE83F8394D90ACACD8E19DD /* WindowAndDragTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowAndDragTests.swift; sourceTree = "<group>"; };
6083A7DAD962E287FC2FFE94 /* ShortcutAndCommandPaletteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutAndCommandPaletteTests.swift; sourceTree = "<group>"; };
BC39DE4B96D1931C52AF7D68 /* SidebarOrderingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarOrderingTests.swift; sourceTree = "<group>"; };
D1A0B0C1A1B2C3D4E5F60719 /* SidebarMarkdownRendererTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarMarkdownRendererTests.swift; sourceTree = "<group>"; };
B09C007F42697761B5F1A2AB /* OmnibarAndToolsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmnibarAndToolsTests.swift; sourceTree = "<group>"; };
D2C075029771815DD5DA1332 /* NotificationAndMenuBarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationAndMenuBarTests.swift; sourceTree = "<group>"; };
42092CDB2109E250F7F2A76E /* TabManagerUnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabManagerUnitTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1133,6 +1137,7 @@
B9000013A1B2C3D4E5F60719 /* JumpToUnreadUITests.swift */,
B9000022A1B2C3D4E5F60719 /* CloseWorkspaceCmdDUITests.swift */,
B9000019A1B2C3D4E5F60719 /* CloseWorkspaceConfirmDialogUITests.swift */,
D1A0B0C3A1B2C3D4E5F60719 /* CloseWorkspacesConfirmDialogUITests.swift */,
BEEF50100000000000000004 /* WindowCloseConfirmCancelUITests.swift */,
B9000016A1B2C3D4E5F60719 /* MultiWindowNotificationsUITests.swift */,
818DBCD4AB69EB72573E8138 /* SidebarResizeUITests.swift */,
Expand Down Expand Up @@ -1180,6 +1185,7 @@
BEE83F8394D90ACACD8E19DD /* WindowAndDragTests.swift */,
6083A7DAD962E287FC2FFE94 /* ShortcutAndCommandPaletteTests.swift */,
BC39DE4B96D1931C52AF7D68 /* SidebarOrderingTests.swift */,
D1A0B0C1A1B2C3D4E5F60719 /* SidebarMarkdownRendererTests.swift */,
AGSV0006A1B2C3D4E5F60719 /* AgentSupervisionTests.swift */,
AGOV0004A1B2C3D4E5F60719 /* AgentOverviewTests.swift */,
B09C007F42697761B5F1A2AB /* OmnibarAndToolsTests.swift */,
Expand Down Expand Up @@ -1573,6 +1579,7 @@
B9000012A1B2C3D4E5F60719 /* AutomationSocketUITests.swift in Sources */,
B9000014A1B2C3D4E5F60719 /* JumpToUnreadUITests.swift in Sources */,
B900001AA1B2C3D4E5F60719 /* CloseWorkspaceConfirmDialogUITests.swift in Sources */,
D1A0B0C2A1B2C3D4E5F60719 /* CloseWorkspacesConfirmDialogUITests.swift in Sources */,
B9000023A1B2C3D4E5F60719 /* CloseWorkspaceCmdDUITests.swift in Sources */,
BEEF50100000000000000003 /* WindowCloseConfirmCancelUITests.swift in Sources */,
B9000015A1B2C3D4E5F60719 /* MultiWindowNotificationsUITests.swift in Sources */,
Expand Down Expand Up @@ -1621,6 +1628,7 @@
063BC42CEE257D6213A2E30C /* WindowAndDragTests.swift in Sources */,
1521D55DC63D5E5FC4955E31 /* ShortcutAndCommandPaletteTests.swift in Sources */,
CB23911D7E131E8FBC9B82B6 /* SidebarOrderingTests.swift in Sources */,
D1A0B0C0A1B2C3D4E5F60719 /* SidebarMarkdownRendererTests.swift in Sources */,
AGSV0005A1B2C3D4E5F60719 /* AgentSupervisionTests.swift in Sources */,
AGOV0003A1B2C3D4E5F60719 /* AgentOverviewTests.swift in Sources */,
4378399A7C0245EF8186F306 /* OmnibarAndToolsTests.swift in Sources */,
Expand Down
9 changes: 8 additions & 1 deletion Sources/TabManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -596,7 +596,14 @@ class TabManager: ObservableObject {

/// Kept in sidebar hierarchy order (see `canonicalizeHierarchyOrderIfNeeded`).
@Published var tabs: [Workspace] = [] {
didSet { canonicalizeHierarchyOrderIfNeeded() }
didSet {
canonicalizeHierarchyOrderIfNeeded()
#if DEBUG
// UI-test seam: lets XCUITests read the workspace count without the control socket.
// No-op unless PROGRAMA_UI_TEST_KEYEQUIV_PATH is set.
UITestRecorder.record(["workspaceCount": String(tabs.count)])
#endif
}
}
@Published var isWorkspaceCycleHot: Bool = false
@Published private(set) var pendingBackgroundWorkspaceLoadIds: Set<UUID> = []
Expand Down
103 changes: 34 additions & 69 deletions programaUITests/CloseWorkspacesConfirmDialogUITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,34 @@ import XCTest
import Foundation

final class CloseWorkspacesConfirmDialogUITests: XCTestCase {
private var socketPath = ""
private var countPath = ""

override func setUp() {
super.setUp()
continueAfterFailure = false
socketPath = "/tmp/programa-ui-test-close-workspaces-\(UUID().uuidString).sock"
try? FileManager.default.removeItem(atPath: socketPath)
countPath = "/tmp/programa-ui-test-close-workspaces-\(UUID().uuidString).json"
try? FileManager.default.removeItem(atPath: countPath)
}

func testCommandPaletteCloseOtherWorkspacesShowsSingleSummaryDialog() {
let app = XCUIApplication()
app.launchEnvironment["PROGRAMA_SOCKET_PATH"] = socketPath
configureLaunch(app)
app.launchEnvironment["PROGRAMA_UI_TEST_FORCE_CONFIRM_CLOSE_WORKSPACE"] = "1"
app.launch()
XCTAssertTrue(
ensureForegroundAfterLaunch(app, timeout: 12.0),
"Expected app to launch for close-workspaces confirmation test. state=\(app.state.rawValue)"
)
XCTAssertTrue(waitForSocketPong(timeout: 12.0), "Expected control socket to respond at \(socketPath)")
XCTAssertTrue(waitForWorkspaceCount(1, timeout: 12.0), "Expected initial workspace. count=\(workspaceCount())")

XCTAssertEqual(socketCommand("new_workspace")?.prefix(2), "OK")
XCTAssertEqual(socketCommand("new_workspace")?.prefix(2), "OK")
app.typeKey("n", modifierFlags: [.command])
XCTAssertTrue(waitForWorkspaceCount(2, timeout: 5.0), "Expected 2 workspaces. count=\(workspaceCount())")
app.typeKey("n", modifierFlags: [.command])
XCTAssertTrue(
waitForWorkspaceCount(3, timeout: 5.0),
"Expected 3 workspaces before running the close-other-workspaces command. list=\(socketCommand("list_workspaces") ?? "<nil>")"
"Expected 3 workspaces before running the close-other-workspaces command. count=\(workspaceCount())"
)
XCTAssertEqual(socketCommand("select_workspace 1"), "OK")
app.typeKey("2", modifierFlags: [.command])

app.typeKey("p", modifierFlags: [.command, .shift])

Expand Down Expand Up @@ -57,26 +58,31 @@ final class CloseWorkspacesConfirmDialogUITests: XCTestCase {
)
XCTAssertTrue(
waitForWorkspaceCount(3, timeout: 5.0),
"Expected all workspaces to remain after cancelling multi-close. list=\(socketCommand("list_workspaces") ?? "<nil>")"
"Expected all workspaces to remain after cancelling multi-close. count=\(workspaceCount())"
)
}

func testCmdShiftWUsesSidebarMultiSelectionSummaryDialog() {
let app = XCUIApplication()
app.launchEnvironment["PROGRAMA_SOCKET_PATH"] = socketPath
configureLaunch(app)
app.launchEnvironment["PROGRAMA_UI_TEST_FORCE_CONFIRM_CLOSE_WORKSPACE"] = "1"
app.launchEnvironment["PROGRAMA_UI_TEST_SIDEBAR_SELECTED_WORKSPACE_INDICES"] = "0,1"
// Applied by the app once the third workspace exists, so later Cmd+N presses cannot reset it.
app.launchEnvironment["PROGRAMA_UI_TEST_SIDEBAR_SELECTED_WORKSPACE_INDICES"] = "1,2"
app.launch()
XCTAssertTrue(
ensureForegroundAfterLaunch(app, timeout: 12.0),
"Expected app to launch for close-workspaces shortcut test. state=\(app.state.rawValue)"
)
XCTAssertTrue(waitForSocketPong(timeout: 12.0), "Expected control socket to respond at \(socketPath)")
XCTAssertTrue(waitForWorkspaceCount(1, timeout: 12.0), "Expected initial workspace. count=\(workspaceCount())")

XCTAssertEqual(socketCommand("new_workspace")?.prefix(2), "OK")
// Three workspaces so the two selected ones are not all of them; closing every
// workspace shows the "Close window?" alert instead.
app.typeKey("n", modifierFlags: [.command])
XCTAssertTrue(waitForWorkspaceCount(2, timeout: 5.0), "Expected 2 workspaces. count=\(workspaceCount())")
app.typeKey("n", modifierFlags: [.command])
XCTAssertTrue(
waitForWorkspaceCount(2, timeout: 5.0),
"Expected 2 workspaces before running Cmd+Shift+W. list=\(socketCommand("list_workspaces") ?? "<nil>")"
waitForWorkspaceCount(3, timeout: 5.0),
"Expected 3 workspaces before running Cmd+Shift+W. count=\(workspaceCount())"
)

app.typeKey("w", modifierFlags: [.command, .shift])
Expand All @@ -93,11 +99,16 @@ final class CloseWorkspacesConfirmDialogUITests: XCTestCase {
"Expected aggregated close-workspaces alert to dismiss after clicking Cancel"
)
XCTAssertTrue(
waitForWorkspaceCount(2, timeout: 5.0),
"Expected both workspaces to remain after cancelling Cmd+Shift+W multi-close. list=\(socketCommand("list_workspaces") ?? "<nil>")"
waitForWorkspaceCount(3, timeout: 5.0),
"Expected all workspaces to remain after cancelling Cmd+Shift+W multi-close. count=\(workspaceCount())"
)
}

private func configureLaunch(_ app: XCUIApplication) {
app.launchEnvironment["PROGRAMA_UI_TEST_MODE"] = "1"
app.launchEnvironment["PROGRAMA_UI_TEST_KEYEQUIV_PATH"] = countPath
}

private func ensureForegroundAfterLaunch(_ app: XCUIApplication, timeout: TimeInterval) -> Bool {
if app.wait(for: .runningForeground, timeout: timeout) {
return true
Expand All @@ -109,16 +120,6 @@ final class CloseWorkspacesConfirmDialogUITests: XCTestCase {
return false
}

private func waitForSocketPong(timeout: TimeInterval) -> Bool {
let expectation = XCTNSPredicateExpectation(
predicate: NSPredicate { _, _ in
self.socketCommand("ping") == "PONG"
},
object: NSObject()
)
return XCTWaiter().wait(for: [expectation], timeout: timeout) == .completed
}

private func waitForWorkspaceCount(_ expectedCount: Int, timeout: TimeInterval) -> Bool {
let expectation = XCTNSPredicateExpectation(
predicate: NSPredicate { _, _ in
Expand All @@ -129,48 +130,12 @@ final class CloseWorkspacesConfirmDialogUITests: XCTestCase {
return XCTWaiter().wait(for: [expectation], timeout: timeout) == .completed
}

/// Reads the workspace count the app records to `PROGRAMA_UI_TEST_KEYEQUIV_PATH`; -1 if unavailable.
private func workspaceCount() -> Int {
guard let response = socketCommand("list_workspaces") else { return -1 }
if response == "No workspaces" {
return 0
}
return response
.split(separator: "\n")
.filter { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
.count
}

private func socketCommand(_ cmd: String) -> String? {
let nc = "/usr/bin/nc"
guard FileManager.default.isExecutableFile(atPath: nc) else { return nil }

let proc = Process()
proc.executableURL = URL(fileURLWithPath: nc)
proc.arguments = ["-U", socketPath, "-w", "2"]

let inPipe = Pipe()
let outPipe = Pipe()
let errPipe = Pipe()
proc.standardInput = inPipe
proc.standardOutput = outPipe
proc.standardError = errPipe

do {
try proc.run()
} catch {
return nil
}

if let data = (cmd + "\n").data(using: .utf8) {
inPipe.fileHandleForWriting.write(data)
}
inPipe.fileHandleForWriting.closeFile()

proc.waitUntilExit()

let outData = outPipe.fileHandleForReading.readDataToEndOfFile()
guard let outStr = String(data: outData, encoding: .utf8) else { return nil }
return outStr.trimmingCharacters(in: .whitespacesAndNewlines)
guard let data = try? Data(contentsOf: URL(fileURLWithPath: countPath)),
let object = try? JSONSerialization.jsonObject(with: data) as? [String: String],
let raw = object["workspaceCount"], let count = Int(raw) else { return -1 }
return count
}

private func isCloseWorkspacesAlertPresent(app: XCUIApplication) -> Bool {
Expand Down
Loading
Loading