Skip to main content

Performing Hand Seals...

MISSIONS (ASSIGNMENTS) ▸ GENIN
02
Academy - Flat tests & locator discipline
Login, search, filters, add-to-cart
MISSION COMPLETE
03

Refactor the flat suite into Page Objects

Genin - Genin Missions - the pivot
ACTIVE MISSION
Scroll: Genin / POM Training Ground: Login & Search Difficulty: ★★☆ Skills: POM, refactor
THE TASK

Take the flat Academy login + search specs and refactor them into Page Objects. Move every locator into a private property, expose user actions as methods, and add a base page for shared navigation. When you're done, no test file should contain a single page.locator call.

DOM CONTRACT
// LoginPage getByLabel('Username') getByLabel('Password') getByRole('button', { name: 'Sign in' }) getByRole('alert') // error region // MenuPage getByRole('searchbox', { name: 'Search menu' }) getByRole('tab', { name: 'All' | 'Miso' | ... }) getByRole('heading', { name: 'Miso Ramen' | ... }) getByRole('button', { name: 'Add Miso Ramen to cart' | ... }) getByRole('listitem') // CartPage getByTestId('cart-count') getByTestId('cart-total')
TEST CASES
POSITIVE Valid credentials land on the menu & greet the user by name.
NEGATIVE Wrong password shows alert text "Invalid credentials".
POSITIVE Search "miso" yields exactly one item & it reads "Miso Ramen".
POSITIVE Add Miso Ramen → cart count shows 1, cart total shows "$13.50".
POSITIVE Cross-page: login → search → add to cart → verify cart total.
Mission Brief - the definition of "good"
0 / 5 met

Self-evaluation checklist (v1). Tick each as you confirm it in your own suite.

No locators in test files - they live in page objects.
No hardcoded waits (waitForTimeout); web-first assertions only.
Assertions check real values / state, not existence alone.
Tests are isolated and deterministic - no order dependence.
Locator priority respected: role / label / text → testid → CSS last.
ACCEPT MISSION

Reference Solution

pages/LoginPage.ts
import { type Page, type Locator } from '@playwright/test';

export class LoginPage {
  private readonly username: Locator;
  private readonly password: Locator;
  private readonly submitBtn: Locator;
  readonly error: Locator;

  constructor(private page: Page) {
    this.username = page.getByLabel('Username');
    this.password = page.getByLabel('Password');
    this.submitBtn = page.getByRole('button', { name: 'Sign in' });
    this.error = page.getByRole('alert');
  }

  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();
  }
}
tests/login.spec.ts
import { test, expect } from '@playwright/test';
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');
});

test('valid login lands on menu', async ({ page }) => {
  const login = new LoginPage(page);
  await login.goto();
  await login.signIn('naruto', 'ramen');

  await expect(page).toHaveURL(/\/restaurants|\/menu/);
});