2

I am testing the existence of a table view cell and the following code works perfectly fine on an iPhone 7:

let complaintCell = self.app.tables.cells.element(boundBy: 0)
XCTAssert(complaintCell.exists)
complaintCell.tap()

Now the problem is that if I run the same test on an iPad where the view controller is embedded inside a split view controller, the test fails:

enter image description here

The table view is still visible on the master view controller:

enter image description here

So I can't find why the test fails, even if the table view is the only visible one. Any hint?

Full code:

func testNavigation() {
    let complaintCell = self.app.tables.cells.element(boundBy: 0)
    XCTAssert(complaintCell.exists)
    complaintCell.tap()

    XCTAssert(self.app.navigationBars["Complaint #100"].exists)
    XCTAssertFalse(self.app.navigationBars["Complaint #99"].exists)

    let editButton = self.app.buttons["editComplaint"]
    XCTAssert(editButton.exists)
    editButton.tap()

    XCTAssert(self.app.navigationBars["Complaint #100"].exists)
    XCTAssertFalse(self.app.navigationBars["Complaint #99"].exists)

    let saveButton = self.app.buttons["Save"]
    XCTAssert(saveButton.exists)
    saveButton.tap()

    let okButton = self.app.buttons["Ok"]
    XCTAssert(okButton.exists)
    okButton.tap()
}

Update

I was able to isolate the problem: if I just create a new project, a master detail application and I set the accessibility identifier of the main table view, and then I test for its existence, the test fails:

let table = self.app.tables["TableView"]
XCTAssert(table.waitForExistence(timeout: 5.0))

The steps necessary in order to reproduce this problem are very simple, you just need to create a master detail application, set the accessibility identifier of the table view and then run the above code. But if you want you can also clone this repository, which I used as a demo to isolate the problem: https://github.com/ralzuhouri/tableViewTestDemo

Ramy Al Zuhouri
  • 21,580
  • 26
  • 105
  • 187
  • Does the app have just the one tableview? Would you be able to test first to make sure that the table itself can be accessed? A separate simple UI Test just to check the table's existence. And you can print logs as well. Something like this:) `print("\napp.tables.count: \(app.tables.count)\n") print("\napp.tables.element(boundBy: 0): \(app.tables.element(boundBy: 0))\n") XCTAssert(app.tables.count > 0) XCTAssert(app.tables.element(boundBy: 0).exists)` – Wattholm Oct 23 '19 at 02:18

4 Answers4

2

I encountered the same problem when trying to test for tableview cell existence on the simulator. On a simulated iPhone device the test would succeed, whereas on an iPad device it would fail.

I found that the problem lay in the fact that a UITest that references a table will fail if the app's current view does not have the tableview whose data you would like to test. On an iPhone the view by default had a back button that would transition the app from its detail view back to the master viewcontroller which contained the tableview. On the iPad simulator this back button was not there, and so it could not transition correctly to the tableview's view, making the entire test fail.

func testTableCellsExist() {

    let app = XCUIApplication()
    app.launch()

    app.navigationBars["AppName.DetailView"].buttons["Root View Controller"].tap()

    let tablesQuery = app.tables["MasterTable"]

    let testCell = tablesQuery.cells.element(boundBy: 49)
    XCTAssert(tablesQuery.cells.count == 50)
    XCTAssert(testCell.exists)
}

What I did to make the test succeed for both iPad and iPhone device simulations was to make it so that the app would launch and display the tableview's viewcontroller at the outset. This was done by adding this code to the UISplitViewController swift file:

class SplitViewController: UISplitViewController {

override func viewDidLoad() {
    super.viewDidLoad()
    self.delegate = self
    self.preferredDisplayMode = .allVisible
}

}

extension SplitViewController: UISplitViewControllerDelegate {
    func splitViewController(
             _ splitViewController: UISplitViewController,
             collapseSecondary secondaryViewController: UIViewController,
             onto primaryViewController: UIViewController) -> Bool {
        // Return true to prevent UIKit from applying its default behavior
        return true
    }

}

The explanation for the above code can be found here:

Open UISplitViewController to Master View rather than Detail

Regardless, after the above modification, the assertions for tableview cell existence should now succeed even on an iPad because the view is now correctly set to the view which has the tableview being queried.

