Puppetaria: סקריפטים של Puppeteer שמתמקדים בנגישות

מפרץ יוהאן
יוהאן ביי

בובן-אפקט והגישה שלו לבוררים

Puppeteer היא ספריית אוטומציה של הדפדפן עבור הצומת: היא מאפשרת לכם לשלוט בדפדפן באמצעות JavaScript API פשוט ומודרני.

משימת הדפדפן החשובה ביותר היא, כמובן, גלישה בדפי אינטרנט. אוטומציה של משימה זו היא למעשה אוטומציה של אינטראקציות עם דף האינטרנט.

ב-Puppeteer אפשר לעשות זאת על ידי שליחת שאילתות לרכיבי DOM באמצעות סלקטורים מבוססי-מחרוזות וביצוע פעולות כמו לחיצה או הקלדת טקסט ברכיבים. לדוגמה, סקריפט שפותח את developer.google.com מוצא את תיבת החיפוש, וחיפושים של puppetaria עשויים להיראות כך:

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

לכן, האופן שבו רכיבים מזוהים באמצעות בוררי שאילתות הוא חלק חשוב בחוויה של ה-Puppeteer. עד עכשיו, הסלקטורים ב-Puppeteer הוגבלו לסלקטורים ב-CSS וב-XPath. הסלקטורים אמנם מאוד עוצמתיים, אבל יש להם חסרונות שקשורים לאינטראקציות מתמשכות של הדפדפן בסקריפטים.

בוררים תחביריים לעומת בוררים סמנטיים

סלקטורים ב-CSS הם תחביריים מטבעם. הם קשורים באופן הדוק לתפעול הפנימי של הייצוג הטקסטואלי של עץ ה-DOM, בכך שהם מפנים למזהים ולשמות מחלקה מה-DOM. ככאלה, הן מספקות כלי בלתי נפרד למפתחי אינטרנט לשינוי או להוספת סגנונות לרכיב בדף, אבל בהקשר הזה יש למפתח שליטה מלאה בדף ובעץ ה-DOM שלו.

מצד שני, סקריפט של ה-Puppeteer הוא צופה חיצוני של דף. לכן, כאשר נעשה שימוש בסלקטורים ב-CSS בהקשר הזה, הוא מציג הנחות נסתרות לגבי אופן היישום של הדף, שהסקריפט של ה-Puppeteer לא שולט עליהן.

כתוצאה מכך, סקריפטים כאלה עלולים להיות שבירים ורגישים לשינויים בקוד המקור. לדוגמה, נניח שמשתמש אחד משתמש בסקריפטים של Puppeteer לבדיקות אוטומטיות עבור אפליקציית אינטרנט שמכילה את הצומת <button>Submit</button> בתור הצאצא השלישי של הרכיב body. קטע טקסט אחד ממקרה בדיקה עשוי להיראות כך:

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

כאן אנחנו משתמשים בבורר 'body:nth-child(3)' כדי למצוא את לחצן השליחה, אבל הוא קשור באופן הדוק לגרסה הזו של דף האינטרנט. אם רכיב נוסף מאוחר יותר מעל הלחצן, הבורר לא פועל יותר.

אלה לא חדשות לבדיקת מחברים: המשתמשים של ה-Puppeteer כבר מנסים לבחור בוררים שמעידים על שינויים כאלה. במשחק Puppetaria, אנחנו מספקים למשתמשים כלי חדש במסע הזה.

ה-Puppeteer כולל עכשיו handler חלופי לשאילתות שמבוססות על שאילתה בעץ הנגישות, ולא על סמך סלקטורים ב-CSS. הפילוסופיה הבסיסית כאן היא שאם רכיב הבטון שאנחנו רוצים לבחור לא השתנה, גם צומת הנגישות המתאים לא אמור להשתנות.

אנחנו קוראים לסלקטורים כאלה 'סלקטורים של ARIA' ותומכים בשליחת שאילתות לגבי השם הנגיש והתפקיד המחושבים של עץ הנגישות. בהשוואה לסלקטורים ב-CSS, המאפיינים האלה הם סמנטיים. הם לא קשורים למאפיינים התחביריים של ה-DOM, אלא מתארים את האופן שבו הדף נצפה בטכנולוגיות מסייעות כמו קוראי מסך.

