From 2c54a3342800f24e08a62e91a7e6c256953e218a Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Fri, 16 Oct 2026 11:43:37 +0700 Subject: [PATCH] refactor(coordinator): move browse cursor sync out of the coordinator hub --- .github/hub-ratchet-baseline.json | 8 +- .../BrowseCursorCoordinator.swift | 95 +++++++++++++++ .../Infrastructure/WorkspaceCloseAction.swift | 2 +- .../WorkspaceRailViewController.swift | 4 +- .../MainContentCoordinator+Navigation.swift | 20 ++-- ...nContentCoordinator+VisibleContainer.swift | 108 ------------------ ...inContentCoordinator+WindowLifecycle.swift | 2 +- .../MainContentCommandActions+BulkClose.swift | 2 +- .../Views/Main/MainContentCoordinator.swift | 13 +-- .../VisibleContainerFollowingTests.swift | 50 ++++---- 10 files changed, 145 insertions(+), 159 deletions(-) create mode 100644 TablePro/Core/Coordinators/BrowseCursorCoordinator.swift delete mode 100644 TablePro/Views/Main/Extensions/MainContentCoordinator+VisibleContainer.swift diff --git a/.github/hub-ratchet-baseline.json b/.github/hub-ratchet-baseline.json index db770637ce..5cd6ad237c 100644 --- a/.github/hub-ratchet-baseline.json +++ b/.github/hub-ratchet-baseline.json @@ -5,7 +5,7 @@ "TabType cases": 9, "SettingsPane cases": 12, "ActiveSheet cases": 17, - "LaunchIntent cases": 16, + "LaunchIntent cases": 15, "shell extensions named for a module or engine": 11, "MainContentCoordinator+ files": 67, "app types AppDelegate names": 65, @@ -18,14 +18,14 @@ "files under TablePro/ named for Dameng": 0, "files under TablePro/ named for Databend": 0, "files under TablePro/ named for DuckDB": 2, - "files under TablePro/ named for DynamoDB": 3, + "files under TablePro/ named for DynamoDB": 2, "files under TablePro/ named for Elasticsearch": 1, "files under TablePro/ named for etcd": 2, "files under TablePro/ named for HANA": 1, "files under TablePro/ named for Kafka": 1, "files under TablePro/ named for libSQL": 0, "files under TablePro/ named for MariaDB": 0, - "files under TablePro/ named for MongoDB": 8, + "files under TablePro/ named for MongoDB": 5, "files under TablePro/ named for MySQL": 7, "files under TablePro/ named for OceanBase": 0, "files under TablePro/ named for Oracle": 1, @@ -36,7 +36,7 @@ "files under TablePro/ named for Snowflake": 0, "files under TablePro/ named for Spanner": 0, "files under TablePro/ named for SQL Server": 2, - "files under TablePro/ named for SQLite": 10, + "files under TablePro/ named for SQLite": 8, "files under TablePro/ named for SurrealDB": 1, "files under TablePro/ named for Teradata": 0, "files under TablePro/ named for TiDB": 0, diff --git a/TablePro/Core/Coordinators/BrowseCursorCoordinator.swift b/TablePro/Core/Coordinators/BrowseCursorCoordinator.swift new file mode 100644 index 0000000000..cbeb72a290 --- /dev/null +++ b/TablePro/Core/Coordinators/BrowseCursorCoordinator.swift @@ -0,0 +1,95 @@ +// +// BrowseCursorCoordinator.swift +// TablePro +// + +import Foundation + +/// Keeps the strip entry a window shows and the database its connection browses together. A switch +/// from the toolbar, sidebar or strip moves the cursor and the window follows once it lands; picking a +/// tab of another entry moves the window first and the cursor follows. +@MainActor +final class BrowseCursorCoordinator { + unowned let parent: MainContentCoordinator + /// Switches queue on the driver, so one landing after a newer request must not bring back the + /// entry the user already left. + private(set) var pendingRequest: Int? + private var requestCount = 0 + /// Not asked for again on activation: dismissing the error sheet makes the window key. + var failedFollow: String? + + init(parent: MainContentCoordinator) { + self.parent = parent + } + + var browsedContainer: String? { + guard let session = parent.services.databaseManager.session(for: parent.connectionId) else { return nil } + return WorkspaceAnchoring.browsedContainer(of: session, target: parent.tabManager.containerTarget) + } + + /// Two windows of a torn-off connection share one cursor, and only the frontmost follows it. Nil + /// from the window manager means no window hosts this coordinator yet. + private var followsBrowseCursor: Bool { + guard let frontmost = WindowManager.shared.frontmostCoordinator(for: parent.connectionId) else { return true } + return frontmost === parent + } + + func beginRequest() -> Int { + requestCount &+= 1 + pendingRequest = requestCount + return requestCount + } + + func endRequest(_ request: Int) -> Bool { + guard pendingRequest == request else { return false } + pendingRequest = nil + return true + } + + func showBrowsedContainer() { + parent.tabManager.show(container: browsedContainer) + } + + /// A move from outside this window. One arriving during this window's own move belongs to an + /// earlier switch. + func browseContainerDidChange() { + guard pendingRequest == nil, followsBrowseCursor else { return } + showBrowsedContainer() + } + + /// A pending switch counts even before the cursor moves: picking a tab of the database being + /// left has to win over it. The saved default database stays, since picking a tab is not + /// choosing one. + func followVisibleContainer() { + guard let container = parent.tabManager.visibleContainer, + container != browsedContainer || pendingRequest != nil, + parent.services.databaseManager.session(for: parent.connectionId)?.isConnected == true, + followsBrowseCursor + else { return } + moveCursor(to: container, persist: false) + } + + func followVisibleContainerOnActivation() { + guard parent.tabManager.visibleContainer != failedFollow else { return } + followVisibleContainer() + } + + func goToEntry(_ container: String) { + parent.tabManager.show(container: container) + guard container != browsedContainer || pendingRequest != nil else { return } + moveCursor(to: container, persist: true) + } + + /// Claimed before the task starts, so a selection made in between sees it in flight and + /// supersedes it. + private func moveCursor(to container: String, persist: Bool) { + failedFollow = nil + let request = beginRequest() + Task { @MainActor [weak self, weak parent] in + guard let self, let parent, self.pendingRequest == request else { return } + await parent.switchContainer(to: container, persist: persist) + guard self.browsedContainer != container, self.pendingRequest == nil else { return } + self.failedFollow = container + } + } +} diff --git a/TablePro/Core/Services/Infrastructure/WorkspaceCloseAction.swift b/TablePro/Core/Services/Infrastructure/WorkspaceCloseAction.swift index 9fadc533c9..c866dd36be 100644 --- a/TablePro/Core/Services/Infrastructure/WorkspaceCloseAction.swift +++ b/TablePro/Core/Services/Infrastructure/WorkspaceCloseAction.swift @@ -317,7 +317,7 @@ internal enum WorkspaceCloseAction { /// on an entry the strip no longer lists. private static func leave(_ workspace: WorkspaceID, in coordinators: [MainContentCoordinator]) { for coordinator in coordinators where coordinator.tabManager.visibleContainer == workspace.container { - coordinator.showBrowsedContainer() + coordinator.browseCursorCoordinator.showBrowsedContainer() } } diff --git a/TablePro/Core/Services/Infrastructure/WorkspaceRailViewController.swift b/TablePro/Core/Services/Infrastructure/WorkspaceRailViewController.swift index 2a99991286..2612ff72ca 100644 --- a/TablePro/Core/Services/Infrastructure/WorkspaceRailViewController.swift +++ b/TablePro/Core/Services/Infrastructure/WorkspaceRailViewController.swift @@ -544,7 +544,7 @@ internal final class WorkspaceRailViewController: NSViewController { } /// Going to an entry of a connection: its tabs come forward, and the object tree follows to its - /// container once the switch lands. `goToEntry` holds both halves, so the order cannot be got + /// container once the switch lands. `BrowseCursorCoordinator.goToEntry` holds both halves, so the order cannot be got /// wrong here. private func moveBrowseCursor(of window: NSWindow, to workspace: WorkspaceID) { guard !workspace.container.isEmpty else { @@ -566,7 +566,7 @@ internal final class WorkspaceRailViewController: NSViewController { return } Self.logger.info("moveBrowseCursor to=\(Self.describe(workspace), privacy: .public)") - coordinator.goToEntry(workspace.container) + coordinator.browseCursorCoordinator.goToEntry(workspace.container) } /// The menu command is a commit, not a highlight: it names the workspace it goes to, so it diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift index 020aa9d119..869a539c97 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift @@ -466,11 +466,11 @@ extension MainContentCoordinator { /// `persist` records the database as the connection's saved default. @discardableResult func switchDatabase(to database: String, persist: Bool = true) async -> Bool { - let request = beginCursorRequest() + let request = browseCursorCoordinator.beginRequest() do { try await DatabaseManager.shared.switchDatabase(to: database, for: connectionId, persist: persist) - if endCursorRequest(request) { - showBrowsedContainer() + if browseCursorCoordinator.endRequest(request) { + browseCursorCoordinator.showBrowsedContainer() } toolbarState.currentDatabase = database toolbarState.currentSchema = DatabaseManager.shared.session(for: connectionId)?.browseSchema @@ -481,7 +481,7 @@ extension MainContentCoordinator { syncSidebarObjectSelection() return true } catch { - _ = endCursorRequest(request) + _ = browseCursorCoordinator.endRequest(request) navigationLogger.error("Failed to switch database: \(error.publicLogShape, privacy: .public)") /// A user who dismissed the password prompt already knows why nothing happened, and /// telling them their own decision failed is noise, not news. @@ -541,7 +541,7 @@ extension MainContentCoordinator { let session = services.databaseManager.session(for: connectionId) /// A switch of this window's own still on its way to somewhere else would land after this /// one and take the window there, so being on the target already is not enough. - let isSettled = pendingCursorRequest == nil + let isSettled = browseCursorCoordinator.pendingRequest == nil switch step { case .database(let name): guard name != session?.resolvedBrowseDatabase || !isSettled else { continue } @@ -557,7 +557,7 @@ extension MainContentCoordinator { } /// Already there is still a request to be there: the window may be showing another entry. if !didSwitch { - showBrowsedContainer() + browseCursorCoordinator.showBrowsedContainer() } } @@ -587,15 +587,15 @@ extension MainContentCoordinator { let previousSchema = toolbarState.currentSchema toolbarState.currentSchema = schema - let request = beginCursorRequest() + let request = browseCursorCoordinator.beginRequest() do { try await DatabaseManager.shared.switchSchema(to: schema, for: connectionId) - if endCursorRequest(request) { - showBrowsedContainer() + if browseCursorCoordinator.endRequest(request) { + browseCursorCoordinator.showBrowsedContainer() } syncSidebarObjectSelection() } catch { - _ = endCursorRequest(request) + _ = browseCursorCoordinator.endRequest(request) /// A switch that waited for the driver is dropped when the connection was closed and /// opened again before its turn. The toolbar now belongs to that new session, so it is /// read back from it rather than restored to what the old one showed, and nothing failed diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+VisibleContainer.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+VisibleContainer.swift deleted file mode 100644 index 3ffd745b03..0000000000 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+VisibleContainer.swift +++ /dev/null @@ -1,108 +0,0 @@ -// -// MainContentCoordinator+VisibleContainer.swift -// TablePro -// - -import Foundation - -/// Keeps the entry a window shows and the database its connection browses together. -/// -/// The two move from different places. A switch from the toolbar, the sidebar or the strip moves -/// the browse cursor and the window shows that entry once it lands. Selecting a tab of another entry -/// moves the window first, because the tab is already on screen, and the cursor follows. -extension MainContentCoordinator { - /// The entry the connection is browsing, named the way this engine names entries. - var browsedContainer: String? { - guard let session = services.databaseManager.session(for: connectionId) else { return nil } - return WorkspaceAnchoring.browsedContainer(of: session, target: tabManager.containerTarget) - } - - var newTabDatabaseName: String { - tabManager.openingDatabase(browsing: browseDatabaseName) - } - - /// Whether this window follows a cursor that something else moved. A connection a tab was torn - /// off from is hosted by two windows that share one cursor, and only the one in front follows, - /// so the window behind keeps the tabs it shows. Nil from the window manager means no window - /// hosts this coordinator yet, and it is the only one there is. - var followsBrowseCursor: Bool { - guard let frontmost = WindowManager.shared.frontmostCoordinator(for: connectionId) else { return true } - return frontmost === self - } - - func beginCursorRequest() -> Int { - cursorRequestCount &+= 1 - pendingCursorRequest = cursorRequestCount - return cursorRequestCount - } - - /// Whether the move that just finished is still the latest one this window asked for. - func endCursorRequest(_ request: Int) -> Bool { - guard pendingCursorRequest == request else { return false } - pendingCursorRequest = nil - return true - } - - func showBrowsedContainer() { - tabManager.show(container: browsedContainer) - } - - /// A cursor something outside this window moved: an MCP client, a script, or the other window - /// of a torn-off tab. A move of this window's own is applied where it lands instead, and an - /// event that arrives while one is under way belongs to an earlier switch. - func browseContainerDidChange() { - guard pendingCursorRequest == nil, followsBrowseCursor else { return } - showBrowsedContainer() - } - - /// The selection took the window to another entry, so the connection browses it too. - /// - /// The saved default database stays: picking a tab is not choosing a database. A switch that - /// fails leaves the entry on screen with the error, because its tabs are still the user's work - /// and a database that cannot be browsed is no reason to hide them. - /// - /// A switch still on its way counts even when the cursor has not moved yet: picking a tab of the - /// database being left, before the switch away lands, has to win over it. - /// - /// A window that is not the connection's frontmost does not move the cursor; it does once it - /// becomes key. - func followVisibleContainer() { - guard let container = tabManager.visibleContainer, - container != browsedContainer || pendingCursorRequest != nil, - services.databaseManager.session(for: connectionId)?.isConnected == true, - followsBrowseCursor - else { return } - moveCursor(to: container, persist: false) - } - - /// Claimed before the task is scheduled, so a selection made before it starts sees it in flight - /// and supersedes it, and a superseded task never switches at all. A failure is remembered so the - /// window coming back to the front, which is what dismissing the error does, does not ask again. - private func moveCursor(to container: String, persist: Bool) { - failedCursorFollow = nil - let request = beginCursorRequest() - Task { @MainActor [weak self] in - guard let self, self.pendingCursorRequest == request else { return } - await self.switchContainer(to: container, persist: persist) - guard self.browsedContainer != container, self.pendingCursorRequest == nil else { return } - self.failedCursorFollow = container - } - } - - /// Two windows of one connection share its cursor, so the one the user comes back to may show an - /// entry the other window has since moved the cursor away from. The window in front decides what - /// the connection browses. A container whose switch just failed is not asked for again on every - /// activation: dismissing the error makes the window key, and retrying there raised it again. - func followVisibleContainerOnActivation() { - guard tabManager.visibleContainer != failedCursorFollow else { return } - followVisibleContainer() - } - - /// Going to one of this connection's strip entries. Its tabs are this window's own, so they come - /// forward at once, and the object browser follows once the switch lands. - func goToEntry(_ container: String) { - tabManager.show(container: container) - guard container != browsedContainer || pendingCursorRequest != nil else { return } - moveCursor(to: container, persist: true) - } -} diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+WindowLifecycle.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+WindowLifecycle.swift index 35f7a36a5e..f70e40665c 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+WindowLifecycle.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+WindowLifecycle.swift @@ -36,7 +36,7 @@ extension MainContentCoordinator { consumeDeferredRestoreLoadIfNeeded() refreshSourceFileDiskChanges() - followVisibleContainerOnActivation() + browseCursorCoordinator.followVisibleContainerOnActivation() syncSidebarObjectSelection() announceActiveTabToVoiceOver() diff --git a/TablePro/Views/Main/MainContentCommandActions+BulkClose.swift b/TablePro/Views/Main/MainContentCommandActions+BulkClose.swift index 7e8a549232..4bb78064c0 100644 --- a/TablePro/Views/Main/MainContentCommandActions+BulkClose.swift +++ b/TablePro/Views/Main/MainContentCommandActions+BulkClose.swift @@ -49,7 +49,7 @@ extension MainContentCommandActions { /// The entry this window shows, named the way this engine names containers. Comparing a /// schema-switching engine's tabs against a database name classified nothing. var shownContainerName: String { - coordinator?.tabManager.visibleContainer ?? coordinator?.browsedContainer ?? "" + coordinator?.tabManager.visibleContainer ?? coordinator?.browseCursorCoordinator.browsedContainer ?? "" } var closeTabsForOtherDatabasesTitle: String { diff --git a/TablePro/Views/Main/MainContentCoordinator.swift b/TablePro/Views/Main/MainContentCoordinator.swift index 742250ac8b..aa5a30d286 100644 --- a/TablePro/Views/Main/MainContentCoordinator.swift +++ b/TablePro/Views/Main/MainContentCoordinator.swift @@ -153,6 +153,9 @@ final class MainContentCoordinator: ObservableObject { var browseDatabaseName: String { services.databaseManager.browseDatabaseName(for: connection) } + var newTabDatabaseName: String { + tabManager.openingDatabase(browsing: browseDatabaseName) + } var safeModeLevel: SafeModeLevel { toolbarState.safeModeLevel } /// The level the toolbar shows, and the floor under it with Agent mode's included, which is what /// the Safe Mode list offers from and what the toolbar's tooltip explains. @@ -172,11 +175,6 @@ final class MainContentCoordinator: ObservableObject { let queryExecutor: QueryExecutor internal var executionGate: any ExecutionGate = ExecutionGateProvider.shared let windowSidebarState: WindowSidebarState - /// The browse-cursor move this window asked for last. Switches queue on the driver, so one that - /// lands after a newer request is stale and must not put back the entry the user already left. - internal var pendingCursorRequest: Int? - internal var cursorRequestCount = 0 - internal var failedCursorFollow: String? // MARK: - Services @@ -191,6 +189,7 @@ final class MainContentCoordinator: ObservableObject { private(set) var queryExecutionCoordinator: QueryExecutionCoordinator! private(set) var paginationCoordinator: PaginationCoordinator! private(set) var rowEditingCoordinator: RowEditingCoordinator! + private(set) lazy var browseCursorCoordinator = BrowseCursorCoordinator(parent: self) /// Stable identifier for this coordinator's window (set by MainContentView on appear) @Published var windowId: UUID? @@ -728,13 +727,13 @@ final class MainContentCoordinator: ObservableObject { changeManager.undoManagerProvider = { [weak self] in self?.contentWindow?.undoManager } changeManager.onUndoApplied = { [weak self] result in self?.handleUndoResult(result) } tabManager.onVisibleContainerReconciled = { [weak self] in - self?.followVisibleContainer() + self?.browseCursorCoordinator.followVisibleContainer() } browseContainerCancellable = services.appEvents.browseContainerChanged .filter { [connectionId = connection.id] changed in changed == connectionId } .receive(on: RunLoop.main) .sink { [weak self] _ in - self?.browseContainerDidChange() + self?.browseCursorCoordinator.browseContainerDidChange() } tabManager.onTabRetargeted = { [weak self] tabId in self?.dataTabDelegate?.tableViewCoordinator?.flushPendingColumnLayoutPersistence() diff --git a/TableProTests/Views/VisibleContainerFollowingTests.swift b/TableProTests/Views/VisibleContainerFollowingTests.swift index e9e82fc3cc..15ee6ca493 100644 --- a/TableProTests/Views/VisibleContainerFollowingTests.swift +++ b/TableProTests/Views/VisibleContainerFollowingTests.swift @@ -54,7 +54,7 @@ struct VisibleContainerFollowingTests { tabManager.selectedTabId = tenantB.id - try await waitUntil { coordinator.browsedContainer == "db_b" } + try await waitUntil { coordinator.browseCursorCoordinator.browsedContainer == "db_b" } #expect(tabManager.visibleContainer == "db_b") #expect(tabManager.selectedTabId == tenantB.id) /// Picking a tab is not choosing a database to come back to. @@ -75,7 +75,7 @@ struct VisibleContainerFollowingTests { try await waitUntil { tabManager.visibleContainer == "db_b" } #expect(tabManager.selectedTabId == tenantB.id) - #expect(coordinator.pendingCursorRequest == nil) + #expect(coordinator.browseCursorCoordinator.pendingRequest == nil) } } @@ -83,12 +83,12 @@ struct VisibleContainerFollowingTests { @Test("Only the latest move this window asked for is applied when it lands") func staleLandingIsNotApplied() async throws { try await withConnectedCoordinator { coordinator, _, _ in - let first = coordinator.beginCursorRequest() - let second = coordinator.beginCursorRequest() + let first = coordinator.browseCursorCoordinator.beginRequest() + let second = coordinator.browseCursorCoordinator.beginRequest() - #expect(!coordinator.endCursorRequest(first)) - #expect(coordinator.endCursorRequest(second)) - #expect(coordinator.pendingCursorRequest == nil) + #expect(!coordinator.browseCursorCoordinator.endRequest(first)) + #expect(coordinator.browseCursorCoordinator.endRequest(second)) + #expect(coordinator.browseCursorCoordinator.pendingRequest == nil) } } @@ -98,13 +98,13 @@ struct VisibleContainerFollowingTests { func followSupersedesAPendingSwitch() async throws { try await withConnectedCoordinator { coordinator, tabManager, _ in tabManager.tabs = [queryTab(database: "db_a")] - let earlier = coordinator.beginCursorRequest() + let earlier = coordinator.browseCursorCoordinator.beginRequest() - coordinator.followVisibleContainer() + coordinator.browseCursorCoordinator.followVisibleContainer() - try await waitUntil { coordinator.pendingCursorRequest == nil } - #expect(!coordinator.endCursorRequest(earlier)) - #expect(coordinator.browsedContainer == "db_a") + try await waitUntil { coordinator.browseCursorCoordinator.pendingRequest == nil } + #expect(!coordinator.browseCursorCoordinator.endRequest(earlier)) + #expect(coordinator.browseCursorCoordinator.browsedContainer == "db_a") } } @@ -148,13 +148,13 @@ struct VisibleContainerFollowingTests { try await withConnectedCoordinator { coordinator, tabManager, _ in tabManager.tabs = [queryTab(database: "db_a")] - coordinator.goToEntry("db_b") + coordinator.browseCursorCoordinator.goToEntry("db_b") - #expect(coordinator.browsedContainer == "db_a") + #expect(coordinator.browseCursorCoordinator.browsedContainer == "db_a") #expect(coordinator.newTabDatabaseName == "db_b") tabManager.addTab(initialQuery: "SELECT 1", databaseName: coordinator.newTabDatabaseName) #expect(tabManager.visibleContainer == "db_b") - try await waitUntil { coordinator.browsedContainer == "db_b" } + try await waitUntil { coordinator.browseCursorCoordinator.browsedContainer == "db_b" } } } @@ -165,12 +165,12 @@ struct VisibleContainerFollowingTests { try await withConnectedCoordinator { coordinator, tabManager, _ in tabManager.tabs = [queryTab(database: "db_a"), queryTab(database: "db_b")] tabManager.show(container: "db_b") - coordinator.followVisibleContainer() + coordinator.browseCursorCoordinator.followVisibleContainer() - coordinator.goToEntry("db_a") + coordinator.browseCursorCoordinator.goToEntry("db_a") - try await waitUntil { coordinator.pendingCursorRequest == nil } - #expect(coordinator.browsedContainer == "db_a") + try await waitUntil { coordinator.browseCursorCoordinator.pendingRequest == nil } + #expect(coordinator.browseCursorCoordinator.browsedContainer == "db_a") #expect(tabManager.visibleContainer == "db_a") } } @@ -182,9 +182,9 @@ struct VisibleContainerFollowingTests { tabManager.tabs = [queryTab(database: "db_b")] tabManager.show(container: "db_b") - coordinator.followVisibleContainerOnActivation() + coordinator.browseCursorCoordinator.followVisibleContainerOnActivation() - try await waitUntil { coordinator.browsedContainer == "db_b" } + try await waitUntil { coordinator.browseCursorCoordinator.browsedContainer == "db_b" } } } @@ -194,12 +194,12 @@ struct VisibleContainerFollowingTests { try await withConnectedCoordinator { coordinator, tabManager, _ in tabManager.tabs = [queryTab(database: "db_b")] tabManager.show(container: "db_b") - coordinator.failedCursorFollow = "db_b" + coordinator.browseCursorCoordinator.failedFollow = "db_b" - coordinator.followVisibleContainerOnActivation() + coordinator.browseCursorCoordinator.followVisibleContainerOnActivation() - #expect(coordinator.pendingCursorRequest == nil) - #expect(coordinator.browsedContainer == "db_a") + #expect(coordinator.browseCursorCoordinator.pendingRequest == nil) + #expect(coordinator.browseCursorCoordinator.browsedContainer == "db_a") } }