# STORYBOOK INTERACTION TESTS
> TARGET AUDIENCE: This document is a required reading for developers.
This is a guide to working with storybook interactions tests covering all possible cases.
- Create an interaction test
- Responsibilities of these tests with respect to the interacting components
- Interaction tests covers
- Considerations
# Getting Started
[Interaction tests](https://storybook.js.org/docs/react/writing-tests/interaction-testing) is a very useful tool offered by storybook where it follows the [testing-library](https://testing-library.com/docs/) standards in order to check and validate the visual aspects of a component.
Unlike normal storybook tests, interaction tests check the behavior of components and how it should change visually. This covers everything from error messages because an input value is invalid to whether a button should be disabled to whether all the fields of a form are filled out.
## Create an interaction test
```typescript
import type { ComponentStory } from '@storybook/react';
import isChromatic from 'chromatic';
import { TranslationsProvider } from '../../translations/translationsProvider';
import { QueryClient, QueryClientProvider } from 'react-query';
import { SomeContainer } from './SomeContainer';
import { within, userEvent } from '@storybook/testing-library';
import { expect } from '@storybook/jest';
export default {
title: 'UseCases/Containers/SomeContainer',
component: SomeContainer,
};
const mockedQueryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: Infinity,
refetchOnWindowFocus: false,
retry: false,
cacheTime: 0,
},
},
});
const Template: ComponentStory<typeof SomeContainer> = (args) => {
return (
<TranslationsProvider>
<QueryClientProvider client={mockedQueryClient}>
<div className="h-[34rem]">
<AvailabilityContainer {...args}/>
</div>
</QueryClientProvider>
</TranslationsProvider>
);
};
/** Mocked props */
const delay = isChromatic() ? 0 : 2000;
const longDelay = 1000000000;
const serviceError = {
type: 'ServiceError',
statusCode: 300,
message: 'Some service error',
};
const someDependencyMock =
(timeout: number, error = false) =>
async () =>
await new Promise<string>((resolve, reject) => {
setTimeout(() => {
if (error) {
reject(serviceError);
} else {
resolve('Something else');
}
}, timeout);
});
/** Interactions test */
export const PlayWithSomeContainer = Template.bind({});
PlayWithSomeContainer.args = {
someDependency: someDependencyMock(0),
};
PlayWithSomeContainer.play = async ({ canvasElement }) => {
const canvas = within(canvasElement);
const someInput = await canvas.findByPlaceholderText('some placeholder');
const someButton = await canvas.findByRole('button', { name: 'some button' });
userEvent.type(someInput, 'some value');
userEvent.click(someButton);
expect(await canvas.findByText(/Saving completed!/i)).toBeInTheDocument();
};
```
**Where**
- `canvasElement` This is the html element rendered in the template.
- `within` It is a function of testing-library to access the tree elements.
- `userEvent` It is the event handler to simulate typing or triggering events such as clicking a button.
- `expect` It is not the same expect from jest, it comes from the dependency '@storybook/jest'.
## Responsibilities of these tests with respect to the interacting components
The purpose of interaction testing is initially to test the use case of the component, instead of testing each validation/input/button individually, the aim is to test the different interactions of the elements of the component.
To elaborate, each interaction test will have the responsibility of:
- Validate that all inputs/dropdowns/buttons exist in what is rendered from the storybook
- Validate the initial state of the different inputs/dropdowns/buttons components (e.g., submit button is disabled)
- Review validations in case erroneous values are entered and how that interacts with the rest of the component (e.g., a text appears with the error message, or renders a toast).
- Validate that error messages can be corrected and that such alerts disappear once the data entered is correct (e.g., the format of an input is invalid and that if a valid value is entered it disappears, or that toast can be closed/canceled or confirmed)
- Once the appropriate corrective actions have been taken, validate that a success message appears.
**Interaction tests will never test the individual behavior of each component, i.e., if a container has the "calendar" component, the interaction tests will not test the behavior of "calendar" exclusively, but of the interaction of "calendar" with other components in the context of that container**
## Considerations
- It is important to check the delay of the screens and the different components. If you try to validate any message before it is rendered this will cause an error.
For these cases, you can use the [waitFor](https://testing-library.com/docs/dom-testing-library/api-async/#waitfor), [waitForElementToBeRemoved](https://testing-library.com/docs/dom-testing-library/api-async/#waitforelementtoberemoved), or add `{ timeout: number}` to the third argument of `canvas.findBy...`
- In case the component uses toast, it renders outside of the component's DOM, so in order to solve this, it is recommended to use `canvasElement.ownerDocument.body`
```typescript=
const canvas = within(canvasElement);
/// Toaster renders outside div#root
const portal = within(canvasElement.ownerDocument.body);
```
- It may be that in some cases you must interact with two buttons to display certain content, or that a button changes the state of another, by default the `userEvent.click` does not have a delay, so it is advisable to create a function
```typescript=
const delayEvent = async (callback: () => void, delay: number) => {
await new Promise<void>((resolve) => {
setTimeout(() => {
callback();
resolve();
}, delay);
});
};
```
And use it as
```typescript=
const button = await portal.findByRole('button', { name: 'some button' }, { timeout: 1000});
await delayEvent(() => {
userEvent.click(button);
}, 1000);
```
- The components should be using TailwindCSS styles, and h-full is usually used as a relative style to adjust to the height, however when rendering the component exclusively the height is not displayed correctly so it is recommended to put a `<div className="h-[34rem]">` over the container to display it properly.
- In case you are looking for an element by an attribute that repeats (e.g. placeholder) use `canvas.findAllBy...` and get the element you need by its position.
```typescript=
const [someInput, someOtherInput] = await canvas.findAllByPlaceholderText('some placeholder');
```
- In some cases certain elements or components will not be very accessibility friendly. For example, a button that contains a div inside and an image or an icon inside. In that case it is advisable to use some attributes that can describe the element to be able to use it in the interaction test, in most cases you can use the attribute `aria-label='some description'` and in the code use as
```typescript=
const someButton = await canvas.findAllByRole('button', { name: /some description/i });
```
- Certain validations are given on the browser context, such as the default timezone or the current day. This can cause some differences in the Chromatic environment, so it is important to use the `isChromatic()` function to know in which context the test will run.