Run Web Automation Tests at Scale with Agentic AI
Generate tests with AI agents or automate test suites with Selenium, Playwright and other frameworks. Run tests in parallel on TestMu AI scalable cloud grid.
Playwright vs Selenium vs Cypress: Key Differences (2026)
Playwright, Selenium or Cypress? Compare speed, browsers, languages, mobile, CI and cost, with the same test in all three and a clear pick for every team.
Last Updated on:
In the Playwright vs Selenium vs Cypress decision, choose Playwright for a new web suite, Selenium for the widest browser and language reach or native mobile through Appium, and Cypress when a JavaScript front-end team wants the best interactive debugging and component testing. All three are free and open source. They differ in architecture, browser reach, languages and what it costs to run them at scale.
TL;DR
Which One Should You Pick?
- Choose Playwright for a new web test suite that needs fast, stable runs across Chromium, Firefox and WebKit, with parallel workers and tracing included.
- Choose Selenium when you need the widest language support, real Safari, a W3C-standard protocol, or you already run a large suite and Appium-based mobile tests.
- Choose Cypress when a JavaScript front-end team wants interactive debugging and component testing, and can live with one tab and no real Safari.
How Do You Run Any of Them at Scale?
All three hit the same ceiling on a laptop, where one machine runs a handful of browsers at a time. TestMu AI runs Playwright, Selenium and Cypress suites in parallel across 3,000+ browser and OS combinations, so the framework you pick stops being an infrastructure decision.
I have shipped suites in all three: Selenium first in Java, then Cypress for about four years, and Playwright most recently.
When I switched from Selenium to Cypress, flaky failures dropped sharply. Tests that had timed out locally and in CI started passing without application code changes.
Playwright later closed the browser-coverage gap that kept Cypress out of some projects, which is why this stays a three-way comparison.
Updated October 11, 2026. Versions compared: Playwright 1.64.0, Selenium 4.51.0 and Cypress 16.1.1, as listed in the Playwright release notes, on the Selenium downloads page and in the Cypress changelog. The Cypress 16.3 release post covers the Cypress changes since then.
Comparing only the two JavaScript-first tools? The Cypress vs Playwright comparison covers that pair, and Puppeteer vs Playwright covers the Chrome-first alternative.
Playwright vs Selenium vs Cypress at a Glance
| Area | Playwright | Selenium | Cypress |
|---|---|---|---|
| Latest version | 1.64.0 (Oct 7, 2026) | 4.51.0 (Oct 9, 2026) | 16.1.1 (Sep 29, 2026) |
| Maintainer and license | Microsoft, Apache-2.0 | Selenium project, Apache-2.0 | Cypress.io, MIT |
| How it drives the browser | Its own protocol to its own browser builds, with an isolated browser context per test | W3C WebDriver through each vendor's driver, with WebDriver BiDi being added | Runs inside the browser in the same run loop as your app, with a Node process behind it |
| Language support | JavaScript, TypeScript, Python, Java, .NET | Java, Python, C#, Ruby, JavaScript | JavaScript and TypeScript only |
| Browser support | Chromium, Firefox and WebKit builds, plus Chrome and Edge channels | Chrome, Edge, Firefox, Safari, and Edge in IE mode | Chrome family, Edge and Firefox, with experimental WebKit |
| Auto-waiting | Yes, actionability checks before each action | No, you write explicit waits | Yes, commands retry for 4 seconds by default |
| Parallel runs | Built in: workers on one machine, --shard across machines | Your test runner (TestNG, JUnit, pytest), with Selenium Grid to spread sessions across machines | The --parallel flag, which needs --record and Cypress Cloud |
| Multiple tabs and origins | Yes | Yes | One tab, and cy.origin() for other origins |
| Mobile | Device emulation, with experimental Android support | Mobile browsers and native apps through Appium, a separate project | Viewport resizing only |
| Debugging tools | Trace Viewer, UI mode, Codegen | Selenium IDE, Grid UI | Command Log with time travel |
| AI features | Test Agents (planner, generator, healer) and Playwright MCP | A docs page and an llms.txt file for AI coding agents | cy.prompt, in beta, which needs a Cypress Cloud account |
| GitHub stars | About 97,500 | About 34,500 | About 51,000 |
| Best for | New web suites, cross-engine coverage, CI speed | Multi-language teams, existing suites, real Safari, mobile through Appium | JavaScript front-end teams, component tests, debugging |
Version numbers and dates come from each project's release page. Star counts were read on October 11, 2026 from the Playwright, Selenium and Cypress repositories on GitHub.
How Do Playwright, Selenium and Cypress Work Under the Hood?
Most of the other differences follow from how each tool talks to the browser.
Playwright
Playwright is an open-source framework from Microsoft that downloads its own builds of Chromium, Firefox and WebKit and drives all three through one API. It does not use WebDriver.
- Every test gets a fresh browser context. The Playwright isolation docs describe contexts as "fast and cheap to create" and "completely isolated", so tests don't share cookies or storage.
- The Firefox and WebKit builds carry Playwright's own patches, which is why it cannot drive the branded versions of Firefox or Safari.
- Playwright can attach to a running Chromium browser over the Chrome DevTools Protocol, but its API docs call that connection "significantly lower fidelity" than the Playwright protocol.
For setup and a first test, start with the Playwright tutorial.
Selenium
Selenium WebDriver implements W3C WebDriver, a web standard that each browser vendor supports through its own driver: chromedriver, geckodriver and safaridriver. Your test drives the branded browser as users install it.
- Selenium 4 replaced the older JSON Wire Protocol with W3C WebDriver. Articles that still describe JSON Wire are describing Selenium 3.
- Selenium 4 is adding WebDriver BiDi, which the Selenium docs call "the W3C standard bidirectional protocol for browser automation". It adds a WebSocket connection so a script can react to network requests and console messages as they happen.
- Selenium Manager downloads the matching driver for you. The Selenium docs describe it as "shipped out of the box with each Selenium release as of version 4.6".
The Selenium tutorial walks through setup, a first test and locators.
Cypress
Cypress runs your test code inside the browser. Its docs say it "is executed in the same run loop as your application", with a Node server process behind it for work outside the browser. That design is what makes the time-travel Command Log and instant reloads possible.
The same design sets its permanent limits. The Cypress trade-offs page lists them: commands run inside a browser, you cannot drive two browsers at the same time, and "the only language we'll ever support is the language of the web: JavaScript". The Cypress tutorial covers the basics.
Is Playwright Faster Than Selenium?
Usually yes for the same scenario, and the reasons are architectural:
- Playwright creates a new browser context for each test. Selenium's own test practices recommend a fresh browser per test, which costs a full browser start each time.
- Playwright waits for elements itself, so suites don't pad runs with sleeps.
- Playwright Test runs test files in parallel by default. The worker count "defaults to half of the number of logical CPU cores", per the TestConfig reference.
- Selenium has no auto-wait and no parallel mode of its own. Both come from the waits you write and the runner you configure, and a suite full of sleep() calls is slow by design.
Against Cypress, the gap shows on large suites. Playwright spreads spec files across workers on one machine with no extra service, while the Cypress --parallel flag only works on runs recorded to Cypress Cloud. The free alternatives are compared in Cypress parallelization.
This page carries no timed benchmark yet. A fair one needs the same suite, hardware and worker count in all three tools, and most speed figures published online state none of that. Time your own suite before you trust a number. The POC in testing guide covers how to run that kind of fair trial on your own flows.
Which Browsers Does Each Tool Support?
Browser coverage, taken from each tool's own docs:
- Playwright: ships its own Chromium, Firefox and WebKit builds. The 1.64 release notes list Chromium 156, Firefox 157 and WebKit 27.2. Branded Google Chrome and Microsoft Edge run through the chrome and msedge channels. The Playwright browsers docs say it "doesn't work with the branded version of Safari since it relies on patches", and they recommend running WebKit on a Mac for the closest-to-Safari experience.
- Selenium: drives Chrome, Edge, Firefox and Safari through each vendor's driver. safaridriver ships with macOS and is switched on with safaridriver --enable, according to the Selenium Safari docs, which point to Appium for Safari on iOS. Standalone Internet Explorer has not been supported since June 2022, but the IE driver still runs Microsoft Edge in IE compatibility mode.
- Cypress: supports Chrome, Chrome for Testing, Chromium, Edge and Firefox. Electron, its original default browser, is now marked deprecated. WebKit support is experimental. It needs the experimentalWebKitSupport flag and the playwright-webkit package, because Cypress runs Playwright's WebKit build there. The Cypress browser docs list cy.origin() and Test Replay as unsupported under WebKit.
If real Safari matters, Selenium is the only one of the three that drives it directly.
Which Languages Can You Write Tests In?
Language support runs from five official bindings in Selenium to JavaScript only in Cypress:
- Selenium: Java, Python, C#, Ruby and JavaScript. The Selenium downloads page lists all five bindings at version 4.51.0.
- Playwright: JavaScript, TypeScript, Python, Java and .NET. Its own test runner is for Node.js only. The Playwright languages page points Python users to the pytest plugin, Java users to JUnit or TestNG, and .NET users to MSTest, NUnit or xUnit.
- Cypress: JavaScript and TypeScript only, because test code is evaluated inside the browser.
Can You Test Mobile Apps?
- Selenium: not by itself. Appium, a separate open-source project, speaks the same WebDriver protocol to automate native, hybrid and mobile-web apps on iOS and Android, so a Selenium team keeps its language, runner and page-object structure. Appium vs Selenium explains how the two split the work.
- Playwright: emulates mobile browsers on desktop engines, including viewport, touch and user agent. The Playwright Android API is labelled experimental and covers Chrome for Android and Android WebView. iOS is emulation only.
- Cypress: resizes the viewport with cy.viewport(). Its trade-offs page states there is no native or mobile events support.
None of the three automates a native app by itself. Selenium paired with Appium is the only route that gets there.
How Do They Handle Waits and Flaky Tests?
- Playwright: before a click, it checks that the locator resolves to exactly one element and that the element is visible, stable, enabled and able to receive events. If the checks don't pass within the timeout, the action fails with a TimeoutError, as the actionability docs describe.
- Cypress: retries queries and assertions for up to 4 seconds, the defaultCommandTimeout. It does not wait for network calls unless you alias them with cy.intercept() and cy.wait(). cy.visit() waits up to 60 seconds for the page load, the pageLoadTimeout in the Cypress configuration reference.
- Selenium: leaves waiting to you. The implicit wait defaults to 0, so a missing element fails at once. Use explicit waits with WebDriverWait, never fixed sleeps. The Selenium waits docs also warn against mixing implicit and explicit waits, because the timeouts become unpredictable. Most "Selenium is flaky" complaints trace back to a missing wait.
Can Cypress Handle Multiple Tabs, Other Origins and iFrames?
Only partly. The limits below come from the Cypress trade-offs page and the cy.origin() reference:
- Cypress controls one browser at a time. For multi-tab flows, its docs point to the @cypress/puppeteer plugin.
- Since Cypress 14.0.0, cy.origin() is required whenever a test navigates between two origins.
- Inside a cy.origin() callback, another cy.origin(), cy.intercept() and cy.session() all throw errors. The callback also cannot run commands in a different window, a different tab or inside an iframe.
- Same-origin iframes can be queried natively. A "switch into an iframe" command is still an open proposal.
Playwright and Selenium handle multiple tabs, popups, origins and iframes natively. If your app has OAuth logins, payment popups or embedded widgets, automate one of those flows in Cypress before you commit to it.
How Do They Run in CI and in Parallel?
- Playwright: parallelism is built in. Tests run across worker processes on one machine, and --shard=1/4 splits the suite across four CI machines. The merge-reports command then combines the results, as the sharding docs show.
- Selenium: parallelism comes from your test runner. TestNG, JUnit 5 or pytest-xdist start several sessions at once, and Selenium Grid routes those sessions to browsers on other machines. You maintain the Grid yourself or rent hosted browsers.
- Cypress: parallel runs go through Cypress Cloud. The parallelization docs state that "running tests in parallel requires the --record flag be passed". The free Starter plan includes parallelization and 500 test results a month, per the Cypress pricing page on October 11, 2026.
How Much Does Each Cost to Run at Scale?
The frameworks are free. Running them is where the money goes:
- Playwright: CI minutes only, unless you need real devices or more machines than your CI provides.
- Selenium: CI minutes plus a Selenium Grid to build, patch and scale, or a subscription to hosted browsers.
- Cypress: CI minutes plus a paid Cypress Cloud plan once you record more test results than the free plan allows. Check the Cypress pricing page for current plan limits.
The Same Login Test in Playwright, Selenium and Cypress
One scenario in three frameworks: open the login page, sign in, and check the dashboard greeting. The selectors use data-testid attributes so all three versions target the same elements, and the URL is a placeholder for your own app. All three ran green on October 11, 2026 against a local page with those four test ids: Playwright 1.64.0 and Cypress 16.1.1 in Chrome 155, and Selenium 4.51.0 with JUnit 5 on JDK 21.
Playwright (TypeScript, Playwright Test)
import { test, expect } from '@playwright/test';
test('user can log in', async ({ page }) => {
await page.goto('https://your-app.example.com/login');
await page.getByTestId('email').fill('qa@example.com');
await page.getByTestId('password').fill(process.env.QA_PASSWORD!);
await page.getByTestId('login-button').click();
await expect(page.getByTestId('greeting')).toHaveText('Welcome back, QA');
});There are no explicit waits. click() waits for the button to be actionable, and toHaveText() retries until the text appears or the timeout hits.
Selenium (Java, Selenium 4)
import org.openqa.selenium.*;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.ui.*;
import org.junit.jupiter.api.Test;
import java.time.Duration;
import static org.junit.jupiter.api.Assertions.assertEquals;
class LoginTest {
@Test
void userCanLogIn() {
WebDriver driver = new ChromeDriver();
try {
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
driver.get("https://your-app.example.com/login");
driver.findElement(By.cssSelector("[data-testid='email']")).sendKeys("qa@example.com");
driver.findElement(By.cssSelector("[data-testid='password']")).sendKeys(System.getenv("QA_PASSWORD"));
driver.findElement(By.cssSelector("[data-testid='login-button']")).click();
WebElement greeting = wait.until(
ExpectedConditions.visibilityOfElementLocated(By.cssSelector("[data-testid='greeting']")));
assertEquals("Welcome back, QA", greeting.getText());
} finally {
driver.quit();
}
}
}Selenium Manager downloads the matching chromedriver. The explicit WebDriverWait is what keeps this test stable.
Cypress (JavaScript)
describe('login', () => {
it('user can log in', () => {
cy.visit('https://your-app.example.com/login');
cy.get('[data-testid=email]').type('qa@example.com');
cy.env(['QA_PASSWORD']).then(({ QA_PASSWORD }) => {
cy.get('[data-testid=password]').type(QA_PASSWORD, { log: false });
});
cy.get('[data-testid=login-button]').click();
cy.get('[data-testid=greeting]').should('have.text', 'Welcome back, QA');
});
});Cypress chains commands and retries should() automatically. The password comes from cy.env(), the replacement for Cypress.env(), which Cypress 16.0.0 removed. The log: false option keeps the value out of the Command Log.
Side by side, the versions differ in these ways:
- The Playwright and Cypress tests are shorter because waiting is built in.
- The Selenium test is the longest, but the same test can be written in any of five languages and run against the branded browser.
- If this login redirected to an identity provider on another domain, the Cypress version would need a cy.origin() block. The Playwright and Selenium versions would not change.
Playwright vs Selenium vs Cypress: Pros and Cons
Playwright is best at fast, reliable web testing across all three browser engines. Selenium is best at breadth and standards: it implements the W3C protocol, drives every major branded browser including Safari, and reaches native mobile through Appium. Cypress is best at the developer experience for JavaScript front-end teams.
In the pros and cons below, the learning curve column reflects how much each tool asks you to assemble before a first passing test.
| Tool | Pros | Cons | Learning curve |
|---|---|---|---|
| Playwright | Auto-waiting, isolated contexts, parallel workers and sharding included. Chromium, Firefox and WebKit from one API. Trace Viewer for CI failures you can't reproduce locally. Test Agents and an MCP server for AI-assisted authoring. | No branded Safari or Firefox. Real-device mobile is limited to experimental Android. The full test runner is Node.js only. No Ruby. | Short. Codegen records a first test as you click. |
| Selenium | W3C standard protocol. Five official languages. Branded browsers including Safari. Native mobile through Appium. Two decades of existing suites and community answers. | No auto-wait. You assemble the runner, waits and reporting yourself. Parallel runs come from your runner, and spreading them across machines needs a Grid. | Longest, because the framework around the tests is yours to build. |
| Cypress | Time-travel Command Log and instant re-runs. Component testing for React, Angular, Vue and Svelte. One install brings the runner and assertions. | JavaScript only. One tab, and cy.origin() for other origins. WebKit is experimental. The --parallel flag needs Cypress Cloud. | Shortest for JavaScript developers. |
Which Should You Choose?
Choose Playwright for a new web suite, Selenium when you need Ruby, real Safari or Appium-based mobile tests, and Cypress for the fastest front-end setup.
A Cypress vs Playwright vs Selenium feature table rarely settles the argument, because your existing stack decides more than any single capability. Match your situation to a row below, or widen the field with the guide to best test automation frameworks.
| Your situation | Pick | Why it wins here |
|---|---|---|
| Starting a new suite in JavaScript or TypeScript | Playwright | Auto-waiting, tracing and parallel workers ship in the box, so a usable suite exists on day one without wiring a toolchain together. |
| Your team writes Ruby, or mixes several languages | Selenium | Official bindings for Java, Python, C#, Ruby and JavaScript at the same version. Playwright covers JavaScript, TypeScript, Python, Java and .NET but not Ruby, and Cypress is JavaScript only. |
| Front-end team that wants a passing test today | Cypress | One npm install brings the runner, assertions and automatic retries. The time-travel debugger is the fastest way to see why a step failed. |
| You already run a Selenium Grid with trained engineers | Selenium | A rewrite buys speed you can also get by moving the same suite onto parallel cloud infrastructure. Migrate the runner before you migrate the framework. |
| Wall-clock time on a large suite is the problem | Playwright | Spec files spread across parallel workers by default. Cypress parallel runs go through Cypress Cloud, and Selenium needs TestNG or JUnit parallel settings plus a Grid to go beyond one machine. |
| You need component tests and end-to-end tests in one tool | Cypress | Component testing is a first-class mode for React, Angular, Vue and Svelte, which keeps component specs beside the end-to-end suite. Playwright removed its experimental component-testing packages after version 1.62 in favor of a story-based approach. |
| Real Safari or Edge in IE mode is required | Selenium | Selenium drives Safari through safaridriver, and its IE driver still runs Microsoft Edge in IE compatibility mode. Playwright uses its own WebKit build, and WebKit support in Cypress is experimental. |
Two rows point the same way. If the complaint is runtime, changing frameworks is the expensive fix, and running the suite you already have in parallel is usually the cheaper one.
If none of the three fits, Puppeteer vs Selenium covers a fourth option with a narrower browser range but tighter Chrome DevTools control, and the guide to Selenium alternatives widens the shortlist further.
When Should You Migrate From Selenium to Playwright?
Migrate when most of these are true:
- Your suite is web-only.
- Flakiness and run time are your top complaints.
- Your team writes JavaScript, TypeScript, Python, Java or .NET.
- You don't depend on branded Safari.
Stay on Selenium, or run both, when you have a large stable suite, you need Ruby, you test real Safari, or you share page objects with Appium mobile tests.
A low-risk migration path looks like this:
- Freeze new Selenium tests. Write every new web test in Playwright from a set date.
- Port by flakiness, not by folder. Move the tests that fail most often first, because that is where the stability gain shows.
- Replace waits instead of translating them. Delete the WebDriverWait and sleep() calls. Playwright's locators and web-first assertions already wait.
- Keep page objects, change what they return. Return Playwright Locators where the Selenium version returned WebElements.
- Run both in CI until parity. Report both suites side by side, then retire each Selenium test once its Playwright twin has proved stable.
Each of those steps hides detail this summary skips, from the strict mode errors that greet the first converted spec to the Selenium commands that have no Playwright equivalent at all. The full Selenium to Playwright migration guide covers the API mapping tables, the wait deletion rule and the harness conversion.
Migrate from Selenium to Playwright with Agent Skills.
Playwright's Test Agents, added in version 1.56, can draft tests from a plan, and the walkthrough of Playwright Agents shows the planner, generator and healer on a real flow. Review generated tests the way you would review a junior engineer's pull request.
Should You Use More Than One?
Yes, when the split follows what each tool is good at:
- Cypress for component tests and front-end developer tests.
- Playwright for cross-browser end-to-end flows in CI.
- Selenium with Appium where real Safari or native mobile apps are required.
The cost is a second or third set of helpers and reports to maintain. Share test data across the suites and send results to one reporting dashboard so they stay comparable.
npm Trends: Playwright vs Selenium vs Cypress
The npm registry's download counts API returned these weekly totals for the same week a year apart:
| Package | Framework | October 5 to 11, 2025 | October 4 to 10, 2026 | Change |
|---|---|---|---|---|
| playwright | Playwright (library) | 20,944,373 | 112,438,501 | 5.4 times |
| @playwright/test | Playwright (test runner) | 12,305,352 | 75,674,770 | 6.1 times |
| cypress | Cypress | 6,305,458 | 6,027,043 | Down 4.4% |
| selenium-webdriver | Selenium (JavaScript binding) | 1,632,476 | 1,756,751 | Up 7.6% |
Read these npm figures with their limits in mind. They count installs, and every CI job and every tool that depends on Playwright installs it again, so they measure activity more than headcount. They also cover JavaScript packages only, which leaves out everyone using Selenium from Java, Python, C# or Ruby.
The live chart is on npm trends for Cypress, Playwright and Selenium.
Hands-On: Setup, Selectors, Parallel Runs and Debugging in Each Tool
The ten walkthroughs below repeat the same tasks in each framework. The screenshots are from the original 2023 runs and show older versions of each tool's interface. In October 2026 these samples were re-run on Playwright 1.64.0, Selenium 4.51.0 and Cypress 16.1.1 and passed: the Playwright search and parallel tests, the Chrome parallel class, the Cypress playground test and all three iframe samples.
Installation & Configuration
Installation is where the three differ first. Playwright and Cypress arrive as one npm package, while Selenium needs a language binding, a test runner and an IDE.
Playwright:
Before installing Playwright:
- Ensure you have NodeJS installed.
- Install a code editor. I will be using Visual Studio Code.
Playwright can be installed in either of these ways:
- Using the VS Code extension.
- Using the init command.
Using the VS Code extension
- Create a folder, e.g., Install_playwright.
- Open the folder in VS Code.
- Search Playwright extension in VS Code and install it.
- Open the Command Palette (Cmd+Shift+P on a Mac, Ctrl+Shift+P elsewhere), run the Test: Install Playwright command, choose the browsers and click OK.
- The installation starts when you click OK.




