지금까지 OrderPage에서 했었던 것은 products의 수량 조절, options의 체크 조절, 각자 금액과 total 금액까지 구하는 기능들을 구현했었다. 그렇다면 이제 구매하려는 상품들을 주문하는 기능을 추가해보자.
이 페이지에서 구현할 UI는 다음과 같다.
우선, SummaryPage에 대한 테스트케이스 작성과 실제 코드 작성을 해보자.
💡 잠깐) 앞서 보았던 컴포넌트들의 테스트케이스에서 context를 받기위한 wrapper를 감싸주었었다. 따라서, SummaryPage.test.js 또한 cumtom render를 사용하자.
import { render, screen } from "@testing-library/react"; 에서 import { render, screen } from "../../../test-utils"; 로 변경
Why? -> React TDD - Travel Product App(1)의 context wrapper 부분 참고
<SummaryPage.test.js>
💡 잠깐) screen
'DOM 테스팅 라이브러리에서 내보낸 모든 쿼리는 컨테이너를 첫 번째 인수로 받아들입니다. 전체 document.body를 쿼리하는 것은 매우 일반적이기 때문에 DOM 테스팅 라이브러리는 또한 document.body에 미리 바인딩된 모든 쿼리가 있는 screen 객체를 내보냅니다(inside 기능 사용). React Testing Library와 같은 래퍼는 화면을 다시 내보내므로 동일한 방식으로 사용할 수 있습니다.' (공식문서)
<SummaryPage.js>
참조: Label태크 & HTMLFor
지금까지 한 테스트케이스는 각자 컴포넌트에 대한 테스트들을 진행하였다.
이제 주문부터 주문확인 그리고 주문완료까지의 모든 과정을 테스트케이스로 작성해보자.
모든 과정이 담긴 최상위 컴포넌트인 App을 테스트
(App.js의 경우 OrderContextProvider로 감싸져 있는 컴포넌트이기 때문에 custom render가 아닌 일반 render를 사용한다.)
orderPage에 대한 테스트 케이스였으므로 orderPage에 실제 코드를 작성.
또한 주문페이지, 주문확인페이지, 주문완료페이지를 서로 연결시켜보자.
주문확인 UI는 총 세 부분으로 나눌 수 있다.
1. 주문확인
<h1>주문 확인</h1>
2. Products: ₩4000
const [orderDatas] = useContext(OrderContext);
...
<h2>Products: ₩ {orderDatas.totals.products}</h2>
3. 상품 목록 (상품 수량, 상품 이름)
const productArray = Array.from(orderDatas.products);
const productList = productArray.map(([key, value]) => (
<li key={key}> {value} {key} </li>
));
...
<ul>{productList}</ul>
💡 잠깐) Array.from
Array.from() 메서드는 유사 배열 객체(array-like object)나 반복 가능한 객체(iterable object)를 얕게 복사해 새로운Array 객체를 만든다.주문확인 페이지에서 console.log를 이용하여 orderDatas와 productArray를 출력해보
았다.<console.log(orderDatas)>
- orderDatas는 Map()으로 생성된 반복가능객체로 다음과 같이 출력.
<console.log(productArray)>- productArray는 Array.from으로 생성된 새로운 배열 객체로, 다음과 같이 출력.
참고
1. Option: ₩ 500 + option 목록
option의 경우 말그대로 option이기 때문에 option의 가격과 목록을 products와 같이 기본적으로 출력하는 것이 아니라, option이 존재할 경우에만 출력되게 조건을 준다.
const hasOptions = orderDatas.options.size > 0;
let optionsRender = null;
if (hasOptions) {
const optionsArray = Array.from(orderDatas.options.keys());
const optionList = optionsArray.map((key) => <li key={key}>{key}</li>);
optionsRender = (
<>
<h2>옵션: {orderDatas.totals.options}</h2>
<ul>{optionList}</ul>
</>
);
}
...
{optionsRender}
1. 주문하려는 것을 확인하셨나요? + 주문확인
2. 주문확인 클릭 시 주문완료페이지로 이동
const [checked, setChecked] = useState(false);
const handleSubmit = (event) => {
event.preventDefault();
setStep(2);
};
...
<form onSubmit={handleSubmit}>
<input
type="checkbox"
checked={checked}
onChange={(e) => setChecked(e.target.checked)}
id="confirm-checkbox"
/>
<label htmlFor="confirm-checkbox">주문하려는 것을 확인하셨나요?</label>
<br />
<button disabled={!checked} type="submit"> 주문 확인 </button>
</form>
<전체코드>
주문완료페이지가 렌더링되는 과정에서 정규식의 case insensitive를 적용된 loading이라는 텍스트를 확인.(loading/i)
'주문이 성공했습니다'라는 headingText를 확인.
데이터가 받아와지고 주문확인페이지가 떴을 때에는 loading문구가 사라져야 한다. 따라서, not.toBeIntheDocument를 이용하여 화면상에 보이지 않는지를 확인.
'첫페이지로'라는 버튼 확인.
1. orderCompleted 실행.
💡 잠깐) 백엔드로부터 받은 데이터는 무엇?
- 서버의 구조는 아래의 Server를 확인하자.
useEffect(() => {
orderCompleted(OrderDatas);
}, []);
const orderCompleted = async (OrderDatas) => {
try {
let response = await axios.post(
"http://localhost:8000/order",
OrderDatas
);
setOrderHistory(response.data);
setLoading(false);
} catch (error) {
setError(true);
}
};
if (error) {
return <ErrorBanner message="에러가 발생했습니다." />;
}
2. orderTable 생성
const orderTable = orderHistory.map((item) => (
<tr key={item.orderNumber}>
<td>{item.orderNumber}</td>
<td>{item.price}</td>
</tr>
));
...
<tbody>
<tr>
<th>주문 번호</th>
<th>주문 가격</th>
</tr>
{orderTable}
</tbody>
3. loading
const orderCompleted = async (OrderDatas) => {
try {
...
setLoading(false);
}
};
if (loading) {
return <div>loading</div>;
} else {
return (. ..);
}
4. resetOrderDatas & 이동
const handleClick = () => {
resetOrderDatas();
setStep(0);
};
...
<button onClick={handleClick}>첫페이지로</button>
💡 resetOrderDatas
해당 함수는 orderContext.js에 존재한다.
앞서 보았던 resetOrderDatas의 역할은 orderDatas를 초기화 시키는 것이다.
value에 해당 함수가 존재하고 return해주어 위에서와 같이 resetOrderDatas를 호출할 수 있는 것이다.const value = useMemo(() => { ... const resetOrderDatas = () => { setOrderCounts({ products: new Map(), options: new Map(), }); }; return [{ ...orderCounts, totals }, updateItemCount, resetOrderDatas]; }, [orderCounts, totals]);
이제 모든 테스트케이스 작성과 실제 코드 작성이 끝났다. 하지만, 다음과 같은 코드를 발견할 수 있다.
리액트에서 나오는 act 경고는 컴포넌트에 아무것도 일어나지 않을 것으로 예상하고 있을 때 컴포넌트에 어떤 일이 일어나면 나오는 경고이다.
원래 컴포넌트에서 무언가가 일어난다고 해주려면 act라는 함수로 감싸 주어야 한다.
act(() => {
/* fire events that update state */
});
지금까지는 act로 감싸준 적이 없다.
react-testing-library 내부에 API에 act를 이미 내포하고 있어서 우리가 일 부로 act로 감싸서 호출하지 않고 렌더링과 업데이트를 할 수 있다. (리액트 콜 스택 안에 있을 때)
현재 react-testing-library 사용중이니 이미 act로 감싸준 것이랑 마찬가지인데 지금은 왜 에러가 날까?
💡중요
컴포넌트가 비동기 API 호출을 할 때나 렌더링이나 어떠한 것이 업데이트 되기 전에 테스트가 종료 될 때는 따로 act로 감싸주어야하기 때문이다.즉, 첫페이지로라는 버튼을 누르면 주문페이지컴포넌트가 비동기호출로 업데이트되어지고 있는 중이다.(비동기 호출로 서버로부터 이미지,텍스트 업데이트).
하지만, 테스트케이스는 버튼을 클릭하자마자 종료되어버리는 것이다.
지금 현재 나의 테스트케이스 작성 상황은 다음과 같다.
ex) 첫페이지로 이동 후 테스트 종료
일어날 일들을 waitFor API를 이용해서 테스트가 끝나기전에 컴포넌트가 다 업데이트 되기를 기다리는 것이다..
💡 잠깐) waitFor?
이전에 학습할 때 findBy는 getBy + waitFor라는 것을 기억할 수 있다.waitFor를 사용하여 기대가 통과할 때까지 기다릴 수 있다.
따라서 waitFor()라는 함수 대신findByXxx() 함수를 사용할 수 있다.
위에서는 getByRole + awaitFor, 아래에서는 findByRole 사용
await screen.findByRole("spinbutton", { name: "America" });
간단한 프로젝트를 마치고 이제 발생하는 문제들에 대해 하나씩 해결해보려고 한다.