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

Johan Koyu
Johan Bay

Puppeteer ve seçicilere yaklaşımı

Puppeteer, Düğümü için bir tarayıcı otomasyon kitaplığıdır: Basit ve modern bir JavaScript API'si kullanarak tarayıcıyı kontrol etmenizi sağlar.

En belirgin tarayıcı görevi elbette web sayfalarına göz atmaktır. Bu görevi otomatikleştirme temelde web sayfasıyla etkileşimleri otomatik hale getirmek anlamına gelir.

Puppeteer'da bu, dize tabanlı seçiciler kullanılarak DOM öğelerinin sorgulanması ve öğelerin üzerine tıklama veya metin yazma gibi işlemler uygulanarak gerçekleştirilir. Örneğin, developer.google.com'u açan, arama kutusunu bulan ve puppetaria için arama yapan bir komut dosyası aşağıdaki gibi 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çiciler kullanılarak öğelerin nasıl tanımlandığı, Puppeteer deneyiminin tanımlayıcı bir parçasıdır. Şimdiye kadar Puppeteer'daki seçiciler CSS ve XPath seçicilerle sınırlıydı. Bunlar ifadesel açıdan çok güçlü olsalar da komut dosyalarındaki kalıcı tarayıcı etkileşimleri için dezavantajlar oluşturabiliyor.

Sözdizimsel ve anlamsal seçiciler

CSS seçiciler doğaları gereği sözdizimseldir; DOM ağacındaki kimliklere ve sınıf adlarına referans verdikleri için DOM ağacının metinsel temsilinin iç işleyişine sıkı bir şekilde bağlıdır. Bu nedenle, web geliştiricilerine bir sayfadaki bir öğeyi değiştirme veya stil ekleme işlemleri için entegre bir araç sağlar. Ancak bu bağlamda, geliştirici sayfa ve DOM ağacı üzerinde tam kontrole sahiptir.

Diğer yandan, Puppeteer komut dosyası bir sayfanın harici gözlemcisidir. Dolayısıyla, bu bağlamda CSS seçiciler kullanıldığında, sayfanın nasıl uygulandığına dair, Puppeteer komut dosyasının üzerinde kontrol sahibi olmadığı gizli varsayımlar oluşturur.

Bunun sonucunda, bu tür komut dosyaları kırılabilir ve kaynak kodu değişikliklerine açık olabilir. Örneğin, bir örnekte body öğesinin üçüncü alt öğesi olarak <button>Submit</button> düğümünü içeren bir web uygulaması için otomatik test amaçlı Puppeteer komut dosyaları kullandığını varsayalım. Test durumundaki bir snippet aşağıdaki gibi 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çiciyi kullanıyoruz, ancak burada web sayfasının tam olarak bu sürümüyle ilgili bir bağlantı var. Daha sonra düğmenin üzerine bir öğe eklenirse bu seçici artık çalışmaz!

Bu, test yazarlarına yönelik bir haber değildir: Kuklacı kullanıcılar zaten bu tür değişikliklere uyum sağlayan seçicileri seçmeye çalışmaktadır. Puppetaria ile kullanıcılara bu görevde yeni bir araç sunuyoruz.

Puppeteer artık CSS seçicilere bağlı kalmak yerine erişilebilirlik ağacının sorgulanmasına dayalı alternatif bir sorgu işleyici sunuyor. Buradaki temel felsefe, seçmek istediğimiz somut öğe değişmediyse karşılık gelen erişilebilirlik düğümünün de değişmemesi gerektiğidir.

Bu tür seçicileri "ARIA seçicileri" olarak adlandırıyoruz ve erişilebilirlik ağacının hesaplanan erişilebilir adı ve rolü için sorgulama desteği sağlıyoruz. CSS seçicilerle karşılaştırıldığında bu özellikler semantik yapıdadır. Bunlar, DOM'un sözdizimsel özelliklerine bağlı değildir. Bunun yerine, ekran okuyucu gibi yardımcı teknolojiler aracılığıyla sayfanın nasıl gözlemlendiğini açıklar.

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

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

Şimdi, düğmemizin metin içeriğini Submit yerine Done olarak değiştirmeye karar verirsek test yine başarısız olur, ancak bu istenen bir durumdur: düğmenin adını değiştirerek sayfanın görsel sunumunun veya DOM'de nasıl yapılandırıldığının aksine sayfanın içeriğini değiştiririz. Bu tür değişikliklerin bilinçli olarak yapıldığından emin olmak için testlerimiz bu tür değişiklikler konusunda bizi uyarmalıdır.

Arama çubuğuyla ilgili büyük örneğe dönersek, yeni aria işleyicisini kullanıp

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

ve

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

arama çubuğunu bulun!

