Puppetaria: erişilebilirliğe öncelik veren Puppeteer metinleri

Johan Bay
Johan Bay

Puppeteer ve seçicilere yaklaşımı

Puppeteer, Node için bir tarayıcı otomasyon kitaplığıdır. Basit ve modern bir JavaScript API kullanarak tarayıcıyı kontrol etmenizi sağlar.

En belirgin tarayıcı görevi, elbette web sayfalarında gezinmektir. Bu görevi otomatikleştirmek, web sayfasıyla etkileşimleri otomatikleştirmek anlamına gelir.

Puppeteer'da bu, dize tabanlı seçiciler kullanarak DOM öğelerini sorgulayıp öğeler üzerinde tıklama veya metin yazma gibi işlemler gerçekleştirerek yapılır. Örneğin, developer.google.com adresini açan, arama kutusunu bulan ve puppetaria ifadesini arayan bir komut dosyası şu şekilde görünebilir:

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

Bu nedenle, öğelerin sorgu seçiciler kullanılarak nasıl tanımlandığı, Puppeteer deneyiminin belirleyici bir parçasıdır. Şimdiye kadar Puppeteer'daki seçiciler, CSS ve XPath seçicilerle sınırlıydı. Bu seçiciler, ifade açısından çok güçlü olsa da komut dosyalarında tarayıcı etkileşimlerini sürdürme konusunda dezavantajlara sahip olabilir.

Söz dizimsel ve anlamsal seçiciler

CSS seçiciler doğası gereği söz dizimseldir. DOM'daki kimliklere ve sınıf adlarına referans vermeleri açısından DOM ağacının metinsel temsilinin iç işleyişine sıkı sıkıya bağlıdırlar. Bu nedenle, web geliştiricilerin bir sayfadaki öğelere stil eklemesi veya stilleri değiştirmesi için önemli bir araçtır. Ancak bu bağlamda geliştirici, sayfa ve DOM ağacı üzerinde tam kontrole sahiptir.

Öte yandan, Puppeteer komut dosyası bir sayfanın harici gözlemcisidir. Bu nedenle, CSS seçiciler bu bağlamda kullanıldığında sayfanın nasıl uygulandığıyla ilgili gizli varsayımlar ortaya çıkar. Puppeteer komut dosyasının bu varsayımlar üzerinde kontrolü yoktur.

Bu nedenle, bu tür komut dosyaları kırılgan olabilir ve kaynak kodu değişikliklerine karşı hassas olabilir. Örneğin, body öğesinin üçüncü alt öğesi olarak <button>Submit</button> düğümünü içeren bir web uygulamasının otomatik testi için Puppeteer komut dosyalarının kullanıldığını varsayalım. Bir test durumundan alınan snippet şu şekilde görünebilir:

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

Burada, gönder düğmesini bulmak için 'body:nth-child(3)' seçicisini kullanıyoruz ancak bu, web sayfasının tam olarak bu sürümüne sıkı sıkıya bağlıdır. Daha sonra düğmenin üzerine bir öğe eklenirse bu seçici artık çalışmaz.

Bu, test yazarları için yeni bir bilgi değildir: Puppeteer kullanıcıları, bu tür değişikliklere karşı dayanıklı seçiciler seçmeye çalışır. Puppetaria ile kullanıcılara bu arayışta yeni bir araç sunuyoruz.

Puppeteer artık CSS seçicilere güvenmek yerine erişilebilirlik ağacını sorgulamaya dayalı alternatif bir sorgu işleyici ile birlikte sunuluyor. Buradaki temel felsefe, seçmek istediğimiz somut öğe değişmediyse ilgili erişilebilirlik düğümünün de değişmemesi gerektiğidir.

Bu tür seçicileri "ARIA seçiciler" olarak adlandırır ve erişilebilirlik ağacının hesaplanan erişilebilir özellikli adı ve rolü için sorgulamayı destekleriz. CSS seçicilerle karşılaştırıldığında bu özellikler doğası gereği semantiktir. DOM'un söz dizimsel özelliklerine bağlı değildirler. Bunun yerine, sayfanın ekran okuyucular gibi yardımcı teknolojiler aracılığıyla nasıl gözlemlendiğini açıklayan tanımlayıcılardır.