בדוגמת סקריפט הבדיקה שלמעלה, נוכל במקום זאת להשתמש בבורר aria/Submit[role="button"] כדי לבחור בלחצן הרצוי, כאשר Submit מתייחס לשם הנגיש של הרכיב:

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

אם נחליט בשלב מאוחר יותר לשנות את תוכן הטקסט של הלחצן שלנו מ-Submit ל-Done, הבדיקה שוב תיכשל, אך במקרה זה רצוי. על ידי שינוי שם הלחצן אנו משנים את תוכן הדף, בניגוד להצגה החזותית שלו או לאופן שבו הוא נוצר ב-DOM. הבדיקות שלנו צריכות להזהיר אותנו מפני שינויים כאלה, כדי להבטיח ששינויים כאלה יהיו מכוונים.

נחזור לדוגמה הגדולה יותר עם סרגל החיפוש. אפשר להשתמש ב-handler החדש של aria ולהחליף אותו

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

עם

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

כדי לאתר את סרגל החיפוש!

באופן כללי, אנחנו מאמינים ששימוש בסלקטורים כאלה של ARIA יכול לספק את היתרונות הבאים למשתמשים של ה-Puppeteer:

  • לשפר את יכולת הבוררות בסקריפטים של הבדיקה לשינויים בקוד המקור.
  • הפיכת הסקריפטים לבדיקה לקריאים יותר (שמות נגישים הם תיאורים סמנטיים).
  • לעודד שיטות טובות להקצאת מאפייני נגישות לרכיבים.

בהמשך המאמר הזה מתוארים הפרטים לגבי האופן שבו יישמנו את פרויקט Puppetaria.

תהליך העיצוב

רקע

כמו שהסברנו למעלה, אנחנו רוצים להפעיל רכיבי שאילתות לפי השם והתפקיד הנגישים שלהם. המאפיינים האלה הם של עץ הנגישות, שהוא כפול לעץ ה-DOM הרגיל, שמשמש מכשירים כמו קוראי מסך כדי להציג דפי אינטרנט.

מעיון במפרט של חישוב השם הנגיש, ברור שחישוב השם של רכיב הוא משימה לא מהותית, ולכן בהתחלה החלטנו שאנחנו רוצים להשתמש שוב בתשתית הקיימת של Chromium לשם כך.

איך התייחסנו ליישום שלו

אפילו אנחנו מגבילים את השימוש בעץ הנגישות של Chromium, ויש כמה דרכים להטמיע שאילתות ARIA ב-Puppeteer. כדי להבין למה, בואו נראה קודם איך Puppeteer שולט בדפדפן.

הדפדפן חושף ממשק לניפוי באגים באמצעות פרוטוקול שנקרא פרוטוקול Chrome DevTools Protocol (CDP). פעולה זו חושפת פונקציונליות כמו "טעינת הדף מחדש" או "הפעלת קטע ה-JavaScript בדף והחזרת התוצאה" דרך ממשק ללא שפה.

גם ממשק הקצה של DevTools וגם Puppeteer משתמשים ב-CDP כדי לדבר עם הדפדפן. כדי להטמיע פקודות CDP, קיימת תשתית של DevTools בתוך כל הרכיבים של Chrome: בדפדפן, בכלי לרינדור וכן הלאה. CDP מטפל בניתוב הפקודות למקום הנכון.

באמצעות פקודות CDP כמו Runtime.evaluate שמעריכים את JavaScript ישירות בהקשר הדף ומחזירים את התוצאה, מתבצעות פעולות של בּוֹבָלִים כמו שאילתות, לחיצה והערכה של ביטויים. פעולות אחרות של ה-Puppeteer, כמו אמולציה של לקות בראיית צבעים, צילום מסך או תיעוד עקבות, משתמשות ב-CDP כדי לתקשר ישירות עם תהליך העיבוד של ההבהוב.

CDP

דבר זה כבר משאיר לנו שתי דרכים ליישום פונקציונליות השאילתות: אנו יכולים:

  • כותבים את לוגיקת השאילתות ב-JavaScript ומחדירים אותה לדף באמצעות Runtime.evaluate, או
  • משתמשים בנקודת קצה של CDP שיכולה לגשת לעץ הנגישות ולשלוח שאילתות לגביו ישירות בתהליך ההבהוב.

