skip to content

When moving a legacy suite to Playwright, how do you decide what a faithful port means?

level: principalimportance: should knowfreq 41%

answer

  1. Fidelity is about intent, not code shape
  2. Same defect must still fail the test
  3. Two categories never cross over
  4. Restructure only where the model fights
  5. Keep the original as an oracle

basics

~20 s

Preserve intent, not implementation. Port each case so it asserts the same thing about the product, but never translate sleeps or hand-written browser lifecycle code, and restructure only where the old test fights Playwright's model rather than everywhere at once.

solid answer

~40 s

Fidelity is measured against the **assertions**, not the code. For each case the question is whether the ported test would fail on the same product defect; if it would, the port is faithful even though the lookups became locators and the waits became `expect(locator)` matchers. Two categories must never be translated literally — timing constants and hand-rolled browser lifecycle — because they are exactly what the new model removes. Restructuring is warranted where the old test worked around a limitation that no longer exists: multiple tabs or origins in one case, stubbed network, or a flow split across several tests because a session could not be shared. Everything else is translated mechanically and reviewed against the original, so the migration stays scoped and each ported case has an oracle.

code

typescript · 8 lines
typescript
import { test, expect } from '@playwright/test';

// Ported one-for-one from the legacy suite; the tag marks the migration slice.
test('@ported renewal quote keeps the no-claims discount', async ({ page }) => {
  await page.goto('https://quotes.example.com/renewal/AB-9931');
  await expect(page.getByTestId('ncd-years')).toHaveText('5');
  await expect(page.getByTestId('quote-total')).toHaveText('£286.40');
});

go deeper

for a junior

Take away one rule: a ported test should still fail for the same product problem as the original. Copying the old code exactly is not what makes it faithful.

for a middle

Be able to name what never crosses over. Timing constants and hand-written browser lifecycle are replaced rather than translated, while data, preconditions and assertions are the coverage you must carry.

for a senior

Show how you would prove fidelity: reintroduce the defect the original caught, keep a mapping from old case to new, and refuse a port that only passes with a raised timeout.

for a principal

Own the scope boundary. Define the invariant, sequence the port in reviewable slices with the original as the oracle, and keep deletion, pipeline arrangement and abstraction redesign as separate decisions with their own owners.

"Port the suite" hides a decision that a lead has to make explicitly, because the two obvious readings both fail. ## The two failure modes - **The literal port.** Every construct is translated one for one, so the sleeps, the shared driver and the "find the first match" habits come across intact. The suite compiles, runs slower than it should, and stays as flaky as it was — with a new framework now blamed for it. - **The unbounded rewrite.** Every case is re-imagined on the new model. Scope grows without a boundary, coverage is lost silently because nothing maps old case to new, and the migration stalls somewhere in the middle with two half-suites. The working definition sits between them: **preserve intent, replace mechanism**. A ported case is faithful when it would fail on the same product defect as the original, regardless of how differently it is written. ## What must never be ported literally 1. **Timing constants.** A fixed pause carried across reproduces the original flakiness. Waiting for a state belongs inside `expect(locator)` matchers, which re-check until they pass. 2. **Browser lifecycle code.** Creating and quitting a browser in setup and teardown is the runner's job now; a ported base class that keeps doing it fights the per-test isolation it is given. 3. **Workarounds for missing capability.** Anything written to compensate for a limitation the new tool does not have — polling loops, retry wrappers around a whole case, sleeping before reading a value — is deleted rather than converted. ## What is safe to translate mechanically | Old element | Treatment | |---|---| | element lookups | translate — they map onto locators nearly one for one | | assertions on visible state | translate into retrying matchers | | test data and preconditions | keep as-is; this is the coverage | | the case's name and intent | keep, so old and new stay traceable | | a flow split across cases to share a session | consider merging — the constraint is gone | | a case that only ever exercised the old tool | drop it from the port and raise it separately | That last row matters: whether a case still earns its place is a **different decision** from whether it ports faithfully, and mixing the two is how migrations acquire an unbounded backlog. Keep the port honest and route "should this test exist at all" to the people who own suite upkeep. ## A sequencing that keeps the port reviewable 1. Port one representative slice first — a handful of quote-comparison cases — and let it set the conventions for locators, hooks and naming. 2. Translate mechanically within a slice, deleting the two forbidden categories above as you go, and review each ported case side by side with its original. 3. Keep the original case as the oracle until its replacement has passed on real product changes, not just once on a green build. 4. Restructure only where a ported case is visibly fighting the model, and record why, so the exception does not silently become the pattern. 5. Retire the original once its replacement has proven it catches the same regressions. ## How you know the port is done Fidelity needs a check that is not "it went green". Useful evidence: the ported case fails when you deliberately reintroduce the defect the original was written for; the mapping from old case to new is complete, so nothing was quietly dropped; and no ported case needed a raised timeout to pass. A case that only passes with a longer timeout has not been ported — it has been rewritten around a symptom, and it will fail again on a slower day. ## The judgment being tested The question is really about scope control. A lead is expected to name the invariant (same defect, same failure), name the exceptions (timing and lifecycle never cross over), and name the boundary (deleting cases, pipeline arrangements and abstraction redesign are separate decisions with their own owners). Answering with "we will rewrite everything properly" or "we will translate it line by line" both signal that the tradeoff has not been thought through.

  • How would you prove a ported case still catches what the original caught?
    Reintroduce the defect the original was written for and confirm the ported case fails on it. A green run proves only that the steps execute; deliberately breaking the behaviour under test is the cheapest evidence that intent survived the translation.
  • A ported case only passes when its timeout is raised. What is your call?
    Treat it as unported and investigate. A retrying assertion that needs a longer budget is reporting either a genuinely slow path in the product or a wait on the wrong signal. Raising the timeout converts a diagnosable failure into an intermittent one.
  • When is restructuring a case during the port justified rather than scope creep?
    When the original was shaped by a constraint that no longer exists — a flow split across cases because a session could not be shared, or tabs and origins the old tool could not hold at once. Record the reason, so the exception does not quietly become the default.

saying these in an interview costs you the question

  • Treating a line-for-line translation as a successful port
  • Rewriting every case at once with no mapping back
  • Raising timeouts until the ported suite goes green
  • Deciding which cases to delete inside the port
  • Judging fidelity by a green run rather than a reintroduced defect