UI Tests - isSelected is always returning false - ios

We have updated out Swift 2.3 project to Swift 3 recently using Xcode 8.2.1 (8C1002), and now most of our UI Tests related with tableViews and the isSelected property aren't working. It's always returning false, even when the object is selected (we can see it in the iOS Simulator).
Has anyone experienced similar issues? Our code used to work normally in Swift 2.3 before the conversion. Here is how we retrieve a tableView cell:
let cell = app.tables.cells.element(at: 4)
Note: app is a XCUIApplication.
And here is how we check if it's selected or not:
XCTAssert(cell.isSelected)
Another observation is that we are sure that the object exists because waitForExpectations is returning true:
let existsPredicate = NSPredicate(format: "exists = 1")
expectation(for: existsPredicate, evaluatedWith: cell, handler: nil)
waitForExpectations(timeout: 20, handler: nil)
EDIT: In order to replace isSelected, I've tried to use NSPredicate with selected = 1 and with isSelected = 1. None worked. I also tried to use acessibilityValue based in other question's answer, however it wasn't that simple since sometimes the items in my table view are selected/unselected programatically. Also, that method involved adding test code to the app, which isn't a good practice.
EDIT AFTER BOUNTY END: Since no one could find a solution for that problem and that's obviously a bug in Xcode, I've submitted a bug report to Apple. I will comment here when they release an Xcode version with the fix.
EXTRA EDIT: One day after my last edit, dzoanb came with a functional answer.

I made a few tests and a little research. You can check out the app created for this purpose >>here<<. It would be great if you could check it out (it required a little bit of work). There are also UI tests to prove it works. Also, two options are available, one is vanilla XCTest and one library with a lot of helpers I'm creating with my colleagues AutoMate. But that's not the point.
Here is what I found out:
1) isSelected property of XCUIElement depends on accessibilityTrait. Element to be selected in XCTest has to have UIAccessibilityTraitSelected set.
2) I couldn't reproduce Your problem but I was able to control isSelected property.
3) Yes, it requires a little bit of code, but should work well with VoiceOver if it is important for You.
All necessary code is in Your custom UITableViewCell subclass. And uses overriding UIAccessibilityElement accessibilityTraits property.
private var traits: UIAccessibilityTraits = UIAccessibilityTraitNone
// MARK: UITableViewCell life cycle
override func awakeFromNib() {
super.awakeFromNib()
traits = super.accessibilityTraits
}
// MARK: UIAccessibilityElement
override var accessibilityTraits: UIAccessibilityTraits {
get {
if isSelected {
return traits | UIAccessibilityTraitSelected
}
return traits
}
set {
traits = newValue
}
}
Hope it helps.

Couldn't get that code to compile under Swift 4.
This worked for me.
public override var accessibilityTraits: UIAccessibilityTraits {
get {
if isSelected {
return super.accessibilityTraits.union(.selected)
}
return super.accessibilityTraits
}
set {
super.accessibilityTraits = newValue
}
}

Have you tried making a break point before and after the tap, and check the value of the cell? Like the WWDC video here: https://youtu.be/7zMGf-0OnoU
(See from 10 minutes in)

isSelected only works on views which inherit from UIControl. UIControl.isSelected informs XCUIElement.isSelected.
Since UITableViewCell does not inherit from UIControl, you aren't seeing the value you want in your tests when you observe cell.isSelected.
I suggest that if you want this to be testable via UI tests that you file a feature request with Apple to make UIControl a protocol, which you could then extend your cells to conform to, or add UITableViewCell.isSelected to the properties that inform XCUIElement.isSelected.

#dzoanb solution can work without adding a private var:
override var accessibilityTraits: UIAccessibilityTraits {
get {
if isSelected {
return super.accessibilityTraits | UIAccessibilityTraitSelected
}
return super.accessibilityTraits
}
set {
super.accessibilityTraits = newValue
}
}

Related

How to disable default keyboard navigation in Mac Catalyst app?