Yukarıdaki test komut dosyası örneğinde, istenen düğmeyi seçmek için aria/Submit[role="button"] seçicisini kullanabiliriz. Burada Submit, öğenin erişilebilir özellikli adını ifade eder:

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

Daha sonra düğmemizin metin içeriğini Submit yerine Done olarak değiştirmeye karar verirsek test yine başarısız olur ancak bu durumda bu istenen bir durumdur. Düğmenin adını değiştirerek sayfanın görsel sunumu veya DOM'da nasıl yapılandırıldığı yerine içeriğini değiştirmiş oluruz. Testlerimiz, bu tür değişikliklerin bilinçli olarak yapıldığından emin olmak için bizi uyarmalıdır.

Arama çubuğuyla ilgili daha büyük örneğe dönersek yeni aria işleyicisinden yararlanabilir ve

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

ile

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

arama çubuğunu bulmak için.

Daha genel olarak, bu tür ARIA seçicilerin kullanılmasının Puppeteer kullanıcılarına aşağıdaki avantajları sağlayabileceğine inanıyoruz:

  • Test komut dosyalarındaki seçicileri kaynak kodu değişikliklerine karşı daha dayanıklı hale getirme
  • Test komut dosyalarını daha okunabilir hale getirme (erişilebilir özellikli adlar, semantik tanımlayıcılardır).
  • Öğelere erişilebilirlik özellikleri atamak için iyi uygulamaları teşvik etme

Bu makalenin geri kalanında, Puppetaria projesini nasıl uyguladığımızla ilgili ayrıntılar ele alınmaktadır.

Tasarım süreci

Arka plan

Yukarıda belirtildiği gibi, öğeleri erişilebilir özellikli adlarına ve rollerine göre sorgulamayı etkinleştirmek istiyoruz. Bunlar, ekran okuyucular gibi cihazlar tarafından web sayfalarını göstermek için kullanılan normal DOM ağacının ikili bir kopyası olan erişilebilirlik ağacının özellikleridir.

Erişilebilir özellikli adı hesaplama spesifikasyonuna baktığımızda, bir öğenin adını hesaplamanın önemsiz bir görev olmadığı açıkça görülüyor. Bu nedenle, en başından beri Chromium'un mevcut altyapısını bu amaçla yeniden kullanmak istedik.

Bu özelliği nasıl uyguladık?

Kendimizi Chromium'un erişilebilirlik ağacını kullanmakla sınırlasak bile Puppeteer'da ARIA sorgulamayı uygulamanın birkaç yolu vardır. Bunun nedenini anlamak için önce Puppeteer'ın tarayıcıyı nasıl kontrol ettiğine bakalım.

Tarayıcı, Chrome Geliştirici Araçları Protokolü (CDP) adlı bir protokol aracılığıyla hata ayıklama arayüzü sunar. Bu, dilden bağımsız bir arayüz aracılığıyla "sayfayı yeniden yükle" veya "sayfada bu JavaScript parçasını yürüt ve sonucu geri ver" gibi işlevleri kullanıma sunar.

Hem Geliştirici Araçları ön ucu hem de Puppeteer, tarayıcıyla iletişim kurmak için CDP'yi kullanır. CDP komutlarını uygulamak için Chrome'un tüm bileşenlerinde (tarayıcıda, oluşturucuda vb.) Geliştirici Araçları altyapısı bulunur. CDP, komutları doğru yere yönlendirir.

Sorgulama, tıklama ve ifadeleri değerlendirme gibi Puppeteer işlemleri, JavaScript'i doğrudan sayfa bağlamında değerlendiren ve sonucu döndüren Runtime.evaluate gibi CDP komutlarından yararlanılarak gerçekleştirilir. Renk körlüğünü taklit etme, ekran görüntüsü alma veya izleri yakalama gibi diğer Puppeteer işlemleri, Blink oluşturma işlemiyle doğrudan iletişim kurmak için CDP'yi kullanır.

CDP

