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.
Related
I am having a no-show menuController and I have checked all of the suggestions in previous questions. It turns out the imageView I have implemented a UILongPressGestureRecognizer on, to show the menu, is returning False on calling .becomeFirstResponder just before setting up the menu controller.
I am coding in swift 4 and can't figure out how to make the imageView return True to calling .becomeFirstResponder. Help!
/*********************************************************/
override func viewDidLoad() {
super.viewDidLoad()
// long tap to show menu that enables deletion of the image.
imageView_1.isUserInteractionEnabled = true
let longPressRecogniser = UILongPressGestureRecognizer(target: self, action: #selector(longPressOnImage(_:)))
//longPressRecogniser.numberOfTapsRequired = 1
//longPressRecogniser.numberOfTouchesRequired = 1
longPressRecogniser.minimumPressDuration = 0.5
imageView_1.addGestureRecognizer(longPressRecogniser)
imageView_1.image = placeHolderImage_1
imageView_2.image = placeHolderImage_2
}
/*********************************************************/
#IBAction func longPressOnImage(_ gestureRecognizer: UILongPressGestureRecognizer) {
print(#function)
if gestureRecognizer.state == .began {
//print("gestureRecognizer.state == .began")
self.tappedView = gestureRecognizer.view!
if tappedView.canResignFirstResponder {
print("can resign first responder")
}
if tappedView.becomeFirstResponder() {
print("returned TRUE to becomeFirstResponder")
} else {
print("returned FALSE to becomeFirstResponder")
}
// Configure the shared menu controller
let menuController = UIMenuController.shared
// Configure the menu item to display
// Create a "delete" menu item
let deleteImage = UIMenuItem(title: "Delete", action: #selector(deleteImage_1))
menuController.menuItems = [deleteImage]
// Set the location of the menu in the view.
let location = gestureRecognizer.location(in: tappedView)
print("location = ", location)
let menuLocation = CGRect(x: location.x, y: location.y, width: 2, height: 2)
menuController.setTargetRect(menuLocation, in: tappedView)
//update the menu settings to force it to display my custom items
menuController.update()
// Show the menu.
menuController.setMenuVisible(true, animated: true)
print("menu should be visible now")
}
}
/*********************************************************/
#objc func deleteImage_1() {
print(#function)
}
My caveman debugging print statements output:
longPressOnImage
can resign first responder
returned FALSE to becomeFirstResponder
location = (207.0, 82.0)
menu should be visible now
Create a custom imageView class and override "canBecomeFirstResponder" property like this:
class ResponsiveImage : UIImageView{
override var canBecomeFirstResponder: Bool{
return true
}
}
Use this ResponsiveImage type and your code will work :)
Thank you to adri. Your answer is the solution to my problem.
I had read in other posts to similar questions about overriding var canBecomeFirstResponder but either overlooked it or it wasn't made explicit that a custom UIImageView class needs to be created.
Just to make it clear to newbies like me, the class of the imageView in storyBoard and its #IBOutlet in its viewController must typed as ResponsiveImage. If only one of these is changed a type casting error is reported.
Many thanks for ending my hours of frustration! :-)
I am working on UITests using XCode. I have multiple CollectionView cells.
When I perform Count in the collectionView it shows the certain count.
I can able to access first two cells but coming to the 3rd cell as 3(depends on device size). It says that specific button I am looking for in 3rd Cell as exists.
But isHittable is false.
Is there any way I can tap on the button on the 3rd Cell.
I have tried using the extension for forceTapElement() which is available online, it didn’t help.
Extension Used:
extension XCUIElement{
func forceTapElement(){
if self.isHittable{
self.tap()
}else{
let coordinate: XCUICoordinate = self.coordinate(withNormalizedOffset: .zero)
coordinate.tap()
}
}
}
Tried to perform swipeUp() and access the button. it still shows isHittable as false
The only way I've found is to swipe up untile the isHittable will be true.
app.collectionViews.cells.staticTexts["TEST"].tap()
Thread.sleep(forTimeInterval: 3)
let collectionView = app.otherElements.collectionViews.element(boundBy: 0)
let testAds = collectionView.cells
let numberOfTestAds = testAds.count
if numberOfTestAds > 0 {
let tester = collectionView.cells.element(boundBy: 2).buttons["ABC"]
for _ in 0..<100 {
guard !tester.isHittable else {
break;
}
collectionView.swipeUp()
}
}
Please note that the swipeUp() method will only move few pixels. If you want to use more comprehensive methods you can get AutoMate library and try swipe(to:untilVisible:times:avoid:from:):
app.collectionViews.cells.staticTexts["TEST"].tap()
Thread.sleep(forTimeInterval: 3)
let collectionView = app.otherElements.collectionViews.element(boundBy: 0)
let testAds = collectionView.cells
let numberOfTestAds = testAds.count
if numberOfTestAds > 0 {
let tester = collectionView.cells.element(boundBy: 2).buttons["ABC"]
collectionView.swipe(to: .down, untilVisible: tester)
// or swipe max 100 times in case 10 times is not enough
// collectionView.swipe(to: .down, untilVisible: tester, times: 100)
}
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.
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
How can you programmatically make sure that the cursor of a tableView-HeaderView-TextField gets active (i.e. is the first responder) ??
My table looks like this (i.e with the custom TextField header). So far, the cursor only gets inside the grey header field by clicking inside the textfield. But I would like to be able to get the cursor inside the textfield programmatically....
The code for my custom tableview-header looks like this :
// drawing a custom Header-View with a TextField on top of the tableView
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let container = UIView(frame: CGRectMake(0, 0, self.view.frame.size.width, 50))
let textField = UITextField(frame: CGRectMake(10, 15, self.view.frame.size.width/2 - 40, 45))
textField.delegate = self
self.txtfield = textField
textField.textColor = UIColor.blackColor()
let placeholder = NSAttributedString(string: "..add player", attributes: [NSForegroundColorAttributeName: UIColor.darkGrayColor()])
textField.attributedPlaceholder = placeholder
textField.backgroundColor = UIColor.lightGrayColor()
container.addSubview(textField)
var headPlusBttn:UIButton = UIButton.buttonWithType(UIButtonType.ContactAdd) as! UIButton
headPlusBttn.center.x = self.view.frame.size.width - 20
headPlusBttn.center.y = 38
headPlusBttn.enabled = true
headPlusBttn.addTarget(self, action: "addTeam:", forControlEvents: UIControlEvents.TouchUpInside)
container.addSubview(headPlusBttn)
return container
}
My first approach was to set the first-responder of the headerViewForSection like this (see code):
// reload entries
func reloadEntries() {
self.tableView.reloadData()
// the following does unfortunately not work !!!!!
self.tableView.headerViewForSection(1)?.becomeFirstResponder()
}
Not sure why this does not work. Maybe, the Section-Nr (Int=1) is wrong. But I tried several section-numbers. No curser where it should be.
Any help appreciated !
Usually adding a delay helps in situations like this. It allows the OS to do everything it wants with the view, and then it won't mess up what you're trying to do at the same time.
Maybe something like this:
func reloadEntries() {
self.tableView.reloadData()
let delay = (Int64(NSEC_PER_SEC) * 0.1)
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, triggerTime), dispatch_get_main_queue(), { () -> Void in
self.tableView.headerViewForSection(1)?.becomeFirstResponder()
})
}
I haven't tested this to do what you want, so you may need to find a different place to put this.
Also, are you sure you want to affect the view in section 1? From your image it looks like you want to mess with the header in section 0.
Be sure to drop into the debugger and check that the header isn't nil. Your code implies that that's a valid condition. You might try writing it like this (at least for testing):
if let header = self.tableView.headerViewForSection(1) {
header.becomeFirstResponder()
}
else {
print("There is no header.")
}
Try
self.tableView.headerViewForSection(1)?.textfield.becomeFirstResponder()