From fcbb9cda129bb5243b63839b04178248a67c275d Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 6 May 2022 19:46:47 +0200 Subject: [PATCH 01/12] Migrated HorizontalBarChart test to react testing library + snapshots for the events on the canvas --- jest.config.js | 6 +- package.json | 2 +- .../visits/charts/HorizontalBarChart.test.tsx | 62 +-- .../HorizontalBarChart.test.tsx.snap | 521 ++++++++++++++++++ 4 files changed, 537 insertions(+), 54 deletions(-) create mode 100644 test/visits/charts/__snapshots__/HorizontalBarChart.test.tsx.snap diff --git a/jest.config.js b/jest.config.js index 70853185..7a2f7fe3 100644 --- a/jest.config.js +++ b/jest.config.js @@ -9,10 +9,10 @@ module.exports = { ], coverageThreshold: { global: { - statements: 85, + statements: 90, branches: 80, - functions: 80, - lines: 85, + functions: 85, + lines: 90, }, }, setupFiles: ['/config/jest/setupBeforeEnzyme.js', '/config/jest/setupEnzyme.js'], diff --git a/package.json b/package.json index 8a272cfa..5c259e5b 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "build:serve": "serve -p 5000 ./build", "test": "jest --env=jsdom --colors --verbose", "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 --ci", "test:pretty": "npm run test:coverage -- --coverageReporters=html", "mutate": "./node_modules/.bin/stryker run --concurrency 4 --ignoreStatic" }, diff --git a/test/visits/charts/HorizontalBarChart.test.tsx b/test/visits/charts/HorizontalBarChart.test.tsx index b37ffb22..44b2bfde 100644 --- a/test/visits/charts/HorizontalBarChart.test.tsx +++ b/test/visits/charts/HorizontalBarChart.test.tsx @@ -1,58 +1,20 @@ -import { shallow, ShallowWrapper } from 'enzyme'; -import { Bar } from 'react-chartjs-2'; -import { prettify } from '../../../src/utils/helpers/numbers'; -import { MAIN_COLOR, MAIN_COLOR_ALPHA } from '../../../src/utils/theme'; -import { HorizontalBarChart } from '../../../src/visits/charts/HorizontalBarChart'; +import { render } from '@testing-library/react'; +import { HorizontalBarChart, HorizontalBarChartProps } from '../../../src/visits/charts/HorizontalBarChart'; describe('', () => { - let wrapper: ShallowWrapper; - const stats = { - foo: 123, - bar: 456, + const setUp = (props: HorizontalBarChartProps) => { + const { container } = render(); + return container.querySelector('canvas')?.getContext('2d')?.__getEvents(); // eslint-disable-line no-underscore-dangle }; - afterEach(() => wrapper?.unmount()); - - it('renders Bar with expected properties', () => { - wrapper = shallow(); - const horizontal = wrapper.find(Bar); - - expect(horizontal).toHaveLength(1); - - const { datasets: [{ backgroundColor, borderColor }] } = horizontal.prop('data') as any; - const { plugins, scales } = (horizontal.prop('options') ?? {}) as any; - - expect(backgroundColor).toEqual(MAIN_COLOR_ALPHA); - expect(borderColor).toEqual(MAIN_COLOR); - expect(plugins.legend).toEqual({ display: false }); - expect(scales).toEqual({ - x: { - beginAtZero: true, - stacked: true, - ticks: { - precision: 0, - callback: prettify, - }, - }, - y: { stacked: true }, - }); - }); - it.each([ - [{ foo: 23 }, [100, 456], [23, 0]], - [{ foo: 50 }, [73, 456], [50, 0]], - [{ bar: 45 }, [123, 411], [0, 45]], - [{ bar: 20, foo: 13 }, [110, 436], [13, 20]], - [undefined, [123, 456], undefined], - ])('splits highlighted data from regular data', (highlightedStats, expectedData, expectedHighlightedData) => { - wrapper = shallow(); - const horizontal = wrapper.find(Bar); + [{ foo: 123, bar: 456 }, undefined], + [{ one: 999, two: 131313 }, { one: 30, two: 100 }], + [{ one: 999, two: 131313, max: 3 }, { one: 30, two: 100 }], + ])('renders chart with expected canvas', (stats, highlightedStats) => { + const events = setUp({ stats, highlightedStats }); - const { datasets: [{ data, label }, highlightedData] } = horizontal.prop('data') as any; - - expect(label).toEqual('Visits'); - expect(data).toEqual(expectedData); - expectedHighlightedData && expect(highlightedData.data).toEqual(expectedHighlightedData); - !expectedHighlightedData && expect(highlightedData).toBeUndefined(); + expect(events).toBeTruthy(); + expect(events).toMatchSnapshot(); }); }); diff --git a/test/visits/charts/__snapshots__/HorizontalBarChart.test.tsx.snap b/test/visits/charts/__snapshots__/HorizontalBarChart.test.tsx.snap new file mode 100644 index 00000000..62b5e81f --- /dev/null +++ b/test/visits/charts/__snapshots__/HorizontalBarChart.test.tsx.snap @@ -0,0 +1,521 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders chart with expected canvas 1`] = ` +Array [ + Object { + "props": Object { + "a": 1, + "b": 0, + "c": 0, + "d": 1, + "e": 0, + "f": 0, + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "setTransform", + }, + Object { + "props": Object { + "value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "font", + }, + Object { + "props": Object { + "text": "foo", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "measureText", + }, + Object { + "props": Object { + "value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "font", + }, + Object { + "props": Object { + "text": "bar", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "measureText", + }, + Object { + "props": Object { + "value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "font", + }, + Object { + "props": Object { + "value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "font", + }, + Object { + "props": Object { + "text": "0", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "measureText", + }, + Object { + "props": Object { + "value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "font", + }, + Object { + "props": Object { + "text": "500", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "measureText", + }, + Object { + "props": Object { + "value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "font", + }, +] +`; + +exports[` renders chart with expected canvas 2`] = ` +Array [ + Object { + "props": Object { + "a": 1, + "b": 0, + "c": 0, + "d": 1, + "e": 0, + "f": 0, + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "setTransform", + }, + Object { + "props": Object { + "value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "font", + }, + Object { + "props": Object { + "text": "one", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "measureText", + }, + Object { + "props": Object { + "value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "font", + }, + Object { + "props": Object { + "text": "two", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "measureText", + }, + Object { + "props": Object { + "value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "font", + }, + Object { + "props": Object { + "value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "font", + }, + Object { + "props": Object { + "text": "0", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "measureText", + }, + Object { + "props": Object { + "value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "font", + }, + Object { + "props": Object { + "text": "200,000", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "measureText", + }, + Object { + "props": Object { + "value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "font", + }, +] +`; + +exports[` renders chart with expected canvas 3`] = ` +Array [ + Object { + "props": Object { + "a": 1, + "b": 0, + "c": 0, + "d": 1, + "e": 0, + "f": 0, + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "setTransform", + }, + Object { + "props": Object { + "value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "font", + }, + Object { + "props": Object { + "text": "one", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "measureText", + }, + Object { + "props": Object { + "value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "font", + }, + Object { + "props": Object { + "text": "two", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "measureText", + }, + Object { + "props": Object { + "value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "font", + }, + Object { + "props": Object { + "text": "max", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "measureText", + }, + Object { + "props": Object { + "value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "font", + }, + Object { + "props": Object { + "value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "font", + }, + Object { + "props": Object { + "text": "0", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "measureText", + }, + Object { + "props": Object { + "value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "font", + }, + Object { + "props": Object { + "text": "200,000", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "measureText", + }, + Object { + "props": Object { + "value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "font", + }, +] +`; From 37adcb52cf3cd0769e4702bf31f93f710b8404a6 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 6 May 2022 19:55:25 +0200 Subject: [PATCH 02/12] Migrated NotFound test to react testing library --- test/common/NotFound.test.tsx | 51 +++++++++++++---------------------- 1 file changed, 18 insertions(+), 33 deletions(-) diff --git a/test/common/NotFound.test.tsx b/test/common/NotFound.test.tsx index 95cd4272..f923a375 100644 --- a/test/common/NotFound.test.tsx +++ b/test/common/NotFound.test.tsx @@ -1,47 +1,32 @@ -import { shallow, ShallowWrapper } from 'enzyme'; -import { Link } from 'react-router-dom'; +import { render, screen } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; import { NotFound } from '../../src/common/NotFound'; -import { SimpleCard } from '../../src/utils/SimpleCard'; describe('', () => { - let wrapper: ShallowWrapper; - const createWrapper = (props = {}) => { - wrapper = shallow().find(SimpleCard); - - return wrapper; - }; - - afterEach(() => wrapper?.unmount()); + const setUp = (props = {}) => render(); it('shows expected error title', () => { - const wrapper = createWrapper(); - - expect(wrapper.contains('Oops! We could not find requested route.')).toEqual(true); + setUp(); + expect(screen.getByText('Oops! We could not find requested route.')).toBeInTheDocument(); }); it('shows expected error message', () => { - const wrapper = createWrapper(); - - expect(wrapper.contains( + setUp(); + expect(screen.getByText( 'Use your browser\'s back button to navigate to the page you have previously come from, or just press this button.', - )).toEqual(true); + )).toBeInTheDocument(); }); - it('shows a link to the home', () => { - const wrapper = createWrapper(); - const link = wrapper.find(Link); + it.each([ + [{}, '/', 'Home'], + [{ to: '/foo/bar', children: 'Hello' }, '/foo/bar', 'Hello'], + [{ to: '/baz-bar', children: <>Foo }, '/baz-bar', 'Foo'], + ])('shows expected link and text', (props, expectedLink, expectedText) => { + setUp(props); + const link = screen.getByRole('link'); - expect(link.prop('to')).toEqual('/'); - expect(link.prop('className')).toEqual('btn btn-outline-primary btn-lg'); - expect(link.prop('children')).toEqual('Home'); - }); - - it('shows a link with provided props', () => { - const wrapper = createWrapper({ to: '/foo/bar', children: 'Hello' }); - const link = wrapper.find(Link); - - expect(link.prop('to')).toEqual('/foo/bar'); - expect(link.prop('className')).toEqual('btn btn-outline-primary btn-lg'); - expect(link.prop('children')).toEqual('Hello'); + expect(link).toHaveAttribute('href', expectedLink); + expect(link).toHaveTextContent(expectedText); + expect(link).toHaveAttribute('class', 'btn btn-outline-primary btn-lg'); }); }); From d327142d00a62df1a24067c43ac48febdfb2e613 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 6 May 2022 20:13:51 +0200 Subject: [PATCH 03/12] Migrated ScrollToTop test to react testing library --- config/jest/setupTests.ts | 1 + src/common/ScrollToTop.tsx | 4 +--- src/common/services/provideServices.ts | 4 ++-- test/common/ScrollToTop.test.tsx | 27 ++++++++++---------------- 4 files changed, 14 insertions(+), 22 deletions(-) diff --git a/config/jest/setupTests.ts b/config/jest/setupTests.ts index b75cbb7d..cb16c9d4 100644 --- a/config/jest/setupTests.ts +++ b/config/jest/setupTests.ts @@ -3,3 +3,4 @@ import 'jest-canvas-mock'; import ResizeObserver from 'resize-observer-polyfill'; (global as any).ResizeObserver = ResizeObserver; +(global as any).scrollTo = () => {}; diff --git a/src/common/ScrollToTop.tsx b/src/common/ScrollToTop.tsx index 85fe9766..0fbdda39 100644 --- a/src/common/ScrollToTop.tsx +++ b/src/common/ScrollToTop.tsx @@ -1,7 +1,7 @@ import { FC, PropsWithChildren, useEffect } from 'react'; import { useLocation } from 'react-router-dom'; -const ScrollToTop = (): FC> => ({ children }) => { +export const ScrollToTop: FC> = ({ children }) => { const location = useLocation(); useEffect(() => { @@ -10,5 +10,3 @@ const ScrollToTop = (): FC> => ({ children }) => { return <>{children}; }; - -export default ScrollToTop; diff --git a/src/common/services/provideServices.ts b/src/common/services/provideServices.ts index dd955dc8..604ab336 100644 --- a/src/common/services/provideServices.ts +++ b/src/common/services/provideServices.ts @@ -1,6 +1,6 @@ import axios from 'axios'; import Bottle from 'bottlejs'; -import ScrollToTop from '../ScrollToTop'; +import { ScrollToTop } from '../ScrollToTop'; import { MainHeader } from '../MainHeader'; import { Home } from '../Home'; import { MenuLayout } from '../MenuLayout'; @@ -23,7 +23,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.service('ReportExporter', ReportExporter, 'window', 'jsonToCsv'); // Components - bottle.serviceFactory('ScrollToTop', ScrollToTop); + bottle.serviceFactory('ScrollToTop', () => ScrollToTop); bottle.serviceFactory('MainHeader', MainHeader, 'ServersDropdown'); diff --git a/test/common/ScrollToTop.test.tsx b/test/common/ScrollToTop.test.tsx index 2720f0d3..f95907fc 100644 --- a/test/common/ScrollToTop.test.tsx +++ b/test/common/ScrollToTop.test.tsx @@ -1,21 +1,14 @@ -import { shallow, ShallowWrapper } from 'enzyme'; -import createScrollToTop from '../../src/common/ScrollToTop'; - -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useLocation: jest.fn().mockReturnValue({}), -})); +import { render, screen } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import { ScrollToTop } from '../../src/common/ScrollToTop'; describe('', () => { - let wrapper: ShallowWrapper; - - beforeEach(() => { - const ScrollToTop = createScrollToTop(); - - wrapper = shallow(Foobar); + it.each([ + ['Foobar'], + ['Barfoo'], + ['Something'], + ])('just renders children', (children) => { + render({children}); + expect(screen.getByText(children)).toBeInTheDocument(); }); - - afterEach(() => wrapper.unmount()); - - it('just renders children', () => expect(wrapper.text()).toEqual('Foobar')); }); From 4ea826ed2ce4f1507f817e85cc5139df7ae0a1e6 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 6 May 2022 20:25:48 +0200 Subject: [PATCH 04/12] Migrated ShlinkVersions test to react testing library --- test/common/ShlinkVersions.test.tsx | 37 +++++++++++++---------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/test/common/ShlinkVersions.test.tsx b/test/common/ShlinkVersions.test.tsx index a81b22cd..04273b32 100644 --- a/test/common/ShlinkVersions.test.tsx +++ b/test/common/ShlinkVersions.test.tsx @@ -1,17 +1,10 @@ -import { shallow, ShallowWrapper } from 'enzyme'; +import { render, screen } from '@testing-library/react'; import { Mock } from 'ts-mockery'; import ShlinkVersions, { ShlinkVersionsProps } from '../../src/common/ShlinkVersions'; import { NonReachableServer, NotFoundServer, ReachableServer } from '../../src/servers/data'; describe('', () => { - let wrapper: ShallowWrapper; - const createWrapper = (props: ShlinkVersionsProps) => { - wrapper = shallow(); - - return wrapper; - }; - - afterEach(() => wrapper?.unmount()); + const setUp = (props: ShlinkVersionsProps) => render(); it.each([ ['1.2.3', Mock.of({ version: '1.0.0', printableVersion: 'foo' }), 'v1.2.3', 'foo'], @@ -22,15 +15,19 @@ describe('', () => { ])( 'displays expected versions when selected server is reachable', (clientVersion, selectedServer, expectedClientVersion, expectedServerVersion) => { - const wrapper = createWrapper({ clientVersion, selectedServer }); - const links = wrapper.find('VersionLink'); - const serverLink = links.at(0); - const clientLink = links.at(1); + setUp({ clientVersion, selectedServer }); + const [serverLink, clientLink] = screen.getAllByRole('link'); - expect(serverLink.prop('project')).toEqual('shlink'); - expect(serverLink.prop('version')).toEqual(expectedServerVersion); - expect(clientLink.prop('project')).toEqual('shlink-web-client'); - expect(clientLink.prop('version')).toEqual(expectedClientVersion); + expect(serverLink).toHaveAttribute( + 'href', + `https://github.com/shlinkio/shlink/releases/${expectedServerVersion}`, + ); + expect(serverLink).toHaveTextContent(expectedServerVersion); + expect(clientLink).toHaveAttribute( + 'href', + `https://github.com/shlinkio/shlink-web-client/releases/${expectedClientVersion}`, + ); + expect(clientLink).toHaveTextContent(expectedClientVersion); }, ); @@ -39,10 +36,10 @@ describe('', () => { ['1.2.3', Mock.of({ serverNotFound: true })], ['1.2.3', Mock.of({ serverNotReachable: true })], ])('displays only client version when selected server is not reachable', (clientVersion, selectedServer) => { - const wrapper = createWrapper({ clientVersion, selectedServer }); - const links = wrapper.find('VersionLink'); + setUp({ clientVersion, selectedServer }); + const links = screen.getAllByRole('link'); expect(links).toHaveLength(1); - expect(links.at(0).prop('project')).toEqual('shlink-web-client'); + expect(links[0]).toHaveAttribute('href', 'https://github.com/shlinkio/shlink-web-client/releases/v1.2.3'); }); }); From 00f154ef4e5ef35a2d22e7d30795a18db4b00bf9 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 6 May 2022 21:01:44 +0200 Subject: [PATCH 05/12] Migrated ShlinkVersionsContainer test to react testing library --- test/common/ShlinkVersionsContainer.test.tsx | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/test/common/ShlinkVersionsContainer.test.tsx b/test/common/ShlinkVersionsContainer.test.tsx index f4e67731..d264051d 100644 --- a/test/common/ShlinkVersionsContainer.test.tsx +++ b/test/common/ShlinkVersionsContainer.test.tsx @@ -1,26 +1,19 @@ -import { shallow, ShallowWrapper } from 'enzyme'; +import { render } from '@testing-library/react'; import { Mock } from 'ts-mockery'; import ShlinkVersionsContainer from '../../src/common/ShlinkVersionsContainer'; import { SelectedServer } from '../../src/servers/data'; import { Sidebar } from '../../src/common/reducers/sidebar'; describe('', () => { - let wrapper: ShallowWrapper; - - const createWrapper = (sidebar: Sidebar) => { - wrapper = shallow(()} sidebar={sidebar} />); - - return wrapper; - }; - - afterEach(() => wrapper?.unmount()); + const setUp = (sidebar: Sidebar) => render( + ()} sidebar={sidebar} />, + ); it.each([ [{ sidebarPresent: false }, 'text-center'], [{ sidebarPresent: true }, 'text-center shlink-versions-container--with-sidebar'], ])('renders proper col classes based on sidebar status', (sidebar, expectedClasses) => { - const wrapper = createWrapper(sidebar); - - expect(wrapper.find('div').prop('className')).toEqual(`${expectedClasses}`); + const { container } = setUp(sidebar); + expect(container.firstChild).toHaveAttribute('class', `${expectedClasses}`); }); }); From 3846ca293c8baf686f83db0a64cfea118f23559e Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 6 May 2022 21:20:14 +0200 Subject: [PATCH 06/12] Migrated SimplePaginator test to react testing library --- src/common/SimplePaginator.tsx | 8 ++--- src/tags/TagsTable.tsx | 2 +- src/visits/VisitsTable.tsx | 2 +- src/visits/charts/SortableBarChartCard.tsx | 2 +- test/common/SimplePaginator.test.tsx | 35 ++++++++++------------ test/tags/TagsTable.test.tsx | 2 +- test/visits/VisitsTable.test.tsx | 2 +- 7 files changed, 24 insertions(+), 29 deletions(-) diff --git a/src/common/SimplePaginator.tsx b/src/common/SimplePaginator.tsx index ed337acb..00e6e185 100644 --- a/src/common/SimplePaginator.tsx +++ b/src/common/SimplePaginator.tsx @@ -17,7 +17,7 @@ interface SimplePaginatorProps { centered?: boolean; } -const SimplePaginator: FC = ({ pagesCount, currentPage, setCurrentPage, centered = true }) => { +export const SimplePaginator: FC = ({ pagesCount, currentPage, setCurrentPage, centered = true }) => { if (pagesCount < 2) { return null; } @@ -35,7 +35,9 @@ const SimplePaginator: FC = ({ pagesCount, currentPage, se disabled={pageIsEllipsis(pageNumber)} active={currentPage === pageNumber} > - {prettifyPageNumber(pageNumber)} + + {prettifyPageNumber(pageNumber)} + ))} = pagesCount}> @@ -44,5 +46,3 @@ const SimplePaginator: FC = ({ pagesCount, currentPage, se ); }; - -export default SimplePaginator; diff --git a/src/tags/TagsTable.tsx b/src/tags/TagsTable.tsx index 8298140c..205196e8 100644 --- a/src/tags/TagsTable.tsx +++ b/src/tags/TagsTable.tsx @@ -2,7 +2,7 @@ import { FC, useEffect, useRef } from 'react'; import { splitEvery } from 'ramda'; import { useLocation } from 'react-router-dom'; import { SimpleCard } from '../utils/SimpleCard'; -import SimplePaginator from '../common/SimplePaginator'; +import { SimplePaginator } from '../common/SimplePaginator'; import { useQueryState } from '../utils/helpers/hooks'; import { parseQuery } from '../utils/helpers/query'; import { TableOrderIcon } from '../utils/table/TableOrderIcon'; diff --git a/src/visits/VisitsTable.tsx b/src/visits/VisitsTable.tsx index d5b02a0e..b6bfd5ca 100644 --- a/src/visits/VisitsTable.tsx +++ b/src/visits/VisitsTable.tsx @@ -4,7 +4,7 @@ import { min, splitEvery } from 'ramda'; import { faCheck as checkIcon, faRobot as botIcon } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { UncontrolledTooltip } from 'reactstrap'; -import SimplePaginator from '../common/SimplePaginator'; +import { SimplePaginator } from '../common/SimplePaginator'; import SearchField from '../utils/SearchField'; import { determineOrderDir, Order, sortList } from '../utils/helpers/ordering'; import { prettify } from '../utils/helpers/numbers'; diff --git a/src/visits/charts/SortableBarChartCard.tsx b/src/visits/charts/SortableBarChartCard.tsx index 0111585e..23790ec3 100644 --- a/src/visits/charts/SortableBarChartCard.tsx +++ b/src/visits/charts/SortableBarChartCard.tsx @@ -2,7 +2,7 @@ import { FC, useState } from 'react'; import { fromPairs, pipe, reverse, sortBy, splitEvery, toLower, toPairs, type, zipObj } from 'ramda'; import { rangeOf } from '../../utils/utils'; import { Order } from '../../utils/helpers/ordering'; -import SimplePaginator from '../../common/SimplePaginator'; +import { SimplePaginator } from '../../common/SimplePaginator'; import { roundTen } from '../../utils/helpers/numbers'; import { OrderingDropdown } from '../../utils/OrderingDropdown'; import PaginationDropdown from '../../utils/PaginationDropdown'; diff --git a/test/common/SimplePaginator.test.tsx b/test/common/SimplePaginator.test.tsx index 47ba7d29..128221d9 100644 --- a/test/common/SimplePaginator.test.tsx +++ b/test/common/SimplePaginator.test.tsx @@ -1,28 +1,23 @@ -import { shallow, ShallowWrapper } from 'enzyme'; -import { identity } from 'ramda'; -import { PaginationItem } from 'reactstrap'; -import SimplePaginator from '../../src/common/SimplePaginator'; +import { render, screen } from '@testing-library/react'; +import { SimplePaginator } from '../../src/common/SimplePaginator'; import { ELLIPSIS } from '../../src/utils/helpers/pagination'; describe('', () => { - let wrapper: ShallowWrapper; - const createWrapper = (pagesCount: number, currentPage = 1) => { - wrapper = shallow(); - - return wrapper; - }; - - afterEach(() => wrapper?.unmount()); + const setUp = (pagesCount: number, currentPage = 1) => render( + , + ); it.each([-3, -2, 0, 1])('renders empty when the amount of pages is smaller than 2', (pagesCount) => { - expect(createWrapper(pagesCount).text()).toEqual(''); + const { container } = setUp(pagesCount); + expect(container.firstChild).toEqual(null); }); describe('ELLIPSIS are rendered where expected', () => { const getItemsForPages = (pagesCount: number, currentPage: number) => { - const paginator = createWrapper(pagesCount, currentPage); - const items = paginator.find(PaginationItem); - const itemsWithEllipsis = items.filterWhere((item) => item?.key()?.includes(ELLIPSIS)); + setUp(pagesCount, currentPage); + + const items = screen.getAllByRole('link'); + const itemsWithEllipsis = items.filter((item) => item.innerHTML.includes(ELLIPSIS)); return { items, itemsWithEllipsis }; }; @@ -30,22 +25,22 @@ describe('', () => { it('renders first ELLIPSIS', () => { const { items, itemsWithEllipsis } = getItemsForPages(9, 7); - expect(items.at(2).html()).toContain(ELLIPSIS); + expect(items[1]).toHaveTextContent(ELLIPSIS); expect(itemsWithEllipsis).toHaveLength(1); }); it('renders last ELLIPSIS', () => { const { items, itemsWithEllipsis } = getItemsForPages(9, 2); - expect(items.at(items.length - 3).html()).toContain(ELLIPSIS); + expect(items[items.length - 2]).toHaveTextContent(ELLIPSIS); expect(itemsWithEllipsis).toHaveLength(1); }); it('renders both ELLIPSIS', () => { const { items, itemsWithEllipsis } = getItemsForPages(20, 9); - expect(items.at(2).html()).toContain(ELLIPSIS); - expect(items.at(items.length - 3).html()).toContain(ELLIPSIS); + expect(items[1]).toHaveTextContent(ELLIPSIS); + expect(items[items.length - 2]).toHaveTextContent(ELLIPSIS); expect(itemsWithEllipsis).toHaveLength(2); }); }); diff --git a/test/tags/TagsTable.test.tsx b/test/tags/TagsTable.test.tsx index 4b14bf78..130bf71f 100644 --- a/test/tags/TagsTable.test.tsx +++ b/test/tags/TagsTable.test.tsx @@ -4,7 +4,7 @@ import { useLocation } from 'react-router-dom'; import { TagsTable as createTagsTable } from '../../src/tags/TagsTable'; import { SelectedServer } from '../../src/servers/data'; import { rangeOf } from '../../src/utils/utils'; -import SimplePaginator from '../../src/common/SimplePaginator'; +import { SimplePaginator } from '../../src/common/SimplePaginator'; import { NormalizedTag } from '../../src/tags/data'; jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useLocation: jest.fn() })); diff --git a/test/visits/VisitsTable.test.tsx b/test/visits/VisitsTable.test.tsx index c8bea5e9..c4ddac6a 100644 --- a/test/visits/VisitsTable.test.tsx +++ b/test/visits/VisitsTable.test.tsx @@ -2,7 +2,7 @@ import { shallow, ShallowWrapper } from 'enzyme'; import { Mock } from 'ts-mockery'; import VisitsTable, { VisitsTableProps } from '../../src/visits/VisitsTable'; import { rangeOf } from '../../src/utils/utils'; -import SimplePaginator from '../../src/common/SimplePaginator'; +import { SimplePaginator } from '../../src/common/SimplePaginator'; import SearchField from '../../src/utils/SearchField'; import { NormalizedVisit } from '../../src/visits/types'; import { ReachableServer, SelectedServer } from '../../src/servers/data'; From 43302ef5a8cff3dd7ee5f30a752fd0ac6f69aaf0 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 6 May 2022 21:24:16 +0200 Subject: [PATCH 07/12] Migrated ShlinkLogo test to react testing library --- test/common/img/ShlinkLogo.test.tsx | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/test/common/img/ShlinkLogo.test.tsx b/test/common/img/ShlinkLogo.test.tsx index 09793371..d1c6cc61 100644 --- a/test/common/img/ShlinkLogo.test.tsx +++ b/test/common/img/ShlinkLogo.test.tsx @@ -1,25 +1,17 @@ -import { shallow, ShallowWrapper } from 'enzyme'; +import { render } from '@testing-library/react'; import { ShlinkLogo, ShlinkLogoProps } from '../../../src/common/img/ShlinkLogo'; import { MAIN_COLOR } from '../../../src/utils/theme'; describe('', () => { - let wrapper: ShallowWrapper; - const createWrapper = (props: ShlinkLogoProps) => { - wrapper = shallow(); - - return wrapper; - }; - - afterEach(() => wrapper?.unmount()); + const setUp = (props: ShlinkLogoProps) => render(); it.each([ [undefined, MAIN_COLOR], ['red', 'red'], ['white', 'white'], ])('renders expected color', (color, expectedColor) => { - const wrapper = createWrapper({ color }); - - expect(wrapper.find('g').prop('fill')).toEqual(expectedColor); + const { container } = setUp({ color }); + expect(container.querySelector('g')).toHaveAttribute('fill', expectedColor); }); it.each([ @@ -27,8 +19,12 @@ describe('', () => { ['foo', 'foo'], ['bar', 'bar'], ])('renders expected class', (className, expectedClassName) => { - const wrapper = createWrapper({ className }); + const { container } = setUp({ className }); - expect(wrapper.prop('className')).toEqual(expectedClassName); + if (expectedClassName) { + expect(container.firstChild).toHaveAttribute('class', expectedClassName); + } else { + expect(container.firstChild).not.toHaveAttribute('class'); + } }); }); From 0b16300a703c52314821f1d23f4b4e7e0551ba11 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 9 May 2022 18:38:14 +0200 Subject: [PATCH 08/12] Migrated DomainRow test to react testing library --- config/jest/setupTests.ts | 1 + test/domains/DomainRow.test.tsx | 95 +++++++++++++++++++++------------ 2 files changed, 63 insertions(+), 33 deletions(-) diff --git a/config/jest/setupTests.ts b/config/jest/setupTests.ts index cb16c9d4..5893b1ca 100644 --- a/config/jest/setupTests.ts +++ b/config/jest/setupTests.ts @@ -4,3 +4,4 @@ import ResizeObserver from 'resize-observer-polyfill'; (global as any).ResizeObserver = ResizeObserver; (global as any).scrollTo = () => {}; +(global as any).matchMedia = (media: string) => ({ matches: false, media }); diff --git a/test/domains/DomainRow.test.tsx b/test/domains/DomainRow.test.tsx index 387c4164..15c176b7 100644 --- a/test/domains/DomainRow.test.tsx +++ b/test/domains/DomainRow.test.tsx @@ -1,4 +1,4 @@ -import { shallow, ShallowWrapper } from 'enzyme'; +import { render, screen } from '@testing-library/react'; import { Mock } from 'ts-mockery'; import { ShlinkDomainRedirects } from '../../src/api/types'; import { DomainRow } from '../../src/domains/DomainRow'; @@ -6,42 +6,71 @@ import { SelectedServer } from '../../src/servers/data'; import { Domain } from '../../src/domains/data'; describe('', () => { - let wrapper: ShallowWrapper; - const createWrapper = (domain: Domain, selectedServer = Mock.all()) => { - wrapper = shallow( - , - ); - - return wrapper; - }; - - afterEach(() => wrapper?.unmount()); - - it.each([ - [undefined, 3], - [Mock.of(), 3], - [Mock.of({ baseUrlRedirect: 'foo' }), 2], - [Mock.of({ invalidShortUrlRedirect: 'foo' }), 2], - [Mock.of({ baseUrlRedirect: 'foo', regular404Redirect: 'foo' }), 1], + const redirectsCombinations = [ + [Mock.of({ baseUrlRedirect: 'foo' })], + [Mock.of({ invalidShortUrlRedirect: 'bar' })], + [Mock.of({ baseUrlRedirect: 'baz', regular404Redirect: 'foo' })], [ Mock.of( - { baseUrlRedirect: 'foo', regular404Redirect: 'foo', invalidShortUrlRedirect: 'foo' }, + { baseUrlRedirect: 'baz', regular404Redirect: 'bar', invalidShortUrlRedirect: 'foo' }, ), - 0, ], - ])('shows expected redirects', (redirects, expectedNoRedirects) => { - const wrapper = createWrapper(Mock.of({ domain: '', isDefault: true, redirects })); - const noRedirects = wrapper.find('Nr'); - const cells = wrapper.find('td'); + ]; + const setUp = (domain: Domain, defaultRedirects?: ShlinkDomainRedirects) => render( + ()} + editDomainRedirects={jest.fn()} + checkDomainHealth={jest.fn()} + />, + ); - expect(noRedirects).toHaveLength(expectedNoRedirects); - redirects?.baseUrlRedirect && expect(cells.at(1).html()).toContain(redirects.baseUrlRedirect); - redirects?.regular404Redirect && expect(cells.at(2).html()).toContain(redirects.regular404Redirect); - redirects?.invalidShortUrlRedirect && expect(cells.at(3).html()).toContain(redirects.invalidShortUrlRedirect); + it.each(redirectsCombinations)('shows expected redirects', (redirects) => { + setUp(Mock.of({ domain: '', isDefault: true, redirects })); + const cells = screen.getAllByRole('cell'); + + redirects?.baseUrlRedirect && expect(cells[1]).toHaveTextContent(redirects.baseUrlRedirect); + redirects?.regular404Redirect && expect(cells[2]).toHaveTextContent(redirects.regular404Redirect); + redirects?.invalidShortUrlRedirect && expect(cells[3]).toHaveTextContent(redirects.invalidShortUrlRedirect); + expect(screen.queryByText('(as fallback)')).not.toBeInTheDocument(); + }); + + it.each([ + [undefined], + [Mock.of()], + ])('shows expected "no redirects"', (redirects) => { + setUp(Mock.of({ domain: '', isDefault: true, redirects })); + const cells = screen.getAllByRole('cell'); + + expect(cells[1]).toHaveTextContent('No redirect'); + expect(cells[2]).toHaveTextContent('No redirect'); + expect(cells[3]).toHaveTextContent('No redirect'); + expect(screen.queryByText('(as fallback)')).not.toBeInTheDocument(); + }); + + it.each(redirectsCombinations)('shows expected fallback redirects', (fallbackRedirects) => { + setUp(Mock.of({ domain: '', isDefault: true }), fallbackRedirects); + const cells = screen.getAllByRole('cell'); + + fallbackRedirects?.baseUrlRedirect && expect(cells[1]).toHaveTextContent( + `${fallbackRedirects.baseUrlRedirect} (as fallback)`, + ); + fallbackRedirects?.regular404Redirect && expect(cells[2]).toHaveTextContent( + `${fallbackRedirects.regular404Redirect} (as fallback)`, + ); + fallbackRedirects?.invalidShortUrlRedirect && expect(cells[3]).toHaveTextContent( + `${fallbackRedirects.invalidShortUrlRedirect} (as fallback)`, + ); + }); + + it.each([[true], [false]])('shows icon on default domain only', (isDefault) => { + const { container } = setUp(Mock.of({ domain: '', isDefault })); + + if (isDefault) { + expect(container.querySelector('#defaultDomainIcon')).toBeInTheDocument(); + } else { + expect(container.querySelector('#defaultDomainIcon')).not.toBeInTheDocument(); + } }); }); From fb0adf74f33423f4f299ec845f14a201f54bc65f Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 9 May 2022 19:03:19 +0200 Subject: [PATCH 09/12] Migrated DomainsSelector test to react testing library --- src/domains/DomainSelector.tsx | 1 + test/domains/DomainSelector.test.tsx | 71 +++++++++++++++------------- 2 files changed, 39 insertions(+), 33 deletions(-) diff --git a/src/domains/DomainSelector.tsx b/src/domains/DomainSelector.tsx index 06a00ba7..0c8671b4 100644 --- a/src/domains/DomainSelector.tsx +++ b/src/domains/DomainSelector.tsx @@ -40,6 +40,7 @@ export const DomainSelector = ({ listDomains, value, domainsList, onChange }: Do outline type="button" className="domains-dropdown__back-btn" + aria-label="Back to domains list" onClick={pipe(unselectDomain, hideInput)} > diff --git a/test/domains/DomainSelector.test.tsx b/test/domains/DomainSelector.test.tsx index e3de5195..3787f55f 100644 --- a/test/domains/DomainSelector.test.tsx +++ b/test/domains/DomainSelector.test.tsx @@ -1,13 +1,10 @@ -import { shallow, ShallowWrapper } from 'enzyme'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { Mock } from 'ts-mockery'; -import { DropdownItem, InputGroup } from 'reactstrap'; import { DomainSelector } from '../../src/domains/DomainSelector'; import { DomainsList } from '../../src/domains/reducers/domainsList'; import { ShlinkDomain } from '../../src/api/types'; -import { DropdownBtn } from '../../src/utils/DropdownBtn'; describe('', () => { - let wrapper: ShallowWrapper; const domainsList = Mock.of({ domains: [ Mock.of({ domain: 'default.com', isDefault: true }), @@ -15,51 +12,59 @@ describe('', () => { Mock.of({ domain: 'bar.com' }), ], }); - const createWrapper = (value = '') => { - wrapper = shallow( - , - ); - - return wrapper; - }; + const setUp = (value = '') => render( + , + ); afterEach(jest.clearAllMocks); - afterEach(() => wrapper.unmount()); it.each([ ['', 'Domain', 'domains-dropdown__toggle-btn'], ['my-domain.com', 'Domain: my-domain.com', 'domains-dropdown__toggle-btn--active'], - ])('shows dropdown by default', (value, expectedText, expectedClassName) => { - const wrapper = createWrapper(value); - const input = wrapper.find(InputGroup); - const dropdown = wrapper.find(DropdownBtn); + ])('shows dropdown by default', async (value, expectedText, expectedClassName) => { + setUp(value); - expect(input).toHaveLength(0); - expect(dropdown).toHaveLength(1); - expect(dropdown.find(DropdownItem)).toHaveLength(5); - expect(dropdown.prop('text')).toEqual(expectedText); - expect(dropdown.prop('className')).toEqual(expectedClassName); + const btn = screen.getByRole('button', { name: expectedText }); + + expect(screen.queryByPlaceholderText('Domain')).not.toBeInTheDocument(); + expect(btn).toHaveAttribute( + 'class', + `dropdown-btn__toggle btn-block ${expectedClassName} dropdown-toggle btn btn-primary`, + ); + fireEvent.click(btn); + + await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument()); + expect(screen.getAllByRole('menuitem')).toHaveLength(4); }); - it('allows toggling between dropdown and input', () => { - const wrapper = createWrapper(); + it('allows toggling between dropdown and input', async () => { + setUp(); - wrapper.find(DropdownItem).last().simulate('click'); - expect(wrapper.find(InputGroup)).toHaveLength(1); - expect(wrapper.find(DropdownBtn)).toHaveLength(0); + expect(screen.queryByPlaceholderText('Domain')).not.toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Domain' })).toBeInTheDocument(); - wrapper.find('.domains-dropdown__back-btn').simulate('click'); - expect(wrapper.find(InputGroup)).toHaveLength(0); - expect(wrapper.find(DropdownBtn)).toHaveLength(1); + fireEvent.click(screen.getByRole('button', { name: 'Domain' })); + fireEvent.click(await screen.findByText('New domain')); + + expect(screen.getByPlaceholderText('Domain')).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Domain' })).not.toBeInTheDocument(); + + fireEvent.click(screen.getByRole('button', { name: 'Back to domains list' })); + + expect(screen.queryByPlaceholderText('Domain')).not.toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Domain' })).toBeInTheDocument(); }); it.each([ - [0, 'default.comdefault'], + [0, 'default.comdefault'], [1, 'foo.com'], [2, 'bar.com'], - ])('shows expected content on every item', (index, expectedContent) => { - const item = createWrapper().find(DropdownItem).at(index); + ])('shows expected content on every item', async (index, expectedContent) => { + setUp(); - expect(item.html()).toContain(expectedContent); + fireEvent.click(screen.getByRole('button', { name: 'Domain' })); + const items = await screen.findAllByRole('menuitem'); + + expect(items[index]).toHaveTextContent(expectedContent); }); }); From b1d51a41038c5157d24fc28c7f51f4f6985a1f47 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 9 May 2022 19:23:35 +0200 Subject: [PATCH 10/12] Migrated ManageDomains test to react testing library --- test/domains/ManageDomains.test.tsx | 110 +++++++++------------------- 1 file changed, 36 insertions(+), 74 deletions(-) diff --git a/test/domains/ManageDomains.test.tsx b/test/domains/ManageDomains.test.tsx index acd8edf3..697334e2 100644 --- a/test/domains/ManageDomains.test.tsx +++ b/test/domains/ManageDomains.test.tsx @@ -1,108 +1,70 @@ -import { shallow, ShallowWrapper } from 'enzyme'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { Mock } from 'ts-mockery'; import { DomainsList } from '../../src/domains/reducers/domainsList'; import { ManageDomains } from '../../src/domains/ManageDomains'; -import Message from '../../src/utils/Message'; -import { Result } from '../../src/utils/Result'; -import SearchField from '../../src/utils/SearchField'; import { ProblemDetailsError, ShlinkDomain } from '../../src/api/types'; -import { ShlinkApiError } from '../../src/api/ShlinkApiError'; -import { DomainRow } from '../../src/domains/DomainRow'; import { SelectedServer } from '../../src/servers/data'; describe('', () => { const listDomains = jest.fn(); const filterDomains = jest.fn(); - let wrapper: ShallowWrapper; - const createWrapper = (domainsList: DomainsList) => { - wrapper = shallow( - ()} - />, - ); - - return wrapper; - }; + const setUp = (domainsList: DomainsList) => render( + ()} + />, + ); afterEach(jest.clearAllMocks); - afterEach(() => wrapper?.unmount()); it('shows loading message while domains are loading', () => { - const wrapper = createWrapper(Mock.of({ loading: true, filteredDomains: [] })); - const message = wrapper.find(Message); - const searchField = wrapper.find(SearchField); - const result = wrapper.find(Result); - const apiError = wrapper.find(ShlinkApiError); + setUp(Mock.of({ loading: true, filteredDomains: [] })); - expect(message).toHaveLength(1); - expect(message.prop('loading')).toEqual(true); - expect(searchField).toHaveLength(0); - expect(result).toHaveLength(0); - expect(apiError).toHaveLength(0); + expect(screen.getByText('Loading...')).toBeInTheDocument(); + expect(screen.queryByText('Error loading domains :(')).not.toBeInTheDocument(); }); - it('shows error result when domains loading fails', () => { - const errorData = Mock.of(); - const wrapper = createWrapper(Mock.of( - { loading: false, error: true, errorData, filteredDomains: [] }, - )); - const message = wrapper.find(Message); - const searchField = wrapper.find(SearchField); - const result = wrapper.find(Result); - const apiError = wrapper.find(ShlinkApiError); + it.each([ + [undefined, 'Error loading domains :('], + [Mock.of(), 'Error loading domains :('], + [Mock.of({ detail: 'Foo error!!' }), 'Foo error!!'], + ])('shows error result when domains loading fails', (errorData, expectedErrorMessage) => { + setUp(Mock.of({ loading: false, error: true, errorData, filteredDomains: [] })); - expect(result).toHaveLength(1); - expect(result.prop('type')).toEqual('error'); - expect(apiError).toHaveLength(1); - expect(apiError.prop('errorData')).toEqual(errorData); - expect(searchField).toHaveLength(1); - expect(message).toHaveLength(0); + expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); + expect(screen.getByText(expectedErrorMessage)).toBeInTheDocument(); }); - it('filters domains when SearchField changes', () => { - const wrapper = createWrapper(Mock.of({ loading: false, error: false, filteredDomains: [] })); - const searchField = wrapper.find(SearchField); + it('filters domains when SearchField changes', async () => { + setUp(Mock.of({ loading: false, error: false, filteredDomains: [] })); expect(filterDomains).not.toHaveBeenCalled(); - searchField.simulate('change'); - expect(filterDomains).toHaveBeenCalledTimes(1); + fireEvent.change(screen.getByPlaceholderText('Search...'), { target: { value: 'Foo' } }); + await waitFor(() => expect(filterDomains).toHaveBeenCalledTimes(1)); }); - it('shows expected headers', () => { - const wrapper = createWrapper(Mock.of({ loading: false, error: false, filteredDomains: [] })); - const headerCells = wrapper.find('th'); + it('shows expected headers and one row when list of domains is empty', () => { + setUp(Mock.of({ loading: false, error: false, filteredDomains: [] })); - expect(headerCells).toHaveLength(7); + expect(screen.getAllByRole('columnheader')).toHaveLength(7); + expect(screen.getByText('No results found')).toBeInTheDocument(); }); - it('one row when list of domains is empty', () => { - const wrapper = createWrapper(Mock.of({ loading: false, error: false, filteredDomains: [] })); - const tableBody = wrapper.find('tbody'); - const regularRows = tableBody.find('tr'); - const domainRows = tableBody.find(DomainRow); - - expect(regularRows).toHaveLength(1); - expect(regularRows.html()).toContain('No results found'); - expect(domainRows).toHaveLength(0); - }); - - it('as many DomainRows as domains are provided', () => { + it('has many rows if multiple domains are provided', () => { const filteredDomains = [ Mock.of({ domain: 'foo' }), Mock.of({ domain: 'bar' }), Mock.of({ domain: 'baz' }), ]; - const wrapper = createWrapper(Mock.of({ loading: false, error: false, filteredDomains })); - const tableBody = wrapper.find('tbody'); - const regularRows = tableBody.find('tr'); - const domainRows = tableBody.find(DomainRow); + setUp(Mock.of({ loading: false, error: false, filteredDomains })); - expect(regularRows).toHaveLength(0); - expect(domainRows).toHaveLength(filteredDomains.length); + expect(screen.getAllByRole('row')).toHaveLength(filteredDomains.length + 1); + expect(screen.getByText('foo')).toBeInTheDocument(); + expect(screen.getByText('bar')).toBeInTheDocument(); + expect(screen.getByText('baz')).toBeInTheDocument(); }); }); From d582a0f9e551370c2767e8e6e8c7a70146d24e64 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 9 May 2022 19:45:09 +0200 Subject: [PATCH 11/12] Migrated DomainStatusIcon test to react testing library --- .../domains/helpers/DomainStatusIcon.test.tsx | 73 ++++----------- .../DomainStatusIcon.test.tsx.snap | 89 +++++++++++++++++++ 2 files changed, 104 insertions(+), 58 deletions(-) create mode 100644 test/domains/helpers/__snapshots__/DomainStatusIcon.test.tsx.snap diff --git a/test/domains/helpers/DomainStatusIcon.test.tsx b/test/domains/helpers/DomainStatusIcon.test.tsx index 7683a3b5..e7e1913b 100644 --- a/test/domains/helpers/DomainStatusIcon.test.tsx +++ b/test/domains/helpers/DomainStatusIcon.test.tsx @@ -1,73 +1,30 @@ -import { shallow, ShallowWrapper } from 'enzyme'; -import { UncontrolledTooltip } from 'reactstrap'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { fireEvent, render, screen } from '@testing-library/react'; import { Mock } from 'ts-mockery'; -import { faTimes, faCheck, faCircleNotch } from '@fortawesome/free-solid-svg-icons'; import { DomainStatus } from '../../../src/domains/data'; import { DomainStatusIcon } from '../../../src/domains/helpers/DomainStatusIcon'; describe('', () => { const matchMedia = jest.fn().mockReturnValue(Mock.of({ matches: false })); - let wrapper: ShallowWrapper; - const createWrapper = (status: DomainStatus) => { - wrapper = shallow(); - - return wrapper; - }; + const setUp = (status: DomainStatus) => render(); beforeEach(jest.clearAllMocks); - afterEach(() => wrapper?.unmount()); - it('renders loading icon when status is "validating"', () => { - const wrapper = createWrapper('validating'); - const tooltip = wrapper.find(UncontrolledTooltip); - const faIcon = wrapper.find(FontAwesomeIcon); - - expect(tooltip).toHaveLength(0); - expect(faIcon).toHaveLength(1); - expect(faIcon.prop('icon')).toEqual(faCircleNotch); - expect(faIcon.prop('spin')).toEqual(true); + it.each([ + ['validating' as DomainStatus], + ['invalid' as DomainStatus], + ['valid' as DomainStatus], + ])('renders expected icon and tooltip when status is not validating', (status) => { + const { container } = setUp(status); + expect(container.firstChild).toMatchSnapshot(); }); it.each([ - [ - 'invalid' as DomainStatus, - faTimes, - 'Oops! There is some missing configuration, and short URLs shared with this domain will not work.', - ], - ['valid' as DomainStatus, faCheck, 'Congratulations! This domain is properly configured.'], - ])('renders expected icon and tooltip when status is not validating', (status, expectedIcon, expectedText) => { - const wrapper = createWrapper(status); - const tooltip = wrapper.find(UncontrolledTooltip); - const faIcon = wrapper.find(FontAwesomeIcon); - const getTooltipText = (): string => { - const children = tooltip.prop('children'); + ['invalid' as DomainStatus], + ['valid' as DomainStatus], + ])('renders proper tooltip based on state', async (status) => { + const { container } = setUp(status); - if (typeof children === 'string') { - return children; - } - - return tooltip.find('span').html(); - }; - - expect(tooltip).toHaveLength(1); - expect(tooltip.prop('autohide')).toEqual(status === 'valid'); - expect(getTooltipText()).toContain(expectedText); - expect(faIcon).toHaveLength(1); - expect(faIcon.prop('icon')).toEqual(expectedIcon); - expect(faIcon.prop('spin')).toEqual(false); - }); - - it.each([ - [true, 'top-start'], - [false, 'left'], - ])('places the tooltip properly based on query match', (isMobile, expectedPlacement) => { - matchMedia.mockReturnValue(Mock.of({ matches: isMobile })); - - const wrapper = createWrapper('valid'); - const tooltip = wrapper.find(UncontrolledTooltip); - - expect(tooltip).toHaveLength(1); - expect(tooltip.prop('placement')).toEqual(expectedPlacement); + container.firstChild && fireEvent.mouseOver(container.firstChild); + expect(await screen.findByRole('tooltip')).toMatchSnapshot(); }); }); diff --git a/test/domains/helpers/__snapshots__/DomainStatusIcon.test.tsx.snap b/test/domains/helpers/__snapshots__/DomainStatusIcon.test.tsx.snap new file mode 100644 index 00000000..0b4d6173 --- /dev/null +++ b/test/domains/helpers/__snapshots__/DomainStatusIcon.test.tsx.snap @@ -0,0 +1,89 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders expected icon and tooltip when status is not validating 1`] = ` + +`; + +exports[` renders expected icon and tooltip when status is not validating 2`] = ` + + + +`; + +exports[` renders expected icon and tooltip when status is not validating 3`] = ` + + + +`; + +exports[` renders proper tooltip based on state 1`] = ` + +`; + +exports[` renders proper tooltip based on state 2`] = ` + +`; From 1f47658c3cf5a87e591410259cdfb4aebacaa30f Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 9 May 2022 19:47:41 +0200 Subject: [PATCH 12/12] Fixed coding styles --- src/common/SimplePaginator.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/common/SimplePaginator.tsx b/src/common/SimplePaginator.tsx index 00e6e185..9ebc2ef4 100644 --- a/src/common/SimplePaginator.tsx +++ b/src/common/SimplePaginator.tsx @@ -17,7 +17,9 @@ interface SimplePaginatorProps { centered?: boolean; } -export const SimplePaginator: FC = ({ pagesCount, currentPage, setCurrentPage, centered = true }) => { +export const SimplePaginator: FC = ( + { pagesCount, currentPage, setCurrentPage, centered = true }, +) => { if (pagesCount < 2) { return null; }