Playwright 공식 문서는 getByRole()부터 쓰라고 한다. 맞는 말인데, Ant Design을 쓰는 UI에서 이걸 맹목적으로 따르다 보면 꽤 자주 막힌다.
내가 처음으로 "왜 안 되지?"를 반복했던 상황들과, 결국 어떻게 해결했는지를 정리한다.
기본 순서는 지킨다
셀렉터 선택 순서는 이게 맞다.
getByRole() → getByText() → locator()
getByRole이 가장 유지보수하기 좋고, locator('.class')는 마크업이 바뀌면 바로 깨진다. 다만 커스텀 컴포넌트에서는 getByRole이 통하지 않는 케이스가 생각보다 많다.
// getByRole이 되는 것들
await page.getByRole('button', { name: '검색', exact: true }).click();
await page.getByRole('textbox', { name: '정책 이름' }).fill('값');
await page.getByRole('checkbox', { name: '사용' }).check();
// 안 되면 getByText
await page.getByText('상세', { exact: true }).click();
// 그래도 안 되면 locator
await page.locator('tbody tr').filter({ hasText: '검색어' }).first().click();
Ant Design 테이블에서 getByRole('cell')이 안 되는 이유
// ❌ 이게 안 된다
await expect(page.getByRole('cell', { name: '정책 이름' })).toBeVisible();
Ant Design의 가상 스크롤 테이블은 <td role="cell">을 쓰지 않는다. Playwright가 찾으려는 role이 DOM에 없으니 당연히 실패한다.
// ✅ 헤더는 이렇게
await expect(page.getByText('정책 이름', { exact: true }).first()).toBeVisible();
// ✅ 특정 행 찾기
const targetRow = page.locator('tbody tr').filter({ hasText: '조회 정책 1' });
await expect(targetRow).toBeVisible();
// ✅ 테이블 로딩 확인
const firstRow = page.locator('tbody tr').first();
await expect(firstRow).toBeAttached({ timeout: 5000 });
데이터가 없는 경우도 처리해야 할 때가 있다.
const noData = page.getByText('데이터가 없습니다');
const isEmpty = await noData.isVisible({ timeout: 1000 }).catch(() => false);
if (isEmpty) {
// 빈 상태 처리
}
사이드바와 테이블에 같은 텍스트가 있을 때
필터를 선택하면 사이드바에 "test_agent_03"이 뜨고, 동시에 테이블 본문에도 "test_agent_03"이 나온다. .first()를 무조건 붙이면 어느 쪽이 잡힐지 모른다.
// ❌ 사이드바가 잡힐 수 있음
await page.getByText('test_agent_03').first().click();
드롭다운 필터 값은 항목 수가 함께 붙는다. test_agent_03 (17건) 이런 식으로. 이 패턴을 쓰면 구분된다.
// ✅ "(N건)" 패턴으로 드롭다운만 선택
await page.getByText(/test_agent_03.*건/).first().click();
// ✅ 영역 한정
await page.locator('main').getByText('test_agent_03').first().click();
await page.locator('[role="tabpanel"]').getByText('설정값').first().click();
fill()이 안 되는 드롭다운
상세 검색에서 처음에 이렇게 했다.
// ❌ 쿼리 파싱이 안 됨
await page.getByRole('textbox', { name: '검색어 입력' }).fill('test_agent_03');
await page.keyboard.press('Enter');
자동완성 드롭다운은 직접 입력이 아니라 목록에서 선택하는 방식이다. fill()로 값을 넣어봤자 내부 쿼리 파싱이 안 된다.
// ✅ 클릭 → 드롭다운에서 선택
await page.getByText('상세', { exact: true }).click();
const searchInput = page.getByRole('textbox', { name: '검색어를 입력하거나 선택하십시오' });
await searchInput.click();
await page.waitForTimeout(500);
await page.getByText('사용자 아이디', { exact: true }).first().click();
await page.waitForTimeout(500);
await page.getByText(':', { exact: true }).first().click();
await page.waitForTimeout(500);
await page.getByText(/test_agent_03.*건/).first().click();
await page.waitForTimeout(500);
await page.keyboard.press('Escape');
await page.getByRole('button', { name: '검색', exact: true }).click();
await page.waitForLoadState('networkidle');
단계마다 waitForTimeout(500)이 있는 건 드롭다운 애니메이션 때문이다. 너무 빨리 다음 동작을 하면 클릭이 무시된다.
히든 라디오 버튼
라디오 버튼이 CSS로 숨겨진 경우 getByRole('radio')는 클릭 대상을 찾지 못한다.
// ❌
await page.getByRole('radio', { name: '상세' }).click();
// ✅ 라벨 텍스트 클릭
await page.getByText('상세', { exact: true }).click();
exact: true는 "상세 검색" 같은 더 긴 텍스트와 구분하기 위해 필요하다.
커스텀 드롭다운
일반 <select>가 아닌 커스텀 드롭다운은 selectOption()이 먹히지 않는다.
// ❌
await page.getByLabel('공격 유형').selectOption('악성코드');
// ✅
await page.getByRole('combobox', { name: '공격 유형' }).click();
await page.waitForTimeout(300);
await page.getByTitle('악성코드').click();
드롭다운 옵션에 title 속성이 설정된 경우가 많아서 getByTitle이 잘 맞는다. 없으면 getByText로 대체.
결과 검증은 테이블 본문에서
필터 결과를 검증할 때도 함정이 있다. 사이드바에도 같은 값이 표시되니까.
// ❌ 사이드바일 수 있음
await expect(page.getByText('test_agent_03').first()).toBeVisible();
// ✅ 테이블 행에서 확인
const row = page.locator('tbody tr').filter({ hasText: 'test_agent_03' });
await expect(row.first()).toBeVisible({ timeout: 5000 });
| 상황 | 잘못된 접근 | 올바른 접근 |
|---|---|---|
| Ant Design 테이블 헤더 | getByRole('cell') |
getByText('컬럼명').first() |
| 동일 텍스트 중복 | .first() 무조건 |
.*건 패턴 또는 영역 한정 |
| 자동완성 드롭다운 | fill() |
클릭 → 드롭다운에서 선택 |
| 히든 라디오 버튼 | getByRole('radio') |
getByText('라벨', {exact:true}) |
| 커스텀 드롭다운 | selectOption() |
click() → getByTitle() |
다음 편에서는 작성 단계 이전으로 돌아간다. TestLink TC를 받아서 스크립트로 옮길 때 첫 실행에서 성공하는 방법.