Testland
Browse all skills & agents

web-e2e-overview

Teaches web end-to-end testing from first principles: what browser-driven E2E covers and how it differs from unit and integration tests, a decision table for choosing between Playwright, Cypress, Selenium WebDriver, WebdriverIO, Puppeteer, TestCafe and the BrowserStack / Sauce Labs / LambdaTest cloud grids based on files already present in the repo, install and first-run commands for each, and the flakiness traps (fixed sleeps, CSS and XPath selectors, state shared between tests) that sink new suites. Use when a web application has no E2E coverage yet, when picking or replacing an E2E framework, or when a first browser test needs to go green end to end.

Install with skills.sh (any agent)

npx skills add testland/qa --skill web-e2e-overview
View source

web-e2e-overview

What web E2E testing is

Web end-to-end (E2E) testing drives a real browser the way a user would: a page is loaded, interactions are performed (clicks, form fills, navigations), and outcomes are asserted against what the user should see. Per playwright.dev/docs/intro (opens in new window): "Playwright Test is an end-to-end test framework for modern web apps. It bundles test runner, assertions, isolation, parallelization and rich tooling." That bundle is what distinguishes an E2E framework from a bare browser-automation library: a runner, an assertion library with retry, per-test isolation, and parallelism.

E2E sits above unit and integration tests, not instead of them. Unit tests call a function; integration tests call a module against a real dependency; E2E boots the whole stack (browser, app, API, database) and checks a user journey. It is the only layer that catches "the button is wired to the wrong handler", and the most expensive layer to own: per martinfowler.com/articles/practical-test-pyramid (opens in new window), E2E tests "are notoriously flaky and often fail for unexpected and unforeseeable reasons" (browser quirks, timing, animations, popups), so "you should aim to reduce the number of end-to-end tests to a bare minimum" and spend them on core journeys only: search a product, add to basket, check out.

Practical starting budget (a convention among practitioners, not a documented standard): five to fifteen E2E specs covering signup, login, the primary create / read / update path, checkout or submit, and one permissions case. Everything else belongs a layer down.

Choosing a framework

Read the repo first, then the table. Every row is keyed on something you can observe without asking anyone. Take the first row that matches, top to bottom.

