DEV Community

Bhaskar Vegesna
Bhaskar Vegesna

Posted on

1

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();
});
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
     });
   }
Enter fullscreen mode Exit fullscreen mode
  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');
Enter fullscreen mode Exit fullscreen mode

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('<TODAY>');         // Current date
const futureDate = utils.dataValueHandler('<TODAY+5>');  // 5 days from now
const pastDate = utils.dataValueHandler('<TODAY-3>');    // 3 days ago

// Personal data
const firstName = utils.dataValueHandler('<FIRSTNAME>');  // Random first name
const lastName = utils.dataValueHandler('<LASTNAME>');    // Random last name
const birthDate = utils.dataValueHandler('<DOB -25Y>');  // Date 25 years ago

// Unique identifiers
const uniqueId = utils.dataValueHandler('<UNIQUEID>');      // 12-digit unique ID
const customId = utils.dataValueHandler('<UNIQUEID8>');     // 8-digit unique ID
Enter fullscreen mode Exit fullscreen mode

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, 
     '<DOB -18Y>',  // Ensures user is 18 years old
     'Birth Date'
   );
Enter fullscreen mode Exit fullscreen mode
  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, '<FIRSTNAME>', 'First Name');
    await this.web.enterText(this.lastNameInput, '<LASTNAME>', 'Last Name');
    await this.web.enterText(this.emailInput, '<FIRSTNAME>.<LASTNAME>@example.com', 'Email');
    await this.web.enterText(this.dobInput, '<DOB -21Y>', 'Date of Birth');
    await this.web.enterText(this.startDateInput, '<TODAY+14>', 'Start Date');
  }
}
Enter fullscreen mode Exit fullscreen mode

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, '<FIRSTNAME>', 'First Name');
Enter fullscreen mode Exit fullscreen mode

The keyword will:

  1. Process the <FIRSTNAME> 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
<TODAY> Current date 02/12/2025
<TODAY+n> Future date 02/17/2025
<TODAY-n> Past date 02/07/2025
<UNIQUEID> 12-digit unique ID 123456789012
<UNIQUEIDn> n-digit unique ID 1234567
<FIRSTNAME> Random first name John
<LASTNAME> Random last name Smith
<DOB> Random date of birth 05/15/1990
<DOB -nY> 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, '<FIRSTNAME>', 'First Name');
  await web.enterText(lastNameInput, '<LASTNAME>', 'Last Name');
  await web.enterText(dobInput, '<DOB -21Y>', 'Date of Birth');
  await web.enterText(idInput, '<UNIQUEID>', 'User ID');
  await web.enterText(startInput, '<TODAY+14>', 'Start Date');

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

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());
    }
});
Enter fullscreen mode Exit fullscreen mode

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.

Heroku

Build apps, not infrastructure.

Dealing with servers, hardware, and infrastructure can take up your valuable time. Discover the benefits of Heroku, the PaaS of choice for developers since 2007.

Visit Site

Top comments (0)