Upgrading tests, replacing Enzyme with TestRenderer

This commit is contained in:
James Barnsley
2022-09-29 10:38:26 +13:00
parent 129ed32f88
commit 6d6a2b4f24
14 changed files with 7381 additions and 160 deletions

View File

@ -1,28 +0,0 @@
{
"presets": [
"@babel/preset-env",
"@babel/preset-react",
"@babel/preset-typescript"
],
"plugins": [
"@babel/plugin-proposal-class-properties"
],
"env": {
"development": {
"plugins": [
"react-element-info"
]
},
"test": {
"plugins": [
"react-element-info",
"@babel/plugin-transform-runtime",
["polyfill-corejs3", { "method": "usage-global" }]
],
"presets": [
"@babel/preset-env",
"@babel/preset-react"
]
}
}
}

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import ShallowRenderer from 'react-test-renderer/shallow' import { render } from './test-wrapper';
import App from '../src/js/App'; import App from '../src/js/App';
jest.mock('react-redux', () => ({ jest.mock('react-redux', () => ({
@ -21,9 +21,7 @@ jest.mock('react-router-dom', () => ({
describe('<App />', () => { describe('<App />', () => {
it('should render', () => { it('should render', () => {
const renderer = new ShallowRenderer(); const result = render(<App />).toJSON();
renderer.render(<App />); expect(result).toMatchSnapshot();
const result = renderer.getRenderOutput();
expect(result.type).toEqual('div');
}); });
}); });

View File

@ -1,70 +1,68 @@
import React from 'react'; import React from 'react';
import { BrowserRouter } from "react-router-dom"; import { render } from '../test-wrapper';
// Testing-specific
import { shallow, mount, render } from 'enzyme';
// Test subjects
import Dater from '../../src/js/components/Dater'; import Dater from '../../src/js/components/Dater';
describe('<Dater />', () => { jest.mock('react-redux', () => ({
useSelector: jest.fn(),
}));
describe('<Dater />', () => {
it('should render null when given invalid props', () => { it('should render null when given invalid props', () => {
// Invalid type // Invalid type
const dom_1 = shallow(<Dater type="invalid-type" data={100000} />); const dom_1 = render(<Dater type="invalid-type" data={100000} />);
expect(dom_1.type()).toEqual(null); expect(dom_1.toJSON()).toEqual(null);
// Missing type // Missing type
const dom_2 = shallow(<Dater data={100000} />); const dom_2 = render(<Dater data={100000} />);
expect(dom_2.type()).toEqual(null); expect(dom_2.toJSON()).toEqual(null);
// Missing data // Missing data
const dom_4 = shallow(<Dater type="ago" />); const dom_4 = render(<Dater type="ago" />);
expect(dom_4.type()).toEqual(null); expect(dom_4.toJSON()).toEqual(null);
}); });
it('should handle milliseconds', () => { it('should handle milliseconds', () => {
const dom = shallow(<Dater type="length" data={30000} />); const dom = render(<Dater type="length" data={30000} />);
expect(dom.text()).toEqual('0:30'); expect(dom.toJSON()).toEqual('0:30');
}); });
it('should handle date (yyyy-mm-dd)', () => { it('should handle date (yyyy-mm-dd)', () => {
const dom = shallow(<Dater type="date" data="2015-10-23" />); const dom = render(<Dater type="date" data="2015-10-23" />);
expect(dom.text()).toEqual("23/10/2015"); expect(dom.toJSON()).toEqual("23/10/2015");
}); });
it('should handle date (mm/dd/yyyy)', () => { it('should handle date (mm/dd/yyyy)', () => {
const dom = shallow(<Dater type="date" data="10/23/2015" />); const dom = render(<Dater type="date" data="10/23/2015" />);
expect(dom.text()).toEqual("23/10/2015"); expect(dom.toJSON()).toEqual("23/10/2015");
}); });
it('should handle ago (days)', () => { it('should handle ago (days)', () => {
var date = new Date(); var date = new Date();
date.setDate( date.getDate() - 3 ); date.setDate( date.getDate() - 3 );
const dom = shallow(<Dater type="ago" data={date} />); const dom = render(<Dater type="ago" data={date} />);
expect(dom.text()).toEqual('3 days'); expect(dom.toJSON().join('')).toEqual('3 days');
}); });
it('should handle ago (hours)', () => { it('should handle ago (hours)', () => {
var date = new Date(); var date = new Date();
date.setTime( date.getTime() - 3 * 3600000 ); date.setTime( date.getTime() - 3 * 3600000 );
const dom = shallow(<Dater type="ago" data={date} />); const dom = render(<Dater type="ago" data={date} />);
expect(dom.text()).toEqual('3 hours'); expect(dom.toJSON().join('')).toEqual('3 hours');
}); });
it('should handle ago (minutes)', () => { it('should handle ago (minutes)', () => {
var date = new Date(); var date = new Date();
date.setTime( date.getTime() - 3 * 60000 ); date.setTime( date.getTime() - 3 * 60000 );
const dom = shallow(<Dater type="ago" data={date} />); const dom = render(<Dater type="ago" data={date} />);
expect(dom.text()).toEqual('3 minutes'); expect(dom.toJSON().join('')).toEqual('3 minutes');
}); });
it('should handle total time', () => { it('should handle total time', () => {
var data = [ const data = [
{ {
duration: 5 * 60000 duration: 5 * 60000
}, },
@ -75,7 +73,7 @@ describe('<Dater />', () => {
duration: 5 * 60000 duration: 5 * 60000
} }
]; ];
const dom = shallow(<Dater type="total-time" data={data} />); const dom = render(<Dater type="total-time" data={data} />);
expect(dom.text()).toEqual('15 mins'); expect(dom.toJSON().join('')).toEqual('15 mins');
}); });
}); });

View File

@ -1,15 +1,11 @@
import React from 'react'; import React from 'react';
import { BrowserRouter } from 'react-router-dom';
import TestRenderer from 'react-test-renderer';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { GridItem } from '../../src/js/components/GridItem'; import { GridItem } from '../../src/js/components/GridItem';
import { render } from '../test-wrapper';
import state from '../state'; import state from '../state';
jest.mock('react-redux', () => ({ jest.mock('react-redux', () => ({
useSelector: jest.fn(), useSelector: jest.fn(),
useDispatch: () => jest.fn(), useDispatch: jest.fn(),
useDrag: jest.fn(),
})); }));
jest.mock('react-router-dom', () => ({ jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'), ...jest.requireActual('react-router-dom'),
@ -27,32 +23,22 @@ describe('<GridItem />', () => {
const { core: { items } } = state; const { core: { items } } = state;
it('should handle album', () => { it('should handle album', () => {
const result = TestRenderer.create( const result = render(
<BrowserRouter> <GridItem item={items['local:album:md5:66fbea3593ba96a15a9d4188bebab50b']} />
<DndProvider backend={HTML5Backend}>
<GridItem item={items['local:album:md5:66fbea3593ba96a15a9d4188bebab50b']} />
</DndProvider>
</BrowserRouter>
).toJSON(); ).toJSON();
expect(result).toMatchSnapshot(); expect(result).toMatchSnapshot();
}); });
it('should handle artist', () => { it('should handle artist', () => {
const result = TestRenderer.create( const result = render(
<BrowserRouter> <GridItem item={items['local:artist:md5:4f6e4f979e2c40c5e6ad1735804c29bc']} />
<DndProvider backend={HTML5Backend}>
<GridItem item={items['local:artist:md5:4f6e4f979e2c40c5e6ad1735804c29bc']} />
</DndProvider>
</BrowserRouter>
).toJSON(); ).toJSON();
expect(result).toMatchSnapshot(); expect(result).toMatchSnapshot();
}); });
it('should handle playlist', () => { it('should handle playlist', () => {
const result = TestRenderer.create( const result = render(
<BrowserRouter> <GridItem item={items['m3u:Local%20tester.m3u8']} />
<DndProvider backend={HTML5Backend}>
<GridItem item={items['m3u:Local%20tester.m3u8']} />
</DndProvider>
</BrowserRouter>
).toJSON(); ).toJSON();
expect(result).toMatchSnapshot(); expect(result).toMatchSnapshot();
}); });

View File

@ -1,28 +1,17 @@
import React from 'react'; import React from 'react';
import { BrowserRouter } from "react-router-dom"; import { render } from '../test-wrapper';
// Testing-specific
import { shallow, mount, render } from 'enzyme';
// Test subjects
import Link from '../../src/js/components/Link'; import Link from '../../src/js/components/Link';
describe('<Link />', () => { describe('<Link />', () => {
const dom = mount( const result = render(
<BrowserRouter> <Link to="test" className="test-classname">Link contents</Link>
<Link to="test" className="test-classname">Link contents</Link> ).toJSON();
</BrowserRouter>
);
it('should render a valid <a> tag', () => { it('should render a valid <a> tag', () => {
const a = dom.find('a'); expect(result.type).toEqual('a');
expect(a.length).toBe(1); expect(result.props.href).toEqual('/test');
expect(a.find('[href]').length).toBe(1); expect(result.children.join('')).toEqual('Link contents');
expect(a.text()).toEqual('Link contents'); expect(result.props.className).toContain('test-classname');
});
it('should handle className prop', () => {
expect(dom.find('a').hasClass('test-classname')).toBe(true);
}); });
}); });

View File

@ -10,7 +10,7 @@ exports[`<GridItem /> should handle album 1`] = `
className=" " className=" "
data-qa-file="Link" data-qa-file="Link"
data-qa-node="RouterLink" data-qa-node="RouterLink"
href="/album/bG9jYWw6YWxidW06bWQ1OjY2ZmJlYTM1OTNiYTk2YTE1YTlkNDE4OGJlYmFiNTBi/Sirens of the Sea" href="/album/bG9jYWw6YWxidW06bWQ1OjY2ZmJlYTM1OTNiYTk2YTE1YTlkNDE4OGJlYmFiNTBi/Sirens%20of%20the%20Sea"
onClick={[Function]} onClick={[Function]}
onContextMenu={[Function]} onContextMenu={[Function]}
> >
@ -31,8 +31,8 @@ exports[`<GridItem /> should handle album 1`] = `
data-qa-file="Thumbnail" data-qa-file="Thumbnail"
data-qa-node="div" data-qa-node="div"
style={ style={
Object { {
"backgroundImage": "url(\\"/local/17338e740316f18dbb5e3331ac6be6c1-500x500.jpeg\\")", "backgroundImage": "url("/local/17338e740316f18dbb5e3331ac6be6c1-500x500.jpeg")",
} }
} }
/> />
@ -47,7 +47,13 @@ exports[`<GridItem /> should handle album 1`] = `
data-qa-file="GridItem" data-qa-file="GridItem"
data-qa-node="div" data-qa-node="div"
> >
Sirens of the Sea <span
data-qa-file="GridItem"
data-qa-node="span"
title="Sirens of the Sea"
>
Sirens of the Sea
</span>
</div> </div>
<div <div
className="grid__item__secondary" className="grid__item__secondary"
@ -125,8 +131,8 @@ exports[`<GridItem /> should handle artist 1`] = `
data-qa-file="Thumbnail" data-qa-file="Thumbnail"
data-qa-node="div" data-qa-node="div"
style={ style={
Object { {
"backgroundImage": "url(\\"https://i.scdn.co/image/4dc7080ef509c36203a131a0eab8dd5e4800d7c2\\")", "backgroundImage": "url("https://i.scdn.co/image/4dc7080ef509c36203a131a0eab8dd5e4800d7c2")",
} }
} }
/> />
@ -141,7 +147,13 @@ exports[`<GridItem /> should handle artist 1`] = `
data-qa-file="GridItem" data-qa-file="GridItem"
data-qa-node="div" data-qa-node="div"
> >
Above & Beyond <span
data-qa-file="GridItem"
data-qa-node="span"
title="Above & Beyond"
>
Above & Beyond
</span>
</div> </div>
<div <div
className="grid__item__secondary" className="grid__item__secondary"
@ -182,7 +194,7 @@ exports[`<GridItem /> should handle playlist 1`] = `
className=" " className=" "
data-qa-file="Link" data-qa-file="Link"
data-qa-node="RouterLink" data-qa-node="RouterLink"
href="/playlist/bTN1OkxvY2FsJTIwdGVzdGVyLm0zdTg=/Local tester" href="/playlist/bTN1OkxvY2FsJTIwdGVzdGVyLm0zdTg=/Local%20tester"
onClick={[Function]} onClick={[Function]}
onContextMenu={[Function]} onContextMenu={[Function]}
> >
@ -203,8 +215,8 @@ exports[`<GridItem /> should handle playlist 1`] = `
data-qa-file="Thumbnail" data-qa-file="Thumbnail"
data-qa-node="div" data-qa-node="div"
style={ style={
Object { {
"backgroundImage": "url(\\"null\\")", "backgroundImage": "url("null")",
} }
} }
/> />
@ -219,7 +231,13 @@ exports[`<GridItem /> should handle playlist 1`] = `
data-qa-file="GridItem" data-qa-file="GridItem"
data-qa-node="div" data-qa-node="div"
> >
Local tester <span
data-qa-file="GridItem"
data-qa-node="span"
title="Local tester"
>
Local tester
</span>
</div> </div>
<div <div
className="grid__item__secondary" className="grid__item__secondary"

View File

@ -0,0 +1,13 @@
const transformer = require("jest-transform-yaml").default
const newTransformer = {
...transformer,
process: function (...params) {
return {
code: transformer?.process(...params),
map: null,
}
},
}
module.exports = newTransformer;

View File

@ -1,4 +0,0 @@
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
Enzyme.configure({ adapter: new Adapter() });

View File

@ -260,3 +260,4 @@ state.core.items['jest:track:three'] = {
}; };
export default state; export default state;
export { state }

35
__tests__/test-wrapper.js Normal file
View File

@ -0,0 +1,35 @@
import React from 'react';
import TestRenderer from 'react-test-renderer';
import { Provider as ReduxProvider } from 'react-redux/src';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { store } from '../src/js/store';
const customRender = (
element,
{
initialState,
mocks,
...options
} = {},
) => {
return TestRenderer.create(
(
<ReduxProvider store={store}>
<DndProvider backend={HTML5Backend}>
<BrowserRouter basename="/">
<Routes>
<Route path="*" element={element} />
</Routes>
</BrowserRouter>
</DndProvider>
</ReduxProvider>
),
{
...options,
},
);
};
export { customRender as render };

View File

@ -1,15 +1,21 @@
import React from 'react'; import React from 'react';
import TestRenderer from 'react-test-renderer'; import { render } from '../test-wrapper';
import Album from '../../src/js/views/Album';
import { Album } from '../../src/js/views/Album'; import { state as mockState } from '../state';
import * as uiActions from '../../src/js/services/ui/actions';
import * as coreActions from '../../src/js/services/core/actions';
import state from '../state';
jest.mock('react-dnd', () => ({
...jest.requireActual('react-dnd'),
useDrag: jest.fn(),
useDrop: jest.fn(),
}));
jest.mock('redux-persist', () => ({
...jest.requireActual('redux-persist'),
persistReducer: jest.fn().mockImplementation((config, reducers) => reducers),
}));
jest.mock('react-redux', () => ({ jest.mock('react-redux', () => ({
useSelector: jest.fn(), useSelector: jest.fn(() => mockState),
useDispatch: () => jest.fn(), useDispatch: jest.fn(fn => fn()),
connect: () => jest.fn(), connect: jest.fn(fn => fn()),
})); }));
jest.mock('react-router-dom', () => ({ jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'), ...jest.requireActual('react-router-dom'),
@ -28,14 +34,8 @@ describe('<Album />', () => {
// Need to rebuild Album to functional component, at which point I'll copy previous // Need to rebuild Album to functional component, at which point I'll copy previous
// snapshot testing approach from other project // snapshot testing approach from other project
it.skip('should render accurately', () => { it('should render accurately', () => {
const result = TestRenderer.create( const result = render(<Album album={album} />).toJSON();
<Album
album={album}
uiActions={uiActions}
coreActions={coreActions}
/>
).toJSON();
expect(result).toMatchSnapshot(); expect(result).toMatchSnapshot();
}); });
}); });

View File

@ -1,36 +1,43 @@
module.exports = { module.exports = {
verbose: true, verbose: true,
collectCoverage: true, collectCoverage: true,
testEnvironment: 'jsdom',
testMatch: [ testMatch: [
"<rootDir>/__tests__/**/*.test.js?(x)", "<rootDir>/__tests__/**/*.test.js?(x)",
], ],
moduleDirectories: [ // moduleDirectories: [
"node_modules", // "node_modules",
"src", // "src",
], // ],
moduleFileExtensions: [ // moduleFileExtensions: [
"js", // "js",
"jsx", // "jsx",
"yaml", // "yaml",
], // ],
moduleNameMapper: { moduleNameMapper: {
/*"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|yml|yaml)$": "<rootDir>/__mocks__/fileMock.js",*/ /*"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|yml|yaml)$": "<rootDir>/__mocks__/fileMock.js",*/
"^.+\\.(css|scss|sass|less)$": "identity-obj-proxy", "^.+\\.(css|scss|sass|less)$": "identity-obj-proxy",
}, },
transform: { transform: {
"\\.yaml$": "yaml-jest", "^.+\\.(j|t)sx?$": "babel-jest",
"^.+\\.jsx$": "babel-jest", "^.+\\.ya?ml$": "<rootDir>/__tests__/jest-yaml-transformer.js",
"^.+\\.js$": "babel-jest",
"^.+\\.ts$": "babel-jest",
"^.+\\.tsx$": "babel-jest",
}, },
transformIgnorePatterns: [
"node_modules/(?!react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|react-redux)"
],
coverageReporters: [ coverageReporters: [
"lcov", "lcov",
], ],
collectCoverageFrom: [ collectCoverageFrom: [
"src/**/*.{js,jsx,ts,tsx}", "src/**/*.{js,jsx,ts,tsx}",
], ],
setupFilesAfterEnv: [ globals: {
"./__tests__/setup.js" window: {
], location: {
hostname: 'localhost',
port: 6680,
protocol: 'http',
},
},
},
}; };

View File

@ -1,6 +1,6 @@
{ {
"name": "mopidy-iris", "name": "mopidy-iris",
"version": "3.64.1", "version": "3.65.0",
"description": "Mopidy HTTP interface", "description": "Mopidy HTTP interface",
"repository": "https://github.com/jaedb/iris", "repository": "https://github.com/jaedb/iris",
"author": "James Barnsley <james@barnsley.nz>", "author": "James Barnsley <james@barnsley.nz>",
@ -41,8 +41,8 @@
"devDependencies": { "devDependencies": {
"@babel/cli": "7.18.6", "@babel/cli": "7.18.6",
"@babel/core": "7.18.6", "@babel/core": "7.18.6",
"@babel/eslint-parser": "7.18.2",
"@babel/plugin-proposal-class-properties": "7.18.6", "@babel/plugin-proposal-class-properties": "7.18.6",
"@babel/plugin-transform-modules-commonjs": "7.18.6",
"@babel/plugin-transform-runtime": "7.18.6", "@babel/plugin-transform-runtime": "7.18.6",
"@babel/polyfill": "^7.12.1", "@babel/polyfill": "^7.12.1",
"@babel/preset-env": "7.18.6", "@babel/preset-env": "7.18.6",
@ -50,7 +50,6 @@
"@babel/preset-stage-2": "7.8.3", "@babel/preset-stage-2": "7.8.3",
"@babel/preset-typescript": "7.18.6", "@babel/preset-typescript": "7.18.6",
"@babel/register": "7.18.6", "@babel/register": "7.18.6",
"@babel/eslint-parser": "7.18.2",
"babel-jest": "28.1.2", "babel-jest": "28.1.2",
"babel-loader": "8.2.5", "babel-loader": "8.2.5",
"babel-plugin-add-module-exports": "^1.0.4", "babel-plugin-add-module-exports": "^1.0.4",
@ -62,8 +61,6 @@
"copy-dir": "^1.3.0", "copy-dir": "^1.3.0",
"core-js": "3.23.3", "core-js": "3.23.3",
"css-loader": "6.7.1", "css-loader": "6.7.1",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.5",
"eslint": "8.19.0", "eslint": "8.19.0",
"eslint-config-airbnb": "19.0.4", "eslint-config-airbnb": "19.0.4",
"eslint-plugin-import": "2.26.0", "eslint-plugin-import": "2.26.0",
@ -71,9 +68,12 @@
"eslint-plugin-react": "7.30.1", "eslint-plugin-react": "7.30.1",
"eslint-webpack-plugin": "3.2.0", "eslint-webpack-plugin": "3.2.0",
"expose-loader": "4.0.0", "expose-loader": "4.0.0",
"fake-indexeddb": "^4.0.0",
"file-loader": "6.2.0", "file-loader": "6.2.0",
"fs-copy-file": "2.1.2", "fs-copy-file": "2.1.2",
"jest": "28.1.2", "jest": "29.0.3",
"jest-environment-jsdom": "^29.1.1",
"jest-transform-yaml": "1.0.0",
"js-sha256": "^0.9.0", "js-sha256": "^0.9.0",
"mini-css-extract-plugin": "2.6.1", "mini-css-extract-plugin": "2.6.1",
"parse5-htmlparser2-tree-adapter": "7.0.0", "parse5-htmlparser2-tree-adapter": "7.0.0",
@ -89,7 +89,6 @@
"webpack-cli": "4.10.0", "webpack-cli": "4.10.0",
"webpack-dev-server": "4.9.3", "webpack-dev-server": "4.9.3",
"webpack-strip": "0.1.0", "webpack-strip": "0.1.0",
"yaml-jest": "1.2.0",
"yaml-loader": "0.6.0" "yaml-loader": "0.6.0"
}, },
"scripts": { "scripts": {

7209
yarn.lock Normal file

File diff suppressed because it is too large Load Diff