Skip to main content

Command Palette

Search for a command to run...

Playwright Typescript Topics

Published
15 min readView as Markdown

npx playwright test

you're invoking Playwright’s built-in test runner (called Playwright Test) using npx. Here's a breakdown: ⚙️ What Each Part Does

npx

A Node.js tool that runs locally installed CLI tools without installing them globally.

It ensures you're using the version of playwright that’s installed in your project.

playwright

The package which includes both the browser automation API and the @playwright/test runner YouTube

test

A sub-command that triggers the @playwright/test runner.

Executes all tests in your project as configured in playwright.config.js/ts Wikipedia+4Playwright+4Cuketest+4 .

🏃 What Happens When You Run It

Auto-compilation & discovery The runner compiles TypeScript (if used), finds .spec.* files in your testDir, and builds a list of tests.

Parallel execution Tests are executed in parallel across multiple browser contexts (Chromium, Firefox, WebKit) unless configured otherwise

Headless by default It runs tests without opening the browser GUI. Use flags (--headed, --debug, or --ui) to override Test Management

Artifacts & reports Automatically captures screenshots, videos, traces, and generates readable output in terminal and optional HTML report.

🔧 Common Flags You Can Add Flag Effect --headed Run with visible browsers, not headless --project=chromium Choose a specific browser project -g "name" Filter tests by name --workers=1 Disable parallel execution --debug Launch Playwright Inspector (slows execution for debugging) --ui Opens an interactive test runner interface --last-failed Rerun only the tests that failed in the last run --only-changed Run tests that were changed since last commit.

# Run all tests headlessly
npx playwright test

# Run a single spec file in Chrome with browser window visible
npx playwright test tests/login.spec.ts --project=chromium --headed

# Debug one test with Inspector:
npx playwright test tests/login.spec.ts --debug

# Run UI test runner locally:
npx playwright test --ui

Locators

🧭 User‐facing (Preferred) Locators

These are recommended for stable and maintainable tests, as they reflect how users and assistive technologies interact with your UI Stack Overflow+15Playwright+15Medium+15:

  1. Rolepage.getByRole(role, { name })
    Targets elements by ARIA role (e.g. "button", "checkbox") and accessible name. Ideal for accessibility-aligned selection.

     await page.getByRole('button', { name: 'Submit' }).click();
    

    Textpage.getByText("...")
    Finds elements containing visible text. Supports substring, exact match, or regex.

await page.getByText(/welcome/i).click();

Labelpage.getByLabel("...")
Locates form controls by their corresponding <label> text.

await page.getByLabel('Email').fill('user@example.com');

Placeholderpage.getByPlaceholder("...")
Finds <input> or <textarea> via placeholder attribute.

await page.getByPlaceholder('name@example.com').fill('...');

Alt Textpage.getByAltText("...")
Targets <img> (and similar) using its alt-text. Great for clickable images.

await page.getByAltText('logo').click();

Titlepage.getByTitle("...")
Selects elements with a matching title="..." attribute—often tooltips.

await page.getByTitle('Close dialog').click();

Test IDpage.getByTestId("...")
Picks elements tagged with data-testid="..." (or custom attribute). Useful for projecting test‑only hooks.

await page.getByTestId('user-menu').click();

CSSpage.locator('css=...')
Leverages standard CSS selectors and supports Playwright’s powerful pseudo‑classes like :has-text() and :visible

await page.locator('ul > li.item:has-text("Table")').click();

XPath – page.locator('xpath=...') Enables selecting nodes via XPath expressions.

await page.locator('xpath=//div[@class="modal"]//button[text()="Close"]').click();

🛠 Comparison & Best Practices

  • First choice: Use role, then text, then label, etc.—the user-first selection philosophy HeadSpin+10Checkly+10LinkedIn+10.

  • Next, resort to CSS when no helpful attributes exist.

  • Avoid XPath unless absolutely needed—it’s harder to maintain and performance may suffer Medium+2Playwright+2Stack Overflow+2.

  • Test IDs (data-testid) are stable hooks, particularly when UI text or structure may change without impacting functionality Stack Overflow+6Cuketest+6HeadSpin+6.

  • Chaining & filtering: You can refine locators further using methods like .filter(), .nth(), or chaining locator.getBy...() for precision.

