Testing CAPTCHA Flows on Your Own Site With Playwright (2026)
Sign-up, login and contact forms usually sit behind a CAPTCHA, and those forms are also the ones you most want covered by automated tests. The two goals collide: a test suite that clicks through a form will run into the widget, and a flaky CAPTCHA step makes the whole suite flaky.
The fix is not to fight the widget. Every major provider publishes official test keys and test modes for exactly this situation. This tutorial shows how to use them in a test environment of your own, with Playwright connected to a JustBrowser profile.
What we're building
A small end-to-end test setup for a form on a site you own or are responsible for testing. It covers reCAPTCHA, hCaptcha and Cloudflare Turnstile, the three most common widgets. It works only on your own properties, staging environments and QA setups, because it depends on configuring the keys yourself. For any site you don't control, the right route is the owner's API or permission, and nothing here applies.
Prerequisites
- JustBrowser ($9.99/month or $99.99/year, with unlimited profiles and local REST API access; the 7-day free trial, card required, includes the API if you're just following along)
- A staging or test environment of your own site where you can set the CAPTCHA keys
- Node.js 18+ with Playwright installed
- Basic familiarity with async/await and REST APIs
Step 1: Understand what a CAPTCHA does in production
Older CAPTCHAs were simple puzzles. Current ones, such as reCAPTCHA v3, hCaptcha Enterprise and Cloudflare Turnstile, are risk-scoring systems that sometimes show a visual challenge. In production they weigh signals about the browser, the network and the interaction, and they return a token or score that your server verifies with a secret key.
That has a direct consequence for testing. A production key gives you a real risk decision, which varies by environment, network and time. A test suite needs a deterministic answer. So tests should not run against production keys at all.
Step 2: Use the provider's test keys
Each provider documents test keys that behave predictably. The values below are the ones each provider documents at the time of writing; check the linked page for the current list before relying on them.
reCAPTCHA (v2 checkbox). Google publishes a test site key and secret key pair in its reCAPTCHA FAQ. With them, the widget shows a warning that it is not for production traffic, and every verification succeeds. They work for v2 only; for v3, Google suggests creating a separate key for your test environment.
Site key: 6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI
Secret key: 6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe
hCaptcha. hCaptcha's integration testing docs list a test site key that never issues a challenge and always passes, and a test secret key. Enterprise accounts get two more test site keys, one labelled safe end user and one labelled bot detected.
Site key: 10000000-ffff-ffff-ffff-000000000001
Secret key: 0x0000000000000000000000000000000000000000
Cloudflare Turnstile. Cloudflare's testing page documents dummy site keys for always-pass, always-fail and forced-interactive behavior (plus invisible-widget variants), and separate dummy secret keys for server verification.
Site key, always passes: 1x00000000000000000000AA
Site key, always fails: 2x00000000000000000000AB
Site key, forced interactive: 3x00000000000000000000FF
Secret key, always passes: 1x0000000000000000000000000000000AA
Secret key, always fails: 2x0000000000000000000000000000000AA
Secret key, token already spent: 3x0000000000000000000000000000000AA
Set these in your staging environment's configuration, in both the front end (site key) and the back end (secret key). Keep them out of production, and make sure your deployment pipeline cannot copy them there.
Step 3: Launch a profile and connect Playwright
Start a profile through JustBrowser's local REST API. Each profile is its own browser, with its own cookies, storage, proxy and identity parameters, so a test account's session stays separate from your everyday browsing and from other test profiles, and a teammate can be given the same profile to reproduce a failure.
const JUSTBROWSER_API = 'http://127.0.0.1:36542/api/v1';
const TOKEN = process.env.JUSTBROWSER_TOKEN; // from the app's API/AI tab
const PROFILE_ID = 'your-test-profile-id';
async function launchProfile() {
const response = await fetch(`${JUSTBROWSER_API}/profiles/${PROFILE_ID}/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${TOKEN}` },
body: JSON.stringify({ headless: false })
});
const { data } = await response.json();
return data.cdp_url;
}
The API starts profiles headless unless the body says {"headless": false}. Now connect plain Playwright:
import { chromium } from 'playwright';
const cdpUrl = await launchProfile();
const browser = await chromium.connectOverCDP(cdpUrl);
const context = browser.contexts()[0];
const page = context.pages()[0] ?? await context.newPage();
Use the profile's own context (contexts()[0]), never browser.newContext(), which opens an incognito context outside the profile. browser.close() only disconnects; end the session with POST /profiles/{id}/stop. The local API binds to 127.0.0.1 and team seats have no API access, so run this on the machine where the profile lives.
Step 4: Test the happy path with an always-pass key
With the always-pass keys in place, the test is an ordinary form test. For Turnstile, the dummy widget resolves on its own, so the test just waits for the response field to fill.
async function testSignupWithTurnstile(page, baseUrl) {
await page.goto(`${baseUrl}/signup`);
await page.fill('#email', '[email protected]');
await page.fill('#password', 'a-test-password');
// With the dummy always-pass key the widget resolves by itself
await page.waitForFunction(() => {
const input = document.querySelector('[name="cf-turnstile-response"]');
return input && input.value.length > 0;
});
await page.click('button[type="submit"]');
await page.waitForURL(/welcome/);
}
For the reCAPTCHA v2 test key, click the checkbox inside its iframe, which Playwright can address through a frame locator:
async function testContactWithRecaptcha(page, baseUrl) {
await page.goto(`${baseUrl}/contact`);
await page.fill('#message', 'Test message');
const frame = page.frameLocator('iframe[title*="reCAPTCHA"]');
await frame.locator('#recaptcha-anchor').click();
await page.click('button[type="submit"]');
await page.waitForSelector('.success-message');
}
hCaptcha follows the same shape: with its test key the checkbox inside the hCaptcha iframe passes immediately.
Step 5: Test the failure path
The unhappy path is where CAPTCHA bugs usually hide. Switch the staging configuration to Turnstile's always-fail site key (2x00000000000000000000AB) and assert that your form handles the rejection:
async function testSignupRejectedWhenCaptchaFails(page, baseUrl) {
await page.goto(`${baseUrl}/signup`);
await page.fill('#email', '[email protected]');
await page.fill('#password', 'a-test-password');
await page.click('button[type="submit"]');
await page.waitForSelector('.captcha-error');
// The account must not have been created
await page.waitForURL(/signup/);
}
Things worth asserting here: the error message is shown and is understandable, the form keeps what the user typed, the widget can be retried, and the server rejects the request even if someone posts directly to the endpoint without a token. That last check is the one that catches a front-end-only CAPTCHA.
Also test the forced-interactive Turnstile key to confirm your layout handles a visible challenge without clipping or covering the submit button.
Step 6: Cover server-side verification
Your back end should verify every response token with the provider's verification endpoint, using the secret key. Test it directly, without a browser, with a small set of cases:
- A valid token from the test environment is accepted.
- A missing token is rejected.
- A malformed or reused token is rejected (Turnstile's "token already spent" secret key reproduces the reused case).
- A provider outage or timeout is handled, with a clear failure mode you chose on purpose (fail closed for sign-up, perhaps fail open with logging for a contact form).
Token lifetime matters too: tokens are short-lived, and a form left open for a few minutes can submit an expired one. Check how your form behaves when verification reports an expired or duplicate token, and confirm the user can recover by refreshing the widget.
Step 7: Do one real pass before release
Test keys skip the risk scoring, so they cannot tell you how real visitors experience your production setup. Before a release that changes a CAPTCHA configuration:
- Point a staging environment at production-type keys for the provider.
- Do a manual pass in a few browsers and from a couple of regions, using separate profiles so each pass starts with clean cookies and storage.
- Look at the provider's dashboard for challenge rates and score distributions, and adjust your thresholds there.
- Make sure users who fail have a way forward, such as a support contact or an accessible alternative.
If real visitors are being challenged too often, the fix is on your side of the table: thresholds, the provider's settings, and what you gate behind the widget.
Common problems and fixes
The widget doesn't render in tests. Confirm the staging front end is actually using the test site key, and that your Content Security Policy allows the provider's script and frame sources.
Tests pass locally but fail in CI. The CI environment may be using production keys, or a different configuration file. Print which site key the page loaded as the first step of a failing run.
Verification fails with a test key. The site key and secret key must come from the same provider and the same test pair. Mixing a test site key with a production secret fails verification.
The form submits before the widget finishes. Wait for the response field to be populated, as in Step 4, rather than sleeping for a fixed time.
Frequently Asked Questions
How do I test a CAPTCHA-protected form in automated tests?
Use the provider's official test keys or test mode in your staging or test environment. reCAPTCHA, hCaptcha and Cloudflare Turnstile each publish test site keys and matching secret keys that always pass (and, for Turnstile, ones that always fail or force an interactive challenge). Your front end renders the real widget and your back end verifies the real response format, but no live scoring is involved, so the test is deterministic.
Why does my form pass in tests but fail for real visitors?
Test keys skip the risk scoring that production keys apply. In production, reCAPTCHA v3, hCaptcha and Turnstile score the session using signals such as the device, network and interaction. A form that passes with test keys can still challenge real visitors, so also run a manual pass with production keys in a staging environment, and tune your thresholds there.
Should the CAPTCHA be switched off in the test environment instead?
Test keys are usually better than removing the widget. With test keys the same code path runs as in production: the widget loads, a response token is produced and your server verifies it. Disabling the CAPTCHA entirely leaves that path untested. Keep one test that asserts a failed verification is handled, too.
Does this work on sites I do not own?
No. This approach only works on sites you control, because it depends on configuring your own keys. A CAPTCHA on someone else's site is there to stop automated access, and getting around it generally breaks that site's terms. If you need automated access to a site you don't own, ask the owner for an API or an allowance.
Next steps
Start with one profile, one form and one CAPTCHA type. Get the happy path, the failure path and the server-side checks working end to end before you add more forms. Then add the real-keys manual pass to your release checklist.
If you measure conversions on the forms you test, JustAnalytics offers cookieless analytics. For the surrounding setup, our WebGL fingerprinting guide, audio, font and hardware fingerprinting guide and ISP proxies setup tutorial cover profile and proxy details. For deeper automation patterns, see the Playwright and Puppeteer integration guide; the JA4 TLS fingerprinting deep-dive explains what a site can read at the network layer, and the BrowserScan, IPHey and Pixelscan guide explains what each browser check measures; and the multi-account guide for affiliates covers profile separation for ad networks.
Try JustBrowser
A multi-account browser for teams, on a custom Chromium engine with native C++ integration, not extension-based, and always current with upstream Chromium. Real C++ engine patches at the canvas / WebGL / audio / font / screen layer, so 40+ identity parameters are genuine, not faked. Local REST API and CDP for Playwright, Puppeteer and Selenium. Windows x64 and macOS (Apple Silicon). $9.99/month or $99.99/year. 7-day free trial, card required — cancel any time in the seven days and you are not charged. Unlimited profiles, unlimited cloud sync, unlimited team seats.
Get started → · How it differs from Multilogin / GoLogin / AdsPower
Ready to manage multiple accounts?
Seven days free, then $9.99/month — one plan, everything included.