הטמענו 3 אבות-טיפוס:

  • מעבר DOM של JS – מבוסס על החדרת JavaScript לדף
  • מעבר בין בובות AXTree – מבוסס על השימוש ב-CDP הקיים בעץ הנגישות
  • מעבר ל-CDP DOM – באמצעות נקודת קצה (endpoint) חדשה של CDP שמיועדת לשליחת שאילתות לגבי עץ הנגישות

מעבר DOM של JS

אב הטיפוס מבצע מעבר מלא של ה-DOM ומשתמש ב-element.computedName וב-element.computedRole, שמוגבלים בדגל ההפעלה של ComputedAccessibilityInfo, כדי לאחזר את השם והתפקיד של כל רכיב במהלך המעבר.

מעבר לעץ הבובות AXTree

כאן במקום זאת, אנחנו מאחזרים את עץ הנגישות המלא דרך CDP וחוצים אותו ב-Puppeteer. לאחר מכן, צומתי הנגישות ממופים לצומתי DOM.

מעבר DOM של CDP

באב הטיפוס הזה הטמענו נקודת קצה (endpoint) חדשה של CDP לצורך שליחת שאילתות לעץ הנגישות. באופן כזה, השאילתה יכולה להתרחש בקצה העורפי באמצעות הטמעת C++ במקום בהקשר של הדף, באמצעות JavaScript.

נקודת השוואה לבדיקת יחידה

האיור הבא משווה את זמן הריצה הכולל של שליחת שאילתה על ארבעה רכיבים, 1,000 פעמים, עבור שלושת אבות הטיפוס. נקודת ההשוואה בוצעה ב-3 תצורות שונות, שמשתנות בגודל הדף וגם אם הופעלה שמירה במטמון של רכיבי נגישות.

נקודת השוואה: זמן הריצה הכולל של שליחת שאילתות לארבעה רכיבים 1,000 פעמים

די ברור שיש פער משמעותי בביצועים בין מנגנון השאילתות בגיבוי CDP לבין שני האחרים שהוטמעו אך ורק ב-Puppeteer, ונראה שההבדל היחסי גדל באופן משמעותי עם גודל הדף. קצת מעניין לראות שאב הטיפוס של החצייה של JS DOM מגיב כל כך טוב להפעלת שמירת נגישות במטמון. כשהשמירה במטמון מושבתת, עץ הנגישות מחושב על פי דרישה והמערכת מוחקת את העץ אחרי כל אינטראקציה אם הדומיין מושבת. הפעלת הדומיין תגרום לכך ש-Chromium ישמור במטמון את העץ המחושב.

במעבר של JS DOM, אנחנו מבקשים את השם והתפקיד הנגיש עבור כל רכיב במהלך החצייה. כך, אם השמירה במטמון מושבתת, Chromium מחשב ומבטל את עץ הנגישות עבור כל רכיב שבו אנחנו מבקרים. מצד שני, בגישות המבוססות על CDP, העץ נמחק רק בין כל קריאה ל-CDP, כלומר עבור כל שאילתה. גם לגישות האלה יכולה להיות תועלת מהפעלת שמירה במטמון, כי עץ הנגישות נשאר פעיל בקריאות CDP, אבל השיפור בביצועים קטן יחסית.

למרות שההפעלה של שמירה במטמון נראית רצוי, היא כרוכה בעלות נוספת של שימוש בזיכרון. זה עלול להיות בעייתי בסקריפטים של Puppeteer, למשל רשומות קובצי מעקב. לכן החלטנו לא להפעיל שמירה במטמון של עצים, כברירת מחדל. המשתמשים יכולים להפעיל בעצמם את השמירה במטמון על ידי הפעלת דומיין הנגישות של CDP.

נקודת ההשוואה בחבילת הבדיקות של כלי הפיתוח

נקודת ההשוואה הקודמת הראתה שהטמעה של מנגנון השאילתות שלנו בשכבת ה-CDP משפרת את הביצועים בתרחיש של בדיקת יחידה קלינית.

כדי לראות אם ההבדל בולט מספיק כך שניתן יהיה להבחין בו בתרחיש מציאותי יותר של הרצת חבילת בדיקות מלאה, תיקנו את חבילת הבדיקות מקצה לקצה של DevTools כך שתשתמש באבי הטיפוס המבוססים על JavaScript ו-CDP והשווינו בין זמני הריצה. בנקודת השוואה זו שינינו סך של 43 סלקטורים מ-[aria-label=…] ל-handler בהתאמה אישית של שאילתות aria/…, שאותו הטמענו באמצעות כל אחד מאבות הטיפוס.

