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;

Reply via email to