Back to Test Automation Strategy
Q1Question 1 of 2

Explaining a mature Playwright framework architecture

Key Concepts
  • Thin test specs express intent in business terms, calling Page Objects and helpers rather than holding raw selectors or waits
  • Page Objects encapsulate each screen with stable locators and intention-revealing methods for single-point maintenance
  • Fixtures provide dependency injection and lifecycle: constructing page objects, seeding data, and providing authenticated sessions
  • API helpers create preconditions fast through the backend and verify UI actions at the source of truth
  • Environment config and test data are externalized so the same suite runs across dev, QA, staging, and production
  • The interviewer asks this to distinguish someone who designed a framework with layers and reuse from someone who wrote one-off scripts

Definition

A mature framework layers specs, Page Objects, fixtures, utilities, API helpers, externalized data and environment config, central auth via storage state, rich reporting, and CI/CD integration, and the interviewer asks for it to tell whether you designed a framework or just wrote scripts.
  • Test specs – Thin files that express what is being verified in business terms, calling lower layers rather than holding raw selectors.
  • Page Objects – Classes encapsulating each screen's locators and intention-revealing methods, enabling single-point locator maintenance.
  • Fixtures – Playwright's dependency-injection layer that constructs page objects, provides authenticated sessions, and manages setup and teardown.
  • API helpers – A layer that talks to the backend to create preconditions fast and verify UI actions at the source of truth.
  • Config and data – Externalized environment settings and test data so the suite runs across any environment by changing settings, not code.
  • Auth handling – A setup project logs in once, saves storage state, and all tests start already authenticated.
  • Reporting and CI/CD – Rich HTML or Allure output with screenshots, videos, and traces, wired into the pipeline with failure notifications.

Think of the National Bank of India's automation as a well-run office rather than a drawer of loose notes. The test specs are the task lists that say what to check. The Page Objects are the specialist clerks who know each screen. The fixtures are the office setup that logs you in and prepares your desk. The API helpers are the fast back-channel to create data directly. Config and data are the files saying which branch and customers to use. Auth is handled once at the front desk and a pass is reused. Reports are the end-of-day summaries, and CI/CD is the schedule that runs everything automatically.

Understanding the Concepts

What are the layers of a mature framework?

A mature framework is not a pile of scripts but an architecture with clear layers, each owning one responsibility. The layers from top to bottom are:

  1. Test specs : express what is being verified
  2. Page Objects : encapsulate how to interact with each screen
  3. Fixtures : manage lifecycle, injection, and setup
  4. Utilities : hold cross-cutting, UI-agnostic helpers
  5. API helpers : create preconditions and verify through the backend
  6. Test data : externalized inputs in JSON, CSV, or factories
  7. Configuration : environment settings, timeouts, reporters
  8. Reporting : generated output with diagnostics
TEXT
framework/
  tests/          Thin specs expressing intent
  pages/          Page Objects with locators and methods
  components/     Shared UI component objects
  fixtures/       Custom fixtures for DI and setup
  utils/          Formatting, random data, custom assertions
  api/            Backend helpers for preconditions
  data/           JSON/CSV test data, factories
  config/         playwright.config.ts, .env files
  reports/        Generated HTML/Allure (gitignored)
  .auth/          Saved storage state (gitignored)

How do test specs stay thin and readable?

Specs call Page Objects and helpers instead of containing raw selectors or waits. A non-author can understand the scenario at a glance.

TYPESCRIPT
// tests/transfer.spec.ts
import { test, expect } from '@playwright/test';
import { TransferPage } from '../pages/TransferPage';

test('Transfer 5000 from savings to current', async ({ page }) => {
  const transfer = new TransferPage(page);
  await transfer.navigate();
  await transfer.selectFromAccount('Savings ACC-001');
  await transfer.enterToAccount('ACC-002');
  await transfer.enterAmount('5000');
  await transfer.submit();

  await expect(transfer.successMessage).toBeVisible();
  await expect(transfer.successMessage).toContainText('5,000.00');
});

How do Page Objects encapsulate interaction?

