Captcha in WebdriverIO-Tests mit Node.js lösen

Ein Captcha-Schritt in WebdriverIO ist ein Node.js-Aufruf, kein Browser-Aufruf. Sie lösen im Testprozess, übergeben das Token per browser.execute an die Seite und senden das Formular ab. Zwei Dinge bringen Leute dabei ins Straucheln, und keines davon hat mit dem Solver zu tun. Legen Sie das Lösen in den Browser-Kontext, hat dieser überhaupt keine Möglichkeit, Ihren Rechner zu erreichen. Lassen Sie Mocha beim Standard-Timeout von dreißig Sekunden, stirbt der Test mitten im Lösevorgang mit einer Meldung, die nichts erklärt. Hier sind der benutzerdefinierte Befehl, die eine Konfigurationsänderung und eine Testdatei, die funktioniert.
Was Sie brauchen
- WebdriverIO 8 oder neuer mit dem Mocha-Framework. Alles Folgende ist asynchron, denn der alte synchrone Modus ist Geschichte.
- Node.js 18 oder höher, sowie das CapSkip-Paket, installiert im Testprojekt.
- Eine Seite, die tatsächlich eine Challenge ausliefert. Ein Test-sitekey, der immer durchgeht, prüft nichts von alldem.
- CapSkip läuft im Local-Modus, wenn die Tests auf Ihrem eigenen Rechner laufen, oder im Server-Modus, wenn sie auf einem CI-Runner oder in einem Grid laufen. Näheres dazu finden Sie unter den Verbindungseinstellungen.
# Install into the project that runs wdio, not into the browser image. npm install capskip
In Node lösen, nicht im Browser
Dieser Fehler sollte zuerst aus dem Weg geräumt werden, denn WebdriverIO macht es leicht, ihn zu begehen. Der Befehl browser.execute serialisiert Ihre Funktion, schickt sie an den Browser und führt sie innerhalb der Seite aus. Dort gibt es keinen Modul-Loader, weshalb das SDK schlicht nicht verfügbar ist. Selbst wenn es das wäre: Die Seite ist das, was Sie testen, und ihr die Adresse Ihres Solvers zu verraten, ist nichts, was Sie tun sollten.
Der triftigere Grund ist das Routing. Sobald sich der Browser irgendwo anders befindet als auf Ihrem eigenen Rechner, und bei einem Grid oder einem Cloud-Geräteanbieter ist das nie der Fall, gehört die Loopback-Adresse in diesem Browser dem Browser-Host. Ihr Solver ist dort nicht zu finden. Der Node-Prozess, der wdio ausführt, ist derjenige, der weiß, wie CapSkip zu erreichen ist, also bleibt das Lösen dort, und nur das fertige Token gelangt in die Seite.
Schritt 1: einen benutzerdefinierten Befehl registrieren
Statt das SDK in jede Testdatei zu importieren, fügen Sie einen einzigen Befehl im before-Hook Ihrer Konfiguration hinzu. Er steht dann in jedem Test am browser-Objekt zur Verfügung, und die Adresse des Solvers existiert an genau einer Stelle.
// 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
});
},
};Innerhalb von addCommand ist der this-Wert der browser-Scope, weshalb getUrl dort funktioniert. Dieses Detail leistet echte Arbeit: Die API benötigt die URL der Seite, auf der das Widget sitzt, und sie vom Browser abzufragen bedeutet, dass die Testdatei nie eine URL wiederholen muss, die sie bereits angesteuert hat.
Lesen Sie den Host aus der Umgebung, statt ihn fest zu verdrahten. Dieselbe Suite läuft dann ohne Codeänderung sowohl gegen einen Solver auf Ihrem Laptop als auch gegen einen gemeinsam genutzten aus CI.
Schritt 2: Token einfügen und absenden
Google legt die Antwort in ein verstecktes textarea mit der id g-recaptcha-response. Es ist versteckt, sodass auch noch so viele setValue-Aufrufe daran nichts ändern. Setzen Sie den Wert direkt und senden Sie das Formular dann so ab, wie es ein Mensch tun würde.
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();Das deckt den üblichen Fall ab: ein Formular, das beim Absenden das textarea ausliest. Manche Seiten deklarieren stattdessen ein data-callback am Widget und schauen sich das textarea nie an. Testen Sie so eine Seite, rufen Sie den Callback nach dem Setzen des Werts im selben browser.execute-Block auf, denn ein Feld zu füllen, das niemand liest, ändert gar nichts.
Schritt 3: das Mocha-Timeout erhöhen
WebdriverIO setzt für Mocha standardmäßig ein Timeout von 30000 Millisekunden, was für das Klicken auf Elemente großzügig bemessen ist und für das Lösen einer Challenge deutlich zu knapp. Ein Test, der sonst bestehen würde, schlägt hier fehl, und die Fehlermeldung nennt Mocha statt irgendetwas, das mit dem Lösevorgang zu tun hat, was Leute einen ganzen Nachmittag lang an der falschen Stelle suchen lässt.
exports.config = {
framework: 'mocha',
mochaOpts: {
// The 30000 default expires mid solve. Give it room.
timeout: 120000,
},
};Setzen Sie die beiden Grenzwerte in der richtigen Reihenfolge. Das SDK gibt nach recaptchaTimeout auf, das standardmäßig 300 Sekunden beträgt, und Mocha gibt nach seinem eigenen Timeout auf. Halten Sie das SDK unter Mocha, erhalten Sie eine TimeoutException, die den Lösevorgang benennt. Halten Sie es darüber, beendet Mocha den Test zuerst, und alles, was Sie lernen, ist, dass irgendetwas zu lange gedauert hat. Ein Solver-Timeout von 90 Sekunden unter einem Mocha-Timeout von 120000 Millisekunden ist eine vernünftige Kombination für eine Suite, die schnell bleiben muss.
Ausführung in CI oder auf einem Grid
Klären Sie, welche Maschine den Solver erreichen muss, denn es ist nicht die naheliegende. Der Browser spricht nie mit CapSkip. Der Prozess, der wdio ausführt, tut das. Auf einem GitHub-Actions-Runner, in einem Container oder auf einem Laptop, der einen Cloud-Browser steuert, ist also dieser Runner der Aufrufer, und ihn auf seine eigene Loopback-Adresse zu verweisen, findet nichts.
| Modus | Lauscht auf | Sinnvoll, wenn |
|---|---|---|
| Lokal | 127.0.0.1, nur dieses Gerät | Sie führen wdio auf derselben Maschine wie den Solver aus |
| Server | Ihre Netzwerkadresse oder öffentliche IP | CI-Runner, Container, eine gemeinsam genutzte Suite, ein Grid |
Der Server-Modus deckt alles aus der zweiten Zeile ab. Ändern Sie die Listen-Adresse in der App, setzen Sie CAPSKIP_HOST auf dem Runner, und jeder Job teilt sich einen Solver. Eine statische öffentliche IP lohnt sich, wenn die Aufrufer außerhalb Ihres Netzwerks sitzen. Nichts davon ändert, was das Produkt ist: Es bleibt Ihre eigene Hardware und bleibt unbegrenzt nutzbar, sodass der Wechsel von der Loopback-Adresse nur verschiebt, wo es läuft, und sonst nichts. CapSkip ist eine Windows-Anwendung, in der Praxis also ein Windows-Rechner, den Ihre Runner ansprechen.
Die vollständige Testdatei
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();
});
});Beachten Sie, wie wenig davon überhaupt mit Captcha zu tun hat. Drei Zeilen tragen die gesamte Integration, der Rest ist der Test, den Sie ohnehin schreiben wollten. Halten Sie Lösen und Absenden im selben Testkörper, damit das Token nur Sekunden alt ist, wenn das Formular es ausliest.
Häufige Fehler
| Was Sie sehen | Ursache | Beheben |
|---|---|---|
| Mocha-Timeout von 30000ms überschritten | Der Lösevorgang dauerte länger als der Standardwert | mochaOpts.timeout erhöhen und recaptchaTimeout darunter ansetzen |
| solver ist innerhalb von browser.execute nicht definiert | Die Funktion lief in der Seite, nicht in Node | Vor dem execute-Aufruf lösen und nur das Token übergeben |
| NetworkException aus CI, lokal einwandfrei | Der Runner erreicht den Solver nicht | Auf den Server-Modus wechseln und CAPSKIP_HOST auf dem Runner setzen |
| Formular lehnt ein sauber gelöstes Token ab | Die Seite nutzt einen Callback und ignoriert das textarea | Den Widget-Callback nach dem Setzen des Werts aufrufen |
| ERROR_GOOGLEKEY | Ein Turnstile-sitekey landete bei der reCAPTCHA-Methode | Für Turnstile-Widgets die turnstile-Methode verwenden |
Die vollständige Liste der Codes und was jeden einzelnen auslöst, finden Sie in der CapSkip-API-Dokumentation.
FAQ
Kann ich den Solver innerhalb von browser.execute aufrufen?
Nein, und das scheitert aus zwei getrennten Gründen. Die Funktion, die Sie übergeben, wird serialisiert und in der Seite ausgeführt, wo es weder einen Modul-Loader noch ein SDK gibt. Selbst mit einem SDK sitzt der Browser oft auf einem völlig anderen Host, sodass die Adresse, die Sie ansprechen würden, gar nicht Ihre ist. Lösen Sie im Node-Prozess und übergeben Sie das fertige Token als Argument.
Funktioniert das auch mit einem entfernten Grid oder einem Cloud-Browser?
Ja, und genau dieser Aufbau ist der Grund, warum das Lösen in Node gehört. Ihr Testprozess läuft lokal oder auf einem Runner und erreicht den Solver direkt, während der Browser woanders sitzt und stets nur ein Token erhält. Führen Sie CapSkip im Server-Modus aus, damit ihn erreicht, welche Maschine auch immer die Suite ausführt, und an der Testdatei ändert sich nichts.
Sollte ich einmal im before-Hook lösen und das Token wiederverwenden?
Nein. Ein reCAPTCHA-Token ist nur einmal verwendbar und bleibt etwa zwei Minuten gültig, sodass der zweite Test, der es verwendet, abgelehnt wird, und auch der erste Test nach einer langsamen Testdatei abgelehnt wird. Lösen Sie in jedem Test, der eines braucht, neu. Ein gemeinsam genutzter Solver verkraftet die zusätzlichen Aufrufe ohne Kosten pro Lösung, es gibt also nichts zu sparen, wenn Sie Tokens horten.
Ich verwende Cucumber oder Jasmine statt Mocha. Was ändert sich?
Nur der Name der Timeout-Einstellung. Der benutzerdefinierte Befehl, das Einfügen des Tokens und die Frage nach dem Server-Modus bleiben identisch. Erhöhen Sie cucumberOpts.timeout oder jasmineOpts.defaultTimeoutInterval statt mochaOpts.timeout, und halten Sie das Solver-Timeout darunter, ganz gleich, welchen Sie setzen.
Die Kurzfassung
Registrieren Sie einen benutzerdefinierten Befehl im before-Hook, setzen Sie das Framework-Timeout höher als die Lösezeit, und fügen Sie das Token mit browser.execute ein, statt zu versuchen, es einzutippen. Für die Client-Seite siehe die Node.js-Captcha-Solver-Seite, für die WebDriver-Seite siehe die Selenium-Captcha-Solver-Seite, und was das Token eigentlich ist, siehe die reCAPTCHA-v2-Solver-Seite. Bei Testsuiten trifft eine Abrechnung pro Lösung am härtesten, weil eine Suite, die bei jedem Pull Request läuft, dasselbe Formular hunderte Male pro Woche löst. Genau das ändert der Besitz: ein unbegrenzter Captcha-Löser auf Hardware, die Sie bereits besitzen, kostet genauso viel, ob CI zweimal am Tag oder zweimal pro Stunde läuft.
