Skip to content

Commit 5300e7d

Browse files
dummdidummbrandonroberts
authored andcommitted
fix(router-store): handle internal navigation error, dispatch cancel/error action with previous state (#1294)
1 parent 3886d57 commit 5300e7d

File tree

2 files changed

+209
-55
lines changed

2 files changed

+209
-55
lines changed

modules/router-store/spec/integration.spec.ts

Lines changed: 147 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1-
import { Component, Provider, Injectable } from '@angular/core';
1+
import { Component, Provider, Injectable, ErrorHandler } from '@angular/core';
22
import { TestBed } from '@angular/core/testing';
3-
import { NavigationEnd, Router, RouterStateSnapshot } from '@angular/router';
3+
import {
4+
NavigationEnd,
5+
Router,
6+
RouterStateSnapshot,
7+
NavigationCancel,
8+
NavigationError,
9+
} from '@angular/router';
410
import { RouterTestingModule } from '@angular/router/testing';
511
import { Store, StoreModule, ScannedActionsSubject } from '@ngrx/store';
612
import { filter, first, mapTo, take } from 'rxjs/operators';
@@ -19,6 +25,7 @@ import {
1925
ROUTER_REQUEST,
2026
ROUTER_NAVIGATED,
2127
NavigationActionTiming,
28+
RouterReducerState,
2229
} from '../src/router_store_module';
2330

2431
describe('integration spec', () => {
@@ -119,7 +126,7 @@ describe('integration spec', () => {
119126
});
120127
});
121128

122-
it('should support rolling back if navigation gets canceled', (done: any) => {
129+
it('should support rolling back if navigation gets canceled (navigation initialized through router)', (done: any) => {
123130
const reducer = (state: string = '', action: RouterAction<any>): any => {
124131
if (action.type === ROUTER_NAVIGATION) {
125132
return {
@@ -176,9 +183,9 @@ describe('integration spec', () => {
176183
{
177184
type: 'store',
178185
state: {
179-
url: '/next',
186+
url: '/',
180187
lastAction: ROUTER_CANCEL,
181-
storeState: { url: '/next', lastAction: ROUTER_NAVIGATION },
188+
storeState: { url: '/', lastAction: ROUTER_NAVIGATION },
182189
},
183190
},
184191
{ type: 'action', action: ROUTER_CANCEL },
@@ -189,7 +196,67 @@ describe('integration spec', () => {
189196
});
190197
});
191198

192-
it('should support rolling back if navigation errors', (done: any) => {
199+
it('should support rolling back if navigation gets canceled (navigation initialized through store)', (done: any) => {
200+
const CHANGE_ROUTE = 'CHANGE_ROUTE';
201+
const reducer = (
202+
state: RouterReducerState,
203+
action: any
204+
): RouterReducerState => {
205+
if (action.type === CHANGE_ROUTE) {
206+
return {
207+
state: { url: '/next', root: <any>{} },
208+
navigationId: 123,
209+
};
210+
} else {
211+
const nextState = routerReducer(state, action);
212+
if (nextState && nextState.state) {
213+
nextState.state.root = <any>{};
214+
}
215+
return nextState;
216+
}
217+
};
218+
219+
createTestModule({
220+
reducers: { reducer },
221+
canActivate: () => false,
222+
config: { stateKey: 'reducer' },
223+
});
224+
225+
const router: Router = TestBed.get(Router);
226+
const store: Store<any> = TestBed.get(Store);
227+
const log = logOfRouterAndActionsAndStore();
228+
229+
router
230+
.navigateByUrl('/')
231+
.then(() => {
232+
log.splice(0);
233+
store.dispatch({ type: CHANGE_ROUTE });
234+
return waitForNavigation(router, NavigationCancel);
235+
})
236+
.then(() => {
237+
expect(log).toEqual([
238+
{ type: 'router', event: 'NavigationStart', url: '/next' },
239+
{
240+
type: 'store',
241+
state: { state: { url: '/next', root: {} }, navigationId: 123 },
242+
},
243+
{ type: 'action', action: CHANGE_ROUTE },
244+
{ type: 'router', event: 'RoutesRecognized', url: '/next' },
245+
{ type: 'router', event: 'GuardsCheckStart', url: '/next' },
246+
{ type: 'router', event: 'GuardsCheckEnd', url: '/next' },
247+
{
248+
type: 'store',
249+
state: { state: { url: '/', root: {} }, navigationId: 2 },
250+
},
251+
{ type: 'action', action: ROUTER_CANCEL },
252+
{ type: 'router', event: 'NavigationCancel', url: '/next' },
253+
]);
254+
255+
done();
256+
});
257+
});
258+
259+
it('should support rolling back if navigation errors (navigation initialized through router)', (done: any) => {
193260
const reducer = (state: string = '', action: RouterAction<any>): any => {
194261
if (action.type === ROUTER_NAVIGATION) {
195262
return {
@@ -246,9 +313,9 @@ describe('integration spec', () => {
246313
{
247314
type: 'store',
248315
state: {
249-
url: '/next',
316+
url: '/',
250317
lastAction: ROUTER_ERROR,
251-
storeState: { url: '/next', lastAction: ROUTER_NAVIGATION },
318+
storeState: { url: '/', lastAction: ROUTER_NAVIGATION },
252319
},
253320
},
254321
{ type: 'action', action: ROUTER_ERROR },
@@ -259,6 +326,75 @@ describe('integration spec', () => {
259326
});
260327
});
261328

329+
it('should support rolling back if navigation errors and hand error to error handler (navigation initialized through store)', (done: any) => {
330+
const CHANGE_ROUTE = 'CHANGE_ROUTE';
331+
const reducer = (
332+
state: RouterReducerState,
333+
action: any
334+
): RouterReducerState => {
335+
if (action.type === CHANGE_ROUTE) {
336+
return {
337+
state: { url: '/next', root: <any>{} },
338+
navigationId: 123,
339+
};
340+
} else {
341+
const nextState = routerReducer(state, action);
342+
if (nextState && nextState.state) {
343+
nextState.state.root = <any>{};
344+
}
345+
return nextState;
346+
}
347+
};
348+
349+
const routerError = new Error('BOOM!');
350+
class SilentErrorHandler implements ErrorHandler {
351+
handleError(error: any) {
352+
expect(error).toBe(routerError);
353+
}
354+
}
355+
356+
createTestModule({
357+
reducers: { reducer },
358+
canActivate: () => {
359+
throw routerError;
360+
},
361+
providers: [{ provide: ErrorHandler, useClass: SilentErrorHandler }],
362+
config: { stateKey: 'reducer' },
363+
});
364+
365+
const router: Router = TestBed.get(Router);
366+
const store: Store<any> = TestBed.get(Store);
367+
const log = logOfRouterAndActionsAndStore();
368+
369+
router
370+
.navigateByUrl('/')
371+
.then(() => {
372+
log.splice(0);
373+
store.dispatch({ type: CHANGE_ROUTE });
374+
return waitForNavigation(router, NavigationError);
375+
})
376+
.then(() => {
377+
expect(log).toEqual([
378+
{ type: 'router', event: 'NavigationStart', url: '/next' },
379+
{
380+
type: 'store',
381+
state: { state: { url: '/next', root: {} }, navigationId: 123 },
382+
},
383+
{ type: 'action', action: CHANGE_ROUTE },
384+
{ type: 'router', event: 'RoutesRecognized', url: '/next' },
385+
{ type: 'router', event: 'GuardsCheckStart', url: '/next' },
386+
{
387+
type: 'store',
388+
state: { state: { url: '/', root: {} }, navigationId: 2 },
389+
},
390+
{ type: 'action', action: ROUTER_ERROR },
391+
{ type: 'router', event: 'NavigationError', url: '/next' },
392+
]);
393+
394+
done();
395+
});
396+
});
397+
262398
it('should call navigateByUrl when resetting state of the routerReducer', (done: any) => {
263399
const reducer = (state: any, action: RouterAction<any>) => {
264400
const r = routerReducer(state, action);
@@ -385,7 +521,7 @@ describe('integration spec', () => {
385521
{ type: 'store', state: null }, // ROUTER_REQEST event in the store
386522
{ type: 'action', action: ROUTER_REQUEST },
387523
{ type: 'router', event: 'NavigationStart', url: '/load' },
388-
{ type: 'store', state: null },
524+
{ type: 'store', state: { url: '', navigationId: 1 } },
389525
{ type: 'action', action: ROUTER_CANCEL },
390526
{ type: 'router', event: 'NavigationCancel', url: '/load' },
391527
]);
@@ -744,12 +880,9 @@ function createTestModule(
744880
TestBed.createComponent(AppCmp);
745881
}
746882

747-
function waitForNavigation(router: Router) {
883+
function waitForNavigation(router: Router, event: any = NavigationEnd) {
748884
return router.events
749-
.pipe(
750-
filter((e): e is NavigationEnd => e instanceof NavigationEnd),
751-
first()
752-
)
885+
.pipe(filter(e => e instanceof event), first())
753886
.toPromise();
754887
}
755888

0 commit comments

Comments
 (0)