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(); }); });