I often have situations where the behavior I'm trying to achieve is like this:
I've often found myself adding multiple timeouts to my tests to try to both assert that the optimistic update was made and then gets rolled back after rejection. So something like this:
it('rolls back optimistic update', async () => {
jest.mocked(doUpdate).mockReturnValue(new Promise((resolve, reject) => {
setTimeout(reject, 1000);
});
render(<App />)
await screen.findByText('not done')
userEvent.click(screen.getByText('do it'))
await screen.findByText('Done!')
await screen.findByText('not done')
});
But this has some pretty big downsides:
How can I test this type of behavior in a way that is as efficient as possible and robust against test environment changes?
I now use this helper function in situations like this, where I want to control the precise order of events and promises are involved:
export default function loadControlledPromise(mock: any) {
let resolve: (value?: unknown) => void = () => {
throw new Error('called resolve before definition');
};
let reject: (value?: unknown) => void = () => {
throw new Error('called reject before definition');
};
const response = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
jest.mocked(mock).mockReturnValue(response);
return { resolve, reject };
}
Using this helper function, the example test becomes:
it('rolls back optimistic update', async () => {
const { reject } = loadControlledPromise(doUpdate);
render(<App />)
await screen.findByText('not done')
userEvent.click(screen.getByText('do it'))
await screen.findByText('Done!')
reject();
await screen.findByText('not done')
});
This ensures: