Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions packages/react-relay/relay-hooks/__tests__/useMutation-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,96 @@ it('calls onComplete when mutation successfully resolved', async () => {
expect(isInFlightFn).toBeCalledWith(false);
});

it('calls onSettled after onCompleted on success', async () => {
const onError = jest.fn<ReadonlyArray<unknown>, unknown>();
const onCompleted = jest.fn<ReadonlyArray<unknown>, unknown>();
const onSettled = jest.fn<ReadonlyArray<unknown>, unknown>();
await render(environment, CommentCreateMutation);
await commit({onCompleted, onError, onSettled, variables});

isInFlightFn.mockClear();
// $FlowFixMe[method-unbinding] added when improving typing for this parameters
const operation = environment.executeMutation.mock.calls[0][0].operation;
await ReactTestingLibrary.act(() =>
environment.mock.resolve(operation, data),
);
expect(onCompleted).toBeCalledTimes(1);
expect(onSettled).toBeCalledTimes(1);
expect(onSettled).toBeCalledWith(
onCompleted.mock.calls[0][0],
null,
null,
);
expect(onError).toBeCalledTimes(0);
expect(isInFlightFn).toBeCalledWith(false);
});

it('calls onSettled with payload errors on success with errors', async () => {
const onError = jest.fn<ReadonlyArray<unknown>, unknown>();
const onCompleted = jest.fn<ReadonlyArray<unknown>, unknown>();
const onSettled = jest.fn<ReadonlyArray<unknown>, unknown>();
await render(environment, CommentCreateMutation);
await commit({onCompleted, onError, onSettled, variables});

isInFlightFn.mockClear();
// $FlowFixMe[method-unbinding] added when improving typing for this parameters
const operation = environment.executeMutation.mock.calls[0][0].operation;
await ReactTestingLibrary.act(() =>
environment.mock.resolve(operation, {
data: data.data as PayloadData,
errors: [{message: '<error0>'}] as Array<PayloadError>,
}),
);
expect(onCompleted).toBeCalledTimes(1);
expect(onSettled).toBeCalledTimes(1);
expect(onSettled).toBeCalledWith(
onCompleted.mock.calls[0][0],
[{message: '<error0>'}],
null,
);
expect(onError).toBeCalledTimes(0);
expect(isInFlightFn).toBeCalledWith(false);
});

it('calls onSettled after onError on error', async () => {
const onError = jest.fn<ReadonlyArray<unknown>, unknown>();
const onCompleted = jest.fn<ReadonlyArray<unknown>, unknown>();
const onSettled = jest.fn<ReadonlyArray<unknown>, unknown>();
const throwingUpdater = () => {
throw new Error('<error0>');
};
await render(environment, CommentCreateMutation);
await commit({onCompleted, onError, onSettled, updater: throwingUpdater, variables});

isInFlightFn.mockClear();
// $FlowFixMe[method-unbinding] added when improving typing for this parameters
const operation = environment.executeMutation.mock.calls[0][0].operation;
await ReactTestingLibrary.act(() =>
environment.mock.resolve(operation, data),
);
expect(onError).toBeCalledTimes(1);
expect(onSettled).toBeCalledTimes(1);
expect(onSettled).toBeCalledWith(null, null, new Error('<error0>'));
expect(onCompleted).toBeCalledTimes(0);
expect(isInFlightFn).toBeCalledWith(false);
});

it('calls onSettled even without onCompleted or onError', async () => {
const onSettled = jest.fn<ReadonlyArray<unknown>, unknown>();
await render(environment, CommentCreateMutation);
await commit({onSettled, variables});

isInFlightFn.mockClear();
// $FlowFixMe[method-unbinding] added when improving typing for this parameters
const operation = environment.executeMutation.mock.calls[0][0].operation;
await ReactTestingLibrary.act(() =>
environment.mock.resolve(operation, data),
);
expect(onSettled).toBeCalledTimes(1);
expect(onSettled.mock.calls[0][2]).toBe(null);
expect(isInFlightFn).toBeCalledWith(false);
});

describe('change useMutation input', () => {
let newEnv;
let CommentCreateMutation2;
Expand Down
5 changes: 5 additions & 0 deletions packages/react-relay/relay-hooks/useMutation.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ export interface UseMutationConfig<TMutation extends MutationParameters> {
configs?: DeclarativeMutationConfig[] | undefined;
onError?: ((error: Error) => void | null) | undefined;
onCompleted?: ((response: TMutation['response'], errors: PayloadError[] | null) => void | null) | undefined;
onSettled?: ((
response: TMutation['response'] | null,
errors: PayloadError[] | null | undefined,
error: Error | null | undefined,
) => void | null) | undefined;
onUnsubscribe?: (() => void | null) | undefined;
}

Expand Down
6 changes: 6 additions & 0 deletions packages/react-relay/relay-hooks/useMutation.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ export type UseMutationConfig<TMutation extends MutationParameters> = {
errors: ?Array<PayloadError>,
) => void,
onNext?: ?() => void,
onSettled?: ?(
response: TMutation['response'] | null,
errors: ?Array<PayloadError>,
error: ?Error,
) => void,
onUnsubscribe?: ?() => void,
optimisticResponse?: {
readonly rawResponse?: {...},
Expand All @@ -55,6 +60,7 @@ type UseMutationConfigInternal<TVariables, TData, TRawResponse> = {
onError?: ?(error: Error) => void,
onCompleted?: ?(response: TData, errors: ?Array<PayloadError>) => void,
onNext?: ?() => void,
onSettled?: ?(response: TData | null, errors: ?Array<PayloadError>, error: ?Error) => void,
onUnsubscribe?: ?() => void,
optimisticResponse?: TRawResponse,
optimisticUpdater?: ?SelectorStoreUpdater<TData>,
Expand Down
122 changes: 122 additions & 0 deletions packages/relay-runtime/mutations/__tests__/commitMutation-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1523,6 +1523,128 @@ describe('commitMutation()', () => {
expect(onError).toBeCalledTimes(1);
expect(onError.mock.calls[0][0]).toBe(error);
});

it('calls onSettled after onCompleted on success', () => {
/* $FlowFixMe[underconstrained-implicit-instantiation] error found when
* enabling Flow LTI mode */
const onSettled = jest.fn<_, void>();
commitMutation(environment, {
mutation,
onCompleted,
onError,
onSettled,
variables,
});
dataSource.next({
data: {
commentCreate: {
comment: {
body: {
text: 'Gave Relay',
},
id: '1',
},
},
},
});
dataSource.complete();

expect(onCompleted).toBeCalledTimes(1);
expect(onSettled).toBeCalledTimes(1);
expect(onSettled.mock.calls[0][0]).toEqual(onCompleted.mock.calls[0][0]);
expect(onSettled.mock.calls[0][1]).toBe(null);
expect(onSettled.mock.calls[0][2]).toBe(null);
expect(onError).toBeCalledTimes(0);
});

it('calls onSettled with payload errors on success with errors', () => {
/* $FlowFixMe[underconstrained-implicit-instantiation] error found when
* enabling Flow LTI mode */
const onSettled = jest.fn<_, void>();
commitMutation(environment, {
mutation,
onCompleted,
onError,
onSettled,
variables,
});
dataSource.next({
data: {
commentCreate: {
comment: {
body: {
text: 'Gave Relay',
},
id: '1',
},
},
},
errors: [
{
locations: [],
message: 'wtf',
severity: 'ERROR',
},
],
} as GraphQLResponseWithoutData);
dataSource.complete();

expect(onCompleted).toBeCalledTimes(1);
expect(onSettled).toBeCalledTimes(1);
expect(onSettled.mock.calls[0][0]).toEqual(onCompleted.mock.calls[0][0]);
expect(onSettled.mock.calls[0][1]).toEqual(onCompleted.mock.calls[0][1]);
expect(onSettled.mock.calls[0][2]).toBe(null);
expect(onError).toBeCalledTimes(0);
});

it('calls onSettled after onError on network error', () => {
/* $FlowFixMe[underconstrained-implicit-instantiation] error found when
* enabling Flow LTI mode */
const onSettled = jest.fn<_, void>();
const error = new Error('network failure');
commitMutation(environment, {
mutation,
onCompleted,
onError,
onSettled,
variables,
});
dataSource.error(error);

expect(onError).toBeCalledTimes(1);
expect(onSettled).toBeCalledTimes(1);
expect(onSettled.mock.calls[0][0]).toBe(null);
expect(onSettled.mock.calls[0][1]).toBe(null);
expect(onSettled.mock.calls[0][2]).toBe(error);
expect(onCompleted).toBeCalledTimes(0);
});

it('calls onSettled even without onCompleted or onError', () => {
/* $FlowFixMe[underconstrained-implicit-instantiation] error found when
* enabling Flow LTI mode */
const onSettled = jest.fn<_, void>();
commitMutation(environment, {
mutation,
onSettled,
variables,
});
dataSource.next({
data: {
commentCreate: {
comment: {
body: {
text: 'Gave Relay',
},
id: '1',
},
},
},
});
dataSource.complete();

expect(onSettled).toBeCalledTimes(1);
expect(onSettled.mock.calls[0][2]).toBe(null);
});
});

describe('commitMutation() cacheConfig', () => {
Expand Down
5 changes: 5 additions & 0 deletions packages/relay-runtime/mutations/commitMutation.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ export interface MutationConfig<TOperation extends MutationParameters> {
| ((response: TOperation['response'], errors: readonly PayloadError[] | null | undefined) => void)
| null
| undefined;
onSettled?: ((
response: TOperation['response'] | null,
errors: readonly PayloadError[] | null | undefined,
error: Error | null | undefined,
) => void) | null | undefined;
onUnsubscribe?: (() => void | null | undefined) | undefined;
/**
* An object whose type matches the raw response type of the mutation. Make sure you decorate
Expand Down
30 changes: 21 additions & 9 deletions packages/relay-runtime/mutations/commitMutation.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ export type MutationConfig<TMutation extends MutationParameters> = Readonly<{
) => void,
onError?: ?(error: Error) => void,
onNext?: ?() => void,
onSettled?: ?(
response: TMutation['response'] | null,
errors: ?Array<PayloadError>,
error: ?Error,
) => void,
onUnsubscribe?: ?() => void,
optimisticResponse?: {
readonly rawResponse?: {...},
Expand All @@ -66,6 +71,11 @@ export type CommitMutationConfig<TVariables, TData, TRawResponse> = Readonly<{
onCompleted?: ?(response: TData, errors: ?Array<PayloadError>) => void,
onError?: ?(error: Error) => void,
onNext?: ?() => void,
onSettled?: ?(
response: TData | null,
errors: ?Array<PayloadError>,
error: ?Error,
) => void,
onUnsubscribe?: ?() => void,
optimisticResponse?: TRawResponse,
optimisticUpdater?: ?SelectorStoreUpdater<TData>,
Expand Down Expand Up @@ -99,8 +109,7 @@ function commitMutation<
throw new Error('commitMutation: Expected mutation to be of type request');
}
let {optimisticResponse, optimisticUpdater, updater} = config;
const {configs, cacheConfig, onError, onUnsubscribe, variables, uploadables} =
config;
const {configs, cacheConfig, onUnsubscribe, variables, uploadables} = config;
const operation = createOperationDescriptor(
mutation,
variables,
Expand Down Expand Up @@ -147,16 +156,19 @@ function commitMutation<
})
.subscribe({
complete: () => {
const {onCompleted} = config;
if (onCompleted) {
const {onCompleted, onSettled} = config;
const payloadErrors = errors.length !== 0 ? errors : null;
if (onCompleted != null || onSettled != null) {
const snapshot = environment.lookup(operation.fragment);
onCompleted(
snapshot.data as $FlowFixMe,
errors.length !== 0 ? errors : null,
);
onCompleted?.(snapshot.data as $FlowFixMe, payloadErrors);
onSettled?.(snapshot.data as $FlowFixMe, payloadErrors, null);
}
},
error: onError,
error: (err: Error) => {
const {onError, onSettled} = config;
onError?.(err);
onSettled?.(null, null, err);
},
next: payload => {
if (Array.isArray(payload)) {
payload.forEach(item => {
Expand Down
Loading