How can I retrieve a subview with accessibilityIdentifier (using quick and nimble)? - ios

I was wondering how I can retrieve a subview using an accessibilityIdentifier.
I'm using quick and nimble for tests.
I've tried this, but unfortunately doesn't work
let subview = sut.accessibilityElements?.first(where: { element in
(element as? UIView)?.accessibilityIdentifier == "element_id"
})

Related

iOS test automation for complex view, UIAccessabilityContainer?

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?

How should I group wrapped UIKit components to improve accessibility support?

I have difficulties understanding what is the right way to make a custom UIView accessible on iOS. As a case study, here is a custom UIControl subclass wrapping a UILabel and a UITextField subviews, that I want to test via a UI test.
View hierarchy:
→ UIControl
↪︎ UILabel
↪︎ UIView
Intended behavior:
1️⃣ The default behavior
By default, the behavior is not great:
The accessibility inspector inspects the two subviews as unrelated;
The accessibility inspector’s audit raises a “hit area is too small” warning since it is ignoring the whole UIControl can be tapped and will give focus to the UITextField.
But this testing code works fine:
let emailField = app.textFields["Email"]
emailField.tap()
emailField.typeText("toto#tata.com")
2️⃣ Becoming an accessibility element
If the UIControl becomes flagged as an accessibility element as following, the accessibility inspector will show only 1 pointer for the whole UIControl but the UI test will fail.
Here is the code:
isAccessibilityElement = true
accessibilityLabel = innerLabel.text
accessibilityValue = innerTextField.text
accessibilityTraits = innerTextField.accessibilityTraits
And the test failure stack, that seems to have an Element subtree limited to the UIControl (aka TextField) itself
UI Test Activity:
Assertion Failure: SampleUITests.swift:21: Failed to synthesize event: Neither element nor any descendant has keyboard focus. Event dispatch snapshot: TextField, label: 'Email'
Element debug description:
Attributes: TextField, {{32.0, 229.0}, {350.0, 52.0}}, label: 'Email'
Element subtree:
→TextField, 0x600000925340, {{32.0, 229.0}, {350.0, 52.0}}, label: 'Email'
3️⃣ Simplifying accessibility information
Inspired by Apple’s documentation on how to simplify your accessibility information , I used the following code:
var elements = [UIAccessibilityElement]()
let groupedElements = UIAccessibilityElement(accessibilityContainer: self)
groupedElements.accessibilityLabel = innerLabel.text
groupedElements.accessibilityTraits = innerTextField.accessibilityTraits
elements.append(groupedElements)
Which seems to do nothing: I’m back to the default behavior.
Wrapping things up
I’d like to have the accessibility structure from 2️⃣ and still be able to run UI tests in the most expressive way ie using the same code as 1️⃣, how can I do this?
What did I do wrong in 3️⃣ that made it behave the same way as 1️⃣?
Edits
As hinted by #Defragged, I tried to improve the UIControls compliance to UIResponder but it didn't really help:
override var canBecomeFirstResponder: Bool {
return innerTextField.canBecomeFirstResponder
}
override func becomeFirstResponder() -> Bool {
return innerTextField.becomeFirstResponder()
}
override var isFirstResponder: Bool {
return innerTextField.isFirstResponder
}
Scrutinizing the UI test error logs a little deeper, I read this:
t = 9.65s Synthesize event
t = 9.68s Get number of matches for: Elements containing elements matching predicate 'hasKeyboardFocus == 1'
t = 9.72s Requesting snapshot of accessibility hierarchy for app with pid 24879
t = 9.74s Find: Descendants matching type TextField
t = 9.74s Find: Elements matching predicate '"Email" IN identifiers'
t = 9.74s Find: Elements containing elements matching predicate 'hasKeyboardFocus == 1'
It seems that XCUIElement instances have a undocumented hasKeyboardFocus attribute that can be inspected with a debugger but that I have no clue how to control 🤷‍♂️.

Collapsible input form Swift 3

