Automate Web Application Testing with WebDriverIO
WebDriverIO test expert skill for config, page objects, custom commands, and CI-ready parallel end-to-end suites.
Maintainer of this project? Claim this page to edit the listing.
1.0.0Add to Favorites
Why it matters
Leverage WebDriverIO expertise to build and maintain robust, scalable end-to-end test suites for web applications. This asset specializes in creating maintainable test configurations, implementing effective Page Object Models, and employing advanced testing patterns for reliable automation.
Outcomes
What it gets done
Configure WebDriverIO for local, staging, and production environments.
Implement Page Object Models for modular and reusable test components.
Develop robust test cases using Mocha and best practices for element selection and waits.
Create custom WebDriverIO commands for enhanced test functionality.
Install
Add it to your toolbox
Run in your project directory:
curl -fsSL https://spark.entire.vc/get/vb-webdriverio-test | bash Overview
WebDriverIO Test Expert
WebDriverIO skill covering wdio.conf.js configuration, Page Object Model structure, custom commands, data-driven tests, and CI-ready parallel/headless execution for end-to-end test suites. Use when configuring, structuring, or hardening WebDriverIO test suites for reliability and CI performance; not for testing individual UI components in isolation.
What it does
WebDriverIO test automation expert skill covering configuration, page object modeling, test structuring, and performance patterns for robust, maintainable, scalable end-to-end test suites. It covers a base wdio.conf.js structure (runner, specs glob, capabilities/browser options, timeouts, retry counts, Mocha framework with BDD-style timeout, spec/allure reporters); environment-specific configuration that merges a base config with per-environment overrides (local/staging/prod base URLs) selected via a TEST_ENV variable; a Page Object Model with a BasePage class providing reusable helpers (open, waitForPageLoad, scrollToElement, safeClick, safeSetValue) and a page-specific subclass such as LoginPage using data-testid-based element getters and composed login/error-handling methods; robust Mocha test specs using beforeEach setup and explicit assertions on URL and page state; custom WebDriverIO commands (browser.addCommand) for both browser-level and element-level reusable actions; data-driven tests iterating over a JSON fixture of test cases; and parallel/headless execution configuration for CI.
When to use - and when NOT to
Use it when building or hardening WebDriverIO end-to-end test suites - configuring wdio.conf.js, structuring page objects, writing resilient specs, adding custom commands, or optimizing for CI performance. Its guiding principle is to write tests that are independent, deterministic, and give clear failure feedback, and to test user journeys rather than individual UI components.
Inputs and outputs
Inputs are the application under test (via baseUrl and element selectors, preferring data-testid attributes over CSS selectors or brittle XPath expressions) and test scenario data (hardcoded in specs or loaded from a JSON fixture for data-driven tests). Reliable wait strategies favor explicit, condition-based waits over fixed delays:
// Good: Explicit waits with meaningful timeouts
await element.waitForDisplayed({ timeout: 10000 })
// Better: Wait for specific conditions
await browser.waitUntil(async () => {
const elements = await $$('.list-item')
return elements.length >= 5
}, { timeout: 15000, timeoutMsg: 'List did not load expected items' })
// Best: Combine multiple wait conditions
await Promise.all([
element.waitForDisplayed(),
element.waitForEnabled(),
element.waitForClickable()
])
Output is a configured, running WebDriverIO test suite reporting via the spec and allure reporters, and on assertion failure, an automatic screenshot capture plus a descriptive error message (expected vs. actual) for debugging.
Integrations
Runs on Mocha (BDD interface) as the test framework, uses deepmerge to layer environment-specific config over a base wdio.base.conf.js, and supports Chrome via goog:chromeOptions (including --headless, --disable-gpu, --no-sandbox for CI) with configurable maxInstances for parallel execution.
Who it's for
QA engineers and developers building end-to-end browser test suites with WebDriverIO who need reusable page objects, resilient wait strategies, custom commands, and CI-ready parallel or headless configuration.
Source README
WebDriverIO Test Expert
You are an expert in WebDriverIO test automation, specializing in creating robust, maintainable, and scalable end-to-end test suites. You have deep knowledge of WebDriverIO's API, configuration patterns, page object models, and testing best practices.
Core Configuration Principles
Base Configuration Structure
// wdio.conf.js
exports.config = {
runner: 'local',
specs: ['./test/specs/**/*.js'],
exclude: [],
maxInstances: 10,
capabilities: [{
browserName: 'chrome',
'goog:chromeOptions': {
args: ['--no-sandbox', '--disable-dev-shm-usage']
}
}],
logLevel: 'info',
bail: 0,
baseUrl: 'http://localhost:3000',
waitforTimeout: 10000,
connectionRetryTimeout: 120000,
connectionRetryCount: 3,
framework: 'mocha',
reporters: ['spec', 'allure'],
mochaOpts: {
ui: 'bdd',
timeout: 60000
}
}
Environment-Specific Configurations
// wdio.conf.js
const merge = require('deepmerge')
const baseConfig = require('./wdio.base.conf.js')
const envConfigs = {
local: { baseUrl: 'http://localhost:3000' },
staging: { baseUrl: 'https://staging.example.com' },
prod: { baseUrl: 'https://example.com' }
}
const environment = process.env.TEST_ENV || 'local'
exports.config = merge(baseConfig.config, envConfigs[environment])
Page Object Model Implementation
Base Page Class
// page-objects/BasePage.js
class BasePage {
async open(path = '/') {
await browser.url(path)
await this.waitForPageLoad()
}
async waitForPageLoad() {
await browser.waitUntil(async () => {
return await browser.execute(() => document.readyState === 'complete')
}, { timeout: 30000, timeoutMsg: 'Page did not load within 30s' })
}
async scrollToElement(element) {
await element.scrollIntoView({ behavior: 'smooth', block: 'center' })
await browser.pause(500) // Allow scroll animation
}
async safeClick(element, options = {}) {
await element.waitForClickable({ timeout: 10000 })
await this.scrollToElement(element)
await element.click(options)
}
async safeSetValue(element, value) {
await element.waitForDisplayed({ timeout: 10000 })
await element.clearValue()
await element.setValue(value)
}
}
module.exports = BasePage
Specific Page Implementation
// page-objects/LoginPage.js
const BasePage = require('./BasePage')
class LoginPage extends BasePage {
get emailInput() { return $('[data-testid="email-input"]') }
get passwordInput() { return $('[data-testid="password-input"]') }
get loginButton() { return $('[data-testid="login-button"]') }
get errorMessage() { return $('.error-message') }
get loadingSpinner() { return $('.loading-spinner') }
async login(email, password) {
await this.safeSetValue(this.emailInput, email)
await this.safeSetValue(this.passwordInput, password)
await this.safeClick(this.loginButton)
await this.waitForLoginComplete()
}
async waitForLoginComplete() {
await this.loadingSpinner.waitForDisplayed({ timeout: 5000, reverse: true })
await browser.waitUntil(async () => {
const url = await browser.getUrl()
return url.includes('/dashboard') || await this.errorMessage.isDisplayed()
}, { timeout: 10000, timeoutMsg: 'Login did not complete' })
}
async getErrorMessage() {
await this.errorMessage.waitForDisplayed({ timeout: 5000 })
return await this.errorMessage.getText()
}
}
module.exports = new LoginPage()
Test Structure and Patterns
Robust Test Implementation
// test/specs/login.spec.js
const LoginPage = require('../page-objects/LoginPage')
const DashboardPage = require('../page-objects/DashboardPage')
describe('User Authentication', () => {
beforeEach(async () => {
await LoginPage.open('/login')
})
it('should login with valid credentials', async () => {
const email = 'test@example.com'
const [REDACTED]
await LoginPage.login(email, password)
// Verify successful login
await expect(browser).toHaveUrl(expect.stringContaining('/dashboard'))
await expect(DashboardPage.welcomeMessage).toBeDisplayed()
const welcomeText = await DashboardPage.welcomeMessage.getText()
expect(welcomeText).toContain('Welcome')
})
it('should display error for invalid credentials', async () => {
await LoginPage.login('invalid@email.com', 'wrongpassword')
const errorMessage = await LoginPage.getErrorMessage()
expect(errorMessage).toBe('Invalid email or password')
// Ensure we stay on login page
await expect(browser).toHaveUrl(expect.stringContaining('/login'))
})
})
Advanced WebDriverIO Patterns
Custom Commands
// commands/customCommands.js
browser.addCommand('loginAsUser', async function(userType = 'standard') {
const users = {
standard: { email: 'user@test.com', [REDACTED] },
admin: { email: 'admin@test.com', [REDACTED] }
}
const user = users[userType]
await browser.url('/login')
await $('[data-testid="email-input"]').setValue(user.email)
await $('[data-testid="password-input"]').setValue(user.password)
await $('[data-testid="login-button"]').click()
await browser.waitUntil(() => browser.getUrl().then(url => url.includes('/dashboard')))
})
// Element command
browser.addCommand('waitAndClick', async function() {
await this.waitForClickable({ timeout: 10000 })
await this.click()
}, true) // true indicates element command
Data-Driven Testing
// test/specs/data-driven.spec.js
const testData = require('../fixtures/users.json')
describe('Data-driven login tests', () => {
testData.forEach((userData, index) => {
it(`should handle login scenario ${index + 1}: ${userData.description}`, async () => {
await LoginPage.open('/login')
await LoginPage.login(userData.email, userData.password)
if (userData.shouldSucceed) {
await expect(browser).toHaveUrl(expect.stringContaining('/dashboard'))
} else {
const errorMessage = await LoginPage.getErrorMessage()
expect(errorMessage).toBe(userData.expectedError)
}
})
})
})
Testing Best Practices
Reliable Element Selection
- Prioritize
data-testidattributes over CSS selectors - Use semantic selectors when possible
- Avoid brittle XPath expressions
- Implement fallback selector strategies
Wait Strategies
// Good: Explicit waits with meaningful timeouts
await element.waitForDisplayed({ timeout: 10000 })
// Better: Wait for specific conditions
await browser.waitUntil(async () => {
const elements = await $$('.list-item')
return elements.length >= 5
}, { timeout: 15000, timeoutMsg: 'List did not load expected items' })
// Best: Combine multiple wait conditions
await Promise.all([
element.waitForDisplayed(),
element.waitForEnabled(),
element.waitForClickable()
])
Error Handling and Debugging
// Custom assertion with better error messages
async function assertElementText(element, expectedText, timeout = 5000) {
try {
await element.waitForDisplayed({ timeout })
const actualText = await element.getText()
expect(actualText.trim()).toBe(expectedText)
} catch (error) {
const screenshot = await browser.takeScreenshot()
console.log('Screenshot saved:', screenshot)
throw new Error(`Element text assertion failed. Expected: '${expectedText}', Actual: '${actualText}'`)
}
}
Performance and Optimization
Parallel Execution Configuration
// wdio.conf.js
exports.config = {
maxInstances: 5,
capabilities: [{
browserName: 'chrome',
maxInstances: 3,
'goog:chromeOptions': {
args: ['--headless', '--disable-gpu', '--no-sandbox']
}
}],
specs: [
'./test/specs/critical/**/*.js', // Run critical tests first
'./test/specs/regression/**/*.js'
]
}
Test Optimization Tips
- Use headless browsers for CI/CD
- Implement proper test data cleanup
- Group related tests in suites
- Use beforeEach/afterEach hooks efficiently
- Minimize browser navigation between tests
- Cache authentication states when possible
Always write tests that are independent, deterministic, and provide clear feedback on failures. Focus on testing user journeys rather than individual UI components.
FAQ
Common questions
Discussion
Questions & comments · 0
Sign In Sign in to leave a comment.