Skip to content

Commit 57b2b51

Browse files
committed
purpleio#6 샘플 상품 목록 페이지 Jest 단위 테스트 코드를 작성한다
1 parent a5dc8d5 commit 57b2b51

File tree

6 files changed

+276
-3
lines changed

6 files changed

+276
-3
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
import {fireEvent, render, screen, waitFor} from "@testing-library/react"
2+
import "@testing-library/jest-dom"
3+
import ProductListPage from "@/pages/sample/product/list";
4+
import mockRouter from "next-router-mock";
5+
import {when} from "jest-when";
6+
import {useProducts} from "@/client/sample/product";
7+
import dayjs from "dayjs";
8+
9+
jest.mock("@/client/sample/product");
10+
11+
describe("테이블 랜더링", () => {
12+
it("URL 파라메터가 없으면 1페이지에 해당하는 아이템을 5개씩 테이블에 보여준다", async () => {
13+
// given
14+
when(useProducts as jest.Mock).calledWith({
15+
page: 1
16+
}).mockReturnValue({
17+
data: {
18+
data: {
19+
page: {
20+
currentPage: 1,
21+
pageSize: 5,
22+
totalPage: 1,
23+
totalCount: 6
24+
},
25+
items: [
26+
{
27+
id: 1,
28+
code: "A0001",
29+
brand: "apple",
30+
name: "iPhone 14 Pro",
31+
price: 1550000,
32+
status: "SALE",
33+
createdAt: "2023-02-02T10:00:00+09:00",
34+
updatedAt: "2023-02-02T10:00:00+09:00",
35+
},
36+
{
37+
id: 2,
38+
code: "A0002",
39+
brand: "파타고니아",
40+
name: "클래식 레트로-X 후리스 플리스 자켓",
41+
price: 230000,
42+
status: "SALE",
43+
createdAt: "2023-02-02T11:00:00+09:00",
44+
updatedAt: "2023-02-02T11:00:00+09:00",
45+
},
46+
{
47+
id: 3,
48+
code: "A0003",
49+
brand: "다이슨",
50+
name: "dyson v15 detect complete",
51+
price: 1290000,
52+
status: "SOLDOUT",
53+
createdAt: "2023-02-02T12:00:00+09:00",
54+
updatedAt: "2023-02-02T12:00:00+09:00",
55+
},
56+
{
57+
id: 4,
58+
code: "A0004",
59+
brand: "Aēsop",
60+
name: "레저렉션 아로마틱 핸드 워시",
61+
price: 47000,
62+
status: "NOTSALE",
63+
createdAt: "2023-02-02T13:00:00+09:00",
64+
updatedAt: "2023-02-02T13:00:00+09:00",
65+
},
66+
{
67+
id: 5,
68+
code: "A0005",
69+
brand: "LUSH",
70+
name: "더티 보디 스프레이",
71+
price: 60000,
72+
status: "SALE",
73+
createdAt: "2023-02-02T14:00:00+09:00",
74+
updatedAt: "2023-02-02T14:00:00+09:00",
75+
},
76+
]
77+
}
78+
}
79+
});
80+
81+
// when
82+
await mockRouter.push("/sample/product/list");
83+
render(<ProductListPage/>)
84+
await waitFor(() => screen.getByTestId("product-table"));
85+
86+
// then
87+
const productTable = screen.getByTestId("product-table");
88+
expect(productTable).toBeInTheDocument();
89+
90+
// 테이블 헤더 확인
91+
const tableHeader = productTable.querySelector("thead > tr");
92+
expect(tableHeader).not.toBeNull();
93+
const headerColumns = (tableHeader as Element).querySelectorAll("th");
94+
expect(headerColumns).toHaveLength(7);
95+
expect(headerColumns[0]).toHaveTextContent("");
96+
expect(headerColumns[1]).toHaveTextContent("상품코드");
97+
expect(headerColumns[2]).toHaveTextContent("상품명");
98+
expect(headerColumns[3]).toHaveTextContent("금액");
99+
expect(headerColumns[4]).toHaveTextContent("판매상태");
100+
expect(headerColumns[5]).toHaveTextContent("생성일시");
101+
expect(headerColumns[6]).toHaveTextContent("수정일시");
102+
103+
// 테이블 바디 확인
104+
const tableBodyRows = productTable.querySelectorAll("tbody > tr");
105+
expect(tableBodyRows).toHaveLength(6); // ant table은 컨텐츠이 담기지 않는 TR이 하나 존재하기 때문에 컨텐츠 + 1를 확인한다.
106+
107+
const bodyRow1Columns = tableBodyRows[1].querySelectorAll("td");
108+
expect(bodyRow1Columns).toHaveLength(8);
109+
expect(bodyRow1Columns[2]).toHaveTextContent("A0001");
110+
expect(bodyRow1Columns[3]).toHaveTextContent("appleiPhone 14 Pro");
111+
expect(bodyRow1Columns[4]).toHaveTextContent("1,550,000원");
112+
expect(bodyRow1Columns[5]).toHaveTextContent("SALE");
113+
expect(bodyRow1Columns[6]).toHaveTextContent("2023/02/0210:00");
114+
expect(bodyRow1Columns[7]).toHaveTextContent("2023/02/0210:00");
115+
116+
const bodyRow2Columns = tableBodyRows[2].querySelectorAll("td");
117+
expect(bodyRow2Columns).toHaveLength(8);
118+
expect(bodyRow2Columns[2]).toHaveTextContent("A0002");
119+
expect(bodyRow2Columns[3]).toHaveTextContent("파타고니아클래식 레트로-X 후리스 플리스 자켓");
120+
expect(bodyRow2Columns[4]).toHaveTextContent("230,000원");
121+
expect(bodyRow2Columns[5]).toHaveTextContent("SALE");
122+
expect(bodyRow2Columns[6]).toHaveTextContent("2023/02/0211:00");
123+
expect(bodyRow2Columns[7]).toHaveTextContent("2023/02/0211:00");
124+
125+
const bodyRow3Columns = tableBodyRows[3].querySelectorAll("td");
126+
expect(bodyRow3Columns).toHaveLength(8);
127+
expect(bodyRow3Columns[2]).toHaveTextContent("A0003");
128+
expect(bodyRow3Columns[3]).toHaveTextContent("다이슨dyson v15 detect complete");
129+
expect(bodyRow3Columns[4]).toHaveTextContent("1,290,000원");
130+
expect(bodyRow3Columns[5]).toHaveTextContent("SOLDOUT");
131+
expect(bodyRow3Columns[6]).toHaveTextContent("2023/02/0212:00");
132+
expect(bodyRow3Columns[7]).toHaveTextContent("2023/02/0212:00");
133+
134+
const bodyRow4Columns = tableBodyRows[4].querySelectorAll("td");
135+
expect(bodyRow4Columns).toHaveLength(8);
136+
expect(bodyRow4Columns[2]).toHaveTextContent("A0004");
137+
expect(bodyRow4Columns[3]).toHaveTextContent("Aēsop레저렉션 아로마틱 핸드 워시");
138+
expect(bodyRow4Columns[4]).toHaveTextContent("47,000원");
139+
expect(bodyRow4Columns[5]).toHaveTextContent("NOTSALE");
140+
expect(bodyRow4Columns[6]).toHaveTextContent("2023/02/0201:00");
141+
expect(bodyRow4Columns[7]).toHaveTextContent("2023/02/0201:00");
142+
143+
const bodyRow5Columns = tableBodyRows[5].querySelectorAll("td");
144+
expect(bodyRow5Columns).toHaveLength(8);
145+
expect(bodyRow5Columns[2]).toHaveTextContent("A0005");
146+
expect(bodyRow5Columns[3]).toHaveTextContent("LUSH더티 보디 스프레이");
147+
expect(bodyRow5Columns[4]).toHaveTextContent("60,000원");
148+
expect(bodyRow5Columns[5]).toHaveTextContent("SALE");
149+
expect(bodyRow5Columns[6]).toHaveTextContent("2023/02/0202:00");
150+
expect(bodyRow5Columns[7]).toHaveTextContent("2023/02/0202:00");
151+
});
152+
153+
it("URL 파라메터 page=2 이면 2페이지에 해당하는 아이템을 최대 5개를 테이블에 보여준다", async () => {
154+
// given
155+
when(useProducts as jest.Mock).calledWith({
156+
page: 2
157+
}).mockReturnValue({
158+
data: {
159+
data: {
160+
page: {
161+
currentPage: 1,
162+
pageSize: 5,
163+
totalPage: 1,
164+
totalCount: 6
165+
},
166+
items: [
167+
{
168+
id: 6,
169+
code: "A0006",
170+
brand: "블루보틀",
171+
name: "쓰리 아프리카스",
172+
price: 25000,
173+
status: "SALE",
174+
createdAt: dayjs("2023-02-02T15:00:00+09:00"),
175+
updatedAt: dayjs("2023-02-02T15:00:00+09:00"),
176+
},
177+
]
178+
}
179+
}
180+
});
181+
182+
// when
183+
await mockRouter.push("/sample/product/list?page=2");
184+
render(<ProductListPage/>)
185+
await waitFor(() => screen.getByTestId("product-table"));
186+
187+
// then
188+
const productTable = screen.getByTestId("product-table");
189+
expect(productTable).toBeInTheDocument();
190+
191+
// 테이블 바디 확인
192+
const tableBodyRows = productTable.querySelectorAll("tbody > tr");
193+
expect(tableBodyRows).toHaveLength(2); // ant table은 컨텐츠이 담기지 않는 TR이 하나 존재하기 때문에 컨텐츠 + 1를 확인한다.
194+
195+
const bodyRow1Columns = tableBodyRows[1].querySelectorAll("td");
196+
expect(bodyRow1Columns).toHaveLength(8);
197+
expect(bodyRow1Columns[2]).toHaveTextContent("A0006");
198+
expect(bodyRow1Columns[3]).toHaveTextContent("블루보틀쓰리 아프리카스");
199+
expect(bodyRow1Columns[4]).toHaveTextContent("25,000원");
200+
expect(bodyRow1Columns[5]).toHaveTextContent("SALE");
201+
expect(bodyRow1Columns[6]).toHaveTextContent("2023/02/0203:00");
202+
expect(bodyRow1Columns[7]).toHaveTextContent("2023/02/0203:00");
203+
});
204+
});
205+
206+
describe("버튼 이벤트", () => {
207+
it("상품 등록 버튼을 클릭하면 상품 등록 페이지로 이동한다", async () => {
208+
// given
209+
when(useProducts as jest.Mock).calledWith({
210+
page: 1
211+
}).mockReturnValue({});
212+
213+
await mockRouter.push("/sample/product/list");
214+
render(<ProductListPage/>)
215+
await waitFor(() => screen.getByTestId("create-product-btn"));
216+
217+
// when
218+
const button = screen.getByTestId("create-product-btn");
219+
fireEvent.click(button);
220+
221+
// then
222+
expect(mockRouter).toMatchObject({
223+
pathname: "/sample/product/new",
224+
query: {},
225+
});
226+
});
227+
});

