Building a Robust Playwright Framework for Regulated Environments

Disclaimer: This project serves as a reference implementation to demonstrate design patterns and approaches for building a test automation framework using Playwright. While the concepts and architecture are production-ready, the current implementation is not fully tested for production use. Please review, test, and adapt the code to your specific needs before using in a production environment. When it comes to test automation in regulated environments like healthcare, finance, or government sectors, traceability and detailed reporting are not just nice-to-have features—they're essential requirements. In this post, I'll share my implementation of a reusable Playwright TypeScript framework that not only streamlines test automation but also meets the stringent requirements of regulated environments. The Challenge Traditional Playwright tests, while powerful, often lack the detailed step-by-step traceability required in regulated environments. Consider this standard Playwright test: test('standard playwright test', async ({ page }) => { await page.goto('https://playwright.dev/'); await page.getByRole('link', { name: 'Get started' }).click(); await expect(page).toBeVisible(); }); While functional, this approach provides minimal logging and traceability in test reports. For regulated environments, it's essential to know exactly: What action was performed When it was performed The status of each step Detailed error information if something fails The Solution: A Two-Level Architecture The framework implements a strategic separation between page-level and element-level operations through two main classes: 1. WebKeywords: Element-Level Operations async click(el: Locator, label: string) { try { await test.step(`Click on ${label}`, async () => { await expect(el).toBeVisible(); await el.click(); await expect(el).toBeFocused() .catch(() => { // Some elements might not receive focus after click }); }); } catch (error) { console.error(`Error clicking ${label}:`, error); throw error; } } 2. PageKeywords: Page-Level Operations async navigateToUrl(page: Page, url: string) { try { await test.step(`Navigate to and verify ${url}`, async () => { await page.goto(url); const currentUrl = page.url(); expect(currentUrl).toContain(url); }); } catch (error) { console.error(`Error navigating to ${url}:`, error); throw error; } } Key Benefits 1. Enhanced Traceability & Automatic Verification The framework enhances standard Playwright reporting and reliability with: Human-Readable Action Descriptions Native Playwright: await el.fill('John') Framework: "Enter and verify John into First Name field" Built-in Value Assertions // Framework automatically verifies after each action: async enterText(el: Locator, dataValue: string, label: string) { await test.step(`Enter and verify ${dataValue} into ${label}`, async () => { const val = this.comn.dataValueHandler(dataValue); await el.fill(val); // Automatic verification const actualValue = await el.inputValue(); expect(actualValue).toBe(val); }); } Comprehensive Input Verification Text inputs: Verifies entered value matches expected Dropdowns: Confirms selected option Checkboxes: Validates checked state Radio buttons: Ensures correct selection Structured Evidence & Organization Automatic screenshot capture at key points Hierarchical step organization Clear test flow visualization 2. Multi-Tab Support The separation of WebKeywords (element-level) from PageKeywords (page-level) enables seamless interaction across multiple browser tabs: // Works across different tabs with single initialization const webKeywords = new WebKeywords(); await webKeywords.click(elementInTab1, 'Button in Tab 1'); await webKeywords.click(elementInTab2, 'Button in Tab 2'); 3. Robust Error Handling Each keyword includes comprehensive error handling: Detailed error messages Automatic logging Screenshot capture on failure Stack trace preservation 4. Clean, Readable Reports The framework generates detailed, hierarchical reports that show: Test case name and description Each step performed Screenshots (both success and failure cases) Error details when failures occur The Results Let's look at the actual test reports to see the difference: Standard Playwright Test Report The basic test report shows: Simple action logging like page.goto() and click() Basic step information Technical action descriptions Framework-Enhanced Test Report The framework automatically enhances the report with: Test Execution Timestamps Test execution started - 02/12/2025, 20:12:56 Test execution completed - 02/12/2025, 20:12:57 Precise tracking of test execution timeli

Feb 12, 2025 - 12:40
 0
Building a Robust Playwright Framework for Regulated Environments

Disclaimer: This project serves as a reference implementation to demonstrate design patterns and approaches for building a test automation framework using Playwright. While the concepts and architecture are production-ready, the current implementation is not fully tested for production use. Please review, test, and adapt the code to your specific needs before using in a production environment.

When it comes to test automation in regulated environments like healthcare, finance, or government sectors, traceability and detailed reporting are not just nice-to-have features—they're essential requirements. In this post, I'll share my implementation of a reusable Playwright TypeScript framework that not only streamlines test automation but also meets the stringent requirements of regulated environments.