Using the init command
- Create a folder, e.g., playwright_new_script.
- Open the folder in VS Code.
- Run from the project's root directory npm init playwright@latest.
The command installs the current release, 1.64.0 when this page was updated, and asks whether you want TypeScript or JavaScript.


The installer adds an example spec file:

For the full setup, see this guide on how to install Playwright.
Selenium:
The steps below set up Selenium for Java. Other bindings swap the JDK and TestNG for their own runtime and runner, such as Python with pytest.
- Install a JDK, then install an IDE. The Eclipse downloads page covers the IDE step.
- Set up the IDE. This guide to configuring Eclipse and IntelliJ walks through it.
- Add the selenium-java dependency to your Maven or Gradle build. The Selenium downloads page lists the current version, and the Selenium docs say Java installation "is accomplished using a build tool". You no longer download driver binaries by hand, because Selenium Manager fetches the matching driver.
- Create a TestNG project and run the Selenium test script. See creating TestNG project in Eclipse.


Cypress:
Cypress installs as one npm package, and its Launchpad generates the configuration and folder structure on first launch.
To set up Cypress, follow the steps below:
- Pre-requisites - Node should already be installed.
- Generate package.json using the command npm init.
- npm install cypress --save-dev installs the current release, Cypress 16.1.1 when this page was updated.
- Once installed, package.json looks like the one below.
- Run npx cypress open and choose E2E Testing. The screenshot below is from Cypress 11.2.0 and shows the example specs that release scaffolded.


