Playwright Typescript Topics
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:
Role –
page.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();Text –
page.getByText("...")
Finds elements containing visible text. Supports substring, exact match, or regex.
await page.getByText(/welcome/i).click();
Label – page.getByLabel("...")
Locates form controls by their corresponding <label> text.
await page.getByLabel('Email').fill('user@example.com');
Placeholder – page.getByPlaceholder("...")
Finds <input> or <textarea> via placeholder attribute.
await page.getByPlaceholder('name@example.com').fill('...');
Alt Text – page.getByAltText("...")
Targets <img> (and similar) using its alt-text. Great for clickable images.
await page.getByAltText('logo').click();
Title – page.getByTitle("...")
Selects elements with a matching title="..." attribute—often tooltips.
await page.getByTitle('Close dialog').click();
Test ID – page.getByTestId("...")
Picks elements tagged with data-testid="..." (or custom attribute). Useful for projecting test‑only hooks.
await page.getByTestId('user-menu').click();
CSS – page.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 chaininglocator.getBy...()for precision.
📋 Summary Table
| Locator Type | Usage Example | Ideal For |
| Role | getByRole('button', {name: 'OK'}) | Buttons, checkboxes, etc. |
| Text | getByText(/submit/i) | Any element with visible text |
| Label | getByLabel('Username') | Form inputs with <label> |
| Placeholder | getByPlaceholder('Search') | Empty inputs with placeholder |
| Alt Text | getByAltText('Logo') | Images, icons |
| Title | getByTitle('Close dialog') | Elements with title=... |
| Test ID | getByTestId('nav-item') | Elements with test-specific ids |
| CSS | locator('div.nav > a') | Complex selectors |
| XPath | locator('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: noneorvisibility: hiddenVisible, 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 (disabledattribute ORaria-disabled)
- Checks if a form control (
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()orplaywright.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()
Simulates a human-like click (single by default).
Waits for the element to be visible, stable, and ready.
Supports options like:
{ button: 'right' }for right-click{ modifiers: ['Shift'] }for shift-click{ force: true }to bypass visibility and hitability checks Stack Overflow+9Playwright+9testomat.io+9Codoid+5Checkly+5Stack Overflow+5
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, andchangeevents.Works on
<input>,<textarea>, and content‑editable elements. Stack Overflow+9Playwright+9Playwright+9
type()
Types text character by character, triggering
keydown,keypress,input, andkeyup.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()
Toggles checkboxes or radio buttons.
Auto-scrolls and ensures correct final state. Codoid+3Playwright+3Playwright+3Playwright+2Stack Overflow+2YouTube+2
selectOption()
Selects one or multiple options in a
<select>.You can match by value or label. Stack Overflow+6Playwright+6Playwright+6
🛠 Additional Element Actions
These are less commonly mentioned but can be very useful:
focus() / blur()
focus()gives keyboard and visual focus to an element.blur()removes focus. PlaywrightCodoid+1Playwright+1Stack Overflow+2Medium+2YouTube+2
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:
Waiting for the element to be in the DOM
Checking visibility and display styles
Ensuring the element isn’t moving or obscured
Scrolling it into view
Retrying if it becomes detached mid-action Stack Overflow+5Codoid+5Stack Overflow+5Checkly+9Playwright+9Playwright+9
✅ When to Use What
| Situation | Action |
| Clicking a button/link | click() |
| Clicking twice (e.g., selecting text) | dblclick() |
| Hovering to reveal menus/tooltips | hover() |
| Filling text inputs or textareas | fill() |
| Simulating typing per character | type() or press() |
| Checking/unchecking checkbox/radio | check() / uncheck() |
| Choosing dropdown options | selectOption() |
| Drag-and-drop interactions | dragTo() |
| Triggering JS-defined events | dispatchEvent() |
| Scrolling into view manually | scrollIntoViewIfNeeded() |
| Focusing/blurring input fields | focus() / 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
inputevent (andchangeon blur).Ideal for: Filling forms quickly with exact values.
Under the hood: Focuses the element, clears
value, fills newtext, 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
| Criterion | fill() | type() |
| Input behavior | Set value in one go | Simulates authentic typing |
| Events triggered | Single input event | keydown/keypress/keyup per char |
| Use case | Forms, quick fill | Autocomplete, masked inputs, UX logic |
| Speed control | No delay option | Delay 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(ornullif 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 forwindow.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 asgoto| 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. | |domcontentloadedevent | HTML parsed | Fast interactions or scripts post-DOM ready. | |loadevent | All page assets and scripts loaded | Full resource readiness. | |networkidleevent | 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:
| Part | Explanation |
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. |
newPage | Represents 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()orpage.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()
| Feature | frameLocator() | frame() |
| Style | Locator-style chaining | Traditional frame object |
| Use | Directly find and act on elements | Full control of frame API |
| Syntax | More concise & preferred | Verbose, more flexible |
| Async Access | Not required | Requires extracting frame object first |
| When to use | Simple interactions in frame | Advanced 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
| Tip | Description |
Prefer frameLocator() | It’s more readable, chainable, and integrates better with locators |
| Always wait for iframe | If dynamically loaded, use waitForSelector() |
| Use frame URL or name | When using frame() to identify the frame |
| Avoid hard waits | Use 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
| Topic | frameLocator() | frame() |
| Purpose | Locator-style frame interaction | Full frame object access |
| Best for | Simpler tasks like click/fill | Complex frame handling |
| Syntax | frameLocator('iframe').locator() | const frame = page.frame(...) |
| Nesting | Supports .frameLocator().frameLocator() | Requires frame chaining manually |
🔸 Hook Overview
🔸 1. beforeAll and afterAll |
These are used for once-per-suite setup/teardown.
✅ Example:
| Runs | Use Case | |
beforeAll | Once before all tests in the file/group | Setup shared resources (e.g., DB, login) |
afterAll | Once after all tests in the file/group | Cleanup (e.g., close DB, clear session) |
beforeEach | Before each individual test | Fresh page, login, mocks |
afterEach | After each individual test | Cleanup, 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/);
});
});