Cara Memecahkan CAPTCHA di Tes Cypress dengan cy.task

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

Masalah captcha di Cypress adalah masalah runtime sebelum menjadi masalah pemecahan. Kode spec Anda berjalan di dalam browser yang sedang diuji, jadi klien Node yang berbicara dengan pemecah tidak bisa berada di sana. Daftarkan klien itu sebagai task di cypress.config.js saja, lalu panggil task tersebut dari spec, dan tulis sendiri token ke kolom tersembunyi. Tiga hal membuatnya bekerja: task-nya, timeout yang dinaikkan, dan penulisan DOM langsung alih-alih klik Cypress. Panduan ini membahas ketiganya.

Apa yang Anda butuhkan

  • Cypress 10 atau lebih baru, versi tempat cypress.config.js dan setupNodeEvents mulai ada. Versi yang lebih lama memakai file plugins lama dan idenya tetap sama
  • Node.js 18 atau lebih baru
  • CapSkip berjalan dan dapat dijangkau. Mode Local mendengarkan di 127.0.0.1 port 8080 untuk pengujian yang dijalankan di mesin yang sama, dan Mode Server mendengarkan di jaringan atau IP publik Anda sehingga runner CI atau mesin lain bisa memanggilnya. Keduanya ada di pengaturan koneksi
  • Klien pemecah, dipasang sebagai dev dependency
# The client only ever runs in the Node half of Cypress.
npm install --save-dev capskip

Mengapa pemecahan tidak bisa ditaruh di dalam spec

Cypress terbagi menjadi dua proses dan inilah seluruh alasan versi naifnya gagal. File spec Anda di-bundle dan dijalankan di dalam browser, bersebelahan dengan aplikasinya. Semua yang ada di cypress.config.js berjalan di Node, di luar browser itu.

Karena itu, melakukan require pada klien pemecah di bagian atas sebuah spec akan menarik klien HTTP Node ke dalam bundle browser. Bahkan ketika bundler meloloskannya, browser kemudian memblokir panggilan tersebut: permintaan dari origin aplikasi Anda ke 127.0.0.1 pada port 8080 bersifat cross origin, dan pemecah tidak mengirim header CORS yang membuatnya sah.

Cypress memberi Anda dua pintu menuju Node, dan keduanya sama-sama baik:

  • cy.task menjalankan fungsi sembarang yang Anda daftarkan di config. Di sinilah tempat SDK seharusnya berada, karena logika polling dan backoff-nya kemudian berjalan di Node, tempat logika itu memang dirancang untuk berjalan.
  • cy.request melakukan panggilan HTTP dari proses Node Cypress, bukan dari browser, dan itulah sebabnya dokumentasi Cypress menyebut perintah ini melewati CORS sepenuhnya. Cocok jika Anda lebih suka memanggil API mentah dan melewatkan dependency-nya.

Langkah 1: daftarkan pemecahan sebagai sebuah task

Satu fungsi, didaftarkan sekali, tersedia untuk setiap spec.

// 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
        },
      });
    },
  },
});

Satu aturan tentang task yang membuat semua orang kehilangan satu jam pada percobaan pertama: sebuah task harus mengembalikan sebuah nilai atau null, tidak pernah undefined. Lupakan pernyataan return dan Cypress akan menggagalkan perintahnya dengan pesan bahwa task mengembalikan undefined, yang terbaca seolah pemecahnya rusak padahal pemecah itu tidak pernah dipanggil.

Langkah 2: panggil task dan suntikkan token

Baca sitekey dari halaman, serahkan ke task, lalu letakkan jawabannya di tempat yang seharusnya diisi oleh widget.

// 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');
});

Perhatikan penulisan DOM-nya. Kolom respons adalah textarea tersembunyi, dan Cypress menolak mengetik ke elemen yang dianggapnya tidak terlihat, jadi versi paling jelas dengan panggilan get dan type gagal pada pemeriksaan visibilitas sebelum sempat mendekati token. Melewati cy.document menghindari hal itu, persis seperti yang dilakukan JavaScript milik widget.

