{"id":25534,"date":"2026-09-04T16:43:25","date_gmt":"2026-09-04T16:43:25","guid":{"rendered":"https:\/\/capskip.com\/?p=25534"},"modified":"2026-09-04T16:43:25","modified_gmt":"2026-09-04T16:43:25","slug":"testcafe-captcha","status":"publish","type":"post","link":"https:\/\/capskip.com\/zh\/testcafe-captcha\/","title":{"rendered":"\u5982\u4f55\u5728 TestCafe \u6d4b\u8bd5\u4e2d\u8bc6\u522b\u9a8c\u8bc1\u7801\uff08Node.js SDK\uff09"},"content":{"rendered":"<p>A TestCafe captcha step is shorter than it is in most frameworks, because TestCafe test code already runs in Node. You call the solver straight from the test file, then write the token into the page with a ClientFunction. There is one thing to check before any of that, though, and getting it wrong wastes an afternoon: whether your run is using native automation or the old URL rewriting proxy. On the proxy, reCAPTCHA is broken before a solver is anywhere near it.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">What you need<\/h2>\n<ul>\n<li>TestCafe 3.0 or newer and Node.js 18 or newer, plus the CapSkip Node.js SDK.<\/li>\n<li>A Chromium browser, so Chrome or Edge. Native automation does not cover Firefox or Safari.<\/li>\n<li>The page URL of the form under test, and its sitekey.<\/li>\n<li>CapSkip running in Local mode when the test runner and the solver share a machine, or in Server mode when they do not. Both modes are described under <a href=\"https:\/\/capskip.com\/setup-guide\/#connection-settings\">connection settings<\/a>.<\/li>\n<\/ul>\n<div data-no-translation>\n<pre data-enlighter-language=\"bash\" class=\"EnlighterJSRAW\"># npm install capskip\nnpm install --save-dev testcafe\nnpm install capskip<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Check first: native automation or the proxy?<\/h2>\n<p>TestCafe has two ways of driving a browser and they behave completely differently around CAPTCHA. The original one is a web proxy called hammerhead. It sits between the browser and the site, injects its automation scripts into every page, and rewrites every URL on the resource so it points back at the proxy. That is what let TestCafe support any browser without a driver, and it is also what breaks reCAPTCHA.<\/p>\n<p>Two failures show up on the proxy, both reported against hammerhead and neither fixable from your test. reCAPTCHA tries to start a web worker from Google&#8217;s origin and the browser refuses, because the document&#8217;s origin is now the proxy&#8217;s own host and port. And pages served through the proxy come back with a reCAPTCHA v3 score of 0.1 every time, which most sites treat as a bot outright.<\/p>\n<p>Native automation replaced all of that. TestCafe drives Chromium over the DevTools protocol instead, so there is no proxy in the path and no URL rewriting at all. It landed as an experiment in v2.5.0 and it has been the default since v3.0.0. If your suite is on a current TestCafe and running in Chrome, you already have it.<\/p>\n<p>So the first debugging step is to confirm nothing has switched it off. TestCafe disables native automation automatically on Firefox and Safari, and the CLI flag named disable-native-automation plus its config file twin turn it off on Chromium too. Suites that added that flag years ago to work around something else are the common case. Search for it before you write any solver code.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"bash\" class=\"EnlighterJSRAW\"># Run in Chrome, which uses native automation by default.\nnpx testcafe chrome tests\/checkout.js\n\n# This flag puts you back on the proxy and breaks reCAPTCHA.\n# npx testcafe chrome tests\/checkout.js --disable-native-automation<\/pre>\n<\/div>\n<p>One thing is worth saying plainly, because TestCafe says it too: if you own the site under test, the best answer is not to solve anything. Google publishes a v2 test sitekey that always passes, and setting up a separate v3 key with a relaxed threshold is a five minute change in the reCAPTCHA console. TestCafe&#8217;s own <a href=\"https:\/\/testcafe.io\/documentation\/402794\/recipes\/integrations\/test-websites-that-use-recaptcha\" rel=\"nofollow noopener\" target=\"_blank\">reCAPTCHA recipe<\/a> walks through both. Solving is for the cases where that door is closed: a third party checkout inside the flow, a staging environment sharing production keys, or a smoke test that has to run against the real site.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 1: read the sitekey out of the page<\/h2>\n<p>Selectors in TestCafe are lazy and they retry, so a selector written before the widget renders still resolves once it appears. Pull the sitekey off the widget element rather than pasting a literal into the test, and the same test survives a key rotation.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\/\/ npm install capskip\nimport { Selector } from 'testcafe';\n\nconst PAGE_URL = 'https:\/\/example.com\/page-with-recaptcha';\n\nfixture('Checkout').page(PAGE_URL);\n\ntest('submits behind reCAPTCHA', async t =&gt; {\n    const widget = Selector('.g-recaptcha');\n    const sitekey = await widget.getAttribute('data-sitekey');\n});<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 2: solve it from the test file<\/h2>\n<p>Here is where TestCafe is easier than the browser side runners. Your test function is ordinary Node, so the SDK is a plain import and the call is a plain await. There is no bridge to build and no task to register, which is the part people expect to need after coming from Cypress.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\/\/ npm install capskip\nimport { CapSkip } from 'capskip';\n\n\/\/ Local mode. Change only the host to talk to a solver\n\/\/ running on another machine.\nconst solver = new CapSkip({ host: '127.0.0.1', port: 8080 });\n\nconst result = await solver.recaptcha(sitekey, PAGE_URL);\nconst token = result.code;<\/pre>\n<\/div>\n<p>That one method covers reCAPTCHA v2, Invisible, Enterprise and v3. The variants are options on the third argument rather than separate calls: invisible set to 1, enterprise set to 1, or version set to v3 with an action string. Turnstile and GeeTest have their own methods with the same shape. Every parameter is listed in <a href=\"https:\/\/capskip.com\/api-docs\/\">the CapSkip API documentation<\/a>.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 3: write the token in with a ClientFunction<\/h2>\n<p>The response field is a hidden textarea, so the normal typing action will not touch it. TestCafe actions only work on visible elements, and that is deliberate. A ClientFunction runs your code inside the page instead, which is the right tool for a field a real user never types into.<\/p>\n<p>The trap here catches almost everyone once. <strong>A ClientFunction cannot see variables from the surrounding test.<\/strong> The function body is serialised and shipped to the browser, so a token captured from the enclosing scope arrives as an undefined identifier at run time. Pass it as an argument or as a declared dependency.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\/\/ The token is a parameter, not a closure variable.\nimport { ClientFunction } from 'testcafe';\n\nconst injectToken = ClientFunction(value =&gt; {\n    const field = document.getElementById('g-recaptcha-response');\n    field.value = value;\n    field.dispatchEvent(new Event('change', { bubbles: true }));\n});\n\nawait injectToken(token);<\/pre>\n<\/div>\n<p>TestCafe&#8217;s guidance is not to use client functions to permanently alter how a site behaves, and that guidance is worth keeping. Writing one value into one form field for one run is not that. You are filling in a field rather than patching the page&#8217;s behaviour, and the value is gone the moment the run ends.<\/p>\n<p>Some forms wait on a callback instead of reading the textarea. If the widget declares a data-callback attribute, invoke that function with the token in the same ClientFunction and the page proceeds exactly as it does for a human.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Full working example<\/h2>\n<p>The whole test. Read the sitekey, solve, inject, submit, assert.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\/\/ npm install capskip\nimport { Selector, ClientFunction } from 'testcafe';\nimport { CapSkip, NetworkException } from 'capskip';\n\nconst PAGE_URL = 'https:\/\/example.com\/page-with-recaptcha';\nconst solver = new CapSkip({ host: '127.0.0.1', port: 8080 });\n\nconst injectToken = ClientFunction(value =&gt; {\n    const field = document.getElementById('g-recaptcha-response');\n    field.value = value;\n    field.dispatchEvent(new Event('change', { bubbles: true }));\n});\n\nfixture('Checkout').page(PAGE_URL);\n\ntest('submits the protected form', async t =&gt; {\n    const sitekey = await Selector('.g-recaptcha').getAttribute('data-sitekey');\n\n    let token;\n    try {\n        token = (await solver.recaptcha(sitekey, PAGE_URL)).code;\n    } catch (err) {\n        if (err instanceof NetworkException) {\n            throw new Error('CapSkip is not reachable on 127.0.0.1:8080.');\n        }\n        throw err;\n    }\n\n    await injectToken(token);\n    await t.click(Selector('button[type=submit]'));\n    await t.expect(Selector('.thank-you').exists).ok();\n});<\/pre>\n<\/div>\n<p>Solve as late as you can. A token is single use and it expires in about two minutes, so a token solved in a fixture hook that runs before three other tests is dead by the time the fourth one submits it. Put the call in the test that needs it.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Timeouts, and the one that actually bites<\/h2>\n<p>A reCAPTCHA solve takes tens of seconds, which is longer than several TestCafe defaults. The good news is that the timeouts people reach for first are not involved. Your solver call is an await inside the test function rather than a page action, so the selector timeout of 10 seconds and the assertion timeout of 3 seconds never see it.<\/p>\n<p>The limit that matters is the test execution timeout, which caps how long a single test may run. It has no default, so it only bites once someone sets it. If your CI config passes a test execution timeout, make sure the value leaves room for a slow solve on top of everything else the test does. The SDK has its own ceiling too: recaptchaTimeout defaults to 300 seconds and raises a TimeoutException when a solve outlasts it.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Running the solver somewhere else<\/h2>\n<p>Tests move to CI, and a CI runner is not your desk. Nothing in the code above changes except the host string.<\/p>\n<p>CapSkip has two connection modes. Local binds to 127.0.0.1 and answers that device only, which is what you want while you are writing the test. Server binds to your network or public IP, so a build agent, a container or a VM calls the same Windows machine over the API. A static public IP keeps that address stable. It is your hardware and it stays unmetered in both modes, so a suite that solves five hundred a night costs exactly what a suite that solves five does.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\/\/ Same SDK, same call. Only the host moves.\nconst solver = new CapSkip({\n    host: process.env.CAPSKIP_HOST || '127.0.0.1',\n    port: 8080,\n    apiKey: process.env.CAPSKIP_API_KEY,\n});<\/pre>\n<\/div>\n<p>The SDK reads CAPSKIP_HOST, CAPSKIP_PORT and CAPSKIP_API_KEY from the environment on its own, so a CI job can point the same test file at a remote solver with two variables and no code change. Turn on key validation once the solver listens on a network address, and give each runner its own key so one can be revoked without touching the others. Both modes are walked through in the <a href=\"https:\/\/capskip.com\/setup-guide\/#connection-settings\">CapSkip setup guide<\/a>.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Common errors and what they mean<\/h2>\n<table>\n<thead>\n<tr>\n<th>What you see<\/th>\n<th>Cause<\/th>\n<th>Fix<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Failed to construct Worker: script cannot be accessed from origin<\/td>\n<td>The hammerhead proxy is in the path, so the page origin is not the site<\/td>\n<td>Drop the disable-native-automation flag and run in Chrome or Edge<\/td>\n<\/tr>\n<tr>\n<td>Every v3 score comes back as 0.1<\/td>\n<td>Same cause. The proxy pins the score no matter what the test does<\/td>\n<td>Same fix. Native automation removes the proxy entirely<\/td>\n<\/tr>\n<tr>\n<td>ReferenceError saying the token is not defined<\/td>\n<td>The ClientFunction body cannot read outer scope variables<\/td>\n<td>Pass the token as an argument or a declared dependency<\/td>\n<\/tr>\n<tr>\n<td>The typing action fails on the response field<\/td>\n<td>The textarea is hidden, and actions need a visible element<\/td>\n<td>Set the value in a ClientFunction instead<\/td>\n<\/tr>\n<tr>\n<td>The form rejects a token that looks fine<\/td>\n<td>It was solved in a hook, minutes before the submit<\/td>\n<td>Solve inside the test, immediately before submitting<\/td>\n<\/tr>\n<tr>\n<td>NetworkException<\/td>\n<td>CapSkip is not running, or the host is wrong<\/td>\n<td>Start the app, or point host at the server address<\/td>\n<\/tr>\n<tr>\n<td>TimeoutException<\/td>\n<td>The solve outlasted recaptchaTimeout<\/td>\n<td>Raise it above the default of 300 seconds<\/td>\n<\/tr>\n<tr>\n<td>ValidationException<\/td>\n<td>A missing or malformed sitekey or page URL<\/td>\n<td>Log both before the call and check the sitekey is the live one<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">FAQ<\/h2>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Do I need a task or a plugin, the way Cypress does?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">No. Cypress runs your test code inside the browser, so anything needing Node has to cross a bridge. TestCafe runs your test code in Node from the start and only the ClientFunction bodies go to the browser, so the solver call is an ordinary import. The Cypress version of this job is written up in <a href=\"https:\/\/capskip.com\/cypress-captcha\/\">the Cypress CAPTCHA guide<\/a>.<\/p>\n<\/details>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Can I do this in Firefox or Safari?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">You can run the test, but expect the widget itself to misbehave, because TestCafe falls back to the proxy on those browsers and that is the configuration reCAPTCHA does not survive. Keep the CAPTCHA bearing tests on Chrome or Edge, and let the cross browser matrix cover the pages that have no widget on them.<\/p>\n<\/details>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Does this work for Turnstile as well?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Yes, with two differences. The method is turnstile rather than recaptcha, and the field to fill is the hidden input named cf-turnstile-response. A full challenge page also needs the data and pagedata values plus the user agent that comes back with the token, which is covered on <a href=\"https:\/\/capskip.com\/cloudflare-turnstile-solver\/\">the Cloudflare Turnstile solver page<\/a>.<\/p>\n<\/details>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Should CAPTCHA tests run on every commit?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Usually not, and the reason is speed rather than cost. Solving is unmetered here, but tens of seconds per test is a slow pull request check. Tag them and run them on a nightly or pre release job, and keep the fast suite pointed at a build that uses test keys.<\/p>\n<\/details>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">The short version<\/h2>\n<p>Confirm native automation is on, because the legacy proxy breaks reCAPTCHA on its own. Read the sitekey with a Selector, call the solver from the test file since it is already Node, and write the token in through a ClientFunction with the value passed as an argument. Solve immediately before you submit.<\/p>\n<p>The rest of the Node.js surface is covered on <a href=\"https:\/\/capskip.com\/nodejs-captcha-solver\/\">the Node.js CAPTCHA solver page<\/a>. Everything specific to that CAPTCHA type sits on <a href=\"https:\/\/capskip.com\/recaptcha-v2-solver\/\">the reCAPTCHA v2 solver page<\/a>. The same job in a WebDriver based runner is written up in <a href=\"https:\/\/capskip.com\/webdriverio-captcha\/\">the WebdriverIO guide<\/a>.<\/p>\n<p>One last thing before you wire this into CI. CapSkip is a <a href=\"https:\/\/capskip.com\/\">local captcha solver<\/a> that runs on hardware you already own, so a nightly suite that solves a thousand costs the same as one that solves ten.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>TestCafe \u7684\u6d4b\u8bd5\u4ee3\u7801\u8fd0\u884c\u5728 Node \u4e2d\uff0c\u6240\u4ee5\u4f60\u53ef\u4ee5\u76f4\u63a5\u8c03\u7528\u8bc6\u522b\u5de5\u5177\uff0c\u518d\u7528 ClientFunction \u628a token \u5199\u8fdb\u9875\u9762\u3002\u5148\u786e\u8ba4\u539f\u751f\u81ea\u52a8\u5316\u6a21\u5f0f\u5df2\u5f00\u542f\uff0c\u56e0\u4e3a\u65e7\u7684\u4ee3\u7406\u673a\u5236\u5728\u8bc6\u522b\u5de5\u5177\u4ecb\u5165\u4e4b\u524d\u5c31\u4f1a\u628a reCAPTCHA \u5f04\u574f\u3002<\/p>","protected":false},"author":1,"featured_media":25533,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"TestCafe CAPTCHA: Solve It in Native Automation | CapSkip","rank_math_description":"A testcafe captcha step is one solver call, because TestCafe tests run in Node. Check native automation is on first: the legacy proxy breaks reCAPTCHA.","rank_math_focus_keyword":"testcafe captcha","footnotes":""},"categories":[70],"tags":[],"class_list":["post-25534","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-captcha"],"_links":{"self":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/25534","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/comments?post=25534"}],"version-history":[{"count":2,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/25534\/revisions"}],"predecessor-version":[{"id":25539,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/25534\/revisions\/25539"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media\/25533"}],"wp:attachment":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media?parent=25534"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/categories?post=25534"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/tags?post=25534"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}