├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.png └── index.html └── src ├── components ├── App │ ├── App.css │ ├── App.jsx │ ├── App.spec.js │ └── __snapshots__ │ │ └── App.spec.js.snap ├── Calculator │ ├── Calculator.css │ ├── Calculator.jsx │ ├── Calculator.spec.js │ └── __snapshots__ │ │ └── Calculator.spec.js.snap ├── Display │ ├── Display.css │ ├── Display.jsx │ ├── Display.spec.js │ └── __snapshots__ │ │ └── Display.spec.js.snap ├── Key │ ├── Key.css │ ├── Key.jsx │ ├── Key.spec.js │ └── __snapshots__ │ │ └── Key.spec.js.snap └── Keypad │ ├── Keypad.css │ ├── Keypad.jsx │ ├── Keypad.spec.js │ └── __snapshots__ │ └── Keypad.spec.js.snap ├── index.css ├── index.js └── setupTests.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TDD React 2 | 3 | A simple Calculator app created for a blog post at [testdriven.io](https://testdriven.io/blog/tdd-with-react-jest-and-enzyme-part-one/). 4 | 5 | ## Local Setup 6 | 7 | ```sh 8 | $ git clone git@github.com:calebpollman/react-calculator.git 9 | ``` 10 | 11 | ```sh 12 | $ cd react-calculator 13 | ``` 14 | 15 | ```sh 16 | $ npm install 17 | ``` 18 | 19 | ## Run Locally 20 | 21 | ```sh 22 | $ npm start 23 | ``` 24 | 25 | ## Run Tests 26 | 27 | ```sh 28 | $ npm test 29 | ``` 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tdd-react-calculator", 3 | "author": "caleb pollman", 4 | "version": "0.1.0", 5 | "private": true, 6 | "engines": { 7 | "node": "14.16.1", 8 | "npm": "6.14.12" 9 | }, 10 | "dependencies": { 11 | "react": "^17.0.2", 12 | "react-dom": "^17.0.2" 13 | }, 14 | "devDependencies": { 15 | "@wojtekmaj/enzyme-adapter-react-17": "^0.6.1", 16 | "enzyme": "^3.11.0", 17 | "enzyme-to-json": "^3.6.2", 18 | "prettier": "^2.2.1", 19 | "react-scripts": "4.0.3", 20 | "react-test-renderer": "^17.0.2" 21 | }, 22 | "scripts": { 23 | "start": "react-scripts start", 24 | "build": "react-scripts build", 25 | "test": "react-scripts test --env=jsdom", 26 | "eject": "react-scripts eject" 27 | }, 28 | "browserslist": [ 29 | ">0.2%", 30 | "not dead", 31 | "not ie <= 11", 32 | "not op_mini all" 33 | ], 34 | "jest": { 35 | "snapshotSerializers": [ 36 | "enzyme-to-json/serializer" 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/calebpollman/react-calculator/f6b109f2c2b194c8881047e918516282edd40a11/public/favicon.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Calcutronic 5000 10 | 11 | 12 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /src/components/App/App.css: -------------------------------------------------------------------------------- 1 | .app-container { 2 | align-items: center; 3 | display: flex; 4 | height: 100vh; 5 | justify-content: center; 6 | width: 100vw; 7 | } 8 | -------------------------------------------------------------------------------- /src/components/App/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Calculator from '../Calculator/Calculator'; 3 | import './App.css'; 4 | 5 | const App = () => ( 6 |
7 | 8 |
9 | ); 10 | 11 | export default App; 12 | -------------------------------------------------------------------------------- /src/components/App/App.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import App from './App'; 4 | import Calculator from '../Calculator/Calculator'; 5 | 6 | describe('App', () => { 7 | let wrapper; 8 | beforeEach(() => (wrapper = shallow())); 9 | 10 | it('should render correctly', () => expect(wrapper).toMatchSnapshot()); 11 | 12 | it('should render a
', () => { 13 | expect(wrapper.find('div').length).toEqual(1); 14 | }); 15 | 16 | it('should render the Calculator Component', () => { 17 | expect(wrapper.containsMatchingElement()).toEqual(true); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/components/App/__snapshots__/App.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`App should render correctly 1`] = ` 4 |
7 | 8 |
9 | `; 10 | -------------------------------------------------------------------------------- /src/components/Calculator/Calculator.css: -------------------------------------------------------------------------------- 1 | .calculator-container { 2 | background-color: var(--calculator-background-color); 3 | height: var(--calculator-height); 4 | width: var(--calculator-width); 5 | } 6 | -------------------------------------------------------------------------------- /src/components/Calculator/Calculator.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import './Calculator.css'; 3 | 4 | import Display from '../Display/Display'; 5 | import Keypad from '../Keypad/Keypad'; 6 | import './Calculator.css'; 7 | 8 | class Calculator extends Component { 9 | state = { 10 | displayValue: '0', 11 | numbers: ['9', '8', '7', '6', '5', '4', '3', '2', '1', '.', '0', 'ce'], 12 | operators: ['/', 'x', '-', '+'], 13 | selectedOperator: '', 14 | storedValue: '', 15 | }; 16 | 17 | componentDidMount = () => { 18 | document.addEventListener('keydown', this.handleKeyPress); 19 | }; 20 | 21 | componentWillUnmount = () => { 22 | document.removeEventListener('keydown', this.handleKeyPress); 23 | }; 24 | 25 | callOperator = () => { 26 | let { displayValue, selectedOperator, storedValue } = this.state; 27 | const updateStoredValue = displayValue; 28 | 29 | displayValue = parseInt(displayValue, 10); 30 | storedValue = parseInt(storedValue, 10); 31 | 32 | switch (selectedOperator) { 33 | case '+': 34 | displayValue = storedValue + displayValue; 35 | break; 36 | case '-': 37 | displayValue = storedValue - displayValue; 38 | break; 39 | case 'x': 40 | displayValue = storedValue * displayValue; 41 | break; 42 | case '/': 43 | displayValue = storedValue / displayValue; 44 | break; 45 | default: 46 | displayValue = '0'; 47 | } 48 | 49 | displayValue = displayValue.toString(); 50 | selectedOperator = ''; 51 | if (displayValue === 'NaN' || displayValue === 'Infinity') displayValue = '0'; 52 | 53 | this.setState({ displayValue, selectedOperator, storedValue: updateStoredValue }); 54 | }; 55 | 56 | handleKeyPress = (event) => { 57 | const { numbers, operators } = this.state; 58 | 59 | if (event.key === 'Backspace') this.updateDisplay('ce'); 60 | if (event.key === 'Enter' || event.key === '=') this.callOperator(); 61 | 62 | numbers.forEach((number) => { 63 | if (event.key === number) this.updateDisplay(number); 64 | }); 65 | 66 | operators.forEach((operator) => { 67 | if (event.key === operator) this.setOperator(operator); 68 | }); 69 | }; 70 | 71 | setOperator = (value) => { 72 | let { displayValue, selectedOperator, storedValue } = this.state; 73 | 74 | if (selectedOperator === '') { 75 | storedValue = displayValue; 76 | displayValue = '0'; 77 | selectedOperator = value; 78 | } else { 79 | selectedOperator = value; 80 | } 81 | 82 | this.setState({ displayValue, selectedOperator, storedValue }); 83 | }; 84 | 85 | updateDisplay = (value) => { 86 | let { displayValue } = this.state; 87 | 88 | if (value === '.' && displayValue.includes('.')) value = ''; 89 | 90 | if (value === 'ce') { 91 | displayValue = displayValue.substr(0, displayValue.length - 1); 92 | if (displayValue === '') displayValue = '0'; 93 | } else { 94 | displayValue === '0' ? (displayValue = value) : (displayValue += value); 95 | } 96 | 97 | this.setState({ displayValue }); 98 | }; 99 | 100 | render() { 101 | const { displayValue, numbers, operators } = this.state; 102 | 103 | return ( 104 |
105 | 106 | 114 |
115 | ); 116 | } 117 | } 118 | 119 | export default Calculator; 120 | -------------------------------------------------------------------------------- /src/components/Calculator/Calculator.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount, shallow } from 'enzyme'; 3 | import Calculator from './Calculator'; 4 | import Display from '../Display/Display'; 5 | import Keypad from '../Keypad/Keypad'; 6 | 7 | describe('Calculator', () => { 8 | let wrapper; 9 | beforeEach(() => (wrapper = shallow())); 10 | 11 | it('should render correctly', () => expect(wrapper).toMatchSnapshot()); 12 | 13 | it('should render a
', () => { 14 | expect(wrapper.find('div').length).toEqual(1); 15 | }); 16 | 17 | it('should render the Display and Keypad Components', () => { 18 | expect( 19 | wrapper.containsAllMatchingElements([ 20 | , 21 | , 29 | ]) 30 | ).toEqual(true); 31 | }); 32 | 33 | describe('updateDisplay', () => { 34 | it('updates displayValue', () => { 35 | wrapper.instance().updateDisplay('5'); 36 | expect(wrapper.state('displayValue')).toEqual('5'); 37 | }); 38 | 39 | it('concatenates displayValue', () => { 40 | wrapper.instance().updateDisplay('5'); 41 | wrapper.instance().updateDisplay('0'); 42 | expect(wrapper.state('displayValue')).toEqual('50'); 43 | }); 44 | 45 | it('removes leading "0" from displayValue', () => { 46 | wrapper.instance().updateDisplay('0'); 47 | expect(wrapper.state('displayValue')).toEqual('0'); 48 | wrapper.instance().updateDisplay('5'); 49 | expect(wrapper.state('displayValue')).toEqual('5'); 50 | }); 51 | 52 | it('prevents multiple leading "0"s from displayValue', () => { 53 | wrapper.instance().updateDisplay('0'); 54 | wrapper.instance().updateDisplay('0'); 55 | expect(wrapper.state('displayValue')).toEqual('0'); 56 | }); 57 | 58 | it('removes last char of displayValue', () => { 59 | wrapper.instance().updateDisplay('5'); 60 | wrapper.instance().updateDisplay('0'); 61 | wrapper.instance().updateDisplay('ce'); 62 | expect(wrapper.state('displayValue')).toEqual('5'); 63 | }); 64 | 65 | it('prevents multiple instances of "." in displayValue', () => { 66 | wrapper.instance().updateDisplay('.'); 67 | wrapper.instance().updateDisplay('.'); 68 | expect(wrapper.state('displayValue')).toEqual('.'); 69 | }); 70 | 71 | it('will set displayValue to "0" if displayValue is equal to an empty string', () => { 72 | wrapper.instance().updateDisplay('ce'); 73 | expect(wrapper.state('displayValue')).toEqual('0'); 74 | }); 75 | }); 76 | 77 | describe('setOperator', () => { 78 | it('updates the value of selectedOperator', () => { 79 | wrapper.instance().setOperator('+'); 80 | expect(wrapper.state('selectedOperator')).toEqual('+'); 81 | wrapper.instance().setOperator('/'); 82 | expect(wrapper.state('selectedOperator')).toEqual('/'); 83 | }); 84 | 85 | it('updates the value of storedValue to the value of displayValue', () => { 86 | wrapper.setState({ displayValue: '5' }); 87 | wrapper.instance().setOperator('+'); 88 | expect(wrapper.state('storedValue')).toEqual('5'); 89 | }); 90 | 91 | it('updates the value of displayValue to "0"', () => { 92 | wrapper.setState({ displayValue: '5' }); 93 | wrapper.instance().setOperator('+'); 94 | expect(wrapper.state('displayValue')).toEqual('0'); 95 | }); 96 | 97 | it('selectedOperator is not an empty string, does not update storedValue', () => { 98 | wrapper.setState({ displayValue: '5' }); 99 | wrapper.instance().setOperator('+'); 100 | expect(wrapper.state('storedValue')).toEqual('5'); 101 | wrapper.instance().setOperator('-'); 102 | expect(wrapper.state('storedValue')).toEqual('5'); 103 | }); 104 | }); 105 | 106 | describe('callOperator', () => { 107 | it('updates displayValue to the sum of storedValue and displayValue', () => { 108 | wrapper.setState({ storedValue: '3' }); 109 | wrapper.setState({ displayValue: '2' }); 110 | wrapper.setState({ selectedOperator: '+' }); 111 | wrapper.instance().callOperator(); 112 | expect(wrapper.state('displayValue')).toEqual('5'); 113 | }); 114 | 115 | it('updates displayValue to the difference of storedValue and displayValue', () => { 116 | wrapper.setState({ storedValue: '3' }); 117 | wrapper.setState({ displayValue: '2' }); 118 | wrapper.setState({ selectedOperator: '-' }); 119 | wrapper.instance().callOperator(); 120 | expect(wrapper.state('displayValue')).toEqual('1'); 121 | }); 122 | 123 | it('updates displayValue to the product of storedValue and displayValue', () => { 124 | wrapper.setState({ storedValue: '3' }); 125 | wrapper.setState({ displayValue: '2' }); 126 | wrapper.setState({ selectedOperator: 'x' }); 127 | wrapper.instance().callOperator(); 128 | expect(wrapper.state('displayValue')).toEqual('6'); 129 | }); 130 | 131 | it('updates displayValue to the quotient of storedValue and displayValue', () => { 132 | wrapper.setState({ storedValue: '3' }); 133 | wrapper.setState({ displayValue: '2' }); 134 | wrapper.setState({ selectedOperator: '/' }); 135 | wrapper.instance().callOperator(); 136 | expect(wrapper.state('displayValue')).toEqual('1.5'); 137 | }); 138 | 139 | it('updates displayValue to "0" if operation results in "NaN"', () => { 140 | wrapper.setState({ storedValue: '3' }); 141 | wrapper.setState({ displayValue: 'string' }); 142 | wrapper.setState({ selectedOperator: '/' }); 143 | wrapper.instance().callOperator(); 144 | expect(wrapper.state('displayValue')).toEqual('0'); 145 | }); 146 | 147 | it('updates displayValue to "0" if operation results in "Infinity"', () => { 148 | wrapper.setState({ storedValue: '7' }); 149 | wrapper.setState({ displayValue: '0' }); 150 | wrapper.setState({ selectedOperator: '/' }); 151 | wrapper.instance().callOperator(); 152 | expect(wrapper.state('displayValue')).toEqual('0'); 153 | }); 154 | 155 | it('updates displayValue to "0" if selectedOperator does not match cases', () => { 156 | wrapper.setState({ storedValue: '7' }); 157 | wrapper.setState({ displayValue: '10' }); 158 | wrapper.setState({ selectedOperator: 'string' }); 159 | wrapper.instance().callOperator(); 160 | expect(wrapper.state('displayValue')).toEqual('0'); 161 | }); 162 | 163 | it('updates displayValue to "0" if called with no value for storedValue or selectedOperator', () => { 164 | wrapper.setState({ storedValue: '' }); 165 | wrapper.setState({ displayValue: '10' }); 166 | wrapper.setState({ selectedOperator: '' }); 167 | wrapper.instance().callOperator(); 168 | expect(wrapper.state('displayValue')).toEqual('0'); 169 | }); 170 | }); 171 | }); 172 | 173 | describe('mounted Calculator', () => { 174 | let wrapper; 175 | beforeEach(() => (wrapper = mount())); 176 | 177 | it('calls updateDisplay when a number key is clicked', () => { 178 | const spy = jest.spyOn(wrapper.instance(), 'updateDisplay'); 179 | wrapper.instance().forceUpdate(); 180 | expect(spy).toHaveBeenCalledTimes(0); 181 | wrapper 182 | .find('.number-key') 183 | .first() 184 | .simulate('click'); 185 | expect(spy).toHaveBeenCalledTimes(1); 186 | }); 187 | 188 | it('calls setOperator when an operator key is clicked', () => { 189 | const spy = jest.spyOn(wrapper.instance(), 'setOperator'); 190 | wrapper.instance().forceUpdate(); 191 | expect(spy).toHaveBeenCalledTimes(0); 192 | wrapper 193 | .find('.operator-key') 194 | .first() 195 | .simulate('click'); 196 | expect(spy).toHaveBeenCalledTimes(1); 197 | }); 198 | 199 | it('calls callOperator when the submit key is clicked', () => { 200 | const spy = jest.spyOn(wrapper.instance(), 'callOperator'); 201 | wrapper.instance().forceUpdate(); 202 | expect(spy).toHaveBeenCalledTimes(0); 203 | wrapper.find('.submit-key').simulate('click'); 204 | expect(spy).toHaveBeenCalledTimes(1); 205 | }); 206 | }); 207 | -------------------------------------------------------------------------------- /src/components/Calculator/__snapshots__/Calculator.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Calculator should render correctly 1`] = ` 4 |
7 | 10 | 40 |
41 | `; 42 | -------------------------------------------------------------------------------- /src/components/Display/Display.css: -------------------------------------------------------------------------------- 1 | .display-container { 2 | align-items: center; 3 | background: var(--display-background-color); 4 | display: flex; 5 | height: var(--display-height); 6 | padding: 0 4%; 7 | width: var(--display-width); 8 | } 9 | 10 | .display-value { 11 | color: var(--display-text-color); 12 | font-size: var(--display-text-size); 13 | font-family: var(--main-font); 14 | font-weight: var(--display-text-weight); 15 | margin-left: auto; 16 | overflow: hidden; 17 | } 18 | -------------------------------------------------------------------------------- /src/components/Display/Display.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import './Display.css'; 4 | 5 | const Display = ({ displayValue }) => ( 6 |
7 |

{displayValue}

8 |
9 | ); 10 | 11 | Display.propTypes = { displayValue: PropTypes.string.isRequired }; 12 | 13 | Display.defaultProps = { displayValue: 'default' }; 14 | 15 | export default Display; 16 | -------------------------------------------------------------------------------- /src/components/Display/Display.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import Display from './Display'; 4 | 5 | const MOCK_DISPLAY_VALUE = 'a display value'; 6 | 7 | describe('Display', () => { 8 | let wrapper; 9 | beforeEach(() => (wrapper = shallow())); 10 | 11 | it('should render correctly', () => expect(wrapper).toMatchSnapshot()); 12 | 13 | it('should render a
', () => { 14 | expect(wrapper.find('div').length).toEqual(1); 15 | }); 16 | 17 | it('renders the value of displayValue', () => { 18 | expect(wrapper.text()).toEqual(MOCK_DISPLAY_VALUE); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/components/Display/__snapshots__/Display.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Display should render correctly 1`] = ` 4 |
7 |

10 | a display value 11 |

12 |
13 | `; 14 | -------------------------------------------------------------------------------- /src/components/Key/Key.css: -------------------------------------------------------------------------------- 1 | .key-container { 2 | align-items: center; 3 | display: flex; 4 | height: 25%; 5 | justify-content: center; 6 | transition: background-color 0.3s linear; 7 | } 8 | 9 | .key-container:hover { 10 | cursor: pointer; 11 | } 12 | 13 | .operator-key { 14 | background-color: var(--action-key-color); 15 | width: 100%; 16 | } 17 | 18 | .operator-key:hover { 19 | background-color: var(--action-key-color-hover); 20 | } 21 | 22 | .number-key { 23 | background-color: var(--number-key-color); 24 | width: calc(100% / 3); 25 | } 26 | 27 | .number-key:hover { 28 | background-color: var(--number-key-color-hover); 29 | } 30 | 31 | .submit-key { 32 | background-color: var(--submit-key-color); 33 | height: 100%; 34 | width: 100%; 35 | } 36 | 37 | .submit-key:hover { 38 | background-color: var(--submit-key-color-hover); 39 | } 40 | 41 | .key-value { 42 | color: var(--key-text-color); 43 | font-family: var(--main-font); 44 | font-size: var(--key-text-size); 45 | font-weight: var(--key-text-weight); 46 | } 47 | -------------------------------------------------------------------------------- /src/components/Key/Key.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import './Key.css'; 4 | 5 | const Key = ({ handleKeyPress, keyAction, keyType, keyValue }) => { 6 | const keyClass = `key-container ${keyType}`; 7 | 8 | return ( 9 |
keyAction(keyValue)} onKeyPress={event => handleKeyPress(event)}> 10 |

{keyValue}

11 |
12 | ); 13 | }; 14 | 15 | Key.propTypes = { 16 | handleKeyPress: PropTypes.func.isRequired, 17 | keyAction: PropTypes.func.isRequired, 18 | keyType: PropTypes.string.isRequired, 19 | keyValue: PropTypes.string.isRequired, 20 | }; 21 | 22 | Key.defaultProps = { 23 | keyType: 'default', 24 | keyAction: 'default', 25 | }; 26 | 27 | export default Key; 28 | -------------------------------------------------------------------------------- /src/components/Key/Key.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import Key from './Key'; 4 | 5 | const MOCK_HANDLE_KEY_PRESS = jest.fn(); 6 | const MOCK_KEY_ACTION = jest.fn(); 7 | const MOCK_KEY_TYPE = 'number-key'; 8 | const MOCK_KEY_VALUE = '7'; 9 | 10 | describe('Key', () => { 11 | let wrapper; 12 | beforeEach(() => { 13 | wrapper = shallow( 14 | 20 | ); 21 | }); 22 | 23 | it('should render correctly', () => expect(wrapper).toMatchSnapshot()); 24 | 25 | it('should render a
', () => { 26 | expect(wrapper.find('div').length).toEqual(1); 27 | }); 28 | 29 | it('should render the value of keyValue', () => { 30 | wrapper.setProps({ keyValue: 'test' }); 31 | expect(wrapper.text()).toEqual('test'); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/components/Key/__snapshots__/Key.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Key should render correctly 1`] = ` 4 |
9 |

12 | 7 13 |

14 |
15 | `; 16 | -------------------------------------------------------------------------------- /src/components/Keypad/Keypad.css: -------------------------------------------------------------------------------- 1 | .keypad-container { 2 | display: flex; 3 | flex-direction: row; 4 | flex-wrap: wrap; 5 | height: var(--keypad-height); 6 | padding: 2%; 7 | width: var(--keypad-width); 8 | } 9 | 10 | .numbers-container { 11 | display: flex; 12 | flex-direction: row; 13 | flex-wrap: wrap; 14 | height: 80%; 15 | width: 75%; 16 | } 17 | 18 | .operators-container { 19 | display: flex; 20 | flex-direction: column; 21 | height: 80%; 22 | width: 25%; 23 | } 24 | 25 | .submit-container { 26 | height: 20%; 27 | width: 100%; 28 | } 29 | -------------------------------------------------------------------------------- /src/components/Keypad/Keypad.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import './Keypad.css'; 4 | 5 | import Key from '../Key/Key'; 6 | import './Keypad.css'; 7 | 8 | const Keypad = ({ operators, callOperator, handleKeyPress, numbers, setOperator, updateDisplay }) => { 9 | const numberKeys = numbers.map((number, iterator) => ( 10 | 17 | )); 18 | 19 | const operatorKeys = operators.map((operator, iterator) => ( 20 | 27 | )); 28 | 29 | return ( 30 |
31 |
{numberKeys}
32 |
{operatorKeys}
33 |
34 | 35 |
36 |
37 | ); 38 | }; 39 | 40 | Keypad.propTypes = { 41 | callOperator: PropTypes.func.isRequired, 42 | handleKeyPress: PropTypes.func.isRequired, 43 | numbers: PropTypes.array.isRequired, 44 | operators: PropTypes.array.isRequired, 45 | setOperator: PropTypes.func.isRequired, 46 | updateDisplay: PropTypes.func.isRequired, 47 | }; 48 | 49 | Keypad.defaultProps = { 50 | numbers: [], 51 | operators: [], 52 | }; 53 | 54 | export default Keypad; 55 | -------------------------------------------------------------------------------- /src/components/Keypad/Keypad.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount, shallow } from 'enzyme'; 3 | import Keypad from './Keypad'; 4 | 5 | const MOCK_CALL_OPERATOR = jest.fn(); 6 | const MOCK_HANDLE_KEY_PRESS = jest.fn(); 7 | const MOCK_NUMBERS = ['7', '13']; 8 | const MOCK_OPERATORS = ['+', '-']; 9 | const MOCK_SET_OPERATOR = jest.fn(); 10 | const MOCK_UPDATE_DISPLAY = jest.fn(); 11 | 12 | describe('Keypad', () => { 13 | let wrapper; 14 | beforeEach(() => { 15 | wrapper = shallow( 16 | 24 | ); 25 | }); 26 | 27 | it('should render correctly', () => expect(wrapper).toMatchSnapshot()); 28 | 29 | it("should render 4
's", () => { 30 | expect(wrapper.find('div').length).toEqual(4); 31 | }); 32 | 33 | it('should render an instance of the Key component for each index of the numbers and operators props, and the submit Key', () => { 34 | const submitKeyValue = 1; 35 | const keyTotal = MOCK_NUMBERS.length + MOCK_OPERATORS.length + submitKeyValue; 36 | expect(wrapper.find('Key').length).toEqual(keyTotal); 37 | }); 38 | }); 39 | 40 | describe('mounted Keypad', () => { 41 | let wrapper; 42 | beforeEach(() => { 43 | wrapper = mount( 44 | 52 | ); 53 | }); 54 | 55 | it('renders the values of the numbers prop to the DOM', () => { 56 | const numbers = MOCK_NUMBERS.join(''); 57 | expect(wrapper.find('.numbers-container').text()).toEqual(numbers); 58 | }); 59 | 60 | it('renders the values of the operators prop to the DOM', () => { 61 | const operators = MOCK_OPERATORS.join(''); 62 | expect(wrapper.find('.operators-container').text()).toEqual(operators); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /src/components/Keypad/__snapshots__/Keypad.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Keypad should render correctly 1`] = ` 4 |
7 |
10 | 17 | 24 |
25 |
28 | 35 | 42 |
43 |
46 | 52 |
53 |
54 | `; 55 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | /* 2 | app variables 3 | */ 4 | 5 | :root { 6 | /* background colors */ 7 | --action-key-color: #545454; 8 | --action-key-color-hover: #2a2a2a; 9 | --calculator-background-color: #696969; 10 | --display-background-color: #1d1f1f; 11 | --number-key-color: #696969; 12 | --number-key-color-hover: #3f3f3f; 13 | --submit-key-color: #d18800; 14 | --submit-key-color-hover: #aa6e00; 15 | 16 | /* font */ 17 | --main-font: 'Orbitron', sans-serif; 18 | 19 | /* font colors */ 20 | --display-text-color: #23e000; 21 | --key-text-color: #d3d3d3; 22 | 23 | /* font sizes */ 24 | --display-text-size: 4em; 25 | --key-text-size: 3em; 26 | 27 | /* font weights */ 28 | --display-text-weight: 400; 29 | --key-text-weight: 700; 30 | 31 | /* calculator dimensions */ 32 | --calculator-height: 72%; 33 | --calculator-width: 36%; 34 | 35 | /* display dimensions */ 36 | --display-height: 24%; 37 | --display-width: 92%; 38 | 39 | /* keypad dimensions */ 40 | --keypad-height: 72%; 41 | --keypad-width: 96%; 42 | } 43 | 44 | @media screen and (max-width: 1024px) { 45 | :root { 46 | /* font sizes */ 47 | --display-text-size: 6em; 48 | --key-text-size: 3em; 49 | 50 | /* calculator dimensions */ 51 | --calculator-height: 100%; 52 | --calculator-width: 100%; 53 | } 54 | } 55 | 56 | /* 57 | app CSS reset 58 | */ 59 | 60 | body, 61 | div, 62 | p { 63 | margin: 0; 64 | padding: 0; 65 | } 66 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './components/App/App'; 4 | import './index.css'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | import { configure } from 'enzyme'; 2 | import Adapter from '@wojtekmaj/enzyme-adapter-react-17'; 3 | 4 | configure({ adapter: new Adapter() }); 5 | --------------------------------------------------------------------------------