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.
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.
asyncmarks a function that returns a Promise and may useawaitinside.awaitpauses 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()returningfalseright after the click, whiletoBeVisible()on the next line waited and passed. UseisVisible()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,getByTestIdrather 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.