by thetestingacademy
Node.js E2E testing framework with TypeScript support, smart assertions, and parallel execution
npx @qaskills/cli add testcafe-testingAuto-detects your AI agent and installs the skill. Works with Claude Code, Cursor, Copilot, and more.
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.
t.expect(Selector(...).exists).ok() automatically wait and retry until the timeout expires.fixture blocks. Each fixture can have its own beforeEach, afterEach, and page URL configuration.Selector() with withText(), withAttribute(), and nth() for robust element targeting. Prefer data-testid attributes over structural CSS paths.fixture, test, Selector, ClientFunction, or Role APIsproject-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
{
"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
}
}
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);
}
}
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();
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('testuser@example.com', '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('invalid@example.com', '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');
});
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"]', 'admin@example.com')
.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"]', 'user@example.com')
.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();
});
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);
});
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();
});
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();
});
t.wait() calls. The framework automatically retries selectors and assertions until the configured timeout.Role for authentication to avoid repeating login steps in every test. Roles cache authentication state and restore it efficiently.--concurrency N to speed up execution. Ensure tests are fully isolated to avoid conflicts.RequestMock to isolate frontend tests from backend dependencies. Mock API responses for predictable, fast test execution.withText() and withAttribute() over complex CSS selectors for filtering elements. These produce more readable and resilient selectors.screenshots.takeOnFails to automatically capture failure screenshots for debugging in CI environments.ClientFunction for browser-side operations that cannot be expressed through selectors, like checking localStorage or window.location.test.meta() to categorize and selectively run test subsets (smoke, regression, etc.).t.wait(N) for synchronization -- Static waits slow tests and mask timing issues. TestCafe's smart assertions handle waiting automatically.div.form > div:nth-child(2) > input -- These break on minor DOM restructuring. Use data-testid attributes.ClientFunction -- Running complex logic in the browser context makes debugging harder. Keep client functions minimal and focused.--browsers chrome,firefox for multi-browser coverage..testcaferc.json to configure URLs per environment.# 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
# 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
- name: Install QA Skills
run: npx @qaskills/cli add testcafe-testing10 of 29 agents supported