XCUIElement exists, but is not hittable - xcode

In my UI tests, I create a UIView programmatically using (shortened)
let topMarker = UIView.init(frame: CGRect.init())
…
topMarker.accessibilityIdentifier = kTopMarker
The topMarker view is in a custom table view cell.
In my UI tests, I use
let new1stCell = app.cells.element(boundBy: 0)
let topMarker = new1stCell.otherElements[kTopMarker]
let topMarkerExists = topMarker.waitForExistence(timeout: 15)
XCTAssertTrue(topMarkerExists, "Top marker does not exist")
XCTAssertTrue(topMarker.isHittable, "Top marker is not hittable")
When I set a Test Failure breakpoint, the test stops at the last line, i.e. topMarker exists, but is not hittable.
On the other hand, I can see the view in the snapshot, i.e. it exists and is visible.
This is stange, because the docs say:
isHittable returns true if the element exists and can be clicked, tapped, or pressed at its current location. It returns false if the element does not exist, is offscreen, or is covered by another element.
I thought, maybe it is visible, but cannot be clicked, tapped, or pressed, because userInteractionEnable is not true, but even if I set this property to true, the view does not become hittable.
What am I missing?

Problem solved:
An XCUIElement is only hittable, if its isAccessibilityElement property is set to true.
The docs to property isAccessibilityElement say
The default value for this property is false unless the receiver is a standard UIKit control, in which case the value is true.
Assistive applications can get information only about objects that are represented by accessibility elements. Therefore, if you implement a custom control or view that should be accessible to users with disabilities, set this property to true.
My UIView that I instantiated programmatically is not a standard UIKit control. As soon as I added
topMarker.isAccessibilityElement = true
the test
XCTAssertTrue(topMarker.isHittable, "Top marker is not hittable")
succeeded.

Related

NSOutlineView reloadItem/reloadData not working when replacing an item

