Playwright Transformer is an open-source utility that helps teams turn recorded Playwright UI tests into clean, maintainable, and data-driven automation. Itβs built for developers and test engineers who want to move quickly without accumulating brittle or hard-to-maintain test code.
Instead of hard-coding values directly into test scripts, Playwright Transformer extracts test data, externalizes it into JSON files, and refactors tests to follow data-driven patterns. This keeps test logic focused on behavior, makes updates easier when UI or data changes, and helps test suites scale as applications grow.
Whether youβre experimenting with Playwright for the first time or maintaining a large automation suite, Playwright Transformer aims to reduce duplication, improve readability, and make UI tests easier to evolve over time.
Automated Value Extraction β’ Pattern-Based Transformation
-
Smart Pattern Matching. Automatically identifies and extracts values from Playwright actions like
.fill(),.selectOption(),.click(), and more using configurable regex patterns and externalize these into data files. -
Data-Driven Conversion. Transforms hardcoded test values into data references, enabling easy test data management and parameterization. Generated external data file is json array which enables test data addition.
-
Flexible Configuration. Uses JSON-based configuration files for patterns, replacements, and transformationsβno code changes needed to customize behavior.
-
Pre-processing Pipeline. Automatically inserts boilerplate code, removes noise lines, and applies custom transformations before pattern processing.
- Features
- Installation
- Quick Start
- Usage
- How It Works
- Configuration
- Examples
- API Reference
- Development
- Contributing
- License
- Related Projects
- Support
- Node.js >= 18.0.0 (required for building the package)
- npm or yarn
npm install -D egain-playwright-transformerOr using yarn:
yarn add -D egain-playwright-transformerAfter installation, you can use the CLI directly via below command:
node node_modules/egain-playwright-transformer/dist/cli.mjs --all \
--input-dir tests/input \
--output-dir tests/output \
--data-dir data/outputIf you've cloned or forked this repository and want to use it directly from source:
-
Clone the repository:
git clone https://github.com/egain/playwright-transformer.git cd playwright-transformer -
Install dependencies:
npm install
-
Build the project:
npm run build
-
Use the CLI from the built files:
node dist/cli.mjs --all \ --input-dir tests/input \ --output-dir tests/output \ --data-dir data/output
Note: You'll need to rebuild (
npm run build) after making any changes to the source code.
- Make sure application under test uses static ids - preferably follow best practise to use data-testid.
- Before running the transformer, ensure you have a
config/directory in your project root with all required configuration files:
fill_patterns.jsonreplace_texts.jsoninsert_lines.jsonprepend.tstest_start.ts
The transformer will auto-detect the config/ directory relative to your current working directory.
The easiest way to get started is using the command-line interface as below where:
--input-dir= folder with recorded test scripts--output-dir= folder where transformed script will be generated--data-dir= folder where output data files will be generated
# Transform all test files
node node_modules/egain-playwright-transformer/dist/cli.mjs \
--all \
--input-dir tests/input \
--output-dir tests/output \
--data-dir data/output# First, make sure you've built the project (npm run build)
# Then run:
node dist/cli.mjs \
--all \
--input-dir tests/input \
--output-dir tests/output \
--data-dir data/outputAdd to your package.json:
{
"scripts": {
"transform:all": "node node_modules/egain-playwright-transformer/dist/cli.mjs --all --input-dir tests/input --output-dir tests/output --data-dir tests/data"
}
}Then run:
# Transform all files
npm run transform:all
The CLI provides a simple interface for transforming test files:
# Transform all test files
node node_modules/egain-playwright-transformer/dist/cli.mjs --all \
--input-dir tests/input \
--output-dir tests/output \
--data-dir data/output
# Make sure you've built the project first (npm run build)
node dist/cli.mjs \
--all \
--input-dir tests/input \
--output-dir tests/output \
--data-dir data/output| Option | Short | Description | Required |
|---|---|---|---|
--input-dir |
-i |
Directory containing source test files (.spec.ts) |
β Yes |
--output-dir |
-o |
Directory for transformed test files | β Yes |
--data-dir |
-d |
Directory for JSON data files | β Yes |
--all |
-a |
Transform all files in input directory (default: first file only) | β No |
--help |
-h |
Show help message | β No |
Note: By default (without
--allflag), the transformer processes only the first test file found in the input directory. Use the--allflag to transform all test files.
# Transform all test files
node node_modules/egain-playwright-transformer/dist/cli.mjs --all \
--input-dir ./tests/input \
--output-dir ./tests/output \
--data-dir ./data/outputFor more control, use the programmatic API:
import { transform, PlaywrightTransformer } from 'egain-playwright-transformer';
// Advanced usage - transform all files
const transformer = new PlaywrightTransformer({
inputDir: './tests/input',
outputDir: './tests/output',
dataDir: './data/output',
});
// Transform all files in the input directory
const result = await transformer.transformAll();
if (result.success) {
console.log(`Transformed ${result.transformedFiles} files`);
} else {
console.error('Errors:', result.errors);
}Note: The
transform()function andtransformer.transform()method process only the first test file found in the input directory. Usetransformer.transformAll()to process all files.
Playwright Transformer follows a multi-phase transformation pipeline:
- Apply Insert Line Patterns: Processes patterns from
insert_lines.jsonto insert, update, or remove lines - Remove Noise: Filters out unnecessary lines like goto steps for browser redirect based on skip patterns
- Prepend Boilerplate: Injects import statements and setup code from
prepend.tsat the beginning of each test file
For each line in the test file:
-
Pattern Matching: Analyzes the line against configured patterns
-
Value Extraction: Identifies hardcoded values in actions like:
.fill('value')β extracts'value'.selectOption('option')β extracts'option'- And all other pattern types in
fill_patterns.json
-
Data Mapping: Stores extracted values in maps:
jsonMap: Maps field names to values (for JSON output)reverseJsonMap: Maps values to data references (for replacement)dynamicIdsMap: Tracks dynamic selectors
-
Fill Pattern Handler: Replaces hardcoded values with data references. The
uniqueIndexis automatically appended to values unless the key is specified innonUniqueKeysorkeysToBeTreatedAsConstants:// Before await page.getByTestId('username').fill('john@example.com'); await page.getByTestId('email').fill('user@example.com'); // email in nonUniqueKeys // After (if username is not in nonUniqueKeys) await page.getByTestId('username').fill(data.username + uniqueIndex); await page.getByTestId('email').fill(data.email); // no uniqueIndex for email
-
TestCase Start Handler: When a
test()line is encountered, it usestest_start.tsas a template, replacing placeholders like[[TEST_CASE_NAME_MATCHER]]with the actual test name and data references -
Default Pattern Handler: Applies text replacements from
replace_texts.json:- Data value replacement in locators and selectors
- Skip constant value substitution
- Exclude unique index addition for keys defined in
keysToBeTreatedAsConstants
-
Special Handlers: Handle test structure, file endings, complete file name placeholders, and special cases
- Transformed Test File: Writes the data-driven test to the output directory
- JSON Data File: Generates a JSON file with all extracted values:
{ "tcName": "TC01", "username": "john@example.com", "password": "secret123" }
Input (input/TC01_Login.spec.ts):
import { test } from '@playwright/test';
test('test', async ({ page }) => {
await page.goto('https://example.com/login');
await page.getByTestId('username').fill('john@example.com');
await page.getByTestId('password').fill('secret123');
await page.getByRole('button', { name: 'Sign in' }).click();
});Output (output/TC01_Login.spec.ts):
import { test } from '@playwright/test';
import input from '@data/output/TC01_Login.json';
for (const data of input) {
test(`${data['tcName']}`, async ({ page }) => {
await page.goto(process.env.BASE_URL);
await page.getByTestId('username').fill(data.username);
await page.getByTestId('password').fill(data.password);
await page.getByRole('button', { name: 'Sign in' }).click();
});
}Data File (data/output/TC01_Login.json):
[
{
"tcName": "TC01",
"username": "john@example.com",
"password": "secret123"
}
]Playwright Transformer uses JSON configuration files located in the config/ directory. The config/ directory must exist in your project root (where you run the CLI command) with all required configuration files as mentioned below. Refer to configuration files used in different examples in example folder.
| File | Purpose | Description |
|---|---|---|
fill_patterns.json |
Value Extraction | Defined regex patterns for extracting test data values from recorded script steps. |
replace_texts.json |
Text Replacement | Rules for replacing hardcoded data values in recorded scripts with data references from externalized data |
insert_lines.json |
Code Injection | Inserts/Updates steps based on defined "existingLines" in json |
prepend.ts |
Boilerplate | Define all import statements code needed for tests |
test_start.ts |
Boilerplate | Define initial steps of test case include any custom method initialization if needed |
Edit the JSON configuration files to customize transformation behavior:
Below patterns will get data from all steps which has
- "getByTestId()" and "fill()"
- "getByTestId()" and "selectOption()" to parameterize the value in fill() and selectOption() in external data file. This will create data file json with data for username and password when transformer is run.
[
{
"regex": "getByTestId\\(['\"]([^'\"]+)['\"]\\)\\.fill\\(['\"]([^'\"]+)['\"]\\)",
"groupNoForKey": 1, //group 1 from above regex will be used as key in data file
"groupNoForValue": 2, //group 2 from above regex will be used as value in data file
"keysToBeTreatedAsConstants": "", //comma separated values of testids which should be parameterized
"nonUniqueKeys": "email", //this ensures that unique index is NOT added for step with this key.
"isKeyFrameset": "false",
"isContentFrameHandlingNeeded": "false", //set this to true if you want fill() to replaced with pressSequentially() to simulate typing of content
"ignorePathInValue": "false", //this will ignore the path of attachment file and only parameterize file name.
"isFileUpload": "false" //this is set to true for steps which has file upload.
},
{
"regex": "(?=.*getByTestId\\('([^']*)'\\))(?=.*selectOption\\('(.*)'\\)).*",
"groupNoForKey": "1",
"groupNoForValue": "2",
"keysToBeTreatedAsConstants": "",
"nonUniqueKeys": "country",
"isKeyFrameset": "false",
"isContentFrameHandlingNeeded": "false",
"ignorePathInValue": "false",
"isFileUpload": "false"
}
]Below will update the value in all the steps which have texts prepended ".getByTestId(" and appended by "')" to use the data from the json data file thereby parameterizing the data.
[
{
"dataPrependedBy": ".getByTestId('",
"dataAppendedBy": "')",
"isWholeWordMatch": "false",
"removeDateAtTheEnd": "true", //if there is timestamp in the end - it will be removed/kept based on value of this field
"keysToBeTreatedAsConstants": "email", //this will not append unique index to step wherever key "email" is used from data file in the the test script
"replaceWithDynamicIds": "false",
"keysForFileUploading": "",
"valuesToBeIgnored": "0,1" //values defined in this will not be parameterized
}
]Configure this file to include existing patterns which needs to be transformed with different steps. In addition to below patterns, add any other transformation which is required for any step to below list of transformation.
[
//this will remove the import statement mentioned below
{
"existingLines": "import { test, expect } from '@playwright/test'",
"removeLines": "0",
"separator": "\\|"
},
{
//This will remove all redirects in recorded script, pattern can be updated to exclude any specific goto which need not be removed.
"existingLines": ".goto\\('[^']*'\\)",
"isRegex": "true",
"removeLines": "0"
}
]In addition to below values include any other imports which are needed for your tests
import { expect, test } from '@playwright/test';
import input from '@[[DATA_SOURCE_PATH_PLACEHOLDER]]/[[COMPLETE_TEST_FILE_NAME]].json' assert { type: 'json' };
import { faker } from '@faker-js/faker';
import path from 'path';This file serves as a template for the test case structure. When the transformer encounters a test() line in the input file, it uses this template and replaces placeholders:
[[TEST_CASE_NAME_MATCHER]]- Replaced with the original test name plus${data['tcName']}- Other placeholders are replaced during transformation
Include any additional steps needed in your tests before goto() steps. This is useful when you want to initialize any class, set any variables in your tests at start - for using it later in transformation in insert_lines.json file.
for (const data of input) {
test(`[[TEST_CASE_NAME_MATCHER]]`, async ({ page }) => {
const uniqueIndex = "_" + faker.string.alphanumeric(10) //this unique index will be used in all test data, can be updated to empty string if not needed to be used.
await page.goto(process.env.BASE_URL); //This ensures that browser is launched using the BASE_URL defined in env fileInput:
await page.getByTestId('email').fill('user@example.com');
await page.getByTestId('name').fill('John Doe');fill_patterns.json:
[
{
"regex": "getByTestId\\(['\"]([^'\"]+)['\"]\\)\\.fill\\(['\"]([^'\"]+)['\"]\\)",
"groupNoForKey": 1,
"groupNoForValue": 2,
"keysToBeTreatedAsConstants": "",
"nonUniqueKeys": "email",
"isKeyFrameset": "false",
"isContentFrameHandlingNeeded": "false",
"ignorePathInValue": "false",
"isFileUpload": "false"
}
]replace_texts.json:
[
{
"dataPrependedBy": ".getByTestId('",
"dataAppendedBy": "')",
"isWholeWordMatch": "false",
"removeDateAtTheEnd": "true", //if there is timestamp in the end - it will be removed/kept based on value of this field
"keysToBeTreatedAsConstants": "email", //this will not append unique index to step wherever key "email" is used from data file in the the test script
"keysForFileUploading": "",
"valuesToBeIgnored": "0,1" //values defined in this will not be parameterized
}
]Transformed:
await page.getByTestId('email').fill(data.email);
await page.getByTestId('name').fill(data.name + uniqueIndex);JSON Output:
[
{
"tcName": "TC01",
"email": "user@example.com",
"name": "John Doe"
}
]Input:
await page.getByTestId('country').selectOption('United States');fill_patterns.json:
[
{
"regex": "(?=.*getByTestId\\('([^']*)'\\))(?=.*selectOption\\('(.*)'\\)).*",
"groupNoForKey": "1",
"groupNoForValue": "2",
"keysToBeTreatedAsConstants": "",
"nonUniqueKeys": "country", //this will not append unique index to step with this key
"isKeyFrameset": "false",
"isContentFrameHandlingNeeded": "false",
"ignorePathInValue": "false",
"isFileUpload": "false"
}
]replace_texts.json:
[
{
"dataPrependedBy": ".selectOption('",
"dataAppendedBy": "')",
"isWholeWordMatch": "false",
"removeDateAtTheEnd": "true", //if there is timestamp in the end - it will be removed/kept based on value of this field
"keysToBeTreatedAsConstants": "country", //this will not append unique index to step with this key
"keysForFileUploading": "",
"valuesToBeIgnored": "0,1" //values defined in this will not be parameterized
}
]Transformed:
await page.getByTestId('country').selectOption(data.country);JSON Output:
[
{
"tcName": "TC01",
"country": "United States"
}
]Input:
await page.getByTestId('upload').setInputFiles('path/to/file.pdf');fill_patterns.json:
[
{
"regex": "(?=.*getByTestId\\('([^']*)'\\))(?=.*setInputFiles\\('(.*)'\\)).*",
"groupNoForKey": "1",
"groupNoForValue": "2",
"keysToBeTreatedAsConstants": "", //comma separated test-dataid to be added if transformation not needed for any element with this id.
"nonUniqueKeys": "",
"isKeyFrameset": "false",
"isContentFrameHandlingNeeded": "false",
"ignorePathInValue": "true", //if this is set to true the relative path to the file will be ignored in test data and attachment file should be kept in attachment folder in root directory
"isFileUpload": "true" //this should be true as setInputFiles is used for fileupload step
}
]Transformed:
const filePath_1 = path.join(process.cwd(), 'attachments', data.upload);
await page.locator('//input[@type="file"]').setInputFiles(filePath_1);JSON Output:
[
{
"tcName": "TC01",
"upload": "file.pdf"
}
]Transforms the first test file found in the input directory. This is a convenience function that internally calls transformer.transform().
Parameters:
config.inputDir(string): Directory containing source test filesconfig.outputDir(string): Directory for transformed test filesconfig.dataDir(string): Directory for JSON data files
Returns: Promise<TransformResult>
Example:
// Transforms only the first file
const result = await transform({
inputDir: './tests/input',
outputDir: './tests/output',
dataDir: './data/output',
});Note: To transform all files, use
PlaywrightTransformer.transformAll()instead.
Main transformer class for advanced usage.
new PlaywrightTransformer(config: TransformerConfig)Transforms the first test file found in the input directory.
Returns: Promise<TransformResult>
Transforms all test files in the input directory.
Returns: Promise<TransformResult>
interface TransformResult {
success: boolean;
transformedFiles: number;
errors: string[];
}interface TransformerConfig {
inputDir: string;
outputDir: string;
dataDir: string;
patternsFile?: string;
constantsFile?: string;
prependFile?: string;
}
interface TransformResult {
success: boolean;
transformedFiles: number;
errors: string[];
}npm run buildnpm testnpm run devContributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
- Playwright - End-to-end testing framework
- Playwright Test - Playwright's test runner
For issues, questions, or contributions, please open an issue on GitHub.
Made with β€οΈ by the eGain Team