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

View file

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

View file

@ -1,6 +1,6 @@
import Bottle from 'bottlejs'; import Bottle from 'bottlejs';
import { appUpdateAvailable, resetAppUpdate } from '../reducers/appUpdates'; import { appUpdateAvailable, resetAppUpdate } from '../reducers/appUpdates';
import App from '../App'; import { App } from '../App';
import { ConnectDecorator } from '../../container/types'; import { ConnectDecorator } from '../../container/types';
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { 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> <h4 className="mb-4">This app has just been updated!</h4>
<p className="mb-0"> <p className="mb-0">
Restart it to enjoy the new features. 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 && <>Restart now <FontAwesomeIcon icon={reloadIcon} className="ms-1" /></>}
{isUpdating && <>Restarting...</>} {isUpdating && <>Restarting...</>}
</Button> </Button>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,43 +1,31 @@
import { shallow, ShallowWrapper } from 'enzyme'; import { fireEvent, render, screen } from '@testing-library/react';
import { Button } from 'reactstrap';
import { AppUpdateBanner } from '../../src/common/AppUpdateBanner'; import { AppUpdateBanner } from '../../src/common/AppUpdateBanner';
import { SimpleCard } from '../../src/utils/SimpleCard';
describe('<AppUpdateBanner />', () => { describe('<AppUpdateBanner />', () => {
const toggle = jest.fn(); const toggle = jest.fn();
const forceUpdate = jest.fn(); const forceUpdate = jest.fn();
let wrapper: ShallowWrapper;
beforeEach(() => { beforeEach(() => render(<AppUpdateBanner isOpen toggle={toggle} forceUpdate={forceUpdate} />));
wrapper = shallow(<AppUpdateBanner isOpen toggle={toggle} forceUpdate={forceUpdate} />);
});
afterEach(jest.clearAllMocks); afterEach(jest.clearAllMocks);
afterEach(() => wrapper?.unmount());
it('renders an alert with expected props', () => { it('renders initial state', () => {
expect(wrapper.prop('className')).toEqual('app-update-banner'); expect(screen.getByRole('heading')).toHaveTextContent('This app has just been updated!');
expect(wrapper.prop('isOpen')).toEqual(true); expect(screen.queryByText('Restarting...')).not.toBeInTheDocument();
expect(wrapper.prop('toggle')).toEqual(toggle); expect(screen.getByText('Restart now')).not.toHaveAttribute('disabled');
expect(wrapper.prop('tag')).toEqual(SimpleCard);
expect(wrapper.prop('color')).toEqual('secondary');
}); });
it('invokes toggle when alert is toggled', () => { it('invokes toggle when alert is closed', () => {
(wrapper.prop('toggle') as Function)(); expect(toggle).not.toHaveBeenCalled();
fireEvent.click(screen.getByLabelText('Close'));
expect(toggle).toHaveBeenCalled(); expect(toggle).toHaveBeenCalled();
}); });
it('triggers the update when clicking the button', () => { it('triggers the update when clicking the button', async () => {
expect(wrapper.find(Button).html()).toContain('Restart now');
expect(wrapper.find(Button).prop('disabled')).toEqual(false);
expect(forceUpdate).not.toHaveBeenCalled(); expect(forceUpdate).not.toHaveBeenCalled();
fireEvent.click(screen.getByText(/^Restart now/));
wrapper.find(Button).simulate('click');
expect(wrapper.find(Button).html()).toContain('Restarting...');
expect(wrapper.find(Button).prop('disabled')).toEqual(true);
expect(forceUpdate).toHaveBeenCalled(); 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 { Mock } from 'ts-mockery';
import { MemoryRouter } from 'react-router-dom';
import asideMenuCreator from '../../src/common/AsideMenu'; import asideMenuCreator from '../../src/common/AsideMenu';
import { ReachableServer } from '../../src/servers/data'; import { ReachableServer } from '../../src/servers/data';
import { SemVer } from '../../src/utils/helpers/version';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn().mockReturnValue({ pathname: '' }),
}));
describe('<AsideMenu />', () => { describe('<AsideMenu />', () => {
let wrapped: ShallowWrapper; const AsideMenu = asideMenuCreator(() => <>DeleteServerButton</>);
const DeleteServerButton = () => null; const setUp = (version: SemVer, id: string | false = 'abc123') => render(
<MemoryRouter>
<AsideMenu selectedServer={Mock.of<ReachableServer>({ id: id || undefined, version })} />
</MemoryRouter>,
);
beforeEach(() => { it.each([
const AsideMenu = asideMenuCreator(DeleteServerButton); ['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' })} />); const links = screen.getAllByRole('link');
});
afterEach(() => wrapped.unmount());
it('contains links to different sections', () => { expect.assertions(links.length + 1);
const links = wrapped.find('[to]'); expect(links).toHaveLength(expectedAmountOfLinks);
links.forEach((link) => expect(link.getAttribute('href')).toContain('abc123'));
expect(links).toHaveLength(5);
links.forEach((link) => expect(link.prop('to')).toContain('abc123'));
}); });
it('contains a button to delete server', () => { it.each([
expect(wrapped.find(DeleteServerButton)).toHaveLength(1); ['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 { fireEvent, render, screen } from '@testing-library/react';
import { Button } from 'reactstrap';
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import createErrorHandler from '../../src/common/ErrorHandler'; import { ErrorHandler as createErrorHandler } from '../../src/common/ErrorHandler';
import { SimpleCard } from '../../src/utils/SimpleCard';
const ComponentWithError = () => {
throw new Error('Error!!');
};
describe('<ErrorHandler />', () => { describe('<ErrorHandler />', () => {
const reload = jest.fn();
const window = Mock.of<Window>({ const window = Mock.of<Window>({
location: { location: { reload },
reload: jest.fn(),
},
}); });
const console = Mock.of<Console>({ error: jest.fn() }); const cons = Mock.of<Console>({ error: jest.fn() });
let wrapper: ShallowWrapper; const ErrorHandler = createErrorHandler(window, cons);
beforeEach(() => { beforeEach(() => {
const ErrorHandler = createErrorHandler(window, console); jest.spyOn(console, 'error').mockImplementation(() => {}); // Silence react errors
wrapper = shallow(<ErrorHandler children={<span>Foo</span>} />);
}); });
afterEach(jest.resetAllMocks);
afterEach(() => wrapper.unmount());
it('renders children when no error has occurred', () => { it('renders children when no error has occurred', () => {
expect(wrapper.text()).toEqual('Foo'); render(<ErrorHandler children={<span>Foo</span>} />);
expect(wrapper.find(Button)).toHaveLength(0);
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', () => { 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(screen.getByText('Oops! This is awkward :S')).toBeInTheDocument();
expect(wrapper.find(SimpleCard).contains( expect(screen.getByRole('button')).toBeInTheDocument();
'It seems that something went wrong. Try refreshing the page or just click this button.', });
)).toEqual(true);
expect(wrapper.find(Button)).toHaveLength(1); 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 { Mock } from 'ts-mockery';
import Home, { HomeProps } from '../../src/common/Home'; import { MemoryRouter } from 'react-router-dom';
import { ServerWithId } from '../../src/servers/data'; import { Home } from '../../src/common/Home';
import { ShlinkLogo } from '../../src/common/img/ShlinkLogo'; import { ServersMap, ServerWithId } from '../../src/servers/data';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: jest.fn().mockReturnValue(jest.fn()),
}));
describe('<Home />', () => { describe('<Home />', () => {
let wrapped: ShallowWrapper; const setUp = (servers: ServersMap = {}) => render(
const createComponent = (props: Partial<HomeProps> = {}) => { <MemoryRouter>
const actualProps = { resetSelectedServer: jest.fn(), servers: {}, ...props }; <Home servers={servers} />
</MemoryRouter>,
);
wrapped = shallow(<Home {...actualProps} />); it('renders title', () => {
setUp();
return wrapped; expect(screen.getByRole('heading', { name: 'Welcome!' })).toBeInTheDocument();
};
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.each([ it.each([
@ -33,14 +21,20 @@ describe('<Home />', () => {
{ {
'1a': Mock.of<ServerWithId>({ name: 'foo', id: '1' }), '1a': Mock.of<ServerWithId>({ name: 'foo', id: '1' }),
'2b': Mock.of<ServerWithId>({ name: 'bar', id: '2' }), '2b': Mock.of<ServerWithId>({ name: 'bar', id: '2' }),
'3c': Mock.of<ServerWithId>({ name: 'baz', id: '3' }),
}, },
0, 3,
], ],
[{}, 3], [{}, 2],
])('shows link to create or set-up server only when no servers exist', (servers, expectedParagraphs) => { ])('shows link to create or set-up server only when no servers exist', (servers, expectedServers) => {
const wrapped = createComponent({ servers }); setUp(servers);
const p = wrapped.find('p'); 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 { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { useLocation } from 'react-router-dom'; import { Router } from 'react-router-dom';
import { Collapse, NavbarToggler, NavLink } from 'reactstrap'; import { createMemoryHistory } from 'history';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { MainHeader as createMainHeader } from '../../src/common/MainHeader';
import createMainHeader from '../../src/common/MainHeader';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn().mockReturnValue({}),
}));
describe('<MainHeader />', () => { describe('<MainHeader />', () => {
const ServersDropdown = () => null; const MainHeader = createMainHeader(() => <>ServersDropdown</>);
const MainHeader = createMainHeader(ServersDropdown); const setUp = (pathname = '') => {
let wrapper: ShallowWrapper; const history = createMemoryHistory();
history.push(pathname);
const createWrapper = (pathname = '') => { return render(
(useLocation as any).mockReturnValue({ pathname }); <Router location={history.location} navigator={history}>
<MainHeader />
wrapper = shallow(<MainHeader />); </Router>,
);
return wrapper;
}; };
afterEach(jest.clearAllMocks);
afterEach(() => wrapper?.unmount());
it('renders ServersDropdown', () => { it('renders ServersDropdown', () => {
const wrapper = createWrapper(); setUp();
expect(screen.getByText('ServersDropdown')).toBeInTheDocument();
expect(wrapper.find(ServersDropdown)).toHaveLength(1);
}); });
it.each([ it.each([
@ -38,31 +28,38 @@ describe('<MainHeader />', () => {
['/settings/foo', true], ['/settings/foo', true],
['/settings/bar', true], ['/settings/bar', true],
])('sets link to settings as active only when current path is settings', (currentPath, isActive) => { ])('sets link to settings as active only when current path is settings', (currentPath, isActive) => {
const wrapper = createWrapper(currentPath); setUp(currentPath);
const settingsLink = wrapper.find(NavLink);
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', () => { 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'); const toggle = screen.getByLabelText('Toggle navigation');
wrapper.find(NavbarToggler).simulate('click'); const icon = toggle.firstChild;
expect(wrapper.find(NavbarToggler).find(FontAwesomeIcon).prop('className')).toEqual(
'main-header__toggle-icon main-header__toggle-icon--opened', 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'); fireEvent.click(toggle);
expect(wrapper.find(NavbarToggler).find(FontAwesomeIcon).prop('className')).toEqual('main-header__toggle-icon'); expect(icon).toHaveAttribute('class', expect.stringMatching(/main-header__toggle-icon$/));
}); });
it('opens Collapse when clicking toggle', () => { it('opens Collapse when clicking toggle', async () => {
const wrapper = createWrapper(); const { container } = setUp();
const collapse = container.querySelector('.collapse');
const toggle = screen.getByLabelText('Toggle navigation');
expect(wrapper.find(Collapse).prop('isOpen')).toEqual(false); expect(collapse).not.toHaveAttribute('class', expect.stringContaining('show'));
wrapper.find(NavbarToggler).simulate('click'); fireEvent.click(toggle);
expect(wrapper.find(Collapse).prop('isOpen')).toEqual(true); await waitFor(() => expect(collapse).toHaveAttribute('class', expect.stringContaining('show')));
wrapper.find(NavbarToggler).simulate('click');
expect(wrapper.find(Collapse).prop('isOpen')).toEqual(false);
}); });
}); });

View file

@ -1,70 +1,89 @@
import { shallow, ShallowWrapper } from 'enzyme'; import { render, screen } from '@testing-library/react';
import { Route, useParams } from 'react-router-dom'; import { Router, useParams } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { Mock } from 'ts-mockery'; 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 { NonReachableServer, NotFoundServer, ReachableServer, SelectedServer } from '../../src/servers/data';
import { NoMenuLayout } from '../../src/common/NoMenuLayout';
import { SemVer } from '../../src/utils/helpers/version'; import { SemVer } from '../../src/utils/helpers/version';
jest.mock('react-router-dom', () => ({ jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useParams: jest.fn() }));
...jest.requireActual('react-router-dom'),
useParams: jest.fn().mockReturnValue({}),
useLocation: jest.fn().mockReturnValue({}),
}));
describe('<MenuLayout />', () => { describe('<MenuLayout />', () => {
const ServerError = jest.fn(); const MenuLayout = createMenuLayout(
const C = jest.fn(); () => <>TagsList</>,
const MenuLayout = createMenuLayout(C, C, C, C, C, C, C, C, C, ServerError, C, C, C); () => <>ShortUrlsList</>,
let wrapper: ShallowWrapper; () => <>AsideMenu</>,
const createWrapper = (selectedServer: SelectedServer) => { () => <>CreateShortUrl</>,
(useParams as any).mockReturnValue({ serverId: 'abc123' }); () => <>ShortUrlVisits</>,
() => <>TagVisits</>,
() => <>DomainVisits</>,
() => <>OrphanVisits</>,
() => <>NonOrphanVisits</>,
() => <>ServerError</>,
() => <>Overview</>,
() => <>EditShortUrl</>,
() => <>ManageDomains</>,
);
const setUp = (selectedServer: SelectedServer, currentPath = '/') => {
const history = createMemoryHistory();
history.push(currentPath);
wrapper = shallow( return render(
<MenuLayout <Router location={history.location} navigator={history}>
sidebarNotPresent={jest.fn()} <MenuLayout
sidebarPresent={jest.fn()} sidebarNotPresent={jest.fn()}
selectServer={jest.fn()} sidebarPresent={jest.fn()}
selectedServer={selectedServer} selectServer={jest.fn()}
/>, selectedServer={selectedServer}
/>
</Router>,
); );
return wrapper;
}; };
beforeEach(() => {
(useParams as any).mockReturnValue({ serverId: 'abc123' });
});
afterEach(jest.clearAllMocks); afterEach(jest.clearAllMocks);
afterEach(() => wrapper?.unmount());
it.each([ it('shows loading indicator while loading server', () => {
[null, NoMenuLayout], setUp(null);
[Mock.of<NotFoundServer>({ serverNotFound: true }), ServerError],
])('returns error when server is not found', (selectedServer, ExpectedComp) => {
const wrapper = createWrapper(selectedServer);
const comp = wrapper.find(ExpectedComp);
expect(comp).toHaveLength(1); expect(screen.getByText('Loading...')).toBeInTheDocument();
}); expect(screen.queryByText('ServerError')).not.toBeInTheDocument();
it('returns error if server is not reachable', () => {
const wrapper = createWrapper(Mock.of<NonReachableServer>()).dive();
const serverError = wrapper.find(ServerError);
expect(serverError).toHaveLength(1);
}); });
it.each([ it.each([
['2.6.0' as SemVer, 10], [Mock.of<NotFoundServer>({ serverNotFound: true })],
['2.7.0' as SemVer, 10], [Mock.of<NonReachableServer>({ serverNotReachable: true })],
['2.8.0' as SemVer, 11], ])('shows error for non reachable servers', (selectedServer) => {
['2.10.0' as SemVer, 11], setUp(selectedServer);
['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);
expect(routes).toHaveLength(expectedAmountOfRoutes); expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
expect(routes.findWhere((element) => element.prop('index'))).toHaveLength(1); 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 { shallow, ShallowWrapper } from 'enzyme';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import NotFound from '../../src/common/NotFound'; import { NotFound } from '../../src/common/NotFound';
import { SimpleCard } from '../../src/utils/SimpleCard'; import { SimpleCard } from '../../src/utils/SimpleCard';
describe('<NotFound />', () => { describe('<NotFound />', () => {

View file

@ -9,12 +9,6 @@ import { Settings } from '../../src/settings/reducers/settings';
import { ReportExporter } from '../../src/common/services/ReportExporter'; import { ReportExporter } from '../../src/common/services/ReportExporter';
import { SelectedServer } from '../../src/servers/data'; 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 />', () => { describe('<NonOrphanVisits />', () => {
const exportVisits = jest.fn(); const exportVisits = jest.fn();
const getNonOrphanVisits = jest.fn(); const getNonOrphanVisits = jest.fn();