Cypress Testlerinde cy.task ile CAPTCHA Nasıl Çözülür

cypress captcha - How to Solve CAPTCHA in Cypress Tests With cy.task

Bir Cypress captcha sorunu, bir çözme sorunu olmadan önce bir çalışma zamanı sorunudur. Spec kodunuz test edilen tarayıcının içinde çalışır; dolayısıyla çözücüyle konuşan Node istemcisi orada yaşayamaz. Bunun yerine onu cypress.config.js içinde bir task olarak kaydedin, bu task'ı spec'ten çağırın ve token'ı gizli alana kendiniz yazın. Üç şey bunu çalışır kılar: task, yükseltilmiş bir zaman aşımı ve Cypress tıklaması yerine doğrudan bir DOM yazımı. Bu kılavuz üçünü de ele alıyor.

Neye ihtiyacınız var

  • Cypress 10 veya daha yenisi; cypress.config.js ve setupNodeEvents bu sürümle geldi. Daha eski sürümler eski plugins dosyasını kullanır ve aynı fikir yine geçerlidir
  • Node.js 18 veya daha yenisi
  • CapSkip'in çalışıyor ve erişilebilir olması. Local mod, aynı makinedeki bir test çalıştırması için 127.0.0.1 üzerinde 8080 portunu dinler; Server mod ise ağınızı veya genel IP'nizi dinler, böylece bir CI runner'ı ya da başka bir makine ona çağrı yapabilir. Her ikisi de şurada anlatılıyor: bağlantı ayarları
  • Dev bağımlılığı olarak kurulmuş çözücü istemcisi
# The client only ever runs in the Node half of Cypress.
npm install --save-dev capskip

Çözüm işlemi neden spec içinde olamaz

Cypress iki sürece ayrılır ve saf yaklaşımın başarısız olmasının bütün nedeni budur. Spec dosyanız paketlenir ve uygulamanın yanında, tarayıcının içinde çalıştırılır. cypress.config.js içindeki her şey ise onun dışında, Node'da çalışır.

Bu nedenle çözücü istemcisini bir spec'in en üstünde require etmek, bir Node HTTP istemcisini tarayıcı paketinin içine çeker. Paketleyici buna izin verse bile tarayıcı çağrıyı engeller: uygulamanızın kaynağından 127.0.0.1 üzerindeki 8080 portuna yapılan bir istek çapraz kaynaklıdır ve çözücü bunu meşru kılacak CORS başlıklarını göndermez.

Cypress size Node'a açılan iki kapı sunar ve ikisi de uygundur:

  • cy.task yapılandırmada kaydettiğiniz herhangi bir fonksiyonu çalıştırır. SDK'nın yeri burasıdır, çünkü yoklama ve geri çekilme mantığı böylece tasarlandığı yerde, Node içinde çalışır.
  • cy.request HTTP çağrısını tarayıcı yerine Cypress Node sürecinden yapar; Cypress dokümantasyonunun onun CORS'u tamamen atladığını söylemesinin nedeni budur. Ham API'ye gitmeyi ve bağımlılıktan kurtulmayı tercih ediyorsanız iyi bir seçenek.

Adım 1: çözümü bir task olarak kaydedin

Tek bir fonksiyon, bir kez kaydedilir, her spec'te kullanılabilir.

// npm install --save-dev capskip
const { defineConfig } = require('cypress');
const { CapSkip } = require('capskip');

// Local mode. Point host at a server IP to share one solver.
const solver = new CapSkip({ host: '127.0.0.1', port: 8080 });

module.exports = defineConfig({
  // 60000 is the default, and a v2 solve can outlast it.
  taskTimeout: 180000,

  e2e: {
    setupNodeEvents(on) {
      on('task', {
        async solveRecaptcha({ sitekey, url }) {
          const result = await solver.recaptcha(sitekey, url);
          return result.code;   // the token
        },
      });
    },
  },
});

Task'lar hakkında ilk seferde herkese bir saate mal olan bir kural: bir task ya bir değer ya da null döndürmelidir, asla undefined değil. Return ifadesini unutun, Cypress komutu task'ın undefined döndürdüğüne dair bir mesajla başarısız kılar; bu da çözücü hiç çağrılmamışken çözücü bozulmuş gibi okunur.

Adım 2: task'ı çağırın ve token'ı enjekte edin

Sitekey'i sayfadan okuyun, task'a verin ve cevabı widget'ın koyacağı yere koyun.

// cypress/e2e/login.cy.js
it('logs in through the reCAPTCHA', () => {
  cy.visit('/login');

  cy.get('[data-sitekey]')
    .invoke('attr', 'data-sitekey')
    .then((sitekey) => {
      const url = 'https://example.com/login';

      cy.task('solveRecaptcha', { sitekey, url }).then((token) => {
        // The widget writes into a hidden textarea. Do the same.
        cy.document().then((doc) => {
          doc.getElementById('g-recaptcha-response').value = token;
        });
      });
    });

  cy.get('button[type=submit]').click();
  cy.contains('Welcome back');
});