📋 Summary Table

Locator TypeUsage ExampleIdeal For
RolegetByRole('button', {name: 'OK'})Buttons, checkboxes, etc.
TextgetByText(/submit/i)Any element with visible text
LabelgetByLabel('Username')Form inputs with <label>
PlaceholdergetByPlaceholder('Search')Empty inputs with placeholder
Alt TextgetByAltText('Logo')Images, icons
TitlegetByTitle('Close dialog')Elements with title=...
Test IDgetByTestId('nav-item')Elements with test-specific ids
CSSlocator('div.nav > a')Complex selectors
XPathlocator('xpath=...')Rare/edge-case structured DOM

Assertions

👀 await expect(locator).toBeVisible()

  • What it checks

    • The element is in the DOM

    • Bounding box is non-empty (has size)

    • Not display: none or visibility: hidden

    • Visible, but can still be transparent (opacity: 0)

  • Auto-retries

    • Will poll repeatedly until visible or timeout (default ~5s)
  • Use case

    • Validates that content has appeared before interacting (e.g., ensures button is actually visible before clicking)

✍️ await expect(locator).toHaveText(expected) & .toHaveValue(expected)

  • toHaveText(text or regex)

    • Waits until inner-text equals expected, including normalization of whitespace
  • toHaveValue(value or regex)

    • Waits until the input (or select) has that value
  • Auto-retries

    • Both methods retry until pass or timeout
  • Use case

    • When you need to confirm dynamic content (e.g., after typing or loading)

☑️ await expect(locator).toBeChecked()

  • What it checks

    • Applies to checkboxes/radios

    • Confirms they are actually checked (input.checked === true)

  • Auto-retries

    • Retries until the checked state is confirmed or timeout
  • Use case

  •       await page.getByLabel('Subscribe').check();
          await expect(page.getByLabel('Subscribe')).toBeChecked();
    

    await expect(locator).toBeDisabled()

    • What it checks

      • Checks if a form control (<button>, <input>, <select>, etc.) is disabled (disabled attribute OR aria-disabled)
    • Auto-retries

      • Retries until disabled state detected, or times out
    • Use case

      • Confirm controls are correctly disabled during processing before further interactions

🧠 Behind the curtains — Auto-Retry & Defaults

All these are LocatorAssertions, meaning they automatically refetch the locator and retry the assertion until:

  • The condition is met, or

  • The timeout is reached (default ~5 seconds, configurable via expect.setTimeout() or playwright.config.ts)

This smart behavior handles timing issues gracefully and hugely reduces flakiness.

  •       import { test, expect } from '@playwright/test';
    
          test('form interaction example', async ({ page }) => {
            await page.goto('/settings');
    
            // wait until form loads
            await expect(page.getByText('Settings')).toBeVisible();
    
            // checkbox toggling
            const notify = page.getByLabel('Email notifications');
            await notify.check();
            await expect(notify).toBeChecked();
    
            // value change
            const input = page.getByPlaceholder('Username');
            await input.fill('newuser');
            await expect(input).toHaveValue('newuser');
    
            // form submission
            const submit = page.getByRole('button', { name: 'Save' });
            await expect(submit).toBeEnabled();
            await submit.click();
    
            // confirmation message
            await expect(page.getByText('Changes saved')).toBeVisible();
          });
    

    🧩 TL;DR

    | Assertion | Checks | Auto-Retry | Use Case | | --- | --- | --- | --- | | toBeVisible() | Element exists and is visible | ✅ | Await dynamic content/display updates | | toHaveText() | Exact inner text match | ✅ | Validate dynamic or loaded text | | toHaveValue() | Input/select value match | ✅ | Confirm typing or selection updates | | toBeChecked() | Checkbox/radio is checked | ✅ | Verify toggles or checkboxes | | toBeDisabled() | Form control is disabled | ✅ | Ensure controls are correctly disabled |

Element Actions

Here’s a breakdown of the most common element actions in Playwright, what they do, and when to use them:


🎯 Core Actions

click()

dblclick()

  • Performs a double-click on the element.

  • Equivalent to two rapid clicks. Playwright

hover()

  • Moves the mouse over the element.

  • Useful for activating hover menus or tooltips. Codoid

