Playwright 1.60 on May 11 Makes E2E Debugging Less Annoying: 4 Features Worth Using Now
Playwright 1.60 is the kind of release test teams actually feel: HAR tracing, real drag-and-drop, fail-fast aborts, and cleaner multi-tab event handling.
Playwright 1.60 shipped on May 11, 2026. The release page picked up 43 reactions fast, and I get why. This is one of those boring releases that saves real time. No giant framework detour. No fake productivity theater. Just a handful of APIs that clean up the little hacks a lot of us have been dragging around in browser tests.
I’m basing this on the v1.60 release, the official release notes, and the API docs for tracing, `locator.drop()`, `test.abort()`, and BrowserContext events. If your suite is mostly fine already, this still matters, because the new stuff lands exactly where flake and maintenance pain usually show up.
1. HAR Recording Finally Belongs Inside Tracing
The biggest quality-of-life change for me is tracing.startHar() and tracing.stopHar(). Before this, grabbing a useful HAR in the middle of a failing flow usually meant extra plumbing, separate recording setup, or one more custom script nobody wanted to own.
Now it sits right in the tracing API, which is where it should have been in the first place.
import { test, expect } from '@playwright/test';
test('captures checkout API traffic', async ({ context, page }) => {
await using har = await context.tracing.startHar('artifacts/checkout.har.zip', {
mode: 'minimal',
urlFilter: '**/api/**'
});
await page.goto('/checkout');
await page.getByRole('button', { name: 'Place order' }).click();
await expect(page.getByText('Order confirmed')).toBeVisible();
});That urlFilter bit is the real win. Most teams do not need a full archive of every font, image, analytics ping, and third-party widget. If the flaky part lives in /api/**, record that and move on.
One important nuance from the tracing docs: context.tracing still does not record test assertions. So I would not replace your normal Playwright Test trace config with this. I’d use HAR recording as the network-focused companion to your regular failure trace, not as a substitute.
2. locator.drop() Means Fewer Fake Upload Tests
Custom dropzones are one of those test areas that get weird fast. Hidden file inputs, synthetic events, app-specific drag handlers, and somebody on the team swearing that setInputFiles() is close enough. Sometimes it is. Sometimes it absolutely is not.
locator.drop() is better because it targets the actual drop interaction path.
test('uploads a file through the real dropzone', async ({ page }) => {
await page.goto('/upload');
await page.locator('[data-testid=dropzone]').drop({
files: {
name: 'invoice.csv',
mimeType: 'text/csv',
buffer: Buffer.from('id,total\n1,42\n')
}
});
await expect(page.getByText('invoice.csv')).toBeVisible();
});This matters most when your UI does real work on dragenter, dragover, or drop, not just on the hidden input change event. That’s common in React apps with prettier upload components, and it is exactly where tests drift away from user behavior.
The docs also call out clipboard-like data and URLs, which is nice for editors, canvases, CMS blocks, and all the other places where dropping text or links is part of the product.
I like features like this because they kill a whole category of awkward test-only workarounds. If you have a drag-and-drop uploader in production, your test runner should know how to act like one.
3. test.abort() Is a Clean Way to Hard-Stop Bad Runs
test.abort() is small, but I think a lot of teams will end up using it.
There are situations where a test should not politely keep going. If a spec is pointed at the wrong environment, or it is about to hit a route that should never run in shared staging, I do not want a soft annotation. I want a loud failure right there.
That is what test.abort() gives you.
import { test } from '@playwright/test';
test('never publishes from the shared staging workspace', async ({ page }) => {
await page.route('**/publish', route => {
test.abort('This spec hit /publish. Use a cloned workspace instead.');
return route.abort();
});
await page.goto('/editor');
});This is different from test.skip() or test.fixme(). Those are planning tools. test.abort() is an emergency brake.
I can see this becoming standard in a few places:
Guarding destructive routes in shared environments
Failing fast when a required fixture was misconfigured
Killing tests that accidentally cross tenant or account boundaries
Stopping a run the second a known bad state appears
That sounds niche until you have been burned once. After that, it sounds pretty reasonable.
4. Context-Level Page Lifecycle Events Clean Up Popup Flows
Multi-page tests are usually where neat test architecture goes to die. You start with one clean page, then a popup appears, then a receipt window, then an auth tab, and suddenly your listeners are scattered everywhere.
Playwright 1.60 adds context-level lifecycle hooks like browserContext.on('pageload') and browserContext.on('pageclose'), plus mirrored page events like download, frameattached, framedetached, and framenavigated.
That means you can watch the whole browser context from one place instead of wiring every page individually.
import { test, expect } from '@playwright/test';
test('tracks popup lifecycle from one place', async ({ context, page }) => {
const loaded: string[] = [];
context.on('pageload', popup => {
loaded.push(new URL(popup.url()).pathname);
});
context.on('pageclose', popup => {
console.log('closed', popup.url());
});
await page.goto('/billing');
await page.getByRole('button', { name: 'Open receipt' }).click();
expect(loaded).toContain('/receipt');
});This is not flashy, but it is a real cleanup for payment flows, SSO, OAuth popups, report exports, and any app that opens more than one tab during a critical path.
I’d rather centralize that visibility at the context level than keep stapling listeners onto whichever page happens to exist at the time.
Should You Upgrade This Week?
Probably yes, if your suite hits uploads, popups, or hard-to-debug network flows. This is the kind of release that pays back quickly because it replaces ugly local patterns with official ones.
The one thing I would check first is the breaking-change section in the release. Playwright 1.60 removes a few long-deprecated APIs and options, including Locator.ariaRef(), the old handle option on binding exposure, videosPath and videoSize, and the logger option on browser connect APIs. If your repo has been carrying old config around for a while, run the suite once before you call this a painless upgrade.
Still, my read is simple: Playwright 1.60 is a better release than it looks at first glance. Not because it is loud, but because it is practical. One boring release that removes four recurring annoyances is worth more than a dozen testing demos that look cool in a keynote and never survive contact with a real CI pipeline.