Let me paint you a picture.
It is 2019. You have a Selenium test suite that takes 45 minutes to run. Half the tests are flaky. There is a custom wrapper around WebDriver that one person wrote and only one person understands. That person left the company in 2020.
You are that person now.
Then Cypress came along and saved a lot of us. Fast, developer friendly, genuinely good developer experience. We migrated. We were happy. For a while.
Then you hit the wall. Cross browser testing? Painful. Multiple tabs? Good luck. Iframe handling? Let us not talk about it. File downloads? I have a story about that one but my therapist says I should stop bringing it up.
Enter Playwright. Built by Microsoft. Quietly battle tested by teams at serious scale. Currently sitting at v1.58 as of early 2026. And honestly? It fixed almost everything I had quietly accepted as just the way testing works.
This is not a beginner's guide. You already know what end to end testing is. You have opinions about async/await. You have been burned before. So let me just show you the parts that made me switch and genuinely never look back.
📌 All code examples are tested against v1.58.
Auto Waiting That Actually Works 🧘
Every action in Playwright, whether it is a click, a fill, or a check, automatically waits for the element to be attached to the DOM, visible on screen, stable and not mid animation, enabled, and not obscured by something else sitting on top of it.
All of that. Before it does anything. Without you asking.
// Goodbye forever:
// await page.waitForSelector('#submit-btn')
// await driver.wait(until.elementIsVisible(...))
// Hello:
await page.click('#submit-btn');
That one change alone eliminated roughly 30% of our flaky tests overnight. Not because those tests were badly written. Because half of our so called flakiness was just timing issues we had been papering over with sleep() calls like it was 2011. 🐒
True Cross Browser Support That Does Not Make You Want to Cry 🌐
Playwright ships with three browser engines built right in: Chromium, Firefox, and WebKit. These are not thin wrappers or third party drivers. They are first class, actively maintained engines.
And as of v1.57, Playwright moved from generic Chromium builds over to Chrome for Testing builds. Same test behavior, but now you are actually running on the same Chrome your users see. Headed mode uses chrome and headless mode uses chrome-headless-shell.
📖 Chrome for Testing in v1.57 — playwright.dev release notes
// playwright.config.ts
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
{ name: 'mobile-chrome', use: { ...devices['Pixel 7'] } },
{ name: 'mobile-safari', use: { ...devices['iPhone 14'] } },
]
One command. Five browsers. Done. No Sauce Labs subscription needed for basic cross browser coverage. No separate CI pipeline per browser to babysit.
Cypress added Firefox support at some point. WebKit support there is still experimental. With Playwright, both were first class from the very beginning because the whole thing was designed that way from scratch.
Multiple Tabs and Windows Like a Normal Person 🗂️
OAuth flows. Payment redirects. Anything that pops open a new tab. All of it is handled natively in Playwright without needing workarounds or third party plugins.
// Just wait for the new tab and carry on
const [newPage] = await Promise.all([
context.waitForEvent('page'),
page.click('a[target="_blank"]')
]);
await newPage.waitForLoadState();
await expect(newPage).toHaveTitle(/Payment Confirmation/);
Browser contexts are where Playwright really pulls ahead though. Each context is a fully isolated browser session with its own cookies, its own local storage, and its own auth state. You can spin up multiple contexts inside a single test and simulate two different users interacting with the same page simultaneously.
// Two users. One test. Zero drama.
const userA = await browser.newContext();
const userB = await browser.newContext();
const pageA = await userA.newPage();
const pageB = await userB.newPage();
Try doing that cleanly in Selenium. I will be right here waiting. ⏳
📖 Browser Contexts — playwright.dev
📖 Pages and Tabs — playwright.dev
Network Interception That Does Not Feel Like a Hack 🔌
// Mock a specific API endpoint
await page.route('**/api/users', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ users: [{ id: 1, name: 'Test User' }] })
});
});
// Silently kill analytics requests so they stop slowing down your tests
await page.route('**/*analytics*', route => route.abort());
// Fetch the real response, change one thing, send it back
await page.route('**/api/config', async route => {
const response = await route.fetch();
const json = await response.json();
json.featureFlags.newDashboard = true;
await route.fulfill({ response, json });
});
That third one is genuinely my favorite trick in Playwright. You get the real API response, flip one feature flag, and return it. You are testing the actual behavior of your app without mocking an entire endpoint. I reach for this all the time.
And as of v1.57, Playwright also reports and routes network requests from Service Workers through BrowserContext in Chromium. If you are building any kind of PWA, that is a big deal.
📖 Network Mocking — playwright.dev
📖 Service Worker Routing in v1.57 — playwright.dev release notes
The Clock API: Finally, Time Travel for Your Tests ⏰
This feature gets criminally undermentioned. page.clock gives you full control over time inside the browser: Date, setTimeout, setInterval, all of it.
// Freeze time at a specific moment
await page.clock.install({ time: new Date('2025-01-01T08:00:00') });
await page.goto('http://localhost:3000');
// Skip ahead two hours to trigger session expiry
await page.clock.fastForward('02:00:00');
await expect(page.getByText('Session expired')).toBeVisible();
// Or just park it at a specific time and assert
await page.clock.pauseAt(new Date('2025-01-01T10:00:00'));
await expect(page.getByTestId('current-time')).toHaveText('10:00 AM');
No more sneaking Date.now() mocks into your application code just to make a test pass. No more jest.useFakeTimers() gymnastics that only sort of work. Playwright reaches directly into the browser clock and takes over. Countdown timers, token expiry, scheduled notifications, time sensitive UI states — all testable without touching your production code.
The Trace Viewer and the New Speedboard 🪄
When a test fails in CI with Selenium, you get a stack trace and maybe a screenshot if someone remembered to configure it. Playwright's trace viewer is a completely different experience. It is a full recording of your test: DOM snapshots at every action, every network request, every console log, screenshots throughout. You can scrub through it like rewinding a video.
// playwright.config.ts
use: {
trace: 'retain-on-failure',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
}
npx playwright show-trace trace.zip
Full interactive replay of your failed test, straight from a CI artifact, without ever needing to reproduce it locally. For any senior engineer who has wasted hours trying to reproduce a flaky test that only breaks on Tuesday in CI, this changes your life.
The newer addition I am equally excited about is the Speedboard, which landed in v1.57 and got improved in v1.58. It is a tab in the HTML reporter that lists all your tests sorted from slowest to fastest. At a glance you can see which tests are dragging the whole suite down and start asking useful questions about why.
And with the Timeline view added in v1.58, when you are using merged reports across sharded runs, you get a full picture of what ran when across every machine. Sharding across ten workers and still having complete visibility? That is how a grown up CI setup looks.
The UI Mode and Trace Viewer also got quality of life updates in v1.58: native OS dark and light mode support, Cmd/Ctrl+F search inside code editors, and JSON responses that format themselves automatically. Small things. The kind of small things that make you feel like the team actually uses their own tool every day.
📖 Timeline and UI Mode updates in v1.58 — playwright.dev release notes
Playwright Agents: AI That Writes and Heals Your Tests 🤖
This is the newest addition and honestly the most interesting direction the project is heading. Starting in v1.56, Playwright ships with three official AI agent definitions: planner explores your app and produces a structured Markdown test plan, generator takes that plan and turns it into actual Playwright test files, and healer runs your suite and automatically fixes tests that broke because a selector changed.
npx playwright init-agents --loop=claude
# or if you prefer
npx playwright init-agents --loop=vscode
The healer is where I spend most of my attention. Any sufficiently large test suite has tests that break not because the feature broke but because someone renamed a button or restructured a component. Instead of manually hunting down 40 selectors after a big UI refactor, the healer finds what broke and repairs the locators.
Is it production perfect yet? Not entirely. But it ships as a first class part of Playwright itself, not some community plugin that might go unmaintained in six months. The trajectory here is very clear.
📖 Playwright Test Agents in v1.56 — playwright.dev release notes
📖 Playwright Agents Guide — playwright.dev
Codegen Now Writes Assertions For You 🧠
The test generator got a nice upgrade in v1.58. When you use npx playwright codegen to record your interactions, it now automatically generates toBeVisible() assertions alongside the actions. Before this, Codegen was great for capturing clicks and fills but you still had to go back and add assertions yourself. Now it scaffolds them as you go, and you can toggle the behavior off in the Codegen settings UI if you prefer the old way.
// What Codegen produces now after you submit a form:
await page.getByRole('button', { name: 'Submit' }).click();
await expect(page.getByText('Success! Form submitted.')).toBeVisible(); // generated for you ✨
Less manual work. Faster test writing. Good change.
📖 Codegen — playwright.dev
📖 Auto assertions in Codegen, v1.58 — playwright.dev release notes
webServer Finally Knows When Your App Is Actually Ready 🚦
This one sounds small but it has saved me multiple CI headaches. In v1.57, the webServer config got a wait field that accepts a regex. Playwright now holds off on running tests until your dev server logs something that matches.
// playwright.config.ts
export default defineConfig({
webServer: {
command: 'npm run start',
wait: {
stdout: /Listening on port (?<my_server_port>\d+)/
},
},
});
Use a named capture group and Playwright automatically passes it through as an environment variable, so you can pick up the dynamic port in your test config.
test.use({ baseURL: `http://localhost:${process.env.MY_SERVER_PORT ?? 3000}` });
No more hardcoded ports. No more waitForTimeout(5000) at the top of every file because someone got burned by a slow CI machine once and quietly added it three years ago and nobody knows why it is still there.
📖 webServer wait field in v1.57 — playwright.dev release notes
📖 webServer config — playwright.dev
TypeScript Just Works 🟦
Zero configuration. No plugins to install. No @types/ packages to track down. You write TypeScript and Playwright is fine with it.
import { test, expect, Page } from '@playwright/test';
async function loginAs(page: Page, email: string, password: string) {
await page.goto('/login');
await page.fill('[name="email"]', email);
await page.fill('[name="password"]', password);
await page.click('[type="submit"]');
await expect(page).toHaveURL('/dashboard');
}
test('admin can access settings', async ({ page }) => {
await loginAs(page, 'admin@example.com', 'secret');
await page.click('nav >> text=Settings');
await expect(page.locator('h1')).toHaveText('Settings');
});
Full type inference on the page object. Autocomplete on locators. Typed fixture overrides. It just feels good to work in.
The One Thing I Still Miss From Cypress
The time travel debugging in the Cypress test runner. That interactive, step through experience when you are writing a new test locally is still the most comfortable way to author tests I have ever used. Playwright's UI mode is genuinely closing the gap but Cypress still wins on raw first time test writing comfort.
That is the only thing on my list. Just the one. Think about what that means.
Should You Actually Switch?
If you are on Selenium, yes. Please. There is no honest argument for Selenium on a modern web app in 2026 unless you have legacy constraints you genuinely cannot escape yet.
If you are on Cypress, it depends. If you keep running into the walls I described, cross browser, multi tab flows, PWA testing, the Clock API, then yes the migration is worth it. If Cypress is genuinely working well for your team and productivity is good, the switching cost might not make sense today. But watch this space. The gap between the two tools is growing with every Playwright release.
If you are starting from scratch, Playwright is the default answer. Not because Cypress is bad but because Playwright was designed to handle everything from the beginning.
📖 Cypress to Playwright migration: A step-by-step guide — playwright.dev
The Short Version 👋
Playwright v1.58 gives you auto waiting that actually eliminates flakiness (docs), real cross browser support running on Chrome for Testing builds (v1.57 notes), native multi tab and multi user testing (docs), clean network interception including Service Worker routing (docs), a Clock API that controls browser time directly (docs), a Trace Viewer and Speedboard that make debugging CI failures actually manageable (docs), AI agents that write and repair your tests (v1.56 notes), smarter Codegen with automatic assertions (v1.58 notes), and TypeScript support with zero setup (docs).
It is what end to end testing should have been all along. And the team is still shipping.
Go write some tests. Good ones this time. 🎭
Already on Playwright? Drop your favorite underrated feature in the comments. I am always looking for the next trick I did not know existed.



Top comments (0)