I have a view-based NSOutlineView with a dataSource/delegate model instead of binding to a tree controller (I want control over the insert/update animations).
I'm replacing an item in my model and would like to update that specific row in the outline view without having to call reloadData().
I cannot get this to work. Either the item does not update at all or the item's expanded state doesn't update. There seems to be some caching being done inside of NSOutlineView according to this, but even with these suggestions, I could not get it to work.
What I have:
(1) The outline view represents a folder structure
(2) At first, there is a singe file:
(3) The file is then replaced with a folder item:
// Model update
let oldFileItem = rootItem.children.first!
rootItem.children.remove(at: 0)
rootItem.children.append(Item(title: "Folder", children:[], isExpandable:true))
Expected result:
Actual result (reloadItem):
outlineView.reloadItem(oldFileItem) // I kept a reference
Icon and title reloaded, but note that the expansion triangle is missing.
I can somewhat understand that reloadItem() might not work in this case, because the old item is not part of the data model anymore. Strangely enough, the item's title and icon update, but not the expansion state.
Actual result (reloadData(forRowIndexes:columnIndexes:):
outlineView.reloadData(forRowIndexes: IndexSet(integer:0), columnIndexes: IndexSet(integer:0))
No effect whatsoever. This is the one that I would have expected to work.
Actual result (remove/insert):
outlineView.removeItems(at: IndexSet(integer:0), inParent: rootItem, withAnimation: [])
outlineView.insertItems(at: IndexSet(integer:0), inParent: rootItem, withAnimation: [])
No effect whatsoever.
The docs say about removeItems(): "The method does nothing if parent is not expanded." and isExpanded does indeed return false for the root node, although its children are visible. Is this special behavior for items that are direct children of the root node? What am I missing here?
For reference, my data model:
class Item:NSObject {
var title:String
var children:[Item]
var isExpandable:Bool
init(title:String, children:[Item], isExpandable:Bool) {
self.title = title
self.children = children
self.isExpandable = isExpandable
}
}
For reference:
It turned out to be an issue with how I used the API. NSOutlineView.removeItems/insertItems expect nil for the inParent parameter for the root item. I was handing in the actual root item. Using nil instead of the root item solved the problem.

Xcode UITest scrolling to the bottom of an UITableView

I am writing an UI test case, in which I need to perform an action, and then on the current page, scroll the only UITableView to the bottom to check if specific text shows up inside the last cell in the UITableView.
Right now the only way I can think of is to scroll it using app.tables.cells.element(boundBy: 0).swipeUp(), but if there are too many cells, it doesn't scroll all the way to the bottom. And the number of cells in the UITableView is not always the same, I cannot swipe up more than once because there might be only one cell in the table.
One way you could go about this is by getting the last cell from the tableView. Then, run a while loop that scrolls and checks to see if the cell isHittable between each scroll. Once it's determined that isHittable == true, the element can then be asserted against.
https://developer.apple.com/documentation/xctest/xcuielement/1500561-ishittable
It would look something like this (Swift answer):
In your XCTestCase file, write a query to identify the table. Then, a subsequent query to identify the last cell.
let tableView = app.descendants(matching: .table).firstMatch
guard let lastCell = tableView.cells.allElementsBoundByIndex.last else { return }
Use a while loop to determine whether or not the cell isHittable/is on screen. Note: isHittable relies on the cell's userInteractionEnabled property being set to true
//Add in a count, so that the loop can escape if it's scrolled too many times
let MAX_SCROLLS = 10
var count = 0
while lastCell.isHittable == false && count < MAX_SCROLLS {
apps.swipeUp()
count += 1
}
Check the cell's text using the label property, and compare it against the expected text.
//If there is only one label within the cell
let textInLastCell = lastCell.descendants(matching: .staticText).firstMatch
XCTAssertTrue(textInLastCell.label == "Expected Text" && textInLastCell.isHittable)
Blaines answer lead me to dig a little bit more into this topic and I found a different solution that worked for me:
func testTheTest() {
let app = XCUIApplication()
app.launch()
// Opens a menu in my app which contains the table view
app.buttons["openMenu"].tap()
// Get a handle for the tableView
let listpagetableviewTable = app.tables["myTableView"]
// Get a handle for the not yet existing cell by its content text
let cell = listpagetableviewTable.staticTexts["This text is from the cell"]
// Swipe down until it is visible
while !cell.exists {
app.swipeUp()
}
// Interact with it when visible
cell.tap()
}
One thing I had to do for this in order to work is set isAccessibilityElement to true and also assign accessibilityLabel as a String to the table view so it can be queried by it within the test code.
This might not be best practice but for what I could see in my test it works very well. I don't know how it would work when the cell has no text, one might be able to reference the cell(which is not really directly referenced here) by an image view or something else. It's obviously missing the counter from Blaines answer but I left it out for simplicity reasons.

Preselecting a NSComboBox Entry

I have a modal window that contains a NSComboBox. "Uses Data Source" is set to turn and the combobox correctly lists the entries. The view controller is the delegate for the data source. When I call the modal window to update an existing object I would like the combobox to display the selected entry if there is one. How do I do this?
I've tried to access the entries in viewWillAppear. I get and error saying there are no entries. The various print statements I have in the code indicate that the values aren't loaded until the drop down arrow is clicked. The two function I'm supplying as the delegate are:
func numberOfItems(in comboBox: NSComboBox) -> Int
func comboBox(_ comboBox: NSComboBox, objectValueForItemAt index: Int) -> Any?
Would it be possible to set the selected entry in the second function above?
As NSComboBox is a subclass of NSTextField you should be able to set its text by setting the stringValue property of your comboBox.
self.comboBox.stringValue = "Hello World"

Binding and NSPopUpButton: changing the value of the current key doesn't change the selection

I've a really simple UI with a single NSPopUpButton. Its selectedIndex is bound to an int value ViewController.self.my_selection. As soon as I change the selection from the UI (i.e. a select the third item of the NSPopUpButton) I see that my_selection value changes. So far so good, what I'm trying to obtain is the opposed direction though. I want to change the my_selection value programmatically and see the NSPopUpButton selecting the item a the index that I've defined in my_selection. I erroneously supposed that behaviour was the default behaviour for bindings...
This is what I'm obtaining now:
NSPoPUpButton ---> select item at index 2 ----> my_selection becomes equal to 2
This is what I want to achieve (keeping also the previous behaviour)
my_selection ---> set value to 3----> NSPoPUpButton selected index = 3
Without a bit more info (see my comment) it's hard to see exactly what you're doing wrong. Here's how I got it working: First, create a simple class...
// Create a simple class
class Beatle: NSObject {
convenience init(name: String) {
self.init()
self.name = name
}
dynamic var name: String?
}
Then, in the AppDelegate I created a four-item array called beatles:
dynamic var beatles: [Beatle]?
func applicationDidFinishLaunching(aNotification: NSNotification) {
beatles = [Beatle(name: "John"),
Beatle(name: "Paul"),
Beatle(name: "Ringo"),
Beatle(name: "George")]
}
In Interface Builder I set things up so that this array provides the pop-up with its content:
This class also has a selectedIndex property that is bound to the pop-up button's selectedIndex binding - this property provides read-write access to the pop-up button's selection:
// Set the pop-up selection by changing the value of this variable.
// (Make sure you mark it as dynamic.)
dynamic var selectedIndex: Int = 0

Testing if an element is visible with Xcode 7 UITest

I want to verify if an element is visible or not depending on its .hidden property but I don't find a valid way to do that using the new Xcode 7 UI test stuff.
I've tried with myelement.exists and myelement.hittable but they doesn't seems to work as I expected. I suppose they work on conjunction with the hidden property. An hidden element shouldn't exists and is not hittable... but this is not the current behaviour (I can understand the exists behaviour... but a hidden element should be not hittable IMO).
Is there another way to verify the "hidden" property value?
As of Xcode 7.1 Beta 3, UI Testing does not currently support validating the visibility of an element. I suggest filing a radar to bring the necessary attention to Apple.
Xcode 7.1 has fixed this issue. hittable now checks to see if the element is correct.
1) I am testing the UI with swift in Xcode 7.3 and I using both .hittable and .exists for testing whether a label is hidden or not and they both work. I test for 'true' and 'false' to make sure either way agree with the result.
I have a label whose static text is "Track Info" and set to be hidden when app is first loaded, then later on I press a button to show the label, and here is the result after the label is shown.
// test fails
let trackInfoLabel = app.staticTexts["Track info"]
XCTAssertEqual(trackInfoLabel.exists, true)
XCTAssertEqual(trackInfoLabel.hittable, true)
// test passes
XCTAssertEqual(trackInfoLabel.exists, false)
XCTAssertEqual(trackInfoLabel.hittable, false)
// test passes
let trackInfoLabel = app.staticTexts["Track Info"]
XCTAssertEqual(trackInfoLabel.exists, true)
XCTAssertEqual(trackInfoLabel.hittable, true)
// test fails
XCTAssertEqual(trackInfoLabel.exists, false)
XCTAssertEqual(trackInfoLabel.hittable, false)
Leter on when I press the button to hide the label, all the results turned opposite. This confirms that both properties (hittable and exists) works for label.hidden setting.
2) Another way to find out if an element is hidden, you can do is element.frame.size.width == 0 || element.frame.size.height == 0
XCUIElement.hittable works for me (in my particular test case where I am checking several UIButton elements for visibility) - quite sure it is not a right way to do it though
Next code worked for me.
At the end of the test you can past code as follow:
while ([app.staticTexts matchingIdentifier:#"accesibilityId"].count > 0) {
sleep(1);
}
I agree hittable doesn't always work for buttons (Swift 2.0, XCode 7.2)
I just discovered that if button is visible, you can find it ONLY among buttons, while if button is hidden, you can find it's tag in staticTexts as well!
XCTAssertFalse(app.buttons["Log out"].exists && app.staticTexts["Log out"].exists) // This button is visible (hidden = false)
XCTAssert(app.buttons["Log in"].exists && app.staticTexts["Log in"].exists) // This one is hidden
Hacky, but works for buttons. Apple should just introduce .hidden or .visible along .hittable and .exists
The syntax is now .isHittable:
isHittable only returns true if the element is already visible and
hittable onscreen. It returns false for an offscreen element in a
scrollable view, even if the element would be scrolled into a hittable
position by calling click(), tap(), or another hit-point-related
interaction method.
Using the .isHittable property will work because hidden elements are not visible or hittable on screen.
My solution is to add accessibilityIdentifier dynamicly
func someMethod() {
label.isHidden = true
label. accessibilityIdentifier = "isHidden"
}
func someOtherMethod {
label.isHidden = false
label. accessibilityIdentifier = "isVisible"
}
and the in UITest you can use it as
if app.staticTexts["MyLabel"].identifier == "isHidden" {
dosomething()
}

Resources