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"
- Native Playwright:
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()
andclick()
- 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 timeline
-
Human-Readable Action Descriptions
Navigate to and verify https://playwright.dev/
Click on Get Started Link
- Each step clearly describes both action and target
-
Built-in Verification Steps
- Element visibility checks before actions
- URL verification after navigation
- Input value validation after entry
-
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
Benefits
-
Consistent Pattern Language
- Team members can easily understand and use data patterns
- Patterns are self-documenting
- Reduces the need for external test data files
-
Dynamic Data Generation
- Data is generated at runtime
- Each test run gets fresh data
- Reduces test data maintenance
Flexible Date Handling
// Registration with age verification
await webKeywords.enterText(
birthdateInput,
'<DOB -18Y>', // Ensures user is 18 years old
'Birth Date'
);
- 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');
}
}
This integration provides several benefits:
- Cleaner Code: No need for separate data generation steps
- Automatic Logging: The actual generated values are logged in test steps
- Consistent Handling: All data patterns are processed the same way
- 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');
The keyword will:
- Process the
<FIRSTNAME>
pattern to generate a random name - Log both the pattern and generated value in the test step
- Enter the generated value into the input
- 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');
});
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.
Top comments (0)