You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2026/09/28 17:52:16 UTC
(superset) branch master updated: test(dashboard): cover drill to detail payloads and page reset (#44673)
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 98c74d8d8bf test(dashboard): cover drill to detail payloads and page reset (#44673)
98c74d8d8bf is described below
commit 98c74d8d8bfbd4d27e2f3d4f12369088d844ecfc
Author: Joe Li <jo...@preset.io>
AuthorDate: Mon Sep 28 10:52:08 2026 -0700
test(dashboard): cover drill to detail payloads and page reset (#44673)
Co-authored-by: Claude Opus 5.5 <no...@anthropic.com>
---
.../src/BigNumber/BigNumberViz.test.tsx | 131 ++++++++++++++++++++-
.../test/Treemap/EchartsTreemap.test.tsx | 123 +++++++++++++++++++
.../test/utils/eventHandlers.test.ts | 74 ++++++++++++
.../Chart/DrillDetail/DrillDetailPane.test.tsx | 74 +++++++++++-
4 files changed, 400 insertions(+), 2 deletions(-)
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.test.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.test.tsx
index dd7a495ee89..db8d4e50f9d 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.test.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.test.tsx
@@ -17,9 +17,23 @@
* under the License.
*/
-import { getNumberFormatter } from '@superset-ui/core';
+import {
+ DTTM_ALIAS,
+ getNumberFormatter,
+ TimeFormatter,
+} from '@superset-ui/core';
import { render, fireEvent } from '../../../../spec/helpers/testing-library';
import BigNumberVis from './BigNumberViz';
+import Echart from '../components/Echart';
+import { EventHandlers } from '../types';
+import { BigNumberWithTrendlineFormData } from './types';
+
+jest.mock('../components/Echart', () => ({
+ __esModule: true,
+ default: jest.fn(() => null),
+}));
+
+const mockedEchart = jest.mocked(Echart);
/**
* Tests for the color threshold formatter logic in BigNumberViz.
@@ -117,3 +131,118 @@ describe('BigNumberViz context menu', () => {
expect(ancestorHandler).not.toHaveBeenCalled();
});
});
+
+describe('BigNumberViz trendline context menu', () => {
+ beforeEach(() => {
+ mockedEchart.mockClear();
+ });
+
+ const renderWithTrendline = (
+ onContextMenu: jest.Mock,
+ xAxis: string = 'ds',
+ ) => {
+ render(
+ <BigNumberVis
+ width={200}
+ height={100}
+ bigNumber={42}
+ headerFormatter={getNumberFormatter()}
+ headerFontSize={0.3}
+ subheaderFontSize={0.125}
+ subtitleFontSize={0.125}
+ subtitle=""
+ refs={{}}
+ showTrendLine
+ trendLineData={[
+ [1577836800000, 10],
+ [1577923200000, 20],
+ ]}
+ echartOptions={{}}
+ formData={
+ {
+ xAxis,
+ granularitySqla: 'legacy_ds',
+ timeGrainSqla: 'P1D',
+ vizType: 'big_number',
+ } as unknown as BigNumberWithTrendlineFormData
+ }
+ xValueFormatter={
+ new TimeFormatter({
+ id: 'test-time-formatter',
+ formatFunc: (value: Date) => `formatted-${value.getTime()}`,
+ })
+ }
+ onContextMenu={onContextMenu}
+ />,
+ );
+
+ const lastCall =
+ mockedEchart.mock.calls[mockedEchart.mock.calls.length - 1];
+ const { eventHandlers } = lastCall[0] as {
+ eventHandlers: EventHandlers;
+ };
+ return eventHandlers;
+ };
+
+ test('right-clicking a trendline point drills to detail for that point', () => {
+ const onContextMenu = jest.fn();
+ const stop = jest.fn();
+ const eventHandlers = renderWithTrendline(onContextMenu);
+
+ eventHandlers.contextmenu({
+ data: [1577836800000, 10],
+ event: {
+ stop,
+ event: { clientX: 15, clientY: 25 },
+ },
+ });
+
+ expect(stop).toHaveBeenCalledTimes(1);
+ expect(onContextMenu).toHaveBeenCalledTimes(1);
+ const [x, y, payload] = onContextMenu.mock.calls[0];
+ expect(x).toBe(15);
+ expect(y).toBe(25);
+ expect(payload.drillToDetail).toEqual([
+ {
+ col: 'ds',
+ grain: 'P1D',
+ op: '==',
+ val: 1577836800000,
+ formattedVal: 'formatted-1577836800000',
+ },
+ ]);
+ });
+
+ test('drills on the legacy time column when the x-axis is the timestamp alias', () => {
+ const onContextMenu = jest.fn();
+ const eventHandlers = renderWithTrendline(onContextMenu, DTTM_ALIAS);
+
+ eventHandlers.contextmenu({
+ data: [1577836800000, 10],
+ event: {
+ stop: jest.fn(),
+ event: { clientX: 15, clientY: 25 },
+ },
+ });
+
+ const [, , payload] = onContextMenu.mock.calls[0];
+ expect(payload.drillToDetail).toEqual([
+ expect.objectContaining({ col: 'legacy_ds', grain: 'P1D' }),
+ ]);
+ });
+
+ test('does not call onContextMenu when the point has no data', () => {
+ const onContextMenu = jest.fn();
+ const eventHandlers = renderWithTrendline(onContextMenu);
+
+ eventHandlers.contextmenu({
+ data: undefined,
+ event: {
+ stop: jest.fn(),
+ event: { clientX: 15, clientY: 25 },
+ },
+ });
+
+ expect(onContextMenu).not.toHaveBeenCalled();
+ });
+});
diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Treemap/EchartsTreemap.test.tsx b/superset-frontend/plugins/plugin-chart-echarts/test/Treemap/EchartsTreemap.test.tsx
new file mode 100644
index 00000000000..1a3fa069405
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-echarts/test/Treemap/EchartsTreemap.test.tsx
@@ -0,0 +1,123 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { render } from '../../../../spec/helpers/testing-library';
+import EchartsTreemap from '../../src/Treemap/EchartsTreemap';
+import Echart from '../../src/components/Echart';
+import { EventHandlers, TreePathInfo } from '../../src/types';
+import {
+ EchartsTreemapFormData,
+ TreemapTransformedProps,
+} from '../../src/Treemap/types';
+
+jest.mock('../../src/components/Echart', () => ({
+ __esModule: true,
+ default: jest.fn(() => null),
+}));
+
+const mockedEchart = jest.mocked(Echart);
+
+const renderTreemap = (
+ onContextMenu?: jest.Mock,
+): { eventHandlers: EventHandlers } => {
+ mockedEchart.mockClear();
+
+ const props: TreemapTransformedProps = {
+ echartOptions: {},
+ formData: {
+ groupby: ['country', 'state'],
+ dateFormat: 'smart_date',
+ numberFormat: 'SMART_NUMBER',
+ vizType: 'treemap',
+ } as unknown as EchartsTreemapFormData,
+ height: 400,
+ width: 400,
+ refs: {},
+ groupby: ['country', 'state'],
+ labelMap: { 'USA,CA': ['USA', 'CA'] },
+ selectedValues: {},
+ setDataMask: jest.fn(),
+ emitCrossFilters: false,
+ coltypeMapping: {},
+ onContextMenu,
+ };
+
+ render(<EchartsTreemap {...props} />);
+
+ const lastCall = mockedEchart.mock.calls[mockedEchart.mock.calls.length - 1];
+ const { eventHandlers } = lastCall[0] as { eventHandlers: EventHandlers };
+ return { eventHandlers };
+};
+
+test('right-clicking a treemap node drills to detail for its groupby path', () => {
+ const onContextMenu = jest.fn();
+ const { eventHandlers } = renderTreemap(onContextMenu);
+
+ const treePathInfo: TreePathInfo[] = [
+ { name: 'sum__num', dataIndex: 0, value: 100 },
+ { name: 'USA', dataIndex: 1, value: 60 },
+ { name: 'CA', dataIndex: 2, value: 40 },
+ ];
+ const stop = jest.fn();
+
+ eventHandlers.contextmenu({
+ data: { name: 'CA', value: 40 },
+ treePathInfo,
+ event: {
+ stop,
+ event: { clientX: 50, clientY: 80 },
+ },
+ });
+
+ expect(stop).toHaveBeenCalledTimes(1);
+ expect(onContextMenu).toHaveBeenCalledTimes(1);
+ const [x, y, payload] = onContextMenu.mock.calls[0];
+ expect(x).toBe(50);
+ expect(y).toBe(80);
+ expect(payload.drillToDetail).toEqual([
+ { col: 'country', op: '==', val: 'USA', formattedVal: 'USA' },
+ { col: 'state', op: '==', val: 'CA', formattedVal: 'CA' },
+ ]);
+ expect(payload.drillBy).toEqual({
+ filters: [
+ { col: 'country', op: '==', val: 'USA', formattedVal: 'USA' },
+ { col: 'state', op: '==', val: 'CA', formattedVal: 'CA' },
+ ],
+ groupbyFieldName: 'groupby',
+ });
+});
+
+test('right-clicking the root node does not call onContextMenu', () => {
+ const onContextMenu = jest.fn();
+ const { eventHandlers } = renderTreemap(onContextMenu);
+
+ const treePathInfo: TreePathInfo[] = [
+ { name: 'sum__num', dataIndex: 0, value: 100 },
+ ];
+
+ eventHandlers.contextmenu({
+ data: { name: 'sum__num', value: 100, children: [] },
+ treePathInfo,
+ event: {
+ stop: jest.fn(),
+ event: { clientX: 50, clientY: 80 },
+ },
+ });
+
+ expect(onContextMenu).not.toHaveBeenCalled();
+});
diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/utils/eventHandlers.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/utils/eventHandlers.test.ts
index 4c60aa85a36..cef1a79ac94 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/test/utils/eventHandlers.test.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/test/utils/eventHandlers.test.ts
@@ -279,3 +279,77 @@ test('context menu offers no cross-filter for an unresolvable name (#42340)', ()
// contextMenuEventHandler already bails out before opening the menu
expect(onContextMenu).not.toHaveBeenCalled();
});
+
+test('context menu passes drillToDetail and drillBy filters for a resolved groupby value', () => {
+ const onContextMenu = jest.fn();
+ const props = buildProps({
+ groupby: ['topics'],
+ labelMap: { cancellations: ['cancellations'] },
+ selectedValues: {},
+ onContextMenu,
+ });
+
+ const handlers = allEventHandlers(props);
+ handlers.contextmenu({
+ name: 'cancellations',
+ event: {
+ stop: jest.fn(),
+ event: { clientX: 10, clientY: 20 } as unknown as PointerEvent,
+ },
+ });
+
+ expect(onContextMenu).toHaveBeenCalledWith(
+ 10,
+ 20,
+ expect.objectContaining({
+ drillToDetail: [
+ {
+ col: 'topics',
+ op: '==',
+ val: 'cancellations',
+ formattedVal: 'cancellations',
+ },
+ ],
+ drillBy: {
+ filters: [
+ {
+ col: 'topics',
+ op: '==',
+ val: 'cancellations',
+ formattedVal: 'cancellations',
+ },
+ ],
+ groupbyFieldName: 'groupby',
+ },
+ }),
+ );
+});
+
+test('context menu passes empty drill filters when the chart has no groupby', () => {
+ const onContextMenu = jest.fn();
+ const props = buildProps({
+ groupby: [],
+ labelMap: {},
+ selectedValues: {},
+ onContextMenu,
+ });
+
+ const handlers = allEventHandlers(props);
+ handlers.contextmenu({
+ name: 'total',
+ event: {
+ stop: jest.fn(),
+ event: { clientX: 3, clientY: 4 } as unknown as PointerEvent,
+ },
+ });
+
+ expect(onContextMenu).toHaveBeenCalledWith(
+ 3,
+ 4,
+ expect.objectContaining({
+ drillToDetail: [],
+ drillBy: { filters: [], groupbyFieldName: 'groupby' },
+ crossFilter: undefined,
+ }),
+ );
+});
diff --git a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailPane.test.tsx b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailPane.test.tsx
index 8ac25a6f105..e066c133c73 100644
--- a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailPane.test.tsx
+++ b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailPane.test.tsx
@@ -17,7 +17,11 @@
* under the License.
*/
import fetchMock from 'fetch-mock';
-import { QueryFormData, SupersetClient } from '@superset-ui/core';
+import {
+ BinaryQueryObjectFilterClause,
+ QueryFormData,
+ SupersetClient,
+} from '@superset-ui/core';
import {
fireEvent,
render,
@@ -51,6 +55,9 @@ const waitForRender = (overrides: Record<string, any> = {}) =>
const SAMPLES_ENDPOINT =
'end:/datasource/samples?force=false&datasource_type=table&datasource_id=7&per_page=50&page=1';
+const SAMPLES_ENDPOINT_PAGE_2 =
+ 'end:/datasource/samples?force=false&datasource_type=table&datasource_id=7&per_page=50&page=2';
+
const DATASET_ENDPOINT = 'glob:*/api/v1/dataset/*';
const MOCKED_DATASET = {
@@ -144,6 +151,26 @@ const fetchWithPaginatedData = () => {
});
};
+const fetchWithTwoPages = () => {
+ setupDatasetEndpoint();
+ fetchMock.post(SAMPLES_ENDPOINT, {
+ result: {
+ total_count: 100,
+ data: [{ year: 1996, na_sales: 11.27, eu_sales: 8.89 }],
+ colnames: ['year', 'na_sales', 'eu_sales'],
+ coltypes: [0, 0, 0],
+ },
+ });
+ fetchMock.post(SAMPLES_ENDPOINT_PAGE_2, {
+ result: {
+ total_count: 100,
+ data: [{ year: 2020, na_sales: 42.42, eu_sales: 24.24 }],
+ colnames: ['year', 'na_sales', 'eu_sales'],
+ coltypes: [0, 0, 0],
+ },
+ });
+};
+
afterEach(() => {
fetchMock.clearHistory().removeRoutes();
supersetGetCache.clear();
@@ -416,3 +443,48 @@ test('renders a semantic-view resource with Not available metadata rows', async
const notAvailable = await screen.findAllByText('Not available');
expect(notAvailable.length).toBeGreaterThan(0);
});
+
+test('resets to page 1 and refetches when the drill filters change', async () => {
+ jest.restoreAllMocks();
+ fetchWithTwoPages();
+ const initialFilters: BinaryQueryObjectFilterClause[] = [
+ { col: 'year', op: '==', val: 1996, formattedVal: '1996' },
+ ];
+ await waitForRender({ initialFilters });
+ expect(await screen.findByText('11.27')).toBeInTheDocument();
+
+ await userEvent.click(screen.getByTitle('2'));
+ expect(await screen.findByText('42.42')).toBeInTheDocument();
+ expect(screen.queryByText('11.27')).not.toBeInTheDocument();
+
+ // Removing the filter tag changes `filters`, which should clear the cached
+ // pages and drop the pane back to page 1 rather than continuing to request
+ // page 2 under the new filter set.
+ await userEvent.click(screen.getByLabelText('Close'));
+
+ expect(await screen.findByText('11.27')).toBeInTheDocument();
+ expect(screen.queryByText('42.42')).not.toBeInTheDocument();
+ const page1Calls = fetchMock.callHistory.calls(SAMPLES_ENDPOINT);
+ expect(page1Calls).toHaveLength(2);
+ expect(JSON.parse(page1Calls[0].options.body as string).filters).toEqual([
+ expect.objectContaining({ col: 'year', val: 1996 }),
+ ]);
+ expect(JSON.parse(page1Calls[1].options.body as string).filters).toEqual([]);
+});
+
+test('resets to page 1 and refetches on reload', async () => {
+ jest.restoreAllMocks();
+ fetchWithTwoPages();
+ await waitForRender();
+ expect(await screen.findByText('11.27')).toBeInTheDocument();
+
+ await userEvent.click(screen.getByTitle('2'));
+ expect(await screen.findByText('42.42')).toBeInTheDocument();
+ expect(screen.queryByText('11.27')).not.toBeInTheDocument();
+
+ await userEvent.click(screen.getByRole('button', { name: 'Reload' }));
+
+ expect(await screen.findByText('11.27')).toBeInTheDocument();
+ expect(screen.queryByText('42.42')).not.toBeInTheDocument();
+ expect(fetchMock.callHistory.calls(SAMPLES_ENDPOINT)).toHaveLength(2);
+});