tldr: Playwright 1.61 retires three hacks you have been running for years: faking passkeys, reading localStorage through evaluate(), and losing the video on the run that actually failed. Here is what shipped, the combination nobody is pointing at, and the one thing that breaks if you skipped 1.60.
The release that kills your setup hacks
Playwright 1.61.0 shipped June 15. A patch, 1.61.1, landed June 23 and is the current stable. If you upgraded for the agentic features in 1.59, like browser.bind() for sharing a session with MCP servers, you skipped a release.
Here is the thesis worth holding while you read. Every headline feature in 1.61 replaces a workaround you were already running. Passkeys got faked with CDP or skipped entirely. localStorage got read through an evaluate() round-trip. The flaky run was the one with no video, because retention was tuned for green.
1.61 kills all three. Most "what's new" posts will list the APIs. The more useful read is which hack each one retires, and the one combination that changes how you set up authenticated tests.
What you missed in 1.60
1.60 was a quieter release, but you cannot skip it cleanly, because its removals are the only thing in this upgrade that breaks existing code. More on that at the end. The additions worth knowing:
locator.drop() simulates a real external drag-and-drop. It dispatches genuine dragenter, dragover, and drop events with a synthetic DataTransfer, so you can finally test a file dropped onto an upload zone, not just a hidden file input.
test.abort() stops the current test from inside a fixture, hook, or route handler, with an optional message. Clean stop instead of a cascade of timeouts when a precondition fails.
expect(page).toMatchAriaSnapshot() now works at the page level, not just on a locator, and a new boxes option appends bounding boxes to each node as [box=x,y,width,height].
HAR capture became a first-class tracing API: context.tracing.startHar() and stopHar(), with content, mode, and urlFilter options. It returns a disposable, so await using writes it on scope exit.
Passkeys finally became testable
This is the headline feature, and it closes the widest coverage gap in the release.
Passkeys were close to untestable in an automated suite. The flow depends on a platform authenticator and an OS biometric prompt, neither of which a normal test can reach. So teams either skipped sign-in coverage or held it together with brittle CDP virtual-authenticator hacks.
1.61 ships a first-party virtual authenticator on browserContext.credentials. It answers the navigator.credentials.create() and navigator.credentials.get() ceremonies in software, across all three browsers, with no hardware key.
// Install a virtual authenticator before the flow runs
await context.credentials.install();
await page.goto('/signin');
await page.getByRole('button', { name: 'Sign in with a passkey' }).click();
// The virtual authenticator answers the WebAuthn ceremony
await expect(page.getByText('Welcome back')).toBeVisible();
You can also create() a credential up front and get() the ones already registered, which is how you cover the returning-user path.
One thing to be honest about: this tests your app's WebAuthn integration, not the operating system. The authenticator answers the ceremony, but it does not exercise the real Touch ID or Windows Hello prompt. That boundary is correct for E2E, just know where it sits before you call passkey sign-in "fully covered."
The combination nobody is pointing at
Here is the part you will not find in the other 1.61 posts. The passkey authenticator matters most when you read it next to the web storage API, also new in 1.61.
page.localStorage and page.sessionStorage read and write storage for the current origin directly, with setItem(), getItem(), and items(). No evaluate() round-trip.
await page.localStorage.setItem('onboarded', 'true');
const onboarded = await page.localStorage.getItem('onboarded');
const all = await page.localStorage.items(); // everything for the origin at once
On its own, that is a nice cleanup. Together with the authenticator, it removes the last reasons you were logging in through the UI or reaching for CDP to set up a test.
// A fully scripted authenticated start. No UI login, no CDP hack.
await context.credentials.install(); // answer the passkey ceremony
await page.goto('/app');
await page.localStorage.setItem('plan', 'pro'); // seed client flags directly
await page.localStorage.setItem('onboarded', 'true');
await expect(page.getByTestId('dashboard')).toBeVisible();
Combine that with setStorageState() from 1.59 for cookies and you can script a complete authenticated starting state for any auth model, including passwordless, before the test does any work. That used to mean a slow UI login per test or a pile of init scripts.
It also matters for agents. A Playwright test agent driving the browser through MCP can now start from a real, passkey-authenticated state instead of stalling at a sign-in wall it cannot pass. The 1.59 agentic features assumed you could get past login. 1.61 is the release that lets you.
Keeping the trace when a test only fails sometimes
Flaky tests have a cruel property. The run that fails is the one you have no artifact for, because retention was tuned for the happy path. I have lost more debugging hours to this than to any actual bug.
1.61 adds video recording modes that target exactly this. Set video to 'on-all-retries', 'retain-on-first-failure', or 'retain-on-failure-and-retries', and you keep footage of the attempts that matter.
// playwright.config.ts
export default defineConfig({
use: {
video: 'retain-on-failure-and-retries',
},
});
retain-on-failure-and-retries is the one most teams want. It keeps the video for the failing attempt and the passing retry, so when a test fails on attempt one and passes on attempt two, you compare both instead of guessing. It pairs with the trace mode of the same name from 1.59.
HAR and trace now also capture WebSocket traffic, so a flaky realtime feature finally shows its network activity. One operator note: 1.61.0 shipped a bug where WebSocket message times in the trace viewer were downscaled by 1000. The 1.61.1 patch fixed it. If you debug realtime flows from traces, be on 1.61.1, not 1.61.0.
This is the unglamorous work of running a suite that stays green for the right reasons. It is also most of what a forward-deployed engineer does on Passmark, our open-source Playwright-based engine: keep the suite honest as the app and the framework move, with a person verifying every red run instead of a config that quietly hides the evidence.
Smaller wins worth knowing
apiResponse.securityDetails() and apiResponse.serverAddr() expose TLS details and the resolved server address on API responses, matching what the browser already saw.
expect.soft.poll() brings soft assertions to polling, so a value that settles late can be retried without failing the test on the first miss.
fullConfig.argv gives you the runner's process.argv, and fullConfig.failOnFlakyTests mirrors that setting into config. testInfo.errors now lists sub-errors from an AggregateError separately, which makes a multi-error failure readable. There is also a -G shorthand for --grep-invert.
On infrastructure: 1.61 adds Ubuntu 26.04 support and bundles Chromium 149, Firefox 151, and WebKit 26.5.
What is overhyped, what is underrated
The feature list treats everything as equal weight. It is not.
Underrated: page.localStorage and page.sessionStorage. This will touch more of your suite than the headline passkey feature, because almost every test seeds or asserts client state. Sweep your codebase for storage-only evaluate() calls and delete them.
Underrated: the video retention modes. One config line, and the next flaky failure comes with the footage you used to wish you had. Cheapest debugging win in the release.
Properly hyped, but narrow: the WebAuthn authenticator. Genuinely the most important feature if you ship passkeys, and completely irrelevant if you do not. Most coverage will not say the second half.
Skip the marketing on these: Ubuntu 26.04, the -G shorthand, fullConfig.argv. Fine additions. None of them are why you upgrade.
What to adopt first
Not every feature deserves the same urgency.
First: the video retention modes. One line, immediate payoff the next time CI flakes. No reason to wait.
First, if you ship passkeys: the virtual authenticator. It is the difference between testing real sign-in and skipping it. Same week.
Soon: page.localStorage and the scripted auth-state pattern above. Lower-risk cleanup that makes setup shorter and faster across the suite.
When relevant: the network details and WebSocket trace capture. Reach for these when you are debugging a specific TLS or realtime issue, not as a blanket change.
If you are upgrading from 1.59
Most readers are jumping straight from 1.59 to 1.61, so here is the whole bridge in one place.
The new APIs are additive. Nothing in 1.61 itself breaks existing code. The breaks are all in 1.60, and they are the only thing to plan around. 1.60 removed Locator.ariaRef(), the handle option on exposeBinding, the logger option on connect and connectOverCDP, and the videosPath / videoSize context options. If you still set video paths the old way, move to recordVideo before you upgrade.
Then land on 1.61.1, not 1.61.0, so you get the WebSocket trace-timing fix along with the rest.
npm install -D @playwright/test@latest
npx playwright install
FAQs
What's new in Playwright 1.61?
Playwright 1.61 adds a WebAuthn virtual authenticator (browserContext.credentials) for testing passkeys, a direct page storage API (page.localStorage and page.sessionStorage), new video retention modes (on-all-retries, retain-on-first-failure, retain-on-failure-and-retries), apiResponse.securityDetails() and serverAddr(), WebSocket capture in HAR and trace, soft polling via expect.soft.poll(), and Ubuntu 26.04 support. It bundles Chromium 149, Firefox 151, and WebKit 26.5. The current stable is the 1.61.1 patch.
Is Playwright 1.61.1 different from 1.61.0?
1.61.1 is a patch released June 23. It carries no new features, just five bug fixes on top of 1.61.0, including a trace-viewer fix where WebSocket message times were downscaled by 1000 and a couple of Node loader regressions. If you use the new WebSocket trace capture, upgrade to 1.61.1.
How do I upgrade to Playwright 1.61?
Update the package and reinstall the browser binaries:
npm install -D @playwright/test@latest
npx playwright install
If you use Yarn, run yarn add -D @playwright/test@latest and then npx playwright install. Run your suite afterward to confirm nothing broke.
Is Playwright 1.61 backwards compatible?
1.61 itself documents no breaking changes. The breaks are in 1.60, so they matter if you skipped it. 1.60 removed Locator.ariaRef(), the handle option on exposeBinding, the logger option on connect and connectOverCDP, and the videosPath / videoSize context options. Move video config to recordVideo before upgrading.
How do you test passkeys in Playwright?
Use the virtual authenticator on browserContext.credentials, new in 1.61. Call context.credentials.install() to attach a software authenticator, and the browser answers the navigator.credentials.create() and navigator.credentials.get() ceremonies without a hardware key. You can create() a credential to test a returning user. It works across Chromium, Firefox, and WebKit. Note that it tests your app's WebAuthn integration, not the OS biometric prompt.
How does Bug0 keep tests current across Playwright releases?
When Playwright ships a release like 1.61, a suite written against older APIs starts to drift. With Bug0, a forward-deployed engineer plans your coverage and builds the suite on Passmark, our open-source Playwright-based engine. The engine runs the suite on every deploy and self-heals it when your UI moves, and the engineer verifies every result before it reaches your team. You get the new framework capabilities without owning the upgrade and maintenance work.
Playwright keeps moving the same way: fewer hacks, more first-party APIs for the things teams actually test. 1.61 takes three of the most annoying ones, passkeys, storage, and flaky-run evidence, and retires them. Turn on the retention modes today. Adopt the rest as your app needs them.






