Hero Background

Power Your Software Testing with AI Agents and Cloud

The Native AI-Agentic Cloud Platform to Supercharge Quality Engineering. Test Intelligently and Ship Faster.

Tutorial

E2E Web Testing With TestCafe and JavaScript

E2E web testing with TestCafe 3.7 and JavaScript: page objects, assertions, parallel and headless runs, and a verified cloud run on Chrome, Firefox, and Safari.

Last Updated on:

A registration test I ran with TestCafe 3.7.6 typed six fields, ticked the privacy checkbox, clicked Continue, and printed 1 passed. The page it left behind said "Warning: E-Mail Address is already registered!". The test had no assertion, so all it proved was that the clicks happened.

This guide builds an end-to-end testing suite with TestCafe that fails when the app misbehaves: page objects, assertions, parallel and headless runs, and the same three tests on Chrome, Firefox, and Safari on the TestMu AI cloud grid. The test files and console output come from runs on TestCafe 3.7.6, Chrome 154, Firefox 156, and Safari 26.4. If TestCafe is new to you, start with getting started with TestCafe.

Overview

Web testing with TestCafe means writing end-to-end tests in JavaScript or TypeScript that drive real browsers without WebDriver. TestCafe 3.x automates local Chromium through the Chrome DevTools Protocol and other browsers through its own proxy, and the same test files run on TestMu AI cloud browsers through a provider plugin.

Which TestCafe Selectors Should You Know?

  • ID Selector: The ID Selector method targets one element by its unique ID, such as Selector('#search_box'), and stays stable for as long as the app keeps that ID.
  • Class Selector: The Class Selector approach targets elements with a specific class name, such as Selector('.product-item'), which is helpful for selecting groups of elements in TestCafe.
  • Attribute Selector: The Attribute Selector method targets elements based on custom or standard attributes, such as Selector('a[title*="store"]'), making it ideal for dynamic attributes in TestCafe.
  • Text Selector: The Text Selector method targets elements containing specific text, such as Selector('button').withText('Submit'), which is useful for buttons and text-driven interactions in TestCafe.
  • Nth Selector: The Nth Selector method targets specific occurrences of an element, such as Selector('li').nth(0) for the first list item, making it ideal for list-based or repeated content in TestCafe.

How Does TestCafe Handle Dynamic Elements?

  • Built-in Waiting: TestCafe selectors wait up to 10 seconds by default for an element to appear in the DOM before acting on it, so most tests need no manual waits or timeouts.
  • Smart Assertions: TestCafe assertions on selector properties, such as expect(Selector('h1').innerText), retry until they pass or the 3-second assertion timeout ends, which covers content that renders after a request.

What Are the Best Practices for TestCafe Automation?

  • Page Object Model (POM): In TestCafe, the Page Object Model keeps selectors and user actions in page classes, so a changed ID means one edit in the model instead of one edit per test.
  • Parallel Testing: The TestCafe -c flag runs several instances of one browser at once; with three Chrome instances, this guide's three-test suite ran in a median 17 seconds instead of 30.
  • Headless Mode: TestCafe runs Chrome and Firefox headless with the :headless suffix, which also avoids the resource-saving mode browsers apply to minimized windows and background tabs.

What Do You Need to Run TestCafe?

  • Node.js 20 or Later: TestCafe 3.7.6 requires Node.js 20 or later and installs with npm. Requires WebDriver: No, and no separate browser driver is needed.
  • TestMu AI Provider Plugin: The testcafe-browser-provider-lambdatest plugin runs the same suite on TestMu AI cloud browsers, including Safari on macOS from a Windows machine, and starts the tunnel automatically.
  • TestCafe Agent Skill: TestMu AI's testcafe-skill gives AI coding agents TestCafe page model, assertion, and cloud patterns, and installs with one npx agentskillsforall command.

What Is TestCafe?