חלק מהסלקטורים משמשים מספר פעמים בסקריפטים של בדיקה, כך שמספר ההפעלות בפועל של ה-handler של השאילתות aria היה 113 בכל הפעלה של החבילה. המספר הכולל של בחירות השאילתה היה 2253, כך שרק חלק קטן מאפשרויות השאילתה בוצעו דרך אבות הטיפוס.

נקודת השוואה: חבילת בדיקות e2e

כפי שאפשר לראות באיור שלמעלה, ניתן להבחין בהבדל בזמן הריצה הכולל. הנתונים רועשים מכדי להסיק משהו ספציפי, אבל ברור שהפער בביצועים בין שני אבות הטיפוס בא לידי ביטוי גם בתרחיש הזה.

נקודת קצה חדשה של CDP

לאור נקודות ההשוואה שלמעלה, ומכיוון שהגישה שמבוססת על דגלים בהשקה באופן כללי לא הייתה רצויה, החלטנו להתקדם עם הטמעת פקודת CDP חדשה לצורך ביצוע שאילתות על עץ הנגישות. עכשיו היינו צריכים להבין את הממשק של נקודת הקצה החדשה הזו.

בתרחיש לדוגמה שלנו ב-Puppeteer, אנחנו צריכים שנקודת הקצה תקבל את מה שנקרא RemoteObjectIds כארגומנט, וכדי לאפשר לנו למצוא את רכיבי ה-DOM המתאימים לאחר מכן, היא צריכה להחזיר רשימה של אובייקטים שמכילה את ה-backendNodeIds עבור רכיבי ה-DOM.

כפי שניתן לראות בתרשים הבא, ניסינו כמה גישות שמתאימות לממשק הזה. לאחר מכן גילינו שגודל האובייקטים שהוחזרו, כלומר אם הוחזרנו צומתי נגישות מלאים או רק ה-backendNodeIds, לא היה הבדל ברור. מצד שני, גילינו שהשימוש ב-NextInPreOrderIncludingIgnored הקיים לא היה בחירה נכונה להטמעת לוגיקת המעבר כאן, כי הוא הוביל להאטה משמעותית.

נקודת השוואה: השוואה בין אבות טיפוס למעבר AXTree המבוססים על CDP

סיכום של כל המידע

עכשיו, לאחר שנקודת הקצה של CDP הוגדרה, הטמענו את מטפל השאילתות בצד של ה-Puppeteer. החלק הקשה בעבודה כאן היה בנייה מחדש של קוד הטיפול בשאילתות, כדי לאפשר פתרון של שאילתות ישירות באמצעות CDP, במקום לשלוח שאילתות באמצעות JavaScript שמוערך בהקשר הדף.

מה השלב הבא?

ה-handler החדש של aria נשלח עם Puppeteer גרסה 5.4.0 כ-handler מובנה של שאילתות. נשמח לראות איך המשתמשים יאמכו את הסקריפט הזה בסקריפטים לבדיקה שלהם, ונשמח לשמוע את הרעיונות שלכם לשיפור התוצאות.

מורידים את הערוצים של התצוגה המקדימה.

כדאי להשתמש ב-Chrome Canary, Dev או בטא כדפדפן הפיתוח שמוגדר כברירת מחדל. ערוצי התצוגה המקדימה האלה מעניקים לך גישה לתכונות החדשות של כלי הפיתוח, בודקים ממשקי API מתקדמים של פלטפורמת האינטרנט ומוצאים בעיות באתר לפני שהמשתמשים נתקלים בבעיות!

יצירת קשר עם צוות כלי הפיתוח ל-Chrome

אפשר להשתמש באפשרויות הבאות כדי לדון בתכונות ובשינויים החדשים בפוסט, או בכל דבר אחר שקשור לכלי הפיתוח.

  • שלחו לנו הצעה או משוב בכתובת crbug.com.
  • כדי לדווח על בעיה בכלי הפיתוח, לוחצים על אפשרויות נוספות   עוד   > עזרה > דיווח על בעיות בכלי הפיתוח בכלי הפיתוח.
  • אפשר לשלוח ציוץ אל @ChromeDevTools.
  • אפשר לכתוב תגובות לגבי 'מה חדש' בסרטוני YouTube או בקטע 'טיפים לשימוש בכלי הפיתוח' בסרטוני YouTube.