← All cases

05 · Case study

Selenium to Playwright migration

Selenium — Java, C# or PythonPlaywright · TypeScript · Page Objects

The system

Source languages
3
Readiness gates
31
Sign-off claims
35
Source repo
untouched

The problem

The regression suite is the only written record of how the application is supposed to behave, and it is held to a driver API, explicit waits and a page-object layer that has drifted over years. The two usual ways out are both bad. A hand rewrite takes months and stops the moment someone is pulled onto a release. A model asked to “convert this to Playwright” renames the calls, invents accessible names for fields it has never seen rendered, and hands back a suite where a failure tells you nothing — because you cannot say whether the application broke or the migration lied.

What ran

  1. Read the suite

    The same structural analysis the workbench runs on any repository, over the Selenium source: page objects with their action methods, locators sitting inline in tests, data-driven sets, and every explicit wait. That becomes a Test IR that no longer knows whether it came from Java, C# or Python.

  2. Ask the one thing the source cannot answer

    The application URL. Everything else is derived. A questionnaire that asks what a parser already knows is how a migration gets configured wrongly by a person in a hurry.

  3. Emit the project, deterministically

    Page Objects with readonly locators that select exactly what the Selenium originals selected, specs grouped by feature carrying the suite’s own execution tags, one spec per source test, and the hardcoded data lifted into typed files a spec reads by name.

  4. Port each test under a build gate

    The steps are ported through the Page Object, then the project is built. The gate checks types, checks that every spec lists, and checks that a test which runs actually asserts something — so an unfinished test stays unfinished instead of passing empty.

  5. Measure the result against the suite it came from

    Any Selenium wait or driver call that survived, any fixed sleep reintroduced under a Playwright name, any locator that compiles but can match nothing, any spec still hoarding locators that belong on a Page Object, any source test that reached no spec at all.

  6. Sign off, claim by claim

    Thirty-five claims, each settled against the artifact that would look different if the claim were false. A claim whose evidence is missing reads NOT VERIFIED; it never passes by silence.

What it did

Locators are provable equivalents, not guesses

By.id("email") becomes page.locator('#email'), not getByLabel('Email'). An accessible name is a property of the rendered page, not of source code — an id of email does not tell you the label reads “Email”, and a guess produces a locator that matches nothing. By.linkText is the one exception, because the value is the link’s visible text, which is evidence rather than inference.

One file reads the environment

src/config/environment.ts is the only file in the project that touches process.env; the runner config imports the base URL from it rather than resolving its own. Every address is parsed and its scheme checked before the first test, so BASE_URL=test is refused by name at run start instead of failing later inside a navigation. A value the source assembled at run time is reported and left out, because a template is not an address.

Unfinished work looks unfinished

Unported specs are test.fixme, action stubs throw, and an empty data set throws rather than returning a blank. Every source test is accounted for as ported, pending, or out of scope with a reason. Nothing incomplete can report a pass.

Where it ended

A self-contained Playwright project that type-checks, lints and lists every spec, with a migration report written from what is on disk. It does not claim your suite passes, and it says so: no application was running and no browser was launched at migration time. The Selenium repository is left exactly as it was — the migration writes into its own folder and touches nothing else.

What happens next

  • Point it at a running application and run it. That is the first thing you do with it, and it is the one thing the migration deliberately does not claim.
  • Run the healer against a live page to promote the plain locators to role-based ones. It reads the accessibility tree and anchors on the Selenium original recorded above every Page Object field.
  • Take the pending specs. They are already listed, already named, and already failing loudly.

The whole design follows from one refusal: never write a locator that cannot be proved equivalent to the one it replaces. It makes the output look plainer than a demo would, and it is the only reason a red test after the migration means something.