Created
September 8, 2025 19:06
-
-
Save kapral18/eed433b6ad35d5b124f5bd8c7959e914 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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