The Challenge

Traditional Playwright tests, while powerful, often lack the detailed step-by-step traceability required in regulated environments. Consider this standard Playwright test:

test('standard playwright test', async ({ page }) => {
  await page.goto('https://playwright.dev/');
  await page.getByRole('link', { name: 'Get started' }).click();
  await expect(page).toBeVisible();
});

While functional, this approach provides minimal logging and traceability in test reports. For regulated environments, it's essential to know exactly:

  • What action was performed
  • When it was performed
  • The status of each step
  • Detailed error information if something fails

The Solution: A Two-Level Architecture

The framework implements a strategic separation between page-level and element-level operations through two main classes:

1. WebKeywords: Element-Level Operations

async click(el: Locator, label: string) {
  try {
    await test.step(`Click on ${label}`, async () => {
      await expect(el).toBeVisible();
      await el.click();
      await expect(el).toBeFocused()
        .catch(() => {
          // Some elements might not receive focus after click
        });
    });
  } catch (error) {
    console.error(`Error clicking ${label}:`, error);
    throw error;
  }
}

2. PageKeywords: Page-Level Operations

async navigateToUrl(page: Page, url: string) {
  try {
    await test.step(`Navigate to and verify ${url}`, async () => {
      await page.goto(url);
      const currentUrl = page.url();
      expect(currentUrl).toContain(url);
    });
  } catch (error) {
    console.error(`Error navigating to ${url}:`, error);
    throw error;
  }
}

Key Benefits

1. Enhanced Traceability & Automatic Verification

The framework enhances standard Playwright reporting and reliability with:

  1. Human-Readable Action Descriptions

    • Native Playwright: await el.fill('John')
    • Framework: "Enter and verify John into First Name field"
  2. Built-in Value Assertions

   // Framework automatically verifies after each action:
   async enterText(el: Locator, dataValue: string, label: string) {
     await test.step(`Enter and verify ${dataValue} into ${label}`, async () => {
       const val = this.comn.dataValueHandler(dataValue);
       await el.fill(val);
       // Automatic verification
       const actualValue = await el.inputValue();
       expect(actualValue).toBe(val);
     });
   }
  1. Comprehensive Input Verification

    • Text inputs: Verifies entered value matches expected
    • Dropdowns: Confirms selected option
    • Checkboxes: Validates checked state
    • Radio buttons: Ensures correct selection
  2. Structured Evidence & Organization

    • Automatic screenshot capture at key points
    • Hierarchical step organization
    • Clear test flow visualization

2. Multi-Tab Support

The separation of WebKeywords (element-level) from PageKeywords (page-level) enables seamless interaction across multiple browser tabs:

// Works across different tabs with single initialization
const webKeywords = new WebKeywords();
await webKeywords.click(elementInTab1, 'Button in Tab 1');
await webKeywords.click(elementInTab2, 'Button in Tab 2');

3. Robust Error Handling

Each keyword includes comprehensive error handling:

  • Detailed error messages
  • Automatic logging
  • Screenshot capture on failure
  • Stack trace preservation

4. Clean, Readable Reports

The framework generates detailed, hierarchical reports that show:

  • Test case name and description
  • Each step performed
  • Screenshots (both success and failure cases)
  • Error details when failures occur

The Results

Let's look at the actual test reports to see the difference:

Standard Playwright Test Report

Standard Playwright Test Report

The basic test report shows:

  • Simple action logging like page.goto() and click()
  • Basic step information
  • Technical action descriptions

Framework-Enhanced Test Report

Framework-Enhanced Test Report

The framework automatically enhances the report with:

  1. Test Execution Timestamps

    • Test execution started - 02/12/2025, 20:12:56
    • Test execution completed - 02/12/2025, 20:12:57
    • Precise tracking of test execution timeline
  2. Human-Readable Action Descriptions

    • Navigate to and verify https://playwright.dev/
    • Click on Get Started Link
    • Each step clearly describes both action and target
  3. Built-in Verification Steps

    • Element visibility checks before actions
    • URL verification after navigation
    • Input value validation after entry
  4. Structured Organization

    • Clear Before/After hooks
    • Test execution boundaries
    • Automatic evidence collection

Dynamic Test Data Management

One of the most challenging aspects of test automation is managing test data effectively. The framework includes a powerful DataValueHandler that simplifies this through pattern-based data generation:

Pattern-Based Data Generation

const utils = new CommonUtils();

