Skip to main content
Back to Blog
Guide
2026-09-24

Playwright 1.63 New Features: Test Locks, Cross-Frame Locators, and Aria Traces

Playwright 1.63 new features explained for QA teams: test locks, frame locators, visible locators, richer traces, and reliable CI upgrade payoffs.

Playwright 1.63 New Features: Test Locks, Cross-Frame Locators, and Aria Traces

Playwright 1.63 is a high-leverage release for QA teams that run large suites in parallel, debug accessibility-heavy failures, or maintain tests across embedded iframes. The headline Playwright 1.63 new features are named test locks, selectorless cross-frame frameLocator(), locator.visible(), structured step subtitles and params, aria and screen snapshots in traces, a built-in perfetto reporter, and browser updates to Chromium 153.0.8010.12, Firefox 155.0, and WebKit 26.6.

The practical payoff is not just new syntax. Test locks give teams a first-class way to serialize access to shared state without turning an entire project serial. Cross-frame locators reduce brittle iframe plumbing. Aria traces make failures easier to explain to accessibility reviewers and AI coding agents because the trace can show the rendered screenshot next to the accessibility snapshot. If your suite already uses parallel projects, shared tenant accounts, iframe-heavy widgets, or trace-first debugging, this release is worth planning rather than casually bumping.

The official sources for this guide are the Playwright release notes at https://playwright.dev/docs/release-notes and the GitHub release at https://github.com/microsoft/playwright/releases/tag/v1.63.0. The release was published on GitHub on September 4, 2026. Features below are attributed to 1.63 only when the 1.63 notes list them.

Release Map for QA Leads

The fastest way to read Playwright 1.63 is as a coordination and observability release. It does not ask you to rewrite your suite, but it gives you cleaner primitives for the problems mature suites eventually hit: shared resources, iframe ambiguity, hidden duplicate elements, sparse trace context, and hard-to-read performance timelines.

AreaNew in 1.63Best first userUpgrade risk
Parallel executionNamed lock on tests and describesSuites that share accounts, queues, feature flags, or admin stateMedium if you overuse locks and flatten parallelism
Framespage.frameLocator() and frame.frameLocator() without a selectorApps with payment, auth, analytics, or embedded admin framesMedium when the target appears in more than one frame
Locatorslocator.visible()Pages with hidden templates, menu portals, duplicated buttonsLow, but strict mode still matters
Reportingtest.step() accepts subtitle and paramsTeams using custom reporters, trace review, or AI triageLow, unless params include secrets
Tracingsnapshots: { dom, aria, screen }Accessibility and design-system QAMedium because traces can get larger
Runner output--add-reporter, reporter omitTags, HTML step waterfall, perfetto reporterCI owners and flake investigatorsLow
PlatformUbuntu 20.04 no longer supportedDocker and self-hosted runner ownersHigh if images are pinned to 20.04

If you recently adopted Playwright 1.61 features, treat 1.63 as the next layer rather than a competing direction. Passkeys, storage, and video improvements from 1.61 make tests richer; 1.63 makes the suite easier to coordinate and inspect when that richness creates parallel pressure.

Test Locks Replace Ad Hoc Serialization

Test locks are the most operationally important Playwright 1.63 new feature. A test can declare { lock: "name" }, and tests with the same lock name never run concurrently across files, workers, and projects. Everything that does not hold the same lock remains parallel. test.describe() can also accept a lock for a whole group, and a test can hold multiple locks.

Before 1.63, teams often solved shared-resource conflicts by making a file serial, reducing workers, splitting projects by hand, or moving fragile tests into a nightly job. Those workarounds make one noisy test safer by slowing a much larger surface area. Locks let you serialize only the resource that is actually shared.

import { test, expect } from "@playwright/test";

test("admin can rotate the shared banner", { lock: "marketing-banner" }, async ({ page }) => {
  await page.goto("/admin/banner");
  await page.getByLabel("Message").fill("Maintenance window at 22:00 UTC");
  await page.getByRole("button", { name: "Publish" }).click();
  await expect(page.getByRole("status")).toHaveText("Published");
});