# Run at the repo root before choosing anything.
ls playwright.config.* cypress.config.* wdio.conf.* .testcaferc.* nightwatch.conf.* 2>/dev/null
ls -d cypress e2e tests/e2e 2>/dev/null
grep -iE '"(@playwright/test|cypress|webdriverio|@wdio/cli|puppeteer|testcafe|nightwatch|selenium-webdriver)"' package.json
grep -riE 'selenium-(java|webdriver)|Selenium\.WebDriver' pom.xml build.gradle* requirements.txt *.csproj Gemfile 2>/dev/null
What you observeUseWhy
playwright.config.ts / .js, or @playwright/test in package.jsonPlaywrightAlready chosen. Add specs, do not add a second runner.
cypress.config.ts / .js, or a cypress/ directoryCypressAlready chosen. Extend it; a migration is a separate project with its own budget.
wdio.conf.ts / .jsWebdriverIOAlready chosen.
.testcaferc.json / .testcaferc.jsTestCafeAlready chosen.
nightwatch.conf.*, or nightwatch in package.jsonNightwatch (legacy)Keep it running, but plan a migration to Playwright or WebdriverIO rather than growing the suite.
selenium-java, selenium-webdriver, or Selenium.WebDriver in a build fileSelenium WebDriverAlready chosen, usually with years of page objects behind it.
No E2E config anywhere, and the repo has a package.jsonPlaywrightDefault for greenfield. One install covers Chromium, Firefox and WebKit [pwi (opens in new window)].
No E2E config, and the team will not add Node (pure Java / C# / Ruby / Python shop)Selenium WebDriverThe only option with first-class bindings in Java, Python, C#, Ruby, JavaScript and Kotlin [sellib (opens in new window)].
Safari / WebKit coverage is a stated requirementPlaywrightShips WebKit in the default install [pwi (opens in new window)]. Cypress lists WebKit as experimental and opt-in [cybr (opens in new window)].
The same suite must also drive a native mobile or Electron appWebdriverIOAutomates web, hybrid and native mobile via Appium, and native desktop such as Electron [wdiowhy (opens in new window)].
The task is scraping, screenshotting or HTML-to-PDF, with no assertionsPuppeteerA library, not a test framework: "a JavaScript library which provides a high-level API to control Chrome or Firefox" [ppt (opens in new window)]. No runner, no isolation, no parallelism.
A no-WebDriver architecture is a hard constraint and its wait model appealsTestCafeRuns on Node.js and waits automatically for selectors, actions, assertions, XHR and redirects [tcwait (opens in new window)].
Real iOS / Android devices, old browser versions, or a large OS matrix are requiredKeep the runner above, add a cloud gridSee "Real devices and browser matrices" below. This is an execution target, not a framework choice.

Tie-break when two rows both look true: whatever is already in the repo wins. A second E2E framework doubles the CI cost, the selector conventions and the flake surface, and buys coverage you could get by adding one grid config to the framework you have. If the repo shows two competing frameworks (@playwright/test AND cypress both in devDependencies), stop and ask which is canonical - do not silently pick one. And never recommend a framework swap on an existing project without a stated reason: "modernity" alone burns years of page-object investment for marginal gain.

First runnable path: Playwright

For the default choice, per playwright.dev/docs/intro (opens in new window):

# 1. Scaffold: prompts for TS/JS, tests folder, CI workflow, browser binaries.
npm init playwright@latest

# 2. Run the generated example to confirm the install is healthy.
npx playwright test

The wizard writes playwright.config.ts and tests/example.spec.ts, and updates package.json [pwi (opens in new window)]. npx playwright test runs headless and in parallel by default across Chromium, Firefox and WebKit [pwi (opens in new window)]. Success looks like: every example spec passes on a clean machine, with no browser window appearing. If it fails here, the problem is the install, not your app.

Then open tests/example.spec.ts, delete it, and write your first real spec in that shape:

import { test, expect } from '@playwright/test';

test('user can reach the pricing page', async ({ page }) => {
  await page.goto('https://example.com/');
  await page.getByRole('link', { name: 'Pricing' }).click();
  await expect(page.getByRole('heading', { name: 'Pricing' })).toBeVisible();
});

Two things in that snippet are the whole discipline: locators named after what the user sees, and an await expect(...) assertion that retries. Both are covered below.

First runnable path for the other tools

ToolInstallFirst run
Cypressnpm install cypress --save-dev [cyins (opens in new window)]npx cypress open, then pick end-to-end or component mode [cyins (opens in new window)]
Selenium (Python)pip install selenium [sellib (opens in new window)]run your test file with the project's runner (pytest, JUnit, NUnit)
Selenium (Java/Maven)add the org.seleniumhq.selenium:selenium-java dependency to pom.xml [sellib (opens in new window)]mvn test
WebdriverIOnpm init wdio@latest . [wdio (opens in new window)]npx wdio run ./wdio.conf.js [wdio (opens in new window)]
Puppeteernpm i puppeteer (downloads a compatible browser) [ppt (opens in new window)]node your-script.js
TestCafenpm install -g testcafe [tcgs (opens in new window)]testcafe chrome getting-started.js [tcgs (opens in new window)]

Selenium drives "a browser natively, as a user would, either locally or on a remote machine using the Selenium server" [selwd (opens in new window)], which is why it has no bundled runner: you bring JUnit, pytest, NUnit or RSpec yourself.

Real devices and browser matrices

A local runner covers the engines installed on the machine. When the requirement is real hardware, legacy browser versions, or dozens of OS/browser pairs, point the same suite at a hosted grid instead of rewriting it.

ProviderCredentialsBest fit
BrowserStack Automateusername + access key from the account profile, set in browserstack.yml [bs (opens in new window)]broadest device matrix - advertises "3000+ real devices and desktop browsers" [bs (opens in new window)] - and enterprise procurement
Sauce Labsusername + access key from User Settings [sl (opens in new window)]Selenium-grid-centric parallel CI farms; region-specific hubs, e.g. https://ondemand.us-west-1.saucelabs.com/wd/hub, https://ondemand.eu-central-1.saucelabs.com/wd/hub [sl (opens in new window)]
LambdaTestLT_USERNAME and LT_ACCESS_KEY environment variables [lt (opens in new window)]cost-sensitive, smaller scale; grid endpoint and capabilities are generated from the dashboard [lt (opens in new window)]

A grid is recommended in addition to the chosen framework, never as a substitute. Two pairing cautions: Cypress runs its own runner, not WebDriver, so it cannot target a WebDriver hub URL - use Cypress Cloud or the vendor's Cypress-specific runner instead. And Puppeteer stays Chromium-only no matter what grid sits behind it - for cross-browser coverage pick Playwright. Setup for all three vendors is in cloud-grid-e2e.

Decide the matrix from analytics, not from anxiety. Grid minutes are billed, and a matrix of twenty combinations turns a two-minute suite into a twenty-minute one.

The traps that bite first

1. Fixed sleeps instead of retrying assertions. This is the single largest source of flake. A hard-coded pause is either too short (fails on a slow CI box) or too long (a suite that takes an hour). Cypress states plainly that "waiting for arbitrary time periods using cy.wait(Number) is an anti-pattern" and directs you to "use route aliases or assertions to guard Cypress from proceeding until an explicit condition is met" [cybp (opens in new window)]. Playwright's equivalent: "By using web first assertions Playwright will wait until the expected condition is met" [pwbp (opens in new window)], and before every action it runs actionability checks for visibility, stability, event reception and enabled state [pwact (opens in new window)].

// Wrong: guesses at timing, fails on a slow runner.
await page.waitForTimeout(3000);
expect(await page.locator('.toast').isVisible()).toBe(true);

// Right: retries until the condition holds or the timeout expires.
await expect(page.getByRole('status')).toHaveText('Saved');

Corollary: isVisible() and friends return a boolean once, immediately, with no waiting [pwbp (opens in new window)]. If you find yourself adding a sleep to make one pass, you wanted a retrying assertion instead.

2. CSS and XPath selectors instead of role and label locators. Selectors bound to class names, nth-child positions or generated IDs break on the next CSS refactor even though nothing a user can perceive has changed. Playwright's guidance is to "prefer user-facing attributes to XPath or CSS selectors" and reach for page.getByRole('button', { name: 'submit' }) [pwbp (opens in new window)]. Cypress, which has no role locator by default, instead tells you to "add data-* attributes to make it easier to target elements" and use [data-cy="submit"] selectors, "isolated from styling or behavioral changes" [cybp (opens in new window)].

Preference order that works in every framework: accessible role plus accessible name, then label or placeholder text, then visible text, then a dedicated test attribute (data-testid / data-cy), and only then CSS. Never XPath positions. The bonus is real: a test that can only find the button by its role is a test that fails when the button loses its accessible name.

3. State shared between tests. Order-dependent tests pass locally in one order and fail in CI in another, and they fail catastrophically under parallelism. Playwright: "Each test should be completely isolated from another test and should run independently with its own local storage, session storage, data, cookies etc." [pwbp (opens in new window)]. Cypress: "Tests should always be able to be run independently from one another and still pass" [cybp (opens in new window)]. Both recommend beforeEach for setup rather than letting test N depend on test N-1 [pwbp (opens in new window)] [cybp (opens in new window)].

This matters more than it sounds because parallelism is on by default. Playwright runs tests in worker processes that are "OS processes, running independently", each with its own browser, and "you can't communicate between the workers" [pwpar (opens in new window)]. A shared login, a shared seeded record, or a fixed username will collide the moment two workers touch it. Create per-test data with a unique suffix, and set up auth state programmatically rather than by driving the login form in every spec.

4. Treating E2E as the coverage strategy. Every validation rule pushed into a browser test costs a browser boot. When a case can be checked one layer down, check it one layer down [ptp (opens in new window)].

Going deeper

Optional. Each tool above has a dedicated in-depth component if the full set is installed alongside this one; nothing here depends on them.

ToolDeeper reference
Playwrightplaywright-testing
Cypresscypress-testing
Selenium WebDriverselenium-testing
WebdriverIOwebdriverio-testing
Puppeteerplaywright-testing (Puppeteer is a browser-automation library, not a test framework; migrate to Playwright)
TestCafeplaywright-testing (marginal adoption; not recommended for new projects)
BrowserStack / Sauce Labs / LambdaTestcloud-grid-e2e (one pattern, per-vendor references)

Related skills

browser-matrix-strategy-reference

Pure-reference for designing and reviewing a browser / OS / device test matrix from traffic data - the T1/T2/T3 tier-membership heuristics (T1 >=5% traffic, T2 1-5% or statutory, T3 <1% with customer demand), the traffic-share sources (own analytics, StatCounter, MDN browser-compat-data), a worked matrix template with tier-change log, the matrix review checklist (staleness, T1 oversize, below-threshold T1 entries, missing real-device coverage), how to justify dropping a legacy browser (IE11, old iOS Safari), and the compatibility budget (tier caps, CI cost formula, published support statement) in references/compatibility-budget.md. Use when designing an initial matrix, capping or publishing a support policy, running a quarterly re-tier review, or making the case to drop a browser. This is the WHAT-to-test strategy reference - to execute the matrix use playwright-testing browser projects (bundled engines), selenium-grid-4-runner (self-hosted), or cloud-grid-e2e (managed grids).

cloud-grid-e2e

Author and run E2E tests on a cloud browser grid - BrowserStack Automate, Sauce Labs, or LambdaTest. All three follow one pattern: username + access-key env vars, a W3C WebDriver hub URL, a vendor options dict inside the capabilities (bstack:options / sauce:options / LT:Options), a local tunnel binary for internal apps, session pass/fail reporting, and a CI matrix throttled to the plan's parallel-session limit. Worked example uses BrowserStack; per-vendor deltas live in references/. Use for cross-browser regression on real devices + browsers beyond the engines bundled on the local machine - distinct from a local matrix runner and from self-hosted Selenium Grid.

cypress-testing

Authors and improves Cypress E2E tests - installs Cypress, configures `cypress.config.ts`, authors `cy.*` command chains, refactors existing specs (`cy.wait(ms)` sleeps into assertions, repeated flows into `cy.session` custom commands), and debugs with the time-travel GUI; Cypress Cloud for parallel runs and recording. Use for both greenfield test authoring and improving hand-written specs already in the codebase. For automated refactor of raw Cypress Studio recordings specifically, use a dedicated codegen-review pass.

playwright-testing

Authors and remediates Playwright E2E tests across Chromium, Firefox, WebKit - `npm init playwright@latest` scaffolding, `playwright.config.ts` browser projects, accessibility-first locators (`getByRole`/`getByLabelText`) to replace brittle CSS selectors, web-first assertions to eliminate `waitForTimeout` flakiness, Page Object pattern, trace viewer debugging, sharded parallel execution with merged HTML reporting, mobile-web emulation via the `devices` catalog (viewport / DPR / touch per-device projects), the cross-browser matrix with branded channels (chrome / msedge) in references/browser-matrix.md, and GitHub Actions CI integration. Use for new test authoring, flakiness remediation, mobile-breakpoint regression, cross-browser matrix setup, and CI setup; for reviewing codegen output specifically, use a dedicated codegen-review pass.

selenium-grid-4-runner

Author and operate Selenium Grid 4 - self-hosted distributed WebDriver. Covers the six-component architecture (Router / Distributor / Session Map / Event Bus / New Session Queue / Node), standalone vs hub-and-node modes, the Docker-image stack (selenium/standalone-chrome, selenium/hub, selenium/node-chrome), node registration, session-queue tuning, and observability. Use for self-hosted cross-browser testing when data residency or cost-control require an on-prem grid. This is the self-hosted execution RUNNER - for the zero-infra alternative use playwright-testing browser projects (bundled engines); for managed cloud grids use cloud-grid-e2e (BrowserStack / Sauce Labs / LambdaTest); to decide WHICH browsers and tiers to run use browser-matrix-strategy-reference.

selenium-testing

Authors Selenium WebDriver tests in any of its 6+ supported languages (Java, Python, JavaScript, C#, Ruby, Kotlin, PHP) - picks the appropriate language binding, configures WebDriver per browser, uses `By.*` locators with the team's accessibility-first preference where supported, runs locally + via Selenium Grid for distributed execution, parses results to JUnit XML. Use for legacy Selenium-locked stacks; new projects pick Playwright or Cypress.

webdriverio-testing

Authors WebdriverIO E2E tests - `npm init wdio@latest` scaffolding, services architecture (sauce, browserstack, appium, devtools), reporters (spec, allure, junit), built-in Mocha/Jasmine/Cucumber framework integrations. WebdriverIO sits between Selenium (W3C protocol) and Playwright (modern API) - Selenium-protocol-compatible with rich plugin ecosystem. Use when the team needs WebDriver protocol + service-based device-farm integration.