Playwright Testing With TypeScript
Use when asked to write, explain, debug, or extend Playwright browser automation code in TypeScript — locating elements, performing actions, waiting, and writing real assertions with @playwright/test.
What Playwright is
Playwright (https://playwright.dev/) is a browser automation library that drives Chromium, Firefox, and WebKit through one API. The playwright npm package ships its own TypeScript type definitions — Browser, BrowserContext, Page, Locator, and more — so there is no separate @types/playwright package to install.
Install commands:
# Library only, for scripting/automation:
npm install playwright
# Full TypeScript test-runner scaffold (recommended for real tests):
npm init playwright@latest
npm init playwright@latest scaffolds a project in TypeScript by default — no extra typing setup is needed, because @playwright/test is itself written in and for TypeScript.
The four core concepts, for Playwright specifically
Any browser automation code — in any tool — comes down to four things: locating an element, acting on it, waiting for it to be ready, and asserting on it. Playwright's typed locator API is built around doing all four safely.
Locating
page.locator(...) returns a Locator — a typed, lazy handle to element(s) matching a selector. It does not query the DOM immediately; it re-queries every time you act on it. Playwright supports several selector strategies:
page.locator('#id-example-1'); // by id
page.locator('[name="name-example-1"]'); // by attribute
page.locator('.class-example-1'); // by class
page.locator('a', { hasText: 'Link Example 1' }); // by visible text
page.locator('xpath=//input[@type="submit"]'); // by XPath
Acting
Once you have a Locator, call an action method: .click(), .fill(value), .check(), .uncheck(), .selectOption(...), and so on. These are async methods returning Promise<void> (or a value, for reads like .inputValue()), so they must be awaited.
Waiting — auto-waiting and auto-retry
This is Playwright's headline advantage over Selenium: locators auto-wait and auto-retry until the element is actionable. Before performing a click, fill, check, or similar action, Playwright automatically waits for the element to be attached to the DOM, visible, stable (not animating), and enabled — retrying the underlying check internally until a timeout elapses. You do not write manual polling loops, explicit waits, or sleeps for this. You only reach for an explicit wait (page.waitForSelector, locator.waitFor(), etc.) for conditions outside the standard actionability checks.
Asserting
Assertions are the difference between a walkthrough and a real test — see "From walkthrough to real test" below.
How the types help
Because page.locator(...) is typed to return Locator, and Page/Browser/BrowserContext are all real interfaces, TypeScript catches whole classes of mistakes at compile time that plain JavaScript would only surface at runtime (or silently get wrong):
- Calling a method that doesn't exist on
LocatororPage(typo, or confusing an element handle with a locator) is a compile error. - Passing the wrong argument shape to
.selectOption(...)or.evaluate(...)is a compile error. - The callback passed to
.evaluate((el: Element) => ...)is typed, so you get autocomplete and type-checking on the DOM element itself.
A full worked example
This is real, working code (adapted from https://github.com/testingexamples/demo-playwright-typescript), run with ts-node against the free fixture page https://testingexamples.github.io:
#!/usr/bin/env ts-node
import { chromium, Browser, BrowserContext, Page, Locator, ChromiumBrowser } from 'playwright';
async function demo(): Promise<void> {
const browser: ChromiumBrowser = await chromium.launch({ headless: false });
const context: BrowserContext = await browser.newContext();
const page: Page = await context.newPage();
try {
await page.goto("https://testingexamples.github.io");
// Find an element by id.
const elementById: Locator = page.locator('#id-example-1');
console.log(await elementById.evaluate((el: Element): string => el.outerHTML));
// Find an element by name attribute.
const elementByName: Locator = page.locator('[name="name-example-1"]');
console.log(await elementByName.evaluate((el: Element): string => el.outerHTML));
// Find an element by class name.
const elementByClassName: Locator = page.locator('.class-example-1');
console.log(await elementByClassName.evaluate((el: Element): string => el.outerHTML));
// Find a link element by its visible text.
const elementByLinkText: Locator = page.locator('a', { hasText: 'Link Example 1' });
console.log(await elementByLinkText.evaluate((el: Element): string => el.outerHTML));
// Find an element by XPath.
const elementByXPath: Locator = page.locator('xpath=//input[@type="submit"]');
console.log(await elementByXPath.evaluate((el: Element): string => el.outerHTML));
// Fill a text input.
const text: Locator = page.locator('#text-example-1-id');
await text.fill("hello");
// Check a checkbox.
const checkbox: Locator = page.locator('#checkbox-example-1-id');
await checkbox.check();
// Check a radio button.
const radio: Locator = page.locator('#radio-example-1-option-1-id');
await radio.check();
// Select an option by index.
const selectElement: Locator = page.locator('#select-example-1-id');
await selectElement.selectOption({ index: 0 });
const selectedValue: string = await selectElement.inputValue();
console.log(`Selected option value: ${selectedValue}`);
} catch (err: unknown) {
if (err instanceof Error) {
console.log(err.message);
}
} finally {
await browser.close();
}
}
demo().catch((err: Error): void => {
console.error(err);
process.exit(1);
});
Note what this script does: it locates elements, acts on them, and logs what it finds. It does not check that anything is correct — it has no assertions. That makes it a walkthrough, not a test.
From walkthrough to real test
The script above is a walkthrough: it exercises the API and prints results for a human to read. It will "pass" (exit 0) even if #id-example-1 has the wrong text, because nothing checks the text — it's just logged.
A real test uses the @playwright/test test runner and its expect() — with web-first assertions that auto-retry until the condition is true or a timeout elapses:
import { test, expect } from '@playwright/test';
test('id example has expected text', async ({ page }) => {
await page.goto('https://testingexamples.github.io');
await expect(page.locator('#id-example-1')).toHaveText('Id Example 1');
});
Why this matters:
test()andexpect()come from@playwright/test, which is written in and for TypeScript — no extra typing setup, no separate assertion library to configure.expect(locator).toHaveText(...)(and siblings like.toBeVisible(),.toHaveValue(),.toHaveCount()) are web-first assertions: Playwright re-checks the condition against the live page repeatedly until it passes or the timeout is hit. You never write a manual polling loop orsleepto wait for text to appear — the assertion itself retries.@playwright/testalso gives you fixtures (the{ page }argument is injected and torn down automatically — no manualbrowser.close()bookkeeping) and parallel execution across test files out of the box.- If the assertion never becomes true, the test fails with a clear diff (expected vs. actual), unlike a walkthrough script that would just log the wrong value and exit successfully.
Rule of thumb: reach for a plain script like the worked example above only for exploration or one-off scraping. For anything you want to keep running as a check of correctness (CI, regression protection), write it as an @playwright/test test with real expect(...) assertions.
Common pitfalls
- Forgetting
await. TypeScript's type checker will not flag a missingawaiton its own —page.locator(...).click()withoutawaitcompiles fine and returns an unhandledPromise<void>, causing the action to fire-and-forget and the script to race ahead. The@typescript-eslint/no-floating-promiseslint rule catches this; the compiler alone does not. - Confusing execution paths.
tsc(compile only),ts-node(compile-and-run a script directly), and the@playwright/testrunner's own built-in TypeScript transform are three different ways to execute.tsfiles, with different module resolution and config defaults. Know which one you're invoking — a script that runs fine underts-nodeis not automatically whatnpx playwright testwill run, and vice versa. - Strict-mode locator violations. By default, Playwright locators are strict: if a selector matches more than one element, an action on it (like
.click()) throws, rather than silently acting on the first match. Narrow the selector, or use.first()/.nth()/.filter()explicitly when multiple matches are expected. - Over-typing. Playwright infers most types already —
chromium.launch()already returnsPromise<Browser>(or the more specificChromiumBrowser), andpage.locator(...)already returnsLocator. Explicit annotations, as used throughout the worked example above, are there for clarity and teaching; they are not required for the code to type-check correctly.
Learn more / real examples
- https://github.com/testingexamples/demo-playwright-typescript — locator-strategy walkthrough against https://testingexamples.github.io (the generic fixture target).
- https://github.com/testingexamples/demo-playwright-typescript-for-google-search — same patterns applied to Google Search. Illustrative only: Google's Terms of Service restrict automated querying of Google Search, so this repo is meant to show tool syntax, not to be run repeatedly against the live site.
- https://github.com/testingexamples/demo-playwright-typescript-for-google-maps — same patterns applied to Google Maps. Illustrative only, for the same reason: Google's Terms of Service restrict automated querying of Google Maps.
- https://github.com/testingexamples/demo-playwright-typescript-for-nhs-wales — a real test suite with real assertions against https://www.nhs.wales/.
- https://playwright.dev/docs/intro — official Playwright documentation.
- https://testingexamples.github.io/ — the free fixture page these demos target; safe to run against repeatedly.
---
AGENTS.md and spec/index.md in this repo are the source of truth for this skill's own scope — if this file ever disagrees with those, they win.