From 4735ca7462adfb2e5d20ee6cee0817e91ffd58ca Mon Sep 17 00:00:00 2001 From: Dima Vyshniakov Date: Wed, 15 May 2024 23:51:55 +0200 Subject: [PATCH 1/3] Add Redux Toolkit support --- app/icon.ico | Bin 0 -> 3585 bytes {src/pages => app}/index.css | 4 + app/layout.tsx | 18 +++++ app/page.tsx | 22 +++++ app/second/page.tsx | 19 +++++ package.json | 2 +- pnpm-lock.yaml | 73 ++++++++++------- src/components/Counter/Counter.module.css | 2 +- src/components/Counter/Counter.spec.tsx | 2 +- src/components/Counter/Counter.tsx | 4 +- src/components/Random/Random.module.css | 4 +- src/components/Random/Random.spec.tsx | 76 ++++++++++++------ src/components/Random/Random.tsx | 6 +- .../Random/__snapshots__/Random.spec.tsx.snap | 16 ++-- src/features/counter/selectors.ts | 2 + .../counter/useIncrementCounter.spec.tsx | 2 +- src/features/random/RandomReducer.spec.tsx | 6 +- src/features/random/RandomReducer.ts | 4 +- .../random/useGetRandomNumberQuery.spec.tsx | 63 ++++++++++----- .../random/useGetRandomNumberQuery.ts | 8 +- src/layout/Head/Head.tsx | 35 -------- src/layout/Head/index.ts | 1 - src/layout/NavHeader/NavHeader.tsx | 2 +- src/layout/NavLink/NavLink.tsx | 2 + src/pages/_app.tsx | 15 ---- src/pages/index.tsx | 19 ----- src/pages/second.tsx | 14 ---- src/state/ReduxProvider.tsx | 39 --------- src/state/StoreProvider.tsx | 22 +++++ src/state/index.ts | 1 - src/state/promiseResolverMiddleware.ts | 17 ++-- src/state/store.ts | 35 ++++++++ tsconfig.json | 15 ++-- 33 files changed, 310 insertions(+), 240 deletions(-) create mode 100644 app/icon.ico rename {src/pages => app}/index.css (64%) create mode 100644 app/layout.tsx create mode 100644 app/page.tsx create mode 100644 app/second/page.tsx delete mode 100644 src/layout/Head/Head.tsx delete mode 100644 src/layout/Head/index.ts delete mode 100644 src/pages/_app.tsx delete mode 100644 src/pages/index.tsx delete mode 100644 src/pages/second.tsx delete mode 100644 src/state/ReduxProvider.tsx create mode 100644 src/state/StoreProvider.tsx delete mode 100644 src/state/index.ts create mode 100644 src/state/store.ts diff --git a/app/icon.ico b/app/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..354202a4e6baec3429e928e13d54144f8eebb62c GIT binary patch literal 3585 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 Hu0mjfhdT@% literal 0 HcmV?d00001 diff --git a/src/pages/index.css b/app/index.css similarity index 64% rename from src/pages/index.css rename to app/index.css index 4471d80..43ac7ad 100644 --- a/src/pages/index.css +++ b/app/index.css @@ -2,3 +2,7 @@ body { font-family: system-ui; margin: 0; } + +main { + padding: 36px; +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..65a5c92 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,18 @@ +import type {ReactNode} from 'react'; + +import {StoreProvider} from '@/src/state/StoreProvider'; +import './index.css'; + +type Props = { + readonly children: ReactNode; +}; + +export default function RootLayout({children}: Props) { + return ( + + + {children} + + + ); +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..2f5dce8 --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,22 @@ +import type {Metadata} from 'next'; +import {Fragment} from 'react'; + +import Counter from '@/src/components/Counter'; +import Random from '@/src/components/Random'; +import {NavHeader} from '@/src/layout/NavHeader'; + +export default function IndexPage() { + return ( + + +
+ + +
+
+ ); +} + +export const metadata: Metadata = { + title: 'First page', +}; diff --git a/app/second/page.tsx b/app/second/page.tsx new file mode 100644 index 0000000..8c94242 --- /dev/null +++ b/app/second/page.tsx @@ -0,0 +1,19 @@ +import type {Metadata} from 'next'; +import {Fragment} from 'react'; + +import {NavHeader} from '@/src/layout/NavHeader'; + +export default function IndexPage() { + return ( + + +
+

Boring second page. Use to test global state and routes behavior.

+
+
+ ); +} + +export const metadata: Metadata = { + title: 'First page', +}; diff --git a/package.json b/package.json index 87683f7..211c941 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "test": "jest" }, "dependencies": { - "axios": "1.6.8", + "@reduxjs/toolkit": "^2.2.4", "classnames": "^2.5.1", "next": "14.2.3", "react": "18.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a82b847..b9028a8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,9 @@ importers: .: dependencies: - axios: - specifier: 1.6.8 - version: 1.6.8 + '@reduxjs/toolkit': + specifier: ^2.2.4 + version: 2.2.4(react-redux@8.1.3(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(redux@5.0.1))(react@18.3.1) classnames: specifier: ^2.5.1 version: 2.5.1 @@ -1474,6 +1474,17 @@ packages: '@polka/url@1.0.0-next.25': resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==} + '@reduxjs/toolkit@2.2.4': + resolution: {integrity: sha512-EoIC9iC2V/DLRBVMXRHrO/oM3QBT7RuJNeBRx8Cpnz/NHINeZBEqgI8YOxAYUjLp+KYxGgc4Wd6KoAKsaUBGhg==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + '@rushstack/eslint-patch@1.2.0': resolution: {integrity: sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==} @@ -1922,9 +1933,6 @@ packages: resolution: {integrity: sha512-/BQzOX780JhsxDnPpH4ZiyrJAzcd8AfzFPkv+89veFSr1rcMjuq2JDCwypKaPeB6ljHp9KjXhPpjgCvQlWYuqg==} engines: {node: '>=4'} - axios@1.6.8: - resolution: {integrity: sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==} - axobject-query@3.1.1: resolution: {integrity: sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg==} @@ -2811,15 +2819,6 @@ packages: resolution: {integrity: sha512-0OEk9Gr+Yj7wjDW2KgaNYUypKau71jAfFyeLQF5iVtxqc6uJHag/MT7pmaEApf4qM7u86DkBcd4ualddYMfbLw==} engines: {node: '>=0.4.0'} - follow-redirects@1.15.6: - resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} @@ -3101,6 +3100,9 @@ packages: resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} engines: {node: '>= 4'} + immer@10.1.1: + resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==} + import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} @@ -4361,9 +4363,6 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} - proxy-from-env@1.1.0: - resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - psl@1.9.0: resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} @@ -4450,6 +4449,11 @@ packages: redux-mock-store@1.5.4: resolution: {integrity: sha512-xmcA0O/tjCLXhh9Fuiq6pMrJCwFRaouA8436zcikdIpYWWCjU76CRk+i2bHx8EeiSiMGnB85/lZdU3wIJVXHTA==} + redux-thunk@3.1.0: + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + redux@4.2.1: resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==} @@ -4512,6 +4516,9 @@ packages: requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + reselect@5.1.0: + resolution: {integrity: sha512-aw7jcGLDpSgNDyWBQLv2cedml85qd95/iszJjN988zX1t7AVRJi19d9kto5+W7oCfQ94gyo40dVbT6g2k4/kXg==} + resolve-cwd@3.0.0: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} engines: {node: '>=8'} @@ -7550,6 +7557,16 @@ snapshots: '@polka/url@1.0.0-next.25': {} + '@reduxjs/toolkit@2.2.4(react-redux@8.1.3(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(redux@5.0.1))(react@18.3.1)': + dependencies: + immer: 10.1.1 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.0 + optionalDependencies: + react: 18.3.1 + react-redux: 8.1.3(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(redux@5.0.1) + '@rushstack/eslint-patch@1.2.0': {} '@rushstack/eslint-patch@1.5.1': {} @@ -8110,14 +8127,6 @@ snapshots: axe-core@4.6.3: {} - axios@1.6.8: - dependencies: - follow-redirects: 1.15.6 - form-data: 4.0.0 - proxy-from-env: 1.1.0 - transitivePeerDependencies: - - debug - axobject-query@3.1.1: dependencies: deep-equal: 2.2.0 @@ -9410,8 +9419,6 @@ snapshots: flow-parser@0.236.0: {} - follow-redirects@1.15.6: {} - for-each@0.3.3: dependencies: is-callable: 1.2.7 @@ -9691,6 +9698,8 @@ snapshots: ignore@5.2.4: {} + immer@10.1.1: {} + import-fresh@3.3.0: dependencies: parent-module: 1.0.1 @@ -11241,8 +11250,6 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 - proxy-from-env@1.1.0: {} - psl@1.9.0: {} punycode@2.3.0: {} @@ -11326,6 +11333,10 @@ snapshots: dependencies: lodash.isplainobject: 4.0.6 + redux-thunk@3.1.0(redux@5.0.1): + dependencies: + redux: 5.0.1 + redux@4.2.1: dependencies: '@babel/runtime': 7.21.5 @@ -11394,6 +11405,8 @@ snapshots: requires-port@1.0.0: {} + reselect@5.1.0: {} + resolve-cwd@3.0.0: dependencies: resolve-from: 5.0.0 diff --git a/src/components/Counter/Counter.module.css b/src/components/Counter/Counter.module.css index 3e16459..dc20593 100644 --- a/src/components/Counter/Counter.module.css +++ b/src/components/Counter/Counter.module.css @@ -1,6 +1,6 @@ .counter { border: 1px solid lightgray; - margin: 36px 24px; + margin-bottom: 24px; padding: 24px; text-align: center; width: 240px; diff --git a/src/components/Counter/Counter.spec.tsx b/src/components/Counter/Counter.spec.tsx index b704760..ddd5dc5 100644 --- a/src/components/Counter/Counter.spec.tsx +++ b/src/components/Counter/Counter.spec.tsx @@ -3,7 +3,7 @@ import {Provider} from 'react-redux'; import {render, fireEvent} from '@testing-library/react'; import configureStore from 'redux-mock-store'; -import {Actions} from '@/features/counter/actionTypes'; +import {Actions} from '@/src/features/counter/actionTypes'; import Counter from './Counter'; diff --git a/src/components/Counter/Counter.tsx b/src/components/Counter/Counter.tsx index 95de98c..3a30479 100644 --- a/src/components/Counter/Counter.tsx +++ b/src/components/Counter/Counter.tsx @@ -1,7 +1,9 @@ +'use client'; + import type {FC} from 'react'; import React from 'react'; -import {useCountValue, useIncrementCounter} from '@/features/counter'; +import {useCountValue, useIncrementCounter} from '@/src/features/counter'; import classes from './Counter.module.css'; diff --git a/src/components/Random/Random.module.css b/src/components/Random/Random.module.css index d19a8a2..a806b74 100644 --- a/src/components/Random/Random.module.css +++ b/src/components/Random/Random.module.css @@ -1,6 +1,6 @@ -.counter { +.random { border: 1px solid lightgray; - margin: 36px 24px; + margin-bottom: 24px; padding: 24px; width: 240px; } diff --git a/src/components/Random/Random.spec.tsx b/src/components/Random/Random.spec.tsx index 84aea0f..16f804e 100644 --- a/src/components/Random/Random.spec.tsx +++ b/src/components/Random/Random.spec.tsx @@ -1,12 +1,12 @@ import React from 'react'; import {Provider} from 'react-redux'; +import type {Store, Action} from 'redux'; import {render, fireEvent, waitFor, screen, waitForElementToBeRemoved} from '@testing-library/react'; -import axios from 'axios'; import configureStore from 'redux-mock-store'; -import {GET_RANDOM_NUMBER} from '@/features/random/actionTypes'; -import {store as realStore} from '@/state/ReduxProvider'; -import {promiseResolverMiddleware} from '@/state/promiseResolverMiddleware'; +import {GET_RANDOM_NUMBER} from '@/src/features/random/actionTypes'; +import {makeStore} from '@/src/state/store'; +import {promiseResolverMiddleware} from '@/src/state/promiseResolverMiddleware'; import Random from './Random'; @@ -14,18 +14,34 @@ import Random from './Random'; * Create mock store * @see https://github.com/dmitry-zaets/redux-mock-store */ -// @ts-expect-error TS2322: Type +// @ts-expect-error TS2322 const mockStore = configureStore([promiseResolverMiddleware]); -jest.mock('axios'); - /* We use these strings to match HTMLElements */ const pristineText = 'Click the button to get random number'; const loadingText = 'Getting number'; const errorText = 'Ups...'; const response = 6; +let realStore: Store; + +const fetchBackup = global.fetch; + +const fetchMock = jest.fn(); + describe('components > Random', () => { + beforeAll(() => { + global.fetch = fetchMock; + }); + + beforeEach(() => { + realStore = makeStore(); + fetchMock.mockClear(); + }); + + afterAll(() => { + global.fetch = fetchBackup; + }); /** * Provide table of values to run tests with * @see https://jestjs.io/docs/en/api#describeeachtablename-fn-timeout @@ -67,11 +83,14 @@ describe('components > Random', () => { it('handles successful request', async () => { /** - * Mock axios successful response - * @see https://www.robinwieruch.de/axios-jest + * Mock fetch successful response + * @see https://developer.mozilla.org/en-US/docs/Web/API/fetch */ - // @ts-expect-error TS2339: Property mockImplementationOnce does not exist on type - axios.get.mockImplementationOnce(() => Promise.resolve({data: response})); + fetchMock.mockImplementationOnce(() => + Promise.resolve({ + json: () => Promise.resolve(response), + }) + ); /** * `getByRole`: @@ -105,11 +124,14 @@ describe('components > Random', () => { it('handles rejected request', async () => { /** - * Mock axios rejected response - * @see https://www.robinwieruch.de/axios-jest + * Mock fetch rejected response + * @see https://developer.mozilla.org/en-US/docs/Web/API/fetch */ - // @ts-expect-error TS2339: Property mockImplementationOnce does not exist on type - axios.get.mockImplementationOnce(() => Promise.reject(new Error(''))); + fetchMock.mockImplementationOnce(() => + Promise.reject({ + json: () => Promise.reject(new Error('')), + }) + ); /** * `getByRole`: @@ -151,11 +173,14 @@ describe('components > Random', () => { }); /** - * Mock axios successful response - * @see https://www.robinwieruch.de/axios-jest + * Mock fetch successful response + * @see https://developer.mozilla.org/en-US/docs/Web/API/fetch */ - // @ts-expect-error TS2339: Property mockImplementationOnce does not exist on type - axios.get.mockImplementationOnce(() => Promise.resolve({data: response})); + fetchMock.mockImplementationOnce(() => + Promise.resolve({ + json: () => Promise.resolve(response), + }) + ); /** * `getByRole`: @@ -182,7 +207,7 @@ describe('components > Random', () => { }); /** Second dispatched action should deliver response from API */ - expect(store.getActions()[1].payload.data).toEqual(response); + expect(store.getActions()[1].payload).toEqual(response); }); it('dispatches an action sequence on rejected request made', async () => { @@ -195,11 +220,14 @@ describe('components > Random', () => { }); /** - * Mock axios rejected response - * @see https://www.robinwieruch.de/axios-jest + * Mock fetch rejected response + * @see https://developer.mozilla.org/en-US/docs/Web/API/fetch */ - // @ts-expect-error TS2339: Property mockImplementationOnce does not exist on type - axios.get.mockImplementationOnce(() => Promise.reject(new Error(''))); + fetchMock.mockImplementationOnce(() => + Promise.reject({ + json: () => Promise.reject(new Error('')), + }) + ); /** * `getByRole`: diff --git a/src/components/Random/Random.tsx b/src/components/Random/Random.tsx index 27e7b72..fa1a452 100644 --- a/src/components/Random/Random.tsx +++ b/src/components/Random/Random.tsx @@ -1,6 +1,8 @@ +'use client'; + import React from 'react'; -import {useGetRandomNumberQuery, useRandomNumber, useLoadingState} from '@/features/random'; +import {useGetRandomNumberQuery, useRandomNumber, useLoadingState} from '@/src/features/random'; import classes from './Random.module.css'; @@ -18,7 +20,7 @@ const Random = () => { const isPristine = !isLoading && !hasError && !isFulfilled; return ( -
+

Async Random

+
+ Total value: {count} +
+
+ ); +}; diff --git a/templates/Component/index.ts b/templates/Component/index.ts new file mode 100644 index 0000000..9347b65 --- /dev/null +++ b/templates/Component/index.ts @@ -0,0 +1 @@ +export {TemplateName} from './TemplateName'; diff --git a/templates/Feature/README.md b/templates/Feature/README.md new file mode 100644 index 0000000..a014eb5 --- /dev/null +++ b/templates/Feature/README.md @@ -0,0 +1,44 @@ +## TemplateName + +Tba + +## Selectors + +### `useTemplateNameLoadingState` + +Returns request loading state from the store. + +```javascript +import {useTemplateNameLoadingState} from 'features/templateName'; + +// Needs to be run from inside React component or other hook. +const {isLoading, hasError, isFulfilled} = useTemplateNameLoadingState(); +``` + +### `useTemplateName` + +Returns random number value from the store + +```javascript +import {useTemplateName} from 'features/templateName'; + +// Needs to be run from inside React component or other hook. +const number = useTemplateName(); +``` + +## Action creators + +### `useGetTemplateNameQuery` + +Performs AJAX query to get TemplateName. + +```javascript +import {useGetTemplateNameQuery} from 'features/templateName'; + +// Needs to be run from inside React component or other hook. +const getTemplateName = useGetTemplateNameQuery(); +const handleClick = () => { + getTemplateName(); +} +``` + diff --git a/templates/Feature/TemplateNameReducer.spec.tsx b/templates/Feature/TemplateNameReducer.spec.tsx new file mode 100644 index 0000000..9542aa4 --- /dev/null +++ b/templates/Feature/TemplateNameReducer.spec.tsx @@ -0,0 +1,42 @@ +import {Actions} from './actionTypes'; +import {TemplateNameReducer} from './TemplateNameReducer'; + +describe('features > random > TemplateNameReducer', () => { + it('returns initial state, if non matched action is dispatched', () => { + const initialState = { + isLoading: false, + hasError: false, + isFulfilled: false, + }; + + const action = { + type: Actions.GET_TEMPLATE_NAME, + payload: 0, + }; + + expect(TemplateNameReducer(initialState, action)).toBe(initialState); + }); + + /** + * Provide table of values to run test case against + * @see https://jestjs.io/docs/en/api#testeachtablename-fn-timeout + */ + it.each([ + [Actions.GET_TEMPLATE_NAME_FULFILLED], + [Actions.GET_TEMPLATE_NAME_PENDING], + [Actions.GET_TEMPLATE_NAME_REJECTED], + ])(`updates state according to dispatched action`, actionType => { + const initialState = { + value: 0, + }; + + const payload = actionType === Actions.GET_TEMPLATE_NAME_FULFILLED ? 1 : undefined; + + const action = { + type: actionType as keyof typeof Actions, + payload, + }; + + expect(TemplateNameReducer(initialState, action)).toMatchSnapshot(); + }); +}); diff --git a/templates/Feature/TemplateNameReducer.ts b/templates/Feature/TemplateNameReducer.ts new file mode 100644 index 0000000..741c148 --- /dev/null +++ b/templates/Feature/TemplateNameReducer.ts @@ -0,0 +1,52 @@ +import {Actions} from './actionTypes'; + +export type State = { + value?: number; + isLoading?: boolean; + hasError?: boolean; + isFulfilled?: boolean; +}; + +const initialState = { + templateName: undefined, + isLoading: false, + hasError: false, + isFulfilled: false, +} as State; + +export type Action = { + type: keyof typeof Actions; + payload?: number; +}; + +export const TemplateNameReducer = (state = initialState, action: Action): State => { + switch (action.type) { + case Actions.GET_TEMPLATE_NAME_PENDING: + return { + ...state, + isFulfilled: false, + isLoading: true, + hasError: false, + value: undefined, + }; + + case Actions.GET_TEMPLATE_NAME_FULFILLED: + return { + isFulfilled: true, + isLoading: false, + hasError: false, + value: action?.payload, + }; + + case Actions.GET_TEMPLATE_NAME_REJECTED: + return { + isFulfilled: false, + isLoading: false, + hasError: true, + value: undefined, + }; + + default: + return state; + } +}; diff --git a/templates/Feature/actionTypes.ts b/templates/Feature/actionTypes.ts new file mode 100644 index 0000000..7ec8721 --- /dev/null +++ b/templates/Feature/actionTypes.ts @@ -0,0 +1,6 @@ +export enum Actions { + GET_TEMPLATE_NAME = 'GET_TEMPLATE_NAME', + GET_TEMPLATE_NAME_REJECTED = 'GET_TEMPLATE_NAME_REJECTED', + GET_TEMPLATE_NAME_FULFILLED = 'GET_TEMPLATE_NAME_FULFILLED', + GET_TEMPLATE_NAME_PENDING = 'GET_TEMPLATE_NAME_PENDING', +} diff --git a/templates/Feature/index.ts b/templates/Feature/index.ts new file mode 100644 index 0000000..d104707 --- /dev/null +++ b/templates/Feature/index.ts @@ -0,0 +1,3 @@ +export {TemplateNameReducer} from './TemplateNameReducer'; +export {useTemplateName, useTemplateNameLoadingState} from './selectors'; +export {useGetTemplateNameQuery} from './useGetTemplateNameQuery'; diff --git a/templates/Feature/selectors.spec.tsx b/templates/Feature/selectors.spec.tsx new file mode 100644 index 0000000..b870c22 --- /dev/null +++ b/templates/Feature/selectors.spec.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import {Provider} from 'react-redux'; +import configureStore from 'redux-mock-store'; +import {renderHook} from '@testing-library/react'; + +import {useTemplateNameLoadingState, useTemplateName} from './selectors'; + +describe('features > counter > useTemplateName', () => { + const mockStore = configureStore([]); + + const state = { + templateName: { + value: 42, + }, + }; + + const store = mockStore(state); + + it('returns count value', () => { + /** + * Render hook, using testing-library utility + * @see https://react-hooks-testing-library.com/reference/api#renderhook + */ + const {result} = renderHook(() => useTemplateName(), { + wrapper: ({children}) => {children}, + }); + + expect(result.current).toBe(state.templateName.value); + }); +}); + +describe('features > counter > useTemplateNameLoadingState', () => { + const mockStore = configureStore([]); + + const state = { + templateName: { + isLoading: true, + hasError: true, + isFulfilled: true, + foo: 'bar', + }, + }; + + const store = mockStore(state); + + it('returns count value', () => { + /** + * Render hook, using testing-library utility + * @see https://react-hooks-testing-library.com/reference/api#renderhook + */ + const {result} = renderHook(() => useTemplateNameLoadingState(), { + wrapper: ({children}) => {children}, + }); + + /* We expect hook to return certain values from the state, but not all state */ + expect(result.current).not.toBe(state.templateName); + expect(state.templateName).toMatchObject(result.current); + }); +}); diff --git a/templates/Feature/selectors.ts b/templates/Feature/selectors.ts new file mode 100644 index 0000000..0ab16e2 --- /dev/null +++ b/templates/Feature/selectors.ts @@ -0,0 +1,16 @@ +import {useSelector} from 'react-redux'; + +import type {State} from './TemplateNameReducer'; + +/** + * Custom React Hooks to get random.org API loading state and response from the state. + * + * @see https://reactjs.org/docs/hooks-custom.html + */ +export const useTemplateNameLoadingState = () => { + const {isLoading, hasError, isFulfilled} = useSelector<{templateName: State}, State>(state => state.templateName); + return {isLoading, hasError, isFulfilled}; +}; + +export const useTemplateName = () => + useSelector<{templateName: State}, number | undefined>(state => state.templateName.value); diff --git a/templates/Feature/useGetTemplateNameQuery.spec.tsx b/templates/Feature/useGetTemplateNameQuery.spec.tsx new file mode 100644 index 0000000..8753ef9 --- /dev/null +++ b/templates/Feature/useGetTemplateNameQuery.spec.tsx @@ -0,0 +1,125 @@ +import React from 'react'; +import {Provider} from 'react-redux'; +import configureStore from 'redux-mock-store'; +import {waitFor, renderHook} from '@testing-library/react'; + +import {promiseResolverMiddleware} from '@/src/state/promiseResolverMiddleware'; + +import {Actions} from './actionTypes'; +import {useGetTemplateNameQuery} from './useGetTemplateNameQuery'; + +const fetchBackup = global.fetch; + +const fetchMock = jest.fn(); + +/** Create mock store with middlewares */ +// @ts-expect-error TS2322 +const mockStore = configureStore([promiseResolverMiddleware]); + +const store = mockStore({ + random: { + isLoading: false, + hasError: false, + isFulfilled: false, + }, +}); + +describe('features > counter > useGetTemplateNameQuery', () => { + beforeAll(() => { + global.fetch = fetchMock; + }); + + beforeEach(() => { + fetchMock.mockClear(); + }); + + afterAll(() => { + global.fetch = fetchBackup; + }); + + it('returns function', () => { + /** + * Render hook, using testing-library utility + * @see https://react-hooks-testing-library.com/reference/api#renderhook + */ + const {result} = renderHook(() => useGetTemplateNameQuery(), { + wrapper: ({children}) => {children}, + }); + + expect(result.current).toBeInstanceOf(Function); + }); + + describe('gets number', () => { + afterEach(() => { + store.clearActions(); + }); + + /** Note that tests functions are async */ + it(`handles successful API query`, async () => { + const {result} = renderHook(() => useGetTemplateNameQuery(), { + wrapper: ({children}) => {children}, + }); + + /** Mock response from API */ + const response = 6; + + /** + * Mock fetch successful response + * @see https://developer.mozilla.org/en-US/docs/Web/API/fetch + */ + fetchMock.mockImplementationOnce(() => + Promise.resolve({ + json: () => Promise.resolve(response), + }) + ); + + /** + * Wait until async action finishes + */ + await result.current(); + + /** First dispatched action should have _PENDING suffix */ + expect(store.getActions()[0]).toEqual({ + type: Actions.GET_TEMPLATE_NAME_PENDING, + }); + + await waitFor(() => { + /** Second dispatched action should have _FULFILLED suffix */ + expect(store.getActions()[1].type).toEqual(Actions.GET_TEMPLATE_NAME_FULFILLED); + /** Second dispatched action should deliver response from API */ + expect(store.getActions()[1].payload).toEqual(response); + }); + }); + + it(`handles rejected API query`, async () => { + const {result} = renderHook(() => useGetTemplateNameQuery(), { + wrapper: ({children}) => {children}, + }); + + /** + * Mock fetch rejected response + * @see https://developer.mozilla.org/en-US/docs/Web/API/fetch + */ + fetchMock.mockImplementationOnce(() => + Promise.reject({ + json: () => Promise.reject(new Error('')), + }) + ); + + /** + * Wait until async action finishes + */ + await result.current(); + + /** First dispatched action should have _PENDING suffix */ + expect(store.getActions()[0]).toEqual({ + type: Actions.GET_TEMPLATE_NAME_PENDING, + }); + + await waitFor(() => { + /** Second dispatched action should have _REJECTED suffix */ + expect(store.getActions()[1].type).toEqual(Actions.GET_TEMPLATE_NAME_REJECTED); + }); + }); + }); +}); diff --git a/templates/Feature/useGetTemplateNameQuery.ts b/templates/Feature/useGetTemplateNameQuery.ts new file mode 100644 index 0000000..c4f88d6 --- /dev/null +++ b/templates/Feature/useGetTemplateNameQuery.ts @@ -0,0 +1,18 @@ +import {useCallback} from 'react'; +import {useDispatch} from 'react-redux'; + +import {Actions} from './actionTypes'; + +export const useGetTemplateNameQuery = () => { + const dispatch = useDispatch(); + return useCallback( + () => + dispatch({ + type: Actions.GET_TEMPLATE_NAME, + payload: fetch('https://www.random.org', { + method: 'GET', + }), + }), + [dispatch] + ); +}; diff --git a/templates/Loading/TemplateName.module.css b/templates/Loading/TemplateName.module.css new file mode 100644 index 0000000..bee4745 --- /dev/null +++ b/templates/Loading/TemplateName.module.css @@ -0,0 +1,37 @@ +.templateName { + border: 1px solid lightgray; + margin-bottom: 24px; + padding: 24px; + width: 240px; +} + +.header { + font-size: 24px; + font-weight: normal; + margin: 0 0 12px; + text-align: center; +} + +.button { + background: lightseagreen; + border: none; + border-radius: 5px; + color: white; + cursor: pointer; + display: block; + font-size: 16px; + margin: 0 auto 24px; + padding: 12px 24px; + text-shadow: 1px 1px 1px rgb(0 0 0 / 50%); +} + +.button:disabled { + background: lightgray; + cursor: not-allowed; +} + +.button:active:not(:disabled) { + left: 1px; + position: relative; + top: 1px; +} diff --git a/templates/Loading/TemplateName.spec.tsx b/templates/Loading/TemplateName.spec.tsx new file mode 100644 index 0000000..5c5606f --- /dev/null +++ b/templates/Loading/TemplateName.spec.tsx @@ -0,0 +1,256 @@ +import React from 'react'; +import {Provider} from 'react-redux'; +import type {Store, Action} from 'redux'; +import {render, fireEvent, waitFor, screen, waitForElementToBeRemoved} from '@testing-library/react'; +import configureStore from 'redux-mock-store'; + +import {GET_RANDOM_NUMBER} from '@/src/features/random/actionTypes'; +import {makeStore} from '@/src/state/store'; +import {promiseResolverMiddleware} from '@/src/state/promiseResolverMiddleware'; + +import {TemplateName} from './TemplateName'; + +/** + * Create mock store + * @see https://github.com/dmitry-zaets/redux-mock-store + */ +// @ts-expect-error TS2322 +const mockStore = configureStore([promiseResolverMiddleware]); + +/* We use these strings to match HTMLElements */ +const pristineText = 'Click the button to get random number'; +const loadingText = 'Getting number'; +const errorText = 'Ups...'; +const response = 6; + +let realStore: Store; + +const fetchBackup = global.fetch; + +const fetchMock = jest.fn(); + +describe('components > TemplateName', () => { + beforeAll(() => { + global.fetch = fetchMock; + }); + + beforeEach(() => { + realStore = makeStore(); + fetchMock.mockClear(); + }); + + afterAll(() => { + global.fetch = fetchBackup; + }); + /** + * Provide table of values to run tests with + * @see https://jestjs.io/docs/en/api#describeeachtablename-fn-timeout + */ + describe.each` + isLoading | hasError | isFulfilled + ${false} | ${false} | ${false} + ${true} | ${false} | ${false} + ${false} | ${true} | ${false} + ${false} | ${false} | ${true} + `('renders different store states', ({isLoading, hasError, isFulfilled}) => { + it(`when isLoading === ${isLoading} && hasError === ${hasError} && isFulfilled === ${isFulfilled}`, () => { + const store = mockStore({ + random: { + isLoading, + hasError, + isFulfilled, + number: isFulfilled ? 1 : undefined, + }, + }); + + /** + * `asFragment`: + * @see https://testing-library.com/docs/react-testing-library/api#asfragment + * `wrapper`: + * @see https://testing-library.com/docs/react-testing-library/api#wrapper + */ + const {asFragment} = render(, { + wrapper: ({children}) => {children}, + }); + + /** + * Basic snapshot test to check, if rendered component + * matches expected footprint. + */ + expect(asFragment()).toMatchSnapshot(); + }); + }); + + it('handles successful request', async () => { + /** + * Mock fetch successful response + * @see https://developer.mozilla.org/en-US/docs/Web/API/fetch + */ + fetchMock.mockImplementationOnce(() => + Promise.resolve({ + json: () => Promise.resolve(response), + }) + ); + + /** + * `getByRole`: + * @see https://testing-library.com/docs/dom-testing-library/api-queries#byrole + */ + const {asFragment, getByRole} = render(, { + wrapper: ({children}) => ( + /* We use real store here, to get action through */ + {children} + ), + }); + + /** + * Search for the button and make testing library click on it + * @see https://testing-library.com/docs/react-testing-library/cheatsheet#events + */ + fireEvent.click(getByRole('button')); + + /** Check that initial message has changed to loading. */ + expect(asFragment()).toMatchSnapshot(); + expect(screen.queryByText(pristineText)).not.toBeInTheDocument(); + expect(screen.queryByText(loadingText)).toBeInTheDocument(); + + /** Check that loading message has changed to success. */ + await waitForElementToBeRemoved(() => screen.queryByText(loadingText)); + expect(asFragment()).toMatchSnapshot(); + expect(screen.queryByText(pristineText)).not.toBeInTheDocument(); + expect(screen.queryByText(loadingText)).not.toBeInTheDocument(); + expect(screen.queryByText(response)).toBeInTheDocument(); + }); + + it('handles rejected request', async () => { + /** + * Mock fetch rejected response + * @see https://developer.mozilla.org/en-US/docs/Web/API/fetch + */ + fetchMock.mockImplementationOnce(() => + Promise.reject({ + json: () => Promise.reject(new Error('')), + }) + ); + + /** + * `getByRole`: + * @see https://testing-library.com/docs/dom-testing-library/api-queries#byrole + */ + const {asFragment, getByRole} = render(, { + wrapper: ({children}) => ( + /* We use real store here, to get action through */ + {children} + ), + }); + + /** + * Search for the button and make testing library click on it + * @see https://testing-library.com/docs/react-testing-library/cheatsheet#events + */ + fireEvent.click(getByRole('button')); + + /** Check that initial message has changed to loading. */ + expect(asFragment()).toMatchSnapshot(); + expect(screen.queryByText(pristineText)).not.toBeInTheDocument(); + expect(screen.queryByText(loadingText)).toBeInTheDocument(); + + /** Check that loading message has changed to error. */ + await waitForElementToBeRemoved(() => screen.queryByText(loadingText)); + expect(asFragment()).toMatchSnapshot(); + expect(screen.queryByText(pristineText)).not.toBeInTheDocument(); + expect(screen.queryByText(loadingText)).not.toBeInTheDocument(); + expect(screen.queryByText(errorText)).toBeInTheDocument(); + }); + + it('dispatches an action sequence on successful request made', async () => { + const store = mockStore({ + random: { + isLoading: false, + hasError: false, + isFulfilled: false, + }, + }); + + /** + * Mock fetch successful response + * @see https://developer.mozilla.org/en-US/docs/Web/API/fetch + */ + fetchMock.mockImplementationOnce(() => + Promise.resolve({ + json: () => Promise.resolve(response), + }) + ); + + /** + * `getByRole`: + * @see https://testing-library.com/docs/dom-testing-library/api-queries#byrole + */ + const {getByRole} = render(, { + wrapper: ({children}) => {children}, + }); + + /** + * Search for the button and make testing library click on it + * @see https://testing-library.com/docs/react-testing-library/cheatsheet#events + */ + fireEvent.click(getByRole('button')); + + /** First dispatched action should have _PENDING suffix */ + expect(store.getActions()[0]).toEqual({ + type: `${GET_RANDOM_NUMBER}_PENDING`, + }); + + await waitFor(() => { + /** Second dispatched action should have _FULFILLED suffix */ + expect(store.getActions()[1].type).toEqual(`${GET_RANDOM_NUMBER}_FULFILLED`); + }); + + /** Second dispatched action should deliver response from API */ + expect(store.getActions()[1].payload).toEqual(response); + }); + + it('dispatches an action sequence on rejected request made', async () => { + const store = mockStore({ + random: { + isLoading: false, + hasError: false, + isFulfilled: false, + }, + }); + + /** + * Mock fetch rejected response + * @see https://developer.mozilla.org/en-US/docs/Web/API/fetch + */ + fetchMock.mockImplementationOnce(() => + Promise.reject({ + json: () => Promise.reject(new Error('')), + }) + ); + + /** + * `getByRole`: + * @see https://testing-library.com/docs/dom-testing-library/api-queries#byrole + */ + const {getByRole} = render(, { + wrapper: ({children}) => {children}, + }); + + /** + * Search for the button and make testing library click on it + * @see https://testing-library.com/docs/react-testing-library/cheatsheet#events + */ + fireEvent.click(getByRole('button')); + + /** First dispatched action should have _PENDING suffix */ + expect(store.getActions()[0]).toEqual({ + type: `${GET_RANDOM_NUMBER}_PENDING`, + }); + + await waitFor(() => { + /** Second dispatched action should have _REJECTED suffix */ + expect(store.getActions()[1].type).toEqual(`${GET_RANDOM_NUMBER}_REJECTED`); + }); + }); +}); diff --git a/templates/Loading/TemplateName.tsx b/templates/Loading/TemplateName.tsx new file mode 100644 index 0000000..6e546ab --- /dev/null +++ b/templates/Loading/TemplateName.tsx @@ -0,0 +1,38 @@ +'use client'; + +import React from 'react'; + +import {useGetRandomNumberQuery, useRandomNumber, useLoadingState} from '@/src/features/random'; + +import classes from './TemplateName.module.css'; + +export const TemplateName = () => { + /** Loading state of random.org request from Redux store */ + const {isLoading, hasError, isFulfilled} = useLoadingState(); + + /** Random number value */ + const number = useRandomNumber(); + + /** Create incrementCounter action, using custom hook from feature */ + const getNumber = useGetRandomNumberQuery(); + + /** Define pristine state condition, when user didn't do any actions */ + const isPristine = !isLoading && !hasError && !isFulfilled; + + return ( +
+

