diff --git a/test/__helpers__/MemoryRouterWithParams.tsx b/test/__helpers__/MemoryRouterWithParams.tsx new file mode 100644 index 00000000..ff95a44a --- /dev/null +++ b/test/__helpers__/MemoryRouterWithParams.tsx @@ -0,0 +1,23 @@ +import type { FC, PropsWithChildren } from 'react'; +import { useMemo } from 'react'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; + +export type MemoryRouterWithParamsProps = PropsWithChildren<{ + params: Record; +}>; + +/** + * Wrap any component using useParams() with MemoryRouterWithParams, in order to determine wat the hook should return + */ +export const MemoryRouterWithParams: FC = ({ children, params }) => { + const pathname = useMemo(() => `/${Object.values(params).join('/')}`, [params]); + const pathPattern = useMemo(() => `/:${Object.keys(params).join('/:')}`, [params]); + + return ( + + + + + + ); +}; diff --git a/test/common/ShlinkWebComponentContainer.test.tsx b/test/common/ShlinkWebComponentContainer.test.tsx index d39b00ea..61dade0d 100644 --- a/test/common/ShlinkWebComponentContainer.test.tsx +++ b/test/common/ShlinkWebComponentContainer.test.tsx @@ -1,14 +1,9 @@ import { render, screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; -import { useParams } from 'react-router-dom'; import { ShlinkWebComponentContainerFactory } from '../../src/common/ShlinkWebComponentContainer'; import type { NonReachableServer, NotFoundServer, SelectedServer } from '../../src/servers/data'; import { checkAccessibility } from '../__helpers__/accessibility'; - -vi.mock('react-router-dom', async () => ({ - ...(await vi.importActual('react-router-dom')), - useParams: vi.fn(), -})); +import { MemoryRouterWithParams } from '../__helpers__/MemoryRouterWithParams'; describe('', () => { const ShlinkWebComponentContainer = ShlinkWebComponentContainerFactory(fromPartial({ @@ -18,13 +13,11 @@ describe('', () => { ServerError: () => <>ServerError, })); const setUp = (selectedServer: SelectedServer) => render( - , + + + , ); - beforeEach(() => { - (useParams as any).mockReturnValue({ serverId: 'abc123' }); - }); - it('passes a11y checks', () => checkAccessibility(setUp(fromPartial({ version: '3.0.0' })))); it('shows loading indicator while loading server', () => { diff --git a/test/servers/CreateServer.test.tsx b/test/servers/CreateServer.test.tsx index 5a7d6256..7bb14e5b 100644 --- a/test/servers/CreateServer.test.tsx +++ b/test/servers/CreateServer.test.tsx @@ -1,16 +1,12 @@ import { fireEvent, screen, waitFor } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; -import { useNavigate } from 'react-router-dom'; +import { createMemoryHistory } from 'history'; +import { Router } from 'react-router-dom'; import { CreateServerFactory } from '../../src/servers/CreateServer'; import type { ServersMap } from '../../src/servers/data'; import { checkAccessibility } from '../__helpers__/accessibility'; import { renderWithEvents } from '../__helpers__/setUpTest'; -vi.mock('react-router-dom', async () => ({ - ...(await vi.importActual('react-router-dom')), - useNavigate: vi.fn(), -})); - type SetUpOptions = { serversImported?: boolean; importFailed?: boolean; @@ -19,13 +15,10 @@ type SetUpOptions = { describe('', () => { const createServersMock = vi.fn(); - const navigate = vi.fn(); const defaultServers: ServersMap = { foo: fromPartial({ url: 'https://existing_url.com', apiKey: 'existing_api_key' }), }; const setUp = ({ serversImported = false, importFailed = false, servers = defaultServers }: SetUpOptions = {}) => { - (useNavigate as any).mockReturnValue(navigate); - let callCount = 0; const useTimeoutToggle = vi.fn().mockImplementation(() => { const result = [callCount % 2 === 0 ? serversImported : importFailed, () => null]; @@ -36,8 +29,16 @@ describe('', () => { ImportServersBtn: () => <>ImportServersBtn, useTimeoutToggle, })); + const history = createMemoryHistory({ initialEntries: ['/foo', '/bar'] }); - return renderWithEvents(); + return { + history, + ...renderWithEvents( + + + , + ), + }; }; it('passes a11y checks', () => checkAccessibility(setUp())); @@ -67,7 +68,7 @@ describe('', () => { }); it('creates server data when form is submitted', async () => { - const { user } = setUp(); + const { user, history } = setUp(); expect(createServersMock).not.toHaveBeenCalled(); @@ -81,12 +82,12 @@ describe('', () => { url: 'https://the_url.com', apiKey: 'the_api_key', })]); - expect(navigate).toHaveBeenCalledWith(expect.stringMatching(/^\/server\//)); + expect(history.location.pathname).toEqual(expect.stringMatching(/^\/server\//)); expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); }); it('displays dialog when trying to create a duplicated server', async () => { - const { user } = setUp(); + const { user, history } = setUp(); await user.type(screen.getByLabelText(/^Name/), 'the_name'); await user.type(screen.getByLabelText(/^URL/), 'https://existing_url.com'); @@ -97,6 +98,6 @@ describe('', () => { await user.click(screen.getByRole('button', { name: 'Discard' })); expect(createServersMock).not.toHaveBeenCalled(); - expect(navigate).toHaveBeenCalledWith(-1); + expect(history.location.pathname).toEqual('/foo'); // Goes back to first route from history's initialEntries }); }); diff --git a/test/servers/DeleteServerModal.test.tsx b/test/servers/DeleteServerModal.test.tsx index 2af0fe63..af228aad 100644 --- a/test/servers/DeleteServerModal.test.tsx +++ b/test/servers/DeleteServerModal.test.tsx @@ -1,34 +1,33 @@ import { screen, waitFor } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; -import { useNavigate } from 'react-router-dom'; +import { createMemoryHistory } from 'history'; +import { Router } from 'react-router-dom'; import { DeleteServerModal } from '../../src/servers/DeleteServerModal'; import { checkAccessibility } from '../__helpers__/accessibility'; import { renderWithEvents } from '../__helpers__/setUpTest'; import { TestModalWrapper } from '../__helpers__/TestModalWrapper'; -vi.mock('react-router-dom', async () => ({ - ...(await vi.importActual('react-router-dom')), - useNavigate: vi.fn(), -})); - describe('', () => { const deleteServerMock = vi.fn(); - const navigate = vi.fn(); const serverName = 'the_server_name'; const setUp = () => { - (useNavigate as any).mockReturnValue(navigate); - - return renderWithEvents( - ( - + ( + + )} /> - )} - />, - ); + , + ), + }; }; it('passes a11y checks', () => checkAccessibility(setUp())); @@ -51,22 +50,23 @@ describe('', () => { [() => screen.getByRole('button', { name: 'Cancel' })], [() => screen.getByLabelText('Close')], ])('toggles when clicking cancel button', async (getButton) => { - const { user } = setUp(); + const { user, history } = setUp(); + expect(history.location.pathname).toEqual('/foo'); await user.click(getButton()); expect(deleteServerMock).not.toHaveBeenCalled(); - expect(navigate).not.toHaveBeenCalled(); + expect(history.location.pathname).toEqual('/foo'); // No navigation happens, keeping initial pathname }); it('deletes server when clicking accept button', async () => { - const { user } = setUp(); + const { user, history } = setUp(); expect(deleteServerMock).not.toHaveBeenCalled(); - expect(navigate).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(navigate).toHaveBeenCalledTimes(1)); + await waitFor(() => expect(history.location.pathname).toEqual('/')); }); }); diff --git a/test/servers/EditServer.test.tsx b/test/servers/EditServer.test.tsx index 640505cc..9400f065 100644 --- a/test/servers/EditServer.test.tsx +++ b/test/servers/EditServer.test.tsx @@ -1,20 +1,15 @@ import { fireEvent, screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; -import { MemoryRouter, useNavigate } from 'react-router-dom'; +import { createMemoryHistory } from 'history'; +import { Router } from 'react-router-dom'; import type { ReachableServer, SelectedServer } from '../../src/servers/data'; import { EditServerFactory } from '../../src/servers/EditServer'; import { checkAccessibility } from '../__helpers__/accessibility'; import { renderWithEvents } from '../__helpers__/setUpTest'; -vi.mock('react-router-dom', async () => ({ - ...(await vi.importActual('react-router-dom')), - useNavigate: vi.fn(), -})); - describe('', () => { const ServerError = vi.fn(); const editServerMock = vi.fn(); - const navigate = vi.fn(); const defaultSelectedServer = fromPartial({ id: 'abc123', name: 'the_name', @@ -22,15 +17,17 @@ describe('', () => { apiKey: 'the_api_key', }); const EditServer = EditServerFactory(fromPartial({ ServerError })); - const setUp = (selectedServer: SelectedServer = defaultSelectedServer) => renderWithEvents( - - - , - ); - - beforeEach(() => { - (useNavigate as any).mockReturnValue(navigate); - }); + const setUp = (selectedServer: SelectedServer = defaultSelectedServer) => { + const history = createMemoryHistory({ initialEntries: ['/foo', '/bar'] }); + return { + history, + ...renderWithEvents( + + + , + ), + }; + }; it('passes a11y checks', () => checkAccessibility(setUp())); @@ -56,7 +53,7 @@ describe('', () => { }); it('edits server and redirects to it when form is submitted', async () => { - const { user } = setUp(); + const { user, history } = setUp(); await user.type(screen.getByDisplayValue('the_name'), ' edited'); await user.type(screen.getByDisplayValue('the_url'), ' edited'); @@ -69,6 +66,8 @@ describe('', () => { url: 'the_url edited', apiKey: 'the_api_key', }); - expect(navigate).toHaveBeenCalledWith(-1); + + // After saving we go back, to the first route from history's initialEntries + expect(history.location.pathname).toEqual('/foo'); }); }); diff --git a/vite.config.ts b/vite.config.ts index 9acca6a4..8f3124c3 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -28,6 +28,7 @@ export default defineConfig({ // Vitest config test: { globals: true, + allowOnly: true, environment: 'jsdom', setupFiles: './config/test/setupTests.ts', coverage: {