Commit da8a92e0 authored by Ryan Diehl's avatar Ryan Diehl

feat(progress): progress actions and interceptor

ported from @psu/progress
parent b56440fe
Pipeline #99716 passed with stages
in 4 minutes and 6 seconds
# utils-ngrx # @psu/utils/ngrx
This library was generated with [Nx](https://nx.dev). NgRx extensions for common utilities
## Running unit tests ## Window
Run `nx test utils-ngrx` to execute the unit tests. Actions and effects for opening links (usually in a new window/tab)
## Progress
Actions and an HTTP Interceptor for handling progress.
export * from './lib/ngrx.module'; export * from './lib/ngrx.module';
export * from './lib/progress';
export * from './lib/window'; export * from './lib/window';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { EffectsModule } from '@ngrx/effects'; import { EffectsModule } from '@ngrx/effects';
import { StoreModule } from '@ngrx/store';
import { progressReducer, progressSliceName } from './progress/progress.reducer';
import { WindowEffects } from './window/window.effects'; import { WindowEffects } from './window/window.effects';
@NgModule({ @NgModule({
imports: [CommonModule, EffectsModule.forFeature([WindowEffects])] imports: [
CommonModule,
StoreModule.forFeature(progressSliceName, progressReducer),
EffectsModule.forFeature([WindowEffects])
]
}) })
export class NgrxModule {} export class NgrxModule {}
export * from './progress.actions';
export * from './progress.interceptor';
export {
ProgressPartialState,
progressSliceName,
ProgressState,
selectProgressPending,
selectProgressState
} from './progress.reducer';
import { createAction } from '@ngrx/store';
const PREFIX = '[@psu/utils Progress]';
export const progressStart = createAction(`${PREFIX} Start`);
export const progressEnd = createAction(`${PREFIX} End`);
import { HttpClient, HTTP_INTERCEPTORS } from '@angular/common/http';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { inject, TestBed } from '@angular/core/testing';
import { Store, StoreModule } from '@ngrx/store';
import { of } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { progressEnd, progressStart } from './progress.actions';
import { ProgressInterceptor } from './progress.interceptor';
import { progressReducer, progressSliceName } from './progress.reducer';
describe('ProgressInterceptor', () => {
let store: Store<any>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
StoreModule.forFeature(progressSliceName, progressReducer),
StoreModule.forRoot({}),
HttpClientTestingModule
],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: ProgressInterceptor,
multi: true
}
]
});
store = TestBed.get(Store);
spyOn(store, 'dispatch').and.callThrough();
});
it('should create', () => {
expect(TestBed.get(HTTP_INTERCEPTORS)[0]).toBeDefined();
});
describe('intercepting http calls', () => {
beforeEach(inject([HttpClient, HttpTestingController], (http: HttpClient, httpMock: HttpTestingController) => {
http
.get('/data')
.pipe(catchError(err => of(null)))
.subscribe();
}));
it('should dispatch progress start action', () => {
expect(store.dispatch).toHaveBeenCalledWith(progressStart());
});
describe('when http response is successful', () => {
beforeEach(inject([HttpClient, HttpTestingController], (http: HttpClient, httpMock: HttpTestingController) => {
const req = httpMock.expectOne('/data');
req.flush({});
httpMock.verify();
}));
it('should dispatch progress end action', () => {
expect(store.dispatch).toHaveBeenCalledWith(progressEnd());
expect(store.dispatch).toHaveBeenCalledTimes(2);
});
});
describe('when http response is error', () => {
beforeEach(inject([HttpClient, HttpTestingController], (http: HttpClient, httpMock: HttpTestingController) => {
const req = httpMock.expectOne('/data');
req.flush({ errorMessage: 'Uh oh!' }, { status: 500, statusText: 'Server Error' });
httpMock.verify();
}));
it('should dispatch progress end action', () => {
expect(store.dispatch).toHaveBeenCalledWith(progressEnd());
expect(store.dispatch).toHaveBeenCalledTimes(2);
});
});
describe('when http event errors or is cancelled', () => {
beforeEach(inject([HttpClient, HttpTestingController], (http: HttpClient, httpMock: HttpTestingController) => {
const req = httpMock.expectOne('/data');
req.error({} as any);
httpMock.verify();
}));
it('should dispatch progress end action', () => {
expect(store.dispatch).toHaveBeenCalledWith(progressEnd());
expect(store.dispatch).toHaveBeenCalledTimes(2);
});
});
});
});
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { progressEnd, progressStart } from './progress.actions';
@Injectable({ providedIn: 'root' })
export class ProgressInterceptor implements HttpInterceptor {
constructor(private store: Store<any>) {}
public intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
this.store.dispatch(progressStart());
return next.handle(req).pipe(
finalize(() => {
this.store.dispatch(progressEnd());
})
);
}
}
import { progressEnd, progressStart } from './progress.actions';
import { progressReducer, ProgressState } from './progress.reducer';
describe('ProgressReducer', () => {
let state: ProgressState;
let initialState: ProgressState;
beforeEach(() => {
initialState = {
pending: false,
counter: 0
};
});
describe('ProgressStart action', () => {
beforeEach(() => {
state = progressReducer(undefined, progressStart());
});
it('should set pending to true', () => {
expect(state.pending).toBe(true);
});
it('should increment counter', () => {
expect(state.counter).toBe(1);
});
describe('after another start action', () => {
beforeEach(() => {
state = progressReducer(state, progressStart());
});
it('should set pending to true', () => {
expect(state.pending).toBe(true);
});
it('should increment counter', () => {
expect(state.counter).toBe(2);
});
});
});
describe('ProgressEnd action', () => {
describe('when counter is 1', () => {
beforeEach(() => {
state = progressReducer(
{
pending: true,
counter: 1
},
progressEnd()
);
});
it('should set pending to false', () => {
expect(state.pending).toBe(false);
});
it('should set counter to 0', () => {
expect(state.counter).toBe(0);
});
});
describe('when counter is 2', () => {
beforeEach(() => {
state = progressReducer(
{
pending: true,
counter: 2
},
progressEnd()
);
});
it('should set pending to true', () => {
expect(state.pending).toBe(true);
});
it('should set counter to 1', () => {
expect(state.counter).toBe(1);
});
});
describe('when counter is 0', () => {
beforeEach(() => {
state = progressReducer(
{
pending: false,
counter: 0
},
progressEnd()
);
});
it('should set pending to false', () => {
expect(state.pending).toBe(false);
});
it('should set counter to 0', () => {
expect(state.counter).toBe(0);
});
});
});
describe('unknown action', () => {
beforeEach(() => {
state = progressReducer(undefined, { type: 'GARBAGE' });
});
it('should not modify state', () => {
expect(state).toEqual(initialState);
});
});
});
import { Action, createFeatureSelector, createReducer, createSelector, on } from '@ngrx/store';
import { progressEnd, progressStart } from './progress.actions';
export const progressSliceName = 'utils-progress';
export interface ProgressState {
counter: number;
pending: boolean;
}
export const initialProgressState: ProgressState = {
pending: false,
counter: 0
};
export interface ProgressPartialState {
[progressSliceName]: ProgressState;
}
export const selectProgressState = createFeatureSelector<ProgressState>(progressSliceName);
export const selectProgressPending = createSelector(
selectProgressState,
s => s.pending
);
const reducer = createReducer(
initialProgressState,
on(progressStart, state => ({ ...state, pending: true, counter: state.counter + 1 })),
on(progressEnd, state => {
const counter = Math.max(0, state.counter - 1);
return {
...state,
counter,
pending: counter > 0
};
})
);
export function progressReducer(state: ProgressState | undefined, action: Action) {
return reducer(state, action);
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment