- Published on
Form 복습과 팝오버 테스트
온디맨드 선데이 아이스크림: Form복습과 팝오버
프로젝트 초기화
npx create-react-app [project-name]
ESLint와 Prettier 설정
eslint plugin
package 설치npm install eslint-plugin-testing-library eslint-plugin-jest-dom
- testing-library
- jest-dom
package.json
내eslintConfig
제거.eslintrc.json
생성 및 규칙을 포함하는 구성 추가{ "plugins": ["jest-dom", "testing-library"], "extends": [ "react-app", "react-app/jest", "plugin:testing-library/recommended", "plugin:testing-library/react", "plugin:jest-dom/recommened" ] }
.gitignore
파일에.vscode
,.eslintcache
추가.vscode .eslintcache
VSCode
프로젝트를 위한 설정 및 구성 추가settings.json
{ "eslint.options": { "configFile": ".eslintrc.json" }, "eslint.validate": ["javascript", "javascriptreact"], "editor.codeActionsOnSave": { "source.fixAll.eslint": true }, "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true }
- ESLint 설정 파일 지정 및 저장 시 ESLint 교정 행위, 포맷팅 설정
코드 구성과 SummaryForm 소개
요구사항
- 체크박스가 버튼을 활성화하는지 테스트
- 이용 약관
팝오버
테스트- 페이지에서 사라진 요소를 위한 테스트
체계화
- 각 페이지 디렉토리의 테스트 서브디렉토리 구성
- Jest는 전체 디렉토리 트리 내의
.test.js
로 끝나는 모든 파일을 찾아 실행하므로 폴더 구성은 따로 필요없지만, 따로 테스트 디렉토리를 구성 - 특정 컴포넌트에 대해서는
pages
디렉토리를 만들고 하위 폴더 및 파일 구성src/pages/summary
OrderSummary.jsx
SummaryForm.jsx
src/pages/summary/test
SummaryForm.test.jsx
OrderSummary.test.jsx
체크박스 활성화 버튼 테스트
요구사항
- 기본값으로 체크박스에 체크가 되어 있지 않아야 함
- 체크박스에 체크를 하면 버튼이 활성화
- 체크하지 않으면 비활성화
- 앱을 렌더링하지 않고 특정 컴포넌트(
<SummaryForm /
>만 렌더링 - 목업 모형에 있는
{name}
옵션을 통해 체크박스와 버튼 요소를 찾고, 레드-그린 테스트 진행
SummaryForm.jsx
export default function SummaryForm() {
return <div />
}
- 레드-그린 테스트의 레드 부분을 진행할 때 테스트가 중단되는 것을 방지하기 위한 기본 구성을 정의
SummaryForm.test.jsx
import { render, screen, fireEvent } from '@testing-library/react'
import SummaryForm from '../SummaryForm'
test('초기화 조건', () => {
render(<SummaryForm />)
const checkbox = screen.getByRole('checkbox', {
name: /terms and conditions/i,
})
expect(checkbox).not.toBeChecked()
const confirmButton = screen.getByRole('button', { name: /confirm order/i })
expect(confirmButton).toBeDisabled()
})
test('체크박스는 첫 번째 클릭 시 활성화, 두 번째 클릭 시 비활성화되어야 함', () => {
render(<SummaryForm />)
const checkbox = screen.getByRole('checkbox', {
name: /terms and conditions/i,
})
expect(checkbox).not.toBeChecked()
const confirmButton = screen.getByRole('button', { name: /confirm order/i })
fireEvent.click(checkbox)
expect(confirmButton).toBeEnabled()
fireEvent.click(checkbox)
expect(confirmButton).toBeDisabled()
})
render
- 컴포넌트 렌더링을 위한 컴포넌트를 설정
screen
- 컴포넌트의 요소를 찾기 위한 선언
- 문자열 혹은 정규표현식을 통해 요소를 찾아야 함
/terms and conditions/i
,/confirm order/i
- 대문자를 무시하고
name
option이terms and conditions
을 가지는 체크박스 요소,confirm order
를 포함하는 정규 표현식
- 대문자를 무시하고
fireEvent
- 찾은 요소와의 DOM 상호작용을 위해 선언
SummaryForm 체크박스와 버튼 요소 구성
SummaryForm.jsx
import React, { useState } from 'react'
import Form from 'react-bootstrap/Form'
import Button from 'react-bootstrap/Button'
export default function SummaryForm() {
const [tcChecked, setTcChecked] = useState(false)
const checkboxLabel = (
<span>
I agree to <span style={{ color: 'blue' }}> Terms and Conditions</span>
</span>
)
return (
<Form>
<Form.Group controlId="terms-and-conditions">
<Form.Check
type="checkbox"
checked={tcChecked}
onChange={(e) => setTcChecked(e.target.checked)}
label={checkboxLabel}
/>
</Form.Group>
<Button variant="primary" type="submit" disabled={!tcChecked}>
Confirm order
</Button>
</Form>
)
}
- 체크박스의 변경 유무를 form에서 감지하여
useState
훅으로 체크박스의 상태를 관리- 더불어, 체크박스의 상태를 추적하여 버튼의 활성화를 제어할 수 있음
- 체크박스에 체크가 있으면 활성화, 체크가 해제되면 비활성화
React BootStrap - Popover & Testing Library - userEvent
Popover
- 이용 약관을 부트스트랩 템플릿의 팝오버 컴포넌트를 이용하여, 마우스를 특정 텍스트 위로 올려두었을 때 팝오버가 노출되게 구현
userEvent
이전 예제에서 쓰였던
fireEvent
와 같은 흐름의userEvent
가 있는데 fireEvent에 비해 사용자 이벤트를 더욱 완전하고 현실적인 방식으로 시뮬레이션하는 장점이 있음testing-library/user-event
와testing-library/dom
설치npm install @testing-library/user-event @testing-library/dom
SummaryForm.test.js
import userEvent from '@testing-library/user-event' //... test('체크박스는 첫 번째 클릭 시 활성화, 두 번째 클릭 시 비활성화되어야 함', () => { render(<SummaryForm />) const checkbox = screen.getByRole('checkbox', { name: /terms and conditions/i, }) expect(checkbox).not.toBeChecked() const confirmButton = screen.getByRole('button', { name: /confirm order/i }) userEvent.click(checkbox) expect(confirmButton).toBeEnabled() userEvent.click(checkbox) expect(confirmButton).toBeDisabled() })
- 기존의
fireEvent
를userEvent
로 변경
- 기존의
Screen 쿼리 메서드
- 아래의
Matcher
들을 조합해서DOM
에서 찾고자 하는 내용을 가장 적절한 방식으로 사용 command[All]ByQueryType
- command
get
: 요소가 DOM 내에 존재하는지 기대함query
: 요소가 DOM 내에 존재하지 않는지 기대함find
: 요소가 비동기적으로 나타날 경우를 기대함- DOM에 비동기적인 업데이트가 있고, 단언문 실행 전 기다리고자 할 때 사용
[All]
- 포함을 시키거나 포함을 시키지 않는 부분
- 하나 이상의 매칭 포인트를 [All]을 포함시켜 처리할 수 있음
QueryType
- 무엇으로 검색하는지를 의미함
Role
: 코드의 접근성을 보장하기 위해 가장 많이 사용함AltText
: 이미지를 찾기위해서 사용Text
: 요소를 화면에 출력하기 위해서 사용Form Elements
: 폼 양식의 요소를 찾을 때 사용- PlaceholderText
- LabelText
- DisplayValue
- command
- 참고 링크
Popover 테스트
SummaryForm.text.jsx
test('팝오버가 hover 이벤트에 반응해야 함', () => {
render(<SummaryForm />)
// 페이지가 로딩되면 팝오버는 표시되지 않음
const nullPopover = screen.queryByText(/no ice cream will actually be delivered/i)
expect(nullPopover).not.toBeInTheDocument()
// 체크박스 라벨로 마우스를 올리면 팝오버는 표시됨
const termsAndConditions = screen.getByText(/terms and condition/i)
userEvent.hover(termsAndConditions)
const popover = screen.getByText(/no ice cream will actually be delivered/i)
expect(popover).toBeInTheDocument()
// 체크박스 라벨에서 마우스가 벗어나면 팝오버는 사라짐
userEvent.unhover(termsAndConditions)
const nullPopoverAgain = screen.queryByText(/no ice cream will actually be delivered/i)
expect(nullPopoverAgain).not.toBeInTheDocument()
})
페이지가 로딩되면 팝오버는 표시되지 않음
screen.queryByText
- 페이지가 로딩된 후 팝오버는 표시되지 않아야하므로,
queryByText
로 작성queryBy
를 사용할 시, 매칭되는 게 없으면null
을 반환
- 페이지가 로딩된 후 팝오버는 표시되지 않아야하므로,
expect(nullPopover).not.toBeInTheDocument()
- 팝오버 요소가
DOM
존재하지 않는지를 테스트
- 팝오버 요소가
체크박스 라벨로 마우스를 올리면 팝오버는 표시됨
userEvent.hover(termsAndConditions)
- 마우스오버를 시뮬레이션하기 위해
termsAndConditions
요소를userEvent
를 사용해 hover 상호 작용
- 마우스오버를 시뮬레이션하기 위해
expect(popover).toBeInTheDocument()
hover
했을 때, 해당 요소가DOM
에 존재하는지 테스트
체크박스 라벨에서 마우스가 벗어나면 팝오버는 사라짐
userEvent.unhover(termsAndConditions)
- 마우스오버가 비활성화 상태 즉, 마우스가 해당 요소르 벗어났을 때의 상호 작용을 정의
expect(nullPopoverAgain).not.toBeInTheDocument()
- 팝오버 요소가
DOM
존재하지 않는지를 테스트
- 팝오버 요소가
팝 오버 요소 구성
SummaryForm.jsx
import React, { useState } from 'react'
import Form from 'react-bootstrap/Form'
import Button from 'react-bootstrap/Button'
import Popover from 'react-bootstrap/Popover'
import OverlayTrigger from 'react-bootstrap/OverlayTrigger'
export default function SummaryForm() {
const [tcChecked, setTcChecked] = useState(false)
const popover = (
<Popover id="termsandconditions-popover">No ice cream will actually be delivered</Popover>
)
const checkboxLabel = (
<span>
I agree to
<OverlayTrigger placement="right" overlay={popover}>
<span style={{ color: 'blue' }}> Terms and Conditions</span>
</OverlayTrigger>
</span>
)
return (
<Form>
<Form.Group controlId="terms-and-conditions">
<Form.Check
type="checkbox"
checked={tcChecked}
onChange={(e) => setTcChecked(e.target.checked)}
label={checkboxLabel}
/>
</Form.Group>
<Button variant="primary" type="submit" disabled={!tcChecked}>
Confirm order
</Button>
</Form>
)
}
Trouble Shooting
위의 리액트 코드를 작성한 후, 아래와 같은 오류가 발생함
Warning: An update to Overlay inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
- 비동기 업데이트가 일어날 때 흔히 발생하는 문제점으로, 테스트 내
Overlay
로의 업데이트를 act(…)로 감싸지 않았다는 내용 - 또, 리액트 상태 업데이트 시키는 코드를
act(…)
로 래핑할 것을 제안(하지 않는 것을 권장함) - 참고 링크
- 비동기 업데이트가 일어날 때 흔히 발생하는 문제점으로, 테스트 내
해결점
- 테스트가 끝난 후 무엇이 변경되었는지 파악해야 함
- 위의 에러는 테스트가 끝난 후 DOM이 업데이트 되었으므로, 어떤 것이 변경되었는지 확인
test('팝오버가 hover 이벤트에 반응해야 함', () => {
// ...
// 체크박스 라벨에서 마우스가 벗어나면 팝오버는 사라짐
userEvent.unhover(termsAndConditions)
const nullPopoverAgain = screen.queryByText(/no ice cream will actually be delivered/i)
expect(nullPopoverAgain).not.toBeInTheDocument()
})
expect(nullPopoverAgain).not.toBeInTheDocument();
테스트가 완료된 후 팝오버 요소가 비동기적으로 사라지는 문제를 야기
팝업이 사라진 후까지 기다렸다가 테스트를 마저 진행하게 수정
test('팝오버가 hover 이벤트에 반응해야 함', async () => { // ... // 체크박스 라벨에서 마우스가 벗어나면 팝오버는 사라짐 userEvent.unhover(termsAndConditions) await waitForElementToBeRemoved(() => screen.queryByText(/no ice cream will actually be delivered/i) ) })
waitForElementToBeRemoved
를 사용하는 함수 전체를async
키워드로 감싸고,await
키워드로 요소를 찾는 쿼리앞에 설정
Referenced