So with the new xcode update apple has revamped the way we do UI testing. In instruments we used java script function \"isVisible\" to determine if our targeted element is visib
Expanding on @Kade's answer, in my case, had to account for tabbar in scrollToElement
, else might get a tabbar button tapped if the view was under the tabbar:
func scrollToElement(element: XCUIElement) {
while !element.visible() {
swipeUp()
}
// Account for tabBar
let tabBar = XCUIApplication().tabBars.element(boundBy: 0)
if (tabBar.visible()) {
while element.frame.intersects(tabBar.frame) {
swipeUp()
}
}
}
Solutions using swipeUp()
and swipeDown()
are not ideal because they can potentially scroll past the target element due to the momentum of the swipe. After much searching and frustration I found a magical method on XCUICoordinate
:
func press(forDuration duration: TimeInterval, thenDragTo otherCoordinate: XCUICoordinate)
So we can do something like:
let topCoordinate = XCUIApplication().statusBars.firstMatch.coordinate(withNormalizedOffset: .zero)
let myElement = XCUIApplication().staticTexts["My Element"].coordinate(withNormalizedOffset: .zero)
// drag from element to top of screen (status bar)
myElement.press(forDuration: 0.1, thenDragTo: topCoordinate)
As far as checking whether something is visible goes, you want to use isHittable
in conjunction with exists
. see scrollDownToElement
in the extension below
Here's a handy extension that will scroll until an element is on screen and then scroll that element to the top of the screen :)
extension XCUIApplication {
private struct Constants {
// Half way accross the screen and 10% from top
static let topOffset = CGVector(dx: 0.5, dy: 0.1)
// Half way accross the screen and 90% from top
static let bottomOffset = CGVector(dx: 0.5, dy: 0.9)
}
var screenTopCoordinate: XCUICoordinate {
return windows.firstMatch.coordinate(withNormalizedOffset: Constants.topOffset)
}
var screenBottomCoordinate: XCUICoordinate {
return windows.firstMatch.coordinate(withNormalizedOffset: Constants.bottomOffset)
}
func scrollDownToElement(element: XCUIElement, maxScrolls: Int = 5) {
for _ in 0..<maxScrolls {
if element.exists && element.isHittable { element.scrollToTop(); break }
scrollDown()
}
}
func scrollDown() {
screenBottomCoordinate.press(forDuration: 0.1, thenDragTo: screenTopCoordinate)
}
}
extension XCUIElement {
func scrollToTop() {
let topCoordinate = XCUIApplication().screenTopCoordinate
let elementCoordinate = coordinate(withNormalizedOffset: .zero)
// Adjust coordinate so that the drag is straight up, otherwise
// an embedded horizontal scrolling element will get scrolled instead
let delta = topCoordinate.screenPoint.x - elementCoordinate.screenPoint.x
let deltaVector = CGVector(dx: delta, dy: 0.0)
elementCoordinate.withOffset(deltaVector).press(forDuration: 0.1, thenDragTo: topCoordinate)
}
}
Gist over here with added scrollUp
methods
All the previous answers are not 100% fail proof. The problem I was facing is that swipeUp() has a larger offset and I couldn't find a way to stop the scrolling when I have the element in view port. Sometimes the element gets scrolled away because of the excessive scroll and as a result test case fails. However I managed to control the scroll using the following piece of code.
/**
Scrolls to a particular element until it is rendered in the visible rect
- Parameter elememt: the element we want to scroll to
*/
func scrollToElement(element: XCUIElement)
{
while element.visible() == false
{
let app = XCUIApplication()
let startCoord = app.collectionViews.element.coordinateWithNormalizedOffset(CGVector(dx: 0.5, dy: 0.5))
let endCoord = startCoord.coordinateWithOffset(CGVector(dx: 0.0, dy: -262));
startCoord.pressForDuration(0.01, thenDragToCoordinate: endCoord)
}
}
func visible() -> Bool
{
guard self.exists && self.hittable && !CGRectIsEmpty(self.frame) else
{
return false
}
return CGRectContainsRect(XCUIApplication().windows.elementBoundByIndex(0).frame, self.frame)
}
Note : Please use app.tables if your view is tableview based
Update to @ravisekahrp's answer for newer Swift:
extension XCUIElement {
func isVisible() -> Bool {
if !self.exists || !self.isHittable || self.frame.isEmpty {
return false
}
return XCUIApplication().windows.element(boundBy: 0).frame.contains(self.frame)
}
}
extension XCTestCase {
func scrollToElement(_ element: XCUIElement) {
while !element.isVisible() {
let app = XCUIApplication()
let startCoord = app.tables.element.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5))
let endCoord = startCoord.withOffset(CGVector(dx: 0.0, dy: -262))
startCoord.press(forDuration: 0.01, thenDragTo: endCoord)
}
}
}
Here is my version which I think is bullet proof (swift 4.0):
import XCTest
enum TestSwipeDirections {
case up
case down
case left
case right
}
fileprivate let min = 0.05
fileprivate let mid = 0.5
fileprivate let max = 0.95
fileprivate let leftPoint = CGVector(dx: min, dy: mid)
fileprivate let rightPoint = CGVector(dx: max, dy: mid)
fileprivate let topPoint = CGVector(dx: mid, dy: min)
fileprivate let bottomPoint = CGVector(dx: mid, dy: max)
extension TestSwipeDirections {
var vector: (begin: CGVector, end: CGVector) {
switch self {
case .up:
return (begin: bottomPoint,
end: topPoint)
case .down:
return (begin: topPoint,
end: bottomPoint)
case .left:
return (begin: rightPoint,
end: leftPoint)
case .right:
return (begin: leftPoint,
end: rightPoint)
}
}
}
extension XCUIElement {
@discardableResult func swipeOnIt(_ direction: TestSwipeDirections,
swipeLimit: Int = 6,
swipeDuration: TimeInterval = 1.0,
until: () -> Bool) -> Bool {
XCTAssert(exists)
let begining = coordinate(withNormalizedOffset: direction.vector.begin)
let ending = coordinate(withNormalizedOffset: direction.vector.end)
var swipesRemaining = swipeLimit
while !until() && swipesRemaining > 0 {
begining.press(forDuration: swipeDuration, thenDragTo: ending)
swipesRemaining = swipesRemaining - 1
}
return !until()
}
@discardableResult func swipeOnIt(_ direction: TestSwipeDirections,
swipeLimit: Int = 6,
swipeDuration: TimeInterval = 1.0,
untilHittable element: XCUIElement) -> Bool {
return swipeOnIt(direction, swipeLimit: swipeLimit, swipeDuration: swipeDuration) { element.isHittable }
}
@discardableResult func swipeOnIt(_ direction: TestSwipeDirections,
swipeLimit: Int = 6,
swipeDuration: TimeInterval = 1.0,
untilExists element: XCUIElement) -> Bool {
return swipeOnIt(direction, swipeLimit: swipeLimit, swipeDuration: swipeDuration) { element.exists }
}
}
It take into account that item may not be found (in this case it should not hang). Also scroll is performed in steps of size of the item so search element will not pass through visible area what is possible in case of swipe.
Unfortunately .exists
doesn't confirm that an element is currently visible - something like this still isn't perfect but it will provide more reliable validation working with table or collection view cells:
extension XCUIElement {
var displayed: Bool {
guard self.exists && !CGRectIsEmpty(frame) else { return false }
return CGRectContainsRect(XCUIApplication().windows.elementBoundByIndex(0).frame, frame)
}
}
then you can write a simple loop like:
func scrollDownUntilVisible(element: XCUIElement) {
while !element.displayed {
swipeDown()
}
}