UITesting Xcode 7: How to tell if XCUIElement is visible?

Charlie Seligman picture Charlie Seligman · Oct 1, 2015 · Viewed 9.7k times · Source

I am automating an app using UI Testing in Xcode 7. I have a scrollview with XCUIElements (including buttons, etc) all the way down it. Sometimes the XCUIElements are visible, sometimes they hidden too far up or down the scrollview (depending on where I am on the scrollview).

Is there a way to scroll items into view or maybe tell if they are visible or not?

Thanks

Answer

Tucker Sherman picture Tucker Sherman · Nov 5, 2015

Unfortunately Apple hasn't provided any scrollTo method or a .visible parameter on XCUIElement. That said, you can add a couple helper methods to achieve some of this functionality. Here is how I've done it in Swift.

First for checking if an element is visible:

func elementIsWithinWindow(element: XCUIElement) -> Bool {
    guard element.exists && !CGRectIsEmpty(element.frame) && element.hittable else { return false }
    return CGRectContainsRect(XCUIApplication().windows.elementBoundByIndex(0).frame, element.frame)
}

Unfortunately .exists returns true if an element has been loaded but is not on screen. Additionally we have to check that the target element has a frame larger than 0 by 0 (also sometimes true) - then we can check if this frame is within the main window.

Then we need a method for scrolling a controllable amount up or down:

func scrollDown(times: Int = 1) {
    let topScreenPoint = app.mainWindow().coordinateWithNormalizedOffset(CGVector(dx: 0.5, dy: 0.05))
    let bottomScreenPoint = app.mainWindow().coordinateWithNormalizedOffset(CGVector(dx: 0.5, dy: 0.90))
    for _ in 0..<times {
        bottomScreenPoint.pressForDuration(0, thenDragToCoordinate: topScreenPoint)
    }
}

func scrollUp(times: Int = 1) {
    let topScreenPoint = app.mainWindow().coordinateWithNormalizedOffset(CGVector(dx: 0.5, dy: 0.05))
    let bottomScreenPoint = app.mainWindow().coordinateWithNormalizedOffset(CGVector(dx: 0.5, dy: 0.90))
    for _ in 0..<times {
        topScreenPoint.pressForDuration(0, thenDragToCoordinate: bottomScreenPoint)
    }
}  

Changing the CGVector values for topScreenPoint and bottomScreenPoint will change the scale of the scroll action - be aware if you get too close to the edges of the screen you will pull out one of the OS menus.

With these two methods in place you can write a loop that scrolls to a given threshold one way until an element becomes visible, then if it doesn't find its target it scrolls the other way:

func scrollUntilElementAppears(element: XCUIElement, threshold: Int = 10) {
    var iteration = 0

    while !elementIsWithinWindow(element) {
        guard iteration < threshold else { break }
        scrollDown()
        iteration++
    }

    if !elementIsWithinWindow(element) { scrollDown(threshold) }

    while !elementIsWithinWindow(element) {
        guard iteration > 0 else { break }
        scrollUp()
        iteration--
    }
}

This last method isn't super efficient, but it should at least enable you to find elements off screen. Of course if you know your target element is always going to be above or below your starting point in a given test you could just write a scrollDownUntil or a scrollUpUntill method without the threshold logic here. Hope this helps!