Puppetaria: skrip Puppeteer yang mengutamakan aksesibilitas

Johan Bay
Johan Bay

Puppeteer dan pendekatannya terhadap pemilih

Puppeteer adalah library otomatisasi browser untuk Node: library ini memungkinkan Anda mengontrol browser menggunakan JavaScript API yang sederhana dan modern.

Tugas browser yang paling penting tentu saja adalah menjelajahi halaman web. Mengotomatiskan tugas ini pada dasarnya sama dengan mengotomatiskan interaksi dengan halaman web.

Di Puppeteer, hal ini dicapai dengan membuat kueri elemen DOM menggunakan pemilih berbasis string dan melakukan tindakan seperti mengklik atau mengetik teks pada elemen. Misalnya, skrip yang membuka developer.google.com, menemukan kotak penelusuran, dan menelusuri puppetaria dapat terlihat seperti ini:

(async () => {
   const browser = await puppeteer.launch({ headless: false });
   const page = await browser.newPage();
   await page.goto('https://developers.google.com/', { waitUntil: 'load' });
   // Find the search box using a suitable CSS selector.
   const search = await page.$('devsite-search > form > div.devsite-search-container');
   // Click to expand search box and focus it.
   await search.click();
   // Enter search string and press Enter.
   await search.type('puppetaria');
   await search.press('Enter');
 })();

Oleh karena itu, cara elemen diidentifikasi menggunakan pemilih kueri adalah bagian yang menentukan pengalaman Puppeteer. Hingga saat ini, pemilih di Puppeteer telah dibatasi pada pemilih CSS dan XPath yang, meskipun secara ekspresif sangat canggih, dapat memiliki kelemahan untuk mempertahankan interaksi browser dalam skrip.

Pemilih sintaksis vs. semantik

Pemilih CSS bersifat sintaksis; pemilih CSS terikat erat dengan cara kerja bagian dalam representasi tekstual hierarki DOM dalam arti bahwa pemilih CSS mereferensikan ID dan nama class dari DOM. Dengan demikian, alat ini menyediakan alat integral bagi developer web untuk mengubah atau menambahkan gaya ke elemen di halaman, tetapi dalam konteks tersebut, developer memiliki kontrol penuh atas halaman dan hierarki DOM-nya.

Di sisi lain, skrip Puppeteer adalah pengamat eksternal halaman, sehingga saat pemilih CSS digunakan dalam konteks ini, pemilih CSS akan memperkenalkan asumsi tersembunyi tentang cara halaman diterapkan yang tidak dapat dikontrol oleh skrip Puppeteer.

Efeknya adalah skrip tersebut dapat menjadi rapuh dan rentan terhadap perubahan kode sumber. Misalnya, seseorang menggunakan skrip Puppeteer untuk pengujian otomatis bagi aplikasi web yang berisi node <button>Submit</button> sebagai turunan ketiga dari elemen body. Satu cuplikan dari kasus pengujian mungkin terlihat seperti ini:

const button = await page.$('body:nth-child(3)'); // problematic selector
await button.click();

Di sini, kita menggunakan pemilih 'body:nth-child(3)' untuk menemukan tombol kirim, tetapi pemilih ini terikat erat dengan versi halaman web ini. Jika elemen ditambahkan di atas tombol nanti, pemilih ini tidak akan berfungsi lagi.

Hal ini bukan berita baru bagi penulis pengujian: Pengguna Puppeteer sudah mencoba memilih pemilih yang tahan terhadap perubahan tersebut. Dengan Puppetaria, kami memberi pengguna alat baru dalam misi ini.

Puppeteer kini dilengkapi dengan pengendali kueri alternatif berdasarkan kueri hierarki aksesibilitas, bukan mengandalkan pemilih CSS. Filosofi yang mendasarinya adalah jika elemen konkret yang ingin kita pilih belum berubah, node aksesibilitas yang sesuai juga tidak akan berubah.

