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'si kullanarak tarayıcı kontrol etmenize olanak tanır.

Tarayıcıların en önemli işlevi 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 kullanılarak DOM öğeleri sorgulanarak ve öğeleri tıklama veya öğelere metin yazma gibi işlemler gerçekleştirerek yapılır. Örneğin, developer.google.com adresini açan, arama kutusunu bulan ve puppetaria için arama yapan bir komut dosyası şöyle 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, sorgu seçicileri kullanılarak öğelerin nasıl tanımlandığı Puppeteer deneyiminin belirleyici bir parçasıdır. Şimdiye kadar Puppeteer'daki seçiciler, ifade açısından çok güçlü olsa da komut dosyalarında tarayıcı etkileşimlerini sürdürme konusunda dezavantajları olabilecek CSS ve XPath seçicileriyle sınırlıydı.

Söz dizimi ve semantik seçiciler

CSS seçicileri yapısal olarak söz dizimi kurallarına bağlıdır. DOM'daki kimliklere ve sınıf adlarına referans verdikleri için DOM ağacının metinsel temsilinin iç işleyişine sıkı sıkıya bağlıdırlar. Bu nedenle, web geliştiricileri için bir sayfadaki öğeleri değiştirmek veya öğelere stil eklemek üzere temel bir araç sağlarlar. 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 bağlamda CSS seçicileri kullanıldığında, sayfanın nasıl uygulandığıyla ilgili gizli varsayımlar ortaya çıkar. Puppeteer komut dosyasının bu varsayımlar üzerinde hiçbir kontrolü yoktur.

Bunun sonucunda bu tür komut dosyaları kararsız ve kaynak kod 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ı kullanıldığını varsayalım. Bir test durumundaki snippet şöyle 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 seçici, web sayfasının tam olarak bu sürümüne sıkı sıkıya bağlıdır. Düğmenin üzerine daha sonra bir öğe eklenirse bu seçici artık çalışmaz.

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

Puppeteer artık CSS seçicilere güvenmek yerine erişilebilirlik ağacını sorgulamayı temel alan alternatif bir sorgu işleyiciyle birlikte gönderiliyor. Buradaki temel felsefe, seçmek istediğimiz somut öğe değişmediyse ilgili erişilebilirlik düğümünün de değişmemiş olması gerektiğidir.

Bu tür seçicileri "ARIA seçicileri" olarak adlandırırız ve erişilebilirlik ağacının hesaplanan erişilebilir adı ve rolü için sorgu desteklenir. CSS seçicilere kıyasla bu özellikler anlamsal niteliktedir. Bunlar DOM'un söz dizimi özelliklerine bağlı değildir. Bunun yerine, sayfanın ekran okuyucu gibi yardımcı teknolojiler aracılığıyla nasıl gözlemlendiğine dair 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 adını ifade eder:

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

Şimdi, 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 sunumunu veya DOM'da nasıl yapılandırıldığını değil, içeriğini değiştiririz. Testlerimiz, bu tür değişikliklerin kasıtlı olarak yapıldığından emin olmak için bizi bu değişiklikler hakkında uyarır.

Arama çubuğu içeren daha büyük örneği ele alırsak 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 bulun.

Daha genel olarak, bu tür ARIA seçicilerinin 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 kod değişikliklerine karşı daha dayanıklı hale getirin.
  • Test komut dosyalarını daha okunaklı hale getirin (erişilebilir adlar, anlamsal tanımlayıcılardır).
  • Öğelere erişilebilirlik özellikleri atamayla ilgili iyi uygulamaları teşvik edin.

Bu makalenin geri kalanında, Puppetaria projesini nasıl uyguladığımızla ilgili ayrıntılara değinilmektedir.

Tasarım süreci

Arka plan

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

Erişilebilir adı hesaplama spesifikasyonuna bakıldığında, bir öğenin adını hesaplamanın basit bir iş olmadığı açıkça görülüyor. Bu nedenle, baştan beri Chromium'un mevcut altyapısını bu amaç için yeniden kullanmak istediğimize karar verdik.

