Playwright 1.63 Release: Test Locks, Cross-Frame Locators, and Aria Traces
Playwright 1.63 adds test locks, cross-frame locators, visible-only locators, step params, and aria traces. What changed and how to upgrade.

Playwright can now put a name tag on a test that says "only one of us at a time", and the test runner honors it across every file and machine. That is the headline of Playwright 1.63, which shipped on September 4, 2026, about 6 weeks after 1.62.
The official notes list 5 headline features and more than a dozen smaller ones, but they are written for people who already live in the changelog. It is easy to skim them, upgrade, and only notice the useful parts months later.
This guide walks through every change in the playwright 1.63 release notes in plain language, with the problem it solves and code you can paste into your suite. If you skipped the last version, the playwright 1.62 release guide covers isolated retries and AbortSignal, which several 1.63 features build on.
What's new in Playwright 1.63 at a glance
Playwright 1.63 is the September 2026 release of Microsoft's Playwright test framework. It adds named test locks, cross-frame locators, a visible-only locator method, structured step data for reporters, and aria plus screen snapshots inside traces. It ships Chromium 153, Firefox 155, and WebKit 26.6, and drops support for Ubuntu 20.04.
If you only have 2 minutes, the table below covers everything. Each row links to a section further down where the feature gets a problem statement, a code sample, and the caveats the release notes leave out.
| Feature | What it does | Where it helps |
|---|---|---|
| Test locks | Tests that share a lock name never run at the same time, across files, workers, and projects | Shared accounts, external services, global settings |
| Locate across frames | frameLocator() with no selector searches every frame on the page | Payment widgets, embedded editors, third-party iframes |
| Visible-only locators | locator.visible() matches only visible elements, replacing :visible | Menus, tabs, and duplicated buttons in the DOM |
| Step params and subtitles | test.step() carries structured data that reporters and the trace viewer show | Custom reporters, HTML report, debugging |
| Aria and screen snapshots | Traces can capture DOM, aria, and screenshot snapshots per action | Trace viewer, accessibility debugging |
| Perfetto reporter | Built-in timeline export with a lane per worker | Finding slow workers and long fixtures |
Alongside the big 5, there are new APIs for HTTP credentials, dialog events, typed API responses, reporter flags, and installation. There is also one deprecation that affects anyone still on the experimental component testing packages.
Nothing in this list changes existing test code. Every feature is additive, so the upgrade risk sits entirely in your CI image and your component testing setup, and both get their own section below. The place to start is the feature the Playwright team put first, because it fixes a problem that appears the moment 2 tests share a resource.

1. Test locks: one test at a time on shared resources
Parallel workers are the reason Playwright suites finish fast. They are also the reason 2 tests that both edit the same admin setting will eventually fail each other, and the "playwright test locks" feature exists to end that fight.
The problem
Most tests are isolated by design, since each worker gets its own browser context, cookies, and storage. The trouble starts with state that lives outside the browser: a shared staging account, a rate-limited third-party API, or a global feature flag.
Until now the fixes were blunt. You could run the whole file in serial mode, drop the worker count to 1, or move the risky tests into a separate project that runs last. All 3 slow down tests that never needed slowing down.
What Playwright 1.63 does
A test can now declare a lock with a name. Tests that share that name never run concurrently, and the guarantee holds across files, worker processes, and projects. Every other test keeps running in parallel exactly as before.
import { test, expect } from '@playwright/test';
test('update notification settings', { lock: 'user-settings' }, async ({ page }) => {
await page.goto('/settings');
await page.getByLabel('Email alerts').check();
await expect(page.getByText('Saved')).toBeVisible();
});
import { test, expect } from '@playwright/test';
// Never overlaps with 'update notification settings', even in a different project.
test('rename the account', { lock: 'user-settings' }, async ({ page }) => {
await page.goto('/profile');
await page.getByLabel('Display name').fill('QA Bot');
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByText('Profile updated')).toBeVisible();
});
A test can hold several locks at once, and it only starts when all of them are free. A test.describe block also accepts a lock, which applies it to every test inside the group.
import { test } from '@playwright/test';
test('reset the seed data', { lock: ['database', 'external-api'] }, async ({ request }) => {
await request.post('/api/test/reset');
});
test.describe('billing admin', { lock: 'billing' }, () => {
test('change the plan', async ({ page }) => { /* ... */ });
test('add a payment method', async ({ page }) => { /* ... */ });
});
Playwright acquires every lock before the test starts and releases them when it finishes. Because locks are named strings, there is nothing to configure in the config file, and nothing to import.