TestCafe is a free, open-source Node.js framework for end-to-end web testing, maintained by DevExpress under the MIT license. You write tests in JavaScript or TypeScript, and TestCafe launches and drives the browsers itself, so there is no WebDriver binary to install or keep in step with your browser version.

Since version 3.0, TestCafe automates Chromium-based browsers such as Chrome and Edge in native automation mode over the Chrome DevTools Protocol. Native automation cannot run remote, cloud, or mobile browsers, so for those and for non-Chromium browsers TestCafe uses its Hammerhead proxy, which emulates browser events with client-side automation scripts, as the TestCafe native automation FAQ explains.

The TestCafe browser support page lists Chromium, Google Chrome, Chrome Canary, Chromium-based Microsoft Edge, Mozilla Firefox, Opera, and Safari. TestCafe 3.0 dropped Internet Explorer 11, and it does not support the legacy, pre-Chromium Edge.

The framework is still widely installed: npm's download counts API reports 774,545 downloads of the testcafe package in September 2026.

Does TestCafe Use Selenium?

No. TestCafe never calls WebDriver to type, click, or assert; WebDriver appears only when a browser provider uses it to launch a remote browser. TestMu AI's TestCafe provider does exactly that on the cloud grid, opening a WebDriver session that points the browser at TestCafe's proxy on your machine, and TestCafe drives the page from there.

The session log from my final cloud run shows that split. The grid recorded eight WebDriver commands for the Chrome session: one to create it, one to open TestCafe's connection URL, five keepalive scripts sent 30 seconds apart, and one to end it. The typing, clicks, and assertions in all three tests went through TestCafe's proxy instead.

The TestCafe command-line reference sets a 10,000 ms selector timeout and a 3,000 ms assertion timeout by default, the figures behind the Waiting row below.

AspectTestCafeSelenium WebDriver
How it drives the browserNative CDP automation for local Chromium; the Hammerhead proxy for other, remote, and cloud browsersThe W3C WebDriver protocol, through a browser driver such as chromedriver
LanguagesJavaScript and TypeScriptJava, Python, C#, Ruby, and JavaScript
Driver setupNone; install the npm package and runSelenium Manager, bundled since Selenium 4.6, finds or downloads the driver
WaitingSelectors wait up to 10 seconds by default, and assertions retry for 3 secondsExplicit or implicit waits that you write
Parallel runsThe built-in -c flag runs several browser instancesYour test runner's parallel mode or Selenium Grid
Cloud runs on TestMu AIThe testcafe-browser-provider-lambdatest plugin, which starts the tunnel for youRemoteWebDriver pointed at the TestMu AI hub

The Selenium entry in the Driver setup row comes from the Selenium Manager documentation. Pick TestCafe when the team writes JavaScript or TypeScript and wants the runner, waits, and parallel runs in one npm package. Pick Selenium when tests must be written in Java, Python, or C#, or when other tools in your pipeline already speak WebDriver; the guide to Selenium alternatives compares the wider field.

How to Set Up a TestCafe Project

TestCafe 3.7.6 declares Node.js 20 or later in its package metadata. Create a project folder and install TestCafe as a dev dependency:

mkdir testcafe-e2e-demo
cd testcafe-e2e-demo
npm init -y
npm install --save-dev testcafe
npx testcafe -v

The last command printed 3.7.6. Running TestCafe through npx uses the project's own copy, so you never need a global install, and every teammate and CI job runs the version recorded in package.json.

The suite keeps page models and tests in separate folders:

testcafe-e2e-demo/
  package.json
  page-models/
    login-page.js
    register-page.js
    search-page.js
  tests/
    login.test.js
    register.test.js
    search.test.js

The tests run against the ecommerce playground, an OpenCart demo store run by LambdaTest, now TestMu AI, with registration, login, and product search pages.

How to Write TestCafe E2E Tests With Page Objects

A page model keeps selectors and user actions in one class, so when the store renames an ID you fix one file rather than every test that uses it. The models below follow the TestCafe skill from TestMu AI's agent skills, covered at the end of this guide: selectors in the constructor, and actions as async methods that use the imported test controller t. The Page Object Model guide explains the pattern in depth.