Each screen is represented by a class that holds stable locators and exposes intention-revealing methods. When the UI changes, you fix one Page Object instead of editing many specs.

TYPESCRIPT
// pages/TransferPage.ts
import { Page, Locator } from '@playwright/test';

export class TransferPage {
  readonly fromAccount: Locator;
  readonly toAccount: Locator;
  readonly amount: Locator;
  readonly submitButton: Locator;
  readonly successMessage: Locator;

  constructor(private page: Page) {
    this.fromAccount = page.getByLabel('From Account');
    this.toAccount = page.getByLabel('To Account');
    this.amount = page.getByLabel('Amount');
    this.submitButton = page.getByRole('button', { name: 'Transfer' });
    this.successMessage = page.getByText('Transfer Successful');
  }

  async navigate() {
    await this.page.goto('/transfer');
  }

  async selectFromAccount(name: string) {
    await this.fromAccount.selectOption(name);
  }

  async enterToAccount(account: string) {
    await this.toAccount.fill(account);
  }

  async enterAmount(amount: string) {
    await this.amount.fill(amount);
  }

  async submit() {
    await this.submitButton.click();
  }
}

How do fixtures provide dependency injection?

Fixtures construct Page Objects, provide authenticated sessions, and seed data, so specs declare what they need and get it automatically.

TYPESCRIPT
// fixtures/bankFixtures.ts
import { test as base } from '@playwright/test';
import { TransferPage } from '../pages/TransferPage';
import { LoginPage } from '../pages/LoginPage';
import { ApiHelper } from '../api/ApiHelper';

type BankFixtures = {
  transferPage: TransferPage;
  loginPage: LoginPage;
  api: ApiHelper;
};

export const test = base.extend<BankFixtures>({
  transferPage: async ({ page }, use) => {
    const transferPage = new TransferPage(page);
    await use(transferPage);
  },
  loginPage: async ({ page }, use) => {
    await use(new LoginPage(page));
  },
  api: async ({ request }, use) => {
    await use(new ApiHelper(request));
  },
});

export { expect } from '@playwright/test';
TYPESCRIPT
// Using the fixture in a spec
import { test, expect } from '../fixtures/bankFixtures';

test('Transfer via fixture', async ({ transferPage, api }) => {
  // API helper seeds the account
  await api.seedAccount('ACC-001', 50000);
  await transferPage.navigate();
  await transferPage.enterAmount('5000');
  await transferPage.submit();
  await expect(transferPage.successMessage).toBeVisible();
  // API helper verifies the balance
  const balance = await api.getBalance('ACC-001');
  expect(balance).toBe(45000);
});

Why are API helpers a first-class layer?

API helpers talk directly to the backend to create preconditions fast and reliably. They also provide source-of-truth verification of UI actions.

TYPESCRIPT
// api/ApiHelper.ts
import { APIRequestContext } from '@playwright/test';

export class ApiHelper {
  constructor(private request: APIRequestContext) {}

  async seedAccount(id: string, balance: number) {
    await this.request.post('/api/test/accounts', {
      data: { id, balance },
    });
  }

  async getBalance(id: string): Promise<number> {
    const res = await this.request.get(`/api/accounts/${id}/balance`);
    const json = await res.json();
    return json.balance;
  }

  async createTransaction(from: string, to: string, amount: number) {
    await this.request.post('/api/transfer', {
      data: { fromAccount: from, toAccount: to, amount },
    });
  }
}

How do you handle authentication centrally?

A setup project logs in once, saves storage state, and all other tests start already authenticated. This is dramatically faster and less flaky than logging in per test.

TYPESCRIPT
// tests/auth.setup.ts
import { test as setup } from '@playwright/test';

setup('Authenticate as teller', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Username').fill(process.env.TEST_USER!);
  await page.getByLabel('Password').fill(process.env.TEST_PASS!);
  await page.getByRole('button', { name: 'Sign In' }).click();
  await page.context().storageState({ path: '.auth/teller.json' });
});

How do you externalize configuration and data?

The same suite runs against any environment by changing settings, not code.

