Playwright Actions: Click, Fill, Type, Check, Press, Hover and More

Locators find elements; actions make things happen. In this lesson you will learn every Playwright action a tester needs, how auto-waiting protects each one, and which method to choose, with runnable examples on free practice pages.

📖 What You’ll Learn: actionability checks, click options, dblclick and right-click, fill vs pressSequentially, clear, press and the keyboard API, check/uncheck/setChecked, hover, focus, drag and drop, scrolling, and reading values from the page.


🤖 How Playwright Actions Work

An action is anything a user does: click, type, tick a checkbox, press a key, hover, drag. In Playwright you call actions on a locator, and Playwright handles the timing for you.

Before each action, Playwright runs actionability checks and waits until they pass (up to the timeout):

Checkclickfillcheckhover
Attached to the DOM✅✅✅✅
Visible✅✅✅✅
Stable (not animating)✅✅✅
Receives events (not covered)✅✅✅
Enabled✅✅✅
Editable✅

This is why you do not need sleeps before clicks. If a check never passes, the error tells you exactly which one failed, for example “element is not visible” or “another element would receive the click”. Learn more in Playwright waits.


🖱️ click() and Its Options

const login = page.getByRole('button', { name: 'Login' });

await login.click();                                  // left click
await login.click({ button: 'right' });              // right click
await login.click({ clickCount: 2 });                 // same as dblclick()
await login.dblclick();                               // double click
await login.click({ modifiers: ['Shift'] });          // Shift + click ('Control', 'Alt', 'Meta')
await login.click({ position: { x: 5, y: 5 } });      // click a specific point
await login.click({ trial: true });                   // run checks only, do not click

Example on a practice page that adds a button every time you click:

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

test('add three elements', async ({ page }) => {
  await page.goto('https://the-internet.herokuapp.com/add_remove_elements/');
  const add = page.getByRole('button', { name: 'Add Element' });

  for (let i = 0; i < 3; i++) {
    await add.click();
  }
  await expect(page.locator('.added-manually')).toHaveCount(3);

  await page.locator('.added-manually').first().click(); // delete one
  await expect(page.locator('.added-manually')).toHaveCount(2);
});

⚠️ Common mistake: Avoid click({ force: true }). It skips the actionability checks and clicks even when the element is hidden or covered, so your test can pass while a real user would be blocked. Fix the cause (wait for the overlay to disappear) instead.


⌨️ Typing Text: fill(), clear() and pressSequentially()

const username = page.getByPlaceholder('Username');

await username.fill('standard_user');     // clears the field, then sets the value in one step
await username.clear();                   // empties the field
await username.pressSequentially('standard_user', { delay: 100 }); // types key by key
MethodUse it when
fill()Almost always: fastest and most reliable for inputs and textareas
pressSequentially()The page reacts to each keystroke (autocomplete, live search, input masks)
clear()You need an empty field

📝 Note: locator.type() is deprecated. Use fill(), or pressSequentially() when you really need individual key events.

Verify what was typed with toHaveValue:

test('number input', async ({ page }) => {
  await page.goto('https://the-internet.herokuapp.com/inputs');
  const number = page.locator('input[type="number"]');

  await number.fill('42');
  await expect(number).toHaveValue('42');

  await number.press('ArrowUp');           // increments the number input
  await expect(number).toHaveValue('43');
});

🔑 Pressing Keys: press() and page.keyboard

await page.getByLabel('Search').press('Enter');
await page.getByLabel('Search').press('Control+A');   // key combination
await page.getByLabel('Search').press('Backspace');

// Page-level keyboard (acts on whatever has focus)
await page.keyboard.press('Tab');
await page.keyboard.type('Hello');
await page.keyboard.down('Shift');
await page.keyboard.press('ArrowLeft');
await page.keyboard.up('Shift');

Common key names: Enter, Tab, Escape, Backspace, Delete, ArrowUp, ArrowDown, ArrowLeft, ArrowRight, Home, End, PageDown, F1 to F12, and modifiers Shift, Control, Alt, Meta. Use ControlOrMeta for shortcuts that are Ctrl on Windows/Linux and Cmd on macOS.

test('key press is detected', async ({ page }) => {
  await page.goto('https://the-internet.herokuapp.com/key_presses');
  await page.locator('#target').press('Escape');
  await expect(page.locator('#result')).toContainText('You entered:');
});

☑️ Checkboxes and Radio Buttons: check(), uncheck(), setChecked()

test('toggle checkboxes', async ({ page }) => {
  await page.goto('https://the-internet.herokuapp.com/checkboxes');
  const boxes = page.locator('#checkboxes input[type="checkbox"]');

  await boxes.nth(0).check();             // ticks it (no-op if already ticked)
  await expect(boxes.nth(0)).toBeChecked();

  await boxes.nth(1).uncheck();
  await expect(boxes.nth(1)).not.toBeChecked();

  await boxes.nth(1).setChecked(true);     // set to a specific state
  await expect(boxes.nth(1)).toBeChecked();
});

Prefer check() over click() for checkboxes: check() is idempotent and verifies the final state, while click() simply toggles. Radio buttons work the same way: page.getByLabel('Express delivery').check().


🔽 Dropdowns, File Uploads and Dialogs

These have their own detailed lesson, Alerts, Popups, Dropdowns, iFrames and Tabs. Quick reference:

await page.locator('#dropdown').selectOption({ label: 'Option 2' });   // native select
await page.locator('#file-upload').setInputFiles('data/sample.txt');    // upload
page.once('dialog', d => d.accept());                                    // JS alert

🖐️ Hover, Focus, Drag and Scroll

test('hover reveals the caption', async ({ page }) => {
  await page.goto('https://the-internet.herokuapp.com/hovers');
  await page.locator('.figure').nth(1).hover();
  await expect(page.getByText('name: user2')).toBeVisible();
});

test('right-click opens a context-menu alert', async ({ page }) => {
  await page.goto('https://the-internet.herokuapp.com/context_menu');
  page.once('dialog', async dialog => {
    expect(dialog.message()).toBe('You selected a context menu');
    await dialog.accept();
  });
  await page.locator('#hot-spot').click({ button: 'right' });
});
await page.getByLabel('Email').focus();                         // focus without clicking
await page.getByLabel('Email').blur();                          // trigger validation on blur
await page.locator('#source').dragTo(page.locator('#target'));  // drag and drop
await page.getByText('Footer links').scrollIntoViewIfNeeded();  // scroll to an element
await page.mouse.wheel(0, 800);                                 // scroll the page down

Playwright scrolls elements into view automatically before acting, so you rarely need to scroll yourself. Explicit scrolling is useful for infinite-scroll pages and lazy-loaded content.


📖 Reading Values from the Page

Actions often go together with reading data:

MethodReturns
textContent()All text, including hidden text (string or null)
innerText()Visible, rendered text
inputValue()Current value of an input, textarea or select
getAttribute('href')An attribute value
allTextContents()Text of every matching element (string array)
count()Number of matching elements
isVisible(), isChecked(), isEnabled()Current state, checked once without waiting

💡 Tip: To verify something, use web-first assertions such as toHaveText, toHaveValue and toBeChecked. They wait and retry. Use the read methods above only when you need the value for later steps.


📝 Assignment

  1. On https://the-internet.herokuapp.com/add_remove_elements/ add five elements, delete two, and assert three remain.
  2. On https://the-internet.herokuapp.com/checkboxes make both boxes checked using check(), then both unchecked using setChecked(false), with assertions after each step.
  3. On https://the-internet.herokuapp.com/inputs type 100, press ArrowDown three times and assert the value is 97.
  4. On https://the-internet.herokuapp.com/hovers hover each of the three figures in a loop and assert the matching caption (name: user1 to name: user3) is visible.
  5. On https://www.saucedemo.com log in using pressSequentially for the username and press('Enter') in the password field instead of clicking the button.
  6. Bonus: read all three hover captions with allTextContents() and assert the array length is 3.

❓ FAQ

🤔 What is the difference between fill and type in Playwright?

fill() sets the whole value at once and is the recommended method. The old type() is deprecated; its replacement pressSequentially() types key by key for pages that react to each keystroke.

🤔 How do I double-click or right-click in Playwright?

Use locator.dblclick() for a double click and locator.click({ button: 'right' }) for a right click.

🤔 How do I press Enter in Playwright?

await locator.press('Enter') on an element, or await page.keyboard.press('Enter') for whatever has focus.

🤔 Why does Playwright say “element is not visible” or “intercepts pointer events”?

The element is hidden or covered by another element, such as a loader or cookie banner. Wait for the overlay to disappear or close it first, instead of using force: true.

🤔 How do I check a checkbox in Playwright?

await page.getByLabel('Remember me').check(), then verify with await expect(...).toBeChecked().


🎯 Next Steps

🎬 Watch the full Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

CSS and XPath Selectors in Playwright: Complete Guide with Examples

Role-based locators are the first choice in Playwright, but every real project also needs CSS selectors and XPath. This lesson teaches both from scratch with cheat sheets and runnable examples on real practice pages.

📖 What You’ll Learn: how Playwright detects CSS and XPath, CSS selector syntax, XPath functions and axes, reading table cells three ways, chaining and filtering locators, strict mode, testing selectors in DevTools, and CSS vs XPath.


🧭 Where CSS and XPath Fit in Playwright

Playwright recommends user-facing locators first, such as getByRole, getByLabel and getByText, because they match how users see the page and survive UI refactors. You learned them in Playwright Locators.

But real applications are messy: missing labels, duplicate text, generated tables, legacy markup. That is when CSS selectors and XPath become essential. Both work through page.locator() and get all Playwright benefits: auto-waiting, retries and strictness.

