πŸ’‘ If you like this website, please share it with your friends and network! πŸš€
Playwright commands
TypeScript & Node.js

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.

Level:
🎭

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.

Browser Control

Launch Chromium Browser

Core

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.

ID: #1
Browser Control

Create New Browser Context

Core

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!

ID: #2
Browser Control

Open New Page

Core

Opens a new page (tab) within the specified browser context.

const page = await context.newPage();
ID: #3
Browser Control

Navigate to URL

Core

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.

ID: #4
Browser Control

Navigate with Network Idle Wait

Advanced

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.

ID: #5
Browser Control

Get Page Title

Core

Retrieves the document title of the active page.

const title = await page.title();
ID: #6
Browser Control

Get Page Current URL

Core

Retrieves the absolute URL of the webpage currently active in the page.

const url = page.url();
ID: #7
Browser Control

Go Back in History

Core

Navigates back to the previous page in history.

await page.goBack();
ID: #8
Browser Control

Go Forward in History

Core

Navigates forward to the next page in history.

await page.goForward();
ID: #9
Browser Control

Refresh Page

Core

Reloads the current active document.

await page.reload();
ID: #10
Browser Control

Close Page (Tab)

Core

Closes the page session. Releases memory and resources associated with the tab.

await page.close();
ID: #11
Browser Control

Close Browser Session

Core

Closes the browser instance and terminates all active processes.

await browser.close();
ID: #12
Locators

Locate by Role

Core

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!

ID: #13
Locators

Locate by Text

Core

Locates an element by matching its visible inner text content.

const element = page.getByText('Welcome, User', { exact: true });
ID: #14
Locators

Locate by Test ID

Core

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.

ID: #15
Locators

Locate by Label Text

Core

Locates a form input element (like input, textarea) using its associated <label> text.

const input = page.getByLabel('Username');
ID: #16
Locators

Locate by Placeholder

Core

Locates a text field element by searching its placeholder value.

const input = page.getByPlaceholder('Enter your email');
ID: #17
Locators

Locate by Alt Text

Core

Locates an image element matching the spec attribute 'alt' text value.

const image = page.getByAltText('Company Logo');
ID: #18
Locators

Locate by Title

Core

Locates an element matching the HTML title attribute value.

const tooltip = page.getByTitle('Close window');
ID: #19
Locators

CSS Selector Locator

Core

Locates elements matching a standard CSS selector expression.

const element = page.locator('div.container > button.btn-submit');
ID: #20
Locators

XPath Selector Locator

Core

Locates elements using standard XML XPath expressions.

const element = page.locator('//button[@type="submit"]');
ID: #21
Locators

Chaining Locators

Core

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!

ID: #22
Locators

Locate by Child Sibling (has/hasText)

Advanced

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') });
ID: #23
Locators

Locate Shadow DOM Element

Advanced

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.

ID: #24
Locators

Filter Locators

Core

Filters an active locator collection to only match elements satisfying additional criteria.

const completedTasks = page.locator('.task').filter({ hasText: 'Done' });
ID: #25
Locators

Nth Element Locator

Core

Matches the element at the specified index within a matching collection (0-indexed).

const thirdItem = page.locator('.item').nth(2);
ID: #26
Element Interactions

Click Element

Core

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!

ID: #27
Element Interactions

Type Text (Fill)

Core

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');
ID: #28
Element Interactions

Type Character by Character (Press Sequentially)

Advanced

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.

ID: #29
Element Interactions

Check/Uncheck Checkbox or Radio

Core

Checks an option box. Auto-detects active checked status, avoiding double toggles.

await page.getByLabel('Accept terms').check();
ID: #30
Dropdown Controls

Select Option in Dropdown

Core

Selects options inside an HTML <select> dropdown by value, label, or index.

await page.locator('select#country').selectOption({ label: 'India' });
ID: #31
Dropdown Controls

Select Multiple Options

Core

Selects multiple option values inside a multi-select dropdown control.

await page.locator('select#colors').selectOption(['Red', 'Green', 'Blue']);
ID: #32
Actions API

