Skip to main content

A CAPTCHA exists to stop automation, so a test that has to get past a real one is fighting its own application. The three big vendors publish test keys for this: with them the widget gives a known answer and the vendor’s server accepts (or refuses) the result on purpose. You run the application under test with those keys, and the browser test clicks the test widget, checks that the token field was filled, and submits.

Warning

This page is about your own application. It is not about getting through real CAPTCHAs on sites you do not own: the keys below only work on a page whose site key is one of them, and solve_captcha is a click helper, not a solver. Whether you may use it on a site you do not control is a question for that site’s terms, and it is out of scope here.

Info

What you will learn

  • Which test keys Google reCAPTCHA, hCaptcha and Cloudflare Turnstile document, and where.
  • How to give the test deployment of your app those keys.
  • How to click the widget with BaseCase (WebDriver) and with sb_cdp (pure CDP), read the token, and submit.
  • How to test the failure paths, and what to do about reCAPTCHA v3.

How the pieces fit#

  1. Your page embeds the vendor’s widget with a site key.
  2. When the widget is satisfied it writes a token into a hidden field of your form.
  3. Your server sends the token and its secret key to the vendor’s verification endpoint (siteverify) and acts on the answer.

Who talks to whom.

With the vendor’s test keys, step 1 hands out a token without a real risk check and step 3 accepts it (or refuses it, for the keys made to fail). Only the keys differ from production, so the code under test is the code you ship.

Configure the app under test#

Keep the keys in the environment, as for any other deploy-specific setting, and set the test keys only in the test deployment:

.env.test
  • Use a site key and secret of the same set. Cloudflare’s test secrets accept only the dummy token and production secrets reject it, so a dummy site key next to a real secret fails.
  • Make the siteverify URL a setting too. Then a test can point the server at a stub, which is the way to cover reCAPTCHA v3 and any answer a test key cannot give. The same setting can switch the check off: pointed at a stub, or at any server that always answers success: true, it makes your server accept every token. Outside the test deployment, have the application refuse to start unless the URL is the vendor’s own siteverify URL (listed below); production keys are real, so the test-key check in the next point does not catch a wrong URL.
  • Test keys protect nothing, and the secrets are public. Have the application refuse to start with a known test key when it runs in production.

The test keys#

Every value below was compared character by character with the vendor’s page on 2026-10-15 and each page can change, so check it again before you rely on it.

Source: reCAPTCHA FAQ, “I’d like to run automated tests with reCAPTCHA”.

Value
Site key6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI
Secret key6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe

Google says that with these keys the user never gets a challenge and every verification request passes, and that the widget shows a warning so the keys are not used for production traffic. The page is marked deprecated and points to Google Cloud’s documentation, but the keys still worked when checked.

  • Markup and token field: the script is https://www.google.com/recaptcha/api.js, the widget is <div class="g-recaptcha" data-sitekey="...">, and the token is the form field g-recaptcha-response (display docs).
  • Verification: POST https://www.google.com/recaptcha/api/siteverify with secret and response (verify docs). With the test secret, any response string returned success: true (hostname testkey.google.com).
  • What a browser showed: the usual “I’m not a robot” checkbox with the warning text; one click ticked it with no image challenge and filled a token of about 1,700 characters into textarea[name="g-recaptcha-response"]. That textarea is in your page but hidden, so read its value; do not wait for it to be visible. The widget’s frame has the title reCAPTCHA and a src starting with https://www.google.com/recaptcha/api2/anchor.

What siteverify answered#

These calls were made on 2026-10-15 with the keys above, to check that the secrets behave as documented:

CallAnswer
Turnstile, secret 1x...AA, dummy tokensuccess: true
Turnstile, secret 2x...AA, dummy tokensuccess: false, invalid-input-response
Turnstile, secret 3x...AA, dummy tokensuccess: false, timeout-or-duplicate
hCaptcha, Publisher or Pro secret and tokensuccess: true
reCAPTCHA, test secret, any responsesuccess: true

Your own server tests can use the same pairs: they cover its handling of a rejected and of an already-used token without opening a browser.

Click the widget and read the token#

