UITesting Xcode 7: How to tell if XCUIElement is visible? - swift

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

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

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.

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.

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

Looks like this is a known bug :-(
https://forums.developer.apple.com/thread/9934

Related

How to solve SceneKit Renderer "EXC_BAD_ACCESS (code=1, address=0xf000000010a10c10)"?

Here is the error I am getting, check the attached picture for more info.
com.apple.scenekit.scnview-renderer (17): EXC_BAD_ACCESS (code=1, address=0xf000000010a10c10)
Here is a log of the error:
I can reproduce this error when I call the following function but only if this function gets called many times within a second. This would happen If the user rapidly taps the button to cycle to the next available car.
As you can see, I tried wrapping this in a DispatchQueue to solve my problem.
You'll also notice that I created a Bool alreadyCyclingCars to track whether the cycleCarNext() function is finished before allowing it to be called again.
This function essentially iterates through all the unlockedCars in the unlockedCars array.
If the type of car matches the one we are currently looking for, I break the loop.
Then we determine the index of the current car to see whether the next car I need to show is the next one in the array (if there is one) if not, we have arrived at the end of the array so I show the first car in the array.
Does anyone know more than I do about this?
Would really be appreciated thank you!
func cycleCarNext() {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
if !self.alreadyCyclingCars {
self.alreadyCyclingCars = true
var indexOfCurrentCar = 0
for (index, car) in UserData.shared.unlockedCars.enumerated() {
if car.type == self.overlayScene.currentCarOnDisplay {
indexOfCurrentCar = index
break
}
}
if indexOfCurrentCar < UserData.shared.unlockedCars.count - 1 {
let nextCar = UserData.shared.unlockedCars[indexOfCurrentCar+1]
self.playerNode.removeFromParentNode()
self.playerNode = nextCar
self.playerNode.name = "player"
self.playerNode.position = SCNVector3(x: 17, y: 0.3, z: 0)
self.playerNode.eulerAngles = SCNVector3(x: 0, y: toRadians(angle: 45),z: 0)
self.scene.rootNode.addChildNode(self.playerNode)
self.overlayScene.currentCarOnDisplay = nextCar.type
self.overlayScene.updateGarageInterface()
} else {
guard let nextCar = UserData.shared.unlockedCars.first else { return }
self.playerNode.removeFromParentNode()
self.playerNode = nextCar
self.playerNode.name = "player"
self.playerNode.position = SCNVector3(x: 17, y: 0.3, z: 0)
self.playerNode.eulerAngles = SCNVector3(x: 0, y: toRadians(angle: 45),z: 0)
self.scene.rootNode.addChildNode(self.playerNode)
self.overlayScene.currentCarOnDisplay = nextCar.type
self.overlayScene.updateGarageInterface()
}
self.alreadyCyclingCars = false
}
}
}
It's my experience that those kind of errors occur when you attempt to modify SceneKit's scene graph (add/remove nodes, etc) outside the SCNSceneRendererDelegate delegate methods.
Imagine you have one thread that is performing rendering at 60fps, and another (eg; the main thread) that removes a node from what is to be rendered. At some point the rendering thread is going to be part way through rendering when what it is rendering is removed by the other thread. This is when the EXC_BAD_ACCESS occurs. The more times the scene is modified, the more likely you are to see this conflict, hence why button mashing could more readily reproduce the issue.
The fix is to only modify your scene in one of SCNSceneRendererDelegate delegate methods. I'd try something like...
func cycleCarNext() {
self.cycleNextCar = true
}
func renderer(renderer: SCNSceneRenderer, updateAtTime time: NSTimeInterval) {
if (self.cycleNextCar) {
self.doCycleNextCar()
self.cycleNextCar = false
}
}
func doCycleNextCar() {
var indexOfCurrentCar = 0
for (index, car) in UserData.shared.unlockedCars.enumerated() {
if car.type == self.overlayScene.currentCarOnDisplay {
indexOfCurrentCar = index
break
}
}
if indexOfCurrentCar < UserData.shared.unlockedCars.count - 1 {
let nextCar = UserData.shared.unlockedCars[indexOfCurrentCar+1]
self.playerNode.removeFromParentNode()
self.playerNode = nextCar
self.playerNode.name = "player"
self.playerNode.position = SCNVector3(x: 17, y: 0.3, z: 0)
self.playerNode.eulerAngles = SCNVector3(x: 0, y: toRadians(angle: 45),z: 0)
self.scene.rootNode.addChildNode(self.playerNode)
self.overlayScene.currentCarOnDisplay = nextCar.type
self.overlayScene.updateGarageInterface()
} else {
guard let nextCar = UserData.shared.unlockedCars.first else { return }
self.playerNode.removeFromParentNode()
self.playerNode = nextCar
self.playerNode.name = "player"
self.playerNode.position = SCNVector3(x: 17, y: 0.3, z: 0)
self.playerNode.eulerAngles = SCNVector3(x: 0, y: toRadians(angle: 45),z: 0)
self.scene.rootNode.addChildNode(self.playerNode)
self.overlayScene.currentCarOnDisplay = nextCar.type
self.overlayScene.updateGarageInterface()
}
}
cycleCarNext is to be called by your main thread as it currently is. You'll likely need to set the SCNView's delegate somewhere too (eg; sceneView.delegate = self)
The idea is that while the cycleCarNext boolean is set immediately in the main thread, the scene isn't changed. Instead the change occurs at the correct time/thread in the SceneKit rendering loop.
Faced with the same problem and figured out that this crash caused by personSegmentationWithDepth on devices with LiDAR
if ARWorldTrackingConfiguration.supportsFrameSemantics(.personSegmentationWithDepth) {
configuration.frameSemantics.insert(.personSegmentationWithDepth)
}
I've been struggling with this error for a while, and I just wanted to add a note in case it helps anyone using SCNRenderer, which is that I was using
.render(withViewport:commandBuffer:passDescriptor:)
but this does not call the delegate render method. Instead, use
.render(atTime:viewport:commandBuffer:passDescriptor:)
even if you are not using the time interval parameter, and then the delegate method renderer(_:updateAtTime:) will be called, where you can make the scene updates safely.
If "GPU Frame Capture" is enabled in your scheme disabling it fixes this issue:
In Xcode go to your current scheme -> select »Edit Scheme…« -> Run/Options: set »GPU Frame Capture« to Disabled.
Taken from here.

