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
3 changes: 2 additions & 1 deletion macapp/WorkroomApp/Core/Terminal/TerminalPanes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2572,7 +2572,8 @@ final class TerminalPanes {
/// not in a window yet: the focus change is still being rendered (#598). A surface becoming first
/// responder then was not clicked, since the pane the user sees is about to be replaced. It is
/// AppKit handing the window's first responder back to it, as a closing popover does (the
/// diagnosis popover's Investigate opens a tab and closes the popover in one action), and taking
/// diagnosis popover's Investigate opens a tab and closes the popover in one action; so does a
/// bell row opening a notification from another tab of the workroom in front, #607), and taking
/// it as focus would put the old tab back over the new one. A content tab has no surface, so a
/// click into a terminal beside a focused diff still focuses the terminal.
private func isFocusBeingShown(otherThan tabID: TerminalTab.ID, in target: TerminalTarget)
Expand Down
76 changes: 76 additions & 0 deletions macapp/WorkroomAppTests/NotificationRoutingTests.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import AppKit
import WorkroomDomain
import XCTest

Expand Down Expand Up @@ -61,4 +62,79 @@ final class NotificationRoutingTests: XCTestCase {
XCTAssertEqual(p.userInfo["tabID"], tab.uuidString)
XCTAssertEqual(p.userInfo["notifID"], notifID.uuidString)
}

// MARK: Opening a notification (WindowRegistry.open, #607)

private let sid = SidebarID.workroom(project: "/p", name: "a")

/// A window showing workroom `a` with two terminals, the second in front, registered with a fresh
/// registry (never `.shared`) whose notification list the window shares.
@MainActor
private func makeWindow() -> (WindowRegistry, AppStore, TerminalTarget, TerminalTab, TerminalTab)
{
let registry = WindowRegistry()
let projects = ProjectStore()
projects.projects = [
Project(
path: "/p", vcs: "git",
workrooms: [Workroom(name: "a", path: "/p/a", vcsName: "workroom/a", warnings: [])])
]
let store = AppStore(projectStore: projects, notifications: registry.notifications)
store.services.notifier = SilentNotifier()
store.confirmOnCloseOverrideForTesting = false
isolate(store.terminals, service: isolatedPaneSessionService())
registry.register(window: NSWindow(), store: store)
store.selectedTargetID = sid
let t = store.target(for: sid)!
let first = store.terminals.addTab(for: t)
let second = store.terminals.addTab(for: t)
return (registry, store, t, first, second)
}

/// Value: protects=opening a notification from a background tab of the workroom in front selects
/// that tab, though the workroom is already selected (#607); fails_when=the pane that was in front
/// takes focus back as the bell's popover closes, which hands the window's first responder back
/// to it; why_new=only cross-workroom opens were pinned, where the workroom switch alone shows the
/// routing worked; seam=the `.focused` a surface's becomeFirstResponder raises, sent with no window
/// (as `RunCommandTests` does for #598); no_claim=the popover close itself
/// (`WorkroomWorkflowUITests` clicks the bell's row).
@MainActor
func testOpeningANotificationSelectsTheBackgroundTabThatRaisedIt() throws {
let (registry, store, t, first, second) = makeWindow()
XCTAssertEqual(store.terminals.focusedTab(for: t)?.id, second.id)
let note = try XCTUnwrap(
registry.notifications.record(
targetID: t.id, tabID: first.id, activity: .osc(title: "done", body: nil), focused: false))

registry.open(note)
XCTAssertEqual(store.terminals.focusedTab(for: t)?.id, first.id, "the tab that raised it")
// The popover closing gives the window's first responder back to the pane that was in front.
try XCTUnwrap(store.terminals.surface(second)).onEvent?(.focused)

XCTAssertEqual(store.selectedTargetID, sid)
XCTAssertEqual(
store.terminals.focusedTab(for: t)?.id, first.id,
"the tab that raised it keeps focus while it is still being shown")
XCTAssertTrue(registry.notifications.items.isEmpty, "opening dismisses it")
}

/// Value: protects=a notification whose tab has since closed still opens its workroom, on the tab
/// in front there (#607's fallback); fails_when=open does nothing, or leaves the row; why_new=the
/// existing fallback test opens a workroom with no tabs; seam=none; no_claim=window raise.
@MainActor
func testANotificationWhoseTabClosedOpensItsWorkroomOnTheTabInFront() throws {
let (registry, store, t, first, second) = makeWindow()
let note = try XCTUnwrap(
registry.notifications.record(
targetID: t.id, tabID: second.id, activity: .osc(title: "done", body: nil), focused: false))
store.terminals.focus(first.id, for: t)
store.terminals.closeTab(second.id, for: t)
store.selectedTargetID = nil

registry.open(note)

XCTAssertEqual(store.selectedTargetID, sid, "its workroom opens")
XCTAssertEqual(store.terminals.focusedTab(for: t)?.id, first.id)
XCTAssertTrue(registry.notifications.items.isEmpty, "opening dismisses it")
}
}
46 changes: 46 additions & 0 deletions macapp/WorkroomAppUITests/WorkroomWorkflowUITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,52 @@ final class WorkroomWorkflowUITests: XCTestCase {
"opening the notification should have dismissed it")
}

