TERESA is a compact CSS Selector based Selenium WebDriver wrapper and enabler published on Github. Instead of wrapping WebDriver functions, TERESA focuses more on the optimization the whole process of mapping web pages to classes, mechanisms and procedures of locating elements and performing operation with LEAST lines of code.
A 90 seconds demo video (http://youtu.be/rFwQJBkbrC4) shows how 4 fragments and 1 page are composed with almost no executable codes to map the Single Page Application of Seek.com.au, and a test function of about 40 lines of codes is used to perform over 30 operations, some of them are otherwise quite challenging, on the page.
To run the sample, you need to extract all files to a folder, then add reference to following files to make it running with Chrome:
- Newtonsoft.dll
- nunit.framework.dll
- Teresa.dll
- WebDriver.dll
Based on combined application of Enum, unified Indexer interface for getting/setting operations, and lots of Reflection, TERESA provides a systematic way to build test cases over WebDriver:
- Identify elements with Enum and Attribute, and the CSS Selectors are automatically deducted from the Enum type names and entry names, so as to relieve WebDriver users from the tedious work of composing CSS/XPATH selectors manually.
- Mapping the hierarchical relationships of elements directly with the nested Enum types; a concise but crafty mechanism is used to locate elements more efficiently and effectively.
- Unified Enum based Indexer interface to enable Finding element and Getting/Setting operations in a single self-explaining sentence. Consequently, most one-off functions can be avoided and productivity can be enhanced dramatically.
- Special element wrapper (SelectLocator, TextLocator and etc.), rich set of extension methods (Control-click, Hover, Highlight and etc.), as well as some novel tools (IndexPredicate, Wait.Until ...) are provided to make operations on elements easier or more intuitive with almost no extra work.
Some detail explantion of the features can be found from:
The document is divided into four parts:
- Background: introduces WebDriver and this project.
- Implementation: introduces the solution architecture, technical details and examples.
- Tutorial: gives guideline of using TERESA with step-by-step design and coding two examples. It might be a good start point to understand what this Framework can bring to you before dig into the implementation details.
- Design Considerations: explains why three non-canonical techniques are used heavily.
- Appendix: quick references to design Enum and execute getting/setting operations.
Although this article is focused on WebDriver users, for those who are not interested with Automation Testing, this article discloses some novel skills and techniques that may give your some inspiration.
WebDriver is a powerful tool for writing Automated tests of websites.
It defines an interface IWebElement (the interface through which the user controls elements on the page.) that is referred frequently in this document as both the RemoteWebElement supported by WebDriver and the actual element on the page.
There are two important methods supported by IWebElement (by implementing ISearchContext): “IWebElement FindElement(By by)” and “ReadOnlyCollection<IWebElement> FindElements(By by)” to locate a single element or a set of elements with same finding strategy.
In most WebDriver sample projects, however, the finding element is mainly carried by IWebDriver.FindElement(). This may due to lack of defining elements in a tree like manner to show the hierarchical organization of HTML elements within a page. Consequently, defining the strategy to locate an element is always time-consuming and hard to maintain.
TERESA is designed to use Enum to compose and carry locators of element, and build a parental tree by define Enum types in different layers of a Page class and execute operations in a streamlined manner.
This part introduces the technical realization details of TERESA, which involves quite some non-canonical practice.
This part including following contents:
- an overview of the project functional blocks;
- recap the critical technique used to get CSS selector with Enum;
- a brief introduction of Locator class and the wrapped Enum;
- how Fragment/Page classes are organized to hold the Locators;
- the mechanism used by this framework works to locate an IWebElement;
- explanation and examples of how to operate an IWebElement with TERESA.
Functional Blocks Brief
As the diagram below shows, there are three projects packed together:
- TERESA is the library project to be discussed in this part.
- TERESAExample illustrates how to use this framework to do Automation Testing.
- TERESAUnitTesting includes functional tests to verify some basic features of TERESA.
| There are three folders within the TERESA project:
- AdvancedEnum: contains functions and definitions for Enum processing,
especially generating CSS selectors used to locate IWebElement using
FindElement()/FindElements(). The implement details are revealed in
Advanced Enum Usage with examples, and more usage examples are
included in the next section. - Locators: is the main part functional block of this framework, including
the very key FindElement() function wrapping FindElement(By.CssSelector())
/FindElements(By.CssSelector()) of either IWebDriver or IWebElement as
well as logics to perform solid operations upon the object Web Elements. - Utility: includes tools to enable Wait.Until() conveniently as discussed in
Lightweight Wait.Until() Mechanism, and help tools to produce Predicates
with signature of Func<IWebElement, bool>, though currently there is
only Index Predicate included to enable Func<IWebElement, int, bool>
needed in Where/Select clauses.
|
| |
To give you a brief impression of the functional blocks, key functions of the classes/files are summarized as below:
- Within the AdvancedEnum folder:
- HtmlTagName and Mechanisms define two Enum type to enumerate most HTML element types and CSS Selecting mechanisms respectively. Their memebers are assigned with different format strings used by calling String.ToFormat(string, obj[]) twice to compose the CSS selector of a specific Enum associated with a IWebElement. The detail meaning and example of them can be found in HtmlTagName and Mechanisms.
- EnumTypeAttribute is an Attribute associated with the names of a Enum types. It parses these type names, that should have a form of “TagnameByMechanism”, and get the hinted HtmlTagName and Mechanisms and buffers such information for further use by EnumMemberAttribute.
- EnumMemberAttribute is another Attribute associated with a member of a Enum type. Each EnumMember attribute holds both information from EnumTypeAttribute and information related with the member only. With such information, the EnumMemberAttribute can deduct and buffer the CSS Selector of the IWebElement associated with the member.
- EnumExtensions, as a static class, provides some extension methods of Enum and also keeps a dictionary to refer to the string value of an Enum entry denoted, including the format strings of HtmlTagName and Mechanisms, as well as keys of Enum members to be used constructing CSS selectors.
- Within the Locators folder:
- Locator: is the cornerstone of the framework.
- First, instead of wrapper of IWebElement, it is a wrapper of the tools and information needed to find the IWebElement, including CSS selector and its parent Locator.
- Second, it provides the finding mechanism with a single function (public IWebElement FindElement(Func<IWebElement,bool>, int) to calling both FindElement() and FindElements() of the underlying WebDriver to get the expected IWebElement.
- Finally, Locator provides a unique indexer (string this[Enum, string, Func<IWebElement, bool>]{set;get}) to perform all read/write operations on the IWebElement associated with the Enum member.
- The Enum member used to store CSS Selectors has already identify the type of element by its type name like Tex, Button, Link, Radio, Checkbox, Table and etc.. Some types of elements shall have different behaviors than others and their functions are encapsulated as CheckboxLocator, RadioLocator, SelectLocator, TableLocator and TextLocator respectively by extending Locator to provide many advanced operations on the underlying nodes.
- Fragment (by extending Locator) and Page (by extending Fragment) are used to define Enum members identifying various kinds of web elements. The Fragment can be used to hold a set of basic Locators denoting web elements contained within a container element. The definitions of Fragment can be nested (a Fragment can be defined within another Fragment or Page) and reused (a Fragment can be included by multiple Page objects), thus all Locators (including Fragments) are organized in a hierarchical manner by defining them in different places, and the default constructor of Fragment/Page would build up the trees of Locators with Reflection: you don’t need to write a line of code! To access the Locator nodes of the tree generated from the Enum definitions, a single entrance (string this[Enum, string, Func<IWebElement, bool>]{set;get}) is used to perform any operations upon the contained element.
- Within the Utility folder.
- Wait and GenericWait<T> are used to support the IWait defined in WebDriver.support with a simple mechanism discussed in Lightweight Wait.Until() Mechanism.
- GenericPredicate means to build up Predicate (Func<IWebElement, bool>) used in the indexers within Locator/Fragment/Page thought there is only one Index based Predicate is supported.
- In the root folder, the IWebElementExtension provides some new behaviors like Hover(), DoubleClick() and ControlClick() to the IWebElement, as well as some handy methods to help generation of Predicate.
As discussed in Advanced Enum Usage with examples, TERESA use a mechanism, based on method extension, attribute and reflection over Enum types, to enable its users to associate an Enum member with a CSS selector. All functions involved are contained within the folder of “AdvancedEnum”, this mechnism:
- As a special kind of class, entitles Enum with a rich set of features in “EnumExtension.cs”. There is a static Dictionary to store unique string values linked with EACH Enum entries and many helper functions are defined to retrieve the string value or other extended attribute values of EACH Enum member.
Tips: As a hint, you can build up such a Dictionary to link each Enum entry with ANY kinds of class and then use the Enum entry to retrieve a rich set of information universally. For example, a static Dictionary<Enum, RichInfoSet> can be used to enable SomeEnumEntry.RichInfoSet().
- To convey more information with an Enum member without typing too much, the Enum type names in a pattern of “TagnameByMechanism” or “Tagname
AllByMechanism” are used to define the Tagname of the web elements and Mechanism used to find it. Anyway, you have to define an Enum entry as a member of an Enum type, and refer it along with its type name. Self-explaining names of Enum types in this pattern would not only make parsing them very convenient, but also provide enough clues when you are using them to perform any operation and enable the Locators instantiated with the right type to provide more functions. - The Tagname and Mechanism from a valid type name in a pattern of “TagnameByMechanism” are also defined as two Enum types: HtmlTagName and Mechanisms respectively.
- There are 18 members of Mechanisms listed in order of their preference: ById, ByClass, ByAdjacent, ByParent, ByAncestor, BySibling, ByUrl, ByName, ByValue, ByAttribute/ByAttr, ByOrder, ByOrderLast, ByChild, ByChildLast, ByCustom, ByTag and ByText.
- You can find the description of them from source codes in “Mechanisms.cs” and a good introduction of CSS Selectors in “The 30 CSS Selectors you Must Memorize” except the Mechanisms.ByText and Mechanisms.ByCustom.
- “ByCustom” allows you to compose a selector string which would be append to the tagname.
- “ByText” is actually not a CSS selector, it would enumerate all elements with tag specified by HtmlTagName, then uses LINQ to find out the first one containing expected text.
- There are 45 entries defined in HtmlTagName, such as HtmlTagName.Text, HtmlTagName.Button, HtmlTagName.Link, HtmlTagName.Div and etc., and each entry is associated with a string value to help build up the CSS selector.
- For example, there are five common tags are regarded as buttons: <button>, <input[type=button]>, <input.button>, <input.btn> and <div[role=button]>, thus as you can try with CssComposingTest of TERESAUnitTesting project, the Enum entry of “ButtonById.go” would get a CSS selector of “button#go, input[type=button]#go, input.btn#go, input.button#go, div[role=button]#go” to match all five tags.
- With 45 HtmlTagName and 18 Mechanisms, there could be 45*18=810 combinations, though there are some meaningless ones like HtmlByXXX, to define Enum type names to cover most cases you may encounter.
- When you do need some complex CSS selectors, then “XXXByCustom” would use the value of EnumMemberAttribute as the final CSS selector, but the type of IWebElement is still be defined by “XXX” to generate special Locator to provide extra functions. For example, you can define “button#id” as a member of SelectByCustom to locate an IWebElement and treat it as a <select> Element even it would throw Exceptions when you try to select an option of the <select>.
- The parsing of Enum type names happens in a static function of EnumTypeAttribute (public static EnumTypeAttribute TypeAttributeOf(Type enumType)).
- Each such an attribute is associated with a type of Enum to provide three properties: TagName, Mechnism and IsCollection. The first two are associated with Tagname and Mechanism respectively.
- “IsCollection” is set to true when the type names is in the pattern of “TagnameAllByMechanism” like “ButtonAllByClass” to denote CSS selectors of several matched IWebElements by calling FindElements() instead of FindElement() in WebDriver.
- After specifying type of IWebElement and finding mechanism by defining the name of an Enum type, the Enum members are used to carry the keys to generate the CSS Selectors explicitly or implicitly.
- The name of the Enum member would be treated as its value after replacing ‘_’(underscore) with ‘ ’(space).
- This value would be used to compose the final CSS selector string considering both HtmlTagName and Mechanisms properties from the type of the Enum member simply with two round of String.Format(), using the value string associated with HtmlTagName and Mechanisms.
- For example, HtmlTagName.Text has a value of "{0} input[type]{1}, {0} textarea{1}", and Mechanisms.ByName has a value of "[name*='{0}']", so the Enum member of “TextByName.xyz” would get a CSS Selector of “input[type][name*='xyz'], textarea[name*='xyz']”. You can use the CssComposingTest of TERESAUnitTesting project to see how CSS selectors are composed for all combination of HtmlTagName and Mechanisms.
- Personally, I prefer copying the keys (either id, name, class) or part of them as the Enum member name directly, especially if these keys are meaningful words to save typing and find the tags by searching HTML source directly.
- To compose more complicated selector, or you prefer defining a more meaningful or more well-formated name for the Enum members, then the keys must be defined explicitly with EnumMemberAttribute.
- As the name suggested, EnumMemberAttributes are associated with Enum members only to contains both information inherited from the type of these member via EnumTypeAttribute, three new properties (Value, IsFragment and Description) and most importantly, Css.
- IsFragment identify if the Enum member is used to identify a Fragment, or a container of many children Enum members, its usage would be discussed later.
- Value: is a string bound with the Enum member just as the format string "[name*='{0}']" of Mechanisms.ByName. While for Enum member denoting an IWebElement, it contains the key used to compose final CSS selector. Though most Mechanisms need only one parameter like Mechanisms.ById, more detail with two parameters are critical in some cases when the string of Value can be split by ‘=’ to two substrings.
- For example, suppose we need to locate a <section> by its class “class1”, but there are too many such <section> tags while the one we concerned has a sibling of <div> whose id is “bar”, then we can define an Enum in this way: public enum SectionByAdjacent {[EnumMember(“div#bar=.class1”)]region1}. So the Value is split to two parameters: “div#bar” and “.class1” to compose the final CSS string of “div#bar+ section.class1” that is exactly what we need here.
- Noticeably, the Mechanisms.ByAttribute or Mechanisms.ByAttr in short can be used to support multiple strategies dependent on a specific attribute of the element contained within “[]”: [{A0}] , [{A0}={A1}] , [{A0}*={A1}] , [{A0}^={A1}] , [{A0}$={A1}] , [{A0}~={A1}] , [{A0}-*={A1}] means with attribute, match exactly, contains, starts with, ends with, with value of, names starts with A0 resepectively depending on if the Value contains “=” or if “=” follows ‘*’, ‘^’, ‘$’, ‘~’, ‘-*’.
- Although this design can alleviate pain of composing CSS selectors, it is always wise to try the final CSS selectors manually in your browser to see what will happen.
This section reviews the technique, discussed in , used to generate CSS selectors. However, this approach is designed to support short and simple CSS selectors that SHOULD be used within their parent container, instead of the whole HTML document range when locating an IWebElements. The detail of how to locate IWebElement is covered in Locating the IWebElement.
Even with the method to use Enum type name and member value to compose CSS selectors efficiently, they are just some strings without much value if not using them in FindElement()/FindElements() function of WebDriver. Especially when the CSS Selectors are only working in a limited branch of the element tree, or there are multiple matches within the page.
Taken the hierarchical architecture of a web page into consideration, the Enum members shall be organized in a tree like structure so as to use them by calling FindElement()/FindElements() of their parent element instead of those of IWebDriver globally. This is apparently out of capability of the Enum types, so a class Locator is defined as the wrapper of the CSS seletor carried with Enums and their parent container. The classes diagram of it are shown in the diagram below.
TextLocator, RadioLocator, CheckboxLocator, SelectLocator and TableLocator, as their name suggested, are used to provide additional functions to IWebElements identified by Enum members whose type name are “TextByXXX”, “RadioByXXX”, “CheckboxByXXX”, “SelectByXXX” and “TableByXXX” respectively. There are some subtle different operations supported by these special Locators and would be explained after introducing Locator.
Fragment and Page are class to hold the definition of Enum members directly or children Fragments directly or by reference. The use of them would be addressed in Locating the IWebElement.
Unlike other wrapper of WebDriver, the Locator in TERESA is NOT a wrapper of an IWebElement: though there is a field “lastFoundElement” in each Locator instance to store the last IWebElement found by calling its FindElement(), that field can store ANY other one if the matching criteria changes. Actually, a Locator instance encapsulates the information needed for locating an IWebElement (CSS Selector and parent Locator) and perform reading or writing operations on it via a unique Indexer.
In brief, Locator has two Properties: “Identifier” to keep the Enum member identifying an IWebElement , and “Parent” to refer to a Fragment object that holds same set of information of its container. The FindElement() function would return IWebElement by calling FindElement()/FindElements() of WebDriver recursively and the process and underlying mechanism would be explained in Locating wih Locators. So does the TryFindElement() function that also returns IWebElement but via only FindElements() of WebDriver recursively.
Although there is a TryExecute() method defined to try operating on the IWebElement found by TryFindElement(), it just wraps all Setting/Getting operations on the IWebElement provided by the unique interface of Indexer(public virtual string this[string extraInfo, Func<IWebElement, bool> filters = null]). This section discusses how, why and what this single Indexer can support to operate element by calling underlying WebDriver functions.
In addition, the Indexer of each Locator can be called via the similar Indexer of Fragment/Page (public string this[Enum theEnum, string extraInfo = null, Func<IWebElement, bool> filters = null]), the execute sequence will be discussed in Locating the IWebElement with Locators, and more uses of the them are discussed in Tutorial.
WebDriver IWebElement (implemented by RemoteWebElement) defines some common read-only properties, including TagName, Text, Enabled, Selected, Location, Size and Displayed, and a function(string GetAttribute(string attributeName)) to retrieve HTML attributes or custom attributes associated with the IWebElement, such as “id”, “class”, “href” and so on as defined here. With a string parameter to identify them, the getter of the Indexer can support all of them as summarized in Appendix.
public virtual string this[string extraInfo, Func<IWebElement, bool> filters = null]
{
get
{
IWebElement element = FindElement(filters);
if (element == null)
throw new NoSuchElementException("Failed to find element with " + Identifier.Description());
string resultString;
if (string.IsNullOrEmpty(extraInfo))
{
resultString = element.Text;
Console.WriteLine("\"{0}\"=[{1}]", resultString, Identifier.FullName());
return resultString;
}
switch (extraInfo.ToLower())
{
case Operation.TextOf:
resultString = element.Text;
break;
case Operation.TagName:
resultString = element.TagName;
break;
case Operation.Selected:
resultString = element.Selected.ToString();
break;
case Operation.Enabled:
resultString = element.Enabled.ToString();
break;
case Operation.Displayed:
resultString = element.Displayed.ToString();
break;
case Operation.Location:
resultString = element.Location.ToString();
break;
case Operation.Size:
resultString = element.Size.ToString();
break;
case Operation.Css:
resultString = Identifier.Css();
break;
case Operation.ParentCss:
resultString = Parent==null ? "" : Parent.Identifier.Css();
break;
case Operation.FullCss:
resultString = Identifier.FullCss();
break;
default:
resultString = element.GetAttribute(extraInfo);
break;
}
Console.WriteLine("\"{0}\"=[{1}, {2}]", resultString, Identifier.FullName(), extraInfo);
return resultString;
}
}
The Getter of the Indexer would:
- First, find the target IWebElement with CSS selector carried by the Identifier and the Predicate (Func<IWebElement, bool>).
- Check to see if there is a string "extraInfo" provided to identify what kind of information is requested, or return Text of the IWebElement by default when "extraInfo" is missing/null/empty;
- Simply compare the value of “extraInfo” to retreive corresponding property or call GetAttribute of the underlying IWebElement of WebDriver.
- Match with some other constant string values to get other customized values, for example, CSS Selector, and CSS Selector of its parent Locator when extraInfo is “CSS” or “ParentCss” respectively.
The use of "extraInfo" is summarized with examples as below, and more uses are listed in Appendix:
- SomeLocator[null] or SomeLocator[“”] or SomeLocator[“text”] would all return the Text of the IWebElement identified by the Identifier Enum.
- SomeLocator[“selected”] would return “True” or “False” to show if the IWebElement is selected.
- SomeLocator[“href”] would call GetAttribute() and get the link string associated with a link.
- SomeLocator[“onclick”] would return a string of script if it does exists.
- SomeLocator[“meaningless”] would return null for a non-existed attribute.
- SomeLocator[“onclick”] would return a string of script if it does exists.
- SomeLocator[“css”] would return the CSS selector composed with the Enum Identifier.
To enable the “TryDoing()” mechanism widely used in .NET, the Getter() would simply detect if the “extraInfo” parameter starts with “Try”: if “false”, then execute in the above sequence with the assumption that the element is operatable; if “true”, then the logic would check to see if the FindElement() returns a valid IWebElement first, then executes intended command or quietly returns.
In this somewhat non-canonical way, via the Getter of the Indexer, you can access any property or attribute as they are shown in the source of web pages in string form conveniently and modify it by just changing the single string parameter.
The common operations supported by IWebElement include: Clear(), Click(), SendKeys() and Submit(). In principle, you can perform all these 4 types operation on any valid IWebElement, but it may not always be proper. For example, after finding a Button whose Tag is <button>, call Clear() of it would throw an InvalidElementStateException.
With TERESA, however, particular behaviors are associated with IWebElements identified with meaningful Enum type names, then the HtmlTagName implicited by the Enum type names can be used to construct special Locator instances (TextLocator, RadioLocator, CheckboxLocator, SelectLocator and TableLocator that are covered in next section) to provide more tailored behaviors. In addition, some common operations, such as Hover(), DoubleClick(), ControlClick(), are supported in Locator class, as well as supported as extension methods of IWebElement.
All these functions on the general IWebElement are also supported by the Indexer (virtual string this[string, Func<IWebElement, bool>]) with its Setter:
set
{
if (value == null)
throw new ArgumentNullException();
IWebElement element = FindElement(filters);
if (string.Compare(extraInfo, Operation.SendKeys, StringComparison.InvariantCultureIgnoreCase)==0)
{
if (element == null || !element.IsValid())
throw new NoSuchElementException("Failed to find element with " + Identifier.FullName());
element.SendKeys(value);
Console.WriteLine("[{0}, \"{1}\"]=\"{2}\";", Identifier.FullName(), extraInfo, value);
return;
}
string action = value.ToLowerInvariant();
bool isTry = action.StartsWith(Operation.Try);
if (isTry)
{
LastTrySuccess = false;
if (element == null || !element.IsValid())
return;
action = action.Substring(Operation.TryLength);
}
if (element == null || !element.IsValid())
throw new NoSuchElementException("Failed to find element with " + Identifier.FullName());
switch (action)
{
case Operation.True:
case Operation.Click:
element.Click();
break;
case Operation.Submit:
element.Submit();
break;
case Operation.ClickScript:
element.ClickByScript();
break;
case Operation.Hover:
element.Hover();
break;
case Operation.ControlClick:
element.ControlClick();
break;
case Operation.ShiftClick:
element.ShiftClick();
break;
case Operation.DoubleClick:
element.DoubleClick();
break;
case Operation.Show:
element.Show();
break;
case Operation.HighLight:
element.HighLight();
break;
default:
Console.WriteLine("Failed to parse command associated with '{0}'.", value);
return;
}
if (isTry)
LastTrySuccess = true;
Console.WriteLine("[{0}]=\"{1}\"", Identifier.FullName(), value);
}
Just as the Getter(), the Setter() locates the target IWebElement with its Identifier and the Predicate (Func<IWebElement, bool>) to find the right element. While the “extraInfo” is only used to support SendKeys() of WebDriver, the value string is parsed to try to support more kinds of actions. If it does mean some action, then perform corresponding method calling as below:
- If the value string is “true” or “click”, then IWebElement.Click() is performed;
- If the value string is “submit”, then IWebElement.Submit() is performed;
- If the value string is “clicksript”, then ScriptClick() is performed on the IWebElement; this function is an extension method, as the other following ones defined in “IWebElementExtension.cs”, to use JavaScript to perform clicking on element that is hidden by others.
- If the value string is “controlclick” or “shiftclick”, then relative extension methods are called to perform “Ctrl+Click” and “Shift+Click” respectively on the target element. If the target is a link, then they would open the link in a new tab or in a new window respectively.
- If the value string is “hover”, then the codes of “public static void Hover(this IWebElement element, int remainMillis = 1000)” is executed, which calls “Actions.MoveToElement(element)” and remains for a period specified by remainMillis to emulate mouse hovering on the IWebElement;
- If the value string is “doubleclick”, then relative extension methods are called to invoke “Actions.DoubleClick(element)”;
- If the value string is “show” then Show() method is called to scroll the browser to bring the IWebElement into visible part by calling “ILocatable.LocationOnScreenOnceScrolledIntoView” which “scroll the browser to show the element into view.”.
- If the value string is “highlight”, then extension method HighLight(string, int) with default values are called to render the target IWebElement with the style provided as the string parameter for a period specified by the int parameter before restore the previous style. It can be used to make the test execution more intuitive.
- If the value specified by “extraInfo” is not matched in the switch() block, then nothing would be done to the IWebElement but displaying a message of "Failed to parse command associated with '{0}'.".
- The supported commands are summarized in Setting Operations. More useful methods would be defined as Extension Methods of IWebElement in “IWebElementExtension.cs” and be triggered with keyword specified by “extraInfo” later.
To enable the “TryDoing()” mechanism widely used in .NET, the Setter() would simply detect if the “extraInfo” parameter is started with “Try”: if “false”, then execute in the above sequence with the assumption that the element is operatable; if “true”, then the logic would check to see if the FindElement() returns a valid IWebElement first, then executes intended command or quietly returns.
The usage of the Indexer would be demonstrated in the examples in Tutorial.
As you might notice, there is no calling of the “SendKeys()” within Locator class at all. Indeed, this important function in Automation Testing with WebDriver should be used only by elements interactive by keyboard, such as input elements identified with HtmlTagName.Select (matching these IWebElement whose tag is <input[type=checkbox]>)and HtmlTagName.Text (matching these IWebElement whose tag is <input> or <textarea>, notice that you must avoid assign it to HTML elements such as Buttons whose tag are <input[type=button]>). During the initiation phase, IWebElements whose HtmlTagName are HtmlTagName.Text and HtmlTagName.Select are generated as TextLocator and SelectLocator respectively in the factory method (public static Locator LocatorOf(Enum id, Fragment parent)) of Locator class.
The TextLocator overrides the Indexer of Locator as below:
get { return base[extraInfo??"value", filters]; }
set
{
IWebElement element = FindElement(filters);
if (!string.IsNullOrEmpty(extraInfo))
{
base[extraInfo, filters] = string.IsNullOrEmpty(value) ? extraInfo : value;
}
else if (string.IsNullOrEmpty(value))
{
element.Clear();
Console.WriteLine("[{0}]=\"{1}\";", Identifier.FullName(), "");
}
else
{
element.SendKeys(Keys.Control + "a");
element.SendKeys(value + Keys.Tab);
Console.WriteLine("[{0}]=\"{1}\";", Identifier.FullName(), value);
}
}
The Getter() would by default return “value” attribute, that is used to store text value in elements like <input type="text">. (For this kind of element, IWebElement.Text always returns empty.)
The Setter() would clear the text input element or fill it with the string value by selecting the existing text with hotkey of “Ctrl+A”, then after input the value string, appending with an extra “Tab”. As discussed in Operates WebDriver Elements Efficiently [Tip/Trick], the extra “Ctrl+A” and “Tab” won’t affect normal input but can be very useful when the element is auto-filled with AJAX, a “Tab” would select the most matched item and fill the element with it and that means you need to only send the first few characters instead of the whole sentence when working with auto-filled text.
The same tactic of typing several characters plus “Tab” can also be used to select item from a dropdown list, that is particular useful when <optiongrp> is used to contain multiple options, as discussed in Operates WebDriver Elements Efficiently [Tip/Trick]. As a commonly used component, the HTML <select> elements deserve more attention when you need to select an option by its value or index. The “SelectElement” of WebDriver.Support.UI provides some useful utilities to follow, such as “void SelectByText(stringtext)”, “void SelectByValue(stringvalue)” and “void SelectByIndex(intindex)”, and they are also supported by SelectLocator with functions of similar name/signature, but there is a boolean value “bool toSelect = true” to indicate if the options shall be selected or de-selected to avoid functions like DeselectByText(). Take SelectByIndex() for example:
public void SelectByIndex(string indexKey, bool toSelect = true)
{
if (indexKey == null)
throw new ArgumentNullException("indexKey", "indexKey must not be null");
IWebElement targetOption = Options.FirstOrDefault(x => x.GetAttribute("index") == indexKey);
if (targetOption != null)
{
if (targetOption.Selected != toSelect)
targetOption.Click();
return;
}
int index = -1;
if (!int.TryParse(indexKey, out index))
throw new NoSuchElementException("Failed to locate option with index of " + indexKey);
if (index < 0 || index >= Options.Count)
throw new IndexOutOfRangeException("Index is out of range: '{0}'.", indexKey);
targetOption = Options[index];
if (targetOption.Selected != toSelect)
targetOption.Click();
}
This function assume the options have “index” attribute defined and try to match them with the “indexKey” first and then select or deselect the matched option with Click(). Only when such matching is failed, it would treat the “indexKey” as a number and try to locate the option by its order in “Options”.
The “Options” is actually a “ReadOnlyCollection<IWebElement>” property initialized with each execution of FindElement() after finding the <select> element associated with the Enum Identifier, since operations with <select> is always concerned with <option> tags, so does “bool IsMultiple” property. The above initialization happens with the override FindElement() that would be explained later:
public override IWebElement FindElement(Func<IWebElement, bool> filters = null, int waitInMills = DefaultWaitToFindElement)
{
IWebElement lastFound = lastFoundElement;
IWebElement result = base.FindElement(filters, waitInMills);
if (result == null)
throw new NoSuchElementException();
if (result != lastFound)
{
bool isMultiple = false;
string valueOfMultipleAttribute = result.GetAttribute(MULTIPLE);
bool.TryParse(valueOfMultipleAttribute, out isMultiple);
IsMultiple = isMultiple;
Options = result.FindElementsByCss(OPTION);
}
return result;
}
As the codes shown, “Options” and “IsMultiple” are updated only when new IWebElement is found, then the SelectLocator is served as a cache to perform “Select” or “Deselect” much more efficiently and neglect the <optiongrp> tags.
The Indexer of Locator class (string this[string extraInfo, Func<IWebElement, bool> filters = null])is override accordingly to support these extra functions by its Setter:
set
{
IWebElement element = FindElement(filters);
string prefix = string.IsNullOrEmpty(extraInfo) ? null :
ReservedPrefixes.FirstOrDefault(s => extraInfo.StartsWith(s, StringComparison.InvariantCultureIgnoreCase));
if (prefix == null)
{
string keysToBeSend = extraInfo ?? value;
foreach (char ch in keysToBeSend)
{
element.SendKeys(ch.ToString());
Thread.Sleep(100);
}
element.SendKeys(Keys.Tab);
Thread.Sleep(500);
Console.WriteLine("[{0}]=\"{1}\";", Identifier.FullName(), keysToBeSend);
return;
}
string key = extraInfo.Substring(prefix.Length);
bool toSelect = false;
if (!bool.TryParse(value, out toSelect))
{
Console.WriteLine("Failed to parse command associated with '{0}'.", value);
return;
}
if (prefix.EndsWith(KEY_PREFIX))
prefix = prefix.Substring(0, prefix.Length - KEY_PREFIX.Length);
switch (prefix)
{
case Operation.TextOf:
SelectByText(key, toSelect);
break;
case Operation.IndexOf:
case Operation.IndexSign:
SelectByIndex(key, toSelect);
break;
case Operation.ValueOf:
case Operation.ValueSign:
SelectByValue(key, toSelect);
break;
case Operation.AllOptions:
SelectAll(toSelect);
break;
}
Console.WriteLine("[{0}, \"{1}\"]=\"{2}\";", Identifier.FullName(), extraInfo, value);
}
The Setter() would first call the override FindElement() which would refresh “Options” and “IsMultiple” if needed. Then depending on if “extraInfo” is used and with what value:
- If it is null or empty, then the SelectLocator would perform SendKeys() and allow Browsers to select the best matched one after press “Tab” as discussed in Operates WebDriver Elements Efficiently [Tip/Trick].
- Otherwise, it would inform the Setter of the matching strategy:
- “text=textKey” would trigger SelectByText(string textKey, bool toSelect);
- “index=indexKey” or “#indexKey” would trigger SelectByIndex(string indexKey, bool toSelect);
- “value=valueKey” or “$valueKey” would trigger SelectByValue(string valueKey, bool toSelect);
- “alloptions”: depend on "value" would select or de-select all options of the <select multiple> IWebElement by calling SelectAll(bool toSelect).
The Getter() is also override to retrieve extra information, especially these of the first Selected option:
get
{
IWebElement element = FindElement(filters);
string resultString;
if (string.IsNullOrEmpty(extraInfo))
{
resultString = Selected==null ? FindElement().Text : Selected.Text;
Console.WriteLine("\"{0}\"=[{1}];", resultString, Identifier.FullName());
return resultString;
}
switch (extraInfo.ToLowerInvariant())
{
case Operation.TextOf:
resultString = Selected == null ? FindElement().Text : Selected.Text;
break;
case Operation.IndexOf:
case Operation.IndexSign:
if (Selected == null)
resultString = "-1";
else
{
resultString = Selected.GetAttribute(INDEX);
resultString = resultString == null ? Options.IndexOf(Selected).ToString() : resultString;
}
break;
case Operation.ValueOf:
case Operation.ValueSign:
resultString = Selected==null ? null : Selected.GetAttribute("value");
break;
case Operation.IsMultiple:
resultString = IsMultiple.ToString();
break;
case Operation.AllOptions:
var optionTexts = Options.Select(e => e.Text);
resultString = string.Join(", ", optionTexts);
break;
case Operation.AllSelected:
{
var allSelectedText = AllSelected.Select(e => e.Text);
resultString = string.Join(", ", allSelectedText);
break;
}
default:
return base[extraInfo, filters];
}
Console.WriteLine("\"{0}\"=[{1}, \"{2}\"];", resultString, Identifier.FullName(), extraInfo);
return resultString;
}
The Getter() would also first try to find the Select element by calling FindElement(), which may not all FindElement() of WebDriver, as will be disclosed in next part, but would make sure the “IsMultiple” and “Options” are updated. Then depending on if “extraInfo” is used and with what value:
- If it is null, empty or “text”, then the SelectLocator would try to get the text of the Selected option, if there is no option selected, it would return text of the select, or text of all options being put together.
- Otherwise, it would inform the Setter of the matching strategy:
- “index” or “#” would return value of the “index” attribute of the selected option, or its order in “Options”(first one returns “0”);
- “value” or “$” would return value of the “value” attribute of the selected option if it has, or null when it doesn’t have;
- “alloptions” would return text of all options as a concated string.
- “allselected” would return text of all SELECTED options as a concated string.
- “ismultiple” would return “true” or “false” to show if the select IWebElement support multiple selection.
The CheckboxLacator and RadioLocator are quite simple and similar, by overriding the Indexer Getter() of Locator, they just support new key of “checked”: set it to “extraInfo” would the text of the selected item; by overriding the Indexer Setter(), they would check/uncheck the target IWebElement.
The TableLocator, on the other hand, is quite interesting and could be optimized further to override the Indexer of Locator to provide more convenient functions. But there are some helper functions for those who prefer calling WebDriver functions directly by returning table cell IWebElement when working with standard <table> where each row has same number of cells.
There are four extra Properties defined and they are initialized in the overrided FindElement() method when the <table> element is found for first time:
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; }
...
public override IWebElement FindElement(Func<IWebElement, bool> filters = null, int waitInMills = DefaultWaitToFindElement)
{
IWebElement lastFound = lastFoundElement;
IWebElement result = base.FindElement(filters, waitInMills);
if (result == null)
throw new NoSuchElementException();
if (result != lastFound)
{
var rows = result.FindElementsByCss("tr").ToList();
IWebElement firstRow = rows.FirstOrDefault();
if (firstRow==null)
throw new NoSuchElementException("Failed to found table rows.");
var headers = firstRow.FindElementsByCss("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;
firstRow = rows.FirstOrDefault();
ColumnCount = firstRow == null ? 0 : firstRow.FindElementsByCss("td").Count();
}
}
return result;
}
The function would count the number of rows and columns, detect if there is a row containing <th> tags and record the strings appeared in the header if there is a <tr> containing <th>.
Then there are three read-only Indexers to assist locating the table cell: IWebElement CellOf(int rowIndex, int columnIndex), IWebElement CellOf(int rowIndex, string headerKeyword), IWebElement CellOf(string headerKeyword, Func<string, bool> predicate).
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.");
return FindElement().FindElementByCss(
string.Format("tr:nth-of-type({0}) {1}:nth-of-type({2})",
rowIndex + 1,
rowIndex == 0 ? "th" : "td",
columnIndex)
);
}
public IWebElement CellOf(int rowIndex, string headerKeyword)
{
int columnIndex = Headers.FindIndex(s => s.Contains(headerKeyword));
return CellOf(rowIndex, columnIndex);
}
The first one would just compose a CSS selector after checking the indexes to call the underlying FindElement() with correct order of <tr> and <td> tag. The second function would just get the column index of a specific column header before calling the first function.
The third function is listed as below:
public IWebElement CellOf(string headerKeyword, Func<string, bool> predicate)
{
int columnIndex = Headers.FindIndex(s => s.Contains(headerKeyword));
var allRows = FindElement().FindElementsByCss("tr").ToList();
if (WithHeaders)
{
allRows.RemoveAt(0);
}
foreach (var row in allRows)
{
var td = ((IWebElement)row).FindElementByCss(string.Format("td:nth-of-type({0})", columnIndex + 1));
if (predicate(td.Text))
return row;
}
return null;
}
It would search a column that is identified by its header to find the one with specific text.
As you can see, most routine operations needed in Automation Testing has been encapsulated within a single Indexer(). This approach avoids defining huge amount of atom functions to perform simple functions on each IWebElement by using a self-explaining Enum member to enable people to deduct its objective and functionality; further, with a string of “extraInfo”, the Getter() and Setter() of the Indexer could support a rich set of getting and setting operations.
In CSS Selector from Enum, the Enum based CSS selector generation mechanism is introduced. In Locators: IWebElement Finder and Operator, the wrappers of the Enum members, also the consumer of these CSS selectors accompanied, are introduced without disclosing how these CSS Selectors can be used to locate the IWebElements. However, these Locators by themselves are not effective enough, sometime even not sufficiently to be used to locate IWebElement by calling IWebElement.FindElement() of WebDriver that needs the parent IWebElement to execute. For this reason, and also for describing Document tree of a page effectively, we need some containers of Locators: Fragment & Page, to build up a hierarchical tree to describe the parental/sibling relationships between them.
The elements of a web page are organized hierarchically as a document tree: the leaf IWebElements, such as <button>s, <label>, checkbox(<input type=’checkbox’>) and links(<a>), are contained by container IWebElements like <div>, <form> or <segement>, and these container IWebElements are contained by their parents until <body> is contained by <html>. Organizing Locators in the same way is very nature to mapping the content under test, which means there should be some Locators that function like a container as the IWebElement container associated with them. In TERESA, there are two such Locators are defined: Fragment and Page.
As the class diagram of Locator showed, Fragment inherits Locator and Page inherits Fragment to function as the root container just as <html> does in a HTML page. Besides of storing “Identifier” Enum and “Parent” Locator, and finding IWebElement by using them as ordinary Locator does, the Fragment provdes two important functions to support this framework: constructing and keeping its Locator/Fragment children by Reflection, routing the calling of child Locator properly via the single Indexer interface: string this[Enum theEnum, string extraInfo = null, Func<IWebElement, bool> filters = null].
There are some utility methods to support the keeping of child Locator by Reflection and GetNestedElementIds() is the most important one:
protected static List<Enum> GetNestedElementIds(Type fragmentType)
{
if (fragmentType != FragmentType && !fragmentType.IsSubclassOf(FragmentType)
&& fragmentType != FragmentTypeInCallingAssembly && !fragmentType.IsSubclassOf(FragmentTypeInCallingAssembly))
throw new Exception("This shall only be called from Fragment classes");
var types = fragmentType.GetNestedTypes().Where(t => t.IsEnum).ToList();
if (types.Count == 0)
return null;
var result = new List<Enum>();
foreach (var type in types)
{
var enumValues = type.GetEnumValues().OfType<Enum>().ToList();
result.AddRange(enumValues);
}
return result;
}
This function parses all nested Enum types and records all members of these Enum types that would be used to construct Locator/Fragment later. The FragmentTypeInCallingAssembly is the type of Fragment in your projects that has only different AppDomain name than the type of Fragment in TERESA execution library, and it is initialized during the static constructor of Fragment, where the technique of Get Calling Assembly from StackTrace is used to get the calling Assembly.
Compared with its base class Locator, Fragment has two extra properties:
-
public Dictionary<Enum, Locator> Children { get; protected set; }
-
public virtual ICollection<Enum> FragmentEnums { get { return null; }}
The ICollection<Enum> FragmentEnums shall be override to provide the Enum Ids of other Fragments defined out of the current Fragment class, then corresponding Fragment instances are constructed with their Parent pointing to the Fragment instance; in addtion, these Fragment instances, as well as their child Locators are kept by Dictionary<Enum, Locator> Children. This Dictionary storing all Locators of IWebElement within the IWebElement referred by the Fragment, is then used to route the calling of Index (string this[Enum, string, Func<IWebElement, bool>]) to Index of Locator (string this[string, Func<IWebElement, bool>]) referred by the Enum Id to perform expected operations.
The constructor of Fragment:
- Not only instantiate itself as normal Locator does;
- But also create instances of its direct children Locator/Fragments, as well as external Fragments referred in , and initializes these Locator/Fragments to set their Parent to itself.
This happens mainly in Populate() function:
protected void Populate()
{
Type thisType = this.GetType();
var nestedIds = GetNestedElementIds(thisType);
if (nestedIds != null && nestedIds.Count != 0)
{
nestedIds.Remove(Identifier);
foreach (var id in nestedIds)
{
Locator childLocator = id.IsFragment() ?
new Fragment(id, this)
: Locator.LocatorOf(id, this);
Children.Add(id, childLocator);
}
}
var nestedFragmentTypes = thisType.GetNestedTypes().Where(t => t.IsSubclassOf(FragmentType));
foreach (Type nestedFragmentType in nestedFragmentTypes)
{
Load(nestedFragmentType);
}
if (FragmentEnums == null || FragmentEnums.Count == 0)
return;
foreach (Enum fragmentEnum in FragmentEnums)
{
Load(fragmentEnum);
}
}
It would call the GetNestedElementIds() to retrieve all nested Enum members except the one to identify this Fragment itself, then depend on the IsFragment() of the member to create new instance of either Fragment or suitable Locator before keeping them in the “Children” Dictionary. For nested Fragment types, Load(type) would be called to not only instantiate them, but also keep these Fragments and their children Locators to the Dictionary. Finally, to share Fragments defined out of the parent Fragment, “FragmentEnums” is enumerated and corresponding children Fragments are loaded with Load(Enum). The Load(Enum) function also use Reflection to construct new instances and initialize them with Parent of the current Fragment. In this way, with all functions defined within Fragment class, you only need to define Enum types&members in a hierarchical manner within a class extended from Page or Fragment, without a single line of executable codes, to get a tree of Locators.
The Page class, as its name suggested, is a nature mapping of a HTML page, so its Parent is null and Identifier is <html>. Due to efficiency consideration, all Page extended classes are instantiated in its static constructor, and Page implements IEquatable<Uri> interface to enable automatic choosing the right Page instance as Page.CurrentPage by comparing them with current browser URL. To enable a Page instance to be used with multiple web contents (for example: google.com, google.com.uk and google.com.au can be matched to the same Page instance), the bool Equals(Uri other) function shall be override, and switching of Page.CurrentPage would trigger the unique Page instance loading the ActualUri and which could be used to extract QueryNameValues after “?” of the address Uri.
The following sample diagram shows how Locators are organized together within some imaginary page/fragment classes.
Just as the names suggested, the LoginPage and ServicePage are both inherited from Page, while LoginFragment and NavbarFragment are inherited from Fragment. These Fragment/Page classes are the only place needed to define the Document tree by declaring various Enum members with straightforward type names following the pattern of “TagnameByMechanism”, and these Enum members are then be used to construct Locator objects and stored as a child of Fragment/Page in a Enum to Locator Dictionary ( public Dictionary<Enum, Locator> Children { get; protected set; }).
For example, The “FormById.login” is associated with the <form> containing <input> tags for user to input password and name respectively, because the IsFragment of the EnumMemberAttribute is “true” (as highlighted in red font), it is also the Locator of the LoginFragment. As a nested class of LoginPage, an instance of LoginFragment is generated during the constructing phase and can be accessed with the Enum whose fullname looks like “LoginPage.LoginFragment.FormById.login”. The same is true for Locators inside of LoginFragment, such as ButtonByText.Login and TextById.username: though there would be only a single instance be initiated for each Enum within NavbarFragment by constructor of NavbarFragment, these Enums and corresponding Locators are also stored in LoginPage and ServicePage.
The Enum type names, besides of assisting composing CSS selectors, the implicit HtmlTagName is also used to hint the possibly different behaviors. For example, elements with <select> tags shall support choosing from a group of options by either clicking or typing while it is no sense to try to type something to a link. Instead of exposing them as a single kind of Locator, Enum members could be used to create the most suitable Locator object. For example, SelectLocator, TextLocator or just Locator in this example would be constructed for Enum whose names are SelectByXYZ, TextByXYZ and ButtonByXYZ respectively. These different kind of Locator would be used to find the IWebElement by calling FindElement()/FindElements function of WebDriver with the CSS embedded with the Enum member in the same way, but might perform different operations even with the same function.
You might have noticed that NavbarFragment is referred in FragmentEnum of both LoginPage and ServicePage, both of them would instantiate a instance of NavbarFragment and whose Parent is LoginPage and ServicePage respectively.
As a result, taking the LoginPage for example, the logical relationship of the Locators/Fragaments/Page is shown below: the descendant Locators keep their Parent information, while the parents Locator (either Fragment or Page) keep the references to all their direct/indirect children in the “Dictionary<Enum, Locator> Children”, thus the parent Locator can access any sibling Locator with its Enum Identifier.
Noticeably, a Locator is just a wrapper of means to find an IWebElement instead of an IWebElement associated even there is a field to store the last IWebElement using the CSS selector associated with the Enum. That means you can include any Enum member within a Page even if there is no element could be found with corresponding CSS selectors at all, but you would be notified only when you are trying to find the IWebElement with WebDriver functions before operating on it.
Finding out the right IWebElement before operating it is maybe the most challenging part of using WebDriver to design test cases. Now it is time to explains the conceptional design, logical procedure, and techniques used by TERESA to find the right IWebElement and execute expected operation on it.
Suppose there is a SomePage instance that inherits Page and has three layers of descendants, and one of its inner-most child is a Link identified by ‘enum3’ with CSS selector of ‘a.class3’, then the procedure to execute a click command on it is shown in following diagram:
This command is composed as “SomePage[enum3]=”click”; as discussed inSetting Operations, and there are 8 steps to fulfill it:
- The Page instance SomePage holds references to all Locators related with Enum members contained within it, directly or indirectly as we have discussed in Locator Container: Fragment & Page, so it could find the ‘Locator3’ associated with ‘enum3’ and executes locator[extraInfo, filters] = “click”.
- Locator3 would execute “IWebElement element = FindElement(filters);” to try to get the right IWebElement associated with itself, but before searching with the CSS selector of “a.class3”, the FindElement() needs its parent IWebElement associated with Fragment3, its container.
- The Fragment3 is contained by Fragment2, so it would raise the same request to its Parent - Fragment2.
- Fragment2 has not executed FindElement() yet, realizing its Parent is actually the root node <html>, it would call FindElement() of IWebDriver directly with its CSS selector “div#container”. Because the id of “container” is unique, so IWebDriver find the <div1> immediately.
- <div1> is returned to Fragment3 as a response to its request in Step3.
- Fragment3 would call FindElement() of <div1> instead of IWebDriver to limit the searching of <div> element whose id is “part” within a limited scope, so even if Fragment1 or SomePage have other <div> with the same id of “part”, FindElement() of <div1> would return the one contained by itself.
- <div2> is returned to Locator3 as a response to its request in Step2.
- Finally, after getting the IWebElement <div2> containing the link, Locator3 would call FindElement() of <div2> using its CSS Selector “a.class3” to get the link and perform clicking as demanded by “SomePage[enum3]=”click”;.
During the execution of this command, most steps (step2 - step8) happen recursively only in function of FindElement() defined in Locator class as below:
public virtual IWebElement FindElement(Func<IWebElement, bool> filters = null, int waitInMills = DefaultWaitToFindElement)
{
if (lastFoundElement != null && (filters == null || filters == lastFilters)
&& lastFoundElement.IsValid())
return lastFoundElement;
IWebElement parentElement = null;
if (Parent != null && ! Parent.Identifier.Equals(HtmlByCustom.Root))
{
parentElement = Parent.FindElement(filters, waitInMills);
if (parentElement == null || !parentElement.IsValid())
{
return null;
}
}
string css = Identifier.Css();
if (Identifier.IsCollection() || Identifier.Mechanism().Equals(Mechanisms.ByText))
{
Func<string, ReadOnlyCollection<IWebElement>> findFunc = (parentElement == null) ?
(Func<string, ReadOnlyCollection<IWebElement>>)DriverManager.FindElementsByCssSelector : parentElement.FindElementsByCss;
var candidates = WaitToFindElements(findFunc, css, waitInMills);
if (Identifier.IsCollection())
{
lastFilters = filters ?? lastFilters;
if (lastFilters == null)
throw new NullReferenceException();
var filtered = candidates.Where(lastFilters);
lastFoundElement = filtered.FirstOrDefault();
}
else
{
lastFoundElement = candidates.FirstOrDefault(item => item.Text.Equals(Identifier.Value()));
}
}
else
{
Func<string, IWebElement> find = (parentElement == null)
? (Func<string, IWebElement>)DriverManager.FindElementByCssSelector
: parentElement.FindElementByCss;
lastFoundElement = GenericWait<IWebElement>.TryUntil( () => find(css),
x => x != null & x.IsValid(), ImmediateWaitToFindElement );
}
return lastFoundElement;
}
The first block of this function checks weather the IWebElement has been located before, if it is still valid and there is no need to perform another round of finding, just return the last found one.
The recursive calling Parent.FindElement(filters, waitInMills) happens in the second block of codes:
IWebElement parentElement = null;
if (Parent != null && ! Parent.Identifier.Equals(HtmlByCustom.Root))
{
parentElement = Parent.FindElement(filters, waitInMills);
if (parentElement == null || !parentElement.IsValid())
return null;
}
If the parent refers <html>, then parentElement would still be null, and it is used to select Function delegate of either the parentElement or IWebDriver like this:
Func<string, ReadOnlyCollection<IWebElement>> findFunc = (parentElement == null) ? (Func<string, ReadOnlyCollection<IWebElement>>)DriverManager.FindElementsByCssSelector : parentElement.FindElementsByCss;
Or:
Func<string, IWebElement> find = (parentElement==null)?(Func<string, IWebElement>)DriverManager.FindElementByCssSelector:parentElement.FindElementByCss;
The first choice happens when the Enum Identifier is used to locate several IWebElement with the same CSS Selector, which would be covered in next section. When the Enum Identifier can be used to locate an IWebElement uniquely within the range of its Parent, the second delegate is used to locate IWebElement and save it to lastFoundElement:
lastFoundElement = GenericWait<IWebElement>.Until(() => find(css), x => x != null & x.IsValid());
For cases like this, each Enum Id uniquely identify an IWebElement, without considering searching efficiency, there are no apparent benefits than simply calling FindElement() of the IWebDriver. However, this approach would show its value when there are some duplicated elements with same Id, Class, Name and attributes in next section.
Challenges Confronted
Both of the two kernel classes, RemoteWebDriver and RemoteWebElement, have two sets of methods (FindElementById/ClassName/Tag ...) based on two methods with very similar signatures:
-
protected IWebElement FindElement(string mechanism, string value)
-
protected ReadOnlyCollection<IWebElement> FindElements(string mechanism, string value)
The first one would return a single IWebElement while the second could return multiple ones with a specific finding strategy defined with “string mechanism” and “string value”.
To encapsulate these two functions within Locator class, in previous build of the project, I have also defined two Function delegates to wrap them with signatures as below:
-
public Func<string, Options, IWebElement> FindDelegate { get; protected set; }
-
public Func<string, Options, IEnumerable<IWebElement>> FindAllDelegate { get; protected set; }
Then Locator then need to initialize and call only one of them based on whether the Enum Identifier denoting an unique IWebElement or a collection. However, this straightforward approach is quite awkward for me: first, it is now very hard to expose all operations on IWebElement via a single Indexer (string this[Enum, string, Func<IWebElement, bool>]); second, there must be extra codes to choose one from the IEnumerable<IWebElement>> returned by FindAllDelegate that means more work to do to develop a test case; finally, get a collection of IWebElement is actually not really needed when we usually just need one of them to act on.
The logic way to find and operate an IWebElement from a group is:
- Call FindElements() of WebDriver directly or indirectly (FindElementsById(), FindElementsByClass() and so on);
- Perform some selection criteria to get the desired IWebElement from the returned ReadOnlyCollection<IWebElement>;
- Then execute command on that IWebElement or using it to locate its children IWebElement to execute;
- Repeats step 1) - 3) if needed.
By examining the above procedures with that of using FindElement() to locate an IWebElement uniquely identified by a CSS Selector, it seems only a Predicate (Func<IWebElement, bool>) is enough to cope with the requirement of selecting one from a collection. But how to deliver it to the right Locator/Fragment?
The answer comes from the execution procedures of locating an IWebElement itself, but first let us see something about the Predicate.
The criteria enforced in Step 2) is exactly the “Func<IWebElement, bool> filters” appeared in signature of “IWebElement FindElement(Func<IWebElement, bool> filters)” and the Indexer of Fragment (string this[Enum theEnum, string extraInfo=null, Func<IWebElement, bool> filters=null]). This parameter, “Func<IWebElement, bool> filters” has been used in three places of the FindElement():
- In the first two lines of codes, check if it has been used to locate “lastFoundElement” and returns it if the matching criteria is not changed. The usage of this part will be discussed in Google Search Example: Filters Kept By Locator.
- In the following few lines to get the “parentElement”, this Predicate is used to locate parent IWebElement by calling FindElement() of parent Fragment recursively. It is quite important and also explains why the parameter name is “filters”, instead of “filter”: the very same Predicate would be used to match the intended IWebElement, as well as all its parent containers. This would be evident in Google Search Example: Filters for Multiple Locators.
- The consumption of “filters” happens only when “Identifier.IsCollection()” is true, or the Mechanism associated with the Identifier is Mechanisms.ByText, just like the common use of Predicate in Where and FirstOrDefault clauses:
if (Identifier.IsCollection() || Identifier.Mechanism().Equals(Mechanisms.ByText))
{
Func<string, ReadOnlyCollection<IWebElement>> findFunc = (parentElement == null) ?
(Func<string, ReadOnlyCollection<IWebElement>>) DriverManager.FindElementsByCssSelector : parentElement.FindElementsByCss;
var candidates = WaitToFindElements(findFunc, css, waitInMills);
if (Identifier.IsCollection())
{
lastFilters = filters ?? lastFilters;
if (lastFilters == null)
throw new NullReferenceException();
var filtered = candidates.Where(lastFilters);
lastFoundElement = filtered.FirstOrDefault();
}
else
{
lastFoundElement = candidates.FirstOrDefault(item => item.Text.Equals(Identifier.Value()));
}
}
These simple lines of codes can then be used to locate every IWebElement effectively as the Google Search example demonstrated in next 6 sections.
Taken the Google Search as an example (which is also used in the Tutorial part) to explain the detail procedures of locating an IWebElement in TERESA. In the following captured picture, you can find the displayed content, the corresponding elements in HTML source and Enum entries with their CSS selector to identify them with different colors.
The related part of GooglePage.cs is listed to show how the Enum Identifiers are defined:
public class GooglePage : Page
{...
public class ResultItemFragment : Fragment
{
public enum ListItemAllByClass
{
[EnumMember(true)]
g,
[EnumMember("action-menu-item")]
action_menu_item
}
public enum LinkByParent
{
[EnumMember("h3")]
Title,
[EnumMember("div.action-menu")]
DownArrow
}
public enum AnyByCustom
{
[EnumMember(" cite")]
LinkAddress
}
public enum SpanByClass
{
[EnumMember("st")]
Description
}
}
}
Some of the IWebElements and their Enum identifiers are explained here:
- Each result item is contained by a ListItem <li class=”g”> as the red box shows whose Enum Identity is “ListItemAllByClass.g”. Instead of “ListItemByClass”, “ListItemAllByClass” is used as the type name, it is because there are multiple <li> elements whose class is “g” and can be located with the same CSS selector of “li.g”. The type name of “ListItemAllByClass” means its “IsCollection” is “true” as described in CSS Selector from Enum. In addition, “[EnumMember(true)]g” means “g” identifies a Fragment that holds some Child Locators.
- There are five kinds of item contained within the <li class=”g”>, and they are intentionally defined with different Mechanisms of Enum Ids as below:
- LinkByParent.Title("h3" => CSS: h3>a): the Title of each result is in the blue box;
- AnyByCustom.LinkAddress(" cite"=> CSS: cite): the line under the title to show address
- LinkByParent.DownArrow("div.action-menu"=> CSS: div.action-menu>a): the clickable downward arrow in the green box;
- SpanByClass.Description("st"=> CSS:span.st): the description of the item in purple box;
- ListItemAllByClass.action_menu_item(EnumMember("action-menu-item")=> CSS:li.action-menu-item): container of clickable links like “Cached” and “Similar” shown in the picture. the “All” of “ListItemAllByClass” indicates this Enum entry has “IsCollection”=”true” to demonstrate more complex element finding process;
Noticeably, there are multiple “Title”, “LinkAddress”, “Description” can be matched within the whole Page. But within their container identified by “ListItemAllByClass.g”, they are unique thus can be located by calling FindElement(), instead of FindElements(), of the parent IWebElement. On the other hand, although ListItemAllByClass.action_menu_item can be defined as “ListItemByText”, it is a good way to show how to use FindElement() when there are multiple IWebElement collections are involved in Google Search Example: Filters for Multiple Locators.
Google Search Example: Filters to Parent
When the “Func<IWebElement, bool> filters” is applied on the target IWebElement itself when it is identified by an Enum whose IsCollection is “true”, then FindElement() works like a simple wrapper of ReadOnlyCollection<IWebElement>>.Where(filters). However, the Fragment may not point to the IWebElement we are really intend to operate; more likely, it would be used only as a container to hold Locators to operate target IWebElement, just like the <li class=”g”> in this example.
Now suppose we need to click the link of a result item specify by a Predicate “somePredicate”, the simplified codes when using WebDriver directly might look like this:
var resultItems = driver.FindElements(By.ClassName("g"));
var oneItem = resultItems.FirstOrDefault(somePredicate);
IWebElement title = oneItem.FindElement(By.CssSelector("h3 > a"));
title.Click();
With TERESA, only one line is needed to execute these 4 steps:
SomePage[GooglePage.ResultItemFragment.LinkByParent.Title, null,filters]="click";
It should be noticed in this single line of codes: only the Enum id of the target IWebElement (GooglePage.ResultItemFragment.SpanByClass.Description) is referred, and the internal mechanism of Locator.FindElement() makes sure the filters (Func<IWebElement, bool>) is actually consumed by its parent (ResultItemFragment, identified by ListItemAllByClass.g) as the diagram below shows.
The call flow is same with the example discussed in , notice how “filters” (in Red color) is transferred along the route. For Locator1, it would forward “filters” to its parent (ResultItemFragment) in Step2. But because its IsCollection is false, so there is no need to use it in Step5 when it calls IWebElement.FindElement(By.CssSelector(“h3>a”)) to locate the unique IWebElement within the container <li.g> element.
A common scenario of using Predicate to select an IWebElement after getting ReadOnlyCollection<IWebElement>> by calling FindElements() is to choose it by its order within the Collection.
For example, if we are clicking the second result item of the Google Search, then a simplified implementation could be:
var resultItems = driver.FindElements(By.ClassName("g"));
var secondItem = resultItems.Where((x, index)=>index==2).FirstOrDefault();
IWebElement title = secondItem.FindElement(By.CssSelector("h3 > a"));
title.Click();
Instead of “Func<IWebElement, bool>”, the Predicate used by “Where((x, index)=>index==2)” has a signature of “Func<IWebElement, int, bool>”. To support this useful way of locating one from a collection, I have even considered of replacing the Indexer signature (string this[Enum, string, Func<IWebElement, bool>]) to (string this[Enum, string, Func<IWebElement, int, bool>]) before I find a way to use Func<IWebElement, bool> to do the job of Func<IWebElement, int, bool> as implemented in Utility.GenericPredicate.cs:
public class GenericPredicate<T>
{
public static Func<T, bool> IndexPredicateOf(int index) {
return new GenericPredicate<T>(index).Predicate;
}
public readonly int Index;
private int count = -1;
public Func<T, bool> Predicate {
get
{
return (T t) =>
{
count++;
return count == Index;
};
}
}
public GenericPredicate(int index)
{
Index = index;
}
}
The factory method of this Generic class would create an instance with a preset value “Index”. Each time when the Predicate is called, the "count" increased by 1, it would return “true” only when it is called at the expected times defined by “Index”. For the above example, to click the link of the second result item, we can still use one line of codes (the index is set to 1 because first calling would set the "count" to 0:
SomePage[GooglePage.ResultItemFragment.LinkByParent.Title, null, GenericPredicate<IWebElement>.IndexPredicateOf(1)]="click";
Although we can use order of the container IWebElement to choose the right target to operate, more frequently, the container itself may not be the right one to apply the “filters”: it might need to check something from its child Locator for validation, then there are some extension methods in IWebElementExtension.cs to get the “filters” and make the task much easier.
public static bool HasChildOf(this IWebElement element, Enum childEnum, Func<IWebElement, bool> predicate)
{
string childLocatorCss = childEnum.Css();
IWebElement child = null;
try
{
ReadOnlyCollection<IWebElement> candidates = null;
candidates = GenericWait<ReadOnlyCollection<IWebElement>>.Until(
() => candidates = element.FindElementsByCss(childLocatorCss),
x => x.Count != 0, 200);
var qualified = candidates.Where(predicate).ToList();
bool result = qualified.Count != 0;
return result;
}
catch (Exception)
{
return false;
}
}
public static bool HasChildOfText(this IWebElement element, Enum childEnum, string partialText)
{
return element.HasChildOf(childEnum, x => x.Text.Contains(partialText));
}
public static bool HasChildOfLink(this IWebElement element, Enum childEnum, string partialText)
{
Func<IWebElement, bool> predicate = (x) =>
{
string link = x.GetAttribute("href");
return link.Contains(partialText);
};
return element.HasChildOf(childEnum, predicate);
}
The HasChildOf(Enum childEnum, Func<IWebElement, bool> predicate) provides a unified interface to check if the IWebElement identified by childEnum meet conditions of predicate. HasChildOfText(Enum childEnum, string partialText) and HasChildOfLink(Enum childEnum, string partialText) specify the predicate to check the Text and “href” attribute respectively.
For example, refer to the captured picture in Google Search Example: Introduction, suppose we hope to click the link that starts with “stackoverflow.com”, instead of the text appeared within the title of the result item (“WebDriver : Compilation ...”), then still a single line of codes is enough:
SomePage[GooglePage.ResultItemFragment.LinkByParent.Title, null, (e) => e.HasChildOfText(GooglePage.ResultItemFragment.AnyByCustom.LinkAddress,"stackoverflow.com")] = "click";
The Predicate composed by LINQ means: only the IWebElement that contains a child element, that can be located with the CSS Selector linked with ResultItemFragment.AnyByCustom.LinkAddress, containing the text of "stackoverflow.com" would returns “true”.
The call flow is same as that discussed in Google Search Example: Introduction. Even the “LinkByParent.Title” Locator ignores this filters by itself, it would forward it to its parent ResultItemFragment. The later would use this Predicate to check each result item container, and each container would call its FindElements() with CSS of “AnyByCustom.LinkAddress” to see any one of them has text of "stackoverflow.com" before returning “true” or “false”. Finally, the first matched <li.g> associated with ResultItemFragment would call FindElement() with CSS of “LinkByParent.Title” and execute clicking action.
There are quite some locating and matching happen behind this line of codes, but now it is possible to refer these IWebElements with their Enum Identifiers and that might be enough for you to guess out its intention.
In Using Predicate As Parameter, it is mentioned the Predicate “Func<IWebElement, bool> filters” is used in three blocks, and firstly it is used to check whether “lastFoundElement” has been located with the same filters to avoid unnecessary locating. The related codes in Locator are shown below:
protected IWebElement lastFoundElement = null;
private Func<IWebElement, bool> lastFilters = null;
...
public virtual IWebElement FindElement(Func<IWebElement, bool> filters = null, int waitInMills = DefaultWaitToFindElement)
{
if (lastFoundElement != null && (filters == null || filters == lastFilters)
&& lastFoundElement.IsValid())
return lastFoundElement;
...
if (Identifier.IsCollection() || Identifier.Mechanism().Equals(Mechanisms.ByText))
{
...
if (Identifier.IsCollection())
{
lastFilters = filters ?? lastFilters;
if (lastFilters == null)
throw new NullReferenceException();
var filtered = candidates.Where(lastFilters);
lastFoundElement = filtered.FirstOrDefault();
}
else
{
lastFoundElement = candidates.FirstOrDefault(item => item.Text.Equals(Identifier.Value()));
}
}
else
{
Func<string, IWebElement> find = (parentElement == null)
? (Func<string, IWebElement>)DriverManager.FindElementByCssSelector
: parentElement.FindElementByCss;
lastFoundElement = GenericWait<IWebElement>.TryUntil( () => find(css),
x => x != null & x.IsValid(), ImmediateWaitToFindElement );
}
return lastFoundElement;
}
The lastFoundElement is used by Locator to cache the last IWebElement located by calling FindElement(Func<IWebElement, bool>, int), while the lastFilters is used only by Locator whose IsCollection is “true” to keep the recent Predicate. With such a design, if lastFoundElement is still valid (by checking if it is displayed), if there is no new filters provided when there is already one kept by lastFilters, then no new locating need to be performed and lastFoundElement would be returned immediately.
Not ony for improving performance and efficiency, this design could make the test case development more convenient. When we perform test on a page, it usually happens in batch. For example, when we are trying to input data into a <table> with many row (<tr>), and each row has multiple cell (<td>) to fill, then it is nature for us to fill all cells of a row before moving to the next row. Using WebDriver, it means to keep the row IWebElement first, and then use its FindElement() to locate its children cells to perform operations. The same things also happen in TERESA, but it is in background.
Now suppose before clicking the link that starts with “stackoverflow.com”, we prefer highlight the source (link address under the title of the result item)of the locating as below, before clicking the link which shows “WebDriver : Compilation error...”.
Then two lines of codes are needed:
SomePage[GooglePage.ResultItemFragment.AnyByCustom.LinkAddress, null, (e) => e.HasChildOfText(GooglePage.ResultItemFragment.AnyByCustom.LinkAddress,"stackoverflow.com")] = "highlight";
SomePage[GooglePage.ResultItemFragment.LinkByParent.Title] = "click";
The first line changes the background and border style of the IWebElement identified by “AnyByCustom.LinkAddress” by calling IWebElement.HighLight() defined in IWebElementExtension.cs. The "filters" is only consumed by Locators whose IsCollection is "true", so for the link identified would just neglect it, and the LINQ must use "HasChildOfText()" instead of "string.Contains()".
Noticeably, the second line of codes is quite concise, without any “filters” used to locate the link above the highlighted item. This happens because when the Locator of “LinkByParent.Title” calls FindElement(), the FindElement() would call its parent ResultItemFragment recursively. Within the ResultItemFragment, because the previous line of code has already triggered its FindElement() and stored the “filters” of “(e) => e.HasChildOfText(AnyByCustom.LinkAddress,"stackoverflow.com")” in “lastFilters”, the first line of FindElement(): “if (lastFoundElement != null && (filters == null || filters == lastFilters) && lastFoundElement.IsValid())” is “true”, so the function just return the IWebElement stored in lastFoundElement as result.
In this way, after locating an IWebElement within a container identified by Enum of IsCollection=”true”, you don't need to provide the same “filters” to access another IWebElement within that container. Just like the “highlight” effect in this example: with just an extra line of codes, you can make the Browser showing the testing progress more evident and even impress your customers during the UAT without too much extra effort.
In Using Predicate As Parameter, I have mentioned that the very same Predicate would be used to match the intended IWebElement, as well as all its parent containers. Now I am going to explain why the Predicate is named as “filters” instead of “filter”, again using the Google Search Example to show how it can be used to select two different kinds of IWebElement that needs calling FindElements() of WebDriver.
In GooglePage.cs, the Enum type of “ListItemAllByClass” is defined as below:
public class GooglePage : Page
{...
public class ResultItemFragment : Fragment
{
public enum ListItemAllByClass
{
[EnumMember(true)]
g,
[EnumMember("action-menu-item")]
action_menu_item
}
...
}
}
As discussed in CSS Selector from Enum, the keyword “All” within “ListItemAllByClass” would be interpreted as “IsCollection = true”, and its member “g” and “action_menu_item” are used to locate two collections of IWebElement as diagram below illustrated:
Notice that although “ListItemAllByClass.g” is defined within ResultItemFragment, its attribute definition(“[EnumMember(true)]”) denotes it as Identifier of ResultItemFragment itself. During initialization, “ListItemAllByClass.g” would not appear as one of the Children of ResultItemFragment, but only in the Children Dictionary of GooglePage, thus it would only be used by calling FindElements() of the IWebDriver to locate the right ResultItemFragment based on “filters”.
If we try to click the menu item “Similar” as highlighted in the picture, then following codes could be executed:
Func<IWebElement, bool> predicate = (e) =>
{
return e.HasChildOfText(GooglePage.ResultItemFragment.AnyByCustom.LinkAddress,
"stackoverflow.com") || e.Text == "Similar";
};
Page.CurrentPage[GooglePage.ResultItemFragment.LinkByParent.DownArrow, null, predicate] = "click";
Page.CurrentPage[GooglePage.ResultItemFragment.ListItemAllByClass.action_menu_item, null, predicate] = "click";
The “predicate” is defined to be composed as two parts: first one is used to match the LinkAddress as previous section discussed, the second is used to find the Action Menu Item whose text is “Similar”. To make it clearer, the “predicate” can be defined as below:
Func<IWebElement, bool> predicate = (e) =>
{
string elementClass = e.GetAttribute("class");
return (elementClass=="g" && e.HasChildOfText(GooglePage.ResultItemFragment.AnyByCustom.LinkAddress, "stackoverflow.com") || (elementClass.Contains("action-menu-item") &&e.Text == "Similar"));
};
Because the “filters” is always used by each Locator and delivered to its parent as a parameter whenever “IWebElement FindElement(Func<IWebElement, bool>, int)” is called, every Locators along the call flow can: either ignore it when the Locator’s IsCollection=’false’; or use it in the Where() clause and save it to “lastFilters” when its IsCollection=’true’. Consequently, you can and MUST define the predicate to filter BOTH result items (pink blocks in the picture) and menu items (blue boxes) in this example to make the clicking on “Similar” item happen.
It also need to emphasize that the last line of code cannot omit “null, predicate” when operates on “ListItemAllByClass.action_menu_item”, because the Locator associated has not been accessed when “LinkByParent.DownArrow” is clicked, thus there is no way to save the “filters”, as parameter, to local variable of “lastFilters”. As a result, no means to decide if to choose “Similar” or “Cached” item in this example.
As you can see, TERESA provides a simple way to cope with some most difficult element finding and an Indexer as unified interface to merge both finding element and operating element within one sentence.
This part introduces how to use TERESA to facilitate your Automation Testing by outlining the steps of building a new project, demonstrating basic operations via Facebook example, and more advanced features in Google Search example.
Procedures of Using TERESA
Here I assume you have some experience of using WebDriver and NUIT to develop UAT cases.
Similar to the way of using WebDriver directly, you might need to follow this procedure to build a new project to test a web page:
- Inspect the web page under test, highlight the elements to be used for Getting or Setting and get the keys that distinguish them from others for CSS selector composing. For those who are not familiar with CSS Selector, The 30 CSS Selectors you Must Memorize is a good article introducing all mechanisms we used in TERESA.
- “id”, “class” are best choices if they are uniquely used throughout the page or within a segment you have identified with Enum (IsCollection=”true”);
- For elements appeared under or adjacent to other elements that are easier to be located, then sometime it is very reliable to use such relationships with more significant elements.
- If the targets element are to be mapped to Locator instances instead of TexLocator/SelectLocator/..., and they are the only visible child of their container, then locating the parent shall be sufficient to support actions like “click”, “hover” and “highlight”. “ListItemAllByClass.action_menu_item” in previous section is a good example, the “click” action on the IWebElement referred by it actually happen on the its only child <a class=”f1” href=...>.
- Some commonly used attributes, such as “name”/”href”/”value”, can be very helpful to show the meaning or reason of the locating, especially when you have some codes using them.
- Sometime, elements may lack significant feature to distinguish them from others, then some other uncommon attributes can be very helpful. For example, the navigation bar items usually have different functions defined by “onclick()” and can be accessed by calling IWebElement.GetAttribute(string), then keywords after that may be copied directly for locating with clear intention meaning.
- Another case you might encounter is when there are a serial of very similar elements are positioned with fixed order, while the text, class, value of them are either exactly same or changed with culture, just like the Navigation bar appeared above the search result of Google. Then using their order could be a very convenient manner to avoid modifying CSS selectors from time to time.
- Sometime, you also need to locate an element based on its text, though it is not supported directly by CSS selector, as an effective means, it can still be considered.
- Then you need to define a tree of Enum types/members within a class extended from Page class. The quick reference of HtmlTagName and Mechnisms might be helpful for you to decide the locating strategy.
- Usually, you don’t need to implement any other function or constructor than the property of “Uri SampleUri”, you can specify the address of the target page you are testing.
- The Enum types, following the rules discussed in CSS Selector from Enum to identify both element type and mechanism used (in a format of “TagnameByMechanism” or “Tagname
AllByMechanism”), shall be defined as nested types of the extended Page class just as the nested target element within a web page. - Each member of these Enum types is used both as Identifier of the target element in the web page and CSS selector to locate corresponding IWebElement. You can use meaningful string as their name, but must include the key needed by the mechanism of CSS selector in EnumMemberAttribute. In addition, if the Enum member is used to identify a Fragment (A special Locator functions as Container of its children Locators), then “IsFragment=true” is also be clarified in its EnumMemberAttribute.
- Notice: The Locator generated from these Enum members are wrapper of tools to find some kind of IWebElement, instead of the IWebElement themselves. Thus you are free to define multiple Enum members with different mechanisms to locate one IWebElemetn, or you can define any Enum members within this Page file even if their CSS Selectors are only usable in other web pages as long as you don’t use it to try to locate an non-existed IWebElement.
- Finally it is time to design test cases, and the quick reference of Getting Operations and Setting Operations might be useful.
- The unique interface of target Page: the Indexer (string this[Enum theEnum, string extraInfo = null, Func<IWebElement, bool> filters = null]) would simply choose the Locator identified by “Enum theEnum”, then use its unique Indexer interface (string this[string extraInfo, Func<IWebElement, bool> filters = null]) to perform any operation introduced in Getting Operations & Setting Operations is always preferred. For example, the calling of Getter() as below would get the “style” of a text input element:
string attribute = Page.CurrentPage[GooglePage.TextByName.textToBeSearched, "style"];
While using the Setter() in this way would click a link whose Text is “Web”.
Page.CurrentPage[SearchNavFragment.LinkByText.Web] = "click";
- Although using the Indexer is always perferred, some function of WebDriver might have not been supported by TERESA. However, you can still use the Locator based mechanism to find IWebElement and perform operation directly on it. To perform the above clicking, following codes can be used:
Locator web = Page.CurrentPage.LocatorOf(SearchNavFragment.LinkByText.Web);
IWebElement element = web.FindElement();
element.Click();
Next, with two examples, I will show you how to use TERESA step by step.
This example means to demonstrate the basic Getting/Setting operations supported by using the unique Indexer interface of the Page instance (string this[Enum theEnum, string extraInfo, Func<IWebElement, bool> filters = null]).
Mark the page source
As the captured picture shows, most of elements to be used in this example are marked out with the source and Enum Identifier.
Page class to map elements
Then we need to define a FacebookPage class, following the guideline of Organizing Locators Together, like this:
public class FacebookLoginPage : Page
{
public override Uri SampleUri
{
get { return new Uri("https://www.facebook.com"); }
}
public enum TextByName
{
firstname,
lastname,
[EnumMember("reg_email__")]
regEmail,
[EnumMember("reg_email_confirmation__")]
regEmailConfirmation,
[EnumMember("reg_passwd__")]
regPassword
}
public enum SelectById
{
month,
day,
year
}
public enum RadioByCustom
{
[EnumMember("[name='sex'][value='1']")]
female,
[EnumMember("span#u_0_g span:nth-of-type(2) input")]
male
}
public enum LabelByText
{
Female,
Male
}
public enum ButtonByText
{
[EnumMember("Sign Up")]
SignUp
}
}
It just includes the overrided “Uri SampleUri” property and a serial of Enum types and their members. For the five text boxes (<input type=”text”...> as that of “First Name”), there are three straightforward and convenient means to build up the CSS Selector: the id “u_0_1”, the name “firstname” and attribute “aria-label”=”First Name”. “aria-label” seems to be not a popular attribute for me, so I tried to avoid using it. Although Mechanisms.ById is always preferred, the meaning of “u_0_1” is too ambiguous thus all five elements need to be defined a meaningful name manually. So finally, I choose Mechanisms.ByName by defining enum type name as “TextByName” for all these five text input fields collectively. Then I just copy the names of “firstname” and “lastname” directly as its members, but for the remaining three (“Your Email”, “Re-enter Email” and “New Password”), their “name”attributes look too long, so I did some extra work by defining “regEmail”, “regEmailConfirmation” and “regPassword” to convey the real values of “reg_email__”, “reg_email_confirmation__” and “reg_passwd__” respectively.
For the three select input for “Birthday”, their ids of “month”, “day” and “year” are quite good for direct Copy+Paste, so I did that within a new Enum type “SelectById”.
For the two radio buttons of “Female” and “Male”, there are two sets of Enums defined to locate the radio buttons themselves and the labels attached with them respectively. Though it is quite convenient to use the IDs (“u_0_d” and “u_0_e”) directly, the Mechanisms.ByCustom is used instead to show how to define the whole CSS selector by your self. The “Female” radio element is assigned with “[name='sex'][value='1']” and “span#u_0_g span:nth-of-type(2) input” for “Male”. However, you must make sure the CSS selector points to the <input type=”radio”> element. I have wasted quite some time when I forgot to append “input” to the second value and as a result, the “Male” can be checked, but the “Selected” always returns “False”.
Anyway, that is basically what you need to do to define the locating strategies for each elements on the page under testing. Now I would explain the steps to perform basic Getting/Setting operations on them in “FacebookLoginTest.cs” within the attached “TERESAExample” project.
Codes to Get/Set
The first block of codes means to demonstrate how to read property/attribute of the target IWebElements located with its Enum Identifiers as below:
string valueString = Page.CurrentPage[FacebookLoginPage.ButtonByText.SignUp];
Assert.AreEqual(valueString, "Sign Up");
valueString = Page.CurrentPage[FacebookLoginPage.ButtonByText.SignUp, "class"];
Assert.AreEqual(valueString, "_6j mvm _6wk _6wl _58mi _3ma _6o _6v");
valueString = Page.CurrentPage[FacebookLoginPage.ButtonByText.SignUp, "non-existed"];
Assert.IsNull(valueString);
valueString = Page.CurrentPage[FacebookLoginPage.TextByName.firstname, "name"];
Assert.AreEqual("firstname", valueString);
valueString = Page.CurrentPage[FacebookLoginPage.TextByName.firstname, "css"];
Assert.IsTrue(valueString.Contains(@"input[type][name*='firstname']"));
valueString = Page.CurrentPage[FacebookLoginPage.TextByName.firstname, "parentcss"];
The meaning and usage of the Indexer (string this[Enum theEnum, string extraInfo, Func<IWebElement, bool> filters = null]) of the page instance can be found in Getting Operations. The output of these Getting operation via Indexer is below:
"Sign Up"=[TERESAExample.Pages.FacebookLoginPage+ButtonByText.SignUp];
"_6j mvm _6wk _6wl _58mi _3ma _6o _6v"=[TERESAExample.Pages.FacebookLoginPage+ButtonByText.SignUp, "class"];
""=[TERESAExample.Pages.FacebookLoginPage+ButtonByText.SignUp, "non-existed"];
"firstname"=[TERESAExample.Pages.FacebookLoginPage+TextByName.firstname, "name"];
" input[type][name*='firstname'], textarea[name*='firstname']"=[TERESAExample.Pages.FacebookLoginPage+TextByName.firstname, "css"];
""=[TERESAExample.Pages.FacebookLoginPage+TextByName.firstname, "parentcss"];
The second part is means to input data into the five text boxes. Notice that you can still use TERESA as a means to help you locating the IWebElement only, then encapsulate the operations directly on IWebElement as the clear and input “Tom” into “First Name” shows.
Page.CurrentPage[FacebookLoginPage.TextByName.firstname] = "Jack";
Page.CurrentPage[FacebookLoginPage.TextByName.firstname, "sendkeys"] = "Tom";
Assert.AreEqual("JackTom", Page.CurrentPage[FacebookLoginPage.TextByName.firstname]);
Locator firstNameLocator = Page.CurrentPage.LocatorOf(FacebookLoginPage.TextByName.firstname);
IWebElement firstNameElement = firstNameLocator.FindElement();
firstNameElement.Clear();
firstNameElement.SendKeys("Tom");
Page.CurrentPage[FacebookLoginPage.TextByName.lastname] = "Smith";
Page.CurrentPage[FacebookLoginPage.TextByName.regEmail] = "youremail@hotmail.com";
Page.CurrentPage[FacebookLoginPage.TextByName.regEmailConfirmation] = "youremail@hotmail.com";
Page.CurrentPage[FacebookLoginPage.TextByName.regPassword] = "Password#$@0";
The third part demonstrates how to operate <input type=”select”> by SendKeys(), text appeared, value or index as discussed in More Tailored Operations by Special Locators.
The last part handles the two radios and the “Sign Up” button as below:
Page.CurrentPage[FacebookLoginPage.LabelByText.Female] = "true";
Assert.AreEqual(true.ToString(), Page.CurrentPage[FacebookLoginPage.RadioByCustom.female, "selected"]);
Assert.AreEqual(false.ToString(), Page.CurrentPage[FacebookLoginPage.RadioByCustom.male]);
Page.CurrentPage[FacebookLoginPage.LabelByText.Male] = "true";
Assert.AreEqual(true.ToString(), Page.CurrentPage[FacebookLoginPage.RadioByCustom.male, ""]);
Page.CurrentPage[FacebookLoginPage.ButtonByText.SignUp] = "true";
As you can see, each line of the above codes can perform one set of Getting/Setting operations on a specific element by wrapping the atomic operations like FindElement()/SendKeys()/Click()/GetAttributes by simply calling the Indexer with their Enum Identifier. Because of strict rule enforced on the Enum type names, you can avoid composing CSS selectors manually and with meaningful Enum member names, it should be not hard for others to guess out the meaning of these codes.
In this example, all Enum types are defined within FacebookPage class directly instead of nested within some Fragment classes, and these Enum members are used to identify some unique elements thus not calling FindElemenets() of WebDriver. In next example, I would use Google Search to show how to exploit this framework with some more advanced skills.
Most of the sample operations have been discussed in “Locating the IWebElement with Locators”.
Classes to map elements
To demonstrate the layered structure of Locator tree as discussed in Organizing Locators Together, there are two Fragment contained within the GooglePage instance: SearchNavbarFragment and ResultItemFragment. The former is refered by the “ICollection<Enum> FragmentEnums” with its Enum Identifier of “SearchNavFragment.DivById.hdtb” and defined in a separate file to map the button like links above the searched result as below:
public class SearchNavFragment : Fragment
{
public enum DivById
{
[EnumMember(true)]
hdtb
}
public enum DivByCustom
{
[EnumMember(".hdtb_mitem:nth-of-type(1)")]
Web,
[EnumMember(".hdtb_mitem:nth-of-type(2)")]
Images,
[EnumMember(".hdtb_mitem:nth-of-type(3)")]
Videos,
[EnumMember(".hdtb_mitem:nth-of-type(4)")]
Shopping,
[EnumMember(".hdtb_mitem:nth-of-type(5)")]
News,
[EnumMember(".hdtb_mitem:nth-of-type(6)")]
More,
[EnumMember(".hdtb_mitem:nth-of-type(7)")]
SearchTools
}
public enum LinkByText
{
Web,
Images,
Videos,
Shopping,
News,
More,
[EnumMember("Search tools")]
SearchTools
}
}
As you might have noticed, the Enum type DivByCustom and LinkByText share a same set of members. Actually, if you just need to click on the link, then DivByCustom can be used more securely because the link identified by LinkByText would disappear when it is clicked. However, using a simple mechanism of “tryclick” and checking if it is success might be an elegant option as described later.
The ResultItemFragment, as we have discussed in Google Search Example: Introduction, defines result items as the captured image demonstrated:
The GooglePage class also defines some Enum types as its children as below:
public class GooglePage : Page
{
public enum TextByName
{
[EnumMember("q")]
textToBeSearched
}
public enum LinkByClass
{
[EnumMember("gsst_a")]
SearchByVoice
}
public enum ButtonByName
{
[EnumMember("btnK")]
SearchBeforeInput,
[EnumMember("btnG")]
Search,
}
public enum LinkByUrl
{
[EnumMember("options")]
Options,
[EnumMember("Login")]
Login
}
public enum LinkById
{
[EnumMember("pnnext")]
Next
}
public class ResultItemFragment : Fragment
{
...
}
public override ICollection<Enum> FragmentEnums
{
get { return new Enum[]{SearchNavFragment.DivById.hdtb}; }
}
public override Uri SampleUri
{
get { return new Uri("http://www.google.com/"); }
}
public override bool Equals(Uri other)
{
return other.Host.Contains("google.com");
}
...
}
With this structure, shared Fragments like SearchNavFragment can be shared among Page classes although each Page instance need to construct a different instance of it pointing to the Page itself. At the same time, the embedded Fragment like would also be constructed with Reflection technique. By putting Enum types in different Page/Fragment, their Enum members are organized in a hierarchical manner, thus Locators generated with Reflection technique would present an exact hierarchical pattern to enable using IWebElement.FindElement() instead of IWebDriver.FindElement().
It also need to be mentioned that when I try to open "http://www.google.com/", the browser would be re-directed to "http://www.google.com.au/". So I have to override the default Equals(Uri) to make the above Page can be used wherever you are.
Codes to Get/Set
To demonstrate the locating mechanisms, I have included two test cases and you can find the source codes in “GoogleSearchTest.cs”. The codes of the first test “public void SearchTest()” are explained here in their order.
The first three sentences:
Page.CurrentPage[GooglePage.TextByName.textToBeSearched] = "WebDriver wrapper";
Assert.AreEqual("WebDriver wrapper", Page.CurrentPage[GooglePage.TextByName.textToBeSearched]);
Page.CurrentPage[GooglePage.TextByName.textToBeSearched, "highlight"] = "";
They are used to show after input keyword and confirmation, how to use “HighLight(string)” defined in IWebElementExtension.cs to highlight the target element with JavaScript.
It shall be noticed that the format of perform “highlight” on Text box is different from other Locators, actually TextLocator swap the “value” and “extraInfo” string parameters before calling the Setter of Locator class which would output like this:
[TERESAExample.GooglePages.GooglePage+TextByName.textToBeSearched]="WebDriver wrapper";
"WebDriver wrapper"=[TERESAExample.GooglePages.GooglePage+TextByName.textToBeSearched, "value"];
[TERESAExample.GooglePages.GooglePage+TextByName.textToBeSearched]="highlight";
After entering the keyword, now we must click the blue button. However, this button is a totally different one than the “Google Search” button appeared in a blank page that has a name of "btnK", while this has "btnG". Suppose you need to perform some action but cannot guarantee its success, then “try” + “action” can be used to avoid throwing exception:
Page.CurrentPage[GooglePage.ButtonByName.SearchBeforeInput] = "tryclick";
if (!Page.LastTrySuccess)
Page.CurrentPage[GooglePage.ButtonByName.Search] = "click";
The "tryclick" would fail, because the button identified by “ButtonByName.SearchBeforeInput” is gone, by checking “Page.LastTrySuccess”, we can confirm it’s failed and "click" the button associated with “ButtonByName.Search” and it will succeed.
Page.CurrentPage[GooglePage.LinkByClass.SearchByVoice] = "hover";
Page.CurrentPage[GooglePage.LinkById.Next] = "show";
The above two sentences are just used to show effect of Hover() and Show(). The first one would move mouse over the image of microphone, then “Search by voice” would appear. The “Next” button, usually in the very bottom of the page, could be made visible by scroll page to bottom using script.
Assert.Catch<NoSuchElementException>(() =>
Page.CurrentPage[SearchNavFragment.LinkByText.Web] = "click");
Page.CurrentPage[SearchNavFragment.LinkByText.Web] = "tryclick";
Page.CurrentPage[SearchNavFragment.DivByCustom.Web] = "click";
As mentioned earlier, the link “<a>” related with LinkByText.Web does not exist in dynamic page like Google, so clicking on it would throw NoSuchElementException. However, you can always click on the empty container associated with DivByCustom.Web, then there will be nothing happen or just like you click on a link. Executing “tryclick” command on a non-exist element, like the link identified with LinkByText.Web, can be safe although need some time to wait for its execution. By the way, two functions within Locator (bool TryExecute(string, string, Func<IWebElement, bool>) and IWebElement TryFindElement(Func<IWebElement, bool>, int)) can be used to get similar result if you prefer using Locator instance directly.
By so far, the “Func<IWebElement, bool> filters” of the Indexer (string this[Enum theEnum, string extraInfo, Func<IWebElement, bool> filters = null]) has not been used. But the following codes would show you how it can be helpful to handle collective elements/containers.
Following lines of codes are used to show when “filters” parameter is mandatory to locate one IWebElement that is either one of a collection ones with same CSS Selector, or contained by one of them.
Assert.Catch<NullReferenceException>(() =>
Page.CurrentPage[GooglePage.ResultItemFragment.LinkByParent.Title] = "click");
Page.CurrentPage[GooglePage.ResultItemFragment.LinkByParent.DownArrow, null,
GenericPredicate<IWebElement>.IndexPredicateOf(2)] = "click";
Page.CurrentPage[GooglePage.ResultItemFragment.AnyByCustom.LinkAddress] = "highlight";
Page.CurrentPage[GooglePage.ResultItemFragment.ListItemAllByClass.g] = "highlight";
The “LinkByParent.Title” refers the clickable page titles like “horejsek/python-webdriverwrapper · GitHub” and “WebDriver : Compilation error in custom created Wrapper ...” that show in your browser. Although the Enum type name means the IWebElement is unique, that uniqueness is only valid within their container - ResultItemFragment identified by “ListItemAllByClass.g”. Because of the cascading procedure of locating an IWebElement actually starts from locating the parent IWebElement first, the Locator of “LinkByParent.Title” would fail to get the parent IWebElement with only CSS selectors without conditions specified by “filters”. Consequently, the first sentence “Page.CurrentPage[GooglePage.ResultItemFragment.LinkByParent.Title] = "click"” would throw NullReferenceException.
Then the second sentence provides a IndexPredicate (that is introduced in Google Search Example: Filters Counting IWebElements) as below:
"Page.CurrentPage[GooglePage.ResultItemFragment.LinkByParent.DownArrow, null, GenericPredicate<IWebElement>.IndexPredicateOf(2)] = "click""
As a result, the Locator of “ListItemAllByClass.g” would choose the third result item after calling IWebDriver.FindElemens() to get a “ReadOnlyCollection<IWebElement>” and deliver it back to Locator of “LinkByParent.Title” to get the unique IWebElement identified by “LinkByParent.DownArrow” and click it.
Because the “filters” is sticky (the Locator consuming it would keep it until another non-null “filters” is provided), the next two sentences would highlight both the container (ResultItemFragment identified by “ListItemAllByClass.g”) and another sibling IWebElement (by AnyByCustom.LinkAddress) as pictures below, and you can find more explanation from Google Search Example: Filters with Buffering.
The following two sentences are used to show more practical use of matching IWebElement by conditions of its sibling within the same container discussed in Google Search Example: Filters Involving Other IWebElement:
Page.CurrentPage[GooglePage.ResultItemFragment.AnyByCustom.LinkAddress, null, (e)=>e.HasChildOfText(GooglePage.ResultItemFragment.AnyByCustom.LinkAddress,"stackoverflow.com")] = "highlight";
Page.CurrentPage[GooglePage.ResultItemFragment.LinkByParent.Title] = "click";
Because type name of “AnyByCustom” doesn’t contain “All” like “AnyAllByCustom”, thus “AnyByCustom.LinkAddress” would simply ignore any filters within its FindElement(), the provided “filters” ((e)=>e.HasChildOfText(GooglePage.ResultItemFragment.AnyByCustom.LinkAddress,"stackoverflow.com") is actually consumed by the container identified by “ListItemAllByClass.g”, and the extended methods HasChildOfText(Enum childEnum, string partialText) and HasChildOfLink(Enum childEnum, string partialText) encapsulate the logic needed to find an IWebElement based on a child of it. So instead of comparing all text of a container, only concerned part is used to increase matching efficiency and accuracy.
The first sentence would highlight the link address of a result item as figure below shows, and then perform clicking on the actual link component above it.
These two-step procedure is not necessary if you do not need to indicate the source of the matching: you can use only one sentence to perform clicking on a result item based on matching with another one associated just like this:
Page.CurrentPage[GooglePage.ResultItemFragment.LinkByParent.Title, null,(e) => e.HasChildOfText(GooglePage.ResultItemFragment.AnyByCustom.LinkAddress,"code.google")] = "controlclick";
It would open the web page in another tab, whose link address contains "code.google".
In case we need to perform operations on one IWebElement of a similar collection associated with a Locator, and it is also contained by another collective container, then actually we need to combine two predicates together as “filters” used in the Indexer as discussed in Google Search Example: Filters for Multiple Locators. The related codes is listed here:
Func<IWebElement, bool> predicate = (e) =>
{
string elementClass = e.GetAttribute("class");
return (elementClass=="g" && e.HasChildOfText(GooglePage.ResultItemFragment.AnyByCustom.LinkAddress, "stackoverflow.com") || (elementClass.Contains("action-menu-item") &&e.Text == "Similar"));};
Page.CurrentPage[GooglePage.ResultItemFragment.LinkByParent.DownArrow, null, predicate] = "click";
Page.CurrentPage[GooglePage.ResultItemFragment.ListItemAllByClass.action_menu_item, null, predicate] = "tryhighlight";
if (Page.LastTrySuccess)
Page.CurrentPage[GooglePage.ResultItemFragment.ListItemAllByClass.action_menu_item] = "click";
Since the action menu item “Similar” does exist, so it is highlighted and then clicked, as the picture below shows.
Unlike most canonical projects, there are three techniques, they are also most favorite tools for me, widely used in TERESA: Enum combined with Attribute, Indexer with Function delegate as parameter and Reflection. The major reason behind of this is to enhance coding efficiency: I always try to typing less lines of codes to support as much functionality as I could.
Enum vs. Class
Enum types, as a special kind of class, are good media to function as Identifiers carrying lot of static information. As you have seen in previous examples, their type names, member names and especially the attributes associated can be used to extract a lot of information without defining numerous Dictionary and sophisticate querying mechanisms to get it. Unlike Enum type in JAVA, there can be no methods defined within Enum types, however, because its uniqueness and nature of class, it is not hard to define extension methods or even link some static classes with them to provide extra service logic. So it is always very exciting for me to try to exploit its potential to use Enum as a key to store/retrieve various kinds of information, and the succinct styles of definition, strongly typed and constant nature are always helpful during this process.
By contrast, the Class could be equipped with a rich set of methods/properties/attributes to do almost everything to replace Enum in many scenarios. However, the definition and initiation always means a lot of codes to write and maintain. To use class instead of Enum in TERESA, it seems to me that each Enum types defined within the Page class shall be defined as a class, and each Enum member needs to be defined as extended classes and that would make the whole solution meaningless.
Indexer instead of Functions
To use WebDriver to perform any task, it always take two steps: find the element first, then call some functions/properties to read or write it, so it is preferred to merge the element finding and operatings of a transaction into a singel line of codes.
The best solution for me is to have a function or Indexer act as the unique reading/writing interface when using TERESA to perform operations by calling various WebDriver methods. Indexer is quite suitable for this role, especially when I found it is hard to name such a function when it has so many roles. By merging finding and executing together, the codes of TERESA is easier to maintain: service logics is within a single block thus modifying is much convenient.
More importantly, using Indexer in this way wipe out the needs of defining lots of atomic functions to perform just very trivial tasks like inputFirstName(), selectAge() and etc. These throwaway functions are time-consuming, heavily depending on the underlying WebDriver API and element finding strategy.
Another important reason for me to using Indexer is due to the repeat-ability of test execution. I have developed another library to generate data pseudo-randomly based on the names of Enum identifiers. For example Enum member with name of “email” would be assign a email format string, “phone” would be assigned with a fixed or mobile phone by random and “firstName” would be given a common English given name. So instead of specifying what to be input to a field, most items could be filled randomly to reduce typing and unleash WebDriver to test various combination of user behavior automatically with some pre-defined operation logic. However, one challenge I encountered was how to re-produce the failure when one case failed. As you might notice when execute the sample test, the console would output something like this:
[TERESAExample.GooglePages.GooglePage+TextByName.textToBeSearched]="WebDriver wrapper";
"WebDriver wrapper"=[TERESAExample.GooglePages.GooglePage+TextByName.textToBeSearched, "value"];
[TERESAExample.GooglePages.GooglePage+TextByName.textToBeSearched]="highlight";
[TERESAExample.GooglePages.GooglePage+ButtonByName.Search]="click";
...
Isn’t it very similar to the codes of the test function? The output file could be conveniently converted to codes to execute in exactly the same sequence and target by “Copy&Paste”, or with a simple function to convert if all operations are performed via the Indexer.
Actually, even the Predicate(Func<IWebElement, bool>) used in the Indexer is usually simple and limited, thus can be described with some script like strings. If that really happen, then test cases can be written in WORD and executed as codes effortless.
Reflection vs Performance
Although Reflection is quite expensive, for me, it is a good deal to let CPU run more busier to save the programmer from typing repetitive codes, and it is especially true in Automation Testing when WebDriver would spend a lot of time to wait for loading / rendering of the page.
The repetitive codes, for me, means constructors of extended classes, iteration of specific types, initialization of class instances and functions that could be implemented by the base class. As you can see, in this project I have used Reflection to avoid the first three kinds of codes.
On the other hand, I have also taken some measures to limit the impact on performance:
- Use Dictionary whenever possible to store relative static items like EnumMemberAttribute, Values of Enum.
- Define Locator/Fragment/Page as wrapper of IWebElement searching tools, instead of wrapper of IWebElement.
- Use Reflection only once, usually within the static constructor of a relative class, and get all needed information once for all.
- Construct instances of all Page classes, and use them with the static property of Page.CurrentPage.
Anyway, in my opinion, when there are a lot of classes/enums sharing some similarity, Reflection can help a lot to get a full set of functional entities.
The following table lists the Enum members of HtmlTagName discussed in CSS Selector from Enum, their values defined in “static EnumTypeAttribute()”, related HTML tags and description.
HtmlTagName
| Value
| Tags
| Description
|
Any
| {0} {1}
|
| Any element
|
Html
| html
| <html>
| Defines the root of an HTML document
|
Title
| title
| <title>
| Defines a title for the document
|
Body
| body
| <body>
| Defines the document's body
|
Text
| {0} input[type]{1}, {0} textarea{1}
| <input> <textarea>
| Input control or a multiline input control (text area)
|
Image / Img
| {0} img{1}
| <img>
| Defines an image
|
Button
| {0} button{1}, {0} input[type=button]{1}, {0} input.btn{1}, {0} input.button{1}, {0} div[role=button]{1}
| <button>, <input>, <div>
| Any element functions as a clickable button.
|
Link / A
| {0} a{1}
| <a>
| Defines a hyperlink
|
Select
| {0} select{1}
| <select>
| Defines a drop-down list
|
Label
| {0} label{1}
| <label>
| Defines a label for an <input> element
|
Checkbox / Check
| {0} input[type=checkbox]{1}
| <input>
| <input> control whose “type” is “chechbox”
|
Radio
| {0} input[type=radio]{1}
| <input>
| <input> control whose “type” is “radio”
|
Div
| {0} div{1}
| <div>
| Defines a section in a document
|
Span
| {0} span{1}
| <span>
| Defines a section in a document
|
Section
| {0} section{1}
| <section>
| Defines a section in a document
|
Paragraph / P
| {0} p{1}
| <p>
| Defines a paragraph
|
Fieldset
| {0} fieldset{1}
| <fieldset>
| Groups related elements in a form
|
ListItem
| {0} li{1}
| <li>
| Defines a list item
|
List
| {0} ol{1}, {0} ul{1}
| <ul>, <ol>
| Defines an unordered or ordered list
|
Header
| {0} header{1}
| <header>
| Defines a header for a document or section
|
H1/H2/H3/H4/H5/H6
| {0} h1{1} / ...
| <h1> to <h6>
| Defines HTML headings
|
Form
| {0} form{1}
| <form>
| Defines an HTML form for user input
|
Table
| {0} table{1}
| <table>
| Defines a table
|
TableHead / Thead
| {0} thead{1}
| <thead>
| Groups the header content in a table
|
TableBody / Tbody
| {0} tbody{1}
| <tbody>
| Groups the body content in a table
|
TableFoot / Tfoot
| {0} tfoot{1}
| <tfoot>
| Groups the footer content in a table
|
TableRow / Tr
| {0} tr{1}
| <tr>
| Defines a row in a table
|
TableCell / Td
| {0} td{1}
| <td>
| Defines a cell in a table
|
TableHeadCell / Th
| {0} th{1}
| <th>
| Defines a header cell in a table
|
Mechanisms
The following table lists the Enum members of Mechanisms discussed in CSS Selector from Enum, their values defined in “static EnumTypeAttribute()”, related CSS selector prototype, meaning and example.
To understand the meaning of these mechanisms, it is helpful to visit The 30 CSS Selectors you Must Memorize. Within the table, the {T}, {A}, {A0}, {A1} appeared below is placeholder of the Values of the EnumMemberAttribute:
- {T} is the tag name determined by the HtmlTagName
- {A} is specified by the EnumMemberAttribute.Value() where there is no "="
- {A0} is specified by the first part of EnumMemberAttribute.Value() before "="
- {A1} is optional second part of EnumMemberAttribute.Value() after "="
Mechanisms
| Value
| CSS prototype
| Meaning
| Example
|
ById
| #{0}
| {T}#{A}
| Using Id for element finding
| SelectById.x: select#YYY
|
ByClass
| .{0}
| {T}.{A}
| Using Classname for finding
| SelectByClass.x: select.x
|
ByAdjacent
| {0}+{1}
| {A}+{T}
| Select only the element {T} that is immediately preceded by the former element {A}
| SelectByAdjacent.x(“A0=A1”): A0+ selectA1
|
ByParent
| {0}>{1}
| {A}>{T}
| Finding tag {T} that is direct child of some parent tag {A}
| SelectByParent.x(“A0=A1”): A0> selectA1
|
ByAncestor
| {0} {1}
| {A} {T}
| Finding tag {T} that is descendant of some parent tag {A}
| SelectByAncestor.x(“A0=A1”): A0 selectA1
|
BySibling
| {0}~{1}
| {A}~{T}
| Select only the element {T} that is preceded by the former element {A}
| SelectBySibling.x(“A0=A1”): A0~ selectA1
|
ByUrl
| [href*='{0}']
| {T}[href*={A}]
| Selects tag with link contains keyword, equals to ByAttribute when the attribute is "href"
| SelectByUrl.x: select[href*='x']
|
ByName
| [name*='{0}']
| {T}[name*={A}]
| Selects tag with name contains keyword, equals to ByAttribute when the attribute is "name"
| SelectByName.x: select[name*='x']
|
ByValue
| [value*='{0}']
| {T}[value*={A}]
| Selects tag with value contains keyword, equals to ByAttribute when the attribute is "value"
| SelectByValue.x: select[value*='x']
|
ByAttribute / ByAttr
| [{0}='{1}']
| {T}[{A}]
| [{A}] can also represent any selector of below:[{A0}] , [{A0}={A1}] , [{A0}*={A1}] , [{A0}^={A1}] , [{A0}$={A1}] , [{A0}~={A1}] , [{A0}-*={A1}] means:
with attribute, match exactly, contains, starts with, ends with, with value of, names starts with A0 resepectivel
| SelectByAttr.x(“A0=A1”): select[A0='A1']
|
ByOrder
| {0}:nth-of-type({1})
| {A0} {T}:nth-of-type({A1})
| Selects Nth(from 1) of the type
| SelectByOrder.x(“A0=A1”): A0 select:nth-of-type(A1)
|
ByOrderLast
| {0}:nth-last-of-type({1})
| {A0} {T}:nth-last-of-type({A1})
| Selects, from the end, Nth(from 1) of the type
| SelectByOrderLast.x(“A0=A1”): A0 select:nth-last-of-type(A1)
|
ByChild
| {0}:nth-child({1})
| {A0} {T}:nth-child({A1})
| By Nth(from 1) of the nested children
| SelectByChild.x(“A0=A1”): A0 select:nth-child(A1)
|
ByChildLast
| {0}:nth-last-child({1})
| {A0} {T}:nth-last-child({A1})
| Selects, from the end, Nth(from 1) of the nested children
| SelectByChildLast.x(“A0=A1”): A0 select:nth-last-child(A1)
|
ByCustom
| {0}
| {A}
| All css text definded by Value() of EnumMemberAttribute
| SelectByCustom.x(“select[A0=A1]”): select[A0=A1]
|
ByTag
| none
| {T}
| Use only the default tagnames to locate IWebElement
| SelectByTag.x(“select[A0=A1]”): select
|
ByText
| none
| {T}
| Using LINQ instead to select the intended element
| SelectByText.x(“A0=A1”): select
|
Operation supported by Indexer
Tables in this section summarize operation supported via Indexer of Page classes (string this[Enum, string, Func<IWebElement, bool>]) that are executed by Indexer of corresponding Locators(string this[string, Func<IWebElement, bool>]), you can use them as a quick reference strito design your own test cases.
The table below lists the Getting operations, by which you can get properties or values of a specific IWebElement, their format and meanings.
Key
| Example
| Description
|
Getting Operations by Locator
|
|
Null / ""
| string text = Page.CurrentPage[SearchNavFragment.DivByCustom.Web];
| Get the text “Web” of <div> element identified by “DivByCustom.Web”
|
"text"
| string text = Page.CurrentPage[SearchNavFragment.DivByCustom.Web, "text"];
| Get the text “Web” of <div> element identified by “DivByCustom.Web”
|
"tagname"
| string tagname = Page.CurrentPage[SearchNavFragment.DivByCustom.Web, "tagname"];
| Get the tagname “div” of <div> element identified by “DivByCustom.Web”
|
"selected"
| string selected = Page.CurrentPage[SearchNavFragment.DivByCustom.Web, "selected"];
| Get the “Selected” property of <div> element identified by “DivByCustom.Web”, either “True” or “False”
|
"enabled"
| string valueString = Page.CurrentPage[FacebookLoginPage.ButtonByText.SignUp, "enabled"];
| Get the “Enabled” property of <div> element identified by “ButtonByText.SignUp” and as a string “True”
|
"displayed"
| string valueString = Page.CurrentPage[FacebookLoginPage.ButtonByText.SignUp, "displayed"];
| Get the “Displayed” property of <div> element identified by “ButtonByText.SignUp”, as a string of “True”.
|
"location"
| string valueString = Page.CurrentPage[FacebookLoginPage.ButtonByText.SignUp, "location"];
| Get the “Location” property of <div> element identified by “ButtonByText.SignUp”, as a string of “{X=765,Y=556}”.
|
"size"
| string valueString = Page.CurrentPage[FacebookLoginPage.ButtonByText.SignUp, "size"];
| Get the “Size” property of <div> element identified by “ButtonByText.SignUp”, as a string of “{Width=194, Height=39}”.
|
"css"
| string css = Page.CurrentPage[GooglePage.ResultItemFragment.LinkByParent.Title, "css"];
| Get the CSS selector string of the link identified by “LinkByParent.Title”, that is “h3> a”.
|
"parentcss"
| string parentCss = Page.CurrentPage[GooglePage.ResultItemFragment.LinkByParent.Title, "parentcss"];
| Get the CSS selector string of the parent of the link identified by “LinkByParent.Title”, that is “ li.g”.
|
"fullcss"
| string fullCss = Page.CurrentPage[GooglePage.ResultItemFragment.LinkByParent.Title, "fullcss"];
| Get the full CSS selector string of the link identified by “LinkByParent.Title”: “ li.g h3> a”.
|
others
| string href = Page.CurrentPage[GooglePage.ResultItemFragment.LinkByParent.Title, "href"];
| Get the “href” attribute of the link identified by “LinkByParent.Title”: “http://...”.
|
Extra Getting Operations by TextLocator
|
|
Null / ""
| Assert.AreEqual("WebDriver", Page.CurrentPage[GooglePage.TextByName.textToBeSearched]);
| Get the text “WebDriver” of <input> element identified by “TextByName.textToBeSearched”
|
Extra Getting Operations by SelectLocator
|
|
Null / ""
| string temp = Page.CurrentPage[SelectExamplesPage.SelectByName.food];
| Get the text of selected option of <select> element identified by “SelectByName.food”
|
"text"
| string temp = Page.CurrentPage[SelectExamplesPage.SelectByName.food, "text"];
| Get the text of selected option of <select> element identified by “SelectByName.food”
|
"index" / "#"
| string temp = Page.CurrentPage[SelectExamplesPage.SelectByName.food, "index"];
| Get the “index” attribute of selected option of <select> element identified by “SelectByName.food” if it is defined, or the order of the selected option (from 0)
|
"value" / "$"
| string temp = Page.CurrentPage[SelectExamplesPage.SelectByName.food, "value"];
| Get the “value” attribute of selected option of <select> element identified by “SelectByName.food” if it is defined
|
"ismultiple"
| string temp = Page.CurrentPage[SelectExamplesPage.SelectByName.food, "ismultiple"];
| “True” or “False” to indicate if the <select> element can select multiple options at once
|
"alloptions"
| string temp = Page.CurrentPage[SelectExamplesPage.SelectByName.food, "alloptions"];
| Get all options of the <select> element identified by “SelectByName.food” split by “,”
|
"allselected"
| string temp = Page.CurrentPage[SelectExamplesPage.SelectByName.food, "allselected"];
| Get all selected options of the <select> element identified by “SelectByName.food” split by “,”
|
Extra Getting Operations by CheckboxLocator
|
|
Null / ""
| string temp = Page.CurrentPage[SomePage.CheckboxById.ok];
| “True” or “False” to indicate if the <input type=checkbox> element is selected
|
"selected" / "checked"
| string temp = Page.CurrentPage[SomePage.CheckboxById.ok, "checked"];
| Get the text of selected option of <select> element identified by “SelectByName.food”
|
Extra Getting Operations by RadioLocator
|
|
Null / ""
| Assert.AreEqual(true.ToString(), Page.CurrentPage[FacebookLoginPage.RadioByCustom.male, ""]);
| “True” or “False” to indicate if the <input type=radio> element identified by “RadioByCustom.male” is selected
|
"selected"
| Assert.AreEqual(true.ToString(), Page.CurrentPage[FacebookLoginPage.RadioByCustom.female, "selected"]);
| “True” or “False” to indicate if the <input type=radio> element identified by “RadioByCustom.female” is selected
|
Setting Operations
The table below lists the Setting operations, by which you can operate or set properties or values of a specific IWebElement, their format and meanings.
Key
| Example
| Description
|
Getting Operations by Locator
|
|
“try” + “actionname”
| Page.CurrentPage[SearchNavFragment.LinkByText.Web] = "tryclick";
| Attempt to execute some action only when the IWebElement is successfully located, otherwise just silently return
|
"sendkeys"
| Page.CurrentPage[GooglePage.ButtonByName.Search, "sendkeys"] = Keys.Enter;
| Call SendKeys() of the <button> element identified by “ButtonByName.Search” to simulate press “Enter”
|
"true" / "click"
| Page.CurrentPage[SearchNavFragment.DivByCustom.Web] = "click";
| Click the element identified by “DivByCustom.Web”
|
"submit"
| Page.CurrentPage[FacebookLoginPage.ButtonByText.SignUp] = "submit"
| Call Submit() of the element identified by “ButtonByText.SignUp”
|
"clickscript"
| Page.CurrentPage[GooglePage.ButtonByName.Search] = "clickscript";
| Click the element identified by “ButtonByName.Search” by script
|
"hover"
| Page.CurrentPage[GooglePage.LinkByClass.SearchByVoice] = "hover";
| Hover the mouse over the element identified by “LinkByClass.SearchByVoice” and wait for a while.
|
"controlclick"
| Page.CurrentPage[GooglePage.ResultItemFragment.LinkByParent.Title] = "controlclick";
| CtrlClick the link identified by “LinkByParent.Title”, which would open it in another tab
|
"shiftclick"
| Page.CurrentPage[GooglePage.ResultItemFragment.LinkByParent.Title] = "shiftclick";
| ShiftClick the link identified by “LinkByParent.Title”, which would open it in another window
|
"doubleclick"
| Page.CurrentPage[GooglePage.ResultItemFragment.LinkByParent.Title] = "doubleclick";
| DoubleClick the link identified by “LinkByParent.Title”
|
"show"
| Page.CurrentPage[GooglePage.LinkById.Next] = "show";
| Scroll the page to make the element identified by “LinkById.Next” is visible in view
|
"highlight"
| Page.CurrentPage[SearchNavFragment.DivByCustom.Web] = "highlight";
| Highlight the element identified by “DivByCustom.Web” by changing its style and keep for a while before restoring it back with script
|
Extra Getting Operations by SelectLocator
|
|
"text="
| Page.CurrentPage[SelectExamplesPage.SelectByName.cars, "text=Volvo"] = "true";
| Click the option whose text is “Volvo” if it is not selected from select element identified by SelectByName.cars
|
"index=" / "#"
| Page.CurrentPage[SelectExamplesPage.SelectByName.food, "#8"] = "true";
| Click the option, whose index is “8” if it is not selected, from select element identified by SelectByName.food
|
"value=" / "$"
| Page.CurrentPage[SelectExamplesPage.SelectByName.food, "value=2"] = "true";
| Select the option whose “value” attribute is “2” from select element identified by SelectByName.food
|
"alloptions"
| Page.CurrentPage[SelectExamplesPage.SelectByName.sports, "alloptions"] = "true";
| Select all options of select element identified by SelectByName.sports
|
otherwise
| Page.CurrentPage[SelectExamplesPage.SelectByName.sports, "soc"] = "true";
| Call SendKeys() of the element identified by “SelectByName.sports” to enter “soc” + TAB, which would select the first matched option
|
History
Keep a running update of any changes or improvements you've made here.