If you don't want your app to start with this view by default, then you'll have to make sure that your test code transitions to the tableview's view before the assertions are made.

Also, if you follow my example through, be sure to remove this line in the test case, because it no longer becomes necessary to navigate to the tableview's VC after the modifications to the UISplitViewController code:

app.navigationBars["AppName.DetailView"].buttons["Root View Controller"].tap()

UPDATE (October 25): Master Detail App Project - Basic TableView Existence Test

I attempted to create a basic Master Detail app as you suggested, and tried the test as well. A basic test for the tableview failed again with me when I selected an iPad device to simulate, because it shows only the detail view (no table). I modified my test so that if the device is an iPad, it will check its orientation, and set the landscape orientation as required, before checking for the table's existence. I only modified the test and nothing else, and what was previously a failure became a success. I also set the accesibility identifier for the tableview in the MasterVC's viewDidLoad, but I believe the results would be the same whether we set the identifier or not. Here's the test code:

func testExample() {
    // UI tests must launch the application that they test.
    let app = XCUIApplication()
    app.launch()

    // Use recording to get started writing UI tests.
    // Use XCTAssert and related functions to verify your tests produce the correct results.

    if UIDevice.current.userInterfaceIdiom == .pad {

        print("TESTING AN IPAD\n")
        print(XCUIDevice.shared.orientation.rawValue)

        // For some reason isPortrait returns false
        // If rawValue == 0 it also seems to indicate iPad in portrait mode
        if (XCUIDevice.shared.orientation.isPortrait || XCUIDevice.shared.orientation.rawValue == 0){
            XCUIDevice.shared.orientation = .landscapeRight
        }
        XCTAssert(app.tables["MyTable"].exists)
        //XCTAssert(app.tables["MyTable"].waitForExistence(timeout: 5.0))
    } else if UIDevice.current.userInterfaceIdiom == .phone {
        print("TESTING AN IPHONE\n")
        XCTAssert(app.tables["MyTable"].exists)
    }


    // XCTAssert(app.tables["MyTable"].exists)

}

I added an else for the iPhone case, and print logs to say which type of device is being tested. Whether .exists or .waitForExistence is used, the tests should always succeed. But as was pointed out, .waitForExistence is better in the case where the table view takes time to load up.

Hope this helps. Cheers!

UPDATE (OCTOBER 27): Test Fails on a Real iPad

OP has discerned that the test -- which succeeds for simulated iPhone and iPad devices -- fails on a real device (for more information, please check his informative comment for this answer below). As the test failed on a real iPad which was already in landscape mode (see screenshot), it may be assumed that XC UITest functionality is broken in this regard.

In any case I hope these tests and results will prove helpful (they certainly taught me a thing or two) to others as well.

Cheers :D

Wattholm
  • 849
  • 1
  • 4
  • 8
  • For me it doesn't work. Even if I set the preferred display mode to .allVisible and I implement that delegate method, I see both the primary and the secondary on screen, and the test keeps failing. – Ramy Al Zuhouri Oct 21 '19 at 17:55
  • Hmm, perhaps if you show some more of the relevant code in the app? Like how and where the tableview is populated, as well as the UI Test script. Also, the debug console's messages might provide a clue to the reason the test fails. Finally, also make sure you are referencing the correct cell in the correct tableview (use accessibility or label identifiers), in case you have more than one. – Wattholm Oct 21 '19 at 23:31
  • Now in my opinion it would be too complicated for you and other reviewers to check how the table view is populated, it involves a lot of code and it's also supposed to be proprietary code. But what I can say is that the table view is totally visible when the UI test is run, so I wouldn't worry about that. I edited the question to include the test function. – Ramy Al Zuhouri Oct 22 '19 at 19:22
  • You might want to check my latest update in the question, because I was able to isolate the problem. – Ramy Al Zuhouri Oct 24 '19 at 17:12
  • @RamyAlZuhouri, thanks for letting me know. I have updated my answer as well. The test code I added has a very basic orientation check and set, that made it succeed after it also initially failed for a first run on an iPad (simulator). I tested on different iPad simulators (XCode 11.1). – Wattholm Oct 25 '19 at 02:55
  • Damn xCode, never trust it! the test fails on a real device, but it succeeds in the simulator; on a real device the xcuitest functionality seem to be broken (iPad only). This is why my test was failing in the first place (indeed if you check my first uploaded screenshot, it was in landscape). I could add the answer myself and accept it, but since you already have one and there is already valuable information, if you just edit it and add this detail, I will accept it and give you the bounty. – Ramy Al Zuhouri Oct 27 '19 at 09:06
  • @RamyAlZuhouri That would be great! I have updated my answer once again. I hope it suitably incorporates your findings. I do have an itch to test a bit more if I can find an actual iPad handy. Might take time though. In any case, I will certainly post back here if I come upon any more relevant information. Thanks! – Wattholm Oct 27 '19 at 10:22
  • "It should be noted that we cannot force orientation settings on a real device unlike for simulators" for me that's possible, the device actually rotates. Which version of xcode are you using? – Ramy Al Zuhouri Oct 27 '19 at 18:02
  • it was an erroneous assumption, and I should not have included it in the edit. I was not actually testing on real devices. I have removed the offending statement. Latest XCode though, I believe - 11.1 – Wattholm Oct 27 '19 at 22:15
  • I edited the answer again for clarity, especially the last bit about the test failing on an actual device, even though it succeeds on simulators. Quite a vexing result indeed :D – Wattholm Oct 27 '19 at 22:35
