diff --git a/config/test/setupTests.ts b/config/test/setupTests.ts index 893d13ae..abe354c9 100644 --- a/config/test/setupTests.ts +++ b/config/test/setupTests.ts @@ -22,3 +22,12 @@ afterEach(() => { HTMLCanvasElement.prototype.getContext = (() => {}) as any; (global as any).scrollTo = () => {}; (global as any).matchMedia = () => ({ matches: false }); + +HTMLDialogElement.prototype.showModal = function() { + this.setAttribute('open', ''); +}; +HTMLDialogElement.prototype.close = function() { + this.removeAttribute('open'); + this.dispatchEvent(new CloseEvent('close')); + this.dispatchEvent(new CloseEvent('cancel')); +}; diff --git a/src/servers/CreateServer.tsx b/src/servers/CreateServer.tsx index fc83e75e..7c038810 100644 --- a/src/servers/CreateServer.tsx +++ b/src/servers/CreateServer.tsx @@ -1,11 +1,10 @@ import type { TimeoutToggle } from '@shlinkio/shlink-frontend-kit'; import { useToggle } from '@shlinkio/shlink-frontend-kit'; import type { ResultProps } from '@shlinkio/shlink-frontend-kit/tailwind'; -import { Result } from '@shlinkio/shlink-frontend-kit/tailwind'; +import { Button, Result } from '@shlinkio/shlink-frontend-kit/tailwind'; import type { FC } from 'react'; import { useCallback, useState } from 'react'; import { useNavigate } from 'react-router'; -import { Button } from 'reactstrap'; import { NoMenuLayout } from '../common/NoMenuLayout'; import type { FCWithDeps } from '../container/utils'; import { componentFactory, useDependencies } from '../container/utils'; @@ -29,7 +28,7 @@ type CreateServerDeps = { }; const ImportResult = ({ variant }: Pick) => ( -
+
{variant === 'success' && 'Servers properly imported. You can now select one from the list :)'} {variant === 'error' && 'The servers could not be imported. Make sure the format is correct.'} @@ -74,8 +73,8 @@ const CreateServer: FCWithDeps = ({ servers {!hasServers && ( )} - {hasServers && } - + {hasServers && } + {serversImported && } diff --git a/src/servers/DeleteServerButton.tsx b/src/servers/DeleteServerButton.tsx index 7e514836..78cb7004 100644 --- a/src/servers/DeleteServerButton.tsx +++ b/src/servers/DeleteServerButton.tsx @@ -3,6 +3,8 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { useToggle } from '@shlinkio/shlink-frontend-kit'; import { clsx } from 'clsx'; import type { FC, PropsWithChildren } from 'react'; +import { useCallback } from 'react'; +import { useNavigate } from 'react-router'; import type { FCWithDeps } from '../container/utils'; import { componentFactory, useDependencies } from '../container/utils'; import type { ServerWithId } from './data'; @@ -23,6 +25,13 @@ const DeleteServerButton: FCWithDeps { const { DeleteServerModal } = useDependencies(DeleteServerButton); const [isModalOpen, , showModal, hideModal] = useToggle(); + const navigate = useNavigate(); + const onClose = useCallback((confirmed: boolean) => { + hideModal(); + if (confirmed) { + navigate('/'); + } + }, [hideModal, navigate]); return ( <> @@ -31,7 +40,7 @@ const DeleteServerButton: FCWithDeps{children ?? 'Remove this server'} - + ); }; diff --git a/src/servers/DeleteServerModal.tsx b/src/servers/DeleteServerModal.tsx index 23ca528f..e4561f22 100644 --- a/src/servers/DeleteServerModal.tsx +++ b/src/servers/DeleteServerModal.tsx @@ -1,56 +1,40 @@ +import { CardModal } from '@shlinkio/shlink-frontend-kit/tailwind'; import type { FC } from 'react'; -import { useRef } from 'react'; -import { useNavigate } from 'react-router'; -import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'; +import { useCallback } from 'react'; import type { ServerWithId } from './data'; -export interface DeleteServerModalProps { +export type DeleteServerModalProps = { server: ServerWithId; - toggle: () => void; - isOpen: boolean; - redirectHome?: boolean; -} + onClose: (confirmed: boolean) => void; + open: boolean; +}; -interface DeleteServerModalConnectProps extends DeleteServerModalProps { +type DeleteServerModalConnectProps = DeleteServerModalProps & { deleteServer: (server: ServerWithId) => void; -} - -export const DeleteServerModal: FC = ( - { server, toggle, isOpen, deleteServer, redirectHome = true }, -) => { - const navigate = useNavigate(); - const doDelete = useRef(false); - const toggleAndDelete = () => { - doDelete.current = true; - toggle(); - }; - const onClosed = () => { - if (!doDelete.current) { - return; - } +}; +export const DeleteServerModal: FC = ({ server, onClose, open, deleteServer }) => { + const onConfirm = useCallback(() => { deleteServer(server); - if (redirectHome) { - navigate('/'); - } - }; + onClose(true); + }, [deleteServer, onClose, server]); return ( - - Remove server - -

Are you sure you want to remove {server ? server.name : ''}?

-

- - No data will be deleted, only the access to this server will be removed from this device. - You can create it again at any moment. - -

-
- - - - -
+ onClose(false)} + onConfirm={onConfirm} + confirmText="Delete" + > +

Are you sure you want to remove {server ? server.name : ''}?

+

+ + No data will be deleted, only the access to this server will be removed from this device. + You can create it again at any moment. + +

+
); }; diff --git a/src/servers/ManageServersRowDropdown.tsx b/src/servers/ManageServersRowDropdown.tsx index 4fde175a..60c46ab1 100644 --- a/src/servers/ManageServersRowDropdown.tsx +++ b/src/servers/ManageServersRowDropdown.tsx @@ -48,11 +48,11 @@ const ManageServersRowDropdown: FCWithDeps {isAutoConnect ? 'Do not a' : 'A'}uto-connect - + Remove server - + ); }; diff --git a/test/__helpers__/TestModalWrapper.tsx b/test/__helpers__/TestModalWrapper.tsx index 18959df7..28ade53a 100644 --- a/test/__helpers__/TestModalWrapper.tsx +++ b/test/__helpers__/TestModalWrapper.tsx @@ -1,14 +1,16 @@ -import { useToggle } from '@shlinkio/shlink-frontend-kit'; import type { FC, ReactElement } from 'react'; +import { useCallback, useState } from 'react'; -interface RenderModalArgs { - isOpen: boolean; - toggle: () => void; -} +export type RenderModalArgs = { + open: boolean; + onClose: () => void; +}; export const TestModalWrapper: FC<{ renderModal: (args: RenderModalArgs) => ReactElement }> = ( { renderModal }, ) => { - const [isOpen, toggle] = useToggle(true); - return renderModal({ isOpen, toggle }); + const [open, setOpen] = useState(true); + const onClose = useCallback(() => setOpen(false), []); + + return renderModal({ open, onClose }); }; diff --git a/test/servers/DeleteServerButton.test.tsx b/test/servers/DeleteServerButton.test.tsx index 75693268..9c7eee7b 100644 --- a/test/servers/DeleteServerButton.test.tsx +++ b/test/servers/DeleteServerButton.test.tsx @@ -1,18 +1,28 @@ -import { screen, waitFor } from '@testing-library/react'; +import { screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; +import { createMemoryHistory } from 'history'; import type { ReactNode } from 'react'; +import { Router } from 'react-router'; import { DeleteServerButtonFactory } from '../../src/servers/DeleteServerButton'; import type { DeleteServerModalProps } from '../../src/servers/DeleteServerModal'; +import { DeleteServerModal } from '../../src/servers/DeleteServerModal'; import { checkAccessibility } from '../__helpers__/accessibility'; import { renderWithEvents } from '../__helpers__/setUpTest'; describe('', () => { const DeleteServerButton = DeleteServerButtonFactory(fromPartial({ - DeleteServerModal: ({ isOpen }: DeleteServerModalProps) => <>DeleteServerModal {isOpen ? '[Open]' : '[Closed]'}, + DeleteServerModal: (props: DeleteServerModalProps) => , })); - const setUp = (children?: ReactNode) => renderWithEvents( - {children}, - ); + const setUp = (children?: ReactNode) => { + const history = createMemoryHistory({ initialEntries: ['/foo'] }); + const result = renderWithEvents( + + {children} + , + ); + + return { history, ...result }; + }; it('passes a11y checks', () => checkAccessibility(setUp('Delete me'))); @@ -28,14 +38,21 @@ describe('', () => { }); it('displays modal when button is clicked', async () => { - const { user, container } = setUp(); + const { user } = setUp(); - expect(screen.getByText(/DeleteServerModal/)).toHaveTextContent(/Closed/); - expect(screen.getByText(/DeleteServerModal/)).not.toHaveTextContent(/Open/); - if (container.firstElementChild) { - await user.click(container.firstElementChild); - } + expect(screen.queryByText(/Are you sure you want to remove/)).not.toBeInTheDocument(); + await user.click(screen.getByText('Remove this server')); + expect(screen.getByText(/Are you sure you want to remove/)).toBeInTheDocument(); + }); - await waitFor(() => expect(screen.getByText(/DeleteServerModal/)).toHaveTextContent(/Open/)); + it('navigates to home when deletion is confirmed', async () => { + const { user, history } = setUp(); + + // Open modal + await user.click(screen.getByText('Remove this server')); + + expect(history.location.pathname).toEqual('/foo'); + await user.click(screen.getByRole('button', { name: 'Delete' })); + expect(history.location.pathname).toEqual('/'); }); }); diff --git a/test/servers/DeleteServerModal.test.tsx b/test/servers/DeleteServerModal.test.tsx index dfb8a2c6..dfc24a1b 100644 --- a/test/servers/DeleteServerModal.test.tsx +++ b/test/servers/DeleteServerModal.test.tsx @@ -1,7 +1,5 @@ -import { act, screen, waitFor } from '@testing-library/react'; +import { screen, waitFor } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; -import { createMemoryHistory } from 'history'; -import { Router } from 'react-router'; import { DeleteServerModal } from '../../src/servers/DeleteServerModal'; import { checkAccessibility } from '../__helpers__/accessibility'; import { renderWithEvents } from '../__helpers__/setUpTest'; @@ -10,36 +8,29 @@ import { TestModalWrapper } from '../__helpers__/TestModalWrapper'; describe('', () => { const deleteServerMock = vi.fn(); const serverName = 'the_server_name'; - const setUp = async () => { - const history = createMemoryHistory({ initialEntries: ['/foo'] }); - const result = await act(() => renderWithEvents( - - ( - - )} + const setUp = () => renderWithEvents( + ( + - , - )); - - return { history, ...result }; - }; + )} + />, + ); it('passes a11y checks', () => checkAccessibility(setUp())); - it('renders a modal window', async () => { - await setUp(); + it('renders a modal window', () => { + setUp(); expect(screen.getByRole('dialog')).toBeInTheDocument(); expect(screen.getByRole('heading')).toHaveTextContent('Remove server'); }); - it('displays the name of the server as part of the content', async () => { - await setUp(); + it('displays the name of the server as part of the content', () => { + setUp(); expect(screen.getByText(/^Are you sure you want to remove/)).toBeInTheDocument(); expect(screen.getByText(serverName)).toBeInTheDocument(); @@ -47,25 +38,20 @@ describe('', () => { it.each([ [() => screen.getByRole('button', { name: 'Cancel' })], - [() => screen.getByLabelText('Close')], + [() => screen.getByLabelText('Close dialog')], ])('toggles when clicking cancel button', async (getButton) => { - const { user, history } = await setUp(); + const { user } = setUp(); - expect(history.location.pathname).toEqual('/foo'); await user.click(getButton()); - expect(deleteServerMock).not.toHaveBeenCalled(); - expect(history.location.pathname).toEqual('/foo'); // No navigation happens, keeping initial pathname }); it('deletes server when clicking accept button', async () => { - const { user, history } = await setUp(); + const { user } = setUp(); expect(deleteServerMock).not.toHaveBeenCalled(); - expect(history.location.pathname).toEqual('/foo'); await user.click(screen.getByRole('button', { name: 'Delete' })); await waitFor(() => expect(deleteServerMock).toHaveBeenCalledTimes(1)); - await waitFor(() => expect(history.location.pathname).toEqual('/')); }); }); diff --git a/test/servers/ManageServersRowDropdown.test.tsx b/test/servers/ManageServersRowDropdown.test.tsx index 7ac1930f..9db54e7b 100644 --- a/test/servers/ManageServersRowDropdown.test.tsx +++ b/test/servers/ManageServersRowDropdown.test.tsx @@ -9,8 +9,8 @@ import { renderWithEvents } from '../__helpers__/setUpTest'; describe('', () => { const ManageServersRowDropdown = ManageServersRowDropdownFactory(fromPartial({ - DeleteServerModal: ({ isOpen }: { isOpen: boolean }) => ( - DeleteServerModal {isOpen ? '[OPEN]' : '[CLOSED]'} + DeleteServerModal: ({ open }: { open: boolean }) => ( + DeleteServerModal {open ? '[OPEN]' : '[CLOSED]'} ), })); const setAutoConnect = vi.fn(); diff --git a/test/servers/__snapshots__/ManageServersRowDropdown.test.tsx.snap b/test/servers/__snapshots__/ManageServersRowDropdown.test.tsx.snap index 967d1e10..d597f4bd 100644 --- a/test/servers/__snapshots__/ManageServersRowDropdown.test.tsx.snap +++ b/test/servers/__snapshots__/ManageServersRowDropdown.test.tsx.snap @@ -114,7 +114,7 @@ exports[` > renders expected size and icon 1`] = ` tabindex="-1" />