무엇인가?
-
웹 테스트를 작성할 때 가장 먼저 해결해야 할 문제는 "페이지에서 원하는 요소를 어떻게 찾을 것인가"이다.
-
전통적으로는 CSS 선택자나 XPath를 사용했지만, 이 방식은 DOM 구조가 바뀌면 테스트가 쉽게 깨지는 문제가 있었다.
-
Playwright는 이 문제를 해결하기 위해 Locator라는 개념을 도입했다.
-
Locator는 특정 순간에 페이지에서 요소를 찾는 방법을 표현하는 객체다.
-
핵심은 Locator가 액션을 실행할 때마다 DOM에서 요소를 새로 탐색한다는 점이다.
-
덕분에 렌더링 중 DOM이 바뀌더라도 항상 최신 요소를 대상으로 동작한다.
-
또한 Playwright는 요소가 준비될 때까지 자동으로 기다리는 auto-waiting과, 일시적 실패 시 재시도하는 retry-ability를 Locator 수준에서 기본으로 제공한다.
-
이 덕분에 테스트 코드에
sleep()이나 수동 대기 로직을 삽입할 필요가 없어진다. -
즉, 비동기로 데이터가 로드되거나 인터랙션이 처리되는 상황에서도 Playwright가 타이밍을 알아서 맞춰주므로, 우리는 "언제 준비되는지"를 신경 쓰지 않고 테스트 시나리오 작성에만 집중할 수 있다.
권장 Locator 우선순위: 테스트할 요소 찾기
-
Playwright는 사용자와 보조 기술이 페이지를 인식하는 방식에 가까운 Locator를 우선 사용하도록 권장한다.
-
가장 권장되는 것은
getByRole()로, 요소의 접근성 역할과 이름을 기준으로 찾는다. -
예를 들어 "Sign in"이라는 이름의 버튼은 아래처럼 찾을 수 있다.
await page.getByRole('button', { name: 'Sign in' }).click(); -
폼 요소는
getByLabel()로 연결된label텍스트를 기준으로 찾는 것이 적합하다.await page.getByLabel('Password').fill('secret'); -
label이 없고 placeholder만 있는input은getByPlaceholder()를 사용한다.await page.getByPlaceholder('name@example.com').fill('test@test.com'); -
텍스트 내용으로 요소를 찾을 때는
getByText()를 쓰되, 버튼처럼 인터랙티브한 요소보다는div,span,p같은 비인터랙티브 요소에 적합하다.await expect(page.getByText('Welcome, John')).toBeVisible(); -
이미지처럼
alt속성을 가진 요소는getByAltText()를 사용한다.await page.getByAltText('playwright logo').click(); -
title속성이 있는 요소는getByTitle()을 쓴다.await expect(page.getByTitle('Issues count')).toHaveText('25 issues'); -
위의 방법으로 요소를 특정하기 어려울 때는
data-testid속성을 HTML에 직접 부여하고getByTestId()로 찾는 방식을 사용한다.await page.getByTestId('directions').click(); -
CSS 선택자나 XPath는 DOM 구조에 강하게 결합되어 테스트가 쉽게 깨지므로 가장 마지막 수단으로만 사용해야 한다.
Locator 필터링: 테스트할 요소가 많을 경우 특정하기
-
페이지에 동일한 구조의 요소가 여러 개 있을 때, 원하는 요소 하나를 정확히 특정해야 한다.
-
이때
locator.filter()를 사용하면 기존 Locator 결과를 추가 조건으로 좁힐 수 있다. -
예를 들어 "Product 2" 카드 안의 버튼만 클릭하려면 아래처럼 필터를 걸 수 있다.
await page.getByRole('listitem') .filter({ hasText : 'Product 2' }) .getByRole('button', { name: 'Add to cart' }) .click(); -
hasText는 해당 텍스트를 포함한 요소만,hasNotText는 포함하지 않은 요소만 선택한다. -
텍스트뿐 아니라 특정 자식 Locator의 존재 여부로도 필터링할 수 있으며,
has와hasNot옵션을 사용한다. -
예를 들어 "Product 2"라는 heading을 자식으로 가진 listitem만 선택하려면 아래처럼 쓴다.
await page.getByRole('listitem') .filter({ has: page.getByRole('heading', { name: 'Product 2' }) }) .getByRole('button', { name: 'Add to cart' }) .click(); -
이때
has안에 전달하는 Locator는 바깥 Locator가 찾은 요소의 내부에서만 탐색이 시작된다. -
즉, 위 예시에서
has안의 Locator는<li>하나를 기준으로 그 안에서 heading을 찾는다. -
따라서
<li>바깥에 있는 요소를has의 조건으로 쓰면, 탐색 범위를 벗어나기 때문에 항상 매칭에 실패한다. -
아래 코드는
<ul>전체를 조건으로 쓰기 때문에<li>안에서<ul>을 찾으려 해서 동작하지 않는 잘못된 예시다.// ✖ 잘못된 예시 await page.getByRole('listitem') .filter({ has: page.getByRole('list').getByText('Product 2') }) -
조건이 복잡할 경우
filter()를 여러 번 체이닝해서 단계적으로 좁혀나갈 수 있다.await rowLocator .filter({ hasText: 'Mary' }) .filter({ has: page.getByRole('button', { name: 'Say goodbye' }) }) .click();
Locator 조합 연산자: 여러 Locator로 찾기
-
두 Locator를 동시에 만족하는 요소를 찾고 싶을 때는
locator.and()를 사용한다. -
예를 들어 role이 button이면서 title이 "Subscribe"인 요소를 아래처럼 찾을 수 있다.
const button = page.getByRole('button').and(page.getByTitle('Subscribe')); -
반대로 두 Locator 중 하나라도 매칭되는 요소를 찾을 때는
locator.or()를 사용한다. -
예를 들어 "New email" 버튼이 나타날 수도 있고, 보안 설정 팝업이 먼저 나타날 수도 있는 상황에서는 아래처럼 처리한다.
const newEmail = page.getByRole('button', { name: 'New' }); const dialog = page.getByText('Confirm security settings'); await expect(newEmail.or(dialog).first()).toBeVisible(); if (await dialog.isVisible()) await page.getByRole('button', { name: 'Dismiss' }).click(); await newEmail.click(); -
or()로 연결된 두 요소가 동시에 화면에 존재하면, Playwright가 "어떤 요소에 액션을 해야 하는가"를 결정할 수 없어 오류를 던진다. -
이 경우
.first()를 붙여 "둘 중 먼저 매칭되는 것을 선택하라"고 명시하면 오류를 방지할 수 있다.
Strictness와 주의사항
-
Playwright의 Locator는 기본적으로 "하나의 액션은 하나의 요소에만 적용된다"는 엄격한 규칙을 따른다.
-
예를 들어 페이지에 버튼이 3개 있는데
getByRole('button').click()을 호출하면, Playwright는 "어느 버튼을 클릭해야 하는지 알 수 없다"며 오류를 던진다.// 버튼이 여러 개면 오류 발생 await page.getByRole('button').click(); -
이 오류는 잘못된 코드를 실행하는 것을 막아주는 안전장치 역할을 한다.
-
반면
count()처럼 여러 요소를 대상으로 하는 메서드는 여러 요소가 매칭되어도 정상 동작한다.// 버튼이 여러 개여도 정상 동작 await page.getByRole('button').count(); -
이 오류를 피하기 위해
.first(),.last(),.nth(1)등으로 인덱스를 지정할 수 있다. -
하지만 이 방식은 페이지 구조가 바뀌면 다른 요소를 선택할 수 있어 테스트가 잘못된 대상에 실행되는 위험이 있다.
-
올바른 해결 방법은 애초에 요소를 유일하게 특정할 수 있도록 Locator 자체를 더 정확하게 작성하는 것이다.
-
예를 들어 버튼이 여러 개라면,
filter()나accessible name을 추가해서 원하는 버튼 하나만 매칭되도록 좁혀야 한다.