For a full walkthrough, see Cypress end to end Testing.
Runner
A test runner decides which tests run, runs them and reports the results.
Playwright:
Playwright Test is the first-party runner and the one to use. The docs say that "for end-to-end testing, you'll want to use @playwright/test (Playwright Test), and not playwright (Playwright Library) directly."
The library can still be driven from Jest, Mocha or Vitest, but the docs list what you give up outside the Playwright Test runner: you launch the browser and create each context and page yourself, and there are no web-first assertions that wait and retry.
Playwright runs one test, a folder or the whole suite, headless by default. Results print to the terminal, and npx playwright show-report opens the HTML report.
- Run every test, headless:
npx playwright test
npx playwright test fileName.spec.js
npx playwright test tests/page/home-page/
npx playwright test home-page.spec.js --headed
Playwright also runs test cases in debug mode through Playwright Inspector, a built-in tool.
The Inspector steps through Playwright API calls, shows debug logs, explores selectors, and finds locators when you hover over an element.

Start it with npx playwright test --debug and step through the test line by line. For day-to-day work, npx playwright test --ui opens UI mode, which shows a full trace of each test.

Selenium:
Selenium has no test runner of its own. WebDriver drives the browser, and a runner from your language's ecosystem decides which tests run and reports the results.
- Java teams use TestNG or JUnit. The JUnit tutorial covers test cases, suites and parallel runs.
- Python teams use pytest, JavaScript teams Mocha or Jest, C# teams NUnit and Ruby teams RSpec. Rails sets up Minitest by default, and the RSpec vs Minitest guide compares the two.
- Maven or Gradle starts the runner from the command line, and a continuous integration server such as Jenkins triggers it on every commit.
- Selenium Grid routes the browser sessions to other machines, so the same runner covers more browsers and operating systems.
Cloud testing platforms also act as a test runner, executing the same Selenium scripts across operating systems and browsers you do not maintain locally. This widens coverage without adding nodes.
That removes the driver and browser-version drift that makes a self-hosted grid expensive to keep alive, and it is the usual reason teams stop running cross browser testing on their own hardware.
Cypress:
Cypress has a test runner that displays executing commands alongside the application under test with real-time data.
Hovering over a log entry in the command log shows the matching UI state.