Hover Mouse Over Element

Core

Hovers the virtual pointer coordinates over the center of the matching element.

await page.locator('.menu-item').hover();
ID: #33
Actions API

Double Click

Core

Performs a fast simulated double-click on the element.

await page.locator('.row-item').dblclick();
ID: #34
Actions API

Right Click (Context Click)

Core

Performs a right-click event to open contextual dropdown overlays.

await page.locator('.item').click({ button: 'right' });
ID: #35
Actions API

Click with Modifiers

Advanced

Clicks while holding down specific physical keyboard modifier keys (e.g. Ctrl + Shift + Click).

await page.locator('.link').click({ modifiers: ['Control', 'Shift'] });
ID: #36
Actions API

Keyboard Single Key Press

Core

Dispatches a keyboard stroke to the active viewport context (e.g., Backspace, Escape, ArrowDown).

await page.locator('body').press('Enter');
ID: #37
Actions API

Drag and Drop

Core

Drags a source element over the target position and drops it.

await page.locator('.source').dragTo(page.locator('.target'));
ID: #38
Element Interactions

Clear Input Fields

Core

Clears any text in a text entry box.

await page.getByPlaceholder('Name').clear();
ID: #39
Element Interactions

Focus on Element

Core

Sets direct keyboard focus onto the target element.

await page.locator('#input-name').focus();
ID: #40
Element Interactions

Check Element Visibilities

Core

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()!

ID: #41
Element Interactions

Get Inner Text

Core

Returns the computed visible text of the matching element.

const text = await page.locator('.header').innerText();
ID: #42
Element Interactions

Get Input Value

Core

Retrieves the active value text of form elements.

const val = await page.locator('input#username').inputValue();
ID: #43
Element Interactions

Get Attribute Value

Core

Retrieves the value of a specific HTML attribute on the element.

const type = await page.locator('input#password').getAttribute('type');
ID: #44
Waits & Synchronization

Wait for Selector Presence

Core

Waits until a specific element is present, visible, detached, or hidden.

await page.locator('.lazy-loaded-content').waitFor({ state: 'attached', timeout: 5000 });
ID: #45
Waits & Synchronization

Wait for Page Load State

Advanced

Forces execution to pause until a specific loading stage is completed (load, domcontentloaded, networkidle).

await page.waitForLoadState('networkidle');
ID: #46
Waits & Synchronization

Hardcoded Static Timeout Delay

Core

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!

ID: #47
Waits & Synchronization

Wait for Custom URL Navigation

Core

Pauses until the active page URL matches the specified glob or regex pattern.

await page.waitForURL('**/dashboard');
ID: #48
Waits & Synchronization

Wait for Custom JavaScript Function

Advanced

Suspends executions until a custom function evaluates to a truthy value inside the browser runtime.

await page.waitForFunction(() => window.innerWidth > 1024);
ID: #49
Alerts & Popups

Handle Browser Dialog (Alert/Confirm)

Advanced

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!

ID: #50
Alerts & Popups

Switch Context to Frame

Core

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');
ID: #51
Alerts & Popups

Switch Context to New Tab (Popup)

Advanced

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();
ID: #52
Test Frameworks

Assert Element is Visible

Core

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.

ID: #53
Test Frameworks

Assert Element Text Content

Core

Asserts that the element displays the exact expected text string value.

await expect(page.locator('.profile-name')).toHaveText('Jane Doe');
ID: #54
Test Frameworks

Assert Element Contains Text

Core

Asserts that the element contains the expected text string anywhere within its tree.

await expect(page.locator('.card-body')).toContainText('success');
ID: #55
Test Frameworks

Assert Input Field Value

Core

Asserts the active text value of an input element matches the target value.

await expect(page.locator('input#username')).toHaveValue('john_doe');
ID: #56
Test Frameworks

Assert Checkbox is Checked

Core

Asserts that a checkbox or radio button is actively toggled/checked.

await expect(page.locator('input#checkbox-agree')).toBeChecked();
ID: #57
Test Frameworks