Locks compared with the old workarounds
The docs make one subtle point that is easy to miss. In the default and serial modes, all tests in a file run together in order, so a lock declared on any single test is held for the whole file. If you want fine-grained locking, the file needs fullyParallel or a parallel describe.
| Approach | What slows down | Scope | Best for |
|---|---|---|---|
| lock: 'name' | Only tests sharing the name | Across files, workers, and projects | A few tests touching one shared resource |
| mode: 'serial' | Every test in the file or group | One file or describe | Tests that depend on each other's order |
| workers: 1 | The entire suite | Whole run | Debugging, not CI |
Compared with the playwright parallel execution tricks teams used before, a lock is the only option that costs you nothing for the tests that were never in conflict.
Tip: Reach for a lock only when the resource truly cannot be duplicated. If the conflict is a database row, the docs recommend deriving a unique id from testInfo.testId so parallel tests never collide. That keeps the whole suite parallel and needs no lock at all.
Why it matters
Order-dependent collisions are a stubborn source of flaky tests, because the failure depends on which worker got there first and rarely repeats on a local run. A lock turns that timing lottery into a deterministic rule that lives next to the test.
If you already hold shared state in playwright fixtures, locks compose with them cleanly, since the lock is acquired before any fixture for the test is set up. With scheduling handled, the next 2 features change how you find elements in the first place.
2. Locate across frames without naming the iframe
Iframes have always been the awkward corner of playwright locators. You could not simply ask for a button, because the button lived inside a frame that you had to enter first.
The problem
Payment providers, chat widgets, and embedded editors all render inside iframes, often with ids that change between deploys. Your test needed a selector for the frame before it could write a selector for the element, so every frame change broke 2 things.
What 1.63 does
Calling page.frameLocator() or frame.frameLocator() with no selector now searches in any frame of the subtree, including the main frame. The rest of the locator chain resolves inside a single frame, exactly like a normal locator.
import { test, expect } from '@playwright/test';
test('pay inside the provider iframe', async ({ page }) => {
await page.goto('/checkout');
// Before 1.63: you had to know the iframe first.
// await page.frameLocator('#payment-frame').getByRole('button', { name: 'Pay now' }).click();
// With 1.63: search every frame, then act in the one that matches.
await page.frameLocator().getByRole('button', { name: 'Pay now' }).click();
await expect(page.getByText('Payment complete')).toBeVisible();
});
Note: Strictness still applies. If the locator matches elements in more than 1 frame, Playwright throws instead of guessing. Narrow the locator with a name, a role, or a parent before you fall back to naming the frame again.
This is a good moment to retire hardcoded frame ids from page objects. Search once across frames, and keep the explicit frame selector only for pages where the same widget appears twice. The same "be precise about what you mean" idea drives the next locator change.
3. Visible-only locators with locator.visible()
Many pages render the same control twice: a desktop menu and a mobile menu, or a hidden template and its visible clone. Clicking the first match hits the hidden one, and the test fails with a confusing timeout.
The problem
The old answer was the :visible CSS pseudo-class, as in page.locator('button:visible'). It worked, but it only fit CSS selectors, so it could not be combined with getByRole or getByText, and it is one of the playwright mistakes that hides in older suites.
What 1.63 does
The new locator.visible() returns a locator that only matches visible elements. The docs call it the recommended replacement for :visible, and it chains onto any locator, including the semantic ones.
import { test, expect } from '@playwright/test';
test('open the visible navigation menu', async ({ page }) => {
await page.goto('/');
// Before: CSS only, so it could not chain onto getByRole.
// await page.locator('button:visible').click();
// With 1.63: works on any locator, re-checked every time it is used.
await page.getByRole('button', { name: 'Menu' }).visible().click();
await expect(page.getByRole('navigation').visible()).toBeVisible();
});
One detail from the API reference matters here. Visibility is evaluated every time the locator is used, not at the moment you call visible(), so the locator stays lazy like every other Playwright locator.
Tip: If you are unsure how many elements a locator matches before and after adding visible(), paste your markup into the Locator Playground on TestDino's free tools page. It shows live match counts for getByRole, getByText, getByLabel, CSS, and XPath.
Pair this with web-first playwright assertions and many "which button did it click?" failures disappear. Once the right element is found, the next question is how clearly the report explains what happened, which is what the step changes address.
4. Step params and subtitles for richer reports
Steps have always been the unit that reports and traces are organized around. In Playwright 1.63 they carry structured data, so a report can show what a step acted on, not just what it was called.
The problem
A step titled "Login" tells a reviewer nothing about which user, which region, or which locator was involved. Teams stuffed that detail into the title string, which made titles long and impossible to group in a playwright custom reporter.
What 1.63 does
Two changes land together. Playwright's own API steps now report the target locator and call arguments, and test.step() accepts subtitle and params options for your own steps.
import { test, expect } from '@playwright/test';
test('admin can reach the dashboard', async ({ page }) => {
await test.step('Login', async () => {
await page.goto('/login');
await page.getByLabel('Email').fill('[email protected]');
await page.getByLabel('Password').fill(process.env.ADMIN_PASSWORD!);
await page.getByRole('button', { name: 'Sign in' }).click();
}, { subtitle: 'as admin', params: { user: 'admin', region: 'eu' } });
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});
Reporters read the data through testStep.subtitle and testStep.params. For Playwright's built-in steps, the subtitle is the locator or the navigation URL, so a step called "Click" now shows getByRole('button') next to it in the trace viewer and the HTML report.
import type { Reporter, TestCase, TestResult, TestStep } from '@playwright/test/reporter';
class StepLogger implements Reporter {
onStepEnd(test: TestCase, result: TestResult, step: TestStep) {
if (step.category !== 'test.step') return;
const params = JSON.stringify(step.params ?? {});
console.log(`${step.title} ${step.subtitle ?? ''} ${params} ${step.duration}ms`);
}
}
export default StepLogger;

