Captcha in Cypress-Tests mit cy.task lösen

cypress captcha - How to Solve CAPTCHA in Cypress Tests With cy.task

Ein Captcha-Problem in Cypress ist zuerst ein Laufzeitproblem und erst danach ein Lösungsproblem. Ihr Spec-Code läuft im getesteten Browser, deshalb kann der Node-Client, der mit dem Löser spricht, dort nicht liegen. Registrieren Sie ihn stattdessen als Task in cypress.config.js und rufen Sie diesen Task aus der Spec heraus auf; schreiben Sie das Token selbst in das versteckte Feld. Drei Dinge machen es möglich: der Task, ein erhöhtes Timeout und ein direkter DOM-Schreibzugriff statt eines Cypress-Klicks. Dieser Leitfaden behandelt alle drei.

Was Sie brauchen

  • Cypress 10 oder neuer, denn dort kamen cypress.config.js und setupNodeEvents dazu. Ältere Versionen nutzen die alte Plugins-Datei, und dieselbe Idee gilt weiterhin
  • Node.js 18 oder neuer
  • CapSkip läuft und ist erreichbar. Local mode lauscht auf 127.0.0.1 Port 8080 für einen Testlauf auf derselben Maschine, und Server mode lauscht auf Ihrem Netzwerk oder Ihrer öffentlichen IP, sodass ein CI-Runner oder ein anderer Rechner ihn aufrufen kann. Beides finden Sie in den Verbindungseinstellungen
  • Der Löser-Client, installiert als Dev-Dependency
# The client only ever runs in the Node half of Cypress.
npm install --save-dev capskip

Warum das Lösen nicht in die Spec gehört

Cypress teilt sich in zwei Prozesse auf, und genau daran scheitert die naive Variante. Ihre Spec-Datei wird gebündelt und im Browser ausgeführt, direkt neben der Anwendung. Alles in cypress.config.js läuft in Node, außerhalb davon.

Ein require des Löser-Clients am Anfang einer Spec zieht deshalb einen Node-HTTP-Client in ein Browser-Bundle. Selbst wenn der Bundler das durchlässt, blockiert der Browser anschließend den Aufruf: Eine Anfrage vom Origin Ihrer Anwendung an 127.0.0.1 auf Port 8080 ist Cross-Origin, und der Löser sendet keine CORS-Header, die das erlauben würden.

Cypress bietet zwei Türen nach Node, und beide sind in Ordnung:

  • cy.task führt eine beliebige Funktion aus, die Sie in der Konfiguration registriert haben. Hier gehört das SDK hin, denn seine Polling- und Backoff-Logik läuft dann in Node, wofür sie entworfen wurde.
  • cy.request führt den HTTP-Aufruf im Node-Prozess von Cypress aus statt im Browser, weshalb die Cypress-Dokumentation sagt, dass es CORS vollständig umgeht. Gut, wenn Sie lieber die rohe API ansprechen und sich die Abhängigkeit sparen wollen.

Schritt 1: Das Lösen als Task registrieren

Eine Funktion, einmal registriert, verfügbar für jede Spec.

// npm install --save-dev capskip
const { defineConfig } = require('cypress');
const { CapSkip } = require('capskip');

// Local mode. Point host at a server IP to share one solver.
const solver = new CapSkip({ host: '127.0.0.1', port: 8080 });

module.exports = defineConfig({
  // 60000 is the default, and a v2 solve can outlast it.
  taskTimeout: 180000,

  e2e: {
    setupNodeEvents(on) {
      on('task', {
        async solveRecaptcha({ sitekey, url }) {
          const result = await solver.recaptcha(sitekey, url);
          return result.code;   // the token
        },
      });
    },
  },
});

Eine Regel zu Tasks, die beim ersten Mal jeden eine Stunde kostet: Ein Task muss einen Wert oder null zurückgeben, niemals undefined. Vergessen Sie das return, lässt Cypress den Befehl mit einer Meldung darüber scheitern, dass der Task undefined zurückgegeben hat. Das liest sich, als sei der Löser kaputt, dabei wurde er nie aufgerufen.

Schritt 2: Den Task aufrufen und das Token einsetzen

Lesen Sie den Sitekey von der Seite, übergeben Sie ihn an den Task und legen Sie die Antwort dorthin, wo das Widget sie abgelegt hätte.

// cypress/e2e/login.cy.js
it('logs in through the reCAPTCHA', () => {
  cy.visit('/login');

  cy.get('[data-sitekey]')
    .invoke('attr', 'data-sitekey')
    .then((sitekey) => {
      const url = 'https://example.com/login';

      cy.task('solveRecaptcha', { sitekey, url }).then((token) => {
        // The widget writes into a hidden textarea. Do the same.
        cy.document().then((doc) => {
          doc.getElementById('g-recaptcha-response').value = token;
        });
      });
    });

  cy.get('button[type=submit]').click();
  cy.contains('Welcome back');
});

