Yesterday’s test works, and it hides a problem you cannot see yet. Nothing is wrong with it, today. The problem arrives in three months, when you have forty tests and the designer renames the search box. Every locator lives inside the tests, so you get to fix forty files. That is how suites start dying.
The fix: a page object
A page object is one file that owns everything about one page: its locators and its user actions. Tests stop touching locators entirely; they speak in user language, and the page object translates. When the page changes, you update one file and every test heals.
Four new JavaScript words carry this pattern, so here they are in the Day 3 style before you type them. A class is a blueprint that bundles facts and actions together. new builds a working copy from the blueprint. this means “my own copy” inside the class, so this.page is the page that copy was handed. And export makes the class available to other files, which import it by name.
Create pages/tumble-kitchen-page.js:
export class TumbleKitchenPage {
constructor(page) {
this.page = page;
this.searchBox = page.getByRole('searchbox', { name: 'Search the menu' });
this.searchSubmit = page.getByTestId('search-submit');
this.productCard = page.getByTestId('product-card');
}
async goto() {
await this.page.goto('https://juliapottinger.com/practice/tumble-kitchen/');
}
async searchFor(term) {
await this.searchBox.fill(term);
await this.searchSubmit.click();
}
firstCard() {
return this.productCard.first();
}
priceOf(card) {
return card.getByTestId('product-price');
}
}
Read the class back before moving on. The constructor gathers every pointer in one place. goto and searchFor are user actions with user names. firstCard and priceOf hand locators back to whoever asks. And notice what is missing: nothing in it asserts. A page object states facts about the page; deciding what those facts should mean is the test’s job, and it stays there.
And yesterday’s test becomes:
import { test, expect } from '@playwright/test';
import { TumbleKitchenPage } from '../pages/tumble-kitchen-page';
test('verify that searching for jerk finds the Jerk Chicken Plate with its price', async ({ page }) => {
const store = new TumbleKitchenPage(page);
await store.goto();
await store.searchFor('jerk');
await expect(store.firstCard()).toContainText('Jerk Chicken Plate');
await expect(store.priceOf(store.firstCard())).toHaveText('6,800 JMD');
});
Read the test body out loud: go, search, expect a match, expect a price. Anyone on your team can read that, including people who will never write code, and that readability is what makes failures diagnosable at a glance. Page objects know the page. Tests know the scenario. Keep them apart and the suite survives change.
One page object is rarely the whole store. Each dish also has its own detail page at /practice/tumble-kitchen/dish/
Two habits to build from your very first test
- The moment you type a locator inside a test file, stop. It belongs in the page object. This single habit, kept from day one, is worth more than any tool choice.
- Name tests as sentences. “verify that searching for jerk finds the Jerk Chicken Plate with its price” tells you what broke from the name alone. “test_1” tells you nothing at 2 a.m. on release night.
Do this today
Build both files and run the test. Then extend the page object yourself: add an addToOrder(name) action and a cartCount locator, and write a second test, “verify that adding a dish updates the cart count”. Everything you need is on the store, and the locators are ones you already found on Day 1. That second test, written without me, is the actual lesson. Tomorrow: assertions, and the difference between a test that checks and a test that only looks like it checks.
