adr/0002-page-objects-pattern.md
Proposed
Our Playwright tests currently interact directly with page elements using raw selectors and actions scattered throughout test files. This approach leads to several issues:
To improve maintainability, readability, and test stability, we want to adopt the Page Objects pattern to encapsulate page-specific knowledge and provide a clean API for test interactions.
The Page Objects pattern was originally described by Martin Fowler as a way to "wrap an HTML page, or fragment, with an application-specific API, allowing you to manipulate page elements without digging around in the HTML."
We will adopt the Page Objects pattern for organizing E2E tests. Every page or major UI component should have a corresponding page object class that:
login(), createPost(), navigateToSettings()Following both Fowler's original principles and modern Playwright best practices:
LoginPage, PostEditor, AdminDashboard)fillPostTitle() not typeInTitleInput())getErrorMessage() returns locator)waitForErrorMessage())loginPage.saveButton.click instead of page.locator('[data-testid="save-button"]')/e2e/helpers/pages/ directory with clear naming conventions// e2e/helpers/pages/admin/LoginPage.ts
export class LoginPage extends BasePage {
public readonly emailInput = this.page.locator('[data-testid="email-input"]');
public readonly passwordInput = this.page.locator('[data-testid="password-input"]');
public readonly loginButton = this.page.locator('[data-testid="login-button"]');
public readonly errorMessage = this.page.locator('[data-testid="login-error"]');
constructor(page: Page) {
super(page);
this.pageUrl = '/login';
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
async waitForErrorMessage() {
await this.errorMessage.waitFor({ state: 'visible' });
return this.errorMessage;
}
getErrorMessage() {
return this.errorMessage;
}
}
// In test file
test.describe('Login', () => {
test('invalid credentials', async ({page}) => {
// Arrange
const loginPage = new LoginPage(page);
// Act
await loginPage.goto();
await loginPage.login('[email protected]', 'wrongpassword');
const errorMessage = await loginPage.waitForErrorMessage();
// Assert
await expect(errorMessage).toHaveText('Invalid credentials');
});
}