Recommended priority:

  1. getByRole (with accessible name)
  2. getByLabel, getByPlaceholder, getByText, getByAltText, getByTitle
  3. getByTestId (data-testid or your configured attribute)
  4. CSS selector (#id, [data-test=...], stable classes)
  5. XPath (complex relationships, text plus structure, legacy pages)

🔤 How Playwright Detects CSS vs XPath

page.locator('#user-name');                 // CSS (default)
page.locator('//input[@id="user-name"]');   // XPath: starts with //
page.locator('..');                         // XPath: parent of the current element

// Explicit prefixes (optional)
page.locator('css=#user-name');
page.locator('xpath=//input[@id="user-name"]');

Any selector starting with // or .. is treated as XPath; everything else is CSS unless you add a prefix.


🎨 CSS Selectors Cheat Sheet

All examples use the real login page of https://www.saucedemo.com, whose inputs look like this:

<input placeholder="Username" data-test="username" id="user-name" name="user-name" type="text" class="input_error form_input">
<input placeholder="Password" data-test="password" id="password" name="password" type="password" class="input_error form_input">
<input data-test="login-button" id="login-button" type="submit" value="Login" class="submit-button btn_action">
SelectorMatchesExample
#idElement with that id#user-name
.classElement with that class.submit-button
tagAll elements of that taginput
tag.classTag with a classinput.form_input
[attr="value"]Exact attribute value[data-test="password"]
[attr^="v"]Attribute starts with[id^="user"]
[attr$="v"]Attribute ends with[id$="button"]
[attr*="v"]Attribute contains[placeholder*="ser"]
A BB anywhere inside Aform input
A > BB as a direct child of Aform > div
A + BB immediately after Alabel + input
:nth-child(n)The nth child of its parenttr:nth-child(2)
A, BEither A or B#user-name, #password
import { test, expect } from '@playwright/test';

test('login with CSS selectors', async ({ page }) => {
  await page.goto('https://www.saucedemo.com/');
  await page.locator('#user-name').fill('standard_user');
  await page.locator('[data-test="password"]').fill('secret_sauce');
  await page.locator('input[type="submit"]').click();
  await expect(page).toHaveURL(/inventory/);
});

💡 Tip: Prefer ids and test attributes (data-test, data-testid) over classes. Classes such as css-1x2y3z generated by CSS-in-JS libraries change on every build.


🧬 XPath Cheat Sheet

XPathMatches
//inputAny input anywhere
//input[@id='user-name']Input with id user-name
//input[@type='submit' and @value='Login']Both conditions
//input[@id='x' or @name='x']Either condition
//div[text()='Swag Labs']Element whose own text is exactly Swag Labs
//h4[contains(text(),'Accepted')]Text contains a substring
//input[starts-with(@id,'user')]Attribute starts with
//td[normalize-space()='Jason']Text with surrounding spaces trimmed
(//tr)[2]The second tr in the whole document
//td[2]Second td within each row

XPath axes: move around the DOM

Axes are XPath’s superpower: they let you find an element relative to another, which CSS cannot do upwards.

AxisMoves to
parent::tag or ..The parent element
ancestor::tagAny ancestor (parent, grandparent…)
following-sibling::tagSiblings after the element
preceding-sibling::tagSiblings before the element
descendant::tagAny element inside

📊 Real Example: Tables with CSS, XPath and Filters

The practice page https://the-internet.herokuapp.com/tables has a table #table1 with the columns Last Name, First Name, Email, Due, Web Site and Action. Let’s read Jason Doe’s amount due ($100.00) three different ways.

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

test.describe('read a table cell', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('https://the-internet.herokuapp.com/tables');
  });

  test('with CSS nth-child', async ({ page }) => {
    // 3rd row, 4th column (Due)
    const due = page.locator('#table1 tbody tr:nth-child(3) td:nth-child(4)');
    await expect(due).toHaveText('$100.00');
  });

  test('with XPath following-sibling', async ({ page }) => {
    // From the "Jason" cell, the 2nd cell after it is "Due"
    const due = page.locator("//table[@id='table1']//td[text()='Jason']/following-sibling::td[2]");
    await expect(due).toHaveText('$100.00');
  });

  test('with Playwright filter (recommended)', async ({ page }) => {
    const row = page.locator('#table1 tbody tr').filter({ hasText: 'Jason' });
    await expect(row.locator('td').nth(3)).toHaveText('$100.00');
    await expect(page.locator('#table1 tbody tr')).toHaveCount(4);
  });

  test('click edit in the row of a user (XPath parent)', async ({ page }) => {
    await page.locator("//table[@id='table1']//td[text()='Doe']/parent::tr//a[text()='edit']").click();
    await expect(page).toHaveURL(/#edit/);
  });
});

The filter version is the easiest to read: it finds the row by meaningful text instead of a hard-coded row number, so it keeps working when rows are re-ordered or new rows are added. For more table techniques see reading data from web tables.


🔗 Combine Selectors with Playwright Locator Methods

You do not have to put everything in one long selector. Chain and filter instead:

// Chain: find inside another locator
const form = page.locator('form');
await form.locator('#user-name').fill('standard_user');

// Filter by text or by a child element
page.locator('#table1 tbody tr').filter({ hasText: 'Conway' });
page.locator('tr').filter({ has: page.locator('a', { hasText: 'delete' }) });

// Pick from several matches
page.locator('#table1 tbody tr').first();
page.locator('#table1 tbody tr').last();
page.locator('#table1 tbody tr').nth(1);   // zero-based

// Mix CSS and role locators
page.locator('#table1').getByRole('link', { name: 'edit' }).first();

⚠️ Common mistake: Playwright locators are strict: an action like click() fails if the selector matches more than one element. Fix it by making the selector more specific or by using filter, first() or nth() deliberately, not by habit.


🧪 Test Your Selectors Before Using Them

  • Browser DevTools console: document.querySelectorAll('#table1 tbody tr') for CSS, $x("//td[text()='Jason']") for XPath (Chrome).
  • DevTools Elements panel: press Ctrl + F and type a CSS selector or XPath to highlight matches.
  • Playwright Inspector / VS Code “Pick locator”: click an element and Playwright suggests the best locator. See Playwright Codegen.
  • UI mode: npx playwright test --ui lets you try locators against a live snapshot.

⚖️ CSS vs XPath: Which Should You Use?

CSSXPath
ReadabilityShort and familiarLonger
Match by textNot in standard CSS (use filter or getByText)Yes, text() and contains()
Move to parent or ancestorNoYes, with axes
Shadow DOMPlaywright CSS pierces open shadow DOMDoes not pierce shadow DOM
Typical useids, attributes, structuretext plus structure, relative navigation

Rule of thumb: use role-based locators first, CSS for ids and test attributes, and XPath only when you need text-based or upward navigation that filters cannot express.


🚫 Selectors to Avoid

  • Absolute XPath such as /html/body/div[2]/div/form/div[1]/input: breaks with any layout change.
  • Long CSS chains such as div > div > div:nth-child(3) > span.
  • Auto-generated ids and classes (#ember123, .css-1kq9xd).
  • Index-only selectors when a stable text or attribute exists.

📝 Assignment

Use https://www.saucedemo.com and https://the-internet.herokuapp.com/tables:

  1. Write three different CSS selectors for the Sauce Demo password field and use one in a test.
  2. Write an XPath that finds the login button using both @type and @value.
  3. On the tables page, assert that the email of Frank Bach is fbach@yahoo.com using XPath with following-sibling.
  4. Do the same with filter({ hasText }) and nth().
  5. Count the rows of #table1 and assert there are 4.
  6. Click the delete link in Tim Conway’s row using an XPath with parent::tr.
  7. Bonus: rewrite every selector from tasks 1 to 6 using role-based locators where possible, and note which ones were not possible.

❓ FAQ

🤔 How do I use XPath in Playwright?

Pass it to page.locator(): page.locator("//input[@id='user-name']"). Selectors starting with // are detected as XPath automatically, or prefix them with xpath=.

🤔 How do I use a CSS selector in Playwright?

page.locator('#user-name') or page.locator('[data-test="password"]'). CSS is the default selector engine for locator().

🤔 Is XPath slower than CSS in Playwright?

The difference is negligible in practice. Choose based on readability and stability; prefer role-based locators and CSS, and use XPath when you need its axes or text matching.

🤔 Why does XPath not find my element inside a web component?

XPath does not pierce shadow DOM. Use a CSS selector or a role/text locator, which Playwright resolves inside open shadow roots.

🤔 How do I select by text with CSS in Playwright?

Standard CSS cannot match text. Use getByText(), or locator('tr').filter({ hasText: 'Jason' }).


🎯 Next Steps

🎬 Watch the full Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

JavaScript Promises and Async/Await Explained for Playwright Beginners

Every Playwright test is full of async and await. In this lesson you will finally understand what they mean: synchronous vs asynchronous code, Promises, async functions, error handling and parallel execution, with Playwright examples and the mistakes that cause flaky tests.

📖 What You’ll Learn: synchronous vs asynchronous code, Promise states, then/catch/finally, async and await, try/catch with await, Promise.all, allSettled, race and any, async/await in Playwright tests, and the four most common async mistakes.


⏳ Why Async Matters in Playwright

Almost everything Playwright does talks to a browser: opening a page, clicking, typing, reading text. These operations take time, from a few milliseconds to several seconds. JavaScript does not stop and wait for them by default. Instead they return a Promise, an object that says “the result will arrive later”.

That is why every Playwright test looks like this:

test('login', async ({ page }) => {
  await page.goto('https://www.saucedemo.com/');
  await page.getByPlaceholder('Username').fill('standard_user');
});

To write reliable tests you must understand three words: Promise, async and await. This lesson explains them from zero.


🔁 Synchronous vs Asynchronous Code

Synchronous code runs line by line; each line finishes before the next starts.

console.log('1. Open browser');
console.log('2. Type username');
console.log('3. Click login');
// Output is always 1, 2, 3

Asynchronous code starts a task and moves on without waiting. setTimeout is the simplest example:

console.log('1. Start');
setTimeout(() => console.log('2. Finished after 1 second'), 1000);
console.log('3. End');
// Output: 1. Start, 3. End, 2. Finished after 1 second

Line 3 printed before line 2 because JavaScript did not wait for the timer. If browser actions behaved like this without await, your test would click the login button before the username was typed.


🤝 What Is a Promise?

A Promise represents a value that is not available yet. It is always in one of three states:

StateMeaningRestaurant analogy
pendingStill workingYou ordered and got a token
fulfilled (resolved)Finished successfully with a valueYour food is ready
rejectedFailed with an errorThe kitchen ran out of the dish
// Creating a promise (you rarely do this in tests, but it shows how it works)
function loginApi(username) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (username === 'admin') resolve({ token: 'abc123' });
      else reject(new Error('Invalid user'));
    }, 500);
  });
}

