refactor(core): make Testability compatible with tree-shaking (#45657)

This commit refactors the `Testability`-related logic to extract the necessary providers into a separate array, so that it can later become it's own NgModule (or exposed as an array of providers) and be excluded from the new APIs by default.

PR Close #45657
This commit is contained in:
Andrew Kushnir
2022-04-15 18:29:11 -07:00
parent d380bb49b7
commit c9b40b5100
16 changed files with 149 additions and 67 deletions
+4 -9
View File
@@ -40,7 +40,7 @@ import {setJitOptions} from './render3/jit/jit_options';
import {isStandalone} from './render3/jit/module';
import {createEnvironmentInjector, NgModuleFactory as R3NgModuleFactory} from './render3/ng_module_ref';
import {publishDefaultGlobalUtils as _publishDefaultGlobalUtils} from './render3/util/global_utils';
import {Testability, TestabilityRegistry} from './testability/testability';
import {TESTABILITY} from './testability/testability';
import {isPromise} from './util/lang';
import {scheduleMicroTask} from './util/microtask';
import {stringify} from './util/stringify';
@@ -944,18 +944,13 @@ export class ApplicationRef {
const selectorOrNode = rootSelectorOrNode || componentFactory.selector;
const compRef = componentFactory.create(Injector.NULL, [], selectorOrNode, ngModule);
const nativeElement = compRef.location.nativeElement;
const testability = compRef.injector.get(Testability, null);
const testabilityRegistry = testability && compRef.injector.get(TestabilityRegistry);
if (testability && testabilityRegistry) {
testabilityRegistry.registerApplication(nativeElement, testability);
}
const testability = compRef.injector.get(TESTABILITY, null);
testability?.registerApplication(nativeElement);
compRef.onDestroy(() => {
this.detachView(compRef.hostView);
remove(this.components, compRef);
if (testabilityRegistry) {
testabilityRegistry.unregisterApplication(nativeElement);
}
testability?.unregisterApplication(nativeElement);
});
this._loadComponent(compRef);
+1
View File
@@ -24,6 +24,7 @@ export {ReflectionCapabilities as ɵReflectionCapabilities} from './reflection/r
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';
export {TESTABILITY as ɵTESTABILITY, TESTABILITY_GETTER as ɵTESTABILITY_GETTER} from './testability/testability';
export {coerceToBoolean as ɵcoerceToBoolean} from './util/coercion';
export {devModeEqual as ɵdevModeEqual} from './util/comparison';
export {makeDecorator as ɵmakeDecorator} from './util/decorators';
+47 -16
View File
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Injectable} from '../di';
import {Inject, Injectable, InjectionToken} from '../di';
import {scheduleMicroTask} from '../util/microtask';
import {NgZone} from '../zone/ng_zone';
@@ -48,6 +48,22 @@ interface WaitCallback {
updateCb?: UpdateCallback;
}
/**
* Internal injection token that can used to access an instance of a Testability class.
*
* This token acts as a bridge between the core bootstrap code and the `Testability` class. This is
* needed to ensure that there are no direct references to the `Testability` class, so it can be
* tree-shaken away (if not referenced). For the environments/setups when the `Testability` class
* should be available, this token is used to add a provider that references the `Testability`
* class. Otherwise, only this token is retained in a bundle, but the `Testability` class is not.
*/
export const TESTABILITY = new InjectionToken<Testability>('');
/**
* Internal injection token to retrieve Testability getter class instance.
*/
export const TESTABILITY_GETTER = new InjectionToken<GetTestability>('');
/**
* The Testability service provides testing hooks that can be accessed from
* the browser. Each bootstrapped Angular application on the page will have
@@ -69,7 +85,15 @@ export class Testability implements PublicTestability {
private taskTrackingZone: {macroTasks: Task[]}|null = null;
constructor(private _ngZone: NgZone) {
constructor(
private _ngZone: NgZone, private registry: TestabilityRegistry,
@Inject(TESTABILITY_GETTER) testabilityGetter: GetTestability) {
// If there was no Testability logic registered in the global scope
// before, register the current testability getter as a global one.
if (!_testabilityGetter) {
setTestabilityGetter(testabilityGetter);
testabilityGetter.addToWindow(registry);
}
this._watchAngularEvents();
_ngZone.run(() => {
this.taskTrackingZone =
@@ -213,6 +237,25 @@ export class Testability implements PublicTestability {
getPendingRequestCount(): number {
return this._pendingCount;
}
/**
* Registers an application with a testability hook so that it can be tracked.
* @param token token of application, root element
*
* @internal
*/
registerApplication(token: any) {
this.registry.registerApplication(token, this);
}
/**
* Unregisters an application.
* @param token token of application, root element
*
* @internal
*/
unregisterApplication(token: any) {
this.registry.unregisterApplication(token);
}
/**
* Find providers by name
@@ -235,10 +278,6 @@ export class TestabilityRegistry {
/** @internal */
_applications = new Map<any, Testability>();
constructor() {
_testabilityGetter.addToWindow(this);
}
/**
* Registers an application with a testability hook so that it can be tracked
* @param token token of application, root element
@@ -292,7 +331,7 @@ export class TestabilityRegistry {
* current node
*/
findTestabilityInTree(elem: Node, findInAncestors: boolean = true): Testability|null {
return _testabilityGetter.findTestabilityInTree(this, elem, findInAncestors);
return _testabilityGetter?.findTestabilityInTree(this, elem, findInAncestors) ?? null;
}
}
@@ -308,14 +347,6 @@ export interface GetTestability {
Testability|null;
}
class _NoopGetTestability implements GetTestability {
addToWindow(registry: TestabilityRegistry): void {}
findTestabilityInTree(registry: TestabilityRegistry, elem: any, findInAncestors: boolean):
Testability|null {
return null;
}
}
/**
* Set the {@link GetTestability} implementation used by the Angular testing framework.
* @publicApi
@@ -324,4 +355,4 @@ export function setTestabilityGetter(getter: GetTestability): void {
_testabilityGetter = getter;
}
let _testabilityGetter: GetTestability = new _NoopGetTestability();
let _testabilityGetter: GetTestability|undefined;
@@ -95,9 +95,6 @@
{
"name": "BrowserDomAdapter"
},
{
"name": "BrowserGetTestability"
},
{
"name": "BrowserModule"
},
@@ -446,6 +443,15 @@
{
"name": "Subscription"
},
{
"name": "TESTABILITY"
},
{
"name": "TESTABILITY_GETTER"
},
{
"name": "TESTABILITY_PROVIDERS"
},
{
"name": "THROW_IF_NOT_FOUND"
},
@@ -47,9 +47,6 @@
{
"name": "BrowserDomAdapter"
},
{
"name": "BrowserGetTestability"
},
{
"name": "BrowserModule"
},
@@ -458,6 +455,15 @@
{
"name": "Subscription"
},
{
"name": "TESTABILITY"
},
{
"name": "TESTABILITY_GETTER"
},
{
"name": "TESTABILITY_PROVIDERS"
},
{
"name": "THROW_IF_NOT_FOUND"
},
@@ -50,9 +50,6 @@
{
"name": "BrowserDomAdapter"
},
{
"name": "BrowserGetTestability"
},
{
"name": "BrowserModule"
},
@@ -449,6 +446,15 @@
{
"name": "Subscription"
},
{
"name": "TESTABILITY"
},
{
"name": "TESTABILITY_GETTER"
},
{
"name": "TESTABILITY_PROVIDERS"
},
{
"name": "THROW_IF_NOT_FOUND"
},
@@ -53,9 +53,6 @@
{
"name": "BrowserDomAdapter"
},
{
"name": "BrowserGetTestability"
},
{
"name": "BrowserModule"
},
@@ -641,6 +638,15 @@
{
"name": "SwitchMapSubscriber"
},
{
"name": "TESTABILITY"
},
{
"name": "TESTABILITY_GETTER"
},
{
"name": "TESTABILITY_PROVIDERS"
},
{
"name": "THROW_IF_NOT_FOUND"
},
@@ -23,9 +23,6 @@
{
"name": "BrowserDomAdapter"
},
{
"name": "BrowserGetTestability"
},
{
"name": "BrowserXhr"
},
@@ -71,6 +68,9 @@
{
"name": "DefaultDomRenderer2"
},
{
"name": "DomEventsPlugin"
},
{
"name": "DomRendererFactory2"
},
@@ -137,6 +137,9 @@
{
"name": "Injector"
},
{
"name": "KeyEventsPlugin"
},
{
"name": "LOCALE_ID2"
},
@@ -290,6 +293,15 @@
{
"name": "Subscription"
},
{
"name": "TESTABILITY"
},
{
"name": "TESTABILITY_GETTER"
},
{
"name": "TESTABILITY_PROVIDERS"
},
{
"name": "THROW_IF_NOT_FOUND"
},
@@ -8,7 +8,7 @@
import {EventEmitter} from '@angular/core';
import {Injectable} from '@angular/core/src/di';
import {PendingMacrotask, Testability, TestabilityRegistry} from '@angular/core/src/testability/testability';
import {GetTestability, PendingMacrotask, Testability, TestabilityRegistry} from '@angular/core/src/testability/testability';
import {NgZone} from '@angular/core/src/zone/ng_zone';
import {fakeAsync, flush, tick, waitForAsync} from '@angular/core/testing';
@@ -23,6 +23,14 @@ function microTask(fn: Function): void {
});
}
class NoopGetTestability implements GetTestability {
addToWindow(registry: TestabilityRegistry): void {}
findTestabilityInTree(registry: TestabilityRegistry, elem: any, findInAncestors: boolean):
Testability|null {
return null;
}
}
@Injectable()
class MockNgZone extends NgZone {
/** @internal */
@@ -56,7 +64,7 @@ class MockNgZone extends NgZone {
beforeEach(waitForAsync(() => {
ngZone = new MockNgZone();
testability = new Testability(ngZone);
testability = new Testability(ngZone, new TestabilityRegistry(), new NoopGetTestability());
execute = jasmine.createSpy('execute');
execute2 = jasmine.createSpy('execute');
updateCallback = jasmine.createSpy('execute');
@@ -366,9 +374,9 @@ class MockNgZone extends NgZone {
beforeEach(waitForAsync(() => {
ngZone = new MockNgZone();
testability1 = new Testability(ngZone);
testability2 = new Testability(ngZone);
registry = new TestabilityRegistry();
testability1 = new Testability(ngZone, registry, new NoopGetTestability());
testability2 = new Testability(ngZone, registry, new NoopGetTestability());
}));
describe('unregister testability', () => {
it('should remove the testability when unregistering an existing testability', () => {
+24 -4
View File
@@ -7,7 +7,7 @@
*/
import {CommonModule, DOCUMENT, XhrFactory, ɵPLATFORM_BROWSER_ID as PLATFORM_BROWSER_ID} from '@angular/common';
import {APP_ID, ApplicationModule, ApplicationRef, createPlatformFactory, ErrorHandler, ImportedNgModuleProviders, Inject, InjectionToken, ModuleWithProviders, NgModule, NgZone, Optional, PLATFORM_ID, PLATFORM_INITIALIZER, platformCore, PlatformRef, Provider, RendererFactory2, SkipSelf, StaticProvider, Testability, Type, ɵINJECTOR_SCOPE as INJECTOR_SCOPE, ɵinternalBootstrapApplication as internalBootstrapApplication, ɵsetDocument} from '@angular/core';
import {APP_ID, ApplicationModule, ApplicationRef, createPlatformFactory, ErrorHandler, ImportedNgModuleProviders, Inject, InjectionToken, ModuleWithProviders, NgModule, NgZone, Optional, PLATFORM_ID, PLATFORM_INITIALIZER, platformCore, PlatformRef, Provider, RendererFactory2, SkipSelf, StaticProvider, Testability, TestabilityRegistry, Type, ɵINJECTOR_SCOPE as INJECTOR_SCOPE, ɵinternalBootstrapApplication as internalBootstrapApplication, ɵsetDocument, ɵTESTABILITY as TESTABILITY, ɵTESTABILITY_GETTER as TESTABILITY_GETTER} from '@angular/core';
import {BrowserDomAdapter} from './browser/browser_adapter';
import {SERVER_TRANSITION_PROVIDERS, TRANSITION_ID} from './browser/server-transition';
@@ -62,6 +62,7 @@ export function bootstrapApplication(
rootComponent,
appProviders: [
...BROWSER_MODULE_PROVIDERS,
...TESTABILITY_PROVIDERS,
...(options?.providers ?? []),
],
platformProviders: INTERNAL_BROWSER_PLATFORM_PROVIDERS,
@@ -70,7 +71,6 @@ export function bootstrapApplication(
export function initDomAdapter() {
BrowserDomAdapter.makeCurrent();
BrowserGetTestability.init();
}
export function errorHandler(): ErrorHandler {
@@ -107,6 +107,24 @@ export const platformBrowser: (extraProviders?: StaticProvider[]) => PlatformRef
const BROWSER_MODULE_PROVIDERS_MARKER =
new InjectionToken(NG_DEV_MODE ? 'BrowserModule Providers Marker' : '');
export const TESTABILITY_PROVIDERS = [
{
provide: TESTABILITY_GETTER,
useClass: BrowserGetTestability,
deps: [],
},
{
provide: TESTABILITY,
useClass: Testability,
deps: [NgZone, TestabilityRegistry, TESTABILITY_GETTER]
},
{
provide: Testability, // Also provide as `Testability` for backwards-compatibility.
useClass: Testability,
deps: [NgZone, TestabilityRegistry, TESTABILITY_GETTER]
}
];
export const BROWSER_MODULE_PROVIDERS: StaticProvider[] = [
{provide: INJECTOR_SCOPE, useValue: 'root'},
{provide: ErrorHandler, useFactory: errorHandler, deps: []}, {
@@ -123,7 +141,6 @@ export const BROWSER_MODULE_PROVIDERS: StaticProvider[] = [
{provide: RendererFactory2, useExisting: DomRendererFactory2},
{provide: SharedStylesHost, useExisting: DomSharedStylesHost},
{provide: DomSharedStylesHost, useClass: DomSharedStylesHost, deps: [DOCUMENT]},
{provide: Testability, useClass: Testability, deps: [NgZone]},
{provide: EventManager, useClass: EventManager, deps: [EVENT_MANAGER_PLUGINS, NgZone]},
{provide: XhrFactory, useClass: BrowserXhr, deps: []},
NG_DEV_MODE ? {provide: BROWSER_MODULE_PROVIDERS_MARKER, useValue: true} : []
@@ -139,7 +156,10 @@ export const BROWSER_MODULE_PROVIDERS: StaticProvider[] = [
* @publicApi
*/
@NgModule({
providers: BROWSER_MODULE_PROVIDERS,
providers: [
...BROWSER_MODULE_PROVIDERS, //
...TESTABILITY_PROVIDERS
],
exports: [CommonModule, ApplicationModule],
})
export class BrowserModule {
@@ -7,13 +7,9 @@
*/
import {ɵgetDOM as getDOM} from '@angular/common';
import {GetTestability, setTestabilityGetter, Testability, TestabilityRegistry, ɵglobal as global} from '@angular/core';
import {GetTestability, Testability, TestabilityRegistry, ɵglobal as global} from '@angular/core';
export class BrowserGetTestability implements GetTestability {
static init() {
setTestabilityGetter(new BrowserGetTestability());
}
addToWindow(registry: TestabilityRegistry): void {
global['getAngularTestability'] = (elem: any, findInAncestors: boolean = true) => {
const testability = registry.findTestabilityInTree(elem, findInAncestors);
@@ -7,11 +7,10 @@
*/
import {DOCUMENT, isPlatformBrowser, ɵgetDOM as getDOM} from '@angular/common';
import {APP_INITIALIZER, Compiler, Component, createPlatformFactory, CUSTOM_ELEMENTS_SCHEMA, Directive, ErrorHandler, Inject, InjectionToken, Injector, Input, LOCALE_ID, NgModule, OnDestroy, Pipe, PLATFORM_ID, PLATFORM_INITIALIZER, Provider, Sanitizer, StaticProvider, Type, VERSION} from '@angular/core';
import {APP_INITIALIZER, Compiler, Component, createPlatformFactory, CUSTOM_ELEMENTS_SCHEMA, Directive, ErrorHandler, Inject, InjectionToken, Injector, Input, LOCALE_ID, NgModule, OnDestroy, Pipe, PLATFORM_ID, PLATFORM_INITIALIZER, Provider, Sanitizer, StaticProvider, Testability, TestabilityRegistry, Type, VERSION} from '@angular/core';
import {ApplicationRef, destroyPlatform} from '@angular/core/src/application_ref';
import {Console} from '@angular/core/src/console';
import {ComponentRef} from '@angular/core/src/linker/component_factory';
import {Testability, TestabilityRegistry} from '@angular/core/src/testability/testability';
import {inject, TestBed} from '@angular/core/testing';
import {Log} from '@angular/core/testing/src/testing_internal';
import {BrowserModule} from '@angular/platform-browser';
+3 -6
View File
@@ -9,7 +9,7 @@
import {ɵAnimationEngine} from '@angular/animations/browser';
import {DOCUMENT, PlatformLocation, ViewportScroller, ɵgetDOM as getDOM, ɵNullViewportScroller as NullViewportScroller, ɵPLATFORM_SERVER_ID as PLATFORM_SERVER_ID} from '@angular/common';
import {HttpClientModule} from '@angular/common/http';
import {createPlatformFactory, Injector, NgModule, NgZone, Optional, PLATFORM_ID, PLATFORM_INITIALIZER, platformCore, PlatformRef, Provider, RendererFactory2, StaticProvider, Testability, ɵALLOW_MULTIPLE_PLATFORMS as ALLOW_MULTIPLE_PLATFORMS, ɵsetDocument} from '@angular/core';
import {createPlatformFactory, Injector, NgModule, NgZone, Optional, PLATFORM_ID, PLATFORM_INITIALIZER, platformCore, PlatformRef, Provider, RendererFactory2, StaticProvider, Testability, ɵALLOW_MULTIPLE_PLATFORMS as ALLOW_MULTIPLE_PLATFORMS, ɵsetDocument, ɵTESTABILITY as TESTABILITY} from '@angular/core';
import {BrowserModule, EVENT_MANAGER_PLUGINS, ɵSharedStylesHost as SharedStylesHost} from '@angular/platform-browser';
import {ɵplatformCoreDynamic as platformCoreDynamic} from '@angular/platform-browser-dynamic';
import {NoopAnimationsModule, ɵAnimationRendererFactory} from '@angular/platform-browser/animations';
@@ -23,10 +23,6 @@ import {ServerRendererFactory2} from './server_renderer';
import {ServerStylesHost} from './styles_host';
import {INITIAL_CONFIG, PlatformConfig} from './tokens';
function notSupported(feature: string): Error {
throw new Error(`platform-server does not support '${feature}'.`);
}
export const INTERNAL_SERVER_PLATFORM_PROVIDERS: StaticProvider[] = [
{provide: DOCUMENT, useFactory: _document, deps: [Injector]},
{provide: PLATFORM_ID, useValue: PLATFORM_SERVER_ID},
@@ -74,7 +70,8 @@ export const SERVER_RENDER_PROVIDERS: Provider[] = [
providers: [
SERVER_RENDER_PROVIDERS,
SERVER_HTTP_PROVIDERS,
{provide: Testability, useValue: null},
{provide: Testability, useValue: null}, // Keep for backwards-compatibility.
{provide: TESTABILITY, useValue: null},
{provide: ViewportScroller, useClass: NullViewportScroller},
],
})