Как решать капчу в тестах WebdriverIO с помощью Node.js

Шаг решения капчи в WebdriverIO представляет собой вызов Node.js, а не браузера. Вы решаете капчу в процессе теста, передаёте токен на страницу через browser.execute и отправляете форму. Здесь ошибаются в двух местах, и ни одно из них не связано с решателем. Поместите решение внутрь контекста браузера, и оно вообще не сможет достучаться до вашей машины. Оставьте Mocha на тайм-ауте по умолчанию в тридцать секунд, и тест упадёт на середине решения с сообщением, которое ничего не объясняет. Вот пользовательская команда, одно изменение конфигурации и рабочий тест.
Что понадобится
- WebdriverIO 8 или новее с фреймворком Mocha. Всё, что описано ниже, асинхронно, поскольку старый синхронный режим больше не поддерживается.
- Node.js 18 или новее, а также пакет CapSkip, установленный в проекте с тестами.
- Страница, которая действительно показывает капчу. Тестовый sitekey, который всегда проходит проверку, ничего из этого не задействует.
- CapSkip, запущенный в режиме Local mode, если тесты выполняются на вашей собственной машине, или в режиме Server mode, если они выполняются на CI-раннере или в гриде. Оба варианта описаны в разделе Настройки подключения.
# Install into the project that runs wdio, not into the browser image. npm install capskip
Решайте капчу в Node, а не в браузере
Это первая ошибка, которую стоит устранить, потому что WebdriverIO позволяет легко её допустить. Команда browser.execute сериализует вашу функцию, отправляет её в браузер и выполняет внутри страницы. Загрузчика модулей там нет, поэтому SDK там попросту недоступен. Даже если бы он был доступен, страница представляет собой именно то, что вы тестируете, и передавать ей адрес вашего решателя капчи не стоит.
Более веская причина связана с маршрутизацией. Как только браузер оказывается не на вашей собственной машине (а на гриде или у облачного провайдера устройств это никогда не так), адрес обратной петли внутри этого браузера принадлежит хосту браузера. Вашего решателя там нет. Именно процесс Node, в котором выполняется wdio, знает, как достучаться до CapSkip, поэтому решение капчи происходит там, а на страницу попадает только готовый токен.
Шаг 1: зарегистрируйте пользовательскую команду
Вместо того чтобы импортировать SDK в каждый тестовый файл, добавьте одну команду в хук before вашей конфигурации. Она становится доступна на объекте browser в каждом тесте, а адрес решателя хранится ровно в одном месте.
// npm install capskip
const { CapSkip } = require('capskip');
exports.config = {
framework: 'mocha',
before: function () {
const solver = new CapSkip({
host: process.env.CAPSKIP_HOST || '127.0.0.1',
port: Number(process.env.CAPSKIP_PORT || 8080),
});
browser.addCommand('solveRecaptcha', async function (sitekey) {
const result = await solver.recaptcha(sitekey, await this.getUrl());
return result.code; // the token, ready to inject
});
},
};Внутри addCommand значение this представляет собой область видимости browser, поэтому там и работает getUrl. Эта деталь имеет практическое значение: API нужен URL страницы, на которой размещён виджет, и если запрашивать его у браузера, тесту не придётся повторно указывать URL, на который он уже перешёл.
Считывайте адрес хоста из переменных окружения, а не прописывайте его в коде. Тогда один и тот же набор тестов будет работать и с решателем на вашем ноутбуке, и с общим решателем из CI без изменений кода.
Шаг 2: подставьте токен и отправьте форму
Google помещает ответ в скрытый textarea с id g-recaptcha-response. Он скрыт, поэтому никакой setValue до него не доберётся. Установите значение напрямую, а затем отправьте форму так, как это сделал бы человек.
const token = await browser.solveRecaptcha('YOUR_SITEKEY');
// Put the token where the page already expects to find it.
await browser.execute((value) => {
document.getElementById('g-recaptcha-response').value = value;
}, token);
await $('button[type="submit"]').click();Этого достаточно для обычного случая: форма считывает textarea при нажатии кнопки отправки. Некоторые страницы вместо этого объявляют data-callback на виджете и вообще никогда не заглядывают в textarea. Если тестируете именно такой случай, вызовите callback из того же блока browser.execute после установки значения, потому что заполнение поля, которое никто не читает, ничего не меняет.
Шаг 3: увеличьте тайм-аут Mocha
По умолчанию WebdriverIO задаёт Mocha тайм-аут в 30000 миллисекунд: этого более чем достаточно для кликов по элементам, но слишком мало для решения капчи. Тест, который в остальном прошёл бы успешно, здесь падает, и в сообщении об ошибке фигурирует Mocha, а не что-либо связанное с решением капчи, из-за чего люди целый день ищут проблему не там.
exports.config = {
framework: 'mocha',
mochaOpts: {
// The 30000 default expires mid solve. Give it room.
timeout: 120000,
},
};Задайте оба лимита в правильном порядке. SDK сдаётся по истечении recaptchaTimeout, который по умолчанию равен 300 секундам, а Mocha сдаётся по истечении собственного тайм-аута. Если тайм-аут SDK меньше, чем у Mocha, вы получите TimeoutException, указывающий на решение капчи. Если он больше, Mocha убьёт тест первой, и вы узнаете только то, что что-то заняло слишком много времени. Тайм-аут решателя в 90 секунд при тайм-ауте Mocha в 120000 миллисекунд является разумным сочетанием для набора тестов, который должен оставаться быстрым.
Запуск в CI или в гриде
Определите, какой машине нужно достучаться до решателя, потому что это не та машина, что кажется очевидной. Браузер никогда не общается с CapSkip напрямую. Это делает процесс, в котором выполняется wdio. Поэтому на раннере GitHub Actions, в контейнере или на ноутбуке, управляющем облачным браузером, вызывающей стороной оказывается именно этот раннер, и указание на его собственный адрес обратной петли ничего не найдёт.
| Режим | Прослушивает | Когда использовать |
|---|---|---|
| Локально | 127.0.0.1, только это устройство | Вы запускаете wdio на той же машине, что и решатель |
| Сервер | Ваш сетевой адрес или публичный IP | CI-раннеры, контейнеры, общий набор тестов, грид |
Server mode охватывает всё, что перечислено во второй строке. Смените адрес прослушивания в приложении, задайте CAPSKIP_HOST на раннере, и все задания будут использовать один решатель. Статический публичный IP стоит иметь, если вызывающие стороны находятся вне вашей сети. Ничего из этого не меняет саму суть продукта: это по-прежнему ваше оборудование и по-прежнему безлимитное решение без оплаты за каждый запрос, поэтому уход от адреса обратной петли меняет только то, где выполняется решение, и ничего больше. CapSkip представляет собой Windows-приложение, поэтому на практике речь идёт об одном компьютере с Windows, к которому обращаются ваши раннеры.
Полный файл теста
describe('protected signup form', () => {
it('submits with a solved challenge', async () => {
await browser.url('https://example.com/page-with-recaptcha');
await $('#email').setValue('[email protected]');
// Solve here, submit two lines later. The token is short lived.
const token = await browser.solveRecaptcha('YOUR_SITEKEY');
await browser.execute((value) => {
document.getElementById('g-recaptcha-response').value = value;
}, token);
await $('button[type="submit"]').click();
await expect($('.signup-success')).toBeDisplayed();
});
});Обратите внимание, как мало из этого связано непосредственно с капчей. Всю интеграцию несут на себе три строки, а остальное представляет собой тест, который вы и так собирались написать. Держите решение капчи и отправку формы в теле одного теста, чтобы токен оставался свежим, всего в несколько секунд от создания, когда его считывает форма.
Частые ошибки
| Что вы видите | Причина | Исправить |
|---|---|---|
| Превышен тайм-аут Mocha в 30000 мс | Решение капчи заняло больше времени, чем тайм-аут по умолчанию | Увеличьте mochaOpts.timeout и задайте recaptchaTimeout меньше этого значения |
| solver не определён внутри browser.execute | Функция выполнилась на странице, а не в Node | Решайте капчу до вызова execute и передавайте внутрь только токен |
| NetworkException из CI, но всё в порядке локально | Раннер не может достучаться до решателя | Переключитесь на Server mode и задайте CAPSKIP_HOST на раннере |
| Форма отклоняет токен, который был успешно решён | Страница использует callback и игнорирует textarea | Вызовите callback виджета после установки значения |
| ERROR_GOOGLEKEY | Sitekey от Turnstile был передан в метод reCAPTCHA | Используйте метод turnstile для виджетов Turnstile |
Полный список кодов и то, что вызывает каждый из них, есть в документации CapSkip API.
FAQ
Могу ли я вызвать решатель внутри browser.execute?
Нет, и это не сработает по двум разным причинам. Переданная вами функция сериализуется и выполняется на странице, где нет ни загрузчика модулей, ни SDK. Даже если бы он там был, браузер часто вообще находится на другом хосте, поэтому адрес, по которому вы бы обращались, вам не принадлежит. Решайте капчу в процессе Node и передавайте готовый токен в качестве аргумента.
Работает ли это с удалённым гридом или облачным браузером?
Да, и именно поэтому решение капчи и должно происходить в Node. Ваш тестовый процесс выполняется локально или на раннере и напрямую достучивается до решателя, а браузер находится где-то ещё и всегда получает только токен. Запустите CapSkip в Server mode, чтобы любая машина, выполняющая набор тестов, могла до него достучаться, и в самом тесте при этом ничего не меняется.
Стоит ли решать капчу один раз в хуке before и повторно использовать токен?
Нет. Токен reCAPTCHA одноразовый и остаётся действительным примерно две минуты, поэтому второй тест, который попытается его использовать, будет отклонён, как и первый тест, запустившийся после медленного теста. Решайте капчу внутри каждого теста, которому нужен токен. Общий решатель обрабатывает дополнительные вызовы без платы за каждое решение, поэтому копить токены впрок незачем.
Я использую Cucumber или Jasmine вместо Mocha. Что меняется?
Меняется только название настройки тайм-аута. Пользовательская команда, подстановка токена и вопрос про Server mode остаются точно такими же. Увеличивайте cucumberOpts.timeout или jasmineOpts.defaultTimeoutInterval вместо mochaOpts.timeout и держите тайм-аут решателя меньше того значения, которое вы зададите.
Коротко
Зарегистрируйте одну пользовательскую команду в хуке before, увеличьте тайм-аут фреймворка так, чтобы он был больше времени решения капчи, и подставляйте токен через browser.execute, а не пытайтесь ввести его вручную. О клиентской части читайте на странице сервиса распознавания капчи для Node.js, о стороне WebDriver читайте на странице сервиса распознавания капчи для Selenium, а о том, что такое токен на самом деле, читайте на странице сервиса распознавания reCAPTCHA v2. Именно в наборах тестов оплата за каждое решение ощущается больнее всего, потому что набор, запускаемый при каждом pull request, решает одну и ту же форму сотни раз в неделю. Вот что меняется, когда решатель становится вашим собственным: безлимитный сервис распознавания капчи на оборудовании, которое у вас уже есть, стоит одинаково независимо от того, запускается CI дважды в день или дважды в час.
