This is an automated email from the ASF dual-hosted git repository.
rusackas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 8d25777b995 test(plugin-chart-table): cover server-side sort, query
mode controls, and sort ordering (#44845)
8d25777b995 is described below
commit 8d25777b99577fd3e5a9497f50e99bc9de653e85
Author: Joe Li <[email protected]>
AuthorDate: Fri Oct 2 10:25:11 2026 -0700
test(plugin-chart-table): cover server-side sort, query mode controls, and
sort ordering (#44845)
Co-authored-by: Claude Opus 5.5 <[email protected]>
---
.../plugin-chart-table/test/TableChart.test.tsx | 88 ++++++++++++++++++++++
.../plugin-chart-table/test/buildQuery.test.ts | 42 +++++++++++
.../plugin-chart-table/test/controlPanel.test.ts | 55 ++++++++++++++
3 files changed, 185 insertions(+)
diff --git
a/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx
b/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx
index a22b0452ad5..fb21635a0bb 100644
--- a/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx
+++ b/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx
@@ -3572,3 +3572,91 @@ test('TableChart should NOT emit cross-filter when
clicking a cell in a not-filt
);
expect(crossFilterCall).toBeUndefined();
});
+
+test.each([
+ { orderDesc: true, firstClickDesc: true },
+ { orderDesc: false, firstClickDesc: false },
+])(
+ 'pushes the clicked column sort to the server own state when server
pagination is enabled (order_desc $orderDesc)',
+ ({ orderDesc, firstClickDesc }) => {
+ const setDataMask = jest.fn();
+ const props = transformProps({
+ ...testData.raw,
+ rawFormData: {
+ ...testData.raw.rawFormData,
+ server_pagination: true,
+ order_desc: orderDesc,
+ },
+ hooks: { setDataMask },
+ queriesData: [
+ {
+ ...testData.raw.queriesData[0],
+ colnames: ['name'],
+ coltypes: [GenericDataType.String],
+ data: [{ name: 'Michael' }, { name: 'John' }],
+ },
+ ],
+ });
+ render(
+ ProviderWrapper({
+ children: (
+ <TableChart {...props} setDataMask={setDataMask} sticky={false} />
+ ),
+ }),
+ );
+
+ const lastPushedSortBy = () =>
+ setDataMask.mock.calls
+ .map(([mask]) => mask?.ownState?.sortBy)
+ .filter(Array.isArray)
+ .at(-1);
+
+ expect(lastPushedSortBy()).toBeUndefined();
+
+ // The first click sorts in the configured default direction and the
+ // second click flips it; each change is pushed with the column key.
+ fireEvent.click(screen.getByText('name'));
+ expect(lastPushedSortBy()).toEqual([
+ expect.objectContaining({ key: 'name', desc: firstClickDesc }),
+ ]);
+
+ fireEvent.click(screen.getByText('name'));
+ expect(lastPushedSortBy()).toEqual([
+ expect.objectContaining({ key: 'name', desc: !firstClickDesc }),
+ ]);
+ },
+);
+
+test('does not push a column sort to the server own state when server
pagination is disabled', () => {
+ const setDataMask = jest.fn();
+ const props = transformProps({
+ ...testData.raw,
+ rawFormData: {
+ ...testData.raw.rawFormData,
+ server_pagination: false,
+ },
+ hooks: { setDataMask },
+ queriesData: [
+ {
+ ...testData.raw.queriesData[0],
+ colnames: ['name'],
+ coltypes: [GenericDataType.String],
+ data: [{ name: 'Michael' }, { name: 'John' }],
+ },
+ ],
+ });
+ render(
+ ProviderWrapper({
+ children: (
+ <TableChart {...props} setDataMask={setDataMask} sticky={false} />
+ ),
+ }),
+ );
+
+ fireEvent.click(screen.getByText('name'));
+
+ const pushedSort = setDataMask.mock.calls.some(([mask]) =>
+ Object.prototype.hasOwnProperty.call(mask?.ownState ?? {}, 'sortBy'),
+ );
+ expect(pushedSort).toBe(false);
+});
diff --git
a/superset-frontend/plugins/plugin-chart-table/test/buildQuery.test.ts
b/superset-frontend/plugins/plugin-chart-table/test/buildQuery.test.ts
index 8f9e0481451..33d449ae09d 100644
--- a/superset-frontend/plugins/plugin-chart-table/test/buildQuery.test.ts
+++ b/superset-frontend/plugins/plugin-chart-table/test/buildQuery.test.ts
@@ -212,6 +212,48 @@ describe('plugin-chart-table', () => {
expect(query.columns).toEqual(['rawcol']);
expect(query.post_processing).toEqual([]);
});
+
+ test.each([
+ { orderDesc: true, expectedAscending: false },
+ { orderDesc: false, expectedAscending: true },
+ ])(
+ 'orders by the sort-by metric in aggregate mode when order_desc is
$orderDesc',
+ ({ orderDesc, expectedAscending }) => {
+ const query = buildQueryCached({
+ ...basicFormData,
+ query_mode: QueryMode.Aggregate,
+ groupby: ['col1'],
+ metrics: ['first_metric', 'sort_metric'],
+ timeseries_limit_metric: 'sort_metric',
+ order_desc: orderDesc,
+ }).queries[0];
+ expect(query.orderby).toEqual([['sort_metric', expectedAscending]]);
+ },
+ );
+
+ test('orders by the first metric descending in aggregate mode without a
sort-by metric', () => {
+ const query = buildQueryCached({
+ ...basicFormData,
+ query_mode: QueryMode.Aggregate,
+ groupby: ['col1'],
+ metrics: ['first_metric', 'second_metric'],
+ order_desc: false,
+ }).queries[0];
+ expect(query.orderby).toEqual([['first_metric', false]]);
+ });
+
+ test('maps order_by_cols to orderby in raw records mode', () => {
+ const query = buildQueryCached({
+ ...basicFormData,
+ query_mode: QueryMode.Raw,
+ columns: ['col1', 'col2'],
+ order_by_cols: ['["col1", true]', '["col2", false]'],
+ }).queries[0];
+ expect(query.orderby).toEqual([
+ ['col1', true],
+ ['col2', false],
+ ]);
+ });
test('should prefer extra_form_data.time_grain_sqla over
formData.time_grain_sqla', () => {
const query = buildQueryCached({
...basicFormData,
diff --git
a/superset-frontend/plugins/plugin-chart-table/test/controlPanel.test.ts
b/superset-frontend/plugins/plugin-chart-table/test/controlPanel.test.ts
index af51d5bc117..2cae108e67c 100644
--- a/superset-frontend/plugins/plugin-chart-table/test/controlPanel.test.ts
+++ b/superset-frontend/plugins/plugin-chart-table/test/controlPanel.test.ts
@@ -290,3 +290,58 @@ test('time_grain_sqla is hidden in raw records mode for an
adhoc dimension', ()
),
).toBe(false);
});
+
+/**
+ * Finds the visibility function of a control whether it is declared through a
+ * full `config` (e.g. `all_columns`) or through an `override` of a shared
+ * control (e.g. `groupby`, `metrics`).
+ */
+function getModeVisibility(controlName: string): VisibilityFn {
+ const item = (config.controlPanelSections || [])
+ .flatMap(section => section?.controlSetRows || [])
+ .flat()
+ .find(
+ c =>
+ typeof c === 'object' &&
+ c !== null &&
+ 'name' in c &&
+ (c as { name: string }).name === controlName,
+ ) as
+ | {
+ config?: { visibility?: VisibilityFn };
+ override?: { visibility?: VisibilityFn };
+ }
+ | undefined;
+ const visibility = item?.config?.visibility ?? item?.override?.visibility;
+ if (typeof visibility !== 'function') {
+ throw new Error(`Control "${controlName}" with visibility not found`);
+ }
+ return visibility;
+}
+
+const modeProps = (mode: QueryMode): ControlPanelsContainerProps =>
+ ({
+ controls: { query_mode: { value: mode } },
+ }) as unknown as ControlPanelsContainerProps;
+
+test('raw mode shows the raw-only controls and hides the aggregate-only
controls', () => {
+ const props = modeProps(QueryMode.Raw);
+
+ expect(getModeVisibility('all_columns')(props)).toBe(true);
+ expect(getModeVisibility('order_by_cols')(props)).toBe(true);
+ expect(getModeVisibility('groupby')(props)).toBe(false);
+ expect(getModeVisibility('metrics')(props)).toBe(false);
+ expect(getModeVisibility('percent_metrics')(props)).toBe(false);
+ expect(getModeVisibility('timeseries_limit_metric')(props)).toBe(false);
+});
+
+test('aggregate mode shows the aggregate-only controls and hides the raw-only
controls', () => {
+ const props = modeProps(QueryMode.Aggregate);
+
+ expect(getModeVisibility('groupby')(props)).toBe(true);
+ expect(getModeVisibility('metrics')(props)).toBe(true);
+ expect(getModeVisibility('percent_metrics')(props)).toBe(true);
+ expect(getModeVisibility('timeseries_limit_metric')(props)).toBe(true);
+ expect(getModeVisibility('all_columns')(props)).toBe(false);
+ expect(getModeVisibility('order_by_cols')(props)).toBe(false);
+});