jest.config.mjs

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import nextJest from 'next/jest.js'
2+
3+
const createJestConfig = nextJest({
4+
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
5+
dir: './',
6+
})
7+
8+
// Add any custom config to be passed to Jest
9+
/** @type {import('jest').Config} */
10+
const config = {
11+
rootDir: ".",
12+
setupFilesAfterEnv: ["<rootDir>/jest.setup.js"],
13+
moduleNameMapper: {
14+
"^@/lib/(.*)$": "<rootDir>/src/lib/$1",
15+
"^@/pages/(.*)$": "<rootDir>/src/pages/$1",
16+
"^@/components/(.*)$": "<rootDir>/src/components/$1",
17+
"^@/client/(.*)$": "<rootDir>/src/client/$1",
18+
},
19+
moduleDirectories: ["node_modules", "<rootDir>/"],
20+
testEnvironment: 'jest-environment-jsdom',
21+
}
22+
23+
export default async () => ({
24+
...(await createJestConfig(config)()),
25+
// ESM 모듈을 Jest에서 사용하기 위한 설정 추가
26+
transformIgnorePatterns: ["node_modules/(?!(ky-universal|ky)/)"],
27+
});

jest.setup.js

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Object.defineProperty(window, 'matchMedia', {
2+
value: () => ({
3+
matches: false,
4+
addListener: () => {},
5+
removeListener: () => {},
6+
}),
7+
});
8+
9+
jest.mock('next/router', () => require('next-router-mock'));

