Как решать капчу в Nightwatch.js (очередь команд)

Решение капчи в Nightwatch должно выполняться внутри очереди команд, а не рядом с ней. Nightwatch не выполняет команды браузера там, где вы их написали. Он ставит их в очередь и разбирает её уже после того, как тестовая функция завершилась, поэтому вызов решения капчи, написанный между двумя командами браузера, срабатывает сразу же: страница ещё не загружена, а виджета ещё нет. Лечится это через browser.perform плюс один глобальный параметр, который придётся поднять, потому что решение капчи занимает больше десяти секунд, отведённых Nightwatch на асинхронный колбэк.
Что понадобится
- Nightwatch 3 с рабочим драйвером: либо локальный chromedriver, либо удалённый эндпоинт WebDriver.
- Запущенный CapSkip на машине с Windows и Node-клиент, установленный в том же проекте, что и ваши тесты.
- Sitekey и URL страницы. Считывайте sitekey прямо с виджета, а не прописывайте его в коде: на стейджинге и в проде он редко совпадает.
- Режим Server во всех случаях, когда тесты выполняются не на той машине, где работает решатель, а это любой раннер CI. Это одна настройка в разделе настроек подключения.
# npm install capskip npm install --save-dev nightwatch npm install capskip
Шаг 1: почему обычный вызов решения капчи срабатывает слишком рано
Каждая команда браузера в тесте Nightwatch представляет собой инструкцию, добавленную в очередь. Тестовая функция сначала выполняется сверху вниз и строит эту очередь, и только потом Nightwatch начинает её исполнять. Обычный JavaScript между командами в очередь не попадает, поэтому он работает уже на этапе построения. Вот и вся проблема, уместившаяся в одно предложение.
// WRONG. The solve starts while the queue is still being
// built, so it runs before browser.url() has navigated.
module.exports = {
"signup form": function (browser) {
browser.url("https://example.com/page-with-recaptcha");
solver.recaptcha(sitekey, pageUrl).then((r) => {
// fires first, against a page that is not open yet
});
browser.click("#submit");
},
};Симптом сбивает с толку, потому что ошибки не возникает. Капча решается, тест иногда проходит, а токен относится к загрузке страницы, которой не было. При этом у Nightwatch есть документированный способ поместить свой код в очередь: browser.perform, колбэк которого описан как функция, выполняемая в составе очереди.
// RIGHT. perform() queues the callback, so it runs in
// sequence with the commands either side of it.
browser.url("https://example.com/page-with-recaptcha");
browser.perform(async function () {
const { code } = await solver.recaptcha(sitekey, pageUrl);
return code;
});
browser.click("#submit");Второй вариант: асинхронный тест. Если объявить тестовую функцию через async, команды API начинают возвращать промис, и await у каждой из них сохраняет порядок без perform. Работают оба подхода. Ошибка в том, чтобы их смешивать: внешний промис с await среди команд браузера без await возвращает вас к первому примеру.
Шаг 2: поднимите asyncHookTimeout, иначе решение капчи оборвётся на десятой секунде
Именно на этом обычно теряют полдня. Асинхронное выполнение внутри perform ограничено глобальным параметром asyncHookTimeout, а его значение по умолчанию равно 10000 миллисекунд. Решение reCAPTCHA регулярно занимает от пятнадцати до сорока пяти секунд. Поэтому колбэк прерывается, пока решатель ещё работает, и в ошибке речь идёт о тайм-ауте, а не о капче.
Поднимите значение в конфиге Nightwatch: глобально или отдельно для каждого окружения.
// nightwatch.conf.js
module.exports = {
globals: {
// Default is 10000, which is shorter than most solves.
asyncHookTimeout: 120000,
// waitFor commands default to 5000. The widget is not
// the slow part, but give it room on a cold CI runner.
waitForConditionTimeout: 15000,
},
};На стороне клиента задайте значение ниже, чем asyncHookTimeout, чтобы было понятно, какой потолок действует на самом деле. Node-клиент опрашивает результат по собственному графику: он стартует с 250 миллисекунд и постепенно увеличивает паузу до pollingInterval, поэтому обычно возвращает ответ быстрее, чем цикл, написанный вручную.
// npm install capskip
const { CapSkip } = require("capskip");
const solver = new CapSkip({
host: process.env.CAPSKIP_HOST || "127.0.0.1",
port: 8080,
// Seconds. Keep this under the 120s asyncHookTimeout above.
recaptchaTimeout: 90,
pollingInterval: 3,
});Шаг 3: подставляйте токен через execute, а не через setValue
Поле ответа, которое читает reCAPTCHA, представляет собой скрытую textarea. WebDriver отказывается взаимодействовать с элементами, которые считает недоступными для взаимодействия, поэтому setValue на нём падает с ошибкой element not interactable. Обычное решение состоит в подстановке значения через саму страницу, и в Nightwatch для этого есть execute, который принимает тело функции, массив аргументов и необязательный колбэк.
browser.perform(async function () {
const { code } = await solver.recaptcha(sitekey, pageUrl);
// The function is serialized and run in the page, so it
// cannot close over anything. Pass values in the array.
await browser.execute(
function (token) {
document.getElementById("g-recaptcha-response").value = token;
},
[code]
);
});Если форма вызывает колбэк по завершении, а не читает поле в момент отправки, вызовите его в том же скрипте. Невидимая reCAPTCHA почти всегда работает именно так, а варианты виджета меняют только передаваемые опции: invisible или enterprise со значением 1, version со значением v3 и action, либо turnstile и geetest вместо recaptcha. Полный набор параметров описан на странице сервиса распознавания капчи для Node.js.
Шаг 4: тест целиком
Клиент создаётся один раз на уровне модуля, вне теста, поэтому он не пересобирается для каждого тест-кейса. Sitekey читается со страницы, а не прописан в коде, и именно поэтому один и тот же тест работает и на стейджинге, и в проде.
// npm install capskip
const { CapSkip } = require("capskip");
const solver = new CapSkip({
host: process.env.CAPSKIP_HOST || "127.0.0.1",
port: 8080,
recaptchaTimeout: 90,
});
const PAGE = "https://example.com/page-with-recaptcha";
describe("signup", function () {
it("submits through the reCAPTCHA", async function (browser) {
await browser.url(PAGE);
await browser.waitForElementPresent(".g-recaptcha", 15000);
// Read the sitekey off the widget that is actually there.
const sitekey = await browser.getAttribute(
".g-recaptcha",
"data-sitekey"
);
await browser.perform(async function () {
const { code } = await solver.recaptcha(sitekey.value, PAGE);
await browser.execute(
function (token) {
document.getElementById("g-recaptcha-response").value = token;
},
[code]
);
});
// Submit straight after. The token is not a long-lived value.
await browser.click("#submit");
await browser.assert.textContains(".result", "Thanks");
});
});Решайте капчу как можно позже и сразу отправляйте форму. Токен reCAPTCHA живёт около двух минут, а набор тестов, который решает капчу в before-хуке и отправляет форму тремя тест-кейсами позже, отправляет уже просроченный токен. Про этот интервал стоит прочитать один раз: сколько живёт токен reCAPTCHA.
Шаг 5: запуск на CI и какой режим подключения для этого нужен
Тесты Nightwatch редко остаются на той машине, где они написаны. Как только они переезжают на раннер CI, в контейнер или на узел Selenium Grid, адрес обратной петли перестаёт означать ваш собственный компьютер. Режима подключения два. Local привязывается к 127.0.0.1 и отвечает только этому устройству. Server привязывается к вашему сетевому или публичному IP, поэтому раннер, контейнер или узел grid обращаются к той же машине с Windows по API. Оба режима находятся в разделе Настройки подключения, и режим Server меняет только то, какой адрес слушает решатель. Это по-прежнему ваше железо, и платы за каждое решение по-прежнему нет.
| Где выполняется тестовый процесс | Какой режим подключения |
|---|---|
| Ваша собственная машина, локальный chromedriver | Режим Local. 127.0.0.1 действительно верен |
| Сборочный агент в вашей сети | Server mode с локальным адресом решателя |
| Облачный раннер CI | Server mode со статическим публичным IP и правилом брандмауэра |
| Контейнер, решатель на хосте | Режим Server. Loopback внутри контейнера ведёт в сам контейнер |
Одно различие, на котором спотыкаются в Grid: вызов решателя делает тестовый процесс, а не браузер. Поэтому важен тот адрес, до которого дотягивается процесс Node, а сетевые настройки узла grid к этому отношения не имеют. Это разделение подробно разобрано в руководстве по Selenium Grid, а уровень драйвера, лежащий под обоими вариантами, описан на странице сервиса распознавания капчи для Selenium.
Частые ошибки и что они означают
| Что вы видите | Причина | Исправить |
|---|---|---|
| Решение капчи логируется до перехода на страницу | Вызов находится вне очереди, поэтому выполняется во время её построения | Оберните его в browser.perform |
| Тайм-аут почти ровно на десятой секунде | asyncHookTimeout остался со значением по умолчанию 10000 | Поднимите его в globals выше тайм-аута клиента |
| Ошибка element not interactable на поле ответа | Это скрытая textarea, и WebDriver не станет в неё печатать | Задайте значение через browser.execute |
| Токен отклонён, хотя тест прошёл | Он истёк между решением и отправкой | Решайте капчу непосредственно перед отправкой, а не в хуке |
| NetworkException после переезда набора тестов на CI | Решателя нет на раннере | Режим Server и заданный CAPSKIP_HOST на раннере |
| ERROR_GOOGLEKEY внутри ApiException | Sitekey взят не от того виджета или из URL iframe | Считывайте data-sitekey с того элемента, который решаете |
| CAPCHA_NOT_READY при самописном опросе | Результат прочитали до того, как он был готов | Доверьте опрос клиенту. Он сам увеличивает интервал |
Последний ответ пишется именно так, как выглядит, и пропущенная буква не является опечаткой с нашей стороны: API действительно возвращает его в таком виде. Полностью это объясняется в руководстве по CAPCHA_NOT_READY.
FAQ
Нужен ли для этого плагин Nightwatch?
Нет. Решатель подключается как обычный Node-пакет, который вы подключаете через require в тестовом файле, поэтому регистрировать пользовательскую команду не нужно и в массив plugins добавлять нечего. Если всё же захочется её написать, оборачивать стоит только пару perform и execute: это примерно восемь строк, которые избавят от повторов в разных тестах.
Можно ли решить капчу один раз в глобальном before-хуке и переиспользовать токен?
Нет, и сразу по двум причинам. Токен истекает примерно за две минуты, а набор тестов любого размера живёт дольше. Кроме того, токен привязан к той загрузке страницы, которая его породила, поэтому второму тест-кейсу, который снова открывает страницу, нужен свой. Решайте капчу в каждом тест-кейсе и как можно позже внутри него. Платы за каждое решение нет, так что это стоит вам только нескольких секунд реального времени.
Мои тесты идут параллельно. Меняет ли это что-нибудь?
Только арифметику. У каждого воркера своя очередь и свой вызов решения, поэтому четыре воркера означают четыре одновременных решения. Координировать ничего не нужно, и ничто не выстраивается в очередь за общим балансом, потому что потолок задаёт машина с CapSkip, а не количество кредитов. Немного поднимите asyncHookTimeout, если в неудачный день машина обрабатывает четыре запроса разом.
Чем это отличается от того же самого в WebdriverIO?
WebdriverIO выполняет свои команды там, где вы их написали, поэтому вызов решения капчи между двумя из них оказывается ровно на нужном месте без дополнительных усилий. Nightwatch ставит команды в очередь, и именно поэтому существует perform и поэтому ему здесь отведён целый раздел. Всё, что происходит после получения токена, в обоих случаях одинаково. Полный разбор второго варианта находится в руководстве по WebdriverIO.
Коротко
Помещайте решение капчи внутрь browser.perform, потому что всё, что находится вне очереди, выполняется во время её построения, а не тогда, когда вы задумали. Поднимите asyncHookTimeout выше собственного тайм-аута клиента, потому что значение по умолчанию 10000 короче одного решения. Подставляйте токен через execute, потому что поле ответа скрыто. Отправляйте форму сразу после решения. Задавайте CAPSKIP_HOST из переменных окружения и запускайте CapSkip в режиме Server везде, где тесты работают не на той машине, где стоит решатель.
- Сырые эндпоинты, которые стоят за клиентом, описаны в документации CapSkip API.
- Сама задача с галочкой разобрана на странице сервиса распознавания reCAPTCHA v2.
Что стоит знать, прежде чем добавлять решение капчи в каждый тест набора: CapSkip работает как локальный сервис распознавания капчи, поэтому прогон тестов с двумястами решениями стоит ровно столько же, сколько прогон с одним.
