Last active
September 15, 2025 11:36
-
-
Save kapral18/1032800f0963dbc288dbf5882a6f5e22 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'; | |
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); | |
beforeEach(() => { | |
jest.clearAllMocks(); | |
jest.useFakeTimers(); | |
}); | |
afterEach(() => { | |
jest.useRealTimers(); | |
}); | |
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 work well with promise microtasks queues', async () => { | |
jest.useFakeTimers(); | |
await simpleTimer(callback); | |
await jest.advanceTimersByTimeAsync(4000); | |
expect(callback).toHaveBeenCalledTimes(4); | |
}); | |
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('Success with fake timers', () => { | |
it('shows and hides saved toasts', async () => { | |
render(<AutoSaveForm />); | |
await user.type(screen.getByLabelText('Name', { selector: 'input' }), 'abc'); | |
await act(async () => { | |
await jest.advanceTimersByTimeAsync(999); | |
}); | |
expect(screen.getByRole('status')).toHaveTextContent(/saved/i); | |
await act(async () => { | |
await jest.runAllTimersAsync(); | |
}); | |
expect(screen.queryByRole('status')).not.toBeInTheDocument(); | |
}); | |
it('shows and hides saved toasts (fake timers + waitForElementToBeRemoved)', async () => { | |
render(<AutoSaveForm />); | |
await user.type(screen.getByLabelText('Name', { selector: 'input' }), 'abc'); | |
await act(async () => { | |
await jest.advanceTimersByTimeAsync(999); | |
}); | |
expect(screen.getByRole('status')).toHaveTextContent(/saved/i); | |
await waitForElementToBeRemoved(() => screen.queryByRole('status')); | |
}); | |
it('shows and hides saved toats (fake timers + waitFor)', async () => { | |
render(<AutoSaveForm />); | |
await user.type(screen.getByLabelText('Name', { selector: 'input' }), 'abc'); | |
await act(async () => { | |
await jest.runAllTimersAsync(); | |
}); | |
await waitFor(() => expect(screen.queryByRole('status')).not.toBeInTheDocument()); | |
}); | |
}); | |
describe('Success with real timers', () => { | |
it('shows and hides saved toast after async save (real timers)', async () => { | |
render(<AutoSaveForm />); | |
await user.type(screen.getByLabelText('Name', { selector: 'input' }), 'abc'); | |
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