I am implementing UITests for my iOS app.
So far, I've been able to do some simple testing, but I've come to a tableView where there are two sections. Each section has a sectionHeaderView containing static text, eg. "SECTION 1" and "SECTION 2", in normal sectionHeader style.
When performing app.tables.staticTexts["SECTION 1"].exists it returns true. This is the first section, visible at the very top when the view loads.
When performing the same, but for "SECTION 2", it returns false. The sectionHeaderView for this section is outside the view at this point, so I thought this was the problem, but it turns out it's not..
I tried app.swipeUp(), which successfully brings the second section into the screen. After the swipeUp i sleep for a few seconds for the view to settle, and I perform the same check, but it simply cannot find the second sectionView.
After scrolling down, I have tried to print out app.tables.staticTexts.debugDescription to see what it can find, and it only shows the first section as well as a tableFooterView I have at the very bottom of the tableView.
At the time I perform app.tables.staticTexts["SECTION 2"].exists I can literally see the "SECTION 2" text on the simulator. Yet it does not exist to the test.
Why is my second sectionHeaderView completely invisible to XCTest? Could it be that I have disabled some sort of accessibility-variable on this view in particular? I can't find anything..
Edit, output:
t = 32.25s Find: Descendants matching type Table
t = 32.26s Find: Descendants matching type StaticText
t = 32.26s Find: Elements matching predicate '"SECTION 1" IN identifiers'
Found SECTION 1. Will scroll down to find Section 2.
t = 32.26s Swipe up Target Application 0x6080000bbf60
t = 32.26s Wait for app to idle
t = 32.30s Find the Target Application 0x6080000bbf60
t = 32.30s Snapshot accessibility hierarchy for my.bundle.identifier
t = 33.09s Wait for app to idle
t = 33.14s Synthesize event
t = 33.42s Wait for app to idle
Slept for 3 seconds. Have scrolled down. SECTION 2 in view now.
t = 38.86s Snapshot accessibility hierarchy for my.bundle.identifier
t = 39.64s Find: Descendants matching type Table
t = 39.65s Find: Descendants matching type StaticText
t = 39.65s Find: Elements matching predicate '"SECTION 2" IN identifiers'
t = 39.66s Assertion Failure: MyUITests.swift:347: XCTAssertTrue failed - SECTION 2 does not exist
t = 39.66s Tear Down
Place a breakpoint in your code at:
app.tables.staticTexts["SECTION 2"].exists
When you hit the breakpoint type this into the debug panel and hit enter:
po print(XCUIApplication().debugDescription)
This will list out everything that is available to XCUITest. Look for your Section 2 text in there. Often times when this happens to me, I spelled it wrong or the text in the app has an extra space somewhere. When using .staticText it has to match EXACTLY.
I ran into this problem for table footers. It seems like they are treated as "other" objects, not staticTexts so the following code should work:
XCTAssert(app.otherElements["SECTION 2"].exists)
Thanks to h.w.powers for the debugging tip:
po print(XCUIApplication().debugDescription)
A couple of questions, since I can't comment yet: (I've been doing a LOT of UI testing recently, so I'm learning a little)
What happens when you try to assert app.tables.staticTexts["SECTION 2"].exists after swiping up, and disregarding the "SECTION 1" label?
Is sectionHeaderView a custom subclass?
I've found it's helpful to assign specific views an accessibilityIdentifier and then access them via the XCUIElement proxy with that identifier.
Also, something I've found recently is that unless you're using UIAccessibilityContainer, referring to accessibility traits of superviews can negate the accessibility traits of its subviews.
Just put the debugger in the test function, run the test
and in debugger screen just write po app it will show the view hierarchy of your app containing images , static text etc.
Related
I am using iOS UITest for a Swift application. I use something like,
func testAllScreenNavigation() {
let app = XCUIApplication()
app.tabBars.buttons["Home"].tap()
app.navigationBars["Home"].buttons["More"].tap()
app.sheets.buttons["Cancel"].tap()
}
etc. to navigate some of the specific, tabs, buttons, etc. and switch to respective screens. But i want to navigate each and every screens of my Application (It can be BFS style navigation or DFS style navigation, no matter). Is there any way iOS provides so i can get all navigable elements and then explore deeper and deeper automatically for my App?
I also need to keep trace of which xcuoelement in a screen is already processed and which are not yet processed.
The only way I can think of is using Xcode UI test recorder feature.
While you are recording, navigate through all of your screens via the device/simulator and then the XCUIApplication() variable would be recorded with the appropriate references.
If the button/nav bar/any element has text on it, it will show up in the recorded code or else it will be referenced numerically.
Hope that helps.
Kind regards,
Mukund
I like your idea for getting all views and check whether the layouting and localization for example is fine.
I think you need to specify your criteria for "screens" and how they are accessed.
Basically, one could thing of the following structure
- UITabBarController
-- UISplitViewController
--- UINavigationController
---- UIViewController
----- UIBarButtonItems
----- UIView
----- UIButton
----- UISwitch
----- UITableViewCell
You could now go top down from the UITabBarController to the next controlling instance (might also skip one, e.g. SplitViewControllers on iPhones).
You can use the general property:
XCUIApplication().tabBars
Nevertheless that transition is the problem: How would you get from one ViewController to another and are they all position in the ViewController's View or do you have to loop the subviews of a view.
UIButton -> Touch Up Inside
UISwitch -> Value Changed
UITableViewCell -> DidSelectRowAtIndexPath
UIView -> UILongPressGestureRecognizer
This is how I would basically set it up:
For each UIViewController instance, get the related View (and perform the following call recursively).
Check all the subviews of a view.
For UIViews, go even further and check their subviews
For UIButtons, perform TouchUpInside
and so on.
Make sure to have a condition to stop going deeper, as UITableViews got a lot of subviews or your UIWebViews would of course be set up in a different way.
This way you should be able to navigate through a lot Views in your app hierarchy, but you will need some extensions for UIBarButtonItems, custom Gesture Recognizers and of course also for your "special" controls that might listen to value changes and perform a layout-change.
Accessing specific elements
In addition to the above approach where you simply get an array of elements of a specific type, you can access specific elements (e.g. those where you know they are of a very specific type with certain ValueChangeListeners or something)
To access a specific object in particular, like the TabBar example from above, you can use the accessibilityLabel like so. At first you need to declare the accessibilityLabel in your code or in the .xib-file/.storyboard:
// just to illustrate, so you get an idea:
self.tabBarController.isAccessibilityElement = true
self.tabBarController.accessibilityLabel = "tabBar"
And then do:
let tabBar = XCUIApplication().tabBars["tabBar"]
Here is Apple's documentation for setting these accessibilityLabels:
https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/iPhoneAccessibility/Making_Application_Accessible/Making_Application_Accessible.html
A great way to get the related identifier of an element would be to use the Accessibility Inspector from Apple:
https://developer.apple.com/library/content/technotes/TestingAccessibilityOfiOSApps/TestAccessibilityiniOSSimulatorwithAccessibilityInspector/TestAccessibilityiniOSSimulatorwithAccessibilityInspector.html
Accessing elements in general
To access elements in general, you need to make use of the XCUIElementType of these objects, here you will access the objects based on their classes.
E.g. you could call:
"tabBars", "navBars", "tables", "buttons", and so on from the elements in general.
Still you would be facing the issue with "special controls". As the Apple documentation lacks (imho) some detail about properties and attributes, I do recommend the docs here: https://blog.metova.com/guide-xcode-ui-test/ It provides a great overview of what is accessible and may help you getting some better understanding.
An overview of the available XCUIElementTypes can be found here. Basically, the elementType property is an enumerated value that represents the type of an element. XCUIElementType is a very large enumeration and some of its members do not apply to iOS applications (they apply to MacOS X apps). Some of the more commonly used values are:
Alert
Button
NavigationBar
TabBar
ToolBar
ActivityIndicator
SegmentedControl
Picker
Image
StaticText
TextField
DatePicker
TextView
WebView
https://developer.apple.com/reference/xctest/xcuielementtype?language=objc
I wanna check whether there is a tableViewCell.detailTextLabel with a given string in my UITest. The problem is when I search for app.tables.cells.children(matching: .staticText) it will only look for labels that are tableViewCell.textLabel.
Any ideas on how to query for the detailTextLabel?
You can try a more generic query to determine if the text exists at all. If you are trying to assert that a cell contains "123 Main St.", for example, you can use the following:
let app = XCUIApplication()
XCTAssert(app.staticTexts["123 Main St."]).exists)
Granted, if that text appears in the textLabel the test will still pass.
It sounds like your detail text label isn't accessible.
To see everything that's available to your tests in the view hierarchy, print the debug description of the following query, which may help you to understand whether the element is appearing, what its type is, and what its identifier or label is:
let app = XCUIApplication()
print(app.descendants(matching: .any).debugDescription)
Make sure that the containing view (the cell?) is not accessible, and that the detail text label is accessible, and give it an identifier:
let cell = UITableViewCell!
cell.isAccessibilityElement = false
cell.detailTextLabel.isAccessibilityElement = true
cell.detailTextLabel.accessibilityIdentifier = "myDetailTextLabel"
This is using Xcode 7.2.1 and Swift 2.0.
I have a table cell containing a UILabel which is used to display an error message. So at initial load it is blanked using code like this:
cell.errorLabel.alpha = 0.0
cell.errorLabel.text = nil
Then later on when I've detected an error I want to display I do this:
cell.label.text = "to"
cell.errorLabel.alpha = 1.0
cell.errorLabel.text = "Error !!!!"
This works fine when running the app. However when running in a UI Test, I try to test that the error is being displayed like this:
let toCell = XCUIApplication().tables.... // Finds the cell with label 'to'
XCTAssertTrue(toCell.staticTexts["Error !!!!"].exists)
And it fails. I've verified I'm getting the right cell by checking the other ('to') label is present. Which it is. But the UI testing will not see the error label. I've tested this by adding a break point and using the debugger like this:
(lldb) p toCell.staticTexts.count
t = 427.03s Get number of matches for: Descendants matching type StaticText
t = 427.29s Snapshot accessibility hierarchy for enterprise.com.me
t = 427.31s Find: Descendants matching type Table
(UInt) $R2 = 1
t = 427.31s Find: Descendants matching type Cell
t = 427.32s Find: Elements containing elements matching type StaticText with identifier 'to'
t = 427.32s Find: Descendants matching type StaticText
The (UInt) $R2 = 1 indicating that there is one static text present. However looking at the simulator I can clearly see two UILabels.
I've tried a number of things individually to isolate the issue - Using just alpha or setting the text to nil, or using UIView's hidden property. Using any of these options to initially hide the label renders it invisible to UI tests when later made visible, no matter what I try.
I'm quite confused by this and I suspect it's a bug. Anyone have any ideas how to get UI Tests to see the UILabel once I make it visible?
P.S. I've also tried using a wait loop to wait for the label to appear (using expectationForPredicate(...), but the UILabel has not shown up.
Problem turned out to be that I had not set the accessibilityElements property on the cells. Therefore accessibility was having issues trying to figure out what was in each one.
So if you are building custom cells for a table view, ensure you set the accessibilityElements property so that testing can find the contents of the cells.
I am experimenting with the (Xcode 7) UI XCTestCase test cases and I just stumbled onto an issue with one UIView, in which I have a UITableView with many cells(4000+).
When the app is running normally, only the visible cells are rendered and there is no performance issue at all.
However, if I run the app within the context of recording a XCTestCase and I navigate to this screen, the simulator freezes, apparently because each single cell is rendered as if it were visible.
If I try to script the navigation manually and I run the XCTestCase, the test case fails right after navigating to this screen, exiting with a "UI Testing Failure - Failed to get refreshed snapshot", apparently again because all cells are being rendered and this does not finish in time.
I think this has to do with the fact that the testing framework builds an entire metamodel of the screen under display, adding each of the 4000+ cells into the view tree hierarchy.
I tried adding an expectation, hoping this would give the testing container enough time to finish rendering all cells, but this does not work.
Is there a workaround for this? Is it somehow possible to skip building part of the UI tree hierarchy or something?
My goal is being able to write UI tests for this screen.
You might be able to avoid having the entire table render, if you can use firstMatch instead of element, and also avoid count.
I had a test that checks for expected labels in the first two cells of a table. At first, I was using app.table.cells.element(boundBy: 0) and app.table.cells.element(boundBy: 1) to find the first and second cells. This was resulting in the whole table being rendered before I could access the cells.
I adapted my test to be slightly less precise, but still good enough for me (given the huge amount of time it would take otherwise). Instead, I use matching with predicates on the expected label values, with firstMatch, to find the first cells matching the criteria I want. This way the traversal stops as soon as it finds them (and since they're at the top of the table, it's quick).
Here's the code before and after.
Before (slow, yet more precise):
private func checkRhymes(query: String, expectedFirstRhyme: String, expectedSecondRhyme: String) {
let table = app.tables.element
let cell0 = table.cells.element(boundBy: 0)
let cell1 = table.cells.element(boundBy: 1)
let actualRhyme0 = cell0.staticTexts.matching(identifier: "RhymerCellWordLabel").firstMatch.label
let actualRhyme1 = cell1.staticTexts.matching(identifier: "RhymerCellWordLabel").firstMatch.label
XCTAssertEqual(expectedFirstRhyme, actualRhyme0, "Expected first rhyme for \(query) to be \(expectedFirstRhyme) but found \(actualRhyme0)")
XCTAssertEqual(expectedSecondRhyme, actualRhyme1, "Expected first rhyme for \(query) to be \(expectedSecondRhyme) but found \(actualRhyme1)")
}
Faster, but less precise (but good enough):
private func checkRhymes(query: String, expectedFirstRhyme: String, expectedSecondRhyme: String) {
let table = app.tables.firstMatch
let label0 = table.cells.staticTexts.matching(NSPredicate(format: "label = %#", expectedFirstRhyme)).firstMatch
let label1 = table.cells.staticTexts.matching(NSPredicate(format: "label = %#", expectedSecondRhyme)).firstMatch
// We query for the first cells that we find with the expected rhymes,
// instead of directly accessing the 1st and 2nd cells in the table,
// for performance issues.
// So we can't add assertions for the "first" and "second" rhymes.
// But we can at least add assertions that both rhymes are visible,
// and the first one is above the second one.
XCTAssertTrue(label0.frame.minY < label1.frame.minY)
XCTAssertTrue(label0.isHittable)
XCTAssertTrue(label1.isHittable)
}
Reference:
https://developer.apple.com/documentation/xctest/xcuielementquery/1500515-element
Use the element property to access a query’s result when you expect a
single matching element for the query, but want to check for multiple
ambiguous matches before accessing the result. The element property
traverses your app’s accessibility tree to check for multiple matching
elements before returning, and fails the current test if there is not
a single matching element.
In cases where you know categorically that there will be a single
matching element, use the XCUIElementTypeQueryProvider firstMatch
property instead. firstMatch stops traversing your app’s accessibility
hierarchy as soon as it finds a matching element, speeding up element
query resolution.
I had the same issue, and I agree it is frustrating having to wait for the entire table to load, but that is what I had to do using the following workaround.
This may not be what you are looking for but it may help others:
Basically I am counting the cells in the table 2 times consecutively if they are not equal that means the table is still loading. Put it in a loop it and do that until both counts return the same number which would mean the table is finished loading. I then put in a stop of 30 seconds so that if this takes longer than 30 seconds, the test will fail (this was enough time in my case). If your table will take longer than that you could increase the number to 180 for 3 mins etc...
let startTime = NSDate()
var duration : TimeInterval
var cellCount1 : UInt = app.tables.cells.count
var cellCount2 : UInt = app.tables.cells.count
while (cellCount1 != cellCount2) {
cellCount1 = app.tables.cells.count
cellCount2 = app.tables.cells.count
duration = NSDate().timeIntervalSince(startTime as Date)
if (duration > 30) {
XCTFail("Took too long waiting for cells to load")
}
}
//Now I know the table is finished loading and I can tap on a cell
app.tables.cells.element(boundBy: 1).tap()
The question is actually really simple:
Is there a way to assert the displayed value from a specific label (e.g. UILabel) when using an accessibility label on this object?
As far as I see it, all the assertions (e.g. XCTAssertEquals) made in the examples, be it from a WWDC Talk or Blogposts, are only checking if an element exists for a query like XCTAssertEquals(app.staticTexts["myValue"].exists, true) or if the number of cells in a table is correct XCTAssertEquals(app.tables.cells.count, 5). So, when avoiding accessibility labels it's possible to check if an object has a certain value displayed, but not which object / element.
And when using accessibility labels, it robs me of the opportunity to query against the displayed values, because app.staticTexts["myValue"] will now fail to deliver a result but app.staticTexts["myAccessibilityLabel"] will hit.
Assuming I want to test my "Add new Cell to Table" functionality, I can test that there is really a new cell added to the list, but I have no idea if the new cell is added at the top or the bottom of the list or somewhere in between.
For me, an easy way to check if a specific element has a certain value should be a no-brainer when it comes to UI Testing.
It is possible that due to the missing documentation I might overlook the obvious. If so, just tell me.
Be sure to set the .accessibilityValue property of the UILabel whenever you set its .text value. Then in UITest, you can test the accessibility value like this:
let labelElement = app.staticTexts["myLabel"]
...
XCTAssertEqual(labelElement.value as! String, "the expected text")
I think you are asking a few different things, so I will try to answer each question individually.
Is there a way to assert the displayed value from a specific label (e.g. UILabel) when using an accessibility label on this object?
In short, no. UI Testing works by hooking into accessibility APIs, so you are limited to querying for objects based on that. You can, however, check the -value property of certain elements, such as controls. This is used to test if a switch is on or off. Note that these boil to down using accessibility APIs as well, just a different method (-accessibilityValue over -accessibilityIdentifier and -accessibilityLabel).
...but I have no idea if the new cell is added at the top or the bottom of the list or somewhere in between.
To interrogate an XCUIElement for its frame you can use the new XCUIElementAttributes protocol which exposes -frame. For example:
let app = XCUIApplication()
app.launch()
app.buttons["Add New Cell to Table"].tap()
let lastCell = app.cells["Last Cell"]
let newCell = app.cells["New Cell"]
XCTAssert(newCell.exists)
XCTAssert(newCell.frame.minY > lastCell.frame.maxY)
For me, an easy way to check if a specific element has a certain value should be a no-brainer when it comes to UI Testing.
If you think of everything in terms of accessibility this becomes a non-issue. UI Testing can only interact with your elements via accessibility APIs, so you must implement them. You also get the added benefit of making your app more accessible to user's with those settings enabled.
Try setting both the -accessibilityLabel or -accessibilityIdentifier for the cell to the displayed text. UI Testing can be finicky as to which one it uses.
It is possible that due to the missing documentation I might overlook the obvious. If so, just tell me.
XCTest and UI Testing don't have any official documentation. So I've gone and extracted my own from the header files exposed in the framework. Note than even though they were pulled from source, they are unofficial.
XCTest / UI Testing Documentation
What works for me is to set the accessibility identifier of the UILabel to let's say MyLabel.
func myLabelText() -> String {
let myLabelUIElement: XCUIElement = self.application.staticTexts["MyLabel"]
return myLabelUIElement.label
}
Tested with Xcode 8 and iOS 10
From the apple forums it looks like it is possible to get the value of the label:
The only way I've found is to not set an Accessibility Label, but use identifier instead. Then XCUIElement.label will change to match the current text of the label.
However there is a gotcha: if you have previously set Accessibility Label in XC, and remove it, an entry setting the label to "" remains in the storyboard. In this case, not only will calling .label will return "", but you won't be able to query for the label by it's text!
The only thing you can do is delete and re-add the label, or manually edit the xml.
lastobelus - https://forums.developer.apple.com/thread/10428