Implementation:DevExpress Testcafe Page Object Pattern
| Knowledge Sources | |
|---|---|
| Domains | Testing, Web_Automation, Software_Design |
| Last Updated | 2026-02-12 04:00 GMT |
Overview
Concrete implementation of the Page Object Model pattern in TestCafe using ES6 classes with Selector properties, demonstrating component composition and singleton export.
Description
TestCafe implements the Page Object Model through ES6 class-based patterns, as demonstrated in the official examples. Page classes define Selector properties in the constructor to represent page elements, with optional methods for common interactions. Smaller component classes (like Feature) can be composed into larger page objects. Page objects are typically exported as singleton instances for import in test files, though factory patterns can also be used for pages requiring parameterization.
Usage
Create a class with Selector properties representing page elements. Export as singleton or factory. Import in test files and use properties directly in TestController actions.
Code Reference
Source Location
- Repository: testcafe
- File: examples/basic/page-model.js
- Lines: 1-38
Signature
// Component class pattern
class Component {
constructor(selectorOrElement) {
this.element = Selector(selectorOrElement);
// Define child selectors
}
}
// Page class pattern
class Page {
constructor() {
// Define selectors as properties
this.element1 = Selector('#id');
this.element2 = Selector('.class');
// Compose smaller components
this.component = new Component('#root');
}
// Optional: methods for complex interactions
async performAction(testController) {
await testController.click(this.element1);
}
}
// Singleton export
export default new Page();
// Or factory export
export function createPage() {
return new Page();
}
Import
// In test files
import page from './page-models/my-page';
// Or
import { createPage } from './page-models/my-page';
test('Use page object', async t => {
await t.click(page.element1);
});
I/O Contract
Inputs
| Name | Type | Required | Description |
|---|---|---|---|
| constructor params | varies | No | Optional parameters for page object initialization |
Outputs
| Name | Type | Description |
|---|---|---|
| page object | object | Instance with Selector properties and optional methods |
Usage Examples
Basic Page Object
// page-models/login-page.js
import { Selector } from 'testcafe';
class LoginPage {
constructor() {
this.usernameInput = Selector('#username');
this.passwordInput = Selector('#password');
this.submitButton = Selector('#submit-button');
this.errorMessage = Selector('.error-message');
}
}
export default new LoginPage();
// tests/login.test.js
import { Selector } from 'testcafe';
import loginPage from '../page-models/login-page';
fixture`Login Tests`
.page`https://example.com/login`;
test('Login with valid credentials', async t => {
await t
.typeText(loginPage.usernameInput, 'user@example.com')
.typeText(loginPage.passwordInput, 'password123')
.click(loginPage.submitButton)
.expect(Selector('.dashboard').exists).ok();
});
test('Show error on invalid credentials', async t => {
await t
.typeText(loginPage.usernameInput, 'invalid@example.com')
.typeText(loginPage.passwordInput, 'wrongpassword')
.click(loginPage.submitButton)
.expect(loginPage.errorMessage.visible).ok();
});
Component Composition
// page-models/components/navigation.js
import { Selector } from 'testcafe';
export class Navigation {
constructor() {
this.homeLink = Selector('nav a').withText('Home');
this.aboutLink = Selector('nav a').withText('About');
this.contactLink = Selector('nav a').withText('Contact');
this.userMenu = Selector('#user-menu');
this.logoutButton = Selector('#logout');
}
}
// page-models/dashboard-page.js
import { Selector } from 'testcafe';
import { Navigation } from './components/navigation';
class DashboardPage {
constructor() {
this.nav = new Navigation();
this.welcomeMessage = Selector('.welcome');
this.statsPanel = Selector('.stats-panel');
this.recentActivity = Selector('.recent-activity');
}
}
export default new DashboardPage();
// tests/dashboard.test.js
import dashboardPage from '../page-models/dashboard-page';
fixture`Dashboard Tests`
.page`https://example.com/dashboard`;
test('Navigate to About page', async t => {
await t
.click(dashboardPage.nav.aboutLink)
.expect(Selector('h1').textContent).contains('About');
});
TestCafe Official Example
// From examples/basic/page-model.js
import { Selector } from 'testcafe';
class Feature {
constructor(text) {
this.label = Selector('label').withText(text);
this.checkbox = this.label.find('input[type=checkbox]');
}
}
class Page {
constructor() {
this.nameInput = Selector('#developer-name');
this.triedTestCafeCheckbox = Selector('#tried-test-cafe');
this.populateButton = Selector('#populate');
this.submitButton = Selector('#submit-button');
this.results = Selector('.result-content');
this.commentsTextArea = Selector('#comments');
this.featureList = [
new Feature('Support for testing on remote devices'),
new Feature('Re-using existing JavaScript code for testing'),
new Feature('Running tests in background and/or in parallel'),
new Feature('Easy embedding into a Continuous integration system'),
new Feature('Advanced traffic and markup analysis')
];
}
}
export default new Page();
// Using the official example
import page from '../page-model';
fixture`Page Model Example`
.page`http://devexpress.github.io/testcafe/example`;
test('Fill form using page model', async t => {
await t
.typeText(page.nameInput, 'Peter Parker')
.click(page.triedTestCafeCheckbox)
.click(page.featureList[0].checkbox)
.click(page.featureList[1].checkbox)
.click(page.submitButton)
.expect(page.results.textContent).contains('Peter Parker');
});
Page Object with Methods
// page-models/registration-page.js
import { Selector } from 'testcafe';
class RegistrationPage {
constructor() {
this.firstNameInput = Selector('#first-name');
this.lastNameInput = Selector('#last-name');
this.emailInput = Selector('#email');
this.passwordInput = Selector('#password');
this.confirmPasswordInput = Selector('#confirm-password');
this.termsCheckbox = Selector('#terms');
this.submitButton = Selector('button[type="submit"]');
}
async fillBasicInfo(t, firstName, lastName, email) {
await t
.typeText(this.firstNameInput, firstName)
.typeText(this.lastNameInput, lastName)
.typeText(this.emailInput, email);
}
async fillPassword(t, password) {
await t
.typeText(this.passwordInput, password)
.typeText(this.confirmPasswordInput, password);
}
async acceptTerms(t) {
await t.click(this.termsCheckbox);
}
async submit(t) {
await t.click(this.submitButton);
}
async completeRegistration(t, userData) {
await this.fillBasicInfo(t, userData.firstName, userData.lastName, userData.email);
await this.fillPassword(t, userData.password);
await this.acceptTerms(t);
await this.submit(t);
}
}
export default new RegistrationPage();
// tests/registration.test.js
import registrationPage from '../page-models/registration-page';
fixture`Registration Tests`
.page`https://example.com/register`;
test('Complete registration', async t => {
await registrationPage.completeRegistration(t, {
firstName: 'John',
lastName: 'Doe',
email: 'john.doe@example.com',
password: 'SecurePass123'
});
await t.expect(Selector('.success-message').visible).ok();
});
Parameterized Page Objects
// page-models/product-page.js
import { Selector } from 'testcafe';
class ProductPage {
constructor(productId) {
this.productId = productId;
this.title = Selector('.product-title');
this.price = Selector('.product-price');
this.addToCartButton = Selector('#add-to-cart');
this.quantityInput = Selector('#quantity');
this.reviewsSection = Selector('.reviews');
}
get url() {
return `https://example.com/products/${this.productId}`;
}
}
// Factory function instead of singleton
export function createProductPage(productId) {
return new ProductPage(productId);
}
// tests/product.test.js
import { createProductPage } from '../page-models/product-page';
fixture`Product Tests`;
test('View product details', async t => {
const productPage = createProductPage(12345);
await t
.navigateTo(productPage.url)
.expect(productPage.title.textContent).ok()
.expect(productPage.price.textContent).match(/\$\d+\.\d{2}/);
});
Multi-level Component Hierarchy
// page-models/components/form-field.js
import { Selector } from 'testcafe';
export class FormField {
constructor(fieldName) {
this.container = Selector(`[data-field="${fieldName}"]`);
this.label = this.container.find('label');
this.input = this.container.find('input, textarea, select');
this.errorMessage = this.container.find('.error');
}
}
// page-models/components/contact-form.js
import { Selector } from 'testcafe';
import { FormField } from './form-field';
export class ContactForm {
constructor() {
this.nameField = new FormField('name');
this.emailField = new FormField('email');
this.messageField = new FormField('message');
this.submitButton = Selector('button[type="submit"]');
}
}
// page-models/contact-page.js
import { Selector } from 'testcafe';
import { ContactForm } from './components/contact-form';
class ContactPage {
constructor() {
this.heading = Selector('h1');
this.form = new ContactForm();
this.successMessage = Selector('.success-message');
}
}
export default new ContactPage();
// tests/contact.test.js
import contactPage from '../page-models/contact-page';
fixture`Contact Tests`
.page`https://example.com/contact`;
test('Submit contact form', async t => {
await t
.typeText(contactPage.form.nameField.input, 'Jane Doe')
.typeText(contactPage.form.emailField.input, 'jane@example.com')
.typeText(contactPage.form.messageField.input, 'Hello!')
.click(contactPage.form.submitButton)
.expect(contactPage.successMessage.visible).ok();
});