CAPTCHA Testing Guide

How to Test hCaptcha with Playwright: Complete 2026 Guide

A scenario-by-scenario walkthrough of testing hCaptcha in Playwright. Iframe challenge widgets, test sitekeys and secrets, enterprise passive mode, accessibility cookies, server-side token verification, and the pitfalls that silently break real CAPTCHA test suites.

15%+

hCaptcha protects over 15% of the internet by request volume, serving billions of challenges per month across millions of sites according to Intuition Machines.

Intuition Machines (hCaptcha parent company)

0+Nested iframes per widget
0Test scenarios covered
0%Pass rate with test keys
0%Fewer lines with Assrt

hCaptcha Challenge Flow

BrowserYour ApphCaptcha IframehCaptcha APIYour ServerLoad page with hCaptcha widgetRender iframe (hcaptcha.com)Click checkboxEvaluate challengeReturn token (or visual challenge)Set h-captcha-response hidden inputSubmit form with tokenPOST /siteverify{"success": true}Accept submission

1. Why Testing hCaptcha Is Harder Than It Looks

hCaptcha renders inside a nested iframe served from newassets.hcaptcha.com. That iframe is cross-origin, which means your Playwright locators cannot reach into it directly. You must use frameLocator to target the hCaptcha iframe, then locate elements inside it. But the challenge does not stop there. hCaptcha injects additional nested iframes for its visual challenges, creating a two or three level iframe tree that changes structure depending on the challenge type presented to the user.

The second structural problem is that hCaptcha is designed to block automation. It fingerprints the browser environment, analyzes mouse movement patterns, checks for headless browser signals, and uses proof-of-work challenges that consume real CPU cycles. Running Playwright in default headless mode with no countermeasures will trigger a visual challenge nearly every time, even for legitimate test traffic. This makes it impossible to write a deterministic test that always passes unless you use the official test sitekeys or enterprise configuration options.

There are five structural reasons this widget is hard to test reliably. First, the cross-origin iframe prevents direct DOM access from your page context. Second, hCaptcha's bot detection actively fights headless browsers. Third, the challenge type (checkbox only, image grid, or passive) depends on a server-side risk score you cannot control. Fourth, the response token expires after 120 seconds, creating a tight timing window between solving the challenge and submitting the form. Fifth, server-side verification requires a secret key and a separate HTTP call to hCaptcha's API, and your test must validate that entire round trip.

hCaptcha Widget Rendering Flow

🌐

Page Load

Script tag loads hcaptcha.js

⚙️

Widget Init

hcaptcha.render() called

📦

Iframe Created

Cross-origin iframe injected

🔒

Risk Assessment

Browser fingerprint sent

Challenge Type

Checkbox, visual, or passive

⚙️

Token Issued

h-captcha-response set

hCaptcha Iframe Nesting Structure

🌐

Your Page

document origin

📦

Outer Iframe

hcaptcha.com/captcha/v1

Checkbox Frame

Checkbox + anchor

📦

Challenge Frame

Image grid (if triggered)

A robust hCaptcha test suite accounts for all of these layers. The sections below walk through each scenario with runnable Playwright TypeScript and the exact configuration you need to make tests deterministic.

2. Setting Up a Reliable Test Environment

hCaptcha provides official test credentials specifically for automated testing. These are the foundation of every reliable hCaptcha test suite. The test sitekey 10000000-ffff-ffff-ffff-000000000001 always passes the checkbox challenge without presenting a visual puzzle. The corresponding test secret key 0x0000000000000000000000000000000000000000 always returns {"success": true}from the siteverify endpoint. These are published in hCaptcha's official documentation and are safe to commit to your repository.

hCaptcha Test Environment Setup Checklist

  • Use the official test sitekey (10000000-ffff-ffff-ffff-000000000001) in your test environment
  • Use the official test secret (0x0000000000000000000000000000000000000000) for server-side verification
  • Set environment variables to swap keys between test and production
  • Configure Playwright to use headed or headful mode for debugging hCaptcha issues
  • Add hcaptcha.com to Playwright's allowed origins list
  • Set actionTimeout to at least 10 seconds for iframe interactions
  • Disable any Content Security Policy restrictions that block hcaptcha.com in test
  • For Enterprise accounts, configure passive mode sitekey for zero-friction testing

Environment Variables

.env.test

Playwright Configuration for hCaptcha

