02
MISSION COMPLETE
Academy - Flat tests & locator discipline
Login, search, filters, add-to-cart
03
ACTIVE MISSION
Refactor the flat suite into Page Objects
Genin - Genin Missions - the pivotTHE 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.
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/);
});