From 57b2b511ece5f191477b65de9d6c71817e88151a Mon Sep 17 00:00:00 2001 From: Young-mo Yoo Date: Wed, 27 Sep 2023 15:54:19 +0900 Subject: [PATCH] =?UTF-8?q?#6=20=EC=83=98=ED=94=8C=20=EC=83=81=ED=92=88=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=ED=8E=98=EC=9D=B4=EC=A7=80=20Jest=20?= =?UTF-8?q?=EB=8B=A8=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=EB=A5=BC=20=EC=9E=91=EC=84=B1=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/pages/sample/product/list.test.tsx | 227 ++++++++++++++++++ jest.config.mjs | 27 +++ jest.setup.js | 9 + package.json | 9 + .../page/sample/product/product-list.tsx | 3 +- .../page/sample/product/product-search.tsx | 4 +- 6 files changed, 276 insertions(+), 3 deletions(-) create mode 100644 __tests__/pages/sample/product/list.test.tsx create mode 100644 jest.config.mjs create mode 100644 jest.setup.js diff --git a/__tests__/pages/sample/product/list.test.tsx b/__tests__/pages/sample/product/list.test.tsx new file mode 100644 index 0000000..8a1df49 --- /dev/null +++ b/__tests__/pages/sample/product/list.test.tsx @@ -0,0 +1,227 @@ +import {fireEvent, render, screen, waitFor} from "@testing-library/react" +import "@testing-library/jest-dom" +import ProductListPage from "@/pages/sample/product/list"; +import mockRouter from "next-router-mock"; +import {when} from "jest-when"; +import {useProducts} from "@/client/sample/product"; +import dayjs from "dayjs"; + +jest.mock("@/client/sample/product"); + +describe("테이블 랜더링", () => { + it("URL 파라메터가 없으면 1페이지에 해당하는 아이템을 5개씩 테이블에 보여준다", async () => { + // given + when(useProducts as jest.Mock).calledWith({ + page: 1 + }).mockReturnValue({ + data: { + data: { + page: { + currentPage: 1, + pageSize: 5, + totalPage: 1, + totalCount: 6 + }, + items: [ + { + id: 1, + code: "A0001", + brand: "apple", + name: "iPhone 14 Pro", + price: 1550000, + status: "SALE", + createdAt: "2023-02-02T10:00:00+09:00", + updatedAt: "2023-02-02T10:00:00+09:00", + }, + { + id: 2, + code: "A0002", + brand: "파타고니아", + name: "클래식 레트로-X 후리스 플리스 자켓", + price: 230000, + status: "SALE", + createdAt: "2023-02-02T11:00:00+09:00", + updatedAt: "2023-02-02T11:00:00+09:00", + }, + { + id: 3, + code: "A0003", + brand: "다이슨", + name: "dyson v15 detect complete", + price: 1290000, + status: "SOLDOUT", + createdAt: "2023-02-02T12:00:00+09:00", + updatedAt: "2023-02-02T12:00:00+09:00", + }, + { + id: 4, + code: "A0004", + brand: "Aēsop", + name: "레저렉션 아로마틱 핸드 워시", + price: 47000, + status: "NOTSALE", + createdAt: "2023-02-02T13:00:00+09:00", + updatedAt: "2023-02-02T13:00:00+09:00", + }, + { + id: 5, + code: "A0005", + brand: "LUSH", + name: "더티 보디 스프레이", + price: 60000, + status: "SALE", + createdAt: "2023-02-02T14:00:00+09:00", + updatedAt: "2023-02-02T14:00:00+09:00", + }, + ] + } + } + }); + + // when + await mockRouter.push("/sample/product/list"); + render() + await waitFor(() => screen.getByTestId("product-table")); + + // then + const productTable = screen.getByTestId("product-table"); + expect(productTable).toBeInTheDocument(); + + // 테이블 헤더 확인 + const tableHeader = productTable.querySelector("thead > tr"); + expect(tableHeader).not.toBeNull(); + const headerColumns = (tableHeader as Element).querySelectorAll("th"); + expect(headerColumns).toHaveLength(7); + expect(headerColumns[0]).toHaveTextContent(""); + expect(headerColumns[1]).toHaveTextContent("상품코드"); + expect(headerColumns[2]).toHaveTextContent("상품명"); + expect(headerColumns[3]).toHaveTextContent("금액"); + expect(headerColumns[4]).toHaveTextContent("판매상태"); + expect(headerColumns[5]).toHaveTextContent("생성일시"); + expect(headerColumns[6]).toHaveTextContent("수정일시"); + + // 테이블 바디 확인 + const tableBodyRows = productTable.querySelectorAll("tbody > tr"); + expect(tableBodyRows).toHaveLength(6); // ant table은 컨텐츠이 담기지 않는 TR이 하나 존재하기 때문에 컨텐츠 + 1를 확인한다. + + const bodyRow1Columns = tableBodyRows[1].querySelectorAll("td"); + expect(bodyRow1Columns).toHaveLength(8); + expect(bodyRow1Columns[2]).toHaveTextContent("A0001"); + expect(bodyRow1Columns[3]).toHaveTextContent("appleiPhone 14 Pro"); + expect(bodyRow1Columns[4]).toHaveTextContent("1,550,000원"); + expect(bodyRow1Columns[5]).toHaveTextContent("SALE"); + expect(bodyRow1Columns[6]).toHaveTextContent("2023/02/0210:00"); + expect(bodyRow1Columns[7]).toHaveTextContent("2023/02/0210:00"); + + const bodyRow2Columns = tableBodyRows[2].querySelectorAll("td"); + expect(bodyRow2Columns).toHaveLength(8); + expect(bodyRow2Columns[2]).toHaveTextContent("A0002"); + expect(bodyRow2Columns[3]).toHaveTextContent("파타고니아클래식 레트로-X 후리스 플리스 자켓"); + expect(bodyRow2Columns[4]).toHaveTextContent("230,000원"); + expect(bodyRow2Columns[5]).toHaveTextContent("SALE"); + expect(bodyRow2Columns[6]).toHaveTextContent("2023/02/0211:00"); + expect(bodyRow2Columns[7]).toHaveTextContent("2023/02/0211:00"); + + const bodyRow3Columns = tableBodyRows[3].querySelectorAll("td"); + expect(bodyRow3Columns).toHaveLength(8); + expect(bodyRow3Columns[2]).toHaveTextContent("A0003"); + expect(bodyRow3Columns[3]).toHaveTextContent("다이슨dyson v15 detect complete"); + expect(bodyRow3Columns[4]).toHaveTextContent("1,290,000원"); + expect(bodyRow3Columns[5]).toHaveTextContent("SOLDOUT"); + expect(bodyRow3Columns[6]).toHaveTextContent("2023/02/0212:00"); + expect(bodyRow3Columns[7]).toHaveTextContent("2023/02/0212:00"); + + const bodyRow4Columns = tableBodyRows[4].querySelectorAll("td"); + expect(bodyRow4Columns).toHaveLength(8); + expect(bodyRow4Columns[2]).toHaveTextContent("A0004"); + expect(bodyRow4Columns[3]).toHaveTextContent("Aēsop레저렉션 아로마틱 핸드 워시"); + expect(bodyRow4Columns[4]).toHaveTextContent("47,000원"); + expect(bodyRow4Columns[5]).toHaveTextContent("NOTSALE"); + expect(bodyRow4Columns[6]).toHaveTextContent("2023/02/0201:00"); + expect(bodyRow4Columns[7]).toHaveTextContent("2023/02/0201:00"); + + const bodyRow5Columns = tableBodyRows[5].querySelectorAll("td"); + expect(bodyRow5Columns).toHaveLength(8); + expect(bodyRow5Columns[2]).toHaveTextContent("A0005"); + expect(bodyRow5Columns[3]).toHaveTextContent("LUSH더티 보디 스프레이"); + expect(bodyRow5Columns[4]).toHaveTextContent("60,000원"); + expect(bodyRow5Columns[5]).toHaveTextContent("SALE"); + expect(bodyRow5Columns[6]).toHaveTextContent("2023/02/0202:00"); + expect(bodyRow5Columns[7]).toHaveTextContent("2023/02/0202:00"); + }); + + it("URL 파라메터 page=2 이면 2페이지에 해당하는 아이템을 최대 5개를 테이블에 보여준다", async () => { + // given + when(useProducts as jest.Mock).calledWith({ + page: 2 + }).mockReturnValue({ + data: { + data: { + page: { + currentPage: 1, + pageSize: 5, + totalPage: 1, + totalCount: 6 + }, + items: [ + { + id: 6, + code: "A0006", + brand: "블루보틀", + name: "쓰리 아프리카스", + price: 25000, + status: "SALE", + createdAt: dayjs("2023-02-02T15:00:00+09:00"), + updatedAt: dayjs("2023-02-02T15:00:00+09:00"), + }, + ] + } + } + }); + + // when + await mockRouter.push("/sample/product/list?page=2"); + render() + await waitFor(() => screen.getByTestId("product-table")); + + // then + const productTable = screen.getByTestId("product-table"); + expect(productTable).toBeInTheDocument(); + + // 테이블 바디 확인 + const tableBodyRows = productTable.querySelectorAll("tbody > tr"); + expect(tableBodyRows).toHaveLength(2); // ant table은 컨텐츠이 담기지 않는 TR이 하나 존재하기 때문에 컨텐츠 + 1를 확인한다. + + const bodyRow1Columns = tableBodyRows[1].querySelectorAll("td"); + expect(bodyRow1Columns).toHaveLength(8); + expect(bodyRow1Columns[2]).toHaveTextContent("A0006"); + expect(bodyRow1Columns[3]).toHaveTextContent("블루보틀쓰리 아프리카스"); + expect(bodyRow1Columns[4]).toHaveTextContent("25,000원"); + expect(bodyRow1Columns[5]).toHaveTextContent("SALE"); + expect(bodyRow1Columns[6]).toHaveTextContent("2023/02/0203:00"); + expect(bodyRow1Columns[7]).toHaveTextContent("2023/02/0203:00"); + }); +}); + +describe("버튼 이벤트", () => { + it("상품 등록 버튼을 클릭하면 상품 등록 페이지로 이동한다", async () => { + // given + when(useProducts as jest.Mock).calledWith({ + page: 1 + }).mockReturnValue({}); + + await mockRouter.push("/sample/product/list"); + render() + await waitFor(() => screen.getByTestId("create-product-btn")); + + // when + const button = screen.getByTestId("create-product-btn"); + fireEvent.click(button); + + // then + expect(mockRouter).toMatchObject({ + pathname: "/sample/product/new", + query: {}, + }); + }); +}); \ No newline at end of file diff --git a/jest.config.mjs b/jest.config.mjs new file mode 100644 index 0000000..3eed123 --- /dev/null +++ b/jest.config.mjs @@ -0,0 +1,27 @@ +import nextJest from 'next/jest.js' + +const createJestConfig = nextJest({ + // Provide the path to your Next.js app to load next.config.js and .env files in your test environment + dir: './', +}) + +// Add any custom config to be passed to Jest +/** @type {import('jest').Config} */ +const config = { + rootDir: ".", + setupFilesAfterEnv: ["/jest.setup.js"], + moduleNameMapper: { + "^@/lib/(.*)$": "/src/lib/$1", + "^@/pages/(.*)$": "/src/pages/$1", + "^@/components/(.*)$": "/src/components/$1", + "^@/client/(.*)$": "/src/client/$1", + }, + moduleDirectories: ["node_modules", "/"], + testEnvironment: 'jest-environment-jsdom', +} + +export default async () => ({ + ...(await createJestConfig(config)()), + // ESM 모듈을 Jest에서 사용하기 위한 설정 추가 + transformIgnorePatterns: ["node_modules/(?!(ky-universal|ky)/)"], +}); \ No newline at end of file diff --git a/jest.setup.js b/jest.setup.js new file mode 100644 index 0000000..c2d08f4 --- /dev/null +++ b/jest.setup.js @@ -0,0 +1,9 @@ +Object.defineProperty(window, 'matchMedia', { + value: () => ({ + matches: false, + addListener: () => {}, + removeListener: () => {}, + }), +}); + +jest.mock('next/router', () => require('next-router-mock')); \ No newline at end of file diff --git a/package.json b/package.json index b8dcb37..f591bb7 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "build": "next build", "export": "next export", "start": "next start", + "test": "jest --watch", "lint": "next lint" }, "dependencies": { @@ -29,6 +30,10 @@ "swr": "^2.2.0" }, "devDependencies": { + "@testing-library/jest-dom": "^6.1.3", + "@testing-library/react": "^14.0.0", + "@types/jest": "^29.5.5", + "@types/jest-when": "^3.5.3", "@types/node": "18.11.18", "@types/numeral": "^2.0.2", "@types/qs": "^6.9.7", @@ -38,6 +43,10 @@ "eslint": "8.46.0", "eslint-config-next": "13.4.12", "eslint-config-prettier": "^8.9.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "jest-when": "^3.6.0", + "next-router-mock": "^0.9.10", "postcss": "^8.4.27", "prettier": "^3.0.0", "tailwindcss": "^3.3.3", diff --git a/src/components/page/sample/product/product-list.tsx b/src/components/page/sample/product/product-list.tsx index 8920caa..c562ff4 100644 --- a/src/components/page/sample/product/product-list.tsx +++ b/src/components/page/sample/product/product-list.tsx @@ -151,13 +151,14 @@ const ProductList = () => { - + data-testid="product-table" rowSelection={rowSelection} columns={columns} dataSource={data?.data.items || []} diff --git a/src/components/page/sample/product/product-search.tsx b/src/components/page/sample/product/product-search.tsx index 3db1ba7..d6961d4 100644 --- a/src/components/page/sample/product/product-search.tsx +++ b/src/components/page/sample/product/product-search.tsx @@ -35,7 +35,7 @@ const ProductSearch = () => { - 등록일자 수정일자 @@ -52,7 +52,7 @@ const ProductSearch = () => {
- 상품명 브랜드명