diff --git a/.gitignore b/.gitignore index c52c2f1..bbe3995 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ /node_modules /build .DS_Store +/cypress/downloads +/cypress/screenshots +/cypress/videos \ No newline at end of file diff --git a/README.md b/README.md index 9547d08..751f2d4 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,11 @@ This is a simple React based counter app to allow a user to increment, decrement - CSS in JS styling using emotion - Replicating a basic Redux style store using a JavaScript reducer along with React `useReducer` hook. - Sharing the global state and reducer actions to the application component tree using React `useContext` hook +- Persisting the count value using browser localStorage API - Dark mode support using `prefers-color-scheme` media query -- Husky pre-commit hooks for linting and formatting +- Husky pre-commit hooks for linting and formatting or source-code - GitHub actions for CI / CD pipelines +- End to End testing using Cypress ## Getting started @@ -78,6 +80,14 @@ Pre-commit hooks will run using Husky to: - Lint all committed source (js, jsx, ts, tsx) using ESLint rules - Format all known filetypes using Prettier +### Testing + +Tests can be run (end to end) using [Cypress](https://www.cypress.io/). **Note:** An instance of the application must be running for the test suite to function; run `npm run start` ensuring the default port of `3000` is used. + +```bash +npm run test +``` + ## Deployment To build a production copy for deployment `cd` into the project root and run the below command from your terminal. **Note:** The build will be performed as per the `homepage` key in `package.json`. diff --git a/cypress/e2e/app.cy.ts b/cypress/e2e/app.cy.ts index e67c1fe..05784f8 100644 --- a/cypress/e2e/app.cy.ts +++ b/cypress/e2e/app.cy.ts @@ -26,6 +26,16 @@ describe("React Counter App", () => { cy.get("@countValue").should("contain", "0"); }); + it("persists count changes", () => { + cy.get("@incrementValue").click(); + cy.get("@incrementValue").click(); + cy.get("@countValue").should("contain", "2"); + cy.get("@decrementValue").click(); + cy.get("@countValue").should("contain", "1"); + cy.reload(); + cy.get("@countValue").should("contain", "1"); + }); + it("prevents a user reseting the count value when zero", () => { cy.get("@resetValue").should("be.disabled"); }); diff --git a/package-lock.json b/package-lock.json index 087294a..7197cf9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-counter-app", - "version": "1.11.2", + "version": "1.12.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "react-counter-app", - "version": "1.11.2", + "version": "1.12.0", "license": "MIT", "dependencies": { "@emotion/react": "^11.10.4", diff --git a/package.json b/package.json index d343bf9..93b6c3e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-counter-app", - "version": "1.11.2", + "version": "1.12.0", "description": "React counter app", "engines": { "node": ">=16.15.1 <17.0.0" @@ -54,6 +54,7 @@ "check-formatting": "prettier --check 'src/**/*.{ts,tsx}'", "fix-formatting": "npm run check-formatting -- --write", "test": "cypress open", + "test:headless": "cypress run", "eject": "react-scripts eject" }, "browserslist": [ diff --git a/src/app/button.tsx b/src/app/button.tsx index c561597..ca85cbd 100644 --- a/src/app/button.tsx +++ b/src/app/button.tsx @@ -17,7 +17,7 @@ const Button = styled.button({ "&:not([disabled])": { cursor: "pointer", - "&:hover, &:focus": { + "&:hover": { opacity: "0.9", }, }, diff --git a/src/app/counter-provider.tsx b/src/app/counter-provider.tsx index b973c20..61f1b07 100644 --- a/src/app/counter-provider.tsx +++ b/src/app/counter-provider.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useReducer } from "react"; +import React, { useContext, useEffect, useReducer } from "react"; const counterActionTypes = { INCREMENT: "INCREMENT", @@ -38,13 +38,32 @@ interface counterReducerActions { reset: () => void; } +function useLocalStorageCountValue( + key: string, + defaultValue: number +): [number, (value: number) => void] { + const value = localStorage.getItem(key) + ? Number(localStorage.getItem(key)) + : defaultValue; + + return [ + value, + (value: number) => localStorage.setItem(key, value.toString()), + ]; +} + function useCounterReducer(): { count: number; actions: counterReducerActions; } { - const initialState = { count: initialCount }; + const [localStorageCountValue, setLocalStorageCountValue] = + useLocalStorageCountValue("binaryJimCounterAppValue", initialCount); - const [state, dispatch] = useReducer(counterReducer, initialState); + const [state, dispatch] = useReducer( + counterReducer, + localStorageCountValue, + (count) => ({ count }) + ); const count = state.count; @@ -54,13 +73,9 @@ function useCounterReducer(): { reset: () => dispatch({ type: counterActionTypes.RESET }), }; - /* - For later enhancement to add local storage persistance, much like redux "store.subscribe" - useEffect(() => { - console.log('Count changed') - }, [state.count]) - */ + setLocalStorageCountValue(count); + }, [count, setLocalStorageCountValue]); return { count, actions }; }