A QA lead at a Series B logistics startup told me his team burned two sprints chasing a test suite that passed locally and failed in CI, every single run, on the exact same login step. The culprit wasn't flaky selectors. It was a cy.wait(2000) someone had dropped in six months earlier as a quick fix, and CI simply ran slower than a laptop. Cypress's own documentation calls fixed waits like that an anti-pattern for exactly this reason, arguing you should wait on a network alias or an assertion instead of a clock (Cypress, Best Practices). Interviewers ask about this constantly because it's the fastest way to tell who has actually debugged a flaky suite from who has only followed a getting-started tutorial.
Cypress interviews rarely stay abstract for long. A recruiter might open with "what is Cypress and how is it different from Selenium," but within ten minutes most loops pivot to something concrete: how retry-ability actually works internally, how you'd stub a slow third-party API without breaking a real end-to-end test elsewhere in the suite, or why a test that clicks a button one line after a page navigation sometimes fails and sometimes doesn't. The architecture matters here in a way it doesn't for a lot of testing tools, because Cypress runs its test code inside the same run loop as the application under test, not as a separate remote-controlled process. That single design decision explains most of the "gotcha" questions on this page.
This page covers Cypress interview questions across seven areas: core architecture and how it differs from Selenium/Playwright, selectors and command chaining, automatic waiting and retry-ability, network requests with cy.intercept, fixtures and custom commands, test isolation with hooks and cy.session, and the harder CI, configuration, and cross-origin questions that come up once a team has actually run Cypress in production for a year or more.
Architecture: what Cypress actually is and how it differs from Selenium
Every loop opens here, even for a candidate who's shipped Cypress suites for years. It's a warm-up, but a shallow answer signals the candidate never looked past the API surface.
Easy questions
14Cypress is a JavaScript end-to-end testing framework that runs directly inside the browser, in the same run loop as the application under test, rather than driving the browser remotely over a wire protocol the way Selenium's WebDriver does. Test code, application code, and the DOM all live in the same execution context, which is what gives Cypress native access to network traffic, the DOM, and JavaScript objects without a serialization layer in between.
The practical upshot is speed and stability for the things Cypress is good at, but it also means Cypress historically couldn't drive two different browser tabs or two fully separate origins in one continuous test the way a WebDriver-based tool can, since it isn't remote-controlling an external browser process the same way.
The Test Runner is the interactive browser window Cypress opens when you run cypress open. On one side it renders your application exactly as a real user would see it, and on the other it shows a live command log, every command, assertion, and network request the test triggered, in the order it ran.
Clicking any entry in that command log snapshots the DOM at that exact moment, which is the "time travel" feature people mean when they talk about debugging Cypress tests visually instead of staring at stack traces.
cypress open launches the interactive Test Runner with the live browser and command log, meant for writing and debugging tests locally. cypress run executes tests headlessly from the command line, prints results to the terminal, and is what CI pipelines actually invoke.
cypress open --e2e --browser chrome
cypress run --browser chrome --spec "cypress/e2e/checkout.cy.js"No, not directly. Cypress tests run inside a browser, so it can test the mobile web version of a site rendered in a browser, but it has no way to drive a native iOS or Android app the way Appium does through WebDriver. Teams that need both usually keep Cypress for web and a separate tool for native mobile coverage rather than trying to force one framework to cover both.
A class or ID is usually tied to styling or a framework's internal state, so a designer renaming a class for a CSS refactor, or a build tool hashing class names, breaks tests that were never meant to be coupled to appearance in the first place. Cypress's best practices guide is explicit that selectors should be resilient to changes in CSS and JS, and recommends a dedicated attribute like data-cy that exists purely to be a test hook (Cypress, Best Practices).
<button class="btn btn-primary submit-btn" data-cy="checkout-submit">Place order</button>cy.get('[data-cy=checkout-submit]').click() // survives a CSS rename, a class swap, a redesigncy.get searches the whole document for elements matching a selector. cy.find searches only within the subject already yielded by a previous command, scoping the query down to a specific container instead of the entire page.
cy.get('[data-cy=order-summary]').find('[data-cy=line-item]').should('have.length', 3)find matters most on pages with repeated components, a list of cards each containing a similarly-named button, where cy.get alone would match every card's button instead of just the one inside the card you're actually testing.
cy.intercept() lets you observe or control network requests matching a URL pattern, without changing any application code. Spying means letting the real request go through to the real server while Cypress just watches it, useful for asserting a request happened with the right payload. Stubbing means Cypress intercepts the request and returns a fake response you define, body, status code, headers, and delay all under your control, so the real backend is never hit.
// spy: real request still fires
cy.intercept('POST', '/api/orders').as('createOrder')
// stub: fake response, real backend never called
cy.intercept('GET', '/api/orders', { fixture: 'orders.json' }).as('getOrders')Aliasing an intercept with.as() and then calling cy.wait('@alias') pauses the test until that specific request actually completes, whatever that takes, instead of guessing a duration. It also gives you a much more useful failure message if something goes wrong, Cypress tells you no request matching that route ever fired, rather than a generic timeout on some downstream element that never appeared because the data never loaded.
cy.intercept('GET', '/api/dashboard').as('getDashboard')
cy.visit('/dashboard')
cy.wait('@getDashboard')
cy.get('[data-cy=widget]').should('be.visible')A fixture is a static JSON (or text, or image) file stored under cypress/fixtures, used as fixed test data, usually paired with cy.intercept() to feed a stubbed response, or read directly with cy.fixture() when a test just needs sample data without a network call involved.
// cypress/fixtures/user.json: { "name": "Ada Lovelace", "role": "admin" }
cy.fixture('user.json').then((user) => {
expect(user.role).to.eq('admin')
})A custom command centralizes a repeated action, logging in, adding an item to a cart, in one place, so a single UI change only requires one edit instead of hunting down every test file that duplicated the same steps. It also reads better: cy.login('user@test.com') tells you what a test is doing at a glance, three raw cy.get().type() lines don't.
// cypress/support/commands.js
Cypress.Commands.add('login', (email, password) => {
cy.get('[data-cy=email]').type(email)
cy.get('[data-cy=password]').type(password)
cy.get('[data-cy=submit]').click()
})
// in a test
cy.login('ada@example.com', 'password123')Tests need to be able to run independently, in any order, and even in isolation, one at a time, for the suite to be trustworthy and debuggable. A test that only passes because an earlier test happened to create a record first will fail the moment someone runs it alone to debug it, uses cypress run --spec on just that file, or the test order changes for any reason, all of which happen constantly in a real CI setup.
before runs once, at the very start of a describe block, regardless of how many tests are inside it. beforeEach runs before every single test in that block. before is appropriate for a genuinely one-time, expensive setup step that every test in the block can safely share without mutating it, seeding a read-only reference dataset, say, not creating anything a test is going to modify.
Setting baseUrl means cy.visit('/dashboard') resolves against that base rather than requiring a full URL in every test, which matters more than it sounds like once a suite runs against different environments, local, staging, a preview deploy, since only the config value changes, not every individual test file.
module.exports = {
e2e: {
baseUrl: 'https://staging.example.com',
},
}They're aliases of the same underlying function..and() exists purely for readability when you want to chain a second assertion onto the same subject without repeating should(), it doesn't behave any differently underneath.
cy.get('[data-cy=status]').should('be.visible').and('contain', 'Complete')Medium questions
23Cypress commands don't execute the moment you call them. Calling cy.get() enqueues a command onto an internal queue, and that queue only starts draining after the current test function has finished running, then executes serially, one command completing before the next begins.
it('looks synchronous but is not', () => {
cy.visit('/dashboard')
cy.get('.user-name').should('contain', 'Ada')
console.log('this logs before the visit or get ever run')
})That last console.log fires immediately, before either Cypress command actually executes, because none of the enqueued commands have run yet when that line is reached. Candidates who've only skimmed the docs frequently assume Cypress commands return promises you can await directly, and get bitten the first time they try to mix a raw async/await pattern with the command queue.
For a JavaScript or TypeScript-heavy frontend team testing a single web app, Cypress usually wins on developer experience: faster feedback loops, time-travel debugging in the runner, and no separate WebDriver server to manage. Selenium still wins when you need true multi-tab, multi-window flows, cross-browser coverage that includes older browsers Cypress doesn't support, or driving anything outside a browser, like a native mobile app through Appium's shared WebDriver protocol.
The honest answer to give in an interview is that it depends on what the team is actually testing, not a blanket "Cypress is better." Saying that out loud, with the trade-off attached, reads as more senior than picking a side.
.then() yields the subject of the previous Cypress command as its callback argument, letting you drop into plain JavaScript logic in the middle of a Cypress chain, read a value, do a conditional check, whatever regular code needs to do. It looks like a promise's.then() syntactically, but the callback doesn't return a promise that resolves the chain the same way, it participates in Cypress's own internal queue.
cy.get('[data-cy=cart-total]').then(($el) => {
const total = parseFloat($el.text().replace('$', ''))
expect(total).to.be.greaterThan(0)
})Because cy.get() doesn't return the element synchronously, it enqueues a command that hasn't run yet at the point where the assignment would happen. Assigning const el = cy.get(...) just stores the Cypress chainable object, not the resolved DOM element, so any code that reads el immediately afterward reads garbage or undefined.
// wrong
let text
cy.get('.title').then(($el) => { text = $el.text() })
cy.log(text) // may run before the.then() callback populates text
// right
cy.get('.title').invoke('text').as('titleText')
cy.get('@titleText').should('eq', 'Dashboard')cy.wrap() takes a plain JavaScript value, an object, an array, a jQuery element, whatever, and wraps it as a Cypress-chainable subject so you can call Cypress commands and assertions on it as if it had come from a normal Cypress command. It's the bridge you reach for when you've dropped into a.then() callback and need to hand a value back into the Cypress chain.
cy.get('[data-cy=items]').then(($items) => {
const visibleItems = $items.filter(':visible')
cy.wrap(visibleItems).should('have.length', 2)
})Most Cypress commands and all built-in assertions retry automatically for a set timeout period, four seconds by default for most commands, rather than checking the condition once and immediately failing. Cypress re-queries the DOM and re-checks the assertion repeatedly during that window, and only throws an error once the timeout is reached without success.
cy.get('[data-cy=status]').should('contain', 'Complete')
// retries the query and the assertion together for up to 4s
// until the element both exists and contains that text, or the timeout expiresThis is the single most important mechanical detail in the whole framework. Most flaky-test complaints trace back to a candidate fighting retry-ability instead of using it, adding a manual wait when a should() assertion chained onto the right command would have solved the same problem for free.
An implicit assertion is baked into a command itself, cy.get() asserts an element exists,.click() asserts the target is in an actionable state (visible, not disabled, not covered by another element) before it fires. An explicit assertion is one you write yourself with should() or expect(), checking something the command's built-in behavior wouldn't have caught on its own, like a specific text value or a class name.
A fixed wait makes an assumption about timing that's true on your machine right now and false everywhere else eventually, a slower CI runner, a database under more load, a network hiccup. Cypress's best practices guide calls out waiting for arbitrary time periods as something to avoid specifically because it's both slow when the app is fast and unreliable when the app is slower than the number you picked (Cypress, Best Practices). Waiting on a network alias or an assertion instead adjusts itself to however long the real condition actually takes, with a real timeout as the failure backstop.
Stubbing makes tests fast, typically well under 20ms response time versus a real network round trip, and gives full control over edge cases that are hard to reproduce against a real server, error states, empty states, slow responses. The trade-off is that a stubbed response is only as accurate as whatever fixture you wrote, so it can't catch a real contract break between frontend and backend. Cypress's own guidance is to use stubbing for the majority of tests but keep at least one true end-to-end test per critical flow that hits the real backend (Cypress, Network Requests).
Alias the intercept, then read the request property off the interception object cy.wait() yields, since it exposes both the outgoing request and the incoming response, not just one side of the exchange.
cy.intercept('POST', '/api/orders').as('createOrder')
cy.get('[data-cy=submit]').click()
cy.wait('@createOrder').its('request.body').should('deep.include', {
itemId: 'sku-123',
quantity: 2,
})Pass a delay option to slow down a stubbed response, or a statusCode outside the 200 range to simulate a server error, both directly in the cy.intercept() call. This is one of the clearer wins stubbing has over hitting a real server, you genuinely cannot make a production API reliably return a 500 on demand for a test, but you can trivially fake it here.
cy.intercept('GET', '/api/orders', { statusCode: 500, body: { error: 'Server error' } }).as('getOrdersFail')
cy.intercept('GET', '/api/orders', { delay: 3000, fixture: 'orders.json' }).as('getOrdersSlow')Stub the specific endpoint to return a 429 with whatever body shape the real API sends on a rate limit, then assert the UI shows the right message or retry behavior. This is exactly the kind of edge case cy.intercept() exists for, since reliably provoking a real 429 from a live third-party API on demand in CI is close to impossible.
cy.intercept('POST', '/api/checkout', {
statusCode: 429,
body: { error: 'Too many requests' },
}).as('checkoutRateLimited')
cy.get('[data-cy=submit]').click()
cy.wait('@checkoutRateLimited')
cy.get('[data-cy=error-banner]').should('contain', 'Too many requests')add() creates a brand-new command with a new name. overwrite() replaces the implementation of an existing command, built-in or custom, letting you change how something like cy.visit() behaves globally, adding a default header on every visit, say, without touching every individual test that calls it.
Cypress.Commands.overwrite('visit', (originalFn, url, options) => {
return originalFn(url, {...options, timeout: 20000 })
})Cypress's team has historically pushed back on heavy classic page-object patterns borrowed from Selenium, since Cypress commands already chain and retry in ways a wrapped class method tends to break. A lighter approach that still works well: export plain functions or objects grouping related selectors and actions per page, without wrapping them in classes that hide the chainable Cypress API.
// cypress/support/pages/checkout.js
export const checkoutPage = {
submit: () => cy.get('[data-cy=submit]'),
errorBanner: () => cy.get('[data-cy=error-banner]'),
}
checkoutPage.submit().click()
checkoutPage.errorBanner().should('be.visible')commands.js is where you define custom commands with Cypress.Commands.add(). e2e.js is the file Cypress loads before every spec file runs, typically used to import commands.js and to set up global hooks or configuration that should apply across the entire suite, a global beforeEach that visits a base URL, for instance, or a call to Cypress.on() for uncaught exception handling.
A custom command gives every spec file the choice to call it or not, so tests that don't need to be logged in (a public marketing page, say) skip it entirely. A global beforeEach in support/e2e.js runs before literally every test in the suite whether that test needs it or not, which is the wrong tool the moment even one spec file doesn't want that setup. The safer default is a custom command called explicitly per spec file's own beforeEach, not a suite-wide global hook.
If a test fails or the browser is stopped mid-run, an afterEach hook scheduled to clean up state may never actually execute, leaving stale data behind for the next test to trip over. A beforeEach hook that resets state runs before every test regardless of what happened in the previous one, so cleanup happens reliably even after a crash or an interrupted run, rather than being contingent on the prior test finishing cleanly.
Logging in through the actual UI, typing an email, typing a password, clicking submit, in every single test adds real seconds to every spec and couples every unrelated test to the login flow staying stable. cy.session() caches the result of an authentication step, cookies, localStorage, sessionStorage, across tests within the same spec run, so the expensive login only actually happens once and gets restored instantly afterward, while Cypress still validates the session is still good before reusing it.
Cypress.Commands.add('loginBySession', (email, password) => {
cy.session([email, password], () => {
cy.visit('/login')
cy.get('[data-cy=email]').type(email)
cy.get('[data-cy=password]').type(password)
cy.get('[data-cy=submit]').click()
cy.url().should('include', '/dashboard')
})
})Driving the UI to create prerequisite state, clicking through a multi-step form just to get a record into the state a later assertion needs, is slow and adds points of failure that have nothing to do with what the test is actually verifying. cy.request() bypasses the browser entirely and hits the API directly, so setup that isn't the thing under test happens fast and reliably, leaving the UI interactions in the test focused on what actually needs UI-level verification.
before(() => {
cy.request('POST', '/api/test/seed-orders', { count: 3 })
})e2e testing loads your actual running application in a real browser and tests full user flows against it. Component testing mounts an individual component in isolation, using the framework's own rendering, React, Vue, whatever you've configured, without needing the whole app running, so you can test a component's props and behavior directly, closer to a unit test than a full end-to-end flow.
CI runners are commonly slower and more resource-constrained than a developer's laptop, shared CPU, less memory, sometimes a cold-started database, so anything timing-sensitive that happened to squeak by locally has a much better chance of tripping the default timeout in CI. This is the exact scenario Cypress's retry-ability and network-aliasing patterns exist to protect against, a fixed cy.wait(500) that passes locally is often the first thing to break once the same suite runs on a slower CI box.
force: true skips Cypress's actionability checks, visibility, not being disabled, not being covered by another element, and fires the event regardless. It makes a flaky-looking test pass, but it usually papers over a real problem: an element that a real user genuinely couldn't click either, hidden behind a modal, covered by a sticky header, or still disabled while data loads. Reaching for it as a first fix instead of asking why the element isn't actionable is one of the more common bad habits interviewers probe for.
should('not.exist') is the right assertion, and it does retry, so timing generally isn't the problem. The actual trap is asserting on a stale reference from a chain that already resolved before the removing action ran. Re-querying from cy.get() fresh, after the action that should remove the element, and letting should('not.exist') retry against that fresh query is what makes the assertion trustworthy.
cy.get('[data-cy=delete]').click()
cy.get('[data-cy=item-42]').should('not.exist')Hard questions
11Chaining should() or expect-style assertions directly onto a command means that command re-runs as part of the retry: cy.get() re-queries the DOM on every retry attempt until the assertion passes. Splitting the same check into two separate statements breaks that link, so the get() only runs once and grabs whatever element existed at that instant, and the later assertion checks a stale snapshot instead of a live, retried query.
// retries correctly: get() re-runs each attempt
cy.get('[data-cy=count]').should('have.text', '5')
// flaky: get() runs once, grabs a possibly-stale element, then asserts on it
const el = cy.get('[data-cy=count]')
expect(el).to.have.text('5')The button existed at the moment Cypress first found it, but a framework re-render (React, Vue, Angular, whatever) replaced the underlying DOM node before the click actually landed, so the reference Cypress held became stale. This is a classic single-page-app race condition, not a Cypress bug, and it happens more often against apps that re-render aggressively on initial load or on a data fetch completing.
The fix is usually to assert on something that only becomes true once the app has settled, the button becoming enabled, a loading spinner disappearing, before the click, rather than clicking the instant the element first appears in the DOM.
cy.visit('/checkout')
cy.get('[data-cy=submit]').should('not.be.disabled').click()Yes. Instead of a plain URL string, cy.intercept() accepts a routeMatcher object where you can constrain by method, URL glob, query string, and headers all at once, letting you distinguish between two requests to the same path that differ only in a query parameter or an auth header.
cy.intercept({
method: 'GET',
url: '/api/orders',
query: { status: 'pending' },
}, { fixture: 'pending-orders.json' }).as('getPending')Cypress clears cookies, localStorage, and sessionStorage between tests by default in current versions, specifically to enforce the test-independence principle rather than leaving it to individual authors to remember. This is why cy.session() exists as a deliberate, explicit opt-in to preserve authentication state across that boundary, rather than the framework silently carrying state forward and making tests order-dependent by accident.
No, not if the session ID (the arguments you pass as the first parameter to cy.session()) match. Cypress caches the session the first time it's created and restores the cached cookies and storage on subsequent calls with the same ID, only re-running the actual setup function if the cached session fails validation or the ID changes. That's what makes it fast across dozens of tests, the expensive login only actually runs the number of distinct sessions you need, not the number of tests that use one.
My first assumption is a hidden dependency between tests, shared state one test relies on that an earlier test happens to set up, rather than a Cypress bug. I'd run the failing test alone with cy.only or a filtered spec first, before changing anything, specifically to confirm whether it passes standalone. If it does pass alone and only fails as part of the full run, that's close to a confirmed test-isolation problem, not a real application bug or a genuine race condition.
Before Cypress 14, Cypress automatically injected document.domain into pages so tests could navigate between different subdomains of the same superdomain, www.example.com to docs.example.com, without any special handling, since both shared example.com as their superdomain. As of Cypress 14.0.0, that automatic injection was removed by default, so any origin change at all, even between subdomains of the same site, now requires wrapping the cross-origin commands in cy.origin() explicitly (Cypress, Cross-Origin Testing).
cy.visit('https://app.example.com')
cy.origin('https://accounts.example.com', () => {
cy.get('[data-cy=email]').type('ada@example.com')
cy.get('[data-cy=submit]').click()
})Any team that upgraded past v14 without knowing this saw previously-passing tests suddenly time out on navigation, which makes this a real "did you actually keep up with the changelog" question rather than trivia.
Cypress commands issued after that navigation time out rather than throwing an immediate, clear error, because Cypress loses its ability to communicate with the new origin's window the moment the domain changes without cy.origin() wrapping the commands meant to run there. The failure mode reads like a generic element-not-found timeout, which is what makes it confusing to debug the first time a team hits it.
Two honest options. Wrap the interaction on the third-party domain in cy.origin() if the flow genuinely needs to be exercised end to end and the provider's page is stable enough to select against reliably. Or, more commonly for a payment provider specifically, stub the redirect and response with cy.intercept() and test your own app's handling of the success or failure callback, since real payment providers actively fight automated browser testing with bot detection and aren't something you want a CI pipeline depending on.
Cypress doesn't have native, built-in commands for verifying file downloads the way it does for DOM assertions, since a downloaded file lands in the OS filesystem, outside the browser context Cypress directly controls. The common workaround is checking that the downloaded file exists on disk using Node-side code through a Cypress task, cy.task(), which runs in the Node process behind the scenes rather than in the browser, combined with reading the file's contents or checksum to confirm it downloaded correctly.
setupNodeEvents runs in Node, outside the browser sandbox, so it's where you'd reach for anything a browser-sandboxed test can't do directly: reading or writing files on disk, querying a database directly to set up test data, or modifying browser launch arguments before a browser even opens. cy.task() is how a spec file calls into that Node-side code and gets a value back.
// cypress.config.js
setupNodeEvents(on, config) {
on('task', {
seedDatabase() {
return seedTestDatabase() // real Node code, real DB access
},
})
}How to prepare for a Cypress interview
Skip another read-through of the command reference. Build one small suite against a real app that forces you into the patterns interviewers actually probe: a login flow cached with cy.session(), a checkout test that stubs a slow or failing payment API with cy.intercept(), and one deliberately flaky test you fix by replacing a cy.wait(number) with an aliased network wait. Fixing your own flaky test teaches retry-ability faster than reading the docs section on it ever will.
Across mock interview sessions run through LastRoundAI, candidates trip on the cross-origin and cy.session questions more than any other pair on this page, not because the concepts are hard, but because most tutorials never cover them and most personal projects never get big enough to need them. The retry-ability chaining question, why should() directly on cy.get() behaves differently than a separate expect(), catches almost everyone the first time too, including people with real production Cypress experience who never had to explain the mechanism out loud before.
Explaining retry-ability on paper is not the same as defending it when an interviewer hands you a flaky test snippet and asks you to find the bug live. LastRoundAI's mock interview mode runs live coding rounds with real-time follow-up questions in your browser, and the free plan includes 15 credits a month that reset monthly rather than piling up unused. Starter is $19/mo if a handful of sessions isn't enough runway.
Once your answers hold up under a follow-up, the slower part of the job hunt is usually getting in front of enough QA and frontend roles that actually test Cypress instead of listing it as a bullet point nobody checks. Auto-Apply queues tailored applications for your review, 10 a month on the free plan, up to 400 a month on the Ultimate plan, and nothing goes out until you approve it.
Questions about either product go to contact@lastroundai.com. That's the only inbox we check.
How this list was built
Worth being straight about where these questions come from, because plenty of pages in this category are not. The set was compiled from a research pass across official documentation, vendor release notes, published engineering writing and public discussion of hiring processes, then cross-checked against the current version of each technology so nothing here describes behaviour that has since changed.
What that means in practice: these are the questions the material supports as reasonable and current for this role, not a transcript of any one company's loop. We have not sat in on your interview and we are not going to claim we have. Treat the list as well-sourced preparation rather than a leaked question bank, and expect your panel to phrase things their own way.
If you spot something out of date, tell us at contact@lastroundai.com and we will fix it.
Frequently asked questions
How long does it take to prepare for a Cypress interview?
If you already work with Cypress day to day, a focused week on the areas you avoid in practice is usually enough. Coming in cold, expect three to four weeks. The gap is rarely knowledge; it is being able to explain something you normally just use.
What Cypress topics come up most often?
Interviewers concentrate on the parts that cause production incidents rather than the parts that are pleasant to learn. Expect the fundamentals to be assumed and the follow-up questions to sit one layer below what a tutorial covers.
Do I need hands-on Cypress experience to pass?
It shows quickly either way. Textbook answers hold up until the interviewer asks what you did when it broke, and that is usually the question that separates candidates. A small real project you can discuss honestly beats a longer list of familiarity claims.
Is Cypress still worth learning in 2026?
For interview purposes the question is really whether the teams you are targeting use it, which is worth checking against their actual job postings rather than general popularity rankings. Where it is in use it tends to be deeply embedded and slow to replace.
