FRAMEWORK DESIGNINTERMEDIATE

Create Custom Helper and Wrapper Functions for Playwright Locator Actions

Build a reusable Playwright ActionHelper with structured logging, test-step reporting, sensitive-data masking, and screenshots on failure — and learn when action wrappers add value and when they become overengineering.

iff Solution Academy August 5, 2026 18 min read Updated August 5, 2026
Playwright Wrapper Functions ActionHelper Framework Design TypeScript Logging Fixtures Enterprise Framework

Introduction

A Playwright test often begins with simple, readable actions — fill an email, fill a password, click a sign-in button. That code is already clean and reliable, because Playwright locators auto-wait and perform actionability checks before interacting.

typescript
await page.getByLabel('Email').fill('user@example.com');
await page.getByLabel('Password').fill('Password123');
await page.getByRole('button', { name: 'Sign in' }).click();

As a framework grows, however, teams need extra behaviour around those actions: consistent logging, better failure messages, screenshots when actions fail, standard timeout handling, sensitive-data masking, test-step reporting, action timing, and easier troubleshooting in CI/CD. That is where custom action helpers earn their place.

typescript
await actions.fill(emailInput, user.email, 'Email address');
await actions.fill(passwordInput, user.password, 'Password', { sensitive: true });
await actions.click(signInButton, 'Sign-in button');

The wrapper does not replace Playwright — it adds framework-level behaviour around the native Locator API. In this tutorial you will build a reusable ActionHelper class and learn exactly when wrappers improve a framework and when they become unnecessary overengineering.

What Is an Action Wrapper?

An action wrapper is a reusable function placed around a native Playwright operation. Internally it still calls locator.click(), but it can also add a readable action name, logging, error enrichment, screenshots, reporting, masking, and performance measurements.

typescript
// Native Playwright
await locator.click();

// Wrapped action
await actions.click(locator, 'Submit order button');

The golden rule: a wrapper should enhance the native action without changing its expected behaviour.

Why Create Custom Action Helpers?

Suppose your framework contains 2,000 tests, and engineers repeatedly call click(), fill(), check(), selectOption() and hover(). Now imagine the team wants every failed action to include the action type, a business-friendly element name, the current URL, a screenshot, and the original Playwright error.

Without a wrapper, every Page Object needs its own try/catch logic. With an action helper, this behaviour is implemented once and inherited everywhere.

The Wrong Reason to Build Wrappers

Do not build wrappers only to rename Playwright methods. The example below adds no capability — it just forces engineers to learn a second API.

typescript
// No value added
async clickElement(locator: Locator): Promise<void> {
  await locator.click();
}

A wrapper should solve a real framework problem: diagnostics, reporting, masking, or standardisation.

Recommended Project Structure

text
playwright-framework/
│
├── actions/
│   ├── ActionHelper.ts
│   ├── ActionOptions.ts
│   └── ActionError.ts
│
├── pages/
│   ├── LoginPage.ts
│   ├── ProductsPage.ts
│   └── CheckoutPage.ts
│
├── components/
├── fixtures/
│   └── actions.fixture.ts
│
├── utils/
│   └── Logger.ts
│
├── tests/
├── playwright.config.ts
└── package.json

This keeps interaction infrastructure separate from Page Objects and tests.

Step 1: Create a Logger

typescript
// utils/Logger.ts
export type LogContext = Record<string, string | number | boolean | undefined>;

export class Logger {
  info(message: string, context?: LogContext): void {
    console.log(
      JSON.stringify({
        level: 'INFO',
        timestamp: new Date().toISOString(),
        message,
        ...context
      })
    );
  }

  error(message: string, context?: LogContext): void {
    console.error(
      JSON.stringify({
        level: 'ERROR',
        timestamp: new Date().toISOString(),
        message,
        ...context
      })
    );
  }
}

This writes structured JSON logs. A production framework can swap in Winston, Pino, or a cloud logger — the action helper depends on the logger interface instead of scattering console.log() calls across the codebase.

