This is an automated email from the ASF dual-hosted git repository.

sadpandajoe pushed a commit to branch test/dashboard-header-modals
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 78585a55b809d703945edc79f18df8cee724d5e3
Author: Joe Li <[email protected]>
AuthorDate: Fri Sep 25 15:37:52 2026 -0700

    test(dashboard): cover save and refresh interval modals
    
    - SaveModal: overwrite vs save-as payloads, the default "[copy]" name,
      editing the name switching to save-as, duplicate_slices, the empty
      name guard, overwrite disabled without permission, and cancel
    - RefreshIntervalModal: persisted vs session-only save, the minimum
      refresh limit blocking save, and cancel restoring the original
      frequency
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../components/RefreshIntervalModal.test.tsx       | 114 +++++++++++++
 .../src/dashboard/components/SaveModal.test.tsx    | 178 +++++++++++++++++++++
 2 files changed, 292 insertions(+)

diff --git 
a/superset-frontend/src/dashboard/components/RefreshIntervalModal.test.tsx 
b/superset-frontend/src/dashboard/components/RefreshIntervalModal.test.tsx
new file mode 100644
index 00000000000..54423d55777
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/RefreshIntervalModal.test.tsx
@@ -0,0 +1,114 @@
+/**
+ * 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, screen, userEvent } from 'spec/helpers/testing-library';
+import RefreshIntervalModal from 
'src/dashboard/components/RefreshIntervalModal';
+
+const defaultProps = {
+  show: true,
+  onHide: jest.fn(),
+  refreshFrequency: 60,
+  onChange: jest.fn(),
+  editMode: true,
+  addSuccessToast: jest.fn(),
+  pauseOnInactiveTab: false,
+  onPauseOnInactiveTabChange: jest.fn(),
+};
+
+const setup = (
+  props: Partial<typeof defaultProps> = {},
+  refreshLimitConf: Record<string, unknown> = {},
+) =>
+  render(<RefreshIntervalModal {...defaultProps} {...props} />, {
+    useRedux: true,
+    initialState: {
+      dashboardInfo: { common: { conf: refreshLimitConf } },
+    },
+  });
+
+beforeEach(() => {
+  jest.clearAllMocks();
+});
+
+test('selecting an interval and saving persists it in edit mode', async () => {
+  setup();
+
+  await userEvent.click(screen.getByRole('radio', { name: '5 minutes' }));
+  await userEvent.click(screen.getByRole('button', { name: 'Save' }));
+
+  expect(defaultProps.onChange).toHaveBeenCalledWith(300, true);
+  expect(defaultProps.onPauseOnInactiveTabChange).toHaveBeenCalledWith(false);
+  expect(defaultProps.onHide).toHaveBeenCalledTimes(1);
+  expect(defaultProps.addSuccessToast).toHaveBeenCalledWith(
+    'Refresh interval saved',
+  );
+});
+
+test('saving outside edit mode reports a session-only save', async () => {
+  setup({ editMode: false });
+
+  await userEvent.click(
+    screen.getByRole('button', { name: 'Save for this session' }),
+  );
+
+  expect(defaultProps.onChange).toHaveBeenCalledWith(60, false);
+  expect(defaultProps.addSuccessToast).toHaveBeenCalledWith(
+    'Refresh interval set for this session',
+  );
+});
+
+test('an interval below the configured limit blocks save with an error', async 
() => {
+  setup({}, { SUPERSET_DASHBOARD_PERIODICAL_REFRESH_LIMIT: 60 });
+
+  await userEvent.click(screen.getByRole('radio', { name: '10 seconds' }));
+
+  expect(
+    screen.getByText('Refresh frequency must be at least 60 seconds'),
+  ).toBeInTheDocument();
+  expect(screen.getByRole('button', { name: 'Save' })).toBeDisabled();
+
+  await userEvent.click(screen.getByRole('button', { name: 'Save' }));
+  expect(defaultProps.onChange).not.toHaveBeenCalled();
+});
+
+test('an interval at or above the configured limit does not block save', async 
() => {
+  setup({}, { SUPERSET_DASHBOARD_PERIODICAL_REFRESH_LIMIT: 60 });
+
+  await userEvent.click(screen.getByRole('radio', { name: '5 minutes' }));
+
+  expect(
+    screen.queryByText(/Refresh frequency must be at least/),
+  ).not.toBeInTheDocument();
+  expect(screen.getByRole('button', { name: 'Save' })).toBeEnabled();
+});
+
+test('cancel resets the selection back to the original frequency and does not 
save', async () => {
+  // defaultProps.refreshFrequency (60) maps to the "1 minute" preset.
+  setup();
+  expect(screen.getByRole('radio', { name: '1 minute' })).toBeChecked();
+
+  await userEvent.click(screen.getByRole('radio', { name: '1 hour' }));
+  expect(screen.getByRole('radio', { name: '1 hour' })).toBeChecked();
+
+  await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
+
+  expect(defaultProps.onChange).not.toHaveBeenCalled();
+  expect(defaultProps.onHide).toHaveBeenCalledTimes(1);
+  expect(screen.getByRole('radio', { name: '1 minute' })).toBeChecked();
+  expect(screen.getByRole('radio', { name: '1 hour' })).not.toBeChecked();
+});
diff --git a/superset-frontend/src/dashboard/components/SaveModal.test.tsx 
b/superset-frontend/src/dashboard/components/SaveModal.test.tsx
new file mode 100644
index 00000000000..1621728dcca
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/SaveModal.test.tsx
@@ -0,0 +1,178 @@
+/**
+ * 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, screen, userEvent } from 'spec/helpers/testing-library';
+import SaveModal from 'src/dashboard/components/SaveModal';
+import {
+  SAVE_TYPE_OVERWRITE,
+  SAVE_TYPE_NEWDASHBOARD,
+} from 'src/dashboard/util/constants';
+
+const dashboardTitle = 'Sales Dashboard';
+
+const defaultProps = {
+  addSuccessToast: jest.fn(),
+  addDangerToast: jest.fn(),
+  dashboardId: 1,
+  dashboardTitle,
+  dashboardInfo: {
+    certified_by: '',
+    certification_details: '',
+    editors: [],
+    metadata: { refresh_frequency: 0 },
+  },
+  expandedSlices: {},
+  layout: { ROOT_ID: {} },
+  // SaveModal does not export its SaveType union; the inline cast (rather
+  // than a `const` alias, which TS would narrow back to the literal type at
+  // every use) keeps this assignable to both save types in prop overrides.
+  saveType: SAVE_TYPE_OVERWRITE as
+    | typeof SAVE_TYPE_OVERWRITE
+    | typeof SAVE_TYPE_NEWDASHBOARD,
+  triggerNode: <span>Open save modal</span>,
+  customCss: '',
+  onSave: jest.fn(),
+  canOverwrite: true,
+  shouldPersistRefreshFrequency: false,
+  refreshFrequency: 30,
+  lastModifiedTime: 1700000000,
+};
+
+const setup = (props: Partial<typeof defaultProps> = {}) =>
+  render(<SaveModal {...defaultProps} {...props} />);
+
+const openModal = async (props: Partial<typeof defaultProps> = {}) => {
+  setup(props);
+  await userEvent.click(screen.getByText('Open save modal'));
+  return screen.findByRole('dialog');
+};
+
+beforeEach(() => {
+  jest.clearAllMocks();
+});
+
+test('saving in overwrite mode calls onSave with the overwrite save type', 
async () => {
+  await openModal();
+
+  await userEvent.click(screen.getByTestId('modal-save-dashboard-button'));
+
+  expect(defaultProps.onSave).toHaveBeenCalledTimes(1);
+  const [data, id, saveType] = defaultProps.onSave.mock.calls[0];
+  expect(saveType).toBe(SAVE_TYPE_OVERWRITE);
+  expect(id).toBe(defaultProps.dashboardId);
+  expect(data.dashboard_title).toBe(dashboardTitle);
+});
+
+test('defaults the new dashboard name to "<title> [copy]"', async () => {
+  await openModal();
+
+  expect(screen.getByPlaceholderText('[dashboard name]')).toHaveValue(
+    `${dashboardTitle} [copy]`,
+  );
+});
+
+test('saving in save-as mode without editing the name uses the default copy 
name', async () => {
+  await openModal({ saveType: SAVE_TYPE_NEWDASHBOARD });
+
+  await userEvent.click(screen.getByTestId('modal-save-dashboard-button'));
+
+  expect(defaultProps.onSave).toHaveBeenCalledTimes(1);
+  const [data, , saveType] = defaultProps.onSave.mock.calls[0];
+  expect(saveType).toBe(SAVE_TYPE_NEWDASHBOARD);
+  expect(data.dashboard_title).toMatch(/\[copy\]$/);
+});
+
+test('editing the new dashboard name switches to save-as and is used in the 
payload', async () => {
+  await openModal();
+
+  const nameInput = screen.getByPlaceholderText('[dashboard name]');
+  await userEvent.clear(nameInput);
+  await userEvent.type(nameInput, 'Copied Dashboard');
+
+  await userEvent.click(screen.getByTestId('modal-save-dashboard-button'));
+
+  expect(defaultProps.onSave).toHaveBeenCalledTimes(1);
+  const [data, , saveType] = defaultProps.onSave.mock.calls[0];
+  expect(saveType).toBe(SAVE_TYPE_NEWDASHBOARD);
+  expect(data.dashboard_title).toBe('Copied Dashboard');
+});
+
+test('save-as payload includes duplicate_slices when the checkbox is checked', 
async () => {
+  await openModal();
+
+  await userEvent.click(screen.getByRole('radio', { name: /Save as:/ }));
+  await userEvent.click(
+    screen.getByRole('checkbox', { name: /also copy \(duplicate\) charts/ }),
+  );
+
+  await userEvent.click(screen.getByTestId('modal-save-dashboard-button'));
+
+  const [data] = defaultProps.onSave.mock.calls[0];
+  expect(data.duplicate_slices).toBe(true);
+});
+
+test('overwrite payload does not duplicate slices by default', async () => {
+  await openModal();
+
+  await userEvent.click(screen.getByTestId('modal-save-dashboard-button'));
+
+  const [data] = defaultProps.onSave.mock.calls[0];
+  expect(data.duplicate_slices).toBe(false);
+});
+
+test('blocks save and warns when the new dashboard name is empty', async () => 
{
+  await openModal();
+
+  const nameInput = screen.getByPlaceholderText('[dashboard name]');
+  await userEvent.clear(nameInput);
+
+  await userEvent.click(screen.getByTestId('modal-save-dashboard-button'));
+
+  expect(defaultProps.addDangerToast).toHaveBeenCalledWith(
+    'You must pick a name for the new dashboard',
+  );
+  expect(defaultProps.onSave).not.toHaveBeenCalled();
+  expect(screen.getByRole('dialog')).toBeInTheDocument();
+});
+
+test('disables overwrite and saves as a new dashboard when the user cannot 
overwrite', async () => {
+  await openModal({
+    canOverwrite: false,
+    saveType: SAVE_TYPE_NEWDASHBOARD,
+  });
+
+  expect(
+    screen.getByRole('radio', {
+      name: new RegExp(`Overwrite Dashboard`),
+    }),
+  ).toBeDisabled();
+
+  await userEvent.click(screen.getByTestId('modal-save-dashboard-button'));
+
+  const [, , saveType] = defaultProps.onSave.mock.calls[0];
+  expect(saveType).toBe(SAVE_TYPE_NEWDASHBOARD);
+});
+
+test('cancel closes the modal without calling onSave', async () => {
+  await openModal();
+
+  await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
+
+  expect(defaultProps.onSave).not.toHaveBeenCalled();
+  expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
+});

Reply via email to