UI Testing swipe-to-delete table view cell - ios

I'm trying to build a UI test in a basic shopping list app that makes sure swiping to delete a table view cell does in fact delete the cell from the table view.
I am running the test code below, but when it comes time to swipe the table view cell left (to display the delete button), it doesn't swipe. It appears that it may be tapping it, but it doesn't swipe. Because of this, the test fails when trying to tap the Delete button because "No matches found for button."
How does one test swipe to delete in a table view?
func testDeletingCell() {
let app = XCUIApplication()
app.navigationBars["ShoppingList.ShoppingListView"].buttons["Add"].tap()
let element = app.otherElements.containing(.navigationBar, identifier:"ShoppingList.AddShoppingListItemView").children(matching: .other).element.children(matching: .other).element.children(matching: .other).element
let textField = element.children(matching: .textField).element(boundBy: 0)
textField.tap()
textField.typeText("abc")
let textField2 = element.children(matching: .textField).element(boundBy: 1)
textField2.tap()
textField2.typeText("123")
app.navigationBars["ShoppingList.AddShoppingListItemView"].buttons["Save"].tap()
let tablesQuery = app.tables
tablesQuery.children(matching: .cell).element(boundBy: 0).staticTexts["123"].swipeLeft()
tablesQuery.buttons["Delete"].tap()
XCTAssert(app.tables.cells.count == 0)
}

Try this new Swift 3 syntax instead:
let tablesQuery = app.tables.cells
tablesQuery.element(boundBy: 0).swipeLeft()
tablesQuery.element(boundBy: 0).buttons["Delete"].tap()

Xcode 12.4 | Swift 5
Delete all cells inside a UITableView in XCTestCase:
let app = XCUIApplication()
app.launch()
//Ensure your UIViewController loaded with data
sleep(5)
let tablesQuery = app.tables["<YourTableViewIdentifier>"].cells
for i in 0..<tablesQuery.allElementsBoundByAccessibilityElement.count{
tablesQuery.element(boundBy: 0).swipeLeft()
//Sometimes the swipeLeft() itself is enough to delete the cell,
//but if it is not use what #Rashwan L suggests
//tablesQuery.element(boundBy: 0).buttons["Delete"].tap()
}
XCTAssertEqual(tablesQuery.allElementsBoundByAccessibilityElement.count, 0)
The reason I don't swipe left and then tap delete is because swipeLeft() was already enough to deleted my cell from the UITableView.
If you want to swipe left on a cell and ensure it won't be deleted use:
swipeLeft(velocity: .slow)
This velocity is a XCUIGestureVelocity and you can set it to .fast, .slow, or its .default.

Related

How do you access a textfield which does not have a label or static text in XCTest?

so I've recently started testing an iOS app with xctest. I'm on a time model view where I would like to change the number in a cell. The number in this cell is the number of days after which the selected time model repeats itself. But I'm unable to access this textfield and change the number as it does not have a label / name / static text. When I record a tap on this field, Xcode gives me a strange element hierarchy which I've defined as the parameter 'onDay' below
func testRepetitionTypeMonthsOnDay() throws {
let app = XCUIApplication()
let tablesQuery = app.tables
let cellsQuery = tablesQuery.cells
XCTAssertTrue(app.navigationBars["Zeitmodelle"].waitForExistence(timeout: standardTimeout)) //wait for the time model view to open
app.staticTexts["Wiederholend"].firstMatch.tap() //tapping on a cell
XCTAssertTrue(app.navigationBars["Wiederholend"].waitForExistence(timeout: standardTimeout)) // wait for the cell to open
let repetitionType = app.tables.cells["Am, Am Tag, Expand"] //cell 1 from screenshot
let onDay = tablesQuery.children(matching: .cell).element(boundBy: 7).children(matching: .other).element(boundBy: 1).children(matching: .other).element.children(matching: .textField).element //cell 2
let endDate = app.tables.cells["Endet, Endet nicht, Expand"] // cell 3 from screenshot
onDay.tap()
onDay.clearAndEnterText("5")
}
However, Xcode cannot find the parameter onDay that itself has generated in the previous step, tap on it and enter a new text. I've attached a screenshot of the app with this cell here. The cells above and below the marked cell can be identified easily and the assertions for their existence work. The marked cell, however, is a different matter as it does not have a label / static text. Do you have an idea how I can get around this? Thanks a lot!
So I've finally found a way to access this element. Instead of going into the cell view I've accessed the textfields array from tables view.
func testRepetitionTypeMonthsOnDay() throws {
let app = XCUIApplication()
let tablesQuery = app.tables
XCTAssertTrue(app.navigationBars["Zeitmodelle"].waitForExistence(timeout: standardTimeout)) //wait for the time model view to open
app.staticTexts["Wiederholend"].firstMatch.tap() //tapping on a cell
XCTAssertTrue(app.navigationBars["Wiederholend"].waitForExistence(timeout: standardTimeout)) // wait for the cell to open
let onDay = tablesQuery.textFields.element(boundBy: 3)
onDay.tap()
onDay.clearAndEnterText("5")
}
Because the app had a mixture of picker wheels and textfields in the same view, it was a bit problematic for Xcode to find the element I wanted from a general element index. But the use of the textfields array filtered the index list down to only textfields. Xcode was then able to identify the element without any conflict and perform the test. I hope this helps anyone having the same problem.