TYPESCRIPT
// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  use: {
    baseURL: process.env.BASE_URL || 'http://localhost:3000',
    trace: 'retain-on-failure',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
  projects: [
    { name: 'setup', testMatch: /.*\.setup\.ts/ },
    {
      name: 'chromium',
      dependencies: ['setup'],
      use: { storageState: '.auth/teller.json' },
    },
  ],
  reporter: [['html'], ['allure-playwright']],
});

Why does the interviewer ask for this?

The interviewer asks to distinguish someone who designed a framework from someone who wrote scripts. Scripts duplicate selectors, break en masse on UI changes, hard-code data and credentials, cannot run reliably in parallel, and give poor failure diagnostics.

A designed framework shows you understand:

| Principle | Framework Layer |
|---|---|
| Separation of concerns | Specs, pages, fixtures, API |
| Reuse | Page Objects, component objects, utils |
| Isolation | Fixtures, per-worker data |
| Configurability | Externalized config and data |
| Operability | Reporting, CI/CD, traces |

What are common interview follow-up questions?

  • "How do you share a page object across teams?" Publish it as an internal npm package or monorepo package.
  • "How do you handle test data cleanup?" Use API helpers in afterEach or afterAll to clean data, or use isolated per-test data that does not need cleanup.
  • "How do you run against multiple environments?" Pass the BASE_URL and secrets as environment variables per CI job.

Visual Overview

DIAGRAM
  Framework Architecture Layers
  ─────────────────────────────

  ┌──────────────────────────────────────┐
  │  TEST SPECS                          │  What to verify
  │  tests/transfer.spec.ts              │  (thin, readable)
  └──────────────┬───────────────────────┘
                 │ calls
  ┌──────────────▼───────────────────────┐
  │  PAGE OBJECTS                        │  How to interact
  │  pages/TransferPage.ts               │  (locators + methods)
  │  components/HeaderComponent.ts       │
  └──────────────┬───────────────────────┘
                 │ injected by
  ┌──────────────▼───────────────────────┐
  │  FIXTURES                            │  Setup + injection
  │  fixtures/bankFixtures.ts            │  (auth, data, POs)
  └──────────────┬───────────────────────┘
                 │ uses
  ┌──────────────▼───────────────────────┐
  │  API HELPERS + UTILS + DATA          │
  │  api/ApiHelper.ts                    │  Fast preconditions
  │  utils/moneyFormatter.ts             │  Shared logic
  │  data/accounts.json                  │  Test inputs
  └──────────────┬───────────────────────┘
                 │ configured by
  ┌──────────────▼───────────────────────┐
  │  CONFIG + AUTH + REPORTING           │
  │  playwright.config.ts                │  Env settings
  │  .auth/teller.json                   │  Storage state
  │  reports/                            │  HTML + Allure
  └──────────────────────────────────────┘

Key Takeaway

A mature Playwright framework is a layered architecture, not a pile of scripts. At the top are thin test specs that express what is being verified in business terms. Below them are Page Objects that encapsulate each screen with stable locators and intention-revealing methods, so a UI change is a single edit. Fixtures provide dependency injection: they construct page objects, provide authenticated sessions, and seed data. API helpers create preconditions fast through the backend and verify UI actions at the source of truth. Test data and environment configuration are externalized so the suite runs across any environment by changing settings, not code. Authentication is centralized through storage state. And reporting produces actionable diagnostics wired into CI/CD.

The interviewer asks this to tell whether you designed a framework or only wrote scripts. Scripts duplicate selectors, break en masse on UI changes, hard-code credentials, and give poor diagnostics. A designed framework shows separation of concerns, reuse, isolation, configurability, and operability.

At the National Bank of India I built exactly this. I kept specs thin, pushed all interaction into Page Objects so a screen redesign touched one file, used fixtures to inject page objects and authenticated sessions, added an API-helper layer to seed accounts and verify balances, externalized config so the suite ran across four environments, and wired HTML and Allure reporting into the pipeline.

Remember one sentence: a mature framework separates what to verify from how to interact, injects dependencies through fixtures, creates preconditions through API helpers, and externalizes everything environment-specific, which is what the interviewer is really asking you to prove.

---

Share
Use ← → arrow keys to navigate