package.json

+9
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"build": "next build",
88
"export": "next export",
99
"start": "next start",
10+
"test": "jest --watch",
1011
"lint": "next lint"
1112
},
1213
"dependencies": {
@@ -29,6 +30,10 @@
2930
"swr": "^2.2.0"
3031
},
3132
"devDependencies": {
33+
"@testing-library/jest-dom": "^6.1.3",
34+
"@testing-library/react": "^14.0.0",
35+
"@types/jest": "^29.5.5",
36+
"@types/jest-when": "^3.5.3",
3237
"@types/node": "18.11.18",
3338
"@types/numeral": "^2.0.2",
3439
"@types/qs": "^6.9.7",
@@ -38,6 +43,10 @@
3843
"eslint": "8.46.0",
3944
"eslint-config-next": "13.4.12",
4045
"eslint-config-prettier": "^8.9.0",
46+
"jest": "^29.7.0",
47+
"jest-environment-jsdom": "^29.7.0",
48+
"jest-when": "^3.6.0",
49+
"next-router-mock": "^0.9.10",
4150
"postcss": "^8.4.27",
4251
"prettier": "^3.0.0",
4352
"tailwindcss": "^3.3.3",

src/components/page/sample/product/product-list.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -151,13 +151,14 @@ const ProductList = () => {
151151
<Button className="btn-with-icon" icon={<Download />}>
152152
엑셀 다운로드
153153
</Button>
154-
<Button type="primary" onClick={() => router.push("/sample/product/new")}>
154+
<Button data-testid="create-product-btn" type="primary" onClick={() => router.push("/sample/product/new")}>
155155
상품등록
156156
</Button>
157157
</div>
158158
</DefaultTableBtn>
159159

160160
<DefaultTable<IProduct>
161+
data-testid="product-table"
161162
rowSelection={rowSelection}
162163
columns={columns}
163164
dataSource={data?.data.items || []}

src/components/page/sample/product/product-search.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ const ProductSearch = () => {
3535
<FormSearch>
3636
<FieldInline>
3737
<Form.Item label="기간" name="searchDateType" initialValue="created">
38-
<Select dropdownMatchSelectWidth={false}>
38+
<Select popupMatchSelectWidth={false}>
3939
<Select.Option value="created">등록일자</Select.Option>
4040
<Select.Option value="updated">수정일자</Select.Option>
4141
</Select>
@@ -52,7 +52,7 @@ const ProductSearch = () => {
5252
<div>
5353
<FieldInline>
5454
<Form.Item label="검색조건" name="searchType" initialValue="productName">
55-
<Select dropdownMatchSelectWidth={false}>
55+
<Select popupMatchSelectWidth={false}>
5656
<Select.Option value="productName">상품명</Select.Option>
5757
<Select.Option value="brandName">브랜드명</Select.Option>
5858
</Select>

0 commit comments

Comments
 (0)