Model the Registration Form

Save the registration model as page-models/register-page.js:

import { Selector, t } from 'testcafe';

class RegisterPage {
    constructor() {
        this.firstName = Selector('#input-firstname');
        this.lastName = Selector('#input-lastname');
        this.email = Selector('#input-email');
        this.telephone = Selector('#input-telephone');
        this.password = Selector('#input-password');
        this.confirm = Selector('#input-confirm');
        // The checkbox is visually hidden; its label receives the click
        this.agreeLabel = Selector('label[for="input-agree"]');
        this.continueButton = Selector('input[type="submit"][value="Continue"]');
        this.alert = Selector('.alert-danger');
    }

    async register(user) {
        await t
            .typeText(this.firstName, user.firstName)
            .typeText(this.lastName, user.lastName)
            .typeText(this.email, user.email)
            .typeText(this.telephone, user.telephone)
            .typeText(this.password, user.password)
            .typeText(this.confirm, user.password)
            .click(this.agreeLabel)
            .click(this.continueButton);
    }
}

export default new RegisterPage();

The privacy checkbox is the one selector that needs care. The store hides the real checkbox input behind a styled label, and when I pointed the click at #input-agree directly, TestCafe 3.7.6 printed this warning and clicked the label instead:

TestCafe cannot interact with the <input type="checkbox" name="agree" value="1" class="custom-control-input" id="input-agree"> element because another element obstructs it.
When something overlaps the action target, TestCafe performs the action with the topmost element at the original target's location.
The following element with a greater z-order replaced the original action target: <label class="custom-control-label" for="input-agree">...</label>.
Review your code to prevent this behavior.

Targeting label[for="input-agree"] states that intent in the model and removes the warning. For selector types beyond IDs and attributes, such as withText() and nth(), see the TestCafe selectors tutorial.

Assert What the App Returned

The test from the introduction drove this same model without an assertion and passed. When I added an assertion that the page heading reads "Your Account Has Been Created!", the run failed and showed what the store actually returned (stack trace trimmed):

 Account registration
 × Registers a new customer

   1) AssertionError: expected 'Register Account' to deeply equal 'Your Account Has Been Created!'

      + expected - actual

      -Register Account
      +Your Account Has Been Created!

 1/1 failed (17s)

The address johndoe@example.com already has an account on this shared demo store, so the reliable check is the rejection the app performs. The test below asserts the duplicate-email warning, which also means it creates no new account however often you run it. Save it as tests/register.test.js:

import registerPage from '../page-models/register-page';

fixture('Account registration')
    .page('https://ecommerce-playground.lambdatest.io/index.php?route=account/register')
    // The demo store's own scripts are not under test
    .skipJsErrors({ pageUrl: /ecommerce-playground/ });

test('Rejects an email that is already registered', async t => {
    await registerPage.register({
        firstName: 'John', lastName: 'Doe', email: 'johndoe@example.com',
        telephone: '0712345678', password: 'Qwerty123!'
    });

    await t
        .expect(registerPage.alert.innerText)
        .contains('E-Mail Address is already registered');
});

expect(...).contains() is a smart assertion: TestCafe retries it until it passes or the 3-second assertion timeout runs out, so the test needs no t.wait(). The skipJsErrors line exists because TestCafe fails a test on any uncaught page error, and in my cloud runs the store sometimes threw $ is not defined when jQuery had not loaded. Keep that check on for your own app, where such an error is a bug.

Add Search and Login Tests

The search model types into the header search box and presses Enter. filterVisible() picks the visible input, because the store renders a second search field for small screens:

import { Selector, t } from 'testcafe';

class SearchPage {
    constructor() {
        this.searchInput = Selector('input[name="search"]').filterVisible();
        this.heading = Selector('h1');
        this.productTitles = Selector('.product-thumb h4.title a');
    }

