검색부터 복귀까지 자동 검증
210분 안팎
학습 목표
모의 네트워크로 사용자 흐름을 재현합니다.
개념
화면 연결을 독립된 응답으로 검사합니다
카드에 행사 이름이 보인다고 사용자가 검색을 끝낸 것은 아닙니다. 조건을 넣고 결과를 선택한 다음 목록으로 돌아왔을 때 검색어와 지역이 유지돼야 탐색을 이어갈 수 있습니다. 이 레슨은 화면 조작을 실제 브라우저에 맡기고 외부 응답만 고정합니다. 구현 함수 호출 횟수보다 URL·입력·결과·복귀라는 사용자 계약을 중심으로 자동 검사를 작성합니다.
제공된 e2e/fixtures.cjs는 mock-server.cjs로 HTML과 CSS와 ESM을 제공합니다. 서버는 워커 안에서 동적 포트를 열고 테스트 종료 시 close로 정리합니다. 기존 npm start와 충돌하지 않으며 별도 서버 프로세스를 수동 종료할 필요가 없습니다. 각 검사는 새로운 브라우저 context에서 시작하므로 이전 검색어·히스토리·응답 횟수를 다음 검사에 남기지 않습니다.
API 응답은 page.route로 가로챕니다. route를 page.goto 전에 설치해야 첫 검색 요청도 고정할 수 있습니다. 실제 앱의 fetch와 응답 처리와 DOM 갱신은 그대로 실행하고 /mock/events 경로만 fixture로 답합니다. 이 방식은 연결 동작의 검사이며 운영 API의 정확성이나 실제 서비스의 가용성을 증명하지 않습니다. 응답 계약 검사는 앞 모듈의 API 검사와 함께 유지합니다.
fixture는 부산의 마을 책 모임과 서울의 동네 책 나눔 두 건입니다. 검색어 책과 지역 부산을 제출하면 id=2 한 건입니다. 상세 요청은 배열 대신 객체를 반환하고, 없는 id는 404를 반환합니다. 반환 모양을 모두 배열로 통일하면 실제 API 계약을 무너뜨립니다. 실패 모의에서도 status와 JSON 형태를 같이 제어해 무엇이 잘못됐는지 앱이 구별하도록 합니다.
요소는 가능한 한 역할과 이름으로 찾습니다. getByRole의 searchbox와 검색어 이름은 type=search와 연결된 label을 확인하는 단서입니다. “검색” 버튼은 exact:true로 지정해 다른 이름에 부분 일치하지 않게 합니다. 카드 안의 몇 번째 span을 찾는 선택자는 구조 편집에 쉽게 깨집니다. 이름으로 찾기 실패는 레이블 연결이나 사용자 문구가 바뀌었다는 신호일 수 있습니다.
행동과 완료 사이에는 기다릴 기준이 있어야 합니다. 검색 버튼 click은 조작이 가능한 요소를 기다리지만 그 뒤의 API 결과까지 보장하지 않습니다. 결과 요약의 문구나 카드 개수에 await expect를 적용합니다. 페이지를 열자마자 Node의 일반 assert로 DOM 값을 비교하면 loading을 읽을 수 있습니다. 앱이 완료했다는 관찰이 확인된 뒤 동기 snapshot 단언을 호출합니다.
flow-assertions.mjs의 assertSearch는 완료된 검색 snapshot을 검사합니다. snapshot에는 URL 검색어·URL 지역·폼 입력·카드 id 순서·상세 숨김 여부가 있습니다. 카드만 맞고 URL이 옛 조건이면 새로고침이나 복귀에서 문제가 됩니다. starter는 카드만 검사하도록 되어 있으므로 누락된 단언을 추가합니다. 검사 함수 자체에 잘못된 snapshot을 넣는 반례도 있어 빠진 단언을 드러냅니다.
정상 흐름 검사는 처음 두 건을 기다린 후 조건을 제출하고 한 건을 기다립니다. 이어서 카드 링크를 눌러 상세 제목을 확인하고 목록 링크로 돌아옵니다. 폼과 URL과 id가 검색 직후와 같은지 다시 검사합니다. 브라우저 앞으로와 뒤로 이동도 이어서 실행합니다. 목록 버튼과 브라우저 이동은 이벤트 경로가 달라 둘을 같은 검사로 대신하지 않습니다.
빈 결과 검사는 존재하지 않는 검색어로 시작합니다. 이전 카드가 남지 않았는지, 빈 안내가 보이는지, 조건 초기화 버튼으로 목록을 회복하는지 봅니다. 빈 상태는 오류가 아니므로 재시도만 제시하면 사용자가 조건을 바꿀 수 없습니다. 테스트는 결과 0건이라는 숫자만 확인하지 않고 다음 행동이 가능한지도 확인합니다. 문구가 보이지만 버튼이 작동하지 않는 경우를 잡습니다.
500 재시도는 검사 내부의 횟수 변수로 첫 요청만 실패하게 합니다. 다음 요청은 동일한 URL에 정상 배열을 반환합니다. 서버 오류 안내와 다시 시도 버튼을 기다리고 클릭한 뒤 카드와 재시도 숨김을 확인합니다. 요청 URL 두 개를 비교하면 조건을 초기화하는 잘못된 복구도 발견합니다. 횟수 변수를 파일 전역에 두면 다른 테스트의 실행 순서에 결과가 의존합니다.
연결 실패는 route.abort로 모의하고 HTTP 500과 구별합니다. 응답을 아예 받지 못한 경우에는 status 코드가 없으며 연결 확인 안내가 나옵니다. 빈 결과 안내가 대신 나오지 않는지도 검사합니다. 실패 조건을 만든 목적은 콘솔 오류 자체를 없애는 것이 아니라 사용자가 무엇을 보고 어떤 행동을 선택할 수 있는지 확인하는 데 있습니다.
응답 역전 검사는 A 응답을 보류한 상태에서 B를 완료합니다. B 카드가 보인 뒤 A를 풀고 B가 유지되는지 확인합니다. 테스트에서만 fetch signal을 무시하는 래퍼를 넣어 취소와 별개인 최신성 방어를 확인합니다. 앱에 이 래퍼를 넣지 않습니다. 취소를 정상적으로 따르는 브라우저만 검사하면 generation 보호를 지운 결함이 가려질 수 있습니다.
보류 응답을 사용할 때 임의로 3초씩 기다리지 않습니다. A가 가로채졌는지 확인하는 플래그, B 제목 표시, A 응답 수신 플래그를 순서대로 기다립니다. 완료 뒤 한 작업 턴을 지나 결과를 확인하는 것은 정해진 경계이지 느린 환경을 보상하는 긴 sleep이 아닙니다. 더 세밀한 타이머 정책은 기존 가짜 clock 검사가 249/250ms 경계를 맡습니다.
오류 메시지의 종류를 먼저 나눕니다. strict mode violation은 여러 요소가 일치했다는 뜻이므로 exact나 영역 한정을 검토합니다. 기대 문구 timeout이면 Network에서 경로·status·응답 객체를 보고, 실행 진입점 boot.mjs가 로드됐는지 확인합니다. Executable doesn’t exist는 테스트 단언 실패가 아니라 브라우저 설치 문제입니다. 환경 오류를 starter의 의도된 실패로 세지 않습니다.
실패 trace에는 행동 순서와 그 시점의 화면과 네트워크를 함께 볼 수 있습니다. retain-on-failure 설정은 실패한 검사의 trace를 남기고 retries:0은 첫 실패를 숨기지 않습니다. README의 show-trace 명령에 실제 실패 폴더 경로를 넣습니다. timeout을 늘리기 전에 기다릴 대상이 잘못됐는지, 모의 응답을 늦게 설치했는지, 잘못된 응답 형태를 보냈는지 확인합니다.
실습 완료는 assertSearch 반례와 실제 사용자 시나리오가 함께 보호되는 상태입니다. npm test는 작성한 단언의 누락을 Node에서 먼저 찾고, 외부 환경에서 npm run test:e2e로 브라우저 연결을 검사합니다. 이 작성 환경의 브라우저 결과는 PENDING이며 steps에는 추정한 실행 출력을 적지 않습니다. MANUAL.md에 실행한 명령과 환경과 미확인 범위를 나눠 남깁니다.
자동화가 유지되려면 기대값을 수정 이유 없이 바꾸지 않아야 합니다. 사용자 계약이 바뀐 경우에는 왜 URL이나 복귀 정책이 바뀌는지 명세와 검사 이름을 함께 고칩니다. 구현이 실패한다는 이유만으로 한 건 기대를 두 건으로 바꾸면 잘못된 지역 필터를 정상으로 승인하게 됩니다. 다음 레슨의 키보드 검사는 같은 흐름에서 마우스가 가려 준 문제를 찾아냅니다.
도구 API의 구체적인 사용법은 Playwright 응답 모의 문서와 요소 선택 문서에서 확인합니다. 응답 가로채기와 화면 단언의 책임을 구분해 적용합니다.
따라하기
같은 카드라도 조건이 다르면 실패하게 합니다
snapshot 비교가 무엇을 보호하는지 작은 반례로 확인합니다.
const assert=require('node:assert/strict');const actual={q:'그림',ids:[2]};try{assert.equal(actual.q,'책','URL 검색어');}catch(e){console.log(e.name);console.log(`Actual=${e.actual}, Expected=${e.expected}`);}실행 결과
AssertionError Actual=그림, Expected=책
검색 단언을 완성합니다
다운로드한 starter 루트에서 실행합니다. flow-assertions.mjs의 TODO를 고치고 반례 검사도 유지합니다. Node 검사의 실제 종료 결과를 기록합니다.
node --test flow-test.mjs행동과 완료 조건을 연결합니다
제공된 flows.spec.cjs의 빈 결과 test에서 아래 코드와 동일한 순서를 찾습니다. mock은 요청을 고정하고 expect는 해당 화면 상태를 기다립니다. route 등록 위치를 goto 뒤로 옮기거나 기다림을 없앴을 때 어떤 경계가 사라지는지 설명합니다. 이 코드는 독립 Node 스크립트가 아닌 Playwright test 내부 코드이며 실제 브라우저 실행은 다음 단계에서 합니다.
// e2e/flows.spec.cjs 안의 test에서 같은 방식으로 작성합니다.
await mock(page);
await page.goto(origin + '/results.html?q=없는행사');
await expect(page.locator('#empty')).toBeVisible();
await expect(page.locator('#result-list a')).toHaveCount(0);
await page.locator('#empty-reset').click();
await expect(page.locator('#result-summary')).toHaveText('예시 행사 2건입니다.');격리된 브라우저 흐름을 실행합니다
공통 설치를 마친 외부 환경에서 실행합니다. flows.spec.cjs의 route가 goto 전에 설치됐는지 확인합니다. 이 환경에서는 서버·Chrome 제한으로 미실행이며 출력은 비워 둡니다.
npm run test:e2e -- --grep "검색|빈 결과|500|연결|역전|검사 함수"실패 행동을 추적합니다
검사 실패가 만든 실제 trace.zip 경로를 사용합니다. 화면·요청·단언 순서에서 처음 어긋난 부분을 기록합니다. 경로 예시는 그대로 실행하지 않습니다.
npx playwright show-trace test-results/<실패 폴더>/trace.zip확인 문제
실습
flow-assertions.mjs에서 URL·폼·카드·목록 상태 단언을 완성합니다. 반례를 유지하고 flows.spec.cjs의 검색·복귀·빈 결과·500·연결 실패·역전을 읽고 설명합니다. npm test로 모델을 먼저 검사합니다. 브라우저 설치 후 bash check.sh는 모델과 Playwright를 모두 실행합니다. 서버는 harness가 열고 close로 닫으며 이 샌드박스의 브라우저 판정은 PENDING입니다.
실행 명령
bash check.sh
기대 결과
npm test의 의도된 starter assertion 실패·solution 모델 전부 통과; 설치 후 Playwright starter 실패·solution 전부 통과는 외부 검증 PENDING
모범 답안
모범 답안 내려받기더 읽기
면접 질문
- 로딩·오류·빈 결과 화면을 나눈 이유를 설명합니다.
- 검색 요청이 순서와 다르게 도착할 때의 처리 방식을 설명합니다.