/// A terminal tab by its chip's accessibility id (`terminal.tab.<title>`).
private func terminalChip(_ app: XCUIApplication, _ title: String) -> XCUIElement {
app.descendants(matching: .any).matching(identifier: "terminal.tab.\(title)").firstMatch
}

private func poll(timeout: TimeInterval, until condition: () -> Bool) -> Bool {
let deadline = Date().addingTimeInterval(timeout)
while Date() < deadline {
if condition() { return true }
RunLoop.current.run(until: Date().addingTimeInterval(0.25))
}
return condition()
}

/// A notification raised by a background tab of the workroom in front opens THAT tab from the
/// bell (#607): the workroom is already selected, so only the tab switch shows the routing worked.
/// Terminal 1 raises it after a second, by which time ⌘T has put a new tab in front.
func testABellRowFromAnotherTabOfTheSameWorkroomSelectsThatTab() throws {
let app = launchedApp()
let first = terminalChip(app, "Terminal 1")
XCTAssertTrue(first.waitForExistence(timeout: 15), "the fixture workroom has a terminal")
first.click()
XCTAssertTrue(poll(timeout: 5) { first.isSelected })
let bell = app.buttons["activityBar.notifications"]
XCTAssertTrue(bell.waitForExistence(timeout: 10))
let unreadBefore = bell.label

app.typeText("sleep 1; printf '\\e]9;Same Workroom Ping\\a'\r")
app.typeKey("t", modifierFlags: .command)
XCTAssertTrue(poll(timeout: 5) { !first.isSelected }, "⌘T puts a new tab in front")
XCTAssertTrue(
poll(timeout: 10) { bell.label != unreadBefore }, "Terminal 1's notification arrives")

bell.click()
let popover = app.popovers.firstMatch
XCTAssertTrue(popover.waitForExistence(timeout: 4), "the bell opens its popover")
let row = popover.buttons.matching(NSPredicate(format: "label CONTAINS 'Same Workroom Ping'"))
.firstMatch
XCTAssertTrue(row.waitForExistence(timeout: 4), "the notification is listed")
row.click()

XCTAssertTrue(
poll(timeout: 5) { first.isSelected },
"opening the notification selects Terminal 1, the tab that raised it")
}

// "Deleting a workroom withdraws its notifications" is covered by
// `AppStoreDeleteRaceTests.testDeletingAWorkroomWithdrawsItsNotifications` (a unit test, not
// here): fixture-mode workrooms are never registered with the real CLI, so `deleteWorkroom`'s
Expand Down
Loading