Adlerqa

Playwright has quickly become one of the most powerful end-to-end automation frameworks in the QA Automation space.

Modern automation frameworks are rarely “just UI tests.

A realistic enterprise test suite needs:

  • UI automation
  • API validations
  • Database assertions
  • Cross-layer flows (UI action → API response → DB verification)

Playwright is incredibly powerful because its fixture system allows all three layers—UI, API, and DB—to be integrated cleanly inside a single test runner.

Understanding Playwright Fixtures

Fixtures in Playwright are reusable setup/teardown utilities that help tests stay clean and isolated.

They can:

  • Launch a browser
  • Authenticate a user
  • Provide test data
  • Create database connections
  • Boot APIs
  • Provide page objects
  • Manage any shared state safely

Fixtures run before your test starts and clean up after your test completes.

Built-in vs Custom Fixtures

Built-in (from @playwright/test)

  • browser
  • context
  • page
  • request
  • test (test runner itself)

Custom Fixtures

You can extend Playwright’s test runner to include your custom utilities, such as:

  • Authenticated context
  • API clients
  • Database connection
  • Page Object Manager
  • Fake test data
  • Feature toggles
  • Multi-tenant test config

Fixtures make your framework professional and enterprise-grade.

UI Fixtures (Browser, Page, Page Objects)

Let’s extend Playwright with POM support.

fixtures/uiFixture.ts

  
import { test as base } from '@playwright/test';
import { HomePage } from '../pages/HomePage';
import { LoginPage } from '../pages/LoginPage';

export const test = base.extend({
  homePage: async ({ page }, use) => {
    await use(new HomePage(page));
  },

  loginPage: async ({ page }, use) => {
    await use(new LoginPage(page));
  }
});

export { expect } from '@playwright/test';
  

➡ Provides page objects as fixtures

➡ Every test gets fresh, isolated POM instances

API Fixtures (Authenticated API Client)

Playwright has a built-in request object.
We can create an authenticated API client fixture.

fixtures/apiFixture.ts

  
import { test as base, APIRequestContext, request } from '@playwright/test';

type ApiFixtures = {
  apiClient: APIRequestContext;
  generateToken: () => Promise;
};

export const test = base.extend({
  generateToken: async ({}, use) => {
    const getToken = async () => {
      const req = await request.newContext({
        baseURL: 'https://api.example.com'
      });

      const response = await req.post('/auth/token', {
        data: {
          clientId: process.env.CLIENT_ID,
          clientSecret: process.env.CLIENT_SECRET,
          scope: 'read write'
        }
      });

      const json = await response.json();
      return json.access_token;
    };

    await use(getToken);
  },

  apiClient: async ({ generateToken, request }, use) => {
    const token = await generateToken();

    const client = await request.newContext({
      baseURL: 'https://api.example.com',
      extraHTTPHeaders: {
        Authorization: `Bearer ${token}`,
        'Content-Type': 'application/json'
      }
    });

    await use(client);
    await client.dispose();
  }
});
  

This provides:

  • Reusable authenticated API client with fresh tokens
  • Already configured baseURL
  • Can be accessed across all tests

DB Fixtures (SQL database validation)

There are multiple libraries for sql connection available in the npm. We can use tedious/ mssql based on the OS platform we run.

fixtures/dbFixture.ts

  
import { test as base } from '@playwright/test';
import { Connection, Request } from 'tedious';
import dbConfig from '../dbConfig';

export const test = base.extend({
  db: async ({}, use) => {
    const connection = new Connection(dbConfig);

    await new Promise((resolve, reject) => {
      connection.on('connect', err => err ? reject(err) : resolve(true));
      connection.connect();
    });

    const dbClient = {
      query: (sql: string) =>
        new Promise((resolve, reject) => {
          const rows = [];
          const request = new Request(sql, err => err && reject(err));

          request.on('row', cols => {
            const row = {};
            cols.forEach(col => (row[col.metadata.colName] = col.value));
            rows.push(row);
          });

          request.on('requestCompleted', () => resolve(rows));
          connection.execSql(request);
        })
    };

    await use(dbClient);
    connection.close();
  }
});
  

This fixture returns a DB client with:

  
db.query("SELECT * FROM Users")
  

Combined Fixture: UI + API + DB in One Extended Test

Single unified test context: All UI, API, and DB utilities come from one fixture, making tests cleaner and easier to write.

No duplicate setup: Login, token generation, DB connection, page objects, and clients are initialized once and reused everywhere.

Consistent cross-layer flow: Lets you validate a scenario end-to-end — UI action → API response → DB state — using shared objects.

Centralized maintenance: Any change (endpoint URLs, DB config, login logic) is updated in just one fixture and reflected across all tests.

  
import base from '@playwright/test';
import { test as ui } from './uiFixture';
import { test as api } from './apiFixture';
import { test as db } from './dbFixture';

export const test = mergeTests(ui, api, db);
  

This gives a single test object with:

  • page, browser, context
  • homePage, loginPage
  • apiClient
  • db

All injected automatically.

What This Means for Your Framework

Playwright’s fixture system is powerful enough to replace custom frameworks written in:

  • Selenium + TestNG
  • RestAssured
  • JDBC
  • Third-party mocking tools

Because with fixtures you get:

  • Flexible dependency injection
  • Isolated test environment
  • Clean abstraction layers
  • Full E2E support

Using integrated UI + API + DB fixtures and combined fixtures creates a production-grade, enterprise-ready automation framework—without needing extra tools or complex architecture.