Beachten Sie den DOM-Schreibzugriff. Das Antwortfeld ist eine versteckte textarea, und Cypress weigert sich, in ein Element zu tippen, das es für unsichtbar hält. Die naheliegende Variante mit get und type scheitert also an der Sichtbarkeit, lange bevor sie überhaupt in die Nähe des Tokens kommt. Der Weg über cy.document umgeht das, genau so, wie es auch das JavaScript des Widgets tun würde.

Trägt das Widget-div ein data-callback-Attribut, rufen Sie diese Funktion mit dem Token auf, statt auf den Submit-Button zu klicken. Seiten, die so gebaut sind, verdrahten nie ein normales Formular-Submit, deshalb bewirkt der Klick nichts.

Das Timeout, das wirklich zubeißt, ist taskTimeout

Das ist der Fehler, der am häufigsten dem Löser angelastet wird, und die Behebung ist eine Zeile in der Konfiguration.

Cypress gibt einem Task standardmäßig 60 Sekunden Zeit. Ein reCAPTCHA-v2-Job ist in den ersten 15 bis 20 Sekunden nicht fertig, v3 braucht 10 bis 15, und eine ausgelastete Maschine kann beides verlängern. Ist die Obergrenze erreicht, bricht Cypress den Befehl ab, und der Test scheitert mit einem Timeout, das Ihren Task nennt, nicht das Captcha.

Die Falle daneben ist, die falsche Zahl zu erhöhen. Die meisten Cypress-Tipps zu Timeouts verweisen auf defaultCommandTimeout, das bei 4000 Millisekunden liegt und DOM-Befehle steuert. Auf einen Task hat es keine Wirkung. Drei Werte sind hier wichtig, und sie sind alle voneinander getrennt:

OptionStandardGilt für
taskTimeout60000 mscy.task, also das Lösen
responseTimeout30000 mscy.request, also ein roher API-Aufruf
defaultCommandTimeout4000 msDOM-Befehle, nicht die beiden oben genannten

Setzen Sie es global wie in der Konfiguration oben, oder pro Aufruf, wenn nur ein Test den Spielraum braucht:

// Same task, a longer leash for this one call.
cy.task('solveRecaptcha', { sitekey, url }, { timeout: 180000 });

180 Sekunden sind eine sinnvolle Obergrenze. Das ist etwa das Zehnfache einer normalen Lösung und liegt bewusst unter dem reCAPTCHA-Polling-Limit von 300 Sekunden im SDK, sodass Cypress einen wirklich festhängenden Test scheitern lässt, statt hinter einem Client zu hängen, der noch wartet. Soll lieber der Client zuerst aufgeben, senken Sie recaptchaTimeout auf einen Wert unterhalb Ihres Task-Timeouts.

Oder das SDK weglassen und cy.request nutzen

Die API ist 2captcha-kompatibel, zwei Aufrufe erledigen also die ganze Arbeit. Da cy.request in Node läuft, spielen die Origin-Regeln des Browsers hier überhaupt keine Rolle.

// No task registration needed. Both calls happen in Node.
function pollForToken(id, tries = 20) {
  return cy.request({
    method: 'POST',
    url: 'http://127.0.0.1:8080/res.php',
    form: true,
    body: { key: 'capskip', action: 'get', id },
  }).then((res) => {
    const text = res.body.trim();
    if (text !== 'CAPCHA_NOT_READY') return text.replace('OK|', '');
    if (tries === 0) throw new Error('gave up waiting for ' + id);
    return cy.wait(5000).then(() => pollForToken(id, tries - 1));
  });
}

Zwei Details, die Sie auseinanderhalten sollten. Die Klartext-Antwort von res.php lautet OK|TOKEN bei Erfolg und schlicht CAPCHA_NOT_READY, solange der Job noch läuft, was ein Status ist und kein Fehler. Und ein Ergebnis lässt sich nur einmal lesen, speichern Sie es also in dem Moment, in dem es eintrifft, statt zweimal zu fragen. Jeder Parameter und jede Fehlermeldung steht in der API-Dokumentation.

Die Tests in CI ausführen, während der Löser bleibt, wo er ist

Hier scheitert eine Suite, die auf Ihrem Laptop durchläuft, beim ersten Push. Ein GitHub-Actions-Runner, ein GitLab-Job oder ein Jenkins-Agent hat seine eigene Loopback-Adresse, und dort lauscht nichts auf Port 8080. Local mode ist per Definition auf die jeweilige Maschine beschränkt.

Server mode ist die Antwort. CapSkip lauscht dann auf Ihrem Netzwerk oder Ihrer öffentlichen IP statt auf Loopback, und die Konfiguration liest die Adresse aus der Umgebung.

// npm install --save-dev capskip
const { CapSkip } = require('capskip');

// Same client, different address. The spec never changes.
const solver = new CapSkip({
  host: process.env.CAPSKIP_HOST || '127.0.0.1',
  port: Number(process.env.CAPSKIP_PORT || 8080),
});