Assert Element is Disabled

Core

Asserts that an interactive element is disabled.

await expect(page.locator('button#btn-submit')).toBeDisabled();
ID: #58
Test Frameworks

Assert URL Value

Core

Asserts that the active browser window URL matches the expected glob, string, or regular expression.

await expect(page).toHaveURL(/.*dashboard/);
ID: #59
Test Frameworks

Assert Page Title Value

Core

Asserts that the document title matches the expected string.

await expect(page).toHaveTitle('CareerRaah Dashboard');
ID: #60
Test Frameworks

Assert List Has Correct Size

Core

Asserts that the matching locator matches precisely the expected count of elements.

await expect(page.locator('.task-item')).toHaveCount(5);
ID: #61
Test Frameworks

Soft Assertions

Advanced

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!

ID: #62
JavaScript Executor

Execute JavaScript in Browser

Advanced

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
  };
});
ID: #63
JavaScript Executor

Execute JS on Specific Element

Advanced

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');
ID: #64
Cookies & Storage

Save Storage State (Login Session Cache)

Framework

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!

ID: #65
Cookies & Storage

Load Saved Storage State

Framework

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' });
ID: #66
Cookies & Storage

Get All Cookies

Core

Retrieves a list of all active session cookies.

const cookies = await context.cookies();
ID: #67
Cookies & Storage

Add/Set Specific Cookie

Advanced

Manually injects session cookies directly into the context.

await context.addCookies([{
  name: 'session_id',
  value: 'auth_tok_1234',
  domain: 'careerraah.com',
  path: '/'
}]);
ID: #68
Cookies & Storage

Clear Cookies and Storage

Core

Purges cookies and browser configurations to reset testing boundaries.

await context.clearCookies();
await context.clearPermissions();
ID: #69
Diagnostics

Capture Page Screenshot

Core

Takes a picture of the visible canvas viewport or the entire scrollable page canvas.

await page.screenshot({ path: 'reports/screenshot.png', fullPage: true });
ID: #70
Diagnostics

Capture Element Screenshot

Core

Captures a screenshot cropped strictly around the boundaries of the matching element.

await page.locator('.product-card').screenshot({ path: 'reports/product.png' });
ID: #71
Diagnostics

Record Browser Video

Framework

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 } }
});
ID: #72
Diagnostics

Start Tracing (Playwright Trace Viewer)

Framework

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!

ID: #73
JavaScript Executor

API Request (JSON GET)

Advanced

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();
ID: #74
JavaScript Executor

API Request (JSON POST)

Advanced

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();
ID: #75
Framework Architecture

Network API Mocking (Intercept Route)

Framework

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.

ID: #76
Framework Architecture

Intercept and Modify Request Headers

Framework

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 });
});
ID: #77
Framework Architecture

Define Custom Test Fixture

Framework

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!

ID: #78
Framework Architecture

Parallel Execution Configuration

Framework

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,
});
ID: #79
Framework Architecture

Page Object Model (POM) Structure

Framework

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);
  }
}
ID: #80
Browser Control

Initialize Playwright Project

Core

Scaffolds a complete, ready-to-run Playwright automated testing directory including default browsers, configuration files, and GitHub Actions CI pipelines.

npm init playwright@latest
πŸ’‘

Pro-Tip: This is the standard CLI entry point recommended to set up new TypeScript/JavaScript projects!

ID: #81
Browser Control

Download/Install Browsers

Core

Fetches and configures the standard target headless/headed browser binaries (Chromium, Firefox, and WebKit) managed locally by Playwright.

npx playwright install
ID: #82
Browser Control

Run Automated Test Suite

Core

Dispatches the standard Playwright test execution runner across all configured browser types in parallel.

npx playwright test
πŸ’‘

Pro-Tip: Use --ui to launch the beautiful graphical UI mode of Playwright for visual step-by-step execution analysis.

ID: #83
Framework Architecture

Basic Test Template (TypeScript)

Core

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/);
});
ID: #84
Framework Architecture

