03
Page Object Models
Genin - Genin Missions - the pivotThe pivot from “first green script” to a real suite. A Page Object holds the locators and exposes intent-revealing actions; the test reads like a user story and contains zero locators. This is the exact place AI codegen goes wrong.
THE RULE
Locators are private properties. Actions are methods. Nothing in a test file selects an element directly — the AI mistake is “POM” classes that just relocate page.locator calls into the test anyway.
✓ GOOD - locators private, no leaks
tests/login.spec.ts
// tests/login.spec.ts - reads like a story
import { LoginPage } from '../pages/LoginPage';
test('wrong password is rejected',
async ({ page }) => {
const login = new LoginPage(page);
await login.goto();
await login.signIn('naruto', 'wrong-pass');
await expect(login.error)
.toHaveText('Invalid credentials');
});
✗ BAD - the common AI “POM”
tests/login.spec.ts
// tests/login.spec.ts
test('login', async ({ page }) => {
await page.goto('/dojo/app/login');
// locators living in the test file
await page.locator('#username')
.fill('naruto');
await page.locator('#pwd')
.fill('wrong-pass');
await page.click('.btn-primary');
// existence only - a typo'd message
// would still pass
expect(await page.locator('.error')
.isVisible()).toBeTruthy();
});
What the Page Object looks like
✓ GOOD - a page object
pages/LoginPage.ts
export class LoginPage {
private username = this.page.getByLabel('Username');
private password = this.page.getByLabel('Password');
private submitBtn = this.page.getByRole('button',
{ name: 'Sign in' });
readonly error = this.page.getByRole('alert');
constructor(private page: Page) {}
async goto() {
await this.page.goto('/dojo/app');
}
async signIn(user: string, pass: string) {
await this.username.fill(user);
await this.password.fill(pass);
await this.submitBtn.click();
}
}
✗ BAD - “POM” that leaks locators
pages/LoginPage.ts
export class LoginPage {
// these get used raw in tests
get username() {
return this.page.locator('#username');
}
get password() {
return this.page.locator('#pwd');
}
get submit() {
return this.page.locator('.btn-primary');
}
constructor(private page: Page) {}
// no action methods - tests still
// call .fill(), .click() directly
// on the returned locators
}
Pitfalls
- God-object page classes that grow to 40 methods. Split by logical area.
- Returning raw locators from getters — tests still call .fill() and .click() directly, defeating the purpose.
- Putting assertions inside the page object — assertions belong in the test. The PO just performs actions.
- A “base page” that becomes a junk drawer — keep it to navigation and shared setup only.
← Academy - Setup & Locators
Next: Chunin Exam (Sealed)