What does auto-waiting do in Playwright and Cypress, how do the two differ architecturally, and what can neither of them test?
Auto-waiting runs actionability checks before an action and retries assertions that are bound to a locator, so it removes timing races but never wrongness. Playwright drives the browser from a separate Node process.
What the interviewer is scoring
- Does the candidate describe auto-waiting as a set of readiness checks rather than as a smarter sleep
- Whether they can name a way to write a test in either tool that defeats auto-waiting entirely
- That the in-browser versus out-of-process split is used to explain the capability differences, not just stated
- Whether the stated limitations are the tools' real ones rather than a list of missing features
- Can they place end-to-end tests in a wider strategy instead of treating the tool choice as the strategy
Answer
Auto-waiting is a readiness contract, not a longer sleep
Before Playwright performs an action it checks a defined list of conditions on the target and retries until they all hold or the timeout expires. For a click that list is: the element is attached to the DOM, visible, stable in the sense that its bounding box has not moved between two consecutive animation frames, able to receive events as determined by a hit-target test at the point of the click, and enabled. fill adds editable. Each check exists because it corresponds to a failure people used to write sleeps for, and the animation-stability check is the one that quietly eliminates the mis-click into a sliding modal.
The second half is assertion retrying. expect(locator).toHaveText("Order placed") re-evaluates the whole assertion on a polling loop until it passes or the expect timeout runs out, which is a different thing from retrying a lookup: the element can exist from the first millisecond and the assertion still waits for its contents to settle.
Cypress reaches a similar place by a different route. Its actionability checks cover visibility, not being disabled or read-only, not being detached, not animating, and not being covered by another element, and it scrolls the element into view first. Its retry-ability applies to queries and their assertions, retrying the last query in the chain until the attached assertion passes or the command timeout expires. Commands with side effects do not retry, which is correct behaviour — a click that fired must not fire twice.
The important consequence in both tools is that auto-waiting is per-locator and per-assertion. Nothing in it knows about your application's own notion of finished, so if the thing you should have waited for is a background job or a request whose response updates a different part of the page, auto-waiting will happily confirm that the element you asked about is ready while the data behind it is stale.
How to defeat it in one line
Both tools let you step outside the retrying path, and that line is where most residual flakiness in a modern suite comes from.
// No retry: textContent() resolves once, and expect() receives a plain
// string. The auto-waiting was thrown away at the await.
expect(await page.locator('#status').textContent()).toBe('Order placed');
// Retries the assertion, re-resolving the locator on every attempt.
await expect(page.locator('#status')).toHaveText('Order placed');
The Cypress equivalent is doing the comparison inside a .then callback, where the value has already been yielded and the retry loop has exited. In both cases the code reads as though the framework is protecting you, and it is not. A locator is a description that gets re-resolved on each use, which is also why neither tool produces Selenium's stale element failures — but only while you keep the assertion attached to the description rather than to a value you have already extracted.
One architectural decision, most of the differences
Playwright's test code runs in a Node process outside the browser and speaks to it over a single connection to the browser's remote debugging protocol, using Chromium's protocol directly and patched builds for Firefox and WebKit. Cypress inverts this: your spec is bundled and executed inside the browser, in the same event loop as the application under test, which is loaded in an iframe, with a Node-side process managing the browser and proxying network traffic.
Being outside buys isolation and multiplicity. Playwright creates browser contexts that are independent cookie and storage jars, so two logged-in users can be scripted in one test, and it addresses multiple pages, tabs, popups and origins without ceremony. Because the runner is an ordinary Node process, tests parallelise across worker processes and test code can use anything in Node — a database client, a file system check, a generated fixture — inline.
Being inside buys intimacy. Cypress test code shares a JavaScript realm with the application, so it can stub application-visible functions, replace the clock, and inspect state with no protocol boundary in between, and its command log lets you step back through DOM snapshots because the test and the page are the same context. The cost is that the browser's own security model now applies to your test. Cross-origin work needs cy.origin() and remains constrained; multiple browser tabs are not supported and window.open is redirected into the same tab; you cannot drive two browsers at once; Node APIs are unavailable in the spec and must be reached through cy.task; and because commands are enqueued rather than awaited, ordinary async/await over Cypress commands does not behave the way it looks like it should. Parallelisation is across machines and specs rather than within a browser instance.
Neither architecture is wrong. If your hardest problems are multi-user, multi-tab or multi-origin, the out-of-process model is doing something the other one structurally cannot; if your hardest problems are inside your own front-end state, the in-browser model hands you a screwdriver the other one lacks.
What neither tool covers, whatever the changelog says
Both drive browser engines, so the honest boundary is the browser. Playwright's WebKit is a WebKit build, not Safari: it shares the engine and not the shipped browser, so a Safari-specific rendering or media behaviour can pass in Playwright and fail on a customer's Mac. Cypress's WebKit support is experimental and is itself built on Playwright's WebKit, so it inherits the same caveat. Neither runs on a real iOS device, and testing native mobile applications is out of scope for both, with Playwright's Android support limited to Chrome on Android and marked experimental.
Anything the operating system owns is also outside. File uploads work in both only because the native file chooser is bypassed and the input is set programmatically, print and save dialogs are not testable, and OS-level credential prompts, certificate dialogs and drag-and-drop from the desktop are not reachable. Browser extensions are close to untestable in this layer; Playwright loads Chromium extensions only through a persistent context, and that is the extent of it.
Then there is the category people forget to declare: everything you stubbed. A suite that intercepts the payment provider has not tested the payment provider, and the same goes for email delivery, SMS, webhooks arriving from outside, and third-party scripts. Neither tool measures load or capacity, and neither judges accessibility — an automated axe pass finds a subset of machine-detectable issues and says nothing about whether the flow is usable with a screen reader.
Where the accuracy usually slips
Two claims separate a careful answer here. The first is treating auto-waiting as a guarantee of correctness. It makes the interaction deterministic; it does not make an assertion right, does not know about your application's asynchronous work, and evaporates the moment you compare an extracted value instead of a locator. A candidate who says "we moved to Playwright and the flakiness went away" without being able to name which failure class it removed has usually mistaken a shorter feedback loop for a fixed suite.
The second is reciting a feature comparison from marketing pages rather than from the constraint that generates it. The interesting sentence is not "Cypress cannot do multiple tabs"; it is that a test living inside the page is subject to the page's security boundary, from which the tab, origin and multi-browser limits all follow, and so do the capabilities Cypress has in exchange. If you can derive the list, you can also work out which side a new requirement falls on without looking anything up.
Likely follow-ups
- Your assertion passes locally and fails in CI on the same build. Where do you look first in each tool?
- How would you test a flow that opens a payment provider on a different origin in a new tab?
- When is running test code inside the browser an advantage you would deliberately choose?
- What belongs in this layer at all, once you have component tests and API tests?
Related questions
- How does your order placement change on a venue that allocates pro rata within a price level instead of first in, first out?hardSame kind of round: concept6 min
- Adapter, decorator and facade - what concrete problem does each one solve, and where would reaching for it be over-engineering?mediumSame kind of round: concept6 min
- How would you design a thread-safe component, and why is adding synchronized to every method not a design?hardSame kind of round: concept7 min
- Where does encapsulation or polymorphism change a design, and how do you decide between composition and inheritance?mediumSame kind of round: concept6 min
- When is a factory the right answer, when is a builder, and when should you just call the constructor?mediumSame kind of round: concept7 min
- How do you test a Node service, and what do you refuse to mock?mediumSame kind of round: concept5 min
- How does Node load your modules, and how would you make a Node service use more than one core?mediumSame kind of round: concept6 min
- Angular, Vue and React are answers to the same problems. Compare how each handles change detection, reactivity and dependency injection.hardSame kind of round: concept5 min