UI Testing UITableView cell count - ios

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

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.

How to create an XCUIElement query for the last UITableViewCell in a UITableView?

I am creating an XCTestCase. For one of the test methods, call it func testConfirmLastCellContent() { ... }, I want to confirm that there exists a staticText child element having a value of "98765" within a UITableViewCell (I put the text value in in the textLabel.text property of the table view cell). There are 4 table view cells listed above this cell. Each of them has a String value of "12345" for its textLabel.text, except for the second cell, which has the same text value as the last: "98765".
The String values for the textLabel.text property of each cell, then, looks like this:
"12345"
"98765"
"12345"
"12345"
"98765"
These textLabel.text values are the only visual sub-elements within the table view cells respectively.
If I create a simple XCUIElementQuery like so...
let staticTextForLastCell = XCUIApplication().staticTexts["98765"]
...and assert its existence as true like so...
XCTAssert(staticTextForLastCell.exists == true)
...then the test will pass, but how do I know I got the last cell? Obviously, I cannot know from that query, because it could be pulling from the 2nd cell.
Thus far, I cannot find an XCTest class/method to get ahold of the last UITableViewCell specifically and query its details.
My goal is to get the last cell and confirm its textLabel.text value is something I expect. How can I do this?
Note: I cannot add/subtract/change textual, visual, etc. elements on any of the table view cells.
Edit:
I should also note that the UITableViewCells are generated dynamically by the app being tested. So, I cannot tag them or populate the accessibilityIdentifier property for the textLabel or the cell.
My setup is the following:
Operating System: macOS Mojave 10.14.6
IDE: Xcode 10.3
SDK: iOS 12.4 SDK
Deploy Target: iOS 12.0
Language: Swift 5
You should probably add an extension to XCUIElementQuery
extension XCUIElementQuery {
var lastMatch : XCUIElement {return self.element(boundBy: self.count - 1)}
}
Then you can assert your cell like this:
XCTAssertEqual(app.cells.lastMatch.staticTexts.element.label,
"98765",
"Unexpected text of the last cell")
Here is something that works...
let app = XCUIApplication()
let cells = app.cells
let lastCell = cells.element(boundBy: cells.count - 1)
let textOfLastCell = lastCell.staticTexts["98765"]
XCTAssert(textOfLastCell.exists)
Note: Smart Monkey's answer used an extension, which may be advisable. The above is a more inline way.
Edit: I previously stated that I could not get it to work without querying for tables before querying for cells. It does work without the tables query.
Edit: Changed XCTAssert(textOfLastCell.exists == true) to XCTAssert(textOfLastCell.exists) based on comment from Chris. A bit cleaner.

UI Testing swipe-to-delete table view cell

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.

UITableView changing things of some cells in Swift

I have a UITableView with a custom UITalbeCellView. Every cell has a title a body and a date. When I fetch the data some of the titles must be red so there is a Bool that is set to true for those cells which title has to be red.
The first time the data is fetched it looks fine, but as you scroll up and down a few times all of the titles end up being red.
Im using an array of structures to store the data and on the cellForRowAtIndexPath there is an if and if the boolean in that position of the array is true I change the color to red:
let cell = tableView.dequeueReusableCellWithIdentifier("CommentsRowTVC", forIndexPath: indexPath) as! CommentsRowTVC
let single_comment = self.AOS[indexPath.row]
cell.titulo_comentario?.text = single_comment.titulo_comment
if single_comment.flag == true {
cell.titulo_comentario.textColor = UIColor.redColor()
}
Im gessing it reuses Views or something like that. Does this have any easy fix? Or do I have to implement my own TableView to prevent this from happening?
Im also thinking the method dequeueReusableCellWithIdentifier might be the one causing my problem... but Im new on swift and cant think of any replacement for that.
It is happening similar thing on buttons there are on the cells, when you click a button from a cell it changes its color and disables it, but just the one on that cell. However when clicking on a cell, buttons from other cells are also suffering those changes.
Since cells get reused you end up with cells that were previously red because they hadsingle_comment.flag == true and are now used for another row where there actually is single_comment.flag == false. You have to reset the color in the else branch again:
if single_comment.flag == true {
cell.titulo_comentario.textColor = UIColor.redColor()
} else {
cell.titulo_comentario.textColor = UIColor.whiteColor()
}
UITableViews reuse their cells, meaning that you will get cells inside your cellForRowAtIndexPath which have previously been used before, maybe have have set the textColor to red. Your job is it now to revert the color of the text to its original state.
In general whatever change you make inside the if-branch of cell-customization has to be undone in the else-branch. If you remove a label in the if, add it in the else.

How do I set the accessibility label for a particular segment of a UISegmentedControl?

