Skip to content

Commit

Permalink
test: try to fix flakiness around offline mocks (#7061)
Browse files Browse the repository at this point in the history
* fix(query-core): allow for unconditional resuming of paused mutations if we get an online event

and try to stabilize flaky test

* test: do not rely on dispatching events in tests

that could influence other tests

* test: don't use getAll
  • Loading branch information
TkDodo authored Mar 9, 2024
1 parent 661ddd3 commit 9cb77a7
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 88 deletions.
105 changes: 43 additions & 62 deletions packages/react-query/src/__tests__/useMutation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -453,8 +453,6 @@ describe('useMutation', () => {

const rendered = renderWithClient(queryClient, <Page />)

window.dispatchEvent(new Event('offline'))

await waitFor(() => {
expect(
rendered.getByText('error: null, status: idle, isPaused: false'),
Expand All @@ -471,8 +469,8 @@ describe('useMutation', () => {

expect(count).toBe(0)

onlineMock.mockRestore()
window.dispatchEvent(new Event('online'))
onlineMock.mockReturnValue(true)
queryClient.getMutationCache().resumePausedMutations()

await sleep(100)

Expand All @@ -483,6 +481,7 @@ describe('useMutation', () => {
})

expect(count).toBe(2)
onlineMock.mockRestore()
})

it('should call onMutate even if paused', async () => {
Expand Down Expand Up @@ -515,22 +514,22 @@ describe('useMutation', () => {

await rendered.findByText('data: null, status: idle, isPaused: false')

window.dispatchEvent(new Event('offline'))

fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))

await rendered.findByText('data: null, status: pending, isPaused: true')

expect(onMutate).toHaveBeenCalledTimes(1)
expect(onMutate).toHaveBeenCalledWith('todo')

onlineMock.mockRestore()
window.dispatchEvent(new Event('online'))
onlineMock.mockReturnValue(true)
queryClient.getMutationCache().resumePausedMutations()

await rendered.findByText('data: 1, status: success, isPaused: false')

expect(onMutate).toHaveBeenCalledTimes(1)
expect(count).toBe(1)

onlineMock.mockRestore()
})

it('should optimistically go to paused state if offline', async () => {
Expand Down Expand Up @@ -564,8 +563,6 @@ describe('useMutation', () => {

await rendered.findByText('data: null, status: idle, isPaused: false')

window.dispatchEvent(new Event('offline'))

fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))

await rendered.findByText('data: null, status: pending, isPaused: true')
Expand All @@ -574,22 +571,25 @@ describe('useMutation', () => {
expect(states[0]).toBe('idle, false')
expect(states[1]).toBe('pending, true')

onlineMock.mockRestore()
window.dispatchEvent(new Event('online'))
onlineMock.mockReturnValue(true)
queryClient.getMutationCache().resumePausedMutations()

await rendered.findByText('data: 1, status: success, isPaused: false')

onlineMock.mockRestore()
})

it('should be able to retry a mutation when online', async () => {
const onlineMock = mockOnlineManagerIsOnline(false)
const key = queryKey()

let count = 0
const states: Array<UseMutationResult<any, any, any, any>> = []

function Page() {
const state = useMutation({
mutationKey: key,
mutationFn: async (_text: string) => {
await sleep(1)
await sleep(10)
count++
return count > 1
? Promise.resolve('data')
Expand All @@ -600,69 +600,50 @@ describe('useMutation', () => {
networkMode: 'offlineFirst',
})

states.push(state)

const { mutate } = state

React.useEffect(() => {
setActTimeout(() => {
window.dispatchEvent(new Event('offline'))
mutate('todo')
}, 10)
}, [mutate])

return null
return (
<div>
<button onClick={() => state.mutate('todo')}>mutate</button>
<div>status: {state.status}</div>
<div>isPaused: {String(state.isPaused)}</div>
<div>data: {state.data ?? 'null'}</div>
</div>
)
}

renderWithClient(queryClient, <Page />)

await sleep(50)
const rendered = renderWithClient(queryClient, <Page />)

expect(states.length).toBe(4)
expect(states[0]).toMatchObject({
isPending: false,
isPaused: false,
failureCount: 0,
failureReason: null,
})
expect(states[1]).toMatchObject({
isPending: true,
isPaused: false,
failureCount: 0,
failureReason: null,
})
expect(states[2]).toMatchObject({
isPending: true,
isPaused: false,
failureCount: 1,
failureReason: new Error('oops'),
})
expect(states[3]).toMatchObject({
isPending: true,
await waitFor(() => rendered.getByText('status: idle'))
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
await waitFor(() => rendered.getByText('isPaused: true'))

expect(
queryClient.getMutationCache().findAll({ mutationKey: key }).length,
).toBe(1)
expect(
queryClient.getMutationCache().findAll({ mutationKey: key })[0]?.state,
).toMatchObject({
status: 'pending',
isPaused: true,
failureCount: 1,
failureReason: new Error('oops'),
})

onlineMock.mockRestore()
window.dispatchEvent(new Event('online'))
onlineMock.mockReturnValue(true)
queryClient.getMutationCache().resumePausedMutations()

await sleep(50)
await waitFor(() => rendered.getByText('data: data'))

expect(states.length).toBe(6)
expect(states[4]).toMatchObject({
isPending: true,
isPaused: false,
failureCount: 1,
failureReason: new Error('oops'),
})
expect(states[5]).toMatchObject({
isPending: false,
expect(
queryClient.getMutationCache().findAll({ mutationKey: key })[0]?.state,
).toMatchObject({
status: 'success',
isPaused: false,
failureCount: 0,
failureReason: null,
data: 'data',
})

onlineMock.mockRestore()
})

it('should not change state if unmounted', async () => {
Expand Down
44 changes: 21 additions & 23 deletions packages/react-query/src/__tests__/useQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5216,12 +5216,11 @@ describe('useQuery', () => {
}

const rendered = renderWithClient(queryClient, <Page />)
window.dispatchEvent(new Event('offline'))

await waitFor(() => rendered.getByText('status: pending, isPaused: true'))

onlineMock.mockRestore()
window.dispatchEvent(new Event('online'))
onlineMock.mockReturnValue(true)
queryClient.getQueryCache().onOnline()

await waitFor(() =>
rendered.getByText('status: success, isPaused: false'),
Expand All @@ -5231,6 +5230,7 @@ describe('useQuery', () => {
})

expect(states).toEqual(['paused', 'fetching', 'idle'])
onlineMock.mockRestore()
})

it('online queries should not refetch if you are offline', async () => {
Expand Down Expand Up @@ -5269,7 +5269,6 @@ describe('useQuery', () => {
await waitFor(() => rendered.getByText('data: data1'))

const onlineMock = mockOnlineManagerIsOnline(false)
window.dispatchEvent(new Event('offline'))

fireEvent.click(rendered.getByRole('button', { name: /invalidate/i }))

Expand All @@ -5280,8 +5279,8 @@ describe('useQuery', () => {
)
await waitFor(() => rendered.getByText('failureReason: null'))

onlineMock.mockRestore()
window.dispatchEvent(new Event('online'))
onlineMock.mockReturnValue(true)
queryClient.getQueryCache().onOnline()

await waitFor(() =>
rendered.getByText(
Expand All @@ -5299,6 +5298,8 @@ describe('useQuery', () => {
await waitFor(() => {
expect(rendered.getByText('data: data2')).toBeInTheDocument()
})

onlineMock.mockRestore()
})

it('online queries should not refetch if you are offline and refocus', async () => {
Expand Down Expand Up @@ -5488,7 +5489,6 @@ describe('useQuery', () => {
const onlineMock = mockOnlineManagerIsOnline(false)

const rendered = renderWithClient(queryClient, <Page />)
window.dispatchEvent(new Event('offline'))

await waitFor(() =>
rendered.getByText('status: success, fetchStatus: paused'),
Expand All @@ -5511,10 +5511,8 @@ describe('useQuery', () => {
window.dispatchEvent(new Event('visibilitychange'))
})

onlineMock.mockRestore()
act(() => {
window.dispatchEvent(new Event('online'))
})
onlineMock.mockReturnValue(true)
queryClient.getQueryCache().onOnline()

await waitFor(() =>
rendered.getByText('status: success, fetchStatus: idle'),
Expand All @@ -5524,6 +5522,8 @@ describe('useQuery', () => {
})

expect(count).toBe(1)

onlineMock.mockRestore()
})

it('online queries should pause retries if you are offline', async () => {
Expand Down Expand Up @@ -5560,7 +5560,6 @@ describe('useQuery', () => {
)

const onlineMock = mockOnlineManagerIsOnline(false)
window.dispatchEvent(new Event('offline'))

await sleep(20)

Expand All @@ -5574,7 +5573,7 @@ describe('useQuery', () => {
expect(count).toBe(1)

onlineMock.mockReturnValue(true)
window.dispatchEvent(new Event('online'))
queryClient.getQueryCache().onOnline()

await waitFor(() =>
rendered.getByText('status: error, fetchStatus: idle, failureCount: 3'),
Expand Down Expand Up @@ -5625,16 +5624,14 @@ describe('useQuery', () => {

const rendered = renderWithClient(queryClient, <Page />)

window.dispatchEvent(new Event('offline'))

await waitFor(() =>
rendered.getByText('status: pending, fetchStatus: paused'),
)

fireEvent.click(rendered.getByRole('button', { name: /hide/i }))

onlineMock.mockRestore()
window.dispatchEvent(new Event('online'))
onlineMock.mockReturnValue(true)
queryClient.getQueryCache().onOnline()

await waitFor(() => {
expect(queryClient.getQueryState(key)).toMatchObject({
Expand All @@ -5646,6 +5643,8 @@ describe('useQuery', () => {
// give it a bit more time to make sure queryFn is not called again
await sleep(15)
expect(count).toBe(1)

onlineMock.mockRestore()
})

it('online queries should not fetch if paused and we go online when cancelled and no refetchOnReconnect', async () => {
Expand Down Expand Up @@ -5695,7 +5694,7 @@ describe('useQuery', () => {
expect(count).toBe(0)

onlineMock.mockReturnValue(true)
window.dispatchEvent(new Event('online'))
queryClient.getQueryCache().onOnline()

await sleep(15)

Expand Down Expand Up @@ -5767,7 +5766,7 @@ describe('useQuery', () => {
await sleep(15)

onlineMock.mockReturnValue(true)
window.dispatchEvent(new Event('online'))
queryClient.getQueryCache().onOnline()

await sleep(15)

Expand Down Expand Up @@ -5901,8 +5900,6 @@ describe('useQuery', () => {

const rendered = renderWithClient(queryClient, <Page />)

window.dispatchEvent(new Event('offline'))

await waitFor(() =>
rendered.getByText(
'status: pending, fetchStatus: paused, failureCount: 1',
Expand All @@ -5912,15 +5909,16 @@ describe('useQuery', () => {

expect(count).toBe(1)

onlineMock.mockRestore()
window.dispatchEvent(new Event('online'))
onlineMock.mockReturnValue(true)
queryClient.getQueryCache().onOnline()

await waitFor(() =>
rendered.getByText('status: error, fetchStatus: idle, failureCount: 3'),
)
await waitFor(() => rendered.getByText('failureReason: failed3'))

expect(count).toBe(3)
onlineMock.mockRestore()
})
})

Expand Down
6 changes: 3 additions & 3 deletions packages/react-query/src/__tests__/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { act, render } from '@testing-library/react'
import * as utils from '@tanstack/query-core'
import { QueryClient, QueryClientProvider, onlineManager } from '..'
import type { QueryClientConfig } from '..'
import type { SpyInstance } from 'vitest'
import type { MockInstance } from 'vitest'

export function renderWithClient(
client: QueryClient,
Expand Down Expand Up @@ -48,13 +48,13 @@ export function createQueryClient(config?: QueryClientConfig): QueryClient {

export function mockVisibilityState(
value: DocumentVisibilityState,
): SpyInstance<[], DocumentVisibilityState> {
): MockInstance<[], DocumentVisibilityState> {
return vi.spyOn(document, 'visibilityState', 'get').mockReturnValue(value)
}

export function mockOnlineManagerIsOnline(
value: boolean,
): SpyInstance<[], boolean> {
): MockInstance<[], boolean> {
return vi.spyOn(onlineManager, 'isOnline').mockReturnValue(value)
}

Expand Down

0 comments on commit 9cb77a7

Please sign in to comment.