├── .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 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
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 |
--------------------------------------------------------------------------------