Software Testing & QA Tools
Cypress
Cypress is an open-source (with a paid Cypress Cloud layer for recording, parallelization, and analytics) end-to-end testing framework built specifically for testing web applications in a real browser. Unlike tools that drive the browser from an external process, Cypress's test code runs inside the same run loop as the application under test, which is the source of both its biggest usability wins — time-travel debugging, automatic retries, real-time reload — and its architectural limits, like needing workarounds for multi-tab or multi-origin flows. It's JavaScript/TypeScript-only, and while Chromium-family browsers and Firefox are its stable, officially supported targets, WebKit (Safari's engine) is available only as an experimental, opt-in mode — built, notably, on Playwright's own WebKit binaries.
Why it matters
- It's built around the developer feedback loop, not just test execution
- The Cypress Test Runner shows the app live in a browser pane next to a command log; clicking any past command in that log 'time-travels' the app to a DOM snapshot from that moment, which is a genuinely different debugging experience from reading a stack trace after the fact.
- It automatically retries assertions, similar in spirit to Playwright's auto-waiting
- Commands like cy.get() and most assertions retry against the DOM for a configurable timeout before failing, so tests don't need explicit sleeps for most async UI updates.
- It has a large, mature ecosystem specifically for JS-stack teams
- Plugins for component testing, accessibility checks (cypress-axe), visual diffing, and CI integrations are extensive because Cypress has been the default JS E2E choice since roughly 2017, well before Playwright existed.
- It also does component testing, not just full end-to-end
- Cypress Component Testing mounts an individual React, Vue, Angular, or Svelte component in a real browser and tests it in isolation, using the same command API as full E2E tests.
Running inside the browser: what that actually means
Cypress injects its test runner into the same browser tab as the application and runs the test code in that browser's JavaScript run loop, communicating with a Node.js process behind the scenes for things the browser sandbox can't do (file system access, network-level stubbing). This is architecturally different from Playwright and Selenium, which drive the browser as an external controller through a protocol. The upshot: Cypress has direct, synchronous-feeling access to the DOM and network layer of the page under test, which is why its command log and time-travel debugging work so well — but it also means Cypress traditionally could not control more than one browser tab or origin at once, a limitation that has loosened in recent versions with cy.origin() for controlled cross-origin navigation, but which still shapes how multi-window or third-party-redirect flows (like some OAuth logins) have to be tested.
describe('search', () => {
it('returns results', () => {
cy.visit('https://example.com');
cy.findByRole('searchbox').type('cypress');
cy.findByRole('button', { name: 'Search' }).click();
cy.findByRole('list').should('contain.text', 'Cypress');
});
});Commands, chaining, and automatic retries
Cypress commands (cy.get, cy.click, cy.type, and so on) are not promises, even though they look like it — they're queued and run in order by Cypress's internal driver, which is what lets it retry a failing cy.get().should(...) chain automatically instead of failing immediately. This queuing model is powerful but has a learning curve: mixing cy commands with plain async/await or storing a command's 'return value' in a normal JS variable doesn't work the way it looks like it should, and Cypress's own documentation spends real space warning developers away from that pattern in favor of .then() callbacks or aliases (cy.as()).
Browser coverage and where Cypress stops
Cypress's stable, officially supported browsers are Chromium-family browsers (Chrome, Edge, Electron) and Firefox — the docs commit to supporting the latest three major versions of each. WebKit (Safari's engine) is also available, but only behind the experimentalWebKitSupport config flag and a separately installed playwright-webkit package — Cypress's WebKit runner is literally built on Playwright's own WebKit binaries. That experimental mode has real gaps: cy.origin() isn't supported inside it, Test Replay isn't available, and some event and network details differ from the Chromium/Firefox runs, so a default (non-experimental) Cypress suite still exercises zero Safari-engine coverage. Cypress is also JavaScript/TypeScript only; there are no official language bindings for Python, Java, or .NET, unlike Selenium and Playwright. For teams already fully invested in a JS/TS frontend stack and not maintaining the experimental WebKit config, both limitations tend to matter in practice.
Mistakes people make here
- Storing a cy.get() result in a variable and using it like a normal JS value
- cy.get() returns a Cypress chainable, not the element itself — the command hasn't actually run yet when the next line executes, because Cypress queues and runs commands asynchronously under the hood; the fix is to use .then(el => ...) or an alias, not a plain const.
- Adding cy.wait(3000)-style fixed waits to fix a flaky test
- It papers over the actual timing problem, slows the whole suite down by however many fixed waits accumulate, and can still fail on a slower CI machine — the retry-ability built into cy.get().should(...) is almost always the better fix for a genuine timing issue.
- Trying to test a true multi-tab flow (e.g. a payment popup) the way you would in Selenium or Playwright
- Cypress runs inside one browser tab by design and, historically, had no API for switching to or asserting on a second tab; teams typically have to stub the popup's network response instead of actually driving the second window, which is a real behavioral gap from tools built around external browser control.
- Assuming a passing Cypress suite means the app works in Safari
- By default, Cypress only runs against Chromium and Firefox. It does have an experimental WebKit mode (experimentalWebKitSupport), but it's opt-in, has real gaps like missing cy.origin() and Test Replay support, and most teams don't enable and maintain it — so a standard green Cypress run still says nothing about Safari-engine behavior unless a team has deliberately taken on that extra configuration.
Strengths and trade-offs
Where it is strong
- Time-travel debugging and a live command log make failures fast to diagnose without re-running anything.
- Automatic command retrying removes most manual waiting code for everyday async UI behavior.
- A mature plugin ecosystem and years of JS-community adoption mean most common testing needs have an established pattern.
- Component testing uses the same API as full E2E tests, so teams don't have to learn a second tool for isolated component checks.
The trade-offs
- WebKit/Safari coverage exists only as an experimental, opt-in mode (missing cy.origin(), Test Replay, and other features the Chromium/Firefox runs have) — most teams still don't get real day-to-day Safari-engine coverage from Cypress without extra setup and maintenance.
- JavaScript/TypeScript only, with no official bindings for Python, Java, or .NET, unlike Selenium and Playwright.
- The in-browser architecture historically limited true multi-tab and cross-origin control; cy.origin() has narrowed this but it's not a full external-controller model.
- Full parallelization and run recording/analytics live behind Cypress Cloud, a paid product, though the core test runner itself is free and open source.
Who needs this
Frontend-heavy JS/TS teams that want a fast, debugging-friendly E2E and component testing tool, and don't have a hard requirement for mature Safari/WebKit coverage or non-JS language bindings. Teams needing real cross-engine parity, non-JS test code, or heavy multi-tab flows should weigh Playwright or Selenium instead.
Questions about cypress
- Does Cypress support Safari?
- Only experimentally. Cypress's stable, officially supported browsers are Chromium-family browsers and Firefox, but since version 10.8 it has offered an opt-in WebKit mode (experimentalWebKitSupport) built on Playwright's own WebKit binaries. That mode still has real gaps — no cy.origin() support, no Test Replay — so it isn't a drop-in replacement for dedicated Safari testing, but it's no longer accurate to say Cypress has zero WebKit story.
- Is Cypress free?
- The Cypress test runner and framework are open source and free. Cypress Cloud — which adds test recording, parallelization across CI machines, and analytics dashboards — is a separate paid product with a limited free tier.
- Why can't I just use cy.get() like a normal async function?
- Cypress commands are queued internally rather than executed and awaited immediately, so a variable assigned from cy.get() doesn't hold a resolved value the way await would in Playwright or plain JS. Cypress's docs recommend .then() callbacks or cy.as() aliases for working with a command's result.
- Can Cypress test two browser tabs or windows at once?
- Not in the way Playwright or Selenium can. Cypress runs inside a single tab by design; multi-tab flows like OAuth popups typically have to be handled by stubbing the response rather than actually driving the second window, though cy.origin() does allow controlled navigation across origins within that model.