The test has the same four steps for every vendor:

Wait for the widget

Wait until the widget has drawn.

Click the widget

Click it.

Wait for the token

Wait until the token field is non-empty. If it stays empty for a few seconds, the click probably came before the widget was ready: click again.

Submit

Submit the form and assert what your application does.

VendorWidget, as the page sees itToken field
reCAPTCHA v2iframe[src*="recaptcha/api2/anchor"]textarea[name="g-recaptcha-response"]
hCaptchaiframe[src*="hcaptcha.com"][src*="checkbox"]textarea[name="h-captcha-response"]
Turnstile.cf-turnstileinput[name="cf-turnstile-response"]

The first two widget selectors are the ones Page::solve_captcha itself uses.

To try the code without your own application, save this HTML as recaptcha.html and serve its folder (python3 -m http.server 8765 --bind 127.0.0.1). For hCaptcha or Turnstile swap the script, the div and the site key, and save each as a file of its own (hcaptcha.html, turnstile.html): one vendor per page. Its form has no server behind it, so the token is what a test can check:

What solve_captcha does#

Page::solve_captcha() (and BaseCase::solve_captcha(), which does the same over chromedriver) looks for the first visible widget among Cloudflare Turnstile, reCAPTCHA, hCaptcha, Friendly Captcha and DataDome, in that order, scrolls it to the middle of the window, and presses 28 pixels in from its left edge at half its height with trusted mouse events (the DataDome slider is pressed 30 pixels in and dragged to 30 pixels from the right edge). The widgets live in cross-origin frames whose insides it cannot inspect, so it does not know whether the click was accepted. It returns Some(Captcha::...) for the widget it pressed, None when no supported widget is visible yet, and an error when the script that looks for the widget fails or the browser refuses the input. The script can fail while the page is still navigating (“Execution context was destroyed”), so such an error need not end a test that has just submitted a form or followed a link: wait for the page to load and call it again. Because None is what you get before the widget has drawn, wait for the widget first, and check the token afterwards.

With the gui feature, Page::gui_click_captcha() makes the same press with the machine’s own pointer; test widgets do not need it (see Pure CDP).

With the pure CDP engine#

The same shape works for the other two vendors, with their frame and field from the table. Give each vendor a page of its own: solve_captcha presses the first widget it finds, and it looks for Turnstile before reCAPTCHA and hCaptcha, so on a page with Turnstile and hCaptcha it would press Turnstile even when the test meant hCaptcha. A test key gives a known token, so the test can assert it:

A Turnstile key that always passes (1x...AA, 1x...BB) needs no click: skip press_until_token and wait for the token field with wait_for_function.

With WebDriver (BaseCase)#

BaseCase has no wait for a field’s value, so a small helper does it. The field is hidden, and get_value reads the live value property of an element whether or not it is visible:

Google does not document the markup inside its frame. #recaptcha-anchor is the checkbox’s id as test suites have long used it, but it was not checked here, so look at it in DevTools before you depend on it. To avoid depending on it at all, replace step 2 with solve_captcha, which presses by position and never enters the frame:

As in the pure CDP helper above, a press can come before the checkbox has drawn and be lost: give wait_for_token a few seconds, and press again when it times out.

For hCaptcha and Turnstile use solve_captcha too: Turnstile has no frame to enter, and hCaptcha’s inner markup was not checked here.

Failure paths#

A test key that fails is how you cover your error handling without a flaky real challenge:

  • No token. With Turnstile 2x00000000000000000000AB the widget never produces a token. Assert that your form refuses to submit, or that your server rejects the empty token and shows your error message.
  • Rejected token. Send the dummy token to siteverify with the secret 2x0000000000000000000000000000000AA (answer invalid-input-response), or with 3x0000000000000000000000000000000AA (answer timeout-or-duplicate), to test how your server reports a refused and a used token.
  • Bot detected. hCaptcha’s Enterprise “bot detected” pair stands for a detected bot, for accounts that have Enterprise (it was not run here).
  • reCAPTCHA documents no failing key. Stub siteverify (see above) to make the answer fail.

