import {act, render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
import {
addErrorMessage,
addMessage,
addSuccessMessage,
clearIndicators,
} from 'sentry/actionCreators/indicator';
import Indicators from 'sentry/components/indicators';
import IndicatorStore from 'sentry/stores/indicatorStore';
// Make sure we use `duration: null` to test add/remove
jest.useFakeTimers();
jest.mock('framer-motion', () => ({
...jest.requireActual('framer-motion'),
AnimatePresence: jest.fn(({children}) => children),
}));
describe('Indicators', function () {
beforeEach(function () {
act(() => clearIndicators());
act(jest.runAllTimers);
});
it('renders nothing by default', function () {
const {container} = render();
expect(container).toHaveTextContent('');
});
it('has a loading indicator by default', function () {
const {container} = render();
// when "type" is empty, we should treat it as loading state
act(() => void IndicatorStore.add('Loading'));
expect(screen.getByTestId('loading-indicator')).toBeInTheDocument();
expect(container).toHaveTextContent('Loading');
});
it('adds and removes a toast by calling IndicatorStore directly', function () {
const {container} = render();
// when "type" is empty, we should treat it as loading state
let indicator;
act(() => {
indicator = IndicatorStore.add('Loading');
});
expect(container).toHaveTextContent('Loading');
// Old indicator gets replaced when a new one is added
act(() => IndicatorStore.remove(indicator));
expect(container).toHaveTextContent('');
});
// This is a common pattern used throughout the code for API calls
it('adds and replaces toast by calling IndicatorStore directly', function () {
const {container} = render();
act(() => void IndicatorStore.add('Loading'));
expect(container).toHaveTextContent('Loading');
// Old indicator gets replaced when a new one is added
act(() => void IndicatorStore.add('success', 'success'));
expect(container).toHaveTextContent('success');
});
it('does not have loading indicator when "type" is empty (default)', function () {
const {container} = render();
act(() => addMessage('Loading', '', {duration: null}));
act(jest.runAllTimers);
expect(container).toHaveTextContent('Loading');
expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument();
});
it('has a loading indicator when type is "loading"', function () {
const {container} = render();
act(() => addMessage('Loading', 'loading', {duration: null}));
act(jest.runAllTimers);
expect(container).toHaveTextContent('Loading');
expect(screen.getByTestId('loading-indicator')).toBeInTheDocument();
});
it('adds and removes toast by calling action creators', function () {
const {container} = render();
// action creators don't return anything
act(() => addMessage('Loading', '', {duration: null}));
act(jest.runAllTimers);
expect(container).toHaveTextContent('Loading');
// If no indicator is specified, will remove all indicators
act(() => clearIndicators());
act(jest.runAllTimers);
expect(container).toHaveTextContent('');
expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument();
});
it('adds and replaces toast by calling action creators', function () {
const {container} = render();
act(() => addMessage('Loading', '', {duration: null}));
act(jest.runAllTimers);
expect(container).toHaveTextContent('Loading');
// Old indicator gets replaced when a new one is added
act(() => addMessage('success', 'success', {duration: null}));
act(jest.runAllTimers);
expect(container).toHaveTextContent('success');
expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument();
});
it('adds and replaces toasts by calling action creators helpers', async function () {
const {container} = render();
// Old indicator gets replaced when a new one is added
act(() => addSuccessMessage('success'));
await waitFor(() => {
expect(container).toHaveTextContent('success');
});
act(() => clearIndicators());
act(() => addErrorMessage('error'));
await waitFor(() => {
expect(container).toHaveTextContent('error');
});
});
it('appends toasts', function () {
const {container} = render();
act(() => addMessage('Loading', '', {append: true, duration: null}));
act(jest.runAllTimers);
expect(screen.getByTestId('toast')).toHaveTextContent('Loading');
act(() => addMessage('Success', 'success', {append: true, duration: null}));
act(jest.runAllTimers);
// Toasts get appended to the end
expect(screen.getByTestId('toast')).toHaveTextContent('Loading');
expect(screen.getByTestId('toast-success')).toHaveTextContent('Success');
act(() => addMessage('Error', 'error', {append: true, duration: null}));
act(jest.runAllTimers);
// Toasts get appended to the end
expect(screen.getByTestId('toast')).toHaveTextContent('Loading');
expect(screen.getByTestId('toast-success')).toHaveTextContent('Success');
expect(screen.getByTestId('toast-error')).toHaveTextContent('Error');
// clears all toasts
act(() => clearIndicators());
act(jest.runAllTimers);
expect(container).toHaveTextContent('');
expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument();
});
it('dismisses on click', function () {
const {container} = render();
act(() => addMessage('Loading', '', {append: true, duration: null}));
act(jest.runAllTimers);
expect(screen.getByTestId('toast')).toHaveTextContent('Loading');
userEvent.click(screen.getByTestId('toast'));
act(jest.runAllTimers);
expect(container).toHaveTextContent('');
expect(screen.queryByTestId('toast')).not.toBeInTheDocument();
});
it('hides after 10s', function () {
const {container} = render();
act(() => addMessage('Duration', '', {append: true, duration: 10000}));
act(() => jest.advanceTimersByTime(9000));
expect(screen.getByTestId('toast')).toHaveTextContent('Duration');
// Still visible
act(() => jest.advanceTimersByTime(999));
expect(screen.getByTestId('toast')).toHaveTextContent('Duration');
act(() => jest.advanceTimersByTime(2));
expect(container).toHaveTextContent('');
expect(screen.queryByTestId('toast')).not.toBeInTheDocument();
});
});