Как решить капчу в тестах TestCafe (Node.js SDK)

Шаг с капчей в TestCafe короче, чем в большинстве фреймворков, потому что код тестов TestCafe и так выполняется в Node. Вы вызываете решатель прямо из файла теста, а затем записываете токен в страницу через ClientFunction. Но до всего этого нужно проверить одну вещь, и ошибка здесь съедает полдня: работает ли ваш прогон на нативной автоматизации или на старом прокси с переписыванием URL. На прокси reCAPTCHA сломана ещё до того, как решатель окажется рядом.
Что понадобится
- TestCafe 3.0 или новее и Node.js 18 или новее, плюс CapSkip Node.js SDK.
- Браузер на Chromium, то есть Chrome или Edge. Нативная автоматизация не работает с Firefox и Safari.
- URL страницы с тестируемой формой и её sitekey.
- Запущенный CapSkip в режиме Local, когда тест-раннер и решатель работают на одной машине, или в режиме Server, когда это не так. Оба режима описаны в разделе Настройки подключения.
# npm install capskip npm install --save-dev testcafe npm install capskip
Сначала проверьте: нативная автоматизация или прокси?
У TestCafe есть два способа управлять браузером, и рядом с капчей они ведут себя совершенно по-разному. Первоначальный вариант работает через веб-прокси под названием hammerhead. Он стоит между браузером и сайтом, внедряет свои скрипты автоматизации в каждую страницу и переписывает каждый URL в ресурсе так, чтобы он вёл обратно на прокси. Именно это позволило TestCafe поддерживать любой браузер без драйвера, и именно это ломает reCAPTCHA.
На прокси проявляются два сбоя, оба зарегистрированы как баги hammerhead, и ни один нельзя починить из вашего теста. reCAPTCHA пытается запустить web worker с источника Google, а браузер отказывает, потому что источником документа теперь стали хост и порт самого прокси. Кроме того, страницы, отданные через прокси, каждый раз возвращают оценку reCAPTCHA v3 равную 0.1, что большинство сайтов считает прямым признаком бота.
Нативная автоматизация всё это заменила. Вместо прокси TestCafe управляет Chromium по протоколу DevTools, поэтому в цепочке нет прокси и нет никакого переписывания URL. Она появилась как эксперимент в v2.5.0 и стала вариантом по умолчанию с v3.0.0. Если ваш набор тестов работает на актуальном TestCafe в Chrome, она у вас уже есть.
Поэтому первым делом при отладке убедитесь, что нативную автоматизацию ничего не отключило. TestCafe сам отключает её в Firefox и Safari, а флаг CLI с именем disable-native-automation и его двойник в конфигурационном файле отключают её и в Chromium. Чаще всего виноваты наборы тестов, куда этот флаг добавили годы назад ради обхода какой-то другой проблемы. Поищите его прежде, чем писать код решателя.
# Run in Chrome, which uses native automation by default. npx testcafe chrome tests/checkout.js # This flag puts you back on the proxy and breaks reCAPTCHA. # npx testcafe chrome tests/checkout.js --disable-native-automation
Об одном стоит сказать прямо, тем более что TestCafe говорит то же самое: если тестируемый сайт принадлежит вам, лучше вообще ничего не решать. Google публикует тестовый sitekey для v2, который всегда проходит, а настройка отдельного ключа v3 с мягким порогом занимает в консоли reCAPTCHA пять минут. Собственный рецепт для reCAPTCHA в документации TestCafe разбирает оба варианта. Решать капчу приходится там, где эта дверь закрыта: стороннее оформление заказа внутри сценария, staging-окружение с production-ключами или smoke-тест, который обязан работать по реальному сайту.
Шаг 1: считайте sitekey со страницы
Селекторы в TestCafe ленивые и повторяют попытки, поэтому селектор, написанный до отрисовки виджета, всё равно найдёт его, как только он появится. Берите sitekey с элемента виджета, а не вставляйте литерал в тест, и тогда тот же тест переживёт смену ключа.
// npm install capskip
import { Selector } from 'testcafe';
const PAGE_URL = 'https://example.com/page-with-recaptcha';
fixture('Checkout').page(PAGE_URL);
test('submits behind reCAPTCHA', async t => {
const widget = Selector('.g-recaptcha');
const sitekey = await widget.getAttribute('data-sitekey');
});Шаг 2: решите капчу прямо из файла теста
Вот здесь TestCafe проще, чем раннеры, работающие на стороне браузера. Ваша тестовая функция выполняется в обычном Node, поэтому SDK подключается простым импортом, а вызов остаётся обычным await. Не нужно строить мост и регистрировать task, а именно этого ждут те, кто пришёл из Cypress.
// npm install capskip
import { CapSkip } from 'capskip';
// Local mode. Change only the host to talk to a solver
// running on another machine.
const solver = new CapSkip({ host: '127.0.0.1', port: 8080 });
const result = await solver.recaptcha(sitekey, PAGE_URL);
const token = result.code;Этот один метод покрывает reCAPTCHA v2, Invisible, Enterprise и v3. Варианты задаются опциями в третьем аргументе, а не отдельными вызовами: invisible со значением 1, enterprise со значением 1 или version со значением v3 и строкой action. У Turnstile и GeeTest есть свои методы такой же формы. Все параметры перечислены в документации CapSkip API.
Шаг 3: запишите токен через ClientFunction
Поле ответа представляет собой скрытую textarea, поэтому обычное действие ввода до него не доберётся. Действия TestCafe работают только с видимыми элементами, и сделано это намеренно. ClientFunction вместо этого выполняет ваш код внутри страницы, и для поля, в которое живой пользователь никогда ничего не печатает, это подходящий инструмент.
Ловушка здесь ловит почти каждого хотя бы раз. ClientFunction не видит переменные окружающего теста. Тело функции сериализуется и отправляется в браузер, поэтому токен, захваченный из внешней области видимости, во время выполнения приходит как неопределённый идентификатор. Передавайте его аргументом или объявленной зависимостью.
// The token is a parameter, not a closure variable.
import { ClientFunction } from 'testcafe';
const injectToken = ClientFunction(value => {
const field = document.getElementById('g-recaptcha-response');
field.value = value;
field.dispatchEvent(new Event('change', { bubbles: true }));
});
await injectToken(token);В документации TestCafe советуют не использовать client functions для постоянного изменения поведения сайта, и этого совета стоит держаться. Запись одного значения в одно поле формы на один прогон под него не подпадает. Вы заполняете поле, а не патчите поведение страницы, и значение исчезает, как только прогон заканчивается.
Некоторые формы ждут callback, а не читают textarea. Если виджет объявляет атрибут data-callback, вызовите эту функцию с токеном в той же ClientFunction, и страница поведёт себя ровно так же, как для человека.
Полный рабочий пример
Тест целиком. Считать sitekey, решить капчу, вставить токен, отправить форму, проверить результат.
// npm install capskip
import { Selector, ClientFunction } from 'testcafe';
import { CapSkip, NetworkException } from 'capskip';
const PAGE_URL = 'https://example.com/page-with-recaptcha';
const solver = new CapSkip({ host: '127.0.0.1', port: 8080 });
const injectToken = ClientFunction(value => {
const field = document.getElementById('g-recaptcha-response');
field.value = value;
field.dispatchEvent(new Event('change', { bubbles: true }));
});
fixture('Checkout').page(PAGE_URL);
test('submits the protected form', async t => {
const sitekey = await Selector('.g-recaptcha').getAttribute('data-sitekey');
let token;
try {
token = (await solver.recaptcha(sitekey, PAGE_URL)).code;
} catch (err) {
if (err instanceof NetworkException) {
throw new Error('CapSkip is not reachable on 127.0.0.1:8080.');
}
throw err;
}
await injectToken(token);
await t.click(Selector('button[type=submit]'));
await t.expect(Selector('.thank-you').exists).ok();
});Решайте капчу как можно позже. Токен одноразовый и живёт около двух минут, поэтому тот, что получен в хуке фикстуры, который отрабатывает перед тремя другими тестами, к моменту отправки в четвёртом уже мёртв. Ставьте вызов в тот тест, которому он нужен.
Таймауты и тот единственный, который действительно мешает
Решение reCAPTCHA занимает десятки секунд, а это дольше нескольких значений по умолчанию в TestCafe. Хорошая новость в том, что те таймауты, о которых думают в первую очередь, здесь ни при чём. Вызов решателя выполняется как await внутри тестовой функции, а не как действие над страницей, поэтому таймаут селектора в 10 секунд и таймаут проверки в 3 секунды его вообще не видят.
Значение имеет таймаут выполнения теста, который ограничивает, сколько может идти один тест. Значения по умолчанию у него нет, поэтому он мешает только тогда, когда его кто-то задал. Если ваша конфигурация CI передаёт таймаут выполнения теста, убедитесь, что его значение оставляет запас на медленное решение капчи поверх всего остального, что делает тест. У SDK есть и собственный потолок: recaptchaTimeout по умолчанию равен 300 секундам и выбрасывает TimeoutException, когда решение в него не укладывается.
Запуск решателя на другой машине
Тесты переезжают в CI, а CI-раннер стоит не у вас на столе. В коде выше не меняется ничего, кроме строки с хостом.
У CapSkip два режима подключения. Local слушает 127.0.0.1 и отвечает только этому устройству, и это то, что нужно, пока вы пишете тест. Server слушает ваш сетевой или публичный IP, поэтому агент сборки, контейнер или виртуальная машина обращаются к той же машине с Windows по API. Статический публичный IP делает этот адрес постоянным. Это ваше собственное оборудование, и в обоих режимах оплата за решения не взимается, поэтому набор тестов, решающий пятьсот капч за ночь, стоит ровно столько же, сколько набор, решающий пять.
// Same SDK, same call. Only the host moves.
const solver = new CapSkip({
host: process.env.CAPSKIP_HOST || '127.0.0.1',
port: 8080,
apiKey: process.env.CAPSKIP_API_KEY,
});SDK сам читает CAPSKIP_HOST, CAPSKIP_PORT и CAPSKIP_API_KEY из окружения, поэтому задача в CI может направить тот же файл теста на удалённый решатель двумя переменными и без изменений в коде. Включите проверку ключа, как только решатель начнёт слушать сетевой адрес, и выдайте каждому раннеру собственный ключ, чтобы один можно было отозвать, не трогая остальные. Оба режима подробно разобраны в руководстве по настройке CapSkip.
Частые ошибки и что они означают
| Что вы видите | Причина | Исправить |
|---|---|---|
| Ошибка Failed to construct Worker: script cannot be accessed from origin | В цепочке стоит прокси hammerhead, поэтому origin страницы принадлежит не сайту | Уберите флаг disable-native-automation и запускайте в Chrome или Edge |
| Любая оценка v3 возвращается как 0.1 | Причина та же. Прокси фиксирует оценку независимо от того, что делает тест | Решение то же. Нативная автоматизация полностью убирает прокси |
| ReferenceError о том, что токен не определён | Тело ClientFunction не читает переменные внешней области видимости | Передайте токен аргументом или объявленной зависимостью |
| Действие ввода не срабатывает на поле ответа | textarea скрыта, а действиям нужен видимый элемент | Вместо этого задайте значение в ClientFunction |
| Форма отклоняет токен, который выглядит нормальным | Капчу решили в хуке, за несколько минут до отправки | Решайте капчу внутри теста, непосредственно перед отправкой |
| NetworkException | CapSkip не запущен или указан неверный хост | Запустите приложение или направьте host на адрес сервера |
| TimeoutException | Решение заняло больше, чем recaptchaTimeout | Поднимите его выше значения по умолчанию в 300 секунд |
| ValidationException | Отсутствующий или некорректный sitekey либо URL страницы | Выведите оба значения в лог перед вызовом и проверьте, что sitekey действующий |
FAQ
Нужен ли здесь task или плагин, как в Cypress?
Нет. Cypress выполняет код ваших тестов внутри браузера, поэтому всё, чему нужен Node, вынуждено проходить через мост. TestCafe с самого начала выполняет код тестов в Node, и в браузер уходят только тела ClientFunction, поэтому вызов решателя остаётся обычным импортом. Версия этой же задачи для Cypress описана в руководстве по капче в Cypress.
Можно ли сделать это в Firefox или Safari?
Запустить тест можно, но ждите, что сам виджет будет вести себя неправильно: в этих браузерах TestCafe откатывается на прокси, а именно такую конфигурацию reCAPTCHA не переживает. Держите тесты с капчей на Chrome или Edge, а кроссбраузерную матрицу оставьте для страниц, где виджета нет.
Работает ли это и для Turnstile?
Да, с двумя отличиями. Метод называется turnstile, а не recaptcha, и заполнять нужно скрытое поле input с именем cf-turnstile-response. Для полноценной страницы проверки понадобятся ещё значения data и pagedata плюс user agent, который возвращается вместе с токеном, и всё это разобрано на страницу решателя Cloudflare Turnstile.
Стоит ли гонять тесты с капчей на каждый коммит?
Обычно нет, и причина в скорости, а не в деньгах. Решения здесь не тарифицируются, но десятки секунд на тест делают проверку pull request медленной. Пометьте такие тесты тегом и запускайте их в ночной или предрелизной задаче, а быстрый набор направьте на сборку с тестовыми ключами.
Коротко
Убедитесь, что нативная автоматизация включена, потому что старый прокси ломает reCAPTCHA сам по себе. Считайте sitekey через Selector, вызовите решатель прямо из файла теста, раз это уже Node, и запишите токен через ClientFunction, передав значение аргументом. Решайте капчу непосредственно перед отправкой.
Остальные возможности на стороне Node.js разобраны на странице сервиса распознавания капчи для Node.js. Всё, что относится именно к этому типу капчи, находится на странице сервиса распознавания reCAPTCHA v2. Та же задача в раннере на базе WebDriver описана в руководстве по WebdriverIO.
И последнее, прежде чем встраивать это в CI. CapSkip работает как локальный сервис распознавания капчи на оборудовании, которое у вас уже есть, поэтому ночной набор тестов с тысячей решений стоит столько же, сколько набор с десятью.