Bu durumda, sorgulama işlevimizi uygulamak için iki yolumuz vardır:

  • Sorgulama mantığımızı JavaScript'te yazıp Runtime.evaluate kullanarak sayfaya yerleştirebiliriz veya
  • Blink işleminde erişilebilirlik ağacına doğrudan erişebilen ve sorgulayabilen bir CDP uç noktası kullanın.

3 prototip uyguladık:

  • JS DOM geçişi: Sayfaya JavaScript yerleştirmeye dayalıdır.
  • Puppeteer AXTree geçişi: Erişilebilirlik ağacına mevcut CDP erişimini kullanmaya dayalıdır.
  • CDP DOM geçişi: Erişilebilirlik ağacını sorgulamak için özel olarak tasarlanmış yeni bir CDP uç noktası kullanma

JS DOM geçişi

Bu prototip, DOM'un tam bir geçişini yapar ve geçiş sırasında her öğenin adını ve rolünü almak için ComputedAccessibilityInfo başlatma işaretine bağlı olarak element.computedName ve element.computedRole kullanır.

Puppeteer AXTree geçişi

Burada, CDP aracılığıyla tam erişilebilirlik ağacını alıp Puppeteer'da geziniriz. Elde edilen erişilebilirlik düğümleri daha sonra DOM düğümleriyle eşlenir.

CDP DOM geçişi

Bu prototip için özellikle erişilebilirlik ağacını sorgulamaya yönelik yeni bir CDP uç noktası uyguladık. Bu sayede sorgulama, JavaScript aracılığıyla sayfa bağlamında değil, C++ uygulaması üzerinden arka uçta gerçekleşebilir.

Birim testi karşılaştırması

Aşağıdaki şekilde, 3 prototip için dört öğenin 1.000 kez sorgulanmasının toplam çalışma süresi karşılaştırılmaktadır. Karşılaştırma, sayfa boyutuna ve erişilebilirlik öğelerinin önbelleğe alınmasının etkinleştirilip etkinleştirilmediğine göre değişen 3 farklı yapılandırmada yürütüldü.

Karşılaştırma: Dört öğeyi 1.000 kez sorgulamanın toplam çalışma süresi

CDP destekli sorgulama mekanizması ile yalnızca Puppeteer'da uygulanan diğer iki mekanizma arasında önemli bir performans farkı olduğu ve göreceli farkın sayfa boyutuyla birlikte önemli ölçüde arttığı görülüyor. JS DOM geçiş prototipinin, erişilebilirlik önbelleğe almayı etkinleştirmeye bu kadar iyi yanıt vermesi ilginç. Önbelleğe alma devre dışı bırakıldığında erişilebilirlik ağacı, istek üzerine hesaplanır ve alan devre dışı bırakılırsa her etkileşimden sonra ağacı siler. Alan etkinleştirildiğinde Chromium, hesaplanan ağacı önbelleğe alır.

JS DOM geçişi için, geçiş sırasında her öğenin erişilebilir özellikli adını ve rolünü isteriz. Bu nedenle, önbelleğe alma devre dışı bırakılırsa Chromium, ziyaret ettiğimiz her öğe için erişilebilirlik ağacını hesaplar ve siler. CDP tabanlı yaklaşımlarda ise ağaç yalnızca CDP'ye yapılan her çağrı arasında (yani her sorgu için) silinir. Bu yaklaşımlar, önbelleğe almayı etkinleştirmekten de yararlanır. Çünkü erişilebilirlik ağacı, CDP çağrıları boyunca kalıcı hale gelir ancak performans artışı nispeten daha küçüktür.

Önbelleğe almayı etkinleştirmek burada faydalı görünse de ek bellek kullanımı maliyeti getirir. Örneğin, izleme dosyalarını kaydeden Puppeteer komut dosyaları için bu durum sorunlu olabilir. Bu nedenle, erişilebilirlik ağacı önbelleğe almayı varsayılan olarak etkinleştirmemeye karar verdik. Kullanıcılar, CDP Erişilebilirlik alanını etkinleştirerek önbelleğe almayı kendileri etkinleştirebilir.

Geliştirici Araçları test paketi karşılaştırması

Önceki karşılaştırma, sorgulama mekanizmamızı CDP katmanında uygulamanın klinik birim testi senaryosunda performansı artırdığını gösterdi.