1

Your problem seems to be with the orientation of the device as I noticed when trying it on your test project. I have managed to come up with two solutions, both work perfectly choose between 1 or 1a as per comments in code. Option 1 will work on both iPhones and iPads option 1a is iPad specific or the larger iPhones which display the master in landscape as well.

First set the tableViews accessibility identifier in your code to easily reference the correct tableView:

complaintTableView.accessibilityIdentifier = "ComplaintsTableView"

Then in your tests just reference the identifier - here is the full test according to your question above:

func testNavigation() {
    // 1 - Hit the back button on the collapset master view
    let masterButton = app.navigationBars.buttons.element(boundBy: 0)
    if masterButton.exists {
        masterButton.tap()
        print("BACK TAPPED")
    }
    // 1a - Or just use the line below to rotate the view to landscape which will automatically show the master view
    XCUIDevice.shared.orientation = UIDeviceOrientation.landscapeRight

    let complaintsTable = app.tables["ComplaintsTableView"]
    XCTAssertTrue(complaintsTable.exists, "Table does not exist")
    let complaintCell = complaintsTable.cells.firstMatch
    XCTAssert(complaintCell.exists)
    complaintCell.tap()

    XCTAssert(self.app.navigationBars["Complaint #100"].exists)
    XCTAssertFalse(self.app.navigationBars["Complaint #99"].exists)

    let editButton = self.app.buttons["editComplaint"]
    XCTAssert(editButton.exists)
    editButton.tap()

    XCTAssert(self.app.navigationBars["Complaint #100"].exists)
    XCTAssertFalse(self.app.navigationBars["Complaint #99"].exists)

    let saveButton = self.app.buttons["Save"]
    XCTAssert(saveButton.exists)
    saveButton.tap()

    let okButton = self.app.buttons["Ok"]
    XCTAssert(okButton.exists)
    okButton.tap()
}
AD Progress
  • 4,190
  • 1
  • 14
  • 33
0

You shall probably use waitForExistence() instead of .exists

.exists executes as soon as it is called. You TableView may not be prepared to be tested at this moment.

I would try to replace .exists with waitForExistence()

let complaintCell = self.app.tables.cells.element(boundBy: 0)
XCTAssert(complaintCell.waitForExistence(timeout: 10))
complaintCell.tap()

Also you can ditch this XCTAssert.

tap() will wait for existence for 3 seconds and then produce an error message, if the cell does not exist.

The shortened version will be:

app.tables.cells.firstMatch.tap()
Roman Zakharov
  • 2,185
  • 8
  • 19
0

I've downloaded your demo.

While iPhone app start with Master View, the iPad app goes with SplitView or Detail View (in portrait orientation).

Portrait View should start with Master + Detail View. Other option to change UI tests like this:

func testExample() {
    let table = self.app.tables["TableView"]
    if !table.waitForExistence(timeout: 1) {
        app.navigationBars.element.swipeRight()
        XCTAssert(table.waitForExistence(timeout: 2))
    }
    ...
}
Roman Zakharov
  • 2,185
  • 8
  • 19