Skip to content

Instantly share code, notes, and snippets.

@kapral18
Created September 8, 2025 19:06
Show Gist options
  • Save kapral18/eed433b6ad35d5b124f5bd8c7959e914 to your computer and use it in GitHub Desktop.
Save kapral18/eed433b6ad35d5b124f5bd8c7959e914 to your computer and use it in GitHub Desktop.
/*
* 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 (
<>
<label htmlFor="name">Name</label>
<input id="name" onChange={(e) => onChange(e.target.value)} />
{status !== 'idle' && <output>{status}</output>}
</>
);
}
describe('Failures with fake timers', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.useFakeTimers();
});
afterEach(() => {
jest.runOnlyPendingTimers();
jest.useRealTimers();
});
it('shows and hides saved toasts', async () => {
render(<AutoSaveForm />);
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(<AutoSaveForm />);
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(<AutoSaveForm />);
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(<AutoSaveForm />);
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'));
});
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment