Nightwatch.js में कैप्चा कैसे हल करें (कमांड क्यू)

nightwatch captcha - How to Solve CAPTCHAs in Nightwatch.js (Command Queue)

Nightwatch में कैप्चा सॉल्व कमांड क्यू के अंदर चलना चाहिए, उसके बगल में नहीं। Nightwatch ब्राउज़र कमांड वहाँ नहीं चलाता जहाँ आपने उन्हें लिखा है। वह उन्हें क्यू में डालता है और आपका टेस्ट फ़ंक्शन लौटने के बाद क्यू को खाली करता है, इसलिए दो ब्राउज़र कमांड के बीच लिखी गई सॉल्व कॉल तुरंत चल जाती है, पेज लोड होने से पहले और विजेट बनने से पहले। इसका हल है browser.perform, और साथ में एक ग्लोबल जिसे आपको बढ़ाना ही पड़ेगा, क्योंकि सॉल्व में उन 10 सेकंड से ज़्यादा समय लगता है जितना Nightwatch किसी async कॉलबैक को देता है।

आपको क्या चाहिए

  • Nightwatch 3 और एक चलता हुआ ड्राइवर: या तो लोकल chromedriver, या कोई रिमोट WebDriver endpoint।
  • CapSkip किसी Windows मशीन पर चल रहा हो, और Node क्लाइंट उसी प्रोजेक्ट में इंस्टॉल हो जिसमें आपके टेस्ट हैं।
  • sitekey और पेज URL। sitekey को हार्डकोड करने के बजाय विजेट से पढ़ें, क्योंकि staging और production में यह शायद ही कभी एक जैसा होता है।
  • Server mode, जब भी टेस्ट सॉल्वर की अपनी मशीन के अलावा कहीं और चलें, और इसमें हर CI रनर शामिल है। यह कनेक्शन सेटिंग्स के नीचे बस एक सेटिंग है।
# npm install capskip
npm install --save-dev nightwatch
npm install capskip

स्टेप 1: सीधी-सादी सॉल्व कॉल बहुत जल्दी क्यों चल जाती है

Nightwatch टेस्ट में हर ब्राउज़र कमांड एक निर्देश है जो क्यू में जुड़ता है। टेस्ट फ़ंक्शन पहले ऊपर से नीचे तक चलकर वह क्यू बनाता है, और उसके बाद ही Nightwatch उसे चलाना शुरू करता है। बीच में लिखा साधारण JavaScript क्यू का हिस्सा नहीं होता, इसलिए वह क्यू बनने वाले दौर में ही चल जाता है। पूरी समस्या बस इसी एक वाक्य में है।

// WRONG. The solve starts while the queue is still being
// built, so it runs before browser.url() has navigated.
module.exports = {
  "signup form": function (browser) {
    browser.url("https://example.com/page-with-recaptcha");

    solver.recaptcha(sitekey, pageUrl).then((r) => {
      // fires first, against a page that is not open yet
    });

    browser.click("#submit");
  },
};

लक्षण उलझाने वाला है, क्योंकि कोई एरर नहीं आता। सॉल्व सफल होता है, टेस्ट कभी-कभी पास भी हो जाता है, और टोकन उस पेज लोड का होता है जो कभी हुआ ही नहीं। Nightwatch आपको अपना कोड क्यू में डालने का एक दस्तावेज़ी तरीका देता है: browser.perform, जिसके कॉलबैक को क्यू के हिस्से के तौर पर चलने वाला फ़ंक्शन बताया गया है।

// RIGHT. perform() queues the callback, so it runs in
// sequence with the commands either side of it.
browser.url("https://example.com/page-with-recaptcha");

browser.perform(async function () {
  const { code } = await solver.recaptcha(sitekey, pageUrl);
  return code;
});

browser.click("#submit");

दूसरा विकल्प async टेस्ट है। टेस्ट फ़ंक्शन को async घोषित करने पर API कमांड एक promise लौटाते हैं, इसलिए हर एक को await करने से perform के बिना ही क्रम बना रहता है। दोनों तरीके चलते हैं। गलती है दोनों को मिलाना: बिना await वाले ब्राउज़र कमांड के बीच बैठा हुआ कोई await किया गया बाहरी promise आपको फिर से पहले वाले उदाहरण पर ला खड़ा करता है।

स्टेप 2: asyncHookTimeout बढ़ाएँ, वरना सॉल्व 10 सेकंड पर टाइमआउट हो जाएगा

