diff --git a/macapp/WorkroomApp/Core/Terminal/TerminalPanes.swift b/macapp/WorkroomApp/Core/Terminal/TerminalPanes.swift index e6a5df12..bfa2541c 100644 --- a/macapp/WorkroomApp/Core/Terminal/TerminalPanes.swift +++ b/macapp/WorkroomApp/Core/Terminal/TerminalPanes.swift @@ -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) diff --git a/macapp/WorkroomAppTests/NotificationRoutingTests.swift b/macapp/WorkroomAppTests/NotificationRoutingTests.swift index 97b701ed..09c302ff 100644 --- a/macapp/WorkroomAppTests/NotificationRoutingTests.swift +++ b/macapp/WorkroomAppTests/NotificationRoutingTests.swift @@ -1,3 +1,4 @@ +import AppKit import WorkroomDomain import XCTest @@ -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") + } } diff --git a/macapp/WorkroomAppUITests/WorkroomWorkflowUITests.swift b/macapp/WorkroomAppUITests/WorkroomWorkflowUITests.swift index e32cbecd..bdc55b15 100644 --- a/macapp/WorkroomAppUITests/WorkroomWorkflowUITests.swift +++ b/macapp/WorkroomAppUITests/WorkroomWorkflowUITests.swift @@ -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.