【问题标题】:Scroll until element is visible iOS UI Automation with Xcode 7滚动直到元素可见 iOS UI Automation with Xcode 7
【发布时间】:2015-12-15 06:42:37
【问题描述】:

因此,通过新的 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];
}

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

【问题讨论】:

    标签: ios objective-c xcode-ui-testing


    【解决方案1】:

    您应该扩展 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)
        }
    }
    

    【讨论】:

    • 你是个魔术师!!
    • 此解决方案可以滚动到所需的单元格:(
    • 我不相信visible 方法是严格要求的,你不能使用isHittable 属性中的构建来做同样的事情吗?
    • 我尝试了所有解决方案,但没有奏效。我在 table view 上使用包含然后 firstMatch 方法找到 staticText 。单元格位于第一个位置,但表格视图似乎已完全加载。可见功能在查找窗口失败。有什么建议吗?我在 iOS 14 中使用 Xcode 12.4
    • 这个方法假定元素在滚动视图的下方。如果它更高呢?
    【解决方案2】:

    之前的所有答案都不是 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

    【讨论】:

    • Swift3 更新 let startCoord = XCUIApplication().collectionViews.element.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5))\n let endCoord = startCoord .withOffset(CGVector(dx: 0.0, dy: -262))\n startCoord.press(forDuration: 0.01, thenDragTo: endCoord)
    • 谢谢。如果您在视图中间直接有一个按钮,则正常的滑动手势不起作用。所以我不得不改变滚动位置,但不知道怎么做。这个解决方案非常酷,但是如果有少量框架可见,视图滚动就会停止。我会考虑整个框架的可见性的解决方案。但是谢谢你:)
    • 太棒了!我在这里尝试了几乎所有的答案,这个是有效的,谢谢!
    • 最后一部分CGRectContainsRect 对我不起作用,但无论如何也没有必要。刚把支票改成self.exists && self.hittable
    • 我喜欢这个概念,但我提供了更多改进的版本。
    【解决方案3】:

    使用swipeUp()swipeDown() 的解决方案并不理想,因为它们可能会由于滑动的动量而滚动超过目标元素。经过多次搜索和挫折,我在XCUICoordinate找到了一个神奇的方法:

    func press(forDuration duration: TimeInterval, thenDragTo otherCoordinate: XCUICoordinate)
    

    所以我们可以这样做:

    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)
    

    就检查某物是否可见而言,您希望将isHittableexists 结合使用。在下面的扩展中查看scrollDownToElement

    这是一个方便的扩展,它会滚动直到一个元素出现在屏幕上,然后将该元素滚动到屏幕顶部:)

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

    通过添加scrollUp 方法了解here

    【讨论】:

    • 使用 xcode >= 9.1 的唯一工作解决方案,tnx 用于共享
    【解决方案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 }
        }
    }
    

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

    【讨论】:

    • 你如何使用这个?你能举个例子吗?
    • 请注意这是extenisonXCUIElement。因此,您必须找到要滑动的元素,然后在其上调用swipeOnIt。它会滑动直到满足关闭条件。无法提供完整的示例,因为我已经一年没有使用 Swift 编码了。
    • 知道了!出于某种原因,我可以向下滚动此代码示例以查看实际扩展并不明显。感谢回复!
    【解决方案5】:

    @Kade's answer 上展开,就我而言,必须考虑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()
            }
        }
    }
    

    【讨论】:

      【解决方案6】:

      不幸的是,.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()
          }
      }
      

      【讨论】:

      • 你可以把显示变成计算属性
      【解决方案7】:

      更新到@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)
              }
          }
      }
      

      【讨论】:

        【解决方案8】:

        你可以这样做:

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

        而不是使用 scrollToElement 来查找元素

        【讨论】:

        • 小修正:将“exists”替换为“hittable”。这对我有用。
        • 检查我的版本。如果找不到元素,这将导致测试挂起(测试应该失败)。也有可能在滑动搜索的元素会穿过可见区域。
        • 下面有一些更准确的解决方案,因为您不想检查它是否可命中但实际上是否显示
        【解决方案9】:

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

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

        要求:

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

        我的解决方案:

        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"])
        

        优点:

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

        缺点:

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

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

        【讨论】:

          【解决方案10】:

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

          let app = XCUIApplication()
          app.swipeUp()
          

          app.swipeDown()
          

          【讨论】:

          • 这假设您的表格视图使用整个屏幕。
          猜你喜欢
          • 1970-01-01
          • 2016-12-09
          • 2019-03-22
          • 1970-01-01
          • 2013-10-01
          • 2010-11-27
          • 2016-01-19
          • 2023-03-17
          • 1970-01-01
          相关资源
          最近更新 更多