We use KIF for our functional testing, and it uses the accessibility label of elements to determine where to send events. I'm currently trying to test the behaviour of a UISegmentedControl, but in order to do so I need to set different accessibility labels for the different segments of the control. How do I set the accessibility label for a particular segment?
As Vertex said,
obj-c
[[[self.segmentOutlet subviews] objectAtIndex:3] setAccessibilityLabel:#"GENERAL_SEGMENT"];
swift
self.segmentOutlet.subviews[3].accessibilityLabel = "GENERAL_SEGMENT"
some advice so you don't go crazy like I did:
To scroll in accessibility mode swipe three fingers
The indexes of the segments are backwards than you would expect, i.e. the furthest segment to the right is the 0th index and the furthest to the left is the n'th index where n is the number of elements in the UISegmentControl
I'm just getting started with KIF myself, so I haven't tested this, but it may be worth a try. I'm sure I'll have the same issue soon, so I'd be interested to hear if it works.
First, UIAccessibility Protocol Reference has a note under accessibilityLabel that says:
"If you supply UIImage objects to display in a UISegmentedControl, you can set this property on each image to ensure that the segments are properly accessible."
So, I'm wondering if you could set the accessibilityLabel on each NSString object as well and be able to use that to access each segment with KIF. As a start, you could try creating a couple of strings, setting their accessibility labels, and using [[UISegmentedControl alloc] initWithItems:myStringArray]; to populate it.
Please update us on your progress. I'd like to hear how this goes
Each segment of UISegmentedControl is UISegment class instance which subclass from UIImageView. You can access those instances by subviews property of UISegmentedControl and try to add accessibility for them programmatically.
You can't rely on the index in the subviewsarray for the position. For customisation of the individual subviews I sort the subviews on their X Position before setting any propery.What would also be valid for accesibilityLbel.
let sortedViews = self.subviews.sorted( by: { $0.frame.origin.x < $1.frame.origin.x } )
sortedViews[0].accessibilityLabel = "segment_full"
sortedViews[1].accessibilityLabel = "segment_not_full"
This is an old question but just in case anyone else runs up against this I found that the segments automatically had an accessibility label specified as their text. So if two options were added of Option 1 and Option 2. A call to
[tester tapViewWithAccessibilityLabel:#"Option 2"];
successfully selected the segment.
The solutions with using an indexed subview is not working since you cannot rely on a correct order and it will be difficult to change the number of segments. And sorting by origin does not work, since the frame (at least for current versions) seems to be always at x: 0.
My solution:
(segmentedControl.accessibilityElement(at: 0) as? UIView)?.accessibilityLabel = "Custom VoiceOver Label 1"
(segmentedControl.accessibilityElement(at: 1) as? UIView)?.accessibilityLabel = "Custom VoiceOver Label 2"
(segmentedControl.accessibilityElement(at: 2) as? UIView)?.accessibilityLabel = "Custom VoiceOver Label 3"
Seems to work for me and has the correct order. You also do not rely on an image. Not that pretty either but maybe more reliable than other solutions.
This is an old question but just in case anyone else runs up against this I found that the segments automatically had an accessibility label specified as their text.
Further to Stuart's answer, I found it really useful when writing test cases to turn on 'Accessibility Inspector' on the Simulator (Settings -> General -> Accessibility -> Accessibility Inspector). You'd be surprised how many elements already have accessibility labels included, like in the standard iOS UI elements or even third party frameworks.
Note: Gestures will now be different - Tap to view accessibility information, double tap to select. Minimizing the Accessibility Inspector window (by tapping the X button) will return the gestures back to normal.
You guys want to see how Apple recommends it be done?
It's FUGLY.
This is from this example:
func configureCustomSegmentsSegmentedControl() {
let imageToAccessibilityLabelMappings = [
"checkmark_icon": NSLocalizedString("Done", comment: ""),
"search_icon": NSLocalizedString("Search", comment: ""),
"tools_icon": NSLocalizedString("Settings", comment: "")
]
// Guarantee that the segments show up in the same order.
var sortedSegmentImageNames = Array(imageToAccessibilityLabelMappings.keys)
sortedSegmentImageNames.sort { lhs, rhs in
return lhs.localizedStandardCompare(rhs) == ComparisonResult.orderedAscending
}
for (idx, segmentImageName) in sortedSegmentImageNames.enumerated() {
let image = UIImage(named: segmentImageName)!
image.accessibilityLabel = imageToAccessibilityLabelMappings[segmentImageName]
customSegmentsSegmentedControl.setImage(image, forSegmentAt: idx)
}
customSegmentsSegmentedControl.selectedSegmentIndex = 0
customSegmentsSegmentedControl.addTarget(self,
action: #selector(SegmentedControlViewController.selectedSegmentDidChange(_:)),
for: .valueChanged)
}
They apply the accessibility labels to images, and then attach the images. Not too different from the above answer.
another option if not willing to set accesibility label might be calculating the poistion of each segment part and use
[tester tapScreenAtPoint:segementPosition];
to trigger the actions
If you look at the segmented control thru the accessibility inspector, you find that the segments are UISegment objects. Moreover, they turn out to be direct subviews of the UISegmentedControl. That fact suggests the following insanely crazy but perfectly safe Swift 4 code to set the accessibility labels of the segments of a UISegmentedControl:
let seg = // the UISegmentedControl
if let segclass = NSClassFromString("UISegment") {
let segments = seg.subviews.filter {type(of:$0) == segclass}
.sorted {$0.frame.minX < $1.frame.minX}
let labels = ["Previous article", "Next article"] // or whatever
for pair in zip(segments,labels) {
pair.0.accessibilityLabel = pair.1
}
}
As mentioned in the accepted answer, adding accessibilityLabel to the text should do the trick:
let title0 = "Button1" as NSString
title0.accessibilityLabel = "MyButtonIdentifier1"
segmentedControl.setTitle("\(title0)", forSegmentAt: 0)
let title1 ="Button2" as NSString
title1.accessibilityLabel = "MyButtonIdentifier2"
segmentedControl.setTitle("\(title1)", forSegmentAt: 1)
XCode 12 / iOS 14.3 / Swift 5
This is an old post but I encountered the same problem trying to set accessibility hints for individual segments in a UISegmentedControl. I also had problems with some of the older solutions. The code that's currently working for my app borrows from replies such as those from matt and Ilker Baltaci and then mixes in my own hack using UIView.description.
First, some comments:
For my UISegmentedControl with 3 segments, the subview count is 3 in the viewDidLoad() and viewWillAppear() of the parent UIVIewController. But the subview count is 7 in viewDidAppear().
In viewDidLoad() or viewWillAppear() the subview frames aren't set, so ordering the subviews didn't work for me. Apparently Benjamin B encountered the same problem with frame origins.
In viewDidAppear(), the 7 subviews include 4 views of type UIImageView and 3 views of type UISegment.
UISegment is a private type. Working directly with the private API might flag your app for rejection. (see comment below)
type(of:) didn't yield anything useful for the UISegment subviews
(HACK!) UIView.description can be used to check the type without accessing the private API.
Setting accessibility hints based on X order tightly couples UI segment titles and hints to their current positions. If user testing suggests a change in segment order, then changes must be made both in the UI and in the code to set accessibility hints. It's easy to miss that.
Using an enum to set segment titles is an alternative to relying on X ordering set manually in the UI. If your enum inherits from String and adopts the protocols CaseIterable and RawRepresentable, then it's straightforward to create titles from the enum cases, and to determine the enum case from a segment title.
There's no guarantee the following will work in a future release of the framework, given the reliance on description.contains("UISegment") but it's working for me. Gotta move on.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// get only the UISegment items; ignore UIImageView
let segments = mySegmentedControl.subviews.compactMap(
{ $0.description.contains("UISegment") ? $0 : nil }
)
let sortedSegments = segments.sorted(
by: { $0.frame.origin.x < $1.frame.origin.x }
)
for i in 0 ..< sortedSegments.count {
let segment = sortedSegments[i]
// set .accessibilityHint or .accessibilityLabel by index
// or check for a segment title matching an enum case
// ...
}
}
On Private APIs and Rejection
I'm referring to the April 2016 comment from #dan in Test if object is an instance of class UISegment:
It's a private class. You can check it with [...
isKindOfClass:NSClassFromString(#"UISegment")] but that may get your
app rejected for using private api or stop working in the future if
apple changes the internal class name or structure.
Also:
What exactly is a Private API, and why will Apple reject an iOS App if one is used?
"App rejected due to non-public api's": https://discussions.apple.com/thread/3838251
As Vortex said, the array is right to left with [0] starting on the right. You can set every single accessibility option by accessing the subviews. Since the subviews are optional, it is good to pull out the subview first, and then assign the accessibility traits that you want. Swift 4 example for a simple two option segment control:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
guard let rightSegment = segmentControl.subviews.first, let leftSegment = segmentControl.subviews.last else { return }
rightSegment.accessibilityLabel = "A label for the right segment."
rightSegment.accessibilityHint = "A hint for the right segment."
leftSegment.accessibilityLabel = "A label for the left segment."
leftSegment.accessibilityHint = "A hint for the left segment."
}

Resources