Step 2: Define Common Action Options

typescript
// actions/ActionOptions.ts
export interface ActionOptions {
  timeout?: number;
  force?: boolean;
  sensitive?: boolean;
  screenshotOnFailure?: boolean;
}

These settings let callers customise individual interactions without creating a separate method for every variation.

typescript
await actions.click(button, 'Continue button', { timeout: 15_000 });

await actions.fill(password, user.password, 'Password', { sensitive: true });

Step 3: Create a Custom Action Error

typescript
// actions/ActionError.ts
export class ActionError extends Error {
  constructor(
    message: string,
    readonly action: string,
    readonly elementName: string,
    readonly pageUrl: string,
    options?: ErrorOptions
  ) {
    super(message, options);
    this.name = 'ActionError';
  }
}

When a test fails, the report can now show what action failed, which logical element was involved, which page was open, and the original Playwright error.

Step 4: Build the ActionHelper Class

typescript
// actions/ActionHelper.ts
import { FilePayload, Locator, Page } from '@playwright/test';
import { Logger } from '../utils/Logger';
import { ActionError } from './ActionError';
import { ActionOptions } from './ActionOptions';

type InputFile = string | string[] | FilePayload | FilePayload[];

export class ActionHelper {
  constructor(
    private readonly page: Page,
    private readonly logger: Logger
  ) {}

  async click(locator: Locator, elementName: string, options: ActionOptions = {}): Promise<void> {
    await this.execute('click', elementName, async () => {
      await locator.click({ timeout: options.timeout, force: options.force });
    }, options);
  }

  async fill(locator: Locator, value: string, elementName: string, options: ActionOptions = {}): Promise<void> {
    const loggedValue = options.sensitive ? '[MASKED]' : value;

    await this.execute('fill', elementName, async () => {
      this.logger.info('Preparing input value', { element: elementName, value: loggedValue });
      await locator.fill(value, { timeout: options.timeout, force: options.force });
    }, options);
  }

  async clear(locator: Locator, elementName: string, options: ActionOptions = {}): Promise<void> {
    await this.execute('clear', elementName, async () => {
      await locator.clear({ timeout: options.timeout, force: options.force });
    }, options);
  }

  async check(locator: Locator, elementName: string, options: ActionOptions = {}): Promise<void> {
    await this.execute('check', elementName, async () => {
      await locator.check({ timeout: options.timeout, force: options.force });
    }, options);
  }

  async uncheck(locator: Locator, elementName: string, options: ActionOptions = {}): Promise<void> {
    await this.execute('uncheck', elementName, async () => {
      await locator.uncheck({ timeout: options.timeout, force: options.force });
    }, options);
  }

  async hover(locator: Locator, elementName: string, options: ActionOptions = {}): Promise<void> {
    await this.execute('hover', elementName, async () => {
      await locator.hover({ timeout: options.timeout, force: options.force });
    }, options);
  }

  async press(locator: Locator, key: string, elementName: string, options: ActionOptions = {}): Promise<void> {
    await this.execute(`press ${key}`, elementName, async () => {
      await locator.press(key, { timeout: options.timeout });
    }, options);
  }

  async uploadFiles(locator: Locator, files: InputFile, elementName: string, options: ActionOptions = {}): Promise<void> {
    await this.execute('upload files', elementName, async () => {
      await locator.setInputFiles(files, { timeout: options.timeout });
    }, options);
  }

  async dragTo(source: Locator, target: Locator, description: string, options: ActionOptions = {}): Promise<void> {
    await this.execute('drag', description, async () => {
      await source.dragTo(target, { timeout: options.timeout, force: options.force });
    }, options);
  }

  async textContent(locator: Locator, elementName: string, options: ActionOptions = {}): Promise<string> {
    return this.execute('read text', elementName, async () => {
      const text = await locator.textContent({ timeout: options.timeout });
      return text?.trim() ?? '';
    }, options);
  }