यही वह चीज़ है जो पूरी दोपहर बरबाद करा देती है। perform के अंदर चलने वाला asynchronous काम asyncHookTimeout ग्लोबल से बँधा होता है, और उसका डिफ़ॉल्ट 10000 मिलीसेकंड है। reCAPTCHA सॉल्व में अक्सर 15 से 45 सेकंड लगते हैं। इसलिए सॉल्वर के काम करते रहते ही कॉलबैक मार दिया जाता है, और जो एरर मिलता है वह कैप्चा की नहीं, टाइमआउट की बात करता है।

इसे अपने Nightwatch कॉन्फ़िग में बढ़ाएँ, चाहे ग्लोबल स्तर पर या हर एनवायरनमेंट के लिए अलग-अलग।

// nightwatch.conf.js
module.exports = {
  globals: {
    // Default is 10000, which is shorter than most solves.
    asyncHookTimeout: 120000,

    // waitFor commands default to 5000. The widget is not
    // the slow part, but give it room on a cold CI runner.
    waitForConditionTimeout: 15000,
  },
};

क्लाइंट की तरफ़ की सीमा इससे नीचे तय करें, ताकि साफ़ रहे कि असल में कौन सी सीमा लागू है। Node क्लाइंट अपनी ही रफ़्तार से पोल करता है, 250 मिलीसेकंड से शुरू होकर धीरे-धीरे pollingInterval तक जाता है, इसीलिए वह आमतौर पर हाथ से लिखे लूप से पहले जवाब लौटा देता है।

// npm install capskip
const { CapSkip } = require("capskip");

const solver = new CapSkip({
  host: process.env.CAPSKIP_HOST || "127.0.0.1",
  port: 8080,
  // Seconds. Keep this under the 120s asyncHookTimeout above.
  recaptchaTimeout: 90,
  pollingInterval: 3,
});

स्टेप 3: टोकन को setValue से नहीं, execute से इंजेक्ट करें

reCAPTCHA जिस response फ़ील्ड को पढ़ता है वह एक छिपा हुआ textarea है। WebDriver उन एलिमेंट से बातचीत करने से मना कर देता है जिन्हें वह non-interactable मानता है, इसलिए उस पर setValue, element not interactable एरर के साथ फेल हो जाता है। पेज के ज़रिए इंजेक्ट करना ही सामान्य जवाब है, और Nightwatch इसे execute के रूप में देता है, जो एक फ़ंक्शन बॉडी, एक आर्ग्यूमेंट ऐरे और एक वैकल्पिक कॉलबैक लेता है।

browser.perform(async function () {
  const { code } = await solver.recaptcha(sitekey, pageUrl);

  // The function is serialized and run in the page, so it
  // cannot close over anything. Pass values in the array.
  await browser.execute(
    function (token) {
      document.getElementById("g-recaptcha-response").value = token;
    },
    [code]
  );
});

अगर फ़ॉर्म submit के वक़्त फ़ील्ड पढ़ने के बजाय पूरा होने पर कोई कॉलबैक चलाता है, तो उसे उसी स्क्रिप्ट में कॉल करें। Invisible reCAPTCHA लगभग हमेशा इसी तरह काम करता है, और विजेट के अलग-अलग रूप सिर्फ़ वे ऑप्शन बदलते हैं जो आप भेजते हैं: invisible या enterprise को 1 करना, version को किसी action के साथ v3 करना, या recaptcha की जगह turnstile और geetest देना। पूरा दायरा यहाँ देखें: Node.js कैप्चा सॉल्वर पेज.

स्टेप 4: पूरा टेस्ट

क्लाइंट एक बार मॉड्यूल स्कोप पर, टेस्ट के बाहर बनाया जाता है, ताकि हर टेस्ट केस पर दोबारा न बने। sitekey हार्डकोड करने के बजाय पेज से पढ़ा जाता है, और यही वजह है कि वही स्पेक staging और production दोनों पर चल जाती है।

// npm install capskip
const { CapSkip } = require("capskip");

const solver = new CapSkip({
  host: process.env.CAPSKIP_HOST || "127.0.0.1",
  port: 8080,
  recaptchaTimeout: 90,
});

const PAGE = "https://example.com/page-with-recaptcha";

describe("signup", function () {
  it("submits through the reCAPTCHA", async function (browser) {
    await browser.url(PAGE);
    await browser.waitForElementPresent(".g-recaptcha", 15000);

    // Read the sitekey off the widget that is actually there.
    const sitekey = await browser.getAttribute(
      ".g-recaptcha",
      "data-sitekey"
    );

    await browser.perform(async function () {
      const { code } = await solver.recaptcha(sitekey.value, PAGE);
      await browser.execute(
        function (token) {
          document.getElementById("g-recaptcha-response").value = token;
        },
        [code]
      );
    });

    // Submit straight after. The token is not a long-lived value.
    await browser.click("#submit");
    await browser.assert.textContains(".result", "Thanks");
  });
});

