# Upgrade Playwright Tests: TypeScript Mixin Design Pattern Guide

In the world of test automation, especially with a tool as powerful as Playwright, we often face a classic architectural challenge - the `BasePage` object. It starts small, but soon it grows into a monolithic "God object" containing methods for every shared component across the application (navigation, footers, modals).

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759128609231/08d129f4-185c-4901-9215-0a690cabba90.png align="center")

This leads to deep and rigid inheritance chains. An `ArticlePage` might inherit from `NavPage`, which inherits from `BasePage`. This is not only complex but also violates the single responsibility principle.

What if we could build our Page Objects with components just like we build modern web apps? This is where the **Mixin Design Pattern** comes in, favoring flexible **composition over rigid inheritance**.

---

### What's the Problem with Inheritance?

Traditional inheritance (`class A extends B`) is powerful, but it has limits. A class can only extend **one** other class.

So, what happens when your `ArticlePage` needs navigation *and* table-handling logic, and your `HomePage` needs navigation *and* table logic? You can't `extend NavPage, TablePage`. This forces you to either cram everything into a single `BasePage` or create convoluted inheritance chains.

---

### The Mixin Solution: Composition

The **Mixin Design Pattern** lets you create reusable "feature packs". They are small classes focused on a single piece of functionality (like navigation or table handling). You can then "mix" these feature packs into any Page Object that needs them.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759128683543/d93f33ed-e81f-44e2-99af-2ba5b93386d3.png align="center")

This approach allows you to:

* **Share functionality** across unrelated classes.
    
* **Avoid deep inheritance chains**, keeping your code flat and manageable.
    
* **Keep Page Objects clean** and focused on their primary purpose.
    

In modern front-end development, which is heavily component-based, our testing frameworks should evolve too. By using the Mixin Design Pattern, we can build a more modular, reusable, and maintainable test suite.

---

### 🚀 Implementing Mixins in Playwright with TypeScript

Let's walk through a practical example of adding shared navigation functionality to a `HomePage` object without using inheritance. All examples are made for tha app that I am always using - [Conduit](https://conduit.bondaracademy.com/) (huge shoutouts to mr. Artem Bondar) and there is a complete [Repository](https://github.com/idavidov13/Mixin-Design-Pattern).

#### Step 1: The `applyMixins` Helper Function

TypeScript doesn't have a native `mixin` keyword. So, first, we need a small utility function that does the heavy lifting. This function copies the properties and methods from our "feature packs" to our target class.

Think of this as the engine of our pattern.

TypeScript

```typescript
// helpers/mixin.ts

/**
 * Mixin function to combine multiple base classes into one.
 * This function copies all properties and methods from source classes to the target class.
 *
 * @param derivedCtor - The target class that will receive the mixed-in functionality.
 * @param constructors - Array of source classes to mix in.
 */
export function applyMixins(derivedCtor: any, constructors: any[]): void {
    constructors.forEach((baseCtor) => {
        // Get all property names from the source class prototype
        Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
            // Skip constructor to avoid conflicts
            if (name !== 'constructor') {
                // Copy the property descriptor (including getters, setters, methods)
                Object.defineProperty(
                    derivedCtor.prototype,
                    name,
                    Object.getOwnPropertyDescriptor(baseCtor.prototype, name) ||
                        Object.create(null)
                );
            }
        });
    });
}
```

#### Step 2: Create the "Feature Pack" - `NavPage`

Next, we create our mixin. In this case, it's a `NavPage` class that encapsulates all locators and methods for interacting with the website's main navigation bar, a component present on almost every page.

This class is our reusable piece of code.

TypeScript

