From c1dee4cfe304dd5110d63e7dbb1b2dccbb25514d Mon Sep 17 00:00:00 2001 From: AleksanderBodurri Date: Thu, 1 Jun 2023 20:34:35 -0400 Subject: [PATCH] test(core): unit tests for the injector profiler and injector debugging APIs (#48639) Creates unit tests for the following APIs - setInjectorProfiler - getInjectorProviders - getInjectorResolutionPath - getDependenciesFromInjectable Modifies existing tests in - packages/examples/core/di/ts/injector_spec.ts - packages/core/test/render3/jit/declare_injectable_spec.ts - packages/core/test/render3/jit/declare_factory_spec.ts because they setup framework injector context manually. Exports setInjectorProfilerContext in packages/core/src/core_private_export.ts in order for use in the the modified tests above. PR Close #48639 --- packages/core/src/core_private_export.ts | 1 + .../test/acceptance/injector_profiler_spec.ts | 975 ++++++++++++++++++ .../test/render3/jit/declare_factory_spec.ts | 14 +- .../render3/jit/declare_injectable_spec.ts | 14 +- packages/examples/core/di/ts/injector_spec.ts | 6 +- 5 files changed, 1003 insertions(+), 7 deletions(-) create mode 100644 packages/core/test/acceptance/injector_profiler_spec.ts diff --git a/packages/core/src/core_private_export.ts b/packages/core/src/core_private_export.ts index 9123fce8b9e..df8b889bfd7 100644 --- a/packages/core/src/core_private_export.ts +++ b/packages/core/src/core_private_export.ts @@ -25,6 +25,7 @@ export {InitialRenderPendingTasks as ɵInitialRenderPendingTasks} from './initia export {ComponentFactory as ɵComponentFactory} from './linker/component_factory'; export {clearResolutionOfComponentResourcesQueue as ɵclearResolutionOfComponentResourcesQueue, resolveComponentResources as ɵresolveComponentResources} from './metadata/resource_loading'; export {ReflectionCapabilities as ɵReflectionCapabilities} from './reflection/reflection_capabilities'; +export {InjectorProfilerContext as ɵInjectorProfilerContext, setInjectorProfilerContext as ɵsetInjectorProfilerContext} from './render3/debug/injector_profiler'; export {allowSanitizationBypassAndThrow as ɵallowSanitizationBypassAndThrow, BypassType as ɵBypassType, getSanitizationBypassType as ɵgetSanitizationBypassType, SafeHtml as ɵSafeHtml, SafeResourceUrl as ɵSafeResourceUrl, SafeScript as ɵSafeScript, SafeStyle as ɵSafeStyle, SafeUrl as ɵSafeUrl, SafeValue as ɵSafeValue, unwrapSafeValue as ɵunwrapSafeValue} from './sanitization/bypass'; export {_sanitizeHtml as ɵ_sanitizeHtml} from './sanitization/html_sanitizer'; export {_sanitizeUrl as ɵ_sanitizeUrl} from './sanitization/url_sanitizer'; diff --git a/packages/core/test/acceptance/injector_profiler_spec.ts b/packages/core/test/acceptance/injector_profiler_spec.ts new file mode 100644 index 00000000000..61682b5d556 --- /dev/null +++ b/packages/core/test/acceptance/injector_profiler_spec.ts @@ -0,0 +1,975 @@ +/** + * @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 {PercentPipe} from '@angular/common'; +import {inject} from '@angular/core'; +import {ClassProvider, Component, Directive, Inject, Injectable, InjectFlags, InjectionToken, Injector, NgModule, NgModuleRef, ViewChild} from '@angular/core/src/core'; +import {NullInjector} from '@angular/core/src/di/null_injector'; +import {isClassProvider, isExistingProvider, isFactoryProvider, isTypeProvider, isValueProvider} from '@angular/core/src/di/provider_collection'; +import {EnvironmentInjector, R3Injector} from '@angular/core/src/di/r3_injector'; +import {setupFrameworkInjectorProfiler} from '@angular/core/src/render3/debug/framework_injector_profiler'; +import {getInjectorProfilerContext, InjectedService, InjectedServiceEvent, InjectorCreatedInstanceEvent, InjectorProfilerEvent, InjectorProfilerEventType, ProviderConfiguredEvent, ProviderRecord, setInjectorProfiler} from '@angular/core/src/render3/debug/injector_profiler'; +import {getNodeInjectorLView, NodeInjector} from '@angular/core/src/render3/di'; +import {getDependenciesFromInjectable, getInjectorProviders, getInjectorResolutionPath} from '@angular/core/src/render3/util/injector_discovery_utils'; +import {fakeAsync, tick} from '@angular/core/testing'; +import {TestBed} from '@angular/core/testing/src/test_bed'; +import {BrowserModule} from '@angular/platform-browser'; +import {Router, RouterModule, RouterOutlet} from '@angular/router'; + +describe('setProfiler', () => { + let injectEvents: InjectedServiceEvent[] = []; + let createEvents: InjectorCreatedInstanceEvent[] = []; + let providerConfiguredEvents: ProviderConfiguredEvent[] = []; + + function searchForProfilerEvent( + events: T[], condition: ((event: T) => boolean)): T|undefined { + return events.find(event => condition(event)) as T; + } + + beforeEach(() => { + injectEvents = []; + createEvents = []; + providerConfiguredEvents = []; + + setInjectorProfiler(((injectorProfilerEvent: InjectorProfilerEvent) => { + const {type} = injectorProfilerEvent; + if (type === InjectorProfilerEventType.Inject) { + injectEvents.push( + {service: injectorProfilerEvent.service, context: getInjectorProfilerContext(), type}); + } + if (type === InjectorProfilerEventType.InstanceCreatedByInjector) { + createEvents.push({ + instance: injectorProfilerEvent.instance, + context: getInjectorProfilerContext(), + type + }); + } + if (type === InjectorProfilerEventType.ProviderConfigured) { + providerConfiguredEvents.push({ + providerRecord: injectorProfilerEvent.providerRecord, + context: getInjectorProfilerContext(), + type + }); + } + })); + }); + + afterAll(() => setInjectorProfiler(null)); + + it('should emit DI events when a component contains a provider and injects it', () => { + class MyService {} + + @Component({ + selector: 'my-comp', + template: 'hello world', + providers: [ + MyService, + ] + }) + class MyComponent { + myService = inject(MyService); + } + + TestBed.configureTestingModule({declarations: [MyComponent]}); + const fixture = TestBed.createComponent(MyComponent); + const myComp = fixture.componentInstance; + + // MyService should have been configured + const myServiceProviderConfiguredEvent = searchForProfilerEvent( + providerConfiguredEvents, (event) => event.providerRecord.token === MyService); + expect(myServiceProviderConfiguredEvent).toBeTruthy(); + + // inject(MyService) was called + const myServiceInjectEvent = searchForProfilerEvent( + injectEvents, (event) => event.service.token === MyService); + expect(myServiceInjectEvent).toBeTruthy(); + expect(myServiceInjectEvent!.service.value).toBe(myComp.myService); + expect(myServiceInjectEvent!.service.flags).toBe(InjectFlags.Default); + + // myComp is an angular instance that is able to call `inject` in it's constructor, so a + // create event should have been emitted for it + const componentCreateEvent = searchForProfilerEvent( + createEvents, (event) => (event.instance.value === myComp)); + expect(componentCreateEvent).toBeTruthy(); + }); + + it('should emit the correct DI events when a service is injected with injection flags', () => { + class MyService {} + class MyServiceB {} + class MyServiceC {} + + @Component({ + selector: 'my-comp', + template: 'hello world', + providers: [MyService, {provide: MyServiceB, useValue: 0}] + }) + class MyComponent { + myService = inject(MyService, {self: true}); + myServiceD = inject(MyServiceB, {skipSelf: true}); + myServiceC = inject(MyServiceC, {optional: true}); + } + + TestBed.configureTestingModule({ + providers: [MyServiceB, MyServiceC, {provide: MyServiceB, useValue: 1}], + declarations: [MyComponent] + }); + TestBed.createComponent(MyComponent); + + const myServiceInjectEvent = searchForProfilerEvent( + injectEvents, (event) => event.service.token === MyService); + const myServiceBInjectEvent = + searchForProfilerEvent(injectEvents, (event) => event.service.token === MyServiceB); + const myServiceCInjectEvent = + searchForProfilerEvent(injectEvents, (event) => event.service.token === MyServiceC); + + expect(myServiceInjectEvent!.service.flags).toBe(InjectFlags.Self); + expect(myServiceBInjectEvent!.service.flags).toBe(InjectFlags.SkipSelf); + expect(myServiceBInjectEvent!.service.value).toBe(1); + expect(myServiceCInjectEvent!.service.flags).toBe(InjectFlags.Optional); + }); + + it('should emit correct DI events when providers are configured with useFactory, useExisting, useClass, useValue', + () => { + class MyService {} + class MyServiceB {} + class MyServiceC {} + class MyServiceD {} + class MyServiceE {} + + @Component({ + selector: 'my-comp', + template: 'hello world', + providers: [ + MyService, + {provide: MyServiceB, useFactory: () => new MyServiceB()}, + {provide: MyServiceC, useExisting: MyService}, + {provide: MyServiceD, useValue: 'hello world'}, + {provide: MyServiceE, useClass: class MyExampleClass {}}, + ] + }) + class MyComponent { + myService = inject(MyService); + } + + TestBed.configureTestingModule({declarations: [MyComponent]}); + TestBed.createComponent(MyComponent); + + // MyService should have been configured + const myServiceProviderConfiguredEvent = searchForProfilerEvent( + providerConfiguredEvents, (event) => event.providerRecord.token === MyService); + const myServiceBProviderConfiguredEvent = searchForProfilerEvent( + providerConfiguredEvents, (event) => event.providerRecord.token === MyServiceB); + const myServiceCProviderConfiguredEvent = searchForProfilerEvent( + providerConfiguredEvents, (event) => event.providerRecord.token === MyServiceC); + const myServiceDProviderConfiguredEvent = searchForProfilerEvent( + providerConfiguredEvents, (event) => event.providerRecord.token === MyServiceD); + const myServiceEProviderConfiguredEvent = searchForProfilerEvent( + providerConfiguredEvents, (event) => event.providerRecord.token === MyServiceE); + + expect(isTypeProvider(myServiceProviderConfiguredEvent!.providerRecord.provider!)) + .toBeTrue(); + expect(isFactoryProvider(myServiceBProviderConfiguredEvent!.providerRecord.provider!)) + .toBeTrue(); + expect(isExistingProvider(myServiceCProviderConfiguredEvent!.providerRecord.provider!)) + .toBeTrue(); + expect(isValueProvider(myServiceDProviderConfiguredEvent!.providerRecord.provider!)) + .toBeTrue(); + expect(isClassProvider(myServiceEProviderConfiguredEvent!.providerRecord.provider!)) + .toBeTrue(); + }); + + it('should emit correct DI events when providers are configured with multi', () => { + class MyService {} + + @Component({ + selector: 'my-comp', + template: 'hello world', + providers: [ + {provide: MyService, useClass: MyService, multi: true}, + {provide: MyService, useFactory: () => new MyService(), multi: true}, + {provide: MyService, useValue: 'hello world', multi: true}, + ] + }) + class MyComponent { + myService = inject(MyService); + } + + TestBed.configureTestingModule({declarations: [MyComponent]}); + TestBed.createComponent(MyComponent); + + // MyService should have been configured + const myServiceProviderConfiguredEvent = searchForProfilerEvent( + providerConfiguredEvents, (event) => event.providerRecord.token === MyService); + + expect(((myServiceProviderConfiguredEvent!.providerRecord)?.provider as ClassProvider).multi) + .toBeTrue(); + }); +}); + +describe('getInjectorProviders', () => { + beforeEach(() => setupFrameworkInjectorProfiler()); + afterAll(() => setInjectorProfiler(null)); + + it('should be able to get the providers from a components injector', () => { + class MyService {} + @Component({ + selector: 'my-comp', + template: ` + {{b | percent:'4.3-5' }} + `, + providers: [MyService] + }) + class MyComponent { + b = 1.3495; + } + TestBed.configureTestingModule({declarations: [MyComponent], imports: [PercentPipe]}); + const fixture = TestBed.createComponent(MyComponent); + + const providers = getInjectorProviders(fixture.debugElement.injector); + expect(providers.length).toBe(1); + expect(providers[0].token).toBe(MyService); + expect(providers[0].provider).toBe(MyService); + expect(providers[0].isViewProvider).toBe(false); + }); + + it('should be able to get determine if a provider is a view provider', () => { + class MyService {} + @Component({ + selector: 'my-comp', + template: ` + {{b | percent:'4.3-5' }} + `, + viewProviders: [MyService] + }) + class MyComponent { + b = 1.3495; + } + TestBed.configureTestingModule({declarations: [MyComponent], imports: [PercentPipe]}); + const fixture = TestBed.createComponent(MyComponent); + + const providers = getInjectorProviders(fixture.debugElement.injector); + expect(providers.length).toBe(1); + expect(providers[0].token).toBe(MyService); + expect(providers[0].provider).toBe(MyService); + expect(providers[0].isViewProvider).toBe(true); + }); + + it('should be able to determine import paths after module provider flattening in the NgModule bootstrap case', + () => { + // ┌─────────┐ + // │AppModule│ + // └────┬────┘ + // │ + // imports + // │ + // ┌────▼────┐ + // ┌─imports─┤ ModuleD ├──imports─┐ + // │ └─────────┘ │ + // │ ┌─────▼─────┐ + // ┌───▼───┐ │ ModuleC │ + // │ModuleB│ │-MyServiceB│ + // └───┬───┘ └───────────┘ + // │ + // imports + // │ + // ┌────▼─────┐ + // │ ModuleA │ + // │-MyService│ + // └──────────┘ + + class MyService {} + class MyServiceB {} + + @NgModule({providers: [MyService]}) + class ModuleA { + } + @NgModule({ + imports: [ModuleA], + }) + class ModuleB { + } + + @NgModule({providers: [MyServiceB]}) + class ModuleC { + } + + @NgModule({ + imports: [ModuleB, ModuleC], + }) + class ModuleD { + } + + @Component({ + selector: 'my-comp', + template: 'hello world', + }) + class MyComponent { + } + + @NgModule({ + imports: [ModuleD, BrowserModule], + declarations: [MyComponent], + }) + class AppModule { + } + + TestBed.configureTestingModule({imports: [AppModule]}); + const root = TestBed.createComponent(MyComponent); + root.detectChanges(); + + const appModuleInjector = root.componentRef.injector.get(EnvironmentInjector); + const providers = getInjectorProviders(appModuleInjector); + + const myServiceProvider = providers.find(provider => provider.token === MyService); + const myServiceBProvider = providers.find(provider => provider.token === MyServiceB); + + const testModuleType = root.componentRef.injector.get(NgModuleRef).instance.constructor; + + expect(myServiceProvider).toBeTruthy(); + expect(myServiceBProvider).toBeTruthy(); + + expect(myServiceProvider!.importPath).toBeInstanceOf(Array); + expect(myServiceProvider!.importPath!.length).toBe(5); + expect(myServiceProvider!.importPath![0]).toBe(testModuleType); + expect(myServiceProvider!.importPath![1]).toBe(AppModule); + expect(myServiceProvider!.importPath![2]).toBe(ModuleD); + expect(myServiceProvider!.importPath![3]).toBe(ModuleB); + expect(myServiceProvider!.importPath![4]).toBe(ModuleA); + + expect(myServiceBProvider!.importPath).toBeInstanceOf(Array); + expect(myServiceBProvider!.importPath!.length).toBe(4); + expect(myServiceBProvider!.importPath![0]).toBe(testModuleType); + expect(myServiceBProvider!.importPath![1]).toBe(AppModule); + expect(myServiceBProvider!.importPath![2]).toBe(ModuleD); + expect(myServiceBProvider!.importPath![3]).toBe(ModuleC); + }); + + it('should be able to determine import paths after module provider flattening in the standalone component case', + () => { + // ┌────────────────────imports───────────────────────┐ + // │ │ + // │ ┌───────imports────────┐ │ + // │ │ │ │ + // │ │ │ │ + // ┌─────────┴─┴─────────┐ ┌─────────▼────────────┐ ┌──────────▼───────────┐ + // │MyStandaloneComponent│ │MyStandaloneComponentB│ │MyStandaloneComponentC│ + // └──────────┬──────────┘ └──────────┬────┬──────┘ └────┬────────┬────────┘ + // │ │ │ │ │ + // └──imports─┐ ┌imports┘ └────┐ │ │ + // │ │ │ │ imports + // ┌▼─────▼┐ imports │ │ + // ┌────┤ModuleD├─────┐ │ imports │ + // imports └───────┘ │ │ │ ┌───▼────────┐ + // │ imports ┌──▼─────┐ │ │ ModuleE │ + // ┌──▼────┐ │ │ModuleF │ │ │-MyServiceC │ + // │ModuleB│ │ └────────┘ │ └────────────┘ + // └──┬────┘ ┌─────▼─────┐ │ + // imports │ ModuleC │ │ + // ┌────▼─────┐ │-MyServiceB│◄────────────┘ + // │ ModuleA │ └───────────┘ + // │-MyService│ + // └──────────┘ + + + class MyService {} + class MyServiceB {} + class MyServiceC {} + + @NgModule({providers: [MyService]}) + class ModuleA { + } + @NgModule({ + imports: [ModuleA], + }) + class ModuleB { + } + + @NgModule({providers: [MyServiceB]}) + class ModuleC { + } + + @NgModule({ + imports: [ModuleB, ModuleC], + }) + class ModuleD { + } + + @NgModule({ + providers: [MyServiceC], + }) + class ModuleE { + } + + @NgModule({}) + class ModuleF { + } + + + @Component({ + selector: 'my-comp-c', + template: 'hello world', + imports: [ModuleE, ModuleC], + standalone: true + }) + class MyStandaloneComponentC { + } + + @Component({ + selector: 'my-comp-b', + template: 'hello world', + imports: [ModuleD, ModuleF], + standalone: true + }) + class MyStandaloneComponentB { + } + + @Component({ + selector: 'my-comp', + template: ` + + + `, + imports: [ModuleD, MyStandaloneComponentB, MyStandaloneComponentC], + standalone: true + }) + class MyStandaloneComponent { + } + + const root = TestBed.createComponent(MyStandaloneComponent); + root.detectChanges(); + + const appComponentEnvironmentInjector = root.componentRef.injector.get(EnvironmentInjector); + const providers = getInjectorProviders(appComponentEnvironmentInjector); + + // There are 2 paths from MyStandaloneComponent to MyService + // + // path 1: MyStandaloneComponent -> ModuleD => ModuleB -> ModuleA + // path 2: MyStandaloneComponent -> MyStandaloneComponentB -> ModuleD => ModuleB -> ModuleA + // + // Angular discovers this provider through the first path it visits + // during it's postorder traversal (in this case path 1). Therefore + // we expect myServiceProvider.importPath to have 4 DI containers + // + const myServiceProvider = providers.find(provider => provider.token === MyService); + expect(myServiceProvider).toBeTruthy(); + expect(myServiceProvider!.importPath).toBeInstanceOf(Array); + expect(myServiceProvider!.importPath!.length).toBe(4); + expect(myServiceProvider!.importPath![0]).toBe(MyStandaloneComponent); + expect(myServiceProvider!.importPath![1]).toBe(ModuleD); + expect(myServiceProvider!.importPath![2]).toBe(ModuleB); + expect(myServiceProvider!.importPath![3]).toBe(ModuleA); + + // Similarly to above there are multiple paths from MyStandaloneComponent MyServiceB + // + // path 1: MyStandaloneComponent -> ModuleD => ModuleC + // path 2: MyStandaloneComponent -> MyStandaloneComponentB -> ModuleD => ModuleC + // path 3: MyStandaloneComponent -> MyStandaloneComponentC -> ModuleC + // + // Angular discovers this provider through the first path it visits + // during it's postorder traversal (in this case path 1). Therefore + // we expect myServiceProvider.importPath to have 4 DI containers + // + const myServiceBProvider = providers.find(provider => provider.token === MyServiceB); + expect(myServiceBProvider).toBeTruthy(); + expect(myServiceBProvider!.importPath).toBeInstanceOf(Array); + expect(myServiceBProvider!.importPath!.length).toBe(3); + expect(myServiceBProvider!.importPath![0]).toBe(MyStandaloneComponent); + expect(myServiceBProvider!.importPath![1]).toBe(ModuleD); + expect(myServiceBProvider!.importPath![2]).toBe(ModuleC); + }); + + it('should be able to determine import paths after module provider flattening in the standalone component case with lazy components', + fakeAsync(() => { + class MyService {} + + @NgModule({providers: [MyService]}) + class ModuleA { + } + + @Component( + {selector: 'my-comp-b', template: 'hello world', imports: [ModuleA], standalone: true}) + class MyStandaloneComponentB { + injector = inject(Injector); + } + + @Component({ + selector: 'my-comp', + template: ``, + imports: [MyStandaloneComponentB, RouterOutlet], + standalone: true + }) + class MyStandaloneComponent { + injector = inject(Injector); + @ViewChild(RouterOutlet) routerOutlet: RouterOutlet|undefined; + } + + TestBed.configureTestingModule({ + imports: [RouterModule.forRoot([{ + path: 'lazy', + loadComponent: () => MyStandaloneComponentB, + }])] + }); + const root = TestBed.createComponent(MyStandaloneComponent); + TestBed.inject(Router).navigateByUrl('/lazy'); + tick(); + root.detectChanges(); + + const myStandaloneComponentNodeInjector = root.componentRef.injector; + const myStandaloneComponentEnvironmentInjector = + myStandaloneComponentNodeInjector.get(EnvironmentInjector); + const myStandalonecomponentB = + root.componentRef.instance!.routerOutlet!.component as MyStandaloneComponentB; + const myComponentBNodeInjector = myStandalonecomponentB.injector; + const myComponentBEnvironmentInjector = myComponentBNodeInjector.get(EnvironmentInjector); + const myStandaloneComponentEnvironmentInjectorProviders = + getInjectorProviders(myStandaloneComponentEnvironmentInjector); + const myComponentBEnvironmentInjectorProviders = + getInjectorProviders(myComponentBEnvironmentInjector); + + // Lazy component should have its own environment injector and therefore different + // providers + expect(myStandaloneComponentEnvironmentInjectorProviders) + .not.toEqual(myComponentBEnvironmentInjectorProviders); + + const myServiceProviderRecord = + myComponentBEnvironmentInjectorProviders.find(provider => provider.token === MyService); + + expect(myServiceProviderRecord).toBeTruthy(); + expect(myServiceProviderRecord!.importPath).toBeInstanceOf(Array); + expect(myServiceProviderRecord!.importPath!.length).toBe(2); + expect(myServiceProviderRecord!.importPath![0]).toBe(MyStandaloneComponentB); + expect(myServiceProviderRecord!.importPath![1]).toBe(ModuleA); + })); +}); + +describe('getDependenciesFromInjectable', () => { + beforeEach(() => setupFrameworkInjectorProfiler()); + afterAll(() => setInjectorProfiler(null)); + + it('should be able to determine which injector dependencies come from', fakeAsync(() => { + class MyService {} + class MyServiceB {} + class MyServiceC {} + class MyServiceD {} + class MyServiceG {} + class MyServiceH {} + + const myInjectionToken = new InjectionToken('myInjectionToken'); + const myServiceCInstance = new MyServiceC(); + + @NgModule({ + providers: [ + MyService, {provide: MyServiceB, useValue: 'hello world'}, + {provide: MyServiceC, useFactory: () => 123}, + {provide: myInjectionToken, useValue: myServiceCInstance} + ] + }) + class ModuleA { + } + + @Directive({ + selector: '[my-directive]', + standalone: true, + }) + class MyStandaloneDirective { + serviceFromHost = inject(MyServiceH, {host: true, optional: true}); + injector = inject(Injector); + + ngOnInit() { + onMyStandaloneDirectiveCreated(this); + } + } + + @Component({ + selector: 'my-comp-c', + template: 'hello world', + imports: [], + standalone: true, + }) + class MyStandaloneComponentC { + } + + @Component({ + selector: 'my-comp-b', + template: '', + imports: [MyStandaloneComponentC, MyStandaloneDirective], + standalone: true + }) + class MyStandaloneComponentB { + myService = inject(MyService); + myServiceB = inject(MyServiceB, {optional: true}); + myServiceC = inject(MyServiceC, {skipSelf: true}); + myInjectionTokenValue = inject(myInjectionToken); + injector = inject(Injector, {self: true, host: true}); + myServiceD = inject(MyServiceD); + myServiceG = inject(MyServiceG); + parentComponent = inject(MyStandaloneComponent); + } + + @Component({ + selector: 'my-comp', + template: ``, + imports: [RouterOutlet, ModuleA], + providers: [MyServiceG, {provide: MyServiceH, useValue: 'MyStandaloneComponent'}], + standalone: true + }) + class MyStandaloneComponent { + injector = inject(Injector); + @ViewChild(RouterOutlet) routerOutlet: RouterOutlet|undefined; + } + + TestBed.configureTestingModule({ + providers: [{provide: MyServiceD, useValue: '123'}], + imports: + [RouterModule.forRoot([{path: 'lazy', loadComponent: () => MyStandaloneComponentB}])] + }); + + const root = TestBed.createComponent(MyStandaloneComponent); + TestBed.inject(Router).navigateByUrl('/lazy'); + tick(); + root.detectChanges(); + + const myStandalonecomponentB = + root.componentRef.instance!.routerOutlet!.component as MyStandaloneComponentB; + + const {dependencies: dependenciesOfMyStandaloneComponentB} = + getDependenciesFromInjectable(myStandalonecomponentB.injector, MyStandaloneComponentB)!; + const standaloneInjector = + root.componentInstance.injector.get(EnvironmentInjector) as EnvironmentInjector; + + expect(dependenciesOfMyStandaloneComponentB).toBeInstanceOf(Array); + expect(dependenciesOfMyStandaloneComponentB.length).toBe(8); + + const myServiceDep = dependenciesOfMyStandaloneComponentB[0]; + const myServiceBDep = dependenciesOfMyStandaloneComponentB[1]; + const myServiceCDep = dependenciesOfMyStandaloneComponentB[2]; + const myInjectionTokenValueDep = dependenciesOfMyStandaloneComponentB[3]; + const injectorDep = dependenciesOfMyStandaloneComponentB[4]; + const myServiceDDep = dependenciesOfMyStandaloneComponentB[5]; + const myServiceGDep = dependenciesOfMyStandaloneComponentB[6]; + const parentComponentDep = dependenciesOfMyStandaloneComponentB[7]; + + expect(myServiceDep.token).toBe(MyService); + expect(myServiceBDep.token).toBe(MyServiceB); + expect(myServiceCDep.token).toBe(MyServiceC); + expect(myInjectionTokenValueDep.token).toBe(myInjectionToken); + expect(injectorDep.token).toBe(Injector); + expect(myServiceDDep.token).toBe(MyServiceD); + expect(myServiceGDep.token).toBe(MyServiceG); + expect(parentComponentDep.token).toBe(MyStandaloneComponent); + + expect(dependenciesOfMyStandaloneComponentB[0].flags).toEqual({ + optional: false, + skipSelf: false, + self: false, + host: false, + }); + expect(myServiceBDep.flags).toEqual({ + optional: true, + skipSelf: false, + self: false, + host: false, + }); + expect(myServiceCDep.flags).toEqual({ + optional: false, + skipSelf: true, + self: false, + host: false, + }); + expect(myInjectionTokenValueDep.flags).toEqual({ + optional: false, + skipSelf: false, + self: false, + host: false, + }); + expect(injectorDep.flags).toEqual({ + optional: false, + skipSelf: false, + self: true, + host: true, + }); + expect(myServiceDDep.flags).toEqual({ + optional: false, + skipSelf: false, + self: false, + host: false, + }); + expect(myServiceGDep.flags).toEqual({ + optional: false, + skipSelf: false, + self: false, + host: false, + }); + expect(parentComponentDep.flags).toEqual({ + optional: false, + skipSelf: false, + self: false, + host: false, + }); + + + expect(dependenciesOfMyStandaloneComponentB[0].value).toBe(myStandalonecomponentB.myService); + expect(myServiceBDep.value).toBe('hello world'); + expect(myServiceCDep.value).toBe(123); + expect(myInjectionTokenValueDep.value).toBe(myServiceCInstance); + expect(injectorDep.value).toBe(myStandalonecomponentB.injector); + expect(myServiceDDep.value).toBe('123'); + expect(myServiceGDep.value).toBe(myStandalonecomponentB.myServiceG); + expect(parentComponentDep.value).toBe(myStandalonecomponentB.parentComponent); + + expect(dependenciesOfMyStandaloneComponentB[0].providedIn).toBe(standaloneInjector); + expect(myServiceBDep.providedIn).toBe(standaloneInjector); + expect(myServiceCDep.providedIn).toBe(standaloneInjector); + expect(myInjectionTokenValueDep.providedIn).toBe(standaloneInjector); + expect(injectorDep.providedIn).toBe(myStandalonecomponentB.injector); + expect(myServiceDDep.providedIn).toBe(standaloneInjector.get(Injector, null, { + skipSelf: true + }) as Injector); + expect(getNodeInjectorLView(myServiceGDep.providedIn as NodeInjector)) + .toBe(getNodeInjectorLView( + myStandalonecomponentB.parentComponent.injector as NodeInjector)); + + function onMyStandaloneDirectiveCreated(myStandaloneDirective: MyStandaloneDirective) { + const injector = myStandaloneDirective.injector; + const deps = getDependenciesFromInjectable(injector, MyStandaloneDirective); + expect(deps).not.toBeNull(); + expect(deps!.dependencies.length).toBe(2); // MyServiceH, Injector + expect(deps!.dependencies[0].token).toBe(MyServiceH); + expect(deps!.dependencies[0].flags) + .toEqual({optional: true, host: true, self: false, skipSelf: false}); + // The NodeInjector that provides MyService is not in the host path of this injector. + expect(deps!.dependencies[0].providedIn).toBeUndefined(); + } + })); + + it('should be able to recursively determine dependencies of dependencies by using the providedIn field', + fakeAsync(() => { + @Injectable() + class MyService { + myServiceB = inject(MyServiceB); + } + + @Injectable() + class MyServiceB { + router = inject(Router); + } + + @NgModule({providers: [MyService]}) + class ModuleA { + } + + @NgModule({imports: [ModuleA]}) + class ModuleB { + } + + @NgModule({providers: [MyServiceB]}) + class ModuleC { + } + + @NgModule({imports: [ModuleB, ModuleC]}) + class ModuleD { + } + + @Component( + {selector: 'my-comp', template: 'hello world', imports: [ModuleD], standalone: true}) + class MyStandaloneComponent { + myService = inject(MyService); + } + + TestBed.configureTestingModule({imports: [RouterModule]}); + const root = TestBed.createComponent(MyStandaloneComponent); + + const {instance, dependencies} = getDependenciesFromInjectable( + root.componentRef.injector, root.componentRef.componentType)!; + const standaloneInjector = root.componentRef.injector.get(EnvironmentInjector); + + expect(instance).toBeInstanceOf(MyStandaloneComponent); + expect(dependencies).toBeInstanceOf(Array); + expect(dependencies.length).toBe(1); + + const myServiceDependency = dependencies[0]; + + expect(myServiceDependency.token).toBe(MyService); + expect(myServiceDependency.value).toBe((instance as MyStandaloneComponent).myService); + expect(myServiceDependency.flags) + .toEqual({optional: false, skipSelf: false, self: false, host: false}); + expect(myServiceDependency.providedIn).toBe(standaloneInjector); + + const {instance: myServiceInstance, dependencies: myServiceDependencies} = + getDependenciesFromInjectable( + myServiceDependency.providedIn!, myServiceDependency.token!)!; + expect(myServiceDependencies).toBeInstanceOf(Array); + expect(myServiceDependencies.length).toBe(1); + const myServiceBDependency = myServiceDependencies[0]; + + expect(myServiceBDependency.token).toBe(MyServiceB); + expect(myServiceBDependency.value).toBe((myServiceInstance as MyService).myServiceB); + expect(myServiceBDependency.flags) + .toEqual({optional: false, skipSelf: false, self: false, host: false}); + expect(myServiceBDependency.providedIn).toBe(standaloneInjector); + + const {instance: myServiceBInstance, dependencies: myServiceBDependencies} = + getDependenciesFromInjectable( + myServiceBDependency.providedIn!, myServiceBDependency.token!)!; + expect(myServiceBDependencies).toBeInstanceOf(Array); + expect(myServiceBDependencies.length).toBe(1); + const routerDependency = myServiceBDependencies[0]; + + expect(routerDependency.token).toBe(Router); + expect(routerDependency.value).toBe((myServiceBInstance as MyServiceB).router); + expect(routerDependency.flags) + .toEqual({optional: false, skipSelf: false, self: false, host: false}); + expect(routerDependency.providedIn).toBe((standaloneInjector as R3Injector).parent); + })); +}); + +describe('getInjectorResolutionPath', () => { + beforeEach(() => setupFrameworkInjectorProfiler()); + afterAll(() => setInjectorProfiler(null)); + + it('should be able to inspect injector hierarchy structure', fakeAsync(() => { + class MyServiceA {} + @NgModule({providers: [MyServiceA]}) + class ModuleA { + } + + class MyServiceB {} + @NgModule({providers: [MyServiceB]}) + class ModuleB { + } + + @Component({ + selector: 'lazy-comp', + template: `lazy component`, + standalone: true, + imports: [ModuleB], + }) + class LazyComponent { + constructor() { + onLazyComponentCreated(); + } + } + + @Component({ + standalone: true, + imports: [RouterOutlet, ModuleA], + template: ``, + }) + class MyStandaloneComponent { + nodeInjector = inject(Injector); + envInjector = inject(EnvironmentInjector); + } + + TestBed.configureTestingModule({ + imports: [RouterModule.forRoot([{ + path: 'lazy', + loadComponent: () => LazyComponent, + }])] + }); + const root = TestBed.createComponent(MyStandaloneComponent); + TestBed.inject(Router).navigateByUrl('/lazy'); + tick(); + root.detectChanges(); + + function onLazyComponentCreated() { + const lazyComponentNodeInjector = inject(Injector); + const lazyComponentEnvironmentInjector = inject(EnvironmentInjector); + + const routerOutletNodeInjector = inject(Injector, {skipSelf: true}) as NodeInjector; + + const myStandaloneComponent = inject(MyStandaloneComponent); + const myStandaloneComponentNodeInjector = + myStandaloneComponent.nodeInjector as NodeInjector; + + const path = getInjectorResolutionPath(lazyComponentNodeInjector); + + /** + * + * Here is a diagram of the injectors in our application: + * + * + * + * ┌────────────┐ + * │NullInjector│ + * └─────▲──────┘ + * │ + * ┌────────────┴────────────────┐ + * │EnvironmentInjector(Platform)│ + * └────────────▲────────────────┘ + * │ + * ┌────────────┴────────────┐ + * │EnvironmentInjector(Root)│ + * └───────────────▲─────────┘ + * │ + * │ + * │ + *┌────────────────────────────────────┐ ┌─┴────────────────────────────────────────┐ + *│ NodeInjector(MyStandaloneComponent)├─►| EnvironmentInjector(MyStandaloneComponent│ + *└────────────────▲───────────────────┘ └────────────▲─────────────────────────────┘ + * │ │ + * │ │ + * │ │ + * ┌────────────┴─────────────┐ │ + * │NodeInjector(RouterOutlet)├──────────┐ │ + * └────────────▲─────────────┘ │ │ + * │ │ │ + * │ │ │ + * │ │ │ + * │ │ │ + * ┌─────────────┴──────────────┐ ┌──────▼──────────┴────────────────┐ + * │ NodeInjector(LazyComponent)├──►EnvironmentInjector(LazyComponent)│ + * └────────────────────────────┘ └──────────────────────────────────┘ + * + * + * + * + * + * The Resolution path if we start at NodeInjector(LazyComponent) should be + * [ + * NodeInjector[LazyComponent], + * NodeInjector[RouterOutlet], + * NodeInjector[MyStandaloneComponent], + * R3Injector[LazyComponent], + * R3Injector[MyStandaloneComponent], + * R3Injector[Root], + * R3Injector[Platform], + * NullInjector + * ] + */ + expect(path.length).toBe(8); + + expect(path[0]).toBe(lazyComponentNodeInjector); + + expect(path[1]).toBeInstanceOf(NodeInjector); + expect(getNodeInjectorLView(path[1] as NodeInjector)) + .toBe(getNodeInjectorLView(routerOutletNodeInjector)); + + expect(path[2]).toBeInstanceOf(NodeInjector); + expect(getNodeInjectorLView(path[2] as NodeInjector)) + .toBe(getNodeInjectorLView(myStandaloneComponentNodeInjector)); + + expect(path[3]).toBeInstanceOf(R3Injector); + expect(path[3]).toBe(lazyComponentEnvironmentInjector); + expect((path[3] as R3Injector).scopes.has('environment')).toBeTrue(); + expect((path[3] as R3Injector).source).toBe('Standalone[LazyComponent]'); + + expect(path[4]).toBeInstanceOf(R3Injector); + expect((path[4] as R3Injector).scopes.has('environment')).toBeTrue(); + expect((path[4] as R3Injector).source).toBe('Standalone[MyStandaloneComponent]'); + + expect(path[5]).toBeInstanceOf(R3Injector); + expect((path[5] as R3Injector).scopes.has('environment')).toBeTrue(); + expect((path[5] as R3Injector).scopes.has('root')).toBeTrue(); + + expect(path[6]).toBeInstanceOf(R3Injector); + expect((path[6] as R3Injector).scopes.has('platform')).toBeTrue(); + + expect(path[7]).toBeInstanceOf(NullInjector); + } + })); +}); diff --git a/packages/core/test/render3/jit/declare_factory_spec.ts b/packages/core/test/render3/jit/declare_factory_spec.ts index 8a47ae317dc..a73b7b82ecc 100644 --- a/packages/core/test/render3/jit/declare_factory_spec.ts +++ b/packages/core/test/render3/jit/declare_factory_spec.ts @@ -6,14 +6,22 @@ * found in the LICENSE file at https://angular.io/license */ -import {Injector, ɵcreateInjector, ɵɵFactoryTarget, ɵɵngDeclareFactory} from '@angular/core'; +import {Injector, ɵcreateInjector, ɵInjectorProfilerContext, ɵsetInjectorProfilerContext, ɵɵFactoryTarget, ɵɵngDeclareFactory} from '@angular/core'; import {ɵɵdefineInjector} from '@angular/core/src/di'; import {setCurrentInjector} from '@angular/core/src/di/injector_compatibility'; describe('Factory declaration jit compilation', () => { let previousInjector: Injector|null|undefined; - beforeEach(() => previousInjector = setCurrentInjector(ɵcreateInjector(TestInjector))); - afterEach(() => setCurrentInjector(previousInjector)); + let previousInjectorProfilerContext: ɵInjectorProfilerContext; + beforeEach(() => { + const injector = ɵcreateInjector(TestInjector); + previousInjector = setCurrentInjector(injector); + previousInjectorProfilerContext = ɵsetInjectorProfilerContext({injector, token: null}); + }); + afterEach(() => { + setCurrentInjector(previousInjector); + previousInjectorProfilerContext = ɵsetInjectorProfilerContext(previousInjectorProfilerContext); + }); it('should compile a simple factory declaration', () => { const factory = TestClass.ɵfac as Function; diff --git a/packages/core/test/render3/jit/declare_injectable_spec.ts b/packages/core/test/render3/jit/declare_injectable_spec.ts index f4d0b312a79..223a16835e7 100644 --- a/packages/core/test/render3/jit/declare_injectable_spec.ts +++ b/packages/core/test/render3/jit/declare_injectable_spec.ts @@ -6,12 +6,20 @@ * found in the LICENSE file at https://angular.io/license */ -import {forwardRef, InjectionToken, Injector, ɵcreateInjector, ɵsetCurrentInjector, ɵɵdefineInjector, ɵɵInjectableDeclaration, ɵɵngDeclareInjectable, ɵɵngDeclareInjector, ɵɵngDeclareNgModule} from '@angular/core'; +import {forwardRef, InjectionToken, Injector, ɵcreateInjector, ɵInjectorProfilerContext, ɵsetCurrentInjector, ɵsetInjectorProfilerContext, ɵɵdefineInjector, ɵɵInjectableDeclaration, ɵɵngDeclareInjectable, ɵɵngDeclareInjector, ɵɵngDeclareNgModule} from '@angular/core'; describe('Injectable declaration jit compilation', () => { let previousInjector: Injector|null|undefined; - beforeEach(() => previousInjector = ɵsetCurrentInjector(ɵcreateInjector(TestInjector))); - afterEach(() => ɵsetCurrentInjector(previousInjector)); + let previousInjectorProfilerContext: ɵInjectorProfilerContext; + beforeEach(() => { + const injector = ɵcreateInjector(TestInjector); + previousInjector = ɵsetCurrentInjector(injector); + previousInjectorProfilerContext = ɵsetInjectorProfilerContext({injector, token: null}); + }); + afterEach(() => { + ɵsetCurrentInjector(previousInjector); + previousInjectorProfilerContext = ɵsetInjectorProfilerContext(previousInjectorProfilerContext); + }); it('should compile a minimal injectable declaration that delegates to `ɵfac`', () => { const provider = Minimal.ɵprov as ɵɵInjectableDeclaration; diff --git a/packages/examples/core/di/ts/injector_spec.ts b/packages/examples/core/di/ts/injector_spec.ts index 353cc551fee..d5d875e1613 100644 --- a/packages/examples/core/di/ts/injector_spec.ts +++ b/packages/examples/core/di/ts/injector_spec.ts @@ -6,7 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {inject, InjectFlags, InjectionToken, InjectOptions, Injector, ProviderToken, ɵsetCurrentInjector as setCurrentInjector} from '@angular/core'; +import {inject, InjectFlags, InjectionToken, InjectOptions, Injector, ProviderToken, ɵInjectorProfilerContext, ɵsetCurrentInjector as setCurrentInjector, ɵsetInjectorProfilerContext} from '@angular/core'; + class MockRootScopeInjector implements Injector { constructor(readonly parent: Injector) {} @@ -16,10 +17,13 @@ class MockRootScopeInjector implements Injector { flags: InjectFlags|InjectOptions = InjectFlags.Default): T { if ((token as any).ɵprov && (token as any).ɵprov.providedIn === 'root') { const old = setCurrentInjector(this); + const previousInjectorProfilerContext = + ɵsetInjectorProfilerContext({injector: this, token: null}); try { return (token as any).ɵprov.factory(); } finally { setCurrentInjector(old); + ɵsetInjectorProfilerContext(previousInjectorProfilerContext); } } return this.parent.get(token, defaultValue, flags);