Note: TestMu AI captures video, console logs, and network logs on every run, so a failed spec is debuggable without reproducing it locally. To try it, start free.
Record & Playback
All three can record a test from your clicks, which helps most while you are still learning the tool's selectors.
Playwright:
Codegen records the actions you perform in the browser and writes the matching test code. The command below opens a browser with the Playwright Inspector beside it. In the screenshot, clicks on the Home, Blog and Mega Menu links have each produced a line of code.
npx playwright codegen https://ecommerce-playground.lambdatest.io/

Selenium:
Selenium IDE is the record and playback tool. Its site says getting started "requires no additional setup other than installing the extension on your browser", and you build a test without writing code.
Record your browser interactions to create test cases, then use playback to rerun the scenarios.

Selenium IDE records multiple locators for every element it touches, retrying each one until a locator succeeds during playback.
The run command reuses one test case inside another, so login logic can be shared across a suite.
In the screenshot below, VerifyHomePage test steps have been reused in a separate test case.

Selenium IDE also has control flow commands such as if, while and times.

Cypress:
Cypress Studio records your interactions with the app and writes them into the spec as .type(), .click(), .check(), .uncheck() and .select() commands.
Since Cypress 15.4.0, Studio is available by default and you no longer set the experimentalStudio flag. The current docs give three ways in: click New Test on a spec or suite, hover over a test in the Command Log and click Edit in Studio, or open the Studio panel in the header.
The screenshots below are from an earlier release, where the first two entry points were labelled Add New Test and Add Commands to Test.


Add New Tests
Add New Test creates a test, saves it, and re-runs it.
The screenshot below shows steps recorded by Cypress Studio. Saving the commands writes a new script.

Add commands to Test
Add Commands to Test, now Edit in Studio, appends steps to an existing test.