जितनी देर से हो सके सॉल्व करें और तुरंत सबमिट करें। reCAPTCHA टोकन करीब दो मिनट तक चलता है, और जो सुइट before हुक में सॉल्व करके तीन टेस्ट केस बाद सबमिट करती है वह एक्सपायर्ड टोकन भेज रही होती है। यह समय-सीमा एक बार पढ़ लेने लायक है: reCAPTCHA token कितनी देर चलता है.

स्टेप 5: इसे CI पर चलाना, और उसके लिए कौन सा कनेक्शन मोड चाहिए

Nightwatch टेस्ट शायद ही कभी उसी मशीन पर टिके रहते हैं जहाँ वे लिखे गए थे। जैसे ही वे किसी CI रनर, कंटेनर या Selenium Grid नोड पर जाते हैं, loopback का मतलब आपकी अपनी डेस्क नहीं रह जाता। कनेक्शन के दो मोड हैं। Local mode 127.0.0.1 से बँधता है और सिर्फ़ उसी डिवाइस को जवाब देता है। Server mode आपके नेटवर्क पते या पब्लिक IP से बँधता है, ताकि कोई रनर, कोई कंटेनर या कोई ग्रिड नोड उसी Windows मशीन तक API के ज़रिए पहुँच सके। ये दोनों यहाँ मिलते हैं: कनेक्शन सेटिंग्स। Server mode सिर्फ़ यह बदलता है कि सॉल्वर किस पते पर सुनता है। हार्डवेयर अब भी आपका ही है और इस पर अब भी प्रति-हल कोई शुल्क नहीं लगता।

टेस्ट प्रोसेस कहाँ चलती हैकौन-सा कनेक्शन मोड
आपकी अपनी मशीन, लोकल chromedriverLocal mode। 127.0.0.1 सचमुच सही है
आपके नेटवर्क पर कोई build agentसॉल्वर के LAN पते के साथ Server mode
कोई होस्टेड CI रनरस्टैटिक पब्लिक IP और एक फ़ायरवॉल नियम के साथ Server mode
कोई कंटेनर, सॉल्वर होस्ट परServer mode। कंटेनर के अंदर loopback यानी वही कंटेनर

Grid पर एक फ़र्क़ अक्सर लोगों को उलझा देता है: सॉल्वर को कॉल ब्राउज़र नहीं, टेस्ट प्रोसेस करती है। इसलिए जो पता मायने रखता है वह वही है जहाँ तक Node प्रोसेस पहुँच सकती है, और ग्रिड नोड की नेटवर्किंग से इसका कोई लेना-देना नहीं। इस बँटवारे को विस्तार से यहाँ समझाया गया है: Selenium Grid गाइड। और इन दोनों के नीचे मौजूद ड्राइवर लेयर यहाँ कवर की गई है: Selenium कैप्चा सॉल्वर पेज.

आम errors और उनका मतलब

आप जो देखते हैंकारणफिक्स
पेज नेविगेट होने से पहले ही सॉल्व लॉग हो जाता हैकॉल क्यू के बाहर है, इसलिए वह क्यू बनते वक़्त ही चल जाती हैइसे browser.perform में लपेटें
ठीक करीब 10 सेकंड पर टाइमआउटasyncHookTimeout अब भी अपने 10000 डिफ़ॉल्ट पर हैइसे globals में, क्लाइंट टाइमआउट से ऊपर बढ़ाएँ
response फ़ील्ड पर element not interactableयह छिपा हुआ textarea है और WebDriver इसमें टाइप नहीं करेगावैल्यू browser.execute से सेट करें
टेस्ट पास होने के बावजूद टोकन अस्वीकार हो जाता हैवह solve और submit के बीच expire हो गयाहुक में नहीं, सबमिट करने से ठीक पहले सॉल्व करें
सुइट के CI पर जाते ही NetworkExceptionसॉल्वर रनर पर नहीं हैServer mode चालू करें, और रनर पर CAPSKIP_HOST सेट करें
ApiException के अंदर ERROR_GOOGLEKEYsitekey गलत विजेट से या किसी iframe URL से लिया गयाजिस एलिमेंट को हल कर रहे हैं उसी से data-sitekey पढ़ें
हाथ से लिखे पोल से CAPCHA_NOT_READYनतीजा पूरा होने से पहले ही पढ़ लिया गयाclient को poll करने दें। वह ख़ुद ही धीमा होता जाता है

