angular/packages/router/test/operators/resolve_data.spec.ts
Andrew Scott 0abb67af59 feat(router): allow guards and resolvers to be plain functions (#46684)
The current Router APIs require guards/resolvers to be present in the DI tree. This is because we want to treat all guards/resolvers equally and some may require dependencies. This requirement results in quite a lot of boilerplate for guards. Here are two examples:

```
const MY_GUARD = new InjectionToken<any>('my_guard');
…
providers: {provide: MY_GUARD, useValue: () => window.someGlobalState}
…
const route = {path: 'somePath', canActivate: [MY_GUARD]}
```

```
@Injectable({providedIn: 'root'})
export class MyGuardWithDependency {
  constructor(private myDep: MyDependency) {}

  canActivate() {
    return myDep.canActivate();
  }
}
…
const route = {path: 'somePath', canActivate: [MyGuardWithDependency]}
```

Notice that even when we want to write a simple guard that has no dependencies as in the first example, we still have to write either an InjectionToken or an Injectable class.

With this commit router guards and resolvers can be plain old functions.
 For example:

```
const route = {path: 'somePath', component: EditCmp, canDeactivate: [(component: EditCmp) => !component.hasUnsavedChanges]}
```

Additionally, these functions can still use Angular DI with `inject` from `@angular/core`.

```
const route = {path: 'somePath', canActivate: [() => inject(MyDependency).canActivate()]}
```

PR Close #46684
2022-08-05 10:36:46 -07:00

127 lines
4.7 KiB
TypeScript

/**
* @license
* Copyright Google LLC 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 {EnvironmentInjector, Injector} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {EMPTY, interval, NEVER, of} from 'rxjs';
import {TestScheduler} from 'rxjs/testing';
import {resolveData} from '../../src/operators/resolve_data';
describe('resolveData operator', () => {
let testScheduler: TestScheduler;
let injector: EnvironmentInjector;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
{provide: 'resolveTwo', useValue: (a: any, b: any) => of(2)},
{provide: 'resolveFour', useValue: (a: any, b: any) => 4},
{provide: 'resolveEmpty', useValue: (a: any, b: any) => EMPTY},
{provide: 'resolveInterval', useValue: (a: any, b: any) => interval()},
{provide: 'resolveNever', useValue: (a: any, b: any) => NEVER},
]
});
});
beforeEach(() => {
testScheduler = new TestScheduler(assertDeepEquals);
});
beforeEach(() => {
injector = TestBed.inject(EnvironmentInjector);
});
it('should re-emit updated value from source after all resolvers emit and complete', () => {
testScheduler.run(({hot, cold, expectObservable}) => {
const transition: any = createTransition({e1: 'resolveTwo'}, {e2: 'resolveFour'});
const source = cold('-(t|)', {t: deepClone(transition)});
const expected = '-(t|)';
const outputTransition = deepClone(transition);
outputTransition.guards.canActivateChecks[0].route._resolvedData = {e1: 2};
outputTransition.guards.canActivateChecks[1].route._resolvedData = {e2: 4};
expectObservable(source.pipe(resolveData('emptyOnly', injector))).toBe(expected, {
t: outputTransition
});
});
});
it('should take only the first emitted value of every resolver', () => {
testScheduler.run(({cold, expectObservable}) => {
const transition: any = createTransition({e1: 'resolveInterval'});
const source = cold('-(t|)', {t: deepClone(transition)});
const expected = '-(t|)';
const outputTransition = deepClone(transition);
outputTransition.guards.canActivateChecks[0].route._resolvedData = {e1: 0};
expectObservable(source.pipe(resolveData('emptyOnly', injector))).toBe(expected, {
t: outputTransition
});
});
});
it('should re-emit value from source when there are no resolvers', () => {
testScheduler.run(({hot, cold, expectObservable}) => {
const transition: any = createTransition({});
const source = cold('-(t|)', {t: deepClone(transition)});
const expected = '-(t|)';
const outputTransition = deepClone(transition);
outputTransition.guards.canActivateChecks[0].route._resolvedData = {};
expectObservable(source.pipe(resolveData('emptyOnly', injector))).toBe(expected, {
t: outputTransition
});
});
});
it('should not emit when there\'s one resolver that doesn\'t emit', () => {
testScheduler.run(({hot, cold, expectObservable}) => {
const transition: any = createTransition({e2: 'resolveEmpty'});
const source = cold('-(t|)', {t: deepClone(transition)});
const expected = '-|';
expectObservable(source.pipe(resolveData('emptyOnly', injector))).toBe(expected);
});
});
it('should not emit if at least one resolver doesn\'t emit', () => {
testScheduler.run(({hot, cold, expectObservable}) => {
const transition: any = createTransition({e1: 'resolveTwo'}, {e2: 'resolveEmpty'});
const source = cold('-(t|)', {t: deepClone(transition)});
const expected = '-|';
expectObservable(source.pipe(resolveData('emptyOnly', injector))).toBe(expected);
});
});
it('should complete instantly if at least one resolver doesn\'t emit', () => {
testScheduler.run(({cold, expectObservable}) => {
const transition: any = createTransition({e1: 'resolveEmpty', e2: 'resolveNever'});
const source = cold('-(t|)', {t: deepClone(transition)});
const expected = '-|';
expectObservable(source.pipe(resolveData('emptyOnly', injector))).toBe(expected);
});
});
});
function assertDeepEquals(a: any, b: any) {
return expect(a).toEqual(b);
}
function createTransition(...resolvers: {[key: string]: string}[]) {
return {
targetSnapshot: {},
guards: {
canActivateChecks:
resolvers.map(resolver => ({
route: {_resolve: resolver, pathFromRoot: [{url: '/'}], data: {}},
})),
},
};
}
function deepClone<T>(obj: T): T {
return JSON.parse(JSON.stringify(obj)) as T;
}