RELIABLE SUITES · LESSON 10 OF 24
Page objects without hiding intent
Encapsulate repeated page interactions in a small typed object.
- Page
- Locator
- page objects
- composition
Your runnable checkpoint
Extract into a new practice folder. Each download contains tests, configuration, the complete local practice app and an offline lesson. Use Node.js 22 or 24. Each checkpoint is independent.
Install dependencies and browser binaries using the commands below. The test runner starts the practice server on port 4174. Tests target localhost, never the production EduPoly site.
1. Understand the concepts
A page object groups locators and actions for one interface. It can reduce duplication when many tests use the same form. Keep it small and named after what a user can do.
Accept the Page through the constructor rather than launching a browser inside the object. The runner still owns context creation, isolation and teardown.
Expose focused actions such as addTask and a row locator. The test should still explain its expected outcome. A giant object that performs login, setup, assertions and cleanup can hide the reason a scenario exists.
Use page objects when they simplify real repetition. For a one-off check, direct locators may be clearer. Refactoring should improve readability, not merely move lines into another file.
2. Build and run
These are complete test files and configuration. The download also includes app/ and server.mjs; preserve their paths. Stop any manually started practice server before running the suite.
npm install
npx playwright install chromium
npm run check
npm testtests/lesson.spec.ts
import { test, expect } from '@playwright/test';
import { TaskPage } from '../pages/task-page';
test('use a focused page object', async ({ page }) => {
const tasks = new TaskPage(page);
await tasks.open();
await tasks.add('Page object task');
await expect(tasks.row('Page object task')).toHaveCount(1);
});
pages/task-page.ts
import type { Page, Locator } from '@playwright/test';
export class TaskPage {
constructor(private readonly page: Page) {}
async open(): Promise<void> { await this.page.goto('/'); }
async add(title: string): Promise<void> {
await this.page.getByLabel('Task title', { exact: true }).fill(title);
await this.page.getByRole('button', { name: 'Add task', exact: true }).click();
}
row(title: string): Locator { return this.page.getByTestId('task-row').filter({ hasText: title }); }
}
playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 1 : 0,
workers: process.env.CI ? 2 : undefined,
timeout: 30000,
expect: { timeout: 5000 },
reporter: [['list'], ['html', { open: 'never' }]],
use: { baseURL: 'http://127.0.0.1:4174', trace: 'retain-on-failure', screenshot: 'only-on-failure' },
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
webServer: { command: 'node server.mjs', url: 'http://127.0.0.1:4174', reuseExistingServer: false, timeout: 15000 }
});
package.json
{
"name": "edupoly-playwright-page-object-models",
"version": "1.0.0",
"private": true,
"scripts": {
"test": "playwright test",
"test:headed": "playwright test --headed",
"test:ui": "playwright test --ui",
"check": "tsc --noEmit",
"report": "playwright show-report",
"serve": "node server.mjs"
},
"devDependencies": {
"@playwright/test": "1.56.1",
"typescript": "5.9.3",
"@types/node": "22.20.5"
}
}
tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS",
"moduleResolution": "Node",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"types": [
"node"
],
"noEmit": true
},
"include": [
"tests/**/*.ts",
"pages/**/*.ts",
"playwright.config.ts"
]
}
.gitignore
node_modules/
playwright-report/
test-results/
playwright/.auth/
.env
3. Trace the code and check the result
- The class receives the runner-owned Page.
- row returns a Locator, not a stale element handle.
- The helper names describe user actions.
Verify your result
- The page object opens the app and adds one task.
- The assertion remains visible in the scenario.
4. Practice independently
Add a complete(title) method that acts only inside the matching row.
Show the explanation
Call this.row(title).getByRole("button", { name: "Complete", exact: true }).click() and return its promise. Verify the row’s completed state in the test.
When something goes wrong
If a browser executable is missing, run npx playwright install chromium in the project folder. If port 4174 is occupied, stop your manual practice server. Read assertion call logs for mismatched locators or expectations. Use npm run report to inspect failures; do not add fixed sleeps or broad resets to hide them.
