diff --git a/packages/js/src/shared-admin/components/unsaved-changes-modal.js b/packages/js/src/shared-admin/components/unsaved-changes-modal.js index 0113e60829f..e410118f836 100644 --- a/packages/js/src/shared-admin/components/unsaved-changes-modal.js +++ b/packages/js/src/shared-admin/components/unsaved-changes-modal.js @@ -1,6 +1,6 @@ -import { __ } from "@wordpress/i18n"; -import { Modal, Button, useSvgAria } from "@yoast/ui-library"; import { ExclamationIcon } from "@heroicons/react/outline"; +import { __ } from "@wordpress/i18n"; +import { Button, Modal, useSvgAria } from "@yoast/ui-library"; import { noop } from "lodash"; import PropTypes from "prop-types"; @@ -8,16 +8,16 @@ import PropTypes from "prop-types"; * The unsaved changes modal. * * @param {boolean} isOpen Whether the modal is open. - * @param {Function} onClose The function to call when the modal is closed. + * @param {function} [onClose] The function to call when the modal is closed. + * @param {function} [onDiscard] The function to call when the changes are discarded. * @param {string} title The title of the modal. * @param {string} description The description of the modal. - * @param {Function} onDiscard The function to call when the changes are discarded. * @param {string} dismissLabel The label for the dismiss button. * @param {string} discardLabel The label for the discard button. * * @returns {JSX.Element} The unsaved changes modal. */ -export const UnsavedChangesModal = ( { isOpen, onClose = noop, title, description, onDiscard, dismissLabel, discardLabel } ) => { +export const UnsavedChangesModal = ( { isOpen, onClose = noop, onDiscard = noop, title, description, dismissLabel, discardLabel } ) => { const svgAriaProps = useSvgAria(); return @@ -52,9 +52,9 @@ export const UnsavedChangesModal = ( { isOpen, onClose = noop, title, descriptio UnsavedChangesModal.propTypes = { isOpen: PropTypes.bool.isRequired, onClose: PropTypes.func, + onDiscard: PropTypes.func, title: PropTypes.string.isRequired, description: PropTypes.string.isRequired, - onDiscard: PropTypes.func.isRequired, dismissLabel: PropTypes.string.isRequired, discardLabel: PropTypes.string.isRequired, }; diff --git a/packages/js/tests/shared-admin/components/unsaved-changes-modal.test.js b/packages/js/tests/shared-admin/components/unsaved-changes-modal.test.js index 677167fee95..c5221ce2304 100644 --- a/packages/js/tests/shared-admin/components/unsaved-changes-modal.test.js +++ b/packages/js/tests/shared-admin/components/unsaved-changes-modal.test.js @@ -1,36 +1,91 @@ -import { render, screen } from "../../test-utils"; +import { beforeEach, describe, expect, it, jest } from "@jest/globals"; import { UnsavedChangesModal } from "../../../src/shared-admin/components"; +import { fireEvent, render, screen } from "../../test-utils"; describe( "UnsavedChangesModal", () => { + const onClose = jest.fn(); + const onDiscard = jest.fn(); + beforeEach( () => { + jest.clearAllMocks(); render( ); } ); - it( "should have dismiss and leave page buttons", () => { - const leaveButton = screen.queryByText( "Yes, leave page" ); - expect( leaveButton ).toBeInTheDocument(); + it( "should have a role of dialog", () => { + expect( screen.queryByRole( "dialog" ) ).toBeInTheDocument(); + } ); - expect( screen.queryByText( "Close" ) ).toBeInTheDocument(); + it.each( [ + [ "dismiss", "No, continue editing" ], + [ "discard", "Yes, leave page" ], + ] )( "should have the %s button: %s", ( _, text ) => { + const button = screen.queryByText( text ); + expect( button ).toBeInTheDocument(); + expect( button.tagName ).toBe( "BUTTON" ); + expect( button ).toHaveAttribute( "type", "button" ); + } ); - const dismissButton = screen.queryByText( "No, continue editing" ); - expect( dismissButton ).toBeInTheDocument(); + it( "should have the close button", () => { + const screenReaderText = screen.queryByText( "Close" ); + expect( screenReaderText ).toBeInTheDocument(); + expect( screenReaderText.parentElement ).toHaveAttribute( "type", "button" ); + expect( screenReaderText.parentElement.tagName ).toBe( "BUTTON" ); } ); it( "should have a title", () => { const title = screen.queryByText( "Unsaved changes" ); expect( title ).toBeInTheDocument(); + expect( title.tagName ).toBe( "H1" ); } ); it( "should have a description", () => { - const title = screen.queryByText( "There are unsaved changes in one or more steps of the first-time configuration. Leaving means that those changes will be lost. Are you sure you want to leave this page?" ); - expect( title ).toBeInTheDocument(); + const description = screen.queryByText( "There are unsaved changes in one or more steps of the first-time configuration. Leaving means that those changes will be lost. Are you sure you want to leave this page?" ); + expect( description ).toBeInTheDocument(); + expect( description.tagName ).toBe( "P" ); + } ); + + it( "should call onClose when clicking the close button", () => { + fireEvent.click( screen.queryByText( "Close" ) ); + expect( onClose ).toHaveBeenCalled(); + } ); + + it( "should call onClose when clicking the continue editing button", () => { + fireEvent.click( screen.queryByText( "No, continue editing" ) ); + expect( onClose ).toHaveBeenCalled(); + } ); + + it( "should call onDiscard when clicking the leave page button", () => { + fireEvent.click( screen.queryByText( "Yes, leave page" ) ); + expect( onDiscard ).toHaveBeenCalled(); + } ); + + describe( "fallback props", () => { + it( "should not throw an error when the onClose and onDiscard props are not passed", () => { + const currentImplementation = console.error; + console.error = jest.fn(); + + render( ); + + try { + expect( console.error ).not.toHaveBeenCalled(); + } finally { + // Cleanup. + console.error = currentImplementation; + } + } ); } ); } );