Basic Test Template (CommonJS/JavaScript)

Core

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/);
});
ID: #85
Element Interactions

Upload Single File

Core

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');
ID: #86
Element Interactions

Upload Multiple Files

Advanced

Injects multiple files simultaneously into a multi-select file input target.

await page.setInputFiles('input[type="file"]', ['file1.png', 'file2.png']);
ID: #87
Element Interactions

Download File & Save Locally

Advanced

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!

ID: #88
Cookies & Storage

Manipulate LocalStorage / SessionStorage

Advanced

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');
});
ID: #89
Browser Control

Emulate Viewport Resolution Size

Advanced

Adjusts the resolution scaling of the active browser viewport context on-the-fly.

await context.setViewportSize({ width: 1280, height: 720 });
ID: #90
Browser Control

Grant Browser Permissions

Advanced

Simulates manual permission popups by dynamically granting capabilities like geolocation, notifications, or microphone access to origins.

await context.grantPermissions(['geolocation'], { origin: 'https://careerraah.com' });
ID: #91
Framework Architecture

Group Tests (test.describe)

Core

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 }) => { ... });
});
ID: #92
Framework Architecture

Conditional Test Skip (test.skip)

Core

Instructs the test runner to skip execution under specified browser runtimes or OS flags.

test.skip(browserName === 'webkit', 'Skip on WebKit safari environment');
ID: #93
Framework Architecture

Mark Test as Flaky/Broken (test.fixme)

Core

Flags broken tests that require developers to debug. Playwright will completely skip executing fixme annotated specs.

test.fixme('Fix active locator synchronization issues later');
ID: #94
Framework Architecture

Mark Test as Slow (test.slow)

Advanced

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();
ID: #95
Browser Control

Interactive Keyboard Shortcuts (CLI)

Core

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 report
ID: #96
πŸ—οΈ

Playwright Framework Architect Q&A

Deep-dive senior level scenario answers, folder topologies, parallelization limits, and Selenium migration guides.

Q1

How do you design a scalable Playwright framework?

Direct Answer

A scalable Playwright framework follows a clear separation of concerns. Here's the recommended folder structure:

playwright.framework.ts
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.

Q2

How do you manage browser lifecycle?

Direct Answer

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.

playwright.framework.ts
// βœ… 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.

Q3

How do you handle flaky tests?

Direct Answer

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.framework.ts
// 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 test

Deep 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.

Q4

How do you generate reports with screenshots & videos?

Direct Answer

A good reporting strategy means a developer can diagnose a failure entirely from the CI artifact β€” no local rerun needed.

playwright.framework.ts
// 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.

Q5

How do you manage test tagging strategy?

Direct Answer

Tags let you run targeted subsets of your suite in CI pipelines β€” smoke tests on every PR, full regression nightly.

playwright.framework.ts
// 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).

Q6

How do you handle authentication efficiently?

Direct Answer

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.

playwright.framework.ts
// 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.

Q7

How do you manage environments?

Direct Answer

All environment-specific values (URLs, credentials, API keys) must live outside the codebase β€” in env files locally, in CI secrets in pipelines.

playwright.framework.ts
// .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 @smoke

Deep 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.

Q8

How do you integrate Playwright with CI/CD?

Direct Answer

A production-grade CI pipeline runs tests in parallel, stores artifacts on failure, and reports results inline in the PR.

playwright.framework.ts
# .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: 14

Deep 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.

Q9

How do you implement Page Object Model correctly?

Direct Answer

POM wraps page interactions into reusable classes. The golden rule: Page Objects contain ACTIONS only β€” never assertions. Tests contain assertions.

playwright.framework.ts
// 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 logic

Deep 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.

Q10

How would you improve an existing Selenium framework using Playwright?

Direct Answer

This is a migration strategy question. Show you understand the pain points of Selenium and how Playwright directly addresses each one.

playwright.framework.ts
// 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.

Want to practice in real life?

Open our custom built **QA Playground** to test these playwright locators against dynamic tables, mock login states, auto-waiting spinners, and interactive frames.