39

因此,随着新的 Xcode 更新,Apple 改变了我们进行 UI 测试的方式。在仪器中,我们使用 java 脚本函数“isVisible”来确定我们的目标元素是否可见。

我正在尝试在 Objective-C 中复制它,但我似乎找不到与此等效的东西。我有一个表格视图,一个带有两个标签的原型单元格。这个原型单元可以重复使用 50 次。

我试图滚动直到最后一个单元格可见,我这样做是这样做的:

if (![[[[[[XCUIApplication alloc] init].tables childrenMatchingType:XCUIElementTypeCell] matchingIdentifier:@"cell"] elementBoundByIndex:49].staticTexts[@"text"] exists]) {
        [[[[[[XCUIApplication alloc] init].tables childrenMatchingType:XCUIElementTypeCell] matchingIdentifier:@"cell"] elementBoundByIndex:0].staticTexts[@"text"] swipeUp];
}

但这不会滑动,因为加载视图时元素存在。请帮忙,因为这让我发疯。

4

10 回答 10

58

您应该扩展 XCUIElement 的方法列表。第一个方法 ( scrollToElement:) 将在 tableView 上调用,第二个扩展方法帮助您确定元素是否在主窗口上。

extension XCUIElement {

    func scrollToElement(element: XCUIElement) {
        while !element.visible() {
            swipeUp()
        }
    }

    func visible() -> Bool {
        guard self.exists && !CGRectIsEmpty(self.frame) else { return false }
        return CGRectContainsRect(XCUIApplication().windows.elementBoundByIndex(0).frame, self.frame)
    }

}

滚动代码应如下所示(例如滚动到最后一个单元格):

func testScrollTable() {
    let app = XCUIApplication()
    let table = app.tables.elementBoundByIndex(0)
    let lastCell = table.cells.elementBoundByIndex(table.cells.count-1)
    table.scrollToElement(lastCell)
}

斯威夫特 3:

extension XCUIElement {
    func scrollToElement(element: XCUIElement) {
        while !element.visible() {
            swipeUp()
        }
    }

    func visible() -> Bool {
        guard self.exists && !self.frame.isEmpty else { return false }
        return XCUIApplication().windows.element(boundBy: 0).frame.contains(self.frame)
    }
}
于 2015-11-29T18:50:08.577 回答
21

之前的所有答案都不是 100% 的失败证明。我面临的问题是 swipeUp() 具有更大的偏移量,当我在视口中有元素时,我找不到停止滚动的方法。有时元素会因为过度滚动而被滚动,结果测试用例失败。但是我设法使用以下代码控制滚动。

/**
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)
}

注意:如果您的视图是基于 tableview 的,请使用 app.tables

于 2016-07-24T04:42:35.113 回答
15

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

于 2017-10-26T00:19:11.640 回答
4

在我的情况下,扩展@Kade 的答案scrollToElement必须考虑标签栏,否则如果视图位于标签栏下方,则可能会点击标签栏按钮:

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()
        }
    }
}
于 2016-10-04T20:43:03.670 回答
4

这是我认为是防弹的版本(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 }
    }
}

它考虑到该项目可能找不到(在这种情况下它不应该挂起)。滚动也是按项目大小的步骤执行的,因此搜索元素不会穿过可见区域,这在滑动的情况下是可能的。

于 2018-04-20T15:04:12.520 回答
3

不幸的是.exists,不能确认一个元素当前是可见的——这样的事情仍然不完美,但它将提供更可靠的验证,用于表格或集合视图单元格:

extension XCUIElement {
    var displayed: Bool {
        guard self.exists && !CGRectIsEmpty(frame) else { return false }
        return CGRectContainsRect(XCUIApplication().windows.elementBoundByIndex(0).frame, frame)
    }
}

然后你可以写一个简单的循环,如:

func scrollDownUntilVisible(element: XCUIElement) {
    while !element.displayed {
        swipeDown()
    }
}
于 2015-11-20T00:27:19.877 回答
2

更新@ravisekahrp 对较新 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)
        }
    }
}
于 2019-05-09T08:49:00.983 回答
1

你可以这样做:

extension XCUIElement {
    internal func scrollToElement(element: XCUIElement) {
        while !element.exists {
            swipeDown()
        }
    }
}

而不是使用 scrollToElement 来查找元素

于 2015-10-26T17:15:53.943 回答
0

这里的问题是,在大多数情况下,滑动动量会将所需的单元格滚动到屏幕之外。所以我们最终会在搜索部分出现错误的滑动​​。

尝试滚动到当前表格视图中最后一个可点击单元格之后放置的单元格,并提供所有上述答案。他们将滚动所需的元素,在大多数情况下,我们找不到所需的单元格。

要求:

  • 精确滚动到最后一个可见单元格之后的单元格
  • 如果我们到达表格底部,应该抛出错误或停止滚动

我的解决方案:

public let app = XCUIApplication()

extension XCUIElement {
    
    func scrollTo(_ element: XCUIElement) {
        if self.elementType == .table {
            if element.isHittable { return }
            let lastCell = self.cells.element(boundBy: self.cells.count-1)
            let yOffset = calculatedYOffset()
            
            while !element.isHittable
            {
                if lastCell.isHittable {
                    //Error - Table bottom reached
                }
                
                let start = self.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5))
                let end = start.withOffset(CGVector(dx: 0.0, dy: -yOffset));
                start.press(forDuration: 0.01, thenDragTo: end)
            }
        } else {
            // Error - Only applicable for table views
        }
    }
    
    func calculatedYOffset() -> Double {
        var indexOfLastVisibleCell = 0
        for i in 0...self.cells.count-1 {
            if self.cells.element(boundBy: i).visible() {
                indexOfLastVisibleCell = i
            } else {
                if indexOfLastVisibleCell != 0 { break }
            }
        }
        
        let lastVisibleCellEndYPosition = Double(self.cells.element(boundBy: indexOfLastVisibleCell).frame.maxY)
        let adjustmentYValue = Double(self.cells.firstMatch.frame.minY)
        let screenScale = Double(UIScreen.main.scale)
        
        return (lastVisibleCellEndYPosition-adjustmentYValue)/screenScale
    }
    
    func visible() -> Bool {
        guard self.exists && self.isHittable && !self.frame.isEmpty else { return false }
        return app.windows.element(boundBy: 0).frame.contains(self.frame)
    }
}

现在,这应该可以工作:

App.tables["Bar"].scrollTo(App.cells.staticTexts["Foo"])

优点:

  • 它控制滑动的动量
  • 通知我们是否已到达最后一个单元格

缺点:

  • 第一次,计算部分需要时间检查可命中单元格

注意:此答案仅适用于表格视图和向底部滑动

于 2021-04-29T12:46:38.383 回答
-2

在 swift 4.2 中,如果您的元素存在于表格视图的底部框架或表格视图的顶部框架中,您可以使用此命令向上滚动并向下滚动以查找元素

let app = XCUIApplication()
app.swipeUp()

或者

app.swipeDown()
于 2019-04-29T15:42:36.420 回答