hCaptcha iframes load from a different origin than your application. Playwright handles cross-origin iframes natively, but you need to increase timeouts because the hCaptcha script makes multiple network requests during initialization. The proof-of-work computation can also add 1 to 3 seconds of latency, even with the test sitekey.

playwright.config.ts

hCaptcha Script Integration

Your application loads the hCaptcha script and renders the widget. Here is a minimal integration that your test suite will target. The key detail is the data-sitekey attribute, which must use the test sitekey in your test environment.

components/captcha-form.tsx
Verify hCaptcha Test Keys

3. Scenario: Using hCaptcha Test Sitekey (Always Pass)

The simplest and most reliable way to test hCaptcha is to use the official test sitekey. When your application renders the hCaptcha widget with sitekey 10000000-ffff-ffff-ffff-000000000001, the checkbox click always succeeds without showing a visual challenge. The widget still renders a real iframe, sets the h-captcha-response hidden input, and fires the callback. This means your entire form submission flow works end to end, with the only difference being that the challenge is guaranteed to pass.

1

hCaptcha Test Sitekey: Always Pass

Straightforward

Goal

Load a form with the hCaptcha test sitekey, click the checkbox inside the iframe, verify the response token is set, and submit the form successfully.

Preconditions

  • App running at APP_BASE_URL with the test sitekey configured
  • Server-side verification uses the test secret key
  • No network-level blocking of hcaptcha.com domains

Playwright Implementation

tests/captcha/hcaptcha-test-key.spec.ts

What to Assert Beyond the UI

  • The h-captcha-response hidden textarea contains a non-empty token string
  • The form submission completes successfully (your server accepted the token)
  • The g-recaptcha-response field is also populated (hCaptcha sets both for backward compatibility)

4. Scenario: Clicking the hCaptcha Checkbox in Playwright

The hCaptcha checkbox lives inside a cross-origin iframe. You cannot use page.locator()directly because the checkbox element is not in your page's DOM tree. Playwright's frameLocator()API bridges this gap by letting you target elements inside iframes using CSS selectors or the iframe's src attribute. The tricky part is that hCaptcha sometimes renders multiple iframes, and the one containing the checkbox may not be the first match.

2

Iframe Checkbox Interaction

Moderate

Goal

Reliably locate and click the hCaptcha checkbox inside its nested iframe structure, handling the case where the widget has not finished initializing.

Preconditions

  • hCaptcha widget rendered on the page
  • Test sitekey configured for deterministic behavior

Playwright Implementation

tests/captcha/hcaptcha-iframe.spec.ts

What to Assert Beyond the UI

  • The checkbox aria-checked attribute is "true"
  • The hCaptcha callback function was invoked (check via page.evaluate)
  • The response token textarea is populated before form submission

hCaptcha Checkbox: Playwright vs Assrt

import { test, expect } from '@playwright/test';

test('hCaptcha checkbox', async ({ page }) => {
  await page.goto('/contact');
  await page.waitForFunction(() =>
    document.querySelector('iframe[src*="hcaptcha.com"]')
  );
  const frame = page.frameLocator(
    'iframe[src*="hcaptcha.com"][title*="Widget"]'
  );
  await frame.locator('#checkbox').click();
  await expect(
    frame.locator('#checkbox[aria-checked="true"]')
  ).toBeVisible({ timeout: 10_000 });
  const token = await page.evaluate(() => {
    const el = document.querySelector(
      'textarea[name="h-captcha-response"]'
    ) as HTMLTextAreaElement;
    return el?.value;
  });
  expect(token).toBeTruthy();
});
56% fewer lines

Try Assrt for free

Open-source AI testing framework. No signup required.

Get Started

5. Scenario: Server-Side Token Verification

Clicking the hCaptcha checkbox only generates a client-side token. Your server must verify that token by sending a POST request to https://api.hcaptcha.com/siteverify with your secret key and the response token. If you skip server-side verification, an attacker can submit your form with a fake token and bypass the CAPTCHA entirely. Your test must validate the full round trip: checkbox click, token generation, form submission, server verification, and successful processing.

3

Server-Side Token Verification

Moderate

Goal

Submit a form with an hCaptcha token, intercept the server-side verification request to hCaptcha's API, and confirm the full verification chain completes successfully.

Preconditions

  • Test sitekey and secret configured in the application
  • Server has an endpoint that calls hCaptcha's siteverify API