Async Random

+ + {isPristine &&
Click the button to get random number
} + {isLoading &&
Getting number
} + {isFulfilled && ( +
+ Number from random.org: {number} +
+ )} + {hasError &&
Ups...
} +
+ ); +}; diff --git a/templates/Loading/index.ts b/templates/Loading/index.ts new file mode 100644 index 0000000..9347b65 --- /dev/null +++ b/templates/Loading/index.ts @@ -0,0 +1 @@ +export {TemplateName} from './TemplateName'; diff --git a/tsconfig.json b/tsconfig.json index a6f13cb..6b8bf07 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,7 +20,9 @@ "incremental": true, "baseUrl": ".", "paths": { - "@/*": ["./*"] + "@/*": [ + "./*" + ] }, "plugins": [ { @@ -41,7 +43,8 @@ ".eslintrc.cjs", "templates/**/*.ts", "templates/**/*.tsx", - "build/types/**/*.ts" + "build/types/**/*.ts", + ".next/types/**/*.ts" ], "exclude": [ "node_modules" From b1f1cfc0d549f21fa6a7f87f529c58bf55978c62 Mon Sep 17 00:00:00 2001 From: Dima Vyshniakov Date: Thu, 16 May 2024 18:10:03 +0200 Subject: [PATCH 3/3] Add Next cache --- .github/workflows/merge-jobs.yml | 11 +++++++++++ .github/workflows/pages.yml | 15 +++++++++++++++ .github/workflows/pull-request-jobs.yml | 11 +++++++++++ 3 files changed, 37 insertions(+) diff --git a/.github/workflows/merge-jobs.yml b/.github/workflows/merge-jobs.yml index ff1beea..a413ca2 100644 --- a/.github/workflows/merge-jobs.yml +++ b/.github/workflows/merge-jobs.yml @@ -48,6 +48,17 @@ jobs: restore-keys: | ${{ runner.os }}-pnpm-store- + - name: Setup Next.js cache + uses: actions/cache@v4 + with: + path: | + .next/cache + # Generate a new cache whenever packages or source files change. + key: ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }} + # If source files changed but packages didn't, rebuild from a prior cache. + restore-keys: | + ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}- + - name: Run merge tasks run: | pnpm install diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index cc117e0..b4b0bdd 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -63,6 +63,17 @@ jobs: restore-keys: | ${{ runner.os }}-pnpm-store- + - name: Setup Next.js cache + uses: actions/cache@v4 + with: + path: | + .next/cache + # Generate a new cache whenever packages or source files change. + key: ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }} + # If source files changed but packages didn't, rebuild from a prior cache. + restore-keys: | + ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}- + - name: Build docs run: | pnpm install @@ -70,6 +81,10 @@ jobs: - name: Setup Pages uses: actions/configure-pages@v5 + with: + # Automatically inject basePath in your Next.js configuration file and disable + # server side image optimization (https://nextjs.org/docs/api-reference/next/image#unoptimized). + static_site_generator: next - name: Upload artifact uses: actions/upload-pages-artifact@v3 diff --git a/.github/workflows/pull-request-jobs.yml b/.github/workflows/pull-request-jobs.yml index 142f2e8..fe393c8 100644 --- a/.github/workflows/pull-request-jobs.yml +++ b/.github/workflows/pull-request-jobs.yml @@ -48,6 +48,17 @@ jobs: restore-keys: | ${{ runner.os }}-pnpm-store- + - name: Setup Next.js cache + uses: actions/cache@v4 + with: + path: | + .next/cache + # Generate a new cache whenever packages or source files change. + key: ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }} + # If source files changed but packages didn't, rebuild from a prior cache. + restore-keys: | + ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}- + - name: Run test tasks run: | pnpm install