Uitest refresh control swift

I am trying to add uitest for refresh control but I could not have done it because of I can't get access to refresh control with accessibility identifier (refreshControl.accessibilityIdentifier = "refreshControlList")
launchAppWithArgs(json: OrdersResultJSON.ok, xcuiApp: app)
let table = app.tables["agendaTable"]
DispatchQueue.main.async {
table.otherElements["sectionHeader0"].press(forDuration: 2, thenDragTo: table.otherElements["sectionHeader2"])
}
if !table.otherElements[""].waitForExistence(timeout: 6) {
print("XXX")
}
Any suggestion to test it?
To perform a pull to refresh, simply get the first cell and drag it down:
let firstCell = app.tables["agendaTable"].cells.firstMatch
let start = firstCell.coordinate(withNormalizedOffset: CGVector(dx: 0, dy: 0))
let finish = firstCell.coordinate(withNormalizedOffset: CGVector(dx: 0, dy: 10))
start.press(forDuration: 0, thenDragTo: finish)
Hope this helps!
You could grab the UIRefreshControl's title label and put an identifier on it, like this:
func setSubviewAccessibility(for tableView: UITableView) {
guard let titleLabel = tableView.refreshControl?.subviews.first?.subviews.last as? UILabel else {
return
}
titleLabel.isAccessibilityElement = true
titleLabel.accessibilityIdentifier = "refresh_control_label"
}
Call this method passing the table view that you've set the UIRefreshControl for and you should be good to go.
Then, on the test side:
let refreshControl = XCUIApplication().staticTexts["refresh_control_label"]
guard refreshControl.waitForExistence(timeout: 5) else {
return XCTFail("Refresh control label not found.")
}
The only problem remaining is you'll probably need the list loading to take a little longer than a second so that your tests don't miss the UIRefreshControl. It's always good to use waitForExistence.

How to get current texture from current frame of animation in SKAction?