Using a Promise with then, catch and finally

loginApi('admin')
  .then(result => console.log('Token:', result.token))   // fulfilled
  .catch(error => console.log('Error:', error.message))  // rejected
  .finally(() => console.log('Request finished'));       // always runs

This style works, but long chains of .then() become hard to read. That is why async and await were added to JavaScript.


✨ async and await

  • async before a function makes it return a Promise automatically and allows await inside it.
  • await pauses the async function until the Promise settles, then gives you its value (or throws its error).
async function runLogin() {
  const result = await loginApi('admin'); // waits here
  console.log('Token:', result.token);    // runs after the promise is fulfilled
}

runLogin();

The code now reads top to bottom like synchronous code, but it does not block the rest of the program.

Arrow functions can be async too

const getTitle = async (page) => {
  return await page.title();
};

🛡️ Handling Errors with try, catch and finally

When an awaited Promise is rejected, await throws an error. Catch it with try/catch:

async function safeLogin(user) {
  try {
    const result = await loginApi(user);
    console.log('Logged in:', result.token);
  } catch (error) {
    console.log('Login failed:', error.message);
  } finally {
    console.log('Clean up here');
  }
}

safeLogin('guest'); // Login failed: Invalid user

📝 Note: In Playwright tests you usually do not catch errors. A failed action or assertion should fail the test so you see it in the report. Use try/catch in helpers, for example for optional popups.


🧵 Running Promises in Parallel: Promise.all

await one after another runs tasks in sequence. When tasks are independent, start them together with Promise.all, which waits for all of them:

// Sequential: about 1.5 seconds in total
const a = await loginApi('admin');
const b = await loginApi('admin');
const c = await loginApi('admin');

// Parallel: about 0.5 seconds in total
const [x, y, z] = await Promise.all([loginApi('admin'), loginApi('admin'), loginApi('admin')]);
MethodResolves whenUse it for
Promise.allAll succeed (rejects on the first failure)Independent tasks that must all pass
Promise.allSettledAll finish, success or failureCollect every result, e.g. checking many links
Promise.raceThe first one finishesTimeouts, “whichever comes first”
Promise.anyThe first one succeedsFallback sources

🎭 Async/Await in Playwright: Real Examples

1. Every action and assertion needs await

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

test('add item to cart', async ({ page }) => {
  await page.goto('https://www.saucedemo.com/');
  await page.getByPlaceholder('Username').fill('standard_user');
  await page.getByPlaceholder('Password').fill('secret_sauce');
  await page.getByRole('button', { name: 'Login' }).click();

  await page.locator('[data-test="add-to-cart-sauce-labs-backpack"]').click();
  await expect(page.locator('.shopping_cart_badge')).toHaveText('1');
});

2. Methods that return values

const title = await page.title();                                // string
const price = await page.locator('.inventory_item_price').first().textContent(); // string or null
const names = await page.locator('.inventory_item_name').allTextContents(); // string[]
console.log(title, price, names.length);

3. Wait for an event and trigger it together

When an action opens a new tab or triggers a network request, start waiting before the action. Promise.all is a neat way to write this:

const [newTab] = await Promise.all([
  page.waitForEvent('popup'),                       // start waiting
  page.getByRole('link', { name: 'Open docs' }).click(), // trigger it
]);
await expect(newTab).toHaveURL(/docs/);

More patterns in Playwright waits and handling popups and tabs.

4. Async helper functions

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

export async function login(page: Page, user: string, pass: string): Promise<void> {
  await page.goto('https://www.saucedemo.com/');
  await page.getByPlaceholder('Username').fill(user);
  await page.getByPlaceholder('Password').fill(pass);
  await page.getByRole('button', { name: 'Login' }).click();
}

// in a test
await login(page, 'standard_user', 'secret_sauce'); // don't forget await here too

🐞 Common Async Mistakes (and Fixes)

Mistake 1: forgetting await

// ❌ The test may end before the click happens, or the next step runs too early
page.getByRole('button', { name: 'Login' }).click();

// ✅
await page.getByRole('button', { name: 'Login' }).click();

Mistake 2: await inside forEach

forEach does not wait for async callbacks; all iterations start at once and the test continues immediately.

const products = ['Sauce Labs Backpack', 'Sauce Labs Bike Light'];

// ❌ Does not wait
products.forEach(async name => {
  await page.locator('.inventory_item').filter({ hasText: name }).getByRole('button').click();
});

// ✅ for...of waits for each step
for (const name of products) {
  await page.locator('.inventory_item').filter({ hasText: name }).getByRole('button').click();
}

Mistake 3: await on a locator

Locators are lazy descriptions, not Promises. Await the action, not the locator.

const button = page.getByRole('button', { name: 'Login' }); // ✅ no await needed
await button.click();                                        // ✅ await the action

Mistake 4: checking a value once instead of waiting

// ❌ Reads the text once; fails if the page is still updating
expect(await page.locator('.title').textContent()).toBe('Products');

// ✅ Web-first assertion retries until it passes
await expect(page.locator('.title')).toHaveText('Products');

💡 Tip: Turn on the ESLint rule @typescript-eslint/no-floating-promises. It flags every Promise you forgot to await, which is the most common cause of flaky Playwright tests.


📝 Assignment

  1. Write a function delay(ms) that returns a Promise resolving after ms milliseconds, and use it with await to print three messages one second apart.
  2. Rewrite this chain with async/await and try/catch: loginApi('guest').then(r => console.log(r)).catch(e => console.log(e.message)).
  3. Use Promise.all to run three delay(1000) calls in parallel and measure the total time with Date.now().
  4. In a Playwright test on Sauce Demo, collect all product names with allTextContents() and print how many products there are.
  5. Add two products to the cart using a for...of loop, then assert the cart badge shows 2.
  6. Bonus: remove one await from a passing test, run it several times, and describe what goes wrong.

❓ FAQ

🤔 What is async/await in JavaScript?

async marks a function that returns a Promise, and await pauses that function until a Promise settles. Together they let you write asynchronous code that reads like normal step-by-step code.

🤔 Why do Playwright tests use async and await?

Every browser action returns a Promise because it takes time. await makes each step finish before the next one starts, so the test follows the user flow in order.

🤔 What happens if I forget await in Playwright?

The action starts but the test does not wait for it. Later steps can run too early, causing random failures, or the test can finish while actions are still running.

🤔 What is the difference between Promise.all and await in sequence?

Awaiting one by one runs tasks sequentially. Promise.all starts them together and waits for all, which is faster for independent tasks.

🤔 Do I need await before expect in Playwright?

Yes for web-first assertions on locators and pages, such as await expect(locator).toBeVisible(). Plain value assertions like expect(5).toBe(5) do not need it.


🎯 Next Steps

🎬 Watch the full Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

✎ Practise what you learned: 31 JavaScript async/await programs with solutions →

Playwright CLI Commands: Run, Filter, Debug and Report Tests

Knowing the Playwright command line saves you hours. In this lesson you will learn how to run exactly the tests you want, watch and debug them, control parallelism and retries, generate reports, and turn long commands into simple npm scripts.

📖 What You’ll Learn: running files, folders and single lines, filtering with grep and tags, choosing projects, headed mode, UI mode and the debugger, workers, retries and repeat-each, reporters and traces, codegen and install commands, npm scripts and the VS Code extension.


⌨️ The Basic Command

Every test run starts with:

npx playwright test

npx runs the Playwright version installed in your project. By default this runs all tests, in all projects from your config, headless and in parallel. Everything else in this lesson is an option you add to that command.


📂 Run Specific Files, Folders or Lines

npx playwright test tests/login.spec.ts          # one file
npx playwright test login                         # any file whose path contains "login"
npx playwright test tests/checkout/               # a whole folder
npx playwright test login.spec.ts cart.spec.ts    # several files
npx playwright test tests/login.spec.ts:12        # the test that starts on line 12

🔍 Filter Tests by Name or Tag

npx playwright test -g "locked out user"          # title contains this text (--grep)
npx playwright test --grep "@smoke"               # tests tagged @smoke
npx playwright test --grep-invert "@slow"         # everything except @slow
npx playwright test --grep "@smoke|@regression"   # either tag (regex)

Tags are added in the test title or with the tag option:

test('user can log in', { tag: '@smoke' }, async ({ page }) => {
  // ...
});

Tagging is covered in depth in the lesson on grouping and tagging tests.


🌐 Choose Browsers (Projects)

npx playwright test --project=chromium
npx playwright test --project=chromium --project=firefox
npx playwright test --project="Mobile Chrome"

Project names come from the projects array in playwright.config.ts.


👀 See What Is Happening: Headed, UI Mode and Debug

CommandWhat you get
npx playwright test --headedReal browser windows open while tests run
npx playwright test --uiUI mode: pick tests, watch them, time-travel through each step, re-run on save
npx playwright test --debugPlaywright Inspector: step through the test one action at a time
npx playwright test login.spec.ts:12 --debugDebug one specific test

💡 Tip: UI mode (--ui) is the best way to develop tests. It shows a timeline, DOM snapshots, network calls and console logs for every step, and re-runs tests when you save a file.


⚡ Control Speed, Retries and Failures