// Date patterns
const today = utils.dataValueHandler('');         // Current date
const futureDate = utils.dataValueHandler('');  // 5 days from now
const pastDate = utils.dataValueHandler('');    // 3 days ago

// Personal data
const firstName = utils.dataValueHandler('');  // Random first name
const lastName = utils.dataValueHandler('');    // Random last name
const birthDate = utils.dataValueHandler('');  // Date 25 years ago

// Unique identifiers
const uniqueId = utils.dataValueHandler('');      // 12-digit unique ID
const customId = utils.dataValueHandler('');     // 8-digit unique ID

Benefits

  1. Consistent Pattern Language

    • Team members can easily understand and use data patterns
    • Patterns are self-documenting
    • Reduces the need for external test data files
  2. Dynamic Data Generation

    • Data is generated at runtime
    • Each test run gets fresh data
    • Reduces test data maintenance
  3. Flexible Date Handling

   // Registration with age verification
   await webKeywords.enterText(
     birthdateInput, 
     '',  // Ensures user is 18 years old
     'Birth Date'
   );
  1. Integration with Test Keywords

The real power of DataValueHandler comes from its deep integration into all keyword methods. There's no need to call DataValueHandler separately - it's automatically handled by each keyword:

class RegistrationPage {
  async registerNewUser() {
    // Pass patterns directly to keywords
    await this.web.enterText(this.firstNameInput, '', 'First Name');
    await this.web.enterText(this.lastNameInput, '', 'Last Name');
    await this.web.enterText(this.emailInput, '.@example.com', 'Email');
    await this.web.enterText(this.dobInput, '', 'Date of Birth');
    await this.web.enterText(this.startDateInput, '', 'Start Date');
  }
}

This integration provides several benefits:

  1. Cleaner Code: No need for separate data generation steps
  2. Automatic Logging: The actual generated values are logged in test steps
  3. Consistent Handling: All data patterns are processed the same way
  4. Reduced Boilerplate: No need to initialize utility classes or handle data manually

For example, when entering a first name:

await web.enterText(firstNameInput, '', 'First Name');

The keyword will:

  1. Process the pattern to generate a random name
  2. Log both the pattern and generated value in the test step
  3. Enter the generated value into the input
  4. Verify the entered value matches what was generated

Available Patterns

Pattern Description Example Output
Current date 02/12/2025
Future date 02/17/2025
Past date 02/07/2025
12-digit unique ID 123456789012
n-digit unique ID 1234567
Random first name John
Random last name Smith
Random date of birth 05/15/1990
DOB n years ago 05/15/2005

Example Test Scenario

test('register new user', async ({ page, web }) => {
  // Using patterns directly in keywords - recommended approach
  await web.enterText(firstNameInput, '', 'First Name');
  await web.enterText(lastNameInput, '', 'Last Name');
  await web.enterText(dobInput, '', 'Date of Birth');
  await web.enterText(idInput, '', 'User ID');
  await web.enterText(startInput, '', 'Start Date');

  // Each action includes automatic value verification
  await web.selectByValue(countrySelect, 'US', 'Country');
  await web.setCheckbox(termsCheckbox, true, 'Terms and Conditions');
});

This approach ensures:

  • Clean, maintainable test code
  • Automatic data generation and verification
  • Clear test step logging
  • Built-in value assertions

Package Consumption Example

In my example project, I've demonstrated package consumption using GitHub Packages:

// Example from pw-common-harness project
import { PageKeywords, WebKeywords, CommonUtils } from "@ramam-v/common-pw";

// Extended test fixture showing library consumption
export const test = base.extend<{
    cmn: CommonUtils;
    web: WebKeywords;
    pkey: PageKeywords;
}>({
    // Utilities
    cmn: async ({ }, use) => {
        await use(new CommonUtils());
    },
    web: async ({ }, use) => {
        await use(new WebKeywords());
    },
    pkey: async ({ }, use) => {
        await use(new PageKeywords());
    }
});

Conclusion

In regulated environments, test automation isn't just about functional verification—it's about providing a clear, traceable record of what was tested and how. This Playwright framework addresses these needs while maintaining code reusability and ease of use.

The separation of concerns between page-level and element-level operations, combined with automatic logging and error handling, makes this framework particularly well-suited for teams working in regulated industries where documentation and traceability are paramount.

Resources

Note: Remember to review the disclaimer at the top of this post before implementation. This framework is intended as a reference implementation and learning resource.