Xcode 9.1 | XCUIElement.exists and XCUIElement.isHittable fail on different simulators

Hi,



Recently I discovered an odd behaviour with Xocde 9.1 / iOS 11.1 UI tests.


Running a simple UI tests to navigate between screens will randomly fail based on the simulator in use. For example, running the UI tests on an iPhone 6s simulator will fail most if not all the time on simple checks like the `exists` and `isHittable` properties on `XCUIElement`. While runnining on iPhone 8+ simulator appears to work most of the time. Running the same tests on iOS 10 runtime reliably passes.


The current workaround I found is to use `waitForExistence` to wait for the next screen to appear before performing any checks. This however isn't ideal as it would mean all tests would require a wait after any action which is not very scalable nor desirable (what if the chosen timeout isn't enough?)


Perhaps I am missing something obvious ? Has anyone else encountered this? I've filed a radar (35334066).


Thanks



Sample App:


- UINavigatioController with two screens , Screen 1 & Screen 2

- Screen 1 has a UIBarButtonItem titled "Push" that pushes Screen 2


Sample Tests:


import XCTest


class NavigationTests: XCTestCase {
  
    var app: XCUIApplication!
    override func setUp() {
        super.setUp()
      
        continueAfterFailure = false
        app = XCUIApplication()
        app.launch()
    }
  
    override func tearDown() {
        app = nil
        super.tearDown()
    }

    func testNavigaitonBetweenScreens() {
        XCTAssertTrue(screen1.exists)
        XCTAssertTrue(screen1.isHittable)
      
        pushScreen2.tap()
      
        // Checking the current screen no longer exists
        // will fail randomly depending on the simulator in use
        // e.g. most failures occur on iPhone 6s
        XCTAssertFalse(screen1.exists)
        XCTAssertTrue(screen2.exists)
        XCTAssertTrue(screen2.isHittable)
      
        backToScreen1.tap()
      
        XCTAssertFalse(screen2.exists)
        XCTAssertTrue(screen1.exists)
        XCTAssertTrue(screen1.isHittable)
    }
  
    func testNavigaitonBetweenScreens2() {
        XCTAssertTrue(screen1.exists)
        XCTAssertTrue(screen1.isHittable)
      
        pushScreen2.tap()
      
        // Check the destination screen existing first
        // makes a difference
        XCTAssertTrue(screen2.exists)
      
        // However may not pass the hittable check depending
        // on the simulator in use
        // e.g. most failures occur on iPhone 6s
        XCTAssertTrue(screen2.isHittable)
        XCTAssertFalse(screen1.exists)
      
        backToScreen1.tap()
      
        XCTAssertTrue(screen1.exists)
        XCTAssertTrue(screen1.isHittable)
        XCTAssertFalse(screen2.exists)
    }


    func testNavigaitonBetweenScreensWorkaround() {
        XCTAssertTrue(screen1.exists)
        XCTAssertTrue(screen1.isHittable)
      
        pushScreen2.tap()
      
        XCTAssertTrue(screen2.waitForExistence(timeout: 2))
        XCTAssertFalse(screen1.exists)
        XCTAssertTrue(screen2.exists)
        XCTAssertTrue(screen2.isHittable)
      
        backToScreen1.tap()
      
        XCTAssertTrue(screen1.waitForExistence(timeout: 2))
        XCTAssertFalse(screen2.exists)
        XCTAssertTrue(screen1.exists)
        XCTAssertTrue(screen1.isHittable)
    }
  
    // MARK: - Helpers
    var pushScreen2: XCUIElement {
        return app.navigationBars.buttons["Push"]
    }
  
    var screen1: XCUIElement {
        return app.otherElements["screens.sc1"]
    }
  
    var screen2: XCUIElement {
        return app.otherElements["screens.sc2"]
    }
  
    var backToScreen1: XCUIElement {
        return app.navigationBars.buttons["Screen 1"]
    }
}

Replies

Note also occurs on Xcode9.2 beta

'

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.'


That explains why on smaller screens versions the test fails as that element is not visible yet. Also .exists will always be True even if the element is not visible and its hidden on underneath another element.


Hope that helps!

Thanks for your reply.


In this case, it's a simple screen with a single view that is visible, no cells or offscreen elements.


Upon looking at the screenshots in the logs, it looks like it's attempting to evaluate the query during the push transition. The screenshot itself is somewhere in the middle of the transtion - that explains why it's failing. At the same time quite concerning as requiring a wait for basic operations like pushing a view controller seems like a regression (this wasn't needed on previous Xcode versions).

I'm having this problem more on Xcode 9.3 than I did in the past.
I have found that an XCUIElement can pass the exists test, yet still report "Test Assertion: Failed to determine hittability of <my table view cell> Other: Error copying attributes -25202". After updating to Xcode 9.3, I feel that checking for existence also greatly increases my testing period by a factor of 4x (1 hour test takes 4 hours!). Now I'm trying to run waitForExistence(timeout: 5) before asking table cells for any properties, and I'll see how it goes. But these tests now take so so long it takes a long time to collect proper data on the results.


I've peppered the test with sleep(5) here and there trying to give the system enough time to hit period of no activity before asking for any properties. But this is getting tedious. I feel like I need to add a sleep(5) after every single thing I do at this point.

Another quick update:


I have a table view with cells. Sometimes the first cell can be on screen (as seen in screenshots captured during testing), pass existence, yet fail isHittable. Cells 2 - 5 (for example) are all on screen, pass existence, and pass isHittable.


Run the test again, and it works without any problems. This only happens occasionally, on both 10.3 and 11.3, in Xcode 9.3.
So far, I've seen this behavior happen on the first cell of the table view.