How I Bypass CAPTCHA in Cypress During E2E Tests
How I Bypass CAPTCHA in Cypress During E2E Tests
When I started writing end-to-end tests with Cypress, everything worked smoothly until I hit the first CAPTCHA. That’s the moment where clean automation turns into a brick wall.
CAPTCHAs like reCAPTCHA or Cloudflare Turnstile are designed to stop bots. Unfortunately, Cypress behaves exactly like one.
Over time, I developed a practical strategy. Below I’ll share how I handle CAPTCHAs in different environments, from development to production-level scenarios.
Why CAPTCHA Breaks Cypress Tests
Cypress runs directly inside the browser and executes commands in the same loop as the application. That makes it fast and powerful. But it also creates two major issues.
1. Cross-Domain Iframes
Most CAPTCHAs load inside iframes from external domains like google.com. Due to the browser’s Same-Origin Policy and Cypress security rules, I cannot directly interact with those iframe elements.
2. Anti-Bot Detection
A simple .click() doesn’t fool modern protection systems. They analyze:
-
Mouse movement behavior
-
Browser fingerprint
-
Network patterns
-
Timing consistency
Cypress interactions are too precise and linear. Real humans are messy. Cypress is not.
So instead of fighting the system blindly, I use different approaches depending on the environment.
Part 1: What I Do in Development and Staging
If I control the application environment, I never solve real CAPTCHAs. That would waste time and money.
1. I Use Google Test Keys
For reCAPTCHA v2, Google provides official test credentials that always pass validation.
If I have access to the backend configuration, I switch the test environment to use:
Site key:
6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI
Secret key:
6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe
With these keys, the widget renders, but verification passes automatically. My tests stay clean and fast.
2. I Mock the Verification Request with cy.intercept
Even faster than test keys is simply stubbing the backend validation endpoint.
Instead of solving the CAPTCHA, I intercept the API call and force a successful response.
Example:
describe('Login bypass', () => {
it('should verify user without real captcha', () => {
cy.intercept('POST', '/api/validate-captcha', {
statusCode: 200,
body: { success: true, verificationPassed: true }
}).as('bypassCaptcha');
cy.visit('/login');
cy.get('#submit').click();
cy.wait('@bypassCaptcha');
});
});
This way, I test the login logic without depending on external CAPTCHA systems.
For staging and development, this is always my preferred approach.
Part 2: When I Cannot Disable CAPTCHA
Sometimes I test production systems, external websites, or scraping flows where I cannot disable protection. In that case, I integrate a real CAPTCHA-solving API.
I’ve used services like 2Captcha and SolveCaptcha to handle the solving part externally.
Because Cypress commands are synchronous, I cannot directly wait for an async CAPTCHA solution. So I move the solving logic to the Node.js side using cy.task.
Step 1: Adjust Security Settings
To interact with CAPTCHA elements inside cross-domain iframes and inject tokens, I disable Chrome Web Security in cypress.config.js.
Without this, Cypress blocks access to critical elements.
Step 2: Injecting the Solved Token
The flow I use looks like this:
-
Visit the page
-
Send sitekey and page URL to my solver via
cy.task -
Receive the token
-
Inject it into the hidden response field
-
Submit the form
Example for reCAPTCHA v2:
describe('Automated Captcha Solver Test', () => {
it('bypasses reCAPTCHA automatically', () => {
const pageUrl = 'https://example.com/login';
const siteKey = '6Lc_aXkUAAAAA...';
const apiKey = Cypress.env('CAPTCHA_API_KEY');
cy.visit(pageUrl);
cy.task('solveCaptcha', {
sitekey: siteKey,
pageurl: pageUrl,
apiKey: apiKey
}).then((token) => {
if (!token) throw new Error('Failed to retrieve captcha token');
cy.get('#g-recaptcha-response')
.invoke('attr', 'style', 'display: block')
.invoke('val', token);
cy.get('#submit-btn').click();
});
cy.url().should('include', '/dashboard');
});
});
For Cloudflare Turnstile, the only difference is the hidden field selector:
cy.task('solveCaptcha', {
type: 'turnstile',
sitekey: '0x4AAAAAA...',
pageurl: 'https://example.com',
apiKey: Cypress.env('CAPTCHA_API_KEY')
}).then(token => {
cy.get('[name="cf-turnstile-response"]')
.invoke('val', token);
cy.get('form').submit();
});
Troubleshooting Issues I Encountered
Even with the correct setup, things can fail. Here is what I usually check.
Headless Mode Problems
Some CAPTCHAs become more aggressive in headless mode. If tests fail, I rerun them with:
--headed
Sometimes that alone fixes the issue.
Automation Detection
Advanced systems like Akamai or Cloudflare can detect the window.Cypress variable. In those cases, standard Cypress may not be enough. I experiment with stealth configurations or anti-detect plugins.
Form Not Submitting
Frameworks like React or Vue often don’t detect value changes when using .invoke('val').
In those cases, I manually trigger an event:
cy.get('#g-recaptcha-response')
.invoke('val', token)
.trigger('change');
Without this step, the form may silently refuse submission.
My Final Strategy
Here’s the simple rule I follow:
-
In Development or Staging → I always mock (
cy.intercept) or use Google test keys. -
In Production or when testing external systems → I integrate an API solver via
cy.task.
Cypress was built for testing application logic, not bypassing protection systems. So instead of forcing it to behave like a human, I treat CAPTCHA handling as a separate layer and integrate it strategically.
That approach keeps my tests stable, scalable, and realistic without turning them into fragile hacks.
Комментарии
Отправить комментарий