@netanelh2/playwright-framework
v6.1.0
Published
A robust Playwright TypeScript testing framework with Page Object Model, smart locators, and utilities
Downloads
252
Maintainers
Readme
@netanelh2/playwright-framework
A robust Playwright TypeScript testing framework with Page Object Model, smart locators, and utilities for scalable E2E testing.
See also: AI Coding Agent Guide (AGENTS.md) for advanced usage, conventions, and project structure tips.
Features
- 🎭 Built on Playwright: Leverages the power of Playwright for reliable E2E testing
- 🏗️ Page Object Model: Well-structured page objects extending
BasePagefor consistency - 🎯 Smart Locators: Flexible locator system supporting CSS selectors and role-based locators with automatic fallback
- 🔧 Test Fixtures: Pre-configured fixtures for dependency injection and page management
- 🛠️ Utilities: Helper functions for environment variables, array operations, and more
- 📦 TypeScript 5.x: Full type safety with comprehensive type definitions
- ⚡ ES Modules: Modern module system with optimized imports
- 🔒 Type-Safe Locators: Role-based locator types with
StringOrRoleLocatorType
Installation
npm install --save-dev @netanelh2/playwright-framework @playwright/test typescriptNote: This package uses modern ES modules. Ensure your project has
"type": "module"inpackage.json.
Quick Start
1. Define Locators with Type Safety
import type {StringOrRoleLocatorType} from '@netanelh2/playwright-framework'
// Centralized locators with role-based pattern (preferred)
export const LOGIN_PAGE_LOCATORS = {
usernameField: {role: 'textbox', name: 'Username'},
passwordField: {role: 'textbox', name: 'Password'},
submitButton: {role: 'button', name: 'Sign In'},
errorMessage: '.error-message',
} as const satisfies Record<string, StringOrRoleLocatorType>2. Create Type-Safe Page Objects
import {BasePage, type Page} from '@netanelh2/playwright-framework'
import {test} from '../fixtures/testSetup'
import {LOGIN_PAGE_LOCATORS as L} from '../locators/login/Login_Page'
export class LoginPage extends BasePage {
constructor(page: Page) {
super(page)
}
async navigateTo(): Promise<void> {
await this.gotoURL('/login')
}
async login(username: string, password: string): Promise<void> {
await test.step('User login flow', async () => {
const usernameLocator = this.extractLocator(L.usernameField)
const passwordLocator = this.extractLocator(L.passwordField)
await usernameLocator.fill(username)
await passwordLocator.fill(password)
await this.clickOnElement(L.submitButton)
})
}
async validateLoaded(): Promise<void> {
await this.validateVisibility(L.usernameField)
await this.validateVisibility(L.submitButton)
}
async validateLoginError(): Promise<void> {
await this.validateVisibility(L.errorMessage)
}
}3. Define User Types and Test Data
// src/types/userTypes.ts
export interface UserCredentials {
username: string
password: string
role?: 'admin' | 'user' | 'guest'
}
// src/data/users.ts
import type {UserCredentials} from '../types/userTypes'
export const STANDARD_USER: UserCredentials = {
username: 'standard_user',
password: 'secret_sauce',
role: 'user',
}
export const ADMIN_USER: UserCredentials = {
username: 'admin',
password: 'admin123',
role: 'admin',
}4. Create Custom Fixtures with Type Safety
// src/fixtures/testSetup.ts
import {test as baseTest} from '@netanelh2/playwright-framework'
import {LoginPage} from '../pages/LoginPage'
import {DashboardPage} from '../pages/DashboardPage'
interface CustomFixtures {
loginPage: LoginPage
dashboardPage: DashboardPage
}
export const test = baseTest.extend<CustomFixtures>({
loginPage: async ({page}, use) => {
await use(new LoginPage(page))
},
dashboardPage: async ({page}, use) => {
await use(new DashboardPage(page))
},
})
export {expect} from '@netanelh2/playwright-framework'5. Write Type-Safe Tests
// src/tests/login.spec.ts
import {STANDARD_USER, ADMIN_USER} from '../data/users'
import {test} from '../fixtures/testSetup'
test.describe('Login Flow @sanity', () => {
test('should login successfully with standard user', async ({
loginPage,
dashboardPage,
}) => {
await loginPage.navigateTo()
await loginPage.validateLoaded()
await loginPage.login(STANDARD_USER.username, STANDARD_USER.password)
await dashboardPage.validateUserLoggedIn(STANDARD_USER.username)
})
test('should display error for invalid credentials', async ({loginPage}) => {
await loginPage.navigateTo()
await loginPage.login('invalid_user', 'wrong_password')
await loginPage.validateLoginError()
})
})
test.describe('Admin Login @regression', () => {
test('should allow admin access', async ({loginPage, dashboardPage}) => {
await loginPage.navigateTo()
await loginPage.login(ADMIN_USER.username, ADMIN_USER.password)
await dashboardPage.validateAdminAccess()
})
})Core Classes
BasePage
Base class for all page objects providing common functionality with type-safe methods:
import {
BasePage,
type Page,
type StringOrRoleLocatorType,
} from '@netanelh2/playwright-framework'
export class DashboardPage extends BasePage {
private readonly LOCATORS = {
header: {role: 'banner'},
userMenu: {role: 'button', name: 'User Menu'},
welcomeMessage: '.welcome-message',
} as const satisfies Record<string, StringOrRoleLocatorType>
constructor(page: Page) {
super(page)
}
// Navigation
async navigateTo(): Promise<void> {
await this.gotoURL('/dashboard')
}
// Validation
async validateLoaded(): Promise<void> {
await this.validateVisibility(this.LOCATORS.header)
await this.validateURL('/dashboard')
}
async validateUserLoggedIn(username: string): Promise<void> {
await this.validateText(
this.LOCATORS.welcomeMessage,
`Welcome, ${username}`,
)
}
// Interactions
async openUserMenu(): Promise<void> {
await this.clickOnElement(this.LOCATORS.userMenu)
}
}Available BasePage Methods
Navigation Methods
gotoURL(url: string): Promise<void>- Navigate to a specific URLvalidateURL(expectedURL: string): Promise<void>- Validate current page URL
Element Interaction Methods
clickOnElement(locator: StringOrRoleLocatorType): Promise<void>- Click on an elementhoverOnElement(locator: StringOrRoleLocatorType): Promise<void>- Hover over an elementfillInput(locator: StringOrRoleLocatorType, text: string): Promise<void>- Fill input fields
Validation Methods
validateVisibility(locator: StringOrRoleLocatorType): Promise<void>- Check element visibilityvalidateText(locator: StringOrRoleLocatorType, text: string): Promise<void>- Validate element textwaitForSelectorState(locator: StringOrRoleLocatorType, options?: WaitForSelectorOptions): Promise<void>- Wait for element state
LocatorUtils
Utility class for flexible locator handling with automatic fallback strategies:
import {
LocatorUtils,
type Page,
type StringOrRoleLocatorType,
} from '@netanelh2/playwright-framework'
export class CustomPage extends LocatorUtils {
constructor(page: Page) {
super(page)
}
async getElementText(locator: StringOrRoleLocatorType): Promise<string> {
const element = this.extractLocator(locator)
return (await element.textContent()) ?? ''
}
async isElementVisible(locator: StringOrRoleLocatorType): Promise<boolean> {
const element = this.extractLocator(locator)
return await element.isVisible()
}
}Locator Types
The framework supports multiple locator strategies with TypeScript type safety:
String Locators (CSS, XPath, Text)
// CSS Selector
const button = '#submit-button'
const input = '.username-input'
// XPath
const element = '//button[@type="submit"]'
// Text selector
const link = 'text=Click Here'Role-Based Locators (Preferred)
import type {RoleLocator, AriaRole} from '@netanelh2/playwright-framework'
// Simple role locator
const submitButton: RoleLocator = {
role: 'button',
name: 'Submit',
}
// Role locator with parent context
const usernameInput: RoleLocator = {
parent: '.login-form',
role: 'textbox',
name: 'Username',
}
// Available ARIA roles (type-safe)
const heading: RoleLocator = {
role: 'heading',
name: 'Welcome',
}Combined Locator Definition
import type {StringOrRoleLocatorType} from '@netanelh2/playwright-framework'
export const PAGE_LOCATORS = {
// Role-based (preferred for accessibility)
submitButton: {role: 'button', name: 'Submit'},
emailInput: {role: 'textbox', name: 'Email'},
// CSS selectors (when role-based isn't suitable)
errorMessage: '.error-message',
loadingSpinner: '#loading',
// With parent context
formSubmit: {
parent: '.checkout-form',
role: 'button',
name: 'Complete Order',
},
} as const satisfies Record<string, StringOrRoleLocatorType>Utilities
Environment Variables with Type Safety
import {getEnvCredentials} from '@netanelh2/playwright-framework'
// Load environment variables (from .env file)
const baseUrl = getEnvCredentials('BASE_URL')
const apiKey = getEnvCredentials('API_KEY')
const timeout = getEnvCredentials('DEFAULT_TIMEOUT')
// Usage in page objects
export class ApiPage extends BasePage {
private readonly BASE_URL = getEnvCredentials('BASE_URL')
async navigateTo(path: string): Promise<void> {
await this.gotoURL(`${this.BASE_URL}${path}`)
}
}Array Utilities
import {findItemByProperty} from '@netanelh2/playwright-framework'
interface User {
id: number
email: string
role: string
}
const users: User[] = [
{id: 1, email: '[email protected]', role: 'admin'},
{id: 2, email: '[email protected]', role: 'user'},
]
// Find user by email
const admin = findItemByProperty(users, 'email', '[email protected]')
// Result: { id: 1, email: '[email protected]', role: 'admin' }
// Find user by role
const regularUser = findItemByProperty(users, 'role', 'user')
// Result: { id: 2, email: '[email protected]', role: 'user' }Configuration
The framework works with standard Playwright configuration. Here's a recommended setup:
// playwright.config.ts
import {defineConfig, devices} from '@playwright/test'
export default defineConfig({
testDir: './src/tests',
timeout: 60 * 1000,
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 4 : undefined,
grep: process.env.TEST_TAGS ? new RegExp(process.env.TEST_TAGS) : undefined,
reporter: [
[
'html',
{
open: 'never',
outputFolder: 'playwright-report',
},
],
['list'],
],
use: {
baseURL: 'https://www.saucedemo.com',
trace: 'on',
screenshot: {
mode: 'only-on-failure',
fullPage: true,
},
video: 'retain-on-failure',
},
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
},
},
{
name: 'firefox',
use: {
...devices['Desktop Firefox'],
},
},
{
name: 'webkit',
use: {
...devices['Desktop Safari'],
},
},
],
})Best Practices
1. Extend BasePage for All Page Objects
import {BasePage, type Page} from '@netanelh2/playwright-framework'
export class CheckoutPage extends BasePage {
constructor(page: Page) {
super(page)
}
// Your page-specific methods
}2. Centralize Locators in Separate Files
// src/locators/checkout/Checkout_Page.ts
import type {StringOrRoleLocatorType} from '@netanelh2/playwright-framework'
export const CHECKOUT_PAGE_LOCATORS = {
firstName: {role: 'textbox', name: 'First Name'},
lastName: {role: 'textbox', name: 'Last Name'},
continueButton: {role: 'button', name: 'Continue'},
} as const satisfies Record<string, StringOrRoleLocatorType>3. Use Role-Based Locators When Possible
// ✅ Preferred: Role-based (accessibility-friendly)
const loginButton = {role: 'button', name: 'Login'}
// ⚠️ Acceptable: CSS when role-based isn't suitable
const errorMessage = '.error-message'
// ❌ Avoid: Fragile selectors
const button = 'div > div > button:nth-child(3)'4. Leverage test.step() for Readable Test Reports
async login(username: string, password: string): Promise<void> {
await test.step('Fill login credentials', async () => {
await this.fillInput(L.usernameField, username);
await this.fillInput(L.passwordField, password);
});
await test.step('Submit login form', async () => {
await this.clickOnElement(L.submitButton);
});
}5. Use Environment Variables for Configuration
// .env
BASE_URL=https://staging.example.com
API_ENDPOINT=https://api.example.com
DEFAULT_TIMEOUT=30000
// Usage
import { getEnvCredentials } from '@netanelh2/playwright-framework';
export const BASE_URL = getEnvCredentials('BASE_URL');
export const API_ENDPOINT = getEnvCredentials('API_ENDPOINT');6. Implement Page Validation Methods
export class ProductPage extends BasePage {
async validateLoaded(): Promise<void> {
await this.validateVisibility(PRODUCT_PAGE_LOCATORS.productGrid)
await this.validateVisibility(PRODUCT_PAGE_LOCATORS.filterSection)
await this.validateURL('/products')
}
}Available Imports
The framework provides multiple import paths for different use cases:
// Main framework exports (classes, utilities, types)
import {
BasePage,
LocatorUtils,
getEnvCredentials,
findItemByProperty,
} from '@netanelh2/playwright-framework'
// Type imports
import type {
StringOrRoleLocatorType,
RoleLocator,
AriaRole,
Page,
Locator,
PageFixtures,
} from '@netanelh2/playwright-framework'
// Test fixtures (for custom fixtures)
import {test as baseTest} from '@netanelh2/playwright-framework'
// Re-exported Playwright types
import type {
Browser,
BrowserContext,
PlaywrightTestConfig,
TestInfo,
} from '@netanelh2/playwright-framework'Project Structure
my-project/
├── src/
│ ├── core/ # Custom base classes (optional)
│ ├── data/ # Test data (users, products, etc.)
│ │ └── users.ts
│ ├── fixtures/ # Custom test fixtures
│ │ └── testSetup.ts
│ ├── helpers/ # Utility functions
│ ├── locators/ # Centralized locators
│ │ ├── login/
│ │ │ └── Login_Page.ts
│ │ └── dashboard/
│ │ └── Dashboard_Page.ts
│ ├── pages/ # Page Object Model classes
│ │ ├── LoginPage.ts
│ │ └── DashboardPage.ts
│ ├── tests/ # Test specifications
│ │ ├── login.spec.ts
│ │ └── dashboard.spec.ts
│ └── types/ # TypeScript type definitions
│ ├── fixtureTypes.ts
│ └── userTypes.ts
├── .env # Environment variables (gitignored)
├── .env.example # Environment template
├── biome.json # Biome configuration
├── playwright.config.ts # Playwright configuration
├── package.json
└── tsconfig.json # TypeScript configurationCreating New Projects
For scaffolding new projects with this framework, use the CLI tool:
npx @netanelh2/create-playwright-project my-new-projectThis creates a complete project structure with:
- Pre-configured Playwright settings
- Example page objects and tests
- Proper TypeScript configuration
- Biome for code quality
- Environment setup
TypeScript Configuration
Recommended tsconfig.json for optimal compatibility:
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"lib": ["ES2022"],
"moduleResolution": "bundler",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"types": ["node", "@playwright/test"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}Contributing
Contributions are welcome! Please visit the main repository for contribution guidelines.
License
MIT - see LICENSE for details.
Support
Built with ❤️ for the Playwright testing community
