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

Johan Bay
Johan Bay

‫Puppeteer והגישה שלו לסלקטורים

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

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

ב-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 כולל עכשיו מטפל חלופי בשאילתות שמבוסס על שאילתות בעץ הנגישות, במקום להסתמך על סלקטורים ב-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‏ (CDP). הפרוטוקול הזה חושף פונקציונליות כמו "טעינה מחדש של הדף" או "הפעלה של קטע הקוד הזה של JavaScript בדף והחזרת התוצאה" דרך ממשק שלא תלוי בשפה.

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

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

CDP

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

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

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

  • מעבר ב-DOM של JS – מבוסס על הזרקת JavaScript לדף
  • מעבר על עץ הנגישות של Puppeteer – מבוסס על שימוש בגישה הקיימת של CDP לעץ הנגישות
  • מעבר ב-DOM של CDP – שימוש בנקודת קצה חדשה של CDP שנוצרה במיוחד כדי לשלוח שאילתות לעץ הנגישות

מעבר על DOM ב-JS

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

מעבר על AXTree ב-Puppeteer

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

מעבר ב-DOM של CDP

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

בנצ'מרק לבדיקת יחידה

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

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

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

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

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

בדיקת ביצועים של חבילת מקרים לבדיקה בכלי הפיתוח

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

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

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

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

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

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

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

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

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

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

סיכום

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

מה השלב הבא?

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

הורדת הערוצים בגרסת טרום-השקה (Preview)

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

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

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