Why it matters
Analytics tools group failures by step, and structured params make that grouping accurate. A failure in "Login as admin, region eu" is a different bug from "Login as viewer, region us", and now the data says so without parsing strings.
The same release also gives the playwright html reporter a duration waterfall next to test steps, so slow steps stand out visually. Steps describe what the test did, while the next feature shows what the page looked like while it did it.
5. Aria and screen snapshots inside traces
The trace viewer already recorded a DOM snapshot for every action. Playwright 1.63 lets you add 2 more capture types, and it adds a viewer mode that puts them side by side.
The problem
DOM snapshots show structure, but they do not show what an assistive technology, or an AI agent, would perceive. When a getByRole locator fails, you want the accessibility tree at that exact moment, and until now that meant reproducing the failure locally.
What 1.63 does
The snapshots option of tracing.start() and the trace fixture option now accept an object that selects what to capture on every action. Passing true is still a shortcut for { dom: true }.
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
trace: {
mode: 'retain-on-failure',
snapshots: { dom: true, aria: true, screen: true },
},
},
});
With aria and screen snapshots recorded, the new Display Aria mode in the playwright trace viewer shows the action screenshot next to the aria snapshot. Hovering an aria node highlights the matching region on the screenshot.

Aria snapshots as JSON
The same release adds locator.ariaSnapshotJSON() and page.ariaSnapshotJSON(). They return the same tree as the YAML aria snapshot, but as a JSON value you can walk in code, with mode, depth, and boxes options.
import { test, expect } from '@playwright/test';
test('primary navigation exposes every section', async ({ page }) => {
await page.goto('/');
const tree = await page.getByRole('navigation').ariaSnapshotJSON({ depth: 2, boxes: true });
// Each node has role, name, children, state flags, and a box when boxes: true.
const links = tree[0].children.filter((node: any) => node.role === 'link');
expect(links.map((l: any) => l.name)).toEqual(['Docs', 'Pricing', 'Blog']);
});
Setting mode: 'ai' returns the reference-annotated form that the playwright mcp server uses, including element refs and iframe contents. That makes the accessibility tree a first-class data structure in tests, not just a debugging view.
Note: Screen snapshots add a screenshot per action, so trace files grow. Keep them behind retain-on-failure in CI, and use the Trace Configurator on TestDino's free tools page to estimate artifact size before you turn everything on.
Those are the 5 headline features. The rest of the release is a long tail of small APIs, and several of them remove real friction.
9 smaller additions worth knowing
Each item below is 1 to 3 lines of code. They are grouped by the part of the API they touch.
Browser and context
Credentials for HTTP authentication can now be an array. The first entry whose origin matches the request is used, and entries without an origin match any request, which fixes playwright authentication setups that span 2 protected hosts.
use: {
httpCredentials: [
{ username: 'qa', password: process.env.STAGING_PASS!, origin: 'https://staging.example.com' },
{ username: 'api', password: process.env.API_PASS!, origin: 'https://api.example.com' },
],
},
Storage state gained an opfs option that includes the origin private file system, so apps that cache data there can be saved once and restored later. The docs note it is not yet supported in ephemeral WebKit contexts.
await context.storageState({ path: 'auth.json', opfs: true });
New dialogclosed events fire on the page and the context when a JavaScript dialog is accepted, dismissed, or closed by the user, which is handy for asserting that a confirm box actually went away.
page.on('dialogclosed', dialog => console.log('dialog closed:', dialog.type(), dialog.message()));
Locators and API requests
Request methods accept a type argument that types the parsed body, a small win for playwright api testing in TypeScript.
type User = { id: number; email: string };
const response = await request.get<User>('/api/users/42');
const user = await response.json(); // typed as User
Test runner and reporters
The accessibility emulation options reducedMotion, forcedColors, and contrast are now standalone test options, so a project for playwright accessibility checks can set them directly.
use: { reducedMotion: 'reduce', forcedColors: 'active', contrast: 'more' },
A new --add-reporter flag appends a reporter on top of the ones in your config, instead of replacing them the way --reporter does. An omitTags option for the list, line, dot, github, and junit reporters stops tags from being appended to titles.
npx playwright test --add-reporter=perfetto
The built-in perfetto reporter writes a Trace Event Format file, by default at test-results/perfetto.json, which opens in the Perfetto UI or chrome://tracing as a timeline with a lane per worker. Every test, hook, fixture, and step is a nested slice, so playwright slow tests that hide in setup show up as long bars.
npx playwright test --reporter=perfetto
Command line
Two flags round things out. npx playwright install --no-remove keeps browsers from other Playwright installations, and npx playwright codegen --http-credentials records against pages behind HTTP auth.
npx playwright install --no-remove
npx playwright codegen --http-credentials=qa:secret https://staging.example.com
That is the full feature list. What remains is the part that decides whether the upgrade takes 5 minutes or an afternoon.
Should you upgrade to Playwright 1.63?
Yes, for almost every team. No existing test API changed, and the 3 announcements in the release all concern infrastructure or an experimental package.
Breaking changes and announcements
Ubuntu 20.04 is no longer supported, so a CI image on Focal needs to move to 22.04 or later before upgrading. On Linux arm64, Playwright now downloads the Chrome for Testing build of Chromium, the same build every other platform already used.
The bigger item is component testing. The experimental @playwright/experimental-ct-react, ct-react17, and ct-vue packages will no longer be updated. The docs include a migration guide to the stories model introduced in 1.62, and story ids passed to mount() can now be typed through a generated registry.
| Browser | Playwright 1.62 | Playwright 1.63 |
|---|---|---|
| Chromium | 151.0.7922.34 | 153.0.8010.12 |
| Mozilla Firefox | 153.0 | 155.0 |
| WebKit | 26.5 | 26.6 |
The release was also tested against Google Chrome 153 and Microsoft Edge 153, so the branded channels line up with the bundled Chromium.
Why staying current is cheaper than it looks
Playwright ships roughly every 6 weeks, and the gap between versions is where surprises accumulate. Adoption keeps climbing too, and the npm numbers below show how steep that curve has become over the last year.

