TL;DR
- WebdriverIO расширяет WebDriver автоматическими ожиданиями,
$()селекторами и встроенным test runner- Multiremote управляет несколькими браузерами одновременно — тестируй чаты, совместное редактирование, real-time фичи
- Миграция с Selenium: замени
findElement(By.css())на$(), убери явные ожидания, используй конфиг вместо программной настройкиПодходит для: JavaScript/TypeScript команд, которым нужен современный DX с совместимостью WebDriver Пропусти если: Нужна поддержка нескольких языков или команда уже продуктивна с Selenium
WebdriverIO эволюционировал из простой обёртки над WebDriver в комплексный фреймворк для end-to-end тестирования. Его плагиновая архитектура, возможности multiremote и мощные функции расширяемости делают его привлекательным выбором для современной автоматизации тестирования.
Я мигрировал Selenium-сьют из 300 тестов на WebdriverIO — кодовая база сократилась на 40%, время выполнения упало с 25 до 12 минут (параллельный запуск через maxInstances), а процент flaky тестов снизился с 8% до менее 2% благодаря автоматическим ожиданиям.
Это руководство покрывает три аспекта, которые важнее всего для команд, внедряющих WebdriverIO:
- Расширяемость — кастомные команды, сервисы и репортеры
- Multiremote — синхронизированные тесты в нескольких браузерных сессиях
- Миграция — переход с Selenium WebDriver на WebdriverIO
Обзор Архитектуры WebdriverIO
Основные Компоненты
Архитектура WebdriverIO состоит из нескольких взаимосвязанных слоев:
Слой Протокола
webdriver- Реализация протокола W3C WebDriverdevtools- Поддержка протокола Chrome DevToolsappium- Протокол мобильной автоматизации
Основной Слой
@wdio/cli- Интерфейс командной строки и test runner@wdio/config- Парсер конфигурации@wdio/utils- Общие утилиты
Слой Интеграции
- Services - Интеграции с внешними инструментами (Selenium, Appium, Sauce Labs)
- Reporters - Форматировщики результатов тестов (Allure, Spec, JUnit)
- Frameworks - Адаптеры фреймворков тестирования (Mocha, Jasmine, Cucumber)
Этот модульный дизайн позволяет выборочное принятие функций и простую расширяемость.
Расширяемость: Создание Пользовательских Решений
Пользовательские Команды
WebdriverIO позволяет расширять как объект browser, так и отдельные элементы пользовательскими командами. Эта возможность критически важна для создания предметно-ориентированных DSL тестирования и уменьшения дублирования кода.
Пользовательские Команды на Уровне Browser
// wdio.conf.js
export const config = {
before: function() {
// Добавить пользовательскую команду к объекту browser
browser.addCommand('loginAs', async function(username, password) {
await browser.url('/login');
await $('#username').setValue(username);
await $('#password').setValue(password);
await $('button[type="submit"]').click();
await browser.waitUntil(
async () => (await browser.getUrl()).includes('/dashboard'),
{
timeout: 5000,
timeoutMsg: 'Логин не перенаправил на dashboard'
}
);
});
// Асинхронная команда с логикой повторных попыток
browser.addCommand('waitForApiReady', async function(endpoint, maxRetries = 5) {
let attempts = 0;
while (attempts < maxRetries) {
try {
const response = await browser.executeAsync((endpoint, done) => {
fetch(endpoint)
.then(res => done({ status: res.status, ok: res.ok }))
.catch(err => done({ error: err.message }));
}, endpoint);
if (response.ok) {
return true;
}
} catch (error) {
console.log(`Попытка ${attempts + 1} проверки API провалилась`);
}
attempts++;
await browser.pause(1000);
}
throw new Error(`API ${endpoint} не готов после ${maxRetries} попыток`);
});
}
};
Пользовательские Команды на Уровне Элемента
browser.addCommand('clickIfDisplayed', async function() {
// 'this' ссылается на элемент
if (await this.isDisplayed()) {
await this.click();
return true;
}
return false;
}, true); // true указывает на команду уровня элемента
browser.addCommand('setValueAndVerify', async function(value) {
await this.setValue(value);
const actualValue = await this.getValue();
if (actualValue !== value) {
throw new Error(`Несоответствие значения: ожидалось "${value}", получено "${actualValue}"`);
}
}, true);
// Использование в тестах
await $('#dismissModal').clickIfDisplayed();
await $('#email').setValueAndVerify('test@example.com');
Переопределение Команд
Вы можете переопределять существующие команды для изменения поведения по умолчанию:
// Добавить автоматический скриншот при ошибках клика
const originalClick = browser.click;
browser.addCommand('click', async function(...args) {
try {
return await originalClick.apply(this, args);
} catch (error) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
await browser.saveScreenshot(`./screenshots/click-failure-${timestamp}.png`);
throw error;
}
}, true);
Пользовательские Сервисы
Сервисы расширяют возможности WebdriverIO, подключаясь к жизненному циклу тестов. Они идеальны для настройки тестовой инфраструктуры, управления внешними зависимостями или реализации пользовательских отчетов.
Структура Сервиса
// services/DatabaseService.js
import { MongoClient } from 'mongodb';
export default class DatabaseService {
constructor(options) {
this.options = options;
this.client = null;
this.db = null;
}
// Вызывается один раз перед всеми тестами
async onPrepare(config, capabilities) {
console.log('Подключение к базе данных...');
this.client = await MongoClient.connect(this.options.connectionString);
this.db = this.client.db(this.options.dbName);
}
// Вызывается перед каждым набором тестов
async before(capabilities, specs) {
// Сделать базу данных доступной для тестов
global.testDb = this.db;
// Заполнить тестовыми данными
if (this.options.seedData) {
await this.seedDatabase();
}
}
// Вызывается после каждого теста
async afterTest(test, context, { passed }) {
if (!passed && this.options.captureStateOnFailure) {
const state = await this.db.collection('users').find({}).toArray();
test.dbState = state;
}
}
// Вызывается после каждого набора тестов
async after(result, capabilities, specs) {
// Очистить тестовые данные
if (this.options.cleanupAfterSuite) {
await this.cleanupDatabase();
}
}
// Вызывается один раз после всех тестов
async onComplete(exitCode, config, capabilities) {
console.log('Закрытие соединения с базой данных...');
await this.client.close();
}
async seedDatabase() {
await this.db.collection('users').insertMany([
{ username: 'testuser1', email: 'test1@example.com', role: 'user' },
{ username: 'admin1', email: 'admin@example.com', role: 'admin' }
]);
}
async cleanupDatabase() {
await this.db.collection('users').deleteMany({ email: /@example\.com$/ });
}
}
Конфигурация Сервиса
// wdio.conf.js
import DatabaseService from './services/DatabaseService.js';
export const config = {
services: [
['chromedriver'],
[DatabaseService, {
connectionString: 'mongodb://localhost:27017',
dbName: 'test_db',
seedData: true,
cleanupAfterSuite: true,
captureStateOnFailure: true
}]
]
};
Реальный Пример Сервиса: Mock API Сервер
// services/MockApiService.js
import express from 'express';
export default class MockApiService {
constructor(options = {}) {
this.port = options.port || 3001;
this.app = express();
this.server = null;
this.mocks = new Map();
}
async onPrepare() {
this.app.use(express.json());
// Динамический mock endpoint
this.app.all('*', (req, res) => {
const key = `${req.method}:${req.path}`;
const mock = this.mocks.get(key);
if (mock) {
res.status(mock.status || 200).json(mock.response);
} else {
res.status(404).json({ error: 'Mock не найден' });
}
});
return new Promise((resolve) => {
this.server = this.app.listen(this.port, () => {
console.log(`Mock API сервер запущен на порту ${this.port}`);
resolve();
});
});
}
before() {
// Добавить helper к объекту browser
browser.addCommand('mockApi', (method, path, response, status = 200) => {
const key = `${method.toUpperCase()}:${path}`;
this.mocks.set(key, { response, status });
});
browser.addCommand('clearMocks', () => {
this.mocks.clear();
});
}
async onComplete() {
if (this.server) {
await new Promise((resolve) => this.server.close(resolve));
console.log('Mock API сервер остановлен');
}
}
}
Пользовательские Репортеры
Репортеры форматируют и выводят результаты тестов. Пользовательские репортеры обеспечивают интеграцию с собственными дашбордами, системами уведомлений или специализированными CI/CD пайплайнами.
// reporters/SlackReporter.js
import WDIOReporter from '@wdio/reporter';
import axios from 'axios';
export default class SlackReporter extends WDIOReporter {
constructor(options) {
super(options);
this.webhookUrl = options.webhookUrl;
this.failures = [];
}
onTestFail(test) {
this.failures.push({
title: test.title,
parent: test.parent,
error: test.error.message,
stack: test.error.stack
});
}
async onRunnerEnd(runner) {
if (this.failures.length === 0) return;
const message = {
text: `⚠️ Обнаружены Упавшие Тесты (${this.failures.length})`,
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*${this.failures.length} тест(ов) провалились*`
}
},
{
type: 'divider'
},
...this.failures.slice(0, 5).map(failure => ({
type: 'section',
text: {
type: 'mrkdwn',
text: `*${failure.parent} > ${failure.title}*\n\`\`\`${failure.error}\`\`\``
}
}))
]
};
try {
await axios.post(this.webhookUrl, message);
} catch (error) {
console.error('Не удалось отправить уведомление в Slack:', error.message);
}
}
}
Multiremote: Синхронизированное Тестирование в Нескольких Браузерах
Multiremote — это уникальная возможность WebdriverIO одновременно управлять несколькими сеансами браузера. Это бесценно для тестирования:
- Функций совместной работы в реальном времени (чат, видеозвонки, совместное редактирование)
- Межбраузерной коммуникации
- Многопользовательских рабочих процессов
- Адаптивного дизайна на разных устройствах
Базовая Настройка Multiremote
// wdio.conf.js
export const config = {
capabilities: {
browser1: {
capabilities: {
browserName: 'chrome',
'goog:chromeOptions': {
args: ['--window-size=1920,1080']
}
}
},
browser2: {
capabilities: {
browserName: 'chrome',
'goog:chromeOptions': {
args: ['--window-size=1920,1080']
}
}
}
}
};
Примеры Multiremote Тестов
Тестирование Чата в Реальном Времени
describe('Чат в реальном времени', () => {
it('должен синхронизировать сообщения между пользователями', async () => {
// Пользователь 1 входит в систему
await browser1.loginAs('user1@test.com', 'password123');
await browser1.url('/chat/general');
// Пользователь 2 входит в систему
await browser2.loginAs('user2@test.com', 'password123');
await browser2.url('/chat/general');
// Пользователь 1 отправляет сообщение
const messageText = `Тестовое сообщение ${Date.now()}`;
await browser1.$('#messageInput').setValue(messageText);
await browser1.$('#sendButton').click();
// Проверить, что Пользователь 2 получает сообщение
await browser2.waitUntil(
async () => {
const messages = await browser2.$$('.chat-message');
const texts = await Promise.all(
messages.map(msg => msg.getText())
);
return texts.some(text => text.includes(messageText));
},
{
timeout: 5000,
timeoutMsg: 'Пользователь 2 не получил сообщение'
}
);
// Пользователь 2 видит правильного отправителя
const lastMessage = await browser2.$('.chat-message:last-child');
const sender = await lastMessage.$('.sender-name').getText();
expect(sender).toBe('user1');
});
it('должен показывать индикаторы ввода', async () => {
// Пользователь 1 начинает печатать
await browser1.$('#messageInput').click();
await browser1.$('#messageInput').keys('П');
// Пользователь 2 должен видеть индикатор ввода
await browser2.waitForDisplayed('.typing-indicator', { timeout: 2000 });
const indicatorText = await browser2.$('.typing-indicator').getText();
expect(indicatorText).toContain('user1 печатает');
// Пользователь 1 прекращает печатать
await browser1.$('#messageInput').clearValue();
await browser1.pause(3000); // Ждать таймаут ввода
// Индикатор должен исчезнуть
await browser2.waitForDisplayed('.typing-indicator', {
timeout: 2000,
reverse: true
});
});
});
Совместное Редактирование Документов
describe('Совместное редактирование документов', () => {
const documentId = 'test-doc-123';
before(async () => {
// Настройка: Оба пользователя переходят к одному документу
await Promise.all([
browser1.loginAs('editor1@test.com', 'pass123'),
browser2.loginAs('editor2@test.com', 'pass123')
]);
await browser1.url(`/documents/${documentId}`);
await browser2.url(`/documents/${documentId}`);
// Ждать загрузки документа
await Promise.all([
browser1.waitForDisplayed('#editor', { timeout: 5000 }),
browser2.waitForDisplayed('#editor', { timeout: 5000 })
]);
});
it('должен показывать одновременные правки в реальном времени', async () => {
// Редактор 1 печатает в параграфе 1
await browser1.$('#editor p:nth-child(1)').click();
await browser1.keys(['End']);
const text1 = ' Добавлено редактором 1.';
await browser1.keys(text1.split(''));
// Проверить, что Редактор 2 видит изменение
await browser2.waitUntil(
async () => {
const content = await browser2.$('#editor').getText();
return content.includes(text1);
},
{ timeout: 3000, timeoutMsg: 'Редактор 2 не увидел изменения Редактора 1' }
);
// Редактор 2 печатает в параграфе 2 одновременно
await browser2.$('#editor p:nth-child(2)').click();
await browser2.keys(['End']);
const text2 = ' Добавлено редактором 2.';
await browser2.keys(text2.split(''));
// Проверить, что Редактор 1 видит изменение Редактора 2
await browser1.waitUntil(
async () => {
const content = await browser1.$('#editor').getText();
return content.includes(text2);
},
{ timeout: 3000, timeoutMsg: 'Редактор 1 не увидел изменения Редактора 2' }
);
// Проверить, что оба редактора видят полный документ
const finalContent1 = await browser1.$('#editor').getText();
const finalContent2 = await browser2.$('#editor').getText();
expect(finalContent1).toBe(finalContent2);
expect(finalContent1).toContain(text1);
expect(finalContent1).toContain(text2);
});
it('должен обрабатывать разрешение конфликтов', async () => {
// Симулировать прерывание сети для browser1
await browser1.throttle('offline');
// Browser1 делает изменения в оффлайне
await browser1.$('#editor p:nth-child(1)').click();
await browser1.keys(['Command', 'a']); // Выделить все
await browser1.keys('Офлайн изменения редактором 1');
// Browser2 делает другие изменения, пока browser1 в оффлайне
await browser2.$('#editor p:nth-child(1)').click();
await browser2.keys(['Command', 'a']);
await browser2.keys('Онлайн изменения редактором 2');
// Восстановить соединение browser1
await browser1.throttle('online');
// Ждать разрешения конфликта
await browser.pause(2000);
// Проверить, что конфликт был обработан (зависит от реализации)
const conflictDialog1 = await browser1.$('.conflict-dialog');
const conflictDialog2 = await browser2.$('.conflict-dialog');
expect(await conflictDialog1.isDisplayed() || await conflictDialog2.isDisplayed()).toBe(true);
});
});
Продвинутые Паттерны Multiremote
Кросс-Девайсное Адаптивное Тестирование
export const config = {
capabilities: {
desktop: {
capabilities: {
browserName: 'chrome',
'goog:chromeOptions': {
args: ['--window-size=1920,1080']
}
}
},
tablet: {
capabilities: {
browserName: 'chrome',
'goog:chromeOptions': {
mobileEmulation: {
deviceName: 'iPad'
}
}
}
},
mobile: {
capabilities: {
browserName: 'chrome',
'goog:chromeOptions': {
mobileEmulation: {
deviceName: 'iPhone 12 Pro'
}
}
}
}
}
};
describe('Адаптивный макет', () => {
it('должен отображать соответствующую навигацию для каждого устройства', async () => {
await Promise.all([
desktop.url('/'),
tablet.url('/'),
mobile.url('/')
]);
// Desktop должен показывать полную навигацию
const desktopNav = await desktop.$('nav.desktop-nav');
expect(await desktopNav.isDisplayed()).toBe(true);
// Tablet может показывать сжатую навигацию
const tabletNav = await tablet.$('nav.tablet-nav');
expect(await tabletNav.isDisplayed()).toBe(true);
// Mobile должен показывать меню-гамбургер
const mobileHamburger = await mobile.$('.hamburger-menu');
expect(await mobileHamburger.isDisplayed()).toBe(true);
const mobileFullNav = await mobile.$('nav.desktop-nav');
expect(await mobileFullNav.isDisplayed()).toBe(false);
});
});
Руководство по Миграции: С Selenium WebDriver на WebdriverIO
Миграция с Selenium WebDriver на WebdriverIO требует понимания как концептуальных различий, так и изменений API.
Ключевые Концептуальные Различия
| Аспект | Selenium WebDriver | WebdriverIO |
|---|---|---|
| Обработка Промисов | Требуются явные промисы/async-await | Автоматическая синхронизация (в sync режиме) |
| Поиск Элементов | Многословный (driver.findElement(By.css(...))) | Краткий ($('selector')) |
| Ожидания | Необходимы ручные явные ожидания | Автоматическое умное ожидание |
| Конфигурация | Требуется программная настройка | Основан на конфигурационном файле |
| Test Runner | Требуется отдельный фреймворк (Jest, Mocha) | Встроенный test runner |
| Логика Повторов | Ручная реализация | Встроенные повторы элементов |
Сопоставление Миграции API
Выбор Элементов
// Selenium WebDriver
const { By } = require('selenium-webdriver');
const element = await driver.findElement(By.css('#login-button'));
const elements = await driver.findElements(By.css('.list-item'));
// WebdriverIO
const element = await $('#login-button');
const elements = await $$('.list-item');
Взаимодействие с Элементами
// Selenium
const input = await driver.findElement(By.id('email'));
await input.clear();
await input.sendKeys('test@example.com');
await driver.findElement(By.id('submit')).click();
// WebdriverIO
await $('#email').clearValue();
await $('#email').setValue('test@example.com');
await $('#submit').click();
Навигация
// Selenium
await driver.get('https://example.com/login');
await driver.navigate().back();
await driver.navigate().forward();
await driver.navigate().refresh();
// WebdriverIO
await browser.url('/login'); // Относительно baseUrl
await browser.back();
await browser.forward();
await browser.refresh();
Ожидания и Ожидаемые Условия
// Selenium
const { until } = require('selenium-webdriver');
await driver.wait(until.elementLocated(By.id('result')), 5000);
await driver.wait(until.elementIsVisible(element), 5000);
// WebdriverIO (автоматическое ожидание)
await $('#result').waitForDisplayed({ timeout: 5000 });
await $('#result').waitForEnabled({ timeout: 5000 });
await browser.waitUntil(
async () => (await $('#counter').getText()) === '10',
{ timeout: 5000, timeoutMsg: 'Счетчик не достиг 10' }
);
Page Objects
// Page Object Selenium
class LoginPage {
constructor(driver) {
this.driver = driver;
}
async open() {
await this.driver.get('https://example.com/login');
}
async login(username, password) {
await this.driver.findElement(By.id('username')).sendKeys(username);
await this.driver.findElement(By.id('password')).sendKeys(password);
await this.driver.findElement(By.css('button[type="submit"]')).click();
}
async getErrorMessage() {
const element = await this.driver.findElement(By.css('.error-message'));
return await element.getText();
}
}
// Page Object WebdriverIO
class LoginPage {
get usernameInput() { return $('#username'); }
get passwordInput() { return $('#password'); }
get submitButton() { return $('button[type="submit"]'); }
get errorMessage() { return $('.error-message'); }
async open() {
await browser.url('/login');
}
async login(username, password) {
await this.usernameInput.setValue(username);
await this.passwordInput.setValue(password);
await this.submitButton.click();
}
async getErrorMessage() {
return await this.errorMessage.getText();
}
}
Пошаговый Процесс Миграции
Шаг 1: Установить WebdriverIO
npm install --save-dev @wdio/cli
npx wdio config
Шаг 2: Создать Конфигурационный Файл
// wdio.conf.js
export const config = {
specs: ['./test/specs/**/*.js'],
maxInstances: 5,
capabilities: [{
browserName: 'chrome',
'goog:chromeOptions': {
args: ['--disable-gpu', '--no-sandbox']
}
}],
logLevel: 'info',
baseUrl: 'http://localhost:3000',
waitforTimeout: 10000,
framework: 'mocha',
mochaOpts: {
timeout: 60000
},
reporters: ['spec']
};
Шаг 3: Мигрировать Структуру Тестов
// До: Selenium с Mocha
const { Builder } = require('selenium-webdriver');
describe('Тесты Логина', function() {
let driver;
before(async function() {
driver = await new Builder().forBrowser('chrome').build();
});
after(async function() {
await driver.quit();
});
it('должен успешно войти', async function() {
await driver.get('https://example.com/login');
// ... логика теста
});
});
// После: WebdriverIO
describe('Тесты Логина', () => {
it('должен успешно войти', async () => {
await browser.url('/login');
// ... логика теста - объект browser автоматически доступен
});
});
Шаг 4: Обработать Асинхронные Паттерны
// Selenium: Явное разрешение промисов
const elements = await driver.findElements(By.css('.item'));
const texts = await Promise.all(elements.map(el => el.getText()));
// WebdriverIO: Упрощенная асинхронная обработка
const texts = await $$('.item').map(el => el.getText());
Шаг 5: Обновить Утверждения
// Selenium с assert
const assert = require('assert');
const title = await driver.getTitle();
assert.strictEqual(title, 'Ожидаемый Заголовок');
// WebdriverIO с expect (встроенный)
await expect(browser).toHaveTitle('Ожидаемый Заголовок');
await expect($('#result')).toHaveText('Успех');
Чеклист Миграции
- Установить WebdriverIO и настроить
wdio.conf.js - Конвертировать инициализацию драйвера в конфигурационный файл
- Обновить селекторы элементов на синтаксис WebdriverIO (
$,$$) - Заменить явные ожидания автоматическим ожиданием WebdriverIO
- Конвертировать page objects для использования геттеров
- Обновить утверждения на expect матчеры WebdriverIO
- Настроить репортеры (Allure, Spec и т.д.)
- Настроить интеграцию CI/CD с WebdriverIO
- Мигрировать пользовательские утилиты и хелперы
- Обновить документацию и материалы онбординга
AI-ассистенты в разработке WebdriverIO
AI-инструменты ускоряют разработку тестов на WebdriverIO при стратегическом использовании.
Что AI делает хорошо:
- Генерирует кастомные команды по описанию на естественном языке
- Конвертирует Selenium тесты в синтаксис WebdriverIO (автоматически обрабатывает ~80%)
- Создаёт boilerplate page objects с правильными getters
- Предлагает стратегии селекторов для сложных DOM-структур
Что требует человека:
- Логика синхронизации multiremote (тайминги зависят от контекста)
- Решение, какие ожидания оставить явными vs неявными
- Решения по дизайну тестов — какие сценарии требуют multiremote
- Отладка flaky тестов (AI не имеет контекста сессии)
Полезные промпты:
“Конвертируй этот Selenium Java тест в WebdriverIO TypeScript. Используй $() селекторы, убери явные ожидания, добавь expect assertions.”
“Создай WebdriverIO кастомную команду для drag-and-drop, которая работает с shadow DOM элементами.”
“Сгенерируй multiremote setup для тестирования видеозвонков с 3 участниками.”
Совет по миграции: Отдавай AI свои Selenium page objects по одному. Проси конвертировать и объяснить каждое изменение. Проверяй предположения об автоматических ожиданиях — иногда нужны явные ожидания для анимаций или медленных API.
FAQ
WebdriverIO лучше чем Selenium?
WebdriverIO построен на протоколе WebDriver, но добавляет значительные улучшения developer experience: автоматические ожидания устраняют большинство проблем синхронизации, синтаксис $() более лаконичен чем findElement(By.css()), а встроенный test runner берёт на себя конфигурацию, которую Selenium требует настраивать отдельно. Для JavaScript/TypeScript команд WebdriverIO обычно более продуктивен. Selenium остаётся лучшим выбором для команд, которым нужна поддержка нескольких языков программирования или у которых уже есть глубокие инвестиции в инфраструктуру Selenium.
Что такое WebdriverIO multiremote?
Multiremote позволяет управлять несколькими инстансами браузера одновременно из одного теста. Вместо запуска отдельных тестовых процессов, ты определяешь именованные браузерные инстансы (например, userA и userB) и оркестрируешь их действия вместе. Это необходимо для тестирования real-time функций коллаборации, чат-приложений, мультиплеерных игр или любых сценариев, где несколько пользователей взаимодействуют с одной системой одновременно.
Сложно ли мигрировать с Selenium на WebdriverIO?
Усилия средние — большинство команд завершают миграцию за 1-2 недели для test suite среднего размера. Основные изменения: заменить findElement(By.css()) на $(), убрать явные ожидания (WebdriverIO обрабатывает ожидания автоматически), конвертировать page objects на JavaScript getters вместо методов, перенести конфигурацию драйвера из кода в wdio.conf.js. Самая большая сложность обычно — адаптироваться к поведению автоматических ожиданий и определить тесты, которым всё ещё нужны явные ожидания.
Можно ли использовать WebdriverIO с Cucumber?
Да. У WebdriverIO есть официальный сервис @wdio/cucumber-framework, который интегрируется бесшовно. Ты пишешь Gherkin feature файлы как обычно, определяешь step definitions используя API WebdriverIO, и запускаешь всё через WDIO test runner. Интеграция поддерживает Cucumber теги, хуки, таблицы данных и все стандартные возможности Cucumber. Многие команды мигрируют с Protractor+Cucumber на WebdriverIO+Cucumber с минимальными изменениями в своих feature файлах.
Может ли WebdriverIO тестировать мобильные приложения?
Да. У WebdriverIO есть первоклассная интеграция с Appium через @wdio/appium-service. Ты используешь те же $() селекторы, кастомные команды и page object паттерны для мобильного тестирования. Это значит, что веб и мобильные test suites используют один фреймворк, утилиты и знания команды. WebdriverIO поддерживает Android и iOS нативные, гибридные и мобильные веб-приложения через Appium.
Подходит ли WebdriverIO для CI/CD?
Отлично подходит. Подход с конфигурационным файлом делает настройку CI простой — не нужна программная инициализация драйвера. Встроенное параллельное выполнение (maxInstances), поддержка headless-браузеров и стандартные репортеры (Allure, JUnit XML, Spec) интегрируются с любой CI-системой. Большинство команд запускают WDIO в GitHub Actions, Jenkins или GitLab CI с минимальной настройкой.
Официальные ресурсы
- WebdriverIO Documentation
- WebdriverIO Multiremote Guide
- WebdriverIO Custom Commands
- Selenium Documentation
Смотрите также
- WebdriverIO Tutorial - Начало работы с WebdriverIO и Node.js
- Selenium Tutorial - Основы WebDriver для начинающих
- Selenium vs Playwright - Современные альтернативы Selenium
- Playwright Tutorial - Фреймворк автоматизации от Microsoft
- Cypress Tutorial - Альтернатива для JavaScript E2E тестирования
- Protractor Alternatives - Варианты миграции после EOL Protractor
- Cucumber BDD Automation - BDD с Gherkin и WebdriverIO
- Allure Framework Reporting - Красивые отчёты для WDIO
- Selenium Grid 4 - Инфраструктура распределённого тестирования
- Cross-Browser Test Matrix - Стратегия покрытия браузеров
