Skip to content
This repository was archived by the owner on Feb 23, 2024. It is now read-only.

Commit 4f34e6d

Browse files
authored
Improvements to emitEventWithAbort. (#4158)
* modify emitEventWithAbort to change return value `emitEventWithAbort` now returns an array of responses up to the first response that triggered an error or fail response instead of aborting on success responses too. * update add to cart form context provider * update checkout state context provider * update payment method data context provider * update tests and fix thrown error handling.
1 parent 001af33 commit 4f34e6d

File tree

6 files changed

+154
-85
lines changed

6 files changed

+154
-85
lines changed

assets/js/base/context/event-emit/emitters.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44
import { getObserversByPriority } from './utils';
55
import type { EventObserversType } from './types';
6+
import { isErrorResponse, isFailResponse } from '../hooks/use-emit-response';
67

78
/**
89
* Emits events on registered observers for the provided type and passes along
@@ -44,20 +45,24 @@ export const emitEvent = async (
4445

4546
/**
4647
* Emits events on registered observers for the provided type and passes along
47-
* the provided data. This event emitter will abort and return any value from
48-
* observers that return an object which should contain a type property.
48+
* the provided data. This event emitter will abort if an observer throws an
49+
* error or if the response includes an object with an error type property.
50+
*
51+
* Any successful observer responses before abort will be included in the returned package.
4952
*
5053
* @param {Object} observers The registered observers to omit to.
5154
* @param {string} eventType The event type being emitted.
5255
* @param {*} data Data passed along to the observer when it is invoked.
5356
*
54-
* @return {Promise} Returns a promise that resolves to either boolean or the return value of the aborted observer.
57+
* @return {Promise} Returns a promise that resolves to either boolean, or an array of responses
58+
* from registered observers that were invoked up to the point of an error.
5559
*/
5660
export const emitEventWithAbort = async (
5761
observers: EventObserversType,
5862
eventType: string,
5963
data: unknown
60-
): Promise< unknown > => {
64+
): Promise< Array< unknown > > => {
65+
const observerResponses = [];
6166
const observersByType = getObserversByPriority( observers, eventType );
6267
for ( const observer of observersByType ) {
6368
try {
@@ -67,16 +72,24 @@ export const emitEventWithAbort = async (
6772
}
6873
if ( ! response.hasOwnProperty( 'type' ) ) {
6974
throw new Error(
70-
'If you want to abort event emitter processing, your observer must return an object with a type property'
75+
'Returned objects from event emitter observers must return an object with a type property'
7176
);
7277
}
73-
return response;
78+
if ( isErrorResponse( response ) || isFailResponse( response ) ) {
79+
observerResponses.push( response );
80+
// early abort.
81+
return observerResponses;
82+
}
83+
// all potential abort conditions have been considered push the
84+
// response to the array.
85+
observerResponses.push( response );
7486
} catch ( e ) {
7587
// We don't handle thrown errors but just console.log for troubleshooting.
7688
// eslint-disable-next-line no-console
7789
console.error( e );
78-
return { type: 'error' };
90+
observerResponses.push( { type: 'error' } );
91+
return observerResponses;
7992
}
8093
}
81-
return true;
94+
return observerResponses;
8295
};

assets/js/base/context/event-emit/test/emitters.js

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ describe( 'Testing emitters', () => {
3030
'observerPromiseWithResolvedValue',
3131
{ priority: 10, callback: observerPromiseWithResolvedValue },
3232
],
33+
[
34+
'observerSuccessType',
35+
{
36+
priority: 10,
37+
callback: jest.fn().mockReturnValue( { type: 'success' } ),
38+
},
39+
],
3340
] );
3441
} );
3542
describe( 'Testing emitEvent()', () => {
@@ -39,11 +46,11 @@ describe( 'Testing emitters', () => {
3946
expect( console ).toHaveErroredWith( 'an error' );
4047
expect( observerA ).toHaveBeenCalledTimes( 1 );
4148
expect( observerB ).toHaveBeenCalledWith( 'foo' );
42-
expect( response ).toBe( true );
49+
expect( response ).toEqual( [ { type: 'success' } ] );
4350
} );
4451
} );
4552
describe( 'Testing emitEventWithAbort()', () => {
46-
it( 'does not abort on any return value other than an object with a type property', async () => {
53+
it( 'does not abort on any return value other than an object with an error or fail type property', async () => {
4754
observerMocks.delete( 'observerPromiseWithReject' );
4855
const observers = { test: observerMocks };
4956
const response = await emitEventWithAbort(
@@ -54,17 +61,16 @@ describe( 'Testing emitters', () => {
5461
expect( console ).not.toHaveErrored();
5562
expect( observerB ).toHaveBeenCalledTimes( 1 );
5663
expect( observerPromiseWithResolvedValue ).toHaveBeenCalled();
57-
expect( response ).toBe( true );
64+
expect( response ).toEqual( [ { type: 'success' } ] );
5865
} );
59-
it( 'Aborts on a return value with an object that has a type property', async () => {
66+
it( 'Aborts on a return value with an object that has a a fail type property', async () => {
6067
const validObjectResponse = jest
6168
.fn()
62-
.mockReturnValue( { type: 'success' } );
69+
.mockReturnValue( { type: 'failure' } );
6370
observerMocks.set( 'observerValidObject', {
6471
priority: 5,
6572
callback: validObjectResponse,
6673
} );
67-
observerMocks.delete( 'observerPromiseWithReject' );
6874
const observers = { test: observerMocks };
6975
const response = await emitEventWithAbort(
7076
observers,
@@ -74,7 +80,7 @@ describe( 'Testing emitters', () => {
7480
expect( console ).not.toHaveErrored();
7581
expect( validObjectResponse ).toHaveBeenCalledTimes( 1 );
7682
expect( observerPromiseWithResolvedValue ).not.toHaveBeenCalled();
77-
expect( response ).toEqual( { type: 'success' } );
83+
expect( response ).toEqual( [ { type: 'failure' } ] );
7884
} );
7985
it( 'throws an error on an object returned from observer without a type property', async () => {
8086
const failingObjectResponse = jest.fn().mockReturnValue( {} );
@@ -91,7 +97,7 @@ describe( 'Testing emitters', () => {
9197
expect( console ).toHaveErrored();
9298
expect( failingObjectResponse ).toHaveBeenCalledTimes( 1 );
9399
expect( observerPromiseWithResolvedValue ).not.toHaveBeenCalled();
94-
expect( response ).toEqual( { type: 'error' } );
100+
expect( response ).toEqual( [ { type: 'error' } ] );
95101
} );
96102
} );
97103
describe( 'Test Priority', () => {

assets/js/base/context/hooks/use-emit-response.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,19 @@ const noticeContexts = {
2525
EXPRESS_PAYMENTS: 'wc/express-payment-area',
2626
};
2727

28-
const isSuccessResponse = ( response ) => {
28+
export const isSuccessResponse = ( response ) => {
2929
return isResponseOf( response, responseTypes.SUCCESS );
3030
};
3131

32-
const isErrorResponse = ( response ) => {
32+
export const isErrorResponse = ( response ) => {
3333
return isResponseOf( response, responseTypes.ERROR );
3434
};
3535

36-
const isFailResponse = ( response ) => {
36+
export const isFailResponse = ( response ) => {
3737
return isResponseOf( response, responseTypes.FAIL );
3838
};
3939

40-
const shouldRetry = ( response ) => {
40+
export const shouldRetry = ( response ) => {
4141
return typeof response.retry === 'undefined' || response.retry === true;
4242
};
4343

assets/js/base/context/providers/add-to-cart-form/form-state/index.js

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -206,17 +206,31 @@ export const AddToCartFormStateContextProvider = ( {
206206
*/
207207
useEffect( () => {
208208
if ( addToCartFormState.status === STATUS.AFTER_PROCESSING ) {
209+
// @todo: This data package differs from what is passed through in
210+
// the checkout state context. Should we introduce a "context"
211+
// property in the data package for this emitted event so that
212+
// observers are able to know what context the event is firing in?
209213
const data = {
210214
processingResponse: addToCartFormState.processingResponse,
211215
};
212216

213-
const handleErrorResponse = ( response ) => {
214-
if ( response.message ) {
215-
const errorOptions = response.messageContext
216-
? { context: response.messageContext }
217-
: undefined;
218-
addErrorNotice( response.message, errorOptions );
219-
}
217+
const handleErrorResponse = ( observerResponses ) => {
218+
let handled = false;
219+
observerResponses.forEach( ( response ) => {
220+
const { message, messageContext } = response;
221+
if (
222+
( isErrorResponse( response ) ||
223+
isFailResponse( response ) ) &&
224+
message
225+
) {
226+
const errorOptions = messageContext
227+
? { context: messageContext }
228+
: undefined;
229+
handled = true;
230+
addErrorNotice( message, errorOptions );
231+
}
232+
} );
233+
return handled;
220234
};
221235

222236
if ( addToCartFormState.hasError ) {
@@ -225,13 +239,8 @@ export const AddToCartFormStateContextProvider = ( {
225239
currentObservers,
226240
EMIT_TYPES.ADD_TO_CART_AFTER_PROCESSING_WITH_ERROR,
227241
data
228-
).then( ( response ) => {
229-
if (
230-
isErrorResponse( response ) ||
231-
isFailResponse( response )
232-
) {
233-
handleErrorResponse( response );
234-
} else {
242+
).then( ( observerResponses ) => {
243+
if ( ! handleErrorResponse( observerResponses ) ) {
235244
// no error handling in place by anything so let's fall back to default
236245
const message =
237246
data.processingResponse?.message ||
@@ -252,12 +261,8 @@ export const AddToCartFormStateContextProvider = ( {
252261
currentObservers,
253262
EMIT_TYPES.ADD_TO_CART_AFTER_PROCESSING_WITH_SUCCESS,
254263
data
255-
).then( ( response ) => {
256-
if (
257-
isErrorResponse( response ) ||
258-
isFailResponse( response )
259-
) {
260-
handleErrorResponse( response );
264+
).then( ( observerResponses ) => {
265+
if ( handleErrorResponse( observerResponses ) ) {
261266
// this will set an error which will end up
262267
// triggering the onAddToCartAfterProcessingWithError emitter.
263268
// and then setting to IDLE state.

assets/js/base/context/providers/cart-checkout/checkout-state/index.js

Lines changed: 53 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,26 @@ export const CheckoutStateProvider = ( {
260260
) {
261261
return;
262262
}
263+
264+
const handleErrorResponse = ( observerResponses ) => {
265+
let errorResponse = null;
266+
observerResponses.forEach( ( response ) => {
267+
const { message, messageContext } = response;
268+
if (
269+
( isErrorResponse( response ) ||
270+
isFailResponse( response ) ) &&
271+
message
272+
) {
273+
const errorOptions = messageContext
274+
? { context: messageContext }
275+
: undefined;
276+
errorResponse = response;
277+
addErrorNotice( message, errorOptions );
278+
}
279+
} );
280+
return errorResponse;
281+
};
282+
263283
if ( checkoutState.status === STATUS.AFTER_PROCESSING ) {
264284
const data = {
265285
redirectUrl: checkoutState.redirectUrl,
@@ -275,21 +295,14 @@ export const CheckoutStateProvider = ( {
275295
currentObservers.current,
276296
EMIT_TYPES.CHECKOUT_AFTER_PROCESSING_WITH_ERROR,
277297
data
278-
).then( ( response ) => {
279-
if (
280-
isErrorResponse( response ) ||
281-
isFailResponse( response )
282-
) {
283-
if ( response.message ) {
284-
const errorOptions = {
285-
id: response?.messageContext,
286-
context: response?.messageContext,
287-
};
288-
addErrorNotice( response.message, errorOptions );
289-
}
298+
).then( ( observerResponses ) => {
299+
const errorResponse = handleErrorResponse(
300+
observerResponses
301+
);
302+
if ( errorResponse !== null ) {
290303
// irrecoverable error so set complete
291-
if ( ! shouldRetry( response ) ) {
292-
dispatch( actions.setComplete( response ) );
304+
if ( ! shouldRetry( errorResponse ) ) {
305+
dispatch( actions.setComplete( errorResponse ) );
293306
} else {
294307
dispatch( actions.setIdle() );
295308
}
@@ -326,21 +339,34 @@ export const CheckoutStateProvider = ( {
326339
currentObservers.current,
327340
EMIT_TYPES.CHECKOUT_AFTER_PROCESSING_WITH_SUCCESS,
328341
data
329-
).then( ( response ) => {
330-
if ( isSuccessResponse( response ) ) {
331-
dispatch( actions.setComplete( response ) );
332-
} else if (
333-
isErrorResponse( response ) ||
334-
isFailResponse( response )
335-
) {
336-
if ( response.message ) {
337-
const errorOptions = response.messageContext
338-
? { context: response.messageContext }
342+
).then( ( observerResponses ) => {
343+
let successResponse, errorResponse;
344+
observerResponses.forEach( ( response ) => {
345+
if ( isSuccessResponse( response ) ) {
346+
// the last observer response always "wins" for success.
347+
successResponse = response;
348+
}
349+
if (
350+
isErrorResponse( response ) ||
351+
isFailResponse( response )
352+
) {
353+
errorResponse = response;
354+
}
355+
} );
356+
if ( successResponse && ! errorResponse ) {
357+
dispatch( actions.setComplete( successResponse ) );
358+
} else if ( errorResponse ) {
359+
if ( errorResponse.message ) {
360+
const errorOptions = errorResponse.messageContext
361+
? { context: errorResponse.messageContext }
339362
: undefined;
340-
addErrorNotice( response.message, errorOptions );
363+
addErrorNotice(
364+
errorResponse.message,
365+
errorOptions
366+
);
341367
}
342-
if ( ! shouldRetry( response ) ) {
343-
dispatch( actions.setComplete( response ) );
368+
if ( ! shouldRetry( errorResponse ) ) {
369+
dispatch( actions.setComplete( errorResponse ) );
344370
} else {
345371
// this will set an error which will end up
346372
// triggering the onCheckoutAfterProcessingWithError emitter.

0 commit comments

Comments
 (0)