I'm trying to animate something that spins / left to right, but whenever I call
spinLeft() or spinRight() then the animation always starts from frame 0.
In other words, I want to be able to say spin something 4 out of 10 frames, stop, then
spin in the opposite direction, FROM frame 4. Right now, it resets to frame 0.
var textures = [SKTexture]() // Loaded with 10 images later on.
var sprite = SKSpriteNode()
func spinLeft() {
let action = SKAction.repeatForever(.animate(with: textures, timePerFrame: 0.1))
sprite.run(action)
}
func spinRight() {
let action = SKAction.repeatForever(.animate(with: textures, timePerFrame: 0.1)).reversed()
sprite.run(action)
}
You could do this (Syntax may be a little off, but you get the point):
The key here is the .index(of: ... ) which will get you the index.
func spinUp() {
let index = textures.index(of: sprite.texture)
if index == textures.count - 1 {
sprite.texture = textures[0]
}
else {
sprite.texture = textures[index + 1]
}
}
func spinDown() {
let index = textures.index(of: sprite.texture)
if index == 0 {
sprite.texture = textures[textures.count - 1]
}
else {
sprite.texture = textures[index - 1]
}
}
func changeImage(_ isUp: Bool, _ amountOfTime: CGFloat) {
let wait = SKAction.wait(duration: amountOfTime)
if isUp {
run(wait) {
self.imageUp()
}
}
else {
run(wait) {
self.imageDown()
}
}
}
If you use something like a swipe gesture recognizer, you can use it's direction to set the isUp Bool value and the velocity of that swipe for the amountOfTime for the changeImage function.
The changeImage will only change the image once, so you will need to handle this somewhere else, or create another function if you want it to continuously spin or die off eventually.
Hope this helps!

scrolltoitematindexpath always returns to the same position

I'm trying to implement adding new portion of information at the top, when user is scrolling to the top (to see chat history) without any jumping like in Telegram, Whatsup and so on
Here is my method
private var toItem: NSIndexPath?
private func addMoreCells () {
if collectionView.contentOffset.y <= 0 {
guard let userChatUid = user?.chatUid else { return }
guard let ownChatUid = UsersManager.sharedInstance.currentChatUID() else { return }
guard let offset = conversation?.messages.count else { return }
ConversationsManager.sharedInstance.getConversationsWithMoreMessagesFor(ownChatUid, recipient: userChatUid, offset: offset - 1, successHandler: { [weak weakSelf = self] (conversations) in
dispatch_async(dispatch_get_main_queue()) {
if let messagesFromServer = conversations.first?.messages, var messages = weakSelf?.conversation?.messages {
for message in messagesFromServer {
if !messages.contains(message) {
messages.append(message)
}
}
messages = messages.sort({$0.time?.compare($1.time!) == .OrderedAscending})
weakSelf?.conversation?.messages = messages
weakSelf?.collectionView.reloadData()
weakSelf?.toItem = NSIndexPath(forItem: (messages.count - messagesFromServer.count), inSection: 0)
if let item = weakSelf?.toItem{
weakSelf?.collectionView.scrollToItemAtIndexPath(item, atScrollPosition: .Top, animated: false)
}
}
}
}, failHandler: { (error) in
print(error)
})
}
}
which is launched from
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
addMoreCells()
}
First time when I'm scrolling to the top It works as I expect (new portion is added, no jumping. I can see new data and can continue scrolling to the top), but when I continue scrolling to the top and new data are added, it always returns to the position, when first info was added.
But I expect It should return to value messages.count - messagesFromServer.count. In xCode I see numbers are changed every time when I'am scrolling, but scrollToItemAtIndexPath doesn't work properly
And Every time when I scroll to the top and new data are added, It always returns to first position when first info was added.
Can Anyone know what's problem? Cause I cant find solutions to fix it.
I've solved this problem like this:
After reloadData() I save contentSize of collection view, after that I refresh layout, get new contentSize and subtract them to get needed offset to scroll to this place without jumping.
weakSelf?.collectionView.reloadData()
guard let previousContentSize = weakSelf?.collectionView.contentSize else { return }
weakSelf?.collectionView.collectionViewLayout.invalidateLayout()
weakSelf?.collectionView.collectionViewLayout.prepareLayout()
weakSelf?.collectionView.layoutIfNeeded()
guard let newContentSize = weakSelf?.collectionView.contentSize else { return }
print("previous Content Size \(previousContentSize) and new Content Size \(newContentSize)")
let contentOffset = newContentSize.height - previousContentSize.height
weakSelf?.collectionView.setContentOffset(CGPoint(x: 0.0, y: contentOffset), animated: false)
I works great and as I would expect.

UIScrollView with "Circular" scrolling

