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 görevi elbette web sayfalarında gezinmektir. Bu görevi otomatikleştirmek, temel olarak 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 sayfasını 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ığı, Kuklacı deneyiminin tanımlayıcı 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 entegre 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 test amacıyla Puppeteer komut dosyaları kullandığı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 bağlıdır. Düğmenin üzerine daha sonra bir öğe eklenirse bu seçici artık çalışmaz.

Bu durum test yazarları için yeni bir şey değil: Puppeteer kullanıcıları zaten bu tür değişikliklere karşı dayanıklı seçicileri seçmeye çalışıyor. Puppetaria ile bu görevde kullanıcılara 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 sunulmaktadır. 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çicilerle karşılaştırıldığında bu özellikler anlamsaldır. 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. Bu durumda 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 tür değişiklikler konusunda uyarır.

Arama çubuğunu içeren daha büyük örneğe geri dönecek olursak, yeni aria işleyicisini kullanabilir ve

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

ile

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

tıklayın!

Daha genel anlamda bu tür ARIA seçicileri kullanmanın Puppeteer kullanıcılarına aşağıdaki avantajları sağlayabileceğini düşünü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 atama konusunda iyi uygulamaları teşvik etme.

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

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 önemsiz bir görev olduğu açıktır. Bu nedenle, başından itibaren bu amaçla Chromium'un mevcut altyapısını yeniden kullanmaya 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 görmek için önce Puppeteer'ın tarayıcıyı nasıl kontrol ettiğini inceleyelim.

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.

İfadeleri sorgulama, tıklama ve değerlendirme gibi kuklacıların işlemleri, JavaScript'i doğrudan sayfa bağlamında değerlendiren ve sonucu geri veren Runtime.evaluate gibi CDP komutlarından yararlanarak gerçekleştirilir. Renk körlüğü emülasyonu, ekran görüntüsü alma veya izleri yakalama gibi diğer Puppeteer işlemleri, Blink oluşturma süreciyle doğrudan 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.
  • Doğrudan Blink işleminde erişilebilirlik ağacına erişebilen ve sorgulayabilen bir CDP uç noktası kullanın.

3 prototip uyguladık:

  • JS DOM geçişi: Sayfaya JavaScript eklenmesine dayanır
  • Puppeteer AXTree geçişi: Erişilebilirlik ağacına mevcut CDP erişiminin kullanılmasına dayanır
  • CDP DOM geçişi: Erişilebilirlik ağacını sorgulamak için amaca yönelik yeni bir CDP uç noktası kullanır

JS DOM geçişi

Bu prototip, DOM'de tüm geçişi yapar ve geçiş sırasında her bir öğenin adını ve rolünü almak için ComputedAccessibilityInfo başlatma işaretinde kontrollü 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 özellikle erişilebilirlik ağacını sorgulamak amacıyla 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ırma değeri

Aşağıdaki şekilde, 3 prototip için dört öğenin 1.000 kez sorguladığı 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. 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 adı 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ıyla ilgili bir 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, sorgulama mekanizmamızı CDP katmanında uygulamanın, klinik birim testi senaryosunda 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 ardından prototiplerin her birini kullanarak 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 ışığında, lansman işaretini temel alan yaklaşım genel olarak istenmeyen bir işlem olduğu için 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, RemoteObjectIds olarak adlandırılan bir bağımsız değişkeni alması gerekir. 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ü karşılayan birçok 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) fark yaratmadığını tespit ettik. Diğer yandan, burada geçiş mantığını uygulamak için mevcut NextInPreOrderIncludingIgnored kullanımını kullanmanın kötü bir seçim olduğunu tespit ettik. Çünkü bu belirgin bir yavaşlamaya neden olmuştur.

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

Özet

Şimdi, CDP uç noktası yerine, sorgu işleyiciyi Kuklalayıcı tarafında uyguladık. Buradaki çalışmanın en büyük amacı, sorgu işleme kodunu, sayfa bağlamında değerlendirilen JavaScript üzerinden sorgulama yerine, sorguların doğrudan CDP üzerinden çözümlenmesini sağlayacak şekilde yeniden yapılandırmaktı.

Sırada ne var?

Yeni aria işleyici, yerleşik sorgu işleyici olarak Puppeteer v5.4.0 ile gönderilir. Kullanıcıların bunu test komut dosyalarına nasıl uyarlayacağını görmek için sabırsızlanıyoruz. Bunu nasıl daha da yararlı hale getirebileceğimizle ilgili 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ı, en son DevTools ö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 başka herhangi bir konu hakkında konuşmak için aşağıdaki seçenekleri kullanın.