Skip to content

FxScreenGraph and XCUITest

isabelrios edited this page Feb 5, 2018 · 2 revisions

Overview

When writing a XCUITest, most of the test actions are about navigating within Firefox iOS app. Defining these actions individually on each test is cumbersome, and leads to a lot of duplicated code. If the app's UI is updated, each tests would need to be updated individually as well. There should be a smarter way of doing the UI navigation, and it would be better if we can just define the current state, the desired end state and how to get it for the XCUITest to automatically navigate to the intended UI page.

Hence the screengraph.

Idea

Basically FxScreenGraph is an implementation of the graph network. The app is described as a graph of screen states. Each UI page can be defined as a node, and each node has information regarding where it can go from the current node. Each node's information can be defined with addScreenState() method. It uses the screen graph definition given by MappaMundi

For example,

 addScreenState(BrowserTabMenu) { screenState in
    screenState.tap(app.tables.cells["menu-Settings"], to: SettingsScreen)
    screenState.tap(app.tables.cells["menu-panel-TopSites"], to: HomePanel_TopSites)
    screenState.tap(app.tables.cells["menu-panel-Bookmarks"], to: HomePanel_Bookmarks)
    screenState.tap(app.tables.cells["menu-panel-History"], to: HomePanel_History)
    screenState.tap(app.tables.cells["menu-panel-ReadingList"], to: HomePanel_ReadingList)
    ...

says the following:

  • In Browser Tab Menu, tapping menu-settings goes to SettingsScreen Node
  • Tapping menu-panel-TopSites goes to HomePanel_TopSites Node
  • Tapping menu-panel-Bookmarks goes to HomePanel_Bookmarks Node

In addition to the screenStates, there are nodes on the graph that represent actions the user might take, these are the screenActions. An action could be a tap, type into a text field. The idea is to move all these interactions in the tests into actions. In general, the use of these actions should not be for navigation but for changing the user state.

For example, in FxScreenGraph:

map.addScreenState(NewTabChoiceSettings) { screenState in
    let table = app.tables["NewTabPage.Setting.Options"]
    screenState.gesture(forAction: Action.SelectNewTabAsBlankPage) { UserState in
       table.cells["Blank"].tap()
    }
    screenState.gesture(forAction: Action.SelectNewTabAsBookmarksPage) { UserState in
       table.cells["Bookmarks"].tap()
    }
    screenState.gesture(forAction: Action.SelectNewTabAsHistoryPage) { UserState in
       table.cells["History"].tap()
    }
}

And then tests can perform actions using navigator. With this action user can change the setting selected:

navigator.performAction(Action.SelectNewTabAsBookmarksPage)

There is also a way to keep the state of the app for a screen state and to communicate that data from the test to the graph. This is possible thanks to the concept of userState. There are already a few defined under the FxUserState class, more can be added as per needs.

class FxUserState: MMUserState {
    var isPrivate = false
    var url: String? = nil
    var passcode: String? = nil
    var fxaUsername: String? = nil
    var fxaPassword: String? = nil
    var numTabs: Int = 0
    ...

Here an example about how it can be used. In FxScreenGraph:

map.addScreenState(SetPasscodeScreen) { screenState in
    screenState.gesture(forAction: Action.SetPasscode, transitionTo: PasscodeSettings) { userState in
       type(text: userState.newPasscode)
       type(text: userState.newPasscode)
       userState.passcode = userState.newPasscode
    }
    screenState.gesture(forAction: Action.SetPasscodeTypeOnce) { userState in
       type(text: userState.newPasscode)
    }
    screenState.backAction = navigationControllerBackAction
    }

In the tests a new passcode is selected and it is set with the corresponding action, SetPasscode.

userState.newPasscode = “123456”
navigator.performAction(Action.SetPasscode)

Also thanks to a property of the actions they can lead user to another ScreenState. In the example above, once the new passcode is set, the new Node available is PasscodeSettings:

screenState.gesture(forAction: Action.SetPasscode, transitionTo: PasscodeSettings)

There is another helpul concept, predicates. Thanks to that is possible to make the navigator choose different routes depending on a variable, for example the userState or the iOS device.

Example depending on the userState:

map.addScreenState(PasscodeSettings) { screenState in
    screenState.backAction = navigationControllerBackAction
    let table = app.tables.element(boundBy: 0)
    screenState.tap(table.cells["TurnOnPasscode"], to: SetPasscodeScreen, if: "passcode == nil")
    screenState.tap(table.cells["TurnOffPasscode"], to: DisablePasscodeSettings, if: "passcode != nil”)
}

That means that if there is not any passcode set, user will be routed to SetPasscodeScreen screen, on the contrary if there is a passcode already set user will go to DisablePasscodeSettings when entering in PassscodeSettings screen.

Depending on the iOS device:

screenState.tap(app.tables["Context Menu"].cells["action_remove"], forAction: Action.CloseTabFromPageOptions, Action.CloseTab, transitionTo: HomePanelsScreen, if: "tablet != true”)

Here the action Action.CloseTabFromPageOptions is only available if the device is not an iPad. This predicate is necessary in some transitions due to the different buttons/paths implemented in iPhone or iPad.

There are also instances where it's not easy to define as a node, and that are used in different test suites. In order to avoid duplication there are methods too in the FxScreenGraph:

func closeAllTabs() {
   let app = XCUIApplication()
   app.buttons["TabTrayController.removeTabsButton"].tap()
   app.sheets.buttons["Close All Tabs"].tap()
   self.nowAt(HomePanelsScreen)
}

Above code does the following:

  • Go to Tab Tray Menu UI
  • Tap Close All Tabs button
  • Inform screengraph that we're now in HomePanelsScreen Node.

In order to use screengraph (now known as MappaMundi) , a navigator object that 'traverses' the graph need to be instantiated in the beginning, it happens in BaseTestCase:

class BaseTestCase: XCTestCase {
    var navigator: MMNavigator<FxUserState>!
    var app: XCUIApplication!
    var userState: FxUserState!

Then, use goto() call to see the desired UI state. For example, simply calling navigator.goto(BrowserTabMenu) will make the test to open the Browser Tab Menu, by following the logic inside the FxScreenGraph.swift.

It is strongly recommended to use FxScreenGraph for your XCUITest, and make updates to FxScreenGraph if there is a new UI change or new UI to be tested.


Clone this wiki locally