Daha genel olarak açıklamak gerekirse, bu tür ARIA seçicileri kullanmanı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 dirençli hale getirin.
  • Test komut dosyalarının daha okunabilir olmasını sağlayın (erişilebilir adlar anlamsal tanımlayıcılardır).
  • Öğelere erişilebilirlik özellikleri atamak için iyi uygulamaları motive 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 açıklandığı gibi, öğelerin erişilebilir adlarına ve rollerine göre sorgulanmasını sağlamak istiyoruz. Bunlar, normal DOM ağacının ikilisi olan ve ekran okuyucular gibi cihazlar tarafından web sayfalarını göstermek için kullanılan erişilebilirlik ağacı'nın özellikleridir.

Erişilebilir adı hesaplama spesifikasyonuna bakıldığında, bir öğenin adını hesaplamanın önemsiz bir iş olduğu açıktır. Bu nedenle, en başından beri Chromium'un mevcut altyapısını bunun için yeniden kullanmaya karar verdik.

Veri analizi sürecini uygulama yaklaşımımız

Chromium'un erişilebilirlik ağacını kullanmakla sınırlı kalsak da ARIA sorgularını Puppeteer'da uygulayabileceğimiz pek çok yöntem var. Nedenini görmek 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 bir hata ayıklama arayüzü sunar. Bu, dilden bağımsız bir arayüz üzerinden "sayfayı yeniden yükle" veya "sayfada bu JavaScript parçasını çalıştır ve sonucu geri ver" gibi işlevleri açığa çıkarır.

Hem Geliştirici Araçları kullanıcı arabirimi hem de Puppeteer, tarayıcıyla konuşmak için CDP'yi kullanır. CDP komutlarını uygulamak için Chrome'un tüm bileşenlerinde (tarayıcıda, oluşturucuda vb.) DevTools altyapısı vardır. CDP, komutların doğru yere yönlendirilmesini sağlar.

İfadeleri sorgulama, tıklama ve değerlendirme gibi kuklacı işlemleri, JavaScript'i doğrudan sayfa bağlamında değerlendiren ve sonucu geri sunan Runtime.evaluate gibi CDP komutlarından yararlanılarak gerçekleştirilir. Renk körlüğü emülasyonu, ekran görüntüsü alma veya izleri yakalama gibi diğer kuklacı işlemleri, göz kırpma oluşturma süreciyle doğrudan iletişim kurmak için CDP'yi kullanır.

CDP

Bu durum bize, sorgulama işlevimizi uygulamamız için iki yol bırakıyor. Bunu şöyle yapabiliriz:

  • Sorgulama mantığımızı JavaScript'te yazıp Runtime.evaluate ile sayfaya yerleştirin veya
  • Blink işlemi sırasında doğrudan erişilebilirlik ağacına erişip bunları sorgulayabilen bir CDP uç noktası kullanın.

3 prototip uyguladık:

  • JS DOM geçişi - Sayfaya JavaScript yerleştirilmesine dayalı
  • Puppeteer AXTree geçişi - Erişilebilirlik ağacına mevcut CDP erişimini kullanmaya dayalı
  • CDP DOM geçişi: Erişilebilirlik ağacını sorgulamak için tasarlanmış yeni bir CDP uç noktası kullanma

JS DOM geçişi

Bu prototip, DOM'da tam geçiş yapar ve geçiş sırasında her öğenin adını ve rolünü almak için ComputedAccessibilityInfo başlatma işareti ile sınırlanmış element.computedName ve element.computedRole kullanır.

Kuklacı AXTree geçişi

Burada, bunun yerine CDP aracılığıyla erişilebilirlik ağacının tamamını alıp Puppeteer'da çekiyoruz. Ortaya çıkan 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ı sorgulamak için yeni bir CDP uç noktası uyguladık. Bu şekilde, sorgulama işlemi JavaScript üzerinden sayfa bağlamında değil, bir C++ uygulaması aracılığıyla 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 sorgulandığı toplam çalışma süresi karşılaştırılmaktadır. Karşılaştırma, sayfa boyutlarına göre değişen ve erişilebilirlik öğelerinin önbelleğe alma işleminin etkin olup olmadığı gibi 3 farklı yapılandırmada yürütüldü.

Karşılaştırma: Dört öğenin 1.000 kez sorgulandığı toplam çalışma zamanı

CDP destekli sorgulama mekanizması ile yalnızca Puppeteer'da uygulanan diğer iki yöntem arasında önemli bir performans farkı olduğu çok açık ve göreli fark, sayfa boyutuyla birlikte önemli ölçüde artıyor. JS DOM geçiş prototipinin, erişilebilirlik önbelleğe alma özelliğinin etkinleştirilmesine bu kadar iyi yanıt verdiğini görmek biraz ilginçtir. Önbelleğe alma devre dışıyken erişilebilirlik ağacı istek üzerine hesaplanır ve alan devre dışı bırakılırsa her etkileşimden sonra ağacı siler. Alan adı etkinleştirildiğinde Chromium, hesaplanan ağaç olarak önbelleğe alınır.

JS DOM geçişi için, geçiş sırasında her öğenin erişilebilir adını ve rolünü isteriz. Dolayısıyla, ö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 her CDP çağrısı arasında (yani her sorgu için) silinir. Erişilebilirlik ağacı CDP çağrıları genelinde devam ettirildiğinden, bu yaklaşımlar önbelleğe almanın etkinleştirilmesinden de fayda sağlar. Bu nedenle, performans artışı nispeten daha küçüktür.

