iOS test automation for complex view, UIAccessabilityContainer? - ios

I am building UI automation tests for an app with a complex view hierarchy.
In my situation, I would like to use automation to tap on a UITextField subclass. That textfield is contained within a collectionView, which is itself contained within another collection view.
That textview has a known accessibility label+identifier, but I can't find any way to access this view via the normal XCTest api. For example, the following does not locate the element:
let app = XCUIApplication()
let textField = app.textFields["FOO"]
let textField2 = app.secureTextFields["FOO"]
let textField3 = app.otherElements["FOO"]
As another way to get a reference to the textfield, I tried setting up as a UIAccessabilityContainer, but I'm not sure I'm using it correctly.
//inside the textfield
//should return false if using UIAccessibilityContainer?
self.isAccessibilityElement = false
let e = UIAccessibilityElement(accessibilityContainer: self)
e.accessibilityIdentifier = placeholder
e.accessibilityLabel = placeholder
e.accessibilityTraits = .allowsDirectInteraction
//append this element to the window's list of elements
var eles = window?.accessibilityElements ?? []
eles.append(e)
window?.accessibilityElements = eles
Doing this seems to break all of my existing automation code using the "normal" api. What is the correct way to go about this?

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.

Setting UIView.isAccessibleElement to true disables clicking of subview in voiceovermode

So I have a custom view controller that displays a dialog with a couple of buttons.
When the view appears I want voiceover to read out some basic information describing the dialog.
To achieve this I made the parent view to be an accessible element and the subviews which are two buttons are also accessible elements.
My problems now is that the buttons are not clickable directly.
They must be reached only by swiping right on the screen.
class MyViewController: UIViewController {
let parent = UIView()
let button1 = UIButton()
let button2 = UIButton()
init() {
parent.addSubview(button1)
parent.addSubview(button2)
parent.isAccessibilityElement = true
button1.isAccessibilityElement = true
button2.isAccessibilityElement = true
parent.accessibilityLabel = "Message"
self.view.addSubview(parent)
self.view.accessibilityElements = [parent, button1, button2]
}
override func viewDidAppear(_ animated: Bool) {
}
}
If there is a better way to get voiceover to give description of the view when the opens, I am open to that too.
Also, the view needs to be a modal so that focus is trapped on the view.
To achieve this I made the parent view to be an accessible element and the subviews which are two buttons are also accessible elements.
That's definitely the problem: you can't have the parent view and its children accessible all together ⟹ see the example sheet of this explanation.
If a parent view is accessible, its children won't be seen by VoiceOver and conversely.
If there is a better way to get voiceover to give description of the view when the opens, I am open to that too.
Using VoiceOver, you must be as accurate and brief as possible.
The description of a view is provided by its elements when you explore the screen or by its title itself : in my view, you shouldn't read out a description that a perfect title should provide in addition to the correct implementation of the different components of your page.
There's a great presentation made by a blind person who explains how to write labels inside an app to be well understood.
Also, the view needs to be a modal so that focus is trapped on the view.
The best way to reach this purpose is to use the accessibilityViewIsModal property of your view ⟹ take a look at this example introduced during a WWDC session if need be.
You can post a notification with a message as parameter, so you would not need to set the parent view as an accessibility element. This would solve both of your problems.
Example code:
let parentVc = UIView()
let button1 = UIButton()
let button2 = UIButton()
init() {
parentVc.addSubview(button1)
parentVc.addSubview(button2)
button1.setTitle("btn1", for: .normal)
button2.setTitle("btn2", for: .normal)
button1.isAccessibilityElement = true
button2.isAccessibilityElement = true
self.view.addSubview(parentVc)
self.view.accessibilityElements = [button1, button2]
UIAccessibility.post(notification: UIAccessibility.Notification.screenChanged, argument: "Message here");
}

How to write XCUI test for whether I navigate to correct screen or not?

I am writing test UI test case for following UI
I want to test on Login click whether I am navigating correctly on Dashboard screen or not.
Is there any method to do this?
My current testing code is like
func testExample() {
let usernameTextField = app.textFields["Username"]
usernameTextField.tap()
usernameTextField.typeText("abc#gmail.com")
let passwordTextField = app.textFields["Password"]
passwordTextField.tap()
passwordTextField.typeText("abc123")
app.buttons["Login" ].tap()
//let loginButton = app.staticTexts["Login"]
//XCTAssertEqual(loginButton.exists, true)
app.navigationBars["UIView"].buttons["Back"].tap()
}
UI Tests can become really fragile when depending on text values. What I encourage you to do is to set the Accessibility Identifier for your ViewController's view. That way, even if you change the title or change the whole layout, you can still be sure you're in the correct Page/Screen/View.
class DashVC: UIViewController {
override func viewDidLoad() {
view.accessibilityIdentifier = "view_dashboard"
}
}
func test_login_withValidInput_goesDashBoard() {
let app = XCUIApplication()
//...
app.buttons["Login" ].tap()
let dashBoardView = app.otherElements["view_dashboard"]
let dashBoardShown = dashBoardView.waitForExistence(timeout: 5)
XCTAssert(dashBoardShown)
}
Try this
app.buttons["Login - Login"].tap()
XCTAssertEqual(app.navigationBars.element.identifier, "appname.CalculationView") //If your second view controller is SecondViewController, your identifier is appname.SecondView.Like that my second view controller is CalculationViewController so my identifier is CalculationView
Try adding an accessibility indicator to the back button so you can check for availability using backButton.exists or backButton.hittable and assert accordingly. In any case, if you set
continueAfterFailure = false
in setUp(), your test will fail as it is if it doesn’t find a button with “Back”.

How to type into a UITextView inside a XCTestCase

How do you type into a UITextView inside a XCTestCase? The UITextView is the first responder so it already had focus and the keyboard is up.
I've tried:
app.typeText("footer")
app.textViews.element(boundBy: 0).typeText("foobar")
app.links.element(boundBy: 0).typeText("foobar")
for some reason app.textViews.count always returns 0?
The problem is not that .typeText(_:) doesn't work, it's that your query doesn't resolve to an element.
It can sometimes seem like the query is not working properly when the view you're trying to find is inside an accessible container view of some sort. You can mitigate this by explicitly disabling accessibility in the container view.
Set the stack view that contains the text view to not be an accessibility element and then set an accessibility identifier on the text view.
// app code
let stack: UIStackView!
let textView: UITextView!
stack.isAccessibilityElement = false
textView.isAccessibilityElement = true
textView.accessibilityIdentifier = "myTextView"
// test code
let app = XCUIApplication()
let textView = app.textViews["myTextView"]
textView.typeText("something")
Try setting an unique accessibility identifier for your UITextView and then:
let textView = app.textViews["uniqueId"]
XCTAssert(textView.exists, "uniqueId text view NOT FOUND")
textView.tap()
textView.typeText("foobar")

iOS UITests - How to distinguish two different XCUIElement?

While iOS UITesting, how can I distinguish between two different XCUIElement?
For example I have two different UIButton with same label string "Button". How to check they are different? Do XCUIElement provides ID or any distinct property?
Add an accessibilityIdentifier to each button in your app's code and access each button by its identifier in your tests to tell them apart. Accessibility identifiers are not user-facing, even to Accessibility users, so this will not affect your user experience.
// app code
buttonA.accessibilityIdentifier = "buttonA"
buttonB.accessibilityIdentifier = "buttonB"
// test code
let app = XCUIApplication()
let buttonA = app.buttons["buttonA"]
let buttonB = app.buttons["buttonB"]

Resources