Server-Side Verification Code

api/verify-captcha.ts

Playwright Implementation

tests/captcha/hcaptcha-verification.spec.ts

What to Assert Beyond the UI

  • Your server's API endpoint returns a 200 status with {"success": true}
  • The h-captcha-response token in the form body is non-empty
  • The success page renders, confirming end-to-end verification

6. Scenario: Enterprise Passive Mode (No User Interaction)

hCaptcha Enterprise offers a passive mode where the widget evaluates the user's risk score without displaying any visible challenge or checkbox. The widget loads invisibly, performs browser fingerprinting and behavioral analysis, and sets the response token automatically. From a testing perspective, this is both simpler (no iframe clicking) and more complex (no visible indicator of completion). Your test must wait for the token to appear in the hidden input without any user-facing interaction.

4

Enterprise Passive Mode

Complex

Goal

Load a page with hCaptcha Enterprise in passive mode, wait for the invisible evaluation to complete, and submit the form with the automatically generated token.

Preconditions

  • hCaptcha Enterprise account with passive mode sitekey
  • Widget configured with data-size="invisible"
  • Enterprise API credentials for verification

Playwright Implementation

tests/captcha/hcaptcha-enterprise-passive.spec.ts

What to Assert Beyond the UI

  • The response token appears without any checkbox click or visual challenge
  • The token length is greater than 50 characters (real tokens are 200+ characters)
  • Enterprise API returns a risk score alongside the success boolean

Enterprise Passive Mode: Playwright vs Assrt

import { test, expect } from '@playwright/test';

test('hCaptcha passive mode', async ({ page }) => {
  await page.goto('/checkout');
  await page.waitForFunction(() => {
    const el = document.querySelector(
      'textarea[name="h-captcha-response"]'
    ) as HTMLTextAreaElement;
    return el && el.value.length > 0;
  }, { timeout: 15_000 });
  const token = await page
    .locator('textarea[name="h-captcha-response"]')
    .inputValue();
  expect(token).toBeTruthy();
  await page.fill('#card-number', '4242424242424242');
  await page.click('button[type="submit"]');
  await expect(page).toHaveURL(/\/confirmation/);
});
44% fewer lines

7. Scenario: Accessibility Cookie Bypass

hCaptcha provides an accessibility option for users who cannot complete visual challenges. Users can register at accounts.hcaptcha.com/signup?type=accessibility to receive an accessibility cookie that bypasses visual challenges entirely. In a test environment, you can set this cookie programmatically to skip challenges without relying on the test sitekey. This approach is useful when you need to test with a production sitekey but still want deterministic results.

5

Accessibility Cookie Bypass

Moderate

Goal

Set the hCaptcha accessibility cookie before loading the page, then complete the hCaptcha challenge without encountering a visual puzzle.

Preconditions

  • A valid hCaptcha accessibility cookie obtained from the accessibility signup
  • The cookie is stored as an environment variable for CI use

Playwright Implementation

tests/captcha/hcaptcha-accessibility.spec.ts

What to Assert Beyond the UI

  • No visual challenge iframe (image grid) appeared during the flow
  • The checkbox transitioned directly from unchecked to checked
  • The response token is valid and passes server-side verification

8. Scenario: Error States and Expired Tokens

A robust test suite does not only cover the happy path. You need to verify that your application handles hCaptcha errors gracefully. Tokens expire after 120 seconds. Network failures can prevent the hCaptcha script from loading. The siteverify endpoint can return error codes like invalid-input-response or missing-input-secret. Your application should display a meaningful error and allow the user to retry.

6

Error States and Token Expiry

Complex

Goal

Test expired tokens, network failures during hCaptcha loading, and invalid verification responses. Confirm the application recovers gracefully in each case.

Playwright Implementation

tests/captcha/hcaptcha-errors.spec.ts

What to Assert Beyond the UI

  • The form does not submit successfully when the token is expired or invalid
  • An error message is visible to the user explaining the failure
  • The hCaptcha widget resets and allows the user to retry
  • No data is persisted to the database when verification fails

Error Handling: Playwright vs Assrt

import { test, expect } from '@playwright/test';

