Easy to use Node.js based end-to-end testing solution for web applications using the W3C WebDriver API.
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Structured page object pattern for organizing elements, sections, and custom commands into reusable, maintainable test components.
Define page objects with elements, sections, and custom commands for organized test structure.
/**
* Page object definition interface
*/
interface PageObjectModel {
// Page URL (string or function)
url: string | (() => string);
// Element definitions
elements: Record<string, ElementProperties>;
// Section definitions for nested components
sections: Record<string, SectionProperties>;
// Custom command definitions
commands: Record<string, Function>;
// Custom properties
props?: Record<string, any> | (() => Record<string, any>);
}
/**
* Element properties definition
*/
interface ElementProperties {
selector: string;
locateStrategy?: LocateStrategy;
index?: number;
abortOnFailure?: boolean;
timeout?: number;
retryInterval?: number;
suppressNotFoundErrors?: boolean;
}
/**
* Section properties definition
*/
interface SectionProperties {
selector: string;
locateStrategy?: LocateStrategy;
elements?: Record<string, ElementProperties>;
sections?: Record<string, SectionProperties>;
commands?: Record<string, Function>;
props?: Record<string, any> | (() => Record<string, any>);
}
/**
* Locate strategy options
*/
type LocateStrategy = 'css selector' | 'xpath' | 'tag name' | 'class name' | 'id' | 'name' | 'link text' | 'partial link text';Usage Examples:
// Define a login page object
const loginPage = {
url: 'https://example.com/login',
elements: {
usernameInput: '#username',
passwordInput: '#password',
submitButton: 'button[type="submit"]',
errorMessage: {
selector: '.error-message',
suppressNotFoundErrors: true
}
},
sections: {
header: {
selector: '#header',
elements: {
logo: '.logo',
userMenu: '.user-menu'
}
}
},
commands: {
login: function(username, password) {
return this
.setValue('@usernameInput', username)
.setValue('@passwordInput', password)
.click('@submitButton');
}
}
};
module.exports = loginPage;Runtime page object instances with navigation and element access capabilities.
/**
* Enhanced page object instance interface
*/
interface PageObjectInstance {
// Page object metadata
name: string;
// Nightwatch client and API access
client: NightwatchClient;
api: NightwatchAPI;
// Element access function
element: (selector: string) => Element;
// Element instances map (using @ syntax)
elements: Record<string, ElementInstance>;
// Section instances map
section: Record<string, SectionInstance>;
// Custom properties
props: Record<string, any>;
// Navigation method
navigate(url?: string): Promise<PageObjectInstance>;
}
/**
* Element instance with page object context
*/
interface ElementInstance {
selector: string;
locateStrategy: LocateStrategy;
// Standard element methods
click(): Promise<ElementInstance>;
setValue(value: string): Promise<ElementInstance>;
getText(): Promise<string>;
isVisible(): Promise<boolean>;
// Page object specific methods
waitForElementVisible(timeout?: number): Promise<ElementInstance>;
assert: {
visible(): Promise<ElementInstance>;
present(): Promise<ElementInstance>;
textContains(text: string): Promise<ElementInstance>;
};
}
/**
* Section instance with nested elements and sections
*/
interface SectionInstance {
selector: string;
elements: Record<string, ElementInstance>;
sections: Record<string, SectionInstance>;
// Section-specific commands
[commandName: string]: Function;
}Usage Examples:
// Use page object in test
module.exports = {
'Login Test': function(browser) {
const loginPage = browser.page.loginPage();
loginPage
.navigate()
.login('testuser', 'password123')
.assert.visible('@errorMessage'); // Using @ syntax for elements
}
};Navigate to page URLs with support for dynamic URL generation.
/**
* Navigate to page URL
* @param url - Optional URL override
* @returns Promise resolving with page object instance
*/
pageObject.navigate(url?: string): Promise<PageObjectInstance>;
/**
* Dynamic URL generation function
* @param params - URL parameters
* @returns Generated URL string
*/
type UrlFunction = (params?: Record<string, any>) => string;Usage Examples:
// Static URL page object
const homePage = {
url: 'https://example.com',
elements: {
welcomeMessage: '#welcome'
}
};
// Dynamic URL page object
const userProfilePage = {
url: function(userId) {
return `https://example.com/users/${userId}`;
},
elements: {
profileName: '#profile-name',
profileEmail: '#profile-email'
}
};
// Usage in tests
module.exports = {
'Navigate to pages': function(browser) {
// Navigate to static URL
const home = browser.page.homePage();
home.navigate();
// Navigate to dynamic URL
const profile = browser.page.userProfilePage();
profile.navigate('123'); // Navigates to /users/123
}
};Access page object elements using various syntax patterns.
/**
* Element access using @ syntax (recommended)
*/
pageObject.click('@elementName');
pageObject.setValue('@inputElement', 'value');
/**
* Element access using elements map
*/
pageObject.elements.elementName.click();
pageObject.elements.inputElement.setValue('value');
/**
* Direct selector access
*/
pageObject.click('css selector', '#direct-selector');Usage Examples:
// Page object definition
const formPage = {
elements: {
firstName: '#first-name',
lastName: '#last-name',
submitBtn: 'button[type="submit"]'
},
commands: {
fillForm: function(first, last) {
return this
.setValue('@firstName', first) // @ syntax
.setValue('@lastName', last)
.click('@submitBtn');
}
}
};
// Test usage
module.exports = {
'Form Test': function(browser) {
const form = browser.page.formPage();
// Using @ syntax
form.setValue('@firstName', 'John');
form.setValue('@lastName', 'Doe');
form.click('@submitBtn');
// Using elements map
form.elements.firstName.setValue('Jane');
form.elements.lastName.setValue('Smith');
// Using custom command
form.fillForm('Bob', 'Johnson');
}
};Organize related elements into reusable sections within page objects.
/**
* Section definition with nested elements
*/
interface SectionDefinition {
selector: string;
elements: Record<string, ElementProperties>;
sections?: Record<string, SectionDefinition>;
commands?: Record<string, Function>;
}Usage Examples:
// Page object with sections
const dashboardPage = {
url: '/dashboard',
sections: {
navigation: {
selector: '#nav-bar',
elements: {
homeLink: 'a[href="/"]',
profileLink: 'a[href="/profile"]',
settingsLink: 'a[href="/settings"]'
},
commands: {
navigateToProfile: function() {
return this.click('@profileLink');
}
}
},
sidebar: {
selector: '.sidebar',
elements: {
searchBox: 'input[type="search"]',
filterButton: '.filter-btn'
},
sections: {
filters: {
selector: '.filters-panel',
elements: {
categoryFilter: '#category',
dateFilter: '#date-range'
}
}
}
}
}
};
// Test usage
module.exports = {
'Dashboard Test': function(browser) {
const dashboard = browser.page.dashboardPage();
dashboard.navigate();
// Access section elements
dashboard.section.navigation.click('@profileLink');
dashboard.section.sidebar.setValue('@searchBox', 'test query');
// Access nested section elements
dashboard.section.sidebar.section.filters.click('@categoryFilter');
// Use section commands
dashboard.section.navigation.navigateToProfile();
}
};Define reusable command functions for common page interactions.
/**
* Custom command function signature
* @param this - Page object or section instance
* @param args - Command arguments
* @returns Promise or page object instance for chaining
*/
type CustomCommand = (this: PageObjectInstance | SectionInstance, ...args: any[]) => any;Usage Examples:
// Page object with custom commands
const loginPage = {
url: '/login',
elements: {
username: '#username',
password: '#password',
submitButton: '#submit',
errorMessage: '.error'
},
commands: {
// Simple login command
login: function(user, pass) {
return this
.setValue('@username', user)
.setValue('@password', pass)
.click('@submitButton');
},
// Login with validation
loginAndExpectSuccess: function(user, pass) {
return this
.login(user, pass)
.waitForElementNotVisible('@errorMessage', 2000)
.assert.urlContains('/dashboard');
},
// Async custom command
loginWithDelay: async function(user, pass, delay = 1000) {
await this.setValue('@username', user);
await this.pause(delay);
await this.setValue('@password', pass);
await this.click('@submitButton');
return this;
}
}
};
// Test usage
module.exports = {
'Login Commands Test': function(browser) {
const login = browser.page.loginPage();
login.navigate();
// Use custom commands
login.login('testuser', 'password123');
login.loginAndExpectSuccess('validuser', 'validpass');
}
};Define dynamic properties and computed values for page objects.
/**
* Properties definition (object or function)
*/
interface PropertiesDefinition {
[key: string]: any;
}
type PropertiesFunction = (this: PageObjectInstance) => Record<string, any>;Usage Examples:
// Page object with static properties
const apiPage = {
url: '/api/docs',
props: {
apiBaseUrl: 'https://api.example.com/v1',
defaultHeaders: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
},
elements: {
endpointList: '.endpoint-list'
}
};
// Page object with dynamic properties
const userPage = {
url: function(userId) {
return `/users/${userId}`;
},
props: function() {
return {
currentUser: this.api.globals.currentUser,
baseUrl: this.api.launchUrl,
timestamp: new Date().toISOString()
};
},
elements: {
userProfile: '#user-profile'
},
commands: {
verifyUserData: function() {
const { currentUser } = this.props;
return this.assert.textContains('#user-name', currentUser.name);
}
}
};
// Test usage
module.exports = {
'Properties Test': function(browser) {
const userPage = browser.page.userPage();
// Access static properties
console.log('API Base URL:', userPage.props.apiBaseUrl);
// Access dynamic properties
const timestamp = userPage.props.timestamp;
console.log('Page loaded at:', timestamp);
// Use properties in commands
userPage.verifyUserData();
}
};Best practices for organizing page objects in test projects.
File Structure:
pages/
├── index.js // Export all page objects
├── loginPage.js // Login page object
├── dashboardPage.js // Dashboard page object
└── components/
├── header.js // Reusable header section
└── navigation.js // Reusable navigation sectionUsage Examples:
// pages/loginPage.js
module.exports = {
url: '/login',
elements: {
usernameInput: '#username',
passwordInput: '#password',
submitButton: '#submit'
},
commands: {
login: function(username, password) {
return this
.setValue('@usernameInput', username)
.setValue('@passwordInput', password)
.click('@submitButton');
}
}
};
// pages/index.js - Export all page objects
module.exports = {
loginPage: require('./loginPage'),
dashboardPage: require('./dashboardPage')
};
// nightwatch.conf.js - Configure page objects path
module.exports = {
page_objects_path: './pages',
// ... other config
};
// Test file usage
module.exports = {
'E2E Test': function(browser) {
const login = browser.page.loginPage();
const dashboard = browser.page.dashboardPage();
login
.navigate()
.login('testuser', 'password');
dashboard
.navigate()
.assert.visible('@welcomeMessage');
}
};