This is an automated email from the ASF dual-hosted git repository. sadpandajoe pushed a commit to branch test-table-chart-gaps in repository https://gitbox.apache.org/repos/asf/superset.git
commit 71d613dc84da8cb7e56385637bc987fddbceee6c Author: Joe Li <[email protected]> AuthorDate: Wed Sep 30 17:25:28 2026 -0700 test(plugin-chart-table): cover server-side sort, query mode controls, and sort ordering 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 01ab2b7651f..24c78b656af 100644 --- a/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx +++ b/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx @@ -3246,3 +3246,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 275dbf46097..5a30f24e9c0 100644 --- a/superset-frontend/plugins/plugin-chart-table/test/controlPanel.test.ts +++ b/superset-frontend/plugins/plugin-chart-table/test/controlPanel.test.ts @@ -148,3 +148,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); +});
