Skip to content

Commit

Permalink
feat(cart): add resolver action and effect for loading the cart (#923)
Browse files Browse the repository at this point in the history
  • Loading branch information
lderrickable committed Jun 1, 2020
1 parent 3b0d47a commit ddf84a4
Show file tree
Hide file tree
Showing 5 changed files with 279 additions and 3 deletions.
10 changes: 8 additions & 2 deletions libs/cart/src/actions/cart.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ export enum DaffCartActionTypes {
AddToCartFailureAction = '[DaffCart] Cart Add to Cart Failure Action',
CartClearAction = '[DaffCart] Cart Reset Action',
CartClearSuccessAction = '[DaffCart] Cart Reset Success Action',
CartClearFailureAction = '[DaffCart] Cart Reset Failure Action'
CartClearFailureAction = '[DaffCart] Cart Reset Failure Action',
ResolveCartAction = '[DaffCart] Resolve Cart Action'
}

export class DaffCartStorageFailure implements Action {
Expand Down Expand Up @@ -88,6 +89,10 @@ export class DaffCartClearFailure implements Action {
constructor(public payload: string) {}
}

export class DaffResolveCart implements Action {
readonly type = DaffCartActionTypes.ResolveCartAction;
}

export type DaffCartActions<T extends DaffCart = DaffCart> =
| DaffCartStorageFailure
| DaffCartLoad
Expand All @@ -101,4 +106,5 @@ export type DaffCartActions<T extends DaffCart = DaffCart> =
| DaffAddToCartFailure
| DaffCartClear
| DaffCartClearSuccess<T>
| DaffCartClearFailure;
| DaffCartClearFailure
| DaffResolveCart;
2 changes: 1 addition & 1 deletion libs/cart/src/effects/cart.effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export class DaffCartEffects<T extends DaffCart> {
})
storeId$ = this.actions$.pipe(
ofType(DaffCartActionTypes.CartCreateSuccessAction),
tap((action: DaffCartCreateSuccess<DaffCart>) => {
tap((action: DaffCartCreateSuccess<T>) => {
this.storage.setCartId(String(action.payload.id))
}),
switchMapTo(EMPTY),
Expand Down
1 change: 1 addition & 0 deletions libs/cart/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ export { DaffCartStorageService } from './storage/cart-storage.service';
export * from './errors/public_api';

export * from './drivers/public_api';
export { DaffCartResolverEffects } from './resolvers/cart-resolver.effects';
216 changes: 216 additions & 0 deletions libs/cart/src/resolvers/cart-resolver.effects.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import { TestBed } from '@angular/core/testing';
import { provideMockActions } from '@ngrx/effects/testing';
import { Observable, of } from 'rxjs';
import { hot, cold } from 'jasmine-marbles';

import {
DaffCartFactory,
} from '@daffodil/cart/testing';

import { DaffCartResolverEffects } from './cart-resolver.effects';
import { DaffCartDriver, DaffCartServiceInterface } from '../drivers/public_api';
import { DaffCart } from '../models/cart';
import { DaffCartStorageService } from '../storage/cart-storage.service';
import {
DaffResolveCart,
DaffCartLoadFailure,
DaffCartLoadSuccess,
DaffCartCreate,
DaffCartStorageFailure
} from '../actions/public_api';
import { DaffCartNotFoundError } from '../errors/not-found';
import { DaffStorageServiceError } from '@daffodil/core';

describe('DaffCartResolverEffects', () => {
let actions$: Observable<any>;
let effects: DaffCartResolverEffects;
let driver: jasmine.SpyObj<DaffCartServiceInterface>;

let cartFactory: DaffCartFactory;
let stubCart: DaffCart;

let cartStorageService: jasmine.SpyObj<DaffCartStorageService>;

beforeEach(() => {
TestBed.configureTestingModule({
providers: [
DaffCartResolverEffects,
provideMockActions(() => actions$),
{
provide: DaffCartDriver,
useValue: jasmine.createSpyObj('DaffCartDriver', ['get', 'create', 'clear', 'addToCart'])
},
{
provide: DaffCartStorageService,
useValue: jasmine.createSpyObj('DaffCartStorageService', ['setCartId', 'getCartId'])
}
],
});

effects = TestBed.get(DaffCartResolverEffects);
driver = TestBed.get(DaffCartDriver);
cartFactory = TestBed.get(DaffCartFactory);
cartStorageService = TestBed.get(DaffCartStorageService);
stubCart = cartFactory.create();
});

it('should be created', () => {
expect(effects).toBeTruthy();
});

describe('handling storage errors e.g. being in SSR', () => {
it('should dispatch a DaffCartLoadFailure action', () => {
cartStorageService.getCartId.and.throwError('Storage error');

const loadCartFailureAction = new DaffCartLoadFailure(
'Cart loading has failed',
);

actions$ = hot('--a', { a: new DaffResolveCart() });
const expected = cold('--(b|)', {
b: loadCartFailureAction
});

expect(effects.onResolveCart$).toBeObservable(expected);
});
});

describe('successfully resolving a cart when theres a cart id in storage', () => {
beforeEach(() => {
cartStorageService.getCartId.and.returnValue(
stubCart.id.toString(),
);
driver.get.and.returnValue(of(stubCart));
});

it('should not attempt to create a cart', () => {
const loadCartSuccessAction = new DaffCartLoadSuccess(stubCart);

actions$ = hot('--a', { a: new DaffResolveCart() });
const expected = cold('--(b)', {
b: loadCartSuccessAction,
});

expect(effects.onResolveCart$).toBeObservable(expected);
expect(driver.create).not.toHaveBeenCalled();
});

it('should indicate that a cart has loaded', () => {
const loadCartSuccessAction = new DaffCartLoadSuccess(stubCart);

actions$ = hot('--a', { a: new DaffResolveCart() });
const expected = cold('--(b)', {
b: loadCartSuccessAction
});

expect(effects.onResolveCart$).toBeObservable(expected);
});
});

describe('handling XHR errors when creating a cart', () => {
it('should dispatch DaffCartLoadFailure action', () => {
const response = cold('#', {});
driver.create.and.returnValue(response);

const loadCartFailureAction = new DaffCartLoadFailure(
'Cart loading has failed',
);

actions$ = hot('--a', { a: new DaffResolveCart() });
const expected = cold('--(b|)', {
b: loadCartFailureAction
});

expect(effects.onResolveCart$).toBeObservable(expected);
});
});

describe('handling XHR errors when retrieving a cart', () => {
it('should dispatch a DaffCartLoadFailure action', () => {
const response = cold('#', {});
driver.get.and.returnValue(response);
driver.create.and.returnValue(response);

const loadCartFailureAction = new DaffCartLoadFailure(
'Cart loading has failed',
);

actions$ = hot('--a', { a: new DaffResolveCart() });
const expected = cold('--(b|)', {
b: loadCartFailureAction
});

expect(effects.onResolveCart$).toBeObservable(expected);
});
});

describe('creating a cart when there is no cart id in storage', () => {
it('should create a cart', () => {
cartStorageService.getCartId.and.returnValue(undefined);
driver.create.and.returnValue(of({ id: stubCart.id }));
driver.get.and.returnValue(of(stubCart));

const loadCartSuccessAction = new DaffCartLoadSuccess(stubCart);

actions$ = hot('--a', { a: new DaffResolveCart() });
const expected = cold('--(b)', {
b: loadCartSuccessAction
});

expect(effects.onResolveCart$).toBeObservable(expected);
expect(driver.create).toHaveBeenCalled();
});

it('should set the cart id in local storage', () => {
cartStorageService.getCartId.and.returnValue(undefined);
driver.create.and.returnValue(of({ id: stubCart.id }));
driver.get.and.returnValue(of(stubCart));

const loadCartSuccessAction = new DaffCartLoadSuccess(stubCart);

actions$ = hot('--a', { a: new DaffResolveCart() });
const expected = cold('--(b)', {
b: loadCartSuccessAction
});

expect(effects.onResolveCart$).toBeObservable(expected);
expect(cartStorageService.setCartId).toHaveBeenCalledWith(String(stubCart.id));
});
});

describe('when the error thrown is a DaffCartNotFoundError', () => {

it('should create a new cart', () => {
cartStorageService.getCartId.and.returnValue('id');
const response = cold('#', {}, new DaffCartNotFoundError('error'));
driver.get.and.returnValue(response);

const cartCreateAction = new DaffCartCreate();

actions$ = hot('--a', { a: new DaffResolveCart() });
const expected = cold('--(b|)', {
b: cartCreateAction,
});

expect(effects.onResolveCart$).toBeObservable(expected);
});
});

describe('when the error thrown is a DaffStorageServiceError', () => {

it('should indicate that the storage service has failed', () => {
const response = cold('#', {}, new DaffStorageServiceError());
driver.get.and.returnValue(response);
driver.create.and.returnValue(response);

const cartStorageFailureAction = new DaffCartStorageFailure();

actions$ = hot('--a', { a: new DaffResolveCart() });
const expected = cold('--(b|)', {
b: cartStorageFailureAction
});

expect(effects.onResolveCart$).toBeObservable(expected);
});
});
});
53 changes: 53 additions & 0 deletions libs/cart/src/resolvers/cart-resolver.effects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Injectable, Inject } from '@angular/core';
import { Actions, Effect, ofType } from '@ngrx/effects';
import { Action } from '@ngrx/store';
import { Observable, of } from 'rxjs';
import { switchMap, catchError, map } from 'rxjs/operators';

import {
DaffCartActionTypes,
DaffCartLoadSuccess,
DaffCartCreate,
DaffCartLoadFailure,
DaffCartStorageFailure
} from '../actions/public_api';
import { DaffCartStorageService } from '../storage/cart-storage.service';
import { DaffCartDriver, DaffCartServiceInterface } from '../drivers/public_api';
import { DaffCart } from '../models/cart';
import { DaffCartNotFoundError } from '../errors/not-found';
import { DaffStorageServiceError } from '@daffodil/core';

/**
* An effect for resolving the Cart. It will check local state for a cart id, and retrieve the cart if it exists. If a cart
* of that id does not exist, it will create a new cart.
*/
@Injectable()
export class DaffCartResolverEffects<T extends DaffCart = DaffCart> {
constructor(
private actions$: Actions,
private cartStorage: DaffCartStorageService,
@Inject(DaffCartDriver) private driver: DaffCartServiceInterface<T>,
) {}

@Effect()
onResolveCart$: Observable<Action> = this.actions$.pipe(
ofType(DaffCartActionTypes.ResolveCartAction),
map(() => this.cartStorage.getCartId()),
switchMap(id => id ? of({ id }) : this.driver.create()),
switchMap(({ id }) => this.driver.get(id)),
switchMap(resp => {
this.cartStorage.setCartId(String(resp.id))
return [new DaffCartLoadSuccess(resp)]
}),
catchError(error => {
switch(error.constructor) {
case DaffStorageServiceError:
return of(new DaffCartStorageFailure())
case DaffCartNotFoundError:
return of(new DaffCartCreate());
default:
return of(new DaffCartLoadFailure('Cart loading has failed'));
}
}),
);
}

0 comments on commit ddf84a4

Please sign in to comment.