test("support can update account limits", { lock: "tenant-42" }, async ({ page }) => {
  await page.goto("/admin/accounts/tenant-42");
  await page.getByLabel("Seat limit").fill("25");
  await page.getByRole("button", { name: "Save" }).click();
  await expect(page.getByText("Seat limit updated")).toBeVisible();
});

A lock name should describe the resource, not the test. tenant-42, stripe-sandbox-account, global-feature-flags, and email-inbox-qa age better than billing-tests because unrelated tests can share a resource without pretending they belong to the same domain.

For describe-level coordination, use the smallest scope that owns the conflict:

import { test, expect } from "@playwright/test";

test.describe("shared notification preferences", { lock: "notification-settings-user" }, () => {
  test("can enable product email", async ({ page }) => {
    await page.goto("/settings/notifications");
    await page.getByLabel("Product updates").check();
    await expect(page.getByRole("status")).toHaveText("Preferences saved");
  });

  test("can disable weekly digest", async ({ page }) => {
    await page.goto("/settings/notifications");
    await page.getByLabel("Weekly digest").uncheck();
    await expect(page.getByRole("status")).toHaveText("Preferences saved");
  });
});

What people get wrong: they treat locks as a replacement for isolation. A lock prevents concurrent mutation, but it does not reset the resource. If a locked test changes an account setting and exits early, the next locked test can still inherit dirty state. The durable pattern is lock plus explicit setup and cleanup: put the resource into a known state through an API in beforeEach, and restore it in afterEach so an early exit cannot leak state to the next lock holder.

Shared resourceBetter than serial mode?Add cleanup?Notes
One test tenantYesYesLock tenant id, then reset users, flags, and limits
Global feature flagYesYesUse one lock per flag family, not one suite-wide lock
Payment sandbox accountYesYesKeep idempotent API cleanup outside UI assertions
Email inboxUsuallyUsuallyBetter long-term fix is per-test inboxes
Local file pathSometimesYesIf paths can be unique per worker, avoid the lock

Cross-Frame Locators Without Naming the Frame

Playwright 1.63 lets page.frameLocator() and frame.frameLocator() run without a selector. Called this way, Playwright searches in any frame of the subtree. You no longer need to identify the iframe element first.

import { test, expect } from "@playwright/test";

test("accepts the embedded fraud challenge", async ({ page }) => {
  await page.goto("/checkout");
  await page.getByRole("button", { name: "Pay now" }).click();

  await page.frameLocator().getByRole("button", { name: "Continue" }).click();

  await expect(page.getByRole("heading", { name: "Payment complete" })).toBeVisible();
});

This is especially useful when a provider changes iframe names, generates sandboxed iframes, or nests a challenge inside another frame. It also helps AI coding agents. When a human asks an agent to fix a test around an embedded widget, the agent often wastes time inspecting iframe selectors that are irrelevant to the user intent. A selectorless frameLocator() communicates the intent directly: find this accessible control in any frame under the page.

There is one important constraint. The rest of the locator resolves inside a single frame, like a regular locator, and Playwright throws when the match appears in several frames. That is good. Ambiguous cross-frame actions are exactly how payment tests click the wrong provider button.

import { test, expect } from "@playwright/test";

test("chooses the address inside the shipping iframe", async ({ page }) => {
  await page.goto("/checkout/shipping");

  const shippingForm = page.frameLocator().getByRole("form", { name: "Shipping address" });

  await shippingForm.getByLabel("Postal code").fill("94107");
  await shippingForm.getByRole("button", { name: "Use this address" }).click();

  await expect(page.getByText("Shipping address selected")).toBeVisible();
});

Use the no-selector form for discovery and resilience, but keep role and text names precise. Anchoring on a named region such as the form above keeps the rest of the chain inside the right widget. If two embedded widgets both expose a Continue button, anchor on a region like that or give the controls more specific accessible names. Do not fall back to nth(0) unless the order is the behavior under test.

Visible Locators Make Hidden Duplicates Less Painful

The new locator.visible() returns a locator that matches only visible elements. The release notes call it the recommended replacement for the :visible CSS pseudo-class.

import { test, expect } from "@playwright/test";