Farkın, tam bir test paketi çalıştırmanın daha gerçekçi bir senaryosunda fark edilecek kadar belirgin olup olmadığını görmek için DevTools uçtan uca test paketini yamalayarak JavaScript ve CDP tabanlı prototipleri kullandık ve çalışma sürelerini karşılaştırdık. Bu karşılaştırmada, toplam 43 seçiciyi [aria-label=…] yerine özel bir sorgu işleyici aria/… kullanacak şekilde değiştirdik ve ardından prototiplerin her birini kullanarak uyguladık.

Seçicilerden bazıları test komut dosyalarında birden çok kez kullanıldığından, aria sorgu işleyicisinin gerçek yürütme sayısı, paketin her çalıştırılışında 113 oldu. Toplam sorgu seçimi sayısı 2.253'tü. Dolayısıyla, sorgu seçimlerinin yalnızca küçük bir kısmı prototipler aracılığıyla gerçekleşti.

Karşılaştırma: uçtan uca test paketi

Yukarıdaki şekilde görüldüğü gibi, toplam çalışma süresinde belirgin bir fark vardır. Veriler, belirli bir sonuca varmak için çok fazla gürültülü ancak iki prototip arasındaki performans farkının bu senaryoda da görüldüğü açık.

Yeni bir CDP uç noktası

Yukarıdaki karşılaştırmalar ışığında ve başlatma işaretine dayalı yaklaşım genel olarak istenmediğinden, erişilebilirlik ağacını sorgulamak için yeni bir CDP komutu uygulamaya karar verdik. Şimdi bu yeni uç noktanın arayüzünü belirlememiz gerekiyordu.

Puppeteer'daki kullanım alanımız için uç noktanın, RemoteObjectIds adlı bağımsız değişkeni alması ve daha sonra ilgili DOM öğelerini bulabilmemiz için DOM öğelerinin backendNodeIds değerini içeren bir nesne listesi döndürmesi gerekir.

Aşağıdaki grafikte görüldüğü gibi, bu arayüzü karşılayan pek çok yaklaşım denedik. Bunun sonucunda, döndürülen nesnelerin boyutunun (yani tam erişilebilirlik düğümlerini mi yoksa yalnızca backendNodeIds'i mi döndürdüğümüzün) belirgin bir fark yaratmadığını gördük. Öte yandan, mevcut NextInPreOrderIncludingIgnored'yi kullanmanın, geçiş mantığını burada uygulamak için kötü bir seçim olduğunu ve bunun belirgin bir yavaşlamaya neden olduğunu gördük.

Karşılaştırma: CDP tabanlı AXTree geçiş prototiplerinin karşılaştırması

Özet

CDP uç noktası hazır olduğuna göre sorgu işleyiciyi Puppeteer tarafında uyguladık. Buradaki en zorlu iş, sorguların sayfa bağlamında değerlendirilen JavaScript aracılığıyla sorgulanmak yerine doğrudan CDP aracılığıyla çözümlenmesini sağlamak için sorgu işleme kodunu yeniden yapılandırmaktı.

Sırada ne var?

Yeni aria işleyici, yerleşik bir sorgu işleyici olarak Puppeteer v5.4.0 ile birlikte sunulur. Kullanıcıların bu özelliği test komut dosyalarına nasıl uyarlayacağını görmek için sabırsızlanıyoruz. Bu özelliği daha da kullanışlı hale getirmek için fikirlerinizi duymaktan memnuniyet duyarız.

Önizleme kanallarını indirme

Varsayılan geliştirme tarayıcınız olarak Chrome Canary, Dev veya Beta'yı kullanabilirsiniz. Bu önizleme kanalları, en yeni Geliştirici Araçları özelliklerine erişmenizi sağlar, en yeni web platformu API'lerini test etmenize olanak tanır ve sitenizdeki sorunları kullanıcılarınızdan önce bulmanıza yardımcı olur.

Chrome Geliştirici Araçları ekibiyle iletişime geçme

Yeni özellikler, güncellemeler veya Geliştirici Araçları ile ilgili diğer konuları tartışmak için aşağıdaki seçenekleri kullanın.