I am really getting disappointed that's why I ask this question, I found tutorials online for Collapsible TableView but they are with populating the cells with an array of Strings or something similar.
I want to make an Accordion like this with Swift 3,
for some days already I tried a lot of things with UITableViewController because apparently that's the only thing you can make collapsible if you want different cells.
Anyway I started to do it but as I asked my question here I cannot show different UIs in each Cell of each Section.
I am sure there's a way to do it, anyone has any suggestions how to make this work?
I thought maybe there's another way to do it (e.g. with ScrollView or something)
Here is how you could implement it with UIStackView:
Add a vertical UIStackView via storyboard
Add a UIButton as the first subview within the UIStackView
Add some UILabels as the second, third... subview within the UIStackView
Add an IBAction for the UIButton (the first subview)
Implement the IBAction like this:
#IBAction func sectionHeaderTapped(sender: UIButton) {
guard let stackView = sender.superview as? UIStackView else { return }
guard stackView.arrangedSubviews.count > 1 else { return }
let sectionIsCollapsed = stackView.arrangedSubviews[1].hidden
UIView.animateWithDuration(0.25) {
for i in 1..<stackView.arrangedSubviews.count {
stackView.arrangedSubviews[i].hidden = !sectionIsCollapsed
}
}
}
Create multiple UIStackViews like this (you can always use the same IBAction for the UIButton) and embed all of them in a parent (vertical) UIStackView to create a view like in your screenshot.
Feel free to ask if anything is unclear.
Result:

How to get identifier name of a specific control in SWIFT 3 (Created through storyboard) to set values from DB

I have created like a 100+ controls (labels, textboxes, imageviews), using the storyboard (yes I was that patient :( or are there any easier ways to do this lol) for a certain chart and now my problem is how to iterate through the controls using its identifier name (which I did manually by ctrl + draging to the swift file). Is there any way to get its 'identifier name' (like in Android) so I can set values for that specific control w/ the values from my database. E.g:
for i in 0 ..< chartDetails.count{
for view in self.innerView.subviews as [UIView] {
if let txtFld = view as? UITextField{
if(txtFld.identifier == "TXT_L_" + String((i + 1)))
{
txtFld.text = chartDetails[i].ConValue
}
}
}
}
Other solutions said to use the tag, but I don't think it would work in this scenario.
I tried this but got an error, 'unwrapping optional value'
for i in 0 ..< chartDetails.count{
for view in self.innerView.subviews as [UIView] {
if let txtFld = view as? UITextField{
if let value = txt as? UIAccessibilityIdentification{
print("Test: " + value.accessibilityIdentifier!)
}
}
}
}
EDIT:
Thanks to alexburtnik, in order for you to set values in the controls you created, you need to set accessibility of each control programmatically.
If those controls and labels are similar to each other, you should use a IBOutletCollection for each group instead of individual IBOutlets.
In order to do this just select IBOutletCollection when you ctrl & drag the first element in a group to your class. You should see something like this:
#IBOutlet var textFields: [UITextField]!
All subsequent elements of a group you just drag to the same variable and they will be added to the array in runtime.
Now you can iterate over those elements just as you do with any other array:
for textField in textFields {
if let identifier = textField.accessibilityIdentifier {
print("Test: " + identifier)
}
}
Note, that I print only if accessibilityIdentifier exists, so it won't crash as your code. In fact you should avoid forced unwrapping (!) almost everywhere.

Recognizing subview's class

I decided to animate my objects manually and therefore made an extension for UIView class:
public extension UIView{
func slideOut(){
UIView.animateWithDuration(0.5, animations: { self.frame.origin.x = -self.frame.width }, completion: finishedDisposing)
}
func finishedDisposing(successfully: Bool){
if !successfully{
((UIApplication.sharedApplication().delegate as! AppDelegate).window!.rootViewController as! VC).showSystemMessage("Failed to dispose one or more subviews from superview", ofType: .NOTICE)
}
responder.viewDisposed()
}
}
Which works nice and I have no problems about it, BUT I have a method in VC (Custom UIViewController) viewDisposed() which is called whenever a view slides out of sight and it has such an implementation:
func viewDisposed() {
disposed++
print("Updated disposed: \(disposed) / \(self.view.subviews.count)")
if disposed == self.view.subviews.count - 1{
delegate.vcFinishedDisposing()
}
}
It shows that self.view.subviews contains all my custom views + 3 more (UIView, _UILayoutGuide x 2). They do extend UIView although do not callresponder.viewDisposed method. My decision was to figure out how to get classes of each subview and Mirror(reflecting: subView).subjectType if I print it does it wonderfully. Is there any way to actually compare this to anything or, better, get String representation? Basically, I want you to help me create a method which would create a stack of subviews which are not of type UIView (only subClasses) nor _UILayoutGuide. Thank you!
You'd probably be better off directly creating an array of just the subviews you care about, instead of starting with all subviews and trying to filter out the ones you don't care about. Those layout guides weren't always there—they were added in iOS 7. Who knows what else Apple will add in the future?
Anyway:
let mySubviews = view.subviews.filter {
!["UIView", "_UILayoutGuide"].contains(NSStringFromClass($0.dynamicType))
}

Resources