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.
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.
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 withsb_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#
- Your page embeds the vendor’s widget with a site key.
- When the widget is satisfied it writes a token into a hidden field of your form.
- 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:
- 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
siteverifyURL 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 answerssuccess: 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 ownsiteverifyURL (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 key | 6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI |
| Secret key | 6LeIxAcTAAAAAGG-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 fieldg-recaptcha-response(display docs). - Verification:
POST https://www.google.com/recaptcha/api/siteverifywithsecretandresponse(verify docs). With the test secret, anyresponsestring returnedsuccess: true(hostnametestkey.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 titlereCAPTCHAand asrcstarting withhttps://www.google.com/recaptcha/api2/anchor.
Google documents no test keys for v3 or for Enterprise. The FAQ above advises a separate key for testing environments and warns that v3 scores may be inaccurate because v3 relies on seeing real traffic.
v3 has no checkbox, so there is nothing to click. What works in practice (this is advice, not something the vendor documents):
- Use a separate v3 key registered for the test domain, as Google advises.
- Make the score threshold of your server a setting and lower it in the test
deployment, or point
siteverifyat a stub that answers with the score your test needs. - Drive the action that makes the page ask for a token, then assert on what your server does with it (the page after submit), not on a token field.
Source: hCaptcha, integration testing and test keys.
| Key set | Site key | Secret key | Response token |
|---|---|---|---|
| Publisher or Pro account | 10000000-ffff-ffff-ffff-000000000001 | 0x0000000000000000000000000000000000000000 | 10000000-aaaa-bbbb-cccc-000000000001 |
| Enterprise, safe end user | 20000000-ffff-ffff-ffff-000000000002 | 0x0000000000000000000000000000000000000000 | 20000000-aaaa-bbbb-cccc-000000000002 |
| Enterprise, bot detected | 30000000-ffff-ffff-ffff-000000000003 | 0x0000000000000000000000000000000000000000 | 30000000-aaaa-bbbb-cccc-000000000003 |
The Publisher or Pro pair never challenges and always yields the same response
token, which verifies as success: true. The two Enterprise pairs stand for a
safe user and a detected bot; hCaptcha asks you to send remoteip to siteverify
so every Enterprise field comes back.
- Markup and token field: the script is
https://js.hcaptcha.com/1/api.js, the widget is<div class="h-captcha" data-sitekey="...">, and the token is the form fieldh-captcha-response. - Verification: a form-encoded
POST https://api.hcaptcha.com/siteverifywithsecret,response, and optionallyremoteipandsitekey. The Publisher or Pro secret and token returnedsuccess: true(hostnamedummy-key-pass). - What a browser showed (Publisher or Pro key): a checkbox, the notice “This
hCaptcha is for testing only”, and no image challenge. One click put the
documented token into
textarea[name="h-captcha-response"]; a second textarea,g-recaptcha-response, got the same token (hCaptcha’s reCAPTCHA compatibility mode, on by default; the docs do not promise that second field, so readh-captcha-response). - Do not load reCAPTCHA and hCaptcha on one page. hCaptcha’s compatibility hook
claims
window.grecaptcha, and a.g-recaptchadiv on such a page was drawn as an hCaptcha widget with a “sitekey is incorrect” notice. hCaptcha documents the script optionrecaptchacompat=offfor this (configuration); it was not tried.
Source: Turnstile, test your Turnstile implementation.
| Site key | Behaviour | Widget |
|---|---|---|
1x00000000000000000000AA | always passes | visible |
2x00000000000000000000AB | always fails | visible |
1x00000000000000000000BB | always passes | invisible |
2x00000000000000000000BB | always fails | invisible |
3x00000000000000000000FF | forces an interactive challenge | visible |
| Secret key | Behaviour |
|---|---|
1x0000000000000000000000000000000AA | always passes validation |
2x0000000000000000000000000000000AA | always fails validation |
3x0000000000000000000000000000000AA | fails with timeout-or-duplicate (as if the token were spent) |
The test site keys produce the dummy token XXXX.DUMMY.TOKEN.XXXX. Cloudflare
says a test secret accepts only that token and rejects real ones, a production
secret does the reverse, and the test keys work on any domain including
localhost.
- Markup and token field: the script is
https://challenges.cloudflare.com/turnstile/v0/api.js, the widget is<div class="cf-turnstile" data-sitekey="...">, and the token is the hidden form fieldcf-turnstile-response(client-side rendering). - Verification:
POST https://challenges.cloudflare.com/turnstile/v0/siteverifywithsecretandresponse, and optionallyremoteipandidempotency_key. A token is valid for 300 seconds and works once (server-side validation). - What a browser showed:
1x...AAand1x...BBfilled the dummy token with no click; both2xkeys leftcf-turnstile-responseempty;3x...FFshowed a “Verify you are human” checkbox and filled the dummy token after one click. - The Turnstile frame is not an
iframethat page script can find (none showed up indocument.querySelectorAll("iframe")while the widget was on screen), so there is no frame to switch into. The.cf-turnstilecontainer is what a test can see.
What siteverify answered#
These calls were made on 2026-10-15 with the keys above, to check that the secrets behave as documented:
| Call | Answer |
|---|---|
Turnstile, secret 1x...AA, dummy token | success: true |
Turnstile, secret 2x...AA, dummy token | success: false, invalid-input-response |
Turnstile, secret 3x...AA, dummy token | success: false, timeout-or-duplicate |
| hCaptcha, Publisher or Pro secret and token | success: true |
reCAPTCHA, test secret, any response | success: 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.
| Vendor | Widget, as the page sees it | Token field |
|---|---|---|
| reCAPTCHA v2 | iframe[src*="recaptcha/api2/anchor"] | textarea[name="g-recaptcha-response"] |
| hCaptcha | iframe[src*="hcaptcha.com"][src*="checkbox"] | textarea[name="h-captcha-response"] |
| Turnstile | .cf-turnstile | input[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
2x00000000000000000000ABthe 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
siteverifywith the secret2x0000000000000000000000000000000AA(answerinvalid-input-response), or with3x0000000000000000000000000000000AA(answertimeout-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_captchareturnsNone. 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’sapi.jsadds a second script fromwww.gstatic.comto your page, and hCaptcha’s asset subdomains change over time and by region.Vendor script-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 same style-srcandconnect-src: the sameTurnstile (Content Security Policy) https://challenges.cloudflare.comthe same none Google and Cloudflare prefer a nonce on the
api.jsscript 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_captchain 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#
- 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
siteverifyURL 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#
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
siteverifyanswers in the table above with real calls. reCAPTCHA’sapi.js, fetched the same day, adds its second script fromhttps://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, throughsbrs-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).
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.