---
title: "Playwright 1.65 in CI: Breaking Changes, Bun and Reports"
description: "Upgrade to Playwright 1.65 without a red CI: breaking changes in sharded runs, blob merges and page objects, plus failure-only reports and Bun."
canonical: "https://currents.dev/posts/playwright-1-65-in-ci-breaking-changes-bun-and-reports"
updated: "2026-11-09"
---

# Playwright 1.65 in CI: Breaking Changes, Bun and Reports

Published 2026-11-09 by Walter Galvão.

Canonical URL: https://currents.dev/posts/playwright-1-65-in-ci-breaking-changes-bun-and-reports

Upgrade to Playwright 1.65 without a red CI: breaking changes in sharded runs, blob merges and page objects, plus failure-only reports and Bun.

---


> **TL;DR**
>
> This article covers what changes for CI when you upgrade to Playwright 1.65: the new failures reporter and onlyFailures option, the built-in exclusive test lock, and running the test runner under Bun. It also covers the breaking changes that can turn a green suite red without a single test changing, such as class fields in page objects, sharded runs that match no tests, blob report directories and inert elements.
>
> 1. Move CI's Node.js version to 22, 24 or 26 in the same pull request as the Playwright bump.
> 2. Move locator initializers into page object constructors, and give each blob invocation its own directory.
> 3. Add `--pass-with-no-tests` only where a filter can legitimately match nothing.
> 4. Fix whatever still breaks, and only then turn on the `failures` reporter and `onlyFailures`.

Bump Playwright to 1.65 on a suite that was green, and it can go red without a single test changing. A page object throws as soon as a test builds it, because a field initializer that reads a constructor parameter property now runs before that property is set. Clicks and fills behind an open drawer now wait for the element to stop being inert, and time out if it stays inert. A filtered, sharded run that matches no tests in any shard fails with "No tests found". A pipeline that writes several blob reports into one directory is left with only the last one to merge.

The fixes are a handful of config and pipeline lines you can change before the bump.

## What's new in Playwright 1.65

