Playwright 80+ Commands
Core, Advanced & Enterprise level
An interactive, production-ready reference catalog. Speed up test authorship with modern ARIA locators, isolated browser contexts, storage state persistence, visual screenshot comparison, test fixtures, and mock network routing.
Playwright Premium Prep Track
Level up your automation career! Pair this command cheat sheet with our master-level **Scenario-Based Playwright Q&A** to tackle senior & architect level technical rounds easily.
Launch Chromium Browser
Launches an instance of the Chromium browser in non-headless mode. Useful for visual debugging during development.
const browser = await chromium.launch({ headless: false });Pro-Tip: Playwright runs in headless mode by default. Always configure headless: false during local debugging.
Create New Browser Context
Creates a new, completely isolated browser session (context) that shares no cookies, cache, or local storage with other contexts. Behaves like an incognito window.
const context = await browser.newContext();Pro-Tip: Using browser contexts is extremely fast and light. Run multiple parallel contexts instead of launching multiple browser instances!
Open New Page
Opens a new page (tab) within the specified browser context.
const page = await context.newPage();Navigate to URL
Navigates to the specified URL. By default, it waits until the page fires the 'load' event.
await page.goto('https://careerraah.com');Pro-Tip: You can change the wait strategy using the waitUntil option: load, domcontentloaded, networkidle.
Navigate with Network Idle Wait
Navigates to a URL and waits until there are no network requests for at least 500ms. Perfect for Single Page Apps (SPAs).
await page.goto('https://careerraah.com', { waitUntil: 'networkidle' });Pro-Tip: Use 'networkidle' carefully as slow-loading analytics, chat widgets, or tracking scripts can cause navigation timeouts.
Get Page Title
Retrieves the document title of the active page.
const title = await page.title();Get Page Current URL
Retrieves the absolute URL of the webpage currently active in the page.
const url = page.url();Go Back in History
Navigates back to the previous page in history.
await page.goBack();Go Forward in History
Navigates forward to the next page in history.
await page.goForward();Refresh Page
Reloads the current active document.
await page.reload();Close Page (Tab)
Closes the page session. Releases memory and resources associated with the tab.
await page.close();Close Browser Session
Closes the browser instance and terminates all active processes.
await browser.close();Locate by Role
Locates an element based on its ARIA role (e.g., button, link, checkbox) and matching accessibility label/text. High-priority best practice.
const button = page.getByRole('button', { name: 'Submit' });Pro-Tip: Locating by ARIA role makes your tests resilient to layout modifications and guarantees correct accessibility structure!
Locate by Text
Locates an element by matching its visible inner text content.
const element = page.getByText('Welcome, User', { exact: true });Locate by Test ID
Locates an element matching the data-testid attribute (configured as 'data-testid' by default).
const element = page.getByTestId('submit-btn');Pro-Tip: Use data-testid attributes to build bulletproof locators that never break when developers change CSS classes or HTML structure.
Locate by Label Text
Locates a form input element (like input, textarea) using its associated <label> text.
const input = page.getByLabel('Username');Locate by Placeholder
Locates a text field element by searching its placeholder value.
const input = page.getByPlaceholder('Enter your email');Locate by Alt Text
Locates an image element matching the spec attribute 'alt' text value.
const image = page.getByAltText('Company Logo');Locate by Title
Locates an element matching the HTML title attribute value.
const tooltip = page.getByTitle('Close window');CSS Selector Locator
Locates elements matching a standard CSS selector expression.
const element = page.locator('div.container > button.btn-submit');XPath Selector Locator
Locates elements using standard XML XPath expressions.
const element = page.locator('//button[@type="submit"]');Chaining Locators
Nests locators to narrow down the query scope. Filters search relative to matching parent nodes.
const item = page.locator('.list-group').locator('.list-item').first();Pro-Tip: Chaining locators maintains lazy evaluation. Playwright only resolves the actual element at the moment of interaction!
Locate by Child Sibling (has/hasText)
Locates an element that contains specific text or matching sub-elements inside it.
const row = page.locator('.row', { hasText: 'Active', has: page.locator('.btn-delete') });Locate Shadow DOM Element
Locates elements nested inside open shadow DOM roots. Playwright pierces shadow boundaries natively without extra scripts!
const input = page.locator('custom-element-host >> #shadow-input');Pro-Tip: CSS selectors in Playwright automatically pierce shadow DOM boundaries by default. Just use normal locator tags.
Filter Locators
Filters an active locator collection to only match elements satisfying additional criteria.
const completedTasks = page.locator('.task').filter({ hasText: 'Done' });Nth Element Locator
Matches the element at the specified index within a matching collection (0-indexed).
const thirdItem = page.locator('.item').nth(2);Click Element
Clicks the target element. Automatically scrolls the element into view, waits for it to be visible, enabled, and stable (receives pointer events) before clicking.
await page.getByRole('button', { name: 'Submit' }).click();Pro-Tip: Playwright auto-waits for clickability metrics. No manual checks needed!
Type Text (Fill)
Clears any pre-existing value and inserts the complete text string at once. Fastest way to input strings.
await page.getByPlaceholder('Username').fill('john_doe');Type Character by Character (Press Sequentially)
Simulates actual keyboard keystrokes, firing keydown, keypress, and keyup events with custom delay thresholds.
await page.locator('#search').pressSequentially('Selenium to Playwright', { delay: 100 });Pro-Tip: Use fill() for normal form entries. Only use pressSequentially() for autocomplete dropdowns or searching widgets.
Check/Uncheck Checkbox or Radio
Checks an option box. Auto-detects active checked status, avoiding double toggles.
await page.getByLabel('Accept terms').check();Select Option in Dropdown
Selects options inside an HTML <select> dropdown by value, label, or index.
await page.locator('select#country').selectOption({ label: 'India' });Select Multiple Options
Selects multiple option values inside a multi-select dropdown control.
await page.locator('select#colors').selectOption(['Red', 'Green', 'Blue']);Hover Mouse Over Element
Hovers the virtual pointer coordinates over the center of the matching element.
await page.locator('.menu-item').hover();Double Click
Performs a fast simulated double-click on the element.
await page.locator('.row-item').dblclick();Right Click (Context Click)
Performs a right-click event to open contextual dropdown overlays.
await page.locator('.item').click({ button: 'right' });Click with Modifiers
Clicks while holding down specific physical keyboard modifier keys (e.g. Ctrl + Shift + Click).
await page.locator('.link').click({ modifiers: ['Control', 'Shift'] });Keyboard Single Key Press
Dispatches a keyboard stroke to the active viewport context (e.g., Backspace, Escape, ArrowDown).
await page.locator('body').press('Enter');Drag and Drop
Drags a source element over the target position and drops it.
await page.locator('.source').dragTo(page.locator('.target'));Clear Input Fields
Clears any text in a text entry box.
await page.getByPlaceholder('Name').clear();Focus on Element
Sets direct keyboard focus onto the target element.
await page.locator('#input-name').focus();Check Element Visibilities
Returns a boolean value indicating if the element is visible in the active DOM viewport.
const isVisible = await page.locator('.alert-box').isVisible();Pro-Tip: Use this only for conditional test branch logic. For test assertions, always prefer expect(locator).toBeVisible()!
Get Inner Text
Returns the computed visible text of the matching element.
const text = await page.locator('.header').innerText();Get Input Value
Retrieves the active value text of form elements.
const val = await page.locator('input#username').inputValue();Get Attribute Value
Retrieves the value of a specific HTML attribute on the element.
const type = await page.locator('input#password').getAttribute('type');Wait for Selector Presence
Waits until a specific element is present, visible, detached, or hidden.
await page.locator('.lazy-loaded-content').waitFor({ state: 'attached', timeout: 5000 });Wait for Page Load State
Forces execution to pause until a specific loading stage is completed (load, domcontentloaded, networkidle).
await page.waitForLoadState('networkidle');Hardcoded Static Timeout Delay
Pauses browser execution flow for a fixed duration. Strongly discouraged in production test scripts.
await page.waitForTimeout(2000);Pro-Tip: β Static timeouts make tests slow and flaky. Rely on Playwright's auto-waiting or expect assertions instead!
Wait for Custom URL Navigation
Pauses until the active page URL matches the specified glob or regex pattern.
await page.waitForURL('**/dashboard');Wait for Custom JavaScript Function
Suspends executions until a custom function evaluates to a truthy value inside the browser runtime.
await page.waitForFunction(() => window.innerWidth > 1024);Handle Browser Dialog (Alert/Confirm)
Registers a listener callback to automatically accept, dismiss, or input details into browser alert, confirm, or prompt popups.
page.on('dialog', async dialog => {
console.log(dialog.message());
await dialog.accept();
});Pro-Tip: You must register the page.on('dialog') listener BEFORE the interaction that triggers the dialog!
Switch Context to Frame
Locates a frame (iframe) and enables searching and executing interactions inside its nested DOM boundaries.
const frame = page.frameLocator('iframe#payment-gateway');
await frame.locator('#cardNumber').fill('41112222');Switch Context to New Tab (Popup)
Intercepts page popups and links opening in new browser tabs, returning a target handle to control the new tab context.
const [newTab] = await Promise.all([
context.waitForEvent('page'),
page.locator('#open-link').click()
]);
await newTab.waitForLoadState();Assert Element is Visible
Verifies the element is rendered and visible in the viewport. Includes automatic polling retries.
await expect(page.locator('.alert-success')).toBeVisible({ timeout: 5000 });Pro-Tip: Playwright assertions feature web-first auto-retries! They will poll the DOM repeatedly until the condition passes or timeout is reached.
Assert Element Text Content
Asserts that the element displays the exact expected text string value.
await expect(page.locator('.profile-name')).toHaveText('Jane Doe');Assert Element Contains Text
Asserts that the element contains the expected text string anywhere within its tree.
await expect(page.locator('.card-body')).toContainText('success');Assert Input Field Value
Asserts the active text value of an input element matches the target value.
await expect(page.locator('input#username')).toHaveValue('john_doe');Assert Checkbox is Checked
Asserts that a checkbox or radio button is actively toggled/checked.
await expect(page.locator('input#checkbox-agree')).toBeChecked();Assert Element is Disabled
Asserts that an interactive element is disabled.
await expect(page.locator('button#btn-submit')).toBeDisabled();Assert URL Value
Asserts that the active browser window URL matches the expected glob, string, or regular expression.
await expect(page).toHaveURL(/.*dashboard/);Assert Page Title Value
Asserts that the document title matches the expected string.
await expect(page).toHaveTitle('CareerRaah Dashboard');Assert List Has Correct Size
Asserts that the matching locator matches precisely the expected count of elements.
await expect(page.locator('.task-item')).toHaveCount(5);Soft Assertions
Executes soft assertions. Errors do not terminate the entire test run immediately, but compile a failure log at completion.
await expect.soft(page.locator('.warning')).toBeHidden();Pro-Tip: Soft assertions are excellent for checking non-blocking elements like minor page footnotes, styles, or sidebar states!
Execute JavaScript in Browser
Runs native Javascript expressions inside the page window context, returning structured values back to the Node process.
const dimension = await page.evaluate(() => {
return {
width: window.innerWidth,
height: window.innerHeight
};
});Execute JS on Specific Element
Passes a target element handle directly into a browser-side Javascript function to manipulate inline styles or classes.
const element = page.locator('#title');
await element.evaluate((node) => node.style.border = '2px solid red');Save Storage State (Login Session Cache)
Serializes cookies and local storage tokens, writing them to a JSON state file. Excellent for reusing authenticated states.
await context.storageState({ path: 'auth/state.json' });Pro-Tip: Use storage state to bypass authentication screens in subsequent test runs, saving minutes of test execution overhead!
Load Saved Storage State
Restores local storage, session state, and cookies from a stored JSON state file to launch fully logged-in browser contexts.
const context = await browser.newContext({ storageState: 'auth/state.json' });Get All Cookies
Retrieves a list of all active session cookies.
const cookies = await context.cookies();Add/Set Specific Cookie
Manually injects session cookies directly into the context.
await context.addCookies([{
name: 'session_id',
value: 'auth_tok_1234',
domain: 'careerraah.com',
path: '/'
}]);Clear Cookies and Storage
Purges cookies and browser configurations to reset testing boundaries.
await context.clearCookies();
await context.clearPermissions();Capture Page Screenshot
Takes a picture of the visible canvas viewport or the entire scrollable page canvas.
await page.screenshot({ path: 'reports/screenshot.png', fullPage: true });Capture Element Screenshot
Captures a screenshot cropped strictly around the boundaries of the matching element.
await page.locator('.product-card').screenshot({ path: 'reports/product.png' });Record Browser Video
Instructs the browser context to record video clips of all pages and actions executed inside the session.
const context = await browser.newContext({
recordVideo: { dir: 'reports/videos/', size: { width: 1280, height: 720 } }
});Start Tracing (Playwright Trace Viewer)
Turns on recording to generate a zip file packed with DOM snapshots, console outputs, network payloads, and timings that can be loaded in the Playwright Trace Viewer.
await context.startTracing({ screenshots: true, snapshots: true, sources: true });
// Execute actions...
await context.stopTracing({ path: 'reports/trace.zip' });Pro-Tip: Use Playwright Tracing instead of raw logging. It lets you inspect full browser steps, timeline actions, and network flows retroactively!
API Request (JSON GET)
Executes an HTTP GET API request inside the active context browser session, automatically sharing browser cookies and tokens.
const response = await page.request.get('https://api.careerraah.com/v1/jobs');
expect(response.ok()).toBeTruthy();
const json = await response.json();API Request (JSON POST)
Dispatches an HTTP POST request carrying a JSON payload.
const response = await page.request.post('https://api.careerraah.com/v1/jobs', {
data: { title: 'QA Architect', salary: '200k' },
headers: { 'Content-Type': 'application/json' }
});
const result = await response.json();Network API Mocking (Intercept Route)
Intercepts network requests matching a URL pattern and fulfills them with custom responses (mock status, headers, JSON body).
await page.route('**/api/users', async route => {
const json = [{ id: 1, name: 'Mock QA User' }];
await route.fulfill({ status: 200, json });
});Pro-Tip: Mock heavy third-party APIs or sluggish DB integrations during testing to create highly stable, fast-executing front-end verification pipelines.
Intercept and Modify Request Headers
Intercepts outgoing requests to append or edit client payloads, headers, or tokens before forwarding them to servers.
await page.route('**/*', async route => {
const headers = { ...route.request().headers(), 'X-Test-Mode': 'true' };
await route.continue({ headers });
});Define Custom Test Fixture
Creates customizable test boundaries (fixtures) containing teardown steps, shared helpers, or modular dependencies.
import { test as baseTest } from '@playwright/test';
export const test = baseTest.extend({
todoPage: async ({ page }, use) => {
const todo = new TodoPage(page);
await todo.goto();
await use(todo);
await todo.cleanUp();
}
});Pro-Tip: Fixtures replace fragile beforeEach and afterEach setups, wrapping dependencies in clean, modular, parallel-safe code execution blocks!
Parallel Execution Configuration
Configures the test runner to execute individual spec items concurrently using isolated browser workers.
import { defineConfig } from '@playwright/test';
export default defineConfig({
fullyParallel: true,
workers: process.env.CI ? 4 : undefined,
});Page Object Model (POM) Structure
Encapsulates UI interaction scripts inside modular Page Object classes to support solid DRY design architecture.
export class LoginPage {
readonly page: Page;
readonly usernameInput: Locator;
constructor(page: Page) {
this.page = page;
this.usernameInput = page.locator('#username');
}
async login(user: string) {
await this.usernameInput.fill(user);
}
}Initialize Playwright Project
Scaffolds a complete, ready-to-run Playwright automated testing directory including default browsers, configuration files, and GitHub Actions CI pipelines.
npm init playwright@latestPro-Tip: This is the standard CLI entry point recommended to set up new TypeScript/JavaScript projects!
Download/Install Browsers
Fetches and configures the standard target headless/headed browser binaries (Chromium, Firefox, and WebKit) managed locally by Playwright.
npx playwright installRun Automated Test Suite
Dispatches the standard Playwright test execution runner across all configured browser types in parallel.
npx playwright testPro-Tip: Use --ui to launch the beautiful graphical UI mode of Playwright for visual step-by-step execution analysis.
Basic Test Template (TypeScript)
Standard TypeScript testing boilerplate for a basic end-to-end scenario featuring default fixtures and auto-retrying assertions.
import { test, expect } from '@playwright/test';
test('basic home search test', async ({ page }) => {
await page.goto('https://example.com');
await expect(page).toHaveTitle(/Example/);
});Basic Test Template (CommonJS/JavaScript)
Standard ES5 CommonJS JavaScript boilerplate for environments without active TypeScript compilations.
const { test, expect } = require('@playwright/test');
test('basic home search test', async ({ page }) => {
await page.goto('https://example.com');
await expect(page).toHaveTitle(/Example/);
});Upload Single File
Selects and injects a single file upload target into an input of type 'file'. Autodetects file path boundaries.
await page.setInputFiles('input[type="file"]', 'reports/logo.png');Upload Multiple Files
Injects multiple files simultaneously into a multi-select file input target.
await page.setInputFiles('input[type="file"]', ['file1.png', 'file2.png']);Download File & Save Locally
Listens for standard download dispatcher events, waits for the backend stream download to conclude, and writes it to a persistent local path.
const [download] = await Promise.all([
page.waitForEvent('download'),
page.click('#download-btn')
]);
const path = await download.path();
await download.saveAs('downloads/invoice.pdf');Pro-Tip: Use Promise.all to cleanly avoid race conditions between page clicks and async backend stream downloads!
Manipulate LocalStorage / SessionStorage
Executes client-side actions inside the browser context window to programmatically configure active local storage keys.
await page.evaluate(() => {
localStorage.setItem('theme', 'dark');
sessionStorage.setItem('token', 'session_auth_123');
});Emulate Viewport Resolution Size
Adjusts the resolution scaling of the active browser viewport context on-the-fly.
await context.setViewportSize({ width: 1280, height: 720 });Grant Browser Permissions
Simulates manual permission popups by dynamically granting capabilities like geolocation, notifications, or microphone access to origins.
await context.grantPermissions(['geolocation'], { origin: 'https://careerraah.com' });Group Tests (test.describe)
Combines logically related automated scenarios under modular, named hierarchy groups with shared hook cycles.
test.describe('Login scenarios', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login');
});
test('success login', async ({ page }) => { ... });
});Conditional Test Skip (test.skip)
Instructs the test runner to skip execution under specified browser runtimes or OS flags.
test.skip(browserName === 'webkit', 'Skip on WebKit safari environment');Mark Test as Flaky/Broken (test.fixme)
Flags broken tests that require developers to debug. Playwright will completely skip executing fixme annotated specs.
test.fixme('Fix active locator synchronization issues later');Mark Test as Slow (test.slow)
Triples the default timeout thresholds (e.g., from 30s to 90s) to safely accommodate sluggish legacy pages or massive end-to-end integration flows.
test.slow();Interactive Keyboard Shortcuts (CLI)
High-performance CLI shortcuts available inside the interactive watch/UI console runtime to instantly speed up local validation.
P -> Run Tests
U -> Update Screenshots/Snapshots
T -> Toggle UI Mode
O -> Open last HTML execution reportPlaywright Framework Architect Q&A
Deep-dive senior level scenario answers, folder topologies, parallelization limits, and Selenium migration guides.
How do you design a scalable Playwright framework?
A scalable Playwright framework follows a clear separation of concerns. Here's the recommended folder structure:
my-playwright-framework/
βββ tests/
β βββ ui/ # UI end-to-end tests
β βββ api/ # API-only tests
β βββ integration/ # UI + API hybrid tests
βββ pages/ # Page Object Models
βββ fixtures/ # Custom Playwright fixtures
βββ helpers/ # Reusable utilities (dates, strings)
βββ data/ # Test data (JSON, factory functions)
βββ config/ # Environment configs
β βββ dev.env
β βββ staging.env
β βββ prod.env
βββ playwright.config.ts
βββ package.json
// playwright.config.ts β environment-aware config
import { defineConfig } from '@playwright/test';
const ENV = process.env.TEST_ENV || 'dev';
const config = require(`./config/${ENV}.env.json`);
export default defineConfig({
fullyParallel: true,
workers: process.env.CI ? 4 : 2,
use: {
baseURL: config.BASE_URL,
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
});Deep Dive Explanation
Never hardcode URLs or credentials. Use environment-specific config files loaded at runtime. Keep Page Objects as action-only wrappers β no assertions inside them.
How do you manage browser lifecycle?
Playwright creates one Browser per worker (expensive), one BrowserContext per test (isolated), and one or more Pages per context (tabs). Never share context between tests β it causes state leakage.
// β
Correct: Playwright does this automatically per test
test('isolated test', async ({ page, context }) => {
// context = fresh BrowserContext (no cookies from other tests)
// page = fresh Page within that context
await page.goto('/dashboard');
});
// β
Worker-scoped browser (shared for performance)
// Context-scoped setup (isolated per test)
const test = base.extend<{}, { sharedBrowser: Browser }>({
sharedBrowser: [async ({ playwright }, use) => {
const browser = await playwright.chromium.launch();
await use(browser);
await browser.close();
}, { scope: 'worker' }],
});
// β WRONG β sharing context across tests causes flakiness
let sharedContext: BrowserContext;
test.beforeAll(async ({ browser }) => {
sharedContext = await browser.newContext(); // leaks cookies/state!
});Deep Dive Explanation
Rule: 1 Browser per worker (shared). 1 BrowserContext per test (isolated). 1+ Pages per context. This gives you maximum speed with zero state leakage.
How do you handle flaky tests?
Flaky tests are caused by timing issues, shared state, or network variability. Playwright's auto-wait eliminates most timing issues, but you must still understand async patterns.
// playwright.config.ts β retries for CI stability
export default defineConfig({
retries: process.env.CI ? 2 : 0, // retry twice in CI only
});
// β
Wait for a specific condition β not for time
await page.locator('.spinner').waitFor({ state: 'hidden' }); // wait for loader to disappear
await expect(page.locator('.results')).toHaveCount(10); // wait for data to load
// β
Promise.all β prevents race conditions with tabs/dialogs
const [popup] = await Promise.all([
page.waitForEvent('popup'),
page.getByRole('button', { name: 'Open' }).click(), // triggers popup
]);
// β
Wait for network response before asserting
await Promise.all([
page.waitForResponse(r => r.url().includes('/api/users') && r.status() === 200),
page.getByRole('button', { name: 'Load Users' }).click(),
]);
// β NEVER do this
await page.waitForTimeout(3000); // hard sleep = flaky testDeep Dive Explanation
Root-cause analysis checklist: 1) Is the element actually present? 2) Is there a race condition? 3) Is the test sharing state? 4) Is the backend slow? Fix the root cause, don't just add retries.
How do you generate reports with screenshots & videos?
A good reporting strategy means a developer can diagnose a failure entirely from the CI artifact β no local rerun needed.
// playwright.config.ts β full reporting setup
export default defineConfig({
reporter: [
['html', { open: 'never' }], // HTML report for humans
['junit', { outputFile: 'results.xml' }], // For Jenkins/CI
['github'], // GitHub Actions annotations
],
use: {
screenshot: 'only-on-failure', // Screenshot on fail
video: 'retain-on-failure', // Video on fail
trace: 'on-first-retry', // Trace on retry
},
});
// GitHub Actions β upload reports as artifacts
// .github/workflows/playwright.yml
// - uses: actions/upload-artifact@v3
// if: always()
// with:
// name: playwright-report
// path: playwright-report/
// retention-days: 30
// Custom attachment in test (e.g., API response body)
test('attach API response', async ({ page }, testInfo) => {
const response = await page.request.get('/api/users');
await testInfo.attach('api-response', {
body: await response.text(),
contentType: 'application/json',
});
});Deep Dive Explanation
The Trace Viewer (`npx playwright show-trace trace.zip`) shows DOM snapshots, network requests, console logs, and action timings β everything you need to debug without rerunning.
How do you manage test tagging strategy?
Tags let you run targeted subsets of your suite in CI pipelines β smoke tests on every PR, full regression nightly.
// Tag tests using test.describe or --grep CLI flag
// Option 1: In test title (simple)
test('@smoke @critical login works', async ({ page }) => { /*...*/ });
test('@regression checkout flow', async ({ page }) => { /*...*/ });
// Option 2: Using tags array (Playwright v1.42+, recommended)
test('login works', {
tag: ['@smoke', '@critical'],
}, async ({ page }) => { /*...*/ });
// Option 3: In describe block
test.describe('@api', () => {
test('create user', async ({ request }) => { /*...*/ });
test('delete user', async ({ request }) => { /*...*/ });
});
// Run selective tags via CLI:
// npx playwright test --grep @smoke β smoke only
// npx playwright test --grep @critical β critical only
// npx playwright test --grep-invert @slow β skip slow tests
// GitHub Actions: run smoke on PR, regression on schedule
// on: pull_request β npx playwright test --grep @smoke
// on: schedule β npx playwright test (all tests)Deep Dive Explanation
Recommended tag taxonomy: @smoke (5β10 min, every PR), @regression (full suite, nightly), @api (API-only, fastest), @critical (must-never-fail), @slow (deprioritised in pipeline).
How do you handle authentication efficiently?
Log in once via API, save the session as storageState, and reuse it across all tests. Each test gets a pre-authenticated browser context in milliseconds.
// global-setup.ts β login ONCE before the entire suite
import { chromium } from '@playwright/test';
export default async function globalSetup() {
const browser = await chromium.launch();
const page = await browser.newPage();
// Login via API (fastest β no UI interaction)
const response = await page.request.post('/api/auth/login', {
data: { email: 'admin@test.com', password: process.env.ADMIN_PASS }
});
// Save cookies + localStorage to file
await page.context().storageState({ path: '.auth/admin.json' });
await browser.close();
}
// playwright.config.ts
export default defineConfig({
globalSetup: './global-setup.ts',
projects: [
{
name: 'admin-tests',
use: { storageState: '.auth/admin.json' }, // pre-authenticated!
},
{
name: 'user-tests',
use: { storageState: '.auth/user.json' },
},
],
});
// Tests start pre-logged-in β no login UI interaction needed
test('admin dashboard', async ({ page }) => {
await page.goto('/admin'); // Already authenticated!
await expect(page.getByText('Admin Panel')).toBeVisible();
});Deep Dive Explanation
For OTP/MFA: intercept the OTP via API or email client in global setup. For multiple roles: run globalSetup for each role and save separate storageState files.
How do you manage environments?
All environment-specific values (URLs, credentials, API keys) must live outside the codebase β in env files locally, in CI secrets in pipelines.
// .env.dev (gitignored)
BASE_URL=http://localhost:3000
API_URL=http://localhost:4000
ADMIN_EMAIL=admin@dev.com
// .env.staging
BASE_URL=https://staging.myapp.com
API_URL=https://api.staging.myapp.com
// playwright.config.ts β reads from env
import { defineConfig } from '@playwright/test';
import * as dotenv from 'dotenv';
// Load environment file based on TEST_ENV variable
dotenv.config({ path: `.env.${process.env.TEST_ENV || 'dev'}` });
export default defineConfig({
use: {
baseURL: process.env.BASE_URL!, // never hardcoded
},
});
// GitHub Actions secrets (never in code)
// Settings β Secrets β STAGING_ADMIN_PASS
// env:
// ADMIN_PASS: ${{ secrets.STAGING_ADMIN_PASS }}
// TEST_ENV: staging
// Run against different envs:
// TEST_ENV=dev npx playwright test
// TEST_ENV=staging npx playwright test
// TEST_ENV=prod npx playwright test --grep @smokeDeep Dive Explanation
The .env files must be in .gitignore. Secrets must NEVER be committed. Use CI/CD secret management (GitHub Secrets, AWS SSM, Azure Key Vault) for all credentials.
How do you integrate Playwright with CI/CD?
A production-grade CI pipeline runs tests in parallel, stores artifacts on failure, and reports results inline in the PR.
# .github/workflows/playwright.yml
name: Playwright Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
timeout-minutes: 30
runs-on: ubuntu-latest
strategy:
matrix:
shard: [1, 2, 3, 4] # 4 parallel machines
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Run tests (sharded)
run: npx playwright test --shard=${{ matrix.shard }}/4
env:
TEST_ENV: staging
ADMIN_PASS: ${{ secrets.STAGING_ADMIN_PASS }}
- name: Upload report
uses: actions/upload-artifact@v4
if: always() # upload even on failure
with:
name: report-shard-${{ matrix.shard }}
path: playwright-report/
retention-days: 14Deep Dive Explanation
Sharding splits your test suite across multiple machines (--shard=1/4 runs 25% of tests). 100 tests Γ 4 shards = same wall-clock time as 25 tests. This is how teams keep CI under 10 minutes.
How do you implement Page Object Model correctly?
POM wraps page interactions into reusable classes. The golden rule: Page Objects contain ACTIONS only β never assertions. Tests contain assertions.
// pages/LoginPage.ts β actions only, no expect()
export class LoginPage {
constructor(private page: Page) {}
async goto() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.page.getByLabel('Email').fill(email);
await this.page.getByLabel('Password').fill(password);
await this.page.getByRole('button', { name: 'Sign In' }).click();
}
// β WRONG β assertion inside POM
// async verifyLoggedIn() {
// await expect(this.page).toHaveURL('/dashboard');
// }
}
// tests/login.spec.ts β assertions ONLY in tests
test('valid login', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('admin@test.com', 'secret');
// β
Assertion is in the test, not the POM
await expect(page).toHaveURL('/dashboard');
await expect(page.getByText('Welcome, Admin')).toBeVisible();
});
// When NOT to use POM:
// - One-off tests / exploratory tests
// - Very simple pages (login-only tests can use inline helpers)
// - When fixtures already provide the setup logicDeep Dive Explanation
Bloated POM anti-pattern: one massive 500-line class for the whole app. Solution: split by domain (CheckoutPage, CartPage, ProductPage). Each class should be under 100 lines. If it's bigger, split it.
How would you improve an existing Selenium framework using Playwright?
This is a migration strategy question. Show you understand the pain points of Selenium and how Playwright directly addresses each one.
// BEFORE: Selenium β manual waits, slow, flaky
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(30));
WebElement btn = wait.until(ExpectedConditions.elementToBeClickable(By.id("submit")));
btn.click();
Thread.sleep(2000); // π±
// AFTER: Playwright β auto-wait, no Thread.sleep
await page.getByRole('button', { name: 'Submit' }).click(); // auto-waits
// βββ Network Mocking (impossible in Selenium) ββββββββββββββ
await page.route('/api/products', route => route.fulfill({
json: [{ id: 1, name: 'Widget', price: 9.99 }]
}));
// βββ API Testing (built-in, no RestAssured needed) βββββββββ
const res = await request.post('/api/users', {
data: { name: 'Test User' }
});
expect(res.status()).toBe(201);
// βββ Parallel execution (built-in, no Selenium Grid) βββββββ
// playwright.config.ts
export default defineConfig({
workers: 8, // 8 parallel browsers, zero infra setup
fullyParallel: true,
});
// βββ Migration strategy βββββββββββββββββββββββββββββββββββββ
// Phase 1: New tests β Playwright only
// Phase 2: High-value old tests β migrate to Playwright
// Phase 3: Retire Selenium Grid (save infra cost)Deep Dive Explanation
Migration ROI points to mention: No Selenium Grid = no infra cost. No explicit waits = less maintenance. Built-in API testing = fewer dependencies. Parallel by default = faster feedback. Trace Viewer = faster debugging.