2.1 Locator 전략
Locator는 현재 DOM element를 저장하는 handle이 아니라 element를 다시 찾는 query입니다. React, Vue가 re-render해 node가 교체돼도 action 시점에 최신 DOM에서 평가합니다.
권장 우선순위는 사용자와 접근성 관점에 가깝습니다.
page.getByRole('button', { name: '저장' });
page.getByLabel('이메일');
page.getByText('주문이 완료됐습니다');
page.getByTestId('order-row');
getByRole은 implicit, explicit ARIA role과 accessible name을 사용합니다. Locator가 잘 안 잡힌다면 test selector를 복잡하게 만들기 전에 HTML의 label과 accessibility를 개선할 기회인지 봅니다.
좁히기
const product = page
.getByRole('listitem')
.filter({ hasText: 'Product 2' });
await product.getByRole('button', { name: '장바구니' }).click();
Locator는 strict합니다. 단일 element action에 여러 개가 matching되면 실패합니다. .first()나 .nth()로 모호함을 숨기기보다 accessible name, parent scope, explicit test id로 계약을 명확히 합니다.
긴 CSS와 XPath는 DOM 구조 변경에 취약합니다. Styling class가 제품의 안정된 계약이 아니라면 locator로 사용하지 않습니다.
참고: Locators
Locator 선택 decision table
| UI | 우선 locator | 이유 |
|---|---|---|
| Button/link | getByRole(..., { name }) | role과 accessible name을 함께 검증 |
| Form field | getByLabel() | 사용자 label과 control 연결 |
| Static message | getByText() | visible copy가 계약일 때 |
| Image | getByAltText() | 대체 text 계약 |
| Stable app hook | getByTestId() | 사용자 의미로 구분 불가능할 때 |
| Complex repeated row | parent locator + filter() | scope를 business entity로 제한 |
조합하기
const saveOrRetry = page
.getByRole('button', { name: '저장' })
.or(page.getByRole('button', { name: '다시 시도' }));
const paidOrder = page
.getByRole('row')
.filter({ has: page.getByText('결제 완료') })
.filter({ hasText: 'ORDER-42' });
조건이 복잡해질수록 UI contract가 불명확한지 먼저 검토합니다.
실습
- 같은 text를 가진 button 두 개로 strictness error를 만듭니다.
- Parent region을 추가해 locator를 유일하게 만듭니다.
- CSS locator를 role locator로 바꾸며 필요한 accessibility 개선을 기록합니다.
- Test id가 더 적절한 canvas/custom widget 사례를 하나 설계합니다.