    async searchFor(term) {
        await t
            .typeText(this.searchInput, term, { replace: true })
            .pressKey('enter');
    }
}

export default new SearchPage();

The search test checks the results heading, the result count, and the first product title:

import searchPage from '../page-models/search-page';

fixture('Product search')
    .page('https://ecommerce-playground.lambdatest.io/')
    // The demo store's own scripts are not under test
    .skipJsErrors({ pageUrl: /ecommerce-playground/ });

test('Search returns matching products', async t => {
    await searchPage.searchFor('iPhone');

    await t
        .expect(searchPage.heading.innerText).contains('iPhone')
        .expect(searchPage.productTitles.count).gt(0)
        .expect(searchPage.productTitles.nth(0).innerText).contains('iPhone');
});

The login model fills the two fields and submits the form:

import { Selector, t } from 'testcafe';

class LoginPage {
    constructor() {
        this.email = Selector('#input-email');
        this.password = Selector('#input-password');
        this.loginButton = Selector('input[type="submit"][value="Login"]');
        this.alert = Selector('.alert-danger');
    }

    async login(email, password) {
        await t.typeText(this.email, email)
            .typeText(this.password, password)
            .click(this.loginButton);
    }
}

export default new LoginPage();

The login test builds a new address with Date.now() on every run, so it always exercises the unknown-account path:

import loginPage from '../page-models/login-page';

fixture('Login')
    .page('https://ecommerce-playground.lambdatest.io/index.php?route=account/login')
    // The demo store's own scripts are not under test
    .skipJsErrors({ pageUrl: /ecommerce-playground/ });

test('Rejects an unknown account', async t => {
    await loginPage.login(`nobody-${Date.now()}@example.com`, 'WrongPass123!');

    await t
        .expect(loginPage.alert.innerText)
        .contains('No match for E-Mail Address and/or Password');
});

Run the suite in headless Chrome:

npx testcafe chrome:headless tests/

On my Windows 11 machine with Chrome 154, all three tests passed in 28 seconds.

Note

Note: Run this TestCafe suite on Chrome, Firefox, and Safari at the same time on TestMu AI, from any operating system. Try TestMu AI free!

How to Run TestCafe Tests in Parallel and Headless Mode

Headless mode runs the browser without a window, and TestCafe supports it for Chrome and Firefox with the :headless suffix. It also avoids a trap that TestCafe's getting started guide warns about: a minimized window or a background tab can push the browser into a resource-saving mode that harms the test. The guide to headless browser testing covers when a headed run is still worth it.

The -c flag sets concurrency, opening several instances of the same browser and splitting the tests between them:

npx testcafe -c 3 chrome:headless tests/
 Running tests in:
 - Chrome 154.0.0.0 / Windows 11
 - Chrome 154.0.0.0 / Windows 11
 - Chrome 154.0.0.0 / Windows 11

 Login
 √ Rejects an unknown account

 Account registration
 √ Rejects an email that is already registered

 Product search
 √ Search returns matching products


 3 passed (15s)

Across nine runs of each mode on the same three tests, the sequential suite took 26 to 41 seconds (median 30) and -c 3 took 14 to 23 seconds (median 17). That is the payoff of parallel testing: the suite takes about as long as its slowest test plus browser startup.

To run several browsers in one command, separate the aliases with commas and no spaces. npx testcafe chrome:headless,edge:headless tests/ passed all three tests on Chrome 154 and Edge 154 in 29 seconds.

Both are Chromium browsers, so native automation stays on. Adding Firefox or Safari to the same command turns it off for the whole run, which is why the TestCafe command-line reference recommends separate runs for Chromium and non-Chromium browsers.

How to Run TestCafe Tests on a Cloud Grid

A local machine covers the browsers installed on one operating system, and Safari does not run on Windows at all. TestMu AI's Automation Cloud runs TestCafe suites on 3,000+ browser and OS combinations and records video and console logs for each session, which is how the run below tested Safari 26.4 on macOS Tahoe from a Windows 11 machine.