fill()

  • Clears any existing text and sets the input to the specified string.

  • Triggers focus, input, and change events.

  • Works on <input>, <textarea>, and content‑editable elements. Stack Overflow+9Playwright+9Playwright+9

type()

  • Types text character by character, triggering keydown, keypress, input, and keyup.

  • Typically used only when text needs to be processed per keystroke. Playwright

press()

  • Sends a key or key combination (e.g. 'Enter', 'Control+ArrowRight').

  • Simulates keyboard shortcuts. Playwright

check() / uncheck()

selectOption()


🛠 Additional Element Actions

These are less commonly mentioned but can be very useful:

focus() / blur()

dispatchEvent()

  • Triggers any DOM event programmatically (e.g. 'click', 'dragstart').

  • Useful for simulating events without user interaction. Playwright

dragTo()

  • Clicks and drags one element onto another (supports articles/drag-drop).

scrollIntoViewIfNeeded()

  • Scrolls the element into view if it’s off-screen or hidden beneath the fold.

  • Often used internally before click or input actions. PlaywrightPlaywright


⏱ Action Flow & Auto-Waiting

Playwright actions automatically incorporate checks like:

  1. Waiting for the element to be in the DOM

  2. Checking visibility and display styles

  3. Ensuring the element isn’t moving or obscured

  4. Scrolling it into view

  5. Retrying if it becomes detached mid-action Stack Overflow+5Codoid+5Stack Overflow+5Checkly+9Playwright+9Playwright+9


✅ When to Use What

SituationAction
Clicking a button/linkclick()
Clicking twice (e.g., selecting text)dblclick()
Hovering to reveal menus/tooltipshover()
Filling text inputs or textareasfill()
Simulating typing per charactertype() or press()
Checking/unchecking checkbox/radiocheck() / uncheck()
Choosing dropdown optionsselectOption()
Drag-and-drop interactionsdragTo()
Triggering JS-defined eventsdispatchEvent()
Scrolling into view manuallyscrollIntoViewIfNeeded()
Focusing/blurring input fieldsfocus() / blur()

Handling Forms and Inputs

Difference between fill() and type() :-

🧩 locator.fill(text)

  • All‑at‑once: Clears any existing content and sets the full text immediately.

  • Triggers: Dispatches a single input event (and change on blur).

  • Ideal for: Filling forms quickly with exact values.

  • Under the hood: Focuses the element, clears value, fills new text, and emits events .

  • Best practice:

      await page.getByLabel('Email').fill('alice@example.com');
    

    🎯 locator.type(text, { delay? })

    • Simulates real typing: Sends one character at a time like an actual user.

    • Triggers: Full keyboard event sequence — keydown, keypress, keyup — for each character.

    • Optional delay: You can slow it down (e.g. { delay: 100 } ms) to mimic human speed.

    • Use case: When the page has special keystroke-based logic (e.g. autocomplete, shortcuts, input masks)

    • Example:

  await page.getByPlaceholder('Search...').type('Playwright', { delay: 100 });

🔑 Comparison Summary

Criterionfill()type()
Input behaviorSet value in one goSimulates authentic typing
Events triggeredSingle input eventkeydown/keypress/keyup per char
Use caseForms, quick fillAutocomplete, masked inputs, UX logic
Speed controlNo delay optionDelay between keystrokes
Default recommendation✅ Preferred for most scenarios✅ Use when character timing matters

Auto-Waits

Whenever you perform an action—like click(), fill(), or type()—Playwright performs a set of actionability checks, waiting for each to pass before proceeding :

  • Attached: Element exists in the DOM.

  • Visible: Element is rendered (non-zero size & not hidden).

  • Stable: Not moving or being animated.

  • Enabled: Not disabled (for buttons, inputs).

  • Editable: Can accept text input (for fill/type).

  • Receives events: Not covered by another element and can be clicked.

You don’t need to waitForSelector, wait for a spinner, or sleep()—Playwright handles it all under the hood.


🔍 page.waitForSelector(selector, options?)

  • Purpose: Pauses execution until an element matching the selector meets a specified state.

  • Default behavior: Waits for the element to be attached and visible.

  • Options:

    • state: 'attached' | 'detached' | 'visible' | 'hidden'

    • timeout: custom wait time in milliseconds.

  • Returns: ElementHandle (or null if waiting for disappearance).

Example:

await page.click('button#load-more');
await page.waitForSelector('.item', { state: 'visible', timeout: 5000 });
// Now it's safe to interact with new items

Prefer using locators (page.getBy...) and assertions instead, which auto-wait for you.

⏱️ page.waitForTimeout(ms)

  • Purpose: Simple delay—pauses the script for the specified milliseconds.

  • Use cases: Rarely recommended except for debugging or simulating slow user input.

await page.waitForTimeout(2000); // wait 2 seconds

This is a “hard” wait and can lead to flakiness and wasted time.

🚦 page.waitForLoadState(state?)

  • Purpose: Waits for the page to reach a certain loading milestone.

  • States:

    • 'load' (default): waits for window.load

    • 'domcontentloaded': waits for that event

    • 'networkidle': waits until no network connections for ~500ms (less reliable)

  • Example:

  •     await page.goto('/dashboard');
        await page.waitForLoadState('domcontentloaded');
    

    ✅ Summary Table

    | Method / Event | Waits For | When to Use | | --- | --- | --- | | page.goto(url, waitUntil) | 'commit', 'domcontentloaded', 'load', or 'networkidle' | On initial navigation, ensure correct load level. | | page.reload() | Same as goto | To refresh page state and resources. | | page.goBack() / page.goForward() | Navigation history, optionally wait for load state | Navigating between pages or flows. | | page.waitForLoadState(state) | DOM parsing, full load, or inactivity | Use after clicks/navigations in dynamic apps. | | domcontentloaded event | HTML parsed | Fast interactions or scripts post-DOM ready. | | load event | All page assets and scripts loaded | Full resource readiness. | | networkidle event | No network activity for ~500ms | SPA or AJAX-heavy page stability. |

    🔹 9. Multi-page and Popup Handling in Playwright


    ✅ Use Case:

    Modern web applications may open:

    • A new tab or window (e.g., login with Google)

    • A popup on clicking a button or link

You must handle these new pages/contexts properly to interact with their elements.


🔸 1. Handling Multiple Tabs or Windows

In Playwright, a new tab or window is treated as a new Page object. You can listen for the 'popup' event to detect this new page.


🔹 Example: Handling New Tab

import { test, expect, Page } from '@playwright/test';

test('Handle new tab or popup', async ({ browser }) => {
  const context = await browser.newContext();
  const page = await context.newPage();
  await page.goto('https://example.com');

  // Trigger the new tab
  const [newPage] = await Promise.all([
    page.waitForEvent('popup'), // Listen for new page
    page.click('a[target="_blank"]'), // Action that opens the new tab
  ]);

  // Interact with the new page
  await newPage.waitForLoadState();
  console.log(await newPage.title());
});

🧠 How This Works:

PartExplanation
page.waitForEvent('popup')Waits until a new page is created via popup (like window.open() or target="_blank" link).
Promise.all([...])Ensures we listen before the click triggers the popup, avoiding race conditions.
newPageRepresents the newly opened tab or window.

Frames

🔹 What are Frames & Iframes?

Iframe (inline frame) is an HTML element that allows embedding another HTML document within the current page.

Example:

<iframe src="https://example.com"></iframe>

In UI testing, interacting with content inside iframes needs special handling because iframe content is like a separate page within a page.

🔸 1. Locating and Interacting with Frames

There are two primary ways to work with frames in Playwright:

✅ A. frameLocator(): Locator API Style (Preferred for modern usage)

  • Used to interact with elements inside the iframe without switching context.

  • Similar to locator() but scoped to the iframe.

  •   await page
        .frameLocator('iframe[name="login-frame"]')
        .locator('text=Sign In')
        .click();
    

    ✅ B. frame(): Traditional way (Used when you need access to the whole frame as a Page)

    • First get the frame using page.frame() or page.frames()

    • Then use its own API like frame.click(), frame.locator(), etc.

const frame = page.frame({ name: 'login-frame' }); // or use url/title

await frame?.click('text=Sign In');

🧠 Difference: frameLocator() vs frame()

FeatureframeLocator()frame()
StyleLocator-style chainingTraditional frame object
UseDirectly find and act on elementsFull control of frame API
SyntaxMore concise & preferredVerbose, more flexible
Async AccessNot requiredRequires extracting frame object first
When to useSimple interactions in frameAdvanced needs (e.g. looping frames, nested handling)

