Standing on the Shoulders of Giants
Before we dive in, let's give credit where it's due.
This post—and my composition reference project—wouldn't exist without Ivan Davidov. His article on the Mixin Design Pattern and his reference repository introduced me (and many others) to a better way of structuring Playwright frameworks.
If you haven't read Ivan's work yet, stop right now and go read it. Seriously. Then come back. His explanation of the problem and the solution is excellent, and I'm not going to repeat what he already explained better than I could.
Before continuing, please star Ivan's original repository: github.com/idavidov13/Mixin-Design-Pattern
This post is about the evolution of that idea—taking a well-established pattern and applying more concepts to make it production-ready for your projects.
The Problem We All Know
You start a Playwright project. You create a BasePage with common methods. Life is good.
Then you add navigation methods. Then modal handling. Then table interactions. Then form utilities. Before you know it, your BasePage is 800 lines long and contains methods that 90% of your page objects will never use.
You've created a God object. A monolithic class that knows how to do everything, used by everyone, maintained by no one.
The inheritance chain looks like this:
BasePage (navigation, modals, tables, forms, API calls, cookies, localStorage...)
↓
LoginPage extends BasePage
DashboardPage extends BasePage
ProfilePage extends BasePage
Every page inherits everything, whether it needs it or not.
Want to add a new utility method? You modify BasePage. Now you've potentially affected every single page object in your framework. Hope you didn't break anything.
The Mixin Solution: Composition Over Inheritance
Instead of one bloated base class, we create focused feature packs (mixins):
NavigationMixin- Handles nav bar interactionsModalMixin- Handles modal/dialog operationsTableMixin- Handles data table interactionsFormMixin- Handles form field utilities
Then we compose page objects by mixing in only what they need:
// DashboardPage needs navigation and tables
export interface DashboardPage extends NavigationMixin, TableMixin {}
applyMixins(DashboardPage, [NavigationMixin, TableMixin]);
// LoginPage only needs forms
export interface LoginPage extends FormMixin {}
applyMixins(LoginPage, [FormMixin]);
Each page object has exactly the functionality it needs. Nothing more. Nothing less.
This is the core idea Ivan demonstrated—a well-established design pattern applied to Playwright.
My Application: Combining Patterns with Production Tooling
Ivan's repository demonstrates the mixin pattern cleanly and effectively.
I wanted to see what it would look like to apply this pattern alongside the production tooling I've written about in previous posts—Allure reporting, Winston logging, debug utilities, CI/CD workflows.
That's what playwright-composition-reference is: the mixin pattern combined with the infrastructure pieces you'd need in a real project.
What's Included
Multiple Mixins
Beyond the navigation example, I've added:
TableMixin- Data table/grid interactionsModalMixin- Modal/dialog handlingFormMixin- Form field utilities
All using method prefixes (nav_, table_, modal_, form_) to avoid naming collisions.
Production Infrastructure
The same tooling from my other reference projects:
- Allure reporting integration (covered in previous posts)
- Winston-based logging with test lifecycle tracking
- Debug utilities for troubleshooting mixin composition
- Failure artifact capture (screenshots, videos, traces)
- CI/CD workflow examples
Documentation
Guides on creating your own mixins, debugging composition issues, and adapting the pattern to your application.
Why I'm Keeping the BasePage Version Too
Here's something important: I'm not abandoning my original inheritance-based framework.
Why?
Because my audience is primarily LATAM developers learning Playwright and test automation. Many are just starting out.
Throwing mixins at someone who's still wrapping their head around Page Object Model is like teaching calculus to someone learning multiplication. It's overwhelming and discouraging.
The BasePage approach works. It's simpler to understand. It's a solid foundation for:
- Small projects
- Learning automation fundamentals
- Teams just getting started with Playwright
There's nothing wrong with starting there.
But as your framework grows—as you add more components, more complexity, more shared behaviors—you'll hit the ceiling. The BasePage becomes unwieldy. Inheritance becomes limiting.
That's when composition makes sense.
The Learning Path
Here's how I see the progression:
Phase 1: BasePage (Inheritance)
Repository: playwright-reference-project
Start here if:
- You're new to Playwright
- You're learning Page Object Model
- Your app has a simple structure
- Your team is just getting started
Strength: Easy to understand, quick to implement.
Weakness: Doesn't scale well as complexity grows.
Phase 2: Mixins (Composition)
Repository: playwright-composition-reference
Move here when:
- Your
BasePageis getting bloated - You have shared components across unrelated pages
- You want more flexibility in code reuse
- You're comfortable with TypeScript patterns
Strength: Scales beautifully, promotes modularity.
Weakness: Steeper learning curve, more setup initially.
A Real Example: When to Use Each
Simple Login Application (Use BasePage)
Your app:
- Login page
- Dashboard page
- Profile page
- Settings page
All pages share: navigation bar, logout functionality.
BasePage approach makes sense here. The shared behavior is minimal and consistent across all pages.
Complex SaaS Dashboard (Use Mixins)
Your app:
- Admin dashboard (navigation, tables, modals)
- User management (navigation, tables, forms)
- Analytics (navigation, charts, filters)
- Settings (navigation, forms)
Different pages need different combinations of shared behavior.
Mixin approach shines here. Compose each page with exactly what it needs.
Trade-Offs of the Mixin Pattern
The mixin pattern isn't perfect. Here are real considerations:
Method Name Collisions
If two mixins define the same method name, the last one applied overwrites the previous one. Using prefixes (nav_, table_) helps avoid this, but it adds verbosity to your method names.
Less Obvious Method Origin
With inheritance, method origin is clear:
class LoginPage extends BasePage {
// navigate() comes from BasePage
}
With mixins, you need to remember which mixin provides which method:
interface LoginPage extends NavigationMixin, FormMixin {}
// Where does handleSubmit() come from?
Good naming conventions help. Debug utilities can verify what's mixed in. But it requires more attention during development.
Debugging Complexity
When a test fails and you're stepping through code, you might call a method that was copied from a mixin at runtime. Your debugger might not show you the original mixin source immediately. This is where debug utilities and clear logging become important—not to fix the pattern, but to work with it effectively.
Practical Implementation
Step 1: Start with Ivan's Pattern
Read Ivan's article: idavidov.eu/upgrade-playwright-tests-typescript-mixin-design-pattern-guide
Understand the applyMixins utility:
export function applyMixins(derivedCtor: any, constructors: any[]): void {
constructors.forEach((baseCtor) => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
if (name !== 'constructor') {
Object.defineProperty(
derivedCtor.prototype,
name,
Object.getOwnPropertyDescriptor(baseCtor.prototype, name) ||
Object.create(null)
);
}
});
});
}
This is the engine. It copies methods from your mixins to your target class at runtime.
Step 2: Create Your First Mixin
Start simple. Pick one shared behavior—let's say navigation:
// lib/mixins/navigation-mixin.ts
import { Page, Locator } from '@playwright/test';
export class NavigationMixin {
constructor(protected page: Page) {}
get nav_homeLink(): Locator {
return this.page.getByRole('link', { name: 'Home' });
}
get nav_settingsLink(): Locator {
return this.page.getByRole('link', { name: 'Settings' });
}
async nav_clickSettings(): Promise<void> {
await this.nav_settingsLink.click();
}
async nav_goHome(): Promise<void> {
await this.nav_homeLink.click();
}
}
Step 3: Compose a Page Object
// page-objects/dashboard-page.ts
import { Page, Locator } from '@playwright/test';
import { applyMixins } from '../lib/core/apply-mixins';
import { NavigationMixin } from '../lib/mixins/navigation-mixin';
export class DashboardPage {
constructor(protected page: Page) {}
get welcomeMessage(): Locator {
return this.page.getByText('Welcome to Dashboard');
}
async verifyLoaded(): Promise<void> {
await this.welcomeMessage.waitFor();
}
}
// TypeScript magic: tell the compiler DashboardPage also has NavigationMixin methods
export interface DashboardPage extends NavigationMixin {}
// Runtime magic: actually copy those methods
applyMixins(DashboardPage, [NavigationMixin]);
Step 4: Use in Tests
test('can navigate from dashboard to settings', async ({ dashboardPage }) => {
await dashboardPage.verifyLoaded();
// This method comes from NavigationMixin
await dashboardPage.nav_clickSettings();
await expect(page).toHaveURL(/settings/);
});
When to Migrate
You should consider moving from BasePage to Mixins when:
-
Your BasePage exceeds 300-400 lines
- Sign that too much is crammed into one class
-
You're copying methods between unrelated page objects
- Duplication means you need shared components
-
Page objects inherit methods they never use
- Wasted memory, confusing IntelliSense
-
You avoid adding to BasePage because it's too risky
- Fear of breaking things means it's time to modularize
-
New team members struggle to find methods
- Too much in one place makes navigation difficult
Resources
Original Work (Start Here)
- Ivan Davidov's Article: idavidov.eu/upgrade-playwright-tests-typescript-mixin-design-pattern-guide
- Ivan's Repository: github.com/idavidov13/Mixin-Design-Pattern ⭐ Please star this!
- Connect with Ivan: linkedin.com/in/ivdavidov
Additional Learning
- Composition Over Inheritance - Test Automation (Video): Serenity Dojo on LinkedIn
- TypeScript Handbook - Mixins: typescriptlang.org/docs/handbook/mixins.html
- Gang of Four - Favor Composition Over Inheritance: Classic design pattern principle
My Applications
- BasePage Framework (Inheritance): github.com/StevenG0211/playwright-reference-project
- Mixin Framework (Composition): github.com/StevenG0211/playwright-composition-reference
Final Thoughts
There's no "one true way" to structure a Playwright framework.
The BasePage inheritance pattern works great for simple projects and learning. The mixin composition pattern scales better for complex applications.
Neither is "better"—they solve different problems.
My advice:
- Start with BasePage if you're learning or working on a simple app
- Learn the concepts behind composition and mixins
- Migrate when you feel the pain of inheritance limitations
- Credit those who taught you along the way
Big thanks again to Ivan Davidov for introducing this pattern to the Playwright community. I learned a lot from his work, and that's exactly why I want to share it with more people through this post and my extended framework.
Questions? Find me on TikTok @angry_tester or connect on LinkedIn.
And seriously—go star Ivan's repo: github.com/idavidov13/Mixin-Design-Pattern
angry docs — QA concepts without the fluff
