From b0a2815171e6d072a698075b1417c7f7eb352fdb Mon Sep 17 00:00:00 2001 From: Joaquin Cid Date: Tue, 18 May 2021 05:17:30 -0300 Subject: [PATCH] test: check async observable/promise action scenarios (#1663) * test: handle async action scenarios I - Promise that returns an observable II - Observable that returns a promise * test: integrations ng9 ivy pin jest and jest-preset-angular versions --- packages/store/tests/action.spec.ts | 407 ++++++++++++++++++---------- 1 file changed, 262 insertions(+), 145 deletions(-) diff --git a/packages/store/tests/action.spec.ts b/packages/store/tests/action.spec.ts index 0a2c98b8f..022ef0d80 100644 --- a/packages/store/tests/action.spec.ts +++ b/packages/store/tests/action.spec.ts @@ -1,11 +1,11 @@ import { ErrorHandler, Injectable } from '@angular/core'; import { TestBed, fakeAsync, tick } from '@angular/core/testing'; -import { delay } from 'rxjs/operators'; -import { throwError, of } from 'rxjs'; +import { delay, mapTo } from 'rxjs/operators'; +import { throwError, of, Observable, Subscriber } from 'rxjs'; import { Action } from '../src/decorators/action'; import { State } from '../src/decorators/state'; -import { META_KEY } from '../src/symbols'; +import { META_KEY, StateContext } from '../src/symbols'; import { NgxsModule } from '../src/module'; import { Store } from '../src/store'; @@ -64,54 +64,65 @@ describe('Action', () => { } } - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [NgxsModule.forRoot([BarStore])], - providers: [{ provide: ErrorHandler, useClass: NoopErrorHandler }] + describe('', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [NgxsModule.forRoot([BarStore])], + providers: [{ provide: ErrorHandler, useClass: NoopErrorHandler }] + }); + + store = TestBed.inject(Store); + actions = TestBed.inject(Actions); }); - store = TestBed.inject(Store); - actions = TestBed.inject(Actions); - }); + it('supports multiple actions', () => { + const meta = (BarStore)[META_KEY]; - it('supports multiple actions', () => { - const meta = (BarStore)[META_KEY]; + expect(meta.actions[Action1.type]).toBeDefined(); + expect(meta.actions[Action2.type]).toBeDefined(); + }); - expect(meta.actions[Action1.type]).toBeDefined(); - expect(meta.actions[Action2.type]).toBeDefined(); - }); + it('calls actions on dispatch and on complete', fakeAsync(() => { + const callbacksCalled: string[] = []; - it('calls actions on dispatch and on complete', fakeAsync(() => { - const callbacksCalled: string[] = []; + actions.pipe(ofAction(Action1)).subscribe(() => { + callbacksCalled.push('ofAction'); + }); - actions.pipe(ofAction(Action1)).subscribe(() => { - callbacksCalled.push('ofAction'); - }); + actions.pipe(ofActionDispatched(Action1)).subscribe(() => { + callbacksCalled.push('ofActionDispatched'); + }); - actions.pipe(ofActionDispatched(Action1)).subscribe(() => { - callbacksCalled.push('ofActionDispatched'); - }); + actions.pipe(ofActionSuccessful(Action1)).subscribe(() => { + callbacksCalled.push('ofActionSuccessful'); + expect(callbacksCalled).toEqual([ + 'ofAction', + 'ofActionDispatched', + 'ofAction', + 'ofActionSuccessful' + ]); + }); - actions.pipe(ofActionSuccessful(Action1)).subscribe(() => { - callbacksCalled.push('ofActionSuccessful'); - expect(callbacksCalled).toEqual([ - 'ofAction', - 'ofActionDispatched', - 'ofAction', - 'ofActionSuccessful' - ]); - }); + actions.pipe(ofActionCompleted(Action1)).subscribe(({ result }) => { + callbacksCalled.push('ofActionCompleted'); + expect(result).toEqual({ + canceled: false, + error: undefined, + successful: true + }); + }); - actions.pipe(ofActionCompleted(Action1)).subscribe(({ result }) => { - callbacksCalled.push('ofActionCompleted'); - expect(result).toEqual({ - canceled: false, - error: undefined, - successful: true + store.dispatch(new Action1()).subscribe(() => { + expect(callbacksCalled).toEqual([ + 'ofAction', + 'ofActionDispatched', + 'ofAction', + 'ofActionSuccessful', + 'ofActionCompleted' + ]); }); - }); - store.dispatch(new Action1()).subscribe(() => { + tick(1); expect(callbacksCalled).toEqual([ 'ofAction', 'ofActionDispatched', @@ -119,93 +130,117 @@ describe('Action', () => { 'ofActionSuccessful', 'ofActionCompleted' ]); - }); + })); - tick(1); - expect(callbacksCalled).toEqual([ - 'ofAction', - 'ofActionDispatched', - 'ofAction', - 'ofActionSuccessful', - 'ofActionCompleted' - ]); - })); - - it('calls only the dispatched and error action', fakeAsync(() => { - const callbacksCalled: string[] = []; - - actions.pipe(ofAction(Action1)).subscribe(() => { - callbacksCalled.push('ofAction[Action1]'); - }); - actions.pipe(ofAction(ErrorAction)).subscribe(() => { - callbacksCalled.push('ofAction'); - }); + it('calls only the dispatched and error action', fakeAsync(() => { + const callbacksCalled: string[] = []; - actions.pipe(ofActionDispatched(ErrorAction)).subscribe(() => { - callbacksCalled.push('ofActionDispatched'); - }); + actions.pipe(ofAction(Action1)).subscribe(() => { + callbacksCalled.push('ofAction[Action1]'); + }); + actions.pipe(ofAction(ErrorAction)).subscribe(() => { + callbacksCalled.push('ofAction'); + }); - actions.pipe(ofActionSuccessful(ErrorAction)).subscribe(() => { - callbacksCalled.push('ofActionSuccessful'); - }); + actions.pipe(ofActionDispatched(ErrorAction)).subscribe(() => { + callbacksCalled.push('ofActionDispatched'); + }); + + actions.pipe(ofActionSuccessful(ErrorAction)).subscribe(() => { + callbacksCalled.push('ofActionSuccessful'); + }); + + actions.pipe(ofActionErrored(ErrorAction)).subscribe(() => { + callbacksCalled.push('ofActionErrored'); + expect(callbacksCalled).toEqual([ + 'ofAction', + 'ofActionDispatched', + 'ofAction', + 'ofActionErrored' + ]); + }); + + actions.pipe(ofActionCompleted(ErrorAction)).subscribe(({ result }) => { + callbacksCalled.push('ofActionCompleted'); + expect(result).toEqual({ + canceled: false, + error: Error('this is a test error'), + successful: false + }); + }); + + store.dispatch(new ErrorAction()).subscribe({ + error: () => + expect(callbacksCalled).toEqual([ + 'ofAction', + 'ofActionDispatched', + 'ofAction', + 'ofActionErrored', + 'ofActionCompleted' + ]) + }); - actions.pipe(ofActionErrored(ErrorAction)).subscribe(() => { - callbacksCalled.push('ofActionErrored'); + tick(1); expect(callbacksCalled).toEqual([ 'ofAction', 'ofActionDispatched', 'ofAction', - 'ofActionErrored' + 'ofActionErrored', + 'ofActionCompleted' ]); - }); + })); + + it('calls only the dispatched and canceled action', fakeAsync(() => { + const callbacksCalled: string[] = []; - actions.pipe(ofActionCompleted(ErrorAction)).subscribe(({ result }) => { - callbacksCalled.push('ofActionCompleted'); - expect(result).toEqual({ - canceled: false, - error: Error('this is a test error'), - successful: false + actions.pipe(ofAction(CancelingAction)).subscribe(() => { + callbacksCalled.push('ofAction'); }); - }); - store.dispatch(new ErrorAction()).subscribe({ - error: () => + actions.pipe(ofActionDispatched(CancelingAction)).subscribe(() => { + callbacksCalled.push('ofActionDispatched'); + }); + + actions.pipe(ofActionErrored(CancelingAction)).subscribe(() => { + callbacksCalled.push('ofActionErrored'); + }); + + actions.pipe(ofActionSuccessful(CancelingAction)).subscribe(() => { + callbacksCalled.push('ofActionSuccessful'); expect(callbacksCalled).toEqual([ 'ofAction', 'ofActionDispatched', 'ofAction', - 'ofActionErrored', - 'ofActionCompleted' - ]) - }); - - tick(1); - expect(callbacksCalled).toEqual([ - 'ofAction', - 'ofActionDispatched', - 'ofAction', - 'ofActionErrored', - 'ofActionCompleted' - ]); - })); - - it('calls only the dispatched and canceled action', fakeAsync(() => { - const callbacksCalled: string[] = []; - - actions.pipe(ofAction(CancelingAction)).subscribe(() => { - callbacksCalled.push('ofAction'); - }); + 'ofActionDispatched', + 'ofAction', + 'ofActionCanceled', + 'ofAction', + 'ofActionSuccessful' + ]); + }); - actions.pipe(ofActionDispatched(CancelingAction)).subscribe(() => { - callbacksCalled.push('ofActionDispatched'); - }); + actions.pipe(ofActionCanceled(CancelingAction)).subscribe(() => { + callbacksCalled.push('ofActionCanceled'); + expect(callbacksCalled).toEqual([ + 'ofAction', + 'ofActionDispatched', + 'ofAction', + 'ofActionDispatched', + 'ofAction', + 'ofActionCanceled' + ]); + }); - actions.pipe(ofActionErrored(CancelingAction)).subscribe(() => { - callbacksCalled.push('ofActionErrored'); - }); + store.dispatch([new CancelingAction(), new CancelingAction()]).subscribe(() => { + expect(callbacksCalled).toEqual([ + 'ofAction', + 'ofActionDispatched', + 'ofAction', + 'ofActionDispatched' + ]); + }); - actions.pipe(ofActionSuccessful(CancelingAction)).subscribe(() => { - callbacksCalled.push('ofActionSuccessful'); + tick(1); expect(callbacksCalled).toEqual([ 'ofAction', 'ofActionDispatched', @@ -216,51 +251,133 @@ describe('Action', () => { 'ofAction', 'ofActionSuccessful' ]); + })); + + it('should allow the user to dispatch an object literal', () => { + const callbacksCalled: string[] = []; + + actions.pipe(ofActionCompleted({ type: 'OBJECT_LITERAL' })).subscribe(() => { + callbacksCalled.push('onObjectLiteral'); + }); + + store.dispatch({ type: 'OBJECT_LITERAL' }); + + expect(callbacksCalled).toEqual(['onObjectLiteral']); }); + }); - actions.pipe(ofActionCanceled(CancelingAction)).subscribe(() => { - callbacksCalled.push('ofActionCanceled'); - expect(callbacksCalled).toEqual([ - 'ofAction', - 'ofActionDispatched', - 'ofAction', - 'ofActionDispatched', - 'ofAction', - 'ofActionCanceled' - ]); + describe('Async Action Scenario', () => { + let observableSubscriber: Subscriber; + const observable = new Observable(subscriber => { + observableSubscriber = subscriber; + }); + let promiseResolveFn: () => void; + const promise = new Promise(resolve => { + promiseResolveFn = resolve; }); + class PromiseThatReturnsObs { + static type = 'PromiseThatReturnsObs'; + } - store.dispatch([new CancelingAction(), new CancelingAction()]).subscribe(() => { - expect(callbacksCalled).toEqual([ - 'ofAction', - 'ofActionDispatched', - 'ofAction', - 'ofActionDispatched' - ]); + class ObservableAction { + static type = 'ObservableAction'; + } + + class ObsThatReturnsPromise { + static type = 'ObsThatReturnsPromise'; + } + + class PromiseAction { + static type = 'PromiseAction'; + } + + @State({ + name: 'async_state' + }) + @Injectable() + class AsyncState { + @Action(PromiseThatReturnsObs) + async promiseThatReturnsObs(ctx: StateContext) { + await promise; + return ctx.dispatch(ObservableAction); + } + + @Action(ObsThatReturnsPromise) + obsThatReturnsPromise() { + return observable.pipe(mapTo(promise)); + } + + @Action(ObservableAction) + observableAction() { + // return of({}).pipe(delay(0)); + return observable; + } + + @Action(PromiseAction) + promiseAction() { + // return Promise.resolve(); + return promise; + } + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [NgxsModule.forRoot([AsyncState])], + providers: [{ provide: ErrorHandler, useClass: NoopErrorHandler }] + }); + + store = TestBed.inject(Store); + actions = TestBed.inject(Actions); }); - tick(1); - expect(callbacksCalled).toEqual([ - 'ofAction', - 'ofActionDispatched', - 'ofAction', - 'ofActionDispatched', - 'ofAction', - 'ofActionCanceled', - 'ofAction', - 'ofActionSuccessful' - ]); - })); - - it('should allow the user to dispatch an object literal', () => { - const callbacksCalled: string[] = []; - - actions.pipe(ofActionCompleted({ type: 'OBJECT_LITERAL' })).subscribe(() => { - callbacksCalled.push('onObjectLiteral'); + describe('Promise that returns an observable', () => { + it('completes when promise is resolved', fakeAsync(() => { + const events: string[] = []; + + actions.pipe(ofActionCompleted(ObservableAction)).subscribe(() => { + events.push('ObservableAction - Completed'); + }); + + store + .dispatch(new PromiseThatReturnsObs()) + .subscribe(() => events.push('PromiseThatReturnsObs - Completed')); + + promiseResolveFn(); + tick(); + + expect(events).toEqual(['PromiseThatReturnsObs - Completed']); + + observableSubscriber.complete(); + tick(); + + expect(events).toEqual([ + 'PromiseThatReturnsObs - Completed', + 'ObservableAction - Completed' + ]); + })); }); - store.dispatch({ type: 'OBJECT_LITERAL' }); + describe('Observable that returns a promise', () => { + it('completes when observable is completed', fakeAsync(() => { + const events: string[] = []; + + promise.then(() => { + events.push('promise - resolved'); + }); + + store + .dispatch(new ObsThatReturnsPromise()) + .subscribe(() => events.push('ObsThatReturnsPromise - Completed')); + + observableSubscriber.complete(); - expect(callbacksCalled).toEqual(['onObjectLiteral']); + expect(events).toEqual(['ObsThatReturnsPromise - Completed']); + + promiseResolveFn(); + tick(); + + expect(events).toEqual(['ObsThatReturnsPromise - Completed', 'promise - resolved']); + })); + }); }); });