-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: Prevent "missing act" warning for queued microtasks (#1137)
* Add intended behavior * fix: Prevent "missing act" warning for in-flight promises * Disable TL lint rules in tests * Implementation without macrotask * Now I member
- Loading branch information
Showing
3 changed files
with
182 additions
and
61 deletions.
There are no files selected for viewing
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,73 +1,164 @@ | ||
import * as React from 'react' | ||
import {render, waitForElementToBeRemoved, screen, waitFor} from '../' | ||
|
||
const fetchAMessage = () => | ||
new Promise(resolve => { | ||
// we are using random timeout here to simulate a real-time example | ||
// of an async operation calling a callback at a non-deterministic time | ||
const randomTimeout = Math.floor(Math.random() * 100) | ||
setTimeout(() => { | ||
resolve({returnedMessage: 'Hello World'}) | ||
}, randomTimeout) | ||
}) | ||
|
||
function ComponentWithLoader() { | ||
const [state, setState] = React.useState({data: undefined, loading: true}) | ||
React.useEffect(() => { | ||
let cancelled = false | ||
fetchAMessage().then(data => { | ||
if (!cancelled) { | ||
setState({data, loading: false}) | ||
} | ||
describe.each([ | ||
['real timers', () => jest.useRealTimers()], | ||
['fake legacy timers', () => jest.useFakeTimers('legacy')], | ||
['fake modern timers', () => jest.useFakeTimers('modern')], | ||
])( | ||
'it waits for the data to be loaded in a macrotask using %s', | ||
(label, useTimers) => { | ||
beforeEach(() => { | ||
useTimers() | ||
}) | ||
|
||
afterEach(() => { | ||
jest.useRealTimers() | ||
}) | ||
|
||
return () => { | ||
cancelled = true | ||
const fetchAMessageInAMacrotask = () => | ||
new Promise(resolve => { | ||
// we are using random timeout here to simulate a real-time example | ||
// of an async operation calling a callback at a non-deterministic time | ||
const randomTimeout = Math.floor(Math.random() * 100) | ||
setTimeout(() => { | ||
resolve({returnedMessage: 'Hello World'}) | ||
}, randomTimeout) | ||
}) | ||
|
||
function ComponentWithMacrotaskLoader() { | ||
const [state, setState] = React.useState({data: undefined, loading: true}) | ||
React.useEffect(() => { | ||
let cancelled = false | ||
fetchAMessageInAMacrotask().then(data => { | ||
if (!cancelled) { | ||
setState({data, loading: false}) | ||
} | ||
}) | ||
|
||
return () => { | ||
cancelled = true | ||
} | ||
}, []) | ||
|
||
if (state.loading) { | ||
return <div>Loading...</div> | ||
} | ||
|
||
return ( | ||
<div data-testid="message"> | ||
Loaded this message: {state.data.returnedMessage}! | ||
</div> | ||
) | ||
} | ||
}, []) | ||
|
||
if (state.loading) { | ||
return <div>Loading...</div> | ||
} | ||
test('waitForElementToBeRemoved', async () => { | ||
render(<ComponentWithMacrotaskLoader />) | ||
const loading = () => screen.getByText('Loading...') | ||
await waitForElementToBeRemoved(loading) | ||
expect(screen.getByTestId('message')).toHaveTextContent(/Hello World/) | ||
}) | ||
|
||
test('waitFor', async () => { | ||
render(<ComponentWithMacrotaskLoader />) | ||
await waitFor(() => screen.getByText(/Loading../)) | ||
await waitFor(() => screen.getByText(/Loaded this message:/)) | ||
expect(screen.getByTestId('message')).toHaveTextContent(/Hello World/) | ||
}) | ||
|
||
return ( | ||
<div data-testid="message"> | ||
Loaded this message: {state.data.returnedMessage}! | ||
</div> | ||
) | ||
} | ||
test('findBy', async () => { | ||
render(<ComponentWithMacrotaskLoader />) | ||
await expect(screen.findByTestId('message')).resolves.toHaveTextContent( | ||
/Hello World/, | ||
) | ||
}) | ||
}, | ||
) | ||
|
||
describe.each([ | ||
['real timers', () => jest.useRealTimers()], | ||
['fake legacy timers', () => jest.useFakeTimers('legacy')], | ||
['fake modern timers', () => jest.useFakeTimers('modern')], | ||
])('it waits for the data to be loaded using %s', (label, useTimers) => { | ||
beforeEach(() => { | ||
useTimers() | ||
}) | ||
|
||
afterEach(() => { | ||
jest.useRealTimers() | ||
}) | ||
|
||
test('waitForElementToBeRemoved', async () => { | ||
render(<ComponentWithLoader />) | ||
const loading = () => screen.getByText('Loading...') | ||
await waitForElementToBeRemoved(loading) | ||
expect(screen.getByTestId('message')).toHaveTextContent(/Hello World/) | ||
}) | ||
|
||
test('waitFor', async () => { | ||
render(<ComponentWithLoader />) | ||
const message = () => screen.getByText(/Loaded this message:/) | ||
await waitFor(message) | ||
expect(screen.getByTestId('message')).toHaveTextContent(/Hello World/) | ||
}) | ||
|
||
test('findBy', async () => { | ||
render(<ComponentWithLoader />) | ||
await expect(screen.findByTestId('message')).resolves.toHaveTextContent( | ||
/Hello World/, | ||
) | ||
}) | ||
}) | ||
])( | ||
'it waits for the data to be loaded in a microtask using %s', | ||
(label, useTimers) => { | ||
beforeEach(() => { | ||
useTimers() | ||
}) | ||
|
||
afterEach(() => { | ||
jest.useRealTimers() | ||
}) | ||
|
||
const fetchAMessageInAMicrotask = () => | ||
Promise.resolve({ | ||
status: 200, | ||
json: () => Promise.resolve({title: 'Hello World'}), | ||
}) | ||
|
||
function ComponentWithMicrotaskLoader() { | ||
const [fetchState, setFetchState] = React.useState({fetching: true}) | ||
|
||
React.useEffect(() => { | ||
if (fetchState.fetching) { | ||
fetchAMessageInAMicrotask().then(res => { | ||
return ( | ||
res | ||
.json() | ||
// By spec, the runtime can only yield back to the event loop once | ||
// the microtask queue is empty. | ||
// So we ensure that we actually wait for that as well before yielding back from `waitFor`. | ||
.then(data => data) | ||
.then(data => data) | ||
.then(data => data) | ||
.then(data => data) | ||
.then(data => data) | ||
.then(data => data) | ||
.then(data => data) | ||
.then(data => data) | ||
.then(data => data) | ||
.then(data => data) | ||
.then(data => data) | ||
.then(data => { | ||
setFetchState({todo: data.title, fetching: false}) | ||
}) | ||
) | ||
}) | ||
} | ||
}, [fetchState]) | ||
|
||
if (fetchState.fetching) { | ||
return <p>Loading..</p> | ||
} | ||
|
||
return ( | ||
<div data-testid="message">Loaded this message: {fetchState.todo}</div> | ||
) | ||
} | ||
|
||
test('waitForElementToBeRemoved', async () => { | ||
render(<ComponentWithMicrotaskLoader />) | ||
const loading = () => screen.getByText('Loading..') | ||
await waitForElementToBeRemoved(loading) | ||
expect(screen.getByTestId('message')).toHaveTextContent(/Hello World/) | ||
}) | ||
|
||
test('waitFor', async () => { | ||
render(<ComponentWithMicrotaskLoader />) | ||
await waitFor(() => { | ||
screen.getByText('Loading..') | ||
}) | ||
await waitFor(() => { | ||
screen.getByText(/Loaded this message:/) | ||
}) | ||
expect(screen.getByTestId('message')).toHaveTextContent(/Hello World/) | ||
}) | ||
|
||
test('findBy', async () => { | ||
render(<ComponentWithMicrotaskLoader />) | ||
await expect(screen.findByTestId('message')).resolves.toHaveTextContent( | ||
/Hello World/, | ||
) | ||
}) | ||
}, | ||
) |
This file contains 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