JavaScript for Playwright Automation

Playwright interviews for JavaScript or TypeScript roles rarely stop at "what is a locator?". Interviewers want to know whether you understand the JavaScript underneath — promises, async/await, scope — because that's where most flaky tests come from. This page answers the questions that come up most, and ends with a real test run that shows the difference between code that waits and code that doesn't.

Advertisement

Why Is Playwright Built on async/await?

Almost everything a browser does takes time: navigation, network requests, rendering, animations. Playwright talks to the browser over a connection, so every action — goto, click, fill, reading text — returns a Promise that settles when the browser has responded.

  • async marks a function that returns a Promise and may use await inside.
  • await pauses that function until the Promise settles, so steps run in order.
  • JavaScript is single-threaded; while one test waits, Node can do other work, which is what makes parallel workers and fast I/O possible.

Interview answer: "Browser actions are asynchronous, so every Playwright call returns a promise. I use async test functions and await each step so actions happen in order, and I rely on Playwright's auto-waiting and web-first assertions instead of fixed sleeps."

Which JavaScript Mistakes Cause Flaky Tests?

Mistake What happens Fix
Missing await The next line runs before the action finishes; an assertion may compare a Promise instead of a value Await every Playwright call; enable the @typescript-eslint/no-floating-promises lint rule
Reading state once — count(), isVisible(), textContent() — then asserting These return the value right now; if the page is still loading the test fails (see the run below) Use web-first assertions: toHaveCount, toBeVisible, toHaveText
forEach with an async callback forEach doesn't wait; all iterations start at once for...of with await
var as a loop counter in async code Every callback sees the final value let or const
Fixed waits (waitForTimeout(3000)) Too short on a slow run, wasted time on a fast one Wait for a condition: an element state, a URL, a response
Shared mutable data between tests Tests pass alone and fail in parallel Create data per test; use fixtures

The loop and variable traps are demonstrated with real output in Loops & Conditions and var, let & const.

How Do You Handle Waits in Playwright?

Mostly, you don't write them. Playwright waits automatically in two places:

  • Actions (click, fill, check…) wait until the element is attached, visible, stable, enabled and able to receive events.
  • Web-first assertions (await expect(locator).toHaveText(...) and friends) keep retrying until they pass or the timeout expires.

For everything else, wait for the thing you actually need:

await page.waitForURL('**/dashboard');                                  // navigation finished
const res = await page.waitForResponse(r => r.url().includes('/api/orders') && r.ok());
await expect(page.getByRole('status')).toHaveText('Saved');            // UI updated

Start waiting for a response before the action that triggers it (store the promise, click, then await it), or a fast response can arrive before you start listening.

See It Run: count() vs toHaveCount()

A test page loads three orders 800 ms after a button is clicked — like a slow API. We ran three tests against it with Playwright 1.63 in Chromium:

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

const url = 'file://' + path.resolve(__dirname, 'app.html');

test('reading the count immediately (flaky pattern)', async ({ page }) => {
  await page.goto(url);
  await page.click('#load');
  const count = await page.locator('#orders li').count();    // count() does not wait
  expect(count).toBe(3);
});

test('web-first assertion waits for the list', async ({ page }) => {
  await page.goto(url);
  await page.click('#load');
  await expect(page.locator('#orders li')).toHaveCount(3);   // retries until 3 or timeout
  await expect(page.locator('#orders li').first()).toHaveText('A-101');
});

test('isVisible() does not wait either', async ({ page }) => {
  await page.goto(url);
  await page.click('#load');
  const visible = await page.locator('#orders li').first().isVisible();
  console.log('isVisible() right after click:', visible);
  await expect(page.locator('#orders li').first()).toBeVisible();
});

Result

  ✘  1 › reading the count immediately (flaky pattern) (453ms)
  ✓  2 › web-first assertion waits for the list (1.2s)
isVisible() right after click: false
  ✓  3 › isVisible() does not wait either (1.0s)

    Error: expect(received).toBe(expected)
    Expected: 3
    Received: 0

  1 failed
  2 passed (4.9s)
  • Test 1 read the list once, got 0, and failed — on a faster server it might pass, which is exactly what makes this pattern flaky.

  • Test 2 used toHaveCount(3), which kept checking until the list appeared about 800 ms later.

  • Test 3 shows isVisible() returning false right after the click, while toBeVisible() on the next line waited and passed. Use isVisible() only to decide something optional, never to assert.

Notice all three tests were fully awaited — the flakiness came from what was awaited, not from a missing await.

How Do You Keep Playwright Tests Stable?

  • User-facing locators: getByRole, getByLabel, getByTestId rather than long CSS or XPath chains.
  • Web-first assertions for anything that changes over time.
  • Independent tests: each creates its own data (via API where possible) and uses a fresh browser context.
  • Network control where it helps: page.route() to mock slow or unstable third-party APIs.
  • Traces on retry (trace: 'on-first-retry') so every flaky failure can be investigated in the Playwright Trace Viewer.
  • Retries as a safety net, with flaky tests tracked and fixed — not hidden.

Quick-Fire Questions

What does a Playwright test function receive?

Fixtures, destructured from the first argument — { page }, { request }, { context } or your own custom fixtures.

What's the difference between page.locator() and page.$()?

A locator is lazy and re-finds the element every time it's used, with auto-waiting; page.$() is an older API that returns an element handle once. Prefer locators.

How do you run two independent API calls in parallel in a test?

const [users, orders] = await Promise.all([request.get('/users'), request.get('/orders')]); — but keep actions on one page sequential.

How do you handle errors?

Let assertions fail the test with a clear message. Use try/catch only for genuinely optional steps (a banner that may not appear), and never to swallow a failure.

Practise these questions — with follow-ups — under interview conditions in the Playwright Interview Simulator. More on waiting: Playwright Auto-Wait & Synchronization.

From Real Projects

I've built my automation skills on Selenium with Java and TestNG, and that background maps well onto Playwright: page classes become page objects, TestNG groups and parallel runs become projects and workers, and explicit waits become auto-waiting and web-first assertions. If you're coming from Selenium too, focus on those differences first. Testing Testsigma — a platform where users write automated tests in plain English — meant thinking like its users, who are testers themselves. The most common flaky pattern is reading a value once instead of asserting with a web-first assertion.

📚 Official documentation: Playwright documentation · MDN: JavaScript Guide

FAQs

Why does Playwright use async/await?

Browser actions are asynchronous and return promises; await keeps test steps in order without blocking Node.

What happens if you forget await in Playwright?

The action may not have finished when the next line runs, leading to failures or assertions against a Promise instead of a value.

Why is count() flaky in assertions?

It returns the count at that instant. expect(locator).toHaveCount(n) retries until the count matches.

Do you need explicit waits in Playwright?

Rarely. Actions and web-first assertions auto-wait; for other cases wait for a URL, response or element state — not a fixed time.

Can you use JavaScript instead of TypeScript with Playwright?

Yes. TypeScript adds type checking that catches mistakes such as unawaited promises earlier, which is why many teams prefer it.