In the script below, the two cy.get() lines between the Studio comments were recorded into the first test.
/// <reference types="cypress" />
describe("Cypress Studio -- >", { testIsolation: false }, () => {
it("WHEN User Open the Url", () => {
cy.visit(
"https://ecommerce-playground.lambdatest.io/index.php?route=account/login"
);
/* ==== Generated with Cypress Studio ==== */
cy.get('#widget-navbar-217834 > .navbar-nav > :nth-child(1) > .icon-left > .info > .title').click();
cy.get('#widget-navbar-217834 > .navbar-nav > :nth-child(3) > .icon-left > .info > .title').click();
/* ==== End Cypress Studio ==== */
});
it("AND Login into the application", () => {
cy.get('[id="input-email"]').type("lambdatest@yopmail.com");
cy.get('[id="input-password"]').type("lambdatest");
cy.get('[type="submit"]').eq(0).click();
});
it("AND After login Search the Product", () => {
cy.get('[name="search"]')
.eq(0)
.type("Sony VAIO")
.should("have.value", "Sony VAIO");
cy.get('[type="submit"]').eq(0).click();
});
it("THEN Verify Correct Product should display after search ", () => {
cy.contains("Sony VAIO");
});
});
Element Selection
Element selection is how a test finds the node it acts on: by id, CSS selector, class, attribute, tag name, link text or name. The three tools differ in what they check before acting on that element.
Playwright:
Playwright runs the relevant actionability checks first and performs the action only once they pass. The table is the current list from the Playwright docs. Yes means the check runs before that action, and a dash means it does not apply.
| Action | Visible | Stable | Receives Events | Enabled | Editable |
|---|---|---|---|---|---|
| locator.check() | Yes | Yes | Yes | Yes | - |
| locator.click() | Yes | Yes | Yes | Yes | - |
| locator.dblclick() | Yes | Yes | Yes | Yes | - |
| locator.setChecked() | Yes | Yes | Yes | Yes | - |
| locator.tap() | Yes | Yes | Yes | Yes | - |
| locator.uncheck() | Yes | Yes | Yes | Yes | - |
| locator.hover() | Yes | Yes | Yes | - | - |
| locator.dragTo() | Yes | Yes | Yes | - | - |
| locator.screenshot() | Yes | Yes | - | - | - |
| locator.fill() | Yes | - | - | Yes | Yes |
| locator.clear() | Yes | - | - | Yes | Yes |
| locator.selectOption() | Yes | - | - | Yes | - |
| locator.selectText() | Yes | - | - | - | - |
| locator.scrollIntoViewIfNeeded() | - | Yes | - | - | - |
| locator.blur() | - | - | - | - | - |
| locator.dispatchEvent() | - | - | - | - | - |
| locator.focus() | - | - | - | - | - |
| locator.press() | - | - | - | - | - |
| locator.pressSequentially() | - | - | - | - | - |
| locator.setInputFiles() | - | - | - | - | - |
In the sample below, fill() and press() each wait for the search field to pass those checks:
await page.goto('https://ecommerce-playground.lambdatest.io/');
const search = page.locator('[name="search"]').first();
await search.fill('Sony VAIO');
await search.press('Enter');
await expect(page).toHaveURL(/search=Sony/);Prefer role and text locators, and chain locators where older code chained selector strings. On chained selectors, the Playwright docs now say "We recommend chaining locators instead."
await page.getByRole('link', { name: 'Log in' }).click();
await page.locator('article').filter({ hasText: 'Playwright' }).click();
await page.locator('#nav-bar').getByText('Contact Us').click();Selenium:
Selenium finds elements with the locator types above and returns WebElements. It does not wait: with the implicit wait at its default of 0, findElement throws NoSuchElementException the moment the element is missing.
In my experience this is one of Selenium's biggest limitations on dynamic pages, and implicit or explicit waits are how you work around it. The snippet waits until the button can be clicked before clicking it.
WebElement button = new WebDriverWait(driver, Duration.ofSeconds(30))
.until(ExpectedConditions.elementToBeClickable(By.id("buttonId")));
button.click();
Cypress:
Cypress builds the wait into its queries. cy.get() and the commands chained to it retry until the element exists and passes its assertions, for up to the defaultCommandTimeout of 4000 ms, and then fail the test. Raise defaultCommandTimeout in cypress.config.js when your app needs longer. For a single slow step, Cypress timeout shows the per-command option instead.
const { defineConfig } = require('cypress')
module.exports = defineConfig({
defaultCommandTimeout: 10000
})
Cypress does not wait for network calls unless you alias them with cy.intercept(). Each command below retries for up to 4 seconds until its element is ready.
cy.get('.btn').click() // Click on to the button
cy.focused().click() // Click on element with focus
cy.contains('Welcome').click() // Click on first el containing 'Welcome'
Parallel Execution
Parallel execution runs several tests at once, which cuts total run time. All three support it, through different mechanisms.
Playwright:
Playwright Test runs test files in parallel worker processes on one machine and can shard a suite across machines. Tests inside one file run in order unless fullyParallel is set, which the config scaffolded by npm init playwright turns on. Running tests in parallel exposes any that share state, and the Playwright best practices guide covers keeping each test independent so it passes in any order.
The spec below opens an eCommerce website, clicks the Home and Blog links, and checks each redirect URL.
// @ts-check
const { test, expect } = require("@playwright/test");
test.describe("Test suite", () => {
test.beforeEach(async ({ page }) => {
await page.goto("https://ecommerce-playground.lambdatest.io/");
});
test("Open the Lambdatest Site Click on Home Link and verify user re-direct to correct url", async ({ page }) => {
await page.locator('span:has-text("Home")').click();
await expect(page).toHaveURL(
"https://ecommerce-playground.lambdatest.io/index.php?route=common/home"
);
});
test("Open the Lambdatest Site Click on Blog Link and verify user re-direct to correct url", async ({ page }) => {
await page.locator("#widget-navbar-217834 >> text=Blog").click();
await expect(page).toHaveURL(
"https://ecommerce-playground.lambdatest.io/index.php?route=extension/maza/blog/home"
);
});
});
With the worker count pinned to 1, the two tests run one after the other [1/2]:

In the original 2023 run that took 16s:

Playwright Test does not default to one worker. It uses half the logical CPU cores, and the scaffolded config drops to a single worker only on CI.
To set the count yourself, pass it on the command line, as in npx playwright test --workers 4, or set workers in playwright.config.js. In the screenshot, line 33 is changed so that local runs use 2 workers.

With 2 workers, both tests run at the same time [2/2]:

The same script took 13s in that run. Re-run in October 2026 on Playwright 1.64.0 with Chrome 155, it took about 10s with one worker and about 6s with two.