- TypeScript class fields now follow `useDefineForClassFields` from your `tsconfig.json`, the same as `tsc`, so a field initializer that reads a constructor parameter property now fails, because the initializer runs first.
- A sharded run now fails with "No tests found" when no tests match across all shards; pass `--pass-with-no-tests` to allow this.
- The blob reporter again deletes the content of its output directory before it writes the report.
- Actions such as `locator.click()` now wait for the element to be not inert, and text that the browser does not render is no longer part of accessible names and aria snapshots.
- Cookie matching now follows RFC 6265, and proxy credentials are no longer sent to websites: the credentials in the `proxy` option now answer only proxy challenges.
- String and `URLSearchParams` values of the `params` option in APIRequestContext requests are now appended to the query that the URL already has.
- `.css`, `.less` and `.scss` imports in test files now load as empty modules.
- Test locations of imported calls in CommonJS output now point to the start of the callee.
- Playwright no longer tests on Node.js 20 (use Node.js 22, 24 or 26), and Docker images now include Node.js 26 instead of Node.js 24.
- The test runner now runs under Bun when you pass the `--bun` flag, because without it `bunx playwright test` runs on Node.js.
- A new `failures` reporter prints failures and successful retries as they occur, followed by a summary, without progress output, and a new `onlyFailures` option of the HTML, JSON and JUnit reporters keeps only tests with unexpected outcomes, flaky tests and interrupted tests.
- A new built-in `-exclusive-` test lock runs a test only when no other tests are running.
- The full list of changes is in the [Playwright 1.65 release notes](https://playwright.dev/docs/release-notes#version-165).

## Failure-only reports in CI

Swap your CI reporter for the new `failures` reporter. Playwright's default reporter is `list` locally and `dot` on CI. The new `failures` reporter prints failures and successful retries as they occur, followed by a summary, without progress output. The reporter docs pair it with a complete HTML report. On a large suite, filter the HTML report and keep the JUnit file whole:

```ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  reporter: process.env.CI
    ? [
        ['failures'],
        // onlyFailures is set per reporter, so only the HTML report is filtered
        ['html', { onlyFailures: true }],
        // Full JUnit file: tools that count passes still see every test
        ['junit', { outputFile: 'test-results/junit.xml' }],
      ]
    : 'list',
});
```

`onlyFailures` keeps tests with unexpected outcomes (unexpected passes included), flaky tests even when they pass on retry, and interrupted tests, with every attempt of each kept test. A test that passed on its first attempt is not in the report at all. That is the trade: smaller files, and no record in them of tests that passed on their first attempt.

Turn it on for HTML when report artifacts have grown too big to be worth uploading. A smaller file fixes the upload, not the [other limits of the HTML reporter on large suites](/posts/playwright-html-reporter-why-it-breaks-down-at-scale). The `PLAYWRIGHT_HTML_ONLY_FAILURES` environment variable sets it for the HTML reporter, so you can try it on one CI job without editing the config.

Leave `onlyFailures` off the JUnit reporter when a CI tool reads pass counts from the JUnit file. With the option on, that tool sees only the tests `onlyFailures` kept, and on JUnit the option retains every attempt regardless of `includeRetries`. Set it on JUnit only when nothing downstream counts passes. If you want the passes on record across runs anyway, [Currents Test History](https://docs.currents.dev/dashboard/tests/test-history) shows how each test performed across runs and branches, and it treats results with the same spec file, project and title as one test, so renaming a test detaches its history.

<AntiPattern title="Merging without the config" id="anti-pattern-merging-without-the-config">

You set `onlyFailures` on the HTML reporter in `playwright.config.ts`, the shards write blob reports, and a later job merges them. The option is configured per reporter, and the merge step below never reads the config that sets it:

```bash
# Anti-pattern: onlyFailures lives in playwright.config.ts, which this merge never reads
npx playwright merge-reports --reporter=html ./blob-report
```

<AntiPattern.Fix>

Pass the config file to the merge step, which is how `onlyFailures` applies when you merge blob reports:

```bash
npx playwright merge-reports --config=playwright.config.ts ./blob-report
```

</AntiPattern.Fix>

</AntiPattern>

## The exclusive test lock

Some tests need the whole environment to themselves: one flips a global setting, another resets data that parallel tests read. The new built-in `-exclusive-` lock runs a test only when no other tests are running, and all other tests continue to run in parallel with each other.

Use it for exactly those tests, and sparingly. While an exclusive test runs, no other test does. Each one adds about its full duration to the run's wall-clock time. Tests that only clash with each other over one shared account can share a named lock instead: tests sharing a lock name never run at the same time, while all other tests continue to run in parallel. Our 1.64 guide covers [keeping shared-account tests apart with a lock](/posts/playwright-1-64-shuffle-test-locks-and-breaking-changes#keep-tests-that-share-an-account-apart-with-testdescribeconfigure-lock-).

<AntiPattern title="Exclusive as a flake fix" id="anti-pattern-exclusive-as-a-flake-fix">

A group of flaky tests races over shared state, so someone marks them all exclusive. The failures go quiet. Each of those tests now stalls every other worker while it runs. The race is still in the code.

<AntiPattern.Fix>

Give each test its own data so it can run in parallel, and keep the lock for tests that change something every other test depends on.

</AntiPattern.Fix>

</AntiPattern>

## Running tests under Bun

The test runner now runs under Bun, but only with the `--bun` flag: without it, `bunx playwright test` runs on Node.js.

<AntiPattern title="Calling bunx without the flag" id="anti-pattern-calling-bunx-without-the-flag">

The step calls `bunx`, so it looks switched over, and every run still goes through Node.js:

```bash
# Anti-pattern: without --bun, bunx still starts the test runner on Node.js
bunx playwright test
```

<AntiPattern.Fix>

Pass `--bun` so the test runner runs under Bun:

```bash
bunx --bun playwright test
```

</AntiPattern.Fix>

</AntiPattern>

### What changes under Bun

Bun transpiles your TypeScript test files itself, so Playwright uses neither Babel nor its compilation cache. The new `PLAYWRIGHT_TRANSFORM_CACHE_DIR` environment variable sets the directory for transpiled test files, for example on a Linux machine shared with other users.

Playwright's own CI runs the test runner tests under Bun 1.4.3 on Linux. If you adopt Bun, pin that version and use Linux runners.

Some features work differently under Bun, and these are the ones a pipeline feels:

- `--tsconfig` and the JSX options in `tsconfig.json` have no effect, because Bun loads modules itself.
- Custom ESM loaders are not supported.
- Stack traces do not include async frames.
- API names drop the class name (`click` instead of `locator.click`), which changes some step locations, step titles and error messages.
- Console output of a test does not get into the trace or UI mode.

Locally, there is no watch mode, and `--only-changed` does not follow imports.

### When to skip it

Skip Bun if you [debug CI failures from traces](/posts/playwright-trace-viewer-debug-ci-failures-step-by-step) and need the test's console output in them, or if your setup depends on `--tsconfig` or a custom ESM loader. Also skip it if anything compares step titles or error messages across runs, since API names drop the class name under Bun, which changes some step titles and error messages. Otherwise, try it on one Linux job before moving the rest.

## Empty shards and blob reports

The new "No tests found" failure is narrower than it sounds. An empty shard still passes when other shards have tests, and a sharded run fails with "No tests found" when no tests match across all shards.

That happens when a filter matches nothing in the whole suite, such as a nightly `--grep` for a tag no test carries yet. Add `--pass-with-no-tests` to jobs whose filter can legitimately match nothing, and to no others. Everywhere else, a zero-test run means a broken filter, and CI should go red. For that nightly job, shard 1 of 3 runs:

```bash
# The @experimental tag may be on no test yet, so this job allows an empty match
npx playwright test --grep @experimental --shard=1/3 --pass-with-no-tests
```

If this change has you rethinking how the suite is split in the first place, we compare [fixed shards, workers and orchestration](/posts/optimizing-test-runtime-playwright-sharding-vs-workers) in a separate post.

### Blob reports in one directory

The blob reporter again deletes the content of its output directory before it writes the report, where Playwright 1.64 kept reports with other names. That directory is the reporter's `outputDir`: `PLAYWRIGHT_BLOB_OUTPUT_DIR` sets it, and it defaults to `blob-report`.

Blob files are named `report-<hash>.zip`, or `report-<hash>-<shard_number>.zip` when sharding, with the hash computed from `--grep`, `--grep-invert`, `--project`, tag and file filters passed on the command line. In 1.64, invocations with different filters left differently named files side by side. In 1.65, each invocation empties the directory first, so only the last report is left to merge.

<AntiPattern title="Several runs, one blob directory" id="anti-pattern-several-runs-one-blob-directory">

One runner runs the suite once per project into the default directory, then merges:

```bash
# Anti-pattern: the firefox run empties blob-report/ first, so the chromium report is gone before the merge
npx playwright test --project=chromium --reporter=blob
npx playwright test --project=firefox --reporter=blob
npx playwright merge-reports --reporter=html ./blob-report
```

<AntiPattern.Fix>

Give each invocation its own `PLAYWRIGHT_BLOB_OUTPUT_DIR`, then copy the reports into one folder for the merge. `--project` is one of the hash inputs, so the two files get different names:

```bash
PLAYWRIGHT_BLOB_OUTPUT_DIR=blob-chromium npx playwright test --project=chromium --reporter=blob
PLAYWRIGHT_BLOB_OUTPUT_DIR=blob-firefox npx playwright test --project=firefox --reporter=blob
mkdir -p all-blob-reports
cp blob-chromium/*.zip blob-firefox/*.zip all-blob-reports/
npx playwright merge-reports --reporter=html ./all-blob-reports
```

</AntiPattern.Fix>

</AntiPattern>

## Page object class fields

The class-field change is the one most likely to break a whole suite at once.

TypeScript class fields now follow `useDefineForClassFields` from your `tsconfig.json`, the same as `tsc`. The setting is enabled for `ES2022` and later targets, and when `target` is not specified, so check your `target` first.

<AntiPattern title="Locators as field initializers" id="anti-pattern-locators-as-field-initializers">

A page object builds its locator from `this.page` in a field initializer. A field initializer that reads a constructor parameter property now fails, because the initializer runs first:

```ts
// Anti-pattern: under useDefineForClassFields, this initializer runs before this.page is assigned
import type { Locator, Page } from '@playwright/test';

export class LoginPage {
  readonly emailInput: Locator = this.page.getByLabel('Email');

  constructor(private readonly page: Page) {}

  async goto() {
    await this.page.goto('/login');
  }
}
```

<AntiPattern.Fix>

Move the initialization into the constructor:

```ts
import type { Locator, Page } from '@playwright/test';

export class LoginPage {
  readonly emailInput: Locator;

  constructor(private readonly page: Page) {
    this.emailInput = page.getByLabel('Email');
  }

  async goto() {
    await this.page.goto('/login');
  }
}
```

</AntiPattern.Fix>

</AntiPattern>

### The tsconfig opt-out

The release notes give a second fix: set `"useDefineForClassFields": false` in `tsconfig.json`. Prefer the constructor fix. The tsconfig switch changes how every class field compiles only to rescue a few page objects, and it has a trap of its own. One user reported that a decorated class with a static field fails to load in 1.65 with useDefineForClassFields set to false ([#43936](https://github.com/microsoft/playwright/issues/43936)). The issue was closed as completed on 2026-11-05, with no release named.

## Inert elements and accessible names

Expect new timeouts in tests that act on the page while a modal or drawer is open. `locator.click()`, `locator.check()`, `locator.tap()`, `locator.fill()`, `locator.clear()` and `locator.selectOption()` now wait for the element to be not inert. An action on an element with the `inert` attribute, or inside such an element, now times out if the element stays inert.

<AntiPattern title="Raising the timeout" id="anti-pattern-raising-the-timeout">

Clicks start timing out after the upgrade, so someone raises the action timeout. If the element stays inert, the action times out at any timeout, and the run only takes longer to fail.

<AntiPattern.Fix>

Close the overlay in the test, or finish the step inside it, before acting on the page behind it.

</AntiPattern.Fix>

</AntiPattern>

### Accessible names

The same release changes what role locators and aria snapshots see. Text that the browser does not render, such as text that is not assigned to a slot or text directly inside a closed `<details>`, is no longer part of accessible names and aria snapshots. That can change the results of `page.getByRole()` with a name, `locator.ariaSnapshot()` and `expect(locator).toMatchAriaSnapshot()`.

Review those diffs one by one instead of bulk-updating snapshots. A role locator with a name that stopped matching after the upgrade may have matched text the browser does not render.

## Other breaking changes to check

The rest are smaller, but each can fail a test far from the feature it changes.

### Node.js and browsers

Playwright no longer tests on Node.js 20; use Node.js 22, 24 or 26, as listed in the system requirements. Move CI's Node.js version in the same pull request as the Playwright bump. Docker images now include Node.js 26 instead of Node.js 24, and the latest patch of Chromium on the same branch. The 1.65 browsers are Chromium 158.0.8127.18, Firefox 159.0 and WebKit 27.2.

### Cookies and requests

Cookie matching now follows RFC 6265. With a URL, `browserContext.cookies()` and `apiRequestContext.cookies()` no longer return host-only cookies for subdomains or `Path=/api` cookies for `/apiv2`, and a `Path=/foo/` cookie does not match `/foo`. That breaks a test that passes a subdomain URL to those calls and expects a host-only cookie back, or passes `/apiv2` and expects a `Path=/api` cookie. APIRequestContext sends a cookie without a name as `Cookie: x` instead of `Cookie: =x`.

String and `URLSearchParams` values of the `params` option are now appended to the query that the URL already has, instead of replacing it. That hits an API test whose URL already holds a query string and also passes `params` as a string or `URLSearchParams`: the request now carries both queries, so a test that counted on `params` to override a value in the URL sends both values.

Check API tests that post string bodies with a JSON content type, too. A regression report against 1.65.0 says that string data that is not valid JSON, sent with an application/json;charset=UTF-8 header, is now JSON-encoded ([#43937](https://github.com/microsoft/playwright/issues/43937)). In 1.64.1, the report says, only an exact application/json header did that. The issue was closed as completed on 2026-11-06.

### Proxy credentials

Proxy credentials are no longer sent to websites: the credentials in the `proxy` option now answer only proxy challenges.

<AntiPattern title="Site login through the proxy" id="anti-pattern-site-login-through-the-proxy">

A config puts the site's login in the `proxy` option, so the site never receives it:

```ts
// Anti-pattern: proxy credentials now answer only proxy challenges, so the site never gets them
import { defineConfig } from '@playwright/test';

export default defineConfig({
  use: {
    proxy: {
      server: 'http://proxy.example.com:3128',
      username: process.env.SITE_USER,
      password: process.env.SITE_PASSWORD,
    },
  },
});
```

<AntiPattern.Fix>

To authenticate on a website, use `httpCredentials`, and keep the `proxy` credentials for the proxy:

```ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  use: {
    proxy: {
      server: 'http://proxy.example.com:3128',
      username: process.env.PROXY_USER,
      password: process.env.PROXY_PASSWORD,
    },
    httpCredentials: {
      username: process.env.SITE_USER!,
      password: process.env.SITE_PASSWORD!,
    },
  },
});
```

</AntiPattern.Fix>

</AntiPattern>

### CSS imports

`.css`, `.less` and `.scss` imports in test files now load as empty modules instead of being removed, so the imported file must exist. Make sure the CI job checks out or builds every stylesheet a test file imports.

### Upgrade order

Do the bump on a branch with full reports, so you can compare test counts and passes against your last 1.64 run, not just the failures. On that branch, move CI's Node.js version, move locator initializers into page object constructors, give each blob invocation its own directory, and add `--pass-with-no-tests` only where a filter can legitimately match nothing. Fix whatever still breaks there. Only then turn on the `failures` reporter and `onlyFailures`: a report without passed tests can't tell you whether the upgrade lost any.

## Frequently asked questions


### What are the breaking changes in Playwright 1.65?

TypeScript class fields now follow `useDefineForClassFields` from your `tsconfig.json`, and a sharded run now fails with "No tests found" when no tests match across all shards. The blob reporter again deletes the content of its output directory before it writes the report, and actions such as `locator.click()` now wait for the element to be not inert.


### Why does my page object fail after upgrading to Playwright 1.65?

A field initializer that reads a constructor parameter property now fails, because the initializer runs first. TypeScript class fields now follow `useDefineForClassFields` from your `tsconfig.json`, and the setting is enabled for `ES2022` and later targets, and when `target` is not specified. Move the initialization into the constructor, as [Page object class fields](#page-object-class-fields) shows.


### Why does my sharded Playwright run fail with No tests found?

A sharded run fails with "No tests found" when no tests match across all shards, while an empty shard still passes when other shards have tests. Add `--pass-with-no-tests` to jobs whose filter can legitimately match nothing, and to no others, as [Empty shards and blob reports](#empty-shards-and-blob-reports) explains.


### How do I run Playwright tests with Bun?

Run `bunx --bun playwright test`. Without the `--bun` flag, `bunx playwright test` runs on Node.js, as [Running tests under Bun](#running-tests-under-bun) shows.


### What does the Playwright failures reporter do?

The new `failures` reporter prints failures and successful retries as they occur, followed by a summary, without progress output. The reporter docs pair it with a complete HTML report, as [Failure-only reports in CI](#failure-only-reports-in-ci) shows.


### What does onlyFailures keep in a Playwright report?

`onlyFailures` keeps tests with unexpected outcomes (unexpected passes included), flaky tests even when they pass on retry, and interrupted tests, with every attempt of each kept test. A test that passed on its first attempt is not in the report at all. Leave `onlyFailures` off the JUnit reporter when a CI tool reads pass counts from the JUnit file, as [Failure-only reports in CI](#failure-only-reports-in-ci) explains.


### What is the Playwright exclusive test lock?

The new built-in `-exclusive-` lock runs a test only when no other tests are running, and all other tests continue to run in parallel with each other. Use it for tests that need the whole environment to themselves, and sparingly, since no other test runs while an exclusive test runs, as [The exclusive test lock](#the-exclusive-test-lock) explains.


### Why do clicks time out after upgrading to Playwright 1.65?

`locator.click()`, `locator.fill()`, `locator.check()` and similar actions now wait for the element to be not inert, so an action on an element with the `inert` attribute, or inside such an element, now times out if the element stays inert. Close the overlay in the test, or finish the step inside it, before acting on the page behind it, as [Inert elements and accessible names](#inert-elements-and-accessible-names) shows.




---

## About the author

**Walter Galvão**, Software Engineer. Walter Galvão is a software engineer at Currents. He writes about Playwright in practice: debugging flaky tests, structuring suites for CI, and the engineering workflows that keep large test suites reliable.

LinkedIn: https://www.linkedin.com/in/waltergalvao/
