A production-ready end-to-end test automation framework built with Playwright and TypeScript/JavaScript, following the Page Object Model (POM) pattern. Designed to be scalable, maintainable, and easy to onboard new team members.
- Prerequisites
- Project Structure
- Getting Started
- Environment Configuration
- Test Data Configuration
- Running Tests
- Reporting
- Authentication Setup
- Writing New Tests
- Adding a New Module
- Load Testing with Artillery
- CI/CD Integration (Azure DevOps)
- Framework Architecture
- Troubleshooting
| Tool | Version | Download |
|---|---|---|
| Node.js | >= 18.x | nodejs.org |
| npm | >= 9.x | Comes with Node.js |
| Git | Latest | git-scm.com |
PF-Automation-Framework/
│
├── data/
│ └── globalData.json # Centralized test data (search values, expected messages)
│
├── env/
│ ├── .env.UAT # UAT environment URL
│ ├── .env.QA # QA environment URL
│ ├── .env.PROD # Production environment URL
│ └── .env.GLOBAL # Global environment URL
│
├── images/ # Static images used in tests (e.g. widget uploads)
│
├── pageobjects/ # CSS/XPath selectors organized by module
│ ├── global.js # Shared selectors across all pages
│ ├── loginPage.js
│ ├── dashboards.js
│ └── ...
│
├── pages/ # Page classes (POM) — actions and verifications
│ ├── basePage.js # Base class with 50+ reusable utility methods
│ ├── loginPage.js
│ ├── dashboards.js
│ ├── navigations.js
│ └── ...
│
├── testFixtures/
│ └── fixture.js # Custom Playwright fixtures injecting all page objects
│
├── tests/
│ ├── loging.setup.ts # Authentication setup (runs before all tests)
│ ├── loginPage/
│ ├── dashboards/
│ │ ├── dashboardsLV/
│ │ └── dashboardsWidgets/
│ ├── organization/
│ ├── utils/
│ │ └── auth.js # OAuth2 token helper
│ └── ... # One folder per module
│
├── tests-examples/
│ └── demo-todo-app.spec.ts # Playwright reference example
│
├── screenshots/ # Auto-saved screenshots (gitignored)
├── .auth/ # Saved auth state (gitignored)
│
├── config.js # Central URL and navigation config
├── playwright.config.ts # Playwright configuration
├── package.json # Dependencies and scripts
├── artillery.yml # Load test configuration
└── .gitignore
git clone <your-repo-url>
cd PF-Automation-Frameworknpm installnpx playwright installCopy the environment files and update with your application URLs:
# The env/ folder already contains template files:
# env/.env.UAT, env/.env.QA, env/.env.PROD, env/.env.GLOBALUpdate each file with your actual URL:
URL=https://your-actual-app-url.com/
LINE=''Open data/globalData.json and replace all placeholder values with your actual test data:
{
"user_name": "your_username",
"password": "your_password",
"defaultAssemblyLine": "Your Assembly Line",
...
}Security Note: Never commit real credentials. Use environment variables or a secrets manager in CI/CD.
The framework supports multiple environments. Set the ENV variable before running tests to load the corresponding .env file from the env/ folder.
$env:ENV = "UAT"
npx playwright testset ENV=UAT
npx playwright testENV=UAT npx playwright test| ENV value | File loaded | Description |
|---|---|---|
UAT |
env/.env.UAT |
User Acceptance Testing |
QA |
env/.env.QA |
Quality Assurance |
PROD |
env/.env.PROD |
Production |
GLOBAL |
env/.env.GLOBAL |
Global environment |
All test data is centralized in data/globalData.json. Update the values before running tests:
{
"user_name": "your_username", // Login username
"password": "your_password", // Login password
"defaultAssemblyLine": "Line Name", // Assembly line to select after login
"dashboardToSearch": "My Dashboard", // Dashboard name for search tests
...
}npx playwright test# PowerShell
$env:ENV = "UAT"; npx playwright testnpx playwright test tests/dashboards/dashboardsLV/verifyLVScreenIsWorking.spec.ts# Run only @sanity tests
npx playwright test --grep "@sanity"
# Run only @smoke tests
npx playwright test --grep "@smoke"npx playwright test --headednpx playwright test --debugnpx playwright test --uiTests run fully parallel by default. To limit workers:
npx playwright test --workers=2After a test run, open the report:
npx playwright show-reportThe report is saved to playwright-report/.
Generate and open Allure report:
# Generate
npx allure generate allure-results --clean -o allure-report
# Open
npx allure open allure-reportRequires Java installed. Install Allure CLI:
npm install -g allure-commandline
The framework uses Playwright's storage state to persist login sessions, avoiding repeated logins per test.
tests/loging.setup.tsruns first (it matches*.setup.tspattern inplaywright.config.ts)- It logs in using credentials from
data/globalData.json - Saves the browser storage state to
.auth/login.json - All subsequent tests reuse this saved auth state
npx playwright test tests/loging.setup.tsThe .auth/ folder is gitignored — each developer/CI agent runs setup once locally.
import test from '../../testFixtures/fixture';
import { expect } from '@playwright/test';
import fs from 'fs';
const testData = JSON.parse(fs.readFileSync(`./data/globalData.json`, `utf-8`));
import { baseUrl } from '../../config';
test.describe('@sanity: Feature description', () => {
test('Test name describing what it verifies', async ({ loginPage, dashbaordPage }) => {
await test.step('Step 1: Navigate to the page', async () => {
await dashbaordPage.goToDashboardsLV();
});
await test.step('Step 2: Perform action', async () => {
await dashbaordPage.waitForPageLoad();
});
await test.step('Step 3: Verify result', async () => {
expect(await dashbaordPage.isTableLoaded()).toBe(true);
});
});
});Use tags in test.describe() for filtering:
| Tag | Purpose |
|---|---|
@sanity |
Core happy-path tests — run on every build |
@smoke |
Quick smoke check after deployment |
@regression |
Full regression suite |
All page objects are available as fixtures via testFixtures/fixture.js:
async ({ loginPage, dashbaordPage, organizationPage, measuringPointsPage, ... })See testFixtures/fixture.js for the complete list of available fixtures.
Follow this 4-step pattern to add a new page/module to the framework:
Create pageobjects/myModule.js:
export const addButton = '#addAction'
export const searchInput = '[placeholder="Search"]'
export const mainGridTable = 'table#MyModule_headers'
export const mainTableDataGrid = '#MyModule_scroll'Create pages/myModule.js:
import BasePage from './basePage'
import { myModuleLV } from '../config'
import { addButton, mainGridTable } from '../pageobjects/myModule'
class MyModule extends BasePage {
constructor(page) {
super(page)
}
async goToMyModuleLV() {
await this.open(myModuleLV)
}
async isTableLoaded() {
return await super.isTableLoaded(mainGridTable)
}
async clickOnAddButton() {
await this.waitAndClick(addButton)
}
}
export default MyModuleexport const myModuleLV = baseUrl + 'YourApp/Module/GetListView'import MyModule from '../pages/myModule'
// Inside the fixture.extend({}) block:
myModulePage: async ({ page }, use) => {
await use(new MyModule(page))
},Create tests/myModule/verifyLVScreenIsWorking.spec.ts:
import test from '../../testFixtures/fixture';
import { expect } from '@playwright/test';
test.describe('@sanity: MyModule LV is working correctly', () => {
test('Navigate to MyModule and verify table loads', async ({ myModulePage }) => {
await test.step('Navigate to MyModule', async () => {
await myModulePage.goToMyModuleLV();
});
await test.step('Verify table is loaded', async () => {
expect(await myModulePage.isTableLoaded()).toBe(true);
});
});
});The framework includes Artillery integration for basic load testing.
npm install -g artillery
npm install -g artillery-engine-playwrightUpdate artillery.yml with your target URL:
config:
target: https://your-app-uat.yourdomain.com/artillery run artillery.ymlartillery run artillery.yml --output loadTest.json
artillery report loadTest.jsontrigger:
- main
pool:
vmImage: 'ubuntu-latest'
steps:
- task: NodeTool@0
inputs:
versionSpec: '18.x'
- script: npm ci
displayName: 'Install dependencies'
- script: npx playwright install --with-deps
displayName: 'Install Playwright browsers'
- script: npx playwright test
displayName: 'Run tests'
env:
ENV: UAT
CI: true
- task: PublishTestResults@2
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: 'test-results/**/*.xml'
condition: always()- Go to Pipelines > Library > Variable Groups
- Create a variable group (e.g.
automation-secrets) - Add variables:
USER_NAME,PASSWORD,URL - Mark sensitive variables as secret
- Reference in pipeline:
variables:
- group: automation-secretsThen override in data/globalData.json at runtime, or use process.env.USER_NAME directly in your page classes.
┌─────────────────────────────────────────────┐
│ Test Specs │
│ tests/**/*.spec.ts │
└────────────────────┬────────────────────────┘
│ uses
┌────────────────────▼────────────────────────┐
│ Test Fixtures │
│ testFixtures/fixture.js │
│ (injects page objects into tests) │
└────────────────────┬────────────────────────┘
│ instantiates
┌────────────────────▼────────────────────────┐
│ Page Classes (POM) │
│ pages/*.js │
│ (actions: login, navigate, click, fill) │
└──────────┬─────────────────┬────────────────┘
│ extends │ imports
┌──────────▼──────┐ ┌───────▼──────────────┐
│ BasePage.js │ │ Selectors/Locators │
│ (50+ utilities) │ │ pageobjects/*.js │
└─────────────────┘ └──────────────────────┘
│
┌──────────▼──────────────────────────────────┐
│ Configuration & Data │
│ config.js → URLs, nav paths │
│ data/*.json → Test data, search values │
│ env/.env.* → Environment-specific URLs │
└─────────────────────────────────────────────┘
| Principle | Implementation |
|---|---|
| Separation of concerns | Selectors in pageobjects/, actions in pages/ |
| DRY | BasePage.js provides 50+ shared utility methods |
| Single source of truth | All URLs in config.js, all test data in globalData.json |
| Reusability | Custom fixtures inject page objects cleanly |
| Environment isolation | .env.* files per environment, never hardcoded URLs |
BasePage provides these ready-to-use methods in all page classes:
| Category | Methods |
|---|---|
| Navigation | open(url), getUrl(), getTitle(), waitForPageLoad() |
| Click | waitAndClick(selector), waitAndHardClick(selector) |
| Input | inputText(selector, text), waitAndFill(selector, text), clearTextField(selector) |
| Keyboard | keyPress(selector, key) |
| Assertions | verifyTextOnPage(text), verifyElementText(sel, text), verifyElementVisible(sel), verifyElementContainsText(sel, text), verifyFlashMessage(text) |
| Element state | isElementVisible(sel), isElementNotVisible(sel), isElementEnabled(sel), isElementChecked(sel) |
| Table | isTableLoaded(selector), getTableData(tableSelector, dataGrid) |
| Dropdown | selectValueFromDropdown(sel, text), selectValueInDropDownField(name, input, combo) |
| Screenshots | takeScreenShot(), takeFullPageScreenShot(imgName) |
| File ops | downloadAndVerify(fileName), uploadDocument(fileName), searchInXLSX(filePath, text), isStringInXLSX(filePath, text) |
| Drag & drop | dragAndDropElement(selector, targetDiv) |
| Row actions | clickRowDeleteButton(rowDataKey), clickOnYesOptionDeletePopup(selector) |
| Tabs/Navigation | clickTab(tabName, selector) |
| Misc | generateRandomString(name), delay(ms), centerScroll(), pause() |
Run the setup test first:
npx playwright test tests/loging.setup.tsEnsure ENV is set correctly and the corresponding file exists in env/:
$env:ENV = "UAT" # Must match the filename: env/.env.UATnpx playwright install chromium- Check if the selector in
pageobjects/is still valid for your app version - Increase timeout in
playwright.config.tsif needed - Use
--debugto step through the test interactively
rm -rf node_modules
npm installEnsure the screenshots/ folder exists:
mkdir screenshots- Create a branch:
git checkout -b feature/your-module-name - Follow the Adding a New Module steps
- Ensure tests pass locally before raising a PR
- Test naming convention:
verifyFeatureNameAndBehavior.spec.ts - Use
test.describe()with a clear description and tag (e.g.@sanity)
Internal use only. Not for public distribution.