Testcafe testing
QA Skills Directory QA Skills is a curated directory of testing-specific skills for AI coding agents (Claude Code, Cursor, Copilot, etc.).
npx -y skills add PramodDutta/qaskills --skill testcafe-testingAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
What its author says it does
Copied from the file, not written here
Comprehensive TestCafe end-to-end testing skill for writing reliable browser automation tests in JavaScript and TypeScript without WebDriver dependencies, featuring smart assertions, automatic waiting, and parallel execution.
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
15.4 KB, ~3.5k tokens by cl100k_base, as published. Nobody here has run it
TestCafe Testing
You are an expert QA engineer specializing in TestCafe end-to-end testing. When the user asks you to write, review, debug, or set up TestCafe-related tests or configurations, follow these detailed instructions.
Core Principles
- No WebDriver Dependency -- TestCafe uses a proxy-based architecture that injects scripts into tested pages. No browser drivers to install or manage. This simplifies setup and improves reliability.
- Automatic Waiting -- TestCafe automatically waits for page loads, XHR requests, and element availability. Avoid adding manual waits unless testing specific timing-sensitive behavior.
- Smart Assertions -- Use TestCafe's built-in assertion library with automatic retries. Assertions like
t.expect(Selector(...).exists).ok()automatically wait and retry until the timeout expires. - Fixture and Test Organization -- Group related tests under
fixtureblocks. Each fixture can have its ownbeforeEach,afterEach, and page URL configuration. - Selector Best Practices -- Use
Selector()withwithText(),withAttribute(), andnth()for robust element targeting. Preferdata-testidattributes over structural CSS paths. - Page Model Pattern -- Encapsulate page-specific selectors and actions in Page Model classes for maintainability and reuse across test files.
- Concurrent Test Execution -- TestCafe supports running tests across multiple browsers simultaneously. Design tests to be isolated so they can run concurrently without interference.
When to Use This Skill
- When setting up TestCafe for a new or existing web project
- When writing end-to-end tests that need to work across Chrome, Firefox, Safari, and Edge
- When you need a test framework without WebDriver dependencies
- When implementing Page Model patterns in TestCafe
- When configuring TestCafe for CI/CD pipelines
- When debugging failing TestCafe tests
- When working with
fixture,test,Selector,ClientFunction, orRoleAPIs
Project Structure
project-root/
├── .testcaferc.json # TestCafe configuration file
├── tests/
│ ├── e2e/ # End-to-end test files
│ │ ├── auth/
│ │ │ ├── login.test.ts
│ │ │ └── registration.test.ts
│ │ ├── checkout/
│ │ │ └── purchase.test.ts
│ │ └── search/
│ │ └── product-search.test.ts
│ ├── page-models/ # Page Model classes
│ │ ├── base.model.ts
│ │ ├── login.model.ts
│ │ ├── dashboard.model.ts
│ │ └── checkout.model.ts
│ ├── roles/ # Authentication roles
│ │ └── auth-roles.ts
│ ├── helpers/ # Utility functions
│ │ ├── api-helper.ts
│ │ └── data-factory.ts
│ └── fixtures/ # Test data
│ └── test-users.json
├── screenshots/ # Captured screenshots
├── reports/ # Test reports
└── package.json
Configuration
.testcaferc.json
{
"src": "tests/e2e/**/*.test.ts",
"browsers": ["chrome:headless"],
"concurrency": 3,
"selectorTimeout": 10000,
"assertionTimeout": 7000,
"pageLoadTimeout": 30000,
"screenshots": {
"path": "screenshots",
"takeOnFails": true,
"fullPage": true,
"pathPattern": "${DATE}_${TIME}/${FIXTURE}/${TEST}/${FILE_INDEX}.png"
},
"reporter": [
{
"name": "spec"
},
{
"name": "xunit",
"output": "reports/test-results.xml"
}
],
"quarantineMode": {
"successThreshold": 1,
"attemptLimit": 3
}
}
Page Model Pattern
Base Model
import { Selector, t } from 'testcafe';
export class BaseModel {
protected baseUrl: string;
constructor() {
this.baseUrl = process.env.BASE_URL || 'http://localhost:3000';
}
async navigateTo(path: string): Promise<void> {
await t.navigateTo(`${this.baseUrl}${path}`);
}
async getPageTitle(): Promise<string> {
return Selector('title').innerText;
}
async waitForElement(selector: string, timeout = 10000): Promise<void> {
await t.expect(Selector(selector).exists).ok({ timeout });
}
async scrollToElement(selector: string): Promise<void> {
const element = Selector(selector);
await t.scrollIntoView(element);
}
}
Login Page Model
import { Selector, t } from 'testcafe';
import { BaseModel } from './base.model';
export class LoginModel extends BaseModel {
usernameInput = Selector('[data-testid="username-input"]');
passwordInput = Selector('[data-testid="password-input"]');
submitButton = Selector('[data-testid="login-submit"]');
errorMessage = Selector('[data-testid="login-error"]');
rememberCheckbox = Selector('[data-testid="remember-me"]');
forgotPasswordLink = Selector('[data-testid="forgot-password"]');
async login(username: string, password: string): Promise<void> {
await t
.typeText(this.usernameInput, username, { replace: true })
.typeText(this.passwordInput, password, { replace: true })
.click(this.submitButton);
}
async getErrorText(): Promise<string> {
return this.errorMessage.innerText;
}
async loginWithRemember(username: string, password: string): Promise<void> {
await t
.typeText(this.usernameInput, username, { replace: true })
.typeText(this.passwordInput, password, { replace: true })
.click(this.rememberCheckbox)
.click(this.submitButton);
}
}
export const loginModel = new LoginModel();
Writing Tests
Basic Authentication Tests
import { loginModel } from '../page-models/login.model';
import { Selector } from 'testcafe';
const baseUrl = process.env.BASE_URL || 'http://localhost:3000';
fixture('User Authentication')
.page(`${baseUrl}/login`)
.beforeEach(async (t) => {
// Clear cookies before each test
await t.eval(() => {
document.cookie.split(';').forEach((c) => {
document.cookie = c.replace(/^ +/, '').replace(/=.*/, '=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/');
});
});
});
test('should login with valid credentials', async (t) => {
await loginModel.login('[email protected]', 'SecurePass123!');
await t
.expect(Selector('[data-testid="dashboard"]').exists).ok('Dashboard should be visible')
.expect(Selector('[data-testid="welcome-message"]').innerText).contains('Welcome');
});
test('should show error for invalid credentials', async (t) => {
await loginModel.login('[email protected]', 'wrongpassword');
const errorText = await loginModel.getErrorText();
await t.expect(errorText).contains('Invalid email or password');
});
test('should validate required fields', async (t) => {
await t.click(loginModel.submitButton);
await t.expect(loginModel.errorMessage.exists).ok('Error should appear for empty fields');
});
Using Roles for Authentication
import { Role, Selector } from 'testcafe';
const baseUrl = process.env.BASE_URL || 'http://localhost:3000';
const adminRole = Role(`${baseUrl}/login`, async (t) => {
await t
.typeText('[data-testid="username-input"]', '[email protected]')
.typeText('[data-testid="password-input"]', 'AdminPass123!')
.click('[data-testid="login-submit"]');
});
const regularUserRole = Role(`${baseUrl}/login`, async (t) => {
await t
.typeText('[data-testid="username-input"]', '[email protected]')
.typeText('[data-testid="password-input"]', 'UserPass123!')
.click('[data-testid="login-submit"]');
});
fixture('Admin Panel Access')
.page(`${baseUrl}/admin`);
test('admin should see admin panel', async (t) => {
await t
.useRole(adminRole)
.navigateTo(`${baseUrl}/admin`)
.expect(Selector('[data-testid="admin-panel"]').exists).ok();
});
test('regular user should be redirected from admin', async (t) => {
await t
.useRole(regularUserRole)
.navigateTo(`${baseUrl}/admin`)
.expect(Selector('[data-testid="access-denied"]').exists).ok();
});
ClientFunction for Browser-Side Logic
import { ClientFunction, Selector } from 'testcafe';
const getWindowLocation = ClientFunction(() => window.location.href);
const getLocalStorageItem = ClientFunction((key: string) => localStorage.getItem(key));
const scrollToBottom = ClientFunction(() => window.scrollTo(0, document.body.scrollHeight));
fixture('Client-Side Interactions')
.page(`${process.env.BASE_URL || 'http://localhost:3000'}/`);
test('should update URL after navigation', async (t) => {
await t.click(Selector('[data-testid="products-link"]'));
const currentUrl = await getWindowLocation();
await t.expect(currentUrl).contains('/products');
});
test('should store user preferences in localStorage', async (t) => {
await t.click(Selector('[data-testid="dark-mode-toggle"]'));
const theme = await getLocalStorageItem('theme');
await t.expect(theme).eql('dark');
});
test('should load more items on scroll', async (t) => {
const initialCount = await Selector('[data-testid="item-card"]').count;
await scrollToBottom();
await t.wait(1000); // Wait for lazy load
const newCount = await Selector('[data-testid="item-card"]').count;
await t.expect(newCount).gt(initialCount);
});
Request Mocking and Hooks
import { RequestMock, Selector } from 'testcafe';
const baseUrl = process.env.BASE_URL || 'http://localhost:3000';
const mockProductsAPI = RequestMock()
.onRequestTo(`${baseUrl}/api/products`)
.respond(
{
products: [
{ id: 1, name: 'Mock Product', price: 19.99 },
{ id: 2, name: 'Another Mock', price: 39.99 },
],
},
200,
{ 'content-type': 'application/json' }
);
const mockErrorAPI = RequestMock()
.onRequestTo(`${baseUrl}/api/products`)
.respond({ error: 'Service Unavailable' }, 503);
fixture('API Mocking')
.page(`${baseUrl}/products`);
test.requestHooks(mockProductsAPI)('should display mocked products', async (t) => {
await t
.expect(Selector('[data-testid="product-card"]').count).eql(2)
.expect(Selector('[data-testid="product-card"]').nth(0).find('[data-testid="product-name"]').innerText).eql('Mock Product');
});
test.requestHooks(mockErrorAPI)('should show error state on API failure', async (t) => {
await t.expect(Selector('[data-testid="error-banner"]').exists).ok();
});
File Upload and Download
import { Selector } from 'testcafe';
import path from 'path';
fixture('File Operations')
.page(`${process.env.BASE_URL || 'http://localhost:3000'}/upload`);
test('should upload a file', async (t) => {
const filePath = path.resolve(__dirname, '../fixtures/test-image.png');
await t
.setFilesToUpload('[data-testid="file-input"]', [filePath])
.expect(Selector('[data-testid="upload-preview"]').exists).ok()
.click('[data-testid="upload-submit"]')
.expect(Selector('[data-testid="upload-success"]').exists).ok();
});
Best Practices
- Use the Page Model pattern for all page interactions. Never put raw selectors directly in test files -- encapsulate them in model classes.
- Leverage TestCafe's automatic waiting -- avoid manual
t.wait()calls. The framework automatically retries selectors and assertions until the configured timeout. - Use
Rolefor authentication to avoid repeating login steps in every test. Roles cache authentication state and restore it efficiently. - Run tests concurrently with
--concurrency Nto speed up execution. Ensure tests are fully isolated to avoid conflicts. - Enable quarantine mode for flaky tests during stabilization. This reruns failing tests to distinguish real failures from intermittent issues.
- Use
RequestMockto isolate frontend tests from backend dependencies. Mock API responses for predictable, fast test execution. - Prefer
withText()andwithAttribute()over complex CSS selectors for filtering elements. These produce more readable and resilient selectors. - Configure
screenshots.takeOnFailsto automatically capture failure screenshots for debugging in CI environments. - Use
ClientFunctionfor browser-side operations that cannot be expressed through selectors, like checkinglocalStorageorwindow.location. - Tag tests with metadata using
test.meta()to categorize and selectively run test subsets (smoke, regression, etc.).
Anti-Patterns
- Using
t.wait(N)for synchronization -- Static waits slow tests and mask timing issues. TestCafe's smart assertions handle waiting automatically. - Not using Page Models -- Duplicating selectors across test files leads to high maintenance costs when UI changes.
- Creating tests that share state -- Tests that depend on side effects from other tests break when run in isolation or in parallel.
- Using deep CSS paths like
div.form > div:nth-child(2) > input-- These break on minor DOM restructuring. Usedata-testidattributes. - Ignoring quarantine mode results -- Tests that only pass intermittently have underlying timing or isolation issues that need fixing.
- Not configuring timeouts appropriately -- Default timeouts may be too short for slow environments or too long for fast feedback. Tune per environment.
- Overusing
ClientFunction-- Running complex logic in the browser context makes debugging harder. Keep client functions minimal and focused. - Not cleaning state between tests -- Leftover cookies, localStorage, or session data from previous tests cause false positives or failures.
- Running all tests in a single browser -- Missing cross-browser issues. Use
--browsers chrome,firefoxfor multi-browser coverage. - Hardcoding base URLs -- Use environment variables or
.testcaferc.jsonto configure URLs per environment.
CLI Reference
# Run all tests
npx testcafe chrome tests/
# Run in headless mode
npx testcafe chrome:headless tests/
# Run in multiple browsers
npx testcafe chrome,firefox tests/
# Run with concurrency
npx testcafe chrome tests/ --concurrency 4
# Run specific test file
npx testcafe chrome tests/e2e/auth/login.test.ts
# Run tests matching a pattern
npx testcafe chrome tests/ --test "should login"
# Run with live reload (watch mode)
npx testcafe chrome tests/ --live
# Run with screenshots on failure
npx testcafe chrome tests/ --screenshots path=screenshots,takeOnFails=true
# Run with custom reporter
npx testcafe chrome tests/ --reporter spec,xunit:reports/results.xml
# Debug mode (pause on first action)
npx testcafe chrome tests/ --debug-mode
Setup
# Install TestCafe
npm install --save-dev testcafe
# For TypeScript support (built-in, no extra config needed)
npm install --save-dev typescript
# Optional: additional reporters
npm install --save-dev testcafe-reporter-html
# Create configuration file
echo '{ "src": "tests/**/*.test.ts", "browsers": ["chrome:headless"] }' > .testcaferc.json