This article covers some convenience methods I wrote to handle UITableViewCells in UI tests. As pointed out in my earlier post about NSAttributedStrings in XCUITest, I have recently begun being serious about automated UI tests for Shoptimizer. And since Shoptimizer heavily relies on UITableViewCells for showing lists, items, recipes and so on, I often have to tap, swipe or just find UITableViewCells in my automated tests. 

For the impatient: Just scroll down to the end of the article to find the final outcome. 😉

Before I start with the actual explanation, remember that XCUITest makes use of the accessibility features of iOS and that, therefore, the tests cannot actually access properties of the underlying UITableViewCells. Actually, you do not even handle instance of UITableViewCell in XCUITest. Instead you are given instances of type XCUIElement, which is much more general and has no direct connection to any UI type. You will see later how this works.

Now imagine that you have a list of UITableViewCells in your app, all showing different contents. For Shoptimizer this could be the details of a shopping list, showing the items you want to buy and their amounts. The question now is how you can find one particular cell within your UI test in order to perform some action on it.

Well, first of all, you can access the list of all currently accessible cells by the simple code let cells = XCUIApplication().cells. This way you have a constant of type XCUIElementQuery, which is the basis for the further filtering of the cells. Now, what information do you have present when testing that helps you filter this list? Exactly, only what is written into the UITableViewCell: The label. But how do you find a cell that has a particular label? For this requirement, there is a function called containing(_ predicate: NSPredicate -> XCUIElementQuery that you call on your cells constant. If, for instance, I want to find a list item Sugar, I would write the following code: cells.containing(NSPredicate(format: "label CONTAINS %@", "Sugar")). As you can see in the function definition above, this results in another XCUIElementQuery. Now, if you are absolutely certain that there is only this one cell with the given label, you can use the element property of XCUIElementQuery to access the only cell with the given label. This way the code let searchedCell = cells.containing(NSPredicate(format: "label CONTAINS %@", "Sugar")).element now saves that one desired cell into the constant named searchedCell. But as a reminder, be aware that searchedCell is not of type UITableViewCell, rather it is of type XCUIElement.

But what if there are two items Sugar on the shopping list? How can you distinguish between them? You need more information, of course. Let’s assume that the first occurrence of Sugar has an amount of 500 g and the second one has 1.5 kg. This will mean that the according cells have different overall labels. The first solution that came to my mind was to extend the predicate like so:  cells.containing(NSPredicate(format: "label CONTAINS %@ AND label CONTAINS %@", "Sugar", "500 g")), for finding the Sugar with 500 g to buy. But for some reason this did not work when I tried it. If you know a way how to make this work, please leave a comment and tell me. Another way that does actually work is to chain several calls of containing together. This is valid since the cells constant and the containing function both yield an XCUIElementQuery. On that query you can perform another query. With this in mind the solution would be to write the following piece of code: let searchedCell = cells.containing(NSPredicate(format: "label CONTAINS %@", "Sugar")).containing(NSPredicate(format: "label CONTAINS %@", "500 g")).element. This way the searchedCell constant now contains the cell where the label contains both strings, Sugar as well as 500 g; if there actually is only one cell satisfying that condition, otherwise an error will be raised.

Using that basis, a function can be designed to chain arbitrarily many conditions together for narrowing down the search for the right cell. That could look like this:


func filterCells(containing labels: String..., inApp app: XCUIApplication) -> XCUIElementQuery {
    var cells = app.cells
    for label in labels {
        cells = cells.containing(NSPredicate(format: "label CONTAINS %@", label))
    }
    return cells
}

Great, we now have a function that chains conditions together for each label. But in this scenario we’d still have to pass the XCUIApplication reference around, which is just not that great, right? We can change that by adding the function to an extension of the XCUIApplication type, resulting in something like the following:


extension XCUIApplication {
   func filterCells(containing labels: String...) -> XCUIElementQuery {
        var cells = self.cells

        for label in labels {
            cells = cells.containing(NSPredicate(format: "label CONTAINS %@", label))
        }
        return cells
    }
}

If we now wanted to find the cell with the 500 grams of sugar we would write this:


let app = XCUIApplication()
let searchedCell = app.filterCells(containing: "Sugar", "500 g").element

And that’s it. Much simpler and cleaner than chaining the conditions in place within the test method and repeating the same code over and over. But it gets even better, since now we can also add methods to return the specific searched cell, tap or swipe it, and check if the cell exists in the first place. Since I don’t want to steal any more of your time (this is a rather long article after all) I now present to you the full suite of methods that I have come up with until now. Enjoy.


extension XCUIApplication {

    private func filterCells(containing labels: [String]) -> XCUIElementQuery {
        var cells = self.cells

        for label in labels {
            cells = cells.containing(NSPredicate(format: "label CONTAINS %@", label))
        }
        return cells
    }

    func cell(containing labels: String...) -> XCUIElement {
        return filterCells(containing: labels).element
    }

    func cell(containing labels: [String]) -> XCUIElement {
        return filterCells(containing: labels).element
    }

    func tapCell(containing labels: String...) {
        cell(containing: labels).tap()
    }

    func swipeCellLeft(containing labels: String...) {
        cell(containing: labels).swipeLeft()
    }

    func swipeCellRight(containing labels: String...) {
        cell(containing: labels).swipeRight()
    }

    func existsCell(containing labels: String...) -> Bool {
        return cell(containing: labels).exists
    }
}

As variadic parameters (the String... part) are internally handled as arrays, we need an overloaded version of the method cell that accepts a String array. This set of functions could, for example, also be rewritten to find static texts appearing on screen. Please, feel free to use these methods in your testing code, extend them or rewrite them however you wish. As usual I am looking forward to reading any of your helpful thoughts, additions or corrections (or congratulations 😉 ) in the comments section under this article.

Happy testing!

Advertisements