You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by jl...@apache.org on 2026/09/25 22:26:56 UTC
(superset) branch test/dashboard-header-controls created (now 49a9d414ff3)
This is an automated email from the ASF dual-hosted git repository.
sadpandajoe pushed a change to branch test/dashboard-header-controls
in repository https://gitbox.apache.org/repos/asf/superset.git
at 49a9d414ff3 test(dashboard): cover header refresh controls while charts load
This branch includes the following new commits:
new 49a9d414ff3 test(dashboard): cover header refresh controls while charts load
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(superset) 01/01: test(dashboard): cover header refresh controls while charts load
Posted by jl...@apache.org.
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 <jo...@preset.io>
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 <no...@anthropic.com>
---
.../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;