Kami menamai pemilih tersebut "pemilih ARIA" dan mendukung kueri untuk nama dan peran yang dapat diakses yang dikomputasi dari hierarki aksesibilitas. Dibandingkan dengan pemilih CSS, properti ini bersifat semantik. Elemen ini tidak terikat dengan properti sintaksis DOM, tetapi merupakan deskripsi tentang cara halaman diamati melalui teknologi pendukung seperti pembaca layar.

Dalam contoh skrip pengujian di atas, kita dapat menggunakan pemilih aria/Submit[role="button"] untuk memilih tombol yang diinginkan, dengan Submit merujuk ke nama elemen yang dapat diakses:

const button = await page.$('aria/Submit[role="button"]');
await button.click();

Sekarang, jika nanti kita memutuskan untuk mengubah konten teks tombol dari Submit menjadi Done, pengujian akan gagal lagi, tetapi dalam hal ini hal itu diinginkan; dengan mengubah nama tombol, kita mengubah konten halaman, bukan presentasi visualnya atau cara strukturnya di DOM. Pengujian kami akan memperingatkan kami tentang perubahan tersebut untuk memastikan bahwa perubahan tersebut disengaja.

Kembali ke contoh yang lebih besar dengan kotak penelusuran, kita dapat memanfaatkan pengendali aria baru dan mengganti

const search = await page.$('devsite-search > form > div.devsite-search-container');

dengan

const search = await page.$('aria/Open search[role="button"]');

untuk menemukan kotak penelusuran.

Secara lebih umum, kami yakin bahwa penggunaan pemilih ARIA tersebut dapat memberikan manfaat berikut kepada pengguna Puppeteer:

  • Membuat pemilih dalam skrip pengujian lebih tahan terhadap perubahan kode sumber.
  • Membuat skrip pengujian lebih mudah dibaca (nama yang dapat diakses adalah deskripsi semantik).
  • Motivasi praktik yang baik untuk menetapkan properti aksesibilitas ke elemen.

Bagian lain dalam artikel ini membahas detail tentang cara kami menerapkan project Puppetaria.

Proses desain

Latar belakang

Seperti yang dimotivasi di atas, kita ingin mengaktifkan elemen kueri berdasarkan nama dan peran yang dapat diakses. Ini adalah properti hierarki aksesibilitas, yang merupakan duplikat dari hierarki DOM biasa, yang digunakan oleh perangkat seperti pembaca layar untuk menampilkan halaman web.

Dari melihat spesifikasi untuk menghitung nama yang dapat diakses, jelas bahwa menghitung nama untuk elemen adalah tugas yang tidak mudah, jadi sejak awal kami memutuskan bahwa kami ingin menggunakan kembali infrastruktur Chromium yang ada untuk ini.

Cara kami menerapkannya

Meskipun membatasi diri untuk menggunakan hierarki aksesibilitas Chromium, ada beberapa cara yang dapat kita gunakan untuk menerapkan kueri ARIA di Puppeteer. Untuk mengetahui alasannya, mari kita lihat terlebih dahulu cara Puppeteer mengontrol browser.

Browser mengekspos antarmuka proses debug melalui protokol yang disebut Chrome DevTools Protocol (CDP). Hal ini mengekspos fungsi seperti "muat ulang halaman" atau "jalankan bagian JavaScript ini di halaman dan tampilkan hasilnya" melalui antarmuka yang tidak bergantung pada bahasa.

Frontend DevTools dan Puppeteer menggunakan CDP untuk berkomunikasi dengan browser. Untuk menerapkan perintah CDP, ada infrastruktur DevTools di dalam semua komponen Chrome: di browser, di perender, dan sebagainya. CDP menangani pemilihan rute perintah ke tempat yang tepat.

Tindakan Puppeteer seperti membuat kueri, mengklik, dan mengevaluasi ekspresi dilakukan dengan memanfaatkan perintah CDP seperti Runtime.evaluate yang mengevaluasi JavaScript langsung dalam konteks halaman dan menampilkan hasilnya. Tindakan Puppeteer lainnya seperti mengemulasi kekurangan penglihatan warna, mengambil screenshot, atau merekam rekaman aktivitas menggunakan CDP untuk berkomunikasi langsung dengan proses rendering Blink.