DOM yazımına dikkat edin. Yanıt alanı gizli bir textarea'dır ve Cypress görünmez saydığı bir öğeye yazmayı reddeder; bu yüzden get ve type çağrılarıyla yazılan bariz sürüm, token'a hiç yaklaşamadan görünürlükte başarısız olur. cy.document üzerinden gitmek bunu aşar, tıpkı widget’ın kendi JavaScript'inin yapacağı gibi.

Widget div'i bir data-callback özniteliği taşıyorsa, gönder düğmesine tıklamak yerine o fonksiyonu token ile çağırın. Bu şekilde kurulan sayfalar normal bir form gönderimi bağlamaz, dolayısıyla tıklama hiçbir şey yapmaz.

Asıl canınızı yakan zaman aşımı taskTimeout

En sık çözücüye yüklenen hata budur ve düzeltmesi yapılandırmada tek satırdır.

Cypress bir task'a varsayılan olarak 60 saniye verir. Bir reCAPTCHA v2 işi ilk 15 ila 20 saniye boyunca hazır olmaz, v3 ise 10 ila 15 saniye sürer ve meşgul bir makine ikisini de uzatabilir. Üst sınıra ulaşıldığında Cypress komutu sonlandırır ve test, CAPTCHA'yı değil sizin task'ınızı adlandıran bir zaman aşımıyla başarısız olur.

Yanı başındaki tuzak, yanlış sayıyı yükseltmektir. Cypress zaman aşımı önerilerinin çoğu, 4000 milisaniye olan ve DOM komutlarını yöneten defaultCommandTimeout değerini işaret eder. Bunun bir task üzerinde hiçbir etkisi yoktur. Burada üç değer önemlidir ve üçü de birbirinden ayrıdır:

SeçenekVarsayılanŞunlar için geçerli
taskTimeout60000 mscy.task, yani çözüm işlemi
responseTimeout30000 mscy.request, yani ham bir API çağrısı
defaultCommandTimeout4000 msDOM komutları, yukarıdakilerin ikisi de değil

Yukarıdaki yapılandırmada olduğu gibi genel olarak ayarlayın ya da yalnızca tek bir testin bu alana ihtiyacı varsa çağrı başına ayarlayın:

// Same task, a longer leash for this one call.
cy.task('solveRecaptcha', { sitekey, url }, { timeout: 180000 });

180 saniye makul bir üst sınırdır. Normal bir çözümün kabaca on katıdır ve bilinçli olarak SDK’nin kendi 300 saniyelik reCAPTCHA yoklama sınırının altında durur; böylece Cypress, hâlâ bekleyen bir istemcinin arkasında asılı kalmak yerine gerçekten takılmış bir testi başarısız kılar. İstemcinin önce pes etmesini tercih ederseniz, recaptchaTimeout değerini task zaman aşımınızın altına indirin.

Ya da SDK'yı atlayıp cy.request kullanın

API 2captcha uyumludur, bu yüzden iki çağrı bütün işi görür. cy.request Node'da çalıştığı için tarayıcının kaynak kuralları hiç devreye girmez.

// No task registration needed. Both calls happen in Node.
function pollForToken(id, tries = 20) {
  return cy.request({
    method: 'POST',
    url: 'http://127.0.0.1:8080/res.php',
    form: true,
    body: { key: 'capskip', action: 'get', id },
  }).then((res) => {
    const text = res.body.trim();
    if (text !== 'CAPCHA_NOT_READY') return text.replace('OK|', '');
    if (tries === 0) throw new Error('gave up waiting for ' + id);
    return cy.wait(5000).then(() => pollForToken(id, tries - 1));
  });
}

Aklınızda tutmanız gereken iki ayrıntı. res.php’den gelen düz metin yanıtı, başarı durumunda OK|TOKEN olur; iş hâlâ çalışırken ise yalnızca CAPCHA_NOT_READY dizesidir, ki bu bir hata değil bir durumdur. Ayrıca bir sonuç yalnızca bir kez okunabilir, bu yüzden iki kez sormak yerine geldiği anda saklayın. Her parametre ve her hata dizesi şurada listelenmiştir: API dokümantasyonu.

Çözücü yerinde kalırken testleri CI'da çalıştırmak

Dizüstünüzde geçen bir test paketinin ilk push'ta başarısız olduğu yer burasıdır. Bir GitHub Actions runner'ı, bir GitLab işi ya da bir Jenkins agent'ı kendi loopback adresine sahiptir ve orada 8080 portunu dinleyen hiçbir şey yoktur. Local mod, tanımı gereği makineye özeldir.

Cevap Server mod. CapSkip, loopback yerine ağınızı veya genel IP'nizi dinler ve yapılandırma adresi ortamdan okur.

// npm install --save-dev capskip
const { CapSkip } = require('capskip');

// Same client, different address. The spec never changes.
const solver = new CapSkip({
  host: process.env.CAPSKIP_HOST || '127.0.0.1',
  port: Number(process.env.CAPSKIP_PORT || 8080),
});

