feat: handle unsaved changes in text & problem editors (#1444)

The text & problem xblock editors will display a confirmation box before
cancelling only if user has changed something else it will directly go
back.
This commit is contained in:
Navin Karkera
2024-11-04 23:11:00 +05:30
committed by GitHub
parent 949e4ac94c
commit df8a65dc4e
23 changed files with 353 additions and 76 deletions

View File

@@ -1,21 +1,15 @@
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { act } from 'react-dom/test-utils';
import PromptIfDirty from './PromptIfDirty';
import { renderHook } from '@testing-library/react-hooks';
import usePromptIfDirty from './usePromptIfDirty';
describe('PromptIfDirty', () => {
let container = null;
describe('usePromptIfDirty', () => {
let mockEvent = null;
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
mockEvent = new Event('beforeunload');
jest.spyOn(window, 'addEventListener');
jest.spyOn(window, 'removeEventListener');
jest.spyOn(mockEvent, 'preventDefault');
Object.defineProperty(mockEvent, 'returnValue', { writable: true });
mockEvent.returnValue = '';
});
afterEach(() => {
@@ -23,49 +17,32 @@ describe('PromptIfDirty', () => {
window.removeEventListener.mockRestore();
mockEvent.preventDefault.mockRestore();
mockEvent = null;
unmountComponentAtNode(container);
container.remove();
container = null;
});
it('should add event listener on mount', () => {
act(() => {
render(<PromptIfDirty dirty />, container);
});
renderHook(() => usePromptIfDirty(() => true));
expect(window.addEventListener).toHaveBeenCalledWith('beforeunload', expect.any(Function));
});
it('should remove event listener on unmount', () => {
act(() => {
render(<PromptIfDirty dirty />, container);
});
act(() => {
unmountComponentAtNode(container);
});
const { unmount } = renderHook(() => usePromptIfDirty(() => true));
unmount();
expect(window.removeEventListener).toHaveBeenCalledWith('beforeunload', expect.any(Function));
});
it('should call preventDefault and set returnValue when dirty is true', () => {
act(() => {
render(<PromptIfDirty dirty />, container);
});
act(() => {
window.dispatchEvent(mockEvent);
});
renderHook(() => usePromptIfDirty(() => true));
window.dispatchEvent(mockEvent);
expect(mockEvent.preventDefault).toHaveBeenCalled();
expect(mockEvent.returnValue).toBe('');
expect(mockEvent.returnValue).toBe(true);
});
it('should not call preventDefault when dirty is false', () => {
act(() => {
render(<PromptIfDirty dirty={false} />, container);
});
act(() => {
window.dispatchEvent(mockEvent);
});
renderHook(() => usePromptIfDirty(() => false));
window.dispatchEvent(mockEvent);
expect(mockEvent.preventDefault).not.toHaveBeenCalled();
});

View File

@@ -1,12 +1,13 @@
import { useEffect } from 'react';
import PropTypes from 'prop-types';
const PromptIfDirty = ({ dirty }) => {
const usePromptIfDirty = (checkIfDirty : () => boolean) => {
useEffect(() => {
// eslint-disable-next-line consistent-return
const handleBeforeUnload = (event) => {
if (dirty) {
if (checkIfDirty()) {
event.preventDefault();
// Included for legacy support, e.g. Chrome/Edge < 119
event.returnValue = true; // eslint-disable-line no-param-reassign
}
};
window.addEventListener('beforeunload', handleBeforeUnload);
@@ -14,11 +15,9 @@ const PromptIfDirty = ({ dirty }) => {
return () => {
window.removeEventListener('beforeunload', handleBeforeUnload);
};
}, [dirty]);
}, [checkIfDirty]);
return null;
};
PromptIfDirty.propTypes = {
dirty: PropTypes.bool.isRequired,
};
export default PromptIfDirty;
export default usePromptIfDirty;