diff --git a/src/servers/DeleteServerModal.tsx b/src/servers/DeleteServerModal.tsx index 911ba3d0..8fba141c 100644 --- a/src/servers/DeleteServerModal.tsx +++ b/src/servers/DeleteServerModal.tsx @@ -26,7 +26,7 @@ export const DeleteServerModal: FC = ( return ( - Remove server + Remove server

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

diff --git a/src/servers/ManageServersRowDropdown.tsx b/src/servers/ManageServersRowDropdown.tsx index 51863d58..f0da17a5 100644 --- a/src/servers/ManageServersRowDropdown.tsx +++ b/src/servers/ManageServersRowDropdown.tsx @@ -39,7 +39,7 @@ export const ManageServersRowDropdown = ( Edit server - setAutoConnect(server, !server.autoConnect)}> + setAutoConnect(server, !isAutoConnect)}> {isAutoConnect ? 'Do not a' : 'A'}uto-connect diff --git a/src/utils/Time.tsx b/src/utils/Time.tsx index c50639a2..fc4c3571 100644 --- a/src/utils/Time.tsx +++ b/src/utils/Time.tsx @@ -1,13 +1,13 @@ import { parseISO, format as formatDate, getUnixTime, formatDistance } from 'date-fns'; import { isDateObject } from './helpers/date'; -export interface DateProps { +export interface TimeProps { date: Date | string; format?: string; relative?: boolean; } -export const Time = ({ date, format = 'yyyy-MM-dd HH:mm', relative = false }: DateProps) => { +export const Time = ({ date, format = 'yyyy-MM-dd HH:mm', relative = false }: TimeProps) => { const dateObject = isDateObject(date) ? date : parseISO(date); return ( diff --git a/test/servers/DeleteServerModal.test.tsx b/test/servers/DeleteServerModal.test.tsx index 39b3832e..3bf0ec51 100644 --- a/test/servers/DeleteServerModal.test.tsx +++ b/test/servers/DeleteServerModal.test.tsx @@ -1,5 +1,5 @@ -import { shallow, ShallowWrapper } from 'enzyme'; -import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { Mock } from 'ts-mockery'; import { useNavigate } from 'react-router-dom'; import { DeleteServerModal } from '../../src/servers/DeleteServerModal'; @@ -8,56 +8,63 @@ import { ServerWithId } from '../../src/servers/data'; jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useNavigate: jest.fn() })); describe('', () => { - let wrapper: ShallowWrapper; const deleteServerMock = jest.fn(); const navigate = jest.fn(); const toggleMock = jest.fn(); const serverName = 'the_server_name'; - - beforeEach(() => { + const setUp = () => { (useNavigate as any).mockReturnValue(navigate); - wrapper = shallow( - ({ name: serverName })} - toggle={toggleMock} - isOpen - deleteServer={deleteServerMock} - />, - ); - }); - afterEach(() => wrapper.unmount()); + return { + user: userEvent.setup(), + ...render( + ({ name: serverName })} + toggle={toggleMock} + isOpen + deleteServer={deleteServerMock} + />, + ), + }; + }; + afterEach(jest.clearAllMocks); it('renders a modal window', () => { - expect(wrapper.find(Modal)).toHaveLength(1); - expect(wrapper.find(ModalHeader)).toHaveLength(1); - expect(wrapper.find(ModalBody)).toHaveLength(1); - expect(wrapper.find(ModalFooter)).toHaveLength(1); + 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', () => { - const modalBody = wrapper.find(ModalBody); + setUp(); - expect(modalBody.find('p').first().text()).toEqual( - `Are you sure you want to remove ${serverName}?`, - ); + expect(screen.getByText(/^Are you sure you want to remove/)).toBeInTheDocument(); + expect(screen.getByText(serverName)).toBeInTheDocument(); }); - it('toggles when clicking cancel button', () => { - const cancelBtn = wrapper.find(Button).first(); + it.each([ + [() => screen.getByRole('button', { name: 'Cancel' })], + [() => screen.getByLabelText('Close')], + ])('toggles when clicking cancel button', async (getButton) => { + const { user } = setUp(); - cancelBtn.simulate('click'); + expect(toggleMock).not.toHaveBeenCalled(); + await user.click(getButton()); expect(toggleMock).toHaveBeenCalledTimes(1); expect(deleteServerMock).not.toHaveBeenCalled(); expect(navigate).not.toHaveBeenCalled(); }); - it('deletes server when clicking accept button', () => { - const acceptBtn = wrapper.find(Button).last(); + it('deletes server when clicking accept button', async () => { + const { user } = setUp(); - acceptBtn.simulate('click'); + expect(toggleMock).not.toHaveBeenCalled(); + expect(deleteServerMock).not.toHaveBeenCalled(); + expect(navigate).not.toHaveBeenCalled(); + await user.click(screen.getByRole('button', { name: 'Delete' })); expect(toggleMock).toHaveBeenCalledTimes(1); expect(deleteServerMock).toHaveBeenCalledTimes(1); diff --git a/test/servers/ManageServers.test.tsx b/test/servers/ManageServers.test.tsx index 5cbb81e6..a88d3cdc 100644 --- a/test/servers/ManageServers.test.tsx +++ b/test/servers/ManageServers.test.tsx @@ -1,93 +1,103 @@ import { Mock } from 'ts-mockery'; -import { shallow, ShallowWrapper } from 'enzyme'; -import { Button } from 'reactstrap'; +import userEvent from '@testing-library/user-event'; +import { render, screen, waitFor } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; import ServersExporter from '../../src/servers/services/ServersExporter'; import { ManageServers as createManageServers } from '../../src/servers/ManageServers'; import { ServersMap, ServerWithId } from '../../src/servers/data'; -import { SearchField } from '../../src/utils/SearchField'; -import { Result } from '../../src/utils/Result'; describe('', () => { const exportServers = jest.fn(); const serversExporter = Mock.of({ exportServers }); - const ImportServersBtn = () => null; - const ManageServersRow = () => null; const useTimeoutToggle = jest.fn().mockReturnValue([false, jest.fn()]); - const ManageServers = createManageServers(serversExporter, ImportServersBtn, useTimeoutToggle, ManageServersRow); - let wrapper: ShallowWrapper; + const ManageServers = createManageServers( + serversExporter, + () => ImportServersBtn, + useTimeoutToggle, + ({ hasAutoConnect }) => ManageServersRow {hasAutoConnect ? '[YES]' : '[NO]'}, + ); const createServerMock = (value: string, autoConnect = false) => Mock.of( { id: value, name: value, url: value, autoConnect }, ); - const createWrapper = (servers: ServersMap = {}) => { - wrapper = shallow(); - - return wrapper; - }; + const setUp = (servers: ServersMap = {}) => ({ + user: userEvent.setup(), + ...render(), + }); afterEach(jest.clearAllMocks); - afterEach(() => wrapper?.unmount()); - it('shows search field which allows searching servers, affecting te amount of rendered rows', () => { - const wrapper = createWrapper({ + it('shows search field which allows searching servers, affecting te amount of rendered rows', async () => { + const { user } = setUp({ foo: createServerMock('foo'), bar: createServerMock('bar'), baz: createServerMock('baz'), }); - const searchField = wrapper.find(SearchField); + const search = async (searchTerm: string) => { + await user.clear(screen.getByPlaceholderText('Search...')); + await user.type(screen.getByPlaceholderText('Search...'), searchTerm); + }; - expect(wrapper.find(ManageServersRow)).toHaveLength(3); - expect(wrapper.find('tbody').find('tr')).toHaveLength(0); + expect(screen.getAllByText(/^ManageServersRow/)).toHaveLength(3); + expect(screen.queryByText('No servers found.')).not.toBeInTheDocument(); - searchField.simulate('change', 'foo'); - expect(wrapper.find(ManageServersRow)).toHaveLength(1); - expect(wrapper.find('tbody').find('tr')).toHaveLength(0); + await search('foo'); + await waitFor(() => expect(screen.getAllByText(/^ManageServersRow/)).toHaveLength(1)); + expect(screen.queryByText('No servers found.')).not.toBeInTheDocument(); - searchField.simulate('change', 'ba'); - expect(wrapper.find(ManageServersRow)).toHaveLength(2); - expect(wrapper.find('tbody').find('tr')).toHaveLength(0); + await search('ba'); + await waitFor(() => expect(screen.getAllByText(/^ManageServersRow/)).toHaveLength(2)); + expect(screen.queryByText('No servers found.')).not.toBeInTheDocument(); - searchField.simulate('change', 'invalid'); - expect(wrapper.find(ManageServersRow)).toHaveLength(0); - expect(wrapper.find('tbody').find('tr')).toHaveLength(1); + await search('invalid'); + await waitFor(() => expect(screen.queryByText(/^ManageServersRow/)).not.toBeInTheDocument()); + expect(screen.getByText('No servers found.')).toBeInTheDocument(); }); it.each([ [createServerMock('foo'), 3], [createServerMock('foo', true), 4], ])('shows different amount of columns if there are at least one auto-connect server', (server, expectedCols) => { - const wrapper = createWrapper({ server }); - const row = wrapper.find(ManageServersRow); + setUp({ server }); - expect(wrapper.find('th')).toHaveLength(expectedCols); - expect(row.prop('hasAutoConnect')).toEqual(server.autoConnect); + expect(screen.getAllByRole('columnheader')).toHaveLength(expectedCols); + if (server.autoConnect) { + expect(screen.getByText(/\[YES\]/)).toBeInTheDocument(); + expect(screen.queryByText(/\[NO\]/)).not.toBeInTheDocument(); + } else { + expect(screen.queryByText(/\[YES\]/)).not.toBeInTheDocument(); + expect(screen.getByText(/\[NO\]/)).toBeInTheDocument(); + } }); it.each([ - [{}, 1], - [{ foo: createServerMock('foo') }, 2], + [{}, 0], + [{ foo: createServerMock('foo') }, 1], ])('shows export button if the list of servers is not empty', (servers, expectedButtons) => { - const wrapper = createWrapper(servers); - const exportBtn = wrapper.find(Button); - - expect(exportBtn).toHaveLength(expectedButtons); + setUp(servers); + expect(screen.queryAllByRole('button', { name: 'Export servers' })).toHaveLength(expectedButtons); }); - it('allows exporting servers when clicking on button', () => { - const wrapper = createWrapper({ foo: createServerMock('foo') }); - const exportBtn = wrapper.find(Button).first(); + it('allows exporting servers when clicking on button', async () => { + const { user } = setUp({ foo: createServerMock('foo') }); expect(exportServers).not.toHaveBeenCalled(); - exportBtn.simulate('click'); + await user.click(screen.getByRole('button', { name: 'Export servers' })); expect(exportServers).toHaveBeenCalled(); }); - it('shows an error message if an error occurs while importing servers', () => { - useTimeoutToggle.mockReturnValue([true, jest.fn()]); + it.each([[true], [false]])('shows an error message if an error occurs while importing servers', (hasError) => { + useTimeoutToggle.mockReturnValue([hasError, jest.fn()]); - const wrapper = createWrapper({ foo: createServerMock('foo') }); - const result = wrapper.find(Result); + setUp({ foo: createServerMock('foo') }); - expect(result).toHaveLength(1); - expect(result.prop('type')).toEqual('error'); + if (hasError) { + expect( + screen.getByText('The servers could not be imported. Make sure the format is correct.'), + ).toBeInTheDocument(); + } else { + expect( + screen.queryByText('The servers could not be imported. Make sure the format is correct.'), + ).not.toBeInTheDocument(); + } }); }); diff --git a/test/servers/ManageServersRow.test.tsx b/test/servers/ManageServersRow.test.tsx index 2b66b6ab..7c3e24ff 100644 --- a/test/servers/ManageServersRow.test.tsx +++ b/test/servers/ManageServersRow.test.tsx @@ -1,66 +1,58 @@ -import { shallow, ShallowWrapper } from 'enzyme'; -import { UncontrolledTooltip } from 'reactstrap'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Link } from 'react-router-dom'; +import { render, screen } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; import { ManageServersRow as createManageServersRow } from '../../src/servers/ManageServersRow'; import { ServerWithId } from '../../src/servers/data'; describe('', () => { - const ManageServersRowDropdown = () => null; - const ManageServersRow = createManageServersRow(ManageServersRowDropdown); + const ManageServersRow = createManageServersRow(() => ManageServersRowDropdown); const server: ServerWithId = { name: 'My server', url: 'https://example.com', apiKey: '123', id: 'abc', }; - let wrapper: ShallowWrapper; - const createWrapper = (hasAutoConnect = false, autoConnect = false) => { - wrapper = shallow(); - - return wrapper; - }; - - afterEach(() => wrapper?.unmount()); + const setUp = (hasAutoConnect = false, autoConnect = false) => render( + + + + + +
+
, + ); it.each([ [true, 4], [false, 3], ])('renders expected amount of columns', (hasAutoConnect, expectedCols) => { - const wrapper = createWrapper(hasAutoConnect); - const td = wrapper.find('td'); - const th = wrapper.find('th'); + setUp(hasAutoConnect); + + const td = screen.getAllByRole('cell'); + const th = screen.getAllByRole('columnheader'); expect(td.length + th.length).toEqual(expectedCols); }); it('renders a dropdown', () => { - const wrapper = createWrapper(); - const dropdown = wrapper.find(ManageServersRowDropdown); - - expect(dropdown).toHaveLength(1); - expect(dropdown.prop('server')).toEqual(expect.objectContaining(server)); + setUp(); + expect(screen.getByText('ManageServersRowDropdown')).toBeInTheDocument(); }); it.each([ - [true, 1], - [false, 0], - ])('renders auto-connect icon only if server is autoConnect', (autoConnect, expectedIcons) => { - const wrapper = createWrapper(true, autoConnect); - const icon = wrapper.find(FontAwesomeIcon); - const iconTooltip = wrapper.find(UncontrolledTooltip); - - expect(icon).toHaveLength(expectedIcons); - expect(iconTooltip).toHaveLength(expectedIcons); + [true], + [false], + ])('renders auto-connect icon only if server is autoConnect', (autoConnect) => { + const { container } = setUp(true, autoConnect); + expect(container).toMatchSnapshot(); }); it('renders server props where appropriate', () => { - const wrapper = createWrapper(); - const link = wrapper.find(Link); - const td = wrapper.find('td').first(); + setUp(); - expect(link.prop('to')).toEqual(`/server/${server.id}`); - expect(link.prop('children')).toEqual(server.name); - expect(td.prop('children')).toEqual(server.url); + const link = screen.getByRole('link'); + + expect(link).toHaveAttribute('href', `/server/${server.id}`); + expect(link).toHaveTextContent(server.name); + expect(screen.getByText(server.url)).toBeInTheDocument(); }); }); diff --git a/test/servers/ManageServersRowDropdown.test.tsx b/test/servers/ManageServersRowDropdown.test.tsx index 95470a2a..98ad07ca 100644 --- a/test/servers/ManageServersRowDropdown.test.tsx +++ b/test/servers/ManageServersRowDropdown.test.tsx @@ -1,84 +1,60 @@ -import { shallow, ShallowWrapper } from 'enzyme'; +import userEvent from '@testing-library/user-event'; +import { render, screen } from '@testing-library/react'; import { Mock } from 'ts-mockery'; -import { DropdownItem } from 'reactstrap'; +import { MemoryRouter } from 'react-router-dom'; import { ServerWithId } from '../../src/servers/data'; import { ManageServersRowDropdown as createManageServersRowDropdown } from '../../src/servers/ManageServersRowDropdown'; describe('', () => { - const DeleteServerModal = () => null; - const ManageServersRowDropdown = createManageServersRowDropdown(DeleteServerModal); + const ManageServersRowDropdown = createManageServersRowDropdown( + ({ isOpen }) => DeleteServerModal {isOpen ? '[OPEN]' : '[CLOSED]'}, + ); const setAutoConnect = jest.fn(); - let wrapper: ShallowWrapper; - const createWrapper = (autoConnect = false) => { + const setUp = (autoConnect = false) => { const server = Mock.of({ id: 'abc123', autoConnect }); - - wrapper = shallow(); - - return wrapper; + return { + user: userEvent.setup(), + ...render( + + + , + ), + }; }; afterEach(jest.clearAllMocks); - it('renders expected amount of dropdown items', () => { - const wrapper = createWrapper(); - const items = wrapper.find(DropdownItem); + it('renders expected amount of dropdown items', async () => { + const { user } = setUp(); - expect(items).toHaveLength(5); - expect(items.find('[divider]')).toHaveLength(1); - expect(items.at(0).prop('to')).toEqual('/server/abc123'); - expect(items.at(1).prop('to')).toEqual('/server/abc123/edit'); + expect(screen.queryByRole('menu')).not.toBeInTheDocument(); + await user.click(screen.getByRole('button')); + expect(screen.getByRole('menu')).toBeInTheDocument(); + + expect(screen.getAllByRole('menuitem')).toHaveLength(4); + expect(screen.getByRole('menuitem', { name: 'Connect' })).toHaveAttribute('href', '/server/abc123'); + expect(screen.getByRole('menuitem', { name: 'Edit server' })).toHaveAttribute('href', '/server/abc123/edit'); }); - it('allows toggling auto-connect', () => { - const wrapper = createWrapper(); + it('allows toggling auto-connect', async () => { + const { user } = setUp(); expect(setAutoConnect).not.toHaveBeenCalled(); - wrapper.find(DropdownItem).at(2).simulate('click'); + await user.click(screen.getByRole('button')); + await user.click(screen.getByRole('menuitem', { name: 'Auto-connect' })); expect(setAutoConnect).toHaveBeenCalledWith(expect.objectContaining({ id: 'abc123' }), true); }); - it('renders a modal', () => { - const wrapper = createWrapper(); - const modal = wrapper.find(DeleteServerModal); + it('renders deletion modal', async () => { + const { user } = setUp(); - expect(modal).toHaveLength(1); - expect(modal.prop('redirectHome')).toEqual(false); - expect(modal.prop('server')).toEqual(expect.objectContaining({ id: 'abc123' })); - expect(modal.prop('isOpen')).toEqual(false); - }); + expect(screen.queryByText('DeleteServerModal [OPEN]')).not.toBeInTheDocument(); + expect(screen.getByText('DeleteServerModal [CLOSED]')).toBeInTheDocument(); - it('allows toggling the modal', () => { - const wrapper = createWrapper(); - const modalToggle = wrapper.find(DropdownItem).last(); + await user.click(screen.getByRole('button')); + await user.click(screen.getByRole('menuitem', { name: 'Remove server' })); - expect(wrapper.find(DeleteServerModal).prop('isOpen')).toEqual(false); - - modalToggle.simulate('click'); - expect(wrapper.find(DeleteServerModal).prop('isOpen')).toEqual(true); - - (wrapper.find(DeleteServerModal).prop('toggle') as Function)(); - expect(wrapper.find(DeleteServerModal).prop('isOpen')).toEqual(false); - }); - - it('can be toggled', () => { - const wrapper = createWrapper(); - - expect(wrapper.prop('isOpen')).toEqual(false); - - (wrapper.prop('toggle') as Function)(); - expect(wrapper.prop('isOpen')).toEqual(true); - - (wrapper.prop('toggle') as Function)(); - expect(wrapper.prop('isOpen')).toEqual(false); - }); - - it.each([ - [true, 'Do not auto-connect'], - [false, 'Auto-connect'], - ])('shows different auto-connect toggle text depending on current server status', (autoConnect, expectedText) => { - const wrapper = createWrapper(autoConnect); - const item = wrapper.find(DropdownItem).at(2); - - expect(item.html()).toContain(expectedText); + expect(screen.getByText('DeleteServerModal [OPEN]')).toBeInTheDocument(); + expect(screen.queryByText('DeleteServerModal [CLOSED]')).not.toBeInTheDocument(); }); }); diff --git a/test/servers/ServersListGroup.test.tsx b/test/servers/ServersListGroup.test.tsx index 2f1b8e81..401777e7 100644 --- a/test/servers/ServersListGroup.test.tsx +++ b/test/servers/ServersListGroup.test.tsx @@ -1,6 +1,6 @@ -import { shallow, ShallowWrapper } from 'enzyme'; -import { ListGroup } from 'reactstrap'; +import { render, screen } from '@testing-library/react'; import { Mock } from 'ts-mockery'; +import { MemoryRouter } from 'react-router-dom'; import { ServersListGroup } from '../../src/servers/ServersListGroup'; import { ServerWithId } from '../../src/servers/data'; @@ -9,44 +9,36 @@ describe('', () => { Mock.of({ name: 'foo', id: '123' }), Mock.of({ name: 'bar', id: '456' }), ]; - let wrapped: ShallowWrapper; - const createComponent = (params: { servers?: ServerWithId[]; withChildren?: boolean; embedded?: boolean }) => { + const setUp = (params: { servers?: ServerWithId[]; withChildren?: boolean; embedded?: boolean }) => { const { servers = [], withChildren = true, embedded } = params; - wrapped = shallow( - - {withChildren ? 'The list of servers' : undefined} - , + return render( + + + {withChildren ? 'The list of servers' : undefined} + + , ); - - return wrapped; }; - afterEach(() => wrapped?.unmount()); - it('renders title', () => { - const wrapped = createComponent({}); - const title = wrapped.find('h5'); - - expect(title).toHaveLength(1); - expect(title.text()).toEqual('The list of servers'); + setUp({}); + expect(screen.getByRole('heading')).toHaveTextContent('The list of servers'); }); it('does not render title when children is not provided', () => { - const wrapped = createComponent({ withChildren: false }); - const title = wrapped.find('h5'); - - expect(title).toHaveLength(0); + setUp({ withChildren: false }); + expect(screen.queryByRole('heading')).not.toBeInTheDocument(); }); it.each([ [servers], [[]], ])('shows servers list', (servers) => { - const wrapped = createComponent({ servers }); + setUp({ servers }); - expect(wrapped.find(ListGroup)).toHaveLength(servers.length ? 1 : 0); - expect(wrapped.find('ServerListItem')).toHaveLength(servers.length); + expect(screen.queryAllByRole('list')).toHaveLength(servers.length ? 1 : 0); + expect(screen.queryAllByRole('link')).toHaveLength(servers.length); }); it.each([ @@ -54,9 +46,7 @@ describe('', () => { [false, 'servers-list__list-group'], [undefined, 'servers-list__list-group'], ])('renders proper classes for embedded', (embedded, expectedClasses) => { - const wrapped = createComponent({ servers, embedded }); - const listGroup = wrapped.find(ListGroup); - - expect(listGroup.prop('className')).toEqual(expectedClasses); + setUp({ servers, embedded }); + expect(screen.getByRole('list')).toHaveAttribute('class', `${expectedClasses} list-group`); }); }); diff --git a/test/servers/__snapshots__/ManageServersRow.test.tsx.snap b/test/servers/__snapshots__/ManageServersRow.test.tsx.snap new file mode 100644 index 00000000..850ff084 --- /dev/null +++ b/test/servers/__snapshots__/ManageServersRow.test.tsx.snap @@ -0,0 +1,98 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders auto-connect icon only if server is autoConnect 1`] = ` +

+ + + + + + + + + +
+ + + + My server + + + https://example.com + + + ManageServersRowDropdown + +
+
+`; + +exports[` renders auto-connect icon only if server is autoConnect 2`] = ` +
+ + + + + + + + +
+ + + My server + + + https://example.com + + + ManageServersRowDropdown + +
+
+`; diff --git a/test/utils/Time.test.tsx b/test/utils/Time.test.tsx index a62dc06c..cb1f9c8f 100644 --- a/test/utils/Time.test.tsx +++ b/test/utils/Time.test.tsx @@ -1,30 +1,22 @@ -import { shallow, ShallowWrapper } from 'enzyme'; -import { DateProps, Time } from '../../src/utils/Time'; +import { render } from '@testing-library/react'; +import { TimeProps, Time } from '../../src/utils/Time'; import { parseDate } from '../../src/utils/helpers/date'; describe('