Jest DOM is a utility that extends jest with additional "matchers". Essentially additional methods I can use when I expect()
on an element. Some of these include toBeVisible, toBeValid, toBeInTheDocument, toBeChecked, toHaveStyle and toHaveClass.
For this lesson use toHaveClass
to ensure that our table is in the correct state. First it will show no special class when we first attempt to checkout, then after we attempt to checkout it will show the checkoutLoading
class and then after our API call completes trying to checkout with no items, we'll see our table will have the class checkoutError
.
In general testing for class names is a little bit brittle, but if we use this technique sparingly, such as in cases where we use classes to highlight an error. I think the approach is worth the risk.
We use the .not property of expect
here to explicitly say that our table should not have a class. This property will reverse whatever our matcher is looking for and can come in handy in a lot of situations.
Around 1:47 the code for the test 'cannot checkout with an empty cart' should be:
await userEvent.click(checkout); expect(table).toHaveClass('checkoutLoading'); waitFor(() => { screen.findByText('Cart must not be empty'); expect(table).toHaveClass('checkoutError'); });
Newlines didn't show in the above comment, so just make sure to put a new line after each semi-colon
Actually not sure about this one, but this is the only bit of the test I could get to pass:
test('Cannot checkout with an empty cart', async () => {
checkoutSpy.mockRejectedValueOnce(new Error('Cart must not be empty'));
const { debug } = renderWithContext(<Cart />);
const checkout = screen.getByRole('button', { name: 'Checkout' });
let table = screen.getByRole('table');
expect(table).not.toHaveClass('checkoutLoading');
await userEvent.click(checkout);
// This doesn't work // expect(table).toHaveClass('checkoutLoading');
expect(table).toHaveClass('checkoutError');
screen.getByText('Cart must not be empty');
});
So I was actually able to get the test to pass all the conditions using:
test('Cannot checkout with an empty cart', async () => {
checkoutSpy.mockRejectedValueOnce(new Error('Cart must not be empty'));
renderWithContext(<Cart />);
const checkout = screen.getByRole('button', { name: 'Checkout' });
let table = screen.getByRole('table');
expect(table).not.toHaveClass('checkoutLoading');
await waitFor(async () => {
await userEvent.click(checkout);
expect(table).toHaveClass('checkoutLoading');
});
setTimeout(() => {
expect(table).toHaveClass('checkoutError');
screen.getByText('Cart must not be empty');
}, 10);
});
My previous code is wrong because the timeout happens after the test has already run.
And I wasn't able to get it to work with mockRejectedValueOnce. (Do you know why?)
Sorry, the correct test code is:
test('Cannot checkout with an empty cart', async () => {
checkoutSpy.mockRejectedValue(new Error('Cart must not be empty'));
renderWithContext(<Cart />);
const checkout = screen.getByRole('button', { name: 'Checkout' });
const table = screen.getByRole('table');
expect(table).not.toHaveClass('checkoutLoading');
await waitFor(() => {
userEvent.click(checkout);
expect(table).toHaveClass('checkoutLoading');
});
await new Promise((r) => setTimeout(r, 10));
expect(table).toHaveClass('checkoutError');
screen.getByText('Cart must not be empty');
});
userEvent.click(checkout); doesn't need to be in the waitFor