test("opens the active account menu", async ({ page }) => {
  await page.goto("/dashboard");

  const visibleMenuButton = page.getByRole("button", { name: "Account menu" }).visible();
  await visibleMenuButton.click();

  await expect(page.getByRole("menuitem", { name: "Sign out" })).toBeVisible();
});

This is a small API with outsized maintenance value. Design systems often keep hidden menu templates, off-canvas dialogs, inactive tabs, or responsive duplicate buttons in the DOM. A role locator can find both the hidden and visible versions. Previously, teams often mixed user-facing locators with CSS pseudo-classes, creating selectors that looked less like the accessibility tree the user actually interacts with. locator.visible() keeps the locator chain readable.

The failure mode to diagnose is strict mode ambiguity after filtering. If a locator still resolves to two visible buttons, the problem is not Playwright. Your page has two actionable controls with the same user-facing identity. Fix the accessible name, scope the locator to a region, or assert that only one is supposed to be visible.

Pattern1.63 approachWhy it is safer
Hidden and visible duplicate buttonsgetByRole(...).visible()Keeps role semantics and visibility filtering separate
Responsive desktop/mobile navScope to navigation, then .visible()Avoids relying on DOM order
Portaled dialogsgetByRole("dialog").visible()Matches what the user can perceive
Skeleton templatesWait for stable user-facing contentVisibility alone is not a data-loaded assertion

Structured Steps Help Humans and Agents Read Runs

In Playwright 1.63, test.step() accepts subtitle and params. Reporters receive them through testStep.subtitle and testStep.params. Playwright API steps also report useful subtitles, such as the locator or navigation URL, and the HTML report and trace viewer render the step detail next to the title.

import { test, expect } from "@playwright/test";

test("refunds a settled order", async ({ page }) => {
  const orderId = "ORDER-1001";

  await test.step(
    "Open order",
    async () => {
      await page.goto(`/admin/orders/${orderId}`);
      await expect(page.getByRole("heading", { name: orderId })).toBeVisible();
    },
    { subtitle: "admin order detail", params: { orderId } }
  );

  await test.step(
    "Issue refund",
    async () => {
      await page.getByRole("button", { name: "Refund" }).click();
      await page.getByRole("button", { name: "Confirm refund" }).click();
      await expect(page.getByText("Refund created")).toBeVisible();
    },
    { subtitle: "full amount", params: { orderId, amount: "full" } }
  );
});

Structured params are not a dumping ground. Treat them like log fields in production. Include ids, feature flags, browser channel, tenant class, and scenario labels. Do not include passwords, bearer tokens, session cookies, raw addresses, or payment details. The trace viewer and reports are often uploaded as CI artifacts, and artifacts travel farther than local logs.

Custom reporters can use the new fields to send sharper events to dashboards:

import type { Reporter, TestCase, TestResult, TestStep } from "@playwright/test/reporter";

class StepAuditReporter implements Reporter {
  onStepEnd(test: TestCase, result: TestResult, step: TestStep) {
    if (step.category !== "test.step") return;
    const event = {
      test: test.title,
      step: step.title,
      subtitle: step.subtitle,
      params: step.params,
      duration: step.duration
    };
    console.log(JSON.stringify(event));
  }
}

export default StepAuditReporter;

For AI coding agents, structured steps reduce guesswork. A trace that says Issue refund, subtitle full amount, params orderId, and then shows a failing locator provides more context than a flat series of clicks. This is one of the places where ready-made QA skills installed from qaskills.sh with the qaskills CLI can help standardize how teams name steps and collect artifacts, but the underlying discipline still belongs in your test code.

Aria and Screen Snapshots Change Trace Review

The snapshots option of tracing.start() and the trace fixture option can now accept an object that chooses what to capture on every action: dom, aria, and screen. With aria and screen snapshots recorded, the trace viewer's Display Aria mode shows the action screenshot alongside the aria snapshot, and hovering an aria node highlights it on the screenshot.

import { defineConfig } from "@playwright/test";

export default defineConfig({
  use: {
    trace: {
      mode: "on-first-retry",
      snapshots: { dom: true, aria: true, screen: true }
    }
  }
});

