├── src ├── index.css ├── services │ ├── index.ts │ └── todo │ │ ├── index.ts │ │ ├── todo.service.ts │ │ └── todo.service.spec.ts ├── models │ ├── index.ts │ ├── todo │ │ ├── index.ts │ │ ├── testing │ │ │ ├── index.ts │ │ │ └── todo.mock.ts │ │ └── todo.model.ts │ └── testing │ │ └── index.ts ├── store │ ├── index.ts │ ├── todo │ │ ├── actions │ │ │ ├── index.ts │ │ │ ├── todo.action.ts │ │ │ └── todo.action.spec.ts │ │ ├── reducers │ │ │ ├── index.ts │ │ │ ├── todo.reducer.ts │ │ │ └── todo.reducer.spec.ts │ │ ├── states │ │ │ ├── index.ts │ │ │ └── todo.state.ts │ │ ├── selectors │ │ │ ├── index.ts │ │ │ ├── todo.selector.ts │ │ │ └── todo.selector.spec.ts │ │ └── index.ts │ └── store.ts ├── pages │ └── todo │ │ ├── todo-edit │ │ ├── components │ │ │ ├── index.ts │ │ │ └── todo-edit │ │ │ │ ├── index.ts │ │ │ │ ├── todo-edit.presenter.spec.ts │ │ │ │ ├── todo-edit.component.spec.tsx │ │ │ │ ├── __snapshots__ │ │ │ │ └── todo-edit.component.spec.tsx.snap │ │ │ │ ├── todo-edit.presenter.ts │ │ │ │ └── todo-edit.component.tsx │ │ ├── containers │ │ │ ├── index.ts │ │ │ └── todo-edit │ │ │ │ ├── index.ts │ │ │ │ ├── todo-edit.container.tsx │ │ │ │ ├── todo-edit.facade.ts │ │ │ │ ├── todo-edit.container.spec.tsx │ │ │ │ └── todo-edit.facade.spec.tsx │ │ ├── index.ts │ │ └── todo-edit.page.tsx │ │ ├── todo-list │ │ ├── components │ │ │ ├── index.ts │ │ │ └── todo-list │ │ │ │ ├── index.ts │ │ │ │ ├── todo-list.component.spec.tsx │ │ │ │ ├── __snapshots__ │ │ │ │ └── todo-list.component.spec.tsx.snap │ │ │ │ └── todo-list.component.tsx │ │ ├── containers │ │ │ ├── index.ts │ │ │ └── todo-list │ │ │ │ ├── index.ts │ │ │ │ ├── todo-list.container.tsx │ │ │ │ ├── todo-list.container.spec.tsx │ │ │ │ ├── todo-list.facade.ts │ │ │ │ └── todo-list.facade.spec.tsx │ │ ├── index.ts │ │ ├── todo-list.page.tsx │ │ ├── todo-list.params.tsx │ │ ├── todo-list.page.spec.tsx │ │ └── todo-list.params.spec.tsx │ │ ├── todo-create │ │ ├── components │ │ │ ├── index.ts │ │ │ └── todo-create │ │ │ │ ├── index.ts │ │ │ │ ├── todo-create.presenter.spec.ts │ │ │ │ ├── todo-create.component.spec.tsx │ │ │ │ ├── todo-create.presenter.ts │ │ │ │ ├── todo-create.component.tsx │ │ │ │ └── __snapshots__ │ │ │ │ └── todo-create.component.spec.tsx.snap │ │ ├── containers │ │ │ ├── index.ts │ │ │ └── todo-create │ │ │ │ ├── index.ts │ │ │ │ ├── todo-create.container.tsx │ │ │ │ ├── todo-create.facade.ts │ │ │ │ ├── todo-create.container.spec.tsx │ │ │ │ └── todo-create.facade.spec.tsx │ │ ├── index.ts │ │ └── todo-create.page.tsx │ │ ├── todo-detail │ │ ├── components │ │ │ ├── index.ts │ │ │ └── todo-detail │ │ │ │ ├── index.ts │ │ │ │ ├── todo-detail.component.spec.tsx │ │ │ │ ├── __snapshots__ │ │ │ │ └── todo-detail.component.spec.tsx.snap │ │ │ │ └── todo-detail.component.tsx │ │ ├── containers │ │ │ ├── index.ts │ │ │ └── todo-detail │ │ │ │ ├── index.ts │ │ │ │ ├── todo-detail.container.tsx │ │ │ │ ├── todo-detail.facade.ts │ │ │ │ ├── todo-detail.container.spec.tsx │ │ │ │ └── todo-detail.facade.spec.tsx │ │ ├── index.ts │ │ ├── todo-detail.params.tsx │ │ ├── todo-detail.page.tsx │ │ └── todo-detail.params.spec.tsx │ │ ├── index.ts │ │ └── todo.route.tsx ├── react-app-env.d.ts ├── setupTests.ts ├── app.spec.tsx ├── app.tsx └── index.tsx ├── .prettierrc ├── public ├── favicon.ico ├── logo192.png ├── logo512.png ├── robots.txt ├── manifest.json └── index.html ├── .prettierignore ├── .vscode ├── extensions.json └── settings.json ├── .gitignore ├── tsconfig.json ├── package.json └── README.md /src/index.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './todo' -------------------------------------------------------------------------------- /src/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './todo'; 2 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | export * from './store'; 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /src/models/todo/index.ts: -------------------------------------------------------------------------------- 1 | export * from './todo.model'; 2 | -------------------------------------------------------------------------------- /src/models/testing/index.ts: -------------------------------------------------------------------------------- 1 | export * from '../todo/testing'; 2 | -------------------------------------------------------------------------------- /src/models/todo/testing/index.ts: -------------------------------------------------------------------------------- 1 | export * from './todo.mock'; 2 | -------------------------------------------------------------------------------- /src/store/todo/actions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './todo.action'; 2 | -------------------------------------------------------------------------------- /src/store/todo/reducers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './todo.reducer'; 2 | -------------------------------------------------------------------------------- /src/store/todo/states/index.ts: -------------------------------------------------------------------------------- 1 | export * from './todo.state'; 2 | -------------------------------------------------------------------------------- /src/store/todo/selectors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './todo.selector'; 2 | -------------------------------------------------------------------------------- /src/pages/todo/todo-edit/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './todo-edit'; 2 | -------------------------------------------------------------------------------- /src/pages/todo/todo-edit/containers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './todo-edit'; 2 | -------------------------------------------------------------------------------- /src/pages/todo/todo-list/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './todo-list'; 2 | -------------------------------------------------------------------------------- /src/pages/todo/todo-list/containers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './todo-list'; 2 | -------------------------------------------------------------------------------- /src/pages/todo/todo-create/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './todo-create'; 2 | -------------------------------------------------------------------------------- /src/pages/todo/todo-create/containers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './todo-create'; 2 | -------------------------------------------------------------------------------- /src/pages/todo/todo-detail/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './todo-detail'; 2 | -------------------------------------------------------------------------------- /src/pages/todo/todo-detail/containers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './todo-detail'; 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puku0x/todo-react/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puku0x/todo-react/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puku0x/todo-react/HEAD/public/logo512.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/pages/todo/todo-edit/components/todo-edit/index.ts: -------------------------------------------------------------------------------- 1 | export * from './todo-edit.component'; 2 | -------------------------------------------------------------------------------- /src/pages/todo/todo-edit/containers/todo-edit/index.ts: -------------------------------------------------------------------------------- 1 | export * from './todo-edit.container'; 2 | -------------------------------------------------------------------------------- /src/pages/todo/todo-list/components/todo-list/index.ts: -------------------------------------------------------------------------------- 1 | export * from './todo-list.component'; 2 | -------------------------------------------------------------------------------- /src/pages/todo/todo-list/containers/todo-list/index.ts: -------------------------------------------------------------------------------- 1 | export * from './todo-list.container'; 2 | -------------------------------------------------------------------------------- /src/pages/todo/todo-create/components/todo-create/index.ts: -------------------------------------------------------------------------------- 1 | export * from './todo-create.component'; 2 | -------------------------------------------------------------------------------- /src/pages/todo/todo-create/containers/todo-create/index.ts: -------------------------------------------------------------------------------- 1 | export * from './todo-create.container'; 2 | -------------------------------------------------------------------------------- /src/pages/todo/todo-detail/components/todo-detail/index.ts: -------------------------------------------------------------------------------- 1 | export * from './todo-detail.component'; 2 | -------------------------------------------------------------------------------- /src/pages/todo/todo-detail/containers/todo-detail/index.ts: -------------------------------------------------------------------------------- 1 | export * from './todo-detail.container'; 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | 3 | /build 4 | /coverage 5 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /src/store/todo/index.ts: -------------------------------------------------------------------------------- 1 | export * from './actions'; 2 | export * from './reducers'; 3 | export * from './selectors'; 4 | export * from './states'; 5 | -------------------------------------------------------------------------------- /src/pages/todo/index.ts: -------------------------------------------------------------------------------- 1 | import { lazy } from 'react'; 2 | 3 | export const TodoPage = lazy(() => 4 | import('./todo.route').then((m) => ({ default: m.TodoRoute })) 5 | ); 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "styled-components.vscode-styled-components" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /src/pages/todo/todo-edit/index.ts: -------------------------------------------------------------------------------- 1 | import { lazy } from 'react'; 2 | 3 | export const TodoEditPage = lazy(() => 4 | import('./todo-edit.page').then((m) => ({ default: m.TodoEditPage })) 5 | ); 6 | -------------------------------------------------------------------------------- /src/pages/todo/todo-list/index.ts: -------------------------------------------------------------------------------- 1 | import { lazy } from 'react'; 2 | 3 | export const TodoListPage = lazy(() => 4 | import('./todo-list.page').then((m) => ({ default: m.TodoListPage })) 5 | ); 6 | -------------------------------------------------------------------------------- /src/services/todo/index.ts: -------------------------------------------------------------------------------- 1 | import { TodoService } from './todo.service'; 2 | 3 | export const todoService = new TodoService( 4 | 'https://us-central1-todo-api-f1511.cloudfunctions.net/api' 5 | ); 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": true 4 | }, 5 | "editor.defaultFormatter": "esbenp.prettier-vscode", 6 | "editor.formatOnSave": true 7 | } 8 | -------------------------------------------------------------------------------- /src/pages/todo/todo-create/index.ts: -------------------------------------------------------------------------------- 1 | import { lazy } from 'react'; 2 | 3 | export const TodoCreatePage = lazy(() => 4 | import('./todo-create.page').then((m) => ({ default: m.TodoCreatePage })) 5 | ); 6 | -------------------------------------------------------------------------------- /src/pages/todo/todo-detail/index.ts: -------------------------------------------------------------------------------- 1 | import { lazy } from 'react'; 2 | 3 | export const TodoDetailPage = lazy(() => 4 | import('./todo-detail.page').then((m) => ({ default: m.TodoDetailPage })) 5 | ); 6 | -------------------------------------------------------------------------------- /src/pages/todo/todo-create/todo-create.page.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | 3 | import { TodoCreateContainer } from './containers'; 4 | 5 | export const TodoCreatePage = memo(() => { 6 | return ; 7 | }); 8 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/pages/todo/todo-detail/todo-detail.params.tsx: -------------------------------------------------------------------------------- 1 | import { useParams } from 'react-router-dom'; 2 | 3 | interface RouterParams { 4 | id: string; 5 | } 6 | 7 | export const useTodoDetailParams = () => { 8 | const { id } = useParams(); 9 | 10 | return { 11 | id, 12 | } as const; 13 | }; 14 | -------------------------------------------------------------------------------- /src/pages/todo/todo-detail/todo-detail.page.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | 3 | import { TodoDetailContainer } from './containers'; 4 | import { useTodoDetailParams } from './todo-detail.params'; 5 | 6 | export const TodoDetailPage = memo(() => { 7 | const { id } = useTodoDetailParams(); 8 | 9 | return ; 10 | }); 11 | -------------------------------------------------------------------------------- /src/models/todo/todo.model.ts: -------------------------------------------------------------------------------- 1 | export interface Todo { 2 | id: string; 3 | title: string; 4 | completed: boolean; 5 | createdAt: number; 6 | updatedAt: number; 7 | } 8 | 9 | export interface TodoCreateDto { 10 | title: string; 11 | } 12 | 13 | export interface TodoUpdateDto { 14 | id: string; 15 | title: string; 16 | completed: boolean; 17 | } 18 | -------------------------------------------------------------------------------- /src/app.spec.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | import { MemoryRouter } from 'react-router-dom'; 3 | 4 | import { App } from './app'; 5 | 6 | describe('App', () => { 7 | it('should render', () => { 8 | const { baseElement } = render(, { wrapper: MemoryRouter }); 9 | 10 | expect(baseElement).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/pages/todo/todo-list/todo-list.page.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | 3 | import { TodoListContainer } from './containers'; 4 | import { useTodoListParams } from './todo-list.params'; 5 | 6 | export const TodoListPage = memo(() => { 7 | const { offset, limit } = useTodoListParams(); 8 | 9 | return ; 10 | }); 11 | -------------------------------------------------------------------------------- /src/pages/todo/todo-edit/todo-edit.page.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import { useParams } from 'react-router-dom'; 3 | 4 | import { TodoEditContainer } from './containers'; 5 | 6 | interface RouterParams { 7 | id: string; 8 | } 9 | 10 | export const TodoEditPage = memo(() => { 11 | const { id } = useParams(); 12 | 13 | return ; 14 | }); 15 | -------------------------------------------------------------------------------- /src/pages/todo/todo-create/containers/todo-create/todo-create.container.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | 3 | import { TodoCreate } from '../../components'; 4 | import { useTodoCreateFacade } from './todo-create.facade'; 5 | 6 | export const TodoCreateContainer = memo(() => { 7 | const { isFetching, create } = useTodoCreateFacade(); 8 | 9 | return ; 10 | }); 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | .eslintcache 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/pages/todo/todo-list/todo-list.params.tsx: -------------------------------------------------------------------------------- 1 | import { useLocation } from 'react-router-dom'; 2 | 3 | export const useTodoListParams = () => { 4 | const location = useLocation(); 5 | const params = new URLSearchParams(location.search); 6 | const limitParam = params.get('limit') || '10'; 7 | const offsetParam = params.get('offset') || '0'; 8 | 9 | return { 10 | limit: +limitParam, 11 | offset: +offsetParam, 12 | } as const; 13 | }; 14 | -------------------------------------------------------------------------------- /src/app.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react'; 2 | import { Route, Switch, Redirect } from 'react-router-dom'; 3 | 4 | import { TodoPage } from './pages/todo'; 5 | 6 | export const App = () => { 7 | return ( 8 | Loading...}> 9 | 10 | 11 | } /> 12 | 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /src/store/todo/states/todo.state.ts: -------------------------------------------------------------------------------- 1 | import { EntityState, createEntityAdapter } from '@reduxjs/toolkit'; 2 | 3 | import { Todo } from '../../../models'; 4 | 5 | export const featureKey = 'todos'; 6 | 7 | export interface State extends EntityState { 8 | isFetching: boolean; 9 | selectedId: string | null; 10 | } 11 | 12 | export const adapter = createEntityAdapter(); 13 | 14 | export const initialState: State = adapter.getInitialState({ 15 | isFetching: false, 16 | selectedId: null, 17 | }); 18 | -------------------------------------------------------------------------------- /src/pages/todo/todo-detail/containers/todo-detail/todo-detail.container.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | 3 | import { TodoDetail } from '../../components'; 4 | import { useTodoDetailFacade } from './todo-detail.facade'; 5 | 6 | interface Props { 7 | id: string; 8 | } 9 | 10 | export const TodoDetailContainer = memo((props: Props) => { 11 | const { id } = props; 12 | const { isFetching, todo } = useTodoDetailFacade({ id }); 13 | 14 | return ; 15 | }); 16 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import { Provider } from 'react-redux'; 5 | 6 | import './index.css'; 7 | import { store } from './store'; 8 | import { App } from './app'; 9 | 10 | render( 11 | 12 | 13 | 14 | 15 | 16 | 17 | , 18 | document.getElementById('root') 19 | ); 20 | -------------------------------------------------------------------------------- /src/pages/todo/todo-edit/containers/todo-edit/todo-edit.container.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | 3 | import { TodoEdit } from '../../components'; 4 | import { useTodoEditFacade } from './todo-edit.facade'; 5 | 6 | interface Props { 7 | id: string; 8 | } 9 | 10 | export const TodoEditContainer = memo((props: Props) => { 11 | const { id } = props; 12 | const { isFetching, todo, update } = useTodoEditFacade({ id }); 13 | 14 | return ; 15 | }); 16 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/store/store.ts: -------------------------------------------------------------------------------- 1 | import { 2 | configureStore, 3 | combineReducers, 4 | getDefaultMiddleware, 5 | } from '@reduxjs/toolkit'; 6 | 7 | import * as todo from './todo'; 8 | 9 | const reducer = combineReducers({ 10 | [todo.featureKey]: todo.reducer, 11 | }); 12 | 13 | const middleware = getDefaultMiddleware({ 14 | thunk: true, 15 | immutableCheck: true, 16 | serializableCheck: true, 17 | }); 18 | 19 | export const store = configureStore({ 20 | reducer, 21 | middleware, 22 | devTools: true, 23 | }); 24 | 25 | // export type AppState = ReturnType; 26 | // export type AppDispatch = typeof store.dispatch; 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/pages/todo/todo-detail/todo-detail.params.spec.tsx: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react-hooks'; 2 | import { MemoryRouter, Route } from 'react-router-dom'; 3 | 4 | import { useTodoDetailParams } from './todo-detail.params'; 5 | 6 | describe('useTodoDetailParams', () => { 7 | it('should render', () => { 8 | const { result } = renderHook(() => useTodoDetailParams(), { 9 | wrapper: ({ children }) => ( 10 | 11 | {children} 12 | 13 | ), 14 | }); 15 | const { id } = result.current; 16 | 17 | expect(id).toEqual('1'); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/pages/todo/todo-list/containers/todo-list/todo-list.container.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | 3 | import { TodoList } from '../../components'; 4 | import { useTodoListFacade } from './todo-list.facade'; 5 | 6 | interface Props { 7 | offset: number; 8 | limit: number; 9 | } 10 | 11 | export const TodoListContainer = memo((props: Props) => { 12 | const { offset, limit } = props; 13 | const { isFetching, todos, changeOffset, changeLimit } = useTodoListFacade({ 14 | offset, 15 | limit, 16 | }); 17 | 18 | return ( 19 | 27 | ); 28 | }); 29 | -------------------------------------------------------------------------------- /src/pages/todo/todo.route.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react'; 2 | import { Route, Switch } from 'react-router-dom'; 3 | 4 | import { TodoCreatePage } from './todo-create'; 5 | import { TodoDetailPage } from './todo-detail'; 6 | import { TodoEditPage } from './todo-edit'; 7 | import { TodoListPage } from './todo-list'; 8 | 9 | export const TodoRoute = () => { 10 | return ( 11 | loading...}> 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/store/todo/selectors/todo.selector.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from '@reduxjs/toolkit'; 2 | 3 | import { State, adapter, featureKey } from '../states'; 4 | 5 | interface RootState { 6 | [featureKey]: State; 7 | } 8 | 9 | const featureStateSelector = (state: RootState) => state[featureKey]; 10 | 11 | export const { selectAll: todosSelector, selectEntities: entitiesSelector } = 12 | adapter.getSelectors(featureStateSelector); 13 | 14 | export const isFetchingSelector = createSelector( 15 | featureStateSelector, 16 | (state) => state.isFetching 17 | ); 18 | 19 | export const selectedIdSelector = createSelector( 20 | featureStateSelector, 21 | (state) => state.selectedId 22 | ); 23 | 24 | export const todoSelector = createSelector( 25 | entitiesSelector, 26 | selectedIdSelector, 27 | (entities, id) => (id ? entities[id] || null : null) 28 | ); 29 | -------------------------------------------------------------------------------- /src/pages/todo/todo-create/components/todo-create/todo-create.presenter.spec.ts: -------------------------------------------------------------------------------- 1 | import { act, renderHook } from '@testing-library/react-hooks'; 2 | 3 | import { useTodoCreatePresenter } from './todo-create.presenter'; 4 | 5 | describe('useTodoCreatePresenter', () => { 6 | it('should handle submit', async () => { 7 | const onCreate = jest.fn(); 8 | const { result, waitFor } = renderHook(() => 9 | useTodoCreatePresenter({ onCreate }) 10 | ); 11 | 12 | await act(async () => { 13 | const { values, setValues } = result.current; 14 | setValues({ 15 | ...values, 16 | title: 'title', 17 | }); 18 | }); 19 | 20 | await act(async () => { 21 | const { handleSubmit } = result.current; 22 | handleSubmit(); 23 | }); 24 | 25 | await waitFor(() => { 26 | expect(onCreate).toHaveBeenCalled(); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/pages/todo/todo-detail/containers/todo-detail/todo-detail.facade.ts: -------------------------------------------------------------------------------- 1 | import { unwrapResult } from '@reduxjs/toolkit'; 2 | import { useCallback, useEffect } from 'react'; 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | 5 | import { 6 | fetchTodo, 7 | isFetchingSelector, 8 | todoSelector, 9 | } from '../../../../../store/todo'; 10 | 11 | export const useTodoDetailFacade = (arg: { id: string }) => { 12 | const { id } = arg; 13 | const dispatch = useDispatch(); 14 | const isFetching = useSelector(isFetchingSelector); 15 | const todo = useSelector(todoSelector); 16 | 17 | const fetch = useCallback( 18 | (arg: { id: string }) => { 19 | return dispatch(fetchTodo(arg)).then(unwrapResult); 20 | }, 21 | [dispatch] 22 | ); 23 | 24 | useEffect(() => { 25 | fetch({ id }); 26 | }, [id, fetch]); 27 | 28 | return { 29 | isFetching, 30 | todo, 31 | fetch, 32 | } as const; 33 | }; 34 | -------------------------------------------------------------------------------- /src/pages/todo/todo-list/todo-list.page.spec.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | import { MemoryRouter } from 'react-router-dom'; 3 | 4 | import { TodoListPage } from './todo-list.page'; 5 | import { useTodoListParams } from './todo-list.params'; 6 | 7 | jest.mock('./containers', () => ({ 8 | ...jest.requireActual('./containers'), 9 | TodoListContainer: jest.fn(() => null), 10 | })); 11 | 12 | jest.mock('./todo-list.params', () => ({ 13 | ...jest.requireActual('./todo-list.params'), 14 | useTodoListParams: jest.fn(() => ({ offset: 0, limit: 10 })), 15 | })); 16 | 17 | describe('TodoListPage', () => { 18 | it('should render', () => { 19 | const { baseElement } = render(, { 20 | wrapper: ({ children }) => {children}, 21 | }); 22 | 23 | expect(useTodoListParams).toHaveBeenCalledWith(); 24 | expect(baseElement).toBeTruthy(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/pages/todo/todo-create/components/todo-create/todo-create.component.spec.tsx: -------------------------------------------------------------------------------- 1 | import { act, render } from '@testing-library/react'; 2 | import { Fragment } from 'react'; 3 | import { MemoryRouter } from 'react-router-dom'; 4 | 5 | import { TodoCreate } from './todo-create.component'; 6 | 7 | describe('TodoCreate', () => { 8 | it('render', async () => { 9 | let result = render(); 10 | await act(async () => { 11 | result = render(, { 12 | wrapper: MemoryRouter, 13 | }); 14 | }); 15 | const { asFragment } = result; 16 | 17 | expect(asFragment()).toMatchSnapshot(); 18 | }); 19 | 20 | it('render with isFetching', async () => { 21 | let result = render(); 22 | await act(async () => { 23 | result = render(, { 24 | wrapper: MemoryRouter, 25 | }); 26 | }); 27 | const { asFragment } = result; 28 | 29 | expect(asFragment()).toMatchSnapshot(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/pages/todo/todo-create/containers/todo-create/todo-create.facade.ts: -------------------------------------------------------------------------------- 1 | import { unwrapResult } from '@reduxjs/toolkit'; 2 | import { useCallback } from 'react'; 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | import { useHistory } from 'react-router-dom'; 5 | 6 | import { TodoCreateDto } from '../../../../../models'; 7 | import { createTodo, isFetchingSelector } from '../../../../../store/todo'; 8 | 9 | export const useTodoCreateFacade = () => { 10 | const history = useHistory(); 11 | const dispatch = useDispatch(); 12 | const isFetching = useSelector(isFetchingSelector); 13 | 14 | const create = useCallback( 15 | (dto: TodoCreateDto) => { 16 | return dispatch(createTodo({ todo: dto })) 17 | .then(unwrapResult) 18 | .then((payload) => { 19 | const { todo } = payload; 20 | history.push(`/todos/${todo.id}`); 21 | }); 22 | }, 23 | [history, dispatch] 24 | ); 25 | 26 | return { 27 | isFetching, 28 | create, 29 | } as const; 30 | }; 31 | -------------------------------------------------------------------------------- /src/pages/todo/todo-detail/components/todo-detail/todo-detail.component.spec.tsx: -------------------------------------------------------------------------------- 1 | import { act, render } from '@testing-library/react'; 2 | import { Fragment } from 'react'; 3 | import { MemoryRouter } from 'react-router-dom'; 4 | 5 | import { generateTodoMock } from '../../../../../models/testing'; 6 | import { TodoDetail } from './todo-detail.component'; 7 | 8 | describe('TodoDetail', () => { 9 | it('render', async () => { 10 | const todo = generateTodoMock(); 11 | let result = render(); 12 | await act(async () => { 13 | result = render(, { wrapper: MemoryRouter }); 14 | }); 15 | const { asFragment } = result; 16 | 17 | expect(asFragment()).toMatchSnapshot(); 18 | }); 19 | 20 | it('render with isFetching', async () => { 21 | let result = render(); 22 | await act(async () => { 23 | result = render(, { 24 | wrapper: MemoryRouter, 25 | }); 26 | }); 27 | const { asFragment } = result; 28 | 29 | expect(asFragment()).toMatchSnapshot(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/pages/todo/todo-edit/components/todo-edit/todo-edit.presenter.spec.ts: -------------------------------------------------------------------------------- 1 | import { act, renderHook } from '@testing-library/react-hooks'; 2 | 3 | import { Todo } from '../../../../../models'; 4 | import { useTodoEditPresenter } from './todo-edit.presenter'; 5 | 6 | describe('useTodoEditPresenter', () => { 7 | it('should handle submit', async () => { 8 | const todo: Todo = { 9 | id: '1', 10 | title: 'title', 11 | completed: false, 12 | createdAt: 123456789, 13 | updatedAt: 123456789, 14 | }; 15 | const onUpdate = jest.fn(); 16 | const { result, waitFor } = renderHook(() => 17 | useTodoEditPresenter({ todo, onUpdate }) 18 | ); 19 | 20 | await act(async () => { 21 | const { values, setValues } = result.current; 22 | setValues({ 23 | ...values, 24 | title: 'title', 25 | completed: false, 26 | }); 27 | }); 28 | 29 | await act(async () => { 30 | const { handleSubmit } = result.current; 31 | handleSubmit(); 32 | }); 33 | 34 | await waitFor(() => { 35 | expect(onUpdate).toHaveBeenCalled(); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/models/todo/testing/todo.mock.ts: -------------------------------------------------------------------------------- 1 | import { Todo, TodoCreateDto, TodoUpdateDto } from '../todo.model'; 2 | 3 | export const generateTodosMock = (): Todo[] => { 4 | return [ 5 | { 6 | id: '1', 7 | title: 'title', 8 | completed: false, 9 | createdAt: 123456789, 10 | updatedAt: 123456789, 11 | }, 12 | { 13 | id: '2', 14 | title: 'title', 15 | completed: false, 16 | createdAt: 123456789, 17 | updatedAt: 123456789, 18 | }, 19 | { 20 | id: '3', 21 | title: 'title', 22 | completed: false, 23 | createdAt: 123456789, 24 | updatedAt: 123456789, 25 | }, 26 | ]; 27 | }; 28 | 29 | export const generateTodoMock = (): Todo => { 30 | return { 31 | id: '1', 32 | title: 'title', 33 | completed: false, 34 | createdAt: 123456789, 35 | updatedAt: 123456789, 36 | }; 37 | }; 38 | 39 | export const generateTodoCreateDtoMock = (): TodoCreateDto => { 40 | return { 41 | title: 'title', 42 | }; 43 | }; 44 | 45 | export const generateTodoUpdateDtoMock = (): TodoUpdateDto => { 46 | return { 47 | id: '1', 48 | title: 'title', 49 | completed: false, 50 | }; 51 | }; 52 | -------------------------------------------------------------------------------- /src/pages/todo/todo-create/components/todo-create/todo-create.presenter.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { useFormik } from 'formik'; 3 | import * as Yup from 'yup'; 4 | 5 | import { TodoCreateDto } from '../../../../../models'; 6 | 7 | interface FormValues { 8 | title: string; 9 | } 10 | 11 | const toDto = (values: FormValues) => { 12 | const value: TodoCreateDto = { 13 | title: values.title, 14 | }; 15 | return value; 16 | }; 17 | 18 | export const useTodoCreatePresenter = (arg: { 19 | onCreate?: (todo: TodoCreateDto) => void; 20 | }) => { 21 | const { onCreate } = arg; 22 | 23 | const initialValues = useMemo(() => { 24 | return { 25 | title: '', 26 | } as FormValues; 27 | }, []); 28 | 29 | const validationSchema = useMemo(() => { 30 | return Yup.object().shape({ 31 | title: Yup.string().required(), 32 | }); 33 | }, []); 34 | 35 | const formik = useFormik({ 36 | enableReinitialize: true, 37 | initialValues, 38 | validateOnMount: true, 39 | validationSchema, 40 | onSubmit: (values) => { 41 | onCreate?.(toDto(values)); 42 | }, 43 | }); 44 | 45 | return { 46 | ...formik, 47 | } as const; 48 | }; 49 | -------------------------------------------------------------------------------- /src/pages/todo/todo-list/todo-list.params.spec.tsx: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react-hooks'; 2 | import { MemoryRouter } from 'react-router-dom'; 3 | 4 | import { useTodoListParams } from './todo-list.params'; 5 | 6 | describe('useTodoListParams', () => { 7 | it('should render', () => { 8 | const { result } = renderHook(() => useTodoListParams(), { 9 | wrapper: ({ children }) => ( 10 | 11 | {children} 12 | 13 | ), 14 | }); 15 | const { limit, offset } = result.current; 16 | 17 | expect(limit).toEqual(10); 18 | expect(offset).toEqual(0); 19 | }); 20 | 21 | it('should render with query', () => { 22 | const { result } = renderHook(() => useTodoListParams(), { 23 | wrapper: ({ children }) => ( 24 | 27 | {children} 28 | 29 | ), 30 | }); 31 | const { limit, offset } = result.current; 32 | 33 | expect(limit).toEqual(20); 34 | expect(offset).toEqual(10); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/pages/todo/todo-edit/components/todo-edit/todo-edit.component.spec.tsx: -------------------------------------------------------------------------------- 1 | import { act, render } from '@testing-library/react'; 2 | import { Fragment } from 'react'; 3 | import { MemoryRouter } from 'react-router-dom'; 4 | 5 | import { Todo } from '../../../../../models'; 6 | import { TodoEdit } from './todo-edit.component'; 7 | 8 | describe('TodoEdit', () => { 9 | it('render', async () => { 10 | const todo: Todo = { 11 | id: '1', 12 | title: 'title', 13 | completed: false, 14 | createdAt: 123456789, 15 | updatedAt: 123456789, 16 | }; 17 | let result = render(); 18 | await act(async () => { 19 | result = render(, { 20 | wrapper: MemoryRouter, 21 | }); 22 | }); 23 | const { asFragment } = result; 24 | 25 | expect(asFragment()).toMatchSnapshot(); 26 | }); 27 | 28 | it('render with isFetching', async () => { 29 | let result = render(); 30 | await act(async () => { 31 | result = render(, { 32 | wrapper: MemoryRouter, 33 | }); 34 | }); 35 | const { asFragment } = result; 36 | 37 | expect(asFragment()).toMatchSnapshot(); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/pages/todo/todo-list/components/todo-list/todo-list.component.spec.tsx: -------------------------------------------------------------------------------- 1 | import { act, render } from '@testing-library/react'; 2 | import { Fragment } from 'react'; 3 | import { MemoryRouter } from 'react-router-dom'; 4 | 5 | import { generateTodosMock } from '../../../../../models/testing'; 6 | import { TodoList } from './todo-list.component'; 7 | 8 | describe('TodoList', () => { 9 | it('render', async () => { 10 | const todos = generateTodosMock(); 11 | const offset = 0; 12 | const limit = 10; 13 | let result = render(); 14 | await act(async () => { 15 | result = render( 16 | , 17 | { wrapper: MemoryRouter } 18 | ); 19 | }); 20 | const { asFragment } = result; 21 | 22 | expect(asFragment()).toMatchSnapshot(); 23 | }); 24 | 25 | it('render with isFetching', async () => { 26 | const offset = 0; 27 | const limit = 10; 28 | let result = render(); 29 | await act(async () => { 30 | result = render( 31 | , 32 | { wrapper: MemoryRouter } 33 | ); 34 | }); 35 | const { asFragment } = result; 36 | 37 | expect(asFragment()).toMatchSnapshot(); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/pages/todo/todo-edit/components/todo-edit/__snapshots__/todo-edit.component.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TodoEdit render 1`] = ` 4 | 5 | 8 | Back to detail 9 | 10 |

11 | todo-edit 12 |

13 |
14 |

15 | 20 |

21 | 22 | 23 | 24 | 27 | 34 | 35 | 36 | 39 | 45 | 46 | 47 |
25 | title 26 | 28 | 33 |
37 | completed 38 | 40 | 44 |
48 |
49 |
50 | `; 51 | 52 | exports[`TodoEdit render with isFetching 1`] = ` 53 | 54 | 57 | Back to detail 58 | 59 |

60 | todo-edit 61 |

62 | loading... 63 |
64 | `; 65 | -------------------------------------------------------------------------------- /src/pages/todo/todo-detail/components/todo-detail/__snapshots__/todo-detail.component.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TodoDetail render 1`] = ` 4 | 5 | 8 | Back to list 9 | 10 |

11 | todo-detail 12 |

13 |

14 | 17 | Edit this todo 18 | 19 |

20 | 21 | 22 | 23 | 26 | 29 | 30 | 31 | 34 | 37 | 38 | 39 | 42 | 45 | 46 | 47 | 50 | 53 | 54 | 55 |
24 | title 25 | 27 | title 28 |
32 | completed 33 | 35 | false 36 |
40 | createdAt 41 | 43 | 1970-01-02T10:17:36.789Z 44 |
48 | updatedAt 49 | 51 | 1970-01-02T10:17:36.789Z 52 |
56 |
57 | `; 58 | 59 | exports[`TodoDetail render with isFetching 1`] = ` 60 | 61 | 64 | Back to list 65 | 66 |

67 | todo-detail 68 |

69 | loading... 70 |
71 | `; 72 | -------------------------------------------------------------------------------- /src/pages/todo/todo-edit/components/todo-edit/todo-edit.presenter.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { useFormik } from 'formik'; 3 | import * as Yup from 'yup'; 4 | 5 | import { Todo, TodoUpdateDto } from '../../../../../models'; 6 | 7 | interface FormValues { 8 | title: string; 9 | completed: boolean; 10 | } 11 | 12 | const toDto = (todo: Todo, values: FormValues) => { 13 | const value: TodoUpdateDto = { 14 | id: todo.id, 15 | title: values.title, 16 | completed: values.completed, 17 | }; 18 | return value; 19 | }; 20 | 21 | export const useTodoEditPresenter = (arg: { 22 | todo: Todo | null; 23 | onUpdate?: (id: string, todo: TodoUpdateDto) => void; 24 | }) => { 25 | const { todo, onUpdate } = arg; 26 | 27 | const initialValues = useMemo(() => { 28 | return { 29 | title: todo?.title ?? '', 30 | completed: todo?.completed ?? '', 31 | } as FormValues; 32 | }, [todo]); 33 | 34 | const validationSchema = useMemo(() => { 35 | return Yup.object().shape({ 36 | title: Yup.string().required(), 37 | }); 38 | }, []); 39 | 40 | const formik = useFormik({ 41 | enableReinitialize: true, 42 | initialValues, 43 | validateOnMount: true, 44 | validationSchema, 45 | onSubmit: (values) => { 46 | if (todo) { 47 | onUpdate?.(todo.id, toDto(todo, values)); 48 | } 49 | }, 50 | }); 51 | 52 | return { 53 | ...formik, 54 | } as const; 55 | }; 56 | -------------------------------------------------------------------------------- /src/pages/todo/todo-create/containers/todo-create/todo-create.container.spec.tsx: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import { render } from '@testing-library/react'; 3 | import { PropsWithChildren } from 'react'; 4 | import { Provider } from 'react-redux'; 5 | import { MemoryRouter, MemoryRouterProps } from 'react-router-dom'; 6 | 7 | import { TodoCreateContainer } from './todo-create.container'; 8 | import { useTodoCreateFacade } from './todo-create.facade'; 9 | 10 | jest.mock('../../components', () => ({ 11 | ...jest.requireActual('../../components'), 12 | TodoCreate: jest.fn(() => null), 13 | })); 14 | 15 | jest.mock('./todo-create.facade', () => ({ 16 | ...jest.requireActual('./todo-create.facade'), 17 | useTodoCreateFacade: jest.fn(() => ({})), 18 | })); 19 | 20 | const wrapper = (props: PropsWithChildren) => { 21 | const { children, initialEntries, initialIndex } = props; 22 | const store = configureStore({ reducer: jest.fn() }); 23 | 24 | return ( 25 | 26 | {children} 27 | 28 | ); 29 | }; 30 | 31 | describe('TodoCreateContainer', () => { 32 | it('should render', () => { 33 | const { baseElement } = render(, { wrapper }); 34 | 35 | expect(useTodoCreateFacade).toBeCalledWith(); 36 | expect(baseElement).toBeTruthy(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/pages/todo/todo-edit/containers/todo-edit/todo-edit.facade.ts: -------------------------------------------------------------------------------- 1 | import { unwrapResult } from '@reduxjs/toolkit'; 2 | import { useCallback, useEffect } from 'react'; 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | import { useHistory } from 'react-router-dom'; 5 | 6 | import { TodoUpdateDto } from '../../../../../models'; 7 | import { 8 | fetchTodo, 9 | updateTodo, 10 | isFetchingSelector, 11 | todoSelector, 12 | } from '../../../../../store/todo'; 13 | 14 | export const useTodoEditFacade = (arg: { id: string }) => { 15 | const { id } = arg; 16 | const history = useHistory(); 17 | const dispatch = useDispatch(); 18 | const isFetching = useSelector(isFetchingSelector); 19 | const todo = useSelector(todoSelector); 20 | 21 | const fetch = useCallback( 22 | (arg: { id: string }) => { 23 | return dispatch(fetchTodo(arg)).then(unwrapResult); 24 | }, 25 | [dispatch] 26 | ); 27 | 28 | const update = useCallback( 29 | (id: string, dto: TodoUpdateDto) => { 30 | return dispatch(updateTodo({ id, todo: dto })) 31 | .then(unwrapResult) 32 | .then((payload) => { 33 | const { todo } = payload; 34 | history.push(`/todos/${todo.id}`); 35 | }); 36 | }, 37 | [history, dispatch] 38 | ); 39 | 40 | useEffect(() => { 41 | fetch({ id }); 42 | }, [id, fetch]); 43 | 44 | return { 45 | isFetching, 46 | todo, 47 | fetch, 48 | update, 49 | } as const; 50 | }; 51 | -------------------------------------------------------------------------------- /src/pages/todo/todo-detail/components/todo-detail/todo-detail.component.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import { NavLink } from 'react-router-dom'; 3 | 4 | import { Todo } from '../../../../../models'; 5 | 6 | const datePipe = (date: number) => { 7 | return new Date(date).toISOString(); 8 | }; 9 | 10 | interface Props { 11 | isFetching?: boolean; 12 | todo: Todo | null; 13 | } 14 | 15 | export const TodoDetail = memo((props: Props) => { 16 | const { todo } = props; 17 | 18 | return ( 19 | <> 20 | Back to list 21 |

todo-detail

22 | {todo ? ( 23 | <> 24 |

25 | Edit this todo 26 |

27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
title{todo.title}
completed{`${todo.completed}`}
createdAt{datePipe(todo.createdAt)}
updatedAt{datePipe(todo.updatedAt)}
47 | 48 | ) : ( 49 | <>loading... 50 | )} 51 | 52 | ); 53 | }); 54 | -------------------------------------------------------------------------------- /src/pages/todo/todo-edit/containers/todo-edit/todo-edit.container.spec.tsx: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import { render } from '@testing-library/react'; 3 | import { PropsWithChildren } from 'react'; 4 | import { Provider } from 'react-redux'; 5 | import { MemoryRouter, MemoryRouterProps } from 'react-router-dom'; 6 | 7 | import { TodoEditContainer } from './todo-edit.container'; 8 | import { useTodoEditFacade } from './todo-edit.facade'; 9 | 10 | jest.mock('../../components', () => ({ 11 | ...jest.requireActual('../../components'), 12 | TodoEdit: jest.fn(() => null), 13 | })); 14 | 15 | jest.mock('./todo-edit.facade', () => ({ 16 | ...jest.requireActual('./todo-edit.facade'), 17 | useTodoEditFacade: jest.fn(() => ({})), 18 | })); 19 | 20 | const wrapper = (props: PropsWithChildren) => { 21 | const { children, initialEntries, initialIndex } = props; 22 | const store = configureStore({ reducer: jest.fn() }); 23 | 24 | return ( 25 | 26 | {children} 27 | 28 | ); 29 | }; 30 | 31 | describe('TodoEditContainer', () => { 32 | it('should render', () => { 33 | const id = '1'; 34 | const { baseElement } = render(, { 35 | wrapper, 36 | }); 37 | 38 | expect(useTodoEditFacade).toBeCalledWith({ id }); 39 | expect(baseElement).toBeTruthy(); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/pages/todo/todo-create/components/todo-create/todo-create.component.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import { NavLink } from 'react-router-dom'; 3 | 4 | import { TodoCreateDto } from '../../../../../models'; 5 | import { useTodoCreatePresenter } from './todo-create.presenter'; 6 | 7 | interface Props { 8 | isFetching?: boolean; 9 | onCreate?: (todo: TodoCreateDto) => void; 10 | } 11 | 12 | export const TodoCreate = memo((props: Props) => { 13 | const { isFetching, onCreate } = props; 14 | 15 | const { isValid, handleBlur, handleChange, handleSubmit } = 16 | useTodoCreatePresenter({ onCreate }); 17 | 18 | return ( 19 | <> 20 | Back to list 21 |

todo-create

22 |
23 |

24 | 32 |

33 | 34 | 35 | 36 | 37 | 45 | 46 | 47 |
title 38 | 44 |
48 |
49 | 50 | ); 51 | }); 52 | -------------------------------------------------------------------------------- /src/pages/todo/todo-detail/containers/todo-detail/todo-detail.container.spec.tsx: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import { render } from '@testing-library/react'; 3 | import { PropsWithChildren } from 'react'; 4 | import { Provider } from 'react-redux'; 5 | import { MemoryRouter, MemoryRouterProps } from 'react-router-dom'; 6 | 7 | import { TodoDetailContainer } from './todo-detail.container'; 8 | import { useTodoDetailFacade } from './todo-detail.facade'; 9 | 10 | jest.mock('../../components', () => ({ 11 | ...jest.requireActual('../../components'), 12 | TodoDetail: jest.fn(() => null), 13 | })); 14 | 15 | jest.mock('./todo-detail.facade', () => ({ 16 | ...jest.requireActual('./todo-detail.facade'), 17 | useTodoDetailFacade: jest.fn(() => ({})), 18 | })); 19 | 20 | const wrapper = (props: PropsWithChildren) => { 21 | const { children, initialEntries, initialIndex } = props; 22 | const store = configureStore({ reducer: jest.fn() }); 23 | 24 | return ( 25 | 26 | {children} 27 | 28 | ); 29 | }; 30 | 31 | describe('TodoDetailContainer', () => { 32 | it('should render', () => { 33 | const id = '1'; 34 | const { baseElement } = render(, { 35 | wrapper, 36 | }); 37 | 38 | expect(useTodoDetailFacade).toBeCalledWith({ id }); 39 | expect(baseElement).toBeTruthy(); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/pages/todo/todo-create/components/todo-create/__snapshots__/todo-create.component.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TodoCreate render 1`] = ` 4 | 5 | 8 | Back to list 9 | 10 |

11 | todo-create 12 |

13 |
14 |

15 | 21 |

22 | 23 | 24 | 25 | 28 | 34 | 35 | 36 |
26 | title 27 | 29 | 33 |
37 |
38 |
39 | `; 40 | 41 | exports[`TodoCreate render with isFetching 1`] = ` 42 | 43 | 46 | Back to list 47 | 48 |

49 | todo-create 50 |

51 |
52 |

53 | 59 |

60 | 61 | 62 | 63 | 66 | 72 | 73 | 74 |
64 | title 65 | 67 | 71 |
75 |
76 |
77 | `; 78 | -------------------------------------------------------------------------------- /src/pages/todo/todo-list/containers/todo-list/todo-list.container.spec.tsx: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import { render } from '@testing-library/react'; 3 | import { PropsWithChildren } from 'react'; 4 | import { Provider } from 'react-redux'; 5 | import { MemoryRouter, MemoryRouterProps } from 'react-router-dom'; 6 | 7 | import { TodoListContainer } from './todo-list.container'; 8 | import { useTodoListFacade } from './todo-list.facade'; 9 | 10 | jest.mock('../../components', () => ({ 11 | ...jest.requireActual('../../components'), 12 | TodoList: jest.fn(() => null), 13 | })); 14 | 15 | jest.mock('./todo-list.facade', () => ({ 16 | ...jest.requireActual('./todo-list.facade'), 17 | useTodoListFacade: jest.fn(() => ({ todos: [] })), 18 | })); 19 | 20 | const wrapper = (props: PropsWithChildren) => { 21 | const { children, initialEntries, initialIndex } = props; 22 | const store = configureStore({ reducer: jest.fn() }); 23 | 24 | return ( 25 | 26 | {children} 27 | 28 | ); 29 | }; 30 | 31 | describe('TodoListContainer', () => { 32 | it('should render', () => { 33 | const offset = 0; 34 | const limit = 10; 35 | const { baseElement } = render( 36 | , 37 | { wrapper } 38 | ); 39 | 40 | expect(useTodoListFacade).toBeCalledWith({ offset, limit }); 41 | expect(baseElement).toBeTruthy(); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/store/todo/actions/todo.action.ts: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk } from '@reduxjs/toolkit'; 2 | 3 | import { TodoCreateDto, TodoUpdateDto } from '../../../models'; 4 | import { todoService } from '../../../services'; 5 | import { featureKey } from '../states'; 6 | 7 | export const fetchAllTodos = createAsyncThunk( 8 | `${featureKey}/fetchAll`, 9 | async (arg: { offset?: number; limit?: number } = {}) => { 10 | const { offset, limit } = arg; 11 | const result = await todoService.fetchAll(offset, limit); 12 | return { todos: result }; 13 | } 14 | ); 15 | 16 | export const fetchTodo = createAsyncThunk( 17 | `${featureKey}/fetch`, 18 | async (arg: { id: string }) => { 19 | const { id } = arg; 20 | const result = await todoService.fetch(id); 21 | return { todo: result }; 22 | } 23 | ); 24 | 25 | export const createTodo = createAsyncThunk( 26 | `${featureKey}/create`, 27 | async (arg: { todo: TodoCreateDto }) => { 28 | const { todo } = arg; 29 | const result = await todoService.create(todo); 30 | return { todo: result }; 31 | } 32 | ); 33 | 34 | export const updateTodo = createAsyncThunk( 35 | `${featureKey}/update`, 36 | async (arg: { id: string; todo: TodoUpdateDto }) => { 37 | const { id, todo } = arg; 38 | const result = await todoService.update(id, todo); 39 | return { todo: result }; 40 | } 41 | ); 42 | 43 | export const removeTodo = createAsyncThunk( 44 | `${featureKey}/remove`, 45 | async (arg: { id: string }) => { 46 | const { id } = arg; 47 | await todoService.remove(id); 48 | return { id }; 49 | } 50 | ); 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todo-react", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "react-scripts start", 7 | "build": "react-scripts build", 8 | "test": "react-scripts test", 9 | "eject": "react-scripts eject" 10 | }, 11 | "dependencies": { 12 | "@emotion/css": "~11.7.0", 13 | "@emotion/react": "~11.8.0", 14 | "@emotion/styled": "~11.8.0", 15 | "@reduxjs/toolkit": "~1.7.0", 16 | "formik": "~2.2.0", 17 | "react": "~17.0.0", 18 | "react-dom": "~17.0.0", 19 | "react-redux": "~7.2.0", 20 | "react-router-dom": "~5.3.0", 21 | "redux": "~4.1.0", 22 | "yup": "~0.32.0" 23 | }, 24 | "devDependencies": { 25 | "@testing-library/jest-dom": "~5.16.0", 26 | "@testing-library/react": "~12.1.0", 27 | "@testing-library/react-hooks": "~7.0.0", 28 | "@testing-library/user-event": "~13.5.0", 29 | "@types/jest": "~27.4.0", 30 | "@types/node": "~17.0.0", 31 | "@types/react": "~17.0.0", 32 | "@types/react-dom": "~17.0.0", 33 | "@types/react-redux": "~7.1.0", 34 | "@types/react-router-dom": "~5.3.0", 35 | "react-scripts": "~5.0.0", 36 | "react-test-renderer": "~17.0.0", 37 | "typescript": "~4.4.0" 38 | }, 39 | "eslintConfig": { 40 | "extends": [ 41 | "react-app", 42 | "react-app/jest" 43 | ] 44 | }, 45 | "browserslist": { 46 | "production": [ 47 | ">0.2%", 48 | "not dead", 49 | "not op_mini all" 50 | ], 51 | "development": [ 52 | "last 1 chrome version", 53 | "last 1 firefox version", 54 | "last 1 safari version" 55 | ] 56 | }, 57 | "jest": { 58 | "resetMocks": false 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/store/todo/selectors/todo.selector.spec.ts: -------------------------------------------------------------------------------- 1 | import { generateTodosMock } from '../../../models/testing'; 2 | import { State, adapter, featureKey, initialState } from '../states'; 3 | import { 4 | isFetchingSelector, 5 | selectedIdSelector, 6 | todoSelector, 7 | } from './todo.selector'; 8 | 9 | interface RootState { 10 | [featureKey]: State; 11 | } 12 | 13 | describe('selectors', () => { 14 | it('should handle isFetchingSelector', () => { 15 | const isFetching = true; 16 | const state: RootState = { 17 | [featureKey]: { 18 | ...initialState, 19 | isFetching, 20 | }, 21 | }; 22 | 23 | expect(isFetchingSelector(state)).toEqual(isFetching); 24 | }); 25 | 26 | it('should handle selectedIdSelector', () => { 27 | const selectedId = '1'; 28 | const state: RootState = { 29 | [featureKey]: { 30 | ...initialState, 31 | selectedId, 32 | }, 33 | }; 34 | 35 | expect(selectedIdSelector(state)).toEqual(selectedId); 36 | }); 37 | 38 | it('should handle todoSelector', () => { 39 | const todos = generateTodosMock(); 40 | const state1: RootState = { 41 | [featureKey]: adapter.setAll({ ...initialState, selectedId: '1' }, todos), 42 | }; 43 | const state2: RootState = { 44 | [featureKey]: adapter.setAll( 45 | { ...initialState, selectedId: '999' }, 46 | todos 47 | ), 48 | }; 49 | const state3: RootState = { 50 | [featureKey]: adapter.setAll({ ...initialState }, todos), 51 | }; 52 | 53 | expect(todoSelector(state1)).toEqual(todos[0]); 54 | expect(todoSelector(state2)).toEqual(null); 55 | expect(todoSelector(state3)).toEqual(null); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /src/pages/todo/todo-detail/containers/todo-detail/todo-detail.facade.spec.tsx: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import { renderHook } from '@testing-library/react-hooks'; 3 | import { PropsWithChildren } from 'react'; 4 | import { Provider } from 'react-redux'; 5 | import { MemoryRouter, MemoryRouterProps } from 'react-router-dom'; 6 | 7 | import { fetchTodo } from '../../../../../store/todo'; 8 | import { useTodoDetailFacade } from './todo-detail.facade'; 9 | 10 | const mockDispatch = jest.fn().mockResolvedValue({}); 11 | jest.mock('react-redux', () => ({ 12 | ...jest.requireActual('react-redux'), 13 | useDispatch: () => mockDispatch, 14 | useSelector: () => jest.fn(), 15 | })); 16 | 17 | jest.mock('../../../../../store/todo', () => ({ 18 | ...jest.requireActual('../../../../../store/todo'), 19 | fetchTodo: jest.fn(), 20 | })); 21 | 22 | const wrapper = (props: PropsWithChildren) => { 23 | const { children, initialEntries, initialIndex } = props; 24 | const store = configureStore({ reducer: jest.fn() }); 25 | 26 | return ( 27 | 28 | {children} 29 | 30 | ); 31 | }; 32 | 33 | describe('useTodoDetailFacade', () => { 34 | beforeEach(() => { 35 | jest.clearAllMocks(); 36 | }); 37 | 38 | it('should dispatch fetchTodo', async () => { 39 | const id = '1'; 40 | const { result } = renderHook(() => useTodoDetailFacade({ id }), { 41 | wrapper, 42 | }); 43 | const { fetch } = result.current; 44 | fetch({ id }); 45 | 46 | expect(mockDispatch).toHaveBeenCalled(); 47 | expect(fetchTodo).toHaveBeenCalledWith({ id }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/pages/todo/todo-list/containers/todo-list/todo-list.facade.ts: -------------------------------------------------------------------------------- 1 | import { unwrapResult } from '@reduxjs/toolkit'; 2 | import { useCallback, useEffect } from 'react'; 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | import { useHistory, useLocation } from 'react-router-dom'; 5 | 6 | import { 7 | fetchAllTodos, 8 | isFetchingSelector, 9 | todosSelector, 10 | } from '../../../../../store/todo'; 11 | 12 | export const useTodoListFacade = (arg: { offset?: number; limit?: number }) => { 13 | const { offset, limit } = arg; 14 | const history = useHistory(); 15 | const location = useLocation(); 16 | const dispatch = useDispatch(); 17 | const isFetching = useSelector(isFetchingSelector); 18 | const todos = useSelector(todosSelector); 19 | 20 | const fetchAll = useCallback( 21 | (arg: { offset?: number; limit?: number }) => { 22 | return dispatch(fetchAllTodos(arg)).then(unwrapResult); 23 | }, 24 | [dispatch] 25 | ); 26 | 27 | const changeOffset = useCallback( 28 | (offset: number) => { 29 | const params = new URLSearchParams(location.search); 30 | params.set('offset', `${offset}`); 31 | history.push(`/todos?${params}`); 32 | }, 33 | [history, location.search] 34 | ); 35 | 36 | const changeLimit = useCallback( 37 | (limit: number) => { 38 | const params = new URLSearchParams(location.search); 39 | params.set('limit', `${limit}`); 40 | history.push(`/todos?${params}`); 41 | }, 42 | [history, location.search] 43 | ); 44 | 45 | useEffect(() => { 46 | fetchAll({ offset, limit }); 47 | }, [offset, limit, fetchAll]); 48 | 49 | return { 50 | isFetching, 51 | todos, 52 | changeOffset, 53 | changeLimit, 54 | fetchAll, 55 | } as const; 56 | }; 57 | -------------------------------------------------------------------------------- /src/pages/todo/todo-list/components/todo-list/__snapshots__/todo-list.component.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TodoList render 1`] = ` 4 | 5 |

6 | todo-list 7 |

8 |

9 | 13 | Add a new todo 14 | 15 |

16 |
17 | offset 18 | 24 | limit 25 | 31 |
32 | 58 |
59 | `; 60 | 61 | exports[`TodoList render with isFetching 1`] = ` 62 | 63 |

64 | todo-list 65 |

66 |

67 | 71 | Add a new todo 72 | 73 |

74 |
75 | offset 76 | 82 | limit 83 | 89 |
90 |
    91 | 92 | `; 93 | -------------------------------------------------------------------------------- /src/pages/todo/todo-list/containers/todo-list/todo-list.facade.spec.tsx: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import { renderHook } from '@testing-library/react-hooks'; 3 | import { PropsWithChildren } from 'react'; 4 | import { Provider } from 'react-redux'; 5 | import { MemoryRouter, MemoryRouterProps } from 'react-router-dom'; 6 | 7 | import { fetchAllTodos } from '../../../../../store/todo'; 8 | import { useTodoListFacade } from './todo-list.facade'; 9 | 10 | const mockDispatch = jest.fn().mockResolvedValue({}); 11 | jest.mock('react-redux', () => ({ 12 | ...jest.requireActual('react-redux'), 13 | useDispatch: () => mockDispatch, 14 | useSelector: () => jest.fn(), 15 | })); 16 | 17 | jest.mock('../../../../../store/todo', () => ({ 18 | ...jest.requireActual('../../../../../store/todo'), 19 | fetchAllTodos: jest.fn(), 20 | })); 21 | 22 | const wrapper = (props: PropsWithChildren) => { 23 | const { children, initialEntries, initialIndex } = props; 24 | const store = configureStore({ reducer: jest.fn() }); 25 | 26 | return ( 27 | 28 | {children} 29 | 30 | ); 31 | }; 32 | 33 | describe('useTodoListFacade', () => { 34 | beforeEach(() => { 35 | jest.clearAllMocks(); 36 | }); 37 | 38 | it('should dispatch fetchAllTodos', async () => { 39 | const offset = 0; 40 | const limit = 10; 41 | const { result } = renderHook(() => useTodoListFacade({ offset, limit }), { 42 | wrapper, 43 | }); 44 | const { fetchAll } = result.current; 45 | fetchAll({ offset, limit }); 46 | 47 | expect(mockDispatch).toHaveBeenCalled(); 48 | expect(fetchAllTodos).toHaveBeenCalledWith({ offset, limit }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
    32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/services/todo/todo.service.ts: -------------------------------------------------------------------------------- 1 | import { Todo, TodoCreateDto, TodoUpdateDto } from '../../models'; 2 | 3 | export class TodoService { 4 | fetchAll(offset?: number, limit?: number): Promise { 5 | const url = new URL(`${this.baseUrl}/todos`); 6 | if (offset !== undefined) { 7 | url.searchParams.append('offset', `${offset}`); 8 | } 9 | if (limit !== undefined) { 10 | url.searchParams.append('limit', `${limit}`); 11 | } 12 | 13 | return fetch(url.toString(), { 14 | method: 'GET', 15 | headers: { 'Content-Type': 'application/json' }, 16 | }).then((res) => res.json()); 17 | } 18 | 19 | fetch(id: string): Promise { 20 | const url = new URL(`${this.baseUrl}/todos/${id}`); 21 | 22 | return fetch(url.toString(), { 23 | method: 'GET', 24 | headers: { 'Content-Type': 'application/json' }, 25 | }).then((res) => res.json()); 26 | } 27 | 28 | create(todo: TodoCreateDto): Promise { 29 | const url = new URL(`${this.baseUrl}/todos`); 30 | 31 | return fetch(url.toString(), { 32 | method: 'POST', 33 | body: JSON.stringify(todo), 34 | headers: { 'Content-Type': 'application/json' }, 35 | }).then((res) => res.json()); 36 | } 37 | 38 | update(id: string, todo: TodoUpdateDto): Promise { 39 | const url = new URL(`${this.baseUrl}/todos/${id}`); 40 | 41 | return fetch(url.toString(), { 42 | method: 'PUT', 43 | body: JSON.stringify(todo), 44 | headers: { 'Content-Type': 'application/json' }, 45 | }).then((res) => res.json()); 46 | } 47 | 48 | remove(id: string): Promise { 49 | const url = new URL(`${this.baseUrl}/todos/${id}`); 50 | 51 | return fetch(url.toString(), { 52 | method: 'DELETE', 53 | headers: { 'Content-Type': 'application/json' }, 54 | }).then(() => id); 55 | } 56 | 57 | constructor(private readonly baseUrl: string) {} 58 | } 59 | -------------------------------------------------------------------------------- /src/pages/todo/todo-create/containers/todo-create/todo-create.facade.spec.tsx: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import { renderHook } from '@testing-library/react-hooks'; 3 | import { PropsWithChildren } from 'react'; 4 | import { Provider } from 'react-redux'; 5 | import { MemoryRouter, MemoryRouterProps } from 'react-router-dom'; 6 | 7 | import { 8 | generateTodoCreateDtoMock, 9 | generateTodoMock, 10 | } from '../../../../../models/testing'; 11 | import { createTodo } from '../../../../../store/todo'; 12 | import { useTodoCreateFacade } from './todo-create.facade'; 13 | 14 | const mockDispatch = jest.fn().mockResolvedValue({}); 15 | jest.mock('react-redux', () => ({ 16 | ...jest.requireActual('react-redux'), 17 | useDispatch: () => mockDispatch, 18 | useSelector: () => jest.fn(), 19 | })); 20 | 21 | jest.mock('../../../../../store/todo', () => ({ 22 | ...jest.requireActual('../../../../../store/todo'), 23 | createTodo: jest.fn(), 24 | })); 25 | 26 | const wrapper = (props: PropsWithChildren) => { 27 | const { children, initialEntries, initialIndex } = props; 28 | const store = configureStore({ reducer: jest.fn() }); 29 | 30 | return ( 31 | 32 | {children} 33 | 34 | ); 35 | }; 36 | 37 | describe('useTodoCreateFacade', () => { 38 | beforeEach(() => { 39 | jest.clearAllMocks(); 40 | }); 41 | 42 | it('should dispatch createTodo', async () => { 43 | const { result } = renderHook(() => useTodoCreateFacade(), { wrapper }); 44 | const { create } = result.current; 45 | const dto = generateTodoCreateDtoMock(); 46 | const todo = generateTodoMock(); 47 | 48 | mockDispatch.mockResolvedValue({ payload: { todo } }); 49 | 50 | create(dto); 51 | 52 | expect(mockDispatch).toHaveBeenCalled(); 53 | expect(createTodo).toHaveBeenCalledWith({ todo: dto }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/pages/todo/todo-list/components/todo-list/todo-list.component.tsx: -------------------------------------------------------------------------------- 1 | import { cx } from '@emotion/css'; 2 | import styled from '@emotion/styled'; 3 | import { ChangeEvent, memo, useCallback } from 'react'; 4 | import { NavLink as NavLinkBase } from 'react-router-dom'; 5 | 6 | import { Todo } from '../../../../../models'; 7 | 8 | const NavLink = styled(NavLinkBase)` 9 | &.completed { 10 | text-decoration: line-through; 11 | } 12 | `; 13 | 14 | interface Props { 15 | isFetching?: boolean; 16 | todos: Todo[]; 17 | offset: number; 18 | limit: number; 19 | onChangeOffset?: (offset: number) => void; 20 | onChangeLimit?: (limit: number) => void; 21 | } 22 | 23 | export const TodoList = memo((props: Props) => { 24 | const { todos, offset, limit, onChangeOffset, onChangeLimit } = props; 25 | 26 | const changeOffset = useCallback( 27 | (event: ChangeEvent) => { 28 | const value = Number(event.target.value); 29 | onChangeOffset?.(value); 30 | }, 31 | [onChangeOffset] 32 | ); 33 | 34 | const changeLimit = useCallback( 35 | (event: ChangeEvent) => { 36 | const value = Number(event.target.value); 37 | onChangeLimit?.(value); 38 | }, 39 | [onChangeLimit] 40 | ); 41 | 42 | return ( 43 | <> 44 |

    todo-list

    45 |

    46 | Add a new todo 47 |

    48 |
    49 | {' offset '} 50 | 57 | {' limit '} 58 | 65 |
    66 |
      67 | {todos.map((todo) => ( 68 |
    • 69 | 73 | {todo.title} 74 | 75 |
    • 76 | ))} 77 |
    78 | 79 | ); 80 | }); 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /src/pages/todo/todo-edit/components/todo-edit/todo-edit.component.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import { NavLink } from 'react-router-dom'; 3 | 4 | import { Todo, TodoUpdateDto } from '../../../../../models'; 5 | import { useTodoEditPresenter } from './todo-edit.presenter'; 6 | 7 | interface Props { 8 | isFetching?: boolean; 9 | todo: Todo | null; 10 | onUpdate?: (id: string, todo: TodoUpdateDto) => void; 11 | } 12 | 13 | export const TodoEdit = memo((props: Props) => { 14 | const { isFetching, todo, onUpdate } = props; 15 | 16 | const { isValid, handleBlur, handleChange, handleSubmit } = 17 | useTodoEditPresenter({ todo, onUpdate }); 18 | 19 | return ( 20 | <> 21 | Back to detail 22 |

    todo-edit

    23 | {todo ? ( 24 | <> 25 |
    26 |

    27 | 35 |

    36 | 37 | 38 | 39 | 40 | 49 | 50 | 51 | 52 | 61 | 62 | 63 |
    title 41 | 48 |
    completed 53 | 60 |
    64 |
    65 | 66 | ) : ( 67 | <>loading... 68 | )} 69 | 70 | ); 71 | }); 72 | -------------------------------------------------------------------------------- /src/pages/todo/todo-edit/containers/todo-edit/todo-edit.facade.spec.tsx: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import { renderHook } from '@testing-library/react-hooks'; 3 | import { PropsWithChildren } from 'react'; 4 | import { Provider } from 'react-redux'; 5 | import { MemoryRouter, MemoryRouterProps } from 'react-router-dom'; 6 | 7 | import { 8 | generateTodoUpdateDtoMock, 9 | generateTodoMock, 10 | } from '../../../../../models/testing'; 11 | import { fetchTodo, updateTodo } from '../../../../../store/todo'; 12 | import { useTodoEditFacade } from './todo-edit.facade'; 13 | 14 | const mockDispatch = jest.fn().mockResolvedValue({}); 15 | jest.mock('react-redux', () => ({ 16 | ...jest.requireActual('react-redux'), 17 | useDispatch: () => mockDispatch, 18 | useSelector: () => jest.fn(), 19 | })); 20 | 21 | jest.mock('../../../../../store/todo', () => ({ 22 | ...jest.requireActual('../../../../../store/todo'), 23 | fetchTodo: jest.fn(), 24 | updateTodo: jest.fn(), 25 | })); 26 | 27 | const wrapper = (props: PropsWithChildren) => { 28 | const { children, initialEntries, initialIndex } = props; 29 | const store = configureStore({ reducer: jest.fn() }); 30 | 31 | return ( 32 | 33 | {children} 34 | 35 | ); 36 | }; 37 | 38 | describe('useTodoEditFacade', () => { 39 | beforeEach(() => { 40 | jest.clearAllMocks(); 41 | }); 42 | 43 | it('should dispatch fetchTodo', async () => { 44 | const id = '1'; 45 | const { result } = renderHook(() => useTodoEditFacade({ id }), { 46 | wrapper, 47 | }); 48 | const { fetch } = result.current; 49 | fetch({ id }); 50 | 51 | expect(mockDispatch).toHaveBeenCalled(); 52 | expect(fetchTodo).toHaveBeenCalledWith({ id }); 53 | }); 54 | 55 | it('should dispatch updateTodo', async () => { 56 | const id = '1'; 57 | const { result } = renderHook(() => useTodoEditFacade({ id }), { 58 | wrapper, 59 | }); 60 | const { update } = result.current; 61 | const dto = generateTodoUpdateDtoMock(); 62 | const todo = generateTodoMock(); 63 | 64 | mockDispatch.mockResolvedValue({ payload: { todo } }); 65 | 66 | update(id, dto); 67 | 68 | expect(mockDispatch).toHaveBeenCalled(); 69 | expect(updateTodo).toHaveBeenCalledWith({ id, todo: dto }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /src/store/todo/reducers/todo.reducer.ts: -------------------------------------------------------------------------------- 1 | import { createReducer } from '@reduxjs/toolkit'; 2 | 3 | import { initialState, adapter } from '../states'; 4 | import { 5 | fetchAllTodos, 6 | fetchTodo, 7 | createTodo, 8 | updateTodo, 9 | removeTodo, 10 | } from '../actions'; 11 | 12 | export const reducer = createReducer(initialState, (builder) => 13 | builder 14 | .addCase(fetchAllTodos.pending, (state) => { 15 | return { ...state, isFetching: true }; 16 | }) 17 | .addCase(fetchAllTodos.fulfilled, (state, action) => { 18 | const { todos } = action.payload; 19 | return adapter.setAll({ ...state, isFetching: false }, todos); 20 | }) 21 | .addCase(fetchAllTodos.rejected, (state) => { 22 | return { ...state, isFetching: false }; 23 | }) 24 | .addCase(fetchTodo.pending, (state, action) => { 25 | const { id } = action.meta.arg; 26 | return { ...state, isFetching: true, selectedId: id }; 27 | }) 28 | .addCase(fetchTodo.fulfilled, (state, action) => { 29 | const { todo } = action.payload; 30 | return adapter.upsertOne({ ...state, isFetching: false }, todo); 31 | }) 32 | .addCase(fetchTodo.rejected, (state) => { 33 | return { ...state, isFetching: false }; 34 | }) 35 | .addCase(createTodo.pending, (state) => { 36 | return { ...state, isFetching: true }; 37 | }) 38 | .addCase(createTodo.fulfilled, (state, action) => { 39 | const { todo } = action.payload; 40 | return adapter.addOne({ ...state, isFetching: false }, todo); 41 | }) 42 | .addCase(createTodo.rejected, (state) => { 43 | return { ...state, isFetching: false }; 44 | }) 45 | .addCase(updateTodo.pending, (state) => { 46 | return { ...state, isFetching: true }; 47 | }) 48 | .addCase(updateTodo.fulfilled, (state, action) => { 49 | const { todo } = action.payload; 50 | return adapter.updateOne( 51 | { ...state, isFetching: false }, 52 | { 53 | id: todo.id, 54 | changes: todo, 55 | } 56 | ); 57 | }) 58 | .addCase(updateTodo.rejected, (state) => { 59 | return { ...state, isFetching: false }; 60 | }) 61 | .addCase(removeTodo.pending, (state, action) => { 62 | const { id } = action.meta.arg; 63 | return { ...state, isFetching: true, selectedId: id }; 64 | }) 65 | .addCase(removeTodo.fulfilled, (state, action) => { 66 | const { id } = action.payload; 67 | return adapter.removeOne( 68 | { ...state, isFetching: false, selectedId: null }, 69 | id 70 | ); 71 | }) 72 | .addCase(removeTodo.rejected, (state) => { 73 | return { ...state, isFetching: false }; 74 | }) 75 | ); 76 | -------------------------------------------------------------------------------- /src/store/todo/actions/todo.action.spec.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | 3 | import { 4 | generateTodoMock, 5 | generateTodosMock, 6 | generateTodoCreateDtoMock, 7 | generateTodoUpdateDtoMock, 8 | } from '../../../models/testing'; 9 | import { todoService } from '../../../services'; 10 | import { 11 | fetchAllTodos, 12 | fetchTodo, 13 | createTodo, 14 | updateTodo, 15 | removeTodo, 16 | } from './todo.action'; 17 | 18 | describe('actions', () => { 19 | it(`should create ${fetchAllTodos.fulfilled.type}`, async () => { 20 | const offset = 0; 21 | const limit = 10; 22 | const todos = generateTodosMock(); 23 | const spy = jest.spyOn(todoService, 'fetchAll').mockResolvedValue(todos); 24 | const store = configureStore({ reducer: jest.fn() }); 25 | const { payload } = await store.dispatch(fetchAllTodos({ offset, limit })); 26 | 27 | expect(spy).toHaveBeenCalledWith(offset, limit); 28 | expect(payload).toEqual({ todos }); 29 | }); 30 | 31 | it(`should create ${fetchTodo.fulfilled.type}`, async () => { 32 | const id = '1'; 33 | const todo = generateTodoMock(); 34 | const spy = jest.spyOn(todoService, 'fetch').mockResolvedValue(todo); 35 | const store = configureStore({ reducer: jest.fn() }); 36 | const { payload } = await store.dispatch(fetchTodo({ id })); 37 | 38 | expect(spy).toHaveBeenCalledWith(id); 39 | expect(payload).toEqual({ todo }); 40 | }); 41 | 42 | it(`should create ${createTodo.fulfilled.type}`, async () => { 43 | const dto = generateTodoCreateDtoMock(); 44 | const todo = generateTodoMock(); 45 | const spy = jest.spyOn(todoService, 'create').mockResolvedValue(todo); 46 | const store = configureStore({ reducer: jest.fn() }); 47 | const { payload } = await store.dispatch(createTodo({ todo: dto })); 48 | 49 | expect(spy).toHaveBeenCalledWith(dto); 50 | expect(payload).toEqual({ todo }); 51 | }); 52 | 53 | it(`should create ${updateTodo.fulfilled.type}`, async () => { 54 | const id = '1'; 55 | const dto = generateTodoUpdateDtoMock(); 56 | const todo = generateTodoMock(); 57 | const spy = jest.spyOn(todoService, 'update').mockResolvedValue(todo); 58 | const store = configureStore({ reducer: jest.fn() }); 59 | 60 | const { payload } = await store.dispatch(updateTodo({ id, todo: dto })); 61 | 62 | expect(spy).toHaveBeenCalledWith(id, dto); 63 | expect(payload).toEqual({ todo }); 64 | }); 65 | 66 | it(`should create ${removeTodo.fulfilled.type}`, async () => { 67 | const id = '1'; 68 | const spy = jest.spyOn(todoService, 'remove').mockResolvedValue(id); 69 | const store = configureStore({ reducer: jest.fn() }); 70 | const { payload } = await store.dispatch(removeTodo({ id })); 71 | 72 | expect(spy).toHaveBeenCalledWith(id); 73 | expect(payload).toEqual({ id }); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /src/services/todo/todo.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | generateTodoCreateDtoMock, 3 | generateTodoMock, 4 | generateTodosMock, 5 | generateTodoUpdateDtoMock, 6 | } from '../../models/testing'; 7 | 8 | import { TodoService } from './todo.service'; 9 | 10 | describe('TodoService', () => { 11 | const fetch = globalThis.fetch; 12 | const baseUrl = 'http://localhost:3000'; 13 | const service = new TodoService(baseUrl); 14 | 15 | afterAll(() => { 16 | globalThis.fetch = fetch; 17 | }); 18 | 19 | it('should handle fetchAll', async () => { 20 | const offset = 0; 21 | const limit = 10; 22 | const todos = generateTodosMock(); 23 | 24 | const fetchMock = jest.fn().mockResolvedValueOnce({ json: () => todos }); 25 | globalThis.fetch = fetchMock; 26 | 27 | const result = await service.fetchAll(offset, limit); 28 | 29 | expect(fetchMock).toHaveBeenCalledWith( 30 | `${baseUrl}/todos?offset=${offset}&limit=${limit}`, 31 | { 32 | method: 'GET', 33 | headers: { 34 | 'Content-Type': 'application/json', 35 | }, 36 | } 37 | ); 38 | expect(result).toBe(todos); 39 | }); 40 | 41 | it('should handle fetch', async () => { 42 | const id = '1'; 43 | const todo = generateTodoMock(); 44 | 45 | const fetchMock = jest.fn().mockResolvedValueOnce({ json: () => todo }); 46 | globalThis.fetch = fetchMock; 47 | 48 | const result = await service.fetch(id); 49 | 50 | expect(fetchMock).toHaveBeenCalledWith(`${baseUrl}/todos/${id}`, { 51 | method: 'GET', 52 | headers: { 53 | 'Content-Type': 'application/json', 54 | }, 55 | }); 56 | expect(result).toBe(todo); 57 | }); 58 | 59 | it('should handle create', async () => { 60 | const todo = generateTodoMock(); 61 | const dto = generateTodoCreateDtoMock(); 62 | 63 | const fetchMock = jest.fn().mockResolvedValueOnce({ json: () => todo }); 64 | globalThis.fetch = fetchMock; 65 | 66 | const result = await service.create(dto); 67 | 68 | expect(fetchMock).toHaveBeenCalledWith(`${baseUrl}/todos`, { 69 | method: 'POST', 70 | body: JSON.stringify(dto), 71 | headers: { 72 | 'Content-Type': 'application/json', 73 | }, 74 | }); 75 | expect(result).toBe(todo); 76 | }); 77 | 78 | it('should handle update', async () => { 79 | const id = '1'; 80 | const todo = generateTodoMock(); 81 | const dto = generateTodoUpdateDtoMock(); 82 | 83 | const fetchMock = jest.fn().mockResolvedValueOnce({ json: () => todo }); 84 | globalThis.fetch = fetchMock; 85 | 86 | const result = await service.update(id, dto); 87 | 88 | expect(fetchMock).toHaveBeenCalledWith(`${baseUrl}/todos/${id}`, { 89 | method: 'PUT', 90 | body: JSON.stringify(dto), 91 | headers: { 92 | 'Content-Type': 'application/json', 93 | }, 94 | }); 95 | expect(result).toBe(todo); 96 | }); 97 | 98 | it('should handle remove', async () => { 99 | const id = '1'; 100 | 101 | const fetchMock = jest.fn().mockResolvedValueOnce(undefined); 102 | globalThis.fetch = fetchMock; 103 | 104 | const result = await service.remove(id); 105 | 106 | expect(fetchMock).toHaveBeenCalledWith(`${baseUrl}/todos/${id}`, { 107 | method: 'DELETE', 108 | headers: { 109 | 'Content-Type': 'application/json', 110 | }, 111 | }); 112 | expect(result).toBe(id); 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /src/store/todo/reducers/todo.reducer.spec.ts: -------------------------------------------------------------------------------- 1 | import { Todo } from '../../../models'; 2 | import { State, initialState, adapter } from '../states'; 3 | import { 4 | fetchAllTodos, 5 | fetchTodo, 6 | createTodo, 7 | updateTodo, 8 | removeTodo, 9 | } from '../actions'; 10 | import { reducer } from './todo.reducer'; 11 | import { 12 | generateTodoCreateDtoMock, 13 | generateTodoMock, 14 | generateTodosMock, 15 | generateTodoUpdateDtoMock, 16 | } from '../../../models/testing'; 17 | 18 | describe('reducers', () => { 19 | it('should handle unknown action', () => { 20 | const action = { 21 | type: '', 22 | }; 23 | 24 | expect(reducer(undefined, action)).toEqual(initialState); 25 | }); 26 | 27 | it(`should handle ${fetchAllTodos.pending.type}`, () => { 28 | const state: State = { 29 | ...initialState, 30 | }; 31 | const action = fetchAllTodos.pending('', { offset: 0, limit: 10 }); 32 | const expectedState: State = { 33 | ...state, 34 | isFetching: true, 35 | }; 36 | 37 | expect(reducer(state, action)).toEqual(expectedState); 38 | }); 39 | 40 | it(`should handle ${fetchAllTodos.fulfilled.type}`, () => { 41 | const state: State = { 42 | ...initialState, 43 | isFetching: true, 44 | }; 45 | const action = fetchAllTodos.fulfilled({ todos: generateTodosMock() }, '', { 46 | offset: 0, 47 | limit: 10, 48 | }); 49 | const { todos } = action.payload; 50 | const expectedState: State = adapter.setAll( 51 | { 52 | ...state, 53 | isFetching: false, 54 | }, 55 | todos 56 | ); 57 | 58 | expect(reducer(state, action)).toEqual(expectedState); 59 | }); 60 | 61 | it(`should handle ${fetchAllTodos.rejected.type}`, () => { 62 | const state: State = { 63 | ...initialState, 64 | isFetching: true, 65 | }; 66 | const action = fetchAllTodos.rejected(new Error(), '', { 67 | offset: 0, 68 | limit: 10, 69 | }); 70 | const expectedState: State = { 71 | ...state, 72 | isFetching: false, 73 | }; 74 | 75 | expect(reducer(state, action)).toEqual(expectedState); 76 | }); 77 | 78 | it(`should handle ${fetchTodo.pending.type}`, () => { 79 | const state: State = { 80 | ...initialState, 81 | }; 82 | const action = fetchTodo.pending('', { id: '1' }); 83 | const expectedState: State = { 84 | ...state, 85 | isFetching: true, 86 | selectedId: '1', 87 | }; 88 | 89 | expect(reducer(state, action)).toEqual(expectedState); 90 | }); 91 | 92 | it(`should handle ${fetchTodo.fulfilled.type}`, () => { 93 | const todos = generateTodosMock(); 94 | const state: State = adapter.setAll( 95 | { 96 | ...initialState, 97 | isFetching: true, 98 | selectedId: '1', 99 | }, 100 | todos 101 | ); 102 | const action = fetchTodo.fulfilled({ todo: generateTodoMock() }, '', { 103 | id: '1', 104 | }); 105 | const { todo } = action.payload; 106 | const expectedState: State = adapter.upsertOne( 107 | { 108 | ...state, 109 | isFetching: false, 110 | }, 111 | todo 112 | ); 113 | 114 | expect(reducer(state, action)).toEqual(expectedState); 115 | }); 116 | 117 | it(`should handle ${fetchTodo.rejected.type}`, () => { 118 | const state: State = { 119 | ...initialState, 120 | isFetching: true, 121 | }; 122 | const action = fetchTodo.rejected(new Error(), '', { id: '1' }); 123 | const expectedState: State = { 124 | ...state, 125 | isFetching: false, 126 | }; 127 | 128 | expect(reducer(state, action)).toEqual(expectedState); 129 | }); 130 | 131 | it(`should handle ${createTodo.pending.type}`, () => { 132 | const state: State = { 133 | ...initialState, 134 | }; 135 | const action = createTodo.pending('', { 136 | todo: generateTodoCreateDtoMock(), 137 | }); 138 | const expectedState: State = { 139 | ...state, 140 | isFetching: true, 141 | }; 142 | 143 | expect(reducer(state, action)).toEqual(expectedState); 144 | }); 145 | 146 | it(`should handle ${createTodo.fulfilled.type}`, () => { 147 | const entries: Todo[] = []; 148 | const state: State = adapter.setAll( 149 | { 150 | ...initialState, 151 | isFetching: true, 152 | }, 153 | entries 154 | ); 155 | const action = createTodo.fulfilled({ todo: generateTodoMock() }, '', { 156 | todo: generateTodoCreateDtoMock(), 157 | }); 158 | const { todo } = action.payload; 159 | const expectedState: State = adapter.addOne( 160 | { 161 | ...state, 162 | isFetching: false, 163 | }, 164 | todo 165 | ); 166 | 167 | expect(reducer(state, action)).toEqual(expectedState); 168 | }); 169 | 170 | it(`should handle ${createTodo.rejected.type}`, () => { 171 | const state: State = { 172 | ...initialState, 173 | isFetching: true, 174 | }; 175 | const action = createTodo.rejected(new Error(), '', { 176 | todo: generateTodoCreateDtoMock(), 177 | }); 178 | const expectedState: State = { 179 | ...state, 180 | isFetching: false, 181 | }; 182 | 183 | expect(reducer(state, action)).toEqual(expectedState); 184 | }); 185 | 186 | it(`should handle ${updateTodo.pending.type}`, () => { 187 | const state: State = { 188 | ...initialState, 189 | }; 190 | const action = updateTodo.pending('', { 191 | id: '1', 192 | todo: generateTodoUpdateDtoMock(), 193 | }); 194 | const expectedState: State = { 195 | ...state, 196 | isFetching: true, 197 | }; 198 | 199 | expect(reducer(state, action)).toEqual(expectedState); 200 | }); 201 | 202 | it(`should handle ${updateTodo.fulfilled.type}`, () => { 203 | const todos = generateTodosMock(); 204 | const state: State = adapter.setAll( 205 | { 206 | ...initialState, 207 | isFetching: true, 208 | }, 209 | todos 210 | ); 211 | const action = updateTodo.fulfilled({ todo: generateTodoMock() }, '', { 212 | id: '1', 213 | todo: generateTodoUpdateDtoMock(), 214 | }); 215 | const { todo } = action.payload; 216 | const expectedState: State = adapter.updateOne( 217 | { 218 | ...state, 219 | isFetching: false, 220 | }, 221 | { 222 | id: todo.id, 223 | changes: todo, 224 | } 225 | ); 226 | 227 | expect(reducer(state, action)).toEqual(expectedState); 228 | }); 229 | 230 | it(`should handle ${updateTodo.rejected.type}`, () => { 231 | const state: State = { 232 | ...initialState, 233 | isFetching: true, 234 | }; 235 | const action = updateTodo.rejected(new Error(), '', { 236 | id: '1', 237 | todo: generateTodoUpdateDtoMock(), 238 | }); 239 | const expectedState: State = { 240 | ...state, 241 | isFetching: false, 242 | }; 243 | 244 | expect(reducer(state, action)).toEqual(expectedState); 245 | }); 246 | 247 | it(`should handle ${removeTodo.pending.type}`, () => { 248 | const state: State = { 249 | ...initialState, 250 | isFetching: false, 251 | }; 252 | const action = removeTodo.pending('', { id: '1' }); 253 | const expectedState: State = { 254 | ...state, 255 | isFetching: true, 256 | selectedId: '1', 257 | }; 258 | 259 | expect(reducer(state, action)).toEqual(expectedState); 260 | }); 261 | 262 | it(`should handle ${removeTodo.fulfilled.type}`, () => { 263 | const todos = generateTodosMock(); 264 | const state: State = adapter.setAll( 265 | { 266 | ...initialState, 267 | isFetching: true, 268 | }, 269 | todos 270 | ); 271 | const action = removeTodo.fulfilled({ id: '1' }, '', { id: '1' }); 272 | const { id } = action.payload; 273 | const expectedState: State = adapter.removeOne( 274 | { 275 | ...state, 276 | isFetching: false, 277 | }, 278 | id 279 | ); 280 | 281 | expect(reducer(state, action)).toEqual(expectedState); 282 | }); 283 | 284 | it(`should handle ${removeTodo.rejected.type}`, () => { 285 | const state: State = { 286 | ...initialState, 287 | isFetching: true, 288 | }; 289 | const action = removeTodo.rejected(new Error(), '', { id: '1' }); 290 | const expectedState: State = { 291 | ...state, 292 | isFetching: false, 293 | }; 294 | 295 | expect(reducer(state, action)).toEqual(expectedState); 296 | }); 297 | }); 298 | --------------------------------------------------------------------------------