Bu özelliği uygulamaya nasıl yaklaştık?

Chromium'un erişilebilirlik ağacını kullanmakla sınırlı kalsak bile Puppeteer'da ARIA sorgulamasını uygulamanın birkaç yolu vardır. Bunun nedenini anlamak için önce Puppeteer'ın tarayıcıyı nasıl kontrol ettiğini görelim.

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

Hem DevTools ö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şturma aracında vb.) DevTools 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ğerlendirip sonucu döndüren Runtime.evaluate gibi CDP komutlarından yararlanılarak gerçekleştirilir. Renk görme eksikliğini taklit etme, ekran görüntüsü alma veya izleme yakalama gibi diğer Puppeteer işlemleri, doğrudan Blink oluşturma işlemiyle iletişim kurmak için CDP'yi kullanır.

CDP

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

  • Sorgulama mantığımızı JavaScript'te yazıp Runtime.evaluate kullanarak sayfaya yerleştirin veya
  • Erişilebilirlik ağacına doğrudan Blink sürecinde erişip sorgu oluşturabilen bir CDP uç noktası kullanın.

3 prototip uyguladık:

  • JS DOM traversal (JS DOM'u tarama): Sayfaya JavaScript enjeksiyonuna dayanır.
  • Puppeteer AXTree traversal (Puppeteer AXTree traversal): Erişilebilirlik ağacına mevcut CDP erişimini kullanmaya dayalı
  • CDP DOM'u tarama: Erişilebilirlik ağacını sorgulamak için özel olarak tasarlanmış yeni bir CDP uç noktası kullanılır.

JS DOM geçişi

Bu prototip, DOM'u tamamen tarar ve tarama sırasında her öğenin adını ve rolünü almak için ComputedAccessibilityInfo başlatma işaretinde element.computedName ve element.computedRole'ü kullanır.

Puppeteer AXTree geçişi

Bunun yerine, CDP üzerinden erişilebilirlik ağacının tamamını alır ve Puppeteer'da ağaçta geziniriz. Elde edilen erişilebilirlik düğümleri daha sonra DOM düğümleriyle eşlenir.

CDP DOM geçişi

Bu prototip için, erişilebilirlik ağacını sorgulamak üzere özel olarak yeni bir CDP uç noktası uyguladık. Bu sayede sorgu, JavaScript aracılığıyla sayfa bağlamında değil, arka uçta C++ uygulaması üzerinden yapılabilir.

Birim testi karşılaştırması

Aşağıdaki şekilde, 3 prototip için dört öğenin 1.000 kez sorgulanması işleminin toplam çalışma süresi karşılaştırılmaktadır. Karşılaştırma, sayfa boyutuna ve erişilebilirlik öğelerinin önbelleğe alınıp alınmadığına göre değişen 3 farklı yapılandırmada gerçekleştirildi.

Karşılaştırma: Dört öğenin 1.000 kez sorgulanması için gereken toplam çalışma süresi

CDP destekli sorgu mekanizması ile yalnızca Puppeteer'da uygulanan diğer iki mekanizma arasında önemli bir performans farkı olduğu açıkça görülüyor. Ayrıca, göreceli fark sayfa boyutuyla birlikte önemli ölçüde artıyor. JS DOM tarama prototipinin erişilebilirlik önbelleğe alma özelliğini etkinleştirmeye bu kadar iyi yanıt vermesi ilginç. Önbelleğe alma devre dışıyken erişilebilirlik ağacı isteğe bağlı olarak hesaplanır ve alan devre dışıysa her etkileşimden sonra ağaç atılır. Alan etkinleştirildiğinde Chromium, bunun yerine hesaplanan ağacı önbelleğe alır.

JS DOM tarama işleminde, tarama sırasında her öğenin erişilebilir adını ve rolünü isteriz. Bu nedenle, önbelleğe alma devre dışıysa Chromium, ziyaret ettiğimiz her öğenin erişilebilirlik ağacını hesaplar ve atar. Öte yandan CDP tabanlı yaklaşımlarda ağaç yalnızca CDP'ye yapılan her çağrı arasında, yani her sorgu için atılır. Bu yaklaşımlar, erişilebilirlik ağacı CDP çağrıları arasında kalıcı olduğu için önbelleğe alma özelliğinin etkinleştirilmesinden de yararlanır. Ancak bu nedenle performans artışı nispeten daha küçüktür.

Burada önbelleğe alma özelliğini etkinleştirmek cazip görünse de ek bellek kullanımı maliyeti vardır. Örneğin, iz dosyalarını kaydeden Puppeteer komut dosyaları için bu sorunlu olabilir. Bu nedenle, erişilebilirlik ağacı önbelleğe alma özelliğini varsayılan olarak etkinleştirmemeye karar verdik. Kullanıcılar, CDP Erişim alanı'nı etkinleştirerek önbelleğe almayı kendileri açabilir.

DevTools test paketi karşılaştırması

Önceki karşılaştırma, sorgu mekanizmamızı CDP katmanında uygulamanın klinik bir birim testi senaryosuna performans artışı sağladığını gösterdi.

Farkın, tam bir test paketinin çalıştırıldığı daha gerçekçi bir senaryoda fark edilecek kadar belirgin olup olmadığını görmek için JavaScript ve CDP tabanlı prototiplerden yararlanmak üzere DevTools uçtan uca test paketine bir yama uyguladık ve çalışma sürelerini karşılaştırdık. Bu karşılaştırmada, toplam 43 seçiciyi [aria-label=…]'ten özel bir sorgu işleyici aria/…'ye değiştirdik ve daha sonra prototiplerin her birini kullanarak bu işleyiciyi uyguladık.

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

Karşılaştırma: e2e 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ü içeriyor 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 göz önüne alındığında ve lansman 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ü çözmemiz gerekiyordu.

Puppeteer'daki kullanım alanımızda, uç noktanın sözde RemoteObjectIds değerini bağımsız değişken olarak almasına ihtiyacımız var. Daha sonra ilgili DOM öğelerini bulabilmemiz için uç nokta, DOM öğelerinin backendNodeIds değerini içeren bir nesne listesi döndürmelidir.

Aşağıdaki grafikte görüldüğü gibi, bu arayüzü tatmin edecek oldukça fazla yaklaşım denedik. Bu çalışmadan, döndürülen nesnelerin boyutunun (ör.erişilebilirlik düğümlerinin tamamını mı yoksa yalnızca backendNodeIds değerini mi döndürdüğümüzün) belirgin bir fark oluşturmadığını tespit ettik. Öte yandan, mevcut NextInPreOrderIncludingIgnored öğesinin kullanılmasının, gözle görülür bir yavaşlamaya neden olduğu için burada gezinme mantığını uygulamak için kötü bir seçim olduğunu tespit ettik.

Karşılaştırma: CDP tabanlı AXTree traversal prototiplerinin karşılaştırması

Özet

CDP uç noktası kullanıma sunulduğunda sorgu işleyiciyi Puppeteer tarafına uyguladık. Buradaki çalışmanın ana amacı, sorguları sayfa bağlamında değerlendirilen JavaScript aracılığıyla sorgulamak yerine doğrudan CDP üzerinden çözmek 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 gönderilir. Kullanıcıların bu özelliği test komut dosyalarına nasıl entegre edeceğini görmek için sabırsızlanıyoruz. Ayrıca, bu özelliği nasıl daha da yararlı hale getirebileceğimizle ilgili fikirlerinizi duymak için can atıyoruz.

Önizleme kanallarını indirme

Varsayılan geliştirme tarayıcınız olarak Chrome Canary, Yeni Geliştirilenler veya Beta sürümünü kullanabilirsiniz. Bu önizleme kanalları, en son DevTools özelliklerine erişmenize, en yeni web platformu API'lerini test etmenize ve sitenizdeki sorunları kullanıcılarınızdan önce bulmanıza yardımcı olur.

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

Yeni özellikler, güncellemeler veya Geliştirici Araçları ile ilgili başka herhangi bir konu hakkında konuşmak için aşağıdaki seçenekleri kullanın.