This content originally appeared on DEV Community and was authored by Murat K Ozcan
The debate on Page Object vs module pattern is really just Inheritance vs Composition. Inheritance (PO) is great for describing what something is; the page has x, y, z on it. Composition (module pattern) is great for describing what something does. Which one do you think best fits component based architecture, where components are the building blocks and get reused on pages? How about user flows?
Components compose and if the pages which are built of components are abstracted with classes, there's over-abstraction and inevitable duplication
In modern testing frameworks like Playwright and Cypress, strict Page Object Model (POM) can feel overkill, especially when:
• You’re using data selectors (data-qa, data-cy) for stable locators.
• The tools already offer powerful, built-in utilities for interacting with the UI.
• You don’t need unnecessary abstraction layers that make debugging harder.
1️⃣ Unnecessary Abstraction
• POM adds an extra layer that often doesn’t provide real value.
• Modern test frameworks are already powerful enough without it.
2️⃣ Base Page Inheritance is Overkill
• Having a BasePage class with generic methods (click(), fill()) just to wrap Playwright’s own API makes no sense.
• Playwright already has page.locator(), page.click(), page.fill(), etc.
3️⃣ Harder Debugging
• With POM, if a test fails, you often have to jump between multiple files to find what went wrong.
• With direct helper functions, you see exactly what’s happening.
🔴 Traditional Page Object Model (POM)
❌ Unnecessary complexity → Extra class & inheritance
❌ Harder debugging → Need to jump between files
❌ Wrapping Playwright’s own API for no reason
class LoginPage {
constructor(page) {
this.page = page;
this.usernameField = page.locator('[data-testid="username"]');
this.passwordField = page.locator('[data-testid="password"]');
this.loginButton = page.locator('[data-testid="login-button"]');
}
async login(username, password) {
await this.usernameField.fill(username);
await this.passwordField.fill(password);
await this.loginButton.click();
}
}
export default LoginPage;
import { test, expect } from '@playwright/test';
import LoginPage from './LoginPage.js';
test('User can log in', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.login('testUser', 'password123');
await expect(page.locator('[data-testid="welcome-message"]')).toHaveText('Welcome, testUser');
});
✅ Functional Helper Approach (Better)
📌 Why is this better?
✅ No extra class → Directly use Playwright API
✅ No unnecessary this.page assignments
✅ Much easier to maintain & debug
export async function login(page, username, password) {
await page.fill('[data-testid="username"]', username);
await page.fill('[data-testid="password"]', password);
await page.click('[data-testid="login-button"]');
}
import { test, expect } from '@playwright/test';
import { login } from './loginHelpers.js';
test('User can log in', async ({ page }) => {
await login(page, 'testUser', 'password123');
await expect(page.locator('[data-testid="welcome-message"]')).toHaveText('Welcome, testUser');
});
This content originally appeared on DEV Community and was authored by Murat K Ozcan

Murat K Ozcan | Sciencx (2025-02-22T12:44:13+00:00) Page objects vs Functional helpers. Retrieved from https://www.scien.cx/2025/02/22/page-objects-vs-functional-helpers/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.