This matters for a class of failures that used to be awkward to debug. A screenshot might show a button that looks correct, while the accessibility snapshot reveals that the control has no name, is hidden from assistive technology, or is exposed as the wrong role. Before 1.63, QA engineers had to combine screenshots, DOM snapshots, and separate accessibility tooling to explain that gap. Now the trace can carry the visual and accessibility evidence together.

Use the heavier snapshot mode where it earns its storage cost. A practical setup is on-first-retry for PRs and on for nightly accessibility runs. For very large suites, measure artifact size before enabling all three snapshot types everywhere.

The same release adds locator.ariaSnapshotJSON() and page.ariaSnapshotJSON(), returning aria snapshots as JSON values instead of YAML markup, with mode, depth, and boxes options. JSON output is easier to post-process when you are building custom accessibility audits or feeding a focused artifact into an AI triage workflow.

import { test, expect } from "@playwright/test";

test("primary checkout actions remain accessible", async ({ page }) => {
  await page.goto("/checkout");

  const snapshot = await page.ariaSnapshotJSON({ depth: 4, boxes: true });
  const serialized = JSON.stringify(snapshot);

  expect(serialized).toContain("Pay now");
  expect(serialized).toContain("Order summary");
});

That sample deliberately checks for presence before deeper structure. In real audit code, parse the JSON shape your app depends on and assert exact roles and names.

If your team is already trace-centric, pair this release with a deeper trace workflow such as the Playwright trace viewer guide. 1.63 adds richer raw material; the review habit still determines whether it improves debugging time.

Reporting, Perfetto, and CI Wiring

The new --add-reporter flag appends a reporter on top of the reporters in playwright.config. That is different from --reporter, which replaces configured reporters. This is useful in CI when a local config already produces HTML and list output, but a specific job needs GitHub annotations or a Perfetto timeline.

name: playwright-163

on:
  pull_request:
  workflow_dispatch:

jobs:
  test:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npx playwright test --add-reporter=github,perfetto
      - uses: actions/upload-artifact@v7
        if: always()
        with:
          name: playwright-report-${{ github.run_id }}
          path: playwright-report

Artifact names cannot contain /, so keep report names flat. The example uses playwright-report-${{ github.run_id }} rather than a branch path. Also note the runner image: Playwright 1.63 drops Ubuntu 20.04 support. If your CI uses old self-hosted runners or Docker images based on 20.04, upgrade the operating system before treating test failures as application regressions.

The built-in perfetto reporter writes a Trace Event Format file for Perfetto UI or chrome://tracing. Use it when your question is about scheduling and duration across workers, not just a single test failure. The HTML report also renders a duration waterfall next to test steps, which makes slow setup and teardown easier to spot.

The new reporter omitTags option for list, line, dot, github, and junit suppresses tags that Playwright automatically appends to test titles. This is useful when your test titles are already stable identifiers and appended tags create noisy test case names in external systems.

import { defineConfig } from "@playwright/test";

export default defineConfig({
  reporter: [
    ["list", { omitTags: true }],
    ["html", { open: "never" }],
    ["junit", { outputFile: "test-results/junit.xml", omitTags: true }]
  ],
  use: {
    trace: {
      mode: "retain-on-failure",
      snapshots: { dom: true, aria: true, screen: true }
    }
  }
});

Browser, Context, and CLI Details

Several 1.63 additions are smaller than locks or traces, but they remove real friction.

FeatureExact API or optionPractical use
Multiple HTTP credentialshttpCredentials accepts an arrayTest several protected origins from one context
Origin private file system storagebrowserContext.storageState({ opfs: true })Persist and restore OPFS data for apps that store files in the browser
Dialog close eventspage.on("dialogclosed"), browserContext.on("dialogclosed")Assert a dialog was accepted, dismissed, or closed
Typed API response JSONrequest.get<User>() and other methodsReduce casts in TypeScript API setup
Standalone emulation optionsreducedMotion, forcedColors, contrastMove accessibility display settings out of broad device profiles
Browser install behaviornpx playwright install --no-removeKeep browsers from other Playwright installs on shared machines
Codegen authenticationnpx playwright codegen --http-credentialsRecord behind HTTP basic authentication

