From 728fe69625124cb2c7aad127a2062c00a0f3e49d Mon Sep 17 00:00:00 2001 From: Olivier Combe Date: Thu, 17 Jan 2019 18:48:39 +0100 Subject: [PATCH] feat(ivy): improve stacktrace for `R3Injector` errors (#28207) Improve the stacktrace for `R3Injector` errors by adding the source component (or module) that tried to inject the missing provider, as well as the name of the injector which triggered the error (`R3Injector`). e.g.: ``` R3InjectorError(SomeModule)[car -> SportsCar]: NullInjectorError: No provider for SportsCar! ``` FW-807 #resolve FW-875 #resolve PR Close #28207 --- packages/core/src/di/injector.ts | 37 +++++----- packages/core/src/di/interface/injector.ts | 2 +- packages/core/src/di/r3_injector.ts | 35 ++++++--- packages/core/src/render3/ng_module_ref.ts | 4 +- .../injection/bundle.golden_symbols.json | 62 +++++++++++++++- packages/core/test/di/r3_injector_spec.ts | 71 ++++++++++++++++-- packages/core/test/di/static_injector_spec.ts | 3 +- .../test/linker/ng_module_integration_spec.ts | 14 ++-- packages/core/test/view/provider_spec.ts | 7 +- .../test/browser/bootstrap_spec.ts | 72 ++++++++++--------- 10 files changed, 230 insertions(+), 77 deletions(-) diff --git a/packages/core/src/di/injector.ts b/packages/core/src/di/injector.ts index 3690f12d83..bc85842bb5 100644 --- a/packages/core/src/di/injector.ts +++ b/packages/core/src/di/injector.ts @@ -9,7 +9,6 @@ import {Type} from '../interface/type'; import {getClosureSafeProperty} from '../util/property'; import {stringify} from '../util/stringify'; - import {resolveForwardRef} from './forward_ref'; import {InjectionToken} from './injection_token'; import {inject} from './injector_compatibility'; @@ -42,7 +41,9 @@ export class NullInjector implements Injector { // reason why correctly written application should cause this exception. // TODO(misko): uncomment the next line once `ngDevMode` works with closure. // if(ngDevMode) debugger; - throw new Error(`NullInjectorError: No provider for ${stringify(token)}!`); + const error = new Error(`NullInjectorError: No provider for ${stringify(token)}!`); + error.name = 'NullInjectorError'; + throw error; } return notFoundValue; } @@ -131,7 +132,7 @@ const MULTI_PROVIDER_FN = function(): any[] { export const USE_VALUE = getClosureSafeProperty({provide: String, useValue: getClosureSafeProperty}); const NG_TOKEN_PATH = 'ngTokenPath'; -const NG_TEMP_TOKEN_PATH = 'ngTempTokenPath'; +export const NG_TEMP_TOKEN_PATH = 'ngTempTokenPath'; const enum OptionFlags { Optional = 1 << 0, CheckSelf = 1 << 1, @@ -167,14 +168,7 @@ export class StaticInjector implements Injector { try { return tryResolveToken(token, record, this._records, this.parent, notFoundValue, flags); } catch (e) { - const tokenPath: any[] = e[NG_TEMP_TOKEN_PATH]; - if (token[SOURCE]) { - tokenPath.unshift(token[SOURCE]); - } - e.message = formatError('\n' + e.message, tokenPath, this.source); - e[NG_TOKEN_PATH] = tokenPath; - e[NG_TEMP_TOKEN_PATH] = null; - throw e; + return catchInjectorError(e, token, 'StaticInjectorError', this.source); } } @@ -200,8 +194,6 @@ interface DependencyRecord { options: number; } -type TokenPath = Array; - function resolveProvider(provider: SupportedProvider): Record { const deps = computeDeps(provider); let fn: Function = IDENT; @@ -385,7 +377,20 @@ function computeDeps(provider: StaticProvider): DependencyRecord[] { return deps; } -function formatError(text: string, obj: any, source: string | null = null): string { +export function catchInjectorError( + e: any, token: any, injectorErrorName: string, source: string | null): never { + const tokenPath: any[] = e[NG_TEMP_TOKEN_PATH]; + if (token[SOURCE]) { + tokenPath.unshift(token[SOURCE]); + } + e.message = formatError('\n' + e.message, tokenPath, injectorErrorName, source); + e[NG_TOKEN_PATH] = tokenPath; + e[NG_TEMP_TOKEN_PATH] = null; + throw e; +} + +function formatError( + text: string, obj: any, injectorErrorName: string, source: string | null = null): string { text = text && text.charAt(0) === '\n' && text.charAt(1) == NO_NEW_LINE ? text.substr(2) : text; let context = stringify(obj); if (obj instanceof Array) { @@ -401,9 +406,9 @@ function formatError(text: string, obj: any, source: string | null = null): stri } context = `{${parts.join(', ')}}`; } - return `StaticInjectorError${source ? '(' + source + ')' : ''}[${context}]: ${text.replace(NEW_LINE, '\n ')}`; + return `${injectorErrorName}${source ? '(' + source + ')' : ''}[${context}]: ${text.replace(NEW_LINE, '\n ')}`; } function staticError(text: string, obj: any): Error { - return new Error(formatError(text, obj)); + return new Error(formatError(text, obj, 'StaticInjectorError')); } diff --git a/packages/core/src/di/interface/injector.ts b/packages/core/src/di/interface/injector.ts index 9d761c522d..cf9b3c780b 100644 --- a/packages/core/src/di/interface/injector.ts +++ b/packages/core/src/di/interface/injector.ts @@ -15,8 +15,8 @@ export enum InjectFlags { // TODO(alxhub): make this 'const' when ngc no longer writes exports of it into ngfactory files. + /** Check self and check parent injector if needed */ Default = 0b0000, - /** * Specifies that an injector should retrieve a dependency from any injector until reaching the * host element of the current component. (Only used with Element Injector) diff --git a/packages/core/src/di/r3_injector.ts b/packages/core/src/di/r3_injector.ts index c618d5d5b3..b1415fd970 100644 --- a/packages/core/src/di/r3_injector.ts +++ b/packages/core/src/di/r3_injector.ts @@ -9,10 +9,9 @@ import {OnDestroy} from '../interface/lifecycle_hooks'; import {Type} from '../interface/type'; import {stringify} from '../util/stringify'; - import {resolveForwardRef} from './forward_ref'; import {InjectionToken} from './injection_token'; -import {INJECTOR, Injector, NullInjector, THROW_IF_NOT_FOUND, USE_VALUE} from './injector'; +import {INJECTOR, Injector, NG_TEMP_TOKEN_PATH, NullInjector, USE_VALUE, catchInjectorError} from './injector'; import {inject, injectArgs, setCurrentInjector} from './injector_compatibility'; import {InjectableDef, InjectableType, InjectorType, InjectorTypeWithProviders, getInjectableDef, getInjectorDef} from './interface/defs'; import {InjectFlags} from './interface/injector'; @@ -20,7 +19,6 @@ import {ClassProvider, ConstructorProvider, ExistingProvider, FactoryProvider, S import {APP_ROOT} from './scope'; - /** * Internal type for a single provider in a deep provider array. */ @@ -72,9 +70,9 @@ interface Record { */ export function createInjector( defType: /* InjectorType */ any, parent: Injector | null = null, - additionalProviders: StaticProvider[] | null = null): Injector { + additionalProviders: StaticProvider[] | null = null, name?: string): Injector { parent = parent || getNullInjector(); - return new R3Injector(defType, additionalProviders, parent); + return new R3Injector(defType, additionalProviders, parent, name); } export class R3Injector { @@ -99,6 +97,8 @@ export class R3Injector { */ private readonly isRootInjector: boolean; + readonly source: string|null; + /** * Flag indicating that this injector was previously destroyed. */ @@ -106,8 +106,8 @@ export class R3Injector { private _destroyed = false; constructor( - def: InjectorType, additionalProviders: StaticProvider[]|null, - readonly parent: Injector) { + def: InjectorType, additionalProviders: StaticProvider[]|null, readonly parent: Injector, + source: string|null = null) { // Start off by creating Records for every provider declared in every InjectorType // included transitively in `def`. const dedupStack: InjectorType[] = []; @@ -127,6 +127,9 @@ export class R3Injector { // Eagerly instantiate the InjectorType classes themselves. this.injectorDefTypes.forEach(defType => this.get(defType)); + + // Source name, used for debugging + this.source = source || (def instanceof Array ? null : stringify(def)); } /** @@ -152,7 +155,7 @@ export class R3Injector { } get( - token: Type|InjectionToken, notFoundValue: any = THROW_IF_NOT_FOUND, + token: Type|InjectionToken, notFoundValue: any = Injector.THROW_IF_NOT_FOUND, flags = InjectFlags.Default): T { this.assertNotDestroyed(); // Set the injection context. @@ -182,7 +185,21 @@ export class R3Injector { // Select the next injector based on the Self flag - if self is set, the next injector is // the NullInjector, otherwise it's the parent. const nextInjector = !(flags & InjectFlags.Self) ? this.parent : getNullInjector(); - return nextInjector.get(token, notFoundValue); + return nextInjector.get(token, flags & InjectFlags.Optional ? null : notFoundValue); + } catch (e) { + if (e.name === 'NullInjectorError') { + const path: any[] = e[NG_TEMP_TOKEN_PATH] = e[NG_TEMP_TOKEN_PATH] || []; + path.unshift(stringify(token)); + if (previousInjector) { + // We still have a parent injector, keep throwing + throw e; + } else { + // Format & throw the final error message when we don't have any previous injector + return catchInjectorError(e, token, 'R3InjectorError', this.source); + } + } else { + throw e; + } } finally { // Lastly, clean up the state by restoring the previous injector. setCurrentInjector(previousInjector); diff --git a/packages/core/src/render3/ng_module_ref.ts b/packages/core/src/render3/ng_module_ref.ts index 6442393b70..f13faac4d2 100644 --- a/packages/core/src/render3/ng_module_ref.ts +++ b/packages/core/src/render3/ng_module_ref.ts @@ -16,7 +16,6 @@ import {InternalNgModuleRef, NgModuleFactory as viewEngine_NgModuleFactory, NgMo import {NgModuleDef} from '../metadata/ng_module'; import {assertDefined} from '../util/assert'; import {stringify} from '../util/stringify'; - import {ComponentFactoryResolver} from './component_ref'; import {getNgModuleDef} from './definition'; @@ -52,7 +51,8 @@ export class NgModuleRef extends viewEngine_NgModuleRef implements Interna }, COMPONENT_FACTORY_RESOLVER ]; - this._r3Injector = createInjector(ngModuleType, _parent, additionalProviders) as R3Injector; + this._r3Injector = createInjector( + ngModuleType, _parent, additionalProviders, stringify(ngModuleType)) as R3Injector; this.instance = this.get(ngModuleType); } diff --git a/packages/core/test/bundling/injection/bundle.golden_symbols.json b/packages/core/test/bundling/injection/bundle.golden_symbols.json index 33e1162f8f..d4dabca8c2 100644 --- a/packages/core/test/bundling/injection/bundle.golden_symbols.json +++ b/packages/core/test/bundling/injection/bundle.golden_symbols.json @@ -5,12 +5,21 @@ { "name": "CIRCULAR" }, + { + "name": "CIRCULAR" + }, + { + "name": "EMPTY" + }, { "name": "EMPTY_ARRAY" }, { "name": "EmptyErrorImpl" }, + { + "name": "IDENT" + }, { "name": "INJECTOR" }, @@ -23,15 +32,36 @@ { "name": "InjectionToken" }, + { + "name": "Injector" + }, + { + "name": "MULTI_PROVIDER_FN" + }, + { + "name": "NEW_LINE" + }, { "name": "NG_INJECTABLE_DEF" }, { "name": "NG_INJECTOR_DEF" }, + { + "name": "NG_TEMP_TOKEN_PATH" + }, + { + "name": "NG_TOKEN_PATH" + }, { "name": "NOT_YET" }, + { + "name": "NO_NEW_LINE" + }, + { + "name": "NULL_INJECTOR" + }, { "name": "NULL_INJECTOR" }, @@ -50,6 +80,9 @@ { "name": "R3Injector" }, + { + "name": "SOURCE" + }, { "name": "ScopedService" }, @@ -60,7 +93,7 @@ "name": "SkipSelf" }, { - "name": "THROW_IF_NOT_FOUND" + "name": "StaticInjector" }, { "name": "USE_VALUE" @@ -83,6 +116,12 @@ { "name": "_currentInjector" }, + { + "name": "catchInjectorError" + }, + { + "name": "computeDeps" + }, { "name": "couldBeInjectableType" }, @@ -98,6 +137,9 @@ { "name": "defineInjector" }, + { + "name": "formatError" + }, { "name": "forwardRef" }, @@ -155,19 +197,37 @@ { "name": "makeRecord" }, + { + "name": "multiProviderMixError" + }, { "name": "providerToFactory" }, { "name": "providerToRecord" }, + { + "name": "recursivelyProcessProviders" + }, { "name": "resolveForwardRef" }, + { + "name": "resolveProvider" + }, + { + "name": "resolveToken" + }, { "name": "setCurrentInjector" }, + { + "name": "staticError" + }, { "name": "stringify" + }, + { + "name": "tryResolveToken" } ] \ No newline at end of file diff --git a/packages/core/test/di/r3_injector_spec.ts b/packages/core/test/di/r3_injector_spec.ts index ad9eebad58..cf7328801d 100644 --- a/packages/core/test/di/r3_injector_spec.ts +++ b/packages/core/test/di/r3_injector_spec.ts @@ -6,11 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ -import {InjectionToken} from '../../src/di/injection_token'; -import {INJECTOR, Injector} from '../../src/di/injector'; -import {inject} from '../../src/di/injector_compatibility'; -import {defineInjectable, defineInjector} from '../../src/di/interface/defs'; -import {R3Injector, createInjector} from '../../src/di/r3_injector'; +import {INJECTOR, InjectFlags, InjectionToken, Injector, Optional, defineInjectable, defineInjector, inject} from '@angular/core'; +import {R3Injector, createInjector} from '@angular/core/src/di/r3_injector'; +import {expect} from '@angular/platform-browser/testing/src/matchers'; describe('InjectorDef-based createInjector()', () => { class CircularA { @@ -34,6 +32,13 @@ describe('InjectorDef-based createInjector()', () => { }); } + class OptionalService { + static ngInjectableDef = defineInjectable({ + providedIn: null, + factory: () => new OptionalService(), + }); + } + class StaticService { constructor(readonly dep: Service) {} } @@ -56,6 +61,24 @@ describe('InjectorDef-based createInjector()', () => { }); } + class ServiceWithOptionalDep { + constructor(@Optional() readonly service: OptionalService|null) {} + + static ngInjectableDef = defineInjectable({ + providedIn: null, + factory: () => new ServiceWithOptionalDep(inject(OptionalService, InjectFlags.Optional)), + }); + } + + class ServiceWithMissingDep { + constructor(readonly service: Service) {} + + static ngInjectableDef = defineInjectable({ + providedIn: null, + factory: () => new ServiceWithMissingDep(inject(Service)), + }); + } + class ServiceWithMultiDep { constructor(readonly locale: string[]) {} @@ -135,6 +158,7 @@ describe('InjectorDef-based createInjector()', () => { imports: [IntermediateModule], providers: [ ServiceWithDep, + ServiceWithOptionalDep, ServiceWithMultiDep, {provide: LOCALE, multi: true, useValue: 'en'}, {provide: LOCALE, multi: true, useValue: 'es'}, @@ -158,6 +182,14 @@ describe('InjectorDef-based createInjector()', () => { }); } + class ModuleWithMissingDep { + static ngInjectorDef = defineInjector({ + factory: () => new ModuleWithMissingDep(), + imports: undefined, + providers: [ServiceWithMissingDep], + }); + } + class NotAModule {} class ImportsNotAModule { @@ -198,12 +230,41 @@ describe('InjectorDef-based createInjector()', () => { it('returns the default value if a provider isn\'t present', () => { expect(injector.get(ServiceTwo, null)).toBeNull(); }); + it('should throw when no provider defined', () => { + expect(() => injector.get(ServiceTwo)) + .toThrowError( + `R3InjectorError(Module)[ServiceTwo]: \n` + + ` NullInjectorError: No provider for ServiceTwo!`); + }); + + it('should throw without the module name when no module', () => { + const injector = createInjector([ServiceTwo]); + expect(() => injector.get(ServiceTwo)) + .toThrowError( + `R3InjectorError[ServiceTwo]: \n` + + ` NullInjectorError: No provider for ServiceTwo!`); + }); + + it('should throw with the full path when no provider', () => { + const injector = createInjector(ModuleWithMissingDep); + expect(() => injector.get(ServiceWithMissingDep)) + .toThrowError( + `R3InjectorError(ModuleWithMissingDep)[ServiceWithMissingDep -> Service]: \n` + + ` NullInjectorError: No provider for Service!`); + }); + it('injects a service with dependencies', () => { const instance = injector.get(ServiceWithDep); expect(instance instanceof ServiceWithDep); expect(instance.service).toBe(injector.get(Service)); }); + it('injects a service with optional dependencies', () => { + const instance = injector.get(ServiceWithOptionalDep); + expect(instance instanceof ServiceWithOptionalDep); + expect(instance.service).toBe(null); + }); + it('injects a service with dependencies on multi-providers', () => { const instance = injector.get(ServiceWithMultiDep); expect(instance instanceof ServiceWithMultiDep); diff --git a/packages/core/test/di/static_injector_spec.ts b/packages/core/test/di/static_injector_spec.ts index 1ae7386d7b..778cc96ecf 100644 --- a/packages/core/test/di/static_injector_spec.ts +++ b/packages/core/test/di/static_injector_spec.ts @@ -6,8 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Inject, InjectionToken, Injector, Optional, ReflectiveKey, Self, SkipSelf, forwardRef} from '@angular/core'; -import {getOriginalError} from '@angular/core/src/errors'; +import {Inject, InjectionToken, Injector, Optional, Self, SkipSelf, forwardRef} from '@angular/core'; import {expect} from '@angular/platform-browser/testing/src/matchers'; import {stringify} from '../../src/util/stringify'; diff --git a/packages/core/test/linker/ng_module_integration_spec.ts b/packages/core/test/linker/ng_module_integration_spec.ts index c82c0ae413..58a7afca00 100644 --- a/packages/core/test/linker/ng_module_integration_spec.ts +++ b/packages/core/test/linker/ng_module_integration_spec.ts @@ -734,8 +734,11 @@ function declareTests(config?: {useJit: boolean}) { it('should throw when the aliased provider does not exist', () => { const injector = createInjector([{provide: 'car', useExisting: SportsCar}]); - const e = `NullInjectorError: No provider for ${stringify(SportsCar)}!`; - expect(() => injector.get('car')).toThrowError(e); + let errorMsg = `NullInjectorError: No provider for ${stringify(SportsCar)}!`; + if (ivyEnabled) { + errorMsg = `R3InjectorError(SomeModule)[car -> SportsCar]: \n ` + errorMsg; + } + expect(() => injector.get('car')).toThrowError(errorMsg); }); it('should handle forwardRef in useExisting', () => { @@ -930,8 +933,11 @@ function declareTests(config?: {useJit: boolean}) { it('should throw when no provider defined', () => { const injector = createInjector([]); - expect(() => injector.get('NonExisting')) - .toThrowError('NullInjectorError: No provider for NonExisting!'); + let errorMsg = 'NullInjectorError: No provider for NonExisting!'; + if (ivyEnabled) { + errorMsg = `R3InjectorError(SomeModule)[NonExisting]: \n ` + errorMsg; + } + expect(() => injector.get('NonExisting')).toThrowError(errorMsg); }); it('should throw when trying to instantiate a cyclic dependency', () => { diff --git a/packages/core/test/view/provider_spec.ts b/packages/core/test/view/provider_spec.ts index 3d3bcd88c7..098d87907b 100644 --- a/packages/core/test/view/provider_spec.ts +++ b/packages/core/test/view/provider_spec.ts @@ -11,6 +11,7 @@ import {getDebugContext} from '@angular/core/src/errors'; import {ArgumentType, DepFlags, NodeFlags, Services, anchorDef, asElementData, directiveDef, elementDef, providerDef, textDef} from '@angular/core/src/view/index'; import {TestBed, withModule} from '@angular/core/testing'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; +import {ivyEnabled} from '@angular/private/testing'; import {ARG_TYPE_VALUES, checkNodeInlineOrDynamic, createRootView, createAndGetRootNodes, compViewDef, compViewDefFactory} from './helper'; @@ -147,7 +148,7 @@ import {ARG_TYPE_VALUES, checkNodeInlineOrDynamic, createRootView, createAndGetR expect(() => createAndGetRootNodes(compViewDef(rootElNodes))) .toThrowError( - 'StaticInjectorError(DynamicTestModule)[SomeService -> Dep]: \n' + + `${ivyEnabled ? 'R3InjectorError' : 'StaticInjectorError'}(DynamicTestModule)[SomeService -> Dep]: \n` + ' StaticInjectorError(Platform: core)[SomeService -> Dep]: \n' + ' NullInjectorError: No provider for Dep!'); @@ -161,7 +162,7 @@ import {ARG_TYPE_VALUES, checkNodeInlineOrDynamic, createRootView, createAndGetR expect(() => createAndGetRootNodes(compViewDef(nonRootElNodes))) .toThrowError( - 'StaticInjectorError(DynamicTestModule)[SomeService -> Dep]: \n' + + `${ivyEnabled ? 'R3InjectorError' : 'StaticInjectorError'}(DynamicTestModule)[SomeService -> Dep]: \n` + ' StaticInjectorError(Platform: core)[SomeService -> Dep]: \n' + ' NullInjectorError: No provider for Dep!'); }); @@ -186,7 +187,7 @@ import {ARG_TYPE_VALUES, checkNodeInlineOrDynamic, createRootView, createAndGetR directiveDef(1, NodeFlags.None, null, 0, SomeService, ['nonExistingDep']) ]))) .toThrowError( - 'StaticInjectorError(DynamicTestModule)[nonExistingDep]: \n' + + `${ivyEnabled ? 'R3InjectorError' : 'StaticInjectorError'}(DynamicTestModule)[nonExistingDep]: \n` + ' StaticInjectorError(Platform: core)[nonExistingDep]: \n' + ' NullInjectorError: No provider for nonExistingDep!'); }); diff --git a/packages/platform-browser/test/browser/bootstrap_spec.ts b/packages/platform-browser/test/browser/bootstrap_spec.ts index 3d2bf9c0ef..94d5c3fd3f 100644 --- a/packages/platform-browser/test/browser/bootstrap_spec.ts +++ b/packages/platform-browser/test/browser/bootstrap_spec.ts @@ -18,7 +18,7 @@ import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {DOCUMENT} from '@angular/platform-browser/src/dom/dom_tokens'; import {expect} from '@angular/platform-browser/testing/src/matchers'; -import {fixmeIvy, modifiedInIvy, onlyInIvy} from '@angular/private/testing'; +import {ivyEnabled, modifiedInIvy, onlyInIvy} from '@angular/private/testing'; @Component({selector: 'non-existent', template: ''}) class NonExistentComp { @@ -205,44 +205,48 @@ function bootstrap( }); })); - // TODO(misko): can't use `fixmeIvy.it` because the `it` is somehow special here. - fixmeIvy('FW-875: The source of the error is missing in the `StaticInjectorError` message') - .isEnabled && - it('should throw if no provider', - inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { - const logger = new MockConsole(); - const errorHandler = new ErrorHandler(); - (errorHandler as any)._console = logger as any; + it('should throw if no provider', inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { + const logger = new MockConsole(); + const errorHandler = new ErrorHandler(); + (errorHandler as any)._console = logger as any; - class IDontExist {} + class IDontExist {} - @Component({selector: 'cmp', template: 'Cmp'}) - class CustomCmp { - constructor(iDontExist: IDontExist) {} - } + @Component({selector: 'cmp', template: 'Cmp'}) + class CustomCmp { + constructor(iDontExist: IDontExist) {} + } - @Component({ - selector: 'hello-app', - template: '', - }) - class RootCmp { - } + @Component({ + selector: 'hello-app', + template: '', + }) + class RootCmp { + } - @NgModule({declarations: [CustomCmp], exports: [CustomCmp]}) - class CustomModule { - } + @NgModule({declarations: [CustomCmp], exports: [CustomCmp]}) + class CustomModule { + } - bootstrap(RootCmp, [{provide: ErrorHandler, useValue: errorHandler}], [], [ - CustomModule - ]).then(null, (e: Error) => { - expect(e.message).toContain( - 'StaticInjectorError(TestModule)[CustomCmp -> IDontExist]: \n' + - ' StaticInjectorError(Platform: core)[CustomCmp -> IDontExist]: \n' + - ' NullInjectorError: No provider for IDontExist!'); - async.done(); - return null; - }); - })); + bootstrap(RootCmp, [{provide: ErrorHandler, useValue: errorHandler}], [], [ + CustomModule + ]).then(null, (e: Error) => { + let errorMsg: string; + if (ivyEnabled) { + errorMsg = `R3InjectorError(TestModule)[IDontExist]: \n` + + ' StaticInjectorError(TestModule)[IDontExist]: \n' + + ' StaticInjectorError(Platform: core)[IDontExist]: \n' + + ' NullInjectorError: No provider for IDontExist!'; + } else { + errorMsg = `StaticInjectorError(TestModule)[CustomCmp -> IDontExist]: \n` + + ' StaticInjectorError(Platform: core)[CustomCmp -> IDontExist]: \n' + + ' NullInjectorError: No provider for IDontExist!'; + } + expect(e.message).toContain(errorMsg); + async.done(); + return null; + }); + })); if (getDOM().supportsDOMEvents()) { it('should forward the error to promise when bootstrap fails',