I noticed that I can step through rows in a UITableView in a Mac Catalyst app by pressing the up and down arrow keys on my Mac keyboard. However, this interferes with the existing functionality in one of my view controllers. Is there a way to disable this?
I can't find any reference to this functionality in the UITableView documentation. The Human Interface Guidelines for Mac Catalyst mentions "automatic support for fundamental Mac features, such as ... keyboard navigation," so I guess this is an intentional feature, but I can't find any further reference to it or documentation for it.
I haven't seen any other examples of "automatic" keyboard navigation in my app, but ideally Apple would publish a complete list so we could know how to work with, or if needed, disable, the built-in functionality.
Update as of 2021/11/06
It looks like Apple has been changing how the default focus system works and my previous solution is no longer working or required.
UIKeyCommand has a new wantsPriorityOverSystemBehavior: Bool property which needs to be set to true in order for our subclasses to receive certain types of commands, including the arrow key commands.
As of at least Xcode 13.1 and macOS 11.6, maybe eariler, we can now simply add the following to a UITableViewController subclass to replace the default focus behavior with custom keyboard navigation handing:
class TableViewController: UITableViewController {
override var keyCommands: [UIKeyCommand]? {
let upArrowCommand = UIKeyCommand(
input: UIKeyCommand.inputUpArrow,
modifierFlags: [],
action: #selector(handleUpArrowKeyPress)
)
upArrowCommand.wantsPriorityOverSystemBehavior = true
let downArrowCommand = UIKeyCommand(
input: UIKeyCommand.inputDownArrow,
modifierFlags: [],
action: #selector(handleDownArrowKeyPress)
)
downArrowCommand.wantsPriorityOverSystemBehavior = true
return [
upArrowCommand,
downArrowCommand
]
}
#objc
func handleUpArrowKeyPress () {
}
#objc
func handleDownArrowKeyPress () {
}
}
Previous answer (no longer working or required)
Catalyst automatically assigns UIKeyCommands for the up/down arrows to UITableView instances. This does not happen on iOS. You can see this in action by setting a break point in viewDidLoad() of a UITableViewController and inspecting tableView.keyCommands.
So I created a very simple UITableView subclass and disabled the default keyCommmands by returning nil:
class KeyCommandDisabledTableView: UITableView {
override var keyCommands: [UIKeyCommand]? {
return nil
}
}
I then updated my UITableViewController subclass to use the new KeyCommandDisabledTableView subclass:
class MyTableViewController: UITableViewController {
override func loadView() {
self.view = KeyCommandDisabledTableView(
frame: .zero,
style: .plain // or .grouped
)
}
}
Et voilĂ ! The default arrow key handling is gone and my app's custom arrow key handling is now being called.
Here's another solution I received from Apple DTS. Just add this to the table view delegate:
func tableView(_ tableView: UITableView, canFocusRowAt indexPath: IndexPath) -> Bool {
return false
}
This works in macOS 11.6 and 12.0. I don't have a 10.15 or 11.5 Mac to test with, so I'll keep my earlier resignFirstResponder solution, too.
I further noticed that the default arrow key navigation only begins after clicking a row in a table, so I guessed the table must be assuming the first responder role. I added this to my table's delegate class:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
#if TARGET_OS_MACCATALYST
[tableView performSelector:#selector(resignFirstResponder) withObject:nil afterDelay:0.1];
#endif
}
That fixed it! Now the default keyboard navigation turns off as soon as it turns on, and doesn't interfere with my app's custom keyboard navigation.
(It didn't work without the delay.)
iOS 14 / macOS 11 makes it much easier to disable this behavior thanks to UITableView and UICollectionView's selectionFollowsFocus property:
tableView.selectionFollowsFocus = false

Updating a UILabel Through A Function....Is There Something I'm Missing?

I asked a question similar to this one earlier but this question is more about the general language and fundamentals of Swift. In the code below, shouldn't this function technically work and change the label text? I've been running it for a while now and every time it fails. I've made sure that all of my outlets are linked properly as well. Sorry if the answer is obvious, I'm new to Swift.
func changeLabel() {
DispatchQueue.main.sync(execute: {
self.testText.text = "YES"
})
}
override func viewDidLoad() {
super.viewDidLoad()
let city:String? = nil
if city == nil {
changeLabel()
}
}
viewDidLoad is always called from the main thread (unless a programmer mistakenly does otherwise - and that's a whole other problem).
So there is no point to using DispatchQueue.main.sync to update the label. In fact, it's bad in this case. Calling DispatchQueue.main.sync when already on the main queue will cause the app's user interface to hang until the app is killed.
You have two choices:
Remove the use of DispatchQueue.main.sync since it's not needed in the code you posted.
Change sync to async. This fixes the problem with the app user interface hanging and it also allows you call the changeLabel method from any queue and work properly.
Use this instead:
func changeLabel() {
DispatchQueue.global().async(execute: {
print("teste")
DispatchQueue.main.sync{
self.testText.text = "YES"
}})
Thanks

UIView doesn't change at runtime

I've had this working in other variations but something seems to elude me in the change from objective-c to swift as well as moving some of the setup into it's own class.
So i have:
class ViewController: UIViewController, interfaceDelegate, scrollChangeDelegate{
let scrollControl = scrollMethods()
let userinterface = interface()
override func viewDidLoad(){
super.viewDidLoad()
loadMenu("Start")
}
func loadMenu(menuName: String) {
userinterface.delegate = self
userinterface.scrollDelegate = self
userinterface.removeFromSuperview() //no impact
scrollControl.removeFromSuperview() //no impact
userinterface.configureView(menuName)
view.addSubview(scrollControl)
scrollControl.addSubview(userinterface)
}
}
This sets everything up correctly but the problem occurs when I change loadMenu() at runtime. So if the user calls loadMenu("AnotherMenu") it won't change the UIView. It will call the right functions but it won't update the view. Although if I call loadMenu("AnotherMenu") at the start, the correct menu will display. Or if I call loadMenu("Start") and then loadMenu("AnotherMenu") then the menu displayed will be "AnotherMenu". As in:
override func viewDidLoad(){
super.viewDidLoad()
loadMenu("Start")
loadMenu("AnotherMenu")
}
When I list all the subviews each time loadMenu() is called, they look correct. Even during runtime. But the display is not updated. So something isn't getting the word. I've tried disabling Auto Layout after searching for similar issues but didn't see a difference.
Try adding setNeedsDisplay() to loadMenu
Eg
func loadMenu(menuName: String) {
userinterface.delegate = self
userinterface.scrollDelegate = self
userinterface.removeFromSuperview() //no impact
scrollControl.removeFromSuperview() //no impact
userinterface.configureView(menuName)
view.addSubview(scrollControl)
scrollControl.addSubview(userinterface)
view.setNeedsDisplay()
}
setNeedsDisplay() forces the view to reload the user interface.
I didn't want to post the whole UIView class as it is long and I thought unrelated. But Dan was right that he would need to know what was going on in those to figure out the answer. So I created a dummy UIView class to stand in and intended to update the question with that. I then just put a button on the ViewController's UIView. That button was able to act on the view created by the dummy. So the problem was in the other class. Yet it was calling the methods of the ViewController and seemingly worked otherwise. So then the issue must be that its acting on an instanced version? The way the uiview class worked, it uses performSelector(). But in making these methods into their own class, I had just lazily wrote
(ViewController() as NSObjectProtocol).performSelector(selector)
when it should have been
(delegate as! NSObjectProtocol).performSelector(selector)
so that was annoying and I wasted the better part of a day on that. But thanks again for the help.

How to disable auto-complete when running Xcode UI Tests?

As part of my UI Tests, I'm generating a random string as titles for my objects. The problem is that when this title is input via a keyboard (using XCUIElement.typeText()), iOS sometimes accepts an auto-suggested value instead.
For example, I may want it to type an auto generated string of "calg", but auto correct will choose "calf" instead. When I try to look for this value later on with an assertion, it doesn't exist and fails incorrectly.
Is there a way to tell the UI tests that they shouldn't be using auto correct, or are there an workarounds I can use?
Unless you need auto-suggest for any test scenarios, did you try turning off auto-correction in device/simulator settings.
Settings-> General -> Keyboard -> Auto-Correction
I don't believe you can turn off auto-correction through code from your UI Testing target.
You can, however, turn it off for the individual text view from your production code. To make sure auto-correction is still on when running and shipping the app, one solution would be to subclass UITextField and switch on an environment variable.
First set up your UI Test to set the launchEnvironment property on XCUIApplication.
class UITests: XCTestCase {
let app = XCUIApplication()
override func setUp() {
super.setUp()
continueAfterFailure = false
app.launchEnvironment = ["AutoCorrection": "Disabled"]
app.launch()
}
func testAutoCorrection() {
app.textFields.element.tap()
// type your text
}
}
Then subclass (and use) UITextField to look for this value in the process's environment dictionary. If it's set, turn auto-correction off. If not, just call through to super.
class TestableTextField: UITextField {
override var autocorrectionType: UITextAutocorrectionType {
get {
if NSProcessInfo.processInfo().environment["AutoCorrection"] == "Disabled" {
return UITextAutocorrectionType.No
} else {
return super.autocorrectionType
}
}
set {
super.autocorrectionType = newValue
}
}
}
Here is how I disabled it in my UI Test
app.textFields.element(boundBy: 0).tap()
let keyboards = app.keyboards.count
XCTAssert(keyboards > 0, "You need enable the keyboard in the simulator.")
app.buttons["Next keyboard"].press(forDuration: 2.1)
let predictiveOn = app.switches["Predictive"].value as! String == "1"
if predictiveOn {
app.switches["Predictive"].tap()
} else {
app.buttons["Next keyboard"].tap()
}
app.buttons["Next keyboard"].press(forDuration: 2.1)
let predictiveOff = app.switches["Predictive"].value as! String == "0"
XCTAssert(predictiveOff, "Predictive mode is not disabled")
app.buttons["Next keyboard"].tap()

Swift2 override of highlighted/selected doesn't work

I'm creating a custom UIButton class and i'm trying to override the highlighted/selected methods but they aren't called. After a bit of searching i found that this code should be working:
override var highlighted: Bool {
didSet {
if highlighted {
self.backgroundColor = UIColor.whiteColor()
} else {
self.backgroundColor = UIColor.blackColor()
}
}
}
I did the same for selected. I also tried using willSet but no luck. I'm using swift2.0. Could that make the difference? Anyone knows why it isn't called?
You're going about this all wrong. No need to subclass. Just call setBackgroundImage:forState: with a black image for one state and a white image for the other.
Issue fixed. Due to the fact that i'm wokrking on an SDK shared library, I had to define the Module of my view controller and the Module of my button class. Once I did those, everything was working fluently.

Resources