Das SDK liest CAPSKIP_HOST und CAPSKIP_PORT von selbst, der Fallback oben ist also eine doppelte Absicherung für einen Runner, der ohne sie startet. Für den Rechner mit dem Löser wird eine statische öffentliche IP empfohlen, und die Schritte dazu finden Sie in den Verbindungseinstellungen. Es bleibt Ihre Hardware und bleibt ohne Verbrauchsabrechnung: Geändert hat sich nur, wo der Prozess lauscht.

Häufige Fehler und was sie bedeuten

SymptomUrsacheBeheben
Der Task solveRecaptcha wurde nicht registriertIm falschen Block registriert, oder die Konfiguration wurde nie exportiertInnerhalb von setupNodeEvents unter dem Schlüssel e2e registrieren
Zeitüberschreitung nach 60000 ms Warten auf Ihren TasktaskTimeout steht noch auf dem StandardwertErhöhen Sie es auf 180000, global oder pro Aufruf
Der Task hat undefined zurückgegebenDer Handler hat kein returnGeben Sie das Token zurück, oder null, wenn es nichts gibt
Das Element ist nicht sichtbar, deshalb kann Cypress nicht tippenDas Antwortfeld ist eine versteckte textareaSchreiben Sie den Wert stattdessen über cy.document
ERROR_GOOGLEKEYDer Sitekey war leer, oder er gehört zu einem Turnstile-WidgetLoggen Sie das Attribut vor dem Lösen; Turnstile hat eine eigene Methode
NetworkException bei jedem TestAn diesem Host und Port lauscht nichtsLocal mode ist nur Loopback; nutzen Sie aus CI heraus Server mode
Lokal grün, in CI rotDer Runner erreicht das Loopback Ihrer Maschine nichtRichten Sie CAPSKIP_HOST auf eine erreichbare Adresse

Häufig gestellte Fragen

Sollte ich das Captcha in meiner Testumgebung nicht einfach abschalten?

Wenn das Widget Ihnen gehört, ja. Ein Feature-Flag oder ein Test-Sitekey im Staging-Build ist billiger und schneller als Lösen und hält die Suite deterministisch. Lösen verdient sich seinen Platz in drei Fällen: Das Captcha gehört jemand anderem, Staging muss die Produktion exakt spiegeln, oder das Getestete ist der Challenge-Pfad selbst. Die Captcha-Demoseiten sind für den dritten Fall nützlich, weil Sie eine Spec auf ein Widget richten können, das sich wie das echte verhält.

Funktioniert das im Component Testing von Cypress?

Nicht sinnvoll. Ein Component-Test mountet eine Komponente ohne echte Seite und ohne Server dahinter, ein Token hat also nichts, wogegen es verifiziert werden könnte. Registrieren Sie den Task unter dem Schlüssel component, wenn Sie ihn verfügbar haben wollen, aber halten Sie Challenge-Arbeit in End-to-End-Specs, wo es eine echte Anfrage zum Absenden gibt.

Erreicht ein Lauf, der in Cypress Cloud aufgezeichnet wird, den Löser?

Cypress Cloud zeichnet Ergebnisse auf, es führt Ihre Tests nicht aus. Die Frage betrifft also in Wahrheit die Maschine, die den Browser ausführt. Auf Ihrem Laptop ist das Local mode. Auf einem gehosteten Runner braucht es Server mode und eine Route zur Adresse des Lösers, und die Aufzeichnung funktioniert in beiden Fällen gleich.

Wie viele Lösungen kann ein paralleler Cypress-Lauf gleichzeitig anstoßen?

So viele, wie Sie Spec-Dateien laufen lassen. Jeder Cypress-Prozess hält seinen eigenen Client und wartet auf seinen eigenen Task, auf der Client-Seite gibt es also nichts zu konfigurieren. Das SDK beginnt das Polling bei 250 Millisekunden und fährt bis zur Obergrenze pollingInterval zurück, was eine schnelle Lösung auch dann schnell hält, wenn mehrere gleichzeitig unterwegs sind. Da die Arbeit auf Hardware läuft, die Ihnen gehört, ist das Hinzufügen von Maschinen eine Kapazitätsentscheidung und keine Abrechnungsfrage.

Die Kurzfassung

Legen Sie den Client in cypress.config.js, stellen Sie ihn als Task bereit, erhöhen Sie taskTimeout auf 180000 und schreiben Sie das Token über cy.document in das versteckte Feld. Das ist die gesamte Integration, und was daran bricht, sind die beiden Cypress-Regeln darunter: Spec-Code ist Browser-Code, und ein Task gibt etwas zurück oder er scheitert.

Dass Sie den Löser selbst betreiben, macht es vertretbar, eine wacklige Challenge einfach noch einmal zu versuchen, statt sie einzuplanen, und dieses Argument gilt für jeden Captcha-Löser, egal ob in einer Testsuite oder in der Produktion. Ausführlich beschrieben sind die Client-Optionen im Node.js-Integrationsleitfaden, und die browserseitigen Details, die Cypress mit jedem anderen Runner teilt, behandelt der Playwright-Leitfaden.