  async inputValue(locator: Locator, elementName: string, options: ActionOptions = {}): Promise<string> {
    return this.execute('read input value', elementName, async () => {
      return locator.inputValue({ timeout: options.timeout });
    }, options);
  }

  private async execute<T>(
    action: string,
    elementName: string,
    operation: () => Promise<T>,
    options: ActionOptions
  ): Promise<T> {
    const startedAt = Date.now();

    this.logger.info('Locator action started', {
      action,
      element: elementName,
      url: this.page.url()
    });

    try {
      const result = await operation();

      this.logger.info('Locator action completed', {
        action,
        element: elementName,
        durationMs: Date.now() - startedAt
      });

      return result;
    } catch (error) {
      const originalError = error instanceof Error ? error : new Error(String(error));
      const currentUrl = this.page.url();

      this.logger.error('Locator action failed', {
        action,
        element: elementName,
        url: currentUrl,
        durationMs: Date.now() - startedAt,
        error: originalError.message
      });

      if (options.screenshotOnFailure !== false) {
        await this.captureFailureScreenshot(action, elementName);
      }

      throw new ActionError(
        `Failed to ${action} "${elementName}" at ${currentUrl}. Original error: ${originalError.message}`,
        action,
        elementName,
        currentUrl,
        { cause: originalError }
      );
    }
  }

  private async captureFailureScreenshot(action: string, elementName: string): Promise<void> {
    const safeName = `${action}-${elementName}`
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, '-')
      .replace(/^-|-$/g, '');

    try {
      await this.page.screenshot({
        path: `test-results/action-failures/${safeName}-${Date.now()}.png`,
        fullPage: true
      });
    } catch (screenshotError) {
      this.logger.error('Unable to capture action failure screenshot', {
        action,
        element: elementName,
        error: screenshotError instanceof Error ? screenshotError.message : String(screenshotError)
      });
    }
  }
}

This class creates one standard execution path for locator actions. Every operation gets the same start log, completion log, duration measurement, failure log, screenshot behaviour and enriched exception.

Why Pass a Locator Instead of a Selector String?

Your wrapper should accept a Locator, not a string. Passing a Locator preserves Playwright's locator model and lets callers use getByRole(), getByLabel(), getByTestId(), filtering, chaining and component-scoped locators.

typescript
// Good
await actions.click(page.getByRole('button', { name: 'Login' }), 'Login button');

// Less desirable
await actions.click('#login-button', 'Login button');

Using ActionHelper Inside a Page Object

typescript
// pages/LoginPage.ts
import { Locator, Page } from '@playwright/test';
import { ActionHelper } from '../actions/ActionHelper';

export class LoginPage {
  readonly emailInput: Locator;
  readonly passwordInput: Locator;
  readonly signInButton: Locator;
  readonly rememberMeCheckbox: Locator;

  constructor(
    private readonly page: Page,
    private readonly actions: ActionHelper
  ) {
    this.emailInput = page.getByLabel('Email address');
    this.passwordInput = page.getByLabel('Password');
    this.signInButton = page.getByRole('button', { name: 'Sign in' });
    this.rememberMeCheckbox = page.getByRole('checkbox', { name: 'Remember me' });
  }

  async open(): Promise<void> {
    await this.page.goto('/login');
  }

  async login(email: string, password: string): Promise<void> {
    await this.actions.fill(this.emailInput, email, 'Email address');
    await this.actions.fill(this.passwordInput, password, 'Password', { sensitive: true });
    await this.actions.click(this.signInButton, 'Sign-in button');
  }

  async enableRememberMe(): Promise<void> {
    await this.actions.check(this.rememberMeCheckbox, 'Remember-me checkbox');
  }
}

The Page Object stays responsible for page-specific behaviour; the helper stays responsible for cross-framework action behaviour.

Provide ActionHelper Through a Fixture

