Skip to content

Tutorial

Cypress and Cloudflare Turnstile: Test Your Own Forms End to End

Why Cypress can't pass a live Cloudflare Turnstile widget, and how to test your forms with Cloudflare's testing sitekeys and secrets, per environment, in CI.

By 5 min readPublished

Cypress cannot pass a live Cloudflare Turnstile widget: the widget runs in a cross-origin iframe that Cypress cannot automate, and Cloudflare says “Automated testing suites (like Selenium, Cypress, or Playwright) are detected as bots by Turnstile”. To test your own forms end to end, serve them with Cloudflare’s testing sitekey 1x00000000000000000000AA and validate with the testing secret 1x0000000000000000000000000000000AA. The widget then passes on its own, puts the dummy token XXXX.DUMMY.TOKEN.XXXX in the cf-turnstile-response field, and your server’s siteverify call accepts it. Your Cypress test waits for that value, then submits.

This tutorial is for forms on sites you own. It shows how to pick keys per environment, the Cypress specs, the failure cases worth testing, and the check that keeps testing keys out of production.

Why the widget can’t be clicked from Cypress

Two documented limits meet here:

  • Cross-origin iframes. Cypress’s own guide says: “If your site embeds an <iframe> that is a cross-origin frame, Cypress won’t be able to automate or communicate with this <iframe>.” The Cloudflare Turnstile widget is an iframe served from challenges.cloudflare.com.
  • Bot detection. Cloudflare’s testing page lists what happens when a real widget meets a test runner: “Tests to fail when Turnstile blocks automated browsers”, “Unpredictable test results due to challenge variations”, “Interference with form submission testing” and “Difficulty testing complete user flows”.

Setting chromeWebSecurity: false lets Cypress reach cross-origin iframes in Chrome-based browsers, but it does not make a browser under test look like a person: the second limit still applies. The testing keys are the way through.

Cloudflare’s testing keys

On the page (the sitekey):

Sitekey Behaviour Widget
1x00000000000000000000AA Always passes Visible
2x00000000000000000000AB Always fails Visible
1x00000000000000000000BB Always passes Invisible
2x00000000000000000000BB Always fails Invisible
3x00000000000000000000FF Forces an interactive challenge Visible

On the server (the secret key used with siteverify):

Secret key Behaviour
1x0000000000000000000000000000000AA Always passes validation
2x0000000000000000000000000000000AA Always fails validation
3x0000000000000000000000000000000AA Returns a “token already spent” error

Two rules come with them. “Production secret keys will reject the dummy token. You must also use a dummy secret key for testing purposes.” And the testing keys work on localhost, while Cloudflare recommends “that sitekeys used in production do not allow local domains”. Cloudflare’s testing keys cost nothing to use, and nothing is sent to a solving service.

Pick the keys by environment

The simplest setup is a separate environment: your CI or staging deployment reads its sitekey and secret from its own settings, set to the testing keys, and production reads the real ones.

// server/turnstile-keys.mjs: one place that decides which keys this deployment uses.
export const turnstileKeys = {
sitekey: process.env.TURNSTILE_SITE_KEY,
secret: process.env.TURNSTILE_SECRET_KEY,
};
if (!turnstileKeys.sitekey || !turnstileKeys.secret) {
throw new Error("TURNSTILE_SITE_KEY and TURNSTILE_SECRET_KEY must be set");
}

When one deployment has to serve both real visitors and tests, Cloudflare’s tutorial on excluding Turnstile from end-to-end tests detects test requests on the server (by IP address or by a header only the tests know) and hands them the testing keys. Its Cypress side adds the header with cy.intercept. Scope it to your own origin, so the secret header never goes to third parties:

cypress/support/e2e.js
beforeEach(() => {
cy.intercept({ url: `${Cypress.config("baseUrl")}/**` }, (req) => {
req.headers["x-test-environment"] = Cypress.env("TEST_ENVIRONMENT_SECRET");
});
});

Treat that header’s value as a secret: anyone who has it gets the always-pass keys.

The Cypress test

