From 5ea7ca9333c3f16dda9a248d2ec5e7d91c442619 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Tue, 22 Sep 2026 05:05:39 +0700 Subject: [PATCH 1/5] fix(datagrid): show the filter bar's first row when the bar opens --- TablePro/Views/Filter/FilterPanelView.swift | 8 ++++++++ TablePro/Views/Main/Child/MainEditorContentView.swift | 1 + 2 files changed, 9 insertions(+) diff --git a/TablePro/Views/Filter/FilterPanelView.swift b/TablePro/Views/Filter/FilterPanelView.swift index 09342c2266..f955415b85 100644 --- a/TablePro/Views/Filter/FilterPanelView.swift +++ b/TablePro/Views/Filter/FilterPanelView.swift @@ -8,6 +8,14 @@ import TableProPluginKit struct FilterPanelView: View { @ObservedObject var coordinator: MainContentCoordinator + /// The panel draws `coordinator.selectedTabFilterState`, which lives on `QueryTabManager.tabs` + /// and not on the coordinator, so the store that publishes it has to be named here. Without it + /// SwiftUI compares this view's own stored properties, finds them unchanged and skips `body`: + /// the row `onAppear` adds reached the model and never reached the screen, so ⌘⇧F opened an + /// empty bar with nothing to type into and the keystrokes went to the object list instead. That + /// is what the two closures this view used to carry were hiding, because a closure is never + /// equal to another one and forced a re-evaluation on every parent render. (#3026) + @ObservedObject var tabManager: QueryTabManager let columns: [String] let primaryKeyColumn: String? let databaseType: DatabaseType diff --git a/TablePro/Views/Main/Child/MainEditorContentView.swift b/TablePro/Views/Main/Child/MainEditorContentView.swift index 4cccadc991..96a5e6b017 100644 --- a/TablePro/Views/Main/Child/MainEditorContentView.swift +++ b/TablePro/Views/Main/Child/MainEditorContentView.swift @@ -889,6 +889,7 @@ struct MainEditorContentView: View { } else { FilterPanelView( coordinator: coordinator, + tabManager: tabManager, columns: rows.columns, primaryKeyColumn: changeManager.primaryKeyColumn, databaseType: connection.type, From fd65ea0c41fa92a728e878bc3f855b882a935bd0 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Tue, 22 Sep 2026 05:05:39 +0700 Subject: [PATCH 2/5] fix(hig): name the trailing pane's commands menu for VoiceOver --- .../RowInspector/TrailingPaneHeaderView.swift | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/TablePro/Views/RowInspector/TrailingPaneHeaderView.swift b/TablePro/Views/RowInspector/TrailingPaneHeaderView.swift index a17914e6f7..0f544c4ba3 100644 --- a/TablePro/Views/RowInspector/TrailingPaneHeaderView.swift +++ b/TablePro/Views/RowInspector/TrailingPaneHeaderView.swift @@ -91,8 +91,18 @@ internal struct TrailingPaneHeaderView: View { } } - /// The label is a `Label` with its title hidden, not an image with an accessibility label on the - /// menu: `.accessibilityLabel` on a `Menu` replaces the name its label provides with nothing. + /// The ellipsis draws no text, so the only name VoiceOver has for this control is the one the + /// accessibility modifiers give it, and it takes all three of them. + /// + /// Measured on macOS 27, naming it through the label does not work in any arrangement. A `Label` + /// whose title `.labelStyle(.iconOnly)` has resolved away carries no name, whichever view the + /// style sits on, and `.accessibilityLabel` inside the label closure, which is how + /// `ResultSetMenu` names a pull-down that also draws text, does not reach this one either. + /// `.accessibilityLabel` on the `Menu` alone is likewise ignored. In all three the control falls + /// back to AppKit's own name for an unnamed pull-down, which reads "More" locally and the empty + /// string on the CI runner. Only `.accessibilityElement` publishes the name, and it has to be + /// `.contain`: `.ignore` names the button and then takes the menu's own items out of the tree + /// with it, so nothing can reach Fields, JSON or any other command in it. private func menu(_ model: TrailingPaneHeaderModel) -> some View { Menu { ForEach(Array(model.menuSections.enumerated()), id: \.element) { index, section in @@ -115,6 +125,9 @@ internal struct TrailingPaneHeaderView: View { .menuIndicator(.hidden) .frame(width: 24, height: 22) .help(model.menuLabel) + .accessibilityElement(children: .contain) + .accessibilityLabel(model.menuLabel) + .accessibilityAddTraits(.isButton) .accessibilityIdentifier("trailing-pane-menu") } } From 8191e5bb65bbe28a9eb34c7175952fb61438f44b Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Tue, 22 Sep 2026 05:05:39 +0700 Subject: [PATCH 3/5] test(datagrid): assert the first load's rebuilt browse query, not its text --- .../Views/Main/DefaultSortInitialQueryTests.swift | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/TableProTests/Views/Main/DefaultSortInitialQueryTests.swift b/TableProTests/Views/Main/DefaultSortInitialQueryTests.swift index fd425d1769..f07aa8fc41 100644 --- a/TableProTests/Views/Main/DefaultSortInitialQueryTests.swift +++ b/TableProTests/Views/Main/DefaultSortInitialQueryTests.swift @@ -83,28 +83,32 @@ struct DefaultSortInitialQueryTests { SchemaColumnStore.Entry(columns: ["message", "level"], primaryKeys: [], columnTypes: [:]), for: coordinator.schemaColumnsKey("logs", scope: coordinator.selectedTabScope) ) - let originalQuery = tabManager.tabs[index].content.query await withDefaultSortBehavior(.primaryKey) { let ready = await coordinator.prepareTableTabFirstLoad(tabId: tabManager.tabs[index].id) #expect(ready) } - #expect(tabManager.tabs[index].content.query == originalQuery) + let query = tabManager.tabs[index].content.query + #expect(!query.localizedCaseInsensitiveContains("ORDER BY")) + #expect(query.contains("`logs`")) + #expect(query.contains("LIMIT \(tabManager.tabs[index].pagination.pageSize)")) #expect(!tabManager.tabs[index].sortState.isSorting) } @Test("Schema fetch failure dispatches unsorted instead of blocking the first load") func schemaFetchFailureStillDispatches() async { let (coordinator, tabManager, index) = makeCoordinator(tableName: "users") - let originalQuery = tabManager.tabs[index].content.query await withDefaultSortBehavior(.primaryKey) { let ready = await coordinator.prepareTableTabFirstLoad(tabId: tabManager.tabs[index].id) #expect(ready) } - #expect(tabManager.tabs[index].content.query == originalQuery) + let query = tabManager.tabs[index].content.query + #expect(!query.localizedCaseInsensitiveContains("ORDER BY")) + #expect(query.contains("`users`")) + #expect(!tabManager.tabs[index].sortState.isSorting) } @Test("None behavior takes the fast path and regenerates the browse query from current state") From 4d590dff2bb1ad2acb3b0e4f91c2457bd1f14df8 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Tue, 22 Sep 2026 05:05:40 +0700 Subject: [PATCH 4/5] test(ssh): drop the cooperative pool blocking test's wall clock --- .../CooperativePoolBlockingTests.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Packages/TableProCore/Tests/TableProCoreTypesTests/CooperativePoolBlockingTests.swift b/Packages/TableProCore/Tests/TableProCoreTypesTests/CooperativePoolBlockingTests.swift index 51c2197a90..c7501a5894 100644 --- a/Packages/TableProCore/Tests/TableProCoreTypesTests/CooperativePoolBlockingTests.swift +++ b/Packages/TableProCore/Tests/TableProCoreTypesTests/CooperativePoolBlockingTests.swift @@ -22,10 +22,14 @@ struct CooperativePoolBlockingTests { private static var blockerCount: Int { ProcessInfo.processInfo.activeProcessorCount + 4 } private static let blockMilliseconds = 300 + /// The high-water mark is the whole assertion, because `blockerCount` callers measured inside the + /// blocking call at one instant *is* "every caller at once". A wall clock cannot add to that and + /// only reports the host: libdispatch brings its worker threads up one at a time, so on a loaded + /// CI runner the group took 2.00s and 2.26s against a 0.9s budget while the peak was the full + /// `blockerCount` both times. The companion test below is a peak assertion for the same reason. @Test("Blocking through its own queue runs every caller at once") func runsEveryBlockerConcurrently() async throws { let peak = ConcurrencyPeak() - let started = Date() try await withThrowingTaskGroup(of: Void.self) { group in for index in 0 ..< Self.blockerCount { @@ -44,9 +48,7 @@ struct CooperativePoolBlockingTests { try await group.waitForAll() } - let elapsed = Date().timeIntervalSince(started) #expect(peak.highWaterMark == Self.blockerCount) - #expect(elapsed < Double(Self.blockMilliseconds * 3) / 1_000) } @Test("Blocking a detached task instead caps at the cooperative pool width") From a58d60998ef1208ed96e2e7c12c642db4ab52918 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Tue, 22 Sep 2026 05:05:40 +0700 Subject: [PATCH 5/5] ci(ios): run the simulator suite on one device instead of clones --- .github/workflows/ios-tests.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ios-tests.yml b/.github/workflows/ios-tests.yml index f377564e2a..6a9479f3ef 100644 --- a/.github/workflows/ios-tests.yml +++ b/.github/workflows/ios-tests.yml @@ -135,6 +135,11 @@ jobs: -scheme "$XCODE_SCHEME" \ -skipPackagePluginValidation + # One simulator, not clones. TableProMobileTests is one bundle and xcodebuild never split it: + # every test in every run so far, passing and failing, executed on "Clone 1 of iPhone 17 Pro". + # The extra clone only booted, and on a 3-core runner booting a second iOS 26 simulator is what + # killed three of the last twelve runs with "Failed to launch app ... (ipc/mig) server died" + # while Clone 1 had already finished the suite. - name: Run unit tests run: | set -o pipefail @@ -143,6 +148,7 @@ jobs: -scheme "$XCODE_SCHEME" \ -destination "$TEST_DESTINATION" \ -only-testing:TableProMobileTests \ + -parallel-testing-enabled NO \ -skipPackagePluginValidation \ -resultBundlePath TestResults.xcresult \ CODE_SIGNING_ALLOWED=NO \