-
Notifications
You must be signed in to change notification settings - Fork 2.9k
FxScreenGraph and XCUITest
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.
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.