diff --git a/.circleci/config.yml b/.circleci/config.yml index 2d36678..150755d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -19,4 +19,7 @@ jobs: key: v1-dependencies-{{ checksum "package.json" }} - run: name: Run Test - command: yarn test \ No newline at end of file + command: yarn test + - run: + name: Build the app + command: yarn build \ No newline at end of file diff --git a/README.md b/README.md index e34eb16..4c7bb3b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ ## React features 🤓 +[![CircleCI](https://circleci.com/gh/Will956/react-features/tree/master.svg?style=svg)](https://circleci.com/gh/Will956/react-features/tree/master) + I usually use this repo in order to test new React features. ## Features diff --git a/package.json b/package.json index fb1c34d..32711db 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,9 @@ }, "eslintConfig": { "extends": "react-app", - "plugins": ["react-hooks"], + "plugins": [ + "react-hooks" + ], "rules": { "react-hooks/rules-of-hooks": "error" } @@ -119,6 +121,7 @@ ] }, "devDependencies": { - "eslint-plugin-react-hooks": "^1.0.1" + "eslint-plugin-react-hooks": "^1.0.1", + "react-testing-library": "^5.5.3" } } diff --git a/src/components/Hooks/BasicHooks/index.js b/src/components/Hooks/BasicHooks/index.js new file mode 100644 index 0000000..ba44c57 --- /dev/null +++ b/src/components/Hooks/BasicHooks/index.js @@ -0,0 +1,15 @@ +import React, { useState } from 'react'; + +const BasicHooks = () => { + const [count, setCount] = useState(0); + + return ( + <> + + +

Counter: {count}

+ + ) +}; + +export default BasicHooks; diff --git a/src/components/Hooks/BasicHooks/index.test.js b/src/components/Hooks/BasicHooks/index.test.js new file mode 100644 index 0000000..ba32a3f --- /dev/null +++ b/src/components/Hooks/BasicHooks/index.test.js @@ -0,0 +1,32 @@ +import React from 'react'; +import { cleanup, fireEvent, render } from 'react-testing-library'; +import BasicHooks from './index'; + +afterEach(cleanup); + +describe('BasicHooks', () => { + let counter; + let addBtn; + let removeBtn; + + beforeEach(() => { + const { container, getByText } = render(); + counter = container.querySelector('p'); + addBtn = getByText('Add one more'); + removeBtn = getByText('Remove one more'); + }); + + it('renders BasicHooks', () => { + expect(counter.textContent).toBe('Counter: 0'); + }); + + it('update BasicHooks counter', () => { + fireEvent.click(addBtn); + expect(counter.textContent).toBe('Counter: 1'); + }); + + it('update BasicHooks counter', () => { + fireEvent.click(removeBtn); + expect(counter.textContent).toBe('Counter: -1'); + }); +}); \ No newline at end of file diff --git a/src/components/Hooks/ContextHooks/index.js b/src/components/Hooks/ContextHooks/index.js new file mode 100644 index 0000000..ce362ef --- /dev/null +++ b/src/components/Hooks/ContextHooks/index.js @@ -0,0 +1,11 @@ +import React, { useContext } from 'react'; + +import { ThemeContext } from '../../../containers/Playground'; + +const ContextHooks = () => { + const theme = useContext(ThemeContext); + + return <>

Theme is: {theme}

; +}; + +export default ContextHooks; diff --git a/src/components/Hooks/ContextHooks/index.test.js b/src/components/Hooks/ContextHooks/index.test.js new file mode 100644 index 0000000..34e8664 --- /dev/null +++ b/src/components/Hooks/ContextHooks/index.test.js @@ -0,0 +1,13 @@ +import React from 'react'; +import { cleanup, render } from 'react-testing-library'; +import ContextHooks from './index'; +import { ThemeContext } from '../../../containers/Playground'; + +afterEach(cleanup); + +describe('ContextHooks', () => { + it('renders EffectHooks and should have blue as theme', () => { + const { container } = render(); + expect(container.querySelector('p').textContent).toBe('Theme is: random color'); + }); +}); \ No newline at end of file diff --git a/src/components/Hooks/EffectHooks/index.js b/src/components/Hooks/EffectHooks/index.js new file mode 100644 index 0000000..eb63deb --- /dev/null +++ b/src/components/Hooks/EffectHooks/index.js @@ -0,0 +1,22 @@ +import React, { useState, useEffect } from 'react'; + +const EffectHooks = () => { + const [count, setCount] = useState(0); + const [status, setStatus] = useState(null); + + useEffect(() => { + setStatus(`Component mounted/updated with ${count}`); + return () => console.log('[EffectHooks] Component was unmounted'); + }, [count]); // only re-run if count changes 🤓 + + return ( + <> + + +

Counter: {count}

+ {status &&

{status}

} + + ); +}; + +export default EffectHooks; diff --git a/src/components/Hooks/EffectHooks/index.test.js b/src/components/Hooks/EffectHooks/index.test.js new file mode 100644 index 0000000..04b6fb9 --- /dev/null +++ b/src/components/Hooks/EffectHooks/index.test.js @@ -0,0 +1,31 @@ +import React from 'react'; +import { cleanup, fireEvent, render } from 'react-testing-library'; +import EffectHooks from './index'; + +global.console = { + log: jest.fn() +}; + +afterEach(cleanup); + +describe('BasicHooks', () => { + it('renders EffectHooks', () => { + const { getByTestId } = render(); + expect(getByTestId('counter').textContent).toBe('Counter: 0'); + expect(getByTestId('status').textContent).toBe('Component mounted/updated with 0'); + }); + + it('update status when component did update', () => { + const { getByTestId, getByText } = render(); + const addBtn = getByText('Add one more'); + fireEvent.click(addBtn); + expect(getByTestId('status').textContent).toBe('Component mounted/updated with 1'); + }); + + it('trigger console.log when component did unmount', () => { + const { getByText } = render(); + const addBtn = getByText('Add one more'); + fireEvent.click(addBtn); + expect(console.log).toHaveBeenCalledWith('[EffectHooks] Component was unmounted'); + }); +}); \ No newline at end of file diff --git a/src/components/Hooks/ReducerHooks/index.js b/src/components/Hooks/ReducerHooks/index.js new file mode 100644 index 0000000..e40dcc4 --- /dev/null +++ b/src/components/Hooks/ReducerHooks/index.js @@ -0,0 +1,39 @@ +import React, { useReducer } from 'react'; + +const initialState = { + count: 0, + previous: 0 +}; + +const reducer = (state, action) => { + switch (action.type) { + case 'INCREMENT': + return { + count: state.count + 1, + previous: state.count + }; + case 'DECREMENT': + return { + count: state.count -1, + previous: state.count, + } + + default: + return state; + } +}; + +const ReducerHooks = () => { + const [{ count, previous }, dispatch] = useReducer(reducer, initialState); + + return ( + <> + + +

Count is: {count}

+

Previous was: {previous}

+ + ); +}; + +export default ReducerHooks; diff --git a/src/components/Hooks/ReducerHooks/index.test.js b/src/components/Hooks/ReducerHooks/index.test.js new file mode 100644 index 0000000..ef35624 --- /dev/null +++ b/src/components/Hooks/ReducerHooks/index.test.js @@ -0,0 +1,25 @@ +import React from 'react'; +import { cleanup, fireEvent, render } from 'react-testing-library'; +import ReducerHooks from './index'; + +//TODO: Add more tests (ie. test is fn called..) + +afterEach(cleanup); + +describe('ReducerHooks', () => { + it('renders ReducerHooks', () => { + const { container } = render(); + expect(container.querySelectorAll('p')[0].textContent).toBe('Count is: 0'); + expect(container.querySelectorAll('p')[1].textContent).toBe('Previous was: 0'); + }); + + it('update ReducerHooks counter', () => { + const { container, getByText } = render(); + + const addBtn = getByText('Add one more'); + fireEvent.click(addBtn); + + expect(container.querySelectorAll('p')[0].textContent).toBe('Count is: 1'); + expect(container.querySelectorAll('p')[1].textContent).toBe('Previous was: 0'); + }); +}); \ No newline at end of file diff --git a/src/containers/Playground/index.js b/src/containers/Playground/index.js index f804090..0287fe2 100644 --- a/src/containers/Playground/index.js +++ b/src/containers/Playground/index.js @@ -1,14 +1,35 @@ -import React, { Component } from 'react'; +import React, { createContext } from 'react'; + +import BasicHooks from '../../components/Hooks/BasicHooks'; +import EffectHooks from '../../components/Hooks/EffectHooks'; +import ContextHooks from '../../components/Hooks/ContextHooks'; +import ReducerHooks from '../../components/Hooks/ReducerHooks'; + import './index.scss'; -class Playground extends Component { - render() { - return ( -
-

Edit src/App.js and save to reload.