SDK, CAPSKIP_HOST ve CAPSKIP_PORT değerlerini kendi başına okur; dolayısıyla yukarıdaki yedek değer, bunlar olmadan başlayan bir runner için fazladan bir önlemdir. Çözücü makinesi için statik bir genel IP önerilir ve adımlar şurada: bağlantı ayarları. Bu hâlâ sizin donanımınızdır ve kullanım hâlâ ölçülmez: değişen tek şey, sürecin nerede dinlediğidir.

Sık görülen hatalar ve anlamları

BelirtiNedenDüzeltme
solveRecaptcha task'ı kaydedilmediYanlış blokta kaydedilmiş ya da yapılandırma hiç export edilmemişe2e anahtarı için setupNodeEvents içinde kaydedin
Task'ınız için 60000 ms beklendikten sonra zaman aşımıtaskTimeout hâlâ varsayılan değerindeGenel olarak ya da çağrı başına 180000'e yükseltin
Task undefined döndürdüİşleyicide return ifadesi yokToken'ı döndürün, hiçbir şey yoksa null döndürün
Öğe görünür değil, bu yüzden Cypress yazamıyorYanıt alanı gizli bir textareaDeğeri bunun yerine cy.document üzerinden yazın
ERROR_GOOGLEKEYSitekey boştu ya da bir Turnstile widget'ına aitÇözmeden önce özniteliği loglayın; Turnstile'ın kendi yöntemi var
Her testte NetworkExceptionO host ve portta dinleyen hiçbir şey yokLocal mod yalnızca loopback'tir; CI'dan Server modu kullanın
Yerelde yeşil, CI'da kırmızıRunner, makinenizin loopback adresine erişemezCAPSKIP_HOST değerini erişilebilir bir adrese yöneltin

Sık sorulan sorular

Test ortamımda CAPTCHA'yı kapatsam olmaz mı?

Widget sizinse, evet. Staging derlemesinde bir özellik bayrağı ya da bir test sitekey'i, çözmekten daha ucuz ve daha hızlıdır ve test paketini deterministik tutar. Çözmek üç durumda hak ettiği yeri bulur: CAPTCHA başkasına aitse, staging'in üretimi birebir yansıtması gerekiyorsa ya da test edilen şeyin kendisi doğrulama yolu ise. CAPTCHA demo sayfaları üçüncü durum için kullanışlıdır, çünkü bir spec'i gerçek gibi davranan bir widget'a yöneltebilirsiniz.

Bu, Cypress bileşen testlerinde çalışır mı?

İşe yarar biçimde değil. Bir bileşen testi, arkasında gerçek bir sayfa ve sunucu olmadan bir bileşeni mount eder; dolayısıyla token'ın karşısında doğrulanacağı bir şey yoktur. Erişilebilir olmasını istiyorsanız task'ı component anahtarı altında kaydedin, ancak doğrulama işini, gönderilecek gerçek bir isteğin bulunduğu uçtan uca spec'lerde tutun.

Cypress Cloud'a kaydedilen bir çalıştırma çözücüye erişebilir mi?

Cypress Cloud sonuçları kaydeder, testlerinizi çalıştırmaz; dolayısıyla soru aslında tarayıcıyı hangi makinenin çalıştırdığıyla ilgilidir. Dizüstünüzde bu Local moddur. Barındırılan bir runner'da Server mod ve çözücünün adresine giden bir yol gerekir; kayıt her iki durumda da aynı şekilde çalışır.

Paralel bir Cypress çalıştırması aynı anda kaç çözüm gönderebilir?

Kaç spec dosyanız çalışıyorsa o kadar. Her Cypress süreci kendi istemcisini tutar ve kendi task'ını bekler; dolayısıyla istemci tarafında yapılandırılacak bir şey yoktur. SDK 250 milisaniyede yoklamaya başlar ve pollingInterval üst sınırına doğru kademeli olarak açılır; bu da aynı anda birkaç iş varken bile hızlı bir çözümü hızlı tutar. İş, sahibi olduğunuz donanımda gerçekleştiği için makine eklemek bir faturalandırma değil, bir kapasite kararıdır.

Kısa özet

İstemciyi cypress.config.js içine koyun, onu bir task olarak açın, taskTimeout değerini 180000'e yükseltin ve token'ı cy.document üzerinden gizli alana yazın. Entegrasyonun tamamı budur; bozulan kısımlar ise altındaki iki Cypress kuralıdır: spec kodu tarayıcı kodudur ve bir task ya döndürür ya da başarısız olur.

Çözücüyü kendiniz çalıştırmak, kararsız bir doğrulamayı bütçelemek yerine yeniden denemeyi makul kılar. Bu gerekçe, onu nereye koyarsanız koyun geçerlidir: bir captcha çözücü, ister bir test paketinde ister üretimde olsun. İstemci seçenekleri ayrıntılı olarak şurada ele alınıyor: Node.js entegrasyon kılavuzu. Cypress'in diğer tüm runner'larla paylaştığı tarayıcı tarafındaki ayrıntılar ise şurada: Playwright kılavuzu.