npx playwright test --workers=1          # run one test at a time (no parallelism)
npx playwright test --workers=4          # four parallel workers
npx playwright test --retries=2          # retry failed tests twice
npx playwright test -x                   # stop after the first failure
npx playwright test --max-failures=5     # stop after five failures
npx playwright test --timeout=60000      # 60 s per test
npx playwright test --repeat-each=5      # run each test 5 times (find flaky tests)
npx playwright test --last-failed        # re-run only the tests that failed last time

📝 Note: --repeat-each combined with --workers=1 is a quick way to check whether a test is flaky before you push it.


📊 Reports, Traces and Listing Tests

npx playwright test --reporter=list          # one line per test in the terminal
npx playwright test --reporter=dot           # compact output for CI
npx playwright test --reporter=html          # HTML report
npx playwright show-report                   # open the last HTML report
npx playwright test --trace on               # record a trace for every test
npx playwright show-trace test-results/<folder>/trace.zip   # open a trace file
npx playwright test --list                   # list tests without running them

🛠️ Other Useful Playwright Commands

CommandPurpose
npx playwright codegen https://www.saucedemo.comRecord actions and generate test code
npx playwright installDownload or update the browsers
npx playwright install --with-deps chromiumInstall Chromium plus Linux system libraries
npx playwright --versionShow the installed Playwright version
npx playwright test --update-snapshotsUpdate screenshot baselines for visual tests
npx playwright test --shard=1/3Run the first third of the suite (split across CI machines)
npx playwright test --config=api.config.tsUse a different config file
npx playwright test --helpList every option

📦 Save Commands as npm Scripts

Typing long commands is error-prone. Add shortcuts to package.json:

{
  "scripts": {
    "test": "playwright test",
    "test:headed": "playwright test --headed --project=chromium",
    "test:ui": "playwright test --ui",
    "test:smoke": "playwright test --grep @smoke",
    "test:debug": "playwright test --debug",
    "report": "playwright show-report"
  }
}

Now run npm test, npm run test:smoke or npm run report. Inside npm scripts you do not need npx.


🧑‍💻 Running Tests from VS Code

The Playwright Test for VSCode extension gives you the same power with clicks:

  • ▶ next to a test or file runs it; right-click for Debug Test.
  • Show browser runs headed; Show trace viewer opens traces automatically.
  • Select which projects (browsers) to run in the Playwright panel of the Testing view.
  • Pick locator and Record new use the Inspector and Codegen.

Installation is covered in Install Playwright and set up VS Code.


📝 Assignment

Use the login tests you wrote in your first Playwright test:

  1. Run only the “locked out user” test using -g.
  2. Run all login tests only in Firefox, headed.
  3. Open UI mode, run the suite, and use the timeline to inspect the step where the login button is clicked.
  4. Add the tag @smoke to two tests and run only those tests.
  5. Run your suite with --repeat-each=3 --workers=1 and confirm every run passes.
  6. Add test:smoke and report scripts to package.json and use them.

❓ FAQ

🤔 How do I run a specific test in Playwright?

Use the file and line: npx playwright test tests/login.spec.ts:12, or filter by title: npx playwright test -g "test title". In VS Code, click ▶ next to the test.

🤔 How do I run Playwright tests in headed mode?

Add --headed: npx playwright test --headed. Set headless: false in the config to make it the default.

🤔 How do I run Playwright tests in one browser only?

Use --project=chromium (or another project name from your config).

🤔 How do I run tests one by one instead of in parallel?

Use --workers=1. Tests then run sequentially in a single worker process.

🤔 How do I see all Playwright CLI options?

Run npx playwright test --help for test options and npx playwright --help for all commands.

🎬 Watch the full Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

playwright.config.ts Explained: Every Important Playwright Config Option

Every Playwright project is controlled by one file: playwright.config.ts. In this lesson you will learn what each important option does, how projects work, and how to build a clean, production-ready config step by step.

📖 What You’ll Learn: test runner options (testDir, timeout, workers, retries, reporter), the use block (baseURL, headless, viewport, screenshot, video, trace, timeouts), projects for browsers and devices, setup projects, webServer, test.use overrides and a complete annotated config.


📄 What Is playwright.config.ts?

playwright.config.ts is the control centre of your Playwright project. It sits in the project root and tells Playwright where your tests are, how to run them, which browsers to use, and what to record when something fails. Because it is a TypeScript file wrapped in defineConfig(), your editor autocompletes every option and flags typos.

Here is the config the installer generates (comments shortened):

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  use: {
    // baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox',  use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit',   use: { ...devices['Desktop Safari'] } },
  ],
});

The options fall into three groups: test runner options (top level), browser and context options (inside use), and projects (named variations of use). Let’s go through each.


⚙️ Test Runner Options (Top Level)

OptionWhat it doesExample
testDirFolder that contains your tests'./tests'
testMatch / testIgnoreWhich files count as tests (default: *.spec.ts and *.test.ts)'**/*.e2e.ts'
timeoutMax time for one test, default 30 seconds60_000
expect.timeoutMax time for one assertion, default 5 seconds{ timeout: 10_000 }
fullyParallelRun tests inside a file in parallel tootrue
workersNumber of parallel worker processes4 or '50%'
retriesRe-run failed tests this many times2
forbidOnlyFail the run if test.only was left in code (great for CI)!!process.env.CI
maxFailuresStop the run after N failures10
reporterHow results are reported'html', 'list', or an array
outputDirWhere screenshots, videos and traces are saved'test-results'
globalSetup / globalTeardownFiles that run once before / after all tests'./global-setup.ts'

💡 Tip: process.env.CI is set automatically by most CI systems (including GitHub Actions). The default config uses it to apply stricter settings in CI and faster ones locally.


🌐 The use Block: Browser and Context Options

Everything inside use applies to the browser, context and page that each test receives.

OptionWhat it does
baseURLLets you write page.goto('/login') instead of the full URL
headlessfalse shows the browser window (default true)
viewportBrowser window size, e.g. { width: 1280, height: 720 }
screenshot'off', 'on' or 'only-on-failure'
video'off', 'on', 'retain-on-failure' or 'on-first-retry'
trace'off', 'on', 'retain-on-failure' or 'on-first-retry'
actionTimeoutMax time for each click, fill etc. (default: no separate limit)
navigationTimeoutMax time for goto and navigations
ignoreHTTPSErrorsAccept invalid certificates on test environments
locale / timezoneIdTest in another language or time zone, e.g. 'en-IN', 'Asia/Kolkata'
storageStateStart each test already logged in (see save login session)
testIdAttributeCustom attribute for getByTestId, e.g. 'data-test'
launchOptionsLow-level options such as { slowMo: 500 } to slow down actions

A practical use block for most projects:

use: {
  baseURL: process.env.BASE_URL ?? 'https://www.saucedemo.com',
  headless: true,
  viewport: { width: 1366, height: 768 },
  screenshot: 'only-on-failure',
  video: 'retain-on-failure',
  trace: 'on-first-retry',
  actionTimeout: 15_000,
  testIdAttribute: 'data-test',
},

🧭 Projects: Browsers, Devices and Environments

A project is a named group of settings. Every test runs once per project. Projects are how you run cross-browser tests, mobile emulation, or different environments.

projects: [
  { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
  { name: 'firefox',  use: { ...devices['Desktop Firefox'] } },
  { name: 'webkit',   use: { ...devices['Desktop Safari'] } },

  // Branded browsers
  { name: 'Google Chrome',  use: { ...devices['Desktop Chrome'], channel: 'chrome' } },
  { name: 'Microsoft Edge', use: { ...devices['Desktop Edge'], channel: 'msedge' } },

  // Mobile emulation
  { name: 'Mobile Safari', use: { ...devices['iPhone 13'] } },
  { name: 'Mobile Chrome', use: { ...devices['Pixel 5'] } },
],

Run a single project with npx playwright test --project=chromium.

Setup project: log in once for all tests

Projects can depend on each other. A common pattern is a setup project that logs in and saves the session, which other projects reuse:

projects: [
  { name: 'setup', testMatch: /.*\.setup\.ts/ },
  {
    name: 'chromium',
    use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/user.json' },
    dependencies: ['setup'],
  },
],

🚀 webServer: Start Your App Before Testing

If you test your own application locally, Playwright can start it for you and wait until it is ready:

webServer: {
  command: 'npm run start',
  url: 'http://localhost:3000',
  reuseExistingServer: !process.env.CI,
  timeout: 120_000,
},

📋 A Complete, Annotated Config for This Course

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  timeout: 30_000,
  expect: { timeout: 7_000 },
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 2 : undefined,
  reporter: [
    ['list'],                               // readable terminal output
    ['html', { open: 'never' }],            // HTML report in playwright-report/
  ],
  use: {
    baseURL: process.env.BASE_URL ?? 'https://www.saucedemo.com',
    testIdAttribute: 'data-test',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
    trace: 'on-first-retry',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox',  use: { ...devices['Desktop Firefox'] } },
  ],
});

📝 Note: You can keep several config files (for example one for API tests and one for UI tests) and choose one with --config. See multiple config files, and load secrets from a .env file as shown in environment variables with .env.


🎯 Override Config for One File or Test

Settings in the config are defaults. You can override use options for a file or a describe block with test.use():

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

test.use({ viewport: { width: 390, height: 844 }, locale: 'hi-IN' });

test('mobile layout in Hindi locale', async ({ page }) => {
  await page.goto('/');
  // ...
});

And change the timeout of a single test with test.setTimeout(60_000) or test.slow().


📝 Assignment

  1. Set baseURL to https://www.saucedemo.com and update your login test to use page.goto('/').
  2. Change the reporter to show list output in the terminal and generate the HTML report.
  3. Enable screenshots and videos only for failed tests. Break a test on purpose and find the screenshot and video in the HTML report.
  4. Add a Mobile Chrome project using devices['Pixel 5'] and run only that project.
  5. Set retries: 1 locally, make a test fail, and observe the retry and the trace in the report.
  6. Bonus: set testIdAttribute: 'data-test' and replace one locator with page.getByTestId('login-button').