यह आख़िरी रिस्पॉन्स वैसे ही लिखा जाता है जैसा दिखता है, और उसमें गायब अक्षर हमारी तरफ़ की गलती नहीं है, क्योंकि API सचमुच इसी वर्तनी में जवाब लौटाता है। इसकी पूरी व्याख्या यहाँ है: CAPCHA_NOT_READY गाइड.

FAQ

क्या इसके लिए मुझे कोई Nightwatch प्लगइन चाहिए?

नहीं। सॉल्वर एक साधारण Node पैकेज है जिसे आप स्पेक फ़ाइल में require करते हैं, इसलिए न कोई कस्टम कमांड रजिस्टर करनी है और न plugins ऐरे में कुछ जोड़ना है। अगर आप फिर भी एक लिखना चाहें, तो लपेटने लायक बस perform और execute की जोड़ी है, जो करीब आठ लाइन की होती है और उन्हें हर स्पेक में दोहराने से बचा देती है।

क्या मैं ग्लोबल before हुक में एक बार सॉल्व करके वही टोकन दोबारा इस्तेमाल कर सकता हूँ?

नहीं, दो अलग वजहों से। टोकन करीब दो मिनट में एक्सपायर हो जाता है, और थोड़ी भी बड़ी सुइट इससे ज़्यादा चलती है। दूसरी बात, वह उसी पेज लोड से बँधा होता है जिसने उसे बनाया था, इसलिए पेज दोबारा लोड करने वाले अगले टेस्ट केस को अपना अलग टोकन चाहिए। हर टेस्ट केस में, और उसमें भी जितनी देर से हो सके, सॉल्व करें। सॉल्वर पर प्रति-हल कोई शुल्क नहीं लगता, इसलिए ऐसा करने में कुछ सेकंड के अलावा कुछ भी ख़र्च नहीं होता।

मेरे टेस्ट समानांतर चलते हैं। क्या इससे कुछ बदलता है?

सिर्फ़ गणित। हर वर्कर का अपना क्यू और अपनी सॉल्व कॉल होती है, इसलिए चार वर्कर का मतलब है चार सॉल्व एक साथ। इसमें कुछ भी तालमेल बिठाने की ज़रूरत नहीं, और कुछ भी किसी साझा बैलेंस के पीछे कतार में नहीं लगता, क्योंकि सीमा CapSkip चलाने वाली मशीन है, न कि कोई क्रेडिट गिनती। अगर मशीन किसी सुस्त दिन में चार सॉल्व एक साथ कर रही हो, तो asyncHookTimeout थोड़ा और बढ़ा दें।

WebdriverIO में यही काम करने से यह कितना अलग है?

WebdriverIO अपनी कमांड वहीं हल करता है जहाँ आप उन्हें लिखते हैं, इसलिए दो कमांड के बीच रखी सॉल्व कॉल बिना किसी ताम-झाम के सही जगह पर बैठ जाती है। Nightwatch क्यू बनाता है, इसीलिए perform मौजूद है और इसीलिए इस पोस्ट में उस पर एक पूरा हिस्सा है। टोकन मिलने के बाद का सब कुछ दोनों में एक जैसा है। WebdriverIO वाला तरीका यहाँ विस्तार से दिया गया है: WebdriverIO गाइड.

संक्षेप में

सॉल्व को browser.perform के अंदर रखें, क्योंकि क्यू के बाहर की हर चीज़ तब चलती है जब क्यू बन रहा होता है, तब नहीं जब आपका इरादा था। asyncHookTimeout को क्लाइंट के अपने टाइमआउट से ऊपर बढ़ाएँ, क्योंकि 10000 का डिफ़ॉल्ट एक सॉल्व से छोटा है। टोकन को execute से इंजेक्ट करें, क्योंकि response फ़ील्ड छिपा हुआ है। सॉल्व करते ही तुरंत सबमिट करें। CAPSKIP_HOST को एनवायरनमेंट से सेट करें, और जहाँ भी टेस्ट सॉल्वर की अपनी मशीन पर नहीं चल रहे, वहाँ CapSkip को Server mode में चलाएँ।

सुइट की हर स्पेक में सॉल्व जोड़ने से पहले यह जान लेना काम का है। CapSkip है एक लोकल captcha सॉल्वर, इसलिए 200 बार सॉल्व करने वाले टेस्ट रन की लागत ठीक उतनी ही है जितनी एक बार सॉल्व करने वाले टेस्ट रन की।