With the always-pass sitekey, the widget produces its dummy token by itself after the page loads. The test waits for it in the hidden field, then submits:

cypress/e2e/contact.cy.js
describe("contact form behind Cloudflare Turnstile", () => {
it("submits once the widget has produced its token", () => {
cy.visit("/contact");
cy.get("input[name=email]").type("qa@example.com");
cy.get("textarea[name=message]").type("Hello from CI");
// The always-pass testing sitekey fills the hidden field with Cloudflare's dummy token.
cy.get('input[name="cf-turnstile-response"]', { timeout: 20000 }).should(
"have.value",
"XXXX.DUMMY.TOKEN.XXXX",
);
cy.get("form").submit();
cy.contains("Thanks, we got your message").should("be.visible");
});
});

Cypress retries the should until the value appears or 20 seconds pass, so no fixed wait is needed. If your form renames the field with data-response-field-name, or sends the token from the widget’s callback in a JSON body, assert on what your page does instead: for example, wait on a cy.intercept alias for the form’s request and check its body.

The failure cases worth testing

Each row is one CI job, or one environment variable switched between runs:

Case Sitekey Secret What your app should do
Happy path 1x00000000000000000000AA 1x0000000000000000000000000000000AA Accept the form
Invisible widget 1x00000000000000000000BB 1x0000000000000000000000000000000AA Accept the form with no visible widget
Widget fails 2x00000000000000000000AB 1x0000000000000000000000000000000AA Show your error message; run your error callback or retry path
Server rejects 1x00000000000000000000AA 2x0000000000000000000000000000000AA Refuse the form, keep the user’s input
Token spent 1x00000000000000000000AA 3x0000000000000000000000000000000AA Treat it as timeout-or-duplicate: ask for a fresh token

The server-side cases matter most, because a widget can be skipped by anyone who posts to your form directly. Siteverify errors lists every code your server may get back, and retry and refresh settings covers what the widget does on its own after a failure.

Keep testing keys out of production

Cloudflare’s tutorial asks you to “Validate credentials in CI/CD pipelines to prevent test credentials in production”. A few lines in the deploy job are enough:

// scripts/check-turnstile-keys.mjs: run in the production deploy job.
const TESTING = /^[123]x0{20,}[A-F]{2}$/;
for (const name of ["TURNSTILE_SITE_KEY", "TURNSTILE_SECRET_KEY"]) {
const value = process.env[name] ?? "";
if (value === "" || TESTING.test(value)) {
console.error(`${name} is missing or is a Cloudflare testing key`);
process.exit(1);
}
}
console.log("Cloudflare Turnstile keys look like production keys");

When a real token is what you need

Testing keys prove your code, not Cloudflare’s live widget. To check a production form from outside, as a monitor would, you need a real token for the real sitekey. A solving API gives one: see the Cloudflare Turnstile solver. ZeroCaptcha has no test mode or test keys: every task is real and charged when it succeeds, so keep CI on Cloudflare’s testing keys, which cost nothing, and use real tokens only where a real check is the point.

Sources

The team that builds and runs the ZeroCaptcha API. Articles are drafted with AI tools, then checked against the API's code and the primary sources each one cites.

Questions

Can Cypress click the Cloudflare Turnstile checkbox?

No. The widget runs in an iframe from challenges.cloudflare.com, and Cypress cannot automate a cross-origin iframe. Cloudflare also says automated testing suites such as Cypress are detected as bots by Turnstile.

How do I test a Cloudflare Turnstile form in Cypress?

Serve the page with Cloudflare's always-pass testing sitekey 1x00000000000000000000AA and validate with the testing secret 1x0000000000000000000000000000000AA. The widget then fills cf-turnstile-response with the dummy token XXXX.DUMMY.TOKEN.XXXX, which your test waits for before it submits.

Can I use Cloudflare's testing keys in production?

No. Production secret keys reject the dummy token, and a production page with a testing sitekey protects nothing. Keep the testing keys to development and CI, and check in your pipeline that production never gets them.

Read next

This article is part of the Cloudflare Turnstile solver hub. Every task is charged only when a token is ready.

Get an API key