Burada önbelleğe almanın etkinleştirilmesi istenilen görünse de, ek bellek kullanımı maliyeti de beraberinde gelir. Örneğin, izleme dosyalarını kaydeden Puppeteer komut dosyaları için bu durum sorunlu olabilir. Bu nedenle, erişilebilirlik ağaçlarını önbelleğe almayı varsayılan olarak etkinleştirmemeye karar verdik. Kullanıcılar, CDP Erişilebilirlik alanını etkinleştirerek önbelleğe almayı 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 test paketi çalıştırmaya yönelik daha gerçekçi bir senaryoda fark edilecek kadar telaffuz edilip edilmediğini görmek amacıyla JavaScript ve CDP tabanlı prototiplerden yararlanmak için DevTools uçtan uca test paketine yama uyguladık ve çalışma zamanlarını karşılaştırdık. Bu karşılaştırmada, [aria-label=…] olan toplam 43 seçiciyi aria/… özel sorgu işleyicisi ile değiştirdik ve daha sonra bunu prototiplerin her birini kullanarak uyguladık.

Seçicilerden bazıları test komut dosyalarında birden çok kez kullanıldığından, aria sorgu işleyicinin gerçek yürütme sayısı paketin her çalıştırması için 113'tü. Sorgu seçimlerinin toplam sayısı 2253'tü, bu nedenle sorgu seçimlerinin yalnızca bir kısmı prototipler üzerinden yapıldı.

Karşılaştırma: e2e test paketi

Yukarıdaki şekilde görüldüğü gibi, toplam çalışma zamanı arasında belirgin bir fark vardır. Veriler belirli bir sonuca varmak için fazla gürültülüdür, ancak iki prototip arasındaki performans boşluğunun bu senaryoda da ortaya çıktığı açıktır.

Yeni bir CDP uç noktası

Yukarıdaki karşılaştırmalara göre ve lansmanda bayrak tabanlı yaklaşım genel olarak istenmeyen bir yaklaşım olduğundan, erişilebilirlik ağacını sorgulamak için yeni bir CDP komutu uygulamaya devam etmeye karar verdik. Şimdi, bu yeni uç noktanın arayüzünü çözmemiz gerekiyordu.

Puppeteer'daki kullanım örneğimizde, RemoteObjectIds adlı uç noktanın bağımsız değişken olarak alınması ve karşılık gelen DOM öğelerini daha sonra bulabilmemiz için uç noktanın, DOM öğeleri için backendNodeIds içeren nesnelerin listesini döndürmesi gerekir.

Aşağıdaki grafikte görüldüğü gibi, bu arayüze hitap eden birçok yaklaşım denedik. Döndürülen nesnelerin boyutunun, yani tam erişilebilirlik düğümleri döndürüp döndürmediğimizi veya yalnızca backendNodeIds öğesinin belirgin bir fark yaratmadığını tespit ettik. Diğer yandan, burada geçiş mantığını uygulamak için mevcut NextInPreOrderIncludingIgnored kullanmanın kötü bir seçim olduğunu, çünkü belirgin bir yavaşlamaya neden olduğunu tespit ettik.

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

Özetle

CDP uç noktası hazır olduğunda, sorgu işleyiciyi Puppeteer tarafında uyguladık. Burada yapılan iş, sorgu işleme kodunu, sorguların sayfa bağlamında değerlendirilen JavaScript üzerinden sorgulamak yerine doğrudan CDP üzerinden çözümlenmesini sağlayacak şekilde yeniden yapılandırmaktı.

Sonraki adım

Yeni aria işleyici, yerleşik bir sorgu işleyici olarak Puppeteer v5.4.0 ile gönderildi. Kullanıcıların bu özelliği test komut dosyalarına nasıl dahil ettiklerini görmek için sabırsızlanıyoruz. Bunu daha da yararlı hale nasıl getirebileceğimize ilişkin fikirlerinizi duymak için sabırsızlanı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ı ile Geliştirici Araçları'nın en yeni özelliklerine erişebilir, son teknoloji ürünü web platformu API'lerini test edebilir ve sitenizdeki sorunları kullanıcılarınızdan önce tespit edebilirsiniz.

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

Yayındaki yeni özellikler ve değişiklikler ya da Geliştirici Araçları ile ilgili diğer konular hakkında konuşmak için aşağıdaki seçenekleri kullanın.

  • crbug.com adresinden bize öneri veya geri bildirim gönderin.
  • Geliştirici Araçları'nda Diğer seçenekler > Yardım > Geliştirici Araçları sorunu bildir'i kullanarak Geliştirici Araçları sorunlarını bildirin.Daha fazla
  • @ChromeDevTools adresine tweet gönderin.
  • Geliştirici Araçları YouTube videoları bölümündeki yenilikler veya Geliştirici Araçları İpuçları YouTube videoları bölümlerimize yorum yapın.