{"id":25668,"date":"2026-09-12T07:57:15","date_gmt":"2026-09-12T07:57:15","guid":{"rendered":"https:\/\/capskip.com\/?p=25668"},"modified":"2026-09-12T07:57:15","modified_gmt":"2026-09-12T07:57:15","slug":"nightwatch-captcha","status":"publish","type":"post","link":"https:\/\/capskip.com\/zh\/nightwatch-captcha\/","title":{"rendered":"\u5982\u4f55\u5728 Nightwatch.js \u4e2d\u8bc6\u522b\u9a8c\u8bc1\u7801\uff08\u547d\u4ee4\u961f\u5217\uff09"},"content":{"rendered":"<p>A Nightwatch captcha solve has to run inside the command queue, not beside it. Nightwatch does not execute browser commands where you wrote them. It queues them and drains the queue after your test function returns, so a solving call written between two browser commands fires immediately, before the page has loaded and before the widget exists. The fix is browser.perform, plus one global you have to raise because a solve takes longer than the ten seconds Nightwatch allows an async callback.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">What you need<\/h2>\n<ul>\n<li>Nightwatch 3 with a working driver, either chromedriver locally or a remote WebDriver endpoint.<\/li>\n<li>CapSkip running on a Windows machine, with the Node client installed in the same project as your tests.<\/li>\n<li>The sitekey and the page URL. Read the sitekey off the widget rather than hardcoding it, because staging and production rarely share one.<\/li>\n<li>Server mode whenever the tests run somewhere other than the solver&#8217;s own machine, which includes every CI runner. It is one setting under connection settings.<\/li>\n<\/ul>\n<div data-no-translation>\n<pre data-enlighter-language=\"bash\" class=\"EnlighterJSRAW\"># npm install capskip\nnpm install --save-dev nightwatch\nnpm install capskip<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 1: why a plain solving call runs too early<\/h2>\n<p>Every browser command in a Nightwatch test is an instruction added to a queue. The test function runs top to bottom first, building that queue, and only then does Nightwatch start executing it. Ordinary JavaScript in between is not part of the queue, so it runs during the building pass. That is the whole problem in one sentence.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\/\/ WRONG. The solve starts while the queue is still being\n\/\/ built, so it runs before browser.url() has navigated.\nmodule.exports = {\n  &quot;signup form&quot;: function (browser) {\n    browser.url(&quot;https:\/\/example.com\/page-with-recaptcha&quot;);\n\n    solver.recaptcha(sitekey, pageUrl).then((r) =&gt; {\n      \/\/ fires first, against a page that is not open yet\n    });\n\n    browser.click(&quot;#submit&quot;);\n  },\n};<\/pre>\n<\/div>\n<p>The symptom is confusing because nothing throws. The solve succeeds, the test passes sometimes, and the token is for a page load that never happened. Nightwatch gives you a documented way to put your own code into the queue instead: browser.perform, whose callback is described as the function to run as part of the queue.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\/\/ RIGHT. perform() queues the callback, so it runs in\n\/\/ sequence with the commands either side of it.\nbrowser.url(&quot;https:\/\/example.com\/page-with-recaptcha&quot;);\n\nbrowser.perform(async function () {\n  const { code } = await solver.recaptcha(sitekey, pageUrl);\n  return code;\n});\n\nbrowser.click(&quot;#submit&quot;);<\/pre>\n<\/div>\n<p>An async test is the other option. Declaring the test function async makes the API commands return a promise, so awaiting each one keeps everything in order without perform. Both work. The mistake is mixing them: an awaited external promise sitting among unawaited browser commands puts you back in the first example.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 2: raise asyncHookTimeout, or the solve times out at ten seconds<\/h2>\n<p>This is the one that wastes an afternoon. Asynchronous execution inside perform is bounded by the asyncHookTimeout global, and that default is 10000 milliseconds. A reCAPTCHA solve regularly takes fifteen to forty-five seconds. So the callback is killed while the solver is still working, and the error you get talks about a timeout rather than about the CAPTCHA.<\/p>\n<p>Raise it in your Nightwatch config, globally or per environment.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\/\/ nightwatch.conf.js\nmodule.exports = {\n  globals: {\n    \/\/ Default is 10000, which is shorter than most solves.\n    asyncHookTimeout: 120000,\n\n    \/\/ waitFor commands default to 5000. The widget is not\n    \/\/ the slow part, but give it room on a cold CI runner.\n    waitForConditionTimeout: 15000,\n  },\n};<\/pre>\n<\/div>\n<p>Set the client side to sit under it, so one ceiling is clearly the effective one. The Node client polls on its own schedule, starting at 250 milliseconds and backing off to pollingInterval, which is why it usually returns sooner than a hand-written loop.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\/\/ npm install capskip\nconst { CapSkip } = require(&quot;capskip&quot;);\n\nconst solver = new CapSkip({\n  host: process.env.CAPSKIP_HOST || &quot;127.0.0.1&quot;,\n  port: 8080,\n  \/\/ Seconds. Keep this under the 120s asyncHookTimeout above.\n  recaptchaTimeout: 90,\n  pollingInterval: 3,\n});<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 3: inject the token with execute, not setValue<\/h2>\n<p>The response field reCAPTCHA reads is a hidden textarea. WebDriver refuses to interact with elements it considers non-interactable, so setValue on it fails with an element not interactable error. Injecting through the page is the normal answer, and Nightwatch exposes it as execute, which takes a function body, an argument array and an optional callback.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">browser.perform(async function () {\n  const { code } = await solver.recaptcha(sitekey, pageUrl);\n\n  \/\/ The function is serialized and run in the page, so it\n  \/\/ cannot close over anything. Pass values in the array.\n  await browser.execute(\n    function (token) {\n      document.getElementById(&quot;g-recaptcha-response&quot;).value = token;\n    },\n    [code]\n  );\n});<\/pre>\n<\/div>\n<p>If the form runs a callback on completion rather than reading the field at submit time, call it in the same script. Invisible reCAPTCHA almost always works that way, and the widget variants only change the options you pass: invisible or enterprise set to 1, version set to v3 with an action, or turnstile and geetest instead of recaptcha. The full surface is on <a href=\"https:\/\/capskip.com\/nodejs-captcha-solver\/\">the Node.js CAPTCHA solver page<\/a>.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 4: the full test<\/h2>\n<p>The client is constructed once at module scope, outside the test, so it is not rebuilt per test case. The sitekey is read from the page rather than hardcoded, which is what makes the same spec run against staging and production.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\/\/ npm install capskip\nconst { CapSkip } = require(&quot;capskip&quot;);\n\nconst solver = new CapSkip({\n  host: process.env.CAPSKIP_HOST || &quot;127.0.0.1&quot;,\n  port: 8080,\n  recaptchaTimeout: 90,\n});\n\nconst PAGE = &quot;https:\/\/example.com\/page-with-recaptcha&quot;;\n\ndescribe(&quot;signup&quot;, function () {\n  it(&quot;submits through the reCAPTCHA&quot;, async function (browser) {\n    await browser.url(PAGE);\n    await browser.waitForElementPresent(&quot;.g-recaptcha&quot;, 15000);\n\n    \/\/ Read the sitekey off the widget that is actually there.\n    const sitekey = await browser.getAttribute(\n      &quot;.g-recaptcha&quot;,\n      &quot;data-sitekey&quot;\n    );\n\n    await browser.perform(async function () {\n      const { code } = await solver.recaptcha(sitekey.value, PAGE);\n      await browser.execute(\n        function (token) {\n          document.getElementById(&quot;g-recaptcha-response&quot;).value = token;\n        },\n        [code]\n      );\n    });\n\n    \/\/ Submit straight after. The token is not a long-lived value.\n    await browser.click(&quot;#submit&quot;);\n    await browser.assert.textContains(&quot;.result&quot;, &quot;Thanks&quot;);\n  });\n});<\/pre>\n<\/div>\n<p>Solve as late as you can and submit immediately. A reCAPTCHA token is good for around two minutes, and a suite that solves in a before hook and submits three test cases later is submitting an expired one. That window is worth reading once: <a href=\"https:\/\/capskip.com\/recaptcha-token-expiration\/\">how long a reCAPTCHA token lasts<\/a>.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 5: running it on CI, and which connection mode that needs<\/h2>\n<p>Nightwatch tests rarely stay on the machine that wrote them. The moment they move to a CI runner, a container or a Selenium Grid node, loopback stops meaning your desk. There are two connection modes. Local binds to 127.0.0.1 and answers that device only. Server binds to your network address or public IP, so a runner, a container or a grid node can reach the same Windows machine over the API. Both live under <a href=\"https:\/\/capskip.com\/setup-guide\/#connection-settings\">connection settings<\/a>, and Server mode only changes which address the solver listens on. It is still your hardware and it is still unmetered.<\/p>\n<table>\n<thead>\n<tr>\n<th>Where the test process runs<\/th>\n<th>Which connection mode<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Your own machine, chromedriver local<\/td>\n<td>Local mode. 127.0.0.1 is genuinely correct<\/td>\n<\/tr>\n<tr>\n<td>A build agent on your network<\/td>\n<td>Server mode with the solver&#8217;s LAN address<\/td>\n<\/tr>\n<tr>\n<td>A hosted CI runner<\/td>\n<td>Server mode with a static public IP and a firewall rule<\/td>\n<\/tr>\n<tr>\n<td>A container, solver on the host<\/td>\n<td>Server mode. Loopback inside a container is the container<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>One distinction that trips people up on Grid: the solver call is made by the test process, not by the browser. So the address that matters is the one the Node process can reach, and the grid node&#8217;s networking is irrelevant to it. That split is worked through in <a href=\"https:\/\/capskip.com\/selenium-grid-captcha\/\">the Selenium Grid guide<\/a>, and the driver layer underneath both is covered on <a href=\"https:\/\/capskip.com\/selenium-captcha-solver\/\">the Selenium CAPTCHA solver page<\/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>The solve is logged before the page navigates<\/td>\n<td>The call sits outside the queue, so it runs while the queue is built<\/td>\n<td>Wrap it in browser.perform<\/td>\n<\/tr>\n<tr>\n<td>A timeout at almost exactly ten seconds<\/td>\n<td>asyncHookTimeout is still at its 10000 default<\/td>\n<td>Raise it in globals, above the client timeout<\/td>\n<\/tr>\n<tr>\n<td>Element not interactable on the response field<\/td>\n<td>It is a hidden textarea and WebDriver will not type into it<\/td>\n<td>Set the value with browser.execute<\/td>\n<\/tr>\n<tr>\n<td>The token is rejected although the test passed<\/td>\n<td>It expired between the solve and the submit<\/td>\n<td>Solve immediately before submitting, not in a hook<\/td>\n<\/tr>\n<tr>\n<td>NetworkException once the suite moves to CI<\/td>\n<td>The solver is not on the runner<\/td>\n<td>Server mode, and set CAPSKIP_HOST on the runner<\/td>\n<\/tr>\n<tr>\n<td>ERROR_GOOGLEKEY inside an ApiException<\/td>\n<td>The sitekey came from the wrong widget or an iframe URL<\/td>\n<td>Read data-sitekey off the element you are solving<\/td>\n<\/tr>\n<tr>\n<td>CAPCHA_NOT_READY from a hand-rolled poll<\/td>\n<td>The result was read before it was finished<\/td>\n<td>Let the client poll. It backs off on its own<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>That last response is spelled the way it looks, and the missing letter is not a typo on our side, because the API really does return it that way. It is explained in full in <a href=\"https:\/\/capskip.com\/capcha-not-ready\/\">the CAPCHA_NOT_READY guide<\/a>.<\/p>\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 Nightwatch plugin for this?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">No. The solver is an ordinary Node package that you require in the spec file, so there is no custom command to register and nothing to add to the plugins array. If you find yourself writing one, the only thing worth wrapping is the perform plus execute pair, which is about eight lines and saves repeating them across specs.<\/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 solve once in a global before hook and reuse the token?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">No, for two separate reasons. The token expires in about two minutes, which a suite of any size will outlive. And it is tied to the page load that produced it, so a second test case loading the page again needs its own. Solve per test case, as late in it as possible. Because the solver is unmetered, doing that costs nothing but a few seconds of wall clock.<\/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;\">My tests run in parallel. Does that change anything?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Only the arithmetic. Each worker has its own queue and its own solving call, so four workers means four concurrent solves. Nothing needs coordinating, and nothing queues behind a shared balance, because the ceiling is the machine running CapSkip rather than a credit count. Raise asyncHookTimeout a little if the box is doing four at once on a slow day.<\/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;\">How is this different from doing it in WebdriverIO?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">WebdriverIO resolves its commands where you write them, so a solving call between two of them lands in the right place without ceremony. Nightwatch queues, which is why perform exists and why this post spends a section on it. Everything after the token is identical in both. The WebdriverIO version is worked through in <a href=\"https:\/\/capskip.com\/webdriverio-captcha\/\">the WebdriverIO guide<\/a>.<\/p>\n<\/details>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">The short version<\/h2>\n<p>Put the solve inside browser.perform, because anything outside the queue runs while the queue is being built rather than when you meant it to. Raise asyncHookTimeout above the client&#8217;s own timeout, because the 10000 default is shorter than a solve. Inject the token with execute, because the response field is hidden. Submit straight after solving. Set CAPSKIP_HOST from the environment and run CapSkip in Server mode anywhere the tests are not on the solver&#8217;s own machine.<\/p>\n<ul>\n<li>The raw endpoints behind the client are documented in <a href=\"https:\/\/capskip.com\/api-docs\/\">the CapSkip API documentation<\/a>.<\/li>\n<li>The checkbox challenge itself is explained on <a href=\"https:\/\/capskip.com\/recaptcha-v2-solver\/\">the reCAPTCHA v2 solver page<\/a>.<\/li>\n<\/ul>\n<p>Worth knowing before you add a solve to every spec in the suite: CapSkip is a <a href=\"https:\/\/capskip.com\/\">local captcha solver<\/a>, so a test run that solves two hundred times costs exactly what a test run that solves once costs.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Nightwatch \u4f1a\u628a\u6d4f\u89c8\u5668\u547d\u4ee4\u6392\u5165\u961f\u5217\uff0c\u800c\u4e0d\u662f\u5728\u4f60\u5199\u4e0b\u5b83\u4eec\u7684\u4f4d\u7f6e\u6267\u884c\uff0c\u56e0\u6b64\u8bc6\u522b\u8c03\u7528\u4f1a\u5728\u9875\u9762\u5c1a\u672a\u52a0\u8f7d\u65f6\u5c31\u89e6\u53d1\u3002\u672c\u6587\u8bb2\u6e05\u695a\u8bc6\u522b\u4ee3\u7801\u8be5\u653e\u5728\u54ea\u91cc\uff0c\u4ee5\u53ca\u4f60\u5fc5\u987b\u8c03\u9ad8\u7684\u90a3\u4e00\u4e2a\u8d85\u65f6\u8bbe\u7f6e\u3002<\/p>","protected":false},"author":1,"featured_media":25667,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Nightwatch CAPTCHA: Solve Inside the Queue | CapSkip","rank_math_description":"A nightwatch captcha solve fires too early unless it sits inside the command queue, and it outlives the 10 second async hook timeout. Both fixed here.","rank_math_focus_keyword":"nightwatch captcha","footnotes":""},"categories":[70],"tags":[],"class_list":["post-25668","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\/25668","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=25668"}],"version-history":[{"count":1,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/25668\/revisions"}],"predecessor-version":[{"id":25672,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/25668\/revisions\/25672"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media\/25667"}],"wp:attachment":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media?parent=25668"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/categories?post=25668"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/tags?post=25668"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}