# Deployment and DevOps Standards for Playwright
This document outlines the standards and best practices for deploying and integrating Playwright tests into your DevOps pipelines. These guidelines ensure consistency, reliability, and maintainability across your Playwright testing infrastructure.
## 1. Build Processes and CI/CD Integration
### 1.1. Standard: Use a dedicated CI/CD pipeline for Playwright tests.
**Do This:** Configure a separate CI/CD pipeline stage specifically for running Playwright tests.
**Don't Do This:** Mix Playwright tests with other build or deployment steps without proper isolation.
**Why:** Segregation ensures focused resource allocation, faster test execution times, and easier troubleshooting.
**Example:**
"""yaml
# .gitlab-ci.yml (GitLab CI example)
stages:
- build
- test_playwright
- deploy
build:
stage: build
script:
- npm install
- npm run build
test_playwright:
stage: test_playwright
image: mcr.microsoft.com/playwright:v1.40.0-focal # Use latest version
script:
- npm install
- npx playwright install --with-deps chromium
- npx playwright test
artifacts:
when: always
reports:
junit: playwright-report/junit.xml # For test reporting
"""
**Anti-pattern:** Running Playwright tests directly in the same stage as application build, potentially blocking deployments if tests fail.
### 1.2. Standard: Install dependencies and browsers correctly in the CI environment.
**Do This:** Use "npx playwright install --with-deps chromium" in your CI script.
**Don't Do This:** Rely on globally installed Playwright or manually managed browsers.
**Why:** "playwright install" ensures consistent browser versions and dependencies in the CI environment. The "--with-deps" flag resolves OS-level dependencies, crucial for CI environments. Specifying the browser (e.g., "chromium") reduces installation time and resources if other browsers aren't tested.
**Example:**
"""bash
# Jenkinsfile (Declarative Pipeline)
pipeline {
agent {
docker {
image 'mcr.microsoft.com/playwright:v1.40.0-focal' // Use latest version
}
}
stages {
stage('Playwright Tests') {
steps {
sh 'npm install'
sh 'npx playwright install --with-deps chromium'
sh 'npx playwright test'
}
}
}
post {
always {
// Archive test results
junit 'playwright-report/junit.xml'
}
}
}
"""
**Anti-pattern:** Forgetting to install browser dependencies, leading to test failures in CI.
### 1.3. Standard: Utilize Playwright's built-in reporters for CI integration.
**Do This:** Configure reporters like "junit" in "playwright.config.ts" to generate reports compatible with CI/CD systems.
**Don't Do This:** Rely solely on console output for test results.
**Why:** Structured reports (e.g., JUnit XML) enable comprehensive test result aggregation, trend analysis, and integration with CI/CD dashboards.
**Example:**
"""typescript
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
reporter: [
['list'],
['junit', { outputFile: 'playwright-report/junit.xml' }],
['html', { outputFolder: 'playwright-report/html' }]
],
});
"""
**Anti-pattern:** Ignoring Playwright's reporters and missing out on detailed test analytics in your CI environment.
### 1.4. Standard: Implement robust retry mechanisms and test flakiness detection.
**Do This:** Use Playwright's "retries" option in "playwright.config.ts" and investigate flaky tests identified by CI reporting.
**Don't Do This:** Ignore flaky tests or disable retries to mask underlying issues.
**Why:** Playwright retries help to mitigate transient environmental issues or timing related test failures. Addressing flakiness improves test reliability and confidence.
**Example:**
"""typescript
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
retries: 2, // Retry failing tests up to 2 times
// ... other configurations
});
"""
**Anti-pattern:** Ignoring flaky tests and attributing random failures to "CI glitches."
### 1.5. Standard: Configure environment variables for different environments.
**Do This:** Use environment variables to manage environment-specific configurations like API endpoints, database configurations, and access keys. Access these variables via "process.env".
**Don't Do This:** Hardcode environment-specific values directly into your test code or configuration files.
**Why:** Environment variables enhance portability and security, allowing for easy configuration changes without modifying code. Different CI/CD environments (dev, staging, production) can be configured through variable assignment.
**Example:**
"""typescript
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
baseURL: process.env.BASE_URL || 'http://localhost:3000', // Default fallback
},
});
// In a test file:
import { test, expect } from '@playwright/test';
test('Verify API endpoint', async ({ page }) => {
const apiEndpoint = process.env.API_ENDPOINT;
const response = await page.request.get("${apiEndpoint}/data");
expect(response.status()).toBe(200);
});
"""
**Anti-pattern:** Committing environment-specific configuration details (e.g., API keys) to the source code repository.
### 1.6. Standard: Implement parallel test execution.
**Do This:** Configure the "workers" option in "playwright.config.ts" to maximize parallel execution, use sharding in CI, and consider Playwright's experimental sharding via CLI for optimal performance.
**Don't Do This:** Run tests serially, which significantly increases execution time.
**Why:** Parallel execution dramatically reduces the overall test execution time, especially in large test suites. Note that increasing the number of workers beyond the available CPU cores might lead to diminishing returns. Sharding ensures tests are distributed evenly across parallel jobs in the CI.
**Example:**
"""typescript
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
workers: process.env.CI ? 4 : undefined, // CI typically offers more resources
// ... other configurations
});
//Example using CLI sharding from the cloud provider.
//npx playwright test --shard=1/2
//npx playwright test --shard=2/2
"""
**Anti-pattern:** Running all tests sequentially, resulting in excessively long CI build times. Not maximizing the concurrent execution limits provided by your CI/CD platform.
### 1.7 Standard: Leverage caching to speed up CI builds.
**Do This:** Cache node_modules and Playwright browser binaries in your CI/CD pipeline.
**Don't Do This:** Install dependencies and browsers from scratch on every build.
**Why:** Caching significantly reduces build times, especially for frequently executed pipelines.
**Example:**
"""yaml
# .gitlab-ci.yml
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
- .cache/ms-playwright/
"""
**Anti-pattern:** Not taking advantage of CI/CD caching capabilities, leading to unnecessary delays in build times.
## 2. Production Considerations
### 2.1. Standard: Disable or remove Playwright-specific code in production builds.
**Do This:** Utilize conditional compilation or feature flags to exclude Playwright test setup and teardown logic from production code.
**Don't Do This:** Deploy Playwright-specific testing code to production environments
**Why:** Playwright testing code is not needed in production and can introduce security risks or performance overhead.
**Example:**
"""typescript
// app.ts (Example using feature flags)
const isTesting = process.env.NODE_ENV === 'test';
async function setupApplication() {
// ... production app initialization
if (isTesting) {
// Do nothing in production mode. This entire block and imports
// should be stripped from the production build.
console.log("Running in test environment. Skipping Playwright setup.");
}
}
"""
**Anti-pattern:** Including Playwright libraries or test-specific utilities in production bundles, increasing bundle size and potential security vulnerabilities.
### 2.2. Standard: Secure sensitive data appropriately.
**Do This:** Use environment variables or secret management solutions to store sensitive data like API keys and database passwords.
**Don't Do This:** Hardcode sensitive data in your Playwright tests or configuration files.
**Why:** Hardcoded credentials pose a significant security risk. Environment variables and secret management tools provide a secure and manageable way to handle sensitive data.
**Example:**
"""typescript
// Retrieve API key from environment variable:
const apiKey = process.env.API_KEY;
// Example showing Azure Key Vault integration in CI Pipeline
# azure-pipelines.yml
- task: AzureKeyVault@2
inputs:
azureSubscription: 'your-azure-subscription'
keyVaultName: 'your-key-vault-name'
secretsFilter: '*'
runAsPreJob: true
"""
**Anti-pattern:** Embedding API keys or passwords directly within test scripts, making them accessible in version control systems.
### 2.3. Standard: Monitor and log Playwright test execution.
**Do This:** Integrate Playwright test execution logs into a centralized logging system for analysis and troubleshooting.
**Don't Do This:** Rely solely on CI/CD console output for log analysis.
**Why:** Centralized logging enhances visibility into test execution, facilitates root cause analysis, and allows for proactive identification of performance bottlenecks or issues.
**Example:**
"""typescript
// Example using a logging library (winston)
import winston from 'winston';
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'playwright-test.log' }),
],
});
test('My test', async ({ page }) => {
logger.info('Starting test: My test');
// ... test steps
logger.info('Test completed successfully: My test');
});
"""
**Anti-pattern:** No centralized logging, making debugging and monitoring of test runs difficult.
### 2.4. Standard: Implement comprehensive error handling.
**Do This:** Implement custom error handling and reporting mechanisms to catch and log exceptions during test execution.
**Don't Do This:** Allow tests to crash silently without detailed error information.
**Why:** Proper error handling facilitates prompt identification and resolution of issues, minimizing downtime and improving the overall quality of your application.
**Example:**
"""typescript
import { test, expect } from '@playwright/test';
test('My Test', async ({ page }) => {
try {
await page.goto('https://example.com');
await expect(page.locator('h1')).toHaveText('Example Domain');
} catch (error) {
console.error('Test failed:', error);
throw error; // Re-throw the error to fail the test
}
});
"""
**Anti-pattern:** Ignoring exceptions and missing critical error messages from test runs.
### 2.5 Standard: Version control configurations.
**Do This:** Store Playwright configuration files (playwright.config.ts), test scripts and related setup and helper files in a version control system.
**Don't Do This:** Keep Playwright files outside of version control.
**Why:** Version control systems ensure that changes to Playwright configurations and test scripts are tracked, allowing for easy rollback, collaboration and auditing.
**Example:** Store configurations in Git, and use GitFlow or similar branching strategies to manage changes.
**Anti-pattern:** Having Playwright configuration files not under version control, it makes difficult collaboration and change management.
## 3. Modern Approaches and Patterns
### 3.1. Standard: Embrace containerization (Docker) for consistent test environments.
**Do This:** Define a Dockerfile that encapsulates the entire test environment, ensuring consistent execution across different machines and CI/CD pipelines. Utilize multi-stage builds to minimize the image size.
**Don't Do This:** Rely on inconsistent local environments or manual configuration.
**Why:** Containers provide isolated, reproducible test environments, eliminating discrepancies and ensuring reliable test execution.
**Example:**
"""dockerfile
# Use a lightweight base image
FROM mcr.microsoft.com/playwright:v1.40.0-focal AS base # Use latest version
WORKDIR /app
# Copy package.json and package-lock.json to the working directory
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy the rest of the application code
COPY . .
# Build application
RUN npm run build
# Define command to run the tests
CMD ["npx", "playwright", "test"]
"""
**Anti-pattern:** Running tests in different environments, leading to inconsistent test results and difficult troubleshooting.
### 3.2. Standard: Use Infrastructure as Code (IaC) for managing Playwright infrastructure.
**Do This:** Define infrastructure using tools like Terraform or CloudFormation to automate the provisioning and management of Playwright test environments.
**Don't Do This:** Manually configure test environments, which is error-prone and difficult to scale.
**Why:** IaC enables reproducible, scalable, and auditable infrastructure deployments.
**Example:**
"""terraform
# terraform configuration to create a VM on Azure to run Playwright tests
resource "azurerm_resource_group" "playwright_rg" {
name = "playwright-rg"
location = "West Europe"
}
resource "azurerm_virtual_machine" "playwright_vm" {
name = "playwright-vm"
location = azurerm_resource_group.playwright_rg.location
resource_group_name = azurerm_resource_group.playwright_rg.name
network_interface_ids = [azurerm_network_interface.playwright_nic.id]
vm_size = "Standard_DS2_v2"
storage_image_reference {
publisher = "Canonical"
offer = "UbuntuServer"
sku = "20_04-lts-gen2"
version = "latest"
}
storage_os_disk {
name = "playwright-osdisk"
caching = "ReadWrite"
create_option = "FromImage"
managed_disk_type = "Standard_LRS"
}
os_profile {
computer_name = "playwright-vm"
admin_username = "azureuser"
admin_password = "YourSecurePassword"
}
os_profile_linux_config {
disable_password_authentication = false
}
}
"""
**Anti-pattern:** Manually provisioning test environments, incurring inconsistencies and scalability limitations.
### 3.3. Standard: Integrate Playwright with cloud-based testing platforms.
**Do This:** Use cloud-based platforms like BrowserStack, Sauce Labs, or LambdaTest to run Playwright tests on a scalable and reliable infrastructure.
**Don't Do This:** Rely solely on local machines for test execution, which limits scalability and browser coverage.
**Why:** Cloud-based platforms offer a wide range of browser configurations, scaling capabilities, and detailed reporting features. They are designed to handle the performance and reporting requirements of Playwright. These services often provide specific Playwright integrations, streamlining setup and execution.
**Example:** Configuring Playwright to run on BrowserStack involves setting up the "playwright.config.ts" file with BrowserStack credentials:
"""typescript
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
connectOptions: {
wsEndpoint: "wss://cdp.browserstack.com/playwright?caps=${encodeURIComponent(JSON.stringify({
'browser': "chrome",
'browser_version': 'latest',
'os': 'osx',
'os_version': 'ventura',
'project': 'My Playwright Project',
'build': 'My CI Build',
'name': 'My Test'
}))}",
},
},
});
"""
**Anti-pattern:** Attempting to manage all browser configurations locally, limiting test coverage and scalability.
### 3.4. Standard: Implement health checks for test infrastructure.
**Do This:** Create health check endpoints to monitor the availability and performance of Playwright infrastructure, alerting on anomalies. This ensures that even the test execution environment is stable and reliable.
**Don't Do This:** Assume test infrastructure is always healthy, potentially masking underlying issues
**Why:** Health checks enable proactive detection and resolution of infrastructure problems, ensuring smooth test execution.
**Example (simple health check endpoint):**
"""javascript
// health-check.js (Example health check endpoint)
const http = require('http');
const server = http.createServer((req, res) => {
if (req.url === '/health') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'ok', timestamp: new Date() }));
} else {
res.writeHead(404);
res.end();
}
});
server.listen(3001, () => {
console.log('Health check server listening on port 3001');
});
"""
**Anti-pattern:** Lack of monitoring of test infrastructure, leading to prolonged outages and disruptions to test execution.
By adhering to these standards, development teams can establish a robust and reliable Playwright testing infrastructure, promoting quality, efficiency, and security across the entire software development lifecycle.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# Core Architecture Standards for Playwright This document outlines the core architectural standards for Playwright projects. These standards are designed to promote maintainability, scalability, performance, and security, leveraging the latest features of Playwright. Following these guidelines will ensure consistency across the project and facilitate collaboration among developers. ## 1. Project Structure and Organization A well-defined project structure is crucial for maintainability and scalability. Playwright projects should adopt a modular structure that separates concerns and promotes reusability. ### 1.1 Standard Directory Structure **Do This:** """ playwright-project/ ├── playwright.config.ts # Playwright configuration file ├── package.json # Project dependencies and scripts ├── tests/ # Test files │ ├── e2e/ # End-to-end tests │ │ ├── example.spec.ts # Example test file │ ├── components/ # Component-level tests (if applicable) │ │ ├── button.spec.ts # Example component test ├── page-objects/ # Page object models │ ├── base-page.ts # Base page class with common methods │ ├── home-page.ts # Home page object │ ├── login-page.ts # Login page object ├── utils/ # Utility functions and helpers │ ├── test-utils.ts # Custom test utilities │ ├── api-utils.ts # API interaction utilities ├── data/ # Test data files (e.g., JSON, CSV) │ ├── users.json # Example test data ├── reporters/ # Custom reporters (if needed) ├── .gitignore # Specifies intentionally untracked files that Git should ignore """ **Don't Do This:** * Mixing test files with other project files. * Omitting page object models, leading to duplicated selectors and logic. * Storing test data directly within test files. * Lack of a structured utility directory for reusable functions. **Why:** A clear and consistent directory structure improves discoverability and makes it easier for developers to locate and understand different parts of the project. Separation of concerns reduces complexity and promotes code reusability. **Example:** Below is an example of how to organize the "tests/" and "page-objects/" directories for a simple e-commerce application: """ playwright-project/ ├── tests/ │ ├── e2e/ │ │ ├── home-page.spec.ts # Tests for the home page functionality │ │ ├── product-details.spec.ts # Tests for the product details page │ │ ├── checkout.spec.ts # Tests for the checkout process ├── page-objects/ │ ├── base-page.ts # Base page class with common methods │ ├── home-page.ts # Home page object for the home page │ ├── product-details-page.ts # Page object for product details page │ ├── checkout-page.ts # Page object for the checkout page """ ### 1.2 Configuration Management **Do This:** * Use "playwright.config.ts" to centralize all Playwright configuration settings. * Define environment-specific configurations using environment variables. * Keep the configuration file clean by extracting complex logic into separate modules. **Don't Do This:** * Hardcoding configurations within test files. * Storing sensitive information (e.g., API keys, passwords) directly in the configuration file. **Why:** Centralized configuration management makes it easier to manage and modify project-wide settings. Environment-specific configurations ensure that tests can be run in different environments without code changes. Separation of concerns keeps the configuration file maintainable. **Example:** """typescript // playwright.config.ts import { defineConfig, devices } from '@playwright/test'; import dotenv from 'dotenv'; dotenv.config(); // Load environment variables from .env const baseURL = process.env.BASE_URL || 'https://example.com'; // Use default if not defined export default defineConfig({ testDir: './tests', fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 1 : undefined, reporter: 'html', use: { baseURL: baseURL , trace: 'on-first-retry', }, projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, { name: 'firefox', use: { ...devices['Desktop Firefox'] }, }, ], }); """ ### 1.3 Test Data Management **Do This:** * Store test data in separate files (e.g., JSON, CSV) within the "data/" directory. * Use meaningful names for test data files and variables. * Create data factories or helper functions to generate dynamic test data. **Don't Do This:** * Hardcoding test data within test files. * Using ambiguous or unclear variable names. * Storing sensitive data in plain text. **Why:** Decoupling test data from test logic improves maintainability and makes it easier to update test data independently. Data factories enable the creation of dynamic test data, which is useful for testing different scenarios. **Example:** """json // data/users.json [ { "username": "valid_user", "password": "password123" }, { "username": "invalid_user", "password": "wrong_password" } ] """ """typescript // tests/login.spec.ts import { test, expect } from '@playwright/test'; import users from '../data/users.json'; import { LoginPage } from '../page-objects/login-page'; test.describe('Login Tests', () => { let loginPage: LoginPage; test.beforeEach(async ({ page }) => { loginPage = new LoginPage(page); await page.goto('/login'); }); test('Login with valid credentials', async () => { const validUser = users.find(user => user.username === 'valid_user'); if (!validUser) { throw new Error('Valid user not found in users.json'); } await loginPage.login(validUser.username, validUser.password); await expect(loginPage.page.locator('.success-message')).toBeVisible(); }); test('Login with invalid credentials', async () => { const invalidUser = users.find(user => user.username === 'invalid_user'); if (!invalidUser) { throw new Error('Invalid user not found in users.json'); } await loginPage.login(invalidUser.username, invalidUser.password); await expect(loginPage.page.locator('.error-message')).toBeVisible(); }); }); """ ## 2. Page Object Model (POM) The Page Object Model (POM) is a design pattern that encapsulates page elements and actions into reusable objects. This pattern significantly improves test maintainability and reduces duplication. ### 2.1 Principles of POM **Do This:** * Create a separate class for each page or component of the application. * Define locators for page elements within the corresponding page object. * Implement methods for interacting with page elements. * Use a base page class for common methods and properties. **Don't Do This:** * Defining locators directly in test files. * Implementing business logic within page objects. * Creating overly complex or tightly coupled page objects. **Why:** POM promotes code reusability, reduces duplication, and improves test maintainability. By encapsulating page elements and actions, POM shields tests from changes in the application's UI. **Example:** """typescript // page-objects/base-page.ts import { Page } from '@playwright/test'; export class BasePage { constructor(public readonly page: Page) {} async navigate(url: string): Promise<void> { await this.page.goto(url); } async getTitle(): Promise<string> { return await this.page.title(); } } """ """typescript // page-objects/login-page.ts import { Page } from '@playwright/test'; import { BasePage } from './base-page'; export class LoginPage extends BasePage { readonly usernameInput = this.page.locator('#username'); readonly passwordInput = this.page.locator('#password'); readonly loginButton = this.page.locator('#login-button'); readonly errorMessage = this.page.locator('.error-message'); constructor(page: Page) { super(page); //Calls the constructor from BasePage to assign this.page correctly } async login(username: string, password: string): Promise<void> { await this.usernameInput.fill(username); await this.passwordInput.fill(password); await this.loginButton.click(); } async getErrorMessageText(): Promise<string | null> { return await this.errorMessage.textContent(); } } """ """typescript // tests/login.spec.ts import { test, expect } from '@playwright/test'; import { LoginPage } from '../page-objects/login-page'; test.describe('Login Page Tests', () => { let loginPage: LoginPage; test.beforeEach(async ({ page }) => { loginPage = new LoginPage(page); await loginPage.navigate('/login'); }); test('should display error message for invalid login', async () => { await loginPage.login('invalid_user', 'wrong_password'); await expect(loginPage.errorMessage).toBeVisible(); expect(await loginPage.getErrorMessageText()).toContain('Invalid credentials'); }); }); """ ### 2.2 Advanced POM Techniques **Do This:** * Use component-based page objects for complex UI components. * Implement fluent interfaces for chaining actions. * Use the "getByRole" locator for accessible element selection. **Don't Do This:** * Creating monolithic page objects that are difficult to maintain. * Overusing chaining, leading to unreadable code. **Why:** Component-based page objects promote reusability and reduce complexity. Fluent interfaces improve code readability and make tests more expressive. Using "getByRole" locators ensures accessibility and robustness. **Example (Component-based POM):** """typescript // page-objects/components/navbar.ts import { Page } from '@playwright/test'; export class NavbarComponent { constructor(private readonly page: Page) {} async navigateTo(linkText: string): Promise<void> { await this.page.getByRole('link', { name: linkText }).click(); } } """ """typescript // page-objects/home-page.ts import { Page } from '@playwright/test'; import { BasePage } from './base-page'; import { NavbarComponent } from './components/navbar'; export class HomePage extends BasePage { readonly navbar: NavbarComponent; constructor(page: Page) { super(page); this.navbar = new NavbarComponent(page); } async navigateToProducts(): Promise<void> { await this.navbar.navigateTo('Products'); } } //tests/home-page.spec.ts import { test, expect } from '@playwright/test'; import { HomePage } from '../page-objects/home-page'; test.describe('Home Page', () => { let homePage: HomePage; test.beforeEach(async ({ page }) => { homePage = new HomePage(page); await homePage.navigate('/'); }); test('should navigate to products page', async () => { await homePage.navigateToProducts(); await expect(page).toHaveURL('/products'); }); }); """ ## 3. Layered Architecture and Abstraction A layered architecture separates the test logic from the underlying implementation details, making the tests more resilient to changes and easier to maintain. ### 3.1 Abstraction Layers **Do This:** * Create a service layer to encapsulate API interactions. * Implement a data access layer to manage test data. * Use a configuration layer to centralize configuration settings. **Don't Do This:** * Directly interacting with APIs in test files. * Hardcoding test data within test logic. * Scattering configuration settings throughout the project. **Why:** Abstraction layers decouple different parts of the test automation framework, making it easier to modify or replace individual components without affecting other parts of the system. This improves maintainability, reusability, and testability. **Example (Service Layer for API Interactions):** """typescript // utils/api-utils.ts import { APIRequestContext, request } from '@playwright/test'; export class ApiUtils { private readonly requestContext: APIRequestContext; constructor(baseURL: string) { this.requestContext = request.newContext({ baseURL: baseURL }); } async authenticateUser(username: string, password: string) { const response = await this.requestContext.post('/api/auth/login', { data: { username: username, password: password, }, }); if (response.status() !== 200) { throw new Error("Authentication failed with status: ${response.status()}"); } return await response.json(); } async get(endpoint: string) : Promise<any>{ const response = await this.requestContext.get(endpoint); return response; } async post(endpoint: string, data: any) : Promise<any>{ const response = await this.requestContext.post(endpoint, {data: data}); return response; } async put(endpoint: string, data: any) : Promise<any>{ const response = await this.requestContext.put(endpoint, {data: data}); return response; } async delete(endpoint: string) : Promise<any>{ const response = await this.requestContext.delete(endpoint); return response; } } """ """typescript // tests/api.spec.ts import { test, expect } from '@playwright/test'; import { ApiUtils } from '../utils/api-utils'; test.describe('API Tests', () => { let apiUtils: ApiUtils; test.beforeEach(async ({ baseURL }) => { if (!baseURL) { throw new Error('baseURL is not defined. Check your playwright config file.'); } apiUtils = new ApiUtils(baseURL); }); test('should get user data from API', async () => { const response = await apiUtils.get('/users/1'); expect(response.status()).toBe(200); const userData = await response.json(); expect(userData.id).toBe(1); }); test('should create a new post via API', async () => { const postData = { title: 'My New Post', body: 'This is the body of my new post.', userId: 1 }; const response = await apiUtils.post('/posts', postData); expect(response.status()).toBe(201); // Assuming 201 is the success status for creating a resource const createdPost = await response.json(); expect(createdPost.title).toBe(postData.title); expect(createdPost.body).toBe(postData.body); }); }); """ ### 3.2 Custom Utilities and Helpers **Do This:** * Create custom utility functions for common tasks (e.g., date formatting, data validation). * Implement helper functions to simplify test setup and teardown. * Store utility functions in the "utils/" directory. **Don't Do This:** * Duplicating utility functions across multiple files. * Creating overly complex or tightly coupled utility functions. * Putting utility functions in the wrong directory. **Why:** Custom utilities and helpers promote code reusability and reduce duplication. They also simplify test setup and teardown, making tests more readable and maintainable. Storing utility functions in a dedicated directory improves discoverability and organization. **Example:** """typescript // utils/test-utils.ts import { Page } from '@playwright/test'; export async function login(page: Page, username: string, password: string): Promise<void> { await page.locator('#username').fill(username); await page.locator('#password').fill(password); await page.locator('#login-button').click(); } export function formatDate(date: Date): string { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return "${year}-${month}-${day}"; } """ """typescript // tests/example.spec.ts import { test, expect } from '@playwright/test'; import { login, formatDate } from '../utils/test-utils'; test('Example Test', async ({ page }) => { await page.goto('/login'); await login(page, 'testuser', 'password'); //Using utility function to login await expect(page.locator('.success-message')).toBeVisible(); const formattedDate = formatDate(new Date()); //Using utility function to format date console.log("Current date: ${formattedDate}"); }); """ ## 4. Asynchronous Programming and Error Handling Playwright relies heavily on asynchronous programming. Proper error handling is crucial for robustness and stability. ### 4.1 Async/Await Syntax **Do This:** * Use "async/await" syntax for asynchronous operations. * Handle errors using "try/catch" blocks. **Don't Do This:** * Using callbacks or promises directly without "async/await". * Ignoring unhandled promise rejections. **Why:** "async/await" syntax makes asynchronous code easier to read and reason about. "try/catch" blocks ensure that errors are properly handled, preventing unexpected crashes. **Example:** """typescript //Example Showing proper async await import { test, expect } from '@playwright/test'; test('Asynchronous Test', async ({ page }) => { try { await page.goto('/example'); //Using await to wait for the page to load const title = await page.title(); expect(title).toBe('Example Page'); } catch (error) { console.error("Test failed: ${error}"); throw error; //Re-throwing the error to fail the test } }); """ ### 4.2 Playwright's Auto-Waiting **Do This:** * Rely on Playwright's auto-waiting mechanism for assertions and actions. * Avoid using explicit timeouts unless absolutely necessary. * Use "waitFor*" methods for dynamic elements or conditions. **Don't Do This:** * Using "setTimeout" or "sleep" for arbitrary delays. * Disabling auto-waiting globally. **Why:** Playwright's auto-waiting mechanism automatically retries actions and assertions until they succeed or a timeout is reached. This eliminates the need for explicit timeouts, which can lead to flaky tests. "waitFor*" methods provide more control over waiting conditions. **Example:** """typescript import { test, expect } from '@playwright/test'; test('Auto-Waiting Example', async ({ page }) => { await page.goto('/dynamic-content'); // Playwright will automatically wait for the element to be visible await expect(page.locator('#content')).toBeVisible(); // Wait for a specific condition using waitForFunction await page.waitForFunction(() => document.querySelector('#content')?.textContent?.includes('loaded'), { timeout: 5000 }); const content = await page.locator('#content').textContent(); expect(content).toContain('loaded'); }); """ ### 4.3 Error Handling and Retries **Do This:** * Implement global error handling to catch unhandled exceptions. * Use Playwright's test retries feature to automatically retry failed tests. * Log errors and warnings to provide detailed debugging information. * Consider using "test.extend" to add custom error handling or retry logic. **Don't Do This:** * Ignoring errors or warnings. * Relying solely on test retries without addressing the root cause of failures. * Logging sensitive information in error messages. **Why:** Comprehensive error handling ensures that tests are robust and reliable. Test retries can help mitigate flaky tests caused by transient issues. Logging provides valuable debugging information, making it easier to identify and resolve problems. **Example (Global Error Handling):** In your "playwright.config.ts": """typescript // playwright.config.ts import { defineConfig } from '@playwright/test'; export default defineConfig({ // ... other configurations globalSetup: require.resolve('./global-setup'), }); """ Create a "global-setup.ts" file: """typescript // global-setup.ts async function globalSetup() { process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled Rejection at:', promise, 'reason:', reason); // Optionally, you can also implement custom error reporting logic here }); process.on('uncaughtException', (error) => { console.error('Uncaught Exception:', error); // Implement custom error logging, reporting, or exit the process }); } export default globalSetup; """ This ensures that unhandled promise rejections and uncaught exceptions are logged and reported, helping to identify and address potential issues in the test code. ## 5. Locators and Selectors Choosing appropriate locators and selectors is critical for creating stable and maintainable tests. ### 5.1 Best Practices for Locators **Do This:** * Use "getByRole" for accessible element selection. * Use "getByText" for locating elements by their visible text. * Use "getByLabel" for locating form elements by their associated labels. * Use CSS or XPath selectors when other options are not suitable. **Don't Do This:** * Using brittle or ambiguous selectors (e.g., relying on element position or index). * Overusing XPath selectors, which can be slower and less readable. * Hardcoding selector values within test files. **Why:** Using accessible locators ("getByRole", "getByLabel") ensures that tests are resilient to changes in the UI and improve accessibility. Locating elements by their visible text ("getByText") makes tests more readable and maintainable. CSS selectors are generally faster and more readable than XPath selectors. **Example:** """typescript import { test, expect } from '@playwright/test'; test('Locator Examples', async ({ page }) => { await page.goto('/forms'); // Using getByRole for accessible element selection await page.getByRole('button', { name: 'Submit' }).click(); // Using getByText for locating elements by their visible text await expect(page.getByText('Form submitted successfully')).toBeVisible(); // Using getByLabel for locating form elements await page.getByLabel('Email').fill('test@example.com'); // Using getByPlaceholder await page.getByPlaceholder('Enter your name').fill('John Doe'); // Using CSS selector when other options are not suitable await page.locator('#success-message').isVisible(); }); """ ### 5.2 Selector Strategies **Do This:** * Prioritize stable and semantic selectors that are less likely to change. * Use data attributes to add custom selectors for testing purposes. * Avoid relying on automatically generated IDs or class names, which may change during development. **Don't Do This:** * Using selectors that are tied to specific implementation details. * Creating overly complex selectors that are difficult to understand and maintain. **Why:** Stable selectors ensure that tests are resilient to changes in the UI. Data attributes provide a way to add custom selectors without affecting the application's functionality. Avoiding implementation-specific selectors reduces the risk of test failures due to code changes. **Example (using data attributes):** """html <button data-testid="submit-button">Submit</button> """ """typescript import { test, expect } from '@playwright/test'; test('Data Attribute Selector Example', async ({ page }) => { await page.goto('/forms'); await page.locator('[data-testid="submit-button"]').click(); await expect(page.locator('#success-message')).toBeVisible(); }); """ ## 6. Parallelization and Scalability Running tests in parallel significantly reduces the overall execution time. Playwright provides built-in support for parallelization. ### 6.1 Parallel Test Execution **Do This:** * Enable parallel test execution in "playwright.config.ts". """typescript // playwright.config.ts import { defineConfig } from '@playwright/test'; export default defineConfig({ fullyParallel: true, workers: process.env.CI ? 1 : undefined, //Limit workers in CI environments // ... other configurations }); """ * Use sharding to distribute tests across multiple machines or containers. **Don't Do This:** * Disabling parallelization unnecessarily. * Creating tests with implicit dependencies that prevent parallel execution. **Why:** Parallel test execution significantly reduces the overall test execution time, especially for large test suites. Sharding allows scaling test execution across multiple machines, further reducing the execution time. **Example (Sharding):** You can configure sharding in your CI/CD pipeline or test execution script. For example, using "grep" to filter tests for each shard: """bash # Shard 1: playwright test --grep "\[shard1\]" # Shard 2: playwright test --grep "\[shard2\]" """ Each test file would need to be tagged appropriately: """typescript // tests/example.spec.ts import { test, expect } from '@playwright/test'; test('Example Test [shard1]', async ({ page }) => { /* ... */ }); test('Another Test [shard2]', async ({ page }) => { /* ... */ }); """ ## 7. Security Best Practices Security should be a primary concern in test automation, especially when dealing with sensitive data or authentication. ### 7.1 Handling Sensitive Data **Do This:** * Store sensitive data (e.g., passwords, API keys) in environment variables or secure configuration files. * Avoid hardcoding sensitive data in test files or configuration files. * Use secure protocols (e.g., HTTPS) for all network communication. * Implement proper authentication and authorization mechanisms. **Don't Do This:** * Storing sensitive data in plain text. * Committing sensitive data to version control. * Using weak or default passwords. **Why:** Protecting sensitive data is essential to prevent unauthorized access and data breaches. Environment variables and secure configuration files provide a secure way to store sensitive data. Secure protocols and authentication mechanisms protect against eavesdropping and unauthorized access. **Example:** """typescript // playwright.config.ts import { defineConfig } from '@playwright/test'; import dotenv from 'dotenv'; dotenv.config(); const adminUsername = process.env.ADMIN_USERNAME; const adminPassword = process.env.ADMIN_PASSWORD; export default defineConfig({ use: { baseURL: process.env.BASE_URL || 'https://example.com', }, // ... other configurations }); """ ### 7.2 Input Validation and Sanitization **Do This:** * Validate and sanitize all input data to prevent injection attacks. * Use parameterized queries or prepared statements to prevent SQL injection. * Encode output data to prevent cross-site scripting (XSS) attacks. **Don't Do This:** * Trusting user input without validation. * Constructing queries or commands by concatenating strings. * Displaying user-generated content without proper encoding. **Why:** Input validation and sanitization prevent attackers from injecting malicious code or data into the application. Parameterized queries and output encoding protect against common attack vectors such as SQL injection and XSS. ### 7.3 Cookie Management and Session Handling **Do This:** * Clear cookies and session data after each test to prevent state leakage. * Use secure cookies with appropriate flags (e.g., "HttpOnly", "Secure"). * Validate session tokens and implement proper session expiration. **Don't Do This:** * Sharing cookies or session data between tests. * Using insecure cookies without proper flags. * Storing session tokens in local storage or cookies without encryption. **Why:** Cleaning cookies and session data after each test prevents state leakage, ensuring that tests are independent and repeatable. Secure cookies and session management protect against session hijacking and other attacks. In Playwright you can manage cookies like this: """typescript import { test, expect } from '@playwright/test'; test('Cookie Management', async ({ page, context }) => { await page.goto('/login'); // Perform login actions // Get cookies const cookies = await context.cookies(); console.log('Cookies after login:', cookies); // Clear cookies after the test await context.clearCookies(); const cookiesAfterClear = await context.cookies(); expect(cookiesAfterClear.length).toBe(0); // Verify that cookies are cleared }); """ These core architecture standards for Playwright provide a solid foundation for building robust, maintainable, and secure test automation frameworks. By following these guidelines, development teams can ensure consistent code quality, improve collaboration, and reduce the risk of errors and security vulnerabilities, while leveraging the latest Playwright features.
# Code Style and Conventions Standards for Playwright This document outlines code style and conventions for Playwright tests to promote consistency, readability, maintainability, and collaboration within development teams. These standards are designed to be used by developers and leveraged by AI coding assistants to generate high-quality Playwright code. ## 1. General Formatting and Style ### 1.1. Indentation **Standard:** Use 2 spaces for indentation. Do not use tabs. **Why:** Consistent indentation improves readability and reduces visual clutter. Spaces are less prone to interpretation differences across editors and operating systems compared to tabs. **Do This:** """javascript // Good: Two spaces for indentation test('verify element visibility', async ({ page }) => { await page.goto('https://example.com'); const element = await page.locator('#myElement'); await expect(element).toBeVisible(); }); """ **Don't Do This:** """javascript // Bad: Inconsistent indentation and using tabs test('verify element visibility', async ({ page }) => { await page.goto('https://example.com'); // Tab indentation const element = await page.locator('#myElement'); // Incorrect indentation await expect(element).toBeVisible(); }); """ ### 1.2. Line Length **Standard:** Limit lines to a maximum of 120 characters. **Why:** Excessive line length reduces readability, especially when viewed on smaller screens or side-by-side code review tools. **Do This:** """javascript // Good: Line length within limit test('verify a very long test description that wraps to multiple lines', async ({ page }) => { await page.goto('https://example.com/a/very/long/url/that/forces/line/breaks'); const longLocator = page.locator('#aVeryLongElementIdThatCausesLineBreaks'); await expect(longLocator).toBeVisible(); }); """ **Don't Do This:** """javascript // Bad: Line length exceeds limit, making the code harder to read test('verify a very long test description that wraps to multiple lines', async ({ page }) => { await page.goto('https://example.com/a/very/long/url/that/forces/line/breaks'); const longLocator = page.locator('#aVeryLongElementIdThatCausesLineBreaksWhichMakesTheLineLengthTooLong'); await expect(longLocator).toBeVisible(); }); """ ### 1.3. Whitespace **Standard:** * Use a single blank line to separate logical sections of code within a test. * Add a space after commas in argument lists. * Add spaces around operators (=, +, -, \*, /, etc.). **Why:** Whitespace improves code clarity and makes it easier to visually parse the different parts of a test. **Do This:** """javascript // Good: Proper use of whitespace test('login process', async ({ page }) => { await page.goto('https://example.com/login'); await page.locator('#username').fill('testuser'); await page.locator('#password').fill('password123'); await page.locator('#loginButton').click(); await expect(page.locator('#welcomeMessage')).toBeVisible(); }); """ **Don't Do This:** """javascript // Bad: Lack of whitespace makes the code harder to read test('login process', async ({ page }) => { await page.goto('https://example.com/login'); await page.locator('#username').fill('testuser'); await page.locator('#password').fill('password123'); await page.locator('#loginButton').click(); await expect(page.locator('#welcomeMessage')).toBeVisible(); }); """ ### 1.4. Trailing Commas **Standard:** Use trailing commas in multi-line object literals, array literals, and function parameters. **Why:** Trailing commas simplify diffs in version control systems and reduce the chance of errors when adding or removing elements. **Do This:** """javascript // Good: Trailing commas in object literal const user = { username: 'testuser', email: 'test@example.com', role: 'admin', // Trailing comma }; test('check user data', async ({ page }) => { await page.goto('https://example.com/profile'); // Assertion logic here }); """ **Don't Do This:** """javascript // Bad: No trailing comma const user = { username: 'testuser', email: 'test@example.com', role: 'admin' }; test('check user data', async ({ page }) => { await page.goto('https://example.com/profile'); // Assertion logic here }); """ ## 2. Naming Conventions ### 2.1. Test Files **Standard:** Use the ".spec.ts" or ".test.ts" extension for test files. Name files descriptively, indicating the component or feature being tested. Use kebab-case. **Why:** Clear naming makes it easy to identify and locate test files. **Do This:** * "login.spec.ts" * "product-details.spec.ts" * "shopping-cart.test.ts" **Don't Do This:** * "test.js" * "something.spec.js" * "a.ts" ### 2.2. Test Suites and Tests **Standard:** Use descriptive and concise names for "describe" blocks (test suites) and "test" blocks (individual tests). Names should clearly indicate the functionality being tested. Use sentence case, starting with a lowercase letter. **Why:** Meaningful names improve the self-documenting nature of the tests. **Do This:** """javascript // Good: Descriptive names describe('login functionality', () => { test('should allow users to log in with valid credentials', async ({ page }) => { // Test logic here }); test('should display an error message for invalid credentials', async ({ page }) => { // Test logic here }); }); """ **Don't Do This:** """javascript // Bad: Vague and unhelpful names describe('Test1', () => { test('Test2', async ({ page }) => { // Test logic here }); }); """ ### 2.3. Locators **Standard:** Use descriptive names for locators, reflecting the element they target. Prefer semantic locators (e.g., roles, labels) over fragile CSS selectors or XPaths whenever possible. Avoid overly specific CSS selectors. **Why:** Descriptive locator names improve readability and maintainability. Semantic locators are less susceptible to changes in the underlying DOM structure. **Do This:** """javascript // Good: Semantic locator const loginButton = page.getByRole('button', { name: 'Login' }); // Good: Descriptive CSS locator when necessary const productTitle = page.locator('.product-details h1'); """ **Don't Do This:** """javascript // Bad: Fragile and non-descriptive locators const loginButton = page.locator('body > div > div > form > button'); // Avoid this! const el1 = page.locator('#someElement'); // Not descriptive """ ### 2.4. Variables and Constants **Standard:** Use "camelCase" for variable names and "UPPER_SNAKE_CASE" for constants. Variables should be named descriptively. Use "const" for values that do not change, "let" for values that are reassigned, and avoid "var". **Why:** Consistent variable naming enhances code readability and maintainability. Using "const" and "let" promotes better code scoping and prevents accidental re-declaration. **Do This:** """javascript // Good: "camelCase" for variables, "UPPER_SNAKE_CASE" for constants const MAX_RETRIES = 3; let attempts = 0; const usernameInput = page.locator('#username'); """ **Don't Do This:** """javascript // Bad: Inconsistent naming, using var var maxRetries = 3; // Avoid var let Attempts = 0; // Incorrect casing const user_name_input = page.locator('#username'); // Inconsistent style """ ## 3. Stylistic Consistency ### 3.1. Assertions **Standard:** Use Playwright's built-in "expect" assertions. Favor specific matchers (e.g., "toBeVisible", "toHaveText") over generic matchers (e.g., "toBeTruthy"). Write your tests in the Arrange-Act-Assert pattern. **Why:** "expect" provides a consistent and readable way to verify expected outcomes. Specific matchers provide better error messages and improve code clarity. Arrange-Act-Assert pattern makes tests readable and easy to understand. **Do This:** """javascript // Good: Specific matcher, Arrange-Act-Assert test('element should be visible', async ({ page }) => { // Arrange await page.goto('https://example.com'); const element = page.locator('#myElement'); // Act - No explicit action necessary, checking initial state // Assert await expect(element).toBeVisible(); // Specific matcher }); test('login and check welcome text', async ({ page }) => { // Arrange await page.goto('https://example.com/login'); await page.locator('#username').fill('testuser'); await page.locator('#password').fill('password123'); // Act await page.locator('#loginButton').click(); // Assert await expect(page.locator('#welcomeMessage')).toHaveText('Welcome, testuser!'); }); """ **Don't Do This:** """javascript // Bad: Generic matcher test('element should be visible', async ({ page }) => { await page.goto('https://example.com'); const element = page.locator('#myElement'); expect(await element.isVisible()).toBeTruthy(); // Generic matcher - less clear }); """ ### 3.2. DRY Principle (Don't Repeat Yourself) **Standard:** Avoid code duplication by extracting common logic into reusable functions or page object model (POM) classes. **Why:** Reduces redundancy, improves maintainability, and makes tests easier to update. Promotes reusability. **Do This:** """javascript // Good: Reusable function async function login(page, username, password) { await page.goto('https://example.com/login'); await page.locator('#username').fill(username); await page.locator('#password').fill(password); await page.locator('#loginButton').click(); } test('login with valid credentials', async ({ page }) => { await login(page, 'testuser', 'password123'); await expect(page.locator('#welcomeMessage')).toBeVisible(); }); test('login with invalid credentials', async ({ page }) => { await login(page, 'invaliduser', 'wrongpassword'); await expect(page.locator('#errorMessage')).toBeVisible(); }); // Good: Using Page Object Model class LoginPage { constructor(page) { this.page = page; this.usernameInput = page.locator("#username"); this.passwordInput = page.locator("#password"); this.loginButton = page.locator("#loginButton"); this.welcomeMessage = page.locator("#welcomeMessage"); this.errorMessage = page.locator("#errorMessage"); } async goto() { await this.page.goto('https://example.com/login'); } async login(username, password) { await this.usernameInput.fill(username); await this.passwordInput.fill(password); await this.loginButton.click(); } async checkWelcomeMessage() { await expect(this.welcomeMessage).toBeVisible(); } async checkErrorMessage() { await expect(this.errorMessage).toBeVisible(); } } test('login using page objects', async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.login('testuser', 'password123'); await loginPage.checkWelcomeMessage(); }); """ **Don't Do This:** """javascript // Bad: Duplicate code test('login with valid credentials', async ({ page }) => { await page.goto('https://example.com/login'); await page.locator('#username').fill('testuser'); await page.locator('#password').fill('password123'); await page.locator('#loginButton').click(); await expect(page.locator('#welcomeMessage')).toBeVisible(); }); test('login with invalid credentials', async ({ page }) => { await page.goto('https://example.com/login'); await page.locator('#username').fill('invaliduser'); await page.locator('#password').fill('wrongpassword'); await page.locator('#loginButton').click(); await expect(page.locator('#errorMessage')).toBeVisible(); }); """ ### 3.3. Comments **Standard:** Use comments sparingly to explain complex logic or the intent behind specific code sections. Avoid commenting obvious code. **Why:** Comments should add value by clarifying non-obvious aspects of the code. Over-commenting can create clutter and make the code harder to read. **Do This:** """javascript // Good: Explaining complex logic test('handle network error', async ({ page }) => { // Mock a network request to simulate an error await page.route('**/api/data', async route => { await route.abort('failed'); }); await page.goto('https://example.com'); // Verify error message is displayed await expect(page.locator('#errorMessage')).toBeVisible(); }); """ **Don't Do This:** """javascript // Bad: Commenting obvious code test('check title', async ({ page }) => { await page.goto('https://example.com'); // Go to example.com const title = await page.title(); // Get the title expect(title).toBe('Example Domain'); // Check if the title is 'Example Domain' }); """ ### 3.4. Configuration **Standard:** Store configuration values (e.g., base URLs, API keys) in environment variables or configuration files. Access these values through a central configuration module. **Why:** Makes it easier to manage configurations across different environments (development, staging, production). Prevents hardcoding sensitive information. **Do This:** """javascript // Good: Using environment variables in playwright.config.ts (or playwright.config.js) /** @type {import('@playwright/test').PlaywrightTestConfig} */ const config = { use: { baseURL: process.env.BASE_URL || 'https://example.com', // Default value provided }, }; export default config; // In the test file: test('verify homepage', async ({ page }) => { await page.goto('/'); // Uses the baseURL from the config await expect(page).toHaveTitle('Example Domain'); }); """ **Don't Do This:** """javascript // Bad: Hardcoding the URL test('verify homepage', async ({ page }) => { await page.goto('https://example.com'); // Hardcoded URL - inflexible await expect(page).toHaveTitle('Example Domain'); }); """ ### 3.5. Asynchronous Operations and Error Handling **Standard:** Properly handle asynchronous operations using "async/await". Implement robust error handling using "try/catch" blocks. When appropriate use "auto-retries" in your playwright config and "test.step" to name actions. **Why:** Asynchronous operations require careful handling to prevent race conditions and ensure tests execute correctly. Error handling prevents tests from crashing unexpectedly and provides informative error messages. **Do This:** """javascript // Good: Async/await and try/catch for error handling test('fetch user data', async ({ page }) => { try { await page.goto('https://example.com/api/users/123'); const userData = await page.locator('#userData').textContent(); expect(userData).toContain('John Doe'); } catch (error) { console.error('Error fetching user data:', error); throw error; // Re-throw the error to fail the test } }); // Good: test.step for better reporting test('login and perform action', async ({ page }) => { await test.step('Navigate to login page', async () => { await page.goto('https://example.com/login'); }); await test.step('Enter credentials and submit', async () => { await page.locator('#username').fill('testuser'); await page.locator('#password').fill('password123'); await page.locator('#loginButton').click(); }); await test.step('Verify successful login', async () => { await expect(page.locator('#welcomeMessage')).toBeVisible(); }); }); """ **Don't Do This:** """javascript // Bad: Missing error handling test('fetch user data', async ({ page }) => { await page.goto('https://example.com/api/users/123'); const userData = await page.locator('#userData').textContent(); // No error handling if the element isn't found expect(userData).toContain('John Doe'); }); """ ## 4. Playwright-Specific Practices ### 4.1. Using "page.locator()" effectively **Standard:** Utilize Playwright's "page.locator()" method for element selection. It allows for chaining and filtering, creating more resilient and readable locators. **Why:** "page.locator()" provides a powerful and flexible way to interact with elements on the page. **Do This:** """javascript // Good: Chaining locators, filtering by text const addToCartButton = page.locator('.product-item').filter({ hasText: 'Add to Cart' }).locator('button'); await addToCartButton.click(); """ **Don't Do This:** """javascript // Bad: Overly complex and brittle CSS selector - avoid if possible const addToCartButton = page.locator('div.product-item:nth-child(3) > div > button'); await addToCartButton.click(); """ ### 4.2. Leveraging Auto-Waiting **Standard:** Take advantage of Playwright's auto-waiting feature. Explicitly wait only when necessary (e.g., for animations to complete or for specific conditions to be met). **Why:** Playwright automatically waits for elements to be actionable, reducing the need for explicit waits and making tests more robust. **Do This:** """javascript // Good: Using auto-waiting await page.locator('#submitButton').click(); // Playwright waits for the button to be clickable await expect(page.locator('#successMessage')).toBeVisible(); // Playwright waits for the message to appear. // Explicit wait only when necessary await page.waitForTimeout(500); // Wait for a short animation (use sparingly) """ **Don't Do This:** """javascript // Bad: Excessive explicit waits await page.waitForTimeout(2000); // Unnecessary wait - reduces test speed and reliability. await page.locator('#submitButton').click(); await page.waitForTimeout(2000); // Another unnecessary wait. await expect(page.locator('#successMessage')).toBeVisible(); """ ### 4.3. Browser Context Management **Standard:** Use browser contexts to isolate tests and prevent state leakage. Create a new context for each test or test suite unless there's a specific reason to share a context. **Why:** Browser contexts ensure that each test starts with a clean slate, preventing tests from interfering with each other. **Do This:** """javascript // Good: Using a new context for each test import { test } from '@playwright/test'; test.describe('with clean state', () => { test.use({ storageState: { cookies: [], origins: [] } }); // tests go here. They will have no cookies and localStorage test('test 1', async ({ page }) => { await page.goto('https://example.com'); await expect(page).toHaveTitle('Example Domain'); }); test('test 2', async ({ page }) => { await page.goto('https://example.com'); await expect(page).toHaveTitle('Example Domain'); }); }); """ **Don't Do This:** """javascript // Bad: Sharing a single context across all tests (unless specifically needed) - can lead to flaky tests. // (Avoid unless you have a very specific reason) test('test 1', async ({ page }) => { await page.goto('https://example.com'); await expect(page).toHaveTitle('Example Domain'); }); test('test 2', async ({ page }) => { await page.goto('https://example.com'); await expect(page).toHaveTitle('Example Domain'); }); """ ### 4.4. Using Fixtures **Standard:** Leverage Playwright's fixture system to manage test setup and teardown. Use project-scoped fixtures where appropriate to perform setup shared by all tests in a project. **Why:** Fixtures provide a clean and reusable way to configure test environments and share resources. **Do This:** """typescript // Good: // playwright-fixture.ts import { test as base } from '@playwright/test'; import type { Page } from '@playwright/test'; interface MyFixtures { loggedInPage: Page; } export const test = base.extend<MyFixtures>({ loggedInPage: async ({ page }, use) => { await page.goto('https://example.com/login'); await page.locator('#username').fill('testuser'); await page.locator('#password').fill('password123'); await page.locator('#loginButton').click(); await page.waitForSelector('#welcomeMessage'); await use(page); }, }); export { expect } from '@playwright/test'; // test.spec.ts import { test, expect } from './playwright-fixture'; test('check welcome message', async ({ loggedInPage }) => { await expect(loggedInPage.locator('#welcomeMessage')).toBeVisible(); }); """ **Don't Do This:** """javascript test('check welcome message', async ({ page }) => { await page.goto('https://example.com/login'); await page.locator('#username').fill('testuser'); await page.locator('#password').fill('password123'); await page.locator('#loginButton').click(); await page.waitForSelector('#welcomeMessage'); await expect(page.locator('#welcomeMessage')).toBeVisible(); }); """ ### 4.5. Parallelism and Sharding **Standard:** Configure Playwright to run tests in parallel to reduce execution time. Consider using sharding to distribute tests across multiple machines. **Why:** Parallelism and sharding can significantly speed up test execution, especially for large test suites. **Do This:** """typescript //Good: Configure parallelism in playwright.config.ts // playwright.config.ts /** @type {import('@playwright/test').PlaywrightTestConfig} */ const config = { workers: process.env.CI ? 2 : undefined, //Number of workers defaults to the number of logical processors available on your machine retries: process.env.CI ? 2 : undefined, reporter: 'github', }; export default config; """ This comprehensive guide provides a foundation for writing consistent, readable, and maintainable Playwright tests. Adhering to these code style and convention standards will improve collaboration, reduce errors, and ensure the long-term quality of your test suite. Consistent application of these rules, even when using AI code generation tools, will lead to more robust and maintainable tests.
# State Management Standards for Playwright This document outlines the coding standards for state management in Playwright tests. Effective state management ensures tests are reliable, maintainable, and performant. This guide provides practical recommendations, rationale, and examples to help developers implement robust state management strategies within Playwright. ## 1. Introduction to State Management in Playwright ### 1.1. What is State Management? State management refers to the process of managing and controlling the application's data and the way it's shared and modified across different components or tests. In Playwright, state management is crucial for ensuring tests are isolated, repeatable, and accurate. Poor state management can lead to flaky tests, unpredictable behavior, and increased maintenance overhead. ### 1.2. Why is State Management Important for Playwright? * **Test Isolation:** Each test should operate independently, without relying on the state left behind by previous tests. * **Repeatability:** Tests should produce the same results every time they are run, regardless of the environment or order. * **Maintainability:** Well-structured state management makes tests easier to understand, modify, and debug. * **Performance:** Efficient state management can reduce test execution time and resource consumption. ## 2. Approaches to State Management ### 2.1. Browser Contexts Playwright's browser contexts provide a powerful mechanism for isolating state between tests. Each context is a fresh, isolated environment, akin to a new browser profile. **Do This:** * Utilize "browser.newContext()" to create a new browser context for each test or a group of related tests. * Configure the context with necessary cookies, storage state, or network interception rules. **Don't Do This:** * Reuse the same browser context across multiple unrelated tests, as this can lead to state pollution and unpredictable results. **Why:** Browser contexts ensure complete isolation, preventing tests from interfering with each other. **Example:** """javascript const { chromium } = require('playwright'); describe('State Isolation with Browser Contexts', () => { let browser; beforeAll(async () => { browser = await chromium.launch(); }); afterAll(async () => { await browser.close(); }); it('should test login successfully in a new context', async () => { const context = await browser.newContext(); const page = await context.newPage(); await page.goto('https://example.com/login'); await page.fill('#username', 'testuser'); await page.fill('#password', 'password'); await page.click('button[type="submit"]'); await page.waitForSelector('.dashboard'); expect(await page.textContent('.dashboard')).toContain('Welcome'); await context.close(); // Close the context after the test }); it('should test signup successfully in a new context', async () => { const context = await browser.newContext(); const page = await context.newPage(); await page.goto('https://example.com/signup'); await page.fill('#newUsername', 'newuser'); await page.fill('#newEmail', 'newuser@example.com'); await page.fill('#newPassword', 'password'); await page.click('button[type="submit"]'); await page.waitForSelector('.confirmation'); expect(await page.textContent('.confirmation')).toContain('Confirm your email'); await context.close(); // Close the context after the test }); }); """ ### 2.2. Storage State Playwright allows you to save and restore the storage state (cookies, local storage, session storage) of a browser context. This is useful for persisting login sessions or other application states across tests. **Do This:** * Use "context.storageState()" to save the storage state after performing an action that modifies the state (e.g., logging in). * Use "browser.newContext({ storageState: 'path/to/storageState.json' })" to restore the storage state when creating a new context. **Don't Do This:** * Save the storage state indiscriminately. Only save when necessary to avoid unnecessary overhead. * Store sensitive information (e.g., passwords) directly in the storage state. Use environment variables or configuration files instead. **Why:** Storage state helps to avoid redundant steps in tests and speeds up execution. **Example:** """javascript const { chromium } = require('playwright'); const fs = require('fs'); describe('Storage State Management', () => { let browser; let context; let page; beforeAll(async () => { browser = await chromium.launch(); context = await browser.newContext(); page = await context.newPage(); }); afterAll(async () => { await browser.close(); }); it('should login and save storage state', async () => { await page.goto('https://example.com/login'); await page.fill('#username', 'testuser'); await page.fill('#password', 'password'); await page.click('button[type="submit"]'); await page.waitForSelector('.dashboard'); expect(await page.textContent('.dashboard')).toContain('Welcome'); // Save storage state const storageState = await context.storageState(); fs.writeFileSync('storageState.json', JSON.stringify(storageState, null, 2)); }); it('should reuse storage state', async () => { const newContext = await browser.newContext({ storageState: 'storageState.json' }); const newPage = await newContext.newPage(); await newPage.goto('https://example.com/dashboard'); // navigate directly to dashboard await newPage.waitForSelector('.dashboard'); expect(await newPage.textContent('.dashboard')).toContain('Welcome'); await newContext.close(); }); }); """ ### 2.3. API Requests for State Setup Use Playwright's API request functionality to set up the application's state directly through API calls, bypassing the UI. This approach is faster and more reliable than UI-based setup. **Do This:** * Use "request.newContext()" to create API request contexts. * Use API requests to create users, seed databases, or perform other setup tasks. **Don't Do This:** * Rely solely on UI interactions for state setup, as this can be slow and brittle. **Why:** API-based setup is faster, more reliable, and less prone to UI changes. **Example:** """javascript const { chromium } = require('playwright'); describe('API-Based State Setup', () => { let browser; let context; let page; let request; beforeAll(async () => { browser = await chromium.launch(); context = await browser.newContext(); page = await context.newPage(); request = context.request; }); afterAll(async () => { await browser.close(); }); async function createUser(username, email, password) { const response = await request.post('https://example.com/api/users', { data: { username, email, password }, }); expect(response.status()).toBe(201); const body = await response.json(); return body.id; } async function deleteUser(userId) { const response = await request.delete("https://example.com/api/users/${userId}"); expect(response.status()).toBe(204); } it('should create a user via API and then login via UI', async () => { const userId = await createUser('apiuser', 'apiuser@example.com', 'password'); await page.goto('https://example.com/login'); await page.fill('#username', 'apiuser'); await page.fill('#password', 'password'); await page.click('button[type="submit"]'); await page.waitForSelector('.dashboard'); expect(await page.textContent('.dashboard')).toContain('Welcome apiuser'); await deleteUser(userId); // Cleanup the created user }); }); """ ### 2.4. Database Seeding For applications that rely on databases, seed the database with test data before running tests or reset the database between tests. **Do This:** * Use a dedicated database seed script or library. * Run the seed script before each test suite or before each test if necessary. * Clean up the database after tests to ensure no state pollution. **Don't Do This:** * Manually insert data into the database for each test. * Leave test data in the database after tests are complete. **Why:** Database seeding ensures tests operate on a consistent and known data set. **Example:** """javascript const { chromium } = require('playwright'); const { seedDatabase, cleanupDatabase } = require('./db-utils'); // Assume these utils exist describe('Database Seeding', () => { let browser; let context; let page; beforeAll(async () => { browser = await chromium.launch(); context = await browser.newContext(); page = await context.newPage(); await seedDatabase(); // Seed the database before tests }); afterAll(async () => { await cleanupDatabase(); // Cleanup the database after tests await browser.close(); }); it('should display seeded data', async () => { await page.goto('https://example.com/data'); await page.waitForSelector('.data-item'); expect(await page.textContent('.data-item')).toContain('Seeded Data'); }); }); """ Here's an example of how "db-utils.js" might look: """javascript // db-utils.js const { Pool } = require('pg'); // Example using PostgreSQL const pool = new Pool({ user: 'dbuser', host: 'localhost', database: 'testdb', password: 'dbpassword', port: 5432, }); async function seedDatabase() { const client = await pool.connect(); try { await client.query('CREATE TABLE IF NOT EXISTS data_items (id SERIAL PRIMARY KEY, value TEXT)'); await client.query('INSERT INTO data_items (value) VALUES ($1)', ['Seeded Data']); } finally { client.release(); } } async function cleanupDatabase() { const client = await pool.connect(); try { await client.query('DELETE FROM data_items'); } finally { client.release(); } } module.exports = { seedDatabase, cleanupDatabase }; """ ## 3. Specific State Management Patterns ### 3.1. Page Object Model (POM) The Page Object Model is a design pattern that represents web pages as objects, encapsulating the page's elements and actions. This can help manage state within a specific page. **Do This:** * Create a class for each page or component with methods for interacting with the page. * Store page-specific state (e.g., input values, validation messages) within the page object. **Don't Do This:** * Spread page-specific logic across multiple tests. * Directly access page elements from tests. **Why:** POM promotes code reuse, reduces redundancy, and makes tests more readable and maintainable. **Example:** """javascript // login-page.js const { expect } = require('@playwright/test'); class LoginPage { constructor(page) { this.page = page; this.usernameInput = '#username'; this.passwordInput = '#password'; this.submitButton = 'button[type="submit"]'; this.errorMessage = '.error-message'; } async goto() { await this.page.goto('https://example.com/login'); } async login(username, password) { await this.page.fill(this.usernameInput, username); await this.page.fill(this.passwordInput, password); await this.page.click(this.submitButton); } async getErrorMessageText() { await this.page.waitForSelector(this.errorMessage); return this.page.textContent(this.errorMessage); } } module.exports = { LoginPage }; """ """javascript // login.spec.js const { chromium } = require('playwright'); const { LoginPage } = require('./login-page'); describe('Login Tests', () => { let browser; let context; let page; let loginPage; beforeAll(async () => { browser = await chromium.launch(); }); afterAll(async () => { await browser.close(); }); beforeEach(async () => { context = await browser.newContext(); page = await context.newPage(); loginPage = new LoginPage(page); await loginPage.goto(); }); afterEach(async () => { await context.close(); }); it('should login with valid credentials', async () => { await loginPage.login('testuser', 'password'); await page.waitForSelector('.dashboard'); expect(await page.textContent('.dashboard')).toContain('Welcome'); }); it('should display an error message with invalid credentials', async () => { await loginPage.login('invaliduser', 'wrongpassword'); expect(await loginPage.getErrorMessageText()).toContain('Invalid credentials'); }); }); """ ### 3.2. State Management with Configuration Files Use configuration files to manage environment-specific settings and data. **Do This:** * Store environment-specific URLs, API keys, and test data in configuration files (e.g., ".env", "config.json"). * Load the configuration file based on the environment variable (e.g., "NODE_ENV"). **Don't Do This:** * Hardcode environment-specific values in tests. * Store sensitive information directly in configuration files. **Why:** Configuration files allow tests to be easily run in different environments without modifying the code. **Example:** """javascript // config.js require('dotenv').config(); // Load environment variables from .env const env = process.env.NODE_ENV || 'development'; const config = { development: { baseUrl: 'https://localhost:3000', apiKey: 'dev-api-key', }, test: { baseUrl: 'https://test.example.com', apiKey: 'test-api-key', }, production: { baseUrl: 'https://example.com', apiKey: process.env.PROD_API_KEY, // Access from environment variable }, }; module.exports = config[env]; """ """javascript // example.spec.js const { chromium } = require('playwright'); const config = require('./config'); describe('Environment-Specific Tests', () => { let browser; let context; let page; beforeAll(async () => { browser = await chromium.launch(); }); afterAll(async () => { await browser.close(); }); beforeEach(async () => { context = await browser.newContext(); page = await context.newPage(); }); afterEach(async () => { await context.close(); }); it('should use the correct base URL', async () => { await page.goto(config.baseUrl); expect(page.url()).toContain(config.baseUrl); }); it('should use the correct API key', async () => { // Example: Make an API request using the API key // (This example is illustrative, adapt to your specific API) const apiKey = config.apiKey; expect(apiKey).toBeTruthy(); }); }); """ ### 3.3. Mocking and Stubbing Mocking and stubbing external dependencies can help isolate tests and control the application's state. Playwright supports network interception for mocking API responses. Note: Consider using ["test.use({ baseURL: '...' })"](https://playwright.dev/docs/api/class-testoptions#test-options-base-url) for simplicity when intercepting API calls to the same base URL. **Do This:** * Use "page.route()" (or "context.route()") to intercept network requests and provide mock responses. * Mock external APIs or services to control their behavior. **Don't Do This:** * Mock internal components or functions unnecessarily. * Create overly complex mocks that mirror the actual implementation. **Why:** Mocking and stubbing make tests more predictable, faster, and independent of external systems. **Example:** """javascript const { chromium } = require('playwright'); describe('API Mocking', () => { let browser; let context; let page; beforeAll(async () => { browser = await chromium.launch(); }); afterAll(async () => { await browser.close(); }); beforeEach(async () => { context = await browser.newContext(); page = await context.newPage(); }); afterEach(async () => { await context.close(); }); it('should mock API response', async () => { await page.route('https://example.com/api/data', async route => { await route.fulfill({ contentType: 'application/json', body: JSON.stringify({ message: 'Mocked data' }), }); }); await page.goto('https://example.com/data'); await page.waitForSelector('.data-display'); expect(await page.textContent('.data-display')).toContain('Mocked data'); }); }); """ ## 4. Anti-Patterns and Common Mistakes ### 4.1. Shared State **Anti-Pattern:** Sharing state between tests by reusing the same browser context or failing to clean up after tests. **Consequence:** Flaky tests, unpredictable behavior, and difficult debugging. **Solution:** Use browser contexts for test isolation, clean up after tests, and avoid global variables. ### 4.2. Implicit Dependencies **Anti-Pattern:** Tests that rely on implicit dependencies or assumptions about the application's state. **Consequence:** Tests that fail when the application's state changes unexpectedly. **Solution:** Explicitly define dependencies, use API-based setup, and avoid assumptions about the application's state. ### 4.3. Over-Reliance on UI Interactions **Anti-Pattern:** Setting up the application's state solely through UI interactions. **Consequence:** Slow and brittle tests. **Solution:** Use API requests, database seeding, or other non-UI methods for state setup. ### 4.4. Neglecting Cleanup **Anti-Pattern:** Failing to clean up after tests, leaving the application in an inconsistent state. **Consequence:** State pollution and unpredictable test results. **Solution:** Implement cleanup routines that reset the application's state after each test. ## 5. Advanced Techniques ### 5.1. Test Fixtures Playwright Test provides built-in support for test fixtures, which can be used to manage shared state and resources across tests. **Do This:** * Define custom fixtures to encapsulate state management logic. * Use fixtures to create and manage browser contexts, API request contexts, and other resources. **Don't Do This:** * Define fixtures that are overly complex or tightly coupled to specific tests. **Why:** Fixtures provide a clean and maintainable way to manage shared state and resources. **Example:** """javascript // playwright.config.js const { defineConfig, devices } = require('@playwright/test'); export default defineConfig({ use: { trace: 'on-first-retry', }, projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, ], }); """ """javascript // example.spec.js const { test, expect } = require('@playwright/test'); test.describe('State Management with Fixtures', () => { test('should login using storage state fixture', async ({ browser, page }) => { // Assume you have a login action that saves the storage state to 'storageState.json' const context = await browser.newContext({ storageState: 'storageState.json' }); page = await context.newPage(); await page.goto('https://example.com/dashboard'); await page.waitForSelector('.dashboard'); expect(await page.textContent('.dashboard')).toContain('Welcome'); }); }); """ ### 5.2. Parallel Test Execution When running tests in parallel, ensure that each test has its own isolated state. **Do This:** * Use browser contexts and API request contexts to isolate state between parallel tests. * Implement database seeding or other setup routines that are thread-safe. **Don't Do This:** * Share mutable state between parallel tests. * Rely on global variables or singletons that can be modified by multiple tests simultaneously. **Why:** Parallel test execution can significantly reduce test execution time, but requires careful state management to avoid conflicts. ## 6. Security Considerations ### 6.1. Sensitive Information Avoid storing sensitive information (e.g., passwords, API keys) directly in tests or configuration files. **Do This:** * Use environment variables to store sensitive information. * Encrypt sensitive data in configuration files. * Use Playwright's "context.addCookies" to add encrypted session cookies if necessary. **Don't Do This:** * Commit sensitive information to source control. * Log sensitive information to the console. **Why:** Protecting sensitive information is crucial for security and compliance. ### 6.2. Data Sanitization When working with user-provided data, sanitize the data to prevent security vulnerabilities such as cross-site scripting (XSS) or SQL injection. **Do This:** * Use Playwright's "page.fill()" with appropriate sanitization techniques. * Use parameterized queries or prepared statements when interacting with databases. **Don't Do This:** * Directly insert user-provided data into HTML or SQL queries. * Trust user-provided data without validation. **Why:** Data sanitization prevents security vulnerabilities and protects the application from malicious attacks. ## 7. Conclusion Effective state management is essential for creating reliable, maintainable, and performant Playwright tests. By following the coding standards outlined in this document, developers can ensure that their tests are well-isolated, repeatable, and easy to understand. This contributes to a higher quality testing process and more robust applications. Continuously review and update these standards as Playwright evolves and new best practices emerge.
# Component Design Standards for Playwright This document outlines coding standards specifically related to **component design** within Playwright test suites. These standards aim to promote reusable, maintainable, and reliable test components, leading to more efficient test automation. ## 1. Principles of Component Design in Playwright The foundation of effective component design in Playwright rests on these key principles: * **Reusability:** Components should be designed to be used across multiple test suites and scenarios, minimizing code duplication. * **Maintainability:** Components should be easy to understand, modify, and extend, reducing the cost of future updates. * **Abstraction:** Components should hide complex implementation details, providing a simple and consistent interface for users. * **Testability:** Components should be designed in a way that makes them easy to test independently. * **Locatability:** Each component needs to implement well-defined, robust element selectors. ### 1.1 Why Component Design Matters in Playwright Proper component design directly addresses several key challenges in Playwright testing: * **Reduced Redundancy:** Avoid repeating the same sequences of actions (e.g., login) or locators across multiple tests. * **Improved Maintainability:** When a UI element changes, only the component needs to be updated, not every single test that uses it. * **Increased Readability:** Tests become cleaner and easier to understand as they delegate complex interactions to well-named components. * **Simplified Collaboration:** Standardized components facilitate better collaboration among team members. * **Faster Test Development:** Reusing existing components accelerates the development of new tests. * **Reduced Test Flakiness:** Robust component design with well-defined locators reduces the chance of tests failing due to minor UI changes. ### 1.2 Component Categories Consider these component categories for organization: * **Page Object Components:** Represent a specific section or area of a page (e.g., a login form, navigation bar). * **UI Components:** Standardized reusable components that also exist in the actual application UI (e.g., a button, alert, dropdown). * **Utility Components:** Handle common tasks like API calls, data generation, or report aggregation. * **Business Logic Components:** Encapsulate complex business rules or workflows for testing purposes. ## 2. Defining Playwright Components ### 2.1 Component Structure A well-structured Playwright component typically consists of: * **Locators:** CSS, XPath, or Playwright-specific locators to identify elements within the component. * **Methods (Actions):** Functions that perform actions on the component, such as clicking a button, filling a form, or retrieving text. * **State Verification:** Methods to verify the state of the component, such as checking if an element is visible or enabled. * **Data Members:** Necessary variables that hold test data related to that component (e.g. url). ### 2.2 Do This: Encapsulate Locators * **Standard:** Define all locators within the component file. * **Why:** Centralizes locator definitions, making it easier to update and maintain them. Prevents "magic strings" in tests. Uses explicit locator strings rather than direct implementations. """typescript // components/login.component.ts import { Page } from '@playwright/test'; export class LoginComponent { readonly page: Page; readonly usernameInput = this.page.locator('#username'); readonly passwordInput = this.page.locator('#password'); readonly loginButton = this.page.locator('button[type="submit"]'); readonly errorMessage = this.page.locator('.error-message'); constructor(page: Page) { this.page = page; } async login(username: string, password: string) { await this.usernameInput.fill(username); await this.passwordInput.fill(password); await this.loginButton.click(); } } """ ### 2.3 Don't Do This: Locator Duplication * **Anti-Pattern:** Scattering locators throughout your test files. * **Why:** Makes maintenance difficult, as you have to update locators in multiple places if they change. ### 2.4 Do This: Use "getByRole" Locators * **Standard:** Favor accessible locators ("getByRole") whenever possible. * **Why:** Makes tests more resilient to UI changes and improves accessibility testing. * **Example:** Instead of "page.locator('button.submit')", use "page.getByRole('button', { name: 'Submit' })". ### 2.5 Do This: Implement Component Methods * **Standard:** Define methods to encapsulate actions within the component. * **Why:** Provides a clean and reusable interface for interacting with the component. """typescript // components/product.component.ts import { Page } from '@playwright/test'; export class ProductComponent { readonly page: Page; constructor(page: Page) { this.page = page; } async addToCart(productName: string) { const productLocator = this.page.locator('.product', { hasText: productName }); await productLocator.locator('button:has-text("Add to Cart")').click(); } async viewDetails(productName: string) { const productLocator = this.page.locator('.product', { hasText: productName }); await productLocator.locator('a:has-text("View Details")').click(); } async getPrice(productName:string): Promise<number> { const productLocator = this.page.locator('.product', { hasText: productName }); const priceText = await productLocator.locator('.price').textContent(); if (priceText) { return parseFloat(priceText.replace('$', '')); } return 0; } } """ ### 2.6 Don't Do This: Hardcoded Waits * **Anti-Pattern:** Using "await page.waitForTimeout(5000)" inside components, unless there is ONLY a very specific reason that does not involve resolving the state of an element. * **Why:** Makes tests slow and unreliable as the correct wait time is hard to determine. Causes tests to fail when timeouts are too short. ### 2.7 Do This: Use Playwright's Auto-Waiting * **Standard:** Rely on Playwright's auto-waiting features whenever possible. * **Why:** Simplifies tests and makes them more reliable. * **Example:** """typescript //Preferred await this.page.locator('.success-message').isVisible(); //Instead of: await this.page.waitForSelector('.success-message', {state: 'visible'}); """ ### 2.8 Do This: Implement Methods to Verify State * **Standard:** Include methods to verify the state of the component. * **Why:** Improves test reliability by ensuring the component is in the expected state before and after an action. """typescript // components/notification.component.ts import { Page } from '@playwright/test'; export class NotificationComponent { readonly page: Page; readonly successMessage = this.page.locator('.success-message'); readonly errorMessage = this.page.locator('.error-message'); constructor(page: Page) { this.page = page; } async isSuccessMessageVisible(): Promise<boolean> { return await this.successMessage.isVisible(); } async getSuccessMessageText(): Promise<string | null> { return await this.successMessage.textContent(); } async isErrorMessageVisible(): Promise<boolean> { return await this.errorMessage.isVisible(); } async getErrorMessageText(): Promise<string | null> { return await this.errorMessage.textContent(); } } """ ### 2.9 Do This: Define Component-Specific Type Definitions * **Standard:** Define TypeScript interfaces or types for data associated with the component. * **Why:** Improves code clarity and prevents errors caused by incorrect data types. """typescript // components/product.component.ts export interface Product { name: string; price: number; description: string; } export class ProductComponent { // ... } """ ## 3. Best Practices for Reusable Components ### 3.1 Do This: Parameterize Component Methods * **Standard:** Pass data as arguments to component methods instead of hardcoding values. * **Why:** Makes components more flexible and reusable in different scenarios. """typescript // components/search.component.ts import { Page } from '@playwright/test'; export class SearchComponent { readonly page: Page; readonly searchInput = this.page.locator('#search-input'); readonly searchButton = this.page.locator('#search-button'); constructor(page: Page) { this.page = page; } async searchFor(searchTerm: string) { await this.searchInput.fill(searchTerm); await this.searchButton.click(); } } """ ### 3.2 Do This: Use Component Composition * **Standard:** Build complex components by combining smaller, reusable components. * **Why:** Promotes code reuse and reduces complexity. """typescript // components/checkout.component.ts import { Page } from '@playwright/test'; import { AddressFormComponent } from './address-form.component'; import { PaymentFormComponent } from './payment-form.component'; export class CheckoutComponent { readonly page: Page; readonly addressForm: AddressFormComponent; readonly paymentForm: PaymentFormComponent; constructor(page: Page) { this.page = page; this.addressForm = new AddressFormComponent(page); this.paymentForm = new PaymentFormComponent(page); } async completeCheckout() { await this.addressForm.fillAddress('123 Main St', 'Anytown'); await this.paymentForm.fillPaymentDetails('1234567890123456', '12/24'); await this.page.locator('#submit-button').click(); } } """ ### 3.3 Do This: Design for Different Component States * **Standard:** Design components to handle different states (e.g., enabled/disabled, visible/hidden, loading/idle). * **Why:** Increases robustness and allows for more comprehensive testing. """typescript // components/button.component.ts import { Page } from '@playwright/test'; export class ButtonComponent { readonly page: Page; readonly button = this.page.locator('button'); constructor(page: Page) { this.page = page; } async click() { await this.button.click(); } async isDisabled(): Promise<boolean> { return await this.button.isDisabled(); } async isVisible(): Promise<boolean> { return await this.button.isVisible(); } async getButtonText(): Promise<string | null> { return await this.button.textContent(); } } """ ### 3.4 Do This: Handle Errors Gracefully * **Standard:** Implement error handling within components to prevent tests from crashing. * **Why:** Improves test reliability and provides informative error messages. """typescript // components/api.component.ts import { APIRequestContext } from '@playwright/test'; export class ApiComponent { readonly apiContext: APIRequestContext; constructor(apiContext: APIRequestContext) { this.apiContext = apiContext; } async get(url: string) { try { const response = await this.apiContext.get(url); if (!response.ok()) { throw new Error("API request failed with status ${response.status()}"); } return await response.json(); } catch (error) { console.error("Error during API request: ${error}"); throw error; // Re-throw to fail the test if necessary. } } } """ ## 4. Component Organization and Structure ### 4.1 Do This: Use a Consistent Directory Structure * **Standard:** Organize components into a clear and consistent directory structure within your project. A common approach is a "components" directory. * **Why:** Improves code discoverability and maintainability. """ /tests /components /login.component.ts /product.component.ts /search.component.ts /e2e /example.spec.ts """ ### 4.2 Do This: Use Component Libraries * **Standard** Group related components together to form logical libraries or modules. Use an index.ts file to export all components from library. * **Why:** Facilitates modularity and shareability of related modules/code. Enables you to combine components in different ways. """ /tests /components /auth /login.component.ts /register.component.ts /index.ts /product /product.component.ts /product-list.component.ts /index.ts /index.ts // components/auth/index.ts export { LoginComponent } from './login.component'; export { RegisterComponent } from './register.component'; // components/index.ts export * as AuthComponents from './auth'; export * as ProductComponents from './product'; """ ### 4.3 Do This: Document Components Clearly * **Standard:** Use JSDoc or TSDoc to document the purpose, usage, and parameters of each component and its methods. * **Why:** Makes components easier to understand and use by other developers. """typescript /** * Represents the login form component. */ export class LoginComponent { readonly page: Page; /** * Locator for the username input field. */ readonly usernameInput = this.page.locator('#username'); /** * Locator for the password input field. */ readonly passwordInput = this.page.locator('#password'); /** * Locator for the login button. */ readonly loginButton = this.page.locator('button[type="submit"]'); constructor(page: Page) { this.page = page; } /** * Logs in with the given username and password. * @param username The username to use for login. * @param password The password to use for login. */ async login(username: string, password: string) { await this.usernameInput.fill(username); await this.passwordInput.fill(password); await this.loginButton.click(); } } """ ## 5. Security Considerations ### 5.1 Do This: Protect Sensitive Data * **Standard:** Avoid storing sensitive data (e.g., passwords, API keys) directly in component files. Use environment variables or secure configuration management. * **Why:** Prevents accidental exposure of sensitive information. """typescript // playwright.config.ts import { defineConfig } from '@playwright/test'; export default defineConfig({ // ... use: { baseURL: process.env.BASE_URL || 'http://localhost:3000', adminUsername: process.env.ADMIN_USERNAME, adminPassword: process.env.ADMIN_PASSWORD, // ... }, }); // components/admin.component.ts import { Page, expect } from '@playwright/test'; export class AdminComponent { readonly page: Page; readonly adminUsername: string | undefined; readonly adminPassword: string | undefined; constructor(page: Page) { this.page = page; this.adminUsername = process.env.ADMIN_USERNAME; this.adminPassword = process.env.ADMIN_PASSWORD; if (!this.adminUsername || !this.adminPassword) { throw new Error('Admin username and password must be set as environment variables.'); } } async login() { //...Login using env variables. } } """ ### 5.2 Do This: Validate Inputs * **Standard:** Validate all inputs to component methods to prevent unexpected behavior or security vulnerabilities. * **Why:** Protects against injection attacks and ensures data integrity. """typescript // components/comment.component.ts import { Page } from '@playwright/test'; export class CommentComponent { readonly page: Page; readonly commentInput = this.page.locator('#comment-input'); readonly submitButton = this.page.locator('#submit-button'); constructor(page: Page) { this.page = page; } async addComment(commentText: string) { if (!commentText || commentText.length > 200) { throw new Error('Comment must be between 1 and 200 characters.'); } await this.commentInput.fill(commentText); await this.submitButton.click(); } } """ ### 5.3 Do This: Isolate Test Data * **Standard:** Ensure that test data is isolated between tests to prevent data leakage or interference. Use separate test environments or databases where possible. * **Why:** Improves test reliability and prevents unintended side effects. ## 6. Modern Playwright Component Design ### 6.1 Do This: Utilize Playwright's "expect" Matchers in Components * **Standard:** Integrate Playwright's built-in "expect" matchers directly within components for assertions. * **Why:** Simplifies assertions and aligns with Playwright's testing philosophy. """typescript // components/product-details.component.ts import { Page, expect } from '@playwright/test'; export class ProductDetailsComponent { readonly page: Page; readonly productName = this.page.locator('#product-name'); readonly productDescription = this.page.locator('#product-description'); constructor(page: Page) { this.page = page; } async verifyProductName(expectedName: string) { await expect(this.productName).toHaveText(expectedName); } async verifyProductDescriptionContains(expectedText: string) { await expect(this.productDescription).toContainText(expectedText); } } """ ### 6.2 Do This: Leverage "page.evaluate()" for Complex Component Interactions * **Standard:** Use "page.evaluate()" to execute JavaScript code directly within the browser context for complex interactions or state retrieval. * **Why:** Provides flexibility for handling scenarios that are difficult to achieve with Playwright's standard API. """typescript // components/date-picker.component.ts import { Page } from '@playwright/test'; export class DatePickerComponent { readonly page: Page; readonly datePicker = this.page.locator('.date-picker'); constructor(page: Page) { this.page = page; } async selectDate(date: Date) { await this.page.evaluate( ([date, datePickerSelector]) => { const datePickerElement = document.querySelector(datePickerSelector); if (datePickerElement) { // Complex JavaScript logic to select the date // (e.g., manipulating the date picker's internal state) // This is a simplified example; the actual implementation // will depend on the specific date picker library being used. const day = date.getDate(); const dayElement = datePickerElement.querySelector(".day[data-day="${day}"]"); if(dayElement) { (dayElement as HTMLElement).click(); } } }, [date, '.date-picker'] ); } } """ ### 6.3 Do This: Explore Custom Component Locators (Advanced) * **Standard:** Create custom locators to target elements within a component based on complex criteria. (Use only where clearly valuable). * **Why:** Can improve locator resilience and readability for advanced scenarios. * **Note:** This is an advanced technique. Overuse can reduce maintainability. Use carefully and only when standard locators are insufficient. """typescript // components/grid.component.ts import { Page, Locator } from '@playwright/test'; export class GridComponent { readonly page: Page; readonly grid: Locator; constructor(page: Page, gridSelector: string) { this.page = page; this.grid = page.locator(gridSelector); } getCell(row: number, column: number): Locator { return this.grid.locator("tr:nth-child(${row}) td:nth-child(${column})"); } async getCellValue(row: number, column: number): Promise<string | null> { return await this.getCell(row, column).textContent(); } } """ ## 7. Continuous Improvement Component Design in Playwright, like any software development practice, should be continuously evaluated and improved. Regularly refactor components to reflect changes in the application UI and evolving testing needs. Solicit feedback from other developers and testers to identify areas for improvement. Keep abreast of new Playwright features and best practices to ensure your components remain maintainable and effective.
# API Integration Standards for Playwright This document outlines coding standards for API integration within Playwright tests. It focuses on best practices for interacting with backend services and external APIs to create robust, reliable, and maintainable end-to-end tests. These standards are tailored for the latest Playwright version. ## 1. Architecture and Design Principles ### 1.1. Separation of Concerns **Do This:** Isolate API interaction logic from UI test logic. Create dedicated modules or classes specifically for API calls. **Don't Do This:** Embed API calls directly within UI test functions. **Why:** Separation of concerns improves code readability, maintainability, and reusability. It also makes tests less brittle, as changes to the UI won't affect the API interaction logic, and vice versa. **Example:** """javascript // api_client.js import { APIRequestContext, request } from '@playwright/test'; export class ApiClient { constructor(private readonly baseURL: string) {} async getUsers(request: APIRequestContext, params?: any) { const response = await request.get("${this.baseURL}/users", { params }); expect(response.status()).toBe(200); return await response.json(); } async createUser(request: APIRequestContext, data: any) { const response = await request.post("${this.baseURL}/users", { data }); expect(response.status()).toBe(201); return await response.json(); } // Add more API methods as needed } // test.spec.js import { test, expect } from '@playwright/test'; import { ApiClient } from './api_client'; test.describe('User Management', () => { let apiClient: ApiClient; test.beforeEach(async ({ request }) => { apiClient = new ApiClient('https://api.example.com'); }); test('should create a new user', async ({ request }) => { const userData = { name: 'John Doe', email: 'john.doe@example.com' }; const newUser = await apiClient.createUser(request, userData); expect(newUser.name).toBe('John Doe'); }); test('should retrieve a list of users', async ({ request }) => { const users = await apiClient.getUsers(request); expect(users.length).toBeGreaterThan(0); }); }); """ ### 1.2. Abstraction and Reusability **Do This:** Create reusable API helper functions or classes to encapsulate common API interactions. Utilize environment variables for configuration (e.g., API base URLs, authentication tokens). **Don't Do This:** Duplicate API call logic across multiple tests. Hardcode sensitive information. **Why:** Reusable API helpers reduce code duplication, simplify test maintenance, and improve consistency. Environment variables enhance security and flexibility. **Example:** """javascript // api_helpers.js import { expect } from '@playwright/test'; export async function validateApiResponse(response, expectedStatus) { expect(response.status()).toBe(expectedStatus); const responseBody = await response.json(); return responseBody; } // test.spec.js import { test, expect } from '@playwright/test'; import { validateApiResponse } from './api_helpers'; test('should update an existing user', async ({ request }) => { const userId = 1; // Replace with actual user ID const updatedData = { name: 'Jane Doe' }; const response = await request.put("/users/${userId}", { data: updatedData }); const responseBody = await validateApiResponse(response, 200); expect(responseBody.name).toBe('Jane Doe'); }); """ ### 1.3. Data Fixtures and Test Data Management **Do This:** Use data fixtures or factories to generate realistic and consistent test data. Employ Playwright's fixtures for managing test state, including API-related setup and teardown. **Don't Do This:** Rely on hardcoded or unpredictable data. Modify production data directly in tests. **Why:** Consistent test data ensures test repeatability and isolates tests from external dependencies. Playwright fixtures provide a clean and structured way to manage test context including API state. **Example:** """javascript // playwright.config.js import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ use: { baseURL: 'https://api.example.com', }, projects: [ { name: 'api', testMatch: /.*\.api\.spec\.js/, }, { name: 'e2e', testMatch: /.*\.spec\.js/, dependencies: ['api'], }, ], }); // user_fixtures.js import { test as base } from '@playwright/test'; import { ApiClient } from './api_client'; interface UserFixtures { apiClient: ApiClient; testUser: any; } export const test = base.extend<UserFixtures>({ apiClient: async ({ request }, use) => { const apiClient = new ApiClient('https://api.example.com'); await use(apiClient); }, testUser: async ({ apiClient, request }, use) => { const userData = { name: 'Test User', email: 'test@example.com' }; const testUser = await apiClient.createUser(request, userData); await use(testUser); // Teardown (cleanup after the test) await apiClient.deleteUser(request, testUser.id); }, }); export { expect } from '@playwright/test'; // test.spec.js import { test, expect } from './user_fixtures'; test('should use the test user injected via fixture', async ({ testUser }) => { expect(testUser.name).toBe('Test User'); // Access API via fixtures }); """ ## 2. API Request Handling ### 2.1. Using "APIRequestContext" **Do This:** Use Playwright's "APIRequestContext" for making API calls within tests. Configure the "baseURL" in the Playwright configuration file. **Don't Do This:** Use external HTTP libraries directly. **Why:** "APIRequestContext" integrates seamlessly with Playwright's tracing and reporting capabilities. It provides request/response logging and automatic retries. The baseURL config simplifies relative path usage. **Example:** """javascript import { test, expect } from '@playwright/test'; test('should fetch data from an API endpoint', async ({ request, baseURL }) => { const response = await request.get('/data'); // Uses baseURL from playwright.config.js expect(response.status()).toBe(200); const data = await response.json(); expect(data).toBeDefined(); }); """ ### 2.2. Request Options **Do This:** Use request options to configure headers, query parameters, request bodies, and timeouts. **Don't Do This:** Hardcode headers or query parameters directly in the URL. **Why:** Request options provide a structured and readable way to configure API requests. **Example:** """javascript import { test, expect } from '@playwright/test'; test('should send a POST request with JSON data and custom headers', async ({ request }) => { const data = { key1: 'value1', key2: 'value2' }; const headers = { 'Content-Type': 'application/json', 'Authorization': 'Bearer token' }; const response = await request.post('/resource', { data: data, headers: headers, timeout: 5000 }); expect(response.status()).toBe(201); }); """ ### 2.3. Handling Different HTTP Methods **Do This:** Use the appropriate HTTP method (GET, POST, PUT, DELETE, PATCH) for each API operation based on RESTful principles. **Don't Do This:** Use GET requests to modify data on the server. **Why:** Using the correct HTTP method ensures that the API is used as intended and improves the clarity and correctness of your tests. **Example:** """javascript import { test, expect } from '@playwright/test'; test('should create a new resource', async ({ request }) => { const data = { title: 'New Resource' }; const response = await request.post('/resources', { data: data }); expect(response.status()).toBe(201); // Created }); test('should update an existing resource', async ({ request }) => { const resourceId = 1; const data = { title: 'Updated Resource' }; const response = await request.put("/resources/${resourceId}", { data: data }); expect(response.status()).toBe(200); }); test('should delete an existing resource', async ({ request }) => { const resourceId = 1; const response = await request.delete("/resources/${resourceId}"); expect(response.status()).toBe(204); // No Content - successful deletion }); """ ## 3. Response Handling and Assertions ### 3.1. Status Code Validation **Do This:** Always assert that the HTTP status code is as expected. Use specific status codes (e.g., 200, 201, 204, 400, 404, 500) rather than generic success or error checks. **Don't Do This:** Ignore status codes or only check for a generic "success" code. **Why:** Status code validation is crucial for verifying the success or failure of an API operation. Specific status codes provide more granular information. **Example:** """javascript import { test, expect } from '@playwright/test'; test('should return a 404 status code for a non-existent resource', async ({ request }) => { const response = await request.get('/nonexistent'); expect(response.status()).toBe(404); }); """ ### 3.2. Response Body Validation **Do This:** Validate the structure and content of the response body using "expect". Use specific matchers (e.g., "toBe", "toEqual", "toContain") to verify data values. **Don't Do This:** Assume the response body is always correct. Only check that *something* is returned. **Why:** Response body validation ensures that the API returns the expected data and format. **Example:** """javascript import { test, expect } from '@playwright/test'; test('should return user data with the correct structure and values', async ({ request }) => { const response = await request.get('/user/123'); expect(response.status()).toBe(200); const user = await response.json(); expect(user).toEqual({ id: 123, name: 'John Doe', email: 'john.doe@example.com' }); }); """ ### 3.3 Schema Validation **Do This:** Consider implementing schema validation using libraries like "ajv" or "zod" to ensure that the API responses adhere to a predefined schema. **Don't Do This:** Rely solely on ad-hoc assertions, especially for complex API responses. **Why:** Schema validation provides a more robust and comprehensive way to validate API responses, preventing unexpected data types or missing fields from causing test failures. **Example:** """javascript import { test, expect } from '@playwright/test'; import Ajv from 'ajv'; const ajv = new Ajv(); const userSchema = { type: 'object', properties: { id: { type: 'integer' }, name: { type: 'string' }, email: { type: 'string', format: 'email' }, }, required: ['id', 'name', 'email'], }; test('should validate the user data against the schema', async ({ request }) => { const response = await request.get('/user/123'); expect(response.status()).toBe(200); const user = await response.json(); const validate = ajv.compile(userSchema); const valid = validate(user); if (!valid) { console.log(validate.errors); // Log validation errors } expect(valid).toBe(true); }); """ ### 3.4. Error Handling **Do This:** Implement proper error handling to gracefully handle unexpected API responses (e.g., network errors, server errors, invalid data). Use "try...catch" blocks to catch exceptions and provide informative error messages. **Don't Do This:** Let unhandled exceptions crash the tests. **Why:** Robust error handling prevents tests from failing due to transient issues and provides valuable debugging information. **Example:** """javascript import { test, expect } from '@playwright/test'; test('should handle network errors gracefully', async ({ request }) => { try { const response = await request.get('/unreachable', { timeout: 1 }); expect(response.status()).toBe(200); } catch (error: any) { //console.error('API request failed:', error.message); expect(error.message).toContain('Timeout'); // Expect the timeout error. } }); """ ## 4. Authentication and Authorization ### 4.1. Handling Authentication **Do This:** Use appropriate authentication mechanisms (e.g., API keys, tokens, OAuth) to access protected APIs. Store credentials securely using environment variables or secrets management tools. **Don't Do This:** Hardcode credentials directly in the test code or commit them to the repository. **Why:** Secure authentication protects sensitive data and ensures that only authorized users can access API resources. **Example:** """javascript import { test, expect } from '@playwright/test'; test('should authenticate using an API key', async ({ request }) => { const apiKey = process.env.API_KEY; const headers = { 'X-API-Key': apiKey }; const response = await request.get('/protected-resource', { headers: headers }); expect(response.status()).toBe(200); }); test('should authenticate using bearer token', async ({ request }) => { const bearerToken = process.env.BEARER_TOKEN; const headers = { 'Authorization': "Bearer ${bearerToken}" }; const response = await request.get('/protected-resource', { headers: headers }); expect(response.status()).toBe(200); }); """ ### 4.2. Authorization Checks **Do This:** Verify that users have the necessary permissions to access API resources. Test both authorized and unauthorized scenarios. **Don't Do This:** Assume that all users have access to all resources. **Why:** Authorization checks ensure that the API enforces access control policies correctly. **Example:** """javascript import { test, expect } from '@playwright/test'; test('should return a 403 status code for unauthorized access', async ({ request }) => { const response = await request.get('/admin-resource'); // User without admin role expect(response.status()).toBe(403); // Forbidden }); """ ### 4.3 Session Management **Do This:** Leverage Playwright's "storageState" capabilities to manage API sessions and avoid redundant authentication steps. Store authentication tokens in the storage state for reuse across tests. **Don't Do This:** Authenticate for every single test case. **Why:** Session management improves test performance and reduces load on the authentication server. **Example:** """javascript //auth.setup.ts import { test as setup, expect } from '@playwright/test'; const adminUser = { username: 'admin', password: 'password'}; setup('authenticate and store user session', async ({ request, baseURL }) => { // Perform authentication steps using API calls const response = await request.post('/auth/login', { data: adminUser }); //Authenticates user const token= await response.json() // Save the authentication token to storage state await setup.context().storageState({ path: 'admin.json' }); }); // test.spec.ts import { test, expect } from '@playwright/test'; test.use({ storageState: 'admin.json' }); //use auth from default storageState.json file test('should access a protected resource after authentication', async ({ request }) => { const response = await request.get('/protected-resource'); expect(response.status()).toBe(200); // Assuming authentication was successful }); """ ## 5. Performance and Optimization ### 5.1. Minimize API Calls **Do This:** Reduce the number of API calls by batching operations or retrieving data in a single request. **Don't Do This:** Make excessive API calls that can slow down tests and overload the server. **Why:** Minimizing API calls improves test performance and reduces the load on the backend system. **Example:** Instead of calling an API to retrieve each user individually, call an API to return all users in a single response, then filter the data in your assertion ### 5.2. Parallelization **Do This:** Run API tests in parallel to reduce overall test execution time. Configure parallelization in the Playwright configuration file. **Don't Do This:** Run tests sequentially if they can be run in parallel without conflicts. **Why:** Parallelization significantly reduces test execution time, especially for large test suites. **Example:** Configured in "playwright.config.js" """javascript import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ workers: process.env.CI ? 2 : undefined, }); """ ### 5.3. Caching **Do This:** Consider implementing caching mechanisms for API responses that are frequently accessed. **Don't Do This:** Cache sensitive data or data that changes frequently. **Why:** Caching improves test performance by reducing the need to make repeated API calls. ## 6. Security Considerations ### 6.1. Data Sanitization **Do This:** Sanitize input data to prevent injection attacks (e.g., SQL injection, XSS). **Don't Do This:** Pass unsanitized data directly to the API. **Why:** Data sanitization protects the backend system from malicious input. ### 6.2. Rate Limiting **Do This:** Respect API rate limits and implement retry mechanisms to handle rate limiting errors. **Don't Do This:** Exceed API rate limits, which can lead to temporary or permanent blocking. **Why:** Rate limiting ensures fair usage of API resources and prevents abuse. ### 6.3. Sensitive Data Handling **Do This:** Avoid logging sensitive data (e.g., passwords, API keys) in test logs or reports. Use masking or redaction techniques to protect sensitive information. **Don't Do This:** Expose sensitive data in test outputs. **Why:** Protecting sensitive data prevents accidental exposure and security breaches. ## 7. Code Style and Conventions ### 7.1. Descriptive Naming **Do This:** Use descriptive names for API helper functions, variables, and test cases. **Don't Do This:** Use ambiguous or generic names. **Why:** Descriptive names improve code readability and maintainability. ### 7.2. Consistent Formatting **Do This:** Follow a consistent code formatting style using tools like Prettier or ESLint. **Don't Do This:** Use inconsistent or inconsistent formatting. **Why:** Consistent formatting improves code readability and reduces the risk of errors. ### 7.3. Comments and Documentation **Do This:** Add comments to explain complex API interactions or non-obvious logic. Document API helper functions and classes. **Don't Do This:** Write overly verbose or redundant comments. Omit comments for complex code. **Why:** Comments and documentation improve code understanding and maintainability. By adhering to these coding standards, developers can create robust, reliable, and maintainable API integration tests in Playwright that effectively validate the functionality and performance of backend services and external APIs.