feat: Add confirmation modal for unsaved changes (#33809)

This commit is contained in:
Gabriel Torres Ruiz
2025-07-01 13:38:51 -03:00
committed by GitHub
parent 050ccdcb3d
commit 057218d87f
28 changed files with 1799 additions and 489 deletions

View File

@@ -23,6 +23,7 @@ import {
screen,
userEvent,
waitFor,
within,
} from 'spec/helpers/testing-library';
import fetchMock from 'fetch-mock';
import * as chartAction from 'src/components/Chart/chartAction';
@@ -30,6 +31,7 @@ import * as saveModalActions from 'src/explore/actions/saveModalActions';
import * as downloadAsImage from 'src/utils/downloadAsImage';
import * as exploreUtils from 'src/explore/exploreUtils';
import { FeatureFlag, VizType } from '@superset-ui/core';
import { useUnsavedChangesPrompt } from 'src/hooks/useUnsavedChangesPrompt';
import ExploreHeader from '.';
const chartEndpoint = 'glob:*api/v1/chart/*';
@@ -40,6 +42,10 @@ window.featureFlags = {
[FeatureFlag.EmbeddableCharts]: true,
};
jest.mock('src/hooks/useUnsavedChangesPrompt', () => ({
useUnsavedChangesPrompt: jest.fn(),
}));
const createProps = (additionalProps = {}) => ({
chart: {
id: 1,
@@ -134,6 +140,18 @@ fetchMock.post(
describe('ExploreChartHeader', () => {
jest.setTimeout(15000); // ✅ Applies to all tests in this suite
beforeEach(() => {
jest.clearAllMocks();
(useUnsavedChangesPrompt as jest.Mock).mockReturnValue({
showModal: false,
setShowModal: jest.fn(),
handleConfirmNavigation: jest.fn(),
handleSaveAndCloseModal: jest.fn(),
triggerManualSave: jest.fn(),
});
});
test('Cancelling changes to the properties should reset previous properties', async () => {
const props = createProps();
render(<ExploreHeader {...props} />, { useRedux: true });
@@ -201,35 +219,173 @@ describe('ExploreChartHeader', () => {
});
test('Save chart', async () => {
const setSaveChartModalVisibility = jest.spyOn(
const setSaveChartModalVisibilitySpy = jest.spyOn(
saveModalActions,
'setSaveChartModalVisibility',
);
const setSaveChartModalVisibilityMock =
setSaveChartModalVisibilitySpy as jest.Mock;
const triggerManualSave = jest.fn(() => {
setSaveChartModalVisibilityMock(true);
});
(useUnsavedChangesPrompt as jest.Mock).mockReturnValue({
showModal: false,
setShowModal: jest.fn(),
handleConfirmNavigation: jest.fn(),
handleSaveAndCloseModal: jest.fn(),
triggerManualSave,
});
const props = createProps();
render(<ExploreHeader {...props} />, { useRedux: true });
expect(await screen.findByText('Save')).toBeInTheDocument();
userEvent.click(screen.getByText('Save'));
expect(setSaveChartModalVisibility).toHaveBeenCalledWith(true);
setSaveChartModalVisibility.mockClear();
const saveButton: HTMLElement = await screen.findByRole('button', {
name: /save/i,
});
userEvent.click(saveButton);
expect(triggerManualSave).toHaveBeenCalled();
expect(setSaveChartModalVisibilityMock).toHaveBeenCalledWith(true);
setSaveChartModalVisibilityMock.mockClear();
});
test('Save disabled', async () => {
const setSaveChartModalVisibility = jest.spyOn(
saveModalActions,
'setSaveChartModalVisibility',
);
const triggerManualSave = jest.fn();
(useUnsavedChangesPrompt as jest.Mock).mockReturnValue({
showModal: false,
setShowModal: jest.fn(),
handleConfirmNavigation: jest.fn(),
handleSaveAndCloseModal: jest.fn(),
triggerManualSave,
});
const props = createProps();
render(<ExploreHeader {...props} saveDisabled />, { useRedux: true });
expect(await screen.findByText('Save')).toBeInTheDocument();
userEvent.click(screen.getByText('Save'));
expect(setSaveChartModalVisibility).not.toHaveBeenCalled();
setSaveChartModalVisibility.mockClear();
const saveButton: HTMLElement = await screen.findByRole('button', {
name: /save/i,
});
expect(saveButton).toBeDisabled();
userEvent.click(saveButton);
expect(triggerManualSave).not.toHaveBeenCalled();
});
test('should render UnsavedChangesModal when showModal is true', async () => {
const props = createProps();
(useUnsavedChangesPrompt as jest.Mock).mockReturnValue({
showModal: true,
setShowModal: jest.fn(),
handleConfirmNavigation: jest.fn(),
handleSaveAndCloseModal: jest.fn(),
triggerManualSave: jest.fn(),
});
render(<ExploreHeader {...props} />, { useRedux: true });
expect(await screen.findByRole('dialog')).toBeInTheDocument();
expect(
await screen.findByText('Save changes to your chart?'),
).toBeInTheDocument();
expect(
await screen.findByText("If you don't save, changes will be lost."),
).toBeInTheDocument();
});
test('should call handleSaveAndCloseModal when clicking Save in UnsavedChangesModal', async () => {
const handleSaveAndCloseModal = jest.fn();
(useUnsavedChangesPrompt as jest.Mock).mockReturnValue({
showModal: true,
setShowModal: jest.fn(),
handleConfirmNavigation: jest.fn(),
handleSaveAndCloseModal,
triggerManualSave: jest.fn(),
});
const props = createProps();
render(<ExploreHeader {...props} />, { useRedux: true });
const modal: HTMLElement = await screen.findByRole('dialog');
const saveButton: HTMLElement = within(modal).getByRole('button', {
name: /save/i,
});
userEvent.click(saveButton);
expect(handleSaveAndCloseModal).toHaveBeenCalled();
});
test('should call handleConfirmNavigation when clicking Discard in UnsavedChangesModal', async () => {
const handleConfirmNavigation = jest.fn();
(useUnsavedChangesPrompt as jest.Mock).mockReturnValue({
showModal: true,
setShowModal: jest.fn(),
handleConfirmNavigation,
handleSaveAndCloseModal: jest.fn(),
triggerManualSave: jest.fn(),
});
const props = createProps();
render(<ExploreHeader {...props} />, { useRedux: true });
const modal: HTMLElement = await screen.findByRole('dialog');
const discardButton: HTMLElement = within(modal).getByRole('button', {
name: /discard/i,
});
userEvent.click(discardButton);
expect(handleConfirmNavigation).toHaveBeenCalled();
});
test('should call setShowModal(false) when clicking close button in UnsavedChangesModal', async () => {
const setShowModal = jest.fn();
(useUnsavedChangesPrompt as jest.Mock).mockReturnValue({
showModal: true,
setShowModal,
handleConfirmNavigation: jest.fn(),
handleSaveAndCloseModal: jest.fn(),
triggerManualSave: jest.fn(),
});
const props = createProps();
render(<ExploreHeader {...props} />, { useRedux: true });
const closeButton: HTMLElement = await screen.findByRole('button', {
name: /close/i,
});
userEvent.click(closeButton);
expect(setShowModal).toHaveBeenCalledWith(false);
});
});
describe('Additional actions tests', () => {
jest.setTimeout(15000); // ✅ Applies to all tests in this suite
beforeEach(() => {
(useUnsavedChangesPrompt as jest.Mock).mockReturnValue({
showModal: false,
setShowModal: jest.fn(),
handleConfirmNavigation: jest.fn(),
handleSaveAndCloseModal: jest.fn(),
triggerManualSave: jest.fn(),
});
});
test('Should render a button', async () => {
const props = createProps();
render(<ExploreHeader {...props} />, { useRedux: true });
@@ -356,6 +512,14 @@ describe('Additional actions tests', () => {
beforeEach(() => {
spyDownloadAsImage = sinon.spy(downloadAsImage, 'default');
spyExportChart = sinon.spy(exploreUtils, 'exportChart');
(useUnsavedChangesPrompt as jest.Mock).mockReturnValue({
showModal: false,
setShowModal: jest.fn(),
handleConfirmNavigation: jest.fn(),
handleSaveAndCloseModal: jest.fn(),
triggerManualSave: jest.fn(),
});
});
afterEach(async () => {