Someone on your team has suggested moving from Selenium to Playwright. Maybe a new engineer, maybe a manager who read something. Now you have to decide.
Let me give you the decision first and the translation table second, because most articles do this the wrong way round. The syntax is the easy part. The decision is what costs money.
Start here: should you migrate at all?
Usually the answer is no, and it is worth understanding why before you look at any code.
A migration takes months. At the end of it, you have exactly the same test coverage you started with. Nothing new is tested. No bug is found that was not being found before. That is a very hard thing to justify, and I have watched teams start one, get half way, and spend a year maintaining two suites while neither got proper attention.
"Playwright is more modern" is not a business case. Neither is "the tests would be nicer to write".
When migrating is the right call
There are two situations where it genuinely makes sense.
1. Selenium is blocking something you actually need.
Be specific about what. Real reasons I have seen:
- You need reliable parallel runs and cannot maintain a Selenium Grid.
- Your suite spends its life on flaky waits, and you want built-in waiting rather than writing explicit waits everywhere.
- You need trace-based debugging because failures on the build server are impossible to investigate.
2. You are rewriting the tests anyway.
This is the strongest case, and it is the one people miss. If the suite is so unreliable that nobody trusts it, you are going to rewrite those tests regardless. Rewriting them in Playwright costs almost the same as rewriting them in Selenium, and you get better tools at the end. The migration is nearly free because the work was already scheduled.
When to stay on Selenium
Your team does not write JavaScript. This is the single strongest reason to stay, and it is a good one. Selenium has proper support for Java, Python, C# and Ruby. A Java team will maintain a Java suite far better than a JavaScript suite nobody is comfortable in. Playwright does have other language bindings, but its JavaScript version is where the ecosystem lives.
You have a large suite that works. A suite catching real bugs today has value a rewrite would destroy for months.
You need long-term support guarantees or very old browsers. Some regulated industries have this as a hard requirement, and Selenium's age and W3C standardisation are advantages there.
If you decide to stay, say why out loud. "We stay on Selenium because the team writes Java and the suite catches real bugs" is a senior answer. "We have always used Selenium" is not.
If you are migrating: the translation table
| What you want | Selenium (JavaScript) | Playwright |
|---|---|---|
| Start a browser | new Builder().forBrowser('chrome').build() |
handled by the runner |
| Close the browser | await driver.quit() |
handled by the runner |
| Visit a page | await driver.get('/') |
await page.goto('/') |
| Find one element | driver.findElement(By.css('...')) |
page.locator('...') |
| Find many | driver.findElements(By.css('...')) |
page.locator('...') |
| Find by id | By.id('save') |
page.locator('#save') |
| Find by test id | By.css('[data-testid="save"]') |
page.getByTestId('save') |
| Find by visible text | By.xpath("//button[text()='Save']") |
page.getByRole('button', { name: 'Save' }) |
| Click | await el.click() |
await locator.click() |
| Type | await el.sendKeys('hello') |
await locator.fill('hello') |
| Clear then type | await el.clear(); await el.sendKeys('x') |
await locator.fill('x') |
| Read text | await el.getText() |
await locator.textContent() |
| Wait for an element | driver.wait(until.elementLocated(...), 5000) |
not needed, actions wait |
| Wait for visibility | driver.wait(until.elementIsVisible(el), 5000) |
await expect(locator).toBeVisible() |
| Check text | assert.strictEqual(await el.getText(), '1') |
await expect(locator).toHaveText('1') |
| Check absence | (await driver.findElements(...)).length === 0 |
await expect(locator).toBeHidden() |
| Count elements | (await driver.findElements(...)).length |
await expect(locator).toHaveCount(3) |
| Select dropdown | new Select(el).selectByVisibleText('X') |
await locator.selectOption('X') |
| Take a screenshot | await driver.takeScreenshot() |
automatic on failure |
The four things that change how you think
The table is mechanical. These four are why the migrated suite ends up shorter and steadier.
1. Most of your waiting code disappears
This is the biggest change, and it is why migrated suites often lose 30% of their lines.
In Selenium you write the waiting:
const toast = await driver.wait(
until.elementLocated(By.css('[data-testid="toast"]')),
5000
);
assert.ok((await toast.getText()).includes('Contact created'));
In Playwright the assertion does it:
await expect(page.getByTestId('toast')).toContainText('Contact created');
One line, and it retries until the toast appears or the timeout is reached.
While you are migrating, resist the urge to translate every explicit wait into something. Most of them simply go away. If you find yourself writing waitForSelector before every action, you are writing Selenium in Playwright and losing the benefit.
2. Stale elements stop existing
If you have written much Selenium, you know this error well:
StaleElementReferenceException: element is not attached to the page document
It happens because findElement gives you a reference to one specific element, and a re-render throws that element away.
A Playwright locator is not a reference. It is a description, resolved fresh every time you use it:
const row = page.getByTestId('contact-row').first();
await page.getByTestId('refresh').click();
await expect(row).toBeVisible(); // fine. The locator resolves again
A whole category of bug simply disappears. This alone is worth a lot on an old suite.
3. Browser setup is not your job any more
In Selenium you create the driver and you must close it, usually in an afterEach so it runs even when a test fails. Forget it and browsers pile up on the build machine.
In Playwright the test runner gives each test a fresh, isolated page through the { page } fixture and cleans up afterwards. There is no build() and no quit() in your test code.
4. You assert instead of asserting
Selenium drives the browser and you bring your own assertion library. Playwright includes web-first assertions that retry, which is why they are stronger than a plain assert:
// Selenium: reads once, no retry
assert.strictEqual(await badge.getText(), '1');
// Playwright: retries until it passes or times out
await expect(page.getByTestId('cart-count')).toHaveText('1');
Watch out for the trap here, because it catches everyone coming from Selenium. If you pull the value into a variable first, you lose the retrying and you are back to Selenium behaviour:
const text = await page.getByTestId('cart-count').textContent();
expect(text).toBe('1'); // reads once, unstable
How to run the migration
If you have decided to go ahead, do it in this order.
Run both suites side by side. Do not stop the Selenium suite. It is your safety net while the new one is incomplete.
Move the most valuable tests first, not the easiest ones. Checkout, login, payment. If the migration stalls after two months, you want the important things already moved.
Rewrite, do not translate. A line-by-line translation carries over every explicit wait and every awkward workaround. Read what the test is checking, then write that test in Playwright. It will usually be half the length.
Delete as you go. When a test is migrated and passing for a week, remove the Selenium version. Two suites testing the same thing is how teams end up with 400 tests and no confidence.
Set an end date and tell people. "We will finish next quarter" without a date is how migrations become permanent.
The part people forget
Migrating does not fix bad tests. If your Selenium suite is full of tests that check nothing, you will end up with a Playwright suite full of tests that check nothing, and you will have spent months on it.
Before you migrate, take ten tests and ask one question of each: if this feature broke, would this test fail? If the honest answer is no for most of them, your problem is not the tool. Fix that first, or fix it as part of the rewrite. Otherwise you are paying for new syntax around the same empty checks.