🔸 2. frameLocator() – In-depth

✅ Syntax:

const locator = page.frameLocator(selector);

You can then call:

locator.locator('selector').click()
locator.locator('selector').fill()

✅ Example:

await page
  .frameLocator('iframe[src*="login"]')
  .locator('input[name="username"]')
  .fill('rahat');

await page
  .frameLocator('iframe')
  .locator('button[type="submit"]')
  .click();

🔸 3. frame() – In-depth

✅ Ways to access a frame:

const frameByName = page.frame({ name: 'my-frame' });
const frameByUrl = page.frame({ url: /.*login.*/ });
const allFrames = page.frames();

✅ Example:

const frame = page.frame({ name: 'login-frame' });
await frame?.fill('input[name="email"]', 'rahat@example.com');
await frame?.click('button[type="submit"]');

🔁 Example: Loop Through All Frames

for (const f of page.frames()) {
  console.log('Frame URL:', f.url());
}

🔸 4. Nested Iframes

You can nest frameLocator() calls:

await page
  .frameLocator('#outer-frame')
  .frameLocator('#inner-frame')
  .locator('text=Submit')
  .click();

🔸 5. Wait for Frame to be Available

Sometimes the iframe loads later via JS. You can wait for it:

await page.waitForSelector('iframe[name="my-frame"]');

const frame = await page.frame({ name: 'my-frame' });
await frame?.waitForSelector('input[name="email"]');

✅ Best Practices

TipDescription
Prefer frameLocator()It’s more readable, chainable, and integrates better with locators
Always wait for iframeIf dynamically loaded, use waitForSelector()
Use frame URL or nameWhen using frame() to identify the frame
Avoid hard waitsUse auto-waiting like waitForSelector() inside frame

✅ Real Example: Both Approaches

Using frameLocator()

await page
  .frameLocator('iframe[name="editor-frame"]')
  .locator('textarea')
  .fill('Hello inside iframe');

Using frame()

const frame = page.frame({ name: 'editor-frame' });
await frame?.fill('textarea', 'Hello inside iframe');

🔚 Summary

TopicframeLocator()frame()
PurposeLocator-style frame interactionFull frame object access
Best forSimpler tasks like click/fillComplex frame handling
SyntaxframeLocator('iframe').locator()const frame = page.frame(...)
NestingSupports .frameLocator().frameLocator()Requires frame chaining manually

🔸 Hook Overview

🔸 1. beforeAll and afterAll

These are used for once-per-suite setup/teardown.

✅ Example:

RunsUse Case
beforeAllOnce before all tests in the file/groupSetup shared resources (e.g., DB, login)
afterAllOnce after all tests in the file/groupCleanup (e.g., close DB, clear session)
beforeEachBefore each individual testFresh page, login, mocks
afterEachAfter each individual testCleanup, logout, reset data

🔸 1. beforeAll and afterAll

These are used for once-per-suite setup/teardown.

✅ Example:

import { test, expect, Browser, Page } from '@playwright/test';

let page: Page;

test.describe('User Tests', () => {
  test.beforeAll(async ({ browser }) => {
    const context = await browser.newContext();
    page = await context.newPage();
    await page.goto('https://example.com/login');
    await page.fill('#username', 'admin');
    await page.fill('#password', 'admin123');
    await page.click('button[type="submit"]');
  });

  test.afterAll(async () => {
    await page.close();
  });

  test('Dashboard loads correctly', async () => {
    await expect(page.locator('h1')).toHaveText('Dashboard');
  });

  test('Profile loads', async () => {
    await page.click('text=Profile');
    await expect(page.locator('h2')).toHaveText('Your Profile');
  });
});

🔸 2. beforeEach and afterEach

These run before and after every test case.

test.describe('Isolated Tests', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('https://example.com');
  });

  test.afterEach(async ({ page }) => {
    // Useful if you need to clear local storage or cookies
    await page.context().clearCookies();
  });

  test('Homepage loads', async ({ page }) => {
    await expect(page.locator('h1')).toHaveText('Welcome');
  });

  test('About page loads', async ({ page }) => {
    await page.click('text=About');
    await expect(page).toHaveURL(/.*about/);
  });
});

More from this blog