nx/packages/angular/spec/data-persistence.spec.ts
Brandon Roberts 6516176b65 feat(nx): update to NgRx 8 and add schematics support for creators and entities
Move addUpdateTask util function to @nrwl/workspace to run updates from @nrwl/angular migrations
Run update migration to latest version for NgRx if installed
2019-07-17 14:42:54 -04:00

695 lines
18 KiB
TypeScript

import { Component, Injectable } from '@angular/core';
import { fakeAsync, TestBed, tick } from '@angular/core/testing';
import { Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { Actions, Effect, EffectsModule, ofType } from '@ngrx/effects';
import { provideMockActions } from '@ngrx/effects/testing';
import { StoreRouterConnectingModule } from '@ngrx/router-store';
import { Store, StoreModule } from '@ngrx/store';
import { Observable, of, Subject, throwError } from 'rxjs';
import { delay, withLatestFrom } from 'rxjs/operators';
import {
DataPersistence,
pessimisticUpdate,
optimisticUpdate,
fetch,
NxModule
} from '../index';
import { readAll } from '../testing';
// interfaces
type Todo = {
id: number;
user: string;
};
type Todos = {
selected: Todo;
};
type TodosState = {
todos: Todos;
user: string;
};
// actions
type TodoLoaded = {
type: 'TODO_LOADED';
payload: Todo;
};
type UpdateTodo = {
type: 'UPDATE_TODO';
payload: { newTitle: string };
};
type Action = TodoLoaded;
// reducers
function todosReducer(state: Todos, action: Action): Todos {
if (action.type === 'TODO_LOADED') {
return { selected: action.payload };
} else {
return state;
}
}
function userReducer(): string {
return 'bob';
}
@Component({
template: `
ROOT[<router-outlet></router-outlet>]
`
})
class RootCmp {}
@Component({
template: `
Todo [
<div *ngIf="(todo | async) as t">ID {{ t.id }} User {{ t.user }}</div>
]
`
})
class TodoComponent {
todo = this.store.select('todos', 'selected');
constructor(private store: Store<TodosState>) {}
}
describe('DataPersistence', () => {
describe('navigation', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [RootCmp, TodoComponent],
imports: [
StoreModule.forRoot(
{ todos: todosReducer, user: userReducer },
{
runtimeChecks: {
strictStateImmutability: false,
strictStateSerializability: false
}
}
),
StoreRouterConnectingModule.forRoot(),
RouterTestingModule.withRoutes([
{ path: 'todo/:id', component: TodoComponent }
]),
NxModule.forRoot()
]
});
});
describe('successful navigation', () => {
@Injectable()
class TodoEffects {
@Effect()
loadTodo = this.s.navigation(TodoComponent, {
run: (a, state) => {
return {
type: 'TODO_LOADED',
payload: { id: a.params['id'], user: state.user }
};
},
onError: () => null
});
constructor(private s: DataPersistence<TodosState>) {}
}
beforeEach(() => {
TestBed.configureTestingModule({
imports: [EffectsModule.forRoot([TodoEffects])]
});
});
it('should work', fakeAsync(() => {
const root = TestBed.createComponent(RootCmp);
const router: Router = TestBed.get(Router);
router.navigateByUrl('/todo/123');
tick(0);
root.detectChanges(false);
expect(root.elementRef.nativeElement.innerHTML).toContain('ID 123');
expect(root.elementRef.nativeElement.innerHTML).toContain('User bob');
}));
});
describe('`run` throwing an error', () => {
@Injectable()
class TodoEffects {
@Effect()
loadTodo = this.s.navigation(TodoComponent, {
run: (a, state) => {
if (a.params['id'] === '123') {
throw new Error('boom');
} else {
return {
type: 'TODO_LOADED',
payload: { id: a.params['id'], user: state.user }
};
}
},
onError: (a, e) => ({ type: 'ERROR', payload: { error: e } })
});
constructor(private s: DataPersistence<TodosState>) {}
}
beforeEach(() => {
TestBed.configureTestingModule({
imports: [EffectsModule.forRoot([TodoEffects])]
});
});
it('should work', fakeAsync(() => {
const root = TestBed.createComponent(RootCmp);
const router: Router = TestBed.get(Router);
let actions: any[] = [];
TestBed.get(Actions).subscribe((a: any) => actions.push(a));
router.navigateByUrl('/todo/123');
tick(0);
root.detectChanges(false);
expect(root.elementRef.nativeElement.innerHTML).not.toContain('ID 123');
expect(actions.map(a => a.type)).toContain('ERROR');
expect(
actions.find(a => a.type === 'ERROR').payload.error.message
).toEqual('boom');
// can recover after an error
router.navigateByUrl('/todo/456');
tick(0);
root.detectChanges(false);
expect(root.elementRef.nativeElement.innerHTML).toContain('ID 456');
}));
});
describe('`run` returning an error observable', () => {
@Injectable()
class TodoEffects {
@Effect()
loadTodo = this.s.navigation(TodoComponent, {
run: (a, state) => {
if (a.params['id'] === '123') {
return throwError('boom');
} else {
return {
type: 'TODO_LOADED',
payload: { id: a.params['id'], user: state.user }
};
}
},
onError: (a, e) => ({ type: 'ERROR', payload: { error: e } })
});
constructor(private s: DataPersistence<TodosState>) {}
}
beforeEach(() => {
TestBed.configureTestingModule({
imports: [EffectsModule.forRoot([TodoEffects])]
});
});
it('should work', fakeAsync(() => {
const root = TestBed.createComponent(RootCmp);
const router: Router = TestBed.get(Router);
let actions: any[] = [];
TestBed.get(Actions).subscribe((a: any) => actions.push(a));
router.navigateByUrl('/todo/123');
tick(0);
root.detectChanges(false);
expect(root.elementRef.nativeElement.innerHTML).not.toContain('ID 123');
expect(actions.map(a => a.type)).toContain('ERROR');
expect(actions.find(a => a.type === 'ERROR').payload.error).toEqual(
'boom'
);
router.navigateByUrl('/todo/456');
tick(0);
root.detectChanges(false);
expect(root.elementRef.nativeElement.innerHTML).toContain('ID 456');
}));
});
});
describe('fetch', () => {
beforeEach(() => {
TestBed.configureTestingModule({ providers: [DataPersistence] });
});
describe('no id', () => {
type GetTodos = {
type: 'GET_TODOS';
};
@Injectable()
class TodoEffects {
@Effect()
loadTodos = this.s.fetch<GetTodos>('GET_TODOS', {
run: (a, state) => {
// we need to introduce the delay to "enable" switchMap
return of({
type: 'TODOS',
payload: { user: state.user, todos: 'some todos' }
}).pipe(delay(1));
},
onError: () => null
});
@Effect()
loadTodosWithOperator = this.s.actions.pipe(
ofType<GetTodos>('GET_TODOS'),
withLatestFrom(this.s.store),
fetch({
run: (action, state) => {
return of({
type: 'TODOS',
payload: { user: state.user, todos: 'some todos' }
}).pipe(delay(1));
}
})
);
constructor(private s: DataPersistence<TodosState>) {}
}
function userReducer() {
return 'bob';
}
let actions: Observable<any>;
beforeEach(() => {
actions = new Subject<any>();
TestBed.configureTestingModule({
providers: [TodoEffects, provideMockActions(() => actions)],
imports: [
StoreModule.forRoot(
{ user: userReducer },
{
runtimeChecks: {
strictStateImmutability: false,
strictStateSerializability: false
}
}
)
]
});
});
it('should work', async done => {
actions = of(
{ type: 'GET_TODOS', payload: {} },
{ type: 'GET_TODOS', payload: {} }
);
expect(await readAll(TestBed.get(TodoEffects).loadTodos)).toEqual([
{ type: 'TODOS', payload: { user: 'bob', todos: 'some todos' } },
{ type: 'TODOS', payload: { user: 'bob', todos: 'some todos' } }
]);
done();
});
it('should work with an operator', async done => {
actions = of(
{ type: 'GET_TODOS', payload: {} },
{ type: 'GET_TODOS', payload: {} }
);
expect(
await readAll(TestBed.get(TodoEffects).loadTodosWithOperator)
).toEqual([
{ type: 'TODOS', payload: { user: 'bob', todos: 'some todos' } },
{ type: 'TODOS', payload: { user: 'bob', todos: 'some todos' } }
]);
done();
});
});
describe('id', () => {
type GetTodo = {
type: 'GET_TODO';
payload: { id: string };
};
@Injectable()
class TodoEffects {
@Effect()
loadTodo = this.s.fetch<GetTodo>('GET_TODO', {
id: a => a.payload.id,
run: a => of({ type: 'TODO', payload: a.payload }).pipe(delay(1)),
onError: () => null
});
constructor(private s: DataPersistence<TodosState>) {}
}
function userReducer() {
return 'bob';
}
let actions: Observable<any>;
beforeEach(() => {
actions = new Subject<any>();
TestBed.configureTestingModule({
providers: [TodoEffects, provideMockActions(() => actions)],
imports: [
StoreModule.forRoot(
{ user: userReducer },
{
runtimeChecks: {
strictStateImmutability: false,
strictStateSerializability: false
}
}
)
]
});
});
it('should work', async done => {
actions = of(
{ type: 'GET_TODO', payload: { id: 1, value: '1' } },
{ type: 'GET_TODO', payload: { id: 2, value: '2a' } },
{ type: 'GET_TODO', payload: { id: 2, value: '2b' } }
);
expect(await readAll(TestBed.get(TodoEffects).loadTodo)).toEqual([
{ type: 'TODO', payload: { id: 1, value: '1' } },
{ type: 'TODO', payload: { id: 2, value: '2b' } }
]);
done();
});
});
});
describe('pessimisticUpdate', () => {
beforeEach(() => {
TestBed.configureTestingModule({ providers: [DataPersistence] });
});
describe('successful', () => {
@Injectable()
class TodoEffects {
@Effect()
loadTodo = this.s.pessimisticUpdate<UpdateTodo>('UPDATE_TODO', {
run: (a, state) => ({
type: 'TODO_UPDATED',
payload: { user: state.user, newTitle: a.payload.newTitle }
}),
onError: () => null
});
@Effect()
loadTodoWithOperator = this.s.actions.pipe(
ofType<UpdateTodo>('UPDATE_TODO'),
withLatestFrom(this.s.store),
pessimisticUpdate({
run: (a, state) => ({
type: 'TODO_UPDATED',
payload: { user: state.user, newTitle: a.payload.newTitle }
}),
onError: () => null
})
);
constructor(private s: DataPersistence<TodosState>) {}
}
function userReducer() {
return 'bob';
}
let actions: Observable<any>;
beforeEach(() => {
actions = new Subject<any>();
TestBed.configureTestingModule({
providers: [TodoEffects, provideMockActions(() => actions)],
imports: [
StoreModule.forRoot(
{ user: userReducer },
{
runtimeChecks: {
strictStateImmutability: false,
strictStateSerializability: false
}
}
)
]
});
});
it('should work', async done => {
actions = of({
type: 'UPDATE_TODO',
payload: { newTitle: 'newTitle' }
});
expect(await readAll(TestBed.get(TodoEffects).loadTodo)).toEqual([
{
type: 'TODO_UPDATED',
payload: { user: 'bob', newTitle: 'newTitle' }
}
]);
done();
});
it('should work with an operator', async done => {
actions = of({
type: 'UPDATE_TODO',
payload: { newTitle: 'newTitle' }
});
expect(
await readAll(TestBed.get(TodoEffects).loadTodoWithOperator)
).toEqual([
{
type: 'TODO_UPDATED',
payload: { user: 'bob', newTitle: 'newTitle' }
}
]);
done();
});
});
describe('`run` throws an error', () => {
@Injectable()
class TodoEffects {
@Effect()
loadTodo = this.s.pessimisticUpdate<UpdateTodo>('UPDATE_TODO', {
run: () => {
throw new Error('boom');
},
onError: (a, e: any) => ({
type: 'ERROR',
payload: { error: e }
})
});
constructor(private s: DataPersistence<TodosState>) {}
}
function userReducer() {
return 'bob';
}
let actions: Observable<any>;
beforeEach(() => {
actions = new Subject<any>();
TestBed.configureTestingModule({
providers: [TodoEffects, provideMockActions(() => actions)],
imports: [
StoreModule.forRoot(
{ user: userReducer },
{
runtimeChecks: {
strictStateImmutability: false,
strictStateSerializability: false
}
}
)
]
});
});
it('should work', async done => {
actions = of({
type: 'UPDATE_TODO',
payload: { newTitle: 'newTitle' }
});
const [a]: any = await readAll(TestBed.get(TodoEffects).loadTodo);
expect(a.type).toEqual('ERROR');
expect(a.payload.error.message).toEqual('boom');
done();
});
});
describe('`run` returns an observable that errors', () => {
@Injectable()
class TodoEffects {
@Effect()
loadTodo = this.s.pessimisticUpdate<UpdateTodo>('UPDATE_TODO', {
run: () => {
return throwError('boom');
},
onError: (a, e: any) => ({
type: 'ERROR',
payload: { error: e }
})
});
constructor(private s: DataPersistence<TodosState>) {}
}
function userReducer() {
return 'bob';
}
let actions: Observable<any>;
beforeEach(() => {
actions = new Subject<any>();
TestBed.configureTestingModule({
providers: [TodoEffects, provideMockActions(() => actions)],
imports: [
StoreModule.forRoot(
{ user: userReducer },
{
runtimeChecks: {
strictStateImmutability: false,
strictStateSerializability: false
}
}
)
]
});
});
it('should work', async done => {
actions = of({
type: 'UPDATE_TODO',
payload: { newTitle: 'newTitle' }
});
const [a]: any = await readAll(TestBed.get(TodoEffects).loadTodo);
expect(a.type).toEqual('ERROR');
expect(a.payload.error).toEqual('boom');
done();
});
});
});
describe('optimisticUpdate', () => {
beforeEach(() => {
TestBed.configureTestingModule({ providers: [DataPersistence] });
});
describe('`run` throws an error', () => {
@Injectable()
class TodoEffects {
@Effect()
loadTodo = this.s.optimisticUpdate<UpdateTodo>('UPDATE_TODO', {
run: () => {
throw new Error('boom');
},
undoAction: a => ({
type: 'UNDO_UPDATE_TODO',
payload: a.payload
})
});
@Effect()
loadTodoWithOperator = this.s.actions.pipe(
ofType<UpdateTodo>('UPDATE_TODO'),
withLatestFrom(this.s.store),
optimisticUpdate({
run: () => {
throw new Error('boom');
},
undoAction: a => ({
type: 'UNDO_UPDATE_TODO',
payload: a.payload
})
})
);
constructor(private s: DataPersistence<TodosState>) {}
}
function userReducer() {
return 'bob';
}
let actions: Observable<any>;
beforeEach(() => {
actions = new Subject<any>();
TestBed.configureTestingModule({
providers: [TodoEffects, provideMockActions(() => actions)],
imports: [
StoreModule.forRoot(
{ user: userReducer },
{
runtimeChecks: {
strictStateImmutability: false,
strictStateSerializability: false
}
}
)
]
});
});
it('should work', async done => {
actions = of({
type: 'UPDATE_TODO',
payload: { newTitle: 'newTitle' }
});
const [a]: any = await readAll(TestBed.get(TodoEffects).loadTodo);
expect(a.type).toEqual('UNDO_UPDATE_TODO');
expect(a.payload.newTitle).toEqual('newTitle');
done();
});
it('should work with an operator', async done => {
actions = of({
type: 'UPDATE_TODO',
payload: { newTitle: 'newTitle' }
});
const [a]: any = await readAll(
TestBed.get(TodoEffects).loadTodoWithOperator
);
expect(a.type).toEqual('UNDO_UPDATE_TODO');
expect(a.payload.newTitle).toEqual('newTitle');
done();
});
});
});
});