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

5

14

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

Gow answered 1/10, 2015 at 15:31 Comment(0)
G
0

Looks like this is a known bug :-(

https://forums.developer.apple.com/thread/9934

Gow answered 2/10, 2015 at 8:24 Comment(0)
F
19

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!

Swift 5 Update

func elementIsWithinWindow(element: XCUIElement) -> Bool {
    guard element.exists && !element.frame.isEmpty && element.isHittable else { return false }
    return XCUIApplication().windows.element(boundBy: 0).frame.contains(element.frame)
}

func scrollDown(times: Int = 1) {
    let mainWindow = app.windows.firstMatch
    let topScreenPoint = mainWindow.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.05))
    let bottomScreenPoint = mainWindow.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.90))
    for _ in 0..<times {
        bottomScreenPoint.press(forDuration: 0, thenDragTo: topScreenPoint)
    }
}

func scrollUp(times: Int = 1) {
    let mainWindow = app.windows.firstMatch
    let topScreenPoint = mainWindow.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.05))
    let bottomScreenPoint = mainWindow.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.90))
    for _ in 0..<times {
        topScreenPoint.press(forDuration: 0, thenDragTo: bottomScreenPoint)
    }
}  
    
func scrollUntilElementAppears(element: XCUIElement, threshold: Int = 10) {
    var iteration = 0

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

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

    while !elementIsWithinWindow(element: element) {
        guard iteration > 0 else { break }
        scrollUp()
        iteration -= 1
    }
}
Fathead answered 5/11, 2015 at 6:48 Comment(2)
This works, but I had to get the main window using a different approach: func mainWindow() -> XCUIElement { return XCUIApplication().windows.elementBoundByIndex(0) } Also, what you named 'scrollUp' would more traditionally be named 'scrollDown'Praise
good points isoiphone! that XCUIApplication.mainWindow() was an extension I forgot to add to this code sample, i've edited the answer to be more complete and swapped scrollUp and scrollDown for clarity.Fathead
S
6

What i had to do to address this problem is to actually swipe up or down in my UI testing code. Have you tried this?

XCUIApplication().swipeUp()

Or you can also do WhateverUIElement.swipUp() and it will scroll up/down with respect to that element.

Hopefully they will fix the auto scroll or auto find feature so we don't have to do this manually.

Stillborn answered 25/10, 2015 at 8:50 Comment(1)
Hi Narine. Yeah I do the swipeUp() and down, but no way of knowing when the element is in view.. :-( As you say hopefully they will come up with a way to get round this in the future.Gow
H
4

You should check isHittable property.

If view is not hidden, the corresponding XCUIElement is hittable. But there is a caveat. "View 1" can be overlapped by "View 2", but the element corresponding to "View 1" can be hittable.

Hafiz answered 28/3, 2016 at 10:18 Comment(1)
Apple docs say that when an element is covered by another, isHittable will be false. Ref: developer.apple.com/documentation/xctest/xcuielement/…Superphosphate
U
2

Since you have some XCUIElements in the bottom of the tableview (table footer view), the way of scrolling the tableview all the way to the bottom in the UI test, supposing your tableview has a lot data, is by tap().

.swipeUp() also does the job but the problem is when your test data is huge, it takes forever to swipe, as oppose to .tap() which directly jumps to the bottom of the tableView.

More specially:

XCUIElementsInTheBottomOrTableFooterView.tap()
XCTAssert(XCUIElementsInTheBottomOrTableFooterView.isHittable, "message") 
Unperforated answered 21/9, 2017 at 16:32 Comment(0)
G
0

Looks like this is a known bug :-(

https://forums.developer.apple.com/thread/9934

Gow answered 2/10, 2015 at 8:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.