A typed API setup is straightforward:

import { test, expect } from "@playwright/test";

type User = {
  id: string;
  status: "paid" | "settled";
};

test("loads a seeded paid user", async ({ request }) => {
  const response = await request.get<User>("/api/users/42");
  expect(response.ok()).toBe(true);

  const user = await response.json();
  expect(user.id).toBe("42");
  expect(user.status).toMatch(/^(paid|settled)$/);
});

The type argument is compile-time only. It types what json() returns, but it does not validate the payload, so the runtime assertions still carry the weight. The regex is anchored for the same reason: a loose /paid|settled/ would also accept a broken value such as unpaid.

A Realistic Upgrade Failure Mode

The failure: after upgrading to 1.63, CI shows a burst of WebKit and Chromium install errors on older self-hosted Linux runners, followed by tests that never launch. The application did not change. Local macOS runs pass.

The likely diagnosis is platform drift. Playwright 1.63 no longer supports Ubuntu 20.04. If your runner is pinned to 20.04, or your Docker base image inherits old system libraries, browser installation and launch can fail before the test code is meaningful. Check the OS image first, then browser install logs, then app tests.

node --version
npx playwright --version
npx playwright install --with-deps
npx playwright test --grep "@smoke" --project=chromium

Once the runner is on a supported OS, run a small smoke subset (the --grep filter above) across projects, then enable trace capture on retry to inspect the first real behavior change.

Adoption Checklist for Agent-Heavy Teams

AI coding agents can apply a Playwright upgrade quickly, but they need sharp constraints. Give the agent a checklist that distinguishes syntax migration from suite design.

  1. Bump @playwright/test and playwright together.
  2. Run npx playwright install --with-deps on the same OS image CI uses.
  3. Replace broad serial suites with named locks only where a shared resource exists.
  4. Convert :visible CSS locators to locator.visible() when the locator can remain role-based.
  5. Use selectorless frameLocator() only when cross-frame search is intended, then make the inner locator specific.
  6. Add subtitle and safe params to high-value test.step() blocks.
  7. Enable aria and screen snapshots first on retry or on dedicated accessibility jobs.
  8. Append CI-only reporters with --add-reporter, do not accidentally replace local reporters with --reporter.
  9. Move runners off Ubuntu 20.04 before investigating browser-level failures.
  10. Keep trace artifacts scrubbed of secrets and personal data.

Frequently Asked Questions

Is Playwright 1.63 mainly a flake-reduction release?

Partly, but not only. Test locks can reduce flakes caused by concurrent access to shared resources, and locator.visible() can reduce ambiguity from hidden duplicates. The trace and reporter changes are more about diagnosis than prevention. If a test is flaky because the app has unawaited async work, stale data, or a race in the product, 1.63 will not magically fix it. It can, however, make the race easier to isolate and keep unrelated tests parallel while one shared resource is protected.

Should every shared account test get a lock?

Use locks when the shared account is genuinely unavoidable. A lock is the right short-term tool for global settings, third-party sandboxes, and legacy tenants that cannot be cloned per worker. It is not a substitute for per-test isolation. If you can create a user, inbox, tenant, or file namespace per test, do that instead. Locks serialize access, but they do not clean state, and too many broad locks quietly turn a fast parallel suite into a slow serial one.

Are aria and screen snapshots safe to enable in all CI jobs?

They are safe functionally, but artifact size and data exposure need attention. Aria and screen snapshots can capture user-facing text, labels, and screenshots from the tested application. Enable them first on retry, failure, or accessibility-focused jobs, then measure storage and retention. Do not include secrets in step params, screenshots, or pages under test. For regulated products, review trace retention the same way you review logs and videos.

Do I still need explicit iframe selectors after Playwright 1.63?

Often, yes. Selectorless frameLocator() is excellent when the frame host is unstable or nested, but it intentionally fails when the target appears in multiple frames. For repeated widgets, explicit scoping remains clearer. A good pattern is to use selectorless frame search for discovery, then anchor the frame with a heading, label, or known region. The goal is less iframe plumbing, not less precision.