Playwright JavaScript Framework Best Practices
Playwright JavaScript Framework — Best Practices A comprehensive guide for writing reliable, maintainable, and scalable end-to-end tests using Playwright with JavaScript (and Cucumber BDD) in the KeyControl test automation framework. Table of Contents Project Structure & Organization Page Object Model (POM) Selectors & Locators Assertions Waiting Strategies Test Isolation & State Management Authentication & Login Test Data Management BDD / Cucumber Integration Error Handling & Debugging Retries & Flakiness Parallelism & Performance Configuration Management Reporting & Observability CI/CD Integration Security & Secrets Code Quality & Maintainability Accessibility & Cross-Browser Testing 1. Project Structure & Organization DO Keep a flat, predictable folder structure that mirrors the application's domain (e.g., admin/, performer/, approver/, ess/). Co-locate feature files, step definitions, and page objects by module/domain so related code is easy to find. Use index.js barrel exports to avoid long relative import paths. Store all environment-specific configuration in a single config.js at the root; never hardcode URLs or credentials inside test files. DON'T Don't scatter page objects and step definitions randomly across the project. Don't mix UI concerns with business logic in the same file. Recommended Layout features/ kc/ Admin_Group_Management_Module.feature Performer_Task_Management_KC1.feature step-definitions/ kc/ KeyControlAdminSteps.js KeyControlPerformerSteps.js page-objects/ kc/ basepage/ KeyControlAdminPage.js KeyControlPerformerPage.js utils/ logger.js ExcelHelper.js setup/ hooks.js assertions.js config.js Page Object Model (POM) DO Encapsulate all page interactions (clicks, fills, navigations) inside dedicated Page Object classes. Keep page objects thin — they should only expose methods, not assertions. Compose complex pages from smaller component objects (e.g., TableComponent, ModalComponent). Accept the Playwright page instance via the constructor; never create a new browser context inside a POM. // Good — page-objects/kc/keycontrol/KeyControlAdminPage.js export class KeyControlAdminPage { constructor(page) { this.page = page; // Pre-define locators for reuse this.groupNameInput = page.locator('[data-ga="group-name"]'); this.saveButton = page.locator('[data-ga="save-button"]'); this.successBanner = page.locator('[data-ga="success-banner"]'); } async navigateToGroupManagement() { await this.page.click('[data-ga="group-management"]'); } async createGroup(groupData) { await this.groupNameInput.fill(groupData.name); await this.saveButton.click(); } } DON'T Don't put expect() assertions inside page objects — keep them in step definitions or test files. Don't duplicate selectors across multiple files; define them once in the page object. Selectors & Locators Priority Order (most preferred → least preferred) | Priority | Strategy | Example | |---|---|---| | 1 | data-ga / data-test attributes | [data-ga="save-button"] | | 2 | ARIA roles & labels | page.getByRole('button', { name: 'Save' }) | | 3 | Playwright built-in locators | page.getByLabel('Username') | | 4 | CSS class (stable, non-generated) | .kc-modal-title | | 5 | XPath | //div[@class="header"] | DO Use data-ga or data-test attributes — they are immune to styling and structural changes. Use Playwright's semantic locators (getByRole, getByLabel, getByText, getByPlaceholder) for readability and resilience. Define locators as class properties in page objects to avoid string duplication. Use chaining to scope locators: page.locator('.modal').locator('[data-ga="confirm"]'). // Semantic locator await page.getByRole('button', { name: 'Submit' }).click(); // data-ga attribute await page.locator('[data-ga="group-name"]').fill('Automation Group'); DON'T Don't use auto-generated class names (div.sc-abc123) or positional XPaths (/div[3]/span[1]). Don't use page.$() (legacy Playwright API) — always use page.locator(). Assertions DO Always use Playwright's built-in expect — it has automatic retry, built-in timeouts, and clear error messages. Prefer web-first assertions that wait for the UI state to match: // Web-first assertions (auto-retry) await expect(page.locator('[data-ga="success-banner"]')).toBeVisible(); await expect(page.locator('[data-ga="user-count"]')).toHaveText('5'); await expect(page.locator('[data-ga="save-button"]')).toBeEnabled(); // Use soft assertions when you want to collect multiple failures in one test run: const softExpect = expect.configure({ soft: true }); await softExpect(heading).toHaveText('Dashboard'); await softExpect(logo).toBeVisible(); // All soft assertion failures are reported at the end DON'T Don't use page.isVisible() in if statements as a substitute for assertions. Don't hard-code waitForTimeout before an assertion — let expect do the waiting. Waiting Strategies DO Rely on Playwright's auto-waiting — most click, fill, and expect operations auto-wait for elements to be actionable. Use waitForSelector or waitForResponse only for specific async operations not covered by auto-waiting. Wait for network responses when actions trigger API calls: // Wait for API response after action const [response] = await Promise.all([ page.waitForResponse(resp => resp.url().includes('/api/groups') && resp.status() === 200), page.locator('[data-ga="save-button"]').click() ]); // Use page.waitForLoadState('networkidle') only for pages with complex background requests. DON'T Never use arbitrary page.waitForTimeout(3000) — this is a top cause of slow, flaky tests. Don't poll visibility in a loop; use expect(...).toBeVisible({ timeout: 10000 }) instead. Test Isolation & State Management DO Each Cucumber scenario must be fully independent — it should not rely on state left by a previous scenario. Use Before / After hooks in setup/hooks.js to: Create a fresh browser context per scenario. Navigate to a known starting page. Clean up created test data after each scenario. // setup/hooks.js Before(async function () { this.context = await browser.newContext(); this.page = await this.context.newPage(); }); After(async function (scenario) { if (scenario.result.status === 'FAILED') { await this.page.screenshot({ path: reports/screenshots/${scenario.pickle.name}.png }); } await this.context.close(); }); // Use tagged hooks to apply setup only to relevant scenarios: Before({ tags: '@admin' }, async function () { await loginAsAdmin(this.page); }); DON'T Don't share page instances or logged-in sessions across unrelated scenarios. Don't depend on execution order — scenarios should be runnable in any order. Authentication & Login DO Reuse authenticated state using Playwright's storageState to avoid repeating login for every scenario: // Save auth state once await page.context().storageState({ path: 'setup/auth-state.json' }); // Reuse in playwright.config.js use: { storageState: 'setup/auth-state.json' } // Store credentials only in environment variables — never in code or feature files. // Use a dedicated loginAsRole utility function to support multiple user roles cleanly. // utils/auth.js export async function loginAs(page, role) { const creds = { admin: { user: process.env.ADMIN_USER, pass: process.env.ADMIN_PASS }, performer: { user: process.env.PERFORMER_USER, pass: process.env.PERFORMER_PASS }, approver: { user: process.env.APPROVER_USER, pass: process.env.APPROVER_PASS }, ess: { user: process.env.ESS_USER, pass: process.env.ESS_PASS } }; await page.goto(process.env.KEYCONTROL_URL); await page.fill('[data-ga="username"]', creds[role].user); await page.fill('[data-ga="password"]', creds[role].pass); await page.click('[data-ga="login-button"]'); await expect(page.locator('[data-ga="dashboard"]')).toBeVisible(); } Microsoft SSO See MICROSOFT_SSO_TESTING.md for handling Azure AD / Microsoft login flows. Mock or bypass SSO in lower environments whenever possible to speed up test execution. Test Data Management DO Keep test data separate from test logic — store in test-data/json/ or test-data/excel/. Use unique data per run (e.g., timestamps, UUIDs) to prevent collisions when tests run in parallel. const groupName = AutoGroup_${Date.now()}; // Use factory functions to generate test data objects: // utils/dataFactory.js export function createGroupPayload(overrides = {}) { return { name: AutoGroup_${Date.now()}, description: 'Generated by automation', type: 'standard', ...overrides }; } // Clean up all data created during a test in the After hook. DON'T Don't hardcode test data (names, IDs, dates) inside step definitions. Don't leave orphaned test data in shared environments — it causes noise for manual testers. BDD / Cucumber Integration Feature File Best Practices Write scenarios from the user's perspective using Given / When / Then. One scenario = one behavior. Don't write "super scenarios" that test 10 things at once. Use Background for common pre-conditions, not complex setup logic. Use tags consistently to allow selective execution: @smoke @keycontrol @admin @group-management Feature: Admin Group Management Module Background: Given I am logged in as an admin @create-group Scenario: Admin creates a new group When I navigate to Group Management And I create a group with name "AutoGroup" Then the group "AutoGroup" should appear in the list Step Definition Best Practices Keep steps atomic and reusable across scenarios. Use World object (this) to share state between steps within a scenario — never use module-level globals. Avoid logic-heavy step definitions; delegate to page objects. Thin step, rich page object: When('I create a group with name {string}', async function (name) { await this.adminPage.createGroup(name); }); Use Cucumber Data Tables and Doc Strings for structured input data. Error Handling & Debugging DO Enable screenshots on failure in After hooks (see Section 6). Enable video recording for CI runs to replay failures: // playwright.config.js use: { video: 'retain-on-failure', screenshot: 'only-on-failure', trace: 'retain-on-failure' } Use the Playwright Trace Viewer (npx playwright show-trace trace.zip) to inspect failing steps. Use the built-in logger (utils/logger.js) instead of console.log for structured output. Add meaningful step names so traces and reports are human-readable. Debugging Locally Run in headed mode with Playwright Inspector PWDEBUG=1 npx playwright test Run a single scenario by tag npx test --tags="@create-group" Slow down execution for visual debugging npx playwright test --headed --slowmo=500 Retries & Flakiness Retry Policy Set retries: 1 (max 2) in playwright.config.js for CI — enough to handle transient network issues, not enough to hide real bugs. Never increase retries as a fix for a broken test — find and fix the root cause. // playwright.config.js retries: process.env.CI ? 1 : 0, Flakiness Thresholds | Level | Threshold | Action | |---|---|---| | Acceptable | < 1% over 7 days | Monitor | | Warning | 1% – 5% | Investigate & RCA | | Critical | > 5% | Block release, escalate | Common Flakiness Causes & Fixes | Cause | Fix | |---|---| | waitForTimeout | Replace with expect or waitForResponse | | Fragile selectors | Switch to data-ga / ARIA locators | | Shared state | Isolate each scenario (see Section 6) | | Race conditions | Use Promise.all + network wait | | Dynamic content | Use toBeVisible({ timeout }) instead of hard-wait | Parallelism & Performance DO Start with workers: 2 in CI and increase only after flakiness stabilizes below 1%. Use Playwright sharding to distribute tests across CI runners: npx playwright test --shard=1/3 npx playwright test --shard=2/3 npx playwright test --shard=3/3 Group slow scenarios (e.g., approval workflows) with @slow tags and run them separately. Use storageState to avoid redundant logins (see Section 7). DON'T Don't share a single browser context between parallel workers. Don't run all scenarios in parallel before verifying they are properly isolated. Configuration Management DO Use a single config.js as the source of truth for all configuration values. Override config values via environment variables — never change the config file between environments. Use .env files for local development; inject secrets via CI environment variables in pipelines. // config.js export const config = { baseUrl: process.env.KEYCONTROL_URL || 'https://qa-keycontrol.axaxl-cloud.com', browser: process.env.BROWSER || 'edge', headless: process.env.HEADLESS !== 'false', defaultTimeout: Number(process.env.DEFAULT_TIMEOUT) || 120000, actionTimeout: Number(process.env.ACTION_TIMEOUT) || 30000, }; See ENV_SETUP.md for full environment variable documentation. DON'T Don't commit .env files with real credentials to source control. Don't hardcode environment URLs in test files — always reference config.js. Reporting & Observability DO Use Allure as the primary report for rich test history, attachments, and trend analysis. Attach screenshots, videos, and traces to Allure on failure automatically via hooks. Add Allure labels to enrich reports with metadata: // in step definitions this.allure.attachment('Response Body', JSON.stringify(responseBody, null, 2), 'application/json'); Generate reports as part of every CI pipeline run: npx run generate:report # Generate Allure report npx run open:report # Open in browser locally Maintain a weekly flakiness dashboard from Allure history data and share with the team. Report Locations | Type | Location | |---|---| | Allure HTML | allure-report/ | | Allure Results | allure-results/ | | JSON Report | reports/cucumber_report.json | | Screenshots | reports/screenshots/ | | Videos | test-results/ | CI/CD Integration DO Run tests in headless mode in CI: npm run test:ci Fail the pipeline on any test failure — don't silently ignore failed tests. Archive Allure results and test-results as CI artifacts for post-run analysis. Run linting (eslint) and type checks before executing tests in the pipeline. Use branch-specific tags to run only smoke tests on PRs and full regression on main: Example GitHub Actions step name: Run Smoke Tests (PR) run: npm test -- --tags "@smoke" if: github.event_name == 'pull_request' name: Run Full Suite (Main) run: npm test if: github.ref == 'refs/heads/main' DON'T Don't run tests against production environments from CI without explicit approval gates. Don't skip report generation in CI — reports are essential for debugging failures. Security & Secrets DO Store all credentials in environment variables or a secrets manager (Azure Key Vault, GitHub Secrets). Reference secrets via process.env.VAR_NAME — never hardcode them. Use .gitignore to exclude .env, auth-state.json, and any file that may contain tokens. Rotate test user passwords regularly and update secrets in the CI store. See SECRETS.md for detailed secrets management guidelines. DON'T Don't log credentials or tokens to the console or report output. Don't commit auth-state.json (contains session tokens) to source control. Don't use personal accounts as test users — use dedicated service accounts. Code Quality & Maintainability DO Enable ESLint with a consistent style guide (e.g., Airbnb or StandardJS). Use ES Modules (import/export) consistently — the project already uses "type": "module" in package.json. Use async/await everywhere — avoid mixing Promises and callbacks. Keep functions small and single-purpose (\le 20 lines is a good target). Write JSDoc comments for all page object methods and utilities. Review test code in pull requests — test code is production code. /** Creates a new group with the given data. @param {Object} groupData @param {string} groupData.name @param {string} [groupData.description] */ async createGroup(groupData) { await this.groupNameInput.fill(groupData.name); await this.saveButton.click(); await expect(this.successBanner).toBeVisible(); } DON'T Don't leave commented-out code or console.log statements in committed code. Don't duplicate step definitions — extract shared steps into a common-steps.js. Don't use var — always use const or let. Accessibility & Cross-Browser Testing Accessibility Prefer ARIA roles and labels as primary selectors — this naturally validates accessibility. Run @axe-core/playwright checks on key pages as part of accessibility regression: import { checkA11y } from 'axe-playwright'; await checkA11y(page, undefined, { runOnly: ['wcag2a', 'wcag2aa'] }); Cross-Browser The framework targets Microsoft Edge (Chromium) as the primary browser. Add firefox and webkit project entries in playwright.config.js for critical smoke tests when cross-browser coverage is required: projects: [ { name: 'edge', use: { ...devices['Desktop Edge'], channel: 'msedge' } }, { name: 'firefox', use: { ...devices['Desktop Firefox'] } }, { name: 'webkit', use: { ...devices['Desktop Safari'] } } ] Run cross-browser tests on a schedule (e.g., nightly), not on every PR, to keep PR pipelines fast. Quick Reference Checklist Use this checklist before merging new tests: Tests are isolated — no shared state between scenarios data-ga or ARIA locators used — no fragile XPath/CSS No waitForTimeout calls Assertions use Playwright's expect (web-first) Credentials are in environment variables, not hardcoded Test data uses unique identifiers (timestamp/UUID) Screenshots/video capture is configured on failure Feature file has correct tags for selective execution Page objects contain no assertions Code reviewed and ESLint passes References Playwright Official Documentation Playwright Best Practices Cucumber.js Documentation Allure Playwright Integration ENV_SETUP.md SELECTORS.md FLAKINESS.md SECRETS.md MICROSOFT_SSO_TESTING.md CI_HARNESS.md
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to