diff --git a/package.json b/package.json
index e51db5bc..9185d341 100644
--- a/package.json
+++ b/package.json
@@ -46,5 +46,12 @@
"last 1 firefox version",
"last 1 safari version"
]
+ },
+ "jest": {
+ "collectCoverageFrom": [
+ "src/**/*.{js,jsx,ts,tsx}",
+ "!src/index.tsx",
+ "!src/test-utils/*"
+ ]
}
}
\ No newline at end of file
diff --git a/src/App.test.tsx b/src/App.test.tsx
index 838df452..893840e3 100644
--- a/src/App.test.tsx
+++ b/src/App.test.tsx
@@ -1,7 +1,13 @@
-import { render } from "@testing-library/react";
+import { render, screen } from "@testing-library/react";
import App from "./App";
+import { Provider } from 'react-redux';
+import { store } from './store';
-test("renders learn react link", () => {
- render();
+test("renders App.tsx", () => {
+ render(
+
+ );
+ // screen.debug();
+ // expect(true).toBe(true)
});
diff --git a/src/components/Todo/Todo.test.tsx b/src/components/Todo/Todo.test.tsx
new file mode 100644
index 00000000..b2a3df13
--- /dev/null
+++ b/src/components/Todo/Todo.test.tsx
@@ -0,0 +1,18 @@
+import Todo from './Todo';
+import { render, screen } from '@testing-library/react';
+
+describe('', () => {
+ it('should render done mark when done is false', () => {
+ render()
+ const title = screen.getByText('TODO_TITLE')
+ expect(title.classList.contains('done')).toBe(false)
+ screen.getByText('Done')
+ })
+
+ it('should render undone mark when done is true', () => {
+ render()
+ const title = screen.getByText('TODO_TITLE')
+ expect(title.classList.contains('done')).toBe(true)
+ screen.getByText('Undone')
+ })
+})
\ No newline at end of file
diff --git a/src/components/TodoDetail/TodoDetail.test.tsx b/src/components/TodoDetail/TodoDetail.test.tsx
new file mode 100644
index 00000000..0754a587
--- /dev/null
+++ b/src/components/TodoDetail/TodoDetail.test.tsx
@@ -0,0 +1,52 @@
+import { Provider } from 'react-redux';
+import { TodoState } from '../../store/slices/todo';
+import { getMockStore } from '../../test-utils/mock';
+import { MemoryRouter } from 'react-router';
+import { Navigate, Route, Routes } from 'react-router-dom';
+import TodoDetail from './TodoDetail';
+import { render, screen } from '@testing-library/react';
+import axios from 'axios';
+
+const stubInitialState: TodoState = {
+ todos: [
+ { id: 3, title: 'TODO_TEST_TITLE_3', content: 'TODO_TEST_CONTENT_3', done: false },
+ ],
+ selectedTodo: null,
+}
+
+const mockStore = getMockStore({ todo: stubInitialState })
+
+describe('', () => {
+ let todoDetail: JSX.Element = (
+
+
+
+ } />
+ } />
+
+
+
+ )
+
+ it('should render TodoDetail', async () => {
+ jest.spyOn(axios, 'get').mockImplementation(() => {
+ return Promise.resolve({
+ data: {
+ id: 3,
+ title: 'TODO_TEST_TITLE_3',
+ content: 'TODO_TEST_CONTENT_3',
+ done: false,
+ },
+ })
+ })
+ render(todoDetail)
+ await screen.findByText('TODO_TEST_TITLE_3')
+ await screen.findByText('TODO_TEST_CONTENT_3')
+ })
+
+ it('should not render if there is no todo', async () => {
+ render(todoDetail)
+ jest.spyOn(axios, 'get').mockImplementationOnce(() => Promise.reject())
+ expect(screen.queryAllByText('TODO_TEST_TITLE_3')).toHaveLength(0)
+ })
+})
diff --git a/src/containers/TodoList/NewTodo/NewTodo.test.tsx b/src/containers/TodoList/NewTodo/NewTodo.test.tsx
new file mode 100644
index 00000000..69b825c8
--- /dev/null
+++ b/src/containers/TodoList/NewTodo/NewTodo.test.tsx
@@ -0,0 +1,96 @@
+import { fireEvent, render, screen, waitFor } from '@testing-library/react';
+import NewTodo from './NewTodo';
+import axios from 'axios';
+import * as todoSlice from "../../../store/slices/todo";
+import { Provider } from 'react-redux';
+import { MemoryRouter } from 'react-router';
+import { Navigate, Route, Routes } from 'react-router-dom';
+import TodoDetail from '../../../components/TodoDetail/TodoDetail';
+import { getMockStore } from '../../../test-utils/mock';
+import { TodoState } from '../../../store/slices/todo';
+
+const mockNavigate = jest.fn()
+jest.mock('react-router', () => ({
+ ...jest.requireActual('react-router'),
+ Navigate: (props: any) => {
+ mockNavigate(props.to);
+ return null;
+ },
+ useNavigate: () => mockNavigate,
+}))
+
+const stubInitialState: TodoState = {
+ todos: [],
+ selectedTodo: null,
+}
+
+const mockStore = getMockStore({ todo: stubInitialState })
+
+describe('', () => {
+ let todoDetail: JSX.Element = (
+
+
+
+ )
+
+ it('should render without errors', () => {
+ render(todoDetail)
+ screen.getByText('Add a Todo')
+ })
+ it('should render title input', () => {
+ render(todoDetail)
+ screen.getByLabelText('Title')
+ })
+ it('should render content input', () => {
+ render(todoDetail)
+ screen.getByLabelText('Content')
+ })
+ it('should render submit button', () => {
+ render(todoDetail)
+ screen.getByText('Submit')
+ })
+ it('should render navigate to /todos when submitted', async () => {
+ jest.spyOn(axios, 'post').mockResolvedValueOnce({
+ data: {
+ id: 1,
+ title: 'TITLE',
+ content: 'CONTENT',
+ done: false,
+ },
+ })
+ render(todoDetail)
+ const titleInput = screen.getByLabelText('Title')
+ const contentInput = screen.getByLabelText('Content')
+ const submitButton = screen.getByText('Submit')
+ fireEvent.change(titleInput, { target: { value: 'TITLE' }})
+ fireEvent.change(contentInput, { target: { value: 'CONTENT' }})
+ await screen.findByDisplayValue('TITLE')
+ await screen.findByDisplayValue('CONTENT')
+ fireEvent.click(submitButton)
+
+ await waitFor(() => expect(mockNavigate).toHaveBeenCalledWith('/todos'))
+ })
+ it('should alert error when submitted', async () => {
+ const mockPostTodo = jest.spyOn(todoSlice, 'postTodo')
+ window.alert = jest.fn()
+ console.error = jest.fn()
+ jest.spyOn(axios, 'post').mockRejectedValueOnce(new Error('ERROR'))
+
+ render(todoDetail)
+ const titleInput = screen.getByLabelText('Title')
+ const contentInput = screen.getByLabelText('Content')
+ const submitButton = screen.getByText('Submit')
+ fireEvent.change(titleInput, { target: { value: 'TITLE' }})
+ fireEvent.change(contentInput, { target: { value: 'CONTENT' }})
+ await screen.findByDisplayValue('TITLE')
+ await screen.findByDisplayValue('CONTENT')
+ fireEvent.click(submitButton)
+
+ expect(mockPostTodo).toHaveBeenCalledWith({
+ title: 'TITLE',
+ content: 'CONTENT',
+ })
+ await waitFor(() => expect(mockNavigate).not.toHaveBeenCalled())
+ await waitFor(() => expect(window.alert).toHaveBeenCalledWith('Error on post Todo'))
+ })
+})
diff --git a/src/containers/TodoList/TodoList.test.tsx b/src/containers/TodoList/TodoList.test.tsx
new file mode 100644
index 00000000..74344361
--- /dev/null
+++ b/src/containers/TodoList/TodoList.test.tsx
@@ -0,0 +1,99 @@
+import { IProps as TodoProps } from "../../components/Todo/Todo"
+import { TodoState } from '../../store/slices/todo';
+import { getMockStore } from '../../test-utils/mock';
+import { Provider } from 'react-redux';
+import { MemoryRouter } from 'react-router';
+import { Route, Routes } from 'react-router-dom';
+import TodoList from './TodoList';
+import { fireEvent, render, screen } from '@testing-library/react';
+import exp from 'constants';
+
+jest.mock('../../components/Todo/Todo', () => (props: TodoProps) => (
+
+
+ {props.title}
+
+
+
+
+))
+
+const stubInitialState: TodoState = {
+ todos: [
+ { id: 1, title: "TODO_TEST_TITLE_1", content: "TODO_TEST_CONTENT_1", done: false },
+ { id: 2, title: "TODO_TEST_TITLE_2", content: "TODO_TEST_CONTENT_2", done: false },
+ { id: 3, title: "TODO_TEST_TITLE_3", content: "TODO_TEST_CONTENT_3", done: false },
+ ],
+ selectedTodo: null,
+}
+
+const mockStore = getMockStore({ todo: stubInitialState })
+
+const mockNavigate = jest.fn()
+jest.mock('react-router', () => ({
+ ...jest.requireActual('react-router'),
+ useNavigate: () => mockNavigate,
+}))
+
+const mockDispatch = jest.fn()
+jest.mock('react-redux', () => ({
+ ...jest.requireActual('react-redux'),
+ useDispatch: () => mockDispatch,
+}))
+
+describe('', () => {
+ let todoList: JSX.Element
+ beforeEach(() => {
+ jest.clearAllMocks()
+ todoList = (
+
+
+
+ } />
+
+
+
+ )
+ })
+
+ it('should render TodoList', () => {
+ const { container } = render(todoList)
+ expect(container).toBeTruthy()
+ })
+ it('should render todos', () => {
+ render(todoList)
+ const todos = screen.getAllByTestId('spyTodo')
+ expect(todos).toHaveLength(3)
+ })
+ it('should handle clickDetail', () => {
+ render(todoList)
+ const todos = screen.getAllByTestId('spyTodo')
+ const todo = todos[0]
+ // eslint-disable-next-line testing-library/no-node-access
+ const title = todo.querySelector('.title')
+ fireEvent.click(title!)
+ expect(mockNavigate).toHaveBeenCalledTimes(1)
+ })
+ it('should handle clickDone', () => {
+ render(todoList)
+ const todos = screen.getAllByTestId('spyTodo')
+ const todo = todos[0]
+ // eslint-disable-next-line testing-library/no-node-access
+ const doneButton = todo.querySelector('.doneButton')
+ fireEvent.click(doneButton!)
+ expect(mockDispatch).toHaveBeenCalled()
+ })
+ it('should handle clickDelete', () => {
+ render(todoList)
+ const todos = screen.getAllByTestId('spyTodo')
+ const todo = todos[0]
+ // eslint-disable-next-line testing-library/no-node-access
+ const deleteButton = todo.querySelector('.deleteButton')
+ fireEvent.click(deleteButton!)
+ expect(mockDispatch).toHaveBeenCalled()
+ })
+})
diff --git a/src/store/slices/todo.test.ts b/src/store/slices/todo.test.ts
new file mode 100644
index 00000000..d2bc5d2b
--- /dev/null
+++ b/src/store/slices/todo.test.ts
@@ -0,0 +1,73 @@
+import { AnyAction, configureStore, EnhancedStore } from '@reduxjs/toolkit';
+import reducer, { deleteTodo, fetchTodo, fetchTodos, postTodo, TodoState, toggleDone } from './todo';
+import { ThunkMiddleware } from 'redux-thunk'
+import exp from 'constants';
+import axios from 'axios';
+
+describe('todo reducer', () => {
+ let store: EnhancedStore<{ todo: TodoState }, AnyAction, [ThunkMiddleware<{ todo: TodoState }, AnyAction, undefined>]>
+ const fakeTodo = {
+ id: 1,
+ title: 'test',
+ content: 'test',
+ done: false
+ }
+
+ beforeAll(() => {
+ store = configureStore(
+ { reducer: { todo: reducer }}
+ )
+ })
+
+ it('should handle initial state', () => {
+ expect(reducer(undefined, { type: 'unknown' })).toEqual({
+ todos: [],
+ selectedTodo: null,
+ })
+ })
+ it('should handle fetchTodos', async () => {
+ axios.get = jest.fn().mockResolvedValue({ data: [fakeTodo] })
+ await store.dispatch(fetchTodos())
+ expect(store.getState().todo.todos).toEqual([fakeTodo])
+ })
+ it('should handle fetchTodo', async () => {
+ axios.get = jest.fn().mockResolvedValue({ data: fakeTodo })
+ await store.dispatch(fetchTodo(1))
+ expect(store.getState().todo.selectedTodo).toEqual(fakeTodo)
+ })
+ it('should handle deleteTodo', async () => {
+ axios.delete = jest.fn().mockResolvedValue({ data: null })
+ await store.dispatch(deleteTodo(1))
+ expect(store.getState().todo.todos).toEqual([])
+ })
+ it('should handle postTodo', async () => {
+ jest.spyOn(axios, 'post').mockResolvedValue({
+ data: fakeTodo,
+ })
+ await store.dispatch(postTodo({ title: 'test', content: 'test' }))
+ expect(store.getState().todo.todos).toEqual([fakeTodo])
+ })
+ it('should handle toggleDone', async () => {
+ jest.spyOn(axios, 'put').mockResolvedValue({ data: fakeTodo })
+ await store.dispatch(toggleDone(fakeTodo.id))
+ expect(store.getState().todo.todos.find((v) => v.id === fakeTodo.id)?.done).toEqual(true)
+ })
+ it('should handle error on postTodo', async () => {
+ const mockConsoleError = jest.fn()
+ console.error = mockConsoleError
+ jest.spyOn(axios, 'post').mockRejectedValue({ response: { data: { title: ['error'] }}})
+ await store.dispatch(postTodo({ title: 'test', content: 'test' }))
+ expect(mockConsoleError).toBeCalled()
+ })
+ it('should handle null on fetchTodo', async () => {
+ axios.get = jest.fn().mockResolvedValue({ data: null })
+ await store.dispatch(fetchTodo(1))
+ expect(store.getState().todo.selectedTodo).toEqual(null)
+ })
+ it('should handle not existing todo toggle', async () => {
+ const beforeState = store.getState().todo
+ jest.spyOn(axios, 'put').mockResolvedValue({ data: { ...fakeTodo, id: 10 }})
+ await store.dispatch(toggleDone(2))
+ expect(store.getState().todo).toEqual(beforeState)
+ })
+})
diff --git a/src/store/slices/todo.ts b/src/store/slices/todo.ts
index 5f1a2dce..8b49fa71 100644
--- a/src/store/slices/todo.ts
+++ b/src/store/slices/todo.ts
@@ -60,11 +60,6 @@ export const todoSlice = createSlice({
name: "todo",
initialState,
reducers: {
- getAll: (state, action: PayloadAction<{ todos: TodoType[] }>) => {},
- getTodo: (state, action: PayloadAction<{ targetId: number }>) => {
- const target = state.todos.find((td) => td.id === action.payload.targetId);
- state.selectedTodo = target ?? null;
- },
toggleDone: (state, action: PayloadAction<{ targetId: number }>) => {
const todo = state.todos.find((value) => value.id === action.payload.targetId);
if (todo) {
diff --git a/src/test-utils/mock.ts b/src/test-utils/mock.ts
new file mode 100644
index 00000000..ae612042
--- /dev/null
+++ b/src/test-utils/mock.ts
@@ -0,0 +1,11 @@
+import { PreloadedState } from 'redux';
+import { RootState } from '../store';
+import { configureStore } from '@reduxjs/toolkit';
+import todoReducer from "../store/slices/todo";
+
+export const getMockStore = (preloadedState?: PreloadedState) => {
+ return configureStore({
+ reducer: { todo: todoReducer },
+ preloadedState,
+ })
+}