Even the most carefully designed test automation frameworks will encounter failures and issues that require debugging. This module explores advanced debugging and troubleshooting techniques for WebDriverIO tests, helping you identify and resolve common problems efficiently.
By the end of this module, you will be able to:
When a test fails, it's important to understand the nature of the failure:
A systematic approach to debugging test failures:
// Example of a debugging workflow in practice
describe('Debugging Example', () => {
it('should demonstrate debugging workflow', async () => {
try {
// Step that might fail
await $('#non-existent-element').click();
} catch (error) {
// 1. Analyze the error
console.error('Error type:', error.name);
console.error('Error message:', error.message);
// 2. Capture evidence
await browser.saveScreenshot('./debug/error-screenshot.png');
// 3. Get additional context
const html = await browser.execute(() => document.body.innerHTML);
console.log('Current HTML:', html);
// 4. Check element state
const exists = await $('#non-existent-element').isExisting();
console.log('Element exists:', exists);
// Re-throw to fail the test
throw error;
}
});
});
The browser.debug()
command pauses test execution and opens a REPL interface:
it('should debug element interaction', async () => {
await browser.url('/login');
// Pause execution for debugging
await browser.debug();
// After debugging, continue with the test
await $('#username').setValue('testuser');
await $('#password').setValue('password');
await $('#login-button').click();
});
In the REPL, you can:
browser
and $
/$$
selectorsConfigure WebDriverIO for better debugging:
// wdio.conf.js
exports.config = {
// ...
logLevel: 'debug', // 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'silent'
outputDir: './logs',
// Capture more detailed logs
reporters: [
'spec',
['junit', {
outputDir: './reports',
outputFileFormat: (options) => {
return `results-${options.cid}.xml`;
}
}]
],
// Take screenshots on failure
afterTest: async function(test, context, { error, result, duration, passed, retries }) {
if (error) {
await browser.takeScreenshot();
}
},
// ...
};
WebDriverIO provides access to Chrome DevTools Protocol for advanced debugging:
// Network monitoring
await browser.cdp('Network', 'enable');
browser.on('Network.responseReceived', (params) => {
console.log(`URL: ${params.response.url}`);
console.log(`Status: ${params.response.status}`);
console.log(`Type: ${params.type}`);
});
// Performance monitoring
await browser.cdp('Performance', 'enable');
const performanceMetrics = await browser.cdp('Performance', 'getMetrics');
console.log('Performance metrics:', performanceMetrics.metrics);
// Console monitoring
await browser.cdp('Console', 'enable');
browser.on('Console.messageAdded', (params) => {
console.log(`Browser console [${params.message.level}]: ${params.message.text}`);
});
// JavaScript errors
await browser.cdp('Runtime', 'enable');
browser.on('Runtime.exceptionThrown', (params) => {
console.log('JavaScript error:', params.exceptionDetails.text);
console.log('Stack trace:', params.exceptionDetails.stackTrace);
});
// utils/debugHelper.js
class DebugHelper {
static async highlightElement(selector) {
const element = await $(selector);
await browser.execute((el) => {
const originalStyle = el.getAttribute('style') || '';
el.setAttribute('style', `${originalStyle}; border: 2px solid red; background-color: yellow;`);
// Reset after 2 seconds
setTimeout(() => {
el.setAttribute('style', originalStyle);
}, 2000);
}, element);
// Wait for visual confirmation
await browser.pause(2000);
}
static async takeElementScreenshot(selector, filename) {
const element = await $(selector);
await element.saveScreenshot(`./debug/${filename}.png`);
}
static async logElementState(selector) {
const element = await $(selector);
const state = {
selector,
exists: await element.isExisting(),
displayed: await element.isDisplayed(),
enabled: await element.isEnabled(),
text: await element.getText(),
value: await element.getValue(),
location: await element.getLocation(),
size: await element.getSize()
};
console.log('Element state:', JSON.stringify(state, null, 2));
return state;
}
static async logPageState() {
const url = await browser.getUrl();
const title = await browser.getTitle();
const html = await browser.execute(() => document.documentElement.outerHTML);
console.log(`Current URL: ${url}`);
console.log(`Page title: ${title}`);
// Save HTML to file
const fs = require('fs');
fs.writeFileSync(`./debug/page-${Date.now()}.html`, html);
// Take full page screenshot
await browser.saveScreenshot(`./debug/page-${Date.now()}.png`);
}
}
module.exports = DebugHelper;
// Usage in tests
const DebugHelper = require('../utils/debugHelper');
it('should debug form submission', async () => {
await browser.url('/form');
// Highlight element being interacted with
await DebugHelper.highlightElement('#username');
await $('#username').setValue('testuser');
// Log element state
await DebugHelper.logElementState('#submit-button');
// Take screenshot of specific element
await DebugHelper.takeElementScreenshot('#form', 'form-before-submit');
// Submit form
await $('#submit-button').click();
// Log page state after submission
await DebugHelper.logPageState();
});
// utils/logger.js
const winston = require('winston');
const path = require('path');
const fs = require('fs');
// Create logs directory if it doesn't exist
const logsDir = path.join(process.cwd(), 'logs');
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir);
}
// Create logger
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
defaultMeta: { service: 'wdio-tests' },
transports: [
// Console output
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
}),
// File output
new winston.transports.File({
filename: path.join(logsDir, 'error.log'),
level: 'error'
}),
new winston.transports.File({
filename: path.join(logsDir, 'combined.log')
})
]
});
// Add test context information
logger.addTestContext = function(context) {
this.defaultMeta = {
...this.defaultMeta,
...context
};
};
// Log test step
logger.step = function(message) {
this.info(`STEP: ${message}`);
};
// Log test action
logger.action = function(message) {
this.debug(`ACTION: ${message}`);
};
// Log assertion
logger.assertion = function(message) {
this.debug(`ASSERT: ${message}`);
};
module.exports = logger;
// Usage in tests
const logger = require('../utils/logger');
describe('Login Tests', () => {
beforeEach(async function() {
// Add test context
logger.addTestContext({
test: this.currentTest.title,
suite: this.test.parent.title
});
await browser.url('/login');
logger.step('Navigated to login page');
});
it('should login with valid credentials', async () => {
logger.step('Entering username and password');
logger.action('Setting username');
await $('#username').setValue('testuser');
logger.action('Setting password');
await $('#password').setValue('password');
logger.step('Submitting login form');
await $('#login-button').click();
logger.step('Verifying successful login');
logger.assertion('Dashboard should be displayed');
const dashboardHeader = await $('.dashboard-header');
await dashboardHeader.waitForDisplayed();
expect(await dashboardHeader.isDisplayed()).toBe(true);
});
});
// utils/contextLogger.js
class ContextLogger {
constructor(baseLogger) {
this.logger = baseLogger;
this.context = {};
}
setContext(context) {
this.context = {
...this.context,
...context
};
return this;
}
clearContext() {
this.context = {};
return this;
}
formatMessage(message) {
const contextStr = Object.entries(this.context)
.map(([key, value]) => `[${key}:${value}]`)
.join(' ');
return contextStr ? `${contextStr} ${message}` : message;
}
debug(message) {
this.logger.debug(this.formatMessage(message));
return this;
}
info(message) {
this.logger.info(this.formatMessage(message));
return this;
}
warn(message) {
this.logger.warn(this.formatMessage(message));
return this;
}
error(message, error) {
const formattedMessage = this.formatMessage(message);
if (error) {
this.logger.error(`${formattedMessage}: ${error.message}`, { stack: error.stack });
} else {
this.logger.error(formattedMessage);
}
return this;
}
step(message) {
return this.info(`STEP: ${message}`);
}
action(message) {
return this.debug(`ACTION: ${message}`);
}
assertion(message) {
return this.debug(`ASSERT: ${message}`);
}
}
module.exports = ContextLogger;
// Usage in page objects
const logger = require('../utils/logger');
const ContextLogger = require('../utils/contextLogger');
class LoginPage {
constructor() {
this.logger = new ContextLogger(logger).setContext({ page: 'LoginPage' });
this.usernameInput = $('#username');
this.passwordInput = $('#password');
this.loginButton = $('#login-button');
this.errorMessage = $('.error-message');
}
async login(username, password) {
this.logger.setContext({ action: 'login', username });
this.logger.action('Setting username');
await this.usernameInput.setValue(username);
this.logger.action('Setting password');
await this.passwordInput.setValue(password);
this.logger.action('Clicking login button');
await this.loginButton.click();
this.logger.info('Login attempt completed');
return this;
}
async getErrorMessage() {
this.logger.action('Getting error message');
if (await this.errorMessage.isExisting()) {
const message = await this.errorMessage.getText();
this.logger.info(`Error message: ${message}`);
return message;
}
this.logger.info('No error message found');
return null;
}
}
// utils/tracer.js
class ExecutionTracer {
constructor() {
this.steps = [];
this.startTime = Date.now();
this.currentStep = null;
}
startStep(description) {
// Complete previous step if exists
if (this.currentStep) {
this.completeStep();
}
this.currentStep = {
description,
startTime: Date.now(),
endTime: null,
duration: null,
status: 'running',
error: null,
screenshot: null
};
console.log(`STEP START: ${description}`);
return this;
}
async completeStep(error = null) {
if (!this.currentStep) {
return this;
}
this.currentStep.endTime = Date.now();
this.currentStep.duration = this.currentStep.endTime - this.currentStep.startTime;
this.currentStep.status = error ? 'failed' : 'passed';
this.currentStep.error = error ? {
message: error.message,
stack: error.stack
} : null;
// Take screenshot if step failed
if (error) {
try {
const screenshotPath = `./debug/step-${this.steps.length + 1}-failed.png`;
await browser.saveScreenshot(screenshotPath);
this.currentStep.screenshot = screenshotPath;
} catch (screenshotError) {
console.error('Failed to take screenshot:', screenshotError.message);
}
}
this.steps.push(this.currentStep);
console.log(`STEP ${this.currentStep.status.toUpperCase()}: ${this.currentStep.description} (${this.currentStep.duration}ms)`);
this.currentStep = null;
return this;
}
async executeStep(description, action) {
this.startStep(description);
try {
const result = await action();
await this.completeStep();
return result;
} catch (error) {
await this.completeStep(error);
throw error;
}
}
getReport() {
const endTime = Date.now();
const totalDuration = endTime - this.startTime;
return {
startTime: new Date(this.startTime).toISOString(),
endTime: new Date(endTime).toISOString(),
totalDuration,
steps: this.steps,
passedSteps: this.steps.filter(step => step.status === 'passed').length,
failedSteps: this.steps.filter(step => step.status === 'failed').length
};
}
saveReport(filename = `execution-report-${Date.now()}.json`) {
const fs = require('fs');
const report = this.getReport();
fs.writeFileSync(`./reports/${filename}`, JSON.stringify(report, null, 2));
return report;
}
}
module.exports = ExecutionTracer;
// Usage in tests
const ExecutionTracer = require('../utils/tracer');
describe('Checkout Process', () => {
let tracer;
beforeEach(() => {
tracer = new ExecutionTracer();
});
it('should complete checkout process', async () => {
await tracer.executeStep('Navigate to product page', async () => {
await browser.url('/products/1');
});
await tracer.executeStep('Add product to cart', async () => {
await $('#add-to-cart').click();
await $('.cart-confirmation').waitForDisplayed();
});
await tracer.executeStep('Navigate to cart', async () => {
await $('#cart-icon').click();
await $('.cart-page').waitForDisplayed();
});
await tracer.executeStep('Proceed to checkout', async () => {
await $('#checkout-button').click();
await $('.checkout-page').waitForDisplayed();
});
await tracer.executeStep('Fill shipping information', async () => {
await $('#name').setValue('Test User');
await $('#address').setValue('123 Test St');
await $('#city').setValue('Test City');
await $('#zip').setValue('12345');
await $('#continue-button').click();
});
await tracer.executeStep('Complete payment', async () => {
await $('#card-number').setValue('4111111111111111');
await $('#card-expiry').setValue('12/25');
await $('#card-cvc').setValue('123');
await $('#payment-button').click();
});
await tracer.executeStep('Verify order confirmation', async () => {
await $('.confirmation-page').waitForDisplayed();
const confirmationText = await $('.confirmation-message').getText();
expect(confirmationText).toContain('Your order has been placed');
});
// Save execution report
tracer.saveReport('checkout-execution.json');
});
afterEach(async function() {
// If test failed, save the execution report
if (this.currentTest.state === 'failed') {
tracer.saveReport(`failed-${this.currentTest.title.replace(/\s+/g, '-')}.json`);
}
});
});
// Common issue: Element not found
it('should handle element not found', async () => {
await browser.url('/dynamic-page');
// Problem: Element might not be in DOM yet
// await $('#dynamic-element').click(); // This might fail
// Solution 1: Wait for element to exist
await $('#dynamic-element').waitForExist({ timeout: 10000 });
await $('#dynamic-element').click();
// Solution 2: Use try-catch with retry
const clickWithRetry = async (selector, maxRetries = 3) => {
let retries = 0;
while (retries < maxRetries) {
try {
const element = await $(selector);
await element.waitForExist({ timeout: 2000 });
await element.click();
return;
} catch (error) {
retries++;
if (retries >= maxRetries) {
throw new Error(`Failed to click ${selector} after ${maxRetries} retries: ${error.message}`);
}
console.log(`Retry ${retries}/${maxRetries} for ${selector}`);
await browser.pause(1000);
}
}
};
await clickWithRetry('#another-dynamic-element');
});
// Common issue: Stale element reference
it('should handle stale element references', async () => {
await browser.url('/page-with-updates');
// Problem: Element reference becomes stale after page update
const element = await $('#updating-element');
await browser.execute(() => {
// Simulate page update that causes element to become stale
document.getElementById('updating-element').innerHTML = 'Updated content';
});
// This might fail with stale element reference error
// await element.click();
// Solution 1: Re-query the element
await $('#updating-element').click();
// Solution 2: Create a wrapper function that handles stale elements
const clickWithStaleProtection = async (selector) => {
try {
const element = await $(selector);
await element.click();
} catch (error) {
if (error.message.includes('stale element reference')) {
console.log('Encountered stale element, retrying with fresh reference');
const freshElement = await $(selector);
await freshElement.click();
} else {
throw error;
}
}
};
await clickWithStaleProtection('#another-updating-element');
});
// Common issue: Timing and synchronization
it('should handle timing and synchronization issues', async () => {
await browser.url('/async-page');
// Problem: Element is in DOM but not visible/clickable yet
// await $('#loading-button').click(); // This might fail
// Solution 1: Wait for element to be clickable
await $('#loading-button').waitForClickable({ timeout: 10000 });
await $('#loading-button').click();
// Problem: Need to wait for an AJAX request to complete
await $('#trigger-ajax').click();
// Solution 2: Wait for specific condition
await browser.waitUntil(async () => {
const text = await $('#ajax-result').getText();
return text !== 'Loading...';
}, {
timeout: 10000,
timeoutMsg: 'Expected AJAX request to complete'
});
// Problem: Need to wait for animation to complete
await $('#animate-button').click();
// Solution 3: Wait for animation attribute/class to change
await browser.waitUntil(async () => {
const className = await $('#animated-element').getAttribute('class');
return !className.includes('animating');
}, {
timeout: 5000,
timeoutMsg: 'Expected animation to complete'
});
});
// Common issue: Selector problems
it('should handle selector issues', async () => {
await browser.url('/complex-page');
// Problem: Complex or dynamic selectors
// Solution 1: Use different selector strategies
// CSS selector
await $('button.submit-button').click();
// XPath
await $('//button[contains(text(), "Submit")]').click();
// Element with specific text
await $('button=Submit').click();
// Element containing text
await $('button*=Sub').click();
// Problem: Need to find element within another element
// Solution 2: Use element chaining
const form = await $('#registration-form');
const submitButton = await form.$('.submit-button');
await submitButton.click();
// Problem: Multiple elements match selector
// Solution 3: Use specific index
const buttons = await $$('.action-button');
await buttons[2].click(); // Click the third button
// Solution 4: Filter elements
const visibleButtons = await $$('.action-button').filter(async (button) => {
return await button.isDisplayed();
});
await visibleButtons[0].click(); // Click the first visible button
});
// Common issue: Browser and environment problems
it('should handle browser and environment issues', async () => {
// Problem: Browser window size affecting element visibility
// Solution 1: Set specific window size
await browser.setWindowSize(1920, 1080);
await browser.url('/responsive-page');
// Problem: Browser-specific behavior
// Solution 2: Check browser and adapt behavior
const browserName = browser.capabilities.browserName;
if (browserName === 'chrome') {
// Chrome-specific code
await $('.chrome-element').click();
} else if (browserName === 'firefox') {
// Firefox-specific code
await $('.firefox-element').click();
}
// Problem: Handling alerts
// Solution 3: Try-catch for alerts
try {
await $('#alert-button').click();
await browser.acceptAlert();
} catch (error) {
if (!error.message.includes('no such alert')) {
throw error;
}
// No alert present, continue
}
// Problem: Handling file uploads
// Solution 4: Use remote file path
const filePath = '/path/to/file.jpg';
const remoteFilePath = await browser.uploadFile(filePath);
await $('#file-input').setValue(remoteFilePath);
});
// utils/selfHealingElement.js
class SelfHealingElement {
constructor(selectors, name = '') {
this.selectors = Array.isArray(selectors) ? selectors : [selectors];
this.name = name || this.selectors[0];
this.lastUsedSelector = null;
}
async find() {
let element = null;
let lastError = null;
// Try each selector in order
for (const selector of this.selectors) {
try {
element = await $(selector);
if (await element.isExisting()) {
this.lastUsedSelector = selector;
return element;
}
} catch (error) {
lastError = error;
// Continue to next selector
}
}
// If we get here, no selector worked
throw new Error(`Could not find element "${this.name}" using any of the provided selectors: ${this.selectors.join(', ')}. Last error: ${lastError?.message}`);
}
async click() {
const element = await this.find();
await element.click();
}
async setValue(value) {
const element = await this.find();
await element.setValue(value);
}
async getText() {
const element = await this.find();
return element.getText();
}
async waitForDisplayed(options = {}) {
const element = await this.find();
return element.waitForDisplayed(options);
}
async waitForExist(options = {}) {
const element = await this.find();
return element.waitForExist(options);
}
async waitForClickable(options = {}) {
const element = await this.find();
return element.waitForClickable(options);
}
// Add more methods as needed
}
module.exports = SelfHealingElement;
// Usage in page objects
const SelfHealingElement = require('../utils/selfHealingElement');
class LoginPage {
constructor() {
// Define elements with multiple selector strategies
this.usernameInput = new SelfHealingElement([
'#username',
'[name="username"]',
'//input[@placeholder="Username"]',
'.username-field'
], 'Username Input');
this.passwordInput = new SelfHealingElement([
'#password',
'[name="password"]',
'//input[@type="password"]',
'.password-field'
], 'Password Input');
this.loginButton = new SelfHealingElement([
'#login-button',
'.login-btn',
'button=Login',
'//button[contains(text(), "Login")]'
], 'Login Button');
}
async login(username, password) {
await this.usernameInput.setValue(username);
await this.passwordInput.setValue(password);
await this.loginButton.click();
}
}
// utils/smartWait.js
class SmartWait {
static async forElement(selector, options = {}) {
const defaultOptions = {
timeout: 10000,
interval: 500,
condition: 'exist', // 'exist', 'visible', 'clickable'
message: `Waiting for element "${selector}" to be ${options.condition || 'exist'}`
};
const opts = { ...defaultOptions, ...options };
console.log(opts.message);
try {
const element = await $(selector);
switch (opts.condition) {
case 'visible':
await element.waitForDisplayed({ timeout: opts.timeout });
break;
case 'clickable':
await element.waitForClickable({ timeout: opts.timeout });
break;
case 'exist':
default:
await element.waitForExist({ timeout: opts.timeout });
}
return element;
} catch (error) {
console.error(`Smart wait failed for "${selector}": ${error.message}`);
throw error;
}
}
static async forText(selector, text, options = {}) {
const defaultOptions = {
timeout: 10000,
interval: 500,
contains: true, // true for contains, false for exact match
message: `Waiting for element "${selector}" to ${options.contains ? 'contain' : 'have exact'} text "${text}"`
};
const opts = { ...defaultOptions, ...options };
console.log(opts.message);
await browser.waitUntil(async () => {
const element = await $(selector);
const actualText = await element.getText();
return opts.contains
? actualText.includes(text)
: actualText === text;
}, {
timeout: opts.timeout,
interval: opts.interval,
timeoutMsg: `Expected element "${selector}" to ${opts.contains ? 'contain' : 'have exact'} text "${text}"`
});
return $(selector);
}
static async forUrlToContain(fragment, options = {}) {
const defaultOptions = {
timeout: 10000,
interval: 500,
message: `Waiting for URL to contain "${fragment}"`
};
const opts = { ...defaultOptions, ...options };
console.log(opts.message);
await browser.waitUntil(async () => {
const url = await browser.getUrl();
return url.includes(fragment);
}, {
timeout: opts.timeout,
interval: opts.interval,
timeoutMsg: `Expected URL to contain "${fragment}"`
});
}
static async forPageLoad(options = {}) {
const defaultOptions = {
timeout: 30000,
interval: 500,
message: 'Waiting for page to load completely'
};
const opts = { ...defaultOptions, ...options };
console.log(opts.message);
await browser.waitUntil(async () => {
return await browser.execute(() => document.readyState === 'complete');
}, {
timeout: opts.timeout,
interval: opts.interval,
timeoutMsg: 'Expected page to load completely'
});
}
static async forAjaxCompletion(options = {}) {
const defaultOptions = {
timeout: 10000,
interval: 500,
message: 'Waiting for AJAX requests to complete'
};
const opts = { ...defaultOptions, ...options };
console.log(opts.message);
await browser.waitUntil(async () => {
return await browser.execute(() => {
// For jQuery
if (typeof jQuery !== 'undefined') {
return jQuery.active === 0;
}
// For Fetch API (simplified check)
if (window._activeFetchRequests === 0) {
return true;
}
// For XMLHttpRequest
return document.querySelectorAll('.xhr-in-progress').length === 0;
});
}, {
timeout: opts.timeout,
interval: opts.interval,
timeoutMsg: 'Expected AJAX requests to complete'
});
}
}
module.exports = SmartWait;
// Usage in tests
const SmartWait = require('../utils/smartWait');
it('should use smart waiting strategies', async () => {
await browser.url('/dynamic-page');
// Wait for page to load
await SmartWait.forPageLoad();
// Click button that triggers AJAX
await $('#load-data').click();
// Wait for AJAX to complete
await SmartWait.forAjaxCompletion();
// Wait for specific element to be clickable
const button = await SmartWait.forElement('#dynamic-button', {
condition: 'clickable',
timeout: 15000
});
// Click the button
await button.click();
// Wait for URL to change
await SmartWait.forUrlToContain('/result');
// Wait for specific text to appear
await SmartWait.forText('.result-message', 'Success', {
contains: true,
timeout: 20000
});
});
// utils/retryHelper.js
class RetryHelper {
static async retry(action, options = {}) {
const defaultOptions = {
maxRetries: 3,
interval: 1000,
exponentialBackoff: true,
description: 'action'
};
const opts = { ...defaultOptions, ...options };
let lastError;
for (let attempt = 1; attempt <= opts.maxRetries; attempt++) {
try {
return await action();
} catch (error) {
lastError = error;
if (attempt < opts.maxRetries) {
console.log(`Retry ${attempt}/${opts.maxRetries} for ${opts.description} failed: ${error.message}`);
// Calculate wait time (with exponential backoff if enabled)
const waitTime = opts.exponentialBackoff
? opts.interval * Math.pow(2, attempt - 1)
: opts.interval;
await browser.pause(waitTime);
}
}
}
throw new Error(`${opts.description} failed after ${opts.maxRetries} attempts. Last error: ${lastError.message}`);
}
static async retryClick(selector, options = {}) {
return this.retry(async () => {
const element = await $(selector);
await element.waitForClickable({ timeout: options.timeout || 5000 });
await element.click();
}, {
...options,
description: `clicking element "${selector}"`
});
}
static async retrySetValue(selector, value, options = {}) {
return this.retry(async () => {
const element = await $(selector);
await element.waitForExist({ timeout: options.timeout || 5000 });
await element.setValue(value);
}, {
...options,
description: `setting value "${value}" on element "${selector}"`
});
}
static async retryGetText(selector, options = {}) {
return this.retry(async () => {
const element = await $(selector);
await element.waitForExist({ timeout: options.timeout || 5000 });
return element.getText();
}, {
...options,
description: `getting text from element "${selector}"`
});
}
static async retryAssertion(assertion, options = {}) {
return this.retry(async () => {
await assertion();
}, {
...options,
description: 'assertion'
});
}
}
module.exports = RetryHelper;
// Usage in tests
const RetryHelper = require('../utils/retryHelper');
it('should use retry mechanisms', async () => {
await browser.url('/flaky-page');
// Retry clicking a potentially flaky button
await RetryHelper.retryClick('#flaky-button', {
maxRetries: 5,
interval: 500
});
// Retry setting a value
await RetryHelper.retrySetValue('#flaky-input', 'test value');
// Retry getting text
const text = await RetryHelper.retryGetText('#flaky-text');
// Retry an assertion
await RetryHelper.retryAssertion(async () => {
const result = await $('#result').getText();
expect(result).toContain('Success');
}, {
maxRetries: 10,
interval: 1000
});
});
// wdio.conf.js
exports.config = {
// ...
// Run tests in parallel
maxInstances: 5,
// Group tests by capability
maxInstancesPerCapability: 3,
// Optimize browser startup
connectionRetryTimeout: 120000,
connectionRetryCount: 3,
// Optimize test execution
waitforTimeout: 10000,
// Optimize framework settings
framework: 'mocha',
mochaOpts: {
ui: 'bdd',
timeout: 60000,
bail: true // Stop after first test failure
},
// Optimize hooks
beforeSession: function() {
// Set up environment
},
before: function() {
// Set up browser
browser.setTimeout({
'pageLoad': 10000,
'script': 5000,
'implicit': 0 // Disable implicit waits
});
},
beforeTest: function() {
// Set up test
},
// ...
};
// utils/performanceHelper.js
class PerformanceHelper {
static async disableAnimations() {
await browser.execute(() => {
const style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = `
* {
transition-duration: 0s !important;
transition-delay: 0s !important;
animation-duration: 0s !important;
animation-delay: 0s !important;
}
`;
document.head.appendChild(style);
});
}
static async disableImages() {
await browser.execute(() => {
const style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = `
img {
visibility: hidden;
}
`;
document.head.appendChild(style);
});
}
static async disableLogging() {
await browser.execute(() => {
console.log = console.warn = console.error = () => {};
});
}
static async blockThirdPartyRequests() {
// Using Chrome DevTools Protocol to block third-party requests
if (browser.capabilities.browserName === 'chrome') {
await browser.cdp('Network', 'enable');
await browser.cdp('Network', 'setBlockedURLs', {
urls: [
'*google-analytics.com*',
'*doubleclick.net*',
'*facebook.net*',
'*hotjar.com*'
]
});
}
}
static async clearBrowserData() {
// Clear cookies
await browser.deleteAllCookies();
// Clear local storage
await browser.execute(() => {
localStorage.clear();
sessionStorage.clear();
});
// Clear cache (Chrome only)
if (browser.capabilities.browserName === 'chrome') {
await browser.cdp('Network', 'clearBrowserCache');
}
}
static async measurePageLoad() {
const metrics = await browser.execute(() => {
const perfData = window.performance.timing;
const pageLoadTime = perfData.loadEventEnd - perfData.navigationStart;
const domLoadTime = perfData.domContentLoadedEventEnd - perfData.navigationStart;
return {
pageLoadTime,
domLoadTime,
networkLatency: perfData.responseEnd - perfData.requestStart,
processingTime: perfData.loadEventEnd - perfData.responseEnd,
backendTime: perfData.responseStart - perfData.navigationStart
};
});
console.log('Performance metrics:', metrics);
return metrics;
}
}
module.exports = PerformanceHelper;
// Usage in tests
const PerformanceHelper = require('../utils/performanceHelper');
describe('Performance Optimized Tests', () => {
before(async () => {
// Set up performance optimizations
await PerformanceHelper.disableAnimations();
await PerformanceHelper.disableImages();
await PerformanceHelper.blockThirdPartyRequests();
});
beforeEach(async () => {
// Clear data before each test
await PerformanceHelper.clearBrowserData();
});
it('should measure page load performance', async () => {
await browser.url('/');
const metrics = await PerformanceHelper.measurePageLoad();
// Assert on performance metrics
expect(metrics.pageLoadTime).toBeLessThan(3000);
expect(metrics.domLoadTime).toBeLessThan(1500);
});
});
Create a comprehensive debugging toolkit that includes:
Develop a self-healing test framework that includes:
Implement a performance analysis framework that:
This module explored advanced debugging and troubleshooting techniques for WebDriverIO tests. By implementing systematic debugging approaches, advanced logging strategies, self-healing mechanisms, and performance optimizations, you can create more robust and reliable test automation frameworks.
Key takeaways:
In the next module, we'll explore how to implement a real-world project using WebDriverIO, applying all the concepts and techniques learned throughout the course.