Choosing which browsers belong in that matrix is covered in the TestCafe cross browser testing guide. The steps here get one suite running on three of them.

Install TestMu AI's TestCafe browser provider, which adds the lambdatest browser alias to TestCafe:

npm install --save-dev testcafe-browser-provider-lambdatest

Set your TestMu AI username and access key, from the Credentials section of your account, as environment variables. I ran this bash form from Git Bash on Windows 11, and it works unchanged on macOS and Linux:

export LT_USERNAME="your_username"
export LT_ACCESS_KEY="your_access_key"

In Windows PowerShell, the equivalent lines are:

$env:LT_USERNAME = "your_username"
$env:LT_ACCESS_KEY = "your_access_key"

List the browser aliases your account can use. The full list ran to more than five thousand lines, including these Windows 11 and macOS Tahoe entries:

npx testcafe -b lambdatest
"lambdatest:Chrome@154.0:Windows 11"
"lambdatest:Firefox@156.0:Windows 11"
"lambdatest:MicrosoftEdge@154.0:Windows 11"
"lambdatest:Safari@26.0:MacOS Tahoe"

Prefer @latest over a pinned version. The list included Chrome@156.0:Windows 11, but the grid rejected that alias with "Could not find a valid browserVersionData", while Chrome@latest:Windows 11 resolved to Chrome 154.

Put per-browser capabilities in a JSON file keyed by the exact alias text, and point LT_CAPABILITY_PATH at it. This file turns on network logs for Chrome and Firefox, console logs for Chrome, and a 1920x1080 screen for Safari:

{
    "Chrome@latest:Windows 11": {
        "network": true,
        "console": true
    },
    "Firefox@latest:Windows 11": {
        "network": true
    },
    "Safari@latest:MacOS Tahoe": {
        "resolution": "1920x1080"
    }
}

The provider also reads LT_BUILD, LT_TEST_NAME, LT_RESOLUTION, and LT_TUNNEL_NAME, plus switches such as LT_NETWORK and LT_VIDEO that apply to every browser in the run. Run the suite on three cloud browsers at once:

export LT_BUILD="TestCafe E2E blog 2026-10-05"
export LT_CAPABILITY_PATH=./lt-capabilities.json
npx testcafe "lambdatest:Chrome@latest:Windows 11","lambdatest:Firefox@latest:Windows 11","lambdatest:Safari@latest:MacOS Tahoe" tests/ --skip-uncaught-errors --selector-timeout 30000

The flags at the end of that command fix failures from earlier runs:

  • --skip-uncaught-errors - The provider keeps each session alive with a WebDriver script call every 30 seconds, sent in the legacy JSON Wire format. Firefox 156 answered "HTTP method not allowed" and Safari 26.4 "unknown command", and TestCafe reported each refusal as an unhandled promise rejection in every running test on every browser. The flag ignores unhandled rejections, including any in your own test code, while failed assertions still fail the test.
  • --selector-timeout 30000 - Remote browsers fetch every request through TestCafe's proxy and the tunnel. In one Safari run, typing six fields took 42 seconds and the duplicate-email warning appeared about 8.6 seconds after Continue, close to the 10-second default that an earlier run had exceeded.