typescript
// fixtures/actions.fixture.ts
import { test as base } from '@playwright/test';
import { ActionHelper } from '../actions/ActionHelper';
import { LoginPage } from '../pages/LoginPage';
import { Logger } from '../utils/Logger';

type FrameworkFixtures = {
  actions: ActionHelper;
  loginPage: LoginPage;
};

export const test = base.extend<FrameworkFixtures>({
  actions: async ({ page }, use) => {
    const logger = new Logger();
    await use(new ActionHelper(page, logger));
  },

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

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

Fixtures keep dependency initialisation outside test bodies, so a test can use the Page Object immediately.

typescript
import { test, expect } from '../fixtures/test.fixture';

test('registered user can sign in', async ({ loginPage, page }) => {
  await loginPage.open();
  await loginPage.login('automation@example.com', process.env.TEST_PASSWORD!);
  await expect(page).toHaveURL(/dashboard/);
});

Add Playwright Test Steps to Wrapper Actions

Readable reports are another strong reason to wrap actions. Wrap the execution path in test.step() and every action becomes a named report entry.

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

private async execute<T>(
  action: string,
  elementName: string,
  operation: () => Promise<T>,
  options: ActionOptions
): Promise<T> {
  return test.step(`${action}: ${elementName}`, async () => {
    return this.executeWithDiagnostics(action, elementName, operation, options);
  });
}

private async executeWithDiagnostics<T>(
  action: string,
  elementName: string,
  operation: () => Promise<T>,
  options: ActionOptions
): Promise<T> {
  // Logging, operation, screenshots and error handling
}
text
fill: Email address
fill: Password
click: Sign-in button

Attach Screenshots to the Playwright Report

Writing screenshots to a folder works, but attaching them to the current test report is usually more useful. Pass TestInfo into the helper.

typescript
import { Page, TestInfo } from '@playwright/test';

export class ActionHelper {
  constructor(
    private readonly page: Page,
    private readonly logger: Logger,
    private readonly testInfo: TestInfo
  ) {}
}

// fixture
actions: async ({ page }, use, testInfo) => {
  const logger = new Logger();
  await use(new ActionHelper(page, logger, testInfo));
}

// capture + attach
private async captureFailureScreenshot(action: string, elementName: string): Promise<void> {
  const screenshot = await this.page.screenshot({ fullPage: true });

  await this.testInfo.attach(`${action}-${elementName}`, {
    body: screenshot,
    contentType: 'image/png'
  });
}

Specialised Action Wrappers

Sequential Typing

Some applications trigger behaviour on every keypress — search suggestions, masked fields, typeahead controls, debounced validation. Keep this separate from fill() so engineers make an intentional choice.

typescript
async typeSequentially(
  locator: Locator,
  value: string,
  elementName: string,
  delay = 50,
  options: ActionOptions = {}
): Promise<void> {
  await this.execute('type sequentially', elementName, async () => {
    await locator.pressSequentially(value, { delay, timeout: options.timeout });
  }, options);
}

await actions.typeSequentially(searchInput, 'Playwright', 'Product search', 75);

Dropdown Selection

typescript
async selectByValue(locator: Locator, value: string, elementName: string, options: ActionOptions = {}): Promise<void> {
  await this.execute(`select value "${value}"`, elementName, async () => {
    await locator.selectOption({ value }, { timeout: options.timeout, force: options.force });
  }, options);
}

async selectByLabel(locator: Locator, label: string, elementName: string, options: ActionOptions = {}): Promise<void> {
  await this.execute(`select label "${label}"`, elementName, async () => {
    await locator.selectOption({ label }, { timeout: options.timeout, force: options.force });
  }, options);
}

await actions.selectByLabel(countryDropdown, 'United States', 'Country dropdown');

Checkboxes and Radio Buttons

typescript
async setChecked(locator: Locator, checked: boolean, elementName: string, options: ActionOptions = {}): Promise<void> {
  await this.execute(checked ? 'check' : 'uncheck', elementName, async () => {
    await locator.setChecked(checked, { timeout: options.timeout, force: options.force });
  }, options);
}

await actions.setChecked(termsCheckbox, true, 'Terms and conditions');

File Uploads

typescript
async uploadFile(locator: Locator, filePath: string, elementName: string, options: ActionOptions = {}): Promise<void> {
  await this.execute('upload file', elementName, async () => {
    await locator.setInputFiles(filePath, { timeout: options.timeout });
  }, options);
}

await actions.uploadFile(resumeInput, 'data/files/resume.pdf', 'Resume upload');

await actions.uploadFiles(
  attachmentInput,
  ['data/files/report.pdf', 'data/files/results.csv'],
  'Supporting documents'
);

Reading Text

typescript
const confirmationNumber = await actions.getText(
  confirmationNumberLabel,
  'Confirmation number'
);

Do not replace web-first assertions with immediate text extraction unless you actually need the value. For validation prefer expect(statusMessage).toHaveText('Order submitted') — locator assertions retry until the condition succeeds or the assertion times out.

Wrapper Anti-Patterns

Do Not Put Assertions Inside Every Action

typescript
// Too aggressive
async click(locator: Locator): Promise<void> {
  await expect(locator).toBeVisible();
  await expect(locator).toBeEnabled();
  await locator.click();
}

This duplicates waiting, slows tests, changes semantics from native Playwright, and complicates failure output. Use explicit assertions only when state is a business expectation: expect(submitButton).toBeEnabled() followed by actions.click(submitButton, 'Submit button').

Do Not Add Automatic Retries Around Every Click

typescript
// Hides real defects
for (let attempt = 1; attempt <= 3; attempt++) {
  try {
    await locator.click();
    break;
  } catch {
    // Silently try again
  }
}

Blind retries can hide product defects, duplicate elements, overlays, incorrect locators, navigation problems and race conditions. Add an explicit retry only when you understand the specific transient condition.

Do Not Add Fixed Waits to Wrappers

typescript
// Bad
async click(locator: Locator): Promise<void> {
  await this.page.waitForTimeout(2000);
  await locator.click();
}

// Good — wait for the real condition
await expect(loadingSpinner).toBeHidden();
await actions.click(continueButton, 'Continue button');

Avoid Automatically Using force: true

A forced click bypasses actionability checks and may perform an interaction a real user could not. Use force only for a documented exception, never as the framework default.

A Simpler Functional Alternative

A class is not mandatory. Small projects can use standalone functions when they do not need dependency injection, shared logging, screenshots or complex configuration.

typescript
import { Locator } from '@playwright/test';

export async function click(locator: Locator, elementName: string): Promise<void> {
  try {
    await locator.click();
  } catch (error) {
    throw new Error(
      `Unable to click "${elementName}": ${error instanceof Error ? error.message : String(error)}`
    );
  }
}

export async function fill(locator: Locator, value: string, elementName: string): Promise<void> {
  try {
    await locator.fill(value);
  } catch (error) {
    throw new Error(
      `Unable to fill "${elementName}": ${error instanceof Error ? error.message : String(error)}`
    );
  }
}

Common Wrapper Design Mistakes

  • Rebuilding the entire Playwright API — hundreds of wrappers become a parallel library your team must maintain.
  • Accepting CSS or XPath strings everywhere instead of Locator objects.
  • Hiding every error behind a generic "Element action failed" message.
  • Logging passwords, tokens, API keys or payment data — always use { sensitive: true }.
  • Forcing actions by default with force: true.
  • Taking full-page screenshots after every action, generating enormous artifact volumes.
  • Mixing business logic (loginToApplication) into a generic action helper.

When Should You Use Action Wrappers?

  • You need consistent structured logs across a large suite.
  • CI failure messages must include business context and screenshots.
  • Reports should contain named steps for non-technical stakeholders.
  • Sensitive data must be masked framework-wide.
  • You want action performance metrics and framework-wide timeout policies.

Avoid them when the framework is small, native methods are already clear, the wrapper adds no behaviour, engineers need full access to Playwright options, or the abstraction makes debugging harder. Writing loginButton.click() directly is perfectly valid — do not add abstraction merely to make the framework look advanced.

Recommended Enterprise Design

text
Test
  ↓
Page Object or Component Object
  ↓
Action Helper
  ↓
Playwright Locator API
  ↓
Browser
  • Test: describes and validates the business scenario.
  • Page Object: models a complete page and its business actions.
  • Component Object: models a reusable section of the interface.
  • Action Helper: adds logging, diagnostics, masking and reporting.
  • Playwright Locator: performs native auto-waiting and browser interaction.

Keep locator factories separate from action helpers: a locator helper returns Locators, an action helper executes interactions, and Page Objects define business behaviour.

Complete Example

typescript
// fixtures/test.fixture.ts
import { test as base } from '@playwright/test';
import { ActionHelper } from '../actions/ActionHelper';
import { LoginPage } from '../pages/LoginPage';
import { Logger } from '../utils/Logger';

type Fixtures = {
  actions: ActionHelper;
  loginPage: LoginPage;
};

export const test = base.extend<Fixtures>({
  actions: async ({ page }, use, testInfo) => {
    const logger = new Logger();
    await use(new ActionHelper(page, logger, testInfo));
  },

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

export { expect } from '@playwright/test';
typescript
// tests/login.spec.ts
import { test, expect } from '../fixtures/test.fixture';

test('valid user can log in', async ({ loginPage, page }) => {
  await loginPage.open();

  await loginPage.login(
    process.env.TEST_USERNAME!,
    process.env.TEST_PASSWORD!
  );

  await expect(page).toHaveURL(/dashboard/);
  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});

The final test is short, but the framework still provides rich diagnostics around every single action.

Expected Output

json
{ "level": "INFO", "message": "Locator action started", "action": "fill", "element": "Email field" }
{ "level": "INFO", "message": "Locator action completed", "action": "fill", "element": "Email field", "durationMs": 84 }
text
ActionError:
Failed to click "Login button" at https://qa.example.com/login.

Original error:
locator.click: Timeout 15000ms exceeded.

Troubleshooting

  • Multiple elements matched: your locator is not unique — use getByRole('button', { name: 'Submit order' }) instead of getByRole('button').
  • Click times out though the element looks visible: it may be covered by an overlay, still animating, disabled, or not receiving pointer events. Read the original error before adding force: true.
  • Fill does not trigger search suggestions: the field needs keyboard events — use pressSequentially() for that specific input.
  • Screenshot capture fails: verify the page is still open, the context is active, and the output directory is writable. Screenshot failure must not replace the original action error.
  • Password appears in logs: pass { sensitive: true } and check no other logging layer records the raw value.

Best Practices

  • Accept Locator objects rather than selector strings.
  • Preserve native Playwright action behaviour and auto-waiting.
  • Add only valuable framework functionality.
  • Keep the original Playwright error as the cause.
  • Mask sensitive values with { sensitive: true }.
  • Use named, business-friendly element descriptions.
  • Capture diagnostics on failure, not after every action.
  • Avoid fixed waits, automatic retries and default forced actions.
  • Keep assertions outside generic action helpers.
  • Keep wrapper methods small and predictable.

Frequently Asked Questions

Should every Playwright action use a wrapper?

No. Use wrappers when they add logging, diagnostics, reporting, masking or standardisation. Direct Playwright calls are appropriate when the wrapper adds no meaningful value.

Should wrappers wait for elements before clicking?

Usually not. Playwright's locator actions already perform actionability checks and auto-waiting. Add a separate expectation only when element state is part of the business requirement.

Should the helper accept XPath and CSS strings?

Prefer Locator objects so Page Objects and Component Objects can choose the most appropriate locator strategy.

Should wrappers contain assertions?

Generic action wrappers normally should not contain business assertions. Keep expectations in tests or dedicated assertion classes.

Should wrappers retry failed actions?

Not automatically. Blind retries hide real defects and unstable framework behaviour.

Is an ActionHelper the same as a BasePage?

No. A Base Page provides page-level behaviour such as navigation. An Action Helper provides reusable element interaction behaviour and can be used by both pages and components.

Conclusion

Custom Playwright locator-action wrappers make an enterprise automation framework easier to debug, monitor and maintain — as long as they preserve native locator behaviour instead of replacing Playwright's API. A strong wrapper adds structured logging, test-step reporting, sensitive-data masking, screenshots on failure, better error context and action timing. A weak wrapper simply renames locator.click() to clickElement() and adds another layer nobody needs.

Build wrappers only where they create measurable value. Combined with the Page Object Model, Component Object Model, Base Page design, fixtures and custom locator factories, a well-designed Action Helper becomes a genuinely useful part of a scalable Playwright architecture.

  • Playwright Custom Locator Functions
  • Playwright Base Page Design
  • Playwright Component Object Model
  • Playwright Page Object Model
  • Playwright Fixtures Explained
  • Enterprise Playwright Framework Architecture

Keywords: Playwright custom helper functions, Playwright wrapper functions, Playwright click helper, Playwright fill helper, Playwright reusable actions, Playwright ActionHelper, Playwright locator actions, Playwright TypeScript framework, custom Playwright utilities, enterprise Playwright framework.

Get Playwright tutorials in your inbox

Weekly tips, real-world examples, and framework patterns – no spam, unsubscribe anytime.

Playwright Framework Series

View all →
  1. 1Getting Started with Playwright: Installation, Setup, and Your First Test
  2. 2Playwright Locators: The Complete Guide with Real-World Examples
  3. 3Playwright Actions: Complete Guide to Click, Fill, Hover, Keyboard, Mouse & File Upload
  4. 4Playwright Assertions: The Complete Guide with Real-World Examples
  5. 5Playwright Auto Waiting: The Complete Guide with Real-World Examples
  6. 6Playwright Fixtures: The Complete Guide with Real-World Examples
  7. 7Playwright Browser Context & Multiple Tabs: The Complete Guide with Real-World Examples
  8. 8Playwright Authentication & Session Management: Complete Guide with Enterprise Examples
  9. 9Playwright Network Interception & API Mocking: Complete Guide with Real-World Examples
  10. 10Playwright Page Object Model (POM): The Complete Guide with Enterprise Examples
  11. 11Page Object Model with Playwright: A Practical Guide
  12. 12How to Build a Robust Professional Playwright Framework from Scratch (Step-by-Step)
  13. 13Building an Enterprise Playwright Framework from Scratch

API Testing Series

View all →
  1. 1Playwright API Testing: The Complete Guide with Real-World Examples
  2. 2Part 1 : Playwright API Testing Tutorial: Build Enterprise-Level API Automation Framework
  3. 3Part 2: Playwright API Testing Tutorial: Setting Up Project & Sending Your First API Request
  4. 4Part 3: Mastering CRUD Operations in Playwright API Testing
  5. 5Part 4: Authentication in Playwright API Testing (Bearer Token, JWT, API Key & Reusable Fixtures)
  6. 6Part 5: Building an Enterprise-Level Playwright API Automation Framework
  7. 7Part 6: API Models, Schema Validation & Test Data Management in Playwright
  8. 8Part 7: Custom Fixtures, Hooks, Logging & Parallel Execution in Playwright API Testing
  9. 9Part 8: Building a Production-Ready Playwright API Framework (Environment Management, CI/CD, Reporting & Best Practices)
  10. 10Part 9: Advanced Playwright API Testing Techniques for Enterprise Automation
  11. 11Part 10: Building a Complete Enterprise Playwright API Automation Framework (Final Part)
  12. 12API Testing with Playwright: Request Context, Auth, and Assertions

Recommended Next Articles