TestMu AI runs parallel testing with Playwright across 3,000+ browser and OS combinations.
You can run one test case across many browsers, or many scenarios in the same browser at different versions.
Subscribe to TestMu AI YouTube Channel and stay updated with detailed tutorials around Playwright testing, Selenium automation on TestMu AI, Cypress testing, and more.
Selenium:
Selenium has no parallel mode of its own, so the test runner provides it. In Java that is usually TestNG, through the parallel and thread-count attributes of the suite file:
<suite name="ParallelTesting" parallel="methods" thread-count="3">
The parallel attribute accepts the values below. They are TestNG settings, so they apply to Java projects.
- Methods - All the methods with @Test annotation will run in parallel.
- Instances - Helps execute all methods in the same instance in the same thread.
- Tests - All the test cases inside the <test> tag of the testng.xml file will run in parallel.
- Classes - All the test cases inside a Java class will run in parallel.
- thread-count is a separate attribute: the number of threads that run at once.
With three methods and a thread-count of 2, two methods start at once and the third begins when either thread frees up.
The example opens the eCommerce playground, verifies the title, clicks the Home link and verifies the redirect URL, in three classes: ChromeParallelTest, FirefoxParallelTest and SafariParallelTest. The Firefox and Safari classes were compiled against Selenium 4.51.0 but not run.
ChromeParallelTest:
package ParallelTestCase;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.testng.Assert;
import org.testng.annotations.AfterTest;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.Test;
public class ChromeParallelTest {
WebDriver driver;
@BeforeTest
public void setUp() {
// Selenium Manager resolves the driver binary automatically
driver = new ChromeDriver();
driver.navigate().to("https://ecommerce-playground.lambdatest.io/");
driver.manage().window().maximize();
}
@Test
public void VerifyTitle() {
System.out.println("The thread ID for Running First Test In Chrome Browser is " + Thread.currentThread().getId());
Assert.assertEquals(driver.getTitle(), "Your Store");
}
@Test
public void ClickOnHomeLink() {
System.out.println("The thread ID for Running Second Test In Chrome Browser is " + Thread.currentThread().getId());
WebElement homeLink = driver.findElement(By.xpath("//span[contains(text(),'Home')]"));
homeLink.click();
String redirectURL = driver.getCurrentUrl();
Assert.assertEquals(redirectURL, "https://ecommerce-playground.lambdatest.io/index.php?route=common/home");
}
@AfterTest
public void close() {
driver.quit();
}
}FirefoxParallelTest:
package ParallelTestCase;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.testng.Assert;
import org.testng.annotations.AfterTest;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.Test;
public class FirefoxParallelTest {
WebDriver driver;
@BeforeTest
public void setUp() {
// Selenium Manager resolves the driver binary automatically
driver = new FirefoxDriver();
driver.navigate().to("https://ecommerce-playground.lambdatest.io/");
driver.manage().window().maximize();
}
@Test
public void VerifyTitle() {
System.out.println("The thread ID for Running First Test In Firefox Browser is " + Thread.currentThread().getId());
Assert.assertEquals(driver.getTitle(), "Your Store");
}
@Test
public void ClickOnHomeLink() {
System.out.println("The thread ID for Running Second Test In Firefox Browser is " + Thread.currentThread().getId());
WebElement homeLink = driver.findElement(By.xpath("//span[contains(text(),'Home')]"));
homeLink.click();
String redirectURL = driver.getCurrentUrl();
Assert.assertEquals(redirectURL, "https://ecommerce-playground.lambdatest.io/index.php?route=common/home");
}
@AfterTest
public void close() {
driver.quit();
}
}SafariParallelTest:
package ParallelTestCase;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.safari.SafariDriver;
import org.testng.Assert;
import org.testng.annotations.AfterTest;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.Test;
public class SafariParallelTest {
WebDriver driver;
@BeforeTest
public void setUp() {
// safaridriver ships with macOS. Enable it once with: safaridriver --enable
driver = new SafariDriver();
driver.navigate().to("https://ecommerce-playground.lambdatest.io/");
driver.manage().window().maximize();
}
@Test
public void VerifyTitle() {
System.out.println("The thread ID for Running First Test In Safari Browser is " + Thread.currentThread().getId());
Assert.assertEquals(driver.getTitle(), "Your Store");
}
@Test
public void ClickOnHomeLink() {
System.out.println("The thread ID for Running Second Test In Safari Browser is " + Thread.currentThread().getId());
WebElement homeLink = driver.findElement(By.xpath("//span[contains(text(),'Home')]"));
homeLink.click();
String redirectURL = driver.getCurrentUrl();
Assert.assertEquals(redirectURL, "https://ecommerce-playground.lambdatest.io/index.php?route=common/home");
}
@AfterTest
public void close() {
driver.quit();
}
}To run the test cases in parallel, create a testng.xml file with this config:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="Suite" parallel="tests" thread-count="2">
<test name="Parallel Test Case Execution In Chrome">
<classes>
<class name="ParallelTestCase.ChromeParallelTest"/>
</classes>
</test> <!-- Test Case END -->
<test name="Parallel Test Case Execution In FireFox">
<classes>
<class name="ParallelTestCase.FirefoxParallelTest"/>
</classes>
</test> <!-- Test Case END -->
<test name="Parallel Test Case Execution In Safari">
<classes>
<class name="ParallelTestCase.SafariParallelTest"/>
</classes>
</test> <!-- Test Case END -->
</suite> <!-- Suite END -->
With thread-count set to 2, the first two tests start on two threads and the third starts when a thread frees up. To run the suite from Eclipse, right-click testng.xml and choose Run As, then TestNG Suite.

The console log below, from the original January 2023 run, shows "FirefoxTest" and "ChromeTest" running concurrently on Threads 15 and 16.
"SafariTest" then executed on Thread 15, because that thread was released before 16.

Cypress:
Cypress can run recorded tests in parallel across multiple machines. The cypress run command executes spec files serially by default, and parallel mode pays off most in CI.
A recorded run needs a record key: npx cypress run --record --key your-record-key sends the results, screenshots and videos to Cypress Cloud.
The spec below is the one behind the Cypress Cloud screenshots, which date from 2023:
describe("Verify title of ecommerce-playground.lambdatest.io", function () {
it("Should display the correct title", function () {
cy.visit("https://ecommerce-playground.lambdatest.io/");
cy.title().should("include", "Your Store");
});
it("Should navigate to the Blog page", function () {
cy.visit("https://ecommerce-playground.lambdatest.io/");
cy.contains("Blog").click({ force: true });
cy.url().should("include", "/blog/");
});
});
With the --record flag added, the run appears in Cypress Cloud:

The run below used one machine.

To run in parallel, start the same recorded command with --parallel on several CI machines. Cypress Cloud balances the spec files across them, so the number of machines sets your wall-clock time. This guide on how to run Cypress tests with GitHub Actions has a worked example.
Flakiness
A test is considered flaky when it can pass and fail across multiple retry attempts without any code changes.
The Google Testing Blog reports: "Almost 16% of our tests have some level of flakiness associated with them!"
"This is a staggering number; it means that more than 1 in 7 of the tests written by our world-class engineers occasionally fail in a way not caused by changes to the code or tests."
The same post attributes about 84% of observed pass-to-fail transitions to a flaky test rather than a real regression.
Playwright:
Playwright's defenses against flakiness are auto-waiting, explicit waits for a URL or an element, and retries.
Auto-waiting comes first. Before an action such as a click or a fill, Playwright waits for the element to pass its actionability checks. The manual check in the first snippet is redundant:
test("Verify button enable or not", async ({ page }) => {
const submit = page.locator("button", { hasText: "Previous" });
// Explicit check that the button is enabled
expect(await submit.isEnabled()).toBeTruthy();
await submit.click();
});click() already waits for the button to be enabled, so the test can drop that line. If you do want the assertion, write await expect(submit).toBeEnabled(), which retries, where the isEnabled() version checks once.
test("Verify button enable or not", async ({ page }) => {
const submit = page.locator("button", { hasText: "Previous" });
// Playwright auto-waits for the button to be actionable
await submit.click();
});Explicit waits cover what auto-waiting cannot know about, such as a navigation or an element that appears later:
- page.waitForLoadState() waits for the load or domcontentloaded event. The Playwright API docs mark its networkidle option as discouraged for testing.
- page.waitForURL() waits for a navigation to finish. It replaces waitForNavigation, which the same docs mark as deprecated and "inherently racy".
- A web-first assertion such as toBeVisible() waits for an element. The docs discourage waitForSelector in favor of assertions or locator.waitFor().
await page.waitForLoadState('domcontentloaded');
await page.waitForLoadState('load');await page.getByRole('button', { name: 'Sign in' }).click();
await page.waitForURL('**/dashboard');await expect(page.locator('.myDiv')).toBeVisible();Retries re-run a failed test. They don't fix the cause, but Playwright reports a test that fails and then passes on retry as flaky, which gives you a list to work through. Set the retries option in playwright.config.js:
import { defineConfig } from '@playwright/test';
export default defineConfig({
// Give failing tests 2 retry attempts
retries: 2,
});Selenium:
Selenium tests turn flaky through either the test code or the test infrastructure underneath them.
Infrastructure flakiness is the more tractable half, because pinned browser and driver versions on managed machines remove a whole class of it.
These habits reduce flakiness in Selenium tests:
- Use stable selectors - prefer an id or a dedicated test attribute over XPath tied to the page structure, which breaks when the HTML changes.
- Use explicit waits - they wait for one specific condition, where an implicit wait applies the same timeout to every lookup.
- Add retry logic - Implement retry logic so failed assertions retry a set number of times, absorbing transient network issues.
- Use a stable testing environment - Make sure your tests run in a stable environment with consistent network and server conditions.
- Debug failing tests - trace each failure to its root cause instead of re-running, which is what surfaces the underlying flakiness.
- Use a test framework - JUnit or TestNG structures tests and handles failures, making broken tests easier to identify.
- Use a reliable CI server - Jenkins or Azure DevOps runs tests automatically and tracks results across runs.
Cypress:
In Cypress, these habits do most of the work against flakiness:
- Assert on the state the app must reach before you act, such as a spinner disappearing or a list finishing its load. You rarely need .should('be.visible') before .click(), because Cypress already waits for the element to be actionable.
- Turn on test retries, which you can configure globally, per suite or per test.