CDP

Hal ini memberi kita dua jalur untuk menerapkan fungsi kueri; kita dapat:

  • Tulis logika kueri kita dalam JavaScript dan masukkan ke halaman menggunakan Runtime.evaluate, atau
  • Gunakan endpoint CDP yang dapat mengakses dan membuat kueri hierarki aksesibilitas secara langsung dalam proses Blink.

Kami menerapkan 3 prototipe:

  • Pencarian DOM JS - berdasarkan memasukkan JavaScript ke dalam halaman
  • Puppeteer AXTree traversal - berdasarkan penggunaan akses CDP yang ada ke hierarki aksesibilitas
  • Pencarian DOM CDP - menggunakan endpoint CDP baru yang dibuat khusus untuk membuat kueri hierarki aksesibilitas

Traversal DOM JS

Prototipe ini melakukan penelusuran penuh DOM dan menggunakan element.computedName dan element.computedRole, yang dibatasi pada tanda peluncuran ComputedAccessibilityInfo, untuk mengambil nama dan peran untuk setiap elemen selama penelusuran.

Traversal AXTree Puppeteer

Di sini, kita mengambil hierarki aksesibilitas lengkap melalui CDP dan menjelajahi hierarki tersebut di Puppeteer. Node aksesibilitas yang dihasilkan kemudian dipetakan ke node DOM.

Traversal DOM CDP

Untuk prototipe ini, kami menerapkan endpoint CDP baru khusus untuk membuat kueri hierarki aksesibilitas. Dengan cara ini, kueri dapat terjadi di backend melalui implementasi C++, bukan dalam konteks halaman melalui JavaScript.

Benchmark pengujian unit

Gambar berikut membandingkan total runtime kueri empat elemen sebanyak 1.000 kali untuk 3 prototipe. Benchmark dijalankan dalam 3 konfigurasi berbeda yang memvariasikan ukuran halaman dan apakah caching elemen aksesibilitas diaktifkan atau tidak.

Benchmark: Total runtime kueri empat elemen sebanyak 1.000 kali

Sangat jelas bahwa ada kesenjangan performa yang cukup besar antara mekanisme kueri yang didukung CDP dan dua mekanisme lainnya yang hanya diterapkan di Puppeteer, dan perbedaan relatifnya tampaknya meningkat secara dramatis dengan ukuran halaman. Sangat menarik untuk melihat bahwa prototipe traversal DOM JS merespons dengan sangat baik untuk mengaktifkan penyimpanan dalam cache aksesibilitas. Jika caching dinonaktifkan, hierarki aksesibilitas dihitung sesuai permintaan dan menghapus hierarki setelah setiap interaksi jika domain dinonaktifkan. Mengaktifkan domain akan membuat Chromium meng-cache hierarki yang dihitung.

Untuk penelusuran DOM JS, kita meminta nama dan peran yang dapat diakses untuk setiap elemen selama penelusuran, sehingga jika penyimpanan dalam cache dinonaktifkan, Chromium akan menghitung dan menghapus hierarki aksesibilitas untuk setiap elemen yang kita kunjungi. Di sisi lain, untuk pendekatan berbasis CDP, hierarki hanya dihapus di antara setiap panggilan ke CDP, yaitu untuk setiap kueri. Pendekatan ini juga mendapatkan manfaat dari pengaktifan penyimpanan dalam cache, karena hierarki aksesibilitas kemudian dipertahankan di seluruh panggilan CDP, tetapi peningkatan performanya relatif lebih kecil.

Meskipun mengaktifkan penyimpanan dalam cache terlihat diinginkan di sini, tindakan ini akan menimbulkan biaya penggunaan memori tambahan. Untuk skrip Puppeteer yang misalnya mencatat file rekaman aktivitas, hal ini dapat menjadi masalah. Oleh karena itu, kami memutuskan untuk tidak mengaktifkan penyimpanan dalam cache hierarki aksesibilitas secara default. Pengguna dapat mengaktifkan penyimpanan dalam cache sendiri dengan mengaktifkan Domain aksesibilitas CDP.

