/* eslint-disable jest/no-commented-out-tests */ /* eslint-disable react/jsx-boolean-value */ import * as React from 'react'; import { render, fireEvent, waitFor } from '@testing-library/react'; import type { ModalExtension } from '@atlassian/clientside-extensions'; import type { FunctionComponent } from 'react'; import { ModalHandler, ModalWithActionHandler } from './ModalHandler'; import type { ModalHandlerProps, ModalWithActionHandlerProps } from './ModalHandler'; const onMountCallbackSpy = jest.fn(); const onUnmountCallbackSpy = jest.fn(); const testRenderFn: ModalExtension.ModalRenderExtension = (api) => { api.onMount(onMountCallbackSpy); api.onUnmount(onUnmountCallbackSpy); }; // When an attribute of the extension changes, the useExtensions hook returns a completly new descriptor object // This render function is used to replicate that behaviour. const changedTestRenderFn: ModalExtension.ModalRenderExtension = (api) => { api.onMount(onMountCallbackSpy); api.onUnmount(onUnmountCallbackSpy); }; const emptyRenderer = (): null => null; const emptyCallback = (): null => null; const TestComponent: FunctionComponent> = ({ render: renderProp = testRenderFn, isOpen = true, onClose = emptyCallback, }) => { return ; }; const TestComponentWithActionHandler: FunctionComponent> = ({ render: renderProp = emptyRenderer, children, }) => { return {children}; }; TestComponentWithActionHandler.defaultProps = { render: testRenderFn, } as Partial; describe('ModalHandler', () => { beforeEach(() => { onMountCallbackSpy.mockReset(); onUnmountCallbackSpy.mockReset(); }); describe('ModalHandler', () => { it('should receive an isOpen prop to set the modal as open', async () => { const { rerender, queryByRole } = render(); expect(queryByRole('dialog')).toBeFalsy(); rerender(); await waitFor(() => { expect(queryByRole('dialog')).toBeTruthy(); }); }); it('should provide a closeModal API method to extension that executes a provided onClose prop to close the modal', () => { const onCloseCallbackSpy = jest.fn(); expect(onCloseCallbackSpy).toHaveBeenCalledTimes(0); render( api.closeModal()} onClose={onCloseCallbackSpy} />); expect(onCloseCallbackSpy).toHaveBeenCalledTimes(1); }); it('should provide an onClose API method for extension to prevent the modal from closing unexpectedly (e.g: ESC key)', () => { const onCloseCallbackSpy = jest.fn(); const fireEscKeyPress = () => fireEvent.keyDown(document.body, { key: 'Escape', keyCode: 27, charCode: 27, which: 27, }); expect(onCloseCallbackSpy).toHaveBeenCalledTimes(0); const { rerender, container } = render( api.onClose(() => false)} onClose={onCloseCallbackSpy} />, ); fireEvent.click(container); expect(onCloseCallbackSpy).toHaveBeenCalledTimes(0); rerender( api.onClose(() => true)} onClose={onCloseCallbackSpy} />); fireEscKeyPress(); expect(onCloseCallbackSpy).toHaveBeenCalledTimes(1); }); it('should provide a setTitle API method that sets a title in the modal header', async () => { const TEST_TITLE = 'A modal with a test title'; const { findByRole } = render( api.setTitle(TEST_TITLE)} />); expect((await findByRole('heading')).textContent).toBe(TEST_TITLE); }); it('should provide a setActions API to specify the actions a user can perform when the modal is opened', async () => { const primaryActionSpy = jest.fn(); const secondaryActionSpy = jest.fn(); const actions: ModalExtension.ModalAction[] = [ { text: 'Primary action', onClick: primaryActionSpy, testId: 'primaryAction', }, { text: 'Secondary action', onClick: secondaryActionSpy, testId: 'secondaryAction', }, ]; const { findByTestId } = render( api.setActions(actions)} />); const primaryBtn = await findByTestId('primaryAction'); const secondaryBtn = await findByTestId('secondaryAction'); expect(primaryActionSpy).toHaveBeenCalledTimes(0); expect(secondaryActionSpy).toHaveBeenCalledTimes(0); fireEvent.click(primaryBtn); expect(primaryActionSpy).toHaveBeenCalledTimes(1); expect(secondaryActionSpy).toHaveBeenCalledTimes(0); fireEvent.click(secondaryBtn); expect(primaryActionSpy).toHaveBeenCalledTimes(1); expect(secondaryActionSpy).toHaveBeenCalledTimes(1); }); it('should allow modal actions to be set as disabled', async () => { const primaryActionSpy = jest.fn(); const primaryAction: ModalExtension.ModalAction = { text: 'Primary action', onClick: primaryActionSpy, testId: 'primaryAction', }; const { findByTestId, rerender } = render( api.setActions([primaryAction])} />); expect(primaryActionSpy).toHaveBeenCalledTimes(0); fireEvent.click(await findByTestId('primaryAction')); expect(primaryActionSpy).toHaveBeenCalledTimes(1); primaryAction.isDisabled = true; rerender( api.setActions([primaryAction])} />); fireEvent.click(await findByTestId('primaryAction')); expect(primaryActionSpy).toHaveBeenCalledTimes(1); }); // TODO: find a way with AK team to test that a button is in loading state. // it.only('should allow modal actions to be set as loading', () => {}); // TODO: find a way with AK team to test the size applied to a modal without querying for the actual size of the element. // it('should provide a setWidth API method to modify the width of the modal', () => {}); // TODO: find a way with AK team to test the danger/warning appearance of a modal without involving CSS. // it('should provide a setAppearance API method that changes the appearance of the modal', () => {}); it('should provide an onMount API method to render custom HTML in a container', async () => { const CUSTOM_CONTENT = 'Custom content'; onMountCallbackSpy.mockImplementationOnce((container: HTMLElement) => { container.innerHTML = CUSTOM_CONTENT; }); const { findByText } = render(); expect(onMountCallbackSpy).toHaveBeenCalledTimes(1); expect(await findByText(CUSTOM_CONTENT)).toBeTruthy(); }); it('should only rerender the extension content if something in the extension descriptor changed', async () => { const CUSTOM_CONTENT = 'Custom content'; const DIFFERENT_CONTENT = 'Different content'; onMountCallbackSpy .mockImplementationOnce((container: HTMLElement) => { container.innerHTML = CUSTOM_CONTENT; }) .mockImplementationOnce((container: HTMLElement) => { container.innerHTML = DIFFERENT_CONTENT; }); const { findByText, rerender } = render(); expect(onMountCallbackSpy).toHaveBeenCalledTimes(1); expect(await findByText(CUSTOM_CONTENT)).toBeTruthy(); rerender(); // expect nothing to change expect(onMountCallbackSpy).toHaveBeenCalledTimes(1); expect(await findByText(CUSTOM_CONTENT)).toBeTruthy(); rerender(); // expect the new value to be rendered expect(onMountCallbackSpy).toHaveBeenCalledTimes(2); expect(await findByText(DIFFERENT_CONTENT)).toBeTruthy(); }); it('should provide an onUnmount API method to be called with the container element when destroying the extension', () => { onMountCallbackSpy.mockImplementation((container: HTMLElement) => { expect(container).toHaveProperty('innerHTML'); }); const { unmount } = render(); expect(onUnmountCallbackSpy).toHaveBeenCalledTimes(0); unmount(); expect(onUnmountCallbackSpy).toHaveBeenCalledTimes(1); }); it('should only call cleanUp callback if the extension changes or the container is destroyed', () => { const { rerender, unmount } = render(); expect(onUnmountCallbackSpy).toHaveBeenCalledTimes(0); rerender(); // the container rerenders with the same extension render prop, so no need to call cleanup expect(onUnmountCallbackSpy).toHaveBeenCalledTimes(0); rerender(); // the extension render prop changed, so cleanup is called expect(onUnmountCallbackSpy).toHaveBeenCalledTimes(1); unmount(); // the container is destroyed, so cleanup is called expect(onUnmountCallbackSpy).toHaveBeenCalledTimes(2); }); }); describe('ModalWithActionHandler', () => { const modalButtonText = 'My Modal'; it('should render a button to handle the modal', () => { const { queryByText } = render({modalButtonText}); expect(queryByText(modalButtonText)).toBeTruthy(); }); it('should receive an isOpen prop to set the modal as open', async () => { const { queryByRole, queryByText } = render({modalButtonText}); expect(queryByRole('dialog')).toBeFalsy(); queryByText(modalButtonText)?.click(); await waitFor(() => { expect(queryByRole('dialog')).toBeTruthy(); }); }); }); });