diff --git a/packages/router/src/operators/prioritized_guard_value.ts b/packages/router/src/operators/prioritized_guard_value.ts new file mode 100644 index 0000000000..c44e28acce --- /dev/null +++ b/packages/router/src/operators/prioritized_guard_value.ts @@ -0,0 +1,52 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Observable, OperatorFunction, combineLatest} from 'rxjs'; +import {filter, scan, startWith, switchMap, take} from 'rxjs/operators'; + +import {UrlTree} from '../url_tree'; + +const INITIAL_VALUE = Symbol('INITIAL_VALUE'); +declare type INTERIM_VALUES = typeof INITIAL_VALUE | boolean | UrlTree; + +export function prioritizedGuardValue(): + OperatorFunction[], boolean|UrlTree> { + return switchMap(obs => { + return combineLatest( + ...obs.map(o => o.pipe(take(1), startWith(INITIAL_VALUE as INTERIM_VALUES)))) + .pipe( + scan( + (acc: INTERIM_VALUES, list: INTERIM_VALUES[]) => { + let isPending = false; + return list.reduce((innerAcc, val, i: number) => { + if (innerAcc !== INITIAL_VALUE) return innerAcc; + + // Toggle pending flag if any values haven't been set yet + if (val === INITIAL_VALUE) isPending = true; + + // Any other return values are only valid if we haven't yet hit a pending call. + // This guarantees that in the case of a guard at the bottom of the tree that + // returns a redirect, we will wait for the higher priority guard at the top to + // finish before performing the redirect. + if (!isPending) { + // Early return when we hit a `false` value as that should always cancel + // navigation + if (val === false) return val; + + if (i === list.length - 1 || val instanceof UrlTree) { + return val; + } + } + + return innerAcc; + }, acc); + }, + INITIAL_VALUE), + filter(item => item !== INITIAL_VALUE), take(1)) as Observable; + }); +} diff --git a/packages/router/test/BUILD.bazel b/packages/router/test/BUILD.bazel index 0311351743..aca1627265 100644 --- a/packages/router/test/BUILD.bazel +++ b/packages/router/test/BUILD.bazel @@ -16,6 +16,7 @@ ts_library( "//packages/router/testing", "@rxjs", "@rxjs//operators", + "@rxjs//testing", ], ) diff --git a/packages/router/test/operators/prioritized_guard_value.spec.ts b/packages/router/test/operators/prioritized_guard_value.spec.ts new file mode 100644 index 0000000000..54de147987 --- /dev/null +++ b/packages/router/test/operators/prioritized_guard_value.spec.ts @@ -0,0 +1,182 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + + +import {TestBed} from '@angular/core/testing'; +import {Observable, Observer, of } from 'rxjs'; +import {every, mergeMap} from 'rxjs/operators'; +import {TestScheduler} from 'rxjs/testing'; + +import {prioritizedGuardValue} from '../../src/operators/prioritized_guard_value'; +import {Router} from '../../src/router'; +import {UrlTree} from '../../src/url_tree'; +import {RouterTestingModule} from '../../testing/src/router_testing_module'; + + +describe('prioritizedGuardValue operator', () => { + let testScheduler: TestScheduler; + let router: Router; + const TF = {T: true, F: false}; + + beforeEach(() => { TestBed.configureTestingModule({imports: [RouterTestingModule]}); }); + beforeEach(() => { testScheduler = new TestScheduler(assertDeepEquals); }); + beforeEach(() => { router = TestBed.get(Router); }); + + it('should return true if all values are true', () => { + testScheduler.run(({hot, cold, expectObservable}) => { + + const a = cold(' --(T|)', TF); + const b = cold(' ----------(T|)', TF); + const c = cold(' ------(T|)', TF); + const source = hot('---o--', {o: [a, b, c]}); + + const expected = ' -------------T--'; + + expectObservable(source.pipe(prioritizedGuardValue())) + .toBe(expected, TF, /* an error here maybe */); + }); + }); + + it('should return false if observables to the left of false have produced a value', () => { + testScheduler.run(({hot, cold, expectObservable}) => { + + const a = cold(' --(T|)', TF); + const b = cold(' ----------(T|)', TF); + const c = cold(' ------(F|)', TF); + const source = hot('---o--', {o: [a, b, c]}); + + const expected = ' -------------F--'; + + expectObservable(source.pipe(prioritizedGuardValue())) + .toBe(expected, TF, /* an error here maybe */); + }); + }); + + it('should ignore results for unresolved sets of Observables', () => { + testScheduler.run(({hot, cold, expectObservable}) => { + + const a = cold(' --(T|)', TF); + const b = cold(' -------------(T|)', TF); + const c = cold(' ------(F|)', TF); + + const z = cold(' ----(T|)', TF); + + const source = hot('---o----p----', {o: [a, b, c], p: [z]}); + + const expected = ' ------------T---'; + + expectObservable(source.pipe(prioritizedGuardValue())) + .toBe(expected, TF, /* an error here maybe */); + }); + }); + + it('should return UrlTree if higher priority guards have resolved', () => { + testScheduler.run(({hot, cold, expectObservable}) => { + + const urlTree = router.parseUrl('/'); + + const urlLookup = {U: urlTree}; + + const a = cold(' --(T|)', TF); + const b = cold(' ----------(U|)', urlLookup); + const c = cold(' ------(T|)', TF); + + const source = hot('---o---', {o: [a, b, c]}); + + const expected = ' -------------U---'; + + expectObservable(source.pipe(prioritizedGuardValue())) + .toBe(expected, urlLookup, /* an error here maybe */); + }); + }); + + it('should return false even with UrlTree if UrlTree is lower priority', () => { + testScheduler.run(({hot, cold, expectObservable}) => { + + const urlTree = router.parseUrl('/'); + + const urlLookup = {U: urlTree}; + + const a = cold(' --(T|)', TF); + const b = cold(' ----------(F|)', TF); + const c = cold(' ------(U|)', urlLookup); + + const source = hot('---o---', {o: [a, b, c]}); + + const expected = ' -------------F---'; + + expectObservable(source.pipe(prioritizedGuardValue())) + .toBe(expected, TF, /* an error here maybe */); + }); + }); + + it('should return UrlTree even after a false if the false is lower priority', () => { + testScheduler.run(({hot, cold, expectObservable}) => { + + const urlTree = router.parseUrl('/'); + + const urlLookup = {U: urlTree}; + + const a = cold(' --(T|)', TF); + const b = cold(' ----------(U|)', urlLookup); + const c = cold(' ------(F|)', TF); + + const source = hot('---o---', {o: [a, b, c]}); + + const expected = ' -------------U----'; + + expectObservable(source.pipe(prioritizedGuardValue())) + .toBe(expected, urlLookup, /* an error here maybe */); + }); + }); + + it('should return the highest priority UrlTree', () => { + testScheduler.run(({hot, cold, expectObservable}) => { + + const urlTreeU = router.parseUrl('/u'); + const urlTreeR = router.parseUrl('/r'); + const urlTreeL = router.parseUrl('/l'); + + const urlLookup = {U: urlTreeU, R: urlTreeR, L: urlTreeL}; + + const a = cold(' ----------(U|)', urlLookup); + const b = cold(' -----(R|)', urlLookup); + const c = cold(' --(L|)', urlLookup); + + const source = hot('---o---', {o: [a, b, c]}); + + const expected = ' -------------U---'; + + expectObservable(source.pipe(prioritizedGuardValue())) + .toBe(expected, urlLookup, /* an error here maybe */); + }); + }); + + it('should propagate errors', () => { + testScheduler.run(({hot, cold, expectObservable}) => { + + const a = cold(' --(T|)', TF); + const b = cold(' ------#', TF); + const c = cold(' ----------(F|)', TF); + const source = hot('---o------', {o: [a, b, c]}); + + const expected = ' ---------#'; + + expectObservable(source.pipe(prioritizedGuardValue())) + .toBe(expected, TF, /* an error here maybe */); + }); + }); + + +}); + + + +function assertDeepEquals(a: any, b: any) { + return expect(a).toEqual(b); +}