May 11, 2026

Pramod Dutta discusses 7 Playwright features senior SDETs use daily

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 ...

 


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()}`);
  }
);

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');

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();
});

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],
    },
  },
});

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);
});

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/*"]
    }
  }
}

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/);
});

Further Reading


Happy Testing!

-T.J. Maher
Software Engineer in Test

BlueSky | YouTubeLinkedIn | Articles

No comments:

Post a Comment