I am trying to make "Circular" scrolling in my UIScrollView, but unsuccessful.
What I want to do:
if uiscrollview reaches end, it should move to start
if uiscrollview at start and moving back, it should move to end
Appending scrollview isn't good way in my situation (other methods should get "page id")
Have you any ideas?
I've implemented this method, but it requires paging enabled. Lets assume you have five elements A,B,C,D and E. When you set up your view, you add the last element to the beginning and the first element to the end, and adjust the content offset to view the first element, like this E,[A],B,C,D,E,A. In the UIScrollViewDelegate, check if the user reach any of the ends, and move the offset without animation to the other end.
Imagine the [ ] indicates the view being shown:
E,A,B,C,[D],E,A
User swipes right
E,A,B,C,D,[E],A
User swipes right
E,A,B,C,D,E,[A]
Then, automatically set the content offset to the second element
E,[A],B,C,D,E,A
This way the user can swipe both ways creating the illusion of an infinite scroll.
E,A,[B],C,D,E,A
Update
I've uploaded a complete implementation of this algorithm. It's a very complicated class, because it also has on-click selection, infinite circular scroll and cell reuse. You can use the code as is, modify it or extract the code that you need. The most interesting code is in the class TCHorizontalSelectorView.
Link to the file
Enjoy it!
Update 2
UICollectionView is now the recommended way to achieve this and it can be used to obtain the very same behavior. This tutorial describes in details how to achieve it.
Apple has a Street Scroller demo that appears to have exactly what you want.
There's also a video from WWDC 2011 that demos their demo. ;) They cover infinite scrolling first in the video.
Try to use following code..
Sample code..
- (void)scrollViewDidEndDecelerating:(UIScrollView *)sender
{
if (scrollView.contentOffset.x == 0) {
// user is scrolling to the left from image 1 to image n(last image).
// reposition offset to show image 10 that is on the right in the scroll view
[scrollView scrollRectToVisible:CGRectMake(4000,0,320,480) animated:NO];// you can define your `contensize` for scrollview
}
else if (scrollView.contentOffset.x == 4320) {
// user is scrolling to the right from image n(last image) to image 1.
// reposition offset to show image 1 that is on the left in the scroll view
[scrollView scrollRectToVisible:CGRectMake(320,0,320,480) animated:NO];
}
}
Hope, this will help you...
With reference from #redent84, find the code logic.
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated)
scrollView.contentSize = CGSize(width: scrollView.frame.width * CGFloat(imagesArray.count), height: scrollView.frame.height)
scrollView.isPagingEnabled = true
// imagesArray.insert(imagesArray.last as? UIImage, at: 0)
// imagesArray.insert(imagesArray.first as? UIImage, at: imagesArray.count - 1)
var testArr = ["A", "B", "C", "D"]
let firstItem = testArr.first
let lastItem = testArr.last
testArr.insert(lastItem as! String, at: 0)
testArr.append(firstItem as! String)
print(testArr)
for i in 0..<imagesArray.count{
let imageview = UIImageView()
imageview.image = imagesArray[i]
imageview.contentMode = .scaleAspectFit
imageview.clipsToBounds = true
let xPosition = self.scrollView.frame.width * CGFloat(i)
imageview.frame = CGRect(x: xPosition, y: 0, width: self.scrollView.frame.width, height: self.scrollView.frame.height)
print(imageview)
scrollView.addSubview(imageview)
print("Imageview frames of i \(i) is \(imageview.frame)")
}
newStartScrolling()
}
func newStartScrolling()
{
ViewController.i += 1
let x = CGFloat(ViewController.i) * scrollView.frame.size.width
scrollView.setContentOffset(CGPoint(x: x, y: 0), animated: true)
print("X is \(x) and i is \(ViewController.i)")
if ViewController.i == imagesArray.count - 1 {
ViewController.i = 0
let x = CGFloat(ViewController.i) * scrollView.frame.size.width
//scrollView.setContentOffset(CGPoint(x: x, y: 0), animated: false)
print("X (rotate) is \(x) and i is \(ViewController.i)")
scrollView.contentOffset.x = x
self.newStartScrolling()
}
}
func backScrolling() {
ViewController.i -= 1
let x = CGFloat(ViewController.i) * scrollView.frame.size.width
scrollView.setContentOffset(CGPoint(x: x, y: 0), animated: true)
print("X is \(x) and i is \(ViewController.i)")
if ViewController.i <= 0 {
ViewController.i = imagesArray.count - 1
let x = CGFloat(ViewController.i) * scrollView.frame.size.width
scrollView.setContentOffset(CGPoint(x: x, y: 0), animated: false)
print("X (rotate) is \(x) and i is \(ViewController.i)")
self.backScrolling()
//scrollView.contentOffset.x = x
return
}
}