05 · Case study
Selenium — Java, C# or PythonPlaywright · TypeScript · Page Objects
The system
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
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.
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.
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.
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.
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.
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
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.