Thursday, March 16, 2017

XCUI: How to identify elements on conditions

Identifying an element from the GUI is always the most important part of UI automation. Sometimes you can easily identify an element by name, but most of the time we identify the element by the element status. Today let's see how we can identify an element in XCUI automation.

Let's use a sample to explain the problem. See below slice of a window structure. This window is used to represent a book structure. The book has several chapters while each chapter contains pages. The chapters and pages in gray are the ones that have been disabled on GUI.
Window 0x600000160cc0: Main Window, title: 'My Book'
     SplitGroup 0x600000160d80:
          ScrollView 0x600000160e40:
              Outline 0x600000160f00:
                   OutlineRow 0x600000160fc0:
                        Cell 0x600000161080:
                            TextField 0x6000001612c0: identifier: Chapter 1
                            TextField 0x600000161440: identifier: Page 1
                 
OutlineRow 0x600000161500:
                       Cell 0x6000001615c0:
                           TextField 0x600000161680: identifier: Chapter 2
                           TextField 0x600000161740: identifier: Page 1
                  OutlineRow 0x600000161800:
                       Cell 0x6000001618c0: Keyboard Focused
                           TextField 0x600000161980: identifier: Chapter 3
                           TextField 0x600000161980: identifier: Page 1
                           TextField 0x600000161a40: identifier: Page 2

Based on above structure, we have below requests to achieve.
  • Q1: I want to identify the cell which contains the Chapter 1.
  • Q2: I want to identify the TextField which represents the Chapter 1.
  • Q3: I want to identify all the elements which have been disabled.
To solve the problem, the first step is to understand how XCUI identifies elements and which functions we can use in the automation.

Functions for identifying elements

In XCUI, elements are identified using queries. XCUI provides the class XCUIElementQuery for filtering out the objects by conditions. Below are the functions that are used most frequently for identifying elements:

 
/** Returns a new query that applies the specified attributes or predicate to the receiver. The predicate will be evaluated against objects of type id.*/
open func matching(_ predicate: NSPredicate) -> XCUIElementQuery
open func matching(_ elementType: XCUIElementType, identifier: String?) -> XCUIElementQuery
open func matching(identifier: String) -> XCUIElementQuery


/**  Returns a new query for finding elements that contain a descendant matching the specification. The predicate will be evaluated against objects of type id. */
open func containing(_ predicate: NSPredicate) -> XCUIElementQuery
open func containing(_ elementType: XCUIElementType, identifier: String?) -> XCUIElementQuery 
 

The matching function is applying on the current objects to filter out the ones which have the specific identify value, while the containing function looks into the descendants of current objects and returns the ones which have a child that matches the condition.

Sample Code for solving the problem Q1 - Q3

Let's come back to the questions to see how to use the functions above for solving real problems.

> Q1: I want to identify the cell which contains the chapter 1.
This question requires a "cell" element which contains a descendant that can be identified by the string "Chapter 1". The function containning(XCUIElementType, identify String) is used to find the elements that containing a descendant matching the identifying string. The code below will return the Cell 0x600000161080
 
windows["My Book"].cells.containing(.TextField, identifier: "Chapter 1")  
 

> Q2: I want to identify the TextField which represents the Chapter 1.
This question is similar to the first question, and it also uses the identity string to find objects. Instead of looking into the descendants, it evaluates against the elements themselves. We use the function matching(XCUIElementType, identify String) as below.This code will return TextField 0x6000001612c0.
 
windows["My Book"].textFields.matching(.TextField, identifier: "Chapter 1")
 

> Q3: I want to identify all the elements which have been disabled.
This question does not request on any identity strings,  instead it requests elements on element status "Disable". The functions we used to answer the first two questions could not solve this problem anymore. In this case, we have to use the function matching(NSPredicate). Below is the sample code.
 
let disabledPredicates = NSPredicate(format: "isEnabled == false”)
windows[“My Book”].textFields.matching(disabledPredicates)
 

We first created a NSPredicate instance defining the rule that the property "isEnabled" equals to false. Then we used this predicate to find out the elements which exactly matches the predicate. This method is also often used to identify elements by any properties.

Next part, let's see how we use the NSPredicate in the automation and how many properties can we use to identify the elements.

How to use NSPredicate in Automation.

The NSPredicate class is widely used to define logical conditions used to constrain a search either for a fetch or for in-memory filtering.In Cocoa, a predicate is a logical statement that evaluates to a Boolean value (true or false). There are three types of predicates:
  • Simple comparisons, such as grade == 7 or firstName like 'Mark'
  • Case or diacritic insensitive lookups, such as name contains[cd] 'citroen'
  • Logical operations, such as (firstName beginswith 'M') AND (lastName like 'Adderley')
As XCUI encapsulates the UI object to an XCUIElement, the NSPredicate can be used to define any conditions on XCUIElement properties as below
  • exists: Bool
  • isHittable: Bool
  • identifier: String
  • frame: CGRect
  • value: Any?
  • title: String
  • label: String
  • elementType: XCUIElementType
  • isEnabled: Bool
  • horizontableSizeClass: XCUIUserInterfaceSizeClass
  • verticalSizeClass: XCUIUserInterfaceSizeClass
  • placeholderValue: String?
  • isSelected: Bool
  • debugDescription: String
The code for Q3 is just a sample for practicing on identify objects. The NSPredicate can be widely used in any place where you have an expectation on certain conditions. Here is another example for using NSPredicate in my automation. In this function, I use the NSPredicate to evaluate the condition whether we need to wait for an element to show up at the GUI.

 
extension XCTestCase {
    func waitForHittable(element: XCUIElement, waitSeconds: Double) {
        let isHittablePredicate = NSPredicate(format: "isHittable == true")
        expectation(for: isHittablePredicate, evaluatedWith: element, handler: nil)
        
        waitForExpectations(timeout: waitSeconds) { (error) -> Void in
            if (error != nil) {
                let message = "Failed to find element hittable after \(waitSeconds) seconds."
                XCTFail(message)
            }
        }
    }
    
    func waitForExists(element: XCUIElement, waitSeconds: Double) {
        let existsPredicate = NSPredicate(format: "exists == true")
        expectation(for: existsPredicate, evaluatedWith: element, handler: nil)
        
        waitForExpectations(timeout: waitSeconds) { (error) -> Void in
            if (error != nil) {
                let message = "Failed to find element existing after \(waitSeconds) seconds."
                XCTFail(message)
            }
        }
    }
}
 

No comments :

Post a Comment