❓ FAQ

🤔 Where is the Playwright config file?

In the project root, named playwright.config.ts (or .js). Playwright finds it automatically; use --config path/to/file.ts to choose another.

🤔 How do I increase the timeout in Playwright?

Set timeout for whole tests and expect: { timeout } for assertions in the config, or call test.setTimeout() in a single test. See Playwright waits for all timeouts.

🤔 How do I run Playwright tests in headed mode by default?

Set use: { headless: false } in the config, or pass --headed on the command line.

🤔 What does workers mean in the Playwright config?

Workers are parallel processes that run tests. workers: 4 runs up to four test files at the same time; undefined lets Playwright use about half of your CPU cores.

🤔 What is the difference between use and projects?

use sets default browser and context options for all tests. Each project adds a name and its own use settings on top, and every test runs once per project.

🎬 Watch the full Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

Write and Run Your First Playwright Test with TypeScript

Now that Playwright is installed, it is time to write real tests. In this lesson you will understand every line of a Playwright test, automate a login page, run it in different ways, read a failure message, and view the HTML report.

📖 What You’ll Learn: test anatomy (test, expect, page fixture, async/await), writing a login test, running it from the CLI and VS Code, reading failure messages, negative tests with describe and beforeEach, the HTML report and baseURL.


🧱 Anatomy of a Playwright Test

Open tests/example.spec.ts from your project. Every Playwright test has the same building blocks:

import { test, expect } from '@playwright/test';      // 1. import test and expect

test('has title', async ({ page }) => {               // 2. test name + async function
  await page.goto('https://playwright.dev/');         // 3. actions use await
  await expect(page).toHaveTitle(/Playwright/);       // 4. assertion
});
  1. Import test (to declare tests) and expect (to make assertions) from @playwright/test.
  2. test(name, function): the name appears in reports, so make it describe the behaviour. The function is async because browser actions take time.
  3. { page } is a fixture: Playwright creates a fresh browser tab for each test and passes it in. You never launch the browser yourself.
  4. await in front of every action and assertion, so each step finishes before the next starts.
  5. expect(…) checks the result. Web-first assertions like toHaveTitle retry automatically until they pass or time out.

✍️ Write Your First Real Test: Login

We will automate the login page of Sauce Demo (https://www.saucedemo.com), a free practice shop. Create a new file tests/login.spec.ts:

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

test('standard user can log in', async ({ page }) => {
  // Arrange: open the login page
  await page.goto('https://www.saucedemo.com/');

  // Act: fill the form and submit
  await page.getByPlaceholder('Username').fill('standard_user');
  await page.getByPlaceholder('Password').fill('secret_sauce');
  await page.getByRole('button', { name: 'Login' }).click();

  // Assert: we are on the products page
  await expect(page).toHaveURL(/inventory\.html/);
  await expect(page.locator('.title')).toHaveText('Products');
});

What each line does:

  • page.goto(url) opens the page and waits for it to load.
  • page.getByPlaceholder('Username') finds the input by its placeholder text. This is a locator; you will master locators in Playwright Locators.
  • fill() clears the input and types the text.
  • getByRole('button', { name: 'Login' }) finds the button the way a user or screen reader sees it.
  • toHaveURL and toHaveText verify the outcome.

💡 Tip: Structure tests as Arrange, Act, Assert: set up, perform the user action, then verify. It keeps tests easy to read.


▶️ Run the Test

Run only this file, in Chromium, with the browser visible:

npx playwright test login.spec.ts --project=chromium --headed

Or run it from VS Code: open the Testing view, tick Show browser, and click ▶ next to the test name. When it passes you see a green tick; in the terminal you see:

Running 1 test using 1 worker
  ✓  1 [chromium] › tests/login.spec.ts:3:5 › standard user can log in (1.9s)
  1 passed (2.6s)

❌ Make It Fail on Purpose (and Read the Error)

Learning to read failures is as important as writing tests. Change the expected text to 'Product' (without the s) and run again. Playwright prints exactly what went wrong:

Error: Timed out 5000ms waiting for expect(locator).toHaveText(expected)

Locator: locator('.title')
Expected string: "Product"
Received string: "Products"

The message shows the locator, what was expected and what was received. Change it back to 'Products' so the test passes again.


🧪 Add a Negative Test and Group Tests

Good test suites cover failures too. Use test.describe to group related tests, and test.beforeEach to share setup:

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

test.describe('Login page', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('https://www.saucedemo.com/');
  });

  test('standard user can log in', async ({ page }) => {
    await page.getByPlaceholder('Username').fill('standard_user');
    await page.getByPlaceholder('Password').fill('secret_sauce');
    await page.getByRole('button', { name: 'Login' }).click();
    await expect(page).toHaveURL(/inventory\.html/);
  });

  test('locked out user sees an error', async ({ page }) => {
    await page.getByPlaceholder('Username').fill('locked_out_user');
    await page.getByPlaceholder('Password').fill('secret_sauce');
    await page.getByRole('button', { name: 'Login' }).click();
    await expect(page.locator('[data-test="error"]'))
      .toContainText('Sorry, this user has been locked out.');
  });

  test('empty form shows username required', async ({ page }) => {
    await page.getByRole('button', { name: 'Login' }).click();
    await expect(page.locator('[data-test="error"]')).toContainText('Username is required');
  });
});

Hooks such as beforeEach get a full lesson later in the course.


📊 View the HTML Report

npx playwright test
npx playwright show-report

The report lists each test per browser, with its steps, duration and, for failures, the error, screenshot and trace (depending on your config). Click a test to see every action that ran.


🌐 Use baseURL to Shorten Your Tests

Instead of repeating the full URL, set baseURL in playwright.config.ts:

// playwright.config.ts (inside defineConfig)
use: {
  baseURL: 'https://www.saucedemo.com',
},

Now await page.goto('/') opens the home page. Learn every config option in playwright.config.ts explained.


✅ Beginner Mistakes to Avoid

  • Forgetting await: the test may finish before the action happens. Always write await before page.* actions and expect for locators.
  • Using sleeps: do not add fixed waits; Playwright auto-waits. See Playwright waits.
  • Vague test names: 'test1' tells you nothing in a report. Describe the behaviour.
  • Tests that depend on each other: each test should work alone, in any order.

📝 Assignment

Use https://www.saucedemo.com for all tasks and put them in tests/login.spec.ts:

  1. Write a test that logs in as problem_user and verifies the products page title.
  2. Write a test that enters a wrong password and checks the error contains Username and password do not match.
  3. Write a test that logs in, adds Sauce Labs Backpack to the cart, and verifies the cart badge shows 1 (page.locator('.shopping_cart_badge')).
  4. Run all tests in Chromium with --headed, then in all browsers, and compare the HTML report.
  5. Bonus: make one test fail on purpose and write down the three pieces of information the error message gave you.

❓ FAQ

🤔 How do I write my first Playwright test?

Create a .spec.ts file in the tests folder, import test and expect from @playwright/test, write test('name', async ({ page }) => { ... }) with actions and assertions, and run npx playwright test.

🤔 Why does every Playwright test use async and await?

Browser actions take time and return Promises. await makes each step finish before the next one runs. Learn more in TypeScript Functions and TypeScript for Playwright.

🤔 How do I run a single Playwright test?

Run npx playwright test login.spec.ts for one file, add -g "test name" to filter by title, or click ▶ in the VS Code Testing view. All options are in Playwright CLI commands.

🤔 Why does my test run three times?

The default config has three projects (Chromium, Firefox, WebKit), so each test runs once per browser. Use --project=chromium to run one browser.

🤔 How do I see the browser while the test runs?

Add --headed to the command, use --ui for UI mode, or tick Show browser in the VS Code extension.

🎬 Watch the full Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

How to Install Playwright with TypeScript and Set Up VS Code (Step by Step)

In this lesson you will set up everything you need to write Playwright tests: Node.js, Visual Studio Code, a new Playwright TypeScript project and the official VS Code extension. It takes about 15 minutes, and at the end you will run your first tests in three browsers.

📖 What You’ll Learn: installing Node.js and VS Code, creating a project with npm init playwright, the generated files, running sample tests, the HTML report, the Playwright VS Code extension, updating Playwright and fixing common installation errors.


✅ Prerequisites

You need only three things:

ToolWhyWhere to get it
Node.js (current LTS)Runs Playwright and installs packages with npmnodejs.org
Visual Studio CodeEditor with the official Playwright extensioncode.visualstudio.com
A terminalTo run commandsBuilt into VS Code (View, Terminal)

Git is optional now but useful later when you push your project to GitHub and run it in CI.


1️⃣ Step 1: Install Node.js

  1. Go to nodejs.org and download the LTS version for your operating system.
  2. Run the installer and keep the default options (npm is installed together with Node.js).
  3. Open a new terminal and check the versions:
node -v
npm -v

Both commands should print a version number, for example v22.x.x and 10.x.x.

⚠️ Common mistake: If you see “node is not recognized” on Windows, close and reopen the terminal (or VS Code) so it picks up the new PATH. If it still fails, reinstall Node.js and keep “Add to PATH” selected.


2️⃣ Step 2: Install Visual Studio Code

Download VS Code from code.visualstudio.com and install it. Then create an empty folder for your project, for example playwright-course, and open it with File, Open Folder.


3️⃣ Step 3: Create a Playwright Project

