I want to make use of the inline date picker style that was recently introduced in iOS 14. However, I need to make adjustments to a couple of XCUITests that include the previous picker wheel style.
The problem is I can't change the value of the time picker at all. I noticed that the time picker is not a picker nor a pickerWheel element in XCUITest; Instead, it's a textField element.
E.g.
TextField, {{178.3, 401.7}, {74.7, 36.3}}, label: 'Time', value: 12:01
Changing its value like a typical textfield doesn't work at all (typeText doesn't do anything). I tried to access the picker wheels that seem to be inside the textfield, but checking for its descendants returns empty.
po timeInput.descendants(matching: .any).count
t = 799.62s Get number of matches for: Descendants matching type Any
0 // no descendants found
So how do I change the value of the time picker text field in XCUITest?
EDIT:
The date picker mode for the UIDatePicker is set to time, so I'm not really seeing the calendar view, just the time input field.
I put the date picker inside the contentView of a UITableViewCell, which is then added when another table view cell is tapped (i.e. it's dynamically added).
The date picker is configured like this:
picker.datePickerMode = .time
picker.date = Date(timeInterval: time, since: date)
picker.minuteInterval = intervals
if #available(iOS 14, *) {
picker.preferredDatePickerStyle = .inline
}
Previously, the date picker is displayed as a picker wheel and I had no problem accessing it in the XCUITest. I could simply call this to adjust the value of the picker wheel:
pickerWheels.element(boundBy: index).adjust(toPickerWheelValue: value)
However, with the date picker set to inline, the previous query no longer works. I also checked for picker and datePicker elements, but they also return empty results. I can see a textfield element that has a "Time" label and the value contains whatever value is set in the time picker.
(lldb) po app.pickers.count
t = 2270.22s Get number of matches for: Descendants matching type Picker
0 // No results
(lldb) po app.pickerWheels.count
t = 2277.07s Get number of matches for: Descendants matching type PickerWheel
0 // No results
(lldb) po app.datePickers.count
t = 2286.58s Get number of matches for: Descendants matching type DatePicker
0 // No results
(lldb) po app.textFields.count
t = 2302.55s Get number of matches for: Descendants matching type TextField
1 // 1 element matched
(lldb) po app.textFields
t = 2308.08s Requesting snapshot of accessibility hierarchy for app with pid 55829
t = 2308.46s Find: Descendants matching type TextField
t = 2308.47s Requesting snapshot of accessibility hierarchy for app with pid 55829
t = 2308.61s Find: Descendants matching type TextField
t = 2308.61s Requesting snapshot of accessibility hierarchy for app with pid 55829
Find: Target Application
Output: {
Application, pid: 55829, label: 'Projects'
}
↪︎Find: Descendants matching type TextField
Output: {
TextField, {{178.3, 401.7}, {74.7, 36.3}}, label: 'Time', value: 01:00
}
So I can't see any pickers, but I have textfield whose value is set to the date picker's time input value. I tried changing the textfield's value by using typeText but it doesn't seem to do anything at all.
I ended up creating a sample view controller which contains just the UIDatePicker to see if I can access it in XCUITest.
Interestingly, I was able to detect a datePicker element in the XCUITest which is something I wasn't able to do when dynamically adding the UIDatePicker in a table view.
The descendants of the datePicker looks like this:
Find: Descendants matching type DatePicker
Output: {
DatePicker, {{89.3, 423.3}, {196.7, 53.3}}
}
↪︎Find: Descendants matching type Any
Output: {
Other, {{89.3, 423.3}, {196.7, 53.3}}
Other, {{89.3, 431.3}, {196.7, 37.3}}
Other, {{89.3, 431.3}, {196.7, 37.3}}, label: 'Time Picker'
TextField, {{97.3, 431.3}, {74.7, 36.3}}, label: 'Time', value: 05:54
SegmentedControl, {{178.0, 431.3}, {100.0, 36.3}}
Button, {{178.0, 431.3}, {49.0, 36.3}}, label: 'AM'
Button, {{228.0, 431.3}, {50.0, 36.3}}, label: 'PM', Selected
}
The time input field is still a textfield element but I was able to change its value using typeText. I suspect that there's something in our codebase that handles the valueChanged delegate that causes the typeText to not work.
Bottomline, using typeText to change the value of the time input picker should work just fine.
Before using typeText to change its value, make sure it has focus. Try to tap the textfield first, and then the value can be changed via typeText.
Related
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 🤷♂️.
I have a UIViewController with several UITextFields. When tap one text field, it should present the barcode scanning view controller. Once the scanning is completed, my barcode scanning viewcontroller is disappearing (used "dismissViewcontroller") and the scanned value should entered into the text field I tapped. This is working fine. I have set the delegate for each text field like this.
[field addTarget:metrixUIViewControllerIn action:#selector(executeScriptOnTextFieldChange:) forControlEvents:UIControlEventEditingChanged];
The problem is this :
Lets say I have set an alert to display inside this executeScriptOnTextFieldChange method. Once I tapped on the 1st text field, then the barcode scanner comes. Once I scanned barcode scanner closes and set the value for the first text field and fire the alert.Thats ok. But then if scanned by tapping the 2nd textfield and the string will set to that textfield and fire the alert related to 2nd textfield also fire the alert related to first textfield as well. I want to stop happening this. Is there any way to disable the delegate for one textfield? This happens because I am refreshing the view in the viewDidAppear. But I have to do that as well. Please help me.
UIControlEventEditingChanged for a textField can fire at many different events that are not even directly related to that textField, but related inderectly.
For instance, when your ViewController is presenting the barcodeScanner it may trigger a "resignFirstResponder" event on the textField. Also when the 2nd textField is tapped, cause the 2nd becomes first responder and the 1st suffers a "resignFirstResponder".
I suggest trying to use a UITapGestureRecognizer in your textField instead. Example:
Swift 4
#IBOutlet weak var textField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
self.textField.tag = 1
self.textField.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(fireTextField(_:))))
}
#objc func fireTextField(_ sender: UIGestureRecognizer){
let view = sender.view
guard view != nil else{
//Do nothing
return
}
let condition = view!.tag == 1
if condition{
//or do whatever other stuff you need
self.textField.becomeFirstResponder()
}else{
//Whatever for other textFields
}
}
This way, you could use the "tag" attribute to determine which textField is firing and so adjust "condition". You could also filter the flow with a switch using the "tag".
Not sure if any of this will really help as I would need more info about the flow you need to accomplish. Hope it does help!
I am trying to get the text from a UITextView whose entry method was dictation. In the textViewDidChange delegate method, I have tried printing the following info:
print(textView.text)
print(myTextView.text)
print(textView.hasText)
print(textView.attributedText)
print(textView.textStorage)
all of these come back nil, except the hasText comes back as false.
The text is visible right there in the textView, but it is not being registered. I should say that when I go edit another form in the field, and then try to get the value from this text field, then the the text entered IS visible in the textView's textView.text property. But it's like it takes a few moments of editing other fields to fully "register" with the textView object.
Any idea what could be happening here?
Try this after a while
let when = DispatchTime.now() + 0.5
DispatchQueue.main.asyncAfter(deadline: when) {
print(textView.text)
print(textView.hasText)
print(textView.hasText)
print(textView.attributedText)
print(textView.textStorage)
}
Update: It`s because you are trying to print the text before setting it. It takes a little time to set your text into textView.
I have a static numeric-keyboard made out of a bunch of buttons, I also have three UITextFields, textField1, textField2 and textField3 where I'm inputting the text using the static keyboard.
Here is the code I'm using to detect which textField is currently in focus and to input the content of the buttons. It kind of works but I don't like the fact that I have three IF statements and I'm not sure how to prevent the keyboard from appearing when a textField is tapped.
What would be the best way to implement this functionality?
#IBAction func appendKey(sender: AnyObject) {
let digit = sender.currentTitle!
if(textField1.isFirstResponder()){
textField1.text = textField1.text! + digit!
}else if(textField2.isFirstResponder()){
textField2.text = textField2.text! + digit!
}else if(textField3.isFirstResponder()){
textField3.text = textField3.text! + digit!
}
}
Thanks
If the standard keyboard is displaying then your custom keyboard isn't setup properly. Your custom keyboard should be the inputView of each UITextField. If you do that, the standard keyboard won't appear and yours will instead.
Your custom keyboard should be a separate class that handles all of it's own buttons. It appears you have everything in one view controller - all of the text fields, all of the buttons, and all of the button handling code. This is a bad approach. Create your custom keyboard class view. Put all of the code to handle and display the buttons in that custom view class. Create a single instance of this view in your view controller and assign the custom keyboard view instance to the inputView property of each text field.
In the custom keyboard class, listen for the UITextFieldTextDidBeginEditingNotification notification. This is how you keep track of the current text field. Your custom keyboard class should not have any specific reference to any text field other than track the current one. It should also ensure that the text field's inputView is itself.
In each button handler of the custom keyboard class, get the text you wish to append and then call the text field's insertText: method with the string. That's it. This will ensure the text is inserted and/or replaced based on the current selecting in the text field.
This is my simple UITest (customizing order of tabs in tabbarcontroller):
func testIsOrderOfTabsSaved() {
let app = XCUIApplication()
let tabBarsQuery = app.tabBars
tabBarsQuery.buttons["More"].tap()
app.navigationBars["More"].buttons["Edit"].tap()
tabBarsQuery.buttons["Takeaway"].swipeLeft()
tabBarsQuery.buttons["In Restaurant"].swipeLeft()
//here, how to get position of moved button with text "In Restaurant"?
NOTE:
It is possible to get XCUIElement from XCUIElementQuery by index. Can I do this fro the other way?
It seems that the queries automatically return in order based on position on screen.
for i in 0...tabsQuery.buttons.count {
let ele = tabsQuery.buttons.elementBoundByIndex(i)
}
Where the index i represents the position of the button in the tab bar, 0 being the leftmost tab, i == tabsQuery.buttons.count being the rightmost.
You have various ways to create a position test. The simplest way is to get buttons at indices 0 and 1, then get two buttons by name and compare the arrays are equal: (written without testing)
let buttonOrder = [tabBarsQuery.buttons.elementAtIndex(0), tabBarsQuery.buttons.elementAtIndex(1)]
let takeawayButton = buttons["Takeaway"];
let restaurantButton = buttons["In Restaurant"];
XCTAssert(buttonOrder == [takeawayButton, restaurantButton])
Another option is to directly get the frame of each button and assert that one X coordinate is lower than the other.
To answer your specific question about getting the index of an XCUIElement in a XCUIElementQuery, that's absolutely possible. Just go through all the elements in the query and return the index of the first one equal to the element.
An XCUIElement alone isn't able to tell you its position in within the XCUIElementQuery. You can search the XCUIElementQuery to discover its offset if you know something about the XCUIElement. In the below let's imagine that "More" is the identifier (change 'identifier' to 'label' if that is what you're working with).
If you want to find the offset of the "More" button (as posted in the original question) then:
var offsetWithinQuery : Int? = nil // optional since it's possible the button isn't found.
for i in 0...tapBarsQuery.buttons.count{
if tapBarsQuery.elementAtIndex(i).indentifier == "More" {
offSetWithinQuery = i
}
After the above loop exits, you'll either have the offset or it'll be nil if "More" isn't found.