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 setara 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 untuk 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, jenis alat ini menyediakan alat integral bagi developer web untuk memodifikasi atau menambahkan gaya ke elemen di halaman, namun dalam konteks tersebut developer memiliki kontrol penuh atas halaman dan hierarki DOM-nya.

Di sisi lain, skrip Puppeteer adalah pengamat eksternal halaman, jadi saat pemilih CSS digunakan dalam konteks ini, skrip ini memperkenalkan asumsi tersembunyi tentang bagaimana 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 pembuatan kueri hierarki aksesibilitas, bukan mengandalkan pemilih CSS. Filosofi yang mendasari di sini adalah bahwa jika elemen konkret yang ingin kita pilih tidak berubah, maka 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. Artikel ini tidak terikat pada properti sintaksis DOM, melainkan deskripsi tentang bagaimana 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 pada 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 bagaimana 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 pupeteer seperti membuat kueri, mengklik, dan mengevaluasi ekspresi dilakukan dengan memanfaatkan perintah CDP seperti Runtime.evaluate yang mengevaluasi JavaScript langsung di konteks halaman dan menyerahkan hasilnya. Tindakan Puppeteer lainnya seperti mengemulasi kekurangan penglihatan warna, mengambil screenshot, atau mengambil 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 di JavaScript dan masukkan logika kueri tersebut ke dalam 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
  • Traversal 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.

Puppeteer AXTree traversal

Di sini, kita mengambil hierarki aksesibilitas lengkap melalui CDP dan menelusurinya 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.

Tolok ukur 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 cache dinonaktifkan, hierarki aksesibilitas akan 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 cache tampaknya lebih disukai di sini, tindakan ini memiliki 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 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 eksekusi pengendali kueri aria sebenarnya 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 bagan di bawah, kami mencoba beberapa pendekatan untuk memuaskan 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 menemukan bahwa menggunakan NextInPreOrderIncludingIgnored yang ada merupakan pilihan yang buruk untuk menerapkan logika traversal di sini, karena cara tersebut menghasilkan perlambatan yang kentara.

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 apa pun yang berkaitan dengan DevTools.