Benchmark rangkaian pengujian DevTools

Benchmark sebelumnya menunjukkan bahwa menerapkan mekanisme kueri kami di lapisan CDP memberikan peningkatan performa dalam skenario pengujian unit klinis.

Untuk melihat apakah perbedaannya cukup jelas sehingga dapat terlihat dalam skenario yang lebih realistis saat menjalankan rangkaian pengujian lengkap, kami mem-patch rangkaian pengujian menyeluruh DevTools untuk menggunakan prototipe berbasis JavaScript dan CDP, lalu membandingkan runtime-nya. Dalam benchmark ini, kami mengubah total 43 pemilih dari [aria-label=…] menjadi pengendali kueri kustom aria/…, yang kemudian kami terapkan menggunakan setiap prototipe.

Beberapa pemilih digunakan beberapa kali dalam skrip pengujian, sehingga jumlah sebenarnya eksekusi pengendali kueri aria adalah 113 per pengoperasian suite. Jumlah total pilihan kueri adalah 2.253, sehingga hanya sebagian kecil pilihan kueri yang terjadi melalui prototipe.

Tolok ukur: rangkaian pengujian e2e

Seperti yang terlihat pada gambar di atas, ada perbedaan yang jelas dalam total runtime. Data terlalu berisik untuk menyimpulkan sesuatu yang spesifik, tetapi jelas bahwa kesenjangan performa antara kedua prototipe juga terlihat dalam skenario ini.

Endpoint CDP baru

Mengingat tolok ukur di atas, dan karena pendekatan berbasis flag peluncuran tidak diinginkan secara umum, kami memutuskan untuk melanjutkan dengan menerapkan perintah CDP baru untuk membuat kueri hierarki aksesibilitas. Sekarang, kita harus mencari tahu antarmuka endpoint baru ini.

Untuk kasus penggunaan di Puppeteer, kita memerlukan endpoint untuk menggunakan RemoteObjectIds sebagai argumen dan, agar kita dapat menemukan elemen DOM yang sesuai setelahnya, endpoint harus menampilkan daftar objek yang berisi backendNodeIds untuk elemen DOM.

Seperti yang terlihat pada diagram di bawah, kami mencoba beberapa pendekatan yang memenuhi antarmuka ini. Dari hal ini, kami mendapati bahwa ukuran objek yang ditampilkan, yaitu apakah kita menampilkan node aksesibilitas lengkap atau hanya backendNodeIds, tidak membuat perbedaan yang jelas. Di sisi lain, kami mendapati bahwa menggunakan NextInPreOrderIncludingIgnored yang ada adalah pilihan yang buruk untuk menerapkan logika traversal di sini, karena hal itu menghasilkan penurunan kecepatan yang signifikan.

Benchmark: Perbandingan prototipe traversal AXTree berbasis CDP

Merangkum semuanya

Sekarang, dengan endpoint CDP yang sudah ada, kita telah menerapkan pengendali kueri di sisi Puppeteer. Pekerjaan utama di sini adalah menyusun ulang kode penanganan kueri agar kueri dapat diselesaikan langsung melalui CDP, bukan membuat kueri melalui JavaScript yang dievaluasi dalam konteks halaman.

Apa langkah selanjutnya?

Pengendali aria baru dikirimkan dengan Puppeteer v5.4.0 sebagai pengendali kueri bawaan. Kami menantikan cara pengguna mengadopsinya ke dalam skrip pengujian mereka, dan kami tidak sabar untuk mendengar ide Anda tentang cara kami dapat membuatnya lebih berguna.

Mendownload saluran pratinjau

Sebaiknya gunakan Chrome Canary, Dev, atau Beta sebagai browser pengembangan default Anda. Saluran pratinjau ini memberi Anda akses ke fitur DevTools terbaru, memungkinkan Anda menguji API platform web canggih, dan membantu Anda menemukan masalah di situs sebelum pengguna melakukannya.

Hubungi tim Chrome DevTools

Gunakan opsi berikut untuk membahas fitur baru, update, atau hal lain yang terkait dengan DevTools.