Source: npm registry downloads API (api.npmjs.org/downloads/range) for the playwright package, daily counts summed per calendar month, retrieved September 8, 2026. September 2026 is excluded as a partial month.
The upgrade itself is 2 commands. Pin the exact version so every machine in playwright in github actions or any other CI installs the same browsers.
npm install -D @playwright/[email protected]
npx playwright install --with-deps
If your config has grown over several versions, generate a fresh one with the Config Generator on TestDino's free tools page and diff it against yours. Options that no longer make sense stand out immediately.
Teams that use AI assistants can also point them at the playwright-skill repository. It keeps the assistant's knowledge of current APIs like lock and visible() in sync with the release.

After upgrading, the highest-value change to make first is turning on aria snapshots for failed traces, since it costs almost nothing and pays off on the very next flaky failure. Locks come second, and only where a shared resource already causes collisions. That leaves one question, which is what all of this adds up to.
Conclusion
Playwright 1.63 is a release about precision. A lock names the tests that must not overlap. A cross-frame locator finds an element without caring which frame renders it, and visible() picks the copy a user can actually see.
The reporting side got the same treatment. Steps carry structured params, traces can capture the accessibility tree per action, and the perfetto reporter lays the whole run out on a timeline. None of it requires rewriting a test, which is the best kind of release to adopt.
Upgrade this week if your CI image is already on Ubuntu 22.04 or newer and you are not on the experimental component packages. Then fold the new APIs into your playwright best practices one at a time, starting with aria snapshots on failure.
FAQs

Pratik Patel
Co-founder