TestCafe listed the three cloud browsers, each with a link to its session log:

 Running tests in:
 - Chrome 154.0.0.0 / Windows 10 ( https://automation.lambdatest.com/logs/?sessionID=385ba0b82225798313e034663da5ee4b )
 - Firefox 156.0 / Windows 10 ( https://automation.lambdatest.com/logs/?sessionID=04dc3f40-0f15-452a-982d-507699db0944 )
 - Safari 26.4 / macOS 10.15.7 ( https://automation.lambdatest.com/logs/?sessionID=2F25E4A5-9476-4198-9AAE-BBD6DD93C0A5 )

The labels read Windows 10 and macOS 10.15.7 because TestCafe takes the OS from each browser's user-agent string, which browsers freeze at those versions. The session records on the grid show Windows 11 and macOS Tahoe, each with its video and console log.

Every test passed on every browser:

 Login
 √ Rejects an unknown account

 Account registration
 √ Rejects an email that is already registered

 Product search
 √ Search returns matching products


 3 passed (4m 21s)

This 2020 TestMu AI webinar walks through the same integration in more depth; the commands above reflect the 2026 versions.

Youtube thumbnail

Troubleshooting Local and Cloud Runs

These errors came up while building this guide, each with the fix that worked:

  • Cannot establish one or more browser connections - On Windows 11 with Chrome 154, each local run left its headless Chrome running, and the next run waited two minutes before failing with this error. Ending the leftover chrome.exe whose --user-data-dir points into %TEMP%\testcafe fixed it every time.
  • Error: spawn EBUSY - The first cloud run after installing the provider failed while starting the tunnel, in two separate projects on Windows. The provider had just downloaded its tunnel binary, and running the same command again started the tunnel normally.
  • The environment you requested was unavailable - The grid refuses an alias it cannot serve, even one that testcafe -b lambdatest listed. Switch to @latest or a version from the current list.
  • Unhandled promise rejection: [safeExecute(1)] Error response status: 1 - This is the keepalive refusal from Firefox and Safari described above; add --skip-uncaught-errors.
  • A JavaScript error occurred on - The page threw an uncaught error. Fix it when the page is yours, and scope skipJsErrors to the page URL when the script belongs to someone else.
Austin Siewert

Austin Siewert

Co-Founder, Steadfast Systems

Discovered @TestMu AI yesterday. Best browser testing tool I've found for my use case. Great pricing model for the limited testing I do 👏

2M+ Devs and QAs rely on TestMu AI

Deliver immersive digital experiences with Next-Generation Mobile Apps and Cross Browser Testing Cloud

Generate TestCafe Tests With the TestCafe Agent Skill

TestMu AI's open-source agent skills include a testcafe-skill that gives AI coding agents such as Claude Code the TestCafe patterns used in this guide: page models, smart assertions instead of t.wait(), and the lambdatest alias for cloud runs. This command installed it into .claude/skills/testcafe-skill and reported "Installed 1 skill":

npx agentskillsforall add https://github.com/LambdaTest/agent-skills.git --skill testcafe-skill --agent claude-code -y --copy

With the skill in place, ask your agent for a TestCafe test of one of your own flows, then check its page model against this guide: label clicks for hidden inputs, assertions on what the app returned, and no fixed waits.

Conclusion

Start with the three tests from this guide: run npx testcafe chrome:headless tests/ until all three pass, then run the same folder on lambdatest:Safari@latest:MacOS Tahoe with the flags from the cloud section. The TestMu AI TestCafe setup docs list every provider variable, and TestCafe testing on TestMu AI runs your own suite the same way.

Author

...

Bonnie

Blogs: 5

  • Twitter
  • Linkedin

Bonnie is a software developer, Community Contributor, and co-founder of Tech Content Marketers with 10+ years experience across AI, software development, and software testing technology. She has worked with organizations like TestMu AI, DbVis Software, and CopilotKit, authoring technical content that bridges complex technology with practical insights. Bonnie actively contributes to global tech communities through writing and AI innovation.

Reviewer

...

Sri Harsha

Reviewer

  • Linkedin

Sri Harsha is Engineering Manager of the Open Source Program Office at TestMu AI (formerly LambdaTest), where he leads open-source engineering behind the Selenium and Appium automation grid and builds agentic AI systems for quality engineering. He is a member of the Selenium Technical Leadership Committee and a committer to WebdriverIO and Appium, and was recognized with the LambdaTest Delta Award 2023 for Best Contributor in open-source testing. He brings over 10 years of experience in software testing and automation, with earlier roles at EPAM Systems and ZenQ. Sri Harsha holds a B.Tech in Computer Science from Jawaharlal Nehru Technological University.

TestCafe E2E Testing 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