This is an automated email from the ASF dual-hosted git repository. sadpandajoe pushed a commit to branch test/dashboard-header-controls in repository https://gitbox.apache.org/repos/asf/superset.git
commit 49a9d414ff3ce942b73927dece9775e3f7708e00 Author: Joe Li <[email protected]> AuthorDate: Fri Sep 25 15:26:25 2026 -0700 test(dashboard): cover header refresh controls while charts load - chart-level "Force refresh" is disabled while its chart loads and becomes enabled after the load finishes - dashboard header "Refresh dashboard" and auto-refresh items are disabled while charts load and re-enable once loading completes - header actions menu visibility across view/edit mode and edit/save permissions, including version history Co-Authored-By: Claude Opus 5.5 <[email protected]> --- .../dashboard/components/Header/Header.test.tsx | 143 +++++++++++++++++++++ .../SliceHeaderControls.test.tsx | 43 +++++++ 2 files changed, 186 insertions(+) diff --git a/superset-frontend/src/dashboard/components/Header/Header.test.tsx b/superset-frontend/src/dashboard/components/Header/Header.test.tsx index ce68b3054c2..1525280ae8d 100644 --- a/superset-frontend/src/dashboard/components/Header/Header.test.tsx +++ b/superset-frontend/src/dashboard/components/Header/Header.test.tsx @@ -34,6 +34,7 @@ import Header from '.'; import { DASHBOARD_HEADER_ID } from '../../util/constants'; import { UPDATE_COMPONENTS } from '../../actions/dashboardLayout'; import { AutoRefreshStatus } from '../../types/autoRefresh'; +import { chartUpdateSucceeded } from 'src/components/Chart/chartAction'; const mockHistoryReplace = jest.fn(); // Dashboards render top-level (not iframed) unless a test says otherwise. @@ -741,6 +742,76 @@ test('should refresh the charts', async () => { expect(onRefresh).toHaveBeenCalledTimes(1); }); +test('"Refresh dashboard" and "Set auto-refresh" are disabled while charts are loading', async () => { + setup({ + dashboardState: { + ...initialState.dashboardState, + sliceIds: [1], + }, + charts: { + 1: { chartUpdateStartTime: 2, chartUpdateEndTime: 1 }, + }, + }); + await openActionsDropdown(); + const refreshItem = screen + .getByText('Refresh dashboard') + .closest('[role="menuitem"]'); + const autoRefreshItem = screen + .getByText('Set auto-refresh') + .closest('[role="menuitem"]'); + expect(refreshItem).toHaveAttribute('aria-disabled', 'true'); + expect(autoRefreshItem).toHaveAttribute('aria-disabled', 'true'); + await userEvent.click(screen.getByText('Refresh dashboard')); + expect(onRefresh).not.toHaveBeenCalled(); +}); + +test('"Refresh dashboard" and "Set auto-refresh" become enabled once a loading chart finishes', async () => { + // Mount with a chart still loading, then transition the same rendered tree + // to finished via the real chart-update-succeeded action, so the test + // catches a menu that stays disabled after the loading->finished switch + // (a fresh "already finished" mount would not exercise that transition). + const testStore = createStore( + { + ...initialState, + dashboardState: { + ...initialState.dashboardState, + sliceIds: [1], + }, + charts: { + 1: { chartUpdateStartTime: 2, chartUpdateEndTime: 1 }, + }, + }, + reducerIndex, + ); + + render( + <div className="dashboard"> + <Header /> + </div>, + { + useRedux: true, + useTheme: true, + store: testStore, + }, + ); + + await openActionsDropdown(); + expect( + screen.getByText('Refresh dashboard').closest('[role="menuitem"]'), + ).toHaveAttribute('aria-disabled', 'true'); + + testStore.dispatch(chartUpdateSucceeded([], 1)); + + await waitFor(() => { + expect( + screen.getByText('Refresh dashboard').closest('[role="menuitem"]'), + ).not.toHaveAttribute('aria-disabled', 'true'); + }); + expect( + screen.getByText('Set auto-refresh').closest('[role="menuitem"]'), + ).not.toHaveAttribute('aria-disabled', 'true'); +}); + test('auto-refresh uses onRefresh with skipped filters and toggles refresh state', async () => { jest.useFakeTimers({ advanceTimers: true }); onRefresh.mockResolvedValue(undefined); @@ -1208,3 +1279,75 @@ test('share URL should use browser-absolute pathname to preserve subdirectory pr expect(emailLink.getAttribute('href')).toMatch(/\/pcs\/dashboard/); } }); + +const headerActionsMenuVisibilityCases = [ + { + name: 'view mode with no edit or save permission', + stateOverrides: {}, + visible: ['Refresh dashboard', 'Set auto-refresh'], + hidden: ['Edit properties', 'Save as', 'View version history'], + }, + { + name: 'view mode with save permission', + stateOverrides: { + dashboardInfo: { ...initialState.dashboardInfo, dash_save_perm: true }, + }, + visible: ['Refresh dashboard', 'Set auto-refresh', 'Save as'], + hidden: ['Edit properties', 'View version history'], + }, + { + name: 'view mode with edit permission and version history enabled', + stateOverrides: { + dashboardInfo: { ...initialState.dashboardInfo, dash_edit_perm: true }, + }, + featureFlags: { VERSION_HISTORY: true }, + visible: ['Refresh dashboard', 'Set auto-refresh', 'View version history'], + hidden: ['Edit properties', 'Save as'], + }, + { + name: 'view mode with edit and save permission and version history enabled', + stateOverrides: { + dashboardInfo: { + ...initialState.dashboardInfo, + dash_edit_perm: true, + dash_save_perm: true, + }, + }, + featureFlags: { VERSION_HISTORY: true }, + visible: ['Save as', 'View version history'], + hidden: ['Edit properties'], + }, + { + name: 'edit mode with edit and save permission', + stateOverrides: { + dashboardState: { ...initialState.dashboardState, editMode: true }, + dashboardInfo: { + ...initialState.dashboardInfo, + dash_edit_perm: true, + dash_save_perm: true, + }, + }, + featureFlags: { VERSION_HISTORY: true }, + // Edit properties and Save as are authoring actions available in edit + // mode; Refresh dashboard, auto-refresh, and version history are + // consumption-only actions hidden while editing. + visible: ['Edit properties', 'Save as'], + hidden: ['Refresh dashboard', 'Set auto-refresh', 'View version history'], + }, +]; + +test.each(headerActionsMenuVisibilityCases)( + 'header actions menu shows the right items: $name', + async ({ stateOverrides, featureFlags, visible, hidden }) => { + window.featureFlags = featureFlags ?? {}; + setup(stateOverrides); + await openActionsDropdown(); + visible.forEach(label => { + expect(screen.getByText(label)).toBeInTheDocument(); + }); + hidden.forEach(label => { + expect(screen.queryByText(label)).not.toBeInTheDocument(); + }); + window.featureFlags = {}; + }, +); diff --git a/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx b/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx index 0141944b0ca..b0d37fa2445 100644 --- a/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx +++ b/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx @@ -402,6 +402,49 @@ test('Should "Force refresh"', async () => { expect(props.addSuccessToast).toHaveBeenCalledTimes(1); }); +test('"Force refresh" is disabled while the chart is loading', async () => { + const props = createProps(); + props.chartStatus = 'loading'; + renderWrapper(props); + await openMenu(); + const refreshItem = screen + .getByText('Force refresh') + .closest('[role="menuitem"]'); + expect(refreshItem).toHaveAttribute('aria-disabled', 'true'); + await userEvent.click(screen.getByText('Force refresh')); + expect(props.forceRefresh).not.toHaveBeenCalled(); +}); + +test('"Force refresh" becomes enabled after the chart transitions from loading to done', async () => { + const props = createProps(); + props.chartStatus = 'loading'; + const { rerender } = renderWrapper(props); + await openMenu(); + expect( + screen.getByText('Force refresh').closest('[role="menuitem"]'), + ).toHaveAttribute('aria-disabled', 'true'); + + rerender(<SliceHeaderControls {...props} chartStatus="success" />); + + const refreshItemAfterLoad = screen + .getByText('Force refresh') + .closest('[role="menuitem"]'); + expect(refreshItemAfterLoad).not.toHaveAttribute('aria-disabled', 'true'); + await userEvent.click(screen.getByText('Force refresh')); + expect(props.forceRefresh).toHaveBeenCalledTimes(1); +}); + +test('"Force refresh" is enabled once the chart is done loading', async () => { + const props = createProps(); + props.chartStatus = 'rendered'; + renderWrapper(props); + await openMenu(); + const refreshItem = screen + .getByText('Force refresh') + .closest('[role="menuitem"]'); + expect(refreshItem).not.toHaveAttribute('aria-disabled', 'true'); +}); + test('Should sync local state after entering fullscreen', async () => { const mockDiv = document.createElement('div'); let fullscreenElement: Element | null = null;
