4.4 Page Object와 Component
Page Object Model은 locator와 반복되는 사용자 동작을 한 곳에 모읍니다. Test의 business assertion까지 모두 숨기면 실패 의미가 약해질 수 있습니다.
import type { Locator, Page } from '@playwright/test';
export class TodoPage {
readonly input: Locator;
readonly addButton: Locator;
constructor(private readonly page: Page) {
this.input = page.getByLabel('할 일');
this.addButton = page.getByRole('button', { name: '추가' });
}
async goto() {
await this.page.goto('/');
}
async add(name: string) {
await this.input.fill(name);
await this.addButton.click();
}
}
Test에서는 기대 결과를 명시합니다.
await todos.add('fixture 공부');
await expect(page.getByRole('listitem')).toContainText('fixture 공부');
거대한 AppPage 하나보다 navigation, order form, table처럼 안정된 component 경계로 나눕니다. Page Object method가 CSS 구조를 노출하지 않게 하고 사용자 언어를 사용합니다.
단순한 test 세 개에 abstraction을 먼저 만들 필요는 없습니다. 중복과 변경 패턴이 확인된 뒤 추출합니다.
좋은 Page Object의 경계
- Page/feature의 stable business action을 노출합니다.
- Locator는 가능한 한 private하게 유지합니다.
- Scenario-specific assertion을 모두 숨기지 않습니다.
- API client, database helper, page object를 하나의 거대한 class에 섞지 않습니다.
- Test step으로 report의 business language를 유지합니다.
class LoginPage {
constructor(private readonly page: Page) {}
async login(email: string) {
await test.step(`Login: ${email}`, async () => {
await this.page.getByLabel('이메일').fill(email);
await this.page.getByRole('button', { name: '로그인' }).click();
}, { box: true });
}
}
Component object
Header, modal, table처럼 여러 page에서 반복되는 독립 영역은 component object로 분리할 수 있습니다. Root locator를 constructor로 받아 page 전체를 다시 검색하지 않습니다.
실습
- Todo action을 page object로 추출합니다.
- Assertion을 object 안/밖에 둘 때 test readability를 비교합니다.
- Boxed step을 적용하고 failure source를 확인합니다.
- 세 test 이하의 작은 중복은 abstraction 없이 유지해 봅니다.