So lösen Sie Captchas in Nightwatch.js (Command Queue)

Eine Captcha-Lösung in Nightwatch muss innerhalb der Command Queue laufen, nicht daneben. Nightwatch führt Browser-Befehle nicht dort aus, wo Sie sie geschrieben haben. Es stellt sie in eine Warteschlange und arbeitet diese ab, nachdem Ihre Testfunktion zurückgekehrt ist. Ein Aufruf zum Lösen, der zwischen zwei Browser-Befehlen steht, feuert deshalb sofort, bevor die Seite geladen ist und bevor das Widget überhaupt existiert. Die Abhilfe ist browser.perform, dazu eine globale Einstellung, die Sie anheben müssen, weil eine Lösung länger dauert als die zehn Sekunden, die Nightwatch einem asynchronen Callback zugesteht.
Was Sie brauchen
- Nightwatch 3 mit einem funktionierenden Treiber, entweder chromedriver lokal oder ein entfernter WebDriver-Endpunkt.
- CapSkip läuft auf einer Windows-Maschine, mit dem Node-Client im selben Projekt wie Ihre Tests installiert.
- Der Sitekey und die Seiten-URL. Lesen Sie den Sitekey vom Widget ab, statt ihn fest im Code zu hinterlegen, denn Staging und Produktion teilen sich selten einen.
- Der Server-Modus, sobald die Tests woanders laufen als auf der Maschine des Lösers, und das schließt jeden CI-Runner ein. Es ist eine einzige Einstellung unter den Verbindungseinstellungen.
# npm install capskip npm install --save-dev nightwatch npm install capskip
Schritt 1: Warum ein einfacher Aufruf zum Lösen zu früh läuft
Jeder Browser-Befehl in einem Nightwatch-Test ist eine Anweisung, die einer Warteschlange hinzugefügt wird. Die Testfunktion läuft zuerst von oben nach unten durch und baut diese Warteschlange auf, und erst danach beginnt Nightwatch, sie abzuarbeiten. Gewöhnliches JavaScript dazwischen ist nicht Teil der Warteschlange und läuft daher schon während des Aufbaus. Das ist das ganze Problem in einem Satz.
// 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");
},
};Das Symptom verwirrt, weil nichts eine Ausnahme wirft. Das Lösen gelingt, der Test besteht manchmal, und das Token gehört zu einem Seitenaufruf, den es nie gegeben hat. Nightwatch bietet Ihnen einen dokumentierten Weg, eigenen Code stattdessen in die Warteschlange zu legen: browser.perform, dessen Callback als die Funktion beschrieben wird, die als Teil der Warteschlange ausgeführt wird.
// 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");Die andere Möglichkeit ist ein asynchroner Test. Wenn Sie die Testfunktion als async deklarieren, geben die API-Befehle ein Promise zurück, und ein await auf jeden einzelnen hält alles auch ohne perform in der richtigen Reihenfolge. Beides funktioniert. Der Fehler liegt im Mischen: Ein awaitetes externes Promise zwischen nicht awaiteten Browser-Befehlen bringt Sie zurück zum ersten Beispiel.
Schritt 2: asyncHookTimeout anheben, sonst läuft die Lösung nach zehn Sekunden in einen Timeout
Das ist der Punkt, der einen ganzen Nachmittag kostet. Die asynchrone Ausführung innerhalb von perform wird durch das Global asyncHookTimeout begrenzt, und dessen Standardwert liegt bei 10000 Millisekunden. Eine reCAPTCHA-Lösung dauert regelmäßig fünfzehn bis fünfundvierzig Sekunden. Der Callback wird also abgebrochen, während der Löser noch arbeitet, und die Fehlermeldung spricht von einem Timeout statt vom Captcha.
Heben Sie den Wert in Ihrer Nightwatch-Konfiguration an, global oder pro Umgebung.
// 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,
},
};Stellen Sie die Client-Seite so ein, dass sie darunter liegt, damit klar ist, welche Obergrenze wirklich greift. Der Node-Client pollt nach seinem eigenen Zeitplan, beginnt bei 250 Millisekunden und geht dann auf pollingInterval zurück, weshalb er meist früher antwortet als eine handgeschriebene Schleife.
// 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,
});Schritt 3: Das Token mit execute einfügen, nicht mit setValue
Das Antwortfeld, das reCAPTCHA ausliest, ist eine versteckte Textarea. WebDriver verweigert die Interaktion mit Elementen, die es als nicht interagierbar einstuft, deshalb scheitert setValue darauf mit einem element not interactable-Fehler. Der übliche Weg ist das Einfügen über die Seite, und Nightwatch stellt das als execute bereit, das einen Funktionsrumpf, ein Argument-Array und einen optionalen Callback entgegennimmt.
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]
);
});Wenn das Formular bei Abschluss einen Callback ausführt, statt das Feld beim Absenden auszulesen, rufen Sie ihn im selben Skript auf. Invisible reCAPTCHA arbeitet fast immer so, und die Widget-Varianten ändern nur die Optionen, die Sie übergeben: invisible oder enterprise auf 1 gesetzt, version auf v3 mit einer action, oder turnstile und geetest statt recaptcha. Sämtliche Optionen dazu dokumentiert die Node.js-Captcha-Solver-Seite.
Schritt 4: Der vollständige Test
Der Client wird einmal im Modul-Scope außerhalb des Tests erzeugt, damit er nicht für jeden Testfall neu gebaut wird. Der Sitekey wird von der Seite gelesen statt fest im Code hinterlegt, und genau deshalb läuft dieselbe Spec gegen Staging und gegen Produktion.
// 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");
});
});Lösen Sie so spät wie möglich und senden Sie sofort ab. Ein reCAPTCHA-Token ist rund zwei Minuten lang gültig, und eine Suite, die in einem before-Hook löst und drei Testfälle später absendet, schickt ein abgelaufenes Token. Dieses Zeitfenster sollten Sie einmal nachlesen: wie lange ein reCAPTCHA-Token gültig ist.
Schritt 5: Auf CI ausführen, und welchen Verbindungsmodus das erfordert
Nightwatch-Tests bleiben selten auf der Maschine, auf der sie entstanden sind. Sobald sie auf einen CI-Runner, in einen Container oder auf einen Selenium-Grid-Knoten umziehen, bedeutet Loopback nicht mehr Ihren Schreibtisch. Es gibt zwei Verbindungsmodi. Der Local-Modus bindet an 127.0.0.1 und antwortet nur diesem Gerät. Der Server-Modus bindet an Ihre Netzwerkadresse oder öffentliche IP, sodass ein Runner, ein Container oder ein Grid-Knoten dieselbe Windows-Maschine über die API erreichen kann. Beides finden Sie unter Verbindungseinstellungen, und der Server-Modus ändert nur, auf welcher Adresse der Löser lauscht. Es ist weiterhin Ihre Hardware, und es wird weiterhin nicht pro Lösung abgerechnet.
| Wo der Testprozess läuft | Welcher Verbindungsmodus |
|---|---|
| Ihre eigene Maschine, chromedriver lokal | Local-Modus. 127.0.0.1 ist hier wirklich richtig |
| Ein Build-Agent in Ihrem Netzwerk | Server-Modus mit der LAN-Adresse des Solvers |
| Ein gehosteter CI-Runner | Server-Modus mit einer statischen öffentlichen IP und einer Firewallregel |
| Ein Container, Löser auf dem Host | Server-Modus. Loopback in einem Container ist der Container |
Eine Unterscheidung bringt bei Grid viele durcheinander: Der Aufruf beim Löser kommt vom Testprozess, nicht vom Browser. Entscheidend ist also die Adresse, die der Node-Prozess erreichen kann, und das Netzwerk des Grid-Knotens spielt dafür keine Rolle. Nachlesen können Sie diese Aufteilung im Selenium-Grid-Leitfaden, und einen genaueren Blick auf die darunterliegende Treiberebene bietet die Selenium-Captcha-Solver-Seite.
Häufige Fehler und was sie bedeuten
| Was Sie sehen | Ursache | Beheben |
|---|---|---|
| Die Lösung wird protokolliert, bevor die Seite navigiert | Der Aufruf liegt außerhalb der Warteschlange und läuft daher schon, während diese aufgebaut wird | Packen Sie ihn in browser.perform |
| Ein Timeout nach fast genau zehn Sekunden | asyncHookTimeout steht noch auf dem Standardwert 10000 | Heben Sie ihn in globals an, über das Client-Timeout |
| Element not interactable am Antwortfeld | Es ist eine versteckte Textarea, und WebDriver tippt nicht hinein | Setzen Sie den Wert mit browser.execute |
| Das Token wird abgelehnt, obwohl der Test bestanden hat | Es ist zwischen Lösung und Absenden abgelaufen | Lösen Sie unmittelbar vor dem Absenden, nicht in einem Hook |
| NetworkException, sobald die Suite auf CI umzieht | Der Löser läuft nicht auf dem Runner | Server-Modus, und setzen Sie CAPSKIP_HOST auf dem Runner |
| ERROR_GOOGLEKEY innerhalb einer ApiException | Der Sitekey stammt vom falschen Widget oder aus einer iframe-URL | Lesen Sie data-sitekey von dem Element ab, das Sie lösen |
| CAPCHA_NOT_READY bei einer selbst gebauten Polling-Schleife | Das Ergebnis wurde gelesen, bevor es fertig war | Lassen Sie den Client pollen. Er drosselt von selbst |
Diese letzte Antwort wird tatsächlich so geschrieben, und der fehlende Buchstabe ist kein Tippfehler auf unserer Seite, denn die API gibt sie wirklich so zurück. Ausführlich erklärt wird das im Leitfaden zu CAPCHA_NOT_READY.
FAQ
Brauche ich dafür ein Nightwatch-Plugin?
Nein. Der Löser ist ein gewöhnliches Node-Paket, das Sie in der Spec-Datei per require einbinden, es gibt also keinen eigenen Command zu registrieren und nichts, was in das plugins-Array müsste. Falls Sie doch einen schreiben, lohnt sich als Wrapper einzig das Paar aus perform und execute, rund acht Zeilen, die Sie sonst in jeder Spec wiederholen.
Kann ich einmal in einem globalen before-Hook lösen und das Token wiederverwenden?
Nein, aus zwei getrennten Gründen. Das Token läuft nach etwa zwei Minuten ab, und jede halbwegs große Suite überlebt diese Zeitspanne. Außerdem hängt es an dem Seitenaufruf, der es erzeugt hat, sodass ein zweiter Testfall, der die Seite erneut lädt, ein eigenes braucht. Lösen Sie pro Testfall, und darin so spät wie möglich. Da der Löser nicht pro Lösung abrechnet, kostet Sie das nichts außer ein paar Sekunden Laufzeit.
Meine Tests laufen parallel. Ändert das etwas?
Nur die Rechnung. Jeder Worker hat seine eigene Warteschlange und seinen eigenen Aufruf zum Lösen, vier Worker bedeuten also vier gleichzeitige Lösungen. Nichts muss koordiniert werden, und nichts wartet hinter einem gemeinsamen Guthaben, denn die Obergrenze ist die Maschine, auf der CapSkip läuft, und kein Credit-Zähler. Heben Sie asyncHookTimeout etwas an, wenn die Kiste an einem trägen Tag vier Lösungen gleichzeitig stemmt.
Worin unterscheidet sich das von WebdriverIO?
WebdriverIO löst seine Befehle dort auf, wo Sie sie schreiben, deshalb landet ein Aufruf zum Lösen zwischen zwei Befehlen ohne Umstände an der richtigen Stelle. Nightwatch arbeitet mit einer Warteschlange, und genau dafür gibt es perform, und genau deshalb widmet dieser Beitrag dem Thema einen eigenen Abschnitt. Alles nach dem Token ist in beiden identisch. Die WebdriverIO-Variante finden Sie Schritt für Schritt in der WebdriverIO-Anleitung.
Die Kurzfassung
Legen Sie das Lösen in browser.perform, denn alles außerhalb der Warteschlange läuft schon beim Aufbau der Warteschlange und nicht dann, wenn Sie es vorgesehen haben. Heben Sie asyncHookTimeout über das eigene Timeout des Clients, denn der Standardwert 10000 ist kürzer als eine Lösung. Fügen Sie das Token mit execute ein, denn das Antwortfeld ist versteckt. Senden Sie direkt nach dem Lösen ab. Setzen Sie CAPSKIP_HOST aus der Umgebung und betreiben Sie CapSkip überall dort im Server-Modus, wo die Tests nicht auf der Maschine des Lösers laufen.
- Die rohen Endpunkte hinter dem Client sind dokumentiert in der CapSkip-API-Dokumentation.
- Die Checkbox-Challenge selbst erklärt Ihnen die reCAPTCHA-v2-Solver-Seite.
Bevor Sie in jede Spec der Suite ein Lösen einbauen, sollten Sie eines wissen: CapSkip ist ein lokaler Captcha-Löser, sodass ein Testlauf mit zweihundert Lösungen genau so viel kostet wie ein Testlauf mit einer einzigen.
