Skip to content

Commit

Permalink
fix(angular-query): support throwOnError (#7052)
Browse files Browse the repository at this point in the history
  • Loading branch information
arnoud-dv committed Mar 7, 2024
1 parent b16761a commit 59dbe58
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 11 deletions.
@@ -1,7 +1,7 @@
import { Component, input, signal } from '@angular/core'
import { QueryClient } from '@tanstack/query-core'
import { TestBed } from '@angular/core/testing'
import { describe, expect, test, vi } from 'vitest'
import { describe, expect, vi } from 'vitest'
import { By } from '@angular/platform-browser'
import { injectMutation } from '../inject-mutation'
import { provideAngularQuery } from '../providers'
Expand Down Expand Up @@ -402,4 +402,57 @@ describe('injectMutation', () => {
expect(mutation1!.options.mutationKey).toEqual(['fake', 'value'])
expect(mutation2!.options.mutationKey).toEqual(['fake', 'updatedValue'])
})

describe('throwOnError', () => {
test('should evaluate throwOnError when mutation is expected to throw', async () => {
const err = new Error('Expected mock error. All is well!')
const boundaryFn = vi.fn()
const { mutate } = TestBed.runInInjectionContext(() => {
return injectMutation(() => ({
mutationKey: ['fake'],
mutationFn: () => {
return Promise.reject(err)
},
throwOnError: boundaryFn,
}))
})

mutate()

await resolveMutations()

expect(boundaryFn).toHaveBeenCalledTimes(1)
expect(boundaryFn).toHaveBeenCalledWith(err)
})
})

test('should throw when throwOnError is true', async () => {
const err = new Error('Expected mock error. All is well!')
const { mutateAsync } = TestBed.runInInjectionContext(() => {
return injectMutation(() => ({
mutationKey: ['fake'],
mutationFn: () => {
return Promise.reject(err)
},
throwOnError: true,
}))
})

await expect(() => mutateAsync()).rejects.toThrowError(err)
})

test('should throw when throwOnError function returns true', async () => {
const err = new Error('Expected mock error. All is well!')
const { mutateAsync } = TestBed.runInInjectionContext(() => {
return injectMutation(() => ({
mutationKey: ['fake'],
mutationFn: () => {
return Promise.reject(err)
},
throwOnError: () => true,
}))
})

await expect(() => mutateAsync()).rejects.toThrowError(err)
})
})
Expand Up @@ -201,11 +201,64 @@ describe('injectQuery', () => {
flush()
}))

describe('throwOnError', () => {
test('should evaluate throwOnError when query is expected to throw', fakeAsync(() => {
const boundaryFn = vi.fn()
TestBed.runInInjectionContext(() => {
return injectQuery(() => ({
queryKey: ['key12'],
queryFn: rejectFetcher,
throwOnError: boundaryFn,
}))
})

flush()

expect(boundaryFn).toHaveBeenCalledTimes(1)
expect(boundaryFn).toHaveBeenCalledWith(
Error('Some error'),
expect.objectContaining({
state: expect.objectContaining({ status: 'error' }),
}),
)
}))

test('should throw when throwOnError is true', fakeAsync(() => {
TestBed.runInInjectionContext(() => {
return injectQuery(() => ({
queryKey: ['key13'],
queryFn: rejectFetcher,
throwOnError: true,
}))
})

expect(() => {
flush()
}).toThrowError('Some error')
flush()
}))

test('should throw when throwOnError function returns true', fakeAsync(() => {
TestBed.runInInjectionContext(() => {
return injectQuery(() => ({
queryKey: ['key14'],
queryFn: rejectFetcher,
throwOnError: () => true,
}))
})

expect(() => {
flush()
}).toThrowError('Some error')
flush()
}))
})

test('should set state to error when queryFn returns reject promise', fakeAsync(() => {
const query = TestBed.runInInjectionContext(() => {
return injectQuery(() => ({
retry: false,
queryKey: ['key13'],
queryKey: ['key15'],
queryFn: rejectFetcher,
}))
})
Expand Down Expand Up @@ -238,7 +291,7 @@ describe('injectQuery', () => {
})

flush()
await fixture.detectChanges()
fixture.detectChanges()

expect(fixture.debugElement.nativeElement.textContent).toEqual(
'signal-input-required-test',
Expand Down
24 changes: 19 additions & 5 deletions packages/angular-query-experimental/src/create-base-query.ts
Expand Up @@ -11,6 +11,7 @@ import {
} from '@angular/core'
import { notifyManager } from '@tanstack/query-core'
import { signalProxy } from './signal-proxy'
import { shouldThrowError } from './util'
import { lazyInit } from './util/lazy-init/lazy-init'
import type {
QueryClient,
Expand Down Expand Up @@ -88,11 +89,24 @@ export function createBaseQuery<

// observer.trackResult is not used as this optimization is not needed for Angular
const unsubscribe = observer.subscribe(
notifyManager.batchCalls((val: QueryObserverResult<TData, TError>) => {
ngZone.run(() => {
resultSignal.set(val)
})
}),
notifyManager.batchCalls(
(state: QueryObserverResult<TData, TError>) => {
ngZone.run(() => {
if (
state.isError &&
!state.isFetching &&
// !isRestoring() && // todo: enable when client persistence is implemented
shouldThrowError(observer.options.throwOnError, [
state.error,
observer.getCurrentQuery(),
])
) {
throw state.error
}
resultSignal.set(state)
})
},
),
)
destroyRef.onDestroy(unsubscribe)

Expand Down
17 changes: 14 additions & 3 deletions packages/angular-query-experimental/src/inject-mutation.ts
Expand Up @@ -12,7 +12,7 @@ import { MutationObserver, notifyManager } from '@tanstack/query-core'
import { assertInjector } from './util/assert-injector/assert-injector'
import { signalProxy } from './signal-proxy'
import { injectQueryClient } from './inject-query-client'
import { noop } from './util'
import { noop, shouldThrowError } from './util'

import { lazyInit } from './util/lazy-init/lazy-init'
import type {
Expand Down Expand Up @@ -69,10 +69,21 @@ export function injectMutation<
const unsubscribe = observer.subscribe(
notifyManager.batchCalls(
(
val: MutationObserverResult<TData, TError, TVariables, TContext>,
state: MutationObserverResult<
TData,
TError,
TVariables,
TContext
>,
) => {
ngZone.run(() => {
result.set(val)
if (
state.isError &&
shouldThrowError(observer.options.throwOnError, [state.error])
) {
throw state.error
}
result.set(state)
})
},
),
Expand Down
12 changes: 12 additions & 0 deletions packages/angular-query-experimental/src/util/index.ts
@@ -1 +1,13 @@
export function shouldThrowError<T extends (...args: Array<any>) => boolean>(
throwError: boolean | T | undefined,
params: Parameters<T>,
): boolean {
// Allow throwError function to override throwing behavior on a per-error basis
if (typeof throwError === 'function') {
return throwError(...params)
}

return !!throwError
}

export function noop() {}

0 comments on commit 59dbe58

Please sign in to comment.