Authors TestCafe E2E tests - `npm install testcafe`, fixture/test syntax, `Selector` API for queries, automatic-waits, no WebDriver required (TestCafe injects scripts via a proxy), supports any browser including remote / cloud farms. Use when the team prefers a no-WebDriver architecture and one of TestCafe's specific features (e.g., role-based auth) matters.
73
92%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
High
Do not use without reviewing
TestCafe (DevExpress) is an alternative E2E framework that doesn't use WebDriver. Instead, it injects a JavaScript proxy into the browser; tests run in Node and the proxy synchronizes browser state.
This means:
The trade-off: the proxy approach has its own quirks; less mainstream than Playwright / Cypress / Selenium.
For new projects in 2026+: Playwright is the broader-supported choice.
npm install --save-dev testcafeNo browser-driver setup; TestCafe uses installed browsers directly.
// tests/checkout.test.js
import { Selector } from 'testcafe';
fixture('Checkout flow').page('http://localhost:3000');
test('completes checkout', async (t) => {
await t
.typeText('[data-testid=email]', 'user@example.com')
.typeText('[data-testid=password]', 'pwd')
.click('button[type=submit]');
await t.expect(Selector('h1').withText('Welcome').exists).ok();
await t
.navigateTo('/products/BOOK-001')
.click('[data-testid=add-to-cart]');
await t.expect(Selector('[data-testid=cart-count]').textContent).eql('1');
});The fluent API (t.X.Y.Z) auto-waits.
// Basic
const heading = Selector('h1');
// CSS
const button = Selector('button[type=submit]');
// By text
const submitButton = Selector('button').withText('Submit');
// By attribute
const cartCount = Selector('[data-testid=cart-count]');
// Chained
const linkInsideHeader = Selector('header').find('a').withText('Sign in');
// With state (visible, focused, etc.)
const visibleError = Selector('.error').filterVisible();TestCafe's distinguishing feature: Roles - encapsulated auth state for switching between users in tests.
import { Role } from 'testcafe';
const adminUser = Role('http://localhost:3000/login', async t => {
await t
.typeText('[data-testid=email]', 'admin@example.com')
.typeText('[data-testid=password]', 'admin-pwd')
.click('button[type=submit]');
});
const regularUser = Role('http://localhost:3000/login', async t => {
await t
.typeText('[data-testid=email]', 'user@example.com')
.typeText('[data-testid=password]', 'user-pwd')
.click('button[type=submit]');
});
test('admin sees admin panel', async t => {
await t.useRole(adminUser).expect(Selector('a').withText('Admin').exists).ok();
});
test('regular user does not see admin panel', async t => {
await t.useRole(regularUser).expect(Selector('a').withText('Admin').exists).notOk();
});Roles cache the auth state - subsequent useRole calls don't
re-run the login.
# All tests, default browser (Chrome)
npx testcafe chrome tests/
# Multiple browsers
npx testcafe chrome,firefox tests/
# Headless
npx testcafe chrome:headless tests/
# Specific test
npx testcafe chrome tests/checkout.test.js# JUnit XML (CI-friendly)
npx testcafe chrome:headless tests/ --reporter junit:reports/junit.xml
# Multiple reporters
npx testcafe chrome:headless tests/ --reporter spec,junit:reports/junit.xmlimport { Selector, RequestMock } from 'testcafe';
const mock = RequestMock()
.onRequestTo('https://api.example.com/orders/123')
.respond({ orderId: 123, status: 'shipped' }, 200, {
'access-control-allow-origin': '*',
});
fixture('Order page').page('http://localhost:3000').requestHooks(mock);
test('shows shipped status', async (t) => {
await t.expect(Selector('[data-testid=status]').textContent).eql('shipped');
});jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with: { node-version: '22' }
- run: npm ci
- run: npx testcafe chrome:headless tests/ --reporter junit:reports/junit.xml
- uses: actions/upload-artifact@v4
if: always()
with:
name: testcafe-reports
path: reports/The JUnit XML feeds junit-xml-analysis (in the qa-test-reporting plugin).
| Anti-pattern | Why it fails | Fix |
|---|---|---|
Manual await new Promise(r => setTimeout(r, 2000)) | Defeats auto-wait; flaky. | Trust TestCafe's automatic synchronization. |
| Inline credentials in tests | Secrets in code. | Roles + env vars (Step 4). |
Skipping --reporter | Default spec output not CI-parseable. | --reporter junit:reports/... (Step 6). |
| Using TestCafe for unit-test-shaped scope | E2E framework overhead for unit tests. | Use the unit framework (Jest, etc.) for unit tests. |
testcafe.io.playwright-testing,
cypress-testing,
selenium-testing - alternatives.