כשאנו כותבים אתר מסויים, אנחנו בסופו של דבר מייצרים קובץ HTML (הגי'נרוט של קובץ זה יכול להגיע מכמה טכנולוגיות מאחורי הקלעים, אך כאן זה לא המקום להרחיב עליהם). הדפדפן שאנו עובדים עימו (גוגל כרום \ מוזילה פיירפוקס \ אינטרנט אקספלורר) יודע לתרגם את תוכן ה-HTML לתוכן מסוג DOM (ראשי תיבות של Document Object Model).
ה-DOM הוא למעשה ייצוג ה-Object Oriented של דף ה-HTML אשר מוצג לנו בדפדפן.
אפליקציות Web הולכות ונהיות מורכבות יותר עם הזמן, צצים כל מיני שירותים חדשים בהם מפתחי האפליקציות משתמשים, מהם לוקחים משאבים וספריות. סגנון עבודה זה הביא ליצירת בעיות בעיצוב בעיקר, התנגשויות בין אלמנטים שונים של מחלקות ו-id בתבניות ה-CSS.
הפתרון המתבקש היה לעבוד עם Shadow DOM, מה זה אומר ?
ניתן להסתכל על Shadow DOM כאל תת עץ (sub tree) ב-DOM, זה נותן למפתחים את היכולת להגדיר, לקנפג ולרנדר את אותם אלמנטים בתת עץ זה מבלי לפגוע ב-DOM הראשי, זאת אומרת שה-CSS של ה-DOM הראשי יהיה מופרד מה-CSS של ה-Shadow DOM.
בעולם ה-Shadow, ישנם כמה מושגים:
Shadow Host – זהו אותו אלמנט ב-DOM הראשי שמכיל את ה-Shadow Tree
Shadow Tree – אותו עץ בן שמופרד CSSית מה-DOM הראשי
Shadow Root – האלמנט הראשי ב-Shadow Tree
ניתן לראות כאן איור המציג את המושגים הללו בצורה ברורה:
למי שרוצה להרחיב את אופקיו יותר על Shadow DOM ואיך בונים אותו עם JS , מוזמן לקרוא כאן.
אז מה זה אומר מבחינתינו באוטומציה ?
זה אומר שאם אנחנו נרצה לזהות אלמנט שקיים תחת Shadow DOM אנחנו ניכשל ונחטוף No Such Element Exception.
על כן אנו צריכים לטפל באלמנטים אלו בצורה מעט שונה ממה שהורגלנו עם פונקציית ה-findElement.
בואו נראה קודם דוגמא לאלמנטים מסוג Shadow DOM, למשל באתר polymer-project:
בשביל שנוכל לזהות אלמנטים תחת Shadow DOM נצטרך להיעזר בפונקציית JS שנקראת shadowRoot , עליה ניתן לקרוא כאן.
למי שלא מכיר, ניתן לקרוא לפונקציות JS מתוך התוכניות הסלניומית שלנו ע"י יצירת אובייקט ה-JSExecutor וממנו נפעיל את פונקציית ה-executeScript ונקרא ל-shadowRoot, הנה כך למשל ניתן לראות פונקציה שמימשתי שעושה את הפעולה של הכניסה אל תוך ה-Shadow DOM ומחזירה את האלמנט הפנימי שהוא ה-Shadow Root
public WebElement getShadowRoot(WebElement element) { WebElement shadowElement = (WebElement) ((JavascriptExecutor)driver) .executeScript("return arguments[0].shadowRoot", element); return shadowElement; }
נגיד שאנו רוצים לכתוב תוכנית באוטומציה שיודעת ללחוץ על Ladies T-Shirts בתפריט העליון, אבל מה ? הלינק הזה יושב תחת Shadow DOM כפי שניתן לראות כאן:
על כן אנו נצטרך לקרוא לפונקציית ה-getShadowRoot שמימשנו לפני כן והתוכנית תיראה כך:
@Test public void Test1() { driver.get("https://shop.polymer-project.org/"); WebElement home = driver.findElement(By.cssSelector("shop-app[page='home']")); WebElement shadowHome = getShadowRoot(home); shadowHome.findElement(By.linkText("Ladies T-Shirts")).click(); }
בהתחלה זיהינו את ה-Shadow Host, קראנו לו home, אח"כ שלחנו לפונקציית ה-getShadowRoot שמחזירה את ה-Shadow Root ואליה שלחנו את ה-Shadow Host, ולבסוף הפעלנו על ה-Shadow Root חיפוש של אלמנט ה-Ladies T-Shirts, זה אומר שהחיפוש הוא חיפוש פנימי בתוך אלמנט ה-Shadow
באותו האופן, אם ארצה להיכנס ל-Settings של הדפדפן כרום שלי, אל תוך ה-About ושם ארצה לבדוק את מספר גרסת הדפדפן, אצטרך שוב לקרוא ל-getShadowRoot כי גם דף זה עובד על Shadow DOM:
והתוכנית להדפסת גירסת הכרום, תיראה כך:
@Test public void Test01() { driver.get("chrome://settings/help"); WebElement settings = driver.findElement(By.tagName("settings-ui")); WebElement shadowSettings = getShadowRoot(settings); WebElement main = shadowSettings.findElement(By.id("main")); WebElement shadowMain = getShadowRoot(main); WebElement about = shadowMain.findElement(By.cssSelector("settings-about-page")); WebElement shadowAbout = getShadowRoot(about); WebElement secondary = shadowAbout.findElement(By.className("secondary")); System.out.println(secondary.getText()); }
שימו לב כי כאן, אנחנו צריכים להיכנס ל-Shadow Element ובתוכו לתוך Shadow Element פנימי, ובתוכו שוב פעם לתוך Shadow Element פנימי יותר וכו'…