Pramod Dutta, an SDET from Tekion, posted on LinkedIn "7 Playwright features senior SDETs use daily". The Playwright features, according to Pramod:
"→ addLocatorHandler — auto-dismiss cookie banners, GDPR popups, session modals. One handler. Whole suite cleaner. Stop writing wrapper functions.
"→ browser.bind() — launch one browser. Let your test, Claude Code, and debugger all attach to it simultaneously. The debugging workflow that fixes "works on my machine" forever.
"→ URLSearchParams in request.get() — stop building query strings manually. Eliminates an entire bug class around URL encoding in API tests.
"→ expect.toPass() — polling-based assertions for state that converges over time. The right tool for 'this should eventually be true.' Not the same as expect(locator).toBeVisible(). Different problem, different solution.
"→ page.requestGC() — manually trigger garbage collection in the browser. The only Playwright tool that catches memory leaks before production. Used with WeakRef in page.evaluate().
"→ --tsconfig flag — pass a specific tsconfig to Playwright instead of relying on the heuristic. Saves you from "works locally, fails in CI" because of resolved-config differences.
"→ webServer.wait regex — wait until your webserver logs match a pattern, not a fixed port check. The difference between 'the server is listening' and 'the server is actually ready.'"
I scribbled these features down, then turned them over to Claude.ai, my research assistant, to see if he could explain these methods, come up with sample code using them, then add bullet points to the official documentation, source code, and the release notes.
... Let's see how well Claude did explaining them ...
Global configuration in
TypeScript Config:
Example
Happy Testing! "→ browser.bind() — launch one browser. Let your test, Claude Code, and debugger all attach to it simultaneously. The debugging workflow that fixes "works on my machine" forever.
"→ URLSearchParams in request.get() — stop building query strings manually. Eliminates an entire bug class around URL encoding in API tests.
"→ expect.toPass() — polling-based assertions for state that converges over time. The right tool for 'this should eventually be true.' Not the same as expect(locator).toBeVisible(). Different problem, different solution.
"→ page.requestGC() — manually trigger garbage collection in the browser. The only Playwright tool that catches memory leaks before production. Used with WeakRef in page.evaluate().
"→ --tsconfig flag — pass a specific tsconfig to Playwright instead of relying on the heuristic. Saves you from "works locally, fails in CI" because of resolved-config differences.
"→ webServer.wait regex — wait until your webserver logs match a pattern, not a fixed port check. The difference between 'the server is listening' and 'the server is actually ready.'"
I scribbled these features down, then turned them over to Claude.ai, my research assistant, to see if he could explain these methods, come up with sample code using them, then add bullet points to the official documentation, source code, and the release notes.
... Let's see how well Claude did explaining them ...
page.addLocatorHandler()
v1.42 — March 2024
Registers a callback that fires whenever a specified locator becomes visible
and risks blocking a Playwright action. Playwright checks for the locator
before every actionability check or auto-waiting assertion. When the locator
is visible, the handler runs first, then the test continues. The handler only
fires during actions; appearing passively between actions does not trigger it.
After the handler runs, Playwright waits for the overlay to become hidden
before proceeding (opt out with
noWaitAfter: true).
Use the times option to cap the number of invocations.
The handler receives the triggering locator as its argument.
Cookie banner (fire-and-forget)
// Register once, before navigating.
await page.addLocatorHandler(
page.getByRole('heading', { name: 'You are in control of your cookies.' }),
async () => {
await page.getByRole('button', { name: 'Accept all' }).click();
}
);
// Write the rest of the test normally.
await page.goto('https://www.example.com/');
await page.getByRole('link', { name: 'Shop now' }).click();
Self-removing handler with access to the triggering locator
// The handler receives the matched locator; times: 1 auto-removes it.
const closeBtn = page.getByLabel('Close');
await page.addLocatorHandler(closeBtn, async (locator) => {
await locator.click();
}, { times: 1 });
// Remove manually when no longer needed.
await page.removeLocatorHandler(closeBtn);
Fail fast on an application error overlay
// Instead of dismissing, throw immediately so the test does not hang.
await page.addLocatorHandler(
page.getByRole('heading', { name: "Sorry, there's been a problem" }),
async () => {
throw new Error(`Error overlay appeared on ${await page.url()}`);
}
);
| Resource | Link |
|---|---|
| Official docs | playwright.dev — page.addLocatorHandler() |
| Release notes (v1.42) | playwright.dev/docs/release-notes#version-142 |
| GitHub release (v1.42.0) | github.com — Release v1.42.0 |
| Source code | packages/playwright-core/src/client/page.ts |
browser.bind()
v1.59 — April 2026
Binds a launched browser to a named pipe or WebSocket endpoint, making it
available to
playwright-cli, @playwright/mcp, and any
other Playwright client simultaneously. Before v1.59, sharing a browser between
an MCP agent and a test suite meant two separate processes. Now a single
browser.bind() call exposes one browser session to all of them.
Call browser.unbind() to stop accepting new connections; existing
connections keep working. Run playwright-cli show to open the live
dashboard listing all bound browsers.
Bind via named pipe (local only)
import { chromium } from '@playwright/test';
const browser = await chromium.launch();
const { endpoint } = await browser.bind('my-session', {
workspaceDir: '/my/project',
});
console.log('Session bound:', endpoint);
Bind via WebSocket (shareable over a network)
const { endpoint: wsEndpoint } = await browser.bind('my-session', {
host: 'localhost',
port: 0, // let the OS pick an available port
});
console.log('WebSocket endpoint:', wsEndpoint); // ws://localhost:PORT/...
// A second Playwright client connects to the same browser.
const browser2 = await chromium.connect(wsEndpoint);
const page = await browser2.newPage();
// Stop accepting new connections when done.
await browser.unbind('my-session');
| Resource | Link |
|---|---|
| Official docs | playwright.dev — browser.bind() |
| Release notes (v1.59) | playwright.dev/docs/release-notes#version-159 |
| GitHub release (v1.59.0) | github.com — Release v1.59.0 |
| Source code | packages/playwright-core/src/client/browser.ts |
URLSearchParams in request.get()
v1.46 — August 2024
The
params option on APIRequestContext methods
(get, post, put, patch,
delete) now accepts a native URLSearchParams object
or a plain query string in addition to the existing plain-object shorthand.
This matters when you need to send the same key more than once, which a plain
object cannot express.
test('query params via URLSearchParams', async ({ request }) => {
// URLSearchParams lets you append duplicate keys.
const params = new URLSearchParams();
params.set('userId', '1');
params.append('tag', 'automation');
params.append('tag', 'playwright'); // duplicate key — plain object cannot do this
const response = await request.get(
'https://jsonplaceholder.typicode.com/posts',
{ params }
);
expect(response.status()).toBe(200);
// Or just pass a pre-built query string.
const response2 = await request.get(
'https://jsonplaceholder.typicode.com/posts',
{ params: 'userId=1&tag=automation' }
);
expect(response2.ok()).toBeTruthy();
});
| Resource | Link |
|---|---|
| Official docs | playwright.dev — APIRequestContext.get() |
| Release notes (v1.46) | playwright.dev/docs/release-notes#version-146 |
| GitHub release (v1.46.0) | github.com — Release v1.46.0 |
| Source code | packages/playwright-core/src/client/fetch.ts |
expect.toPass()
intervals config — v1.44 May 2024
Wraps an
async callback and retries it until every assertion
inside passes or a timeout is reached. Unlike Playwright's built-in
auto-retrying locator assertions, toPass can contain arbitrary
logic including API requests, multi-step flows, and non-locator checks.
By default the timeout is 0 (no timeout) and probe intervals
default to [100, 250, 500, 1000] ms. Both are configurable
globally in testConfig.expect.toPass or per-call.
Polling an endpoint until ready
test('waits for background job', async ({ request }) => {
await expect(async () => {
const res = await request.get('https://api.example.com/job/status');
expect(res.status()).toBe(200);
const body = await res.json();
expect(body.status).toBe('complete');
}).toPass({
intervals: [1_000, 2_000, 10_000], // probe at 1 s, 2 s, then every 10 s
timeout: 60_000,
});
});
Global configuration in playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
expect: {
toPass: {
timeout: 30_000,
intervals: [500, 1_000, 5_000],
},
},
});
| Resource | Link |
|---|---|
| Official docs | playwright.dev — Assertions: expect.toPass() |
| Release notes (v1.44) | playwright.dev/docs/release-notes#version-144 |
| GitHub release (v1.44.0) | github.com — Release v1.44.0 |
| Source code | packages/playwright/src/matchers/toPass.ts |
page.requestGC()
v1.46 — August 2024
Asks the browser to run garbage collection on the page. There is no guarantee
that every unreachable object is collected, but in practice it is reliable
enough to detect leaks in combination with JavaScript
WeakRef. The pattern: store a WeakRef to the object
under suspicion, call requestGC(), then assert that
deref() returns undefined.
test('no memory leak on the large object', async ({ page }) => {
await page.goto('https://your-app.com');
// 1. Pin a WeakRef to the object under suspicion.
await page.evaluate(() => {
(globalThis as any).suspectWeakRef = new WeakRef((window as any).largeObject);
});
// 2. Request GC.
await page.requestGC();
// 3. A living WeakRef means a leak; deref() should now be undefined.
const leaked = await page.evaluate(
() => !!(globalThis as any).suspectWeakRef.deref()
);
expect(leaked).toBe(false);
});
| Resource | Link |
|---|---|
| Official docs | playwright.dev — page.requestGC() |
| Release notes (v1.46) | playwright.dev/docs/release-notes#version-146 |
| GitHub release (v1.46.0) | github.com — Release v1.46.0 |
| Source code | packages/playwright-core/src/client/page.ts |
TypeScript Config: --tsconfig flag & testConfig.tsconfig
Playwright looks up the nearest
tsconfig.json for each imported
file by default. Two complementary options override this. The
--tsconfig CLI flag
(v1.46, August 2024)
applies one tsconfig to all imported files from the command line. The
testConfig.tsconfig property
(v1.49, October 2024)
does the same from the config file. Note that Playwright only honours four
tsconfig options: allowJs, baseUrl,
paths, and references.
CLI flag
# Apply one tsconfig to everything Playwright loads.
npx playwright test --tsconfig tsconfig.test.json
Config file property
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
tsconfig: './tsconfig.test.json',
});
Example tests/tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"allowJs": true,
"paths": {
"@helpers/*": ["../src/helpers/*"]
}
}
}
| Resource | Link |
|---|---|
| TypeScript guide | playwright.dev — TypeScript |
testConfig.tsconfig docs |
playwright.dev — TestConfig.tsconfig |
Release notes (--tsconfig, v1.46) |
playwright.dev/docs/release-notes#version-146 |
Release notes (testConfig.tsconfig, v1.49) |
playwright.dev/docs/release-notes#version-149 |
| GitHub release (v1.46.0) | github.com — Release v1.46.0 |
| GitHub release (v1.49.0) | github.com — Release v1.49.0 |
| Source code | packages/playwright/src/transform/transform.ts |
webServer.wait Regex
v1.59 — April 2026
The
webServer block in playwright.config.ts previously
waited for a URL to respond with a 2xx–4xx status. The new
wait.stdout and wait.stderr sub-options accept regular
expressions and watch the server process output instead. This is useful for dev
servers that print a readiness message rather than exposing an HTTP health
endpoint. Named capture groups in the regex are automatically exported as
environment variables, so a varying port can be passed directly to
test.use({ baseURL }).
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
webServer: {
command: 'npm run start',
wait: {
// Named group "my_server_port" is stored in process.env.MY_SERVER_PORT
stdout: /Listening on port (?<my_server_port>\d+)/,
},
},
});
// In a test file — consume the captured port.
import { test } from '@playwright/test';
test.use({
baseURL: `http://localhost:${process.env.MY_SERVER_PORT ?? 3000}`,
});
test('homepage loads', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle(/My App/);
});
| Resource | Link |
|---|---|
| Web server guide | playwright.dev — Web Server |
testConfig.webServer docs |
playwright.dev — TestConfig.webServer |
| Release notes (v1.59) | playwright.dev/docs/release-notes#version-159 |
| GitHub release (v1.59.0) | github.com — Release v1.59.0 |
| Source code | packages/playwright/src/plugins/webServerPlugin.ts |
Further Reading
-T.J. Maher
Software Engineer in Test
BlueSky | YouTube | LinkedIn | Articles
No comments:
Post a Comment