UI Testing UITableView cell count

I'm trying to verify that a table view has been filled with a certain number of cells in my UI test. Here is my code:
XCUIApplication().tables.element.swipeUp()
let count = XCUIApplication().tables.element.children(matching: .cell).count
XCTAssert(count > 0)
The assert fails because the count is always 0, even though the swipe up scrolls the obviously successfully filled table view. I have also tried:
XCTAssert(app.tables.cells.count > 0)
with the same results. I even created an empty project with a table view with 3 static cells, in just one screen, to remove any other possible distractions for the UI test, and it still always returns 0. I'm testing on iOS 11 with Xcode 9.
Cells don't always register as cells, depending on how you have your accessibility configured. Double check that accessibility is actually seeing cells in Xcode menu -> Open Developer Tool -> Accessibility Inspector.
It's likely what accessibility is seeing is staticTexts instead of cells, depending on your layout code -- in that case, you should assert
XCTAssert(app.tables.staticTexts.count > 0)
If you want cells, configure your cells like so in tableView:cellForItemAtIndexPath:
cell.isAccessibilityElement = true
cell.accessibilityLabel = cell.titleLabel.text // (for example)
cell.accessibilityIdentifier = "MyCellId" // (or whatever you want XCUITest to identify it as)
This will mark the cell as the root accessible item, rather than each of the cell's subviews being discrete elements.
XCUIApplication().tables.children(matching: .cell).count
Deletes all rows from the table view. Written using Xcode 11.1 and tested using SwiftUI.
func deleteAllCellsFromTable() {
let app = XCUIApplication()
let tablesQuery = app.tables
app.navigationBars["Navigation Bar Title"].buttons["Edit"].tap()
while app.tables.children(matching: .cell).count > 0 {
let deleteButton = tablesQuery.children(matching: .cell).element(boundBy: 0).buttons["Delete "]
deleteButton.tap()
let deleteButton2 = tablesQuery.children(matching: .cell).element(boundBy: 0).buttons["trailing0"]
deleteButton2.tap()
}
}

ui testing xcode, how to tap on a table view cell button with cellquery

I am currently doing ui tests for my app and stuck on clicking on the log in button of my app. I am unable to find the element which i have gave the identifier signup button(the element is the 3rd by index, this is not the issue).
let cellQuery = self.app.tables.cells.element(boundBy: 3)
let signInButton = cellQuery.buttons["signup button"]
if signInButton.exists {
signInButton.tap()
}
If the button is present in the 3rd cell then it should be:
let cellQuery = self.app.tables.cells.element(boundBy: 2)
cellQuery.buttons["signup button"].tap()
If button is 3rd in the cell then add accessibility for the cells and then:
app.cells["AccessibilityIdentifier"].buttons.element(boundBy: 2).tap()
Can be done like this:
XCUIApplication().tables.cells.element(boundBy: 2).buttons["signup button"].tap()

Voting buttons with Parse in Swift

