Puppetaria: skrip Puppeteer yang mengutamakan aksesibilitas

Teluk Johan
Johan Bay

Puppeteer dan pendekatannya terhadap pemilih

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

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

Di Puppeteer, 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 merupakan bagian penting dari pengalaman Puppeteer. Hingga saat ini, pemilih di Puppeteer dibatasi pada pemilih CSS dan XPath yang, meskipun sangat ekspresif, memiliki kelemahan karena mempertahankan interaksi browser dalam skrip.

Pemilih sintaksis vs. semantik

Pemilih CSS bersifat sintaksis; mereka terikat erat dengan cara kerja bagian dalam representasi tekstual hierarki DOM dalam artian mereferensikan ID dan nama class dari DOM. Dengan demikian, class ini menyediakan alat integral bagi developer web untuk memodifikasi 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, skrip ini memperkenalkan asumsi tersembunyi tentang bagaimana halaman tersebut diimplementasikan, yang tidak dapat dikontrol oleh skrip Puppeteer.

Akibatnya, skrip tersebut bisa rapuh dan rentan terhadap perubahan kode sumber. Misalnya, instance tersebut menggunakan skrip Puppeteer untuk pengujian otomatis bagi aplikasi web yang berisi node <button>Submit</button> sebagai turunan ketiga 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 tombol ini terikat erat dengan versi halaman web ini. Jika elemen ditambahkan kemudian di atas tombol, pemilih ini tidak lagi berfungsi.

Ini bukanlah berita untuk penulis uji coba: Pengguna Puppeteer sudah mencoba memilih pemilih yang kuat terhadap perubahan tersebut. Dengan Puppetaria, kami memberi pengguna alat baru dalam misi ini.

Puppeteer kini hadir dengan pengendali kueri alternatif berdasarkan pembuatan kueri pohon aksesibilitas, bukan mengandalkan pemilih CSS. Filosofi yang mendasari di sini adalah jika elemen konkret yang ingin kita pilih tidak berubah, maka node aksesibilitas yang sesuai juga tidak boleh berubah.

Kami menamai pemilih tersebut "pemilih ARIA" dan mendukung kueri untuk nama yang dapat diakses dan peran hierarki aksesibilitas yang dapat dihitung. Dibandingkan dengan pemilih CSS, properti ini bersifat semantik. Tema ini tidak terikat dengan properti sintaksis DOM tetapi menjelaskan 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 mengacu 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 kasus ini memang diinginkan; dengan mengubah nama tombol, kita mengubah konten halaman, bukan presentasi visualnya atau cara penyusunan struktur di DOM. Pengujian 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 bagi pengguna Puppeteer:

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

Bagian selanjutnya dari artikel ini membahas detail tentang cara kami menerapkan proyek Puppetaria.

Proses desain

Latar belakang

Seperti motivasi di atas, kita ingin mengaktifkan elemen kueri menurut nama dan peran yang dapat diakses. Ini adalah properti hierarki aksesibilitas, yaitu hierarki ganda 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 suatu elemen bukanlah tugas yang mudah. Jadi, sejak awal kami memutuskan bahwa kami ingin menggunakan kembali infrastruktur Chromium yang ada untuk hal ini.

Cara kami menerapkannya

Bahkan membatasi diri pada penggunaan hierarki aksesibilitas Chromium, ada beberapa cara untuk menerapkan kueri ARIA di Puppeteer. Untuk mengetahui alasannya, pertama-tama mari kita lihat bagaimana Puppeteer mengontrol browser.

Browser mengekspos antarmuka proses debug melalui protokol yang disebut Protokol Chrome DevTools (CDP). Fungsi ini mengekspos fungsi seperti "muat ulang halaman" atau "jalankan bagian JavaScript ini di halaman dan berikan hasilnya" melalui antarmuka yang tidak terikat dengan bahasa.

Front-end DevTools dan Puppeteer menggunakan CDP untuk berbicara dengan browser. Untuk mengimplementasikan perintah CDP, ada infrastruktur DevTools di dalam semua komponen Chrome: di browser, di perender, dan seterusnya. CDP menangani {i>routing<i} 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 secara langsung dalam konteks halaman dan menyerahkan hasilnya. Tindakan Puppeteer lainnya seperti mengemulasi kekurangan penglihatan warna, mengambil screenshot, atau menangkap rekaman aktivitas menggunakan CDP untuk berkomunikasi langsung dengan proses rendering Blink.

