Migrated ImportServersBtn test to react testing library

This commit is contained in:
Alejandro Celaya 2022-06-05 11:06:26 +02:00
parent 30f502a51b
commit a012d6206f
2 changed files with 51 additions and 62 deletions

View file

@ -1,4 +1,4 @@
import { useRef, RefObject, ChangeEvent, MutableRefObject, useState, useEffect, FC, PropsWithChildren } from 'react'; import { useRef, ChangeEvent, useState, useEffect, FC, PropsWithChildren } from 'react';
import { Button, UncontrolledTooltip } from 'reactstrap'; import { Button, UncontrolledTooltip } from 'reactstrap';
import { complement, pipe } from 'ramda'; import { complement, pipe } from 'ramda';
import { faFileUpload as importIcon } from '@fortawesome/free-solid-svg-icons'; import { faFileUpload as importIcon } from '@fortawesome/free-solid-svg-icons';
@ -9,8 +9,6 @@ import { ServerData, ServersMap } from '../data';
import { DuplicatedServersModal } from './DuplicatedServersModal'; import { DuplicatedServersModal } from './DuplicatedServersModal';
import './ImportServersBtn.scss'; import './ImportServersBtn.scss';
type Ref<T> = RefObject<T> | MutableRefObject<T>;
export type ImportServersBtnProps = PropsWithChildren<{ export type ImportServersBtnProps = PropsWithChildren<{
onImport?: () => void; onImport?: () => void;
onImportError?: (error: Error) => void; onImportError?: (error: Error) => void;
@ -21,7 +19,6 @@ export type ImportServersBtnProps = PropsWithChildren<{
interface ImportServersBtnConnectProps extends ImportServersBtnProps { interface ImportServersBtnConnectProps extends ImportServersBtnProps {
createServers: (servers: ServerData[]) => void; createServers: (servers: ServerData[]) => void;
servers: ServersMap; servers: ServersMap;
fileRef: Ref<HTMLInputElement>;
} }
const serversFiltering = (servers: ServerData[]) => const serversFiltering = (servers: ServerData[]) =>
@ -30,14 +27,13 @@ const serversFiltering = (servers: ServerData[]) =>
export const ImportServersBtn = ({ importServersFromFile }: ServersImporter): FC<ImportServersBtnConnectProps> => ({ export const ImportServersBtn = ({ importServersFromFile }: ServersImporter): FC<ImportServersBtnConnectProps> => ({
createServers, createServers,
servers, servers,
fileRef,
children, children,
onImport = () => {}, onImport = () => {},
onImportError = () => {}, onImportError = () => {},
tooltipPlacement = 'bottom', tooltipPlacement = 'bottom',
className = '', className = '',
}) => { }) => {
const ref = fileRef ?? useRef<HTMLInputElement>(); const ref = useRef<HTMLInputElement>();
const [serversToCreate, setServersToCreate] = useState<ServerData[] | undefined>(); const [serversToCreate, setServersToCreate] = useState<ServerData[] | undefined>();
const [duplicatedServers, setDuplicatedServers] = useState<ServerData[]>([]); const [duplicatedServers, setDuplicatedServers] = useState<ServerData[]>([]);
const [isModalOpen,, showModal, hideModal] = useToggle(); const [isModalOpen,, showModal, hideModal] = useToggle();
@ -78,7 +74,15 @@ export const ImportServersBtn = ({ importServersFromFile }: ServersImporter): FC
You can create servers by importing a CSV file with columns <b>name</b>, <b>apiKey</b> and <b>url</b>. You can create servers by importing a CSV file with columns <b>name</b>, <b>apiKey</b> and <b>url</b>.
</UncontrolledTooltip> </UncontrolledTooltip>
<input type="file" accept="text/csv" className="import-servers-btn__csv-select" ref={ref} onChange={onFile} /> <input
type="file"
accept="text/csv"
className="import-servers-btn__csv-select"
ref={(el) => {
ref.current = el ?? undefined;
}}
onChange={onFile}
/>
<DuplicatedServersModal <DuplicatedServersModal
isOpen={isModalOpen} isOpen={isModalOpen}

View file

@ -1,46 +1,41 @@
import { ReactNode } from 'react'; import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { shallow, ShallowWrapper } from 'enzyme'; import userEvent from '@testing-library/user-event';
import { UncontrolledTooltip } from 'reactstrap';
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import { import {
ImportServersBtn as createImportServersBtn, ImportServersBtn as createImportServersBtn,
ImportServersBtnProps, ImportServersBtnProps,
} from '../../../src/servers/helpers/ImportServersBtn'; } from '../../../src/servers/helpers/ImportServersBtn';
import { ServersImporter } from '../../../src/servers/services/ServersImporter'; import { ServersImporter } from '../../../src/servers/services/ServersImporter';
import { DuplicatedServersModal } from '../../../src/servers/helpers/DuplicatedServersModal'; import { ServersMap, ServerWithId } from '../../../src/servers/data';
describe('<ImportServersBtn />', () => { describe('<ImportServersBtn />', () => {
let wrapper: ShallowWrapper;
const onImportMock = jest.fn(); const onImportMock = jest.fn();
const createServersMock = jest.fn(); const createServersMock = jest.fn();
const importServersFromFile = jest.fn().mockResolvedValue([]); const importServersFromFile = jest.fn().mockResolvedValue([]);
const serversImporterMock = Mock.of<ServersImporter>({ importServersFromFile }); const serversImporterMock = Mock.of<ServersImporter>({ importServersFromFile });
const click = jest.fn();
const fileRef = { current: Mock.of<HTMLInputElement>({ click }) };
const ImportServersBtn = createImportServersBtn(serversImporterMock); const ImportServersBtn = createImportServersBtn(serversImporterMock);
const createWrapper = (props: Partial<ImportServersBtnProps & { children: ReactNode }> = {}) => { const setUp = (props: Partial<ImportServersBtnProps> = {}, servers: ServersMap = {}) => ({
wrapper = shallow( user: userEvent.setup(),
...render(
<ImportServersBtn <ImportServersBtn
servers={{}} servers={servers}
{...props} {...props}
fileRef={fileRef}
createServers={createServersMock} createServers={createServersMock}
onImport={onImportMock} onImport={onImportMock}
/>, />,
); ),
});
return wrapper;
};
afterEach(jest.clearAllMocks); afterEach(jest.clearAllMocks);
afterEach(() => wrapper.unmount());
it('renders a button, a tooltip and a file input', () => { it('shows tooltip on button hover', async () => {
const wrapper = createWrapper(); const { user } = setUp();
expect(wrapper.find('#importBtn')).toHaveLength(1); expect(screen.queryByText(/^You can create servers by importing a CSV file/)).not.toBeInTheDocument();
expect(wrapper.find(UncontrolledTooltip)).toHaveLength(1); await user.hover(screen.getByRole('button'));
expect(wrapper.find('.import-servers-btn__csv-select')).toHaveLength(1); await waitFor(
() => expect(screen.getByText(/^You can create servers by importing a CSV file/)).toBeInTheDocument(),
);
}); });
it.each([ it.each([
@ -48,53 +43,43 @@ describe('<ImportServersBtn />', () => {
['foo', 'foo'], ['foo', 'foo'],
['bar', 'bar'], ['bar', 'bar'],
])('allows a class name to be provided', (providedClassName, expectedClassName) => { ])('allows a class name to be provided', (providedClassName, expectedClassName) => {
const wrapper = createWrapper({ className: providedClassName }); setUp({ className: providedClassName });
expect(screen.getByRole('button')).toHaveAttribute('class', expect.stringContaining(expectedClassName));
expect(wrapper.find('#importBtn').prop('className')).toEqual(expectedClassName);
}); });
it.each([ it.each([
[undefined, true], [undefined, 'Import from file'],
['foo', false], ['foo', 'foo'],
['bar', false], ['bar', 'bar'],
])('has expected text', (children, expectToHaveDefaultText) => { ])('has expected text', (children, expectedText) => {
const wrapper = createWrapper({ children }); setUp({ children });
expect(screen.getByRole('button')).toHaveTextContent(expectedText);
if (expectToHaveDefaultText) {
expect(wrapper.find('#importBtn').html()).toContain('Import from file');
} else {
expect(wrapper.find('#importBtn').html()).toContain(children);
expect(wrapper.find('#importBtn').html()).not.toContain('Import from file');
}
});
it('triggers click on file ref when button is clicked', () => {
const wrapper = createWrapper();
const btn = wrapper.find('#importBtn');
btn.simulate('click');
expect(click).toHaveBeenCalledTimes(1);
}); });
it('imports servers when file input changes', async () => { it('imports servers when file input changes', async () => {
const wrapper = createWrapper(); const { container } = setUp();
const file = wrapper.find('.import-servers-btn__csv-select'); const input = container.querySelector('[type=file]');
await file.simulate('change', { target: { files: [''] } });
input && fireEvent.change(input, { target: { files: [''] } });
expect(importServersFromFile).toHaveBeenCalledTimes(1); expect(importServersFromFile).toHaveBeenCalledTimes(1);
}); });
it.each([ it.each([
['discard'], ['Save anyway', true],
['save'], ['Discard', false],
])('invokes callback in DuplicatedServersModal events', (event) => { ])('creates expected servers depending on selected option in modal', async (btnName, savesDuplicatedServers) => {
const wrapper = createWrapper(); const existingServer = Mock.of<ServerWithId>({ id: 'abc', url: 'existingUrl', apiKey: 'existingApiKey' });
const newServer = Mock.of<ServerWithId>({ url: 'newUrl', apiKey: 'newApiKey' });
const { container, user } = setUp({}, { abc: existingServer });
const input = container.querySelector('[type=file]');
importServersFromFile.mockResolvedValue([existingServer, newServer]);
wrapper.find(DuplicatedServersModal).simulate(event); expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
input && fireEvent.change(input, { target: { files: [''] } });
await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument());
await user.click(screen.getByRole('button', { name: btnName }));
expect(createServersMock).toHaveBeenCalledTimes(1); expect(createServersMock).toHaveBeenCalledWith(savesDuplicatedServers ? [existingServer, newServer] : [newServer]);
expect(onImportMock).toHaveBeenCalledTimes(1); expect(onImportMock).toHaveBeenCalledTimes(1);
}); });
}); });