In CI#

  • The widgets load their scripts and frames from the vendors’ hosts. A runner without network access to them, or a Content Security Policy (CSP) on your page that blocks them, shows no widget, and solve_captcha returns None. Take the hosts from each vendor’s CSP guidance (below, as read on 2026-10-15), not from a list of what one page load fetched: reCAPTCHA’s api.js adds a second script from www.gstatic.com to your page, and hCaptcha’s asset subdomains change over time and by region.

    Vendorscript-srcframe-srcOther directives
    reCAPTCHA (FAQ, the CSP question)https://www.google.com/recaptcha/, https://www.gstatic.com/recaptcha/https://www.google.com/recaptcha/, https://recaptcha.google.com/recaptcha/connect-src: https://www.google.com/recaptcha/
    hCaptcha (CSP settings)https://hcaptcha.com, https://*.hcaptcha.comthe samestyle-src and connect-src: the same
    Turnstile (Content Security Policy)https://challenges.cloudflare.comthe samenone

    Google and Cloudflare prefer a nonce on the api.js script tag to a host list; the widget passes the nonce on to the scripts it adds. A CI runner that limits outbound traffic needs the same hosts.

  • Treat a token as single-use: Turnstile documents that one works once and expires after 300 seconds. Start every test from a fresh page load instead of reusing a token.

  • Run Chrome headless in CI. The three widgets were clicked successfully by solve_captcha in a headless Chrome (see “What has been checked”).

Troubleshooting#

solve_captcha returns None

Likely cause. The widget has not drawn yet, its script was blocked, or it is hidden.

Fix. Wait for the frame or container first; allow the hosts listed under In CI in your CSP and on the CI runner.

solve_captcha returns Some(..) but the token stays empty

Likely cause. The wrong site key (not a test key), or the click came before the widget was ready.

Fix. Press again after a few seconds, as press_until_token does; check the key in the page source; take a screenshot (save_screenshot) to see the widget.

The widget says the site key is invalid

Likely cause. A production key on a test host, or a key from another vendor.

Fix. Use the test key of the vendor whose script the page loads.

An hCaptcha widget appears where a reCAPTCHA one should

Likely cause. Both scripts on one page.

Fix. One vendor per page, or recaptchacompat=off on the hCaptcha script.

siteverify refuses the dummy token

Likely cause. A production secret with a test site key.

Fix. Use the secret of the same test set.

wait_for_function fails at once with a script error

Likely cause. The field does not exist yet and the expression read .value of null.

Fix. Use ?.value, as above.

Security notes#

Warning
  • Test keys give no protection and their secrets are public. Never deploy them, and make the application fail at start-up when it finds one in production.
  • The same goes for a siteverify URL that is not the vendor’s: in production it would accept whatever the stub accepts. Fail at start-up on that too.
  • Keep production keys out of test environments and out of the repository.
  • Do not point these tests at a site you do not own or have permission to test.

What has been checked#

Check

On 2026-10-15, on macOS:

  • Every key, the markup names and the CSP hosts above against the vendors’ pages (linked in each section), and the siteverify answers in the table above with real calls. reCAPTCHA’s api.js, fetched the same day, adds its second script from https://www.gstatic.com/recaptcha/releases/.
  • A real Chrome, clicked by hand, on a local fixture: the reCAPTCHA v2 test key, the hCaptcha Publisher or Pro key and all five Turnstile site keys behaved as described, including the tokens.
  • This crate’s own Page::solve_captcha, through sbrs-mcp --server cdp (0.3.0, pure CDP, headless Chrome) on the same fixture: it attempted reCAPTCHA, hCaptcha and Turnstile (3x...FF), and each token field was filled (the hCaptcha and Turnstile tokens equal the documented ones).
  • The four Rust listings on this page type-check against this crate (rust-analyzer; tutorials are not compiled by cargo).
Note

Not checked: none of the listings was run; BaseCase::solve_captcha, the frame-switching listing and #recaptcha-anchor were not tried on a live widget; the hCaptcha Enterprise key sets were not exercised in a browser; recaptchacompat=off was not tried; and nothing was tried through the crate in a headed browser.

See also#