Merge pull request #639 from acelaya-forks/feature/more-rtl-tests

Feature/more rtl tests
This commit is contained in:
Alejandro Celaya 2022-05-06 19:16:07 +02:00 committed by GitHub
commit 116c36febc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 832 additions and 884 deletions

1157
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -20,7 +20,7 @@
"test:coverage": "npm run test -- --coverage --coverageReporters=text --coverageReporters=text-summary",
"test:ci": "npm run test:coverage -- --coverageReporters=clover",
"test:pretty": "npm run test:coverage -- --coverageReporters=html",
"mutate": "./node_modules/.bin/stryker run --concurrency 4"
"mutate": "./node_modules/.bin/stryker run --concurrency 4 --ignoreStatic"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^6.0.0",
@ -68,9 +68,9 @@
},
"devDependencies": {
"@shlinkio/eslint-config-js-coding-standard": "~2.0.0",
"@stryker-mutator/core": "^5.6.1",
"@stryker-mutator/jest-runner": "^5.6.1",
"@stryker-mutator/typescript-checker": "^5.6.1",
"@stryker-mutator/core": "^6.0.2",
"@stryker-mutator/jest-runner": "^6.0.2",
"@stryker-mutator/typescript-checker": "^6.0.2",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.1.1",
"@types/classnames": "^2.3.1",

View file

@ -1,7 +1,7 @@
import { useEffect, FC } from 'react';
import { Route, Routes, useLocation } from 'react-router-dom';
import classNames from 'classnames';
import NotFound from '../common/NotFound';
import { NotFound } from '../common/NotFound';
import { ServersMap } from '../servers/data';
import { Settings } from '../settings/reducers/settings';
import { changeThemeInMarkup } from '../utils/theme';
@ -17,7 +17,7 @@ interface AppProps {
appUpdated: boolean;
}
const App = (
export const App = (
MainHeader: FC,
Home: FC,
MenuLayout: FC,
@ -65,5 +65,3 @@ const App = (
</div>
);
};
export default App;

View file

@ -1,6 +1,6 @@
import Bottle from 'bottlejs';
import { appUpdateAvailable, resetAppUpdate } from '../reducers/appUpdates';
import App from '../App';
import { App } from '../App';
import { ConnectDecorator } from '../../container/types';
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {

View file

@ -24,7 +24,7 @@ export const AppUpdateBanner: FC<AppUpdateBannerProps> = ({ isOpen, toggle, forc
<h4 className="mb-4">This app has just been updated!</h4>
<p className="mb-0">
Restart it to enjoy the new features.
<Button disabled={isUpdating} className="ms-2" color="secondary" size="sm" onClick={update}>
<Button role="button" disabled={isUpdating} className="ms-2" color="secondary" size="sm" onClick={update}>
{!isUpdating && <>Restart now <FontAwesomeIcon icon={reloadIcon} className="ms-1" /></>}
{isUpdating && <>Restarting...</>}
</Button>

View file

@ -6,10 +6,10 @@ interface ErrorHandlerState {
hasError: boolean;
}
const ErrorHandlerCreator = (
export const ErrorHandler = (
{ location }: Window,
{ error }: Console,
) => class ErrorHandler extends Component<any, ErrorHandlerState> {
) => class extends Component<any, ErrorHandlerState> {
public constructor(props: object) {
super(props);
this.state = { hasError: false };
@ -44,5 +44,3 @@ const ErrorHandlerCreator = (
return children;
}
};
export default ErrorHandlerCreator;

View file

@ -10,11 +10,11 @@ import { ServersMap } from '../servers/data';
import { ShlinkLogo } from './img/ShlinkLogo';
import './Home.scss';
export interface HomeProps {
interface HomeProps {
servers: ServersMap;
}
const Home = ({ servers }: HomeProps) => {
export const Home = ({ servers }: HomeProps) => {
const navigate = useNavigate();
const serversList = values(servers);
const hasServers = !isEmpty(serversList);
@ -65,5 +65,3 @@ const Home = ({ servers }: HomeProps) => {
</div>
);
};
export default Home;

View file

@ -8,7 +8,7 @@ import { useToggle } from '../utils/helpers/hooks';
import { ShlinkLogo } from './img/ShlinkLogo';
import './MainHeader.scss';
const MainHeader = (ServersDropdown: FC) => () => {
export const MainHeader = (ServersDropdown: FC) => () => {
const [isOpen, toggleOpen, , close] = useToggle();
const location = useLocation();
const { pathname } = location;
@ -41,5 +41,3 @@ const MainHeader = (ServersDropdown: FC) => () => {
</Navbar>
);
};
export default MainHeader;

View file

@ -7,7 +7,7 @@ import { withSelectedServer } from '../servers/helpers/withSelectedServer';
import { useSwipeable, useToggle } from '../utils/helpers/hooks';
import { supportsDomainRedirects, supportsDomainVisits, supportsNonOrphanVisits } from '../utils/helpers/features';
import { isReachableServer } from '../servers/data';
import NotFound from './NotFound';
import { NotFound } from './NotFound';
import { AsideMenuProps } from './AsideMenu';
import './MenuLayout.scss';
@ -16,7 +16,7 @@ interface MenuLayoutProps {
sidebarNotPresent: Function;
}
const MenuLayout = (
export const MenuLayout = (
TagsList: FC,
ShortUrlsList: FC,
AsideMenu: FC<AsideMenuProps>,
@ -86,5 +86,3 @@ const MenuLayout = (
</>
);
}, ServerError);
export default MenuLayout;

View file

@ -4,7 +4,7 @@ import { SimpleCard } from '../utils/SimpleCard';
type NotFoundProps = PropsWithChildren<{ to?: string }>;
const NotFound: FC<NotFoundProps> = ({ to = '/', children = 'Home' }) => (
export const NotFound: FC<NotFoundProps> = ({ to = '/', children = 'Home' }) => (
<div className="home">
<SimpleCard className="p-4">
<h2>Oops! We could not find requested route.</h2>
@ -17,5 +17,3 @@ const NotFound: FC<NotFoundProps> = ({ to = '/', children = 'Home' }) => (
</SimpleCard>
</div>
);
export default NotFound;

View file

@ -1,11 +1,11 @@
import axios from 'axios';
import Bottle from 'bottlejs';
import ScrollToTop from '../ScrollToTop';
import MainHeader from '../MainHeader';
import Home from '../Home';
import MenuLayout from '../MenuLayout';
import { MainHeader } from '../MainHeader';
import { Home } from '../Home';
import { MenuLayout } from '../MenuLayout';
import AsideMenu from '../AsideMenu';
import ErrorHandler from '../ErrorHandler';
import { ErrorHandler } from '../ErrorHandler';
import ShlinkVersionsContainer from '../ShlinkVersionsContainer';
import { ConnectDecorator } from '../../container/types';
import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServer';

View file

@ -1,71 +1,61 @@
import { shallow, ShallowWrapper } from 'enzyme';
import { Route, useLocation } from 'react-router-dom';
import { render, screen } from '@testing-library/react';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { Mock } from 'ts-mockery';
import { Settings } from '../../src/settings/reducers/settings';
import appFactory from '../../src/app/App';
import { AppUpdateBanner } from '../../src/common/AppUpdateBanner';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn().mockReturnValue({}),
}));
import { App as createApp } from '../../src/app/App';
describe('<App />', () => {
let wrapper: ShallowWrapper;
const MainHeader = () => null;
const ShlinkVersions = () => null;
const App = appFactory(
MainHeader,
() => null,
() => null,
() => null,
() => null,
() => null,
() => null,
ShlinkVersions,
const App = createApp(
() => <>MainHeader</>,
() => <>Home</>,
() => <>MenuLayout</>,
() => <>CreateServer</>,
() => <>EditServer</>,
() => <>SettingsComp</>,
() => <>ManageServers</>,
() => <>ShlinkVersions</>,
);
const createWrapper = () => {
wrapper = shallow(
<App
fetchServers={() => {}}
servers={{}}
settings={Mock.all<Settings>()}
appUpdated={false}
resetAppUpdate={() => {}}
/>,
);
const setUp = (activeRoute = '/') => {
const history = createMemoryHistory();
history.push(activeRoute);
return wrapper;
return render(
<Router location={history.location} navigator={history}>
<App
fetchServers={() => {}}
servers={{}}
settings={Mock.all<Settings>()}
appUpdated
resetAppUpdate={() => {}}
/>
</Router>,
);
};
afterEach(jest.clearAllMocks);
afterEach(() => wrapper?.unmount());
it('renders children components', () => {
const wrapper = createWrapper();
setUp();
expect(wrapper.find(MainHeader)).toHaveLength(1);
expect(wrapper.find(ShlinkVersions)).toHaveLength(1);
expect(wrapper.find(AppUpdateBanner)).toHaveLength(1);
expect(screen.getByText('MainHeader')).toBeInTheDocument();
expect(screen.getByText('ShlinkVersions')).toBeInTheDocument();
expect(screen.getByText('This app has just been updated!')).toBeInTheDocument();
});
it('renders app main routes', () => {
const wrapper = createWrapper();
const routes = wrapper.find(Route);
const expectedPaths = [
undefined,
'/settings/*',
'/manage-servers',
'/server/create',
'/server/:serverId/edit',
'/server/:serverId/*',
];
expect.assertions(expectedPaths.length + 1);
expect(routes).toHaveLength(expectedPaths.length + 1);
expectedPaths.forEach((path, index) => {
expect(routes.at(index).prop('path')).toEqual(path);
});
it.each([
['/settings/foo', 'SettingsComp'],
['/settings/bar', 'SettingsComp'],
['/manage-servers', 'ManageServers'],
['/server/create', 'CreateServer'],
['/server/abc123/edit', 'EditServer'],
['/server/def456/edit', 'EditServer'],
['/server/abc123/foo', 'MenuLayout'],
['/server/def456/bar', 'MenuLayout'],
['/other', 'Oops! We could not find requested route.'],
])('renders expected route', async (activeRoute, expectedComponent) => {
setUp(activeRoute);
expect(await screen.findByText(expectedComponent)).toBeInTheDocument();
});
it.each([
@ -73,11 +63,9 @@ describe('<App />', () => {
['/bar', 'shlink-wrapper'],
['/', 'shlink-wrapper d-flex d-md-block align-items-center'],
])('renders expected classes on shlink-wrapper based on current pathname', (pathname, expectedClasses) => {
(useLocation as any).mockReturnValue({ pathname });
const { container } = setUp(pathname);
const shlinkWrapper = container.querySelector('.shlink-wrapper');
const wrapper = createWrapper();
const shlinkWrapper = wrapper.find('.shlink-wrapper');
expect(shlinkWrapper.prop('className')).toEqual(expectedClasses);
expect(shlinkWrapper).toHaveAttribute('class', expectedClasses);
});
});

View file

@ -1,43 +1,31 @@
import { shallow, ShallowWrapper } from 'enzyme';
import { Button } from 'reactstrap';
import { fireEvent, render, screen } from '@testing-library/react';
import { AppUpdateBanner } from '../../src/common/AppUpdateBanner';
import { SimpleCard } from '../../src/utils/SimpleCard';
describe('<AppUpdateBanner />', () => {
const toggle = jest.fn();
const forceUpdate = jest.fn();
let wrapper: ShallowWrapper;
beforeEach(() => {
wrapper = shallow(<AppUpdateBanner isOpen toggle={toggle} forceUpdate={forceUpdate} />);
});
beforeEach(() => render(<AppUpdateBanner isOpen toggle={toggle} forceUpdate={forceUpdate} />));
afterEach(jest.clearAllMocks);
afterEach(() => wrapper?.unmount());
it('renders an alert with expected props', () => {
expect(wrapper.prop('className')).toEqual('app-update-banner');
expect(wrapper.prop('isOpen')).toEqual(true);
expect(wrapper.prop('toggle')).toEqual(toggle);
expect(wrapper.prop('tag')).toEqual(SimpleCard);
expect(wrapper.prop('color')).toEqual('secondary');
it('renders initial state', () => {
expect(screen.getByRole('heading')).toHaveTextContent('This app has just been updated!');
expect(screen.queryByText('Restarting...')).not.toBeInTheDocument();
expect(screen.getByText('Restart now')).not.toHaveAttribute('disabled');
});
it('invokes toggle when alert is toggled', () => {
(wrapper.prop('toggle') as Function)();
it('invokes toggle when alert is closed', () => {
expect(toggle).not.toHaveBeenCalled();
fireEvent.click(screen.getByLabelText('Close'));
expect(toggle).toHaveBeenCalled();
});
it('triggers the update when clicking the button', () => {
expect(wrapper.find(Button).html()).toContain('Restart now');
expect(wrapper.find(Button).prop('disabled')).toEqual(false);
it('triggers the update when clicking the button', async () => {
expect(forceUpdate).not.toHaveBeenCalled();
wrapper.find(Button).simulate('click');
expect(wrapper.find(Button).html()).toContain('Restarting...');
expect(wrapper.find(Button).prop('disabled')).toEqual(true);
fireEvent.click(screen.getByText(/^Restart now/));
expect(forceUpdate).toHaveBeenCalled();
expect(await screen.findByText('Restarting...')).toBeInTheDocument();
expect(screen.queryByText(/^Restart now/)).not.toBeInTheDocument();
});
});

View file

@ -1,32 +1,41 @@
import { shallow, ShallowWrapper } from 'enzyme';
import { render, screen } from '@testing-library/react';
import { Mock } from 'ts-mockery';
import { MemoryRouter } from 'react-router-dom';
import asideMenuCreator from '../../src/common/AsideMenu';
import { ReachableServer } from '../../src/servers/data';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn().mockReturnValue({ pathname: '' }),
}));
import { SemVer } from '../../src/utils/helpers/version';
describe('<AsideMenu />', () => {
let wrapped: ShallowWrapper;
const DeleteServerButton = () => null;
const AsideMenu = asideMenuCreator(() => <>DeleteServerButton</>);
const setUp = (version: SemVer, id: string | false = 'abc123') => render(
<MemoryRouter>
<AsideMenu selectedServer={Mock.of<ReachableServer>({ id: id || undefined, version })} />
</MemoryRouter>,
);
beforeEach(() => {
const AsideMenu = asideMenuCreator(DeleteServerButton);
it.each([
['2.7.0' as SemVer, 5],
['2.8.0' as SemVer, 6],
])('contains links to different sections', (version, expectedAmountOfLinks) => {
setUp(version);
wrapped = shallow(<AsideMenu selectedServer={Mock.of<ReachableServer>({ id: 'abc123' })} />);
});
afterEach(() => wrapped.unmount());
const links = screen.getAllByRole('link');
it('contains links to different sections', () => {
const links = wrapped.find('[to]');
expect(links).toHaveLength(5);
links.forEach((link) => expect(link.prop('to')).toContain('abc123'));
expect.assertions(links.length + 1);
expect(links).toHaveLength(expectedAmountOfLinks);
links.forEach((link) => expect(link.getAttribute('href')).toContain('abc123'));
});
it('contains a button to delete server', () => {
expect(wrapped.find(DeleteServerButton)).toHaveLength(1);
it.each([
['abc', true],
[false, false],
])('contains a button to delete server if appropriate', (id, shouldHaveBtn) => {
setUp('2.8.0', id as string | false);
if (shouldHaveBtn) {
expect(screen.getByText('DeleteServerButton')).toBeInTheDocument();
} else {
expect(screen.queryByText('DeleteServerButton')).not.toBeInTheDocument();
}
});
});

View file

@ -1,38 +1,44 @@
import { shallow, ShallowWrapper } from 'enzyme';
import { Button } from 'reactstrap';
import { fireEvent, render, screen } from '@testing-library/react';
import { Mock } from 'ts-mockery';
import createErrorHandler from '../../src/common/ErrorHandler';
import { SimpleCard } from '../../src/utils/SimpleCard';
import { ErrorHandler as createErrorHandler } from '../../src/common/ErrorHandler';
const ComponentWithError = () => {
throw new Error('Error!!');
};
describe('<ErrorHandler />', () => {
const reload = jest.fn();
const window = Mock.of<Window>({
location: {
reload: jest.fn(),
},
location: { reload },
});
const console = Mock.of<Console>({ error: jest.fn() });
let wrapper: ShallowWrapper;
const cons = Mock.of<Console>({ error: jest.fn() });
const ErrorHandler = createErrorHandler(window, cons);
beforeEach(() => {
const ErrorHandler = createErrorHandler(window, console);
wrapper = shallow(<ErrorHandler children={<span>Foo</span>} />);
jest.spyOn(console, 'error').mockImplementation(() => {}); // Silence react errors
});
afterEach(() => wrapper.unmount());
afterEach(jest.resetAllMocks);
it('renders children when no error has occurred', () => {
expect(wrapper.text()).toEqual('Foo');
expect(wrapper.find(Button)).toHaveLength(0);
render(<ErrorHandler children={<span>Foo</span>} />);
expect(screen.getByText('Foo')).toBeInTheDocument();
expect(screen.queryByText('Oops! This is awkward :S')).not.toBeInTheDocument();
expect(screen.queryByRole('button')).not.toBeInTheDocument();
});
it('renders error page when error has occurred', () => {
wrapper.setState({ hasError: true });
render(<ErrorHandler children={<ComponentWithError />} />);
expect(wrapper.find(SimpleCard).contains('Oops! This is awkward :S')).toEqual(true);
expect(wrapper.find(SimpleCard).contains(
'It seems that something went wrong. Try refreshing the page or just click this button.',
)).toEqual(true);
expect(wrapper.find(Button)).toHaveLength(1);
expect(screen.getByText('Oops! This is awkward :S')).toBeInTheDocument();
expect(screen.getByRole('button')).toBeInTheDocument();
});
it('reloads page on button click', () => {
render(<ErrorHandler children={<ComponentWithError />} />);
expect(reload).not.toHaveBeenCalled();
fireEvent.click(screen.getByRole('button'));
expect(reload).toHaveBeenCalled();
});
});

View file

@ -1,31 +1,19 @@
import { shallow, ShallowWrapper } from 'enzyme';
import { render, screen } from '@testing-library/react';
import { Mock } from 'ts-mockery';
import Home, { HomeProps } from '../../src/common/Home';
import { ServerWithId } from '../../src/servers/data';
import { ShlinkLogo } from '../../src/common/img/ShlinkLogo';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: jest.fn().mockReturnValue(jest.fn()),
}));
import { MemoryRouter } from 'react-router-dom';
import { Home } from '../../src/common/Home';
import { ServersMap, ServerWithId } from '../../src/servers/data';
describe('<Home />', () => {
let wrapped: ShallowWrapper;
const createComponent = (props: Partial<HomeProps> = {}) => {
const actualProps = { resetSelectedServer: jest.fn(), servers: {}, ...props };
const setUp = (servers: ServersMap = {}) => render(
<MemoryRouter>
<Home servers={servers} />
</MemoryRouter>,
);
wrapped = shallow(<Home {...actualProps} />);
return wrapped;
};
afterEach(() => wrapped?.unmount());
it('renders logo and title', () => {
const wrapped = createComponent();
expect(wrapped.find(ShlinkLogo)).toHaveLength(1);
expect(wrapped.find('.home__title')).toHaveLength(1);
it('renders title', () => {
setUp();
expect(screen.getByRole('heading', { name: 'Welcome!' })).toBeInTheDocument();
});
it.each([
@ -33,14 +21,20 @@ describe('<Home />', () => {
{
'1a': Mock.of<ServerWithId>({ name: 'foo', id: '1' }),
'2b': Mock.of<ServerWithId>({ name: 'bar', id: '2' }),
'3c': Mock.of<ServerWithId>({ name: 'baz', id: '3' }),
},
0,
3,
],
[{}, 3],
])('shows link to create or set-up server only when no servers exist', (servers, expectedParagraphs) => {
const wrapped = createComponent({ servers });
const p = wrapped.find('p');
[{}, 2],
])('shows link to create or set-up server only when no servers exist', (servers, expectedServers) => {
setUp(servers);
const links = screen.getAllByRole('link');
expect(p).toHaveLength(expectedParagraphs);
expect(links).toHaveLength(expectedServers);
if (Object.keys(servers).length === 0) {
expect(screen.getByText('This application will help you manage your Shlink servers.')).toBeInTheDocument();
expect(screen.getByText('Learn more about Shlink')).toBeInTheDocument();
}
});
});

View file

@ -1,34 +1,24 @@
import { shallow, ShallowWrapper } from 'enzyme';
import { useLocation } from 'react-router-dom';
import { Collapse, NavbarToggler, NavLink } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import createMainHeader from '../../src/common/MainHeader';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn().mockReturnValue({}),
}));
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { MainHeader as createMainHeader } from '../../src/common/MainHeader';
describe('<MainHeader />', () => {
const ServersDropdown = () => null;
const MainHeader = createMainHeader(ServersDropdown);
let wrapper: ShallowWrapper;
const MainHeader = createMainHeader(() => <>ServersDropdown</>);
const setUp = (pathname = '') => {
const history = createMemoryHistory();
history.push(pathname);
const createWrapper = (pathname = '') => {
(useLocation as any).mockReturnValue({ pathname });
wrapper = shallow(<MainHeader />);
return wrapper;
return render(
<Router location={history.location} navigator={history}>
<MainHeader />
</Router>,
);
};
afterEach(jest.clearAllMocks);
afterEach(() => wrapper?.unmount());
it('renders ServersDropdown', () => {
const wrapper = createWrapper();
expect(wrapper.find(ServersDropdown)).toHaveLength(1);
setUp();
expect(screen.getByText('ServersDropdown')).toBeInTheDocument();
});
it.each([
@ -38,31 +28,38 @@ describe('<MainHeader />', () => {
['/settings/foo', true],
['/settings/bar', true],
])('sets link to settings as active only when current path is settings', (currentPath, isActive) => {
const wrapper = createWrapper(currentPath);
const settingsLink = wrapper.find(NavLink);
setUp(currentPath);
expect(settingsLink.prop('active')).toEqual(isActive);
if (isActive) {
expect(screen.getByText(/Settings$/).getAttribute('class')).toContain('active');
} else {
expect(screen.getByText(/Settings$/).getAttribute('class')).not.toContain('active');
}
});
it('renders expected class based on the nav bar state', () => {
const wrapper = createWrapper();
setUp();
expect(wrapper.find(NavbarToggler).find(FontAwesomeIcon).prop('className')).toEqual('main-header__toggle-icon');
wrapper.find(NavbarToggler).simulate('click');
expect(wrapper.find(NavbarToggler).find(FontAwesomeIcon).prop('className')).toEqual(
'main-header__toggle-icon main-header__toggle-icon--opened',
const toggle = screen.getByLabelText('Toggle navigation');
const icon = toggle.firstChild;
expect(icon).toHaveAttribute('class', expect.stringMatching(/main-header__toggle-icon$/));
fireEvent.click(toggle);
expect(icon).toHaveAttribute(
'class',
expect.stringMatching(/main-header__toggle-icon main-header__toggle-icon--opened$/),
);
wrapper.find(NavbarToggler).simulate('click');
expect(wrapper.find(NavbarToggler).find(FontAwesomeIcon).prop('className')).toEqual('main-header__toggle-icon');
fireEvent.click(toggle);
expect(icon).toHaveAttribute('class', expect.stringMatching(/main-header__toggle-icon$/));
});
it('opens Collapse when clicking toggle', () => {
const wrapper = createWrapper();
it('opens Collapse when clicking toggle', async () => {
const { container } = setUp();
const collapse = container.querySelector('.collapse');
const toggle = screen.getByLabelText('Toggle navigation');
expect(wrapper.find(Collapse).prop('isOpen')).toEqual(false);
wrapper.find(NavbarToggler).simulate('click');
expect(wrapper.find(Collapse).prop('isOpen')).toEqual(true);
wrapper.find(NavbarToggler).simulate('click');
expect(wrapper.find(Collapse).prop('isOpen')).toEqual(false);
expect(collapse).not.toHaveAttribute('class', expect.stringContaining('show'));
fireEvent.click(toggle);
await waitFor(() => expect(collapse).toHaveAttribute('class', expect.stringContaining('show')));
});
});

View file

@ -1,70 +1,89 @@
import { shallow, ShallowWrapper } from 'enzyme';
import { Route, useParams } from 'react-router-dom';
import { render, screen } from '@testing-library/react';
import { Router, useParams } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { Mock } from 'ts-mockery';
import createMenuLayout from '../../src/common/MenuLayout';
import { MenuLayout as createMenuLayout } from '../../src/common/MenuLayout';
import { NonReachableServer, NotFoundServer, ReachableServer, SelectedServer } from '../../src/servers/data';
import { NoMenuLayout } from '../../src/common/NoMenuLayout';
import { SemVer } from '../../src/utils/helpers/version';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useParams: jest.fn().mockReturnValue({}),
useLocation: jest.fn().mockReturnValue({}),
}));
jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useParams: jest.fn() }));
describe('<MenuLayout />', () => {
const ServerError = jest.fn();
const C = jest.fn();
const MenuLayout = createMenuLayout(C, C, C, C, C, C, C, C, C, ServerError, C, C, C);
let wrapper: ShallowWrapper;
const createWrapper = (selectedServer: SelectedServer) => {
(useParams as any).mockReturnValue({ serverId: 'abc123' });
const MenuLayout = createMenuLayout(
() => <>TagsList</>,
() => <>ShortUrlsList</>,
() => <>AsideMenu</>,
() => <>CreateShortUrl</>,
() => <>ShortUrlVisits</>,
() => <>TagVisits</>,
() => <>DomainVisits</>,
() => <>OrphanVisits</>,
() => <>NonOrphanVisits</>,
() => <>ServerError</>,
() => <>Overview</>,
() => <>EditShortUrl</>,
() => <>ManageDomains</>,
);
const setUp = (selectedServer: SelectedServer, currentPath = '/') => {
const history = createMemoryHistory();
history.push(currentPath);
wrapper = shallow(
<MenuLayout
sidebarNotPresent={jest.fn()}
sidebarPresent={jest.fn()}
selectServer={jest.fn()}
selectedServer={selectedServer}
/>,
return render(
<Router location={history.location} navigator={history}>
<MenuLayout
sidebarNotPresent={jest.fn()}
sidebarPresent={jest.fn()}
selectServer={jest.fn()}
selectedServer={selectedServer}
/>
</Router>,
);
return wrapper;
};
beforeEach(() => {
(useParams as any).mockReturnValue({ serverId: 'abc123' });
});
afterEach(jest.clearAllMocks);
afterEach(() => wrapper?.unmount());
it.each([
[null, NoMenuLayout],
[Mock.of<NotFoundServer>({ serverNotFound: true }), ServerError],
])('returns error when server is not found', (selectedServer, ExpectedComp) => {
const wrapper = createWrapper(selectedServer);
const comp = wrapper.find(ExpectedComp);
it('shows loading indicator while loading server', () => {
setUp(null);
expect(comp).toHaveLength(1);
});
it('returns error if server is not reachable', () => {
const wrapper = createWrapper(Mock.of<NonReachableServer>()).dive();
const serverError = wrapper.find(ServerError);
expect(serverError).toHaveLength(1);
expect(screen.getByText('Loading...')).toBeInTheDocument();
expect(screen.queryByText('ServerError')).not.toBeInTheDocument();
});
it.each([
['2.6.0' as SemVer, 10],
['2.7.0' as SemVer, 10],
['2.8.0' as SemVer, 11],
['2.10.0' as SemVer, 11],
['3.0.0' as SemVer, 12],
['3.1.0' as SemVer, 13],
])('has expected amount of routes based on selected server\'s version', (version, expectedAmountOfRoutes) => {
const selectedServer = Mock.of<ReachableServer>({ version });
const wrapper = createWrapper(selectedServer).dive();
const routes = wrapper.find(Route);
[Mock.of<NotFoundServer>({ serverNotFound: true })],
[Mock.of<NonReachableServer>({ serverNotReachable: true })],
])('shows error for non reachable servers', (selectedServer) => {
setUp(selectedServer);
expect(routes).toHaveLength(expectedAmountOfRoutes);
expect(routes.findWhere((element) => element.prop('index'))).toHaveLength(1);
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
expect(screen.getByText('ServerError')).toBeInTheDocument();
});
it.each([
['3.0.0' as SemVer, '/overview', 'Overview'],
['3.0.0' as SemVer, '/list-short-urls/1', 'ShortUrlsList'],
['3.0.0' as SemVer, '/create-short-url', 'CreateShortUrl'],
['3.0.0' as SemVer, '/short-code/abc123/visits/foo', 'ShortUrlVisits'],
['3.0.0' as SemVer, '/short-code/abc123/edit', 'EditShortUrl'],
['3.0.0' as SemVer, '/tag/foo/visits/foo', 'TagVisits'],
['3.0.0' as SemVer, '/orphan-visits/foo', 'OrphanVisits'],
['3.0.0' as SemVer, '/manage-tags', 'TagsList'],
['3.0.0' as SemVer, '/not-found', 'Oops! We could not find requested route.'],
['3.0.0' as SemVer, '/domain/domain.com/visits/foo', 'Oops! We could not find requested route.'],
['3.1.0' as SemVer, '/domain/domain.com/visits/foo', 'DomainVisits'],
['2.10.0' as SemVer, '/non-orphan-visits/foo', 'Oops! We could not find requested route.'],
['3.0.0' as SemVer, '/non-orphan-visits/foo', 'NonOrphanVisits'],
['2.7.0' as SemVer, '/manage-domains', 'Oops! We could not find requested route.'],
['2.8.0' as SemVer, '/manage-domains', 'ManageDomains'],
])(
'renders expected component based on location and server version',
(version, currentPath, expectedContent) => {
setUp(Mock.of<ReachableServer>({ version }), currentPath);
expect(screen.getByText(expectedContent)).toBeInTheDocument();
},
);
});

View file

@ -1,6 +1,6 @@
import { shallow, ShallowWrapper } from 'enzyme';
import { Link } from 'react-router-dom';
import NotFound from '../../src/common/NotFound';
import { NotFound } from '../../src/common/NotFound';
import { SimpleCard } from '../../src/utils/SimpleCard';
describe('<NotFound />', () => {

View file

@ -9,12 +9,6 @@ import { Settings } from '../../src/settings/reducers/settings';
import { ReportExporter } from '../../src/common/services/ReportExporter';
import { SelectedServer } from '../../src/servers/data';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: jest.fn().mockReturnValue(jest.fn()),
useParams: jest.fn().mockReturnValue({}),
}));
describe('<NonOrphanVisits />', () => {
const exportVisits = jest.fn();
const getNonOrphanVisits = jest.fn();