- Give a slow element a longer timeout in place of a fixed cy.wait(). A fixed wait always burns its full duration, while a timeout releases as soon as the element loads.
cy.get("#first-name", {timeout: 10000}).type("Mike")- Alias the network call with cy.intercept() and wait for it. In the screenshot below, cy.wait("@getModel") holds the test until the request behind the form has finished.

Screenshots
A screenshot taken at the point of failure shows what the page looked like when the test broke.
Playwright:
Taking a screenshot in Playwright is one call, at page, buffer or element level. For comparing screenshots across browsers, see automated screenshot testing.
Full page screenshots
await page.screenshot({ path: 'screenshot.png', fullPage: true });
Capture into buffer
Instead of writing into a file, you can get a buffer with the image and post-process it or pass it to a third-party pixel diff facility.
const buffer = await page.screenshot();
console.log(buffer.toString('base64'));
Element screenshot
The same call works on a locator:
await page.locator('.header').screenshot({ path: 'screenshot.png' });
Selenium:
In Selenium, cast the driver to the TakesScreenshot interface and call getScreenshotAs(). The samples copy the file with java.nio, because selenium-java 4.51.0 does not bring in the Apache Commons IO library that older tutorials import. Both classes compile against that version.
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
public class ScreenshotCapture {
public static void main(String[] args) throws IOException {
// Selenium Manager resolves the driver binary automatically
WebDriver driver = new ChromeDriver();
try {
driver.get("https://www.testmuai.com/");
// Take the screenshot as a temporary file
File screenshot = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
// Copy it to a location you choose
Files.copy(screenshot.toPath(), Path.of("page.png"), StandardCopyOption.REPLACE_EXISTING);
} finally {
driver.quit();
}
}
}A WebElement has the same method, which captures only that element:
import org.openqa.selenium.By;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
public class ElementScreenshot {
public static void main(String[] args) throws IOException {
WebDriver driver = new ChromeDriver();
try {
driver.get("https://www.testmuai.com/");
WebElement heading = driver.findElement(By.cssSelector("h1"));
File shot = heading.getScreenshotAs(OutputType.FILE);
Files.copy(shot.toPath(), Path.of("heading.png"), StandardCopyOption.REPLACE_EXISTING);
} finally {
driver.quit();
}
}
}Cypress:
Cypress takes a screenshot automatically when a test fails during cypress run, and cy.screenshot() takes one on demand.
cy.screenshot() takes an optional file name and options:
cy.screenshot()
cy.screenshot(fileName)
cy.screenshot(options)
cy.screenshot(fileName, options)
Screenshots save under cypress/screenshots, in a folder named after the spec:
// cypress/e2e/login.cy.js
describe('Screenshot tests', () => {
it('takes a screenshot', () => {
// screenshot will be saved as
// cypress/screenshots/login.cy.js/Screenshot tests -- takes a screenshot.png
cy.screenshot()
})
})
Failure screenshots are on by default. To turn them off, set screenshotOnRunFailure to false in the Cypress configuration, or add this to the support file, cypress/support/e2e.js:
Cypress.Screenshot.defaults({
screenshotOnRunFailure: false,
})
Video
A video of the run shows where a test broke without you having to reconstruct it.
Playwright:
Playwright records video through the video option in the config file. Videos are off by default.
// @ts-check
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
use: {
video: 'on-first-retry',
},
};
module.exports = config;
The video option accepts these values:
- "off" - Do not record video.
- "on" - Record video for each test.
- "retain-on-failure" - Record video for each test, but remove all videos from successful test runs.
- "on-first-retry" - Record video only when retrying a test for the first time.
Selenium:
Selenium WebDriver has no built-in video recording. Recording comes from around it: the docker-selenium images for Grid include a video recording container, and cloud platforms record each session for you.
Cypress:
Cypress video is off by default. Set video: true in the configuration file, and cypress run records one video per spec file into the cypress/videos folder.

Note: To run the same Playwright suite in parallel across 3,000+ browser and OS combinations without maintaining a grid, start free with TestMu AI.
iFrame
An iframe is an HTML element that embeds one document inside another.
iFrames pull content from another site into a page, which is how embedded YouTube videos and external photos appear.
An iframe looks like this in the page source:
<iframe id="parent_iframe" name="demo_parent_iframe" width="700" height="450" src="https://bit.ly/38erOdQ">
</iframe>
Frames can nest. In the example below, a parent iframe holds a second iframe, and each has its own Click Here button:


Playwright:
Playwright reaches into an iframe with frameLocator(), which resolves the frame and then locates elements inside it.
The three samples in this section read the same text from the same frame, the demo box on jqueryui.com/draggable, so you can compare them line for line. In the page source, that frame is an iframe with the class demo-frame:

import { test, expect } from '@playwright/test';
test('reads text inside an iframe', async ({ page }) => {
await page.goto('https://jqueryui.com/draggable/');
const box = page.frameLocator('.demo-frame').locator('#draggable');
await expect(box).toHaveText('Drag me around');
});Selenium:
Selenium handles iframes by switching context. Move the driver into the frame, act there, then switch back to the main page.
- driver.switchTo().frame("id") switches by the frame's id or name.
- driver.switchTo().frame(0) switches by index, starting at zero.
- driver.switchTo().frame(element) switches to a frame you located as a WebElement.
- driver.switchTo().defaultContent() returns to the main page.
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
public class IFrameTest {
public static void main(String[] args) {
// Selenium Manager resolves the driver binary automatically
WebDriver driver = new ChromeDriver();
try {
driver.get("https://jqueryui.com/draggable/");
// Switch into the demo iframe
driver.switchTo().frame(driver.findElement(By.cssSelector(".demo-frame")));
WebElement box = driver.findElement(By.id("draggable"));
System.out.println("Frame text: " + box.getText());
// Switch back to the main page
driver.switchTo().defaultContent();
} finally {
driver.quit();
}
}
}The run printed "Frame text: Drag me around".
Cypress:
Cypress has no command for switching into an iframe, but it can query a same-origin frame natively. Its FAQ says "a third-party plugin is usually not required": get the iframe element, read its contentDocument.body, and wrap it so the usual commands chain from there.
describe('iframe example', () => {
it('reads text inside a same-origin iframe', () => {
cy.visit('https://jqueryui.com/draggable/');
cy.get('.demo-frame')
.its('0.contentDocument.body')
.should('not.be.empty')
.then(cy.wrap)
.find('#draggable')
.should('contain.text', 'Drag me around');
});
});This works because the jQuery UI demo frame is served from the same origin as the page. A cross-origin frame is out of reach for this approach.
Handling API Requests
End-to-end tests often need the API as well as the UI, to set up data or to check an endpoint directly. Playwright and Cypress can send HTTP requests themselves, and Selenium cannot. The request samples below call reqres.in, which returned the status codes shown when checked on October 11, 2026.
Playwright:
Playwright Test exposes a request fixture for calling your REST API. The examples cover GET, POST, PUT and DELETE.
Playwright also lets UI tests synchronize on backend calls triggered by user actions, through Playwright waitForResponse.
It resolves the moment a matching network response arrives and exposes the full response object for assertions.
For the mocking side of API testing in Playwright, this guide to Playwright mock API testing covers page.route() interception, route.fulfill() with custom status codes, HAR-file mocking, and request blocking.
GET Method
// @ts-check
const { test, expect } = require("@playwright/test");
test.describe("API Testing with Playwright", () => {
const baseurl = "https://reqres.in/api";
test("GET API Request with -- Valid 200 Response ", async ({ request }) => {
const response = await request.get(`${baseurl}/users/2`);
expect(response.status()).toBe(200);
});
});
POST Method
// @ts-check
const { test, expect } = require("@playwright/test");
test.describe("API Testing with Playwright", () => {
const baseurl = "https://reqres.in/api";
test("POST API Request with -- Valid 201 Response ", async ({ request }) => {
const response = await request.post(`${baseurl}/users/2`, {
data: {
id: 123,
},
});
const responseBody = JSON.parse(await response.text());
expect(responseBody.id).toBe(123);
expect(response.status()).toBe(201);
});
});
PUT Method
// @ts-check
const { test, expect } = require("@playwright/test");
test.describe("API Testing with Playwright", () => {
const baseurl = "https://reqres.in/api";
test("PUT API Request with -- Valid 200 Response ", async ({ request }) => {
const response = await request.put(`${baseurl}/users/2`, {
data: {
id: 245,
},
});
const responseBody = JSON.parse(await response.text());
expect(responseBody.id).toBe(245);
expect(response.status()).toBe(200);
});
});
DELETE Method
// @ts-check
const { test, expect } = require("@playwright/test");
test.describe("API Testing with Playwright", () => {
const baseurl = "https://reqres.in/api";
test("DELETE API Request with -- Valid 204 Response ", async ({
request,
}) => {
const response = await request.delete(`${baseurl}/users/2`, {});
expect(response.status()).toBe(204);
});
});
Selenium:
Selenium is a browser automation tool and is not designed for API testing.
It drives browsers and automates user interactions, but it cannot send HTTP requests or assert on an API response directly.
Pair Selenium with an HTTP client in the same test project, such as REST Assured in Java or requests in Python. Assert on the API response there, and reserve Selenium for the UI behaviour that depends on it.
Cypress:
Cypress sends HTTP requests with cy.request(), which takes the method, the URL and an optional body.
GET Method
it("GET API testing Using Cypress", () => {
cy.request("GET", "https://reqres.in/api/users?page=2").should((response) => {
expect(response.status).to.eq(200);
});
});
POST Method
it("POST API testing Using Cypress", () => {
cy.request("POST", "https://reqres.in/api/users", {
name: "morpheus",
job: "leader",
}).should((response) => {
expect(response.status).to.eq(201);
});
});
PUT Method
it("PUT API testing Using Cypress", () => {
cy.request("PUT", "https://reqres.in/api/users/2", {
name: "morpheus",
job: "zion resident",
}).should((response) => {
expect(response.status).to.eq(200);
});
});
DELETE Method
it("DELETE API testing Using Cypress", () => {
cy.request("DELETE", "https://reqres.in/api/users/2").should((response) => {
expect(response.status).to.eq(204);
});
});
Running All Three on TestMu AI
A suite that takes hours locally is usually limited by how many browsers one machine can drive, whichever framework wrote the tests. TestMu AI covers the parts this comparison leaves open:
- Automation Cloud runs your existing Selenium, Cypress and Playwright scripts in parallel across 3,000+ browser and OS combinations. Selenium suites get real Safari on macOS there, which Playwright and Cypress cannot drive directly.
- HyperExecute handles orchestration. It decides how a suite is split and where it runs, and TestMu AI rates it at up to 70% faster than traditional hub-and-node setups.
- Real Device Cloud runs Appium tests on 50,000+ real Android and iOS devices, where Playwright offers emulation.
- Kane CLI runs a flow from a natural-language objective and returns a pass or fail with a replayable evidence pack. It needs no selectors, and it can export runs to Python Playwright.
Network logs, console logs, video and screenshots are captured on every run, so debugging works the same whichever framework you picked. Per-framework setup lives in the Playwright testing documentation.
Testing from the terminal? Kane CLI by TestMu AI lets SDETs and developers plan, write, run and debug tests from the command line.
Author
Kailash Pathak is a Senior QA Lead Manager at 3Pillar Global with over 18 years of experience in software testing and automation. He has built scalable automation frameworks using Selenium, Cypress, and Playwright, integrating them with CI/CD pipelines and aligning them with business goals. He is the author of Web Automation Testing Using Playwright, which ranked #1 in Amazon’s “API & Operating Environments” category for six consecutive months. He is a Microsoft MVP (Most Valuable Professional) in Quality Assurance, a LinkedIn “Top QA Voice” with 19,500+ followers, and a core member of TestMu AI Spartans, DZone, and Applitools Ambassador programs. Kailash holds certifications including AWS (CFL), PMI-ACP®, ITIL®, PRINCE2 Practitioner®, and ISTQB. He has delivered 25+ QA talks across conferences and webinars and actively mentors engineers while driving quality strategies, shift-left testing, and continuous improvement.
Reviewer
Srinivasan Sekar is Director of Engineering at TestMu AI, where he leads engineering and open-source initiatives behind the Selenium and Appium automation grid and owns TestMu AI's MCP Server. A committer to Appium and a contributor to Selenium, WebdriverIO, Taiko, and AppiumTestDistribution, he brings over 15 years of experience in quality engineering and open-source technologies. He is the author of the Apress book 'The MCP Standard: A Developer's Guide to Building Universal AI Tools with the Model Context Protocol,' a Certified Kubernetes and Cloud Native Associate, and an international conference speaker. Before TestMu AI he spent over eight years at Thoughtworks as a Principal Consultant and Quality Architect. Srinivasan holds a B.Tech in Information Technology from Anna University.
On This Page
- At a Glance
- How Each Tool Works
- Speed
- Browser Support
- Language Support
- Mobile Testing
- Waits and Flaky Tests
- Tabs, Origins and iFrames
- CI and Parallel Runs
- Cost at Scale
- Same Login Test, Three Tools
- Pros and Cons
- Which Should You Choose?
- Migrating From Selenium
- Using More Than One
- npm Trends
- Hands-On Walkthroughs
- Running on TestMu AI
Playwright vs Selenium vs Cypress FAQs
Did you find this page helpful?
More Related Blogs
TestMu AI forEnterprise
Get access to solutions built on Enterprise
grade security, privacy, & compliance
- Advanced access controls
- Advanced data retention rules
- Advanced Local Testing
- Premium Support options
- Early access to beta features
- Private Slack Channel
- Unlimited Manual Accessibility DevTools Tests









