Repository: tannerlinsley/react-query
Pull request #2876 (2021-11-04 09:57:46)

test(core): improve code coverage
Add tests to improve code coverage for `./src/core`

Before:
<img width="789" alt="Screenshot 2021-11-04 at 10 30 07" src="https://user-images.githubusercontent.com/20332397/140290570-bb57f375-9f9e-43f6-86be-9e6ae7fe8d2d.png">

After:
<img width="760" alt="Screenshot 2021-11-04 at 10 30 15" src="https://user-images.githubusercontent.com/20332397/140290593-c2ec20c2-3314-4ea4-9db4-91942c059f71.png">

For some tests I had to "force" some use cases  by accessing private functions and props (I use `// Force....` in the test code comments. It means that we can maybe remove some pieces of existing code that are not necessary.

## Not covered
No solution found  to tests the following cases to reach 100% :pensive:
- [ ] `utils.scheduleMicrotask`: Mock `process.on` to test uncaught exception but issue with jest version support.
- [ ]  `utils.isPlainObject`: Find the corresponding object case!
- [ ]  `logger`:  Mock console to undefined or assume that the console is always defined and change the existing code `logger`definition.

## Impacts on existing code
Code changes to improve code coverage:
- [x]  `notifyManager.ts:` added a `notifyManagerTest` singleton for testing only
- [x] `queryCache.ts`: fixed comparison in`find()` filters object is always defined but could be empty.
- [x]  `queryObserver.ts`: removed useless condition  `shouldNotifyListeners` 


vercel[bot]: This pull request is being automatically deployed with Vercel ([learn more](https://vercel.link/github-learn-more)).  
To see the status of your deployment, click below or on the icon next to each commit.

🔍 Inspect: [https://vercel.com/tanstack/react-query/DcPZpkzJAJmBPrTNrynRSW77jw6Z](https://vercel.com/tanstack/react-query/DcPZpkzJAJmBPrTNrynRSW77jw6Z)  
✅ Preview: *In Progress*


TkDodo: also, we just added codecov reporting. We should see a nice increase if you update your PR, and maybe we could even increase the expected coverage in the settings?
codesandbox[bot]: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/tannerlinsley/react-query/pr/2876/builds/185997) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit ca22c597d27e0e4138f480f642ecbea374368b7c:

|Sandbox| Source |
|--|--|
|[tannerlinsley/react-query: basic](https://codesandbox.io/s/tannerlinsley-react-query-basic-oupi4)| Configuration |
|[tannerlinsley/react-query: basic-typescript](https://codesandbox.io/s/tannerlinsley-react-query-basic-typescript-mohdc)| Configuration |

codecov[bot]: # [Codecov](https://codecov.io/gh/tannerlinsley/react-query/pull/2876?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley) Report
> :exclamation: No coverage uploaded for pull request base (`master@69a5d79`). [Click here to learn what that means](https://docs.codecov.io/docs/error-reference?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley#section-missing-base-commit).
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/tannerlinsley/react-query/pull/2876/graphs/tree.svg?width=650&height=150&src=pr&token=jqEbswkEDQ&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley)](https://codecov.io/gh/tannerlinsley/react-query/pull/2876?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley)

```diff
@@            Coverage Diff            @@
##             master    #2876   +/-   ##
=========================================
  Coverage          ?   96.27%           
=========================================
  Files             ?       45           
  Lines             ?     2227           
  Branches          ?      637           
=========================================
  Hits              ?     2144           
  Misses            ?       80           
  Partials          ?        3           
```



------

[Continue to review full report at Codecov](https://codecov.io/gh/tannerlinsley/react-query/pull/2876?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/tannerlinsley/react-query/pull/2876?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley). Last update [69a5d79...ca22c59](https://codecov.io/gh/tannerlinsley/react-query/pull/2876?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Tanner+Linsley).


Patch:
diff --git a/docs/src/pages/reference/QueryCache.md b/docs/src/pages/reference/QueryCache.md
index a0c7778b9..8849faf7a 100644
--- a/docs/src/pages/reference/QueryCache.md
+++ b/docs/src/pages/reference/QueryCache.md
@@ -89,8 +89,8 @@ const queries = queryCache.findAll(queryKey)
 The `subscribe` method can be used to subscribe to the query cache as a whole and be informed of safe/known updates to the cache like query states changing or queries being updated, added or removed
 
 ```js
-const callback = query => {
-  console.log(query)
+const callback = event => {
+  console.log(event.type, event.query)
 }
 
 const unsubscribe = queryCache.subscribe(callback)
@@ -98,9 +98,8 @@ const unsubscribe = queryCache.subscribe(callback)
 
 **Options**
 
-- `callback: (query?: Query) => void`
+- `callback: (event: QueryCacheNotifyEvent) => void`
   - This function will be called with the query cache any time it is updated via its tracked update mechanisms (eg, `query.setState`, `queryClient.removeQueries`, etc). Out of scope mutations to the cache are not encouraged and will not fire subscription callbacks
-  - Additionally, for updates to the cache triggered by a specific query, the `query` will be passed as first argument to the callback
 
 **Returns**
 
diff --git a/src/core/index.ts b/src/core/index.ts
index c09b2614d..5a6035735 100644
--- a/src/core/index.ts
+++ b/src/core/index.ts
@@ -17,6 +17,7 @@ export { dehydrate, hydrate } from './hydration'
 // Types
 export * from './types'
 export type { Query } from './query'
+export type { Mutation } from './mutation'
 export type { Logger } from './logger'
 export type {
   DehydrateOptions,
diff --git a/src/core/notifyManager.ts b/src/core/notifyManager.ts
index 3e4b10ab6..b584f7bf8 100644
--- a/src/core/notifyManager.ts
+++ b/src/core/notifyManager.ts
@@ -10,7 +10,7 @@ type BatchNotifyFunction = (callback: () => void) => void
 
 // CLASS
 
-class NotifyManager {
+export class NotifyManager {
   private queue: NotifyCallback[]
   private transactions: number
   private notifyFn: NotifyFunction
diff --git a/src/core/queryCache.ts b/src/core/queryCache.ts
index 1a72d194b..f57b970ea 100644
--- a/src/core/queryCache.ts
+++ b/src/core/queryCache.ts
@@ -174,7 +174,7 @@ export class QueryCache extends Subscribable<QueryCacheListener> {
   findAll(arg1?: QueryKey | QueryFilters, arg2?: QueryFilters): Query[]
   findAll(arg1?: QueryKey | QueryFilters, arg2?: QueryFilters): Query[] {
     const [filters] = parseFilterArgs(arg1, arg2)
-    return filters
+    return Object.keys(filters).length > 0
       ? this.queries.filter(query => matchQuery(filters, query))
       : this.queries
   }
diff --git a/src/core/queryObserver.ts b/src/core/queryObserver.ts
index 254a19446..f1ab5c8d2 100644
--- a/src/core/queryObserver.ts
+++ b/src/core/queryObserver.ts
@@ -590,10 +590,6 @@ export class QueryObserver<
       return true
     }
 
-    if (result === prevResult) {
-      return false
-    }
-
     const { notifyOnChangeProps, notifyOnChangePropsExclusions } = this.options
 
     if (!notifyOnChangeProps && !notifyOnChangePropsExclusions) {


Test patch:
diff --git a/src/core/tests/focusManager.test.tsx b/src/core/tests/focusManager.test.tsx
new file mode 100644
index 000000000..ca071ec16
--- /dev/null
+++ b/src/core/tests/focusManager.test.tsx
@@ -0,0 +1,106 @@
+import { sleep } from '../utils'
+import { focusManager } from '../focusManager'
+
+describe('focusManager', () => {
+  afterEach(() => {
+    // Reset removeEventListener private property to avoid side effects between tests
+    focusManager['removeEventListener'] = undefined
+  })
+
+  it('should call previous remove handler when replacing an event listener', () => {
+    const remove1Spy = jest.fn()
+    const remove2Spy = jest.fn()
+
+    focusManager.setEventListener(() => remove1Spy)
+    focusManager.setEventListener(() => remove2Spy)
+
+    expect(remove1Spy).toHaveBeenCalledTimes(1)
+    expect(remove2Spy).not.toHaveBeenCalled()
+  })
+
+  it('should use focused boolean arg', async () => {
+    let count = 0
+
+    const setup = (setFocused: (focused?: boolean) => void) => {
+      setTimeout(() => {
+        count++
+        setFocused(true)
+      }, 20)
+      return () => void 0
+    }
+
+    focusManager.setEventListener(setup)
+
+    await sleep(30)
+    expect(count).toEqual(1)
+    expect(focusManager.isFocused()).toBeTruthy()
+  })
+
+  it('should not notify listeners on focus if already focused', async () => {
+    const subscriptionSpy = jest.fn()
+    const unsubscribe = focusManager.subscribe(subscriptionSpy)
+
+    focusManager.setFocused(true)
+    expect(subscriptionSpy).toHaveBeenCalledTimes(1)
+    subscriptionSpy.mockReset()
+
+    focusManager.setFocused(false)
+    expect(subscriptionSpy).toHaveBeenCalledTimes(0)
+
+    unsubscribe()
+  })
+
+  it('should return true for isFocused if document is undefined', async () => {
+    const { document } = globalThis
+
+    // @ts-expect-error
+    delete globalThis.document
+
+    focusManager.setFocused()
+    expect(focusManager.isFocused()).toBeTruthy()
+    globalThis.document = document
+  })
+
+  it('should not set window listener if window.addEventListener is not defined', async () => {
+    const { addEventListener } = globalThis.window
+
+    // @ts-expect-error
+    globalThis.window.addEventListener = undefined
+
+    const setEventListenerSpy = jest.spyOn(focusManager, 'setEventListener')
+
+    const unsubscribe = focusManager.subscribe()
+    expect(setEventListenerSpy).toHaveBeenCalledTimes(0)
+
+    unsubscribe()
+    globalThis.window.addEventListener = addEventListener
+  })
+
+  it('should replace default window listener when a new event listener is set', async () => {
+    const addEventListenerSpy = jest.spyOn(
+      globalThis.window,
+      'addEventListener'
+    )
+
+    const removeEventListenerSpy = jest.spyOn(
+      globalThis.window,
+      'removeEventListener'
+    )
+
+    // Should set the default event listener with window event listeners
+    const unsubscribe = focusManager.subscribe()
+    expect(addEventListenerSpy).toHaveBeenCalledTimes(2)
+
+    // Should replace the window default event listener by a new one
+    // and it should call window.removeEventListener twice
+    focusManager.setEventListener(() => {
+      return () => void 0
+    })
+
+    expect(removeEventListenerSpy).toHaveBeenCalledTimes(2)
+
+    unsubscribe()
+    addEventListenerSpy.mockRestore()
+    removeEventListenerSpy.mockRestore()
+  })
+})
diff --git a/src/core/tests/hydration.test.tsx b/src/core/tests/hydration.test.tsx
index 4bc3bc208..2de31d113 100644
--- a/src/core/tests/hydration.test.tsx
+++ b/src/core/tests/hydration.test.tsx
@@ -63,6 +63,18 @@ describe('dehydration and rehydration', () => {
     hydrationClient.clear()
   })
 
+  test('should not dehydrate queries if dehydrateQueries is set to false', async () => {
+    const queryCache = new QueryCache()
+    const queryClient = new QueryClient({ queryCache })
+    await queryClient.prefetchQuery('string', () => fetchData('string'))
+
+    const dehydrated = dehydrate(queryClient, { dehydrateQueries: false })
+
+    expect(dehydrated.queries.length).toBe(0)
+
+    queryClient.clear()
+  })
+
   test('should use the cache time from the client', async () => {
     const queryCache = new QueryCache()
     const queryClient = new QueryClient({ queryCache })
@@ -328,4 +340,89 @@ describe('dehydration and rehydration', () => {
     client.clear()
     consoleMock.mockRestore()
   })
+
+  test('should not dehydrate mutations if dehydrateMutations is set to false', async () => {
+    const consoleMock = jest.spyOn(console, 'error')
+    consoleMock.mockImplementation(() => undefined)
+
+    const serverAddTodo = jest
+      .fn()
+      .mockImplementation(() => Promise.reject('offline'))
+
+    const queryClient = new QueryClient()
+
+    queryClient.setMutationDefaults('addTodo', {
+      mutationFn: serverAddTodo,
+      retry: false,
+    })
+
+    queryClient
+      .executeMutation({
+        mutationKey: 'addTodo',
+        variables: { text: 'text' },
+      })
+      .catch(() => undefined)
+
+    await sleep(1)
+    const dehydrated = dehydrate(queryClient, { dehydrateMutations: false })
+
+    expect(dehydrated.mutations.length).toBe(0)
+
+    queryClient.clear()
+    consoleMock.mockRestore()
+  })
+
+  test('should not dehydrate mutation if mutation state is set to pause', async () => {
+    const consoleMock = jest.spyOn(console, 'error')
+    consoleMock.mockImplementation(() => undefined)
+
+    const serverAddTodo = jest
+      .fn()
+      .mockImplementation(() => Promise.reject('offline'))
+
+    const queryClient = new QueryClient()
+
+    queryClient.setMutationDefaults('addTodo', {
+      mutationFn: serverAddTodo,
+      retry: 1,
+      retryDelay: 20,
+    })
+
+    queryClient
+      .executeMutation({
+        mutationKey: 'addTodo',
+        variables: { text: 'text' },
+      })
+      .catch(() => undefined)
+
+    // Dehydrate mutation between retries
+    await sleep(1)
+    const dehydrated = dehydrate(queryClient)
+
+    expect(dehydrated.mutations.length).toBe(0)
+
+    await sleep(30)
+    queryClient.clear()
+    consoleMock.mockRestore()
+  })
+
+  test('should not hydrate if the hydratedState is null or is not an object', async () => {
+    const queryCache = new QueryCache()
+    const queryClient = new QueryClient({ queryCache })
+
+    expect(() => hydrate(queryClient, null)).not.toThrow()
+    expect(() => hydrate(queryClient, 'invalid')).not.toThrow()
+
+    queryClient.clear()
+  })
+
+  test('should support hydratedState with undefined queries and mutations', async () => {
+    const queryCache = new QueryCache()
+    const queryClient = new QueryClient({ queryCache })
+
+    expect(() => hydrate(queryClient, {})).not.toThrow()
+    expect(() => hydrate(queryClient, {})).not.toThrow()
+
+    queryClient.clear()
+  })
 })
diff --git a/src/core/tests/infiniteQueryBehavior.test.tsx b/src/core/tests/infiniteQueryBehavior.test.tsx
new file mode 100644
index 000000000..92d5c6c9f
--- /dev/null
+++ b/src/core/tests/infiniteQueryBehavior.test.tsx
@@ -0,0 +1,126 @@
+import { waitFor } from '@testing-library/react'
+import { queryKey, mockConsoleError } from '../../react/tests/utils'
+import {
+  QueryClient,
+  InfiniteQueryObserver,
+  InfiniteQueryObserverResult,
+} from '../..'
+
+describe('InfiniteQueryBehavior', () => {
+  let queryClient: QueryClient
+
+  beforeEach(() => {
+    queryClient = new QueryClient()
+    queryClient.mount()
+  })
+
+  afterEach(() => {
+    queryClient.clear()
+  })
+
+  test('InfiniteQueryBehavior should throw an error if the queryFn is not defined', async () => {
+    const consoleMock = mockConsoleError()
+    const key = queryKey()
+
+    const observer = new InfiniteQueryObserver(queryClient, {
+      queryKey: key,
+      retry: false,
+    })
+
+    let observerResult:
+      | InfiniteQueryObserverResult<unknown, unknown>
+      | undefined
+
+    const unsubscribe = observer.subscribe(result => {
+      observerResult = result
+    })
+
+    await waitFor(() => {
+      return expect(observerResult).toMatchObject({
+        isError: true,
+        error: 'Missing queryFn',
+      })
+    })
+
+    unsubscribe()
+    consoleMock.mockRestore()
+  })
+
+  test('InfiniteQueryBehavior should not refetch the first page if another page refetched', async () => {
+    const key = queryKey()
+    let abortSignal: AbortSignal | null = null
+
+    const queryFnSpy = jest
+      .fn()
+      .mockImplementation(({ pageParam = 1, signal }) => {
+        abortSignal = signal
+        return pageParam
+      })
+
+    const observer = new InfiniteQueryObserver<number>(queryClient, {
+      queryKey: key,
+      queryFn: queryFnSpy,
+      getNextPageParam: lastPage => lastPage + 1,
+    })
+
+    let observerResult:
+      | InfiniteQueryObserverResult<unknown, unknown>
+      | undefined
+
+    const unsubscribe = observer.subscribe(result => {
+      observerResult = result
+    })
+
+    // Wait for the first page to be fetched
+    await waitFor(() =>
+      expect(observerResult).toMatchObject({
+        isFetching: false,
+        data: { pages: [1] },
+      })
+    )
+
+    expect(queryFnSpy).toHaveBeenNthCalledWith(1, {
+      queryKey: [key],
+      pageParam: undefined,
+      meta: undefined,
+      signal: abortSignal,
+    })
+
+    queryFnSpy.mockClear()
+
+    // Fetch the second page
+    await observer.fetchNextPage()
+
+    expect(queryFnSpy).toHaveBeenNthCalledWith(1, {
+      queryKey: [key],
+      pageParam: 2,
+      meta: undefined,
+      signal: abortSignal,
+    })
+
+    expect(observerResult).toMatchObject({
+      isFetching: false,
+      data: { pages: [1, 2] },
+    })
+
+    queryFnSpy.mockClear()
+
+    // Refetch the second page
+    await queryClient.refetchQueries({
+      refetchPage: (_page, index) => index === 1,
+    })
+
+    expect(queryFnSpy).toHaveBeenNthCalledWith(1, {
+      queryKey: [key],
+      pageParam: 2,
+      meta: undefined,
+      signal: abortSignal,
+    })
+
+    expect(observerResult).toMatchObject({
+      data: { pages: [1, 2] },
+    })
+
+    unsubscribe()
+  })
+})
diff --git a/src/core/tests/mutationObserver.test.tsx b/src/core/tests/mutationObserver.test.tsx
new file mode 100644
index 000000000..68521fb4f
--- /dev/null
+++ b/src/core/tests/mutationObserver.test.tsx
@@ -0,0 +1,75 @@
+import { waitFor } from '@testing-library/react'
+import { sleep } from '../../react/tests/utils'
+import { QueryClient, MutationObserver } from '../..'
+
+describe('mutationObserver', () => {
+  let queryClient: QueryClient
+
+  beforeEach(() => {
+    queryClient = new QueryClient()
+    queryClient.mount()
+  })
+
+  afterEach(() => {
+    queryClient.clear()
+  })
+
+  test('onUnsubscribe should not remove the current mutation observer if there is still a subscription', async () => {
+    const mutation = new MutationObserver(queryClient, {
+      mutationFn: async (text: string) => {
+        await sleep(20)
+        return text
+      },
+    })
+
+    const subscription1Handler = jest.fn()
+    const subscription2Handler = jest.fn()
+
+    const unsubscribe1 = mutation.subscribe(subscription1Handler)
+    const unsubscribe2 = mutation.subscribe(subscription2Handler)
+
+    mutation.mutate()
+
+    unsubscribe1()
+
+    await waitFor(() => {
+      // 1 call: loading
+      expect(subscription1Handler).toBeCalledTimes(1)
+      // 2 calls: loading, success
+      expect(subscription2Handler).toBeCalledTimes(2)
+    })
+
+    // Clean-up
+    unsubscribe2()
+  })
+
+  test('should not notify listeners if options.listeners is set to false', async () => {
+    const mutation = new MutationObserver(queryClient, {
+      mutationFn: async (text: string) => {
+        await sleep(20)
+        return text
+      },
+    })
+
+    const subscriptionHandler = jest.fn()
+    const unsubscribe = mutation.subscribe(subscriptionHandler)
+    mutation.mutate()
+
+    await waitFor(() => {
+      // 2 calls: loading, success
+      expect(subscriptionHandler).toBeCalledTimes(2)
+    })
+    subscriptionHandler.mockReset()
+
+    // Force a notification with listeners set to false
+    // because there is no existing usage of notify with listeners set to false
+    mutation['notify']({ listeners: false })
+
+    await waitFor(() => {
+      // 0 call because no notification has been sent
+      expect(subscriptionHandler).toBeCalledTimes(0)
+    })
+
+    unsubscribe()
+  })
+})
diff --git a/src/core/tests/mutations.test.tsx b/src/core/tests/mutations.test.tsx
index 45dcdd547..f606ee602 100644
--- a/src/core/tests/mutations.test.tsx
+++ b/src/core/tests/mutations.test.tsx
@@ -300,4 +300,118 @@ describe('mutations', () => {
     expect(onSuccess).toHaveBeenCalled()
     expect(onSettled).toHaveBeenCalled()
   })
+
+  test('setState should update the mutation state', async () => {
+    const mutation = new MutationObserver(queryClient, {
+      mutationFn: async () => {
+        return 'update'
+      },
+      onMutate: text => text,
+    })
+    await mutation.mutate()
+    expect(mutation.getCurrentResult().data).toEqual('update')
+
+    // Force setState usage
+    // because no use case has been found using mutation.setState
+    const currentMutation = mutation['currentMutation']
+    currentMutation?.setState({
+      context: undefined,
+      variables: undefined,
+      data: 'new',
+      error: undefined,
+      failureCount: 0,
+      isPaused: false,
+      status: 'success',
+    })
+
+    expect(mutation.getCurrentResult().data).toEqual('new')
+  })
+
+  test('addObserver should not add an existing observer', async () => {
+    const mutation = new MutationObserver(queryClient, {
+      mutationFn: async () => {
+        return 'update'
+      },
+      onMutate: text => text,
+    })
+    await mutation.mutate()
+
+    // Force addObserver usage to add an existing observer
+    // because no use case has been found
+    const currentMutation = mutation['currentMutation']!
+    expect(currentMutation['observers'].length).toEqual(1)
+    currentMutation?.addObserver(mutation)
+
+    expect(currentMutation['observers'].length).toEqual(1)
+  })
+
+  test('executeMutation should throw an error if no mutationFn found', async () => {
+    const consoleMock = mockConsoleError()
+
+    const mutation = new MutationObserver(queryClient, {
+      mutationFn: undefined,
+      retry: false,
+    })
+
+    let error: any
+    try {
+      await mutation.mutate()
+    } catch (err) {
+      error = err
+    }
+    expect(error).toEqual('No mutationFn found')
+
+    consoleMock.mockRestore()
+  })
+
+  test('cancel mutation should not call mutationFn if the current retrier is undefined', async () => {
+    const mutationFn = jest.fn().mockImplementation(async () => {
+      await sleep(20)
+      return 'data'
+    })
+
+    const observer = new MutationObserver(queryClient, {
+      mutationKey: 'key',
+      mutationFn,
+    })
+
+    observer.mutate()
+    const mutation = queryClient
+      .getMutationCache()
+      .find({ mutationKey: 'key' })!
+    await sleep(10)
+
+    // Force current mutation retryer to be undefined
+    // because not use case has been found
+    mutation['retryer'] = undefined
+    mutationFn.mockReset()
+    await mutation.cancel()
+
+    await sleep(30)
+    expect(mutationFn).toHaveBeenCalledTimes(0)
+  })
+
+  test('reducer should return the state for an unknown action type', async () => {
+    const observer = new MutationObserver(queryClient, {
+      mutationKey: 'key',
+      mutationFn: async () => 'data',
+    })
+
+    const spy = jest.fn()
+    const unsubscribe = observer.subscribe(spy)
+    observer.mutate()
+    const mutation = queryClient
+      .getMutationCache()
+      .find({ mutationKey: 'key' })!
+    const prevState = observer.getCurrentResult()
+    spy.mockReset()
+
+    // Force dispatch unknown action type
+    // because no use case has been found
+    //@ts-expect-error
+    mutation.dispatch({ type: 'unknown' })
+    expect(spy).toHaveBeenCalledWith(prevState)
+
+    unsubscribe()
+  })
 })
diff --git a/src/core/tests/notifyManager.test.tsx b/src/core/tests/notifyManager.test.tsx
new file mode 100644
index 000000000..53816aa76
--- /dev/null
+++ b/src/core/tests/notifyManager.test.tsx
@@ -0,0 +1,32 @@
+import { NotifyManager } from '../notifyManager'
+import { sleep } from '../../devtools/tests/utils'
+
+describe('notifyManager', () => {
+  it('should use default notifyFn', async () => {
+    const notifyManagerTest = new NotifyManager()
+    const callbackSpy = jest.fn()
+    notifyManagerTest.schedule(callbackSpy)
+    await sleep(1)
+    expect(callbackSpy).toHaveBeenCalled()
+  })
+
+  it('should use default batchNotifyFn', async () => {
+    const notifyManagerTest = new NotifyManager()
+    const callbackScheduleSpy = jest
+      .fn()
+      .mockImplementation(async () => await sleep(20))
+    const callbackBatchLevel2Spy = jest.fn().mockImplementation(async () => {
+      notifyManagerTest.schedule(callbackScheduleSpy)
+    })
+    const callbackBatchLevel1Spy = jest.fn().mockImplementation(async () => {
+      notifyManagerTest.batch(callbackBatchLevel2Spy)
+    })
+
+    notifyManagerTest.batch(callbackBatchLevel1Spy)
+
+    await sleep(30)
+    expect(callbackBatchLevel1Spy).toHaveBeenCalledTimes(1)
+    expect(callbackBatchLevel2Spy).toHaveBeenCalledTimes(1)
+    expect(callbackScheduleSpy).toHaveBeenCalledTimes(1)
+  })
+})
diff --git a/src/core/tests/onlineManager.test.tsx b/src/core/tests/onlineManager.test.tsx
new file mode 100644
index 000000000..2e7d35c3f
--- /dev/null
+++ b/src/core/tests/onlineManager.test.tsx
@@ -0,0 +1,101 @@
+import { onlineManager } from '../onlineManager'
+import { sleep } from '../utils'
+
+describe('onlineManager', () => {
+  afterEach(() => {
+    // Reset removeEventListener private property to avoid side effects between tests
+    onlineManager['removeEventListener'] = undefined
+  })
+
+  test('isOnline should return true if navigator is undefined', () => {
+    const navigatorSpy = jest.spyOn(globalThis, 'navigator', 'get')
+
+    // Force navigator to be undefined
+    //@ts-expect-error
+    navigatorSpy.mockImplementation(() => undefined)
+    expect(onlineManager.isOnline()).toBeTruthy()
+
+    navigatorSpy.mockRestore()
+  })
+
+  test('isOnline should return true if navigator.onLine is true', () => {
+    const navigatorSpy = jest.spyOn(navigator, 'onLine', 'get')
+    navigatorSpy.mockImplementation(() => true)
+
+    expect(onlineManager.isOnline()).toBeTruthy()
+
+    navigatorSpy.mockRestore()
+  })
+
+  test('setEventListener should use online boolean arg', async () => {
+    let count = 0
+
+    const setup = (setOnline: (online?: boolean) => void) => {
+      setTimeout(() => {
+        count++
+        setOnline(false)
+      }, 20)
+      return () => void 0
+    }
+
+    onlineManager.setEventListener(setup)
+
+    await sleep(30)
+    expect(count).toEqual(1)
+    expect(onlineManager.isOnline()).toBeFalsy()
+  })
+
+  test('setEventListener should call previous remove handler when replacing an event listener', () => {
+    const remove1Spy = jest.fn()
+    const remove2Spy = jest.fn()
+
+    onlineManager.setEventListener(() => remove1Spy)
+    onlineManager.setEventListener(() => remove2Spy)
+
+    expect(remove1Spy).toHaveBeenCalledTimes(1)
+    expect(remove2Spy).not.toHaveBeenCalled()
+  })
+
+  test('setEventListener should not set window listener if window.addEventListener is not defined', async () => {
+    const { addEventListener } = globalThis.window
+
+    // @ts-expect-error
+    globalThis.window.addEventListener = undefined
+
+    const setEventListenerSpy = jest.spyOn(onlineManager, 'setEventListener')
+
+    const unsubscribe = onlineManager.subscribe()
+    expect(setEventListenerSpy).toHaveBeenCalledTimes(0)
+
+    unsubscribe()
+    globalThis.window.addEventListener = addEventListener
+  })
+
+  test('it should replace default window listener when a new event listener is set', async () => {
+    const addEventListenerSpy = jest.spyOn(
+      globalThis.window,
+      'addEventListener'
+    )
+
+    const removeEventListenerSpy = jest.spyOn(
+      globalThis.window,
+      'removeEventListener'
+    )
+
+    // Should set the default event listener with window event listeners
+    const unsubscribe = onlineManager.subscribe()
+    expect(addEventListenerSpy).toHaveBeenCalledTimes(2)
+
+    // Should replace the window default event listener by a new one
+    // and it should call window.removeEventListener twice
+    onlineManager.setEventListener(() => {
+      return () => void 0
+    })
+
+    expect(removeEventListenerSpy).toHaveBeenCalledTimes(2)
+
+    unsubscribe()
+    addEventListenerSpy.mockRestore()
+    removeEventListenerSpy.mockRestore()
+  })
+})
diff --git a/src/core/tests/queriesObserver.test.tsx b/src/core/tests/queriesObserver.test.tsx
index e424987ff..8aca22333 100644
--- a/src/core/tests/queriesObserver.test.tsx
+++ b/src/core/tests/queriesObserver.test.tsx
@@ -1,5 +1,12 @@
+import { waitFor } from '@testing-library/react'
 import { sleep, queryKey } from '../../react/tests/utils'
-import { QueryClient, QueriesObserver, QueryObserverResult } from '../..'
+import {
+  QueryClient,
+  QueriesObserver,
+  QueryObserverResult,
+  QueryObserver,
+} from '../..'
+import { QueryKey } from '..'
 
 describe('queriesObserver', () => {
   let queryClient: QueryClient
@@ -230,4 +237,70 @@ describe('queriesObserver', () => {
     expect(queryFn1).toHaveBeenCalledTimes(1)
     expect(queryFn2).toHaveBeenCalledTimes(1)
   })
+
+  test('should not destroy the observer if there is still a subscription', async () => {
+    const key1 = queryKey()
+    const observer = new QueriesObserver(queryClient, [
+      {
+        queryKey: key1,
+        queryFn: async () => {
+          await sleep(20)
+          return 1
+        },
+      },
+    ])
+
+    const subscription1Handler = jest.fn()
+    const subscription2Handler = jest.fn()
+
+    const unsubscribe1 = observer.subscribe(subscription1Handler)
+    const unsubscribe2 = observer.subscribe(subscription2Handler)
+
+    unsubscribe1()
+
+    await waitFor(() => {
+      // 1 call: loading
+      expect(subscription1Handler).toBeCalledTimes(1)
+      // 1 call: success
+      expect(subscription2Handler).toBeCalledTimes(1)
+    })
+
+    // Clean-up
+    unsubscribe2()
+  })
+
+  test('onUpdate should not update the result for an unknown observer', async () => {
+    const key1 = queryKey()
+    const key2 = queryKey()
+
+    const queriesObserver = new QueriesObserver(queryClient, [
+      {
+        queryKey: key1,
+        queryFn: () => 1,
+      },
+    ])
+
+    const newQueryObserver = new QueryObserver<
+      unknown,
+      unknown,
+      unknown,
+      unknown,
+      QueryKey
+    >(queryClient, {
+      queryKey: key2,
+      queryFn: () => 2,
+    })
+
+    // Force onUpdate with an unknown QueryObserver
+    // because no existing use case has been found in the lib
+    queriesObserver['onUpdate'](
+      newQueryObserver,
+      // The current queries observer result is re-used here
+      // to use a typescript friendly result
+      queriesObserver.getCurrentResult()[0]!
+    )
+
+    // Should not alter the result
+    expect(queriesObserver.getCurrentResult()[-1]).toBeUndefined()
+  })
 })
diff --git a/src/core/tests/query.test.tsx b/src/core/tests/query.test.tsx
index ddf5f5a78..583799cfb 100644
--- a/src/core/tests/query.test.tsx
+++ b/src/core/tests/query.test.tsx
@@ -646,4 +646,255 @@ describe('query', () => {
       })
     )
   })
+
+  test('should set default options', async () => {
+    const key = queryKey()
+
+    await queryClient.prefetchQuery(key, () => 'data')
+    const query = queryCache.find(key)!
+
+    query.setDefaultOptions({ retryDelay: 20 })
+
+    await queryClient.prefetchQuery(key, () => 'data', {
+      cacheTime: 100,
+    })
+
+    expect(query.options).toMatchObject({ cacheTime: 100, retryDelay: 20 })
+  })
+
+  test('should refetch the observer when online method is called', async () => {
+    const key = queryKey()
+
+    const observer = new QueryObserver(queryClient, {
+      queryKey: key,
+      queryFn: () => 'data',
+    })
+
+    const refetchSpy = jest.spyOn(observer, 'refetch')
+    const unsubscribe = observer.subscribe()
+    queryCache.onOnline()
+
+    // Should refetch the observer
+    expect(refetchSpy).toHaveBeenCalledTimes(1)
+
+    unsubscribe()
+    refetchSpy.mockRestore()
+  })
+
+  test('should not add an existing observer', async () => {
+    const key = queryKey()
+
+    await queryClient.prefetchQuery(key, () => 'data')
+    const query = queryCache.find(key)!
+    expect(query.getObserversCount()).toEqual(0)
+
+    const observer = new QueryObserver(queryClient, {
+      queryKey: key,
+    })
+    expect(query.getObserversCount()).toEqual(0)
+
+    query.addObserver(observer)
+    expect(query.getObserversCount()).toEqual(1)
+
+    query.addObserver(observer)
+    expect(query.getObserversCount()).toEqual(1)
+  })
+
+  test('should not try to remove an observer that does not exist', async () => {
+    const key = queryKey()
+
+    await queryClient.prefetchQuery(key, () => 'data')
+    const query = queryCache.find(key)!
+    const observer = new QueryObserver(queryClient, {
+      queryKey: key,
+    })
+    expect(query.getObserversCount()).toEqual(0)
+
+    const notifySpy = jest.spyOn(queryCache, 'notify')
+    expect(() => query.removeObserver(observer)).not.toThrow()
+    expect(notifySpy).not.toHaveBeenCalled()
+
+    notifySpy.mockRestore()
+  })
+
+  test('should not dispatch "invalidate" on invalidate() if already invalidated', async () => {
+    const key = queryKey()
+
+    await queryClient.prefetchQuery(key, () => 'data')
+    const query = queryCache.find(key)!
+
+    query.invalidate()
+    expect(query.state.isInvalidated).toBeTruthy()
+
+    const dispatchOriginal = query['dispatch']
+    const dispatchSpy = jest.fn()
+    query['dispatch'] = dispatchSpy
+
+    query.invalidate()
+
+    expect(query.state.isInvalidated).toBeTruthy()
+    expect(dispatchSpy).not.toHaveBeenCalled()
+
+    query['dispatch'] = dispatchOriginal
+  })
+
+  test('reducer should return the state for an unknown action type', async () => {
+    const key = queryKey()
+
+    await queryClient.prefetchQuery(key, () => 'data')
+    const query = queryCache.find(key)!
+
+    // Force unknown action type
+    //@ts-expect-error
+    const reducedState = query['reducer'](query.state, { type: 'unknown' })
+    expect(reducedState).toEqual(query.state)
+  })
+
+  test('fetch should not dispatch "fetch" if state meta and fetchOptions meta are the same object', async () => {
+    const key = queryKey()
+
+    const queryFn = async () => {
+      await sleep(10)
+      return 'data'
+    }
+
+    await queryClient.prefetchQuery(key, queryFn)
+    const query = queryCache.find(key)!
+
+    const meta = { meta1: '1' }
+
+    // This first fetch will set the state.meta value
+    query.fetch(
+      {
+        queryKey: key,
+        queryFn,
+      },
+      {
+        meta,
+      }
+    )
+
+    // Spy on private dispatch method
+    const dispatchOriginal = query['dispatch']
+    const dispatchSpy = jest.fn()
+    query['dispatch'] = dispatchSpy
+
+    // Second fetch in parallel with the same meta
+    query.fetch(
+      {
+        queryKey: key,
+        queryFn,
+      },
+      {
+        meta,
+        // cancelRefetch must be set to true to enter in the case to test
+        // where isFetching is true
+        cancelRefetch: true,
+      }
+    )
+
+    // Should not call dispatch with type set to fetch
+    expect(dispatchSpy).not.toHaveBeenCalledWith({
+      meta,
+      type: 'fetch',
+    })
+
+    // Clean-up
+    await sleep(20)
+    query['dispatch'] = dispatchOriginal
+  })
+
+  test('fetch should not set the signal in the queryFnContext if AbortController is undefined', async () => {
+    const key = queryKey()
+
+    // Mock the AbortController to be undefined
+    const AbortControllerOriginal = globalThis['AbortController']
+    //@ts-expect-error
+    globalThis['AbortController'] = undefined
+
+    let signalTest: any
+    await queryClient.prefetchQuery(key, ({ signal }) => {
+      signalTest = signal
+    })
+
+    expect(signalTest).toBeUndefined()
+
+    // Clean-up
+    //@ts-ignore
+    globalThis['AbortController'] = AbortControllerOriginal
+  })
+
+  test('fetch should throw an error if the queryFn is not defined', async () => {
+    const consoleMock = mockConsoleError()
+    const key = queryKey()
+
+    const observer = new QueryObserver(queryClient, {
+      queryKey: key,
+      queryFn: undefined,
+      retry: false,
+    })
+
+    const unsubscribe = observer.subscribe()
+    await sleep(10)
+    expect(consoleMock).toHaveBeenCalledWith('Missing queryFn')
+
+    unsubscribe()
+    consoleMock.mockRestore()
+  })
+
+  test('fetch should dispatch fetch if is fetching and current promise is undefined', async () => {
+    const key = queryKey()
+
+    const queryFn = async () => {
+      await sleep(10)
+      return 'data'
+    }
+
+    await queryClient.prefetchQuery(key, queryFn)
+    const query = queryCache.find(key)!
+
+    query.fetch({
+      queryKey: key,
+      queryFn,
+    })
+
+    // Force promise to undefined
+    // because no use case have been identified
+    query['promise'] = undefined
+
+    // Spy on private dispatch method
+    const dispatchOriginal = query['dispatch']
+    const dispatchSpy = jest.fn()
+    query['dispatch'] = dispatchSpy
+
+    query.fetch({
+      queryKey: key,
+      queryFn,
+    })
+
+    // Should call dispatch with type set to fetch
+    expect(dispatchSpy).toHaveBeenCalledWith({
+      meta: undefined,
+      type: 'fetch',
+    })
+
+    // Clean-up
+    await sleep(20)
+    query['dispatch'] = dispatchOriginal
+  })
+
+  test('constructor should call initialDataUpdatedAt if defined as a function', async () => {
+    const key = queryKey()
+
+    const initialDataUpdatedAtSpy = jest.fn()
+
+    await queryClient.prefetchQuery({
+      queryKey: key,
+      queryFn: () => 'data',
+      initialData: 'initial',
+      initialDataUpdatedAt: initialDataUpdatedAtSpy,
+    })
+
+    expect(initialDataUpdatedAtSpy).toHaveBeenCalled()
+  })
 })
diff --git a/src/core/tests/queryCache.test.tsx b/src/core/tests/queryCache.test.tsx
index 747d49e7b..cf26eb3ef 100644
--- a/src/core/tests/queryCache.test.tsx
+++ b/src/core/tests/queryCache.test.tsx
@@ -1,5 +1,6 @@
 import { sleep, queryKey, mockConsoleError } from '../../react/tests/utils'
 import { QueryCache, QueryClient } from '../..'
+import { Query } from '.././query'
 
 describe('queryCache', () => {
   let queryClient: QueryClient
@@ -64,6 +65,13 @@ describe('queryCache', () => {
       const query = queryCache.find(key)!
       expect(query).toBeDefined()
     })
+
+    test('find should filter correctly with exact set to false', async () => {
+      const key = queryKey()
+      await queryClient.prefetchQuery(key, () => 'data1')
+      const query = queryCache.find(key, { exact: false })!
+      expect(query).toBeDefined()
+    })
   })
 
   describe('findAll', () => {
@@ -129,6 +137,14 @@ describe('queryCache', () => {
       ).toEqual([query3])
       expect(queryCache.findAll('posts')).toEqual([query4])
     })
+
+    test('should return all the queries when no filters are defined', async () => {
+      const key1 = queryKey()
+      const key2 = queryKey()
+      await queryClient.prefetchQuery(key1, () => 'data1')
+      await queryClient.prefetchQuery(key2, () => 'data2')
+      expect(queryCache.findAll().length).toBe(2)
+    })
   })
 
   describe('QueryCacheConfig.onError', () => {
@@ -158,4 +174,43 @@ describe('queryCache', () => {
       expect(onSuccess).toHaveBeenCalledWith({ data: 5 }, query)
     })
   })
+
+  describe('QueryCache.add', () => {
+    test('should not try to add a query already added to the cache', async () => {
+      const key = queryKey()
+      const hash = `["${key}"]`
+
+      await queryClient.prefetchQuery(key, () => 'data1')
+
+      // Directly add the query from the cache
+      // to simulate a race condition
+      const query = queryCache['queriesMap'][hash] as Query
+      const queryClone = Object.assign({}, query)
+
+      // No error should be thrown when trying to add the query
+      queryCache.add(queryClone)
+      expect(queryCache['queries'].length).toEqual(1)
+
+      // Clean-up to avoid an error when queryClient.clear()
+      delete queryCache['queriesMap'][hash]
+    })
+
+    describe('QueryCache.remove', () => {
+      test('should not try to remove a query already removed from the cache', async () => {
+        const key = queryKey()
+        const hash = `["${key}"]`
+
+        await queryClient.prefetchQuery(key, () => 'data1')
+
+        // Directly remove the query from the cache
+        // to simulate a race condition
+        const query = queryCache['queriesMap'][hash] as Query
+        const queryClone = Object.assign({}, query)
+        delete queryCache['queriesMap'][hash]
+
+        // No error should be thrown when trying to remove the query
+        expect(() => queryCache.remove(queryClone)).not.toThrow()
+      })
+    })
+  })
 })
diff --git a/src/core/tests/queryClient.test.tsx b/src/core/tests/queryClient.test.tsx
index 8757c3112..5e0ee089a 100644
--- a/src/core/tests/queryClient.test.tsx
+++ b/src/core/tests/queryClient.test.tsx
@@ -5,7 +5,9 @@ import {
   QueryClient,
   QueryFunction,
   QueryObserver,
+  MutationObserver,
 } from '../..'
+import { focusManager, onlineManager } from '..'
 
 describe('queryClient', () => {
   let queryClient: QueryClient
@@ -47,6 +49,15 @@ describe('queryClient', () => {
       const newQuery = testClient.getQueryCache().find(key)
       expect(newQuery?.options.cacheTime).toBe(Infinity)
     })
+
+    test('should get defaultOptions', async () => {
+      const queryFn = () => 'data'
+      const defaultOptions = { queries: { queryFn } }
+      const testClient = new QueryClient({
+        defaultOptions,
+      })
+      expect(testClient.getDefaultOptions()).toMatchObject(defaultOptions)
+    })
   })
 
   describe('setQueryDefaults', () => {
@@ -101,6 +112,15 @@ describe('queryClient', () => {
       })
       expect(observer.getCurrentResult().status).toBe('idle')
     })
+
+    test('should update existing query defaults', async () => {
+      const key = queryKey()
+      const queryOptions1 = { queryFn: () => 'data' }
+      const queryOptions2 = { retry: false }
+      queryClient.setQueryDefaults(key, queryOptions1)
+      queryClient.setQueryDefaults(key, queryOptions2)
+      expect(queryClient.getQueryDefaults(key)).toMatchObject(queryOptions2)
+    })
   })
 
   describe('setQueryData', () => {
@@ -170,6 +190,35 @@ describe('queryClient', () => {
       expect(updater).toHaveBeenCalled()
       expect(queryCache.find(key)!.state.data).toEqual('new data + test data')
     })
+
+    test('should use prev data if an isDataEqual function is defined and returns "true"', () => {
+      const key = queryKey()
+
+      queryClient.setDefaultOptions({
+        queries: { isDataEqual: (_prev, data) => data === 'data' },
+      })
+      queryClient.setQueryData(key, 'prev data')
+      queryClient.setQueryData(key, 'data')
+
+      expect(queryCache.find(key)!.state.data).toEqual('prev data')
+    })
+
+    test('should set the new data without comparison if structuralSharing is set to false', () => {
+      const key = queryKey()
+
+      queryClient.setDefaultOptions({
+        queries: {
+          structuralSharing: false,
+        },
+      })
+
+      const oldData = { value: true }
+      const newData = { value: true }
+      queryClient.setQueryData(key, oldData)
+      queryClient.setQueryData(key, newData)
+
+      expect(queryCache.find(key)!.state.data).toBe(newData)
+    })
   })
 
   describe('setQueriesData', () => {
@@ -565,6 +614,25 @@ describe('queryClient', () => {
       })
       consoleMock.mockRestore()
     })
+
+    test('should not revert if revert option is set to false', async () => {
+      const consoleMock = mockConsoleError()
+      const key1 = queryKey()
+      await queryClient.fetchQuery(key1, async () => {
+        return 'data'
+      })
+      queryClient.fetchQuery(key1, async () => {
+        await sleep(1000)
+        return 'data2'
+      })
+      await sleep(10)
+      await queryClient.cancelQueries(key1, {}, { revert: false })
+      const state1 = queryClient.getQueryState(key1)
+      expect(state1).toMatchObject({
+        status: 'error',
+      })
+      consoleMock.mockRestore()
+    })
   })
 
   describe('refetchQueries', () => {
@@ -743,6 +811,30 @@ describe('queryClient', () => {
       expect(queryFn1).toHaveBeenCalledTimes(1)
       expect(queryFn2).toHaveBeenCalledTimes(1)
     })
+
+    test('should throw an error if throwOnError option is set to true', async () => {
+      const consoleMock = mockConsoleError()
+      const key1 = queryKey()
+      const queryFnError = () => Promise.reject('error')
+      try {
+        await queryClient.fetchQuery({
+          queryKey: key1,
+          queryFn: queryFnError,
+          retry: false,
+        })
+      } catch {}
+      let error: any
+      try {
+        await queryClient.refetchQueries(
+          { queryKey: key1 },
+          { throwOnError: true }
+        )
+      } catch (err) {
+        error = err
+      }
+      expect(error).toEqual('error')
+      consoleMock.mockRestore()
+    })
   })
 
   describe('invalidateQueries', () => {
@@ -1040,4 +1132,89 @@ describe('queryClient', () => {
       })
     })
   })
+
+  describe('focusManager and onlineManager', () => {
+    test('should not notify queryCache and mutationCache if not focused or online', async () => {
+      const testClient = new QueryClient()
+      testClient.mount()
+
+      const queryCacheOnFocusSpy = jest.spyOn(
+        testClient.getQueryCache(),
+        'onFocus'
+      )
+      const queryCacheOnOnlineSpy = jest.spyOn(
+        testClient.getQueryCache(),
+        'onOnline'
+      )
+      const mutationCacheOnFocusSpy = jest.spyOn(
+        testClient.getMutationCache(),
+        'onFocus'
+      )
+      const mutationCacheOnOnlineSpy = jest.spyOn(
+        testClient.getMutationCache(),
+        'onOnline'
+      )
+
+      focusManager.setFocused(false)
+      expect(queryCacheOnFocusSpy).not.toHaveBeenCalled()
+      expect(mutationCacheOnFocusSpy).not.toHaveBeenCalled()
+
+      focusManager.setFocused(true)
+      onlineManager.setOnline(false)
+      expect(queryCacheOnOnlineSpy).not.toHaveBeenCalled()
+      expect(mutationCacheOnOnlineSpy).not.toHaveBeenCalled()
+
+      focusManager.setFocused(true)
+      onlineManager.setOnline(false)
+      expect(queryCacheOnOnlineSpy).not.toHaveBeenCalled()
+      expect(mutationCacheOnOnlineSpy).not.toHaveBeenCalled()
+
+      focusManager.setFocused(false)
+      onlineManager.setOnline(true)
+      expect(queryCacheOnOnlineSpy).not.toHaveBeenCalled()
+      expect(mutationCacheOnOnlineSpy).not.toHaveBeenCalled()
+
+      testClient.unmount()
+      onlineManager.setOnline(true)
+      focusManager.setFocused(true)
+      queryCacheOnFocusSpy.mockRestore()
+      mutationCacheOnFocusSpy.mockRestore()
+      queryCacheOnOnlineSpy.mockRestore()
+      mutationCacheOnOnlineSpy.mockRestore()
+    })
+  })
+
+  describe('cancelMutations', () => {
+    test('should cancel mutations', async () => {
+      const key = queryKey()
+      const mutationObserver = new MutationObserver(queryClient, {
+        mutationKey: key,
+        mutationFn: async () => {
+          await sleep(20)
+          return 'data'
+        },
+        onMutate: text => text,
+      })
+      await mutationObserver.mutate()
+      const mutation = queryClient
+        .getMutationCache()
+        .find({ mutationKey: key })!
+      const mutationSpy = jest.spyOn(mutation, 'cancel')
+      queryClient.cancelMutations()
+      expect(mutationSpy).toHaveBeenCalled()
+      mutationSpy.mockRestore()
+    })
+  })
+  describe('setMutationDefaults', () => {
+    test('should update existing mutation defaults', () => {
+      const key = queryKey()
+      const mutationOptions1 = { mutationFn: async () => 'data' }
+      const mutationOptions2 = { retry: false }
+      queryClient.setMutationDefaults(key, mutationOptions1)
+      queryClient.setMutationDefaults(key, mutationOptions2)
+      expect(queryClient.getMutationDefaults(key)).toMatchObject(
+        mutationOptions2
+      )
+    })
+  })
 })
diff --git a/src/core/tests/queryObserver.test.tsx b/src/core/tests/queryObserver.test.tsx
index c94c0bfe1..1cba155cb 100644
--- a/src/core/tests/queryObserver.test.tsx
+++ b/src/core/tests/queryObserver.test.tsx
@@ -4,7 +4,12 @@ import {
   mockConsoleError,
   expectType,
 } from '../../react/tests/utils'
-import { QueryClient, QueryObserver, QueryObserverResult } from '../..'
+import {
+  QueryClient,
+  QueryObserver,
+  QueryObserverResult,
+  focusManager,
+} from '../..'
 
 describe('queryObserver', () => {
   let queryClient: QueryClient
@@ -490,4 +495,275 @@ describe('queryObserver', () => {
     expect(results[0]).toMatchObject({ status: 'success', data: 'placeholder' })
     expect(results[1]).toMatchObject({ status: 'success', data: 'data' })
   })
+
+  test('the retrier should not throw an error when reject if the retrier is already resolved', async () => {
+    const consoleMock = mockConsoleError()
+    const key = queryKey()
+    let count = 0
+
+    const observer = new QueryObserver(queryClient, {
+      queryKey: key,
+      queryFn: () => {
+        count++
+        return Promise.reject(`reject ${count}`)
+      },
+      retry: 1,
+      retryDelay: 20,
+    })
+
+    const unsubscribe = observer.subscribe()
+
+    // Simulate a race condition when an unsubscribe and a retry occur.
+    await sleep(20)
+    unsubscribe()
+
+    // A second reject is triggered for the retry
+    // but the retryer has already set isResolved to true
+    // so it does nothing and no error is thrown
+
+    // Should not log an error
+    queryClient.clear()
+    await sleep(40)
+    expect(consoleMock).not.toHaveBeenNthCalledWith(1, 'reject 1')
+
+    consoleMock.mockRestore()
+  })
+
+  test('should throw an error if enabled option type is not valid', async () => {
+    const key = queryKey()
+
+    expect(
+      () =>
+        new QueryObserver(queryClient, {
+          queryKey: key,
+          queryFn: () => 'data',
+          //@ts-expect-error
+          enabled: null,
+        })
+    ).toThrowError('Expected enabled to be a boolean')
+  })
+
+  test('getCurrentQuery should return the current query', async () => {
+    const key = queryKey()
+
+    const observer = new QueryObserver(queryClient, {
+      queryKey: key,
+      queryFn: () => 'data',
+    })
+
+    expect(observer.getCurrentQuery().queryKey).toEqual(key)
+  })
+
+  test('should throw an error if throwOnError option is true', async () => {
+    const key = queryKey()
+    const consoleMock = mockConsoleError()
+
+    const observer = new QueryObserver(queryClient, {
+      queryKey: key,
+      queryFn: () => Promise.reject('error'),
+      retry: false,
+    })
+
+    let error: string | null = null
+    try {
+      await observer.refetch({ throwOnError: true })
+    } catch (err) {
+      error = err as string
+    }
+
+    expect(error).toEqual('error')
+
+    consoleMock.mockRestore()
+  })
+
+  test('should not refetch in background if refetchIntervalInBackground is false', async () => {
+    const key = queryKey()
+    const queryFn = jest.fn()
+
+    focusManager.setFocused(false)
+    const observer = new QueryObserver(queryClient, {
+      queryKey: key,
+      queryFn,
+      refetchIntervalInBackground: false,
+      refetchInterval: 10,
+    })
+
+    const unsubscribe = observer.subscribe()
+    await sleep(30)
+
+    expect(queryFn).toHaveBeenCalledTimes(1)
+
+    // Clean-up
+    unsubscribe()
+    focusManager.setFocused(true)
+  })
+
+  test('should not use replaceEqualDeep for select value when structuralSharing option is true', async () => {
+    const key = queryKey()
+
+    const data = { value: 'data' }
+    const selectedData = { value: 'data' }
+
+    const observer = new QueryObserver(queryClient, {
+      queryKey: key,
+      queryFn: () => data,
+      select: () => data,
+    })
+
+    const unsubscribe = observer.subscribe()
+
+    await sleep(10)
+    expect(observer.getCurrentResult().data).toBe(data)
+
+    observer.setOptions({
+      queryKey: key,
+      queryFn: () => data,
+      structuralSharing: false,
+      select: () => selectedData,
+    })
+
+    await observer.refetch({ queryKey: key })
+    expect(observer.getCurrentResult().data).toBe(selectedData)
+
+    unsubscribe()
+  })
+
+  test('select function error using placeholderdata should log an error', () => {
+    const key = queryKey()
+    const consoleMock = mockConsoleError()
+
+    new QueryObserver(queryClient, {
+      queryKey: key,
+      queryFn: () => 'data',
+      placeholderData: 'placeholderdata',
+      select: () => {
+        throw new Error('error')
+      },
+    })
+
+    expect(consoleMock).toHaveBeenNthCalledWith(1, new Error('error'))
+
+    consoleMock.mockRestore()
+  })
+
+  test('should not use replaceEqualDeep for select value when structuralSharing option is true and placeholderdata is defined', () => {
+    const key = queryKey()
+
+    const data = { value: 'data' }
+    const selectedData1 = { value: 'data' }
+    const selectedData2 = { value: 'data' }
+    const placeholderData1 = { value: 'data' }
+    const placeholderData2 = { value: 'data' }
+
+    const observer = new QueryObserver(queryClient, {
+      queryKey: key,
+      queryFn: () => data,
+      select: () => data,
+    })
+
+    observer.setOptions({
+      queryKey: key,
+      queryFn: () => data,
+      select: () => {
+        if (true) return selectedData1
+      },
+      placeholderData: placeholderData1,
+    })
+
+    observer.setOptions({
+      queryKey: key,
+      queryFn: () => data,
+      select: () => {
+        return selectedData2
+      },
+      placeholderData: placeholderData2,
+      structuralSharing: false,
+    })
+
+    expect(observer.getCurrentResult().data).toBe(selectedData2)
+  })
+
+  test('should not use an undefined value returned by select as placeholderdata', () => {
+    const key = queryKey()
+
+    const data = { value: 'data' }
+    const selectedData = { value: 'data' }
+    const placeholderData1 = { value: 'data' }
+    const placeholderData2 = { value: 'data' }
+
+    const observer = new QueryObserver(queryClient, {
+      queryKey: key,
+      queryFn: () => data,
+      select: () => data,
+    })
+
+    observer.setOptions({
+      queryKey: key,
+      queryFn: () => data,
+      select: () => {
+        if (true) return selectedData
+      },
+      placeholderData: placeholderData1,
+    })
+
+    expect(observer.getCurrentResult().isPlaceholderData).toBe(true)
+
+    observer.setOptions({
+      queryKey: key,
+      queryFn: () => data,
+      //@ts-expect-error
+      select: () => undefined,
+      placeholderData: placeholderData2,
+    })
+
+    expect(observer.getCurrentResult().isPlaceholderData).toBe(false)
+  })
+
+  test('updateResult should not notify cache listeners if cache option is false', async () => {
+    const key = queryKey()
+
+    const data1 = { value: 'data 1' }
+    const data2 = { value: 'data 2' }
+
+    await queryClient.prefetchQuery(key, () => data1)
+    const observer = new QueryObserver(queryClient, {
+      queryKey: key,
+    })
+    await queryClient.prefetchQuery(key, () => data2)
+
+    const spy = jest.fn()
+    const unsubscribe = queryClient.getQueryCache().subscribe(spy)
+    observer.updateResult({ cache: false })
+
+    expect(spy).toHaveBeenCalledTimes(0)
+
+    unsubscribe()
+  })
+
+  test('should not notify observer when the stale timeout expires and the current result is stale', async () => {
+    const key = queryKey()
+    const queryFn = () => 'data'
+
+    await queryClient.prefetchQuery(key, queryFn)
+    const observer = new QueryObserver(queryClient, {
+      queryKey: key,
+      queryFn,
+      staleTime: 20,
+    })
+
+    const spy = jest.fn()
+    const unsubscribe = observer.subscribe(spy)
+    await queryClient.refetchQueries(key)
+    await sleep(10)
+
+    // Force isStale to true
+    // because no use case has been found to reproduce this condition
+    // @ts-ignore
+    observer['currentResult'].isStale = true
+    spy.mockReset()
+    await sleep(30)
+    expect(spy).not.toHaveBeenCalled()
+
+    unsubscribe()
+  })
 })
diff --git a/src/core/tests/utils.test.tsx b/src/core/tests/utils.test.tsx
index cacc8f634..65bc117bd 100644
--- a/src/core/tests/utils.test.tsx
+++ b/src/core/tests/utils.test.tsx
@@ -3,9 +3,12 @@ import {
   partialDeepEqual,
   isPlainObject,
   mapQueryStatusFilter,
+  parseMutationArgs,
+  matchMutation,
 } from '../utils'
 import { QueryClient, QueryCache, setLogger, Logger } from '../..'
 import { queryKey } from '../../react/tests/utils'
+import { Mutation } from '../mutation'
 
 describe('core/utils', () => {
   it('setLogger should override the default logger', async () => {
@@ -53,6 +56,20 @@ describe('core/utils', () => {
     it('should return `false` for undefined', () => {
       expect(isPlainObject(undefined)).toEqual(false)
     })
+
+    it('should return `true` for object with an undefined constructor', () => {
+      expect(isPlainObject(Object.create(null))).toBeTruthy()
+    })
+
+    it('should return `false` if constructor does not have an Object-specific method', () => {
+      class Foo {
+        abc: any
+        constructor() {
+          this.abc = {}
+        }
+      }
+      expect(isPlainObject(new Foo())).toBeFalsy()
+    })
   })
 
   describe('partialDeepEqual', () => {
@@ -327,4 +344,24 @@ describe('core/utils', () => {
       }
     )
   })
+
+  describe('parseMutationArgs', () => {
+    it('should return mutation options', () => {
+      const options = { mutationKey: 'key' }
+      expect(parseMutationArgs(options)).toMatchObject(options)
+    })
+  })
+
+  describe('matchMutation', () => {
+    it('should return false if mutationKey options is undefined', () => {
+      const filters = { mutationKey: 'key1' }
+      const queryClient = new QueryClient()
+      const mutation = new Mutation({
+        mutationId: 1,
+        mutationCache: queryClient.getMutationCache(),
+        options: {},
+      })
+      expect(matchMutation(filters, mutation)).toBeFalsy()
+    })
+  })
 })