Open the VS Code terminal (Ctrl + ` or View, Terminal) inside your empty folder and run:

npm init playwright@latest

The installer asks a few questions. Choose these answers for this course:

QuestionAnswer
Do you want to use TypeScript or JavaScript?TypeScript
Where to put your end-to-end tests?tests (default)
Add a GitHub Actions workflow?Yes (useful later; you can also say No)
Install Playwright browsers?Yes

Playwright then installs the @playwright/test package and downloads Chromium, Firefox and WebKit. This can take a few minutes the first time.

📝 Note: On Linux, add the system dependencies with npx playwright install --with-deps. On Windows and macOS the browsers work without extra steps.


📂 Step 4: Understand the Generated Files

playwright-course/
├── .github/workflows/playwright.yml   # CI workflow (if you chose Yes)
├── node_modules/                      # installed packages (never edit)
├── tests/
│   └── example.spec.ts                # sample test
├── .gitignore                         # ignores reports and results
├── package.json                       # project info and dependencies
├── package-lock.json                  # exact installed versions
└── playwright.config.ts               # Playwright settings
  • tests/: all your test files go here. Files ending in .spec.ts or .test.ts are treated as tests.
  • playwright.config.ts: browsers, timeouts, reporters and more. It has its own lesson: playwright.config.ts explained.
  • package.json: lists @playwright/test as a dev dependency.

▶️ Step 5: Run the Sample Tests

npx playwright test

Playwright runs the sample tests in all three browsers, in parallel and headless (no visible window). You should see output similar to:

Running 6 tests using 4 workers
  6 passed (8.2s)

To open last HTML report run:
  npx playwright show-report

Open the HTML report to see each test, browser and step:

npx playwright show-report

🧩 Step 6: Install the Playwright VS Code Extension

  1. Open the Extensions view in VS Code (Ctrl + Shift + X).
  2. Search for Playwright Test for VSCode published by Microsoft and click Install.
  3. Open the Testing view (the flask icon in the left sidebar). Your tests appear in a tree.

With the extension you can:

  • Run or debug a single test with the green ▶ icon next to it.
  • Tick Show browser to watch tests run.
  • Use Pick locator to click an element in the browser and get a locator.
  • Use Record new to generate a test with Codegen.

💡 Tip: You can also create a new project from the extension: open the Command Palette (Ctrl + Shift + P) and run Test: Install Playwright.


🔄 Updating Playwright Later

Playwright releases a new version roughly every month. To update:

npm install -D @playwright/test@latest
npx playwright install

The second command downloads browser builds that match the new version.


🐞 Common Installation Problems

ProblemFix
npx: command not foundNode.js is not installed or the terminal was not restarted
Browsers fail to download behind a company proxySet the HTTPS_PROXY environment variable, then run npx playwright install
“Executable doesn’t exist” errorBrowsers are missing or outdated: run npx playwright install
Missing libraries on LinuxRun npx playwright install --with-deps
PowerShell blocks npx scriptsRun Set-ExecutionPolicy -Scope CurrentUser RemoteSigned once, or use Command Prompt

📝 Assignment

  1. Install Node.js and VS Code, and paste the output of node -v and npm -v into a notes file.
  2. Create a project with npm init playwright@latest using TypeScript.
  3. Run npx playwright test and open the HTML report. How many tests ran, and in which browsers?
  4. Install the VS Code extension and run only one test from the Testing view with Show browser enabled.
  5. Bonus: run npx playwright --version and note which Playwright version you installed.

❓ FAQ

🤔 How do I install Playwright with TypeScript?

Install Node.js, open an empty folder and run npm init playwright@latest, then choose TypeScript. Playwright creates the project, config file and sample tests, and downloads the browsers.

🤔 Do I need to install TypeScript separately?

No. Playwright runs .ts test files directly. Install the typescript package only if you want to run type checks with npx tsc --noEmit, covered in TypeScript for Playwright.

🤔 Where does Playwright install the browsers?

In a shared cache folder: %USERPROFILE%\AppData\Local\ms-playwright on Windows, ~/Library/Caches/ms-playwright on macOS and ~/.cache/ms-playwright on Linux.

🤔 Can I use Google Chrome instead of the bundled Chromium?

Yes. Set channel: 'chrome' (or 'msedge') in a project in the config file. Run npx playwright install chrome if Chrome is not installed.

🤔 How do I install Playwright in an existing project?

Run npm init playwright@latest in the project folder, or install it manually with npm install -D @playwright/test followed by npx playwright install.

🎬 Watch the full Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

What Is Playwright? Features, Architecture and Why Testers Use It

Welcome to Lesson 1 of the free Playwright with TypeScript tutorial. Before writing any code, you will understand what Playwright is, what it can test, how it works under the hood, and why it has become one of the most popular test automation tools.

📖 What You’ll Learn: what Playwright is, what you can automate with it, its key features, the architecture, Browser vs BrowserContext vs Page, Playwright Library vs Playwright Test, supported browsers and languages, and its limitations.


🎭 Playwright in One Sentence

Playwright is a free, open-source framework from Microsoft for automating web browsers and testing web applications end to end. You write code that opens a browser, clicks, types and navigates like a real user, then checks that the application behaves correctly.

It was first released in January 2020 by a Microsoft team that had previously built Puppeteer at Google. Since then it has become one of the fastest-growing test automation tools, and many teams now choose it for new projects instead of Selenium or Cypress.


🧰 What Can You Do with Playwright?

  • End-to-end UI testing: log in, fill forms, add items to a cart, check out, and verify the result.
  • API testing: send GET, POST, PUT and DELETE requests and check responses, in the same framework.
  • Cross-browser testing: run the same test on Chromium (Chrome, Edge), Firefox and WebKit (the engine behind Safari).
  • Mobile web testing: emulate phones and tablets such as iPhone and Pixel (screen size, touch, user agent).
  • Visual testing: compare screenshots to catch unexpected UI changes.
  • Network control: intercept, block or mock requests to test edge cases.
  • Web scraping and automation tasks: generate PDFs, take screenshots and automate repetitive browser work.

⭐ Key Features That Make Playwright Popular

FeatureWhat it means for you
Auto-waitingPlaywright waits for elements to be ready before acting, so you rarely write manual waits
Web-first assertionsexpect(...) checks retry automatically until they pass or time out
Browser contextsEach test gets a fresh, isolated browser profile in milliseconds
Built-in test runnerParallel runs, retries, reports, fixtures and annotations without extra libraries
Multiple languagesJavaScript, TypeScript, Python, Java and .NET
Powerful toolingCodegen (record tests), Inspector, UI mode, Trace Viewer, VS Code extension
Multi-tab and multi-userTest several tabs, windows, users and origins in one test
Free and open sourceApache 2.0 licence, no paid tier required for parallel runs

🏗️ Playwright Architecture Explained Simply

Playwright has three layers:

  1. Your test code (for example a .spec.ts file written in TypeScript).
  2. The Playwright driver, a Node.js process that receives your commands.
  3. The browsers, which the driver controls over a fast, persistent WebSocket connection. For Chromium it uses the Chrome DevTools Protocol; for Firefox and WebKit it uses Playwright’s own patched browser builds that expose similar protocols.

Because the connection stays open, commands do not need a new HTTP request each time, which is one reason Playwright is fast and stable compared with tools based on the classic WebDriver protocol. You can read a full comparison in Playwright vs Selenium vs Cypress.

Browser, Context and Page

These three objects appear in every Playwright test, so learn them now:

  • Browser: a running browser instance such as Chromium.
  • BrowserContext: an isolated session inside the browser, like a fresh incognito window, with its own cookies and storage. Contexts are cheap to create, so every test gets its own.
  • Page: a single tab inside a context. Most of your code uses page.
import { chromium } from 'playwright';

(async () => {
  const browser = await chromium.launch();       // Browser
  const context = await browser.newContext();    // BrowserContext (isolated session)
  const page = await context.newPage();          // Page (a tab)

  await page.goto('https://sqatools.in');
  console.log(await page.title());
  await browser.close();
})();

📝 Note: When you use the Playwright test runner (@playwright/test), it creates the browser, context and page for you and passes page into each test. You will see this in your first test.


📦 Playwright Library vs Playwright Test

Playwright Library (playwright)Playwright Test (@playwright/test)
What it isBrowser automation API onlyFull test framework built on the library
Test runner, assertions, reportsNot includedIncluded
Best forScripts, scraping, custom toolsAutomated testing

In this course we use Playwright Test with TypeScript, the setup recommended by the Playwright team for testing.


🌍 Supported Browsers, Platforms and Languages

  • Browsers: Chromium (also branded Google Chrome and Microsoft Edge), Firefox and WebKit.
  • Operating systems: Windows, macOS and Linux, locally or in CI, headed or headless.
  • Languages: TypeScript, JavaScript, Python, Java and C#/.NET.

⚠️ Limitations to Know

  • Playwright tests web applications. It does not automate native Android or iOS apps (use Appium for those) or desktop apps.
  • WebKit is tested through Playwright’s own build, not the branded Safari application.
  • Some features, such as the full test runner with fixtures, are richest in the TypeScript/JavaScript version.

🗺️ What You Will Learn Next

This lesson is the start of the free Playwright Tutorial with TypeScript. Next you will learn testing fundamentals and compare tools, then install Playwright and write your first test.


📝 Assignment

  1. In your own words, write down the difference between a Browser, a BrowserContext and a Page.
  2. List three features of Playwright that reduce flaky tests, and explain why each helps.
  3. Visit playwright.dev and find the list of supported languages. Which one will you use, and why?
  4. Think of one web application you use daily. Write five user actions you would automate with Playwright.

❓ FAQ

🤔 What is Playwright used for?

Mainly for automated end-to-end testing of web applications across Chromium, Firefox and WebKit. It is also used for API testing, visual testing and general browser automation.

🤔 Is Playwright free?

Yes. Playwright is open source under the Apache 2.0 licence, including parallel execution and all tooling.

🤔 Is Playwright a testing tool or a library?

Both. The playwright package is an automation library, and @playwright/test is a complete testing framework built on top of it.

🤔 Who maintains Playwright?

Microsoft maintains Playwright as an open-source project on GitHub, with frequent releases.

🤔 Is Playwright better than Selenium?

For most new web projects it is faster to set up and less flaky. Selenium still wins for wider language support and some legacy setups. See Playwright vs Selenium vs Cypress.

🎬 Watch the full Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

Playwright: Handle Alerts, Popups, Dropdowns, iFrames, New Tabs and File Uploads

Alerts, popups, dropdowns, iframes and new tabs are where most beginners get stuck when moving to Playwright. This guide shows the right Playwright API for each situation with runnable TypeScript examples you can try on a free practice site.

📖 What You’ll Learn: JavaScript alerts, confirms and prompts, new tabs and popup windows, native and custom dropdowns, iframes and nested frames, file uploads, hover, right-click, drag and drop, and the Selenium equivalents.


🗂️ Quick Reference

SituationPlaywright API
JavaScript alert, confirm, promptpage.on(‘dialog’) / page.once(‘dialog’)
Link opens a new tab or popup windowpage.waitForEvent(‘popup’)
Any new page in the browser contextcontext.waitForEvent(‘page’)
Native select dropdownlocator.selectOption()
Custom (div-based) dropdownclick, then getByRole(‘option’)
Element inside an iframepage.frameLocator() or locator.contentFrame()
File uploadlocator.setInputFiles() or the filechooser event
Mouse hover, right-click, drag and drophover(), click({ button: ‘right’ }), dragTo()

All examples below use the free practice site the-internet.herokuapp.com, so you can copy them and run them yourself. You can also practise on the SQA Tools automation practice pages.


🔔 Handle Alerts, Confirms and Prompts (Dialogs)

JavaScript dialogs (alert(), confirm(), prompt() and beforeunload) block the page. By default Playwright dismisses them automatically, so a confirm returns “Cancel”. To accept a dialog, read its message or type into a prompt, register a handler before the action that opens it.

Accept an alert and read its message

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

test('accept a JS alert', async ({ page }) => {
  await page.goto('https://the-internet.herokuapp.com/javascript_alerts');

  page.once('dialog', async dialog => {
    expect(dialog.type()).toBe('alert');
    expect(dialog.message()).toBe('I am a JS Alert');
    await dialog.accept();
  });

  await page.getByRole('button', { name: 'Click for JS Alert' }).click();
  await expect(page.locator('#result')).toHaveText('You successfully clicked an alert');
});

Accept or dismiss a confirm

test('dismiss a confirm', async ({ page }) => {
  await page.goto('https://the-internet.herokuapp.com/javascript_alerts');

  page.once('dialog', dialog => dialog.dismiss()); // click "Cancel"
  await page.getByRole('button', { name: 'Click for JS Confirm' }).click();
  await expect(page.locator('#result')).toHaveText('You clicked: Cancel');
});

Type into a prompt

test('answer a prompt', async ({ page }) => {
  await page.goto('https://the-internet.herokuapp.com/javascript_alerts');

  page.once('dialog', dialog => dialog.accept('Playwright'));
  await page.getByRole('button', { name: 'Click for JS Prompt' }).click();
  await expect(page.locator('#result')).toHaveText('You entered: Playwright');
});

⚠️ Common mistake: Use page.once for a single dialog. With page.on the handler stays active for the rest of the test, and a handler that neither accepts nor dismisses the dialog will make the next action hang.

📝 Note: Website modals built with HTML (cookie banners, “subscribe” overlays) are not dialogs. Handle them like normal elements: await page.getByRole('button', { name: 'Accept' }).click().


🪟 Handle a New Tab or Popup Window

When a link opens a new tab (target="_blank") or window.open() creates a popup, wait for the popup event. Start waiting before the click, then work with the new page object.

test('work with a new tab', async ({ page }) => {
  await page.goto('https://the-internet.herokuapp.com/windows');

  const popupPromise = page.waitForEvent('popup');      // 1. start listening
  await page.getByRole('link', { name: 'Click Here' }).click(); // 2. open it
  const newTab = await popupPromise;                    // 3. get the new page

  await newTab.waitForLoadState();
  await expect(newTab.locator('h3')).toHaveText('New Window');

  // Switch back: just use the original page object again
  await expect(page.locator('h3')).toHaveText('Opening a new window');
  await newTab.close();
});

There is no “switch to window” step like in Selenium: each tab is a separate Page object, and you simply call methods on the one you need.

Track any new page in the context

const pagePromise = page.context().waitForEvent('page');
await page.getByRole('link', { name: 'Open report' }).click();
const reportPage = await pagePromise;

// List every open tab
for (const p of page.context().pages()) {
  console.log(await p.title());
}

🔽 Select from a Dropdown

Native select elements

For a real <select> element, use selectOption(). You can select by value, label or index, and select several options in a multi-select.

test('select from a dropdown', async ({ page }) => {
  await page.goto('https://the-internet.herokuapp.com/dropdown');
  const dropdown = page.locator('#dropdown');

  await dropdown.selectOption('1');                    // by value
  await expect(dropdown).toHaveValue('1');

  await dropdown.selectOption({ label: 'Option 2' });  // by visible text
  await expect(dropdown).toHaveValue('2');

  await dropdown.selectOption({ index: 1 });           // by position
});

// Multi-select: pass an array
// await page.locator('#colors').selectOption(['red', 'blue']);

Read all options to verify the list:

const options = await page.locator('#dropdown option').allTextContents();
expect(options).toEqual(['Please select an option', 'Option 1', 'Option 2']);

Custom dropdowns (div or React components)

Many modern apps draw dropdowns with div and li elements. selectOption() does not work on them. Click to open, then click the option, preferably by role.

await page.getByRole('combobox', { name: 'Country' }).click();
await page.getByRole('option', { name: 'India' }).click();
await expect(page.getByRole('combobox', { name: 'Country' })).toHaveText(/India/);

🖼️ Handle iFrames

Elements inside an <iframe> belong to a different document, so normal page locators cannot see them. Use frameLocator() to reach inside. It auto-waits like any locator, and there is no “switch back” step.

// Single iframe, e.g. an embedded payment form
const payment = page.frameLocator('iframe[title="Secure payment"]');
await payment.getByLabel('Card number').fill('4242 4242 4242 4242');
await payment.getByRole('button', { name: 'Pay' }).click();

Nested frames

test('read text from nested frames', async ({ page }) => {
  await page.goto('https://the-internet.herokuapp.com/nested_frames');

  const middle = page
    .frameLocator('frame[name="frame-top"]')
    .frameLocator('frame[name="frame-middle"]');

  await expect(middle.locator('#content')).toHaveText('MIDDLE');
});

From a locator to its frame

In recent Playwright versions you can also go from an iframe element to its content with contentFrame():

const frame = page.locator('#editor-frame').contentFrame();
await frame.locator('body').fill('Hello from Playwright');

💡 Tip: Need the full Frame object (for example to run evaluate)? Use page.frame({ name: 'frame-name' }) or page.frame({ url: /checkout/ }).


📤 Upload Files

For a normal <input type="file">, call setInputFiles(). It works even when the input is hidden.

import path from 'path';

test('upload a file', async ({ page }) => {
  await page.goto('https://the-internet.herokuapp.com/upload');

  await page.locator('#file-upload').setInputFiles(path.join(__dirname, 'data', 'sample.txt'));
  await page.locator('#file-submit').click();

  await expect(page.locator('#uploaded-files')).toHaveText('sample.txt');
});

// Multiple files:  setInputFiles(['a.png', 'b.png'])
// Clear the input: setInputFiles([])

If the page opens the file picker from a custom button, wait for the filechooser event:

const chooserPromise = page.waitForEvent('filechooser');
await page.getByRole('button', { name: 'Upload resume' }).click();
const chooser = await chooserPromise;
await chooser.setFiles('data/resume.pdf');

For downloads, see how to perform file downloads in Playwright.


🖱️ Mouse Hover, Right-Click, Double-Click and Drag and Drop

// Hover to reveal a hidden menu or caption
await page.goto('https://the-internet.herokuapp.com/hovers');
await page.locator('.figure').first().hover();
await expect(page.getByText('name: user1')).toBeVisible();

// Right-click and double-click
await page.locator('#hot-spot').click({ button: 'right' });
await page.getByText('Edit').dblclick();

// Drag and drop
await page.goto('https://the-internet.herokuapp.com/drag_and_drop');
await page.locator('#column-a').dragTo(page.locator('#column-b'));
await expect(page.locator('#column-a header')).toHaveText('B');

// Keyboard
await page.getByLabel('Search').press('Enter');
await page.keyboard.press('Control+A');

🔄 Selenium vs Playwright for These Scenarios

TaskSeleniumPlaywright
Alertdriver.switchTo().alert().accept()page.once(‘dialog’, d => d.accept())
New windowgetWindowHandles() then switchTo().window()page.waitForEvent(‘popup’)
Dropdownnew Select(el).selectByVisibleText()selectOption({ label })
iFrameswitchTo().frame() then defaultContent()page.frameLocator()
Hovernew Actions(driver).moveToElement(el)locator.hover()

Compare with our Python Selenium guides for alerts, dropdowns and iframes, or read the full Playwright vs Selenium vs Cypress comparison.


✅ Best Practices

  • Register dialog handlers and start waitForEvent promises before the click that triggers them.
  • Prefer page.once over page.on for one-off dialogs.
  • Keep a separate Page variable for each tab; close tabs you no longer need.
  • Use frameLocator with a stable selector (title, name or id) for the iframe.
  • Use role-based locators for custom dropdowns, and selectOption only for native selects.

❓ FAQ

🤔 How do I handle a popup in Playwright?

For a new tab or window, use const popup = await page.waitForEvent('popup') (start it before the click). For a JavaScript alert, use page.once('dialog', d => d.accept()). For an HTML modal, click its buttons like any other element.

🤔 Why does Playwright close my alert automatically?

Playwright dismisses dialogs by default so tests never hang. Add a dialog handler to accept it, read the message or enter prompt text.

🤔 How do I select a value from a dropdown in Playwright?

Use await page.locator('select#id').selectOption({ label: 'Option text' }) for native selects. For custom dropdowns, click the control and then page.getByRole('option', { name: '...' }).click().

🤔 How do I switch to an iframe in Playwright?

You do not switch. Create a frame locator, page.frameLocator('iframe#id'), and use locators on it. The main page stays available through page.

🤔 How do I switch between tabs in Playwright?

Every tab is its own Page object. Keep references (for example page and newTab) and call methods on the one you need; context.pages() lists all open tabs.


🎯 Next Steps

🎬 Watch the full Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

Playwright Waits Explained: Auto-Waiting, waitFor, waitForLoadState, waitForResponse and Timeouts

Flaky tests are almost always waiting problems. Playwright solves most of them with auto-waiting, but you still need to know when and how to wait for elements, pages, URLs and API responses. This guide explains every Playwright wait method with TypeScript examples and clear advice on which one to use.

📖 What You’ll Learn: auto-waiting and actionability checks, web-first assertions, locator.waitFor, waitForLoadState, waitForURL, waitForResponse, why to avoid waitForTimeout and networkidle, waitForFunction, expect.poll, toPass, timeouts and troubleshooting.


🤖 Auto-Waiting: Why You Rarely Need Manual Waits

The biggest difference between Playwright and older tools is auto-waiting. Before every action, Playwright waits until the element is ready, so you do not write sleeps or explicit waits for normal clicks and typing.

Before click(), for example, Playwright checks that the element is:

  • Attached to the page (it exists in the DOM)
  • Visible (not hidden, has a size)
  • Stable (not moving or animating)
  • Receives events (not covered by another element such as a loader)
  • Enabled (not disabled)

fill() additionally waits until the field is editable. If the checks do not pass within the timeout, the action fails with a clear error explaining which check failed.

// No waits needed: Playwright waits for the button to be ready
await page.getByRole('button', { name: 'Login' }).click();
await page.getByLabel('Email').fill('qa@sqatools.in');

💡 Tip: Most “wait” problems in Playwright disappear once you use locators (page.getByRole, page.locator) and web-first assertions (await expect(...)). Reach for the methods below only for special cases.


✅ Web-First Assertions: The Best Way to Wait

Assertions such as toBeVisible(), toHaveText() and toHaveURL() retry automatically until the condition is true or the timeout (5 seconds by default) expires. They are the cleanest way to wait for a result.

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

test('order confirmation appears', async ({ page }) => {
  await page.goto('https://www.saucedemo.com/');
  // ...log in and place an order...

  // Waits up to 5 s (default) for the element and the text
  await expect(page.getByText('Thank you for your order!')).toBeVisible();

  // Custom timeout for a slow step
  await expect(page.locator('.complete-header')).toHaveText('Thank you for your order!', { timeout: 15_000 });

  // Wait for an element to disappear
  await expect(page.locator('.loading-spinner')).toBeHidden();
});

⚠️ Common mistake: Do not write expect(await locator.isVisible()).toBe(true). isVisible() checks once and does not wait. Use await expect(locator).toBeVisible() instead.


👁️ Wait for an Element to Be Visible: locator.waitFor()

When you need to wait for an element without asserting (for example inside a helper), use locator.waitFor() with a state.

const toast = page.locator('.toast-message');

await toast.waitFor();                       // default state: 'visible'
await toast.waitFor({ state: 'hidden' });    // wait until it disappears
await toast.waitFor({ state: 'attached' });  // exists in the DOM (may be hidden)
await toast.waitFor({ state: 'detached', timeout: 10_000 }); // removed from the DOM

🌐 Wait for the Page to Load: waitForLoadState()

page.goto() already waits for the load event by default. Use waitForLoadState() when the page loads after an action, such as a click that triggers a navigation, or when you need a different state.

StateMeaning
loadThe load event fired (all resources such as images loaded). Default for goto.
domcontentloadedHTML parsed; images and styles may still be loading.
networkidleNo network requests for at least 500 ms. Discouraged.
await page.goto('https://sqatools.in', { waitUntil: 'domcontentloaded' });

await page.getByRole('link', { name: 'Blog' }).click();
await page.waitForLoadState('domcontentloaded');

⚠️ Common mistake: networkidle is a common way to “wait for network idle”, but Playwright discourages it: pages with analytics, polling or live chat may never become idle, which makes tests slow or flaky. Wait for the specific element or response you care about instead.


🔗 Wait for a URL: waitForURL()

After a click that navigates, wait for the new URL. waitForNavigation() is deprecated; use waitForURL() or the toHaveURL() assertion.

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

await page.waitForURL('**/inventory.html');   // glob pattern
await page.waitForURL(/inventory/);           // regular expression

// Or as an assertion (also retries)
await expect(page).toHaveURL(/inventory/);

📡 Wait for an API Response: waitForResponse()

Many modern pages load data with API calls. Wait for the exact response, then check its data. Start waiting before the action that triggers the request, otherwise you can miss a fast response.

// 1. Start listening first (no await yet)
const responsePromise = page.waitForResponse(resp =>
  resp.url().includes('/api/products') && resp.status() === 200
);

// 2. Trigger the request
await page.getByRole('button', { name: 'Search' }).click();

// 3. Now await the response
const response = await responsePromise;
const data = await response.json();
expect(data.items.length).toBeGreaterThan(0);

The same pattern works for page.waitForRequest() and other events, such as page.waitForEvent('download') and page.waitForEvent('popup').


⏱️ Wait for 5 Seconds: waitForTimeout() (and Why to Avoid It)

You can pause a test for a fixed time:

await page.waitForTimeout(5000); // waits 5 seconds

This is a hard wait. It makes tests slower when the app is fast and still flaky when the app is slower than expected. Playwright’s documentation says it should be used only for debugging. Replace it with a condition:

Instead ofUse
waitForTimeout after a clickexpect(locator).toBeVisible()
waitForTimeout for a spinnerexpect(spinner).toBeHidden()
waitForTimeout for data to loadpage.waitForResponse(…)
waitForTimeout for navigationpage.waitForURL(…)

🔁 Custom Conditions: waitForFunction, expect.poll and toPass

page.waitForFunction()

Runs a function in the browser repeatedly until it returns a truthy value.

await page.waitForFunction(() => document.querySelectorAll('.inventory_item').length >= 6);

expect.poll()

Retries any Node.js function, for example an API call, until the assertion passes.

await expect.poll(async () => {
  const res = await page.request.get('https://api.example.com/jobs/42');
  return (await res.json()).status;
}, { timeout: 30_000, intervals: [1_000, 2_000, 5_000] }).toBe('COMPLETED');

expect(…).toPass()

Retries a whole block of steps until all of them pass.

await expect(async () => {
  await page.getByRole('button', { name: 'Refresh' }).click();
  await expect(page.getByText('Report ready')).toBeVisible({ timeout: 1_000 });
}).toPass({ timeout: 20_000 });

⚙️ Timeouts: Defaults and How to Change Them

TimeoutDefaultWhere to change
Test timeout30 secondstimeout in config, test.setTimeout()
Assertion (expect) timeout5 secondsexpect: { timeout } in config, or per assertion
Action timeout (click, fill)No limit (bounded by the test timeout)use: { actionTimeout }
Navigation timeout (goto, waitForURL)No limit (bounded by the test timeout)use: { navigationTimeout }
// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  timeout: 60_000,               // each test may run 60 s
  expect: { timeout: 10_000 },   // each assertion waits up to 10 s
  use: {
    actionTimeout: 15_000,       // each click/fill waits up to 15 s
    navigationTimeout: 30_000,   // each goto waits up to 30 s
  },
});
// For one slow test
test('generate yearly report', async ({ page }) => {
  test.slow();               // triples the timeout
  // or: test.setTimeout(120_000);
});

Learn more about config in maintaining multiple config files.


🧹 Legacy Methods You May See in Old Code

  • page.waitForSelector(): still works, but the docs recommend locators with waitFor() or assertions.
  • page.waitForNavigation(): deprecated; use waitForURL().
  • page.waitForTimeout(): only for debugging.

🐞 Troubleshooting Timeout Errors

  • “Timeout 30000ms exceeded” on an action: the element never became actionable. Run with npx playwright test --ui or open the trace to see whether it was hidden, covered or disabled.
  • Element is covered by another element: wait for the overlay or spinner to disappear with expect(overlay).toBeHidden().
  • Strict mode violation: your locator matched several elements. Make it more specific; see Playwright locators.
  • Test passes locally but fails in CI: CI machines are slower. Prefer condition-based waits and enable trace: 'on-first-retry' to inspect failures.

✅ Best Practices

  • Trust auto-waiting; do not add waits before normal actions.
  • Use web-first assertions as your main waiting tool.
  • Start waitForResponse and waitForEvent promises before the triggering action.
  • Avoid waitForTimeout and networkidle in real tests.
  • Increase timeouts for specific slow steps instead of globally.

❓ FAQ

🤔 How do I wait for an element to be visible in Playwright?

Use await expect(locator).toBeVisible() in tests, or await locator.waitFor({ state: 'visible' }) in helper code. Both wait automatically up to the timeout.

🤔 How do I wait for a page to load in Playwright?

page.goto() waits for the load event by default. After a click, use page.waitForURL() or page.waitForLoadState(), or simply assert on an element of the new page.

🤔 How do I wait for 5 seconds in Playwright?

await page.waitForTimeout(5000). Use it only while debugging; in real tests wait for a condition instead.

🤔 Does Playwright have implicit waits like Selenium?

Not as a setting. Playwright’s auto-waiting is applied automatically to every action and web-first assertion, which replaces implicit waits. Compare with Selenium waits.

🤔 What is the default timeout in Playwright?

30 seconds per test and 5 seconds per expect assertion. Actions and navigations have no separate limit by default, so they are bounded by the test timeout.


🎯 Next Steps

🎬 Watch the full Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in