/* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ import { waitFor, waitForElementToBeRemoved, screen, render, act } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; beforeEach(() => { jest.clearAllMocks(); }); describe('Basic scenario', () => { beforeEach(() => { simpleTimer.callCount = 0; }); async function simpleTimer(callback: any) { simpleTimer.callCount += 1; if (simpleTimer.callCount > 4) { return; } // await is the microtask boundary before scheduling the next timer // jest fake timer calls do not advance the microtask queue // only macrotasks (setTimeout, setInterval) are advanced // even if you use runAllTimers or advanceTimersByTime // so the await here means the next setTimeout is never scheduled await callback(); setTimeout(() => { simpleTimer(callback); }, 1000); } simpleTimer.callCount = 0; const callback = jest.fn(); it('fakeTimers dont work well with promise microtasks queues', async () => { jest.useFakeTimers(); await simpleTimer(callback); jest.advanceTimersByTime(4000); expect(callback).toHaveBeenCalledTimes(4); jest.runOnlyPendingTimers(); jest.useRealTimers(); }); it('real timers with waitFor work fine with microtasks', async () => { await simpleTimer(callback); await waitFor(() => { expect(callback).toHaveBeenCalledTimes(4); }); }); }); describe('Component scenarios', () => { const api = { save: jest.fn().mockResolvedValue(null), }; function AutoSaveForm() { const [status, setStatus] = React.useState<'idle' | 'saving' | 'saved'>('idle'); async function onChange(value: string) { setStatus('saving'); await api.save(value); // microtask boundary before scheduling the next timer setStatus('saved'); setTimeout(() => setStatus('idle'), 1000); } return ( <> onChange(e.target.value)} /> {status !== 'idle' && {status}} ); } describe('Failures with fake timers', () => { beforeEach(() => { jest.clearAllMocks(); jest.useFakeTimers(); }); afterEach(() => { jest.runOnlyPendingTimers(); jest.useRealTimers(); }); it('shows and hides saved toasts', async () => { render(); await userEvent.type(screen.getByLabelText('Name', { selector: 'input' }), 'abc'); act(() => { jest.runAllTimers(); }); // Still here: because microtasks are not handled expect(screen.getByRole('status')).toHaveTextContent(/saved/i); }); it('shows and hides saved toasts (fake timers + waitForElementToBeRemoved)', async () => { render(); await userEvent.type(screen.getByLabelText('Name', { selector: 'input' }), 'abc'); act(() => { jest.runAllTimers(); }); // Even waiting for it to be removed times out // Because fake timers mess up the async scheduling await waitForElementToBeRemoved(() => screen.queryByRole('status')); // times out }); it('shows and hides saved toats (fake timers + waitFor)', async () => { render(); await userEvent.type(screen.getByLabelText('Name', { selector: 'input' }), 'abc'); act(() => { jest.runAllTimers(); }); // Even waitFor(() => ...) times out when paired with fake timers // Because of the same reason as above await waitFor(() => expect(screen.getByRole('status')).not.toHaveTextContent(/saved/i)); }); }); describe('Success', () => { it('shows and hides saved toast after async save (real timers)', async () => { render(); await userEvent.type(screen.getByLabelText('Name', { selector: 'input' }), 'abc'); // or waitFor(() => expect(screen.getByText('saved', { selector: 'output' })).toBeInTheDocument()) // same thing expect(await screen.findByText('saved', { selector: 'output' })).toBeInTheDocument(); await waitForElementToBeRemoved(() => screen.queryByRole('status')); }); }); });