Jika div widget membawa atribut data-callback, panggil fungsi itu dengan token alih-alih mengklik tombol submit. Halaman yang dibangun seperti itu tidak pernah menyiapkan submit formulir biasa, jadi kliknya tidak melakukan apa-apa.

Timeout yang benar-benar jadi masalah adalah taskTimeout

Inilah kegagalan yang paling sering ditimpakan pada pemecah, dan perbaikannya hanya satu baris di config.

Cypress memberi sebuah task 60 detik secara bawaan. Job reCAPTCHA v2 belum siap selama 15 sampai 20 detik pertama, v3 butuh 10 sampai 15 detik, dan mesin yang sibuk bisa memperpanjang keduanya. Ketika batas itu tercapai, Cypress menghentikan perintahnya dan tesnya gagal dengan timeout yang menyebut nama task Anda, bukan CAPTCHA-nya.

Jebakan di sebelahnya adalah menaikkan angka yang salah. Sebagian besar saran timeout Cypress mengarah ke defaultCommandTimeout, yang bernilai 4000 milidetik dan mengatur perintah DOM. Nilai itu tidak berpengaruh pada sebuah task. Ada tiga nilai yang penting di sini dan ketiganya terpisah:

OpsiBawaanBerlaku untuk
taskTimeout60000 mscy.task, jadi pemecahannya
responseTimeout30000 mscy.request, jadi panggilan API mentah
defaultCommandTimeout4000 msPerintah DOM, bukan salah satu dari keduanya di atas

Atur secara global seperti pada config di atas, atau per panggilan ketika hanya satu tes yang butuh kelonggaran:

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

180 detik adalah batas yang masuk akal. Angka itu kira-kira sepuluh kali pemecahan normal, dan sengaja ditempatkan di bawah batas polling reCAPTCHA 300 detik milik SDK, sehingga Cypress menggagalkan tes yang benar-benar macet alih-alih menggantung di belakang klien yang masih menunggu. Jika Anda lebih suka klien yang menyerah lebih dulu, turunkan recaptchaTimeout ke nilai di bawah task timeout Anda.

Atau lewati SDK dan pakai cy.request

API-nya kompatibel dengan 2captcha, jadi dua panggilan sudah menyelesaikan seluruh pekerjaannya. Karena cy.request berjalan di Node, aturan origin browser sama sekali tidak ikut campur.

// 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));
  });
}

Dua detail yang perlu diingat. Balasan dalam teks biasa dari res.php adalah OK|TOKEN saat berhasil, dan string polos CAPCHA_NOT_READY selama job masih berjalan, yang merupakan status dan bukan error. Selain itu, hasil hanya bisa dibaca satu kali, jadi simpan begitu tiba alih-alih memintanya dua kali. Setiap parameter dan setiap string error tercantum di dokumentasi API.

Menjalankan tes di CI sementara pemecah tetap di tempatnya

Di sinilah suite yang lolos di laptop Anda gagal pada push pertama. Runner GitHub Actions, job GitLab, atau agen Jenkins punya alamat loopback-nya sendiri, dan tidak ada apa pun yang mendengarkan di port 8080 di sana. Mode Local pada dasarnya hanya berlaku di mesin itu sendiri.

Mode Server adalah jawabannya. CapSkip mendengarkan di jaringan atau IP publik Anda alih-alih di loopback, dan config membaca alamatnya dari environment.

// 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 membaca CAPSKIP_HOST dan CAPSKIP_PORT dengan sendirinya, jadi fallback di atas hanyalah pengaman tambahan untuk runner yang start tanpa keduanya. IP publik statis direkomendasikan untuk mesin pemecah, dan langkah-langkahnya ada di pengaturan koneksi. Perangkat kerasnya tetap milik Anda dan tetap tanpa kuota: satu-satunya yang berubah adalah tempat prosesnya mendengarkan.

Kesalahan umum dan artinya

GejalaPenyebabPerbaiki
Task solveRecaptcha tidak terdaftarDidaftarkan di blok yang salah, atau config tidak pernah dieksporDaftarkan di dalam setupNodeEvents pada key e2e
Kehabisan waktu setelah menunggu 60000ms untuk task AndataskTimeout masih pada nilai bawaannyaNaikkan ke 180000, secara global atau per panggilan
Task mengembalikan undefinedHandler tidak punya pernyataan returnKembalikan token, atau null ketika tidak ada apa pun
Elemen tidak terlihat, sehingga Cypress tidak bisa mengetikKolom respons adalah textarea tersembunyiTulis nilainya lewat cy.document saja
ERROR_GOOGLEKEYsitekey kosong, atau milik widget TurnstileCatat atributnya sebelum memecahkan; Turnstile punya method sendiri
NetworkException pada setiap tesTidak ada yang mendengarkan di host dan port tersebutMode Local hanya loopback; pakai Mode Server dari CI
Hijau di lokal, merah di CIRunner tidak bisa menjangkau loopback mesin AndaArahkan CAPSKIP_HOST ke alamat yang dapat dijangkau

Pertanyaan yang sering diajukan

Haruskah saya mematikan saja CAPTCHA di lingkungan pengujian saya?

Jika widget itu milik Anda, ya. Feature flag atau sitekey uji pada build staging lebih murah dan lebih cepat daripada memecahkan, dan cara itu menjaga suite tetap deterministik. Memecahkan layak dilakukan dalam tiga kasus: CAPTCHA-nya milik orang lain, staging harus mencerminkan produksi secara persis, atau yang sedang diuji justru jalur challenge itu sendiri. Halaman demo CAPTCHA berguna untuk kasus ketiga, karena Anda bisa mengarahkan sebuah spec ke widget yang berperilaku seperti aslinya.

Apakah ini bekerja pada component testing Cypress?

Tidak terlalu berguna. Sebuah component test me-mount satu komponen tanpa halaman nyata dan tanpa server di belakangnya, jadi token tidak punya apa pun untuk diverifikasi. Daftarkan task-nya di bawah key component jika Anda ingin task itu tersedia, tetapi simpan pekerjaan challenge di spec end to end tempat ada permintaan nyata untuk dikirim.

Bisakah run yang direkam ke Cypress Cloud menjangkau pemecah?

Cypress Cloud merekam hasil, bukan menjalankan tes Anda, jadi pertanyaannya sebenarnya soal mesin mana yang menjalankan browser. Di laptop Anda, itu Mode Local. Di runner terkelola, itu butuh Mode Server dan rute menuju alamat pemecah, dan perekamannya bekerja sama saja.

Berapa banyak pemecahan yang bisa didorong sekaligus oleh run Cypress paralel?

Sebanyak file spec yang Anda jalankan. Setiap proses Cypress memegang kliennya sendiri dan menunggu task-nya sendiri, jadi tidak ada yang perlu dikonfigurasi di sisi klien. SDK mulai polling pada 250 milidetik lalu melonggar sampai batas pollingInterval, yang membuat pemecahan cepat tetap cepat bahkan ketika ada beberapa yang berjalan bersamaan. Karena pekerjaannya terjadi di perangkat keras milik Anda, menambah mesin adalah keputusan kapasitas, bukan keputusan tagihan.

Versi singkatnya

Tempatkan klien di cypress.config.js, ekspos sebagai sebuah task, naikkan taskTimeout ke 180000, dan tulis token ke kolom tersembunyi lewat cy.document. Itulah keseluruhan integrasinya, dan bagian yang rusak adalah dua aturan Cypress di baliknya: kode spec adalah kode browser, dan sebuah task harus mengembalikan nilai atau ia gagal.

Menjalankan pemecah sendiri adalah yang membuat masuk akal untuk mencoba ulang challenge yang labil alih-alih menganggarkan biayanya, dan argumen itu berlaku di mana pun Anda menempatkan pemecah captcha, baik dalam test suite maupun di produksi. Opsi klien dibahas secara rinci di panduan integrasi Node.js, dan detail sisi browser yang dibagi Cypress dengan setiap runner lain ada di panduan Playwright.