├── public
├── favicon.ico
├── manifest.json
└── index.html
├── src
├── setupTests.js
├── index.css
├── components
│ ├── ReminderItem.test.jsx
│ ├── __snapshots__
│ │ ├── ScreenWidth.test.jsx.snap
│ │ ├── PageURL.test.jsx.snap
│ │ ├── Other.test.jsx.snap
│ │ ├── Counter.test.jsx.snap
│ │ └── ReminderItem.test.jsx.snap
│ ├── Other.test.jsx
│ ├── Counter.test.jsx
│ ├── PageURL.jsx
│ ├── ReminderItem.jsx
│ ├── Other.jsx
│ ├── ScreenWidth.jsx
│ ├── Counter.jsx
│ ├── PageURL.test.jsx
│ ├── Reminder.jsx
│ ├── ScreenWidth.test.jsx
│ └── Reminder.test.jsx
├── index.js
├── App.js
└── serviceWorker.js
├── .gitignore
├── package.json
├── LICENSE.md
└── README.md
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikeheddes/react-testing-demo/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | import Enzyme from "enzyme";
2 | import Adapter from "enzyme-adapter-react-16";
3 | import { JSDOM } from "jsdom";
4 |
5 | Enzyme.configure({ adapter: new Adapter() });
6 |
7 | const jsdom = new JSDOM("
");
8 | const { window } = jsdom;
9 |
10 | global.window = window;
11 | global.document = window.document;
12 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Testing React",
3 | "name": "React Testing Demo",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
5 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
6 | sans-serif;
7 | -webkit-font-smoothing: antialiased;
8 | -moz-osx-font-smoothing: grayscale;
9 | }
10 |
11 | code {
12 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
13 | monospace;
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/ReminderItem.test.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import renderer from "react-test-renderer";
3 | import "jest-styled-components";
4 |
5 | import ToDoItem from "./ReminderItem";
6 |
7 | describe("", () => {
8 | it("match snapshot", () => {
9 | const onRemove = jest.fn();
10 | const tree = renderer.create(
11 | The Label
12 | );
13 | expect(tree).toMatchSnapshot();
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import * as serviceWorker from './serviceWorker';
6 |
7 | ReactDOM.render(, document.getElementById('root'));
8 |
9 | // If you want your app to work offline and load faster, you can change
10 | // unregister() to register() below. Note this comes with some pitfalls.
11 | // Learn more about service workers: http://bit.ly/CRA-PWA
12 | serviceWorker.unregister();
13 |
--------------------------------------------------------------------------------
/src/components/__snapshots__/ScreenWidth.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` should match snapshot 1`] = `
4 | .c0 {
5 | background-color: #855FEB;
6 | position: relative;
7 | box-shadow: 8px 8px rgba(0,0,0,0.15);
8 | }
9 |
10 | .c1 {
11 | display: block;
12 | text-align: center;
13 | color: white;
14 | font-weight: 500;
15 | font-size: 42px;
16 | padding: 39px 50px;
17 | }
18 |
19 |
23 |
26 | 345
27 | px
28 |
29 |
30 | `;
31 |
--------------------------------------------------------------------------------
/src/components/Other.test.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import renderer from "react-test-renderer";
3 | import "jest-styled-components";
4 |
5 | import { Wrapper, Button, Label, Line } from "./Other";
6 |
7 | describe("Other contains a series of styled components", () => {
8 | it("snapshot wrapper", () => {
9 | const tree = renderer.create();
10 | expect(tree).toMatchSnapshot();
11 | });
12 |
13 | it("snapshot button", () => {
14 | const tree = renderer.create();
15 | expect(tree).toMatchSnapshot();
16 | });
17 |
18 | it("snapshot label", () => {
19 | const tree = renderer.create();
20 | expect(tree).toMatchSnapshot();
21 | });
22 |
23 | it("snapshot line", () => {
24 | const tree = renderer.create();
25 | expect(tree).toMatchSnapshot();
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/src/components/__snapshots__/PageURL.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`PageURL matches snapshot 1`] = `
4 | .c1 {
5 | background-color: #FAA138;
6 | position: relative;
7 | box-shadow: 8px 8px rgba(0,0,0,0.15);
8 | }
9 |
10 | .c2 {
11 | display: block;
12 | text-align: center;
13 | color: white;
14 | font-weight: 500;
15 | font-size: 42px;
16 | padding: 39px 50px;
17 | }
18 |
19 | .c0 {
20 | -webkit-text-decoration: none;
21 | text-decoration: none;
22 | }
23 |
24 |
31 |
35 |
38 | URL:
39 | /initial_url
40 |
41 |
42 |
43 | `;
44 |
--------------------------------------------------------------------------------
/src/components/Counter.test.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 | import renderer from "react-test-renderer";
4 | import "jest-styled-components";
5 | import { Label } from "./Other";
6 | import Counter from "./Counter";
7 |
8 | describe("", () => {
9 | it("should set the state to state + 1 when button is pressed (enzyme)", () => {
10 | const wrapper = shallow();
11 | expect(wrapper.find(Label).text()).toEqual("0");
12 |
13 | wrapper.find('[data-testid="inc"]').simulate("click");
14 | expect(wrapper.find(Label).text()).toEqual("1");
15 |
16 | wrapper.find('[data-testid="dec"]').simulate("click");
17 | expect(wrapper.find(Label).text()).toEqual("0");
18 | });
19 |
20 | it("should match snapshot", () => {
21 | const tree = renderer.create();
22 | expect(tree).toMatchSnapshot();
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-testing-demo",
3 | "version": "0.1.0",
4 | "private": true,
5 | "homepage": "http://mikeheddes.github.io/react-testing-demo",
6 | "dependencies": {
7 | "jest-styled-components": "^6.2.2",
8 | "react": "^16.6.0",
9 | "react-dom": "^16.6.0",
10 | "react-router-dom": "^4.3.1",
11 | "styled-components": "^4.0.3"
12 | },
13 | "devDependencies": {
14 | "enzyme": "^3.7.0",
15 | "enzyme-adapter-react-16": "^1.6.0",
16 | "gh-pages": "^2.0.1",
17 | "react-scripts": "2.1.0",
18 | "react-test-renderer": "^16.6.0"
19 | },
20 | "scripts": {
21 | "start": "react-scripts start",
22 | "build": "react-scripts build",
23 | "predeploy": "yarn build",
24 | "deploy": "gh-pages -d build",
25 | "test": "react-scripts test",
26 | "test:watch": "react-scripts test --watch --coverage",
27 | "eject": "react-scripts eject"
28 | },
29 | "eslintConfig": {
30 | "extends": "react-app"
31 | },
32 | "browserslist": [
33 | ">0.2%",
34 | "not dead",
35 | "not ie <= 11",
36 | "not op_mini all"
37 | ]
38 | }
39 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import { HashRouter } from "react-router-dom";
4 |
5 | import Counter from "./components/Counter";
6 | import ScreenWidth from "./components/ScreenWidth";
7 | import PageURL from "./components/PageURL";
8 | import Reminder from "./components/Reminder";
9 |
10 | const Box = styled.div`
11 | width: 100%;
12 | max-width: 500px;
13 | margin: 80px auto;
14 | `;
15 |
16 | const Title = styled.h2`
17 | text-align: center;
18 | margin-bottom: 30px;
19 | font-weight: 600;
20 | font-size: 42px;
21 | color: #222;
22 | `;
23 |
24 | const App = () => (
25 |
26 | <>
27 |
28 | Counter
29 |
30 |
31 |
32 | ScreenWidth
33 |
34 |
35 |
36 | PageURL
37 |
38 |
39 |
40 | Reminder
41 |
42 |
43 | >
44 |
45 | );
46 |
47 | export default App;
48 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Mike Heddes
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/components/PageURL.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { withRouter, Link } from "react-router-dom";
4 | import styled from "styled-components";
5 |
6 | import { Wrapper, Label } from "./Other";
7 |
8 | export function createRandomString() {
9 | return Math.random()
10 | .toString(26)
11 | .slice(3, 8);
12 | }
13 |
14 | const StyledLink = styled(Link)`
15 | text-decoration: none;
16 | `;
17 |
18 | export const PageURL = ({ location: { pathname }, nextUrl }) => (
19 |
24 |
25 |
26 |
27 |
28 | );
29 |
30 | PageURL.propTypes = {
31 | nextUrl: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
32 | location: PropTypes.shape({
33 | pathname: PropTypes.string.isRequired
34 | }).isRequired
35 | };
36 |
37 | PageURL.defaultProps = {
38 | nextUrl: createRandomString
39 | };
40 |
41 | export default withRouter(PageURL);
42 |
--------------------------------------------------------------------------------
/src/components/ReminderItem.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import styled from "styled-components";
4 |
5 | import { Wrapper, Label, Button, Line } from "./Other";
6 |
7 | const ItemWrapper = styled(Wrapper)`
8 | margin: 30px 0;
9 | display: flex;
10 | flex-direction: row;
11 | `;
12 |
13 | const StyledLabel = styled(Label)`
14 | padding: 20px 30px;
15 | font-size: 34px;
16 | flex: 1 1 auto;
17 | text-align: left;
18 | `;
19 |
20 | const StyledButton = styled(Button)`
21 | padding: 20px 30px;
22 | font-size: 34px;
23 | `;
24 |
25 | const Rotate = styled.span`
26 | transform: rotate(45deg);
27 | transform-origin: center;
28 | display: inline-block;
29 | `;
30 |
31 | const ToDoItem = ({ children, onRemove }) => (
32 |
33 | {children}
34 |
35 |
36 | +
37 |
38 |
39 | );
40 |
41 | ToDoItem.propTypes = {
42 | children: PropTypes.string.isRequired,
43 | onRemove: PropTypes.func.isRequired
44 | };
45 |
46 | export default ToDoItem;
47 |
--------------------------------------------------------------------------------
/src/components/Other.jsx:
--------------------------------------------------------------------------------
1 | import styled, { css } from "styled-components";
2 | import PropTypes from "prop-types";
3 |
4 | export const textStyle = css`
5 | color: white;
6 | font-weight: 500;
7 | font-size: 42px;
8 | padding: 39px 50px;
9 | `;
10 |
11 | const Wrapper = styled.div`
12 | background-color: ${({ color }) => color};
13 | position: relative;
14 | box-shadow: 8px 8px rgba(0, 0, 0, 0.15);
15 | `;
16 |
17 | Wrapper.propTypes = {
18 | color: PropTypes.string
19 | };
20 |
21 | Wrapper.defaultProps = {
22 | color: "#d8d8d8"
23 | };
24 |
25 | export { Wrapper };
26 |
27 | const Label = styled.span`
28 | display: block;
29 | text-align: center;
30 | ${textStyle};
31 | `;
32 |
33 | export { Label };
34 |
35 | const Button = styled.button`
36 | ${textStyle};
37 | appearance: none;
38 | border: none;
39 | outline: none;
40 | background-color: transparent;
41 | user-select: none;
42 | cursor: pointer;
43 |
44 | &:active {
45 | opacity: 0.5;
46 | }
47 | `;
48 |
49 | export { Button };
50 |
51 | const Line = styled.div`
52 | width: 5px;
53 | background-color: white;
54 | flex: 0 0 auto;
55 | `;
56 |
57 | export { Line };
58 |
--------------------------------------------------------------------------------
/src/components/ScreenWidth.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import { Wrapper, Label } from "./Other";
5 |
6 | function withScreenWidth(WrappedComponent) {
7 | return class extends Component {
8 | constructor(props) {
9 | super(props);
10 | this.state = {
11 | width: window.innerWidth
12 | };
13 | this.handleResize = this.handleResize.bind(this);
14 | // Keep separate from state because a change should not cause a rerender
15 | this.updating = false;
16 | }
17 |
18 | componentDidMount() {
19 | window.addEventListener("resize", this.handleResize);
20 | }
21 |
22 | componentWillUnmount() {
23 | window.removeEventListener("resize", this.handleResize);
24 | }
25 |
26 | handleResize() {
27 | if (this.updating) return;
28 | this.updating = true;
29 |
30 | requestAnimationFrame(() => {
31 | this.setState({ width: window.innerWidth }, () => {
32 | this.updating = false;
33 | });
34 | });
35 | }
36 |
37 | render() {
38 | return ;
39 | }
40 | };
41 | }
42 |
43 | export const ScreenWidth = ({ width }) => (
44 |
45 |
46 |
47 | );
48 |
49 | ScreenWidth.propTypes = {
50 | width: PropTypes.number.isRequired
51 | };
52 |
53 | export default withScreenWidth(ScreenWidth);
54 |
--------------------------------------------------------------------------------
/src/components/__snapshots__/Other.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Other contains a series of styled components snapshot button 1`] = `
4 | .c0 {
5 | color: white;
6 | font-weight: 500;
7 | font-size: 42px;
8 | padding: 39px 50px;
9 | -webkit-appearance: none;
10 | -moz-appearance: none;
11 | appearance: none;
12 | border: none;
13 | outline: none;
14 | background-color: transparent;
15 | -webkit-user-select: none;
16 | -moz-user-select: none;
17 | -ms-user-select: none;
18 | user-select: none;
19 | cursor: pointer;
20 | }
21 |
22 | .c0:active {
23 | opacity: 0.5;
24 | }
25 |
26 |
29 | `;
30 |
31 | exports[`Other contains a series of styled components snapshot label 1`] = `
32 | .c0 {
33 | display: block;
34 | text-align: center;
35 | color: white;
36 | font-weight: 500;
37 | font-size: 42px;
38 | padding: 39px 50px;
39 | }
40 |
41 |
44 | `;
45 |
46 | exports[`Other contains a series of styled components snapshot line 1`] = `
47 | .c0 {
48 | width: 5px;
49 | background-color: white;
50 | -webkit-flex: 0 0 auto;
51 | -ms-flex: 0 0 auto;
52 | flex: 0 0 auto;
53 | }
54 |
55 |
58 | `;
59 |
60 | exports[`Other contains a series of styled components snapshot wrapper 1`] = `
61 | .c0 {
62 | background-color: fff;
63 | position: relative;
64 | box-shadow: 8px 8px rgba(0,0,0,0.15);
65 | }
66 |
67 |
71 | `;
72 |
--------------------------------------------------------------------------------
/src/components/Counter.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import styled from "styled-components";
3 |
4 | import { Wrapper, Label, Button, Line } from "./Other";
5 |
6 | const StyledButton = styled(Button)`
7 | width: 50%;
8 | position: absolute;
9 | top: 0;
10 | bottom: 0;
11 | z-index: 0;
12 |
13 | &:first-of-type {
14 | text-align: left;
15 | left: 0;
16 | right: 50%;
17 | }
18 |
19 | &:last-of-type {
20 | text-align: right;
21 | left: 50%;
22 | right: 0;
23 | }
24 | `;
25 |
26 | const StyledLine = styled(Line)`
27 | position: absolute;
28 | left: calc(50% - 5px / 2);
29 | height: 28px;
30 |
31 | &:first-of-type {
32 | top: 0;
33 | }
34 |
35 | &:last-of-type {
36 | bottom: 0;
37 | }
38 | `;
39 |
40 | class Counter extends Component {
41 | state = { value: 0 };
42 |
43 | handleDecreaseCount = () => {
44 | this.setState({ value: this.state.value - 1 });
45 | };
46 |
47 | handleIncreaseCount = () => {
48 | this.setState({ value: this.state.value + 1 });
49 | };
50 |
51 | render() {
52 | const { value } = this.state;
53 | return (
54 |
55 |
56 | -
57 |
58 |
59 |
60 | +
61 |
62 |
63 |
64 |
65 | );
66 | }
67 | }
68 |
69 | export default Counter;
70 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
22 | React Testing Demo
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/src/components/__snapshots__/Counter.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` should match snapshot 1`] = `
4 | .c0 {
5 | background-color: #eb4b4b;
6 | position: relative;
7 | box-shadow: 8px 8px rgba(0,0,0,0.15);
8 | }
9 |
10 | .c2 {
11 | display: block;
12 | text-align: center;
13 | color: white;
14 | font-weight: 500;
15 | font-size: 42px;
16 | padding: 39px 50px;
17 | }
18 |
19 | .c1 {
20 | color: white;
21 | font-weight: 500;
22 | font-size: 42px;
23 | padding: 39px 50px;
24 | -webkit-appearance: none;
25 | -moz-appearance: none;
26 | appearance: none;
27 | border: none;
28 | outline: none;
29 | background-color: transparent;
30 | -webkit-user-select: none;
31 | -moz-user-select: none;
32 | -ms-user-select: none;
33 | user-select: none;
34 | cursor: pointer;
35 | width: 50%;
36 | position: absolute;
37 | top: 0;
38 | bottom: 0;
39 | z-index: 0;
40 | }
41 |
42 | .c1:active {
43 | opacity: 0.5;
44 | }
45 |
46 | .c1:first-of-type {
47 | text-align: left;
48 | left: 0;
49 | right: 50%;
50 | }
51 |
52 | .c1:last-of-type {
53 | text-align: right;
54 | left: 50%;
55 | right: 0;
56 | }
57 |
58 | .c3 {
59 | width: 5px;
60 | background-color: white;
61 | -webkit-flex: 0 0 auto;
62 | -ms-flex: 0 0 auto;
63 | flex: 0 0 auto;
64 | position: absolute;
65 | left: calc(50% - 5px / 2);
66 | height: 28px;
67 | }
68 |
69 | .c3:first-of-type {
70 | top: 0;
71 | }
72 |
73 | .c3:last-of-type {
74 | bottom: 0;
75 | }
76 |
77 |
81 |
88 |
91 | 0
92 |
93 |
100 |
103 |
106 |
107 | `;
108 |
--------------------------------------------------------------------------------
/src/components/__snapshots__/ReminderItem.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` match snapshot 1`] = `
4 | .c2 {
5 | width: 5px;
6 | background-color: white;
7 | -webkit-flex: 0 0 auto;
8 | -ms-flex: 0 0 auto;
9 | flex: 0 0 auto;
10 | }
11 |
12 | .c0 {
13 | background-color: #495265;
14 | position: relative;
15 | box-shadow: 8px 8px rgba(0,0,0,0.15);
16 | margin: 30px 0;
17 | display: -webkit-box;
18 | display: -webkit-flex;
19 | display: -ms-flexbox;
20 | display: flex;
21 | -webkit-flex-direction: row;
22 | -ms-flex-direction: row;
23 | flex-direction: row;
24 | }
25 |
26 | .c1 {
27 | display: block;
28 | text-align: center;
29 | color: white;
30 | font-weight: 500;
31 | font-size: 42px;
32 | padding: 39px 50px;
33 | padding: 20px 30px;
34 | font-size: 34px;
35 | -webkit-flex: 1 1 auto;
36 | -ms-flex: 1 1 auto;
37 | flex: 1 1 auto;
38 | text-align: left;
39 | }
40 |
41 | .c3 {
42 | color: white;
43 | font-weight: 500;
44 | font-size: 42px;
45 | padding: 39px 50px;
46 | -webkit-appearance: none;
47 | -moz-appearance: none;
48 | appearance: none;
49 | border: none;
50 | outline: none;
51 | background-color: transparent;
52 | -webkit-user-select: none;
53 | -moz-user-select: none;
54 | -ms-user-select: none;
55 | user-select: none;
56 | cursor: pointer;
57 | padding: 20px 30px;
58 | font-size: 34px;
59 | }
60 |
61 | .c3:active {
62 | opacity: 0.5;
63 | }
64 |
65 | .c4 {
66 | -webkit-transform: rotate(45deg);
67 | -ms-transform: rotate(45deg);
68 | transform: rotate(45deg);
69 | -webkit-transform-origin: center;
70 | -ms-transform-origin: center;
71 | transform-origin: center;
72 | display: inline-block;
73 | }
74 |
75 |
79 |
82 | The Label
83 |
84 |
87 |
97 |
98 | `;
99 |
--------------------------------------------------------------------------------
/src/components/PageURL.test.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import renderer from "react-test-renderer";
3 | import "jest-styled-components";
4 | import { MemoryRouter } from "react-router-dom";
5 |
6 | import RoutedPageURL, { PageURL, createRandomString } from "./PageURL";
7 |
8 | describe("PageURL", () => {
9 | // Normally a functional component can just be tested with a snapshot
10 | // but here that doesn't work, the Link inside PageURL needs to be in
11 | // the react-router-dom context.
12 | it("throws error when rendered without router context", () => {
13 | console.error = jest.fn(); // removes error log from test output
14 | const shouldThrow = () =>
15 | renderer.create();
16 | expect(shouldThrow).toThrowError();
17 | });
18 |
19 | it("matches snapshot", () => {
20 | // The default nextUrl prop is set to create a random string.
21 | // This will break the snapshots everytime therefor random data
22 | // should never be used with snapshots.
23 | // Snapshot expect predictable behaviour.
24 | const nextUrl = "next_url";
25 | // Add the MemoryRouter with an initial state to inject the
26 | // react-router-dom context the Link component needs.
27 | const tree = renderer.create(
28 |
29 |
30 |
31 | );
32 | expect(tree).toMatchSnapshot();
33 | });
34 |
35 | // Test the createRandomString generator seperatly to see if it
36 | // returns random strings as expected.
37 | describe("createRandomString", () => {
38 | it("returns random strings", () => {
39 | const val1 = createRandomString();
40 | expect(typeof val1).toBe("string");
41 |
42 | const val2 = createRandomString();
43 | expect(typeof val2).toBe("string");
44 |
45 | expect(val1).not.toBe(val2);
46 | });
47 |
48 | it("returns string of length 5", () => {
49 | const val1 = createRandomString();
50 | expect(typeof val1).toBe("string");
51 | expect(val1).toHaveLength(5);
52 | });
53 | });
54 | });
55 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Testing your React App with Jest 🃏
2 |
3 | The following scenarios will be covered:
4 |
5 | ### Basic
6 |
7 | 1. Test an interaction with a DOM element that updates state. [[`Counter`](https://github.com/mikeheddes/react-testing-demo/blob/master/src/components/Counter.test.jsx)]
8 | 1. Test a component wrapped in a [HOC](https://reactjs.org/docs/higher-order-components.html) (e.g. [Redux](https://redux.js.org/)' mapState). [[`ScreenWidth`](https://github.com/mikeheddes/react-testing-demo/blob/master/src/components/ScreenWidth.test.jsx), [`PageURL`](https://github.com/mikeheddes/react-testing-demo/blob/master/src/components/PageURL.test.jsx)]
9 | 1. Create [snapshots](https://jestjs.io/docs/en/snapshot-testing) for the above. [[`All component tests`](https://github.com/mikeheddes/react-testing-demo/blob/master/src/components)]
10 |
11 | ### Advanced
12 |
13 | 4. Test a component that is wrapped in the [React context](https://reactjs.org/docs/context.html). [[`PageURL`](https://github.com/mikeheddes/react-testing-demo/blob/master/src/components/PageURL.test.jsx)]
14 | 1. Test a [React ref](https://reactjs.org/docs/refs-and-the-dom.html) attached to a DOM node. [[`Reminder`](https://github.com/mikeheddes/react-testing-demo/blob/test-solutions/src/components/Reminder.test.jsx)]
15 | 1. Test code that uses browser global variables. [[`ScreenWidth`](https://github.com/mikeheddes/react-testing-demo/blob/master/src/components/ScreenWidth.test.jsx)]
16 |
17 | ### Project structure
18 |
19 | The components and tests are in [`./src/components`](https://github.com/mikeheddes/react-testing-demo/tree/master/src/components).
20 | The tests for `Component.jsx` are in `Component.test.jsx`.
21 |
22 | ## Available Scripts
23 |
24 | In the project directory, you can run:
25 |
26 | ### `npm start`
27 |
28 | Runs the app in the development mode.
29 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
30 |
31 | The page will reload if you make edits.
32 | You will also see any lint errors in the console.
33 |
34 | ### `npm test`
35 |
36 | Runs the tests in watch mode.
37 |
38 | ### `npm run test:watch`
39 |
40 | Runs the tests in watch mode and shows the test coverage.
41 |
--------------------------------------------------------------------------------
/src/components/Reminder.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import styled from "styled-components";
3 |
4 | import { Wrapper, Label, Button, Line } from "./Other";
5 | import Item from "./ReminderItem";
6 |
7 | const Input = styled(Label)`
8 | text-align: left;
9 | appearance: none;
10 | border: none;
11 | outline: none;
12 | background-color: transparent;
13 | flex: 1 1 auto;
14 | min-width: 0px;
15 |
16 | &::placeholder {
17 | color: white;
18 | opacity: 0.7;
19 | }
20 | `;
21 |
22 | const ToDoWrapper = styled(Wrapper)`
23 | display: flex;
24 | flex-direction: row;
25 | `;
26 |
27 | class Reminder extends Component {
28 | input = React.createRef();
29 |
30 | state = { reminders: ["Buy an apple"], value: "" };
31 |
32 | addReminder = () => {
33 | const label = this.state.value;
34 | if (!label) return;
35 | if (this.state.reminders.indexOf(label) !== -1) return;
36 | this.setState({
37 | reminders: [...this.state.reminders, label],
38 | value: ""
39 | });
40 | };
41 |
42 | removeReminder = i => {
43 | const reminders = [...this.state.reminders];
44 | reminders.splice(i, 1);
45 | this.setState({ reminders });
46 | };
47 |
48 | focusInput = () => {
49 | this.input.current.focus();
50 | };
51 |
52 | handleInputChange = e => {
53 | this.setState({ value: e.target.value });
54 | };
55 |
56 | handleKeyPress = e => {
57 | if (e.key === "Enter") {
58 | this.addReminder();
59 | }
60 | };
61 |
62 | render() {
63 | const { reminders, value } = this.state;
64 | return (
65 | <>
66 |
67 |
77 |
78 |
79 |
80 |
81 | {reminders.map((reminder, i) => (
82 | - this.removeReminder(i)} key={reminder}>
83 | {reminder}
84 |
85 | ))}
86 | >
87 | );
88 | }
89 | }
90 |
91 | export default Reminder;
92 |
--------------------------------------------------------------------------------
/src/components/ScreenWidth.test.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 | import renderer from "react-test-renderer";
4 | import "jest-styled-components";
5 |
6 | import HOCScreenWidth, { ScreenWidth } from "./ScreenWidth";
7 |
8 | let attachedEvents = {};
9 |
10 | describe("", () => {
11 | beforeEach(() => {
12 | attachedEvents = {};
13 | global.window.addEventListener = jest.fn((event, cb) => {
14 | attachedEvents[event] = cb;
15 | });
16 |
17 | global.window.removeEventListener = jest.fn((event, cb) => {
18 | delete attachedEvents[event];
19 | });
20 |
21 | global.requestAnimationFrame = jest.fn(cb => {
22 | cb();
23 | });
24 |
25 | });
26 |
27 | it("provide the window.innerWidth as width prop", () => {
28 | global.window.innerWidth = 456;
29 | const wrapper = shallow();
30 | expect(wrapper.find(ScreenWidth).prop("width")).toBe(456);
31 | });
32 |
33 | it("attaches and removes resize eventListener", () => {
34 | const wrapper = shallow();
35 | expect(typeof attachedEvents.resize).toBe('function')
36 | wrapper.unmount();
37 | expect(attachedEvents.resize).toBeUndefined()
38 | });
39 |
40 | it("update the width on resize event", () => {
41 | global.window.innerWidth = 567;
42 |
43 | const wrapper = shallow();
44 | expect(wrapper.find(ScreenWidth).prop("width")).toBe(567);
45 |
46 | global.window.innerWidth = 678;
47 | attachedEvents.resize();
48 | wrapper.update();
49 |
50 | expect(wrapper.find(ScreenWidth).prop("width")).toBe(678);
51 |
52 | wrapper.unmount();
53 | expect(attachedEvents.resize).toBeUndefined()
54 | });
55 |
56 | it("ignore resize event if still updating", () => {
57 | global.window.innerWidth = 567;
58 |
59 | const wrapper = shallow();
60 | expect(wrapper.find(ScreenWidth).prop("width")).toBe(567);
61 |
62 | wrapper.instance().updating = true;
63 |
64 | global.window.innerWidth = 678;
65 | attachedEvents.resize();
66 | wrapper.update();
67 |
68 | expect(wrapper.find(ScreenWidth).prop("width")).toBe(567);
69 | });
70 |
71 | it("should match snapshot", () => {
72 | const tree = renderer.create();
73 | expect(tree).toMatchSnapshot();
74 | });
75 | });
76 |
--------------------------------------------------------------------------------
/src/components/Reminder.test.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { mount } from "enzyme";
3 | import "jest-styled-components";
4 |
5 | import Reminder from "./Reminder";
6 |
7 | describe("", () => {
8 | it("user can add a reminder with button", () => {
9 | const wrapper = mount();
10 |
11 | const input = wrapper.find("input");
12 | input.simulate("change", { target: { value: "Write more tests" } });
13 |
14 | expect(wrapper.state("value")).toBe("Write more tests");
15 |
16 | const submit = wrapper.find("button").first();
17 | submit.simulate("click");
18 |
19 | expect(wrapper.state("reminders")).toContain("Write more tests");
20 | });
21 |
22 | it("user can add a reminder with ENTER key", () => {
23 | const wrapper = mount();
24 |
25 | const input = wrapper.find("input");
26 | input.simulate("change", { target: { value: "Write more tests" } });
27 |
28 | expect(wrapper.state("value")).toBe("Write more tests");
29 |
30 | input.simulate("keyPress", { key: "r" });
31 | expect(wrapper.state("reminders")).not.toContain("Write more tests");
32 |
33 | input.simulate("keyPress", { key: "Enter" });
34 | expect(wrapper.state("reminders")).toContain("Write more tests");
35 | });
36 |
37 | it("does not allow empty reminders to be submited", () => {
38 | const wrapper = mount();
39 |
40 | const initialLength = wrapper.find("ToDoItem").length;
41 |
42 | const submit = wrapper.find("button").first();
43 | submit.simulate("click");
44 |
45 | const nextLength = wrapper.find("ToDoItem").length;
46 | expect(initialLength).toBe(nextLength);
47 | });
48 |
49 | it("does not allow duplicate reminders", () => {
50 | const wrapper = mount();
51 |
52 | const addItem = () => {
53 | wrapper.setState({ value: "Some reminder" });
54 | wrapper.instance().addReminder();
55 | wrapper.update()
56 | };
57 |
58 | addItem();
59 | const firstLength = wrapper.find("ToDoItem").length;
60 |
61 | addItem();
62 | const nextLength = wrapper.find("ToDoItem").length;
63 |
64 | expect(nextLength).toBe(firstLength);
65 | });
66 |
67 | it("allows the user to remove a reminder", () => {
68 | const wrapper = mount();
69 |
70 | expect(wrapper.find("ToDoItem")).toHaveLength(1);
71 |
72 | const FirstTodoItem = wrapper.find("ToDoItem");
73 | const removeButton = FirstTodoItem.find("button");
74 | removeButton.simulate("click");
75 |
76 | expect(wrapper.find("ToDoItem")).toHaveLength(0);
77 | });
78 |
79 | it("focus the input when the mouse enters", () => {
80 | const wrapper = mount();
81 |
82 | const inputElement = wrapper.find("input");
83 | const { input } = wrapper.instance();
84 |
85 | jest.spyOn(input.current, "focus");
86 |
87 | inputElement.simulate("mouseEnter");
88 |
89 | expect(input.current.focus).toHaveBeenCalled();
90 | });
91 | });
92 |
--------------------------------------------------------------------------------
/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read http://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit http://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl)
104 | .then(response => {
105 | // Ensure service worker exists, and that we really are getting a JS file.
106 | const contentType = response.headers.get('content-type');
107 | if (
108 | response.status === 404 ||
109 | (contentType != null && contentType.indexOf('javascript') === -1)
110 | ) {
111 | // No service worker found. Probably a different app. Reload the page.
112 | navigator.serviceWorker.ready.then(registration => {
113 | registration.unregister().then(() => {
114 | window.location.reload();
115 | });
116 | });
117 | } else {
118 | // Service worker found. Proceed as normal.
119 | registerValidSW(swUrl, config);
120 | }
121 | })
122 | .catch(() => {
123 | console.log(
124 | 'No internet connection found. App is running in offline mode.'
125 | );
126 | });
127 | }
128 |
129 | export function unregister() {
130 | if ('serviceWorker' in navigator) {
131 | navigator.serviceWorker.ready.then(registration => {
132 | registration.unregister();
133 | });
134 | }
135 | }
136 |
--------------------------------------------------------------------------------