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
}
}