CDP

Ini sudah memberi kita dua jalur untuk mengimplementasikan fungsionalitas kueri; kita dapat:

  • Tulis logika kueri kita di 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:

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

JS DOM traversal

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

Traversal Puppeteer AXTree

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

Traversal DOM CDP

Untuk prototipe ini, kami mengimplementasikan endpoint CDP baru secara khusus untuk mengkueri hierarki aksesibilitas. Dengan cara ini, pembuatan kueri dapat terjadi di back-end melalui implementasi C++, bukan dalam konteks halaman melalui JavaScript.

Tolok ukur pengujian unit

Gambar berikut membandingkan total waktu proses kueri empat elemen 1000 kali untuk 3 prototipe. Tolok ukur dijalankan dalam 3 konfigurasi berbeda yang bervariasi ukuran halaman dan apakah caching elemen aksesibilitas diaktifkan atau tidak.

Tolok ukur: Total runtime kueri empat elemen 1.000 kali

Cukup 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 tampak meningkat secara dramatis terkait ukuran halaman. Agak menarik untuk melihat bahwa prototipe traversal JS DOM merespons dengan sangat baik untuk mengaktifkan cache aksesibilitas. Dengan menonaktifkan cache, 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 JS DOM traversal, kita meminta nama dan peran yang dapat diakses untuk setiap elemen selama traversal, jadi 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 diuntungkan dengan mengaktifkan cache, karena hierarki aksesibilitas kemudian dipertahankan di seluruh panggilan CDP, tetapi peningkatan performa relatif lebih kecil.

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

Benchmark suite pengujian DevTools

Tolok ukur sebelumnya menunjukkan bahwa menerapkan mekanisme kueri kita pada lapisan CDP memberikan peningkatan performa dalam skenario pengujian unit klinis.

Untuk melihat apakah perbedaannya cukup jelas sehingga 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 implementasikan menggunakan masing-masing prototipe.

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

Tolok ukur: e2e test suite

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 kinerja antara kedua prototipe juga ditunjukkan dalam skenario ini.

Endpoint CDP baru

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

Untuk kasus penggunaan di Puppeteer, endpoint perlu mengambil apa yang disebut RemoteObjectIds sebagai argumen dan, agar 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 untuk memuaskan antarmuka ini. Dari sini, kami menemukan bahwa ukuran objek yang ditampilkan, yaitu apakah kami menampilkan node aksesibilitas penuh atau hanya backendNodeIds yang tidak membuat perbedaan jelas. Di sisi lain, kami menemukan bahwa menggunakan NextInPreOrderIncludingIgnored yang ada adalah pilihan yang buruk untuk menerapkan logika traversal di sini, karena menghasilkan perlambatan yang signifikan.

Tolok ukur: Perbandingan prototipe traversal AXTree berbasis CDP

Menyelesaikan semuanya

Sekarang, setelah endpoint CDP diterapkan, kami menerapkan pengendali kueri di sisi Puppeteer. Tugas utama yang mereka lakukan di sini adalah menyusun ulang kode penanganan kueri agar kueri dapat diselesaikan secara langsung melalui CDP, bukan membuat kueri melalui JavaScript yang dievaluasi dalam konteks halaman.

Apa selanjutnya?

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

Mendownload saluran pratinjau

Pertimbangkan untuk menggunakan Chrome Canary, Dev, atau Beta sebagai browser pengembangan default Anda. Saluran pratinjau ini memberi Anda akses ke fitur DevTools terbaru, menguji API platform web tercanggih, dan menemukan masalah di situs Anda sebelum pengguna melakukannya.

Menghubungi tim Chrome DevTools

Gunakan opsi berikut untuk membahas fitur dan perubahan baru di postingan ini, atau hal lain yang terkait dengan DevTools.

  • Kirim saran atau masukan kepada kami melalui crbug.com.
  • Laporkan masalah DevTools menggunakan Opsi lainnya   Lainnya   > Bantuan > Laporkan masalah DevTools di DevTools.
  • Tweet di @ChromeDevTools.
  • Tuliskan komentar di video YouTube Yang baru di DevTools atau video YouTube di DevTools.