# 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({
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.
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'
# Security Best Practices Standards for Playwright This document outlines security best practices for Playwright tests to ensure reliable and secure end-to-end testing. It focuses on mitigating common vulnerabilities and adopting secure coding patterns within the Playwright framework. These standards promote robust tests and prevent tests themselves from becoming security risks. ## 1. Input Validation and Sanitization ### 1.1. Standard: Validate and sanitize ALL inputs provided to Playwright actions. **Do This:** * Implement robust input validation for text fields, dropdown selections, and other input elements manipulated by your tests. * Use allow lists (whitelisting) to define acceptable input characters and patterns instead of relying on denylists (blacklisting), which are often incomplete. * Sanitize inputs to neutralize potentially harmful characters or sequences, particularly when dealing with user-provided data that might be injected into the application under test. **Don't Do This:** * Assume inputs are always safe or well-formed. * Use regular expressions without understanding their potential performance implications (ReDoS attacks). * Directly inject unsanitized data from external sources (e.g., environment variables) into selectors or assertions. **Why This Matters:** * Prevents Cross-Site Scripting (XSS) attacks by validating the test data doesn't contain malicious scripts. * Guards against injection vulnerabilities if the application under test is susceptible. * Improves test robustness by handling unexpected input formats. * Avoids potential command injection if test input ends up being used in shell commands (directly or indirectly). **Code Example:** """javascript const { test, expect } = require('@playwright/test'); test('Ensure input field sanitization', async ({ page }) => { const maliciousInput = '<script>alert("XSS");</script>Normal Text'; await page.goto('https://example.com/form'); // Replace with your test URL const inputElement = page.locator('#name'); // Replace with your input element selector // Sanitize input (Example: remove script tags) const sanitizedInput = maliciousInput.replace(/<script.*?>.*?<\/script>/gi, ''); await inputElement.fill(sanitizedInput); await page.locator('#submit').click(); // Assert that the alert is NOT displayed by checking the page content. await expect(page.content()).not.toContain('XSS'); // Or check a specific element }); """ ### 1.2. Standard: Parameterize tests when dealing with sensitive data. **Do This:** * Use parameterized tests to explore different input combinations without hardcoding specific values directly within your test logic. This makes testing easier and reveals corner cases. * Store sensitive data (usernames, passwords, API keys) in environment variables or secure configuration files, and access them through Playwright's configuration options. **Don't Do This:** * Hardcode sensitive information directly in test files. You may accidentally check secrets into source control! * Rely on external APIs without proper authentication and authorization. **Why This Matters:** * Prevents accidental exposure of sensitive data in version control systems. * Simplifies test configuration management. * Enhances the flexibility and reusability of tests. **Code Example:** """javascript const { test, expect } = require('@playwright/test'); const validUsername = process.env.VALID_USERNAME; const validPassword = process.env.VALID_PASSWORD; test('Login with valid credentials', async ({ page }) => { await page.goto('https://example.com/login'); // Replace with your test URL await page.locator('#username').fill(validUsername); await page.locator('#password').fill(validPassword); await page.locator('#login-button').click(); await expect(page.locator('#welcome-message')).toContainText('Welcome'); }); """ ### 1.3 Standard: Use "locator.inputValue()" to verify input values. **Do This:** * Prefer "locator.inputValue()" for verifying the actual value of an input, especially when dealing with automated input. It's closer to what the user sees and tests the application more realistically. **Don't Do This:** * Rely solely on DOM attributes (like "value") as a primary means of verification, as these can be manipulated outside of the direct user interaction. **Why This Matters:** * Provides a more reliable representation of the actual input value as perceived by the user. * Catches discrepancies between the intended input and the actual application state. **Code Example:** """javascript const { test, expect } = require('@playwright/test'); test('Verify input value after filling', async ({ page }) => { await page.goto('https://example.com/form'); // Replace with your test URL const inputElement = page.locator('#email'); // Replace with your input element selector const testEmail = 'test@example.com'; await inputElement.fill(testEmail); // Verify the actual input value expect(await inputElement.inputValue()).toBe(testEmail); }); """ ## 2. Secure Context Management ### 2.1. Standard: Isolate test environments for each test run. **Do This:** * Utilize containerization (e.g., Docker) or virtual machines to create isolated test environments, preventing interference between test runs. * Reset the application database or file system to a known clean state before each test suite or test. **Don't Do This:** * Share mutable test data or states between tests. * Run tests against a production environment, as this can lead to data corruption or security breaches. **Why This Matters:** * Ensures reproducibility and reliability of tests. * Prevents test contamination, where one test's actions affect the outcome of subsequent tests. * Reduces the risk of unintended modifications to production data. **Code Example:** While Playwright doesn't directly manage Docker, the following conceptually demonstrates the setup/teardown process using Playwright's "beforeEach" and "afterEach" hooks in "playwright.config.js": """javascript // playwright.config.js const config = { use: { baseURL: 'http://localhost:3000', // Your application's test URL // Assuming a script to reset the database contextOptions: { ignoreHTTPSErrors: true, // Only for testing purposes, not production! }, }, globalSetup: require.resolve('./global-setup'), // Sets up the test environment globalTeardown: require.resolve('./global-teardown'), // Tears down the test environment }; module.exports = config; // global-setup.js async function globalSetup(config) { console.log('Setting up the test environment...'); // Execute a script or command to start the Docker container or VM // For instance: // await exec('docker compose up -d'); console.log('Test environment is ready.'); // Could also seed a test database here. } module.exports = globalSetup; // global-teardown.js async function globalTeardown(config) { console.log('Tearing down the test environment...'); // Execute a script or command to stop the Docker container or VM // For instance: // await exec('docker compose down'); console.log('Test environment has been cleaned up.'); } """ ### 2.2. Standard: Handle Browser Contexts and Storage State Securely. **Do This:** * Use Playwright's browser context isolation features to prevent cookies, cache, and other storage data from being shared between tests. * For sensitive operations (e.g., login), explicitly clear browser storage using "context.clearCookies()" or "context.clearStorage()" after the test. * Utilize "context.storageState()" to save and reuse authentication states, storing these states securely if they contain sensitive data. **Don't Do This:** * Rely on implicit browser state cleanup, as inconsistencies can occur. * Store sensitive authentication details (passwords, API keys) directly in the storage state file. Consider storing only session tokens. **Why This Matters:** * Prevents unintended authentication or authorization bypasses. * Minimizes the risk of data leakage between tests. * Ensures a consistent and predictable test environment. **Code Example:** """javascript const { test, expect } = require('@playwright/test'); const fs = require('fs'); test('Login and save authentication state', async ({ browser }) => { const context = await browser.newContext(); const page = await context.newPage(); await page.goto('https://example.com/login'); // Replace with your test URL await page.locator('#username').fill('testuser'); await page.locator('#password').fill('testpassword'); await page.locator('#login-button').click(); await expect(page.locator('#welcome-message')).toContainText('Welcome'); // Save storage state (cookies, localStorage, etc.) await context.storageState({ path: 'auth.json' }); await context.close(); }); test('Reuse authentication state', async ({ browser }) => { const context = await browser.newContext({ storageState: 'auth.json' }); const page = await context.newPage(); await page.goto('https://example.com/profile'); // Replace with your profile page URL await expect(page.locator('#profile-name')).toContainText('testuser'); // Verify user is logged in // Cleanup after use: await context.clearCookies(); await context.clearStorage(); await fs.promises.unlink('auth.json'); //Optional, remove the saved file. await context.close(); }); """ ## 3. Network Security ### 3.1. Standard: Monitor and control network requests during tests. **Do This:** * Use Playwright's request interception features to inspect, modify, or block network requests made by the application under test. * Verify that sensitive data is transmitted over HTTPS (TLS/SSL) and that certificates are valid. * Implement checks to prevent the application from making unauthorized network requests to external domains. * Utilize "page.route" to mock external API calls, avoiding reliance on live external services during testing (especially for sensitive endpoints). **Don't Do This:** * Allow tests to make uncontrolled network requests to arbitrary destinations. * Ignore certificate errors in production-like test environments (unless explicitly required for testing purposes with self-signed certificates). **Why This Matters:** * Prevents data leakage to untrusted third parties. * Ensures that sensitive information is transmitted securely. * Reduces dependencies on external services, making tests more reliable and isolated. * Helps identify potential vulnerabilities related to cross-origin communication. **Code Example:** """javascript const { test, expect } = require('@playwright/test'); test('Verify HTTPS and block unauthorized requests', async ({ page }) => { await page.route('**/*', route => { const url = route.request().url(); if (url.startsWith('https://')) { // Check if the certificate is valid (this is a simplification; more advanced checks can be added) } else { console.warn("Non-HTTPS request detected: ${url}"); } if (url.includes('unauthorized-domain.com')) { console.warn("Blocking unauthorized request to: ${url}"); return route.abort(); // Block the request } route.continue(); // Allow the request }); await page.goto('https://example.com'); // Replace with your test URL // ... perform actions that trigger network requests ... }); test('Mock external API endpoint', async ({ page }) => { await page.route('https://api.example.com/users', async route => { const json = [{ name: 'Mocked User' }]; await route.fulfill({ json }); }); await page.goto('https://example.com/users'); await expect(page.locator('#user-list')).toContainText('Mocked User'); }); """ ### 3.2. Standard: Handle redirects securely **Do This:** * Verify that redirects are happening as expected and to the correct destination URL. * If sensitive data is involved, ensure redirects are using HTTPS and that the destination is a trusted domain. **Don't Do This:** * Blindly follow redirects without validating the target URL. An attacker could inject a malicious redirect. **Why This Matters:** * Prevents open redirect vulnerabilities, where attackers can redirect users to phishing sites. * Ensures data integrity during the redirect process. **Code Example:** """javascript const { test, expect } = require('@playwright/test'); test('Verify secure redirect', async ({ page }) => { await page.goto('https://example.com/redirect-me'); // Replace with your test URL await page.waitForURL('https://example.com/secure-destination'); // Replace with expected destination expect(page.url()).toBe('https://example.com/secure-destination'); }); test('Prevent insecure redirect', async ({ page }) => { await page.route('https://example.com/malicious-redirect', route => { route.fulfill({ status: 302, headers: { 'Location': 'http://evil.com' }}); }); await page.goto('https://example.com/malicious-redirect'); //If done correctly, the page should NOT redirect to evil.com. await page.waitForTimeout(100); expect(page.url()).not.toContain("evil.com"); }); """ ## 4. Secrets Management ### 4.1. Standard: Implement secure handling of API keys and secrets within tests. **Do This:** * Store API keys and other secrets in environment variables or secure configuration files, *never* directly in the test code. * Use a secrets management tool (HashiCorp Vault, AWS Secrets Manager, Azure Key Vault) to store and retrieve secrets, especially in CI/CD environments. * Rotate API keys and secrets regularly to minimize the impact of potential breaches. **Don't Do This:** * Commit secrets to version control systems. * Expose secrets in test logs or error messages. **Why This Matters:** * Prevents unauthorized access to sensitive resources or functionality. * Reduces the risk of data breaches caused by compromised API keys. * Enables secure automation of test execution in CI/CD pipelines. **Code Example:** """javascript const { test, expect } = require('@playwright/test'); const apiKey = process.env.API_KEY; // Retrieve API key from environment variable test('Use API with API key', async ({ page }) => { // Example: Assuming you make API calls from the page await page.route('https://api.example.com/data', async route => { const request = route.request(); const headers = request.headers(); if (headers['x-api-key'] === apiKey) { await route.fulfill({ json: { message: 'Success' } }); } else { await route.fulfill({ status: 401, json: { error: 'Unauthorized' } }); } }); await page.goto('https://example.com/page-that-uses-api'); await expect(page.locator('#api-response')).toContainText('Success'); }); """ ### 4.2. Standard: Protect sensitive data in trace files. **Do This:** * Be extremely careful when enabling Playwright's tracing features (e.g., "trace.start", "trace.stop"). Trace files can include sensitive data like API responses from the application under test, form inputs, and cookies. * If tracing is enabled, ensure that trace files are stored securely and not exposed to unauthorized users. Prefer temporary storage to a shared drive or source-controlled repository. * Consider redacting sensitive information from trace files before sharing them or storing them permanently. Playwright currently offers limited redaction capabilities so this has to be done with external tools. **Don't Do This:** * Enable tracing in production environments or without careful consideration of the potential security implications. * Share trace files with untrusted parties without redacting sensitive information. * Check trace files into version control without proper access controls. **Why This Matters:** * Prevents the accidental leakage of sensitive data contained within trace files. * Protects user privacy and confidentiality. * Reduces the risk of security breaches caused by exposed credentials or API keys. **Code Example Showing How to Enable Tracing (Use with Caution):** """javascript const { test, expect } = require('@playwright/test'); test.describe('Tracing enabled example', () => { test.use({ trace: 'on', }); test('Sensitive data test', async ({ page }) => { await page.goto('https://example.com
# 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.
# 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.
# 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.
# Performance Optimization Standards for Playwright This document outlines performance optimization standards for Playwright tests, ensuring speed, responsiveness, and efficient resource utilization. These standards are designed to guide developers in writing high-performance Playwright tests and to serve as context for AI coding assistants. ## 1. Efficient Selectors and Locators Choosing the right selectors is crucial for test performance. Slow or ambiguous selectors can dramatically increase test execution time. ### 1.1 Prioritize Specific Selectors Specificity allows Playwright to quickly identify elements. Avoid overly-general selectors that require Playwright to scan the entire DOM. **Do This:** * Use "data-testid" attributes for reliable and performant element selection. These are specifically designed for testing and are less likely to change with UI updates. * Utilize "getByRole()" with specific "name" options when semantic HTML is available. * Combine selectors to target elements more directly. **Don't Do This:** * Rely solely on CSS classes, especially generic ones shared across many elements. * Use deeply nested CSS selectors without a clear need. * Use XPath selectors as the primary means of element location (generally slower than CSS or semantic selectors). **Why:** Specific selectors reduce the search space for Playwright, leading to faster element identification and reduced test execution time. "data-testid" attributes offer stability and are independent of styling, making them ideal for testing. **Code Example:** """javascript // Good: Using data-testid await page.locator('data-testid=submit-button').click(); // Good: Using getByRole with a name option for accessibility and testability await page.getByRole('button', { name: 'Submit' }).click(); // Bad: Generic CSS class selector await page.locator('.button').click(); // Bad: Deeply nested CSS selector await page.locator('div > div > div > button.submit').click(); """ ### 1.2 Leverage "nth" and "first"/"last" Wisely When dealing with multiple elements matching a selector, use "nth", "first", or "last" to target the desired element directly, instead of iterating through a list. **Do This:** * Use "nth(index)" to select a specific element from a list of matching elements. * Use "first()" or "last()" to target the first or last element in a list. **Don't Do This:** * Iterate through all matching elements when you only need to interact with one. **Why:** Directly targeting the needed element avoids unnecessary DOM traversal and reduces test execution time. **Code Example:** """javascript // Good: Using nth() to target the second element await page.locator('.item').nth(1).click(); // Good: Using first() to target the first element await page.locator('.item').first().click(); // Bad: Iterating when you only need one element const items = await page.locator('.item').all(); await items[1].click(); //Slower and more verbose """ ## 2. Minimizing Waits and Timeouts Explicit and implicit waits can introduce significant delays into your tests. Modern Playwright features largely obviate the need for sleeps. ### 2.1 Web-First Assertions Playwright's web-first assertions automatically retry until conditions are met, eliminating the need for arbitrary timeouts. **Do This:** * Use "expect(locator).toBeVisible()", "expect(locator).toHaveText()", and other web-first assertions. * Rely on retry mechanism within framework. **Don't Do This:** * Use "await page.waitForTimeout(duration)" for fixed delays. * Use "await page.waitForSelector(selector, { timeout: duration })" unnecessarily when web-first assertions can be used. **Why:** Web-first assertions wait only as long as necessary, reacting dynamically to the application state, minimizing delays and flakiness. **Code Example:** """javascript // Good: Web-first assertion await expect(page.locator('#success-message')).toBeVisible({ timeout: 5000 }); // Waits up to 5 seconds // Bad: Fixed timeout await page.waitForTimeout(5000); // Waits for 5 seconds, regardless of when the element appears await page.locator('#success-message').isVisible(); // Risk missing the element // Bad: Implicit wait (less preferred) await page.waitForSelector('#success-message', { timeout: 5000 }); // Potentially waits unnecessarily await page.locator('#success-message').isVisible(); // Risk missing the element """ ### 2.2 Actionability Checks Playwright performs actionability checks by default before performing actions like "click()" or "fill()". These ensure the element is visible, enabled, and stable. Disable them only when absolutely necessary and you have a clear understanding of the risks. **Do This:** * Let Playwright perform automatic actionability checks. * If needed, configure actionability checks using the "force: true" option *judiciously* and with comments explaining why it's needed. **Don't Do This:** * Disable actionability checks globally unless there's clear evidence that it substantially improves performance and the risks are well-managed. * Disable checks without understanding the potential for flaky tests. **Why:** Actionability checks prevent tests from interacting with elements that are not ready, reducing flakiness. **Code Example:** """javascript // Good: Relying on Playwright's automatic actionability checks await page.locator('#my-button').click(); // Use with caution after careful consideration await page.locator('#my-button').click({force: true}); // When needed to interact with hidden/overlapped elements // Bad: Disabling actionability checks globally (generally not recommended) // playwright.config.ts: // use: { // actionTimeout: 0 // Disables all actionability checks - NOT RECOMMENDED // } """ ### 2.3 Route interception for eliminating unnecessary delays Route interception can be used to short-circuit expensive calls on the client side during testing. **Do This:** * Use "page.route" to mock or stub API responses when the actual API call is not relevant to the test. * Use "page.route" to block unnecessary resources like images or third-party scripts. **Don't Do This:** * Mock API responses that are critical to the test's logic. * Block resources required for rendering the UI under test. **Why:** Route interception can dramatically reduce test execution time by avoiding network requests that are not essential for the test. **Code Example:** """javascript // Good: Mocking an API response await page.route('/api/data', async route => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ message: 'Mocked data' }), }); }); // Good: Blocking images await page.route('**/*.{png,jpg,jpeg}', route => route.abort()); // Bad: Mocking a critical API response without proper validation await page.route('/api/login', async route => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ success: true }), // Always returns success, masking potential login issues }); }); """ ## 3. Parallelization and Sharding Parallelization and sharding distribute tests across multiple workers, significantly reducing overall test execution time. ### 3.1 Configure Parallelization Correctly Playwright allows easy configuration of parallelization in the "playwright.config.ts" file. **Do This:** * Set "fullyParallel: true" in "playwright.config.ts" to run tests in parallel across multiple workers. Adjust the number of workers based on your machine's resources. * Consider the resources available on your CI/CD system when configuring workers. * Use "shard" to split tests evenly across workers. **Don't Do This:** * Overload your machine with too many workers, leading to performance degradation. * Run tests in parallel without considering potential conflicts or shared state. **Why:** Parallelization dramatically reduces overall test execution time by distributing tests across multiple workers. **Code Example:** """typescript // playwright.config.ts import { defineConfig } from '@playwright/test'; export default defineConfig({ fullyParallel: true, workers: process.env.CI ? 4 : 2, // Adjust based on your machine reporter: 'html', shard: {total: 2, current: 1}, // Example demonstrating sharding. Adjust if needed. }); """ ### 3.2 Isolate Test State Ensure that tests are independent and do not share state. Shared state can lead to unpredictable behavior and flakiness when running in parallel. **Do This:** * Use "context.newPage()" to create a new browser context for each test. This ensures isolation of cookies, storage, and other state. * Use "test.use({ storageState: ... })" to manage user sessions/authentication. * Clean up any data created during a test after it has completed. **Don't Do This:** * Rely on shared cookies or storage across tests without careful consideration. * Modify global state that could affect other tests. **Why:** Isolating test state prevents tests from interfering with each other, especially when running in parallel. **Code Example:** """javascript import { test } from '@playwright/test'; test('isolated test', async ({ browser }) => { const context = await browser.newContext(); const page = await context.newPage(); await page.goto('https://example.com'); // ... your test logic await context.close(); }); test('use pre-baked auth state', async ({browser}) => { const testUserAuthState = JSON.parse(fs.readFileSync('test-user-auth.json', 'utf-8')); const context = await browser.newContext({storageState: testUserAuthState}); const page = await context.newPage(); await page.goto('https://example.com'); // Test logic that assumes authenticated user await context.close(); }) """ ## 4. Browser Context Management Efficiently manage browser contexts to optimize resource utilization and test speed. ### 4.1 Reuse Browser Contexts Wisely Creating a new browser context for each test can be resource-intensive. Reuse browser contexts when appropriate, but ensure proper isolation between tests. **Do This:** * Reuse browser contexts for tests that do not require complete isolation (e.g., tests within the same feature or component). * Clear cookies or storage before each test when reusing contexts within a single file. * Consider using "beforeEach" and "afterEach" hooks to set up and tear down the state for each test. **Don't Do This:** * Reuse browser contexts across different features or components without careful consideration. * Assume that browser contexts are always clean between tests without explicitly clearing state. **Why:** Reusing browser contexts reduces the overhead of creating new contexts for each test, improving performance. **Code Example:** """javascript import { test, expect } from '@playwright/test'; let context; let page; test.beforeAll(async ({ browser }) => { //Setup test context ONCE for the suite. context = await browser.newContext(); page = await context.newPage(); await page.goto('https://example.com'); }); test.afterAll(async () => { await context.close(); // Clean up test context ONCE after the suite finished. }); test.beforeEach(async () => { // Clear cookies and storage before each test await page.context().clearCookies(); await page.evaluate(() => localStorage.clear()); }); test('test 1 - Reusing the browser context and page', async () => { await expect(page.locator('body')).toContainText("Example"); }); test('test 2 - Reusing the browser context and page', async () => { await expect(page.locator('body')).toContainText("Example"); }); """ ### 4.2 Optimize Browser Launch Options Configure browser launch options to reduce overhead and improve startup time. **Do This:** * Use headless mode ("headless: true") for faster test execution when UI interaction is not required. * Disable graphics processing unit (GPU) acceleration ("ignoreDefaultArgs: ['--disable-extensions', '--hide-scrollbars', '--mute-audio']") if not necessary. **Don't Do This:** * Run tests in headed mode unnecessarily (consider when debugging only) as it is significantly slower. * Use unnecessary browser extensions or settings that can slow down test execution. **Why:** Optimizing browser launch options reduces overhead and improves test startup time. Note that some configurations might be incompatible with certain environments, so testing is required. **Code Example:** """typescript // playwright.config.ts import { defineConfig } from '@playwright/test'; export default defineConfig({ use: { headless: true, // Run in headless mode for faster execution ignoreDefaultArgs: ['--disable-extensions', '--hide-scrollbars', '--mute-audio'], // Disable GPU acceleration launchOptions: { // Use 'chromium' by default, can explicitly specify as well. // Channels support: 'chrome', 'msedge' channel: 'chrome' } }, }); """ ## 5. Debugging and Profiling Use Playwright's debugging and profiling tools to identify and address performance bottlenecks. ### 5.1 Use Trace Viewer Playwright's Trace Viewer provides detailed insights into test execution, including network requests, console logs, and DOM snapshots. **Do This:** * Enable tracing ("trace: 'on'" or "trace: 'retain-on-failure'") in "playwright.config.ts". * Use the Trace Viewer to analyze test execution and identify slow selectors, unnecessary waits, and other performance bottlenecks. **Don't Do This:** * Leave tracing enabled in production environments due to the performance overhead. **Why:** Trace Viewer provides valuable information for diagnosing and resolving performance issues. **Code Example:** """typescript // playwright.config.ts import { defineConfig } from '@playwright/test'; export default defineConfig({ use: { trace: 'retain-on-failure', // Enable tracing only on failure }, }); """ ### 5.2 Use Performance Metrics Playwright provides access to performance metrics through the "page.metrics()" method. Use these metrics to measure and track test performance over time. **Do This:** * Collect performance metrics such as CPU usage, memory consumption, and page load time. * Track performance metrics over time to identify regressions and improvements. **Don't Do This:** * Ignore performance metrics when optimizing tests. **Why:** Performance metrics provide objective data for measuring and tracking test performance. **Code Example:** """javascript import { test } from '@playwright/test'; test('performance test', async ({ page }) => { await page.goto('https://example.com'); const metrics = await page.metrics(); console.log(metrics); // ... your test logic }); """ ## 6. Code Review and Continuous Improvement Regular code reviews and continuous improvement are essential for maintaining high-performance Playwright tests. ### 6.1 Conduct Code Reviews Review Playwright tests regularly to identify and address performance issues. **Do This:** * Include performance considerations in code reviews. * Ensure that selectors are efficient, waits are minimized, and browser contexts are managed effectively. **Don't Do This:** * Skip code reviews for Playwright tests. **Why:** Code reviews help identify and address performance issues early in the development process. ### 6.2 Monitor and Optimize Test Performance Continuously monitor and optimize test performance to ensure that tests remain fast and reliable. **Do This:** * Track test execution time and identify slow tests. * Investigate and address performance bottlenecks as they arise. **Why:** Continuous monitoring and optimization ensure that tests remain fast and reliable over time. This document offers a comprehensive guide to performance optimization standards for Playwright. By following these guidelines, developers can create high-performance, reliable, and maintainable Playwright tests. Remember that constant monitoring, analysis, and refinement are vital for continuous improvement in test performance.