-
Notifications
You must be signed in to change notification settings - Fork 91
Improve state mocking #111
Description
This is a proposal on how to improve UI testing in the Katana environment
The problem
We push a lot people to use the global application state to store information. This has several advantages such more or less related to testability. The key point in this area is that, thanks to the props mechanism, it is very easy to mock the ui in a certain state (and then take screenshots, check conditions on the returned children and so on). Most of the time this works, but there are some cases where it is simply not possible to store certain information about the UI in the global state. In these cases we should use the internal NodeDescription state. This has a huge impact when it comes to reproduce some UI states, since it is simply not possible to easily mock them. Here are a list of (some) cases we encountered in our applications:
- Highlighted state of the Button / Table (or Grid) cells / anything has an highlighted state
- Failed/Loading state of the
Imagecomponent that take the image from a remote URL (or the camera roll or any async source really)
We end up fixing the issue with more or less hackish ways which are not related each other and that are tied to the specific problem.
This issue wants to created an unified method to handle all these cases.
Proposed Solution
Technically speaking, the initialization/handling of the node states is deep inside the Node instances that back the descriptions / UIs. The key idea is to create an object that holds the logic to mock the state in the Katana UI. When a node initializes itself, instead of using the default state (that is, invoke the init() method on the state type), it will use this object to get the proper state, if any. If there isn't any mocked state it will fallback to the default case (the empty init).
From the API perspective this is how you would use it
let renderer = Renderer(rootDescription: description, store: store, stateMockProvider: stateMockProvider)Basically, the Renderer takes one extra argument that is used to understand how to mock the state, if needed.
Here is how to create a stateMockProvider:
let mock = StateMockProvider()
mock.mockState(ADescription.State(), for: ADescription.Type, passingFilter: { props in
// return true or false based on the props
})The idea is that you can ask Katana to use the first parameter (the mocked state) for a specific description type, when the filter returns true. You can use the filter to only highlight specific buttons or specific table rows.
Filters are evaluated in the order are passed to the provider, and the first valid state will be picked.
One important thing to note is that the highlighted state is permanent. In case of live screenshots (e.g., test screens of the app where is possible to interact with the UI) the update of the state will be ignored. This is to ensure consistency and avoid things like: "The button is highlighted, you tap on it and the highlight disappear (since the update function will be invoked and the highlighted state removed)". If a state is not mocked, everything will work as usual.
A real world example would be the following:
// supopose that we have a Table element, where each cell has the following props
struct TableCellProps {
// usual props for frame, alpha, ..
let index: Int
}
// and the following state
struct TableCellState {
let isHighlighted: Bool
}
// we want to highlight only the first row of each table, here is the mock state
let highlightedState = TableCellState(isHighlighted: true)
mockStateProvider.mockState(highlightedState, for: Table.self, passingFilter: { (props: TableCellProps) -> Bool in
return props.index == 1
})Technical Details
The idea is to leverage the renderer property in the Node to access to the state mock. This value is used to mock the state, if possible.
The node should also check whether an instance of state mock is available and, if this is the case, skip the update phase when the state changes.