Skip to main content

Performing Hand Seals...

SCROLLS (GUIDES) ▸ GENIN - PAGE OBJECT MODELS
03

Page Object Models

Genin - Genin Missions - the pivot

The 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

← Academy - Setup & Locators Next: Chunin Exam (Sealed)