import React, { useState } from 'react'; import { act, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { axe } from 'jest-axe'; import { getLgIds as getLgModalIds } from '@leafygreen-ui/modal'; import { getLgIds } from '..'; import { ConfirmationModal, Variant } from '..'; const lgIds = getLgIds(); const WrappedModal = ({ open: initialOpen, ...props }: Partial>) => { const [open, setOpen] = useState(initialOpen); return ( setOpen(false)} onCancel={() => setOpen(false)} {...props} > {props.children ?? 'Content text'} ); }; function renderModal( props: Partial> = {}, ) { return render(); } describe('packages/confirmation-modal', () => { // Mock dialog methods for JSDOM environment beforeAll(() => { HTMLDialogElement.prototype.show = jest.fn(function mock( this: HTMLDialogElement, ) { this.open = true; }); HTMLDialogElement.prototype.showModal = jest.fn(function mock( this: HTMLDialogElement, ) { this.open = true; }); HTMLDialogElement.prototype.close = jest.fn(function mock( this: HTMLDialogElement, ) { this.open = false; }); }); describe('a11y', () => { test('does not have basic accessibility issues', async () => { const { container, getByText } = renderModal({ open: true }); const results = await axe(container); expect(results).toHaveNoViolations(); let newResults = null as any; act(() => void userEvent.click(getByText('Confirm'))); await act(async () => { newResults = await axe(container); }); expect(newResults).toHaveNoViolations(); }); }); test('does not render if closed', () => { const { queryByText } = renderModal(); expect(queryByText('Content text')).toBeNull(); }); test('renders if open', () => { const { getByText } = renderModal({ open: true }); expect(getByText('Title text')).toBeVisible(); expect(getByText('Content text')).toBeVisible(); expect(getByText('Confirm')).toBeVisible(); expect(getByText('Cancel')).toBeVisible(); }); describe('initial focus', () => { test(`focuses confirm button when variant is ${Variant.Default}`, async () => { renderModal({ open: true, variant: Variant.Default, }); const confirmButton = screen.getByRole('button', { name: 'Confirm' }); await waitFor(() => { expect(confirmButton).toHaveFocus(); }); }); test(`focuses cancel button when variant is ${Variant.Danger}`, async () => { renderModal({ open: true, variant: Variant.Danger, }); const cancelButton = screen.getByRole('button', { name: 'Cancel' }); await waitFor(() => { expect(cancelButton).toHaveFocus(); }); }); test('focuses text input when requiredInputText is provided', async () => { renderModal({ open: true, requiredInputText: 'Confirm', }); const textInput = screen.getByLabelText( 'Type "Confirm" to confirm your action', { selector: 'input' }, ); await waitFor(() => { expect(textInput).toHaveFocus(); }); }); test('uses custom initialFocus prop when provided', async () => { const customFocusRef = React.createRef(); renderModal({ open: true, initialFocus: customFocusRef, children: ( <> Content text ), }); const customButton = screen.getByRole('button', { name: 'Custom action', }); const confirmButton = screen.getByRole('button', { name: 'Confirm' }); const cancelButton = screen.getByRole('button', { name: 'Cancel' }); await waitFor(() => { expect(customButton).toHaveFocus(); expect(confirmButton).not.toHaveFocus(); expect(cancelButton).not.toHaveFocus(); }); }); test('focuses cancel button when confirm button is disabled via confirmButtonProps', async () => { renderModal({ open: true, variant: Variant.Default, confirmButtonProps: { disabled: true, }, }); const cancelButton = screen.getByRole('button', { name: 'Cancel' }); const confirmButton = screen.getByRole('button', { name: 'Confirm' }); await waitFor(() => { expect(cancelButton).toHaveFocus(); expect(confirmButton).not.toHaveFocus(); }); }); test('focuses cancel button when confirm button is disabled via submitDisabled', async () => { renderModal({ open: true, variant: Variant.Default, submitDisabled: true, }); const cancelButton = screen.getByRole('button', { name: 'Cancel' }); const confirmButton = screen.getByRole('button', { name: 'Confirm' }); await waitFor(() => { expect(cancelButton).toHaveFocus(); expect(confirmButton).not.toHaveFocus(); }); }); }); describe('button text', () => { // TODO: remove - buttonText is deprecated test('renders from "buttonText"', () => { const { getByText } = renderModal({ open: true, buttonText: 'custom button text', }); expect(getByText('custom button text')).toBeVisible(); }); test('renders from "confirmButtonProps"', () => { const { getByText } = renderModal({ open: true, buttonText: undefined, confirmButtonProps: { children: 'custom confirm', }, }); expect(getByText('custom confirm')).toBeVisible(); }); // TODO: remove - buttonText is deprecated test('overrides deprecated "buttonText" with "confirmButtonProps"', () => { const { getByText } = renderModal({ open: true, buttonText: 'custom button text', confirmButtonProps: { children: 'custom confirm', }, }); expect(getByText('custom button text')).toBeVisible(); }); }); describe('on confirm', () => { // TODO: remove test - onConfirm is deprecated test('fires `onConfirm` on confirmation', () => { const confirmSpy = jest.fn(); const cancelSpy = jest.fn(); const { getByText } = renderModal({ open: true, onConfirm: confirmSpy, onCancel: cancelSpy, }); const button = getByText('Confirm'); expect(button).toBeVisible(); userEvent.click(button); expect(confirmSpy).toHaveBeenCalledTimes(1); expect(cancelSpy).not.toHaveBeenCalled(); }); test('fires `onClick` from "confirmButtonProps"', () => { const confirmSpy = jest.fn(); const { getByText } = renderModal({ open: true, onConfirm: undefined, confirmButtonProps: { onClick: confirmSpy, }, }); const button = getByText('Confirm'); expect(button).toBeVisible(); userEvent.click(button); expect(confirmSpy).toHaveBeenCalledTimes(1); }); }); describe('on cancel', () => { // TODO: remove test - OnCancel is deprecated test('fires `onCancel` on cancel', () => { const confirmSpy = jest.fn(); const cancelSpy = jest.fn(); const { getByText } = renderModal({ open: true, onConfirm: confirmSpy, onCancel: cancelSpy, }); const button = getByText('Cancel'); expect(button).toBeVisible(); userEvent.click(button); expect(confirmSpy).not.toHaveBeenCalled(); expect(cancelSpy).toHaveBeenCalledTimes(1); }); test('fires `onClick` from "cancelButtonProps"', () => { const confirmSpy = jest.fn(); const cancelSpy = jest.fn(); const { getByText } = renderModal({ open: true, onCancel: undefined, cancelButtonProps: { onClick: cancelSpy, }, }); const button = getByText('Cancel'); expect(button).toBeVisible(); userEvent.click(button); expect(confirmSpy).not.toHaveBeenCalled(); expect(cancelSpy).toHaveBeenCalledTimes(1); }); test('closes modal when `handleClose` sets state to false', async () => { const confirmSpy = jest.fn(); const handleCloseSpy = jest.fn((_e?: MouseEvent | KeyboardEvent) => {}); const TestWrapper = () => { const [open, setOpen] = useState(true); const handleClose = (e?: MouseEvent | KeyboardEvent) => { handleCloseSpy(e); setOpen(false); }; return ( Content text ); }; const { getByText, getByRole } = render(); const button = getByText('Cancel'); const modal = getByRole('dialog'); expect(button).toBeVisible(); expect(modal).toBeVisible(); // Click the button - the event is not passed to the `onCancel` callback // but we verify the function is called userEvent.click(button); expect(confirmSpy).not.toHaveBeenCalled(); expect(handleCloseSpy).toHaveBeenCalledTimes(1); expect(handleCloseSpy).toHaveBeenCalledWith(undefined); // Verify that the function signature accepts MouseEvent by calling it directly const mockMouseEvent = new MouseEvent('click', { bubbles: true }); handleCloseSpy.mockClear(); handleCloseSpy(mockMouseEvent); expect(handleCloseSpy).toHaveBeenCalledWith(mockMouseEvent); await waitFor(() => expect(modal).not.toBeVisible()); }); }); describe('button id props', () => { test('propagates to the buttons', () => { const { getByText } = renderModal({ open: true, confirmButtonProps: { id: 'my-confirm-btn', children: 'Confirm' }, cancelButtonProps: { id: 'my-cancel-btn' }, }); const confirmButton = getByText('Confirm').closest('button'); expect(confirmButton).toHaveAttribute('id', 'my-confirm-btn'); const cancelButton = getByText('Cancel').closest('button'); expect(cancelButton).toHaveAttribute('id', 'my-cancel-btn'); }); }); describe('closes when', () => { test('escape key is pressed', async () => { const { getByRole } = renderModal({ open: true }); const modal = getByRole('dialog'); userEvent.keyboard('{Escape}'); await waitFor(() => expect(modal).not.toBeVisible()); }); test('x icon is clicked', async () => { const { getByLabelText, getByRole } = renderModal({ open: true }); const modal = getByRole('dialog'); const x = getByLabelText('Close modal'); userEvent.click(x); await waitFor(() => expect(modal).not.toBeVisible()); }); }); describe('requiring text confirmation', () => { test('can only click confirmation button when text confirmation is entered', () => { const { getByText, getByLabelText } = renderModal({ open: true, requiredInputText: 'Confirm', }); const confirmationButton = getByText('Confirm').closest('button'); expect(confirmationButton).toHaveAttribute('aria-disabled', 'true'); const cancelButton = getByText('Cancel').closest('button'); expect(cancelButton).not.toHaveAttribute('aria-disabled', 'true'); const textInput = getByLabelText('Type "Confirm" to confirm your action'); expect(textInput).toBeVisible(); expect(textInput).toBe(document.activeElement); // Should still be disabled after partial entry userEvent.clear(textInput); userEvent.type(textInput, 'Confir'); expect(confirmationButton).toHaveAttribute('aria-disabled', 'true'); userEvent.clear(textInput); userEvent.type(textInput, 'Confirm'); expect(confirmationButton).not.toHaveAttribute('aria-disabled', 'true'); // Should be disabled again userEvent.clear(textInput); userEvent.type(textInput, 'Confirm?'); expect(confirmationButton).toHaveAttribute('aria-disabled', 'true'); // Case matters userEvent.clear(textInput); userEvent.type(textInput, 'confirm'); expect(confirmationButton).toHaveAttribute('aria-disabled', 'true'); }); const requiredInputTextCases = [ { describeCase: 'when requiredInputText is provided, confirm button is reset', requiredInputText: 'Confirm', disabledAfterReopeningModal: true, }, { describeCase: 'when requiredInputText is undefined, confirm button is not reset', requiredInputText: undefined, disabledAfterReopeningModal: false, }, ]; const buttonClickCases = [ { testCase: 'on cancel', testId: lgIds.cancel }, { testCase: 'on confirm', testId: lgIds.confirm }, { testCase: 'on modal close', testId: getLgModalIds(lgIds.root).close }, ]; describe.each(requiredInputTextCases)( '$describeCase', ({ requiredInputText, disabledAfterReopeningModal }) => { test.each(buttonClickCases)('$testCase', async ({ testId }) => { const { findByTestId, getByLabelText, getByRole, rerender } = renderModal({ open: true, requiredInputText, }); const modal = getByRole('dialog'); const confirmationButton = await findByTestId(lgIds.confirm); const buttonToClick = await findByTestId(testId); expect(confirmationButton).toHaveAttribute( 'aria-disabled', disabledAfterReopeningModal.toString(), ); let textInput; if (requiredInputText) { textInput = getByLabelText( `Type "${requiredInputText}" to confirm your action`, ); userEvent.clear(textInput); userEvent.type(textInput, requiredInputText); expect(confirmationButton).not.toHaveAttribute( 'aria-disabled', 'true', ); } userEvent.click(buttonToClick); await waitFor(() => expect(modal).not.toBeVisible()); rerender( Content text , ); const rerenderedConfirmationButton = await findByTestId( lgIds.confirm, ); if (requiredInputText) { textInput = getByLabelText( `Type "${requiredInputText}" to confirm your action`, ); expect(textInput).toHaveValue(''); } expect(rerenderedConfirmationButton).toHaveAttribute( 'aria-disabled', disabledAfterReopeningModal.toString(), ); }); }, ); }); describe('confirm is not disabled when', () => { test('By default', () => { const { getByText } = renderModal({ open: true, }); const confirmationButton = getByText('Confirm').closest('button'); expect(confirmationButton).toHaveAttribute('aria-disabled', 'false'); }); // TODO: remove this test - submitDisabled is deprecated test('"submitDisabled" prop is false and "confirmButtonProps" has "disabled: true"', async () => { const { getByText, getByRole } = renderModal({ open: true, submitDisabled: false, confirmButtonProps: { disabled: true, }, }); const confirmationButton = getByText('Confirm').closest('button'); expect(confirmationButton).toHaveAttribute('aria-disabled', 'false'); const modal = getByRole('dialog'); const button = getByText('Confirm'); expect(button).toBeVisible(); // Modal doesn't close when button is clicked userEvent.click(button); await waitFor(() => expect(modal).not.toBeVisible()); }); test('"confirmButtonProps" has "disabled: false"', async () => { const { getByText, getByRole } = renderModal({ open: true, confirmButtonProps: { disabled: false, }, }); const confirmationButton = getByText('Confirm').closest('button'); expect(confirmationButton).toHaveAttribute('aria-disabled', 'false'); const modal = getByRole('dialog'); const button = getByText('Confirm'); expect(button).toBeVisible(); // Modal doesn't close when button is clicked userEvent.click(button); await waitFor(() => expect(modal).not.toBeVisible()); }); }); describe('confirm is disabled when', () => { test('"confirmButtonProps" includes "disabled"', () => { const { getByText } = renderModal({ open: true, confirmButtonProps: { disabled: true, }, }); const confirmationButton = getByText('Confirm').closest('button'); expect(confirmationButton).toHaveAttribute('aria-disabled', 'true'); const button = getByText('Confirm'); expect(button).toBeVisible(); // Modal doesn't close when button is clicked userEvent.click(button); expect(button).toBeVisible(); }); // TODO: remove this test - submitDisabled is deprecated test('"submitDisabled" prop is set', () => { const { getByText } = renderModal({ open: true, submitDisabled: true, }); const confirmationButton = getByText('Confirm').closest('button'); expect(confirmationButton).toHaveAttribute('aria-disabled', 'true'); const button = getByText('Confirm'); expect(button).toBeVisible(); // Modal doesn't close when button is clicked userEvent.click(button); expect(button).toBeVisible(); }); // TODO: remove this test - submitDisabled is deprecated test('"submitDisabled" prop is true and "confirmButtonProps" has "disabled: false"', () => { const { getByText } = renderModal({ open: true, submitDisabled: true, confirmButtonProps: { disabled: false, }, }); const confirmationButton = getByText('Confirm').closest('button'); expect(confirmationButton).toHaveAttribute('aria-disabled', 'true'); const button = getByText('Confirm'); expect(button).toBeVisible(); // Modal doesn't close when button is clicked userEvent.click(button); expect(button).toBeVisible(); }); test('"confirmButtonProps" has "disabled: true" and the "requiredInputText" prop is also set', () => { const { getByText, getByLabelText } = renderModal({ open: true, confirmButtonProps: { disabled: true, }, requiredInputText: 'Confirm', }); const confirmationButton = getByText('Confirm').closest('button'); expect(confirmationButton).toHaveAttribute('aria-disabled', 'true'); const textInput = getByLabelText('Type "Confirm" to confirm your action'); expect(textInput).toBeVisible(); userEvent.clear(textInput); userEvent.type(textInput, 'Confirm'); expect(confirmationButton).toHaveAttribute('aria-disabled', 'true'); }); // TODO: remove this test - submitDisabled is deprecated test('"submitDisabled" prop is set and the "requiredInputText" prop is also set', () => { const { getByText, getByLabelText } = renderModal({ open: true, submitDisabled: true, requiredInputText: 'Confirm', }); const confirmationButton = getByText('Confirm').closest('button'); expect(confirmationButton).toHaveAttribute('aria-disabled', 'true'); const textInput = getByLabelText('Type "Confirm" to confirm your action'); expect(textInput).toBeVisible(); userEvent.clear(textInput); userEvent.type(textInput, 'Confirm'); expect(confirmationButton).toHaveAttribute('aria-disabled', 'true'); }); }); describe('testid attribute', () => { it('propagates to the dom element', () => { const { getByTestId } = renderModal({ open: true, 'data-testid': 'my-modal', }); const modal = getByTestId('my-modal'); expect(modal).toBeInTheDocument(); }); it('propagates to the buttons', () => { const { getByTestId } = renderModal({ open: true, confirmButtonProps: { 'data-testid': 'my-confirm-btn' }, cancelButtonProps: { 'data-testid': 'my-cancel-btn' }, }); const confirmButton = getByTestId('my-confirm-btn'); expect(confirmButton).toBeInTheDocument(); const cancelButton = getByTestId('my-cancel-btn'); expect(cancelButton).toBeInTheDocument(); }); }); // eslint-disable-next-line jest/no-disabled-tests test.skip('types behave as expected', () => { <> {}} onCancel={() => {}} open={true} submitDisabled={false} > Hey {}, disabled: true, }} > Hey Hey Hey Hey Hey Hey ; }); });