מדוע חשוב להכיר את העבודה עם טבלאות בבדיקות אוטומטיות מקצה לקצה?
טבלאות הם אלמנטים המשמשים להצגת מידע בצורה מובנת ומסודרת והם נפוצים מאד באפליקציות ווב. טבלאות יכולות להיות סטטיות או דינמיות, כלומר, המידע בהן יכול להשתנות בהתאם לפעולות המשתמש או לעדכונים מהשרת. בדיקות אוטומטיות של אפליקציות ווב יכילו גם אלמנטים מסוג טבלה, ולכן נצטרך לבצע פעולות כגון קריאה מהטבלה, לחיצה, סינון, חיפוש, ועוד.
במאמר זה נבדוק אלו אתגרים ישנם בעבודה עם תרשימי טבלה בבדיקות אוטומציה מקצה לקצה, והדרכים להתמודד איתם. על טבלה נשתמש במתודות של WebElement ובלוקייטורים (locators) לאיתור אלמנטים בטבלה. הדוגמאות במאמר זה כתובות ב Javascript עם סלניום אבל ניתן להמיר אותם לכל שפה ובמגוון של כלים.
כדי להמחיש את העבודה עם טבלאות באוטומציה של אפליקציות ווב נסתכל בטבלה הבאה:
כאשר זהו קוד ה HTML של הטבלה:
<table>
<header>
<th>ID</th>
<th>Name</th>
<th>phone</th>
<th>Status</th>
</header>
<tbody>
<tr>
<td><input type="checkbox"/>111</td>
<td>Snorka</td>
<td>9723476</td>
<td>
<button>Click-me</button>
</td>
</tr>
<tr>
<td><input type="checkbox"/>222</td>
<td>Sniff</td>
<td>9834587</td>
<td>
<button>Click-me</button>
</td>
</tr>
<tr>
<td><input type="checkbox"/>333</td>
<td>Little My</td>
<td>9945698</td>
<td>
<button>Click-me</button>
</td>
</tr>
</tbody>
</table>
<style> table, th, td { border: 1px solid black; } table { border-collapse: collapse; } </style>
חיפוש לפי אינדקס
נניח שבטסט שאנו צריכים לכתוב, באחד השלבים מופיעה הטבלה שלמעלה והתרחיש כולל לחיצה על תיבת הסימון (checkbox) הצמודה למספר הזהות של Snorka עם מספר זיהוי 111. ניגש מיד למשימה: בשלב הראשון נוכל להוציא מהשורה השנייה בעמודה הראשונה את התא המתאים ע"י הגדרת הלוקייטור כך:
בעזרת xpath:
const snorkaIdCellLocator = '//table/tbody/tr[2]/td[1]';
או בעזרת CSS 💪:
const snorkaIdCellLocator = 'table>tbody>tr:nth-child(2)>td:nth-child(1)';
אנו מחפשים את התא הרצוי בטבלה לפי האינדקסים של השורה (לוקייטור 'tr') והעמודה (לוקייטור 'td'). ואז נוכל ללחוץ על תיבת הסימון שנמצאת בתוך התא בצורה הבאה:
const snorkaIdCell = await driver.findElement(By.css(snorkaIdCellLocator));
const snorkaCheckbox = await snorkaIdCell.findElement(By.css('input'));
await snorkaCheckbox.click();
בשלב הראשון מצאנו את האלמנט המייצג את התא בטבלה המכיל את מספר הזיהוי ותיבת הסימון, אחר כך אנו מוצאים את תיבת הסימון מתוך תא הטבלה הרלוונטי ולבסוף מבצעים הקלקה על תיבת הסימון.
כל עוד הסביבה שלנו קבועה וצפויה לחלוטין (המצב המומלץ!) והטבלה תישאר בדיוק באותו מצב, האסטרטגיה הזו תעבוד מצוין. הבעיה תתחיל כאשר הסביבה שלנו נתונה לשינויים כגון: הוספת שורות שתשנה את מיקום השורות בטבלה או שינוי סדר העמודות ע"י משתמש או ע"י שינוי הקונפיגורציות של הטבלה. במקרים האלה לא נוכל להסתמך על האינדקסים של השורה והעמודה ונצטרך למצוא אסטרטגיה אחרת 👇.
חיפוש לפי טקסט
אחת הדרכים החילופיות לחיפוש תא בטבלה לפי אינדקס, היא חיפוש לפי טקסט. היתרון של חיפוש לפי טקסט הוא בכך שנוכל לעבוד עם טבלאות דינמיות בהן האינדקס של השורות והעמודות משתנה מהסיבות שפורטו למעלה.
חיפוש לפי טקסט בגוף הטבלה
ניקח את הדוגמה בה השתמשנו בשיטת החיפוש עפ"י אינדקס (לחיצה על תיבת הסימון הצמודה למספר הזהות של Snorka) כדי להדגים כיצד ניתן לבצע את אותה משימה עם חיפוש לפי טקסט. נכתוב שתי מתודות שירות, אחת לחיפוש שורות ואחת לחיפוש עמודות מתוך שורה:
async getTableRows() {
return await driver.findElements(By.css('tr'));
}
async getTableColumns(row, columnLocator = 'td') {
const columns = row.findElements(By.css(columnLocator));
return columns;
}
נכתוב מתודה שמוצאת שורה בטבלה לפי טקסט:
async getTableRowByText(rowText) {
const rows = await this.getTableRows()
const selectedRow = await this.findAsync(rows, (row) => row.getText().includes(rowText));
return selectedRow;
}
שים לב שהמתודה מוצאת את השורה הראשונה שמכילה את הטקס המתאים, לכן יש להשתמש בטקסט ייחודי שיבדיל את השורה משאר השורות בטבלה, כמו מספר זיהוי, ולא להשתמש בטקסט לא ייחודי כמו הטקסט בעמודת 'Status' למשל.
עכשיו בעזרת המתודות שלמעלה, ניתן לכתוב מתודה שמחזירה תא בטבלה המכיל טקסט כמו למשל - מספר זיהוי:
async getTableCellByText(cellText) {
const selectedRow = await this.getTableRowByText(cellText);
const columns = await this.getTableColumns(selectedRow);
const selectedColumn = await this.findAsync(columns, (col) => col.getText() == cellText);
return selectedColumn;
} |
אנו משתמשים כאן בפונקציה findAsync שהיא מקבילה לפונקציה find המובנית ב Javascript, אבל מאפשרת פעולות אסינכרוניות של פעולות על אלמנטים. הקוד של findAsync מצורף בנספח בסוף המאמר.
נשתמש ב getTableCellByText כדי ללחוץ על תיבת הסימון בשורה של Snorka כך:
const snorkaIdCell = await getTableCellByText('111');
const snorkaCheckbox = await snorkaIdCell.findElement(By.css('input'));
await snorkaCheckbox.click();
הקוד מוצא תחילה את התא בטבלה המכיל את מספר הזיהוי 111, בה נמצאת תיבת הסימון, ואח"כ מוצא את תיבת הסימון שבתוכו ולוחץ עליה.
חיפוש לפי טקסט בגוף הטבלה ובכותרת
נוסיף אתגר נוסף: המשימה היא ללחוץ על כפתור ה Click-me (הנמצא תחת העמודה Status) בשורה של Sniff (Id=222), כאשר להזכירכם ההנחה היא שמיקום העמודות והשורות הוא דינמי וישתנה בין הרצות הטסטים.
כמובן שיש יותר מפתרון אחד, אבל הנה אחד מהם 😊: קודם כל נמצא את השורה שמכילה את מספר הזהות של Sniff והוא 222. בשביל זה נשתמש במתודה getTableRowByText שכבר כתבנו למעלה ונעביר לה את הערך 222. האתגר עכשיו הוא למצוא את התא שמכיל את הכפתור Click-me. נצטרך לכתוב מתודה שמוצאת באופן דינאמי את האינדקס של עמודה הרצויה לפי השם בכותרת העמודה, ונשתמש באינדקס הזה כדי לאתר את העמודה בשורה שמצאנו בעזרת getTableRowByText:
async getTableHeaderIndex(headerText) {
const selectedRow = await this.getTableRowByText(headerText);
const columns = await this.getTableColumns(selectedRow, 'th');
const colIndex = await this.findIndexAsync(columns, (col) => col.getText() == headerText);
return colIndex;
}
המתודה משתמשת ב findIndexAsync שהיא מקבילה לפונקציה findIndex המובנית ב Javascript, אבל מאפשרת פעולות אסינכרוניות של פעולות על אלמנטים. הקוד של findIndexAsync מצורף בנספח בסוף המאמר.
שים לב שאנו מעבירים לפונקציה getTableColumns את הלוקייטור של עמודת כותרת מסוג 'th' ולא 'td' כפי שהשתמשנו למציאת עמודה בגוף הטבלה. כל שנותר הוא לכתוב מתודה חדשה שתשתמש בקוד שכתבנו על מנת לאתר את העמודה בשורה הרצויה:
async getTableCellByRowTextAndHeaderText(rowText, headerText) {
const selectedRow = await this.getTableRowByText(rowText);
const headerIndex = await this.getTableHeaderIndex(headerText);
const tableCell = (await this.getTableColumns(selectedRow))[headerIndex];
return tableCell;
} |
הקוד קורא למתודה המחזירה שורה המכילה את הטקסט, לאחר מכן הוא קורא למתודה שמחזירה את האינדקס של העמודה הנדרשת עפ"י טקסט הכותרת, ולבסוף קורא למתודה המחזירה את כל העמודות בשורה שנמצאה ושולף משם את העמודה הרצויה.
נשתמש בפונקציה כדי ללחוץ על הכפתור כך:
const sniffClickMeCell = await this.getTableCellByRowTextAndHeaderText('222', 'Status');
const sniffClickMeButton = await sniffClickMeCell.findElement(By.css('button'));
await sniffClickMeButton.click();
בשורה הראשונה אנו מקבלים את התא מתוך השורה של Sniff המכיל את הכפתור, לאחר מכן הקוד מחפש את הכפתור מתוך אותו תא טבלה, ולבסוף לוחץ על הכפתור.
סיכום
אלמנטים מסוג טבלה הם אלמנטים גנריים באפליקציות ווב, לכן כתיבת קוד שיטפל בטבלאות הוא חלק חשוב בתשתית של פרויקט בדיקות אוטומטיות. המאמר מסביר איך לבצע פעולות שונות על אלמנטים מסוג טבלה באוטומציה של אפליקציות ווב, ואיך לאתר את התאים בה בעזרת חיפוש לפי אינדקס וחיפוש לפי טקסט. המאמר מדגיש את היתרונות והחסרונות של כל שיטה, ואיך לכתוב מתודות בסיסיות לעבודה עם טבלאות.
***
נספח
async findAsync(elementCollection, predicate) {
for (let element of elementCollection) {
let match = await predicate(element);
if (match) {
return element;
}
}
return undefined;
} |
async findIndexAsync(elementCollection, predicate) {
var index = -1;
for (var i = 0; i < elementCollection.length; i++) {
let match = await predicate(element);
if (match) {
index = i;
break;
}
}
return index;
} |
כמובן ישנן דרכים נוספות לעבודה עם טבלאות. אשמח אם מישהו מהקוראים יציג דרך נוספת בתגובות.