@playwright-labs/decorators
v1.0.1
Published
decorators support for Playwright for your OOP style testing
Maintainers
Readme
@playwright-labs/decorators
TypeScript decorators for Playwright Test - Write elegant, object-oriented tests with class-based syntax
Table of Contents
- Quick Start
- Features
- Installation
- Basic Usage
- Core Decorators
- Lifecycle Hooks
- Documentation
- Examples
- TypeScript Configuration
- License
Quick Start
import { describe, test, beforeEach, afterEach } from '@playwright-labs/decorators';
@describe('Login Tests')
class LoginTests {
@beforeEach()
async setup() {
await this.page.goto('https://example.com/login');
}
@test('should login successfully')
async testLogin() {
await this.page.fill('#username', '[email protected]');
await this.page.fill('#password', 'password123');
await this.page.click('#login-button');
await expect(this.page).toHaveURL('/dashboard');
}
@afterEach()
async cleanup() {
await this.page.context().clearCookies();
}
}Features
✨ Class-Based Tests - Organize tests using OOP principles
🎯 Lifecycle Decorators - @beforeAll, @beforeEach, @afterEach, @afterAll
⏱️ Timeout Control - @timeout() for classes, methods, and fixtures
🔧 Test-Specific Hooks - @before() and @after() for individual tests
📝 Rich Annotations - @tag(), @skip(), @fixme(), @slow(), @annotate()
🎭 Page Object Support - Perfect for Page Object Model pattern
📊 Data-Driven Tests - @test.each() with template strings
🔌 Fixture Support - @use() and @use.define() for custom fixtures
📎 Attachments - @attachment() for test artifacts
🔍 Full Type Safety - Complete TypeScript support
✅ CI/CD Ready - Comprehensive runtime validations
Installation
npm install @playwright-labs/decorators @playwright/testBasic Usage
Simple Test Class
import { describe, test } from '@playwright-labs/decorators';
@describe('Calculator Tests')
class CalculatorTests {
@test('should add numbers')
async testAddition() {
const result = 2 + 2;
expect(result).toBe(4);
}
@test('should multiply numbers')
async testMultiplication() {
const result = 3 * 4;
expect(result).toBe(12);
}
}With Lifecycle Hooks
import { describe, test, beforeAll, afterAll, beforeEach, afterEach } from '@playwright-labs/decorators';
@describe('API Tests')
class ApiTests {
static apiUrl: string;
@beforeAll()
static async setupAll() {
this.apiUrl = 'https://api.example.com';
}
@beforeEach()
async setupEach() {
console.log('Running test...');
}
@test('should fetch users')
async testFetchUsers() {
// Test implementation
}
@afterEach()
async cleanupEach() {
console.log('Test completed');
}
@afterAll()
static async cleanupAll() {
// Global cleanup
}
}Core Decorators
Test Decorators
| Decorator | Description | Example |
|-----------|-------------|---------|
| @describe() | Define test suite | @describe('User Tests') |
| @test() | Define test case | @test('should login') |
| @test.each() | Data-driven tests | @test.each([...], 'test $1') |
| @skip() | Skip test | @skip() |
| @fixme() | Mark test as broken | @fixme() |
| @slow() | Mark test as slow | @slow() |
Lifecycle Decorators
| Decorator | Scope | Runs | Example |
|-----------|-------|------|---------|
| @beforeAll() | Suite | Once before all tests | Setup database |
| @beforeEach() | Test | Before each test | Reset state |
| @before() | Test | Before specific test | Test-specific setup |
| @after() | Test | After specific test | Test-specific cleanup |
| @afterEach() | Test | After each test | Clear cookies |
| @afterAll() | Suite | Once after all tests | Close connections |
Utility Decorators
| Decorator | Description | Example |
|-----------|-------------|---------|
| @timeout() | Set timeout | @timeout(5000) |
| @tag() | Add test tags | @tag('smoke', 'critical') |
| @annotate() | Add annotations | @annotate('type', 'description') |
| @attachment() | Add attachments | @attachment('screenshot.png', ...) |
| @use() | Use fixtures | @use('page', 'context') |
Lifecycle Hooks
Complete Lifecycle Order
@describe
↓
@beforeAll (static, once per suite)
↓
┌─ FOR EACH TEST ────────────┐
│ @beforeEach (per test) │
│ ↓ │
│ @before (test-specific) │
│ ↓ │
│ @test method │
│ ↓ │
│ @after (test-specific) │ ← Always runs, even on failure
│ ↓ │
│ @afterEach (per test) │
└─────────────────────────────┘
↓
@afterAll (static, once per suite)Example with All Hooks
@describe('Full Lifecycle Example')
@timeout(30000)
class FullLifecycleTests {
@beforeAll()
static async setupSuite() {
// Runs once before all tests
}
@beforeEach()
async setupTest() {
// Runs before each test
}
@test('with individual hooks')
@timeout(5000)
@before(async (self) => {
// Runs before this specific test
self.testData = await loadTestData();
})
@after(async (self) => {
// Runs after this specific test (even if test fails)
await self.cleanup();
})
async myTest() {
// Test implementation
}
@afterEach()
async cleanupTest() {
// Runs after each test
}
@afterAll()
static async cleanupSuite() {
// Runs once after all tests
}
}Documentation
📚 Full Documentation
- Getting Started Guide - Installation and first test ✅
- Migration Guide - From traditional Playwright tests ✅
- API Reference - Complete API documentation ✅
- Edge Cases - Handling complex scenarios ✅
- Core Concepts - Understanding decorators and lifecycle ✅
- Lifecycle Hooks Guide - Master test lifecycle ✅
- Timeout Configuration - Managing test timeouts ✅
- Data-Driven Tests - Using @test.each() ✅
- Page Object Model - POM pattern with decorators ✅
- Fixtures Guide - Custom fixtures with @use and @use.define ✅
- Best Practices - Patterns and anti-patterns ✅
- Troubleshooting - Common issues and solutions ✅
- CI/CD Integration - Running in CI/CD pipelines ✅
🔧 Additional Resources
- Validation Guide - Runtime validation details
- Examples Directory - Real-world code examples
Examples
Data-Driven Tests
@describe('Login - Data Driven')
class LoginDataDrivenTests {
@test.each([
['[email protected]', 'password123'],
['[email protected]', 'admin456'],
['[email protected]', 'test789'],
], 'should login with $1')
async testLogin(email: string, password: string) {
await this.page.fill('#email', email);
await this.page.fill('#password', password);
await this.page.click('#login');
await expect(this.page).toHaveURL('/dashboard');
}
}Page Object Model
class LoginPage {
constructor(private page: Page) {}
async login(email: string, password: string) {
await this.page.fill('#email', email);
await this.page.fill('#password', password);
await this.page.click('#login');
}
}
@describe('Login with Page Objects')
class LoginPOMTests {
private loginPage: LoginPage;
@beforeEach()
async setup() {
this.loginPage = new LoginPage(this.page);
await this.page.goto('/login');
}
@test('should login successfully')
async testLogin() {
await this.loginPage.login('[email protected]', 'password123');
await expect(this.page).toHaveURL('/dashboard');
}
}Timeout Configuration
@describe('Performance Tests')
@timeout(60000) // 60 seconds for all tests
class PerformanceTests {
@test('fast test')
@timeout(5000) // Override: 5 seconds
async testFast() {
// Quick test
}
@test('slow test')
@timeout(120000) // Override: 2 minutes
async testSlow() {
// Long-running test
}
}Resource Cleanup
@describe('Database Tests')
class DatabaseTests {
@test('should handle transaction')
@before(async (self) => {
self.transaction = await db.beginTransaction();
})
@after(async (self) => {
// Always runs, even if test fails
if (self.transaction.isActive()) {
await self.transaction.rollback();
}
})
async testTransaction() {
await this.transaction.insert({ name: 'test' });
// Test logic
}
}Tags and Annotations
@describe('E2E Tests')
@tag('e2e', 'critical')
class E2ETests {
@test('checkout flow')
@tag('smoke')
@annotate('type', 'user-journey')
@timeout(120000)
async testCheckout() {
// Complete checkout flow
}
@test('slow operation')
@slow()
@annotate('performance', 'requires-optimization')
async testSlowOperation() {
// Slow test
}
}TypeScript Configuration
Add to your tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": false,
"target": "ES2022",
"lib": ["ES2022"]
}
}Note: This library uses TC39 Stage 3 decorators (NOT legacy
experimentalDecorators).
Why Use Decorators?
Before (Traditional Playwright)
test.describe('Login Tests', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login');
});
test('should login', async ({ page }) => {
await page.fill('#email', '[email protected]');
await page.click('#login');
await expect(page).toHaveURL('/dashboard');
});
});After (With Decorators)
@describe('Login Tests')
class LoginTests {
@beforeEach()
async setup() {
await this.page.goto('/login');
}
@test('should login')
async testLogin() {
await this.page.fill('#email', '[email protected]');
await this.page.click('#login');
await expect(this.page).toHaveURL('/dashboard');
}
}Benefits
- ✅ Better Organization - Class-based structure groups related tests
- ✅ Reusability - Share setup/teardown logic via class inheritance
- ✅ Type Safety - Full TypeScript support with proper
thiscontext - ✅ Page Object Pattern - Natural fit for POM architecture
- ✅ Less Boilerplate - Cleaner, more readable test code
- ✅ Flexible Lifecycle - More granular control with @before/@after
- ✅ IDE Support - Better autocomplete and refactoring
License
MIT
Contributing
Contributions are welcome! Please read our contributing guidelines first.
Support
Made with ❤️ by the Playwright community