A quick overview of what I'm trying to make. I'm making an app to show my film reviews. I have a PFQueryTableviewController of which I use a custom cell. This table shows films which come from my Parse database. In each cell, there a 3 UIButtons, of which I want the user to use to vote for the film (happyVote, OkVote, sadVote). Over the top of each button is a UILabel that simply displays a count.
How I want it to work
When a user presses one of the buttons, the vote increases by 1.
When a user presses the same button again, the vote decreases by 1. Or,
If the user had pressed a different button, the vote decreases on the first button and increases on the button just pressed.
The user can only ever vote on one of the buttons.
The vote is shown by the UILabel showing the count, and by the button image changing.
See below for a visual:
This is what I've added in Parse:
So far, I've added the code to increase the vote count in Parse, in my TableViewController.swift:
#IBAction func upVoteButton(sender: AnyObject) {
let hitPoint = sender.convertPoint(CGPointZero, toView: self.tableView)
let hitIndex = self.tableView.indexPathForRowAtPoint(hitPoint)
let object = objectAtIndexPath(hitIndex)
object!.incrementKey("UpVoteCount")
object!.saveInBackground()
self.tableView.reloadData()
}
This kind of works, except, the user can keep increasing the count, and they can vote on all 3 buttons. And it doesn't change the Button Image when pressed.
In my cellForRowAtIndexPath method, I've put:
let downVote = object!.valueForKey("DownVoteCount") as! Int
cell.downVoteLabel.text = "\(downVote)"
let middleVote = object!.valueForKey("MiddleVoteCount") as! Int
cell.middleVoteLabel.text = "\(middleVote)"
let upVote = object!.valueForKey("UpVoteCount") as! Int
cell.upVoteLabel.text = "\(upVote)"
I have searched for a while for some examples of how to figure the rest out, but can't find anything, and I'm really struggling to figure the next steps out.
Any help will be greatly appreciated, and please let me know if you need/want to see anymore of my code. Thanks
In your upVoteButton() method, you can add the other two buttons and set them button.enabled = false inside an if statement like:
if downVoteButton.enabled == true {
downVoteButton.enabled = false
} else if downVoteButton.enabled == false {
downVoteButton.enabled = True
}
And do the same with the middleVoteButton in antoher if statement or the same, whatever floats your boat, also within the other button methods with the appropriate buttons. This helps disables the buttons when pressed and then enables it when it's pressed again.
And for the image changing you can follow this.

iOS 13 UIContextualAction completion triggers unexpected keyboard dismissal

I have a UITableView with cells that have swappable UIContextualActions to delete or rename (edit) individual cell's TextFields.
Since I made the switch to Swift 5 / iOS 13, triggering the rename UIContextualAction on these cells causes the keyboard to launch and instantly dismiss before the user has a chance to type. Not only does the keyboard go away, the particular cell I'm trying to edit becomes completely empty, and the following warning gets generated:
[Snapshotting] Snapshotting a view (0x10c90a470, _UIReplicantView) that has not been rendered at least once requires afterScreenUpdates:YES.
Below is the code for the rename UIContextualAction:
let actionRename = UIContextualAction(style: .normal, title: "") { (action, view, completionHandler) in
let cell = self.tableLayers.cellForRow(at: indexPath) as! LayerUITableViewCell
cell.layerTitle.isEnabled = true // enable UITextField editing
cell.layerTitle.becomeFirstResponder() // launch keyboard
cell.layerTitle.selectedTextRange = cell.layerTitle.textRange(from: (cell.layerTitle.beginningOfDocument), to: (cell.layerTitle.endOfDocument)) // select all text
completionHandler(true)
} // end of let actionRename
I'm guessing the animation of the UIContextual action is somehow triggering the keyboard's resignFirstResponder.
To summarize, prior to swift 5/iOS 13, the order of events went something like this:
user swipes cell left/right
user hits UIContextual button
cell returns to center
text gets selected
keyboard launches
user types, hits return
keyboard resignFirstResponder
Whereas the behavior I'm seeing after the migration looks like this:
user swipes cell left/right
user hits UIContextual button
text gets selected
keyboard launches
cell returns to center (which somehow triggers resignFirstResponder)
keyboard resignFirstResponder
Update 2019/10/02
I have confirmed that it's the cell animation that is causing the premature keyboard dismissal. If I introduce a delay after the completionHandler as follows:
let actionRename = UIContextualAction(style: .normal, title: "") { (action, view, completionHandler) in
completionHandler(true)
self.perform(#selector(self.layerRenameDos), with: nil, afterDelay: 1.0)
// layerRenameDos has the editing/firstResponder code from above
} // end of let actionRename
With this change, the cell animates back to center, keyboard launches, and I'm able to type away. This, however, is obviously a hacky work-around. Any suggestions would be appreciated
I think this is a better solution. Just be sure to execute your code on the next runloop.
CATransaction.begin()
CATransaction.setCompletionBlock {
DispatchQueue.main.async {
//your code on the next runloop after the animation has finished
}
}
complitionHandler(true)
// or tableView.setEditing(false, animated: true)
CATransaction.commit()

Resources