Introduction
Recently, I carried out some automation tests on a Single Page Application using WebDriver and I have developed my own framework to avoid redundant codes. It is quite challenging to even try to input a string into an AJAX auto-suggested text input, choose an option from an select when 'optiongrp
' is used or access a cell from a table.
As a summary of my past work, as well as the first step to present my framework, I would like to introduce the very basic operations on some common HTML elements.
Input Text to a Text Field
It might seem to be too simple when everyone know that "SendKey(string text)
" should work. However, in most cases, SendKey
would append text to the existing one if the text field (input
, textArea
) is not cleared.
So a common practice need clear it first before calling SendKey()
:
IWebElement element = theWebDriver.findElement(By.Id("textId"));
...
element.Clear();
element.SendKey("text to be input");
Another option, instead, is using "Ctrl+A" to select the existing text first before doing the real input:
public static readonly string Control = Convert.ToString
(Convert.ToChar(0xE009, CultureInfo.InvariantCulture), CultureInfo.InvariantCulture);
public static readonly string Tab = Convert.ToString(Convert.ToChar
(0xE004, CultureInfo.InvariantCulture), CultureInfo.InvariantCulture);
public static readonly string Control_a = Control + "a";
IWebElement element = theWebDriver.findElement(By.Id("textId"));
...
element.SendKey(Control_a);
element.SendKey("text to be input");
This, however, is not enough for auto-completed text fields empowered by Angular for example, there would be an alert dialog pinned when I perform the following operations. Again, considering the human operations in such scenario, the problem can be solved by Sending a TAB to the field. So finally, the preferred way for me is:
public static readonly string Tab = Convert.ToString
(Convert.ToChar(0xE004, CultureInfo.InvariantCulture), CultureInfo.InvariantCulture);
...
element.SendKey(Control_a);
?element.SendKey("text to be input" + Tab);
Select an Option from a Dropdown List
The proposed way by Selenium to select an option from a dropdown list is:
SelectElement select = new SelectElement(driver.FindElement(By.TagName("select")));
select.DeselectAll();
select.SelectByText(Edam");
However, it doesn't work when "optiongrp
" is used to contain multiple options. A very simple approach to cope with it is also operate like we do in a browser with SendKeys()
:
IWebElement element = theWebDriver.findElement(By.Id("selectId"));
...
element.SendKey("Edam" + Tab);
In this way, we can only select option by its text instead of its value, but that can be done by matching the values before "SendKey()
" and should be enough for most cases.
Accessing Table
As a collection of table rows/cells, accessing a specific cell of the table is quite problematic, the following discussion is focused only on tables whose rows have the same number of cells.
Basically, there are 3 steps to sort out the structure of the table:
- Get all rows (or child elements whose tagname is "
tr
"); - Parsing the first row to see if it contains headers (elements whose tagname is "
th
") and store the header names; - Keep the actual row number and column number for further operations.
public int RowCount { get; private set; }
public int ColumnCount { get; private set; }
public List<string> Headers { get; private set; }
public bool WithHeaders { get; private set; }
ReadOnlyCollection<IWebElement> rows = driver.FindElements(By.CssSelector("tr"));
IWebElement firstRow = rows[0];
var headers = firstRow.FindElements(By.CssSelecor("th")).ToList();
RowCount = rows.Count();
if (headers.Count() != 0)
{
WithHeaders = true;
ColumnCount = headers.Count();
Headers = headers.Select(x => x.Text).ToList();
}
else
{
RowCount += 1;
WithHeaders = false;
Headers = null;
ColumnCount = rows.FirstOrDefault().FindElements(By.CssSelector("td")).Count();
}
Then, there could be multiple ways to access the desired cells/rows as samples listed below.
- To access the cell by its
rowIndex
and columnIndex
:
IWebElement table = driver.FindElement(By.Id("table"));
public IWebElement cellOf(int rowIndex, int columnIndex)
{
if (columnIndex < 0 || columnIndex >= ColumnCount)
throw new Exception("Column Index ranges from 0 to " + (ColumnCount-1));
if (rowIndex < 0 || rowIndex >= RowCount)
throw new Exception("Row Index ranges from 0 to " + (RowCount-1));
if (rowIndex == 0 && !WithHeaders)
throw new Exception("No headers (rowIndex=0) defined for this table.");
String css = string.Format("tr:nth-of-type({0}) {1}:nth-of-type({2})", rowIndex + 1,
rowIndex == 0 ? "th" : "td",
columnIndex);
return table.FindElement(By.CssSelector(css));
}
- To access the cell by its
rowIndex
and header name:
IWebElement table = driver.FindElement(By.Id("table"));
public IWebElement cellOf(int rowIndex, string headername)
{
int columnIndex = Headers.FindIndex(s => s.Contains(headerKeyword));
return this[rowIndex, columnIndex];
}
- More specifically, return the row which contains a specific value under a headername:
IWebElement table = driver.FindElement(By.Id("table"));
public IWebElement rowOf(string headerKeyword, Func<string, bool> predicate)
{
int columnIndex = Headers.FindIndex(s => s.Contains(headerKeyword));
var allRows = table.FindElements(By.CssSelector("tr"));
if (WithHeader)
allRows.Skip(1);
foreach( var row in allRows)
{
var td = row.FindElement(By.CssSelector(string.Format("td:nth-of-type({0})", columnIndex + 1)));
if (predicate(td.Text))
return row;
}
return null;
}
Point of Interest
In this tip, we have discussed about some very detailed procedures to operate some common HTML elements that are working, but not documented.