From 6159102533dc3f61ba39f0dc5a6d735ccedc2831 Mon Sep 17 00:00:00 2001 From: Andrew Kushnir Date: Fri, 23 Sep 2022 15:17:35 -0700 Subject: [PATCH] fixup! feat(core): support object-based DI flags in TestBed.inject() --- goldens/public-api/core/index.md | 8 +++++ goldens/public-api/core/testing/index.md | 8 ++--- packages/core/src/di/injector.ts | 16 +++++++++ packages/core/src/di/r3_injector.ts | 15 ++++++++ packages/core/test/acceptance/di_spec.ts | 35 ++++++++++++++++++- packages/core/test/test_bed_spec.ts | 15 +++++++- packages/core/testing/src/test_bed.ts | 13 +++---- packages/examples/core/di/ts/injector_spec.ts | 6 ++-- 8 files changed, 102 insertions(+), 14 deletions(-) diff --git a/goldens/public-api/core/index.md b/goldens/public-api/core/index.md index a1abe016bddcb..452b862a8033c 100644 --- a/goldens/public-api/core/index.md +++ b/goldens/public-api/core/index.md @@ -492,6 +492,10 @@ export const ENVIRONMENT_INITIALIZER: InjectionToken<() => void>; export abstract class EnvironmentInjector implements Injector { // (undocumented) abstract destroy(): void; + abstract get(token: ProviderToken, notFoundValue: undefined, options: InjectOptions & { + optional?: false; + }): T; + abstract get(token: ProviderToken, notFoundValue: null | undefined, options: InjectOptions): T | null; abstract get(token: ProviderToken, notFoundValue?: T, options?: InjectOptions): T; // @deprecated abstract get(token: ProviderToken, notFoundValue?: T, flags?: InjectFlags): T; @@ -733,6 +737,10 @@ export abstract class Injector { parent?: Injector; name?: string; }): Injector; + abstract get(token: ProviderToken, notFoundValue: undefined, options: InjectOptions & { + optional?: false; + }): T; + abstract get(token: ProviderToken, notFoundValue: null | undefined, options: InjectOptions): T | null; abstract get(token: ProviderToken, notFoundValue?: T, options?: InjectOptions | InjectFlags): T; // @deprecated abstract get(token: ProviderToken, notFoundValue?: T, flags?: InjectFlags): T; diff --git a/goldens/public-api/core/testing/index.md b/goldens/public-api/core/testing/index.md index 813e54b80bd14..65372303c8a47 100644 --- a/goldens/public-api/core/testing/index.md +++ b/goldens/public-api/core/testing/index.md @@ -117,12 +117,12 @@ export interface TestBed { initTestEnvironment(ngModule: Type | Type[], platform: PlatformRef, options?: TestEnvironmentOptions): void; // (undocumented) inject(token: ProviderToken, notFoundValue: undefined, options: InjectOptions & { - optional: true; - }): T | null; + optional?: false; + }): T; // (undocumented) - inject(token: ProviderToken, notFoundValue?: T, options?: InjectOptions): T; + inject(token: ProviderToken, notFoundValue: null | undefined, options: InjectOptions): T | null; // (undocumented) - inject(token: ProviderToken, notFoundValue: null, options?: InjectOptions): T | null; + inject(token: ProviderToken, notFoundValue?: T, options?: InjectOptions): T; // @deprecated (undocumented) inject(token: ProviderToken, notFoundValue?: T, flags?: InjectFlags): T; // @deprecated (undocumented) diff --git a/packages/core/src/di/injector.ts b/packages/core/src/di/injector.ts index 85de1e1a244fe..685fcbb22cb68 100644 --- a/packages/core/src/di/injector.ts +++ b/packages/core/src/di/injector.ts @@ -51,6 +51,22 @@ export abstract class Injector { * It can **not** be done in minor/patch, since it's breaking for custom injectors * that only implement the old `InjectorFlags` interface. */ + + /** + * Retrieves an instance from the injector based on the provided token. + * @returns The instance from the injector if defined, otherwise the `notFoundValue`. + * @throws When the `notFoundValue` is `undefined` or `Injector.THROW_IF_NOT_FOUND`. + */ + abstract get(token: ProviderToken, notFoundValue: undefined, options: InjectOptions&{ + optional?: false; + }): T; + /** + * Retrieves an instance from the injector based on the provided token. + * @returns The instance from the injector if defined, otherwise the `notFoundValue`. + * @throws When the `notFoundValue` is `undefined` or `Injector.THROW_IF_NOT_FOUND`. + */ + abstract get(token: ProviderToken, notFoundValue: null|undefined, options: InjectOptions): T + |null; /** * Retrieves an instance from the injector based on the provided token. * @returns The instance from the injector if defined, otherwise the `notFoundValue`. diff --git a/packages/core/src/di/r3_injector.ts b/packages/core/src/di/r3_injector.ts index 9cb2c5469de74..b175be228160b 100644 --- a/packages/core/src/di/r3_injector.ts +++ b/packages/core/src/di/r3_injector.ts @@ -77,6 +77,21 @@ interface Record { * @developerPreview */ export abstract class EnvironmentInjector implements Injector { + /** + * Retrieves an instance from the injector based on the provided token. + * @returns The instance from the injector if defined, otherwise the `notFoundValue`. + * @throws When the `notFoundValue` is `undefined` or `Injector.THROW_IF_NOT_FOUND`. + */ + abstract get(token: ProviderToken, notFoundValue: undefined, options: InjectOptions&{ + optional?: false; + }): T; + /** + * Retrieves an instance from the injector based on the provided token. + * @returns The instance from the injector if defined, otherwise the `notFoundValue`. + * @throws When the `notFoundValue` is `undefined` or `Injector.THROW_IF_NOT_FOUND`. + */ + abstract get(token: ProviderToken, notFoundValue: null|undefined, options: InjectOptions): T + |null; /** * Retrieves an instance from the injector based on the provided token. * @returns The instance from the injector if defined, otherwise the `notFoundValue`. diff --git a/packages/core/test/acceptance/di_spec.ts b/packages/core/test/acceptance/di_spec.ts index dc33e72548422..72f516bc1e634 100644 --- a/packages/core/test/acceptance/di_spec.ts +++ b/packages/core/test/acceptance/di_spec.ts @@ -7,7 +7,7 @@ */ import {CommonModule} from '@angular/common'; -import {Attribute, ChangeDetectorRef, Component, ComponentFactoryResolver, ComponentRef, createEnvironmentInjector, Directive, ElementRef, ENVIRONMENT_INITIALIZER, EnvironmentInjector, EventEmitter, forwardRef, Host, HostBinding, ImportedNgModuleProviders, importProvidersFrom, ImportProvidersSource, inject, Inject, Injectable, InjectFlags, InjectionToken, INJECTOR, Injector, Input, LOCALE_ID, ModuleWithProviders, NgModule, NgZone, Optional, Output, Pipe, PipeTransform, Provider, Self, SkipSelf, TemplateRef, Type, ViewChild, ViewContainerRef, ViewEncapsulation, ViewRef, ɵcreateInjector as createInjector, ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID, ɵINJECTOR_SCOPE} from '@angular/core'; +import {Attribute, ChangeDetectorRef, Component, ComponentFactoryResolver, ComponentRef, createEnvironmentInjector, Directive, ElementRef, ENVIRONMENT_INITIALIZER, EnvironmentInjector, EventEmitter, forwardRef, Host, HostBinding, ImportedNgModuleProviders, importProvidersFrom, ImportProvidersSource, inject, Inject, Injectable, InjectFlags, InjectionToken, InjectOptions, INJECTOR, Injector, Input, LOCALE_ID, ModuleWithProviders, NgModule, NgZone, Optional, Output, Pipe, PipeTransform, Provider, Self, SkipSelf, TemplateRef, Type, ViewChild, ViewContainerRef, ViewEncapsulation, ViewRef, ɵcreateInjector as createInjector, ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID, ɵINJECTOR_SCOPE} from '@angular/core'; import {ViewRef as ViewRefInternal} from '@angular/core/src/render3/view_ref'; import {TestBed} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; @@ -3467,6 +3467,39 @@ describe('di', () => { expect(envInjector.get(TOKEN, undefined, InjectFlags.Optional)).toBeNull(); }); + it('should include `null` into the result type when the optional flag is used', () => { + const TOKEN = new InjectionToken('TOKEN'); + + @Component({ + standalone: true, + template: '', + }) + class TestCmp { + nodeInjector = inject(Injector); + envInjector = inject(EnvironmentInjector); + } + + const {nodeInjector, envInjector} = TestBed.createComponent(TestCmp).componentInstance; + + const flags: InjectOptions = {optional: true}; + + let nodeInjectorResult = nodeInjector.get(TOKEN, undefined, flags); + expect(nodeInjectorResult).toBe(null); + + // Verify that `null` can be a valid value (from typing standpoint), + // the line below would fail a type check in case the result doesn't + // have `null` in the type. + nodeInjectorResult = null; + + let envInjectorResult = envInjector.get(TOKEN, undefined, flags); + expect(envInjectorResult).toBe(null); + + // Verify that `null` can be a valid value (from typing standpoint), + // the line below would fail a type check in case the result doesn't + // have `null` in the type. + envInjectorResult = null; + }); + it('should be able to use skipSelf injection in NodeInjector', () => { const TOKEN = new InjectionToken('TOKEN', { providedIn: 'root', diff --git a/packages/core/test/test_bed_spec.ts b/packages/core/test/test_bed_spec.ts index 917698d918362..01c2c8a3ccc64 100644 --- a/packages/core/test/test_bed_spec.ts +++ b/packages/core/test/test_bed_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {APP_INITIALIZER, ChangeDetectorRef, Compiler, Component, Directive, ElementRef, ErrorHandler, getNgModuleById, Inject, Injectable, InjectFlags, InjectionToken, Injector, Input, LOCALE_ID, ModuleWithProviders, NgModule, Optional, Pipe, Type, ViewChild, ɵsetClassMetadata as setClassMetadata, ɵɵdefineComponent as defineComponent, ɵɵdefineInjector as defineInjector, ɵɵdefineNgModule as defineNgModule, ɵɵelementEnd as elementEnd, ɵɵelementStart as elementStart, ɵɵsetNgModuleScope as setNgModuleScope, ɵɵtext as text} from '@angular/core'; +import {APP_INITIALIZER, ChangeDetectorRef, Compiler, Component, Directive, ElementRef, ErrorHandler, getNgModuleById, Inject, Injectable, InjectFlags, InjectionToken, InjectOptions, Injector, Input, LOCALE_ID, ModuleWithProviders, NgModule, Optional, Pipe, Type, ViewChild, ɵsetClassMetadata as setClassMetadata, ɵɵdefineComponent as defineComponent, ɵɵdefineInjector as defineInjector, ɵɵdefineNgModule as defineNgModule, ɵɵelementEnd as elementEnd, ɵɵelementStart as elementStart, ɵɵsetNgModuleScope as setNgModuleScope, ɵɵtext as text} from '@angular/core'; import {TestBed, TestBedImpl} from '@angular/core/testing/src/test_bed'; import {By} from '@angular/platform-browser'; import {expect} from '@angular/platform-browser/testing/src/matchers'; @@ -1866,6 +1866,19 @@ describe('TestBed', () => { expect(TestBed.inject(TOKEN, undefined, InjectFlags.Optional)).toBeNull(); }); + it('should include `null` into the result type when the optional flag is used', () => { + const TOKEN = new InjectionToken('TOKEN'); + + const flags: InjectOptions = {optional: true}; + let result = TestBed.inject(TOKEN, undefined, flags); + expect(result).toBe(null); + + // Verify that `null` can be a valid value (from typing standpoint), + // the line below would fail a type check in case the result doesn't + // have `null` in the type. + result = null; + }); + it('should be able to use skipSelf injection', () => { const TOKEN = new InjectionToken('TOKEN'); TestBed.configureTestingModule({ diff --git a/packages/core/testing/src/test_bed.ts b/packages/core/testing/src/test_bed.ts index aef89fa684a85..a2e7542d3fea4 100644 --- a/packages/core/testing/src/test_bed.ts +++ b/packages/core/testing/src/test_bed.ts @@ -90,10 +90,10 @@ export interface TestBed { compileComponents(): Promise; inject(token: ProviderToken, notFoundValue: undefined, options: InjectOptions&{ - optional: true - }): T|null; + optional?: false + }): T; + inject(token: ProviderToken, notFoundValue: null|undefined, options: InjectOptions): T|null; inject(token: ProviderToken, notFoundValue?: T, options?: InjectOptions): T; - inject(token: ProviderToken, notFoundValue: null, options?: InjectOptions): T|null; /** @deprecated use object-based flags (`InjectOptions`) instead. */ inject(token: ProviderToken, notFoundValue?: T, flags?: InjectFlags): T; /** @deprecated use object-based flags (`InjectOptions`) instead. */ @@ -299,11 +299,12 @@ export class TestBedImpl implements TestBed { return TestBedImpl.INSTANCE.overrideProvider(token, provider); } - static inject(token: ProviderToken, notFoundValue?: T, options?: InjectOptions): T; - static inject(token: ProviderToken, notFoundValue?: T, options?: InjectOptions&{ + static inject(token: ProviderToken, notFoundValue: undefined, options: InjectOptions&{ optional?: false }): T; - static inject(token: ProviderToken, notFoundValue: null, options?: InjectOptions): T|null; + static inject(token: ProviderToken, notFoundValue: null|undefined, options: InjectOptions): + T|null; + static inject(token: ProviderToken, notFoundValue?: T, options?: InjectOptions): T; /** @deprecated use object-based flags (`InjectOptions`) instead. */ static inject(token: ProviderToken, notFoundValue?: T, flags?: InjectFlags): T; /** @deprecated use object-based flags (`InjectOptions`) instead. */ diff --git a/packages/examples/core/di/ts/injector_spec.ts b/packages/examples/core/di/ts/injector_spec.ts index 8638f656197e5..353cc551feee1 100644 --- a/packages/examples/core/di/ts/injector_spec.ts +++ b/packages/examples/core/di/ts/injector_spec.ts @@ -6,12 +6,14 @@ * found in the LICENSE file at https://angular.io/license */ -import {inject, InjectFlags, InjectionToken, Injector, ProviderToken, ɵsetCurrentInjector as setCurrentInjector} from '@angular/core'; +import {inject, InjectFlags, InjectionToken, InjectOptions, Injector, ProviderToken, ɵsetCurrentInjector as setCurrentInjector} from '@angular/core'; class MockRootScopeInjector implements Injector { constructor(readonly parent: Injector) {} - get(token: ProviderToken, defaultValue?: any, flags: InjectFlags = InjectFlags.Default): T { + get( + token: ProviderToken, defaultValue?: any, + flags: InjectFlags|InjectOptions = InjectFlags.Default): T { if ((token as any).ɵprov && (token as any).ɵprov.providedIn === 'root') { const old = setCurrentInjector(this); try {