test('hCaptcha expired token', async ({ page }) => {
  await page.goto('/signup');
  const frame = page.frameLocator(
    'iframe[src*="hcaptcha.com"]'
  );
  await frame.locator('#checkbox').click();
  await expect(
    frame.locator('#checkbox[aria-checked="true"]')
  ).toBeVisible({ timeout: 10_000 });
  await page.route('**/api/submit', async (route) => {
    await route.fulfill({
      status: 400,
      body: JSON.stringify({
        success: false,
        error: 'Token expired',
      }),
    });
  });
  await page.fill('#email', 'test@example.com');
  await page.click('button[type="submit"]');
  await expect(
    page.getByText(/expired|try again/i)
  ).toBeVisible();
});
59% fewer lines

9. Common Pitfalls That Break hCaptcha Test Suites

hCaptcha test suites fail in predictable ways. These are the most common issues sourced from GitHub issues, Stack Overflow threads, and the hCaptcha community forums.

hCaptcha Testing Anti-Patterns

  • Using page.locator() instead of frameLocator() for the checkbox (the element is inside a cross-origin iframe)
  • Forgetting to wait for the hCaptcha script to initialize before interacting with the iframe
  • Running tests with the production sitekey in headless mode (hCaptcha will present unsolvable visual challenges)
  • Not setting actionTimeout high enough for hCaptcha's proof-of-work computation (default 5s is too short)
  • Submitting the form without checking that h-captcha-response has a non-empty value
  • Caching the response token across tests (tokens are single-use and expire after 120 seconds)
  • Blocking hcaptcha.com domains in CSP headers or ad blockers during testing
  • Using the test sitekey in production (it always passes, defeating the purpose of CAPTCHA)

Iframe Selector Fragility

The most common failure is targeting the wrong iframe. hCaptcha can render multiple iframes on the page: one for the checkbox anchor, one for the challenge grid, and sometimes additional iframes for enterprise features. Using a broad selector like iframe[src*="hcaptcha"] may match the challenge iframe instead of the checkbox iframe. Always use the most specific selector available, such as the iframe's title attribute, which hCaptcha sets to a predictable string describing the widget type.

Token Expiration in Slow Tests

hCaptcha tokens expire after 120 seconds. If your test completes the captcha, then performs several slow UI operations before submitting the form, the token may expire before it reaches your server. The siteverify endpoint will return invalid-input-response, and your test will fail intermittently. Solve the captcha as close to form submission as possible, or restructure your test to separate captcha-independent UI testing from the captcha submission test.

Headless Detection and Proof-of-Work

Even with the test sitekey, hCaptcha performs a small proof-of-work challenge in the browser. In CI environments with limited CPU (free-tier GitHub Actions runners, for example), this computation can take 3 to 5 seconds instead of the usual sub-second time on a developer machine. Set your Playwright actionTimeout to at least 15 seconds for hCaptcha interactions to account for slow CI runners. If you see TimeoutError only in CI and never locally, this is almost always the cause.

hCaptcha Test Suite Run

10. Writing These Scenarios in Plain English with Assrt

Every scenario above is 20 to 50 lines of Playwright TypeScript that depends on knowing the exact iframe selector, the checkbox element ID, the hidden textarea name, and the server verification endpoint. If hCaptcha updates their widget DOM structure (which they do periodically when upgrading their challenge system), your selectors break and every test fails until someone investigates and updates the locators. Assrt lets you describe the intent in plain English and regenerates the selectors automatically when the underlying widget changes.

The server verification scenario from Section 5 demonstrates this well. In raw Playwright, you need to know the iframe src pattern, the checkbox element ID, the textarea name for the response token, and the route pattern for your API endpoint. In Assrt, you describe what you want to happen and the framework resolves everything at runtime.

scenarios/hcaptcha-full-suite.assrt

Assrt compiles each scenario block into the same Playwright TypeScript you saw in the preceding sections, committed to your repo as real tests you can read, run, and modify. When hCaptcha updates their iframe structure, renames the checkbox element, or changes the response token field name, Assrt detects the failure, analyzes the new DOM, and opens a pull request with the updated locators. Your scenario files stay untouched.

Start with the test sitekey scenario. Once it is green in your CI, add the iframe interaction test, then the server verification flow, then the enterprise passive mode, then the accessibility bypass, then the error state tests. In a single afternoon you can have comprehensive hCaptcha coverage that protects your forms without the maintenance burden of hand-coded iframe selectors.

Related Guides

Ready to automate your testing?

Assrt discovers test scenarios, writes Playwright tests from plain English, and self-heals when your UI changes.

$npm install @assrt/sdk