From bde8a84699cc5e64f70c3e61ce2f3d1b25f4550e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Thu, 4 Jun 2026 12:04:00 +0700 Subject: [PATCH] fix(datagrid): keep status bar controls clear of the window resize corner (#1569) --- CHANGELOG.md | 4 + TablePro/Models/Query/StatusBarSnapshot.swift | 59 ++++++ .../Components/PaginationControlsView.swift | 11 +- .../Main/Child/MainEditorContentView.swift | 39 ++-- .../Views/Main/Child/MainStatusBarView.swift | 180 +++++++----------- .../Child/StatusBarSnapshot+RowInfo.swift | 38 ++++ .../Results/ColumnVisibilityPopover.swift | 15 +- .../Models/StatusBarSnapshotTests.swift | 104 ++++++++++ .../Views/Main/MainStatusBarLayoutTests.swift | 37 ++-- 9 files changed, 329 insertions(+), 158 deletions(-) create mode 100644 TablePro/Models/Query/StatusBarSnapshot.swift create mode 100644 TablePro/Views/Main/Child/StatusBarSnapshot+RowInfo.swift create mode 100644 TableProTests/Models/StatusBarSnapshotTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bd649285..e273d21ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- The results status bar has a divider above it and balanced left and right margins, and its loading spinner matches the size of the other controls. (#1569) - Custom keyboard shortcuts now work on non-US keyboard layouts, and shifted symbols like Cmd+[ record correctly. - The Keyboard settings list is grouped by where shortcuts act (Editor, Data Grid, Navigation, Connections), and each changed shortcut has its own reset button. - Conflict detection now checks live macOS system shortcuts and the editor's built-in commands, and lets the same key serve the editor and the data grid because focus decides which one runs. @@ -31,6 +32,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Pagination and other status bar buttons no longer get blocked by the window resize zone in the bottom-right corner. (#1569) +- VoiceOver now reads clear labels for the Columns button, Filters toggle, and loading indicators in the results status bar. (#1569) +- The custom rows-per-page popover now points at the page-size menu instead of the center of the pagination controls. (#1569) - DynamoDB AWS Profile auth now reads `~/.aws/config` as well as `~/.aws/credentials` and supports `credential_process`, matching the AWS CLI. Profiles defined only in `~/.aws/config`, including SSO and credential-process profiles, no longer fail with "profile not found". (#1567) - Query result columns now follow the order in the SELECT. Adding or removing a column no longer leaves new columns stuck at the end of the grid. (#1565) - JSON file import works again. It failed to load in 0.48.0. diff --git a/TablePro/Models/Query/StatusBarSnapshot.swift b/TablePro/Models/Query/StatusBarSnapshot.swift new file mode 100644 index 000000000..7636d206e --- /dev/null +++ b/TablePro/Models/Query/StatusBarSnapshot.swift @@ -0,0 +1,59 @@ +// +// StatusBarSnapshot.swift +// TablePro +// + +import Foundation + +struct StatusBarSnapshot: Equatable { + let tabId: UUID? + let tabType: TabType? + let hasRows: Bool + let hasColumns: Bool + let rowCount: Int + let hasTableName: Bool + let pagination: PaginationState + let statusMessage: String? + + init( + tabId: UUID?, + tabType: TabType?, + hasRows: Bool, + hasColumns: Bool, + rowCount: Int, + hasTableName: Bool, + pagination: PaginationState, + statusMessage: String? + ) { + self.tabId = tabId + self.tabType = tabType + self.hasRows = hasRows + self.hasColumns = hasColumns + self.rowCount = rowCount + self.hasTableName = hasTableName + self.pagination = pagination + self.statusMessage = statusMessage + } + + init(tab: QueryTab?, tableRows: TableRows?) { + self.init( + tabId: tab?.id, + tabType: tab?.tabType, + hasRows: !(tableRows?.rows.isEmpty ?? true), + hasColumns: !(tableRows?.columns.isEmpty ?? true), + rowCount: tableRows?.rows.count ?? 0, + hasTableName: tab?.tableContext.tableName != nil, + pagination: tab?.pagination ?? PaginationState(), + statusMessage: tab?.execution.statusMessage + ) + } + + var showsPaginationControls: Bool { + if let total = pagination.totalRowCount, total > 0 { return true } + return isPagedWithUnknownTotal + } + + var isPagedWithUnknownTotal: Bool { + pagination.currentPage > 1 || rowCount >= pagination.pageSize + } +} diff --git a/TablePro/Views/Components/PaginationControlsView.swift b/TablePro/Views/Components/PaginationControlsView.swift index 4abbeda14..169b30cf8 100644 --- a/TablePro/Views/Components/PaginationControlsView.swift +++ b/TablePro/Views/Components/PaginationControlsView.swift @@ -30,9 +30,6 @@ struct PaginationControlsView: View { pageSizeMenu navigationCluster } - .popover(isPresented: $showCustomPopover, arrowEdge: .top) { - customPageSizePopover - } } // MARK: - Page Size Menu @@ -62,6 +59,13 @@ struct PaginationControlsView: View { .controlSize(.small) .help(String(localized: "Rows per page")) .accessibilityLabel(String(localized: "Rows per page")) + .overlay(alignment: .bottom) { + Color.clear + .frame(width: 0, height: 0) + .popover(isPresented: $showCustomPopover, arrowEdge: .top) { + customPageSizePopover + } + } } private var pageSizeBinding: Binding { @@ -96,6 +100,7 @@ struct PaginationControlsView: View { if pagination.isLoading { ProgressView() .controlSize(.small) + .accessibilityLabel(String(localized: "Loading page")) } navButton( diff --git a/TablePro/Views/Main/Child/MainEditorContentView.swift b/TablePro/Views/Main/Child/MainEditorContentView.swift index dcd208707..ab8395d00 100644 --- a/TablePro/Views/Main/Child/MainEditorContentView.swift +++ b/TablePro/Views/Main/Child/MainEditorContentView.swift @@ -501,6 +501,7 @@ struct MainEditorContentView: View { } if tab.display.explainText == nil { + Divider() statusBar(tab: tab) } } @@ -744,25 +745,31 @@ struct MainEditorContentView: View { return MainStatusBarView( snapshot: StatusBarSnapshot(tab: tab, tableRows: resolvedRows), filterState: tab.filterState, - hiddenColumns: tab.columnLayout.hiddenColumns, - allColumns: coordinator.columnsForVisibilityPicker(for: tab, resultColumns: resolvedRows.columns), selectedRowIndices: selectionState.indices, viewMode: resultsViewModeBinding(for: tab), - onFirstPage: onFirstPage, - onPreviousPage: onPreviousPage, - onNextPage: onNextPage, - onLastPage: onLastPage, - onPageSizeChange: onPageSizeChange, - onShowAll: onShowAll, - onGoToPage: onGoToPage, - onToggleColumn: { coordinator.toggleColumnVisibility($0) }, - onShowAllColumns: { coordinator.showAllColumns() }, - onHideAllColumns: { coordinator.hideAllColumns($0) }, + paginationCallbacks: PaginationCallbacks( + onFirst: onFirstPage, + onPrevious: onPreviousPage, + onNext: onNextPage, + onLast: onLastPage, + onPageSizeChange: onPageSizeChange, + onShowAll: onShowAll, + onGoToPage: onGoToPage + ), + columnState: StatusBarColumnState( + hidden: tab.columnLayout.hiddenColumns, + all: coordinator.columnsForVisibilityPicker(for: tab, resultColumns: resolvedRows.columns), + onToggle: { coordinator.toggleColumnVisibility($0) }, + onShowAll: { coordinator.showAllColumns() }, + onHideAll: { coordinator.hideAllColumns($0) } + ), + structureState: StatusBarStructureState( + footer: coordinator.structureFooterState, + onAdd: { coordinator.structureActions?.addRow?() }, + onRemove: { coordinator.structureActions?.removeRow?() } + ), onToggleFilters: { coordinator.toggleFilterPanel() }, - onFetchAll: { coordinator.fetchAllRows() }, - structureFooterState: coordinator.structureFooterState, - onStructureAdd: { coordinator.structureActions?.addRow?() }, - onStructureRemove: { coordinator.structureActions?.removeRow?() } + onFetchAll: { coordinator.fetchAllRows() } ) } diff --git a/TablePro/Views/Main/Child/MainStatusBarView.swift b/TablePro/Views/Main/Child/MainStatusBarView.swift index 3f6fc1b25..f7bdf369e 100644 --- a/TablePro/Views/Main/Child/MainStatusBarView.swift +++ b/TablePro/Views/Main/Child/MainStatusBarView.swift @@ -7,65 +7,42 @@ import SwiftUI -struct StatusBarSnapshot: Equatable { - let tabId: UUID? - let tabType: TabType? - let hasRows: Bool - let hasColumns: Bool - let rowCount: Int - let hasTableName: Bool - let pagination: PaginationState - let statusMessage: String? +struct PaginationCallbacks { + let onFirst: () -> Void + let onPrevious: () -> Void + let onNext: () -> Void + let onLast: () -> Void + let onPageSizeChange: (Int) -> Void + let onShowAll: () -> Void + let onGoToPage: (Int) -> Void +} - init(tab: QueryTab?, tableRows: TableRows?) { - self.tabId = tab?.id - self.tabType = tab?.tabType - self.hasRows = !(tableRows?.rows.isEmpty ?? true) - self.hasColumns = !(tableRows?.columns.isEmpty ?? true) - self.rowCount = tableRows?.rows.count ?? 0 - self.hasTableName = tab?.tableContext.tableName != nil - self.pagination = tab?.pagination ?? PaginationState() - self.statusMessage = tab?.execution.statusMessage - } +struct StatusBarColumnState { + let hidden: Set + let all: [String] + let onToggle: (String) -> Void + let onShowAll: () -> Void + let onHideAll: ([String]) -> Void +} + +struct StatusBarStructureState { + let footer: StructureFooterState + let onAdd: () -> Void + let onRemove: () -> Void } struct MainStatusBarView: View { let snapshot: StatusBarSnapshot let filterState: TabFilterState - let hiddenColumns: Set - let allColumns: [String] let selectedRowIndices: Set @Binding var viewMode: ResultsViewMode - - @State private var showColumnPopover = false - - // Pagination callbacks - let onFirstPage: () -> Void - let onPreviousPage: () -> Void - let onNextPage: () -> Void - let onLastPage: () -> Void - let onPageSizeChange: (Int) -> Void - let onShowAll: () -> Void - let onGoToPage: (Int) -> Void - - // Column visibility callbacks - let onToggleColumn: (String) -> Void - let onShowAllColumns: () -> Void - let onHideAllColumns: ([String]) -> Void - - // Filter visibility callback + let paginationCallbacks: PaginationCallbacks + let columnState: StatusBarColumnState + let structureState: StatusBarStructureState let onToggleFilters: () -> Void + let onFetchAll: (() -> Void)? - // Truncated result callback - var onFetchAll: (() -> Void)? - - // Structure-mode footer accessory (Add/Remove buttons) - let structureFooterState: StructureFooterState? - let onStructureAdd: (() -> Void)? - let onStructureRemove: (() -> Void)? - - private var hasHiddenColumns: Bool { !hiddenColumns.isEmpty } - private var hiddenCount: Int { hiddenColumns.count } + @State private var showColumnPopover = false private var isStructureMode: Bool { viewMode == .structure } private var showsDataChrome: Bool { !isStructureMode } @@ -79,6 +56,14 @@ struct MainStatusBarView: View { return "\(label) (\(combo.displayString))" } + private var columnsAccessibilityLabel: String { + guard !columnState.hidden.isEmpty else { + return String(localized: "Columns") + } + let visible = columnState.all.count - columnState.hidden.count + return String(format: String(localized: "%d of %d columns visible"), visible, columnState.all.count) + } + var body: some View { HStack { if snapshot.tabId != nil { @@ -110,12 +95,14 @@ struct MainStatusBarView: View { HStack(spacing: 4) { if snapshot.pagination.isLoadingMore { ProgressView() - .controlSize(.mini) + .controlSize(.small) + .accessibilityHidden(true) Text("Loading…") .font(.caption) .foregroundStyle(.secondary) + .accessibilityLabel(String(localized: "Loading more rows")) } else { - Text(rowInfoText) + Text(snapshot.rowInfoText(selectedCount: selectedRowIndices.count)) .font(.caption) .foregroundStyle(.secondary) } @@ -149,8 +136,8 @@ struct MainStatusBarView: View { Spacer() HStack(spacing: 8) { - if isStructureMode, let state = structureFooterState, state.isActive { - structureFooterControls(state: state) + if isStructureMode, structureState.footer.isActive { + structureFooterControls(state: structureState.footer) } if showsDataChrome { @@ -159,25 +146,26 @@ struct MainStatusBarView: View { showColumnPopover.toggle() } label: { HStack(spacing: 4) { - Image(systemName: hasHiddenColumns + Image(systemName: !columnState.hidden.isEmpty ? "eye.slash.circle.fill" : "eye.circle") Text("Columns") - if hasHiddenColumns { - let visible = allColumns.count - hiddenCount - Text("(\(visible)/\(allColumns.count))") + if !columnState.hidden.isEmpty { + let visible = columnState.all.count - columnState.hidden.count + Text("(\(visible)/\(columnState.all.count))") .foregroundStyle(.secondary) } } } .controlSize(.small) - .popover(isPresented: $showColumnPopover) { + .accessibilityLabel(columnsAccessibilityLabel) + .popover(isPresented: $showColumnPopover, arrowEdge: .top) { ColumnVisibilityPopover( - columns: allColumns, - hiddenColumns: hiddenColumns, - onToggleColumn: onToggleColumn, - onShowAll: onShowAllColumns, - onHideAll: onHideAllColumns + columns: columnState.all, + hiddenColumns: columnState.hidden, + onToggleColumn: columnState.onToggle, + onShowAll: columnState.onShowAll, + onHideAll: columnState.onHideAll ) } } @@ -201,25 +189,28 @@ struct MainStatusBarView: View { .toggleStyle(.button) .controlSize(.small) .help(filterToggleHelp) + .accessibilityLabel(String(localized: "Filters")) + .accessibilityAddTraits(filterState.isVisible ? .isSelected : []) } - if snapshot.tabType == .table, snapshot.hasTableName, showsPaginationControls { + if snapshot.tabType == .table, snapshot.hasTableName, snapshot.showsPaginationControls { PaginationControlsView( pagination: snapshot.pagination, loadedRowCount: snapshot.rowCount, - onFirst: onFirstPage, - onPrevious: onPreviousPage, - onNext: onNextPage, - onLast: onLastPage, - onPageSizeChange: onPageSizeChange, - onShowAll: onShowAll, - onGoToPage: onGoToPage + onFirst: paginationCallbacks.onFirst, + onPrevious: paginationCallbacks.onPrevious, + onNext: paginationCallbacks.onNext, + onLast: paginationCallbacks.onLast, + onPageSizeChange: paginationCallbacks.onPageSizeChange, + onShowAll: paginationCallbacks.onShowAll, + onGoToPage: paginationCallbacks.onGoToPage ) } } } } - .padding(.horizontal, 0) + .padding(.leading, 8) + .padding(.trailing, 20) .padding(.vertical, 4) .background(Color(nsColor: .controlBackgroundColor)) .onChange(of: snapshot.tabId) { _, _ in @@ -231,7 +222,7 @@ struct MainStatusBarView: View { private func structureFooterControls(state: StructureFooterState) -> some View { ControlGroup { Button { - onStructureAdd?() + structureState.onAdd() } label: { Label(state.addLabel, systemImage: "plus") .labelStyle(.iconOnly) @@ -241,7 +232,7 @@ struct MainStatusBarView: View { .disabled(!state.canAdd) Button { - onStructureRemove?() + structureState.onRemove() } label: { Label(state.removeLabel, systemImage: "minus") .labelStyle(.iconOnly) @@ -254,45 +245,4 @@ struct MainStatusBarView: View { .controlSize(.small) .fixedSize() } - - private var showsPaginationControls: Bool { - if let total = snapshot.pagination.totalRowCount, total > 0 { return true } - return isPagedWithUnknownTotal - } - - private var isPagedWithUnknownTotal: Bool { - let pagination = snapshot.pagination - return pagination.currentPage > 1 || snapshot.rowCount >= pagination.pageSize - } - - private var rowInfoText: String { - let loadedCount = snapshot.rowCount - let selectedCount = selectedRowIndices.count - let pagination = snapshot.pagination - let total = pagination.totalRowCount - - if selectedCount > 0 { - if selectedCount == loadedCount { - return String(format: String(localized: "All %d rows selected"), loadedCount) - } else { - return String(format: String(localized: "%d of %d rows selected"), selectedCount, loadedCount) - } - } else if snapshot.tabType == .query && pagination.hasMoreRows { - let formattedCount = loadedCount.formatted(.number.grouping(.automatic)) - return String(format: String(localized: "Showing %@ rows"), formattedCount) - } else if snapshot.tabType == .table, let total = total, total > 0 { - let formattedTotal = total.formatted(.number.grouping(.automatic)) - let prefix = pagination.isApproximateRowCount ? "~" : "" - - return String(format: String(localized: "%d-%d of %@%@ rows"), pagination.rangeStart, pagination.rangeEnd, prefix, formattedTotal) - } else if snapshot.tabType == .table, isPagedWithUnknownTotal { - let rangeEnd = pagination.currentOffset + loadedCount - return String(format: String(localized: "%d-%d of ? rows"), pagination.rangeStart, rangeEnd) - } else if loadedCount > 0 { - let formattedCount = loadedCount.formatted(.number.grouping(.automatic)) - return String(format: String(localized: "%@ rows"), formattedCount) - } else { - return String(localized: "No rows") - } - } } diff --git a/TablePro/Views/Main/Child/StatusBarSnapshot+RowInfo.swift b/TablePro/Views/Main/Child/StatusBarSnapshot+RowInfo.swift new file mode 100644 index 000000000..158683756 --- /dev/null +++ b/TablePro/Views/Main/Child/StatusBarSnapshot+RowInfo.swift @@ -0,0 +1,38 @@ +// +// StatusBarSnapshot+RowInfo.swift +// TablePro +// + +import Foundation + +extension StatusBarSnapshot { + func rowInfoText(selectedCount: Int) -> String { + let loadedCount = rowCount + let total = pagination.totalRowCount + + if selectedCount > 0 { + if selectedCount == loadedCount { + return String(format: String(localized: "All %d rows selected"), loadedCount) + } + return String(format: String(localized: "%d of %d rows selected"), selectedCount, loadedCount) + } + if tabType == .query, pagination.hasMoreRows { + let formattedCount = loadedCount.formatted(.number.grouping(.automatic)) + return String(format: String(localized: "Showing %@ rows"), formattedCount) + } + if tabType == .table, let total, total > 0 { + let formattedTotal = total.formatted(.number.grouping(.automatic)) + let prefix = pagination.isApproximateRowCount ? "~" : "" + return String(format: String(localized: "%d-%d of %@%@ rows"), pagination.rangeStart, pagination.rangeEnd, prefix, formattedTotal) + } + if tabType == .table, isPagedWithUnknownTotal { + let rangeEnd = pagination.currentOffset + loadedCount + return String(format: String(localized: "%d-%d of ? rows"), pagination.rangeStart, rangeEnd) + } + if loadedCount > 0 { + let formattedCount = loadedCount.formatted(.number.grouping(.automatic)) + return String(format: String(localized: "%@ rows"), formattedCount) + } + return String(localized: "No rows") + } +} diff --git a/TablePro/Views/Results/ColumnVisibilityPopover.swift b/TablePro/Views/Results/ColumnVisibilityPopover.swift index d9dcd440f..323654da1 100644 --- a/TablePro/Views/Results/ColumnVisibilityPopover.swift +++ b/TablePro/Views/Results/ColumnVisibilityPopover.swift @@ -14,9 +14,6 @@ struct ColumnVisibilityPopover: View { @State private var searchText = "" - private var hasHiddenColumns: Bool { !hiddenColumns.isEmpty } - private var hiddenCount: Int { hiddenColumns.count } - private var filteredColumns: [String] { if searchText.isEmpty { return columns @@ -41,11 +38,11 @@ struct ColumnVisibilityPopover: View { } private var headerTitle: String { - let visible = columns.count - hiddenCount - if hasHiddenColumns { - return "\(visible) of \(columns.count)" + guard !hiddenColumns.isEmpty else { + return String(localized: "Columns") } - return String(localized: "Columns") + let visible = columns.count - hiddenColumns.count + return String(format: String(localized: "%d of %d"), visible, columns.count) } private var header: some View { @@ -59,12 +56,12 @@ struct ColumnVisibilityPopover: View { Button("Show All") { onShowAll() } .buttonStyle(.link) .controlSize(.small) - .disabled(!hasHiddenColumns) + .disabled(hiddenColumns.isEmpty) Button("Hide All") { onHideAll(columns) } .buttonStyle(.link) .controlSize(.small) - .disabled(hiddenCount == columns.count) + .disabled(hiddenColumns.count == columns.count) } .padding(.horizontal, 12) .padding(.vertical, 8) diff --git a/TableProTests/Models/StatusBarSnapshotTests.swift b/TableProTests/Models/StatusBarSnapshotTests.swift new file mode 100644 index 000000000..922c0c6b8 --- /dev/null +++ b/TableProTests/Models/StatusBarSnapshotTests.swift @@ -0,0 +1,104 @@ +// +// StatusBarSnapshotTests.swift +// TableProTests +// + +import Foundation +import Testing + +@testable import TablePro + +@Suite("StatusBarSnapshot") +struct StatusBarSnapshotTests { + private func makeSnapshot( + tabType: TabType? = .table, + rowCount: Int = 0, + pagination: PaginationState = PaginationState(), + statusMessage: String? = nil + ) -> StatusBarSnapshot { + StatusBarSnapshot( + tabId: UUID(), + tabType: tabType, + hasRows: rowCount > 0, + hasColumns: rowCount > 0, + rowCount: rowCount, + hasTableName: true, + pagination: pagination, + statusMessage: statusMessage + ) + } + + @Test("Pagination controls show when a positive total is known") + func showsPaginationWithKnownTotal() { + let snapshot = makeSnapshot(rowCount: 1_000, pagination: PaginationState(totalRowCount: 5_000, pageSize: 1_000)) + #expect(snapshot.showsPaginationControls) + } + + @Test("Single page with unknown total hides pagination") + func hidesPaginationOnSinglePage() { + let snapshot = makeSnapshot(rowCount: 10, pagination: PaginationState(totalRowCount: nil, pageSize: 50, currentPage: 1)) + #expect(!snapshot.isPagedWithUnknownTotal) + #expect(!snapshot.showsPaginationControls) + } + + @Test("Page beyond the first is treated as paged with unknown total") + func pagedWhenBeyondFirstPage() { + let snapshot = makeSnapshot(rowCount: 50, pagination: PaginationState(totalRowCount: nil, pageSize: 50, currentPage: 2, currentOffset: 50)) + #expect(snapshot.isPagedWithUnknownTotal) + #expect(snapshot.showsPaginationControls) + } + + @Test("A full first page with unknown total is treated as paged") + func pagedWhenFirstPageIsFull() { + let snapshot = makeSnapshot(rowCount: 50, pagination: PaginationState(totalRowCount: nil, pageSize: 50, currentPage: 1)) + #expect(snapshot.isPagedWithUnknownTotal) + } + + @Test("No rows reports an empty state") + func rowInfoNoRows() { + let snapshot = makeSnapshot(rowCount: 0) + #expect(snapshot.rowInfoText(selectedCount: 0) == String(localized: "No rows")) + } + + @Test("Selecting every loaded row reports the all-selected text") + func rowInfoAllSelected() { + let snapshot = makeSnapshot(rowCount: 5) + #expect(snapshot.rowInfoText(selectedCount: 5) == String(format: String(localized: "All %d rows selected"), 5)) + } + + @Test("Selecting some rows reports the partial-selection text") + func rowInfoPartialSelection() { + let snapshot = makeSnapshot(rowCount: 5) + #expect(snapshot.rowInfoText(selectedCount: 2) == String(format: String(localized: "%d of %d rows selected"), 2, 5)) + } + + @Test("A table with a known total reports the offset range") + func rowInfoTableRange() { + let snapshot = makeSnapshot( + rowCount: 1_000, + pagination: PaginationState(totalRowCount: 5_000, pageSize: 1_000, currentPage: 3, currentOffset: 2_000) + ) + let text = snapshot.rowInfoText(selectedCount: 0) + #expect(text.contains("2001-3000")) + } + + @Test("A paged table with unknown total reports a question mark for the total") + func rowInfoUnknownTotalRange() { + let snapshot = makeSnapshot( + rowCount: 50, + pagination: PaginationState(totalRowCount: nil, pageSize: 50, currentPage: 2, currentOffset: 50) + ) + let text = snapshot.rowInfoText(selectedCount: 0) + #expect(text.contains("51-100")) + #expect(text.contains("?")) + } + + @Test("A truncated query reports the showing-rows text") + func rowInfoTruncatedQuery() { + var pagination = PaginationState(pageSize: 1_000) + pagination.hasMoreRows = true + let snapshot = makeSnapshot(tabType: .query, rowCount: 1_000, pagination: pagination) + let text = snapshot.rowInfoText(selectedCount: 0) + #expect(text.contains("1,000") || text.contains("1000")) + } +} diff --git a/TableProTests/Views/Main/MainStatusBarLayoutTests.swift b/TableProTests/Views/Main/MainStatusBarLayoutTests.swift index 311e1362b..2887daa17 100644 --- a/TableProTests/Views/Main/MainStatusBarLayoutTests.swift +++ b/TableProTests/Views/Main/MainStatusBarLayoutTests.swift @@ -18,24 +18,31 @@ struct MainStatusBarLayoutTests { let view = MainStatusBarView( snapshot: StatusBarSnapshot(tab: nil, tableRows: nil), filterState: TabFilterState(), - hiddenColumns: [], - allColumns: [], selectedRowIndices: [], viewMode: .constant(.data), - onFirstPage: {}, - onPreviousPage: {}, - onNextPage: {}, - onLastPage: {}, - onPageSizeChange: { _ in }, - onShowAll: {}, - onGoToPage: { _ in }, - onToggleColumn: { _ in }, - onShowAllColumns: {}, - onHideAllColumns: { _ in }, + paginationCallbacks: PaginationCallbacks( + onFirst: {}, + onPrevious: {}, + onNext: {}, + onLast: {}, + onPageSizeChange: { _ in }, + onShowAll: {}, + onGoToPage: { _ in } + ), + columnState: StatusBarColumnState( + hidden: [], + all: [], + onToggle: { _ in }, + onShowAll: {}, + onHideAll: { _ in } + ), + structureState: StatusBarStructureState( + footer: StructureFooterState(), + onAdd: {}, + onRemove: {} + ), onToggleFilters: {}, - structureFooterState: nil, - onStructureAdd: nil, - onStructureRemove: nil + onFetchAll: nil ) #expect(type(of: view.body) != Never.self) }