```typescript
// pages/clientSite/baseClasses/navPage.ts

import { Page, Locator, expect } from '@playwright/test';

/**
 * This is the page object for the Navigation functionality.
 */
export class NavPage {
    constructor(protected page: Page) {}

    // ... Locators for navBar, homePageLink, settingsButton, etc.

    get signInNavigationLink(): Locator {
        return this.page.getByRole('link', { name: 'Sign in' });
    }
    get emailInput(): Locator {
        return this.page.getByRole('textbox', { name: 'Email' });
    }
    get passwordInput(): Locator {
        return this.page.getByRole('textbox', { name: 'Password' });
    }
    get signInButton(): Locator {
        return this.page.getByRole('button', { name: 'Sign in' });
    }
    
    /**
     * Logs in the user.
     */
    async logIn(email: string, password: string): Promise<void> {
        await this.navigateToSignInPage();
        await this.emailInput.fill(email);
        await this.passwordInput.fill(password);
        await this.signInButton.click();
        await expect(
            this.page.getByRole('navigation').getByText(process.env.USER_NAME!)
        ).toBeVisible();
    }

    // ... other methods like navigateToSignInPage(), logOut(), etc.
}
```

#### Step 3: Compose the `HomePage` with the `NavPage` Mixin

Now for the magic. We create our `HomePage` object, which is responsible only for elements unique to the home page. Then, we use our `applyMixins` helper to blend in the `NavPage` functionality.

TypeScript

```typescript
// pages/clientSite/homePage.ts

import { Page, Locator, expect } from '@playwright/test';
import { applyMixins } from '../../helpers/mixin';
import { NavPage } from './baseClasses/navPage';

/**
 * This is the page object for the Home Page.
 */
export class HomePage {
    constructor(protected page: Page) {}

    get homeBanner(): Locator {
        return this.page.getByRole('heading', { name: 'conduit' });
    }
    get yourFeedBtn(): Locator {
        return this.page.getByText('Your Feed');
    }

    /**
     * Navigates to the home page as Guest.
     */
    async navigateToHomePageGuest(): Promise<void> {
        await this.page.goto(process.env.URL as string);
        await expect(this.homeBanner).toBeVisible();
    }
}

/**
 * Interface declaration that tells TypeScript that HomePage
 * also has all the methods and properties from NavPage.
 * This enables IntelliSense and type checking.
 */
export interface HomePage extends NavPage {}

/**
 * Apply the mixin at runtime.
 * This actually copies all NavPage methods to HomePage's prototype.
 */
applyMixins(HomePage, [NavPage]);
```

Notice two key parts here:

1. `export interface HomePage extends NavPage {}`: This is crucial for **TypeScript's type-checking**. It tells the compiler, "Trust me, an instance of `HomePage` will also have all the methods of `NavPage`", giving us that sweet, sweet IntelliSense autocompletion.
    
2. `applyMixins(HomePage, [NavPage])`: This is the runtime execution. It takes all the methods from `NavPage` and attaches them to `HomePage`. If you want to attach methods from more than one `baseClass`, you can list them in the array.
    

#### Step 4: Use the Composed Page Object in a Test

Finally, let's see it in action. In our test file, we can instantiate `HomePage` and call methods from `NavPage` directly on it, like `logIn()`.

TypeScript

```typescript
// tests/auth.setup.ts

import { test as setup } from '../fixtures/pom/test-options';

setup('auth user', async ({ homePage, page }) => {
    await setup.step('create logged in user session', async () => {
        await homePage.navigateToHomePageGuest();

        // Calling a method from NavPage on a HomePage instance!
        await homePage.logIn(process.env.EMAIL!, process.env.PASSWORD!);

        await page.context().storageState({ path: '.auth/userSession.json' });
    });
});
```

It just works! Our `HomePage` is clean and focused, yet it has all the power of the navigation functionality when needed.

---

### A Note on Potential Downsides

While powerful, this pattern has trade-offs. The main one is the potential for **method name collisions**. If you mix in two classes that both define a method called `clickSubmit()`, the last one applied will overwrite the previous ones. Additionally, it can sometimes be less obvious where a method originates compared to a clear `extends` chain.

---

### Summary

By embracing a **composition over inheritance** mindset with the Mixin pattern, you can build a far more flexible, modular, and maintainable Playwright test automation framework.

Your Page Objects become cleaner, your code becomes more reusable, and you avoid the architectural headache of the monolithic `BasePage`. This approach better mirrors modern, component-based web development and leads to a more robust and scalable test suite.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1759128652507/59622f00-209c-4425-b1c7-eae34f9eef84.png align="center")
