We tend to create a "view model" object which maintains the state of the UI and interacts with the server. The actual view just forwards commands to this object, and after a command happens the view always re-renders based on the current state of the model. We do this on iOS but I've been pushing for us to do it with React as well since you get the built-in state rendering for free.
So all of the tests look something like this (here's a UI form):
// Arrange
let searchResponse = { usernameTaken: false };
let networkService = NetworkServiceTestDouble(searchResponse);
let vm = UserFormViewModel(
networkService: networkService
);
// Act
vm.username = "newusername@email.com";
vm.checkUsernameAvailable();
vm.submitForm();
// Assert
expect(networkService.calls[0].arguments[0]).to.eq({
username: "newusername@email.com"
});
So you see the view model is kind of like a Page Object, in that it has an API which represents the user's interaction. But, in this case the view model is what's used in the actual production code, it's not just a testing helper. So you just invoke the API as the user would when interacting with the feature, and verify that the correct data gets sent to the server when the form is submitted. After all, the client's job is primarily to interact with the server and maintain state. The feature is functional if the correct data is sent to the server.Then, let's say you're in React, hooking up this view model is quite easy:
import { UserFormViewModel } from "./UserFormViewModel";
let vm = UserFormViewModel(realNetworkService);
function UserForm() {
return (
<button onClick={vm.submitForm()}>Submit</button>
)
}
The buttons / inputs just get wired up to the appropriate view model functions and properties, and you can have the component's state just be the view model's state. The view is then just entirely about appearance and forwarding events.I'll then write some tests that actually render the UI, but they're just sanity checks. How often do you get a bug where you failed to call the correct function in response to a button click? The logic and wiring in response to all of the events is the more interesting part.
Edited to improve the code formatting a bit.