XCUIElement – Discovery

In XCUITest automation, I arrange test code in using a Screen Object model pattern. This is based on the web’s Page Object model. First, there is a group of classes which model screens on an app. Second, a group of classes contain test case functions that rely on the screen classes for instances of screen elements and functions that perform actions on the screen in use for the test. Elements are modeled by the class named XCUIElement.

A screen class contains references to related screens, queries for screen elements and common functions. Common functions include simple wrappers like getLoginButton() which return an element query and getRow(atIndex: Int) which returns row at an index from a table.

Skeletal Screen Object Example

class DemoScreen {
    var app:                XCUIApplication
    var openSafariButton:   XCUIElement

    init(testApp: XCUIApplication) {
        self.app = testApp
        openSafariButton = app.buttons[“safari-button”]
    }
}

In the example above, the bare minimum code is shown for the sort of screen class I frequently implement for my XUCITest automation projects. Below, I illustrate the portions of the class.

var app:    XCUIApplication

This instance variable is key as it is the root element for the screen representing this class. Queries for elements will take advantage of this variable. The variables at the top of the class are not defined as constants since they must get initialized in the init() function.

var openSafariButton:   XCUIElement

This is a declaration for an element on the screen. An element is modeled by the XCUIElement class.

init(testApp: XCUIApplication) {
    self.app = testApp
    openSafariButton = app.buttons[“safari-button”]
}

The init() function is required so that class variables get initialized. The right side of the element assignment is an XCUIElementQuery which uses the variable named app. The query relies on a unique identifier for a button.

My recommendation is to always use a unique identifier wherever possible. This guards against UI changes that break the query and do not invalidate the UI element. If an accessibility identifier is unavailable for an element then there are ways to add one. I will explore these ways in a future post.

XCUIElementQuery

There are several ways that can be used to query for an element on the screen. First, the data source for identifying elements is an element tree. In the test function, testSafariButton(), a line with print(app.debugDescription)will print the output shown second in the test log.

Skeletal Test Class

import XCTest

class DemoAutoUITests: XCTestCase {

    // MARK: - Properties
    let app = XCUIApplication()

    var demoScreen: DemoScreen!

    let safariApp = XCUIApplication(bundleIdentifier: "com.apple.mobilesafari")

    // MARK: - Lifecycle
    override func setUp() {
        super.setUp()
        demoScreen = DemoScreen(testApp: app)

        continueAfterFailure = false
        app.launch()
    }

    override func tearDown() {
        super.tearDown()
    }

    // MARK: - Tests
    func testOpenSafari() {
        let timeout = 15.0

        // prints the Element Tree
        print(app.debugDescription)

        demoScreen.openSafariButton.tap()
        XCTAssertTrue(safariApp.wait(for: .runningForeground, timeout: timeout), "Safari app is not in foreground")
    }
}

Element Tree Logged by print(app.debugDescription

Attributes: Application, pid: 83644, label: 'DemoAuto'
Element subtree:
 →Application, 0x600001da08f0, pid: 83644, label: 'DemoAuto'
    Window (Main), 0x600001da0750, {{0.0, 0.0}, {414.0, 896.0}}
      Other, 0x600001da05b0, {{0.0, 0.0}, {414.0, 896.0}}
        Button, 0x600001da0820, {{22.0, 80.0}, {136.0, 30.0}}, identifier: 'show-text-button', label: 'Show Label'
        StaticText, 0x600001da0a90, {{22.0, 129.0}, {136.0, 21.0}}, identifier: 'show-text-label'
        Button, 0x600001da0b60, {{22.0, 226.0}, {136.0, 30.0}}, label: 'Open Settings'
        Button, 0x600001da0c30, {{22.0, 264.0}, {136.0, 30.0}}, identifier: 'open-safari-button', label: 'Open Safari'
    Window, 0x600001da0d00, {{0.0, 0.0}, {414.0, 896.0}}
      StatusBar, 0x600001da0dd0, {{0.0, 0.0}, {414.0, 44.0}}
        Other, 0x600001da0ea0, {{0.0, 0.0}, {414.0, 44.0}}
          Other, 0x600001da0f70, {{0.0, 0.0}, {414.0, 44.0}}
            Other, 0x600001da1040, {{8.0, -4.0}, {197.0, 11.5}}
            Other, 0x600001da1110, {{210.0, -4.0}, {197.0, 11.5}}
            Other, 0x600001da11e0, {{8.0, 19.0}, {197.0, 11.5}}
            Other, 0x600001da12b0, {{16.5, 14.5}, {70.5, 16.0}}
              StaticText, 0x600001da1380, {{34.5, 15.5}, {34.5, 19.5}}, label: '7:45 AM'
            Other, 0x600001da1450, {{23.0, 10.0}, {61.0, 22.0}}
            Other, 0x600001da1520, {{210.0, 19.0}, {197.0, 11.5}}
            Other, 0x600001da15f0, {{326.5, 14.5}, {70.5, 16.0}}
              Other, 0x600001da16c0, {{326.5, 19.0}, {18.5, 11.5}}, label: 'Cellular', value: No signal
              Other, 0x600001da1790, {{349.5, 19.0}, {16.5, 11.5}}, identifier: '3 of 3 Wi-Fi bars', value: SSID, 3 of 3 Wi-Fi...
              Other, 0x600001da1860, {{370.5, 18.5}, {26.5, 12.5}}, label: '98% battery power', value: Charging
            Other, 0x600001da1930, {{326.5, 14.5}, {70.5, 16.0}}
          Other, 0x600001da1a00, {{8.5, 24.5}, {81.5, 13.5}}
Path to element:
 →Application, pid: 83644, label: 'DemoAuto'
Query chain:
 →Find: Target Application 'com.joeferrara.DemoAuto'
  Output: {
    Application, pid: 83644, label: 'DemoAuto'
  }

An easy way to see the element tree from Xcode is to set a breakpoint in your code after the line which prints the debugDescription property.

Test at breakpoint right after printing the element tree to the system console

Using data derived from the element tree, we can see identifiers, labels and positions of elements on the screen. In this example we have three buttons and one label (a label here is a staticText).

Shown below are three of the ways we can locate an element using an instance of XCUIElementQuery.

        // Located by Accessibility Identifier
        labelStaticText = app.staticTexts["show-text-label"].firstMatch
    
        // Located by Label
        showLabelButton = app.buttons["Show Label"].firstMatch

        // Located by Position
        let secondButtonOnStoryBoard = 1
        openSettingsButton = app.buttons.element(boundBy: secondButtonOnStoryBoard)

I prefer to use an accessibility identifier when querying for an element. Identifiers are most often unique to a screen and have less of a chance of changing than other methods. A label can easily be refined from release to release and a screen position can change if either the external design changes or internal code makes it so that the order of elements changes.

When we can guarantee that there is only one element on the screen with a particular identifier then it is beneficial to use the firstMatch property so that the query engine stops searching for matches on the query.

There are additional functions which are used to match elements including use of direct children elements, descendants, match by identifier, plus ways to identify an element with wildcard matching.

Ways to Learn about Screen Elements

I have shown one way to find the screen elements using print(app.debugDescription)in test code. Below is a list of other ways to find the elements on a screen. I plan to get into more detail for some of these methods in future blog posts.

  • Xcode Test Recorder
  • Include po print(app.debugDescription)in a test function
  • Xcode debugger command – po print ( app.debugDescription )
  • Examine story board or XIB resource for a screen
  • Xcode Accessibility Inspector

Thank you for reading. I welcome your feedback and comments for this post.