├── .dockerignore ├── .github └── workflows │ └── main.yml ├── .gitignore ├── Dockerfile ├── Dockerfile.calculator ├── LICENSE ├── Logotype primary.png ├── README.md ├── docker-compose.yml ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── screenshot.png ├── src ├── __tests__ │ ├── index.js │ └── utils.js ├── component │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── Button.css │ ├── Button.js │ ├── ButtonPanel.css │ ├── ButtonPanel.js │ ├── Display.css │ └── Display.js ├── index.css ├── index.js └── logic │ ├── calculate.js │ ├── calculate.test.js │ ├── isNumber.js │ └── operate.js └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .git 3 | .gitignore 4 | Dockerfile 5 | Dockerfile-calculator -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: 8 | push: 9 | branches: [ master ] 10 | pull_request: 11 | branches: [ master ] 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | # This workflow contains a single job called "build" 16 | build: 17 | # The type of runner that the job will run on 18 | runs-on: ubuntu-latest 19 | 20 | # Steps represent a sequence of tasks that will be executed as part of the job 21 | steps: 22 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 23 | - uses: actions/checkout@v2 24 | 25 | # Runs a single command using the runners shell 26 | - name: React Pinpoint Build 27 | run: docker-compose up --build --abort-on-container-exit 28 | 29 | # - name: React Pinpoint Stop 30 | # run: docker-compose down 31 | 32 | 33 | # # Runs a set of commands using the runners shell 34 | # - name: Run a multi-line script 35 | # run: | 36 | # echo Add other actions to build, 37 | # echo test, and deploy your project. 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://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 | npm-debug.log 15 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM buildkite/puppeteer:5.2.1 2 | 3 | WORKDIR /tests 4 | ADD . . 5 | 6 | COPY package-lock.json package.json ./ 7 | 8 | RUN npm ci 9 | CMD [ "node", "src/__tests__/index.js" ] -------------------------------------------------------------------------------- /Dockerfile.calculator: -------------------------------------------------------------------------------- 1 | FROM node:12-alpine 2 | 3 | WORKDIR /calc 4 | ADD . . 5 | COPY package-lock.json package.json ./ 6 | RUN npm ci 7 | CMD [ "npm", "start" ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Andrew H Farmer 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 | -------------------------------------------------------------------------------- /Logotype primary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/calculator/11632570d74bdc7200aa2d6699c91f974a405837/Logotype primary.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Calculator 2 | --- 3 | 4 | 5 | Created with *create-react-app*. See the [full create-react-app guide](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md). 6 | 7 | 8 | 9 | Try It 10 | --- 11 | 12 | [ahfarmer.github.io/calculator](https://ahfarmer.github.io/calculator/) 13 | 14 | 15 | 16 | Install 17 | --- 18 | 19 | `npm install` 20 | 21 | 22 | 23 | Usage 24 | --- 25 | 26 | `npm start` 27 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | tests: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | command: "wait-for-it.sh web:3000 -- npm test" 8 | links: 9 | - web 10 | web: 11 | build: 12 | context: . 13 | dockerfile: Dockerfile.calculator 14 | command: "npm start" 15 | tty: true 16 | expose: 17 | - "3000" 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "calculator", 3 | "version": "0.1.0", 4 | "license": "MIT", 5 | "homepage": "http://ahfarmer.github.io/calculator", 6 | "devDependencies": { 7 | "chai": "^4.2.0", 8 | "gh-pages": "^2.0.1", 9 | "prettier": "^1.17.1", 10 | "react-pinpoint": "0.0.1-beta.3", 11 | "react-scripts": "^3.0.1" 12 | }, 13 | "dependencies": { 14 | "big.js": "^5.2.2", 15 | "github-fork-ribbon-css": "^0.2.1", 16 | "puppeteer": "^5.2.1", 17 | "react": "^16.8.6", 18 | "react-dom": "^16.8.6" 19 | }, 20 | "scripts": { 21 | "start": "react-scripts start", 22 | "build": "react-scripts build", 23 | "test": "node ./src/__tests__/index.js", 24 | "eject": "react-scripts eject", 25 | "deploy": "gh-pages -d build" 26 | }, 27 | "prettier": { 28 | "trailingComma": "all" 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.2%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/calculator/11632570d74bdc7200aa2d6699c91f974a405837/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | React Calculator 9 | 10 | 11 | 14 |
15 | 19 | Fork me on GitHub 20 | 21 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Calculator", 3 | "name": "React Calculator Example App", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/calculator/11632570d74bdc7200aa2d6699c91f974a405837/screenshot.png -------------------------------------------------------------------------------- /src/__tests__/index.js: -------------------------------------------------------------------------------- 1 | console.log("yeah, this is the mvp!!"); 2 | 3 | const puppeteer = require("puppeteer"); 4 | const path = require("path"); 5 | 6 | (async () => { 7 | const browser = await puppeteer.launch({ 8 | args: ["--no-sandbox", "--disable-setuid-sandbox"], 9 | }); 10 | const page = await browser.newPage(); 11 | 12 | // Set an empty object on devtools hook so react will record fibers 13 | // Must exist before react runs 14 | await page.evaluateOnNewDocument( 15 | () => (window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {}), 16 | ); 17 | console.log("got here though"); 18 | 19 | await page.goto("http://web:3000/calculator"); 20 | 21 | console.log("we in boys"); 22 | 23 | // testing adding react-pinpoint via a script tag 24 | await page.addScriptTag({ 25 | path: path.join(__dirname, "utils.js"), 26 | }); 27 | 28 | await page.evaluate(() => { 29 | const root = document.querySelector("#root"); 30 | mountToReactRoot(root); 31 | }); 32 | 33 | await page.click("#yeah9"); 34 | await page.click("#yeah9"); 35 | await page.click("#yeah9"); 36 | 37 | const slowRenders = await page.evaluate(async () => { 38 | console.log("changes->", changes); 39 | console.log("slow renders->", getAllSlowComponentRenders(0, changes)); 40 | return getAllSlowComponentRenders(0, changes); 41 | }); 42 | 43 | console.log("YEAH->", slowRenders); 44 | 45 | /* tracing using chrome dev tools 46 | await page.tracing.start({path: 'trace.json'}); 47 | await page.goto('http://localhost:3000'); 48 | await page.click('#yeah9') 49 | await page.click('#yeah9') 50 | await page.click('#yeah9') 51 | await page.click('#yeah9') 52 | await page.click('#yeah9') 53 | await page.tracing.stop(); 54 | */ 55 | await browser.close(); 56 | process.exit(1); 57 | })(); 58 | -------------------------------------------------------------------------------- /src/__tests__/utils.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | /* eslint-disable consistent-return */ 3 | let changes = []; 4 | 5 | function recordChangesToObjField(obj, field) { 6 | Object.defineProperty(obj, field, { 7 | get() { 8 | return this._current; 9 | }, 10 | set(value) { 11 | changes.push(parseNode(value)); 12 | console.log(value.selfBaseDuration) 13 | this._current = value; 14 | }, 15 | }); 16 | } 17 | 18 | 19 | function parseNode(node) { 20 | return { 21 | type: node.type, 22 | selfBaseDuration: node.selfBaseDuration, 23 | child: node.child, 24 | sibling: node.sibling 25 | } 26 | } 27 | 28 | function parseCompletedNode(node) { 29 | return { 30 | type: node.type, 31 | selfBaseDuration: node.selfBaseDuration, 32 | } 33 | } 34 | 35 | 36 | function mountToReactRoot(reactRoot) { 37 | // Reset changes 38 | changes = []; 39 | 40 | // Lift parent of react fibers tree 41 | const parent = reactRoot._reactRootContainer._internalRoot; 42 | const current = parent.current; 43 | 44 | // Add listener to react fibers tree so changes can be recorded 45 | recordChangesToObjField(parent, 'current'); 46 | 47 | // Reassign react fibers tree to record initial state 48 | parent.current = current; 49 | return changes; 50 | } 51 | 52 | 53 | function traverseWith(fiber, callback) { 54 | callback(fiber); 55 | if (fiber.child) { 56 | traverseWith(fiber.child, callback); 57 | } 58 | if (fiber.sibling) { 59 | traverseWith(fiber.sibling, callback); 60 | } 61 | } 62 | 63 | 64 | /** 65 | * 66 | * @param {number} threshold The rendering time to filter for. 67 | */ 68 | function getAllSlowComponentRenders(threshold, changesArray) { 69 | const slowRenders = changesArray 70 | .map(flattenTree) // Flatten tree 71 | .flat() // Flatten 2d array into 1d array 72 | .filter((fiber) => checkTime(fiber, threshold)) // filter out all that don't meet threshold 73 | .map(parseCompletedNode) // removes circular references 74 | return slowRenders; 75 | } 76 | 77 | 78 | 79 | function flattenTree(tree) { 80 | // Closured array for storing fibers 81 | const arr = []; 82 | // Closured callback for adding to arr 83 | const callback = (fiber) => { 84 | arr.push(fiber); 85 | }; 86 | traverseWith(tree, callback); 87 | return arr; 88 | } 89 | 90 | 91 | 92 | function checkTime(fiber, threshold) { 93 | return fiber.selfBaseDuration > threshold; 94 | } 95 | 96 | 97 | 98 | // module.exports = { mountToReactRoot, getAllSlowComponentRenders, traverseWith }; 99 | -------------------------------------------------------------------------------- /src/component/App.css: -------------------------------------------------------------------------------- 1 | .component-app { 2 | display: flex; 3 | flex-direction: column; 4 | flex-wrap: wrap; 5 | height: 100%; 6 | } 7 | -------------------------------------------------------------------------------- /src/component/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Display from "./Display"; 3 | import ButtonPanel from "./ButtonPanel"; 4 | import calculate from "../logic/calculate"; 5 | import "./App.css"; 6 | 7 | export default class App extends React.Component { 8 | state = { 9 | total: null, 10 | next: null, 11 | operation: null, 12 | }; 13 | 14 | handleClick = buttonName => { 15 | for (let i = 0; i < 99999; i++) { 16 | this.setState(calculate(this.state, buttonName)); 17 | } // testing to make react slow 18 | }; 19 | 20 | render() { 21 | return ( 22 |
23 | 24 | 25 |
26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/component/App.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | 5 | it("renders without crashing", () => { 6 | const div = document.createElement("div"); 7 | ReactDOM.render(, div); 8 | }); 9 | -------------------------------------------------------------------------------- /src/component/Button.css: -------------------------------------------------------------------------------- 1 | .component-button { 2 | display: inline-flex; 3 | width: 25%; 4 | flex: 1 0 auto; 5 | } 6 | 7 | .component-button.wide { 8 | width: 50%; 9 | } 10 | 11 | .component-button button { 12 | background-color: #e0e0e0; 13 | border: 0; 14 | font-size: 1.5rem; 15 | margin: 0 1px 0 0; 16 | flex: 1 0 auto; 17 | padding: 0; 18 | } 19 | 20 | .component-button:last-child button { 21 | margin-right: 0; 22 | } 23 | 24 | .component-button.orange button { 25 | background-color: #f5923e; 26 | color: white; 27 | } 28 | -------------------------------------------------------------------------------- /src/component/Button.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import "./Button.css"; 4 | 5 | export default class Button extends React.Component { 6 | static propTypes = { 7 | name: PropTypes.string, 8 | orange: PropTypes.bool, 9 | wide: PropTypes.bool, 10 | clickHandler: PropTypes.func, 11 | }; 12 | 13 | handleClick = () => { 14 | this.props.clickHandler(this.props.name); 15 | }; 16 | 17 | render() { 18 | const className = [ 19 | "component-button", 20 | this.props.orange ? "orange" : "", 21 | this.props.wide ? "wide" : "", 22 | ]; 23 | 24 | return ( 25 |
26 | 27 |
28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/component/ButtonPanel.css: -------------------------------------------------------------------------------- 1 | .component-button-panel { 2 | background-color: #858694; 3 | display: flex; 4 | flex-direction: row; 5 | flex-wrap: wrap; 6 | flex: 1 0 auto; 7 | } 8 | 9 | .component-button-panel > div { 10 | width: 100%; 11 | margin-bottom: 1px; 12 | flex: 1 0 auto; 13 | display: flex; 14 | } 15 | -------------------------------------------------------------------------------- /src/component/ButtonPanel.js: -------------------------------------------------------------------------------- 1 | import Button from "./Button"; 2 | import React from "react"; 3 | import PropTypes from "prop-types"; 4 | 5 | import "./ButtonPanel.css"; 6 | 7 | export default class ButtonPanel extends React.Component { 8 | static propTypes = { 9 | clickHandler: PropTypes.func, 10 | }; 11 | 12 | handleClick = buttonName => { 13 | this.props.clickHandler(buttonName); 14 | }; 15 | 16 | render() { 17 | return ( 18 |
19 |
20 |
25 |
26 |
31 |
32 |
37 |
38 |
43 |
44 |
48 |
49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/component/Display.css: -------------------------------------------------------------------------------- 1 | .component-display { 2 | background-color: #858694; 3 | color: white; 4 | text-align: right; 5 | font-weight: 200; 6 | flex: 0 0 auto; 7 | width: 100%; 8 | } 9 | 10 | .component-display > div { 11 | font-size: 2.5rem; 12 | padding: 0.2rem 0.7rem 0.1rem 0.5rem; 13 | } 14 | -------------------------------------------------------------------------------- /src/component/Display.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import "./Display.css"; 5 | 6 | export default class Display extends React.Component { 7 | static propTypes = { 8 | value: PropTypes.string, 9 | }; 10 | 11 | render() { 12 | return ( 13 |
14 |
{this.props.value}
15 |
16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | font-size: 10px; 4 | } 5 | 6 | body { 7 | background-color: black; 8 | margin: 0; 9 | padding: 0; 10 | font-family: sans-serif; 11 | height: 100%; 12 | } 13 | 14 | #root { 15 | height: 100%; 16 | } 17 | 18 | body .github-fork-ribbon:before { 19 | background-color: #333; 20 | } 21 | 22 | @media screen and (max-width: 400px) { 23 | .github-fork-ribbon { 24 | display: none; 25 | } 26 | } 27 | 28 | @media (min-width: 400px) and (min-height: 400px) { 29 | html { 30 | font-size: 20px; 31 | } 32 | } 33 | 34 | @media (min-width: 500px) and (min-height: 500px) { 35 | html { 36 | font-size: 30px; 37 | } 38 | } 39 | 40 | @media (min-width: 600px) and (min-height: 600px) { 41 | html { 42 | font-size: 40px; 43 | } 44 | } 45 | 46 | @media (min-width: 800px) and (min-height: 800px) { 47 | html { 48 | font-size: 50px; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./component/App"; 4 | import "./index.css"; 5 | import "github-fork-ribbon-css/gh-fork-ribbon.css"; 6 | 7 | ReactDOM.render(, document.getElementById("root")); 8 | -------------------------------------------------------------------------------- /src/logic/calculate.js: -------------------------------------------------------------------------------- 1 | import Big from "big.js"; 2 | 3 | import operate from "./operate"; 4 | import isNumber from "./isNumber"; 5 | 6 | /** 7 | * Given a button name and a calculator data object, return an updated 8 | * calculator data object. 9 | * 10 | * Calculator data object contains: 11 | * total:String the running total 12 | * next:String the next number to be operated on with the total 13 | * operation:String +, -, etc. 14 | */ 15 | export default function calculate(obj, buttonName) { 16 | if (buttonName === "AC") { 17 | return { 18 | total: null, 19 | next: null, 20 | operation: null, 21 | }; 22 | } 23 | 24 | if (isNumber(buttonName)) { 25 | if (buttonName === "0" && obj.next === "0") { 26 | return {}; 27 | } 28 | // If there is an operation, update next 29 | if (obj.operation) { 30 | if (obj.next) { 31 | return { next: obj.next + buttonName }; 32 | } 33 | return { next: buttonName }; 34 | } 35 | // If there is no operation, update next and clear the value 36 | if (obj.next) { 37 | const next = obj.next === "0" ? buttonName : obj.next + buttonName; 38 | return { 39 | next, 40 | total: null, 41 | }; 42 | } 43 | return { 44 | next: buttonName, 45 | total: null, 46 | }; 47 | } 48 | 49 | if (buttonName === "%") { 50 | if (obj.operation && obj.next) { 51 | const result = operate(obj.total, obj.next, obj.operation); 52 | return { 53 | total: Big(result) 54 | .div(Big("100")) 55 | .toString(), 56 | next: null, 57 | operation: null, 58 | }; 59 | } 60 | if (obj.next) { 61 | return { 62 | next: Big(obj.next) 63 | .div(Big("100")) 64 | .toString(), 65 | }; 66 | } 67 | return {}; 68 | } 69 | 70 | if (buttonName === ".") { 71 | if (obj.next) { 72 | // ignore a . if the next number already has one 73 | if (obj.next.includes(".")) { 74 | return {}; 75 | } 76 | return { next: obj.next + "." }; 77 | } 78 | return { next: "0." }; 79 | } 80 | 81 | if (buttonName === "=") { 82 | if (obj.next && obj.operation) { 83 | return { 84 | total: operate(obj.total, obj.next, obj.operation), 85 | next: null, 86 | operation: null, 87 | }; 88 | } else { 89 | // '=' with no operation, nothing to do 90 | return {}; 91 | } 92 | } 93 | 94 | if (buttonName === "+/-") { 95 | if (obj.next) { 96 | return { next: (-1 * parseFloat(obj.next)).toString() }; 97 | } 98 | if (obj.total) { 99 | return { total: (-1 * parseFloat(obj.total)).toString() }; 100 | } 101 | return {}; 102 | } 103 | 104 | // Button must be an operation 105 | 106 | // When the user presses an operation button without having entered 107 | // a number first, do nothing. 108 | // if (!obj.next && !obj.total) { 109 | // return {}; 110 | // } 111 | 112 | // User pressed an operation button and there is an existing operation 113 | if (obj.operation) { 114 | return { 115 | total: operate(obj.total, obj.next, obj.operation), 116 | next: null, 117 | operation: buttonName, 118 | }; 119 | } 120 | 121 | // no operation yet, but the user typed one 122 | 123 | // The user hasn't typed a number yet, just save the operation 124 | if (!obj.next) { 125 | return { operation: buttonName }; 126 | } 127 | 128 | // save the operation and shift 'next' into 'total' 129 | return { 130 | total: obj.next, 131 | next: null, 132 | operation: buttonName, 133 | }; 134 | } 135 | -------------------------------------------------------------------------------- /src/logic/calculate.test.js: -------------------------------------------------------------------------------- 1 | import calculate from "./calculate"; 2 | import chai from "chai"; 3 | 4 | // https://github.com/chaijs/chai/issues/469 5 | chai.config.truncateThreshold = 0; 6 | 7 | const expect = chai.expect; 8 | 9 | function pressButtons(buttons) { 10 | const value = {}; 11 | buttons.forEach(button => { 12 | Object.assign(value, calculate(value, button)); 13 | }); 14 | // no need to distinguish between null and undefined values 15 | Object.keys(value).forEach(key => { 16 | if (value[key] === null) { 17 | delete value[key]; 18 | } 19 | }); 20 | return value; 21 | } 22 | 23 | function expectButtons(buttons, expectation) { 24 | expect(pressButtons(buttons)).to.deep.equal(expectation); 25 | } 26 | 27 | function test(buttons, expectation, only = false) { 28 | const func = only ? it.only : it; 29 | func(`buttons ${buttons.join(",")} -> ${JSON.stringify(expectation)}`, () => { 30 | expectButtons(buttons, expectation); 31 | }); 32 | } 33 | 34 | describe("calculate", function() { 35 | test(["6"], { next: "6" }); 36 | 37 | test(["6", "6"], { next: "66" }); 38 | 39 | test(["6", "+", "6"], { 40 | next: "6", 41 | total: "6", 42 | operation: "+", 43 | }); 44 | 45 | test(["6", "+", "6", "="], { 46 | total: "12", 47 | }); 48 | 49 | test(["0", "0", "+", "0", "="], { 50 | total: "0", 51 | }); 52 | 53 | test(["6", "+", "6", "=", "9"], { 54 | next: "9", 55 | }); 56 | 57 | test(["3", "+", "6", "=", "+"], { 58 | total: "9", 59 | operation: "+", 60 | }); 61 | 62 | test(["3", "+", "6", "=", "+", "9"], { 63 | total: "9", 64 | operation: "+", 65 | next: "9", 66 | }); 67 | 68 | test(["3", "+", "6", "=", "+", "9", "="], { 69 | total: "18", 70 | }); 71 | 72 | // When '=' is pressed and there is not enough information to complete 73 | // an operation, the '=' should be disregarded. 74 | test(["3", "+", "=", "3", "="], { 75 | total: "6", 76 | }); 77 | 78 | test(["+"], { 79 | operation: "+", 80 | }); 81 | 82 | test(["+", "2"], { 83 | next: "2", 84 | operation: "+", 85 | }); 86 | 87 | test(["+", "2", "+"], { 88 | total: "2", 89 | operation: "+", 90 | }); 91 | 92 | test(["+", "2", "+", "+"], { 93 | total: "2", 94 | operation: "+", 95 | }); 96 | 97 | test(["+", "2", "+", "5"], { 98 | next: "5", 99 | total: "2", 100 | operation: "+", 101 | }); 102 | 103 | test(["+", "2", "5"], { 104 | next: "25", 105 | operation: "+", 106 | }); 107 | 108 | test(["+", "2", "5"], { 109 | next: "25", 110 | operation: "+", 111 | }); 112 | 113 | test(["+", "6", "+", "5", "="], { 114 | total: "11", 115 | }); 116 | 117 | test(["0", ".", "4"], { 118 | next: "0.4", 119 | }); 120 | 121 | test([".", "4"], { 122 | next: "0.4", 123 | }); 124 | 125 | test([".", "4", "-", ".", "2"], { 126 | total: "0.4", 127 | next: "0.2", 128 | operation: "-", 129 | }); 130 | 131 | test([".", "4", "-", ".", "2", "="], { 132 | total: "0.2", 133 | }); 134 | 135 | // should clear the operator when AC is pressed 136 | test(["1", "+", "2", "AC"], {}); 137 | test(["+", "2", "AC"], {}); 138 | 139 | test(["4", "%"], { 140 | next: "0.04", 141 | }); 142 | 143 | test(["4", "%", "x", "2", "="], { 144 | total: "0.08", 145 | }); 146 | 147 | test(["4", "%", "x", "2"], { 148 | total: "0.04", 149 | operation: "x", 150 | next: "2", 151 | }); 152 | 153 | // the percentage sign should also act as '=' 154 | test(["2", "x", "2", "%"], { 155 | total: "0.04", 156 | }); 157 | 158 | //Test that pressing the multiplication or division sign multiple times should not affect the current computation 159 | test(["2", "x", "x"], { 160 | total: "2", 161 | operation: "x" 162 | }); 163 | 164 | test(["2", "÷", "÷"], { 165 | total: "2", 166 | operation: "÷" 167 | }); 168 | 169 | test(["2", "÷", "x", "+", "-", "x"], { 170 | total: "2", 171 | operation: 'x' 172 | }); 173 | }); 174 | -------------------------------------------------------------------------------- /src/logic/isNumber.js: -------------------------------------------------------------------------------- 1 | export default function isNumber(item) { 2 | return /[0-9]+/.test(item); 3 | } 4 | -------------------------------------------------------------------------------- /src/logic/operate.js: -------------------------------------------------------------------------------- 1 | import Big from "big.js"; 2 | 3 | export default function operate(numberOne, numberTwo, operation) { 4 | const one = Big(numberOne || "0"); 5 | const two = Big(numberTwo || (operation === "÷" || operation === 'x' ? "1": "0")); //If dividing or multiplying, then 1 maintains current value in cases of null 6 | if (operation === "+") { 7 | return one.plus(two).toString(); 8 | } 9 | if (operation === "-") { 10 | return one.minus(two).toString(); 11 | } 12 | if (operation === "x") { 13 | return one.times(two).toString(); 14 | } 15 | if (operation === "÷") { 16 | if (two === "0") { 17 | alert("Divide by 0 error"); 18 | return "0"; 19 | } else { 20 | return one.div(two).toString(); 21 | } 22 | } 23 | throw Error(`Unknown operation '${operation}'`); 24 | } 25 | --------------------------------------------------------------------------------