-
- ); - } -} +export const ThemeContext = createContext(); + +const Playground = () => ( + +
+
+

BasicHooks:

+ +
+
+

EffectHooks:

+ +
+
+

ContextHooks:

+ +
+
+

ReducerHooks:

+ +
+
+
+); export default Playground; diff --git a/src/containers/Playground/index.scss b/src/containers/Playground/index.scss index 924df6c..725f624 100644 --- a/src/containers/Playground/index.scss +++ b/src/containers/Playground/index.scss @@ -1,4 +1,4 @@ -.App-header { +.app-header { background-color: #282c34; min-height: 100vh; display: flex; @@ -8,3 +8,10 @@ font-size: calc(10px + 2vmin); color: white; } + +.app-block { + padding: 30px 30px; + margin: 10px 0; + text-align: center; + border: 1px solid white; +} diff --git a/yarn.lock b/yarn.lock index 35e9ac9..ae28b53 100644 --- a/yarn.lock +++ b/yarn.lock @@ -834,6 +834,13 @@ dependencies: regenerator-runtime "^0.12.0" +"@babel/runtime@^7.1.5", "@babel/runtime@^7.3.1": + version "7.3.1" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.1.tgz#574b03e8e8a9898eaf4a872a92ea20b7846f6f2a" + integrity sha512-7jGW8ppV0ant637pIqAcFfQDDH1orEPGJb8aXfUozuCU3QqX7rX4DA8iwrbPrR1hcH0FTTHz47yQnk+bl5xHQA== + dependencies: + regenerator-runtime "^0.12.0" + "@babel/template@^7.1.0", "@babel/template@^7.1.2", "@babel/template@^7.2.2": version "7.2.2" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.2.2.tgz#005b3fdf0ed96e88041330379e0da9a708eb2907" @@ -885,6 +892,11 @@ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b" integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw== +"@sheerun/mutationobserver-shim@^0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.2.tgz#8013f2af54a2b7d735f71560ff360d3a8176a87b" + integrity sha512-vTCdPp/T/Q3oSqwHmZ5Kpa9oI7iLtGl3RQaA/NyLHikvcrPxACkkKVr/XzkSPJWXHRhKGzVvb0urJsbMlRxi1Q== + "@svgr/core@^2.4.1": version "2.4.1" resolved "https://registry.yarnpkg.com/@svgr/core/-/core-2.4.1.tgz#03a407c28c4a1d84305ae95021e8eabfda8fa731" @@ -3121,6 +3133,16 @@ dom-serializer@0: domelementtype "~1.1.1" entities "~1.1.1" +dom-testing-library@^3.13.1: + version "3.16.5" + resolved "https://registry.yarnpkg.com/dom-testing-library/-/dom-testing-library-3.16.5.tgz#8c71f127c6b4ee48115660798040291b59dfc894" + integrity sha512-t3OaTcDdsAqtAZNeZ13KnOJmt+2HaDJqYWyf0iBRzbG6GwrNtpF0122Ygu/qkerIwcnHMX1ihwZVx/DhaLpmTw== + dependencies: + "@babel/runtime" "^7.1.5" + "@sheerun/mutationobserver-shim" "^0.3.2" + pretty-format "^24.0.0" + wait-for-expect "^1.1.0" + domain-browser@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" @@ -7881,6 +7903,14 @@ pretty-format@^23.6.0: ansi-regex "^3.0.0" ansi-styles "^3.2.0" +pretty-format@^24.0.0: + version "24.0.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.0.0.tgz#cb6599fd73ac088e37ed682f61291e4678f48591" + integrity sha512-LszZaKG665djUcqg5ZQq+XzezHLKrxsA86ZABTozp+oNhkdqa+tG2dX4qa6ERl5c/sRDrAa3lHmwnvKoP+OG/g== + dependencies: + ansi-regex "^4.0.0" + ansi-styles "^3.2.0" + private@^0.1.6, private@^0.1.8: version "0.1.8" resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" @@ -8141,6 +8171,14 @@ react-error-overlay@^5.1.0: resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-5.1.2.tgz#888957b884d4b25b083a82ad550f7aad96585394" integrity sha512-7kEBKwU9R8fKnZJBRa5RSIfay4KJwnYvKB6gODGicUmDSAhQJ7Tdnll5S0RLtYrzRfMVXlqYw61rzrSpP4ThLQ== +react-testing-library@^5.5.3: + version "5.5.3" + resolved "https://registry.yarnpkg.com/react-testing-library/-/react-testing-library-5.5.3.tgz#cfd32da95b29e475003df4f66bd96e34eb5624bd" + integrity sha512-67jMsSJHbbm9M0NWvEzjNikDAKRdxivhP6vnpa9xPg/fYh19zkE4rMsFh5YWLpyoomm+e49fg+ubcXaEBYartA== + dependencies: + "@babel/runtime" "^7.3.1" + dom-testing-library "^3.13.1" + react@^16.8.0: version "16.8.1" resolved "https://registry.yarnpkg.com/react/-/react-16.8.1.tgz#ae11831f6cb2a05d58603a976afc8a558e852c4a" @@ -9837,6 +9875,11 @@ w3c-xmlserializer@^1.0.1: webidl-conversions "^4.0.2" xml-name-validator "^3.0.0" +wait-for-expect@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/wait-for-expect/-/wait-for-expect-1.1.0.tgz#6607375c3f79d32add35cd2c87ce13f351a3d453" + integrity sha512-vQDokqxyMyknfX3luCDn16bSaRcOyH6gGuUXMIbxBLeTo6nWuEWYqMTT9a+44FmW8c2m6TRWBdNvBBjA1hwEKg== + walker@~1.0.5: version "1.0.7" resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb"