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..ba9343e3 100644
--- a/src/App.test.tsx
+++ b/src/App.test.tsx
@@ -1,7 +1,16 @@
+// import { render, screen } from "@testing-library/react";
import { render } from "@testing-library/react";
+import { Provider } from "react-redux";
import App from "./App";
+import { store } from "./store";
-test("renders learn react link", () => {
- render();
+test("renders App.tsx", () => {
+ render(
+
+
+
+ );
+ // screen.debug();
+ //expect(true).toBe(false); // This make failing test case
});
diff --git a/src/components/Todo/Todo.test.tsx b/src/components/Todo/Todo.test.tsx
new file mode 100644
index 00000000..b01bdcb7
--- /dev/null
+++ b/src/components/Todo/Todo.test.tsx
@@ -0,0 +1,25 @@
+import { render, screen } from "@testing-library/react";
+import Todo from "./Todo";
+
+describe("", () => {
+ it("should render without errors", () => {
+ render();
+ screen.getByText("TODO_TITLE"); // Implicit assertion
+ const doneButton = screen.getByText("Done"); // Implicit assertion
+ expect(doneButton).toBeInTheDocument(); // Explicit assertion
+ });
+
+ it("should render done mark when done is true", () => {
+ render();
+ const title = screen.getByText("TODO_TITLE");
+ expect(title.classList.contains("done")).toBe(true);
+ screen.getByText("Undone");
+ });
+
+ it("should render undone mark when done is false", () => {
+ render();
+ const title = screen.getByText("TODO_TITLE");
+ expect(title.classList.contains("done")).toBe(false);
+ screen.getByText("Done");
+ });
+});
diff --git a/src/components/TodoDetail/TodoDetail.test.tsx b/src/components/TodoDetail/TodoDetail.test.tsx
new file mode 100644
index 00000000..7030d978
--- /dev/null
+++ b/src/components/TodoDetail/TodoDetail.test.tsx
@@ -0,0 +1,50 @@
+import { screen } from "@testing-library/react";
+import axios from "axios";
+import { MemoryRouter, Navigate, Route, Routes } from "react-router";
+import { renderWithProviders } from "../../test-utils/mock";
+import TodoDetail from "./TodoDetail";
+
+const renderTodoDetail = () => {
+ renderWithProviders(
+
+
+ } />
+ } />
+
+ ,
+ {
+ preloadedState: {
+ todo: {
+ todos: [
+ { id: 3, title: "TODO_TEST_TITLE_3", content: "TODO_TEST_CONTENT_3", done: false },
+ ],
+ selectedTodo: null,
+ },
+ },
+ }
+ );
+};
+
+describe("", () => {
+ it("should render without errors", async () => {
+ jest.spyOn(axios, "get").mockImplementation(() => {
+ return Promise.resolve({
+ data: {
+ id: 3,
+ title: "TODO_TEST_TITLE_3",
+ content: "TODO_TEST_CONTENT_3",
+ done: false,
+ },
+ });
+ });
+ renderTodoDetail();
+ await screen.findByText("TODO_TEST_TITLE_3");
+ await screen.findByText("TODO_TEST_CONTENT_3");
+ });
+
+ it("should not render if there is no todo", async () => {
+ renderTodoDetail();
+ 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..a6837d45
--- /dev/null
+++ b/src/containers/TodoList/NewTodo/NewTodo.test.tsx
@@ -0,0 +1,78 @@
+import { fireEvent, screen, waitFor } from "@testing-library/react";
+import axios from "axios";
+
+import NewTodo from "./NewTodo";
+import { renderWithProviders } from "../../../test-utils/mock";
+import * as todoSlice 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,
+}));
+
+describe("", () => {
+ it("should render without errors", () => {
+ renderWithProviders();
+ screen.getByText("Add a Todo");
+ });
+ it("should render title input", () => {
+ renderWithProviders();
+ screen.getByLabelText("Title");
+ });
+ it("should render content input", () => {
+ renderWithProviders();
+ screen.getByLabelText("Content");
+ });
+ it("should render submit button", () => {
+ renderWithProviders();
+ 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,
+ },
+ });
+ renderWithProviders();
+ 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"));
+ renderWithProviders();
+ 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..b8b2d4a9
--- /dev/null
+++ b/src/containers/TodoList/TodoList.test.tsx
@@ -0,0 +1,94 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import { Provider } from "react-redux";
+import { MemoryRouter, Route, Routes } from "react-router";
+import { TodoState } from "../../store/slices/todo";
+import { getMockStore } from "../../test-utils/mock";
+import TodoList from "./TodoList";
+import { IProps as TodoProps } from "../../components/Todo/Todo";
+
+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..3d8a92d8
--- /dev/null
+++ b/src/store/slices/todo.test.ts
@@ -0,0 +1,79 @@
+import { AnyAction, configureStore, EnhancedStore } from "@reduxjs/toolkit";
+import axios from "axios";
+import { ThunkMiddleware } from "redux-thunk";
+import reducer, { TodoState } from "./todo";
+import { fetchTodos, fetchTodo, postTodo, deleteTodo, toggleDone } from "./todo";
+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.tsx b/src/test-utils/mock.tsx
new file mode 100644
index 00000000..943f1034
--- /dev/null
+++ b/src/test-utils/mock.tsx
@@ -0,0 +1,35 @@
+import { configureStore, PreloadedState } from "@reduxjs/toolkit";
+import { render, RenderOptions } from "@testing-library/react";
+import { PropsWithChildren } from "react";
+import { Provider } from "react-redux";
+import { AppStore, RootState } from "../store";
+import todoReducer from "../store/slices/todo";
+
+interface ExtendedRenderOptions extends Omit {
+ preloadedState?: PreloadedState;
+ store?: AppStore;
+}
+
+export const getMockStore = (preloadedState?: PreloadedState) => {
+ return configureStore({
+ reducer: { todo: todoReducer },
+ preloadedState,
+ });
+};
+
+export function renderWithProviders(
+ ui: React.ReactElement,
+ {
+ preloadedState,
+ // Automatically create a store instance if no store was passed in
+ store = getMockStore(preloadedState),
+ ...renderOptions
+ }: ExtendedRenderOptions = {}
+) {
+ function Wrapper({ children }: PropsWithChildren): JSX.Element {
+ return {children};
+ }
+
+ // Return an object with the store and all of RTL's query functions
+ return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) };
+}