From b1dffa4abe8321a47d79b2ea29ee32a81acfe031 Mon Sep 17 00:00:00 2001 From: Joey Perrott Date: Fri, 19 Apr 2024 16:13:59 +0000 Subject: [PATCH] refactor: migrate platform-* to prettier formatting (#55423) Migrate formatting to prettier for platform-* from clang-format PR Close #55423 --- .ng-dev/format.mts | 6 + .../src/compiler_factory.ts | 31 +- .../src/platform-browser-dynamic.ts | 5 +- .../src/platform_core_dynamic.ts | 7 +- .../src/platform_providers.ts | 3 +- .../resource_loader/resource_loader_impl.ts | 5 +- .../test/metadata_overrider_spec.ts | 118 +- .../resource_loader_impl_spec.ts | 4 +- .../test/testing_public_browser_spec.ts | 90 +- .../src/platform_core_dynamic_testing.ts | 6 +- .../testing/src/testing.ts | 13 +- .../async/src/async_animation_renderer.ts | 120 +- .../animations/async/src/providers.ts | 14 +- .../async/test/animation_renderer_spec.ts | 655 +-- .../animations/src/animations.ts | 8 +- .../platform-browser/animations/src/module.ts | 20 +- .../animations/src/providers.ts | 46 +- .../test/animation_renderer_spec.ts | 879 ++-- .../test/noop_animations_module_spec.ts | 126 +- packages/platform-browser/src/browser.ts | 98 +- .../src/browser/browser_adapter.ts | 15 +- .../src/browser/generic_browser_adapter.ts | 2 - packages/platform-browser/src/browser/meta.ts | 28 +- .../src/browser/testability.ts | 24 +- .../src/browser/tools/common_tools.ts | 7 +- .../src/browser/tools/tools.ts | 2 + packages/platform-browser/src/dom/debug/by.ts | 16 +- .../platform-browser/src/dom/dom_renderer.ts | 207 +- .../src/dom/events/event_manager.ts | 26 +- .../src/dom/events/hammer_gestures.ts | 96 +- .../src/dom/events/key_events.ts | 25 +- .../src/dom/shared_styles_host.ts | 38 +- packages/platform-browser/src/dom/util.ts | 2 +- packages/platform-browser/src/errors.ts | 2 +- packages/platform-browser/src/hydration.ts | 95 +- .../platform-browser/src/platform-browser.ts | 43 +- .../platform-browser/src/private_export.ts | 5 +- .../src/security/dom_sanitization_service.ts | 45 +- .../test/browser/bootstrap_spec.ts | 422 +- .../test/browser/bootstrap_standalone_spec.ts | 415 +- .../test/browser/meta_spec.ts | 21 +- .../test/browser/tools/tools_spec.ts | 20 +- .../test/dom/dom_renderer_spec.ts | 187 +- .../test/dom/events/event_manager_spec.ts | 904 ++-- .../test/dom/events/hammer_gestures_spec.ts | 111 +- .../test/dom/events/key_events_spec.ts | 686 +-- .../test/dom/shadow_dom_spec.ts | 28 +- .../platform-browser/test/hydration_spec.ts | 55 +- .../test/testing_public_spec.ts | 825 ++-- .../platform-browser/testing/src/browser.ts | 27 +- .../testing/src/browser_util.ts | 9 +- .../platform-browser/testing/src/matchers.ts | 71 +- .../platform-server/src/domino_adapter.ts | 2 +- packages/platform-server/src/http.ts | 13 +- packages/platform-server/src/location.ts | 41 +- .../platform-server/src/private_export.ts | 5 +- .../platform-server/src/provide_server.ts | 5 +- packages/platform-server/src/server.ts | 50 +- packages/platform-server/src/tokens.ts | 5 +- packages/platform-server/src/utils.ts | 54 +- packages/platform-server/test/dom_utils.ts | 18 +- .../platform-server/test/event_replay_spec.ts | 16 +- .../platform-server/test/hydration_spec.ts | 4319 ++++++++--------- .../platform-server/test/integration_spec.ts | 1332 ++--- .../test/transfer_state_spec.ts | 21 +- .../platform-server/testing/src/server.ts | 21 +- 66 files changed, 6732 insertions(+), 5883 deletions(-) diff --git a/.ng-dev/format.mts b/.ng-dev/format.mts index c11465352749c..2510185df46da 100644 --- a/.ng-dev/format.mts +++ b/.ng-dev/format.mts @@ -25,6 +25,9 @@ export const format: FormatConfig = { 'packages/forms/**/*.{js,ts}', 'packages/language-service/**/*.{js,ts}', 'packages/localize/**/*.{js,ts}', + 'packages/platform-browser/**/*.{js,ts}', + 'packages/platform-browser-dynamic/**/*.{js,ts}', + 'packages/platform-server/**/*.{js,ts}', 'packages/misc/**/*.{js,ts}', 'packages/private/**/*.{js,ts}', 'packages/router/**/*.{js,ts}', @@ -81,6 +84,9 @@ export const format: FormatConfig = { '!packages/forms/**/*.{js,ts}', '!packages/language-service/**/*.{js,ts}', '!packages/localize/**/*.{js,ts}', + '!packages/platform-browser/**/*.{js,ts}', + '!packages/platform-browser-dynamic/**/*.{js,ts}', + '!packages/platform-server/**/*.{js,ts}', '!packages/misc/**/*.{js,ts}', '!packages/private/**/*.{js,ts}', '!packages/router/**/*.{js,ts}', diff --git a/packages/platform-browser-dynamic/src/compiler_factory.ts b/packages/platform-browser-dynamic/src/compiler_factory.ts index 9b50a02adb633..e584516431fa4 100644 --- a/packages/platform-browser-dynamic/src/compiler_factory.ts +++ b/packages/platform-browser-dynamic/src/compiler_factory.ts @@ -7,10 +7,18 @@ */ import {CompilerConfig} from '@angular/compiler'; -import {Compiler, CompilerFactory, CompilerOptions, Injector, StaticProvider, ViewEncapsulation} from '@angular/core'; +import { + Compiler, + CompilerFactory, + CompilerOptions, + Injector, + StaticProvider, + ViewEncapsulation, +} from '@angular/core'; -export const COMPILER_PROVIDERS = - [{provide: Compiler, useFactory: () => new Compiler()}]; +export const COMPILER_PROVIDERS = [ + {provide: Compiler, useFactory: () => new Compiler()}, +]; /** * @publicApi * @@ -33,7 +41,8 @@ export class JitCompilerFactory implements CompilerFactory { const opts = _mergeOptions(this._defaultOptions.concat(options)); const injector = Injector.create({ providers: [ - COMPILER_PROVIDERS, { + COMPILER_PROVIDERS, + { provide: CompilerConfig, useFactory: () => { return new CompilerConfig({ @@ -41,10 +50,10 @@ export class JitCompilerFactory implements CompilerFactory { preserveWhitespaces: opts.preserveWhitespaces, }); }, - deps: [] + deps: [], }, - opts.providers! - ] + opts.providers!, + ], }); return injector.get(Compiler); } @@ -52,13 +61,13 @@ export class JitCompilerFactory implements CompilerFactory { function _mergeOptions(optionsArr: CompilerOptions[]): CompilerOptions { return { - defaultEncapsulation: _lastDefined(optionsArr.map(options => options.defaultEncapsulation)), - providers: _mergeArrays(optionsArr.map(options => options.providers!)), - preserveWhitespaces: _lastDefined(optionsArr.map(options => options.preserveWhitespaces)), + defaultEncapsulation: _lastDefined(optionsArr.map((options) => options.defaultEncapsulation)), + providers: _mergeArrays(optionsArr.map((options) => options.providers!)), + preserveWhitespaces: _lastDefined(optionsArr.map((options) => options.preserveWhitespaces)), }; } -function _lastDefined(args: T[]): T|undefined { +function _lastDefined(args: T[]): T | undefined { for (let i = args.length - 1; i >= 0; i--) { if (args[i] !== undefined) { return args[i]; diff --git a/packages/platform-browser-dynamic/src/platform-browser-dynamic.ts b/packages/platform-browser-dynamic/src/platform-browser-dynamic.ts index 6152d99f3bcf0..1346ea50579cd 100644 --- a/packages/platform-browser-dynamic/src/platform-browser-dynamic.ts +++ b/packages/platform-browser-dynamic/src/platform-browser-dynamic.ts @@ -19,4 +19,7 @@ export {JitCompilerFactory} from './compiler_factory'; * @publicApi */ export const platformBrowserDynamic = createPlatformFactory( - platformCoreDynamic, 'browserDynamic', INTERNAL_BROWSER_DYNAMIC_PLATFORM_PROVIDERS); + platformCoreDynamic, + 'browserDynamic', + INTERNAL_BROWSER_DYNAMIC_PLATFORM_PROVIDERS, +); diff --git a/packages/platform-browser-dynamic/src/platform_core_dynamic.ts b/packages/platform-browser-dynamic/src/platform_core_dynamic.ts index 192255b9413df..c7e786fd59213 100644 --- a/packages/platform-browser-dynamic/src/platform_core_dynamic.ts +++ b/packages/platform-browser-dynamic/src/platform_core_dynamic.ts @@ -6,7 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ -import {COMPILER_OPTIONS, CompilerFactory, createPlatformFactory, platformCore} from '@angular/core'; +import { + COMPILER_OPTIONS, + CompilerFactory, + createPlatformFactory, + platformCore, +} from '@angular/core'; import {JitCompilerFactory} from './compiler_factory'; diff --git a/packages/platform-browser-dynamic/src/platform_providers.ts b/packages/platform-browser-dynamic/src/platform_providers.ts index 4319005eed18b..814335fbbe304 100644 --- a/packages/platform-browser-dynamic/src/platform_providers.ts +++ b/packages/platform-browser-dynamic/src/platform_providers.ts @@ -9,7 +9,6 @@ import {ɵPLATFORM_BROWSER_ID as PLATFORM_BROWSER_ID} from '@angular/common'; import {ResourceLoader} from '@angular/compiler'; import {COMPILER_OPTIONS, PLATFORM_ID, StaticProvider} from '@angular/core'; - import {ɵINTERNAL_BROWSER_PLATFORM_PROVIDERS as INTERNAL_BROWSER_PLATFORM_PROVIDERS} from '@angular/platform-browser'; import {ResourceLoaderImpl} from './resource_loader/resource_loader_impl'; @@ -22,7 +21,7 @@ export const INTERNAL_BROWSER_DYNAMIC_PLATFORM_PROVIDERS: StaticProvider[] = [ { provide: COMPILER_OPTIONS, useValue: {providers: [{provide: ResourceLoader, useClass: ResourceLoaderImpl, deps: []}]}, - multi: true + multi: true, }, {provide: PLATFORM_ID, useValue: PLATFORM_BROWSER_ID}, ]; diff --git a/packages/platform-browser-dynamic/src/resource_loader/resource_loader_impl.ts b/packages/platform-browser-dynamic/src/resource_loader/resource_loader_impl.ts index 5e5bcf25cae2a..43d407ac55f2f 100644 --- a/packages/platform-browser-dynamic/src/resource_loader/resource_loader_impl.ts +++ b/packages/platform-browser-dynamic/src/resource_loader/resource_loader_impl.ts @@ -8,7 +8,6 @@ import {ResourceLoader} from '@angular/compiler'; import {Injectable} from '@angular/core'; - @Injectable() export class ResourceLoaderImpl extends ResourceLoader { override get(url: string): Promise { @@ -22,7 +21,7 @@ export class ResourceLoaderImpl extends ResourceLoader { xhr.open('GET', url, true); xhr.responseType = 'text'; - xhr.onload = function() { + xhr.onload = function () { const response = xhr.response; let status = xhr.status; @@ -41,7 +40,7 @@ export class ResourceLoaderImpl extends ResourceLoader { } }; - xhr.onerror = function() { + xhr.onerror = function () { reject(`Failed to load ${url}`); }; diff --git a/packages/platform-browser-dynamic/test/metadata_overrider_spec.ts b/packages/platform-browser-dynamic/test/metadata_overrider_spec.ts index 9f94216f9867e..cd45706380767 100644 --- a/packages/platform-browser-dynamic/test/metadata_overrider_spec.ts +++ b/packages/platform-browser-dynamic/test/metadata_overrider_spec.ts @@ -29,7 +29,7 @@ class SomeMetadata implements SomeMetadataType { this._getterProp = options.getterProp!; this.arrayProp = options.arrayProp!; Object.defineProperty(this, 'getterProp', { - enumerable: true, // getters are non-enumerable by default in es2015 + enumerable: true, // getters are non-enumerable by default in es2015 get: () => this._getterProp, }); } @@ -42,7 +42,7 @@ class OtherMetadata extends SomeMetadata implements OtherMetadataType { super({ plainProp: options.plainProp, getterProp: options.getterProp, - arrayProp: options.arrayProp + arrayProp: options.arrayProp, }); this.otherPlainProp = options.otherPlainProp!; @@ -65,58 +65,79 @@ describe('metadata overrider', () => { }); it('should set individual properties and keep others', () => { - const oldInstance = - new SomeMetadata({plainProp: 'somePlainProp', getterProp: 'someGetterProp'}); - const newInstance = - overrider.overrideMetadata(SomeMetadata, oldInstance, {set: {plainProp: 'newPlainProp'}}); - expect(newInstance) - .toEqual(new SomeMetadata({plainProp: 'newPlainProp', getterProp: 'someGetterProp'})); + const oldInstance = new SomeMetadata({ + plainProp: 'somePlainProp', + getterProp: 'someGetterProp', + }); + const newInstance = overrider.overrideMetadata(SomeMetadata, oldInstance, { + set: {plainProp: 'newPlainProp'}, + }); + expect(newInstance).toEqual( + new SomeMetadata({plainProp: 'newPlainProp', getterProp: 'someGetterProp'}), + ); }); describe('add properties', () => { it('should replace non array values', () => { - const oldInstance = - new SomeMetadata({plainProp: 'somePlainProp', getterProp: 'someGetterProp'}); - const newInstance = - overrider.overrideMetadata(SomeMetadata, oldInstance, {add: {plainProp: 'newPlainProp'}}); - expect(newInstance) - .toEqual(new SomeMetadata({plainProp: 'newPlainProp', getterProp: 'someGetterProp'})); + const oldInstance = new SomeMetadata({ + plainProp: 'somePlainProp', + getterProp: 'someGetterProp', + }); + const newInstance = overrider.overrideMetadata(SomeMetadata, oldInstance, { + add: {plainProp: 'newPlainProp'}, + }); + expect(newInstance).toEqual( + new SomeMetadata({plainProp: 'newPlainProp', getterProp: 'someGetterProp'}), + ); }); it('should add to array values', () => { const oldInstance = new SomeMetadata({arrayProp: ['a']}); - const newInstance = - overrider.overrideMetadata(SomeMetadata, oldInstance, {add: {arrayProp: ['b']}}); + const newInstance = overrider.overrideMetadata(SomeMetadata, oldInstance, { + add: {arrayProp: ['b']}, + }); expect(newInstance).toEqual(new SomeMetadata({arrayProp: ['a', 'b']})); }); }); describe('remove', () => { it('should set values to undefined if their value matches', () => { - const oldInstance = - new SomeMetadata({plainProp: 'somePlainProp', getterProp: 'someGetterProp'}); - const newInstance = overrider.overrideMetadata( - SomeMetadata, oldInstance, {remove: {plainProp: 'somePlainProp'}}); - expect(newInstance) - .toEqual(new SomeMetadata({plainProp: undefined, getterProp: 'someGetterProp'})); + const oldInstance = new SomeMetadata({ + plainProp: 'somePlainProp', + getterProp: 'someGetterProp', + }); + const newInstance = overrider.overrideMetadata(SomeMetadata, oldInstance, { + remove: {plainProp: 'somePlainProp'}, + }); + expect(newInstance).toEqual( + new SomeMetadata({plainProp: undefined, getterProp: 'someGetterProp'}), + ); }); it('should leave values if their value does not match', () => { - const oldInstance = - new SomeMetadata({plainProp: 'somePlainProp', getterProp: 'someGetterProp'}); - const newInstance = overrider.overrideMetadata( - SomeMetadata, oldInstance, {remove: {plainProp: 'newPlainProp'}}); - expect(newInstance) - .toEqual(new SomeMetadata({plainProp: 'somePlainProp', getterProp: 'someGetterProp'})); + const oldInstance = new SomeMetadata({ + plainProp: 'somePlainProp', + getterProp: 'someGetterProp', + }); + const newInstance = overrider.overrideMetadata(SomeMetadata, oldInstance, { + remove: {plainProp: 'newPlainProp'}, + }); + expect(newInstance).toEqual( + new SomeMetadata({plainProp: 'somePlainProp', getterProp: 'someGetterProp'}), + ); }); it('should remove a value from an array', () => { - const oldInstance = - new SomeMetadata({arrayProp: ['a', 'b', 'c'], getterProp: 'someGetterProp'}); - const newInstance = - overrider.overrideMetadata(SomeMetadata, oldInstance, {remove: {arrayProp: ['a', 'c']}}); - expect(newInstance) - .toEqual(new SomeMetadata({arrayProp: ['b'], getterProp: 'someGetterProp'})); + const oldInstance = new SomeMetadata({ + arrayProp: ['a', 'b', 'c'], + getterProp: 'someGetterProp', + }); + const newInstance = overrider.overrideMetadata(SomeMetadata, oldInstance, { + remove: {arrayProp: ['a', 'c']}, + }); + expect(newInstance).toEqual( + new SomeMetadata({arrayProp: ['b'], getterProp: 'someGetterProp'}), + ); }); it('should support types as values', () => { @@ -125,11 +146,13 @@ describe('metadata overrider', () => { class Class3 {} const instance1 = new SomeMetadata({arrayProp: [Class1, Class2, Class3]}); - const instance2 = - overrider.overrideMetadata(SomeMetadata, instance1, {remove: {arrayProp: [Class1]}}); + const instance2 = overrider.overrideMetadata(SomeMetadata, instance1, { + remove: {arrayProp: [Class1]}, + }); expect(instance2).toEqual(new SomeMetadata({arrayProp: [Class2, Class3]})); - const instance3 = - overrider.overrideMetadata(SomeMetadata, instance2, {remove: {arrayProp: [Class3]}}); + const instance3 = overrider.overrideMetadata(SomeMetadata, instance2, { + remove: {arrayProp: [Class3]}, + }); expect(instance3).toEqual(new SomeMetadata({arrayProp: [Class2]})); }); }); @@ -139,15 +162,18 @@ describe('metadata overrider', () => { const oldInstance = new OtherMetadata({ plainProp: 'somePlainProp', getterProp: 'someGetterProp', - otherPlainProp: 'newOtherProp' + otherPlainProp: 'newOtherProp', }); - const newInstance = overrider.overrideMetadata( - OtherMetadata, oldInstance, {set: {plainProp: 'newPlainProp'}}); - expect(newInstance).toEqual(new OtherMetadata({ - plainProp: 'newPlainProp', - getterProp: 'someGetterProp', - otherPlainProp: 'newOtherProp' - })); + const newInstance = overrider.overrideMetadata(OtherMetadata, oldInstance, { + set: {plainProp: 'newPlainProp'}, + }); + expect(newInstance).toEqual( + new OtherMetadata({ + plainProp: 'newPlainProp', + getterProp: 'someGetterProp', + otherPlainProp: 'newOtherProp', + }), + ); }); }); }); diff --git a/packages/platform-browser-dynamic/test/resource_loader/resource_loader_impl_spec.ts b/packages/platform-browser-dynamic/test/resource_loader/resource_loader_impl_spec.ts index 08e71c1b0fa75..728417141b225 100644 --- a/packages/platform-browser-dynamic/test/resource_loader/resource_loader_impl_spec.ts +++ b/packages/platform-browser-dynamic/test/resource_loader/resource_loader_impl_spec.ts @@ -19,14 +19,14 @@ if (isBrowser) { resourceLoader = new ResourceLoaderImpl(); }); - it('should resolve the Promise with the file content on success', done => { + it('should resolve the Promise with the file content on success', (done) => { resourceLoader.get(url200).then((text) => { expect(text.trim()).toEqual('

hey

'); done(); }); }, 10000); - it('should reject the Promise on failure', done => { + it('should reject the Promise on failure', (done) => { resourceLoader.get(url404).catch((e) => { expect(e).toEqual(`Failed to load ${url404}`); done(); diff --git a/packages/platform-browser-dynamic/test/testing_public_browser_spec.ts b/packages/platform-browser-dynamic/test/testing_public_browser_spec.ts index 4d2f9af2494d1..b671e355d7056 100644 --- a/packages/platform-browser-dynamic/test/testing_public_browser_spec.ts +++ b/packages/platform-browser-dynamic/test/testing_public_browser_spec.ts @@ -42,37 +42,40 @@ if (isBrowser) { }); it('should run async tests with ResourceLoaders', waitForAsync(() => { - const resourceLoader = new ResourceLoaderImpl(); - resourceLoader - .get('/base/angular/packages/platform-browser/test/static_assets/test.html') - .then(() => { - actuallyDone = true; - }); - }), - 10000); // Long timeout here because this test makes an actual ResourceLoader. + const resourceLoader = new ResourceLoaderImpl(); + resourceLoader + .get('/base/angular/packages/platform-browser/test/static_assets/test.html') + .then(() => { + actuallyDone = true; + }); + }), 10000); // Long timeout here because this test makes an actual ResourceLoader. }); describe('using the test injector with the inject helper', () => { describe('setting up Providers', () => { beforeEach(() => { - TestBed.configureTestingModule( - {providers: [{provide: FancyService, useValue: new FancyService()}]}); + TestBed.configureTestingModule({ + providers: [{provide: FancyService, useValue: new FancyService()}], + }); }); - it('provides a real ResourceLoader instance', - inject([ResourceLoader], (resourceLoader: ResourceLoader) => { - expect(resourceLoader instanceof ResourceLoaderImpl).toBeTruthy(); - })); + it('provides a real ResourceLoader instance', inject( + [ResourceLoader], + (resourceLoader: ResourceLoader) => { + expect(resourceLoader instanceof ResourceLoaderImpl).toBeTruthy(); + }, + )); - it('should allow the use of fakeAsync', - fakeAsync(inject([FancyService], (service: FancyService) => { - let value: string|undefined; - service.getAsyncValue().then(function(val: string) { - value = val; - }); - tick(); - expect(value).toEqual('async value'); - }))); + it('should allow the use of fakeAsync', fakeAsync( + inject([FancyService], (service: FancyService) => { + let value: string | undefined; + service.getAsyncValue().then(function (val: string) { + value = val; + }); + tick(); + expect(value).toEqual('async value'); + }), + )); }); }); @@ -81,8 +84,7 @@ if (isBrowser) { @NgModule({ id: 'test-module', }) - class TestModule { - } + class TestModule {} TestBed.configureTestingModule({ imports: [TestModule], @@ -97,35 +99,33 @@ if (isBrowser) { // TODO(alxhub): figure out why this is failing on saucelabs xit('should fail with an error from a promise', async () => { @Component({selector: 'bad-template-comp', templateUrl: 'non-existent.html'}) - class BadTemplateUrl { - } + class BadTemplateUrl {} TestBed.configureTestingModule({declarations: [BadTemplateUrl]}); - await expectAsync(TestBed.compileComponents()) - .toBeRejectedWith('Failed to load non-existent.html'); + await expectAsync(TestBed.compileComponents()).toBeRejectedWith( + 'Failed to load non-existent.html', + ); }, 10000); }); }); - describe('TestBed createComponent', function() { + describe('TestBed createComponent', function () { // TODO(alxhub): disable while we figure out how this should work xit('should allow an external templateUrl', waitForAsync(() => { - @Component({ - selector: 'external-template-comp', - templateUrl: '/base/angular/packages/platform-browser/test/static_assets/test.html' - }) - class ExternalTemplateComp { - } + @Component({ + selector: 'external-template-comp', + templateUrl: '/base/angular/packages/platform-browser/test/static_assets/test.html', + }) + class ExternalTemplateComp {} - TestBed.configureTestingModule({declarations: [ExternalTemplateComp]}); - TestBed.compileComponents().then(() => { - const componentFixture = TestBed.createComponent(ExternalTemplateComp); - componentFixture.detectChanges(); - expect(componentFixture.nativeElement.textContent).toEqual('from external template'); - }); - }), - 10000); // Long timeout here because this test makes an actual ResourceLoader - // request, and is slow on Edge. + TestBed.configureTestingModule({declarations: [ExternalTemplateComp]}); + TestBed.compileComponents().then(() => { + const componentFixture = TestBed.createComponent(ExternalTemplateComp); + componentFixture.detectChanges(); + expect(componentFixture.nativeElement.textContent).toEqual('from external template'); + }); + }), 10000); // Long timeout here because this test makes an actual ResourceLoader + // request, and is slow on Edge. }); }); } diff --git a/packages/platform-browser-dynamic/testing/src/platform_core_dynamic_testing.ts b/packages/platform-browser-dynamic/testing/src/platform_core_dynamic_testing.ts index f01c3d9a91b53..dc4f6877b3c54 100644 --- a/packages/platform-browser-dynamic/testing/src/platform_core_dynamic_testing.ts +++ b/packages/platform-browser-dynamic/testing/src/platform_core_dynamic_testing.ts @@ -15,8 +15,4 @@ import {ɵplatformCoreDynamic as platformCoreDynamic} from '@angular/platform-br * @publicApi */ export const platformCoreDynamicTesting: (extraProviders?: any[]) => PlatformRef = - createPlatformFactory( - platformCoreDynamic, - 'coreDynamicTesting', - [], - ); + createPlatformFactory(platformCoreDynamic, 'coreDynamicTesting', []); diff --git a/packages/platform-browser-dynamic/testing/src/testing.ts b/packages/platform-browser-dynamic/testing/src/testing.ts index 37a22bdaf8579..662b33cbce6dc 100644 --- a/packages/platform-browser-dynamic/testing/src/testing.ts +++ b/packages/platform-browser-dynamic/testing/src/testing.ts @@ -20,8 +20,10 @@ export * from './private_export_testing'; * @publicApi */ export const platformBrowserDynamicTesting = createPlatformFactory( - platformCoreDynamicTesting, 'browserDynamicTesting', - INTERNAL_BROWSER_DYNAMIC_PLATFORM_PROVIDERS); + platformCoreDynamicTesting, + 'browserDynamicTesting', + INTERNAL_BROWSER_DYNAMIC_PLATFORM_PROVIDERS, +); /** * NgModule for testing. @@ -30,9 +32,6 @@ export const platformBrowserDynamicTesting = createPlatformFactory( */ @NgModule({ exports: [BrowserTestingModule], - providers: [ - {provide: TestComponentRenderer, useClass: DOMTestComponentRenderer}, - ] + providers: [{provide: TestComponentRenderer, useClass: DOMTestComponentRenderer}], }) -export class BrowserDynamicTestingModule { -} +export class BrowserDynamicTestingModule {} diff --git a/packages/platform-browser/animations/async/src/async_animation_renderer.ts b/packages/platform-browser/animations/async/src/async_animation_renderer.ts index eac2575ac3099..483945b005de7 100644 --- a/packages/platform-browser/animations/async/src/async_animation_renderer.ts +++ b/packages/platform-browser/animations/async/src/async_animation_renderer.ts @@ -6,15 +6,31 @@ * found in the LICENSE file at https://angular.io/license */ -import {ɵAnimationEngine as AnimationEngine, ɵAnimationRenderer as AnimationRenderer, ɵAnimationRendererFactory as AnimationRendererFactory} from '@angular/animations/browser'; -import {inject, Injectable, NgZone, OnDestroy, Renderer2, RendererFactory2, RendererStyleFlags2, RendererType2, ɵAnimationRendererType as AnimationRendererType, ɵChangeDetectionScheduler as ChangeDetectionScheduler, ɵRuntimeError as RuntimeError} from '@angular/core'; +import { + ɵAnimationEngine as AnimationEngine, + ɵAnimationRenderer as AnimationRenderer, + ɵAnimationRendererFactory as AnimationRendererFactory, +} from '@angular/animations/browser'; +import { + inject, + Injectable, + NgZone, + OnDestroy, + Renderer2, + RendererFactory2, + RendererStyleFlags2, + RendererType2, + ɵAnimationRendererType as AnimationRendererType, + ɵChangeDetectionScheduler as ChangeDetectionScheduler, + ɵRuntimeError as RuntimeError, +} from '@angular/core'; import {ɵRuntimeErrorCode as RuntimeErrorCode} from '@angular/platform-browser'; const ANIMATION_PREFIX = '@'; @Injectable() export class AsyncAnimationRendererFactory implements OnDestroy, RendererFactory2 { - private _rendererFactoryPromise: Promise|null = null; + private _rendererFactoryPromise: Promise | null = null; private readonly scheduler = inject(ChangeDetectionScheduler, {optional: true}); private _engine?: AnimationEngine; @@ -23,13 +39,19 @@ export class AsyncAnimationRendererFactory implements OnDestroy, RendererFactory * @param moduleImpl allows to provide a mock implmentation (or will load the animation module) */ constructor( - private doc: Document, private delegate: RendererFactory2, private zone: NgZone, - private animationType: 'animations'|'noop', private moduleImpl?: Promise<{ - ɵcreateEngine: - (type: 'animations'|'noop', doc: Document, - scheduler: ChangeDetectionScheduler|null) => AnimationEngine, - ɵAnimationRendererFactory: typeof AnimationRendererFactory - }>) {} + private doc: Document, + private delegate: RendererFactory2, + private zone: NgZone, + private animationType: 'animations' | 'noop', + private moduleImpl?: Promise<{ + ɵcreateEngine: ( + type: 'animations' | 'noop', + doc: Document, + scheduler: ChangeDetectionScheduler | null, + ) => AnimationEngine; + ɵAnimationRendererFactory: typeof AnimationRendererFactory; + }>, + ) {} /** @nodoc */ ngOnDestroy(): void { @@ -49,23 +71,27 @@ export class AsyncAnimationRendererFactory implements OnDestroy, RendererFactory const moduleImpl = this.moduleImpl ?? import('@angular/animations/browser'); return moduleImpl - .catch((e) => { - throw new RuntimeError( - RuntimeErrorCode.ANIMATION_RENDERER_ASYNC_LOADING_FAILURE, - (typeof ngDevMode === 'undefined' || ngDevMode) && - 'Async loading for animations package was ' + - 'enabled, but loading failed. Angular falls back to using regular rendering. ' + - 'No animations will be displayed and their styles won\'t be applied.'); - }) - .then(({ɵcreateEngine, ɵAnimationRendererFactory}) => { - // We can't create the renderer yet because we might need the hostElement and the type - // Both are provided in createRenderer(). - this._engine = ɵcreateEngine(this.animationType, this.doc, this.scheduler); - const rendererFactory = - new ɵAnimationRendererFactory(this.delegate, this._engine, this.zone); - this.delegate = rendererFactory; - return rendererFactory; - }); + .catch((e) => { + throw new RuntimeError( + RuntimeErrorCode.ANIMATION_RENDERER_ASYNC_LOADING_FAILURE, + (typeof ngDevMode === 'undefined' || ngDevMode) && + 'Async loading for animations package was ' + + 'enabled, but loading failed. Angular falls back to using regular rendering. ' + + "No animations will be displayed and their styles won't be applied.", + ); + }) + .then(({ɵcreateEngine, ɵAnimationRendererFactory}) => { + // We can't create the renderer yet because we might need the hostElement and the type + // Both are provided in createRenderer(). + this._engine = ɵcreateEngine(this.animationType, this.doc, this.scheduler); + const rendererFactory = new ɵAnimationRendererFactory( + this.delegate, + this._engine, + this.zone, + ); + this.delegate = rendererFactory; + return rendererFactory; + }); } /** @@ -99,15 +125,17 @@ export class AsyncAnimationRendererFactory implements OnDestroy, RendererFactory } this._rendererFactoryPromise - ?.then((animationRendererFactory) => { - const animationRenderer = - animationRendererFactory.createRenderer(hostElement, rendererType); - dynamicRenderer.use(animationRenderer); - }) - .catch(e => { - // Permanently use regular renderer when loading fails. - dynamicRenderer.use(renderer); - }); + ?.then((animationRendererFactory) => { + const animationRenderer = animationRendererFactory.createRenderer( + hostElement, + rendererType, + ); + dynamicRenderer.use(animationRenderer); + }) + .catch((e) => { + // Permanently use regular renderer when loading fails. + dynamicRenderer.use(renderer); + }); return dynamicRenderer; } @@ -131,7 +159,7 @@ export class AsyncAnimationRendererFactory implements OnDestroy, RendererFactory */ export class DynamicDelegationRenderer implements Renderer2 { // List of callbacks that need to be replayed on the animation renderer once its loaded - private replay: ((renderer: Renderer2) => void)[]|null = []; + private replay: ((renderer: Renderer2) => void)[] | null = []; readonly ɵtype = AnimationRendererType.Delegated; constructor(private delegate: Renderer2) {} @@ -160,7 +188,7 @@ export class DynamicDelegationRenderer implements Renderer2 { this.delegate.destroy(); } - createElement(name: string, namespace?: string|null) { + createElement(name: string, namespace?: string | null) { return this.delegate.createElement(name, namespace); } @@ -172,7 +200,7 @@ export class DynamicDelegationRenderer implements Renderer2 { return this.delegate.createText(value); } - get destroyNode(): ((node: any) => void)|null { + get destroyNode(): ((node: any) => void) | null { return this.delegate.destroyNode; } @@ -180,15 +208,15 @@ export class DynamicDelegationRenderer implements Renderer2 { this.delegate.appendChild(parent, newChild); } - insertBefore(parent: any, newChild: any, refChild: any, isMove?: boolean|undefined): void { + insertBefore(parent: any, newChild: any, refChild: any, isMove?: boolean | undefined): void { this.delegate.insertBefore(parent, newChild, refChild, isMove); } - removeChild(parent: any, oldChild: any, isHostElement?: boolean|undefined): void { + removeChild(parent: any, oldChild: any, isHostElement?: boolean | undefined): void { this.delegate.removeChild(parent, oldChild, isHostElement); } - selectRootElement(selectorOrNode: any, preserveContent?: boolean|undefined): any { + selectRootElement(selectorOrNode: any, preserveContent?: boolean | undefined): any { return this.delegate.selectRootElement(selectorOrNode, preserveContent); } @@ -200,11 +228,11 @@ export class DynamicDelegationRenderer implements Renderer2 { return this.delegate.nextSibling(node); } - setAttribute(el: any, name: string, value: string, namespace?: string|null|undefined): void { + setAttribute(el: any, name: string, value: string, namespace?: string | null | undefined): void { this.delegate.setAttribute(el, name, value, namespace); } - removeAttribute(el: any, name: string, namespace?: string|null|undefined): void { + removeAttribute(el: any, name: string, namespace?: string | null | undefined): void { this.delegate.removeAttribute(el, name, namespace); } @@ -216,11 +244,11 @@ export class DynamicDelegationRenderer implements Renderer2 { this.delegate.removeClass(el, name); } - setStyle(el: any, style: string, value: any, flags?: RendererStyleFlags2|undefined): void { + setStyle(el: any, style: string, value: any, flags?: RendererStyleFlags2 | undefined): void { this.delegate.setStyle(el, style, value, flags); } - removeStyle(el: any, style: string, flags?: RendererStyleFlags2|undefined): void { + removeStyle(el: any, style: string, flags?: RendererStyleFlags2 | undefined): void { this.delegate.removeStyle(el, style, flags); } diff --git a/packages/platform-browser/animations/async/src/providers.ts b/packages/platform-browser/animations/async/src/providers.ts index ad6b075932436..34a1b936b213c 100644 --- a/packages/platform-browser/animations/async/src/providers.ts +++ b/packages/platform-browser/animations/async/src/providers.ts @@ -7,7 +7,14 @@ */ import {DOCUMENT} from '@angular/common'; -import {ANIMATION_MODULE_TYPE, EnvironmentProviders, makeEnvironmentProviders, NgZone, RendererFactory2, ɵperformanceMarkFeature as performanceMarkFeature} from '@angular/core'; +import { + ANIMATION_MODULE_TYPE, + EnvironmentProviders, + makeEnvironmentProviders, + NgZone, + RendererFactory2, + ɵperformanceMarkFeature as performanceMarkFeature, +} from '@angular/core'; import {ɵDomRendererFactory2 as DomRendererFactory2} from '@angular/platform-browser'; import {AsyncAnimationRendererFactory} from './async_animation_renderer'; @@ -40,8 +47,9 @@ import {AsyncAnimationRendererFactory} from './async_animation_renderer'; * @publicApi * @developerPreview */ -export function provideAnimationsAsync(type: 'animations'|'noop' = 'animations'): - EnvironmentProviders { +export function provideAnimationsAsync( + type: 'animations' | 'noop' = 'animations', +): EnvironmentProviders { performanceMarkFeature('NgAsyncAnimations'); return makeEnvironmentProviders([ { diff --git a/packages/platform-browser/animations/async/test/animation_renderer_spec.ts b/packages/platform-browser/animations/async/test/animation_renderer_spec.ts index 7b279c3a34032..0fe65678a9e54 100644 --- a/packages/platform-browser/animations/async/test/animation_renderer_spec.ts +++ b/packages/platform-browser/animations/async/test/animation_renderer_spec.ts @@ -5,365 +5,400 @@ * 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 {animate, AnimationPlayer, AnimationTriggerMetadata, style, transition, trigger} from '@angular/animations'; -import {ɵAnimationEngine as AnimationEngine, ɵAnimationRenderer as AnimationRenderer, ɵAnimationRendererFactory as AnimationRendererFactory, ɵBaseAnimationRenderer as BaseAnimationRenderer} from '@angular/animations/browser'; +import { + animate, + AnimationPlayer, + AnimationTriggerMetadata, + style, + transition, + trigger, +} from '@angular/animations'; +import { + ɵAnimationEngine as AnimationEngine, + ɵAnimationRenderer as AnimationRenderer, + ɵAnimationRendererFactory as AnimationRendererFactory, + ɵBaseAnimationRenderer as BaseAnimationRenderer, +} from '@angular/animations/browser'; import {DOCUMENT} from '@angular/common'; -import {ANIMATION_MODULE_TYPE, Component, Injectable, NgZone, RendererFactory2, RendererType2, ViewChild, ɵChangeDetectionScheduler as ChangeDetectionScheduler} from '@angular/core'; +import { + ANIMATION_MODULE_TYPE, + Component, + Injectable, + NgZone, + RendererFactory2, + RendererType2, + ViewChild, + ɵChangeDetectionScheduler as ChangeDetectionScheduler, +} from '@angular/core'; import {TestBed} from '@angular/core/testing'; import {ɵDomRendererFactory2 as DomRendererFactory2} from '@angular/platform-browser'; import {InjectableAnimationEngine} from '@angular/platform-browser/animations/src/providers'; import {el} from '@angular/platform-browser/testing/src/browser_util'; -import {AsyncAnimationRendererFactory, DynamicDelegationRenderer} from '../src/async_animation_renderer'; +import { + AsyncAnimationRendererFactory, + DynamicDelegationRenderer, +} from '../src/async_animation_renderer'; type AnimationBrowserModule = typeof import('@angular/animations/browser'); -(function() { -if (isNode) { - it('empty test so jasmine doesnt complain', () => {}); - return; -} - -describe('AnimationRenderer', () => { - let element: any; - beforeEach(() => { - element = el('
'); - - TestBed.configureTestingModule({ - providers: [ - { - provide: RendererFactory2, - useFactory: - (doc: Document, renderer: DomRendererFactory2, zone: NgZone, - engine: MockAnimationEngine) => { - const animationModule = { - ɵcreateEngine: - (_: 'animations'|'noop', _2: Document, _3: ChangeDetectionScheduler|null): - AnimationEngine => engine, - ɵAnimationEngine: MockAnimationEngine as any, - ɵAnimationRenderer: AnimationRenderer, - ɵBaseAnimationRenderer: BaseAnimationRenderer, - ɵAnimationRendererFactory: AnimationRendererFactory, - } satisfies Partialas AnimationBrowserModule; - - return new AsyncAnimationRendererFactory( - doc, renderer, zone, 'animations', Promise.resolve(animationModule)); - }, - deps: [DOCUMENT, DomRendererFactory2, NgZone, AnimationEngine] - }, - {provide: ANIMATION_MODULE_TYPE, useValue: 'BrowserAnimations'}, - {provide: AnimationEngine, useClass: MockAnimationEngine} - ], - }); - }); - - function makeRenderer(animationTriggers: any[] = []): Promise { - const type = { - id: 'id', - encapsulation: null!, - styles: [], - data: {'animation': animationTriggers} - }; - const factory = TestBed.inject(RendererFactory2) as AsyncAnimationRendererFactory; - const renderer = factory.createRenderer(element, type); - return (factory as any)._rendererFactoryPromise.then(() => renderer); +(function () { + if (isNode) { + it('empty test so jasmine doesnt complain', () => {}); + return; } - it('should hook into the engine\'s insert operations when appending children', async () => { - const renderer = await makeRenderer(); - const engine = (renderer as any).delegate.engine as MockAnimationEngine; - const container = el('
'); - - renderer.appendChild(container, element); - expect(engine.captures['onInsert'].pop()).toEqual([element]); - }); + describe('AnimationRenderer', () => { + let element: any; + beforeEach(() => { + element = el('
'); + + TestBed.configureTestingModule({ + providers: [ + { + provide: RendererFactory2, + useFactory: ( + doc: Document, + renderer: DomRendererFactory2, + zone: NgZone, + engine: MockAnimationEngine, + ) => { + const animationModule = { + ɵcreateEngine: ( + _: 'animations' | 'noop', + _2: Document, + _3: ChangeDetectionScheduler | null, + ): AnimationEngine => engine, + ɵAnimationEngine: MockAnimationEngine as any, + ɵAnimationRenderer: AnimationRenderer, + ɵBaseAnimationRenderer: BaseAnimationRenderer, + ɵAnimationRendererFactory: AnimationRendererFactory, + } satisfies Partial as AnimationBrowserModule; + + return new AsyncAnimationRendererFactory( + doc, + renderer, + zone, + 'animations', + Promise.resolve(animationModule), + ); + }, + deps: [DOCUMENT, DomRendererFactory2, NgZone, AnimationEngine], + }, + {provide: ANIMATION_MODULE_TYPE, useValue: 'BrowserAnimations'}, + {provide: AnimationEngine, useClass: MockAnimationEngine}, + ], + }); + }); - it('should hook into the engine\'s insert operations when inserting a child before another', - async () => { - const renderer = await makeRenderer(); - const engine = (renderer as any).delegate.engine as MockAnimationEngine; - const container = el('
'); - const element2 = el('
'); - container.appendChild(element2); - - renderer.insertBefore(container, element, element2); - expect(engine.captures['onInsert'].pop()).toEqual([element]); - }); - - it('should hook into the engine\'s insert operations when removing children', async () => { - const renderer = await makeRenderer(); - const engine = (renderer as any).delegate.engine as MockAnimationEngine; - const container = el('
'); - - renderer.removeChild(container, element, false); - expect(engine.captures['onRemove'].pop()).toEqual([element]); - }); + function makeRenderer(animationTriggers: any[] = []): Promise { + const type = { + id: 'id', + encapsulation: null!, + styles: [], + data: {'animation': animationTriggers}, + }; + const factory = TestBed.inject(RendererFactory2) as AsyncAnimationRendererFactory; + const renderer = factory.createRenderer(element, type); + return (factory as any)._rendererFactoryPromise.then(() => renderer); + } - it('should hook into the engine\'s setProperty call if the property begins with `@`', - async () => { - const renderer = await makeRenderer(); - const engine = (renderer as any).delegate.engine as MockAnimationEngine; + it("should hook into the engine's insert operations when appending children", async () => { + const renderer = await makeRenderer(); + const engine = (renderer as any).delegate.engine as MockAnimationEngine; + const container = el('
'); - renderer.setProperty(element, 'prop', 'value'); - expect(engine.captures['setProperty']).toBeFalsy(); + renderer.appendChild(container, element); + expect(engine.captures['onInsert'].pop()).toEqual([element]); + }); - renderer.setProperty(element, '@prop', 'value'); - expect(engine.captures['setProperty'].pop()).toEqual([element, 'prop', 'value']); - }); + it("should hook into the engine's insert operations when inserting a child before another", async () => { + const renderer = await makeRenderer(); + const engine = (renderer as any).delegate.engine as MockAnimationEngine; + const container = el('
'); + const element2 = el('
'); + container.appendChild(element2); - // https://github.com/angular/angular/issues/32794 - it('should support nested animation triggers', async () => { - const renderer = await makeRenderer([[trigger('myAnimation', [])]]); + renderer.insertBefore(container, element, element2); + expect(engine.captures['onInsert'].pop()).toEqual([element]); + }); - const {triggers} = (renderer as any).delegate.engine as MockAnimationEngine; + it("should hook into the engine's insert operations when removing children", async () => { + const renderer = await makeRenderer(); + const engine = (renderer as any).delegate.engine as MockAnimationEngine; + const container = el('
'); - expect(triggers.length).toEqual(1); - expect(triggers[0].name).toEqual('myAnimation'); - }); + renderer.removeChild(container, element, false); + expect(engine.captures['onRemove'].pop()).toEqual([element]); + }); - describe('listen', () => { - it('should hook into the engine\'s listen call if the property begins with `@`', async () => { + it("should hook into the engine's setProperty call if the property begins with `@`", async () => { const renderer = await makeRenderer(); const engine = (renderer as any).delegate.engine as MockAnimationEngine; - const cb = (event: any): boolean => { - return true; - }; + renderer.setProperty(element, 'prop', 'value'); + expect(engine.captures['setProperty']).toBeFalsy(); + + renderer.setProperty(element, '@prop', 'value'); + expect(engine.captures['setProperty'].pop()).toEqual([element, 'prop', 'value']); + }); + + // https://github.com/angular/angular/issues/32794 + it('should support nested animation triggers', async () => { + const renderer = await makeRenderer([[trigger('myAnimation', [])]]); - renderer.listen(element, 'event', cb); - expect(engine.captures['listen']).toBeFalsy(); + const {triggers} = (renderer as any).delegate.engine as MockAnimationEngine; - renderer.listen(element, '@event.phase', cb); - expect(engine.captures['listen'].pop()).toEqual([element, 'event', 'phase']); + expect(triggers.length).toEqual(1); + expect(triggers[0].name).toEqual('myAnimation'); }); - it('should resolve the body|document|window nodes given their values as strings as input', - async () => { - const renderer = await makeRenderer(); - const engine = ((renderer)['delegate'] as AnimationRenderer).engine as MockAnimationEngine; + describe('listen', () => { + it("should hook into the engine's listen call if the property begins with `@`", async () => { + const renderer = await makeRenderer(); + const engine = (renderer as any).delegate.engine as MockAnimationEngine; - const cb = (event: any): boolean => { - return true; - }; + const cb = (event: any): boolean => { + return true; + }; - renderer.listen('body', '@event', cb); - expect(engine.captures['listen'].pop()[0]).toBe(document.body); + renderer.listen(element, 'event', cb); + expect(engine.captures['listen']).toBeFalsy(); - renderer.listen('document', '@event', cb); - expect(engine.captures['listen'].pop()[0]).toBe(document); + renderer.listen(element, '@event.phase', cb); + expect(engine.captures['listen'].pop()).toEqual([element, 'event', 'phase']); + }); - renderer.listen('window', '@event', cb); - expect(engine.captures['listen'].pop()[0]).toBe(window); - }); + it('should resolve the body|document|window nodes given their values as strings as input', async () => { + const renderer = await makeRenderer(); + const engine = (renderer['delegate'] as AnimationRenderer).engine as MockAnimationEngine; - it('should store animations events passed to the default renderer and register them against the animation renderer', - async () => { - const type = { - id: 'id', - encapsulation: null!, - styles: [], - data: {'animation': []}, - }; + const cb = (event: any): boolean => { + return true; + }; - const factory = TestBed.inject(RendererFactory2) as AsyncAnimationRendererFactory; - const renderer = factory.createRenderer(element, type) as DynamicDelegationRenderer; + renderer.listen('body', '@event', cb); + expect(engine.captures['listen'].pop()[0]).toBe(document.body); - const cb = (event: any): boolean => true; - renderer.listen('body', '@event', cb); - renderer.listen('document', '@event', cb); - renderer.listen('window', '@event', cb); + renderer.listen('document', '@event', cb); + expect(engine.captures['listen'].pop()[0]).toBe(document); - // The animation renderer is not loaded yet - expect((renderer['delegate'] as AnimationRenderer).engine).toBeUndefined(); + renderer.listen('window', '@event', cb); + expect(engine.captures['listen'].pop()[0]).toBe(window); + }); - // This will change the delegate renderer from the default one to the AnimationRenderer - await factory['_rendererFactoryPromise']!.then(() => renderer); + it('should store animations events passed to the default renderer and register them against the animation renderer', async () => { + const type = { + id: 'id', + encapsulation: null!, + styles: [], + data: {'animation': []}, + }; - const engine = (renderer['delegate'] as AnimationRenderer).engine as MockAnimationEngine; + const factory = TestBed.inject(RendererFactory2) as AsyncAnimationRendererFactory; + const renderer = factory.createRenderer(element, type) as DynamicDelegationRenderer; - expect(engine.captures['listen'][0][0]).toBe(document.body); - expect(engine.captures['listen'][1][0]).toBe(document); - expect(engine.captures['listen'][2][0]).toBe(window); - }); - }); + const cb = (event: any): boolean => true; + renderer.listen('body', '@event', cb); + renderer.listen('document', '@event', cb); + renderer.listen('window', '@event', cb); - it('should store animations properties set on the default renderer and set them also on the animation renderer', - async () => { - const type = { - id: 'id', - encapsulation: null!, - styles: [], - data: {'animation': []}, - }; + // The animation renderer is not loaded yet + expect((renderer['delegate'] as AnimationRenderer).engine).toBeUndefined(); - const factory = TestBed.inject(RendererFactory2) as AsyncAnimationRendererFactory; - const renderer = factory.createRenderer(element, type) as DynamicDelegationRenderer; + // This will change the delegate renderer from the default one to the AnimationRenderer + await factory['_rendererFactoryPromise']!.then(() => renderer); - renderer.setProperty(element, '@openClose', 'closed'); - renderer.setProperty(element, '@openClose', 'open'); + const engine = (renderer['delegate'] as AnimationRenderer).engine as MockAnimationEngine; - // The animation renderer is not loaded yet - expect((renderer['delegate'] as AnimationRenderer).engine).toBeUndefined(); + expect(engine.captures['listen'][0][0]).toBe(document.body); + expect(engine.captures['listen'][1][0]).toBe(document); + expect(engine.captures['listen'][2][0]).toBe(window); + }); + }); - // This will change the delegate renderer from the default one to the AnimationRenderer - await factory['_rendererFactoryPromise']!.then(() => renderer); + it('should store animations properties set on the default renderer and set them also on the animation renderer', async () => { + const type = { + id: 'id', + encapsulation: null!, + styles: [], + data: {'animation': []}, + }; - const engine = (renderer['delegate'] as AnimationRenderer).engine as MockAnimationEngine; + const factory = TestBed.inject(RendererFactory2) as AsyncAnimationRendererFactory; + const renderer = factory.createRenderer(element, type) as DynamicDelegationRenderer; - expect(engine.captures['setProperty'][0][2]).toBe('closed'); - expect(engine.captures['setProperty'][1][2]).toBe('open'); - }); + renderer.setProperty(element, '@openClose', 'closed'); + renderer.setProperty(element, '@openClose', 'open'); - describe('registering animations', () => { - it('should only create a trigger definition once even if the registered multiple times'); - }); + // The animation renderer is not loaded yet + expect((renderer['delegate'] as AnimationRenderer).engine).toBeUndefined(); - describe('flushing animations', () => { - beforeEach(() => { - TestBed.resetTestingModule(); + // This will change the delegate renderer from the default one to the AnimationRenderer + await factory['_rendererFactoryPromise']!.then(() => renderer); + + const engine = (renderer['delegate'] as AnimationRenderer).engine as MockAnimationEngine; + + expect(engine.captures['setProperty'][0][2]).toBe('closed'); + expect(engine.captures['setProperty'][1][2]).toBe('open'); }); - // these tests are only meant to be run within the DOM - if (isNode) return; - - it('should flush and fire callbacks when the zone becomes stable', (async) => { - @Component({ - selector: 'my-cmp', - template: '
', - animations: [trigger( - 'myAnimation', - [transition( - '* => state', [style({'opacity': '0'}), animate(500, style({'opacity': '1'}))])])], - }) - class Cmp { - exp: any; - event: any; - onStart(event: any) { - this.event = event; + describe('registering animations', () => { + it('should only create a trigger definition once even if the registered multiple times'); + }); + + describe('flushing animations', () => { + beforeEach(() => { + TestBed.resetTestingModule(); + }); + + // these tests are only meant to be run within the DOM + if (isNode) return; + + it('should flush and fire callbacks when the zone becomes stable', (async) => { + @Component({ + selector: 'my-cmp', + template: '
', + animations: [ + trigger('myAnimation', [ + transition('* => state', [ + style({'opacity': '0'}), + animate(500, style({'opacity': '1'})), + ]), + ]), + ], + }) + class Cmp { + exp: any; + event: any; + onStart(event: any) { + this.event = event; + } } - } - TestBed.configureTestingModule({declarations: [Cmp]}); + TestBed.configureTestingModule({declarations: [Cmp]}); + + const engine = TestBed.inject(AnimationEngine); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; + cmp.exp = 'state'; + fixture.detectChanges(); + fixture.whenStable().then(() => { + expect(cmp.event.triggerName).toEqual('myAnimation'); + expect(cmp.event.phaseName).toEqual('start'); + cmp.event = null; + + engine.flush(); + expect(cmp.event).toBeFalsy(); + async(); + }); + }); + + it('should properly insert/remove nodes through the animation renderer that do not contain animations', (async) => { + @Component({ + selector: 'my-cmp', + template: '
', + animations: [ + trigger('someAnimation', [ + transition('* => *', [ + style({'opacity': '0'}), + animate(500, style({'opacity': '1'})), + ]), + ]), + ], + }) + class Cmp { + exp: any; + @ViewChild('elm') public element: any; + } - const engine = TestBed.inject(AnimationEngine); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; - cmp.exp = 'state'; - fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(cmp.event.triggerName).toEqual('myAnimation'); - expect(cmp.event.phaseName).toEqual('start'); - cmp.event = null; + TestBed.configureTestingModule({declarations: [Cmp]}); - engine.flush(); - expect(cmp.event).toBeFalsy(); - async(); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; + cmp.exp = true; + fixture.detectChanges(); + + fixture.whenStable().then(() => { + cmp.exp = false; + const element = cmp.element; + expect(element.nativeElement.parentNode).toBeTruthy(); + + fixture.detectChanges(); + fixture.whenStable().then(() => { + expect(element.nativeElement.parentNode).toBeFalsy(); + async(); + }); + }); }); - }); - it('should properly insert/remove nodes through the animation renderer that do not contain animations', - (async) => { - @Component({ - selector: 'my-cmp', - template: '
', - animations: [trigger( - 'someAnimation', - [transition( - '* => *', [style({'opacity': '0'}), animate(500, style({'opacity': '1'}))])])], - }) - class Cmp { - exp: any; - @ViewChild('elm') public element: any; - } - - TestBed.configureTestingModule({declarations: [Cmp]}); - - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; - cmp.exp = true; - fixture.detectChanges(); - - fixture.whenStable().then(() => { - cmp.exp = false; - const element = cmp.element; - expect(element.nativeElement.parentNode).toBeTruthy(); - - fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(element.nativeElement.parentNode).toBeFalsy(); - async(); - }); - }); - }); - - it('should only queue up dom removals if the element itself contains a valid leave animation', - () => { - @Component({ - selector: 'my-cmp', - template: ` + it('should only queue up dom removals if the element itself contains a valid leave animation', () => { + @Component({ + selector: 'my-cmp', + template: `
`, - animations: [ - trigger('animation1', [transition('a => b', [])]), - trigger('animation2', [transition(':leave', [])]), - ] - }) - class Cmp { - exp1: any = true; - exp2: any = true; - exp3: any = true; - - @ViewChild('elm1') public elm1: any; - - @ViewChild('elm2') public elm2: any; - - @ViewChild('elm3') public elm3: any; - } - - TestBed.configureTestingModule({declarations: [Cmp]}); - - const engine = TestBed.inject(AnimationEngine); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; - - fixture.detectChanges(); - const elm1 = cmp.elm1; - const elm2 = cmp.elm2; - const elm3 = cmp.elm3; - assertHasParent(elm1); - assertHasParent(elm2); - assertHasParent(elm3); - engine.flush(); - finishPlayers(engine.players); - - cmp.exp1 = false; - fixture.detectChanges(); - assertHasParent(elm1, false); - assertHasParent(elm2); - assertHasParent(elm3); - engine.flush(); - expect(engine.players.length).toEqual(0); - - cmp.exp2 = false; - fixture.detectChanges(); - assertHasParent(elm1, false); - assertHasParent(elm2, false); - assertHasParent(elm3); - engine.flush(); - expect(engine.players.length).toEqual(0); - - cmp.exp3 = false; - fixture.detectChanges(); - assertHasParent(elm1, false); - assertHasParent(elm2, false); - assertHasParent(elm3); - engine.flush(); - expect(engine.players.length).toEqual(1); - }); + animations: [ + trigger('animation1', [transition('a => b', [])]), + trigger('animation2', [transition(':leave', [])]), + ], + }) + class Cmp { + exp1: any = true; + exp2: any = true; + exp3: any = true; + + @ViewChild('elm1') public elm1: any; + + @ViewChild('elm2') public elm2: any; + + @ViewChild('elm3') public elm3: any; + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + + const engine = TestBed.inject(AnimationEngine); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; + + fixture.detectChanges(); + const elm1 = cmp.elm1; + const elm2 = cmp.elm2; + const elm3 = cmp.elm3; + assertHasParent(elm1); + assertHasParent(elm2); + assertHasParent(elm3); + engine.flush(); + finishPlayers(engine.players); + + cmp.exp1 = false; + fixture.detectChanges(); + assertHasParent(elm1, false); + assertHasParent(elm2); + assertHasParent(elm3); + engine.flush(); + expect(engine.players.length).toEqual(0); + + cmp.exp2 = false; + fixture.detectChanges(); + assertHasParent(elm1, false); + assertHasParent(elm2, false); + assertHasParent(elm3); + engine.flush(); + expect(engine.players.length).toEqual(0); + + cmp.exp3 = false; + fixture.detectChanges(); + assertHasParent(elm1, false); + assertHasParent(elm2, false); + assertHasParent(elm3); + engine.flush(); + expect(engine.players.length).toEqual(1); + }); + }); }); -}); })(); @Injectable() @@ -372,13 +407,17 @@ class MockAnimationEngine extends InjectableAnimationEngine { triggers: AnimationTriggerMetadata[] = []; private _capture(name: string, args: any[]) { - const data = this.captures[name] = this.captures[name] || []; + const data = (this.captures[name] = this.captures[name] || []); data.push(args); } override registerTrigger( - componentId: string, namespaceId: string, hostElement: any, name: string, - metadata: AnimationTriggerMetadata): void { + componentId: string, + namespaceId: string, + hostElement: any, + name: string, + metadata: AnimationTriggerMetadata, + ): void { this.triggers.push(metadata); } @@ -396,8 +435,12 @@ class MockAnimationEngine extends InjectableAnimationEngine { } override listen( - namespaceId: string, element: any, eventName: string, eventPhase: string, - callback: (event: any) => any): () => void { + namespaceId: string, + element: any, + eventName: string, + eventPhase: string, + callback: (event: any) => any, + ): () => void { // we don't capture the callback here since the renderer wraps it in a zone this._capture('listen', [element, eventName, eventPhase]); return () => {}; @@ -418,5 +461,5 @@ function assertHasParent(element: any, yes: boolean = true) { } function finishPlayers(players: AnimationPlayer[]) { - players.forEach(player => player.finish()); + players.forEach((player) => player.finish()); } diff --git a/packages/platform-browser/animations/src/animations.ts b/packages/platform-browser/animations/src/animations.ts index 5def93c50dbad..92ef9c6983d1a 100644 --- a/packages/platform-browser/animations/src/animations.ts +++ b/packages/platform-browser/animations/src/animations.ts @@ -12,6 +12,12 @@ * Entry point for all animation APIs of the animation browser package. */ export {ANIMATION_MODULE_TYPE} from '@angular/core'; -export {BrowserAnimationsModule, BrowserAnimationsModuleConfig, NoopAnimationsModule, provideAnimations, provideNoopAnimations} from './module'; +export { + BrowserAnimationsModule, + BrowserAnimationsModuleConfig, + NoopAnimationsModule, + provideAnimations, + provideNoopAnimations, +} from './module'; export * from './private_export'; diff --git a/packages/platform-browser/animations/src/module.ts b/packages/platform-browser/animations/src/module.ts index 5f6416ad568b7..94617a11492ef 100644 --- a/packages/platform-browser/animations/src/module.ts +++ b/packages/platform-browser/animations/src/module.ts @@ -5,7 +5,12 @@ * 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 {ModuleWithProviders, NgModule, Provider, ɵperformanceMarkFeature as performanceMarkFeature} from '@angular/core'; +import { + ModuleWithProviders, + NgModule, + Provider, + ɵperformanceMarkFeature as performanceMarkFeature, +} from '@angular/core'; import {BrowserModule} from '@angular/platform-browser'; import {BROWSER_ANIMATIONS_PROVIDERS, BROWSER_NOOP_ANIMATIONS_PROVIDERS} from './providers'; @@ -48,12 +53,14 @@ export class BrowserAnimationsModule { * class MyNgModule {} * ``` */ - static withConfig(config: BrowserAnimationsModuleConfig): - ModuleWithProviders { + static withConfig( + config: BrowserAnimationsModuleConfig, + ): ModuleWithProviders { return { ngModule: BrowserAnimationsModule, - providers: config.disableAnimations ? BROWSER_NOOP_ANIMATIONS_PROVIDERS : - BROWSER_ANIMATIONS_PROVIDERS + providers: config.disableAnimations + ? BROWSER_NOOP_ANIMATIONS_PROVIDERS + : BROWSER_ANIMATIONS_PROVIDERS, }; } } @@ -95,8 +102,7 @@ export function provideAnimations(): Provider[] { exports: [BrowserModule], providers: BROWSER_NOOP_ANIMATIONS_PROVIDERS, }) -export class NoopAnimationsModule { -} +export class NoopAnimationsModule {} /** * Returns the set of dependency-injection providers diff --git a/packages/platform-browser/animations/src/providers.ts b/packages/platform-browser/animations/src/providers.ts index c9fdef9c2854a..1313efc5cbae8 100644 --- a/packages/platform-browser/animations/src/providers.ts +++ b/packages/platform-browser/animations/src/providers.ts @@ -6,9 +6,27 @@ * found in the LICENSE file at https://angular.io/license */ -import {AnimationDriver, NoopAnimationDriver, ɵAnimationEngine as AnimationEngine, ɵAnimationRendererFactory as AnimationRendererFactory, ɵAnimationStyleNormalizer as AnimationStyleNormalizer, ɵWebAnimationsDriver as WebAnimationsDriver, ɵWebAnimationsStyleNormalizer as WebAnimationsStyleNormalizer} from '@angular/animations/browser'; +import { + AnimationDriver, + NoopAnimationDriver, + ɵAnimationEngine as AnimationEngine, + ɵAnimationRendererFactory as AnimationRendererFactory, + ɵAnimationStyleNormalizer as AnimationStyleNormalizer, + ɵWebAnimationsDriver as WebAnimationsDriver, + ɵWebAnimationsStyleNormalizer as WebAnimationsStyleNormalizer, +} from '@angular/animations/browser'; import {DOCUMENT} from '@angular/common'; -import {ANIMATION_MODULE_TYPE, inject, Inject, Injectable, NgZone, OnDestroy, Provider, RendererFactory2, ɵChangeDetectionScheduler as ChangeDetectionScheduler} from '@angular/core'; +import { + ANIMATION_MODULE_TYPE, + inject, + Inject, + Injectable, + NgZone, + OnDestroy, + Provider, + RendererFactory2, + ɵChangeDetectionScheduler as ChangeDetectionScheduler, +} from '@angular/core'; import {ɵDomRendererFactory2 as DomRendererFactory2} from '@angular/platform-browser'; @Injectable() @@ -17,8 +35,10 @@ export class InjectableAnimationEngine extends AnimationEngine implements OnDest // Since the `ApplicationRef` should be created earlier before the `AnimationEngine`, they // both have `ngOnDestroy` hooks and `flush()` must be called after all views are destroyed. constructor( - @Inject(DOCUMENT) doc: Document, driver: AnimationDriver, - normalizer: AnimationStyleNormalizer) { + @Inject(DOCUMENT) doc: Document, + driver: AnimationDriver, + normalizer: AnimationStyleNormalizer, + ) { super(doc, driver, normalizer, inject(ChangeDetectionScheduler, {optional: true})); } @@ -32,17 +52,21 @@ export function instantiateDefaultStyleNormalizer() { } export function instantiateRendererFactory( - renderer: DomRendererFactory2, engine: AnimationEngine, zone: NgZone) { + renderer: DomRendererFactory2, + engine: AnimationEngine, + zone: NgZone, +) { return new AnimationRendererFactory(renderer, engine, zone); } const SHARED_ANIMATION_PROVIDERS: Provider[] = [ {provide: AnimationStyleNormalizer, useFactory: instantiateDefaultStyleNormalizer}, - {provide: AnimationEngine, useClass: InjectableAnimationEngine}, { + {provide: AnimationEngine, useClass: InjectableAnimationEngine}, + { provide: RendererFactory2, useFactory: instantiateRendererFactory, - deps: [DomRendererFactory2, AnimationEngine, NgZone] - } + deps: [DomRendererFactory2, AnimationEngine, NgZone], + }, ]; /** @@ -51,7 +75,8 @@ const SHARED_ANIMATION_PROVIDERS: Provider[] = [ */ export const BROWSER_ANIMATIONS_PROVIDERS: Provider[] = [ {provide: AnimationDriver, useFactory: () => new WebAnimationsDriver()}, - {provide: ANIMATION_MODULE_TYPE, useValue: 'BrowserAnimations'}, ...SHARED_ANIMATION_PROVIDERS + {provide: ANIMATION_MODULE_TYPE, useValue: 'BrowserAnimations'}, + ...SHARED_ANIMATION_PROVIDERS, ]; /** @@ -60,5 +85,6 @@ export const BROWSER_ANIMATIONS_PROVIDERS: Provider[] = [ */ export const BROWSER_NOOP_ANIMATIONS_PROVIDERS: Provider[] = [ {provide: AnimationDriver, useClass: NoopAnimationDriver}, - {provide: ANIMATION_MODULE_TYPE, useValue: 'NoopAnimations'}, ...SHARED_ANIMATION_PROVIDERS + {provide: ANIMATION_MODULE_TYPE, useValue: 'NoopAnimations'}, + ...SHARED_ANIMATION_PROVIDERS, ]; diff --git a/packages/platform-browser/animations/test/animation_renderer_spec.ts b/packages/platform-browser/animations/test/animation_renderer_spec.ts index aaef9aae74488..6afc4cbd3a086 100644 --- a/packages/platform-browser/animations/test/animation_renderer_spec.ts +++ b/packages/platform-browser/animations/test/animation_renderer_spec.ts @@ -5,472 +5,508 @@ * 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 {animate, AnimationPlayer, AnimationTriggerMetadata, style, transition, trigger} from '@angular/animations'; -import {ɵAnimationEngine as AnimationEngine, ɵAnimationRendererFactory as AnimationRendererFactory,} from '@angular/animations/browser'; -import {APP_INITIALIZER, Component, destroyPlatform, importProvidersFrom, Injectable, NgModule, NgZone, RendererFactory2, RendererType2, ViewChild} from '@angular/core'; +import { + animate, + AnimationPlayer, + AnimationTriggerMetadata, + style, + transition, + trigger, +} from '@angular/animations'; +import { + ɵAnimationEngine as AnimationEngine, + ɵAnimationRendererFactory as AnimationRendererFactory, +} from '@angular/animations/browser'; +import { + APP_INITIALIZER, + Component, + destroyPlatform, + importProvidersFrom, + Injectable, + NgModule, + NgZone, + RendererFactory2, + RendererType2, + ViewChild, +} from '@angular/core'; import {TestBed} from '@angular/core/testing'; import {bootstrapApplication} from '@angular/platform-browser'; import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; -import {BrowserAnimationsModule, ɵInjectableAnimationEngine as InjectableAnimationEngine} from '@angular/platform-browser/animations'; +import { + BrowserAnimationsModule, + ɵInjectableAnimationEngine as InjectableAnimationEngine, +} from '@angular/platform-browser/animations'; import {provideAnimationsAsync} from '@angular/platform-browser/animations/async'; import {DomRendererFactory2} from '@angular/platform-browser/src/dom/dom_renderer'; import {withBody} from '@angular/private/testing'; import {el} from '../../testing/src/browser_util'; -(function() { -if (isNode) return; -describe('AnimationRenderer', () => { - let element: any; - beforeEach(() => { - element = el('
'); +(function () { + if (isNode) return; + describe('AnimationRenderer', () => { + let element: any; + beforeEach(() => { + element = el('
'); - TestBed.configureTestingModule({ - providers: [{provide: AnimationEngine, useClass: MockAnimationEngine}], - imports: [BrowserAnimationsModule] + TestBed.configureTestingModule({ + providers: [{provide: AnimationEngine, useClass: MockAnimationEngine}], + imports: [BrowserAnimationsModule], + }); }); - }); - - function makeRenderer(animationTriggers: any[] = []) { - const type = { - id: 'id', - encapsulation: null!, - styles: [], - data: {'animation': animationTriggers} - }; - return (TestBed.inject(RendererFactory2) as AnimationRendererFactory) - .createRenderer(element, type); - } - - it('should hook into the engine\'s insert operations when appending children', () => { - const renderer = makeRenderer(); - const engine = TestBed.inject(AnimationEngine) as MockAnimationEngine; - const container = el('
'); - renderer.appendChild(container, element); - expect(engine.captures['onInsert'].pop()).toEqual([element]); - }); - - it('should hook into the engine\'s insert operations when inserting a child before another', - () => { - const renderer = makeRenderer(); - const engine = TestBed.inject(AnimationEngine) as MockAnimationEngine; - const container = el('
'); - const element2 = el('
'); - container.appendChild(element2); - - renderer.insertBefore(container, element, element2); - expect(engine.captures['onInsert'].pop()).toEqual([element]); - }); - - it('should hook into the engine\'s insert operations when removing children', () => { - const renderer = makeRenderer(); - const engine = TestBed.inject(AnimationEngine) as MockAnimationEngine; - const container = el('
'); - - renderer.removeChild(container, element); - expect(engine.captures['onRemove'].pop()).toEqual([element]); - }); + function makeRenderer(animationTriggers: any[] = []) { + const type = { + id: 'id', + encapsulation: null!, + styles: [], + data: {'animation': animationTriggers}, + }; + return (TestBed.inject(RendererFactory2) as AnimationRendererFactory).createRenderer( + element, + type, + ); + } - it('should hook into the engine\'s setProperty call if the property begins with `@`', () => { - const renderer = makeRenderer(); - const engine = TestBed.inject(AnimationEngine) as MockAnimationEngine; + it("should hook into the engine's insert operations when appending children", () => { + const renderer = makeRenderer(); + const engine = TestBed.inject(AnimationEngine) as MockAnimationEngine; + const container = el('
'); - renderer.setProperty(element, 'prop', 'value'); - expect(engine.captures['setProperty']).toBeFalsy(); + renderer.appendChild(container, element); + expect(engine.captures['onInsert'].pop()).toEqual([element]); + }); - renderer.setProperty(element, '@prop', 'value'); - expect(engine.captures['setProperty'].pop()).toEqual([element, 'prop', 'value']); - }); + it("should hook into the engine's insert operations when inserting a child before another", () => { + const renderer = makeRenderer(); + const engine = TestBed.inject(AnimationEngine) as MockAnimationEngine; + const container = el('
'); + const element2 = el('
'); + container.appendChild(element2); - // https://github.com/angular/angular/issues/32794 - it('should support nested animation triggers', () => { - makeRenderer([[trigger('myAnimation', [])]]); + renderer.insertBefore(container, element, element2); + expect(engine.captures['onInsert'].pop()).toEqual([element]); + }); - const {triggers} = TestBed.inject(AnimationEngine) as MockAnimationEngine; + it("should hook into the engine's insert operations when removing children", () => { + const renderer = makeRenderer(); + const engine = TestBed.inject(AnimationEngine) as MockAnimationEngine; + const container = el('
'); - expect(triggers.length).toEqual(1); - expect(triggers[0].name).toEqual('myAnimation'); - }); + renderer.removeChild(container, element); + expect(engine.captures['onRemove'].pop()).toEqual([element]); + }); - describe('listen', () => { - it('should hook into the engine\'s listen call if the property begins with `@`', () => { + it("should hook into the engine's setProperty call if the property begins with `@`", () => { const renderer = makeRenderer(); const engine = TestBed.inject(AnimationEngine) as MockAnimationEngine; - const cb = (event: any): boolean => { - return true; - }; + renderer.setProperty(element, 'prop', 'value'); + expect(engine.captures['setProperty']).toBeFalsy(); + + renderer.setProperty(element, '@prop', 'value'); + expect(engine.captures['setProperty'].pop()).toEqual([element, 'prop', 'value']); + }); - renderer.listen(element, 'event', cb); - expect(engine.captures['listen']).toBeFalsy(); + // https://github.com/angular/angular/issues/32794 + it('should support nested animation triggers', () => { + makeRenderer([[trigger('myAnimation', [])]]); - renderer.listen(element, '@event.phase', cb); - expect(engine.captures['listen'].pop()).toEqual([element, 'event', 'phase']); + const {triggers} = TestBed.inject(AnimationEngine) as MockAnimationEngine; + + expect(triggers.length).toEqual(1); + expect(triggers[0].name).toEqual('myAnimation'); }); - it('should resolve the body|document|window nodes given their values as strings as input', - () => { - const renderer = makeRenderer(); - const engine = TestBed.inject(AnimationEngine) as MockAnimationEngine; + describe('listen', () => { + it("should hook into the engine's listen call if the property begins with `@`", () => { + const renderer = makeRenderer(); + const engine = TestBed.inject(AnimationEngine) as MockAnimationEngine; - const cb = (event: any): boolean => { - return true; - }; + const cb = (event: any): boolean => { + return true; + }; - renderer.listen('body', '@event', cb); - expect(engine.captures['listen'].pop()[0]).toBe(document.body); + renderer.listen(element, 'event', cb); + expect(engine.captures['listen']).toBeFalsy(); - renderer.listen('document', '@event', cb); - expect(engine.captures['listen'].pop()[0]).toBe(document); + renderer.listen(element, '@event.phase', cb); + expect(engine.captures['listen'].pop()).toEqual([element, 'event', 'phase']); + }); - renderer.listen('window', '@event', cb); - expect(engine.captures['listen'].pop()[0]).toBe(window); - }); - }); + it('should resolve the body|document|window nodes given their values as strings as input', () => { + const renderer = makeRenderer(); + const engine = TestBed.inject(AnimationEngine) as MockAnimationEngine; - describe('registering animations', () => { - it('should only create a trigger definition once even if the registered multiple times'); - }); + const cb = (event: any): boolean => { + return true; + }; - describe('flushing animations', () => { - // these tests are only meant to be run within the DOM - if (isNode) return; + renderer.listen('body', '@event', cb); + expect(engine.captures['listen'].pop()[0]).toBe(document.body); - it('should flush and fire callbacks when the zone becomes stable', (async) => { - @Component({ - selector: 'my-cmp', - template: '
', - animations: [trigger( - 'myAnimation', - [transition( - '* => state', [style({'opacity': '0'}), animate(500, style({'opacity': '1'}))])])], - }) - class Cmp { - exp: any; - event: any; - onStart(event: any) { - this.event = event; + renderer.listen('document', '@event', cb); + expect(engine.captures['listen'].pop()[0]).toBe(document); + + renderer.listen('window', '@event', cb); + expect(engine.captures['listen'].pop()[0]).toBe(window); + }); + }); + + describe('registering animations', () => { + it('should only create a trigger definition once even if the registered multiple times'); + }); + + describe('flushing animations', () => { + // these tests are only meant to be run within the DOM + if (isNode) return; + + it('should flush and fire callbacks when the zone becomes stable', (async) => { + @Component({ + selector: 'my-cmp', + template: '
', + animations: [ + trigger('myAnimation', [ + transition('* => state', [ + style({'opacity': '0'}), + animate(500, style({'opacity': '1'})), + ]), + ]), + ], + }) + class Cmp { + exp: any; + event: any; + onStart(event: any) { + this.event = event; + } } - } - TestBed.configureTestingModule({ - providers: [{provide: AnimationEngine, useClass: InjectableAnimationEngine}], - declarations: [Cmp] + TestBed.configureTestingModule({ + providers: [{provide: AnimationEngine, useClass: InjectableAnimationEngine}], + declarations: [Cmp], + }); + + const engine = TestBed.inject(AnimationEngine); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; + cmp.exp = 'state'; + fixture.detectChanges(); + fixture.whenStable().then(() => { + expect(cmp.event.triggerName).toEqual('myAnimation'); + expect(cmp.event.phaseName).toEqual('start'); + cmp.event = null; + + engine.flush(); + expect(cmp.event).toBeFalsy(); + async(); + }); }); - const engine = TestBed.inject(AnimationEngine); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; - cmp.exp = 'state'; - fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(cmp.event.triggerName).toEqual('myAnimation'); - expect(cmp.event.phaseName).toEqual('start'); - cmp.event = null; + it('should properly insert/remove nodes through the animation renderer that do not contain animations', (async) => { + @Component({ + selector: 'my-cmp', + template: '
', + animations: [ + trigger('someAnimation', [ + transition('* => *', [ + style({'opacity': '0'}), + animate(500, style({'opacity': '1'})), + ]), + ]), + ], + }) + class Cmp { + exp: any; + @ViewChild('elm') public element: any; + } - engine.flush(); - expect(cmp.event).toBeFalsy(); - async(); + TestBed.configureTestingModule({ + providers: [{provide: AnimationEngine, useClass: InjectableAnimationEngine}], + declarations: [Cmp], + }); + + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; + cmp.exp = true; + fixture.detectChanges(); + + fixture.whenStable().then(() => { + cmp.exp = false; + const element = cmp.element; + expect(element.nativeElement.parentNode).toBeTruthy(); + + fixture.detectChanges(); + fixture.whenStable().then(() => { + expect(element.nativeElement.parentNode).toBeFalsy(); + async(); + }); + }); }); - }); - it('should properly insert/remove nodes through the animation renderer that do not contain animations', - (async) => { - @Component({ - selector: 'my-cmp', - template: '
', - animations: [trigger( - 'someAnimation', - [transition( - '* => *', [style({'opacity': '0'}), animate(500, style({'opacity': '1'}))])])], - }) - class Cmp { - exp: any; - @ViewChild('elm') public element: any; - } - - TestBed.configureTestingModule({ - providers: [{provide: AnimationEngine, useClass: InjectableAnimationEngine}], - declarations: [Cmp] - }); - - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; - cmp.exp = true; - fixture.detectChanges(); - - fixture.whenStable().then(() => { - cmp.exp = false; - const element = cmp.element; - expect(element.nativeElement.parentNode).toBeTruthy(); - - fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(element.nativeElement.parentNode).toBeFalsy(); - async(); - }); - }); - }); - - it('should only queue up dom removals if the element itself contains a valid leave animation', - () => { - @Component({ - selector: 'my-cmp', - template: ` + it('should only queue up dom removals if the element itself contains a valid leave animation', () => { + @Component({ + selector: 'my-cmp', + template: `
`, - animations: [ - trigger('animation1', [transition('a => b', [])]), - trigger('animation2', [transition(':leave', [])]), - ] - }) - class Cmp { - exp1: any = true; - exp2: any = true; - exp3: any = true; - - @ViewChild('elm1') public elm1: any; - - @ViewChild('elm2') public elm2: any; - - @ViewChild('elm3') public elm3: any; - } - - TestBed.configureTestingModule({ - providers: [{provide: AnimationEngine, useClass: InjectableAnimationEngine}], - declarations: [Cmp] - }); - - const engine = TestBed.inject(AnimationEngine); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; - - fixture.detectChanges(); - const elm1 = cmp.elm1; - const elm2 = cmp.elm2; - const elm3 = cmp.elm3; - assertHasParent(elm1); - assertHasParent(elm2); - assertHasParent(elm3); - engine.flush(); - finishPlayers(engine.players); - - cmp.exp1 = false; - fixture.detectChanges(); - assertHasParent(elm1, false); - assertHasParent(elm2); - assertHasParent(elm3); - engine.flush(); - expect(engine.players.length).toEqual(0); - - cmp.exp2 = false; - fixture.detectChanges(); - assertHasParent(elm1, false); - assertHasParent(elm2, false); - assertHasParent(elm3); - engine.flush(); - expect(engine.players.length).toEqual(0); - - cmp.exp3 = false; - fixture.detectChanges(); - assertHasParent(elm1, false); - assertHasParent(elm2, false); - assertHasParent(elm3); - engine.flush(); - expect(engine.players.length).toEqual(1); - }); - }); -}); - -describe('AnimationRendererFactory', () => { - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [{ - provide: RendererFactory2, - useClass: ExtendedAnimationRendererFactory, - deps: [DomRendererFactory2, AnimationEngine, NgZone] - }], - imports: [BrowserAnimationsModule] + animations: [ + trigger('animation1', [transition('a => b', [])]), + trigger('animation2', [transition(':leave', [])]), + ], + }) + class Cmp { + exp1: any = true; + exp2: any = true; + exp3: any = true; + + @ViewChild('elm1') public elm1: any; + + @ViewChild('elm2') public elm2: any; + + @ViewChild('elm3') public elm3: any; + } + + TestBed.configureTestingModule({ + providers: [{provide: AnimationEngine, useClass: InjectableAnimationEngine}], + declarations: [Cmp], + }); + + const engine = TestBed.inject(AnimationEngine); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; + + fixture.detectChanges(); + const elm1 = cmp.elm1; + const elm2 = cmp.elm2; + const elm3 = cmp.elm3; + assertHasParent(elm1); + assertHasParent(elm2); + assertHasParent(elm3); + engine.flush(); + finishPlayers(engine.players); + + cmp.exp1 = false; + fixture.detectChanges(); + assertHasParent(elm1, false); + assertHasParent(elm2); + assertHasParent(elm3); + engine.flush(); + expect(engine.players.length).toEqual(0); + + cmp.exp2 = false; + fixture.detectChanges(); + assertHasParent(elm1, false); + assertHasParent(elm2, false); + assertHasParent(elm3); + engine.flush(); + expect(engine.players.length).toEqual(0); + + cmp.exp3 = false; + fixture.detectChanges(); + assertHasParent(elm1, false); + assertHasParent(elm2, false); + assertHasParent(elm3); + engine.flush(); + expect(engine.players.length).toEqual(1); + }); }); }); - it('should provide hooks at the start and end of change detection', () => { - @Component({ - selector: 'my-cmp', - template: ` + describe('AnimationRendererFactory', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + { + provide: RendererFactory2, + useClass: ExtendedAnimationRendererFactory, + deps: [DomRendererFactory2, AnimationEngine, NgZone], + }, + ], + imports: [BrowserAnimationsModule], + }); + }); + + it('should provide hooks at the start and end of change detection', () => { + @Component({ + selector: 'my-cmp', + template: `
`, - animations: [trigger('myAnimation', [])] - }) - class Cmp { - public exp: any; - } + animations: [trigger('myAnimation', [])], + }) + class Cmp { + public exp: any; + } + + TestBed.configureTestingModule({ + providers: [{provide: AnimationEngine, useClass: InjectableAnimationEngine}], + declarations: [Cmp], + }); - TestBed.configureTestingModule({ - providers: [{provide: AnimationEngine, useClass: InjectableAnimationEngine}], - declarations: [Cmp] + const renderer = TestBed.inject(RendererFactory2) as ExtendedAnimationRendererFactory; + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; + + renderer.log = []; + fixture.detectChanges(); + expect(renderer.log).toEqual(['begin', 'end']); + + renderer.log = []; + fixture.detectChanges(); + expect(renderer.log).toEqual(['begin', 'end']); }); + }); - const renderer = TestBed.inject(RendererFactory2) as ExtendedAnimationRendererFactory; - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; + describe('destroy', () => { + beforeEach(destroyPlatform); + afterEach(destroyPlatform); + + // See https://github.com/angular/angular/issues/39955 + it( + 'should clear bootstrapped component contents when the `BrowserAnimationsModule` is imported', + withBody('
before
after
', async () => { + @Component({selector: 'app-root', template: 'app-root content'}) + class AppComponent {} + + @NgModule({ + imports: [BrowserAnimationsModule], + declarations: [AppComponent], + bootstrap: [AppComponent], + }) + class AppModule {} + + const ngModuleRef = await platformBrowserDynamic().bootstrapModule(AppModule); + + const root = document.body.querySelector('app-root')!; + expect(root.textContent).toEqual('app-root content'); + expect(document.body.childNodes.length).toEqual(3); + + ngModuleRef.destroy(); + + expect(document.body.querySelector('app-root')).toBeFalsy(); // host element is removed + expect(document.body.childNodes.length).toEqual(2); // other elements are preserved + }), + ); + + // See https://github.com/angular/angular/issues/45108 + it( + 'should clear bootstrapped component contents when the animation engine is requested during initialization', + withBody('
before
after
', async () => { + @Injectable({providedIn: 'root'}) + class AppService { + // The `RendererFactory2` is injected here explicitly because we import the + // `BrowserAnimationsModule`. The `RendererFactory2` will be provided with + // `AnimationRendererFactory` which relies on the `AnimationEngine`. We want to ensure + // that `ApplicationRef` is created earlier before the `AnimationEngine`, because + // previously the `AnimationEngine` was created before the `ApplicationRef` (see the + // link above to the original issue). The `ApplicationRef` was created after + // `APP_INITIALIZER` has been run but before the root module is bootstrapped. + constructor(rendererFactory: RendererFactory2) {} + } - renderer.log = []; - fixture.detectChanges(); - expect(renderer.log).toEqual(['begin', 'end']); + @Component({selector: 'app-root', template: 'app-root content'}) + class AppComponent {} + + @NgModule({ + imports: [BrowserAnimationsModule], + declarations: [AppComponent], + bootstrap: [AppComponent], + providers: [ + // The `APP_INITIALIZER` token is requested before the root module is bootstrapped, + // the `useFactory` just injects the `AppService` that injects the + // `RendererFactory2`. + { + provide: APP_INITIALIZER, + useFactory: () => (appService: AppService) => appService, + deps: [AppService], + multi: true, + }, + ], + }) + class AppModule {} + + const ngModuleRef = await platformBrowserDynamic().bootstrapModule(AppModule); + + const root = document.body.querySelector('app-root')!; + expect(root.textContent).toEqual('app-root content'); + expect(document.body.childNodes.length).toEqual(3); + + ngModuleRef.destroy(); + + expect(document.body.querySelector('app-root')).toBeFalsy(); // host element is removed + expect(document.body.childNodes.length).toEqual(2); // other elements are preserved + }), + ); + + // See https://github.com/angular/angular/issues/45108 + it( + 'should clear standalone bootstrapped component contents when the animation engine is requested during initialization', + withBody('
before
after
', async () => { + @Injectable({providedIn: 'root'}) + class AppService { + // The `RendererFactory2` is injected here explicitly because we import the + // `BrowserAnimationsModule`. The `RendererFactory2` will be provided with + // `AnimationRendererFactory` which relies on the `AnimationEngine`. We want to ensure + // that `ApplicationRef` is created earlier before the `AnimationEngine`, because + // previously the `AnimationEngine` was created before the `ApplicationRef` (see the + // link above to the original issue). The `ApplicationRef` was created after + // `APP_INITIALIZER` has been run but before the root module is bootstrapped. + constructor(rendererFactory: RendererFactory2) {} + } - renderer.log = []; - fixture.detectChanges(); - expect(renderer.log).toEqual(['begin', 'end']); + @Component({selector: 'app-root', template: 'app-root content', standalone: true}) + class AppComponent {} + + const appRef = await bootstrapApplication(AppComponent, { + providers: [ + importProvidersFrom(BrowserAnimationsModule), + // The `APP_INITIALIZER` token is requested before the standalone component is + // bootstrapped, the `useFactory` just injects the `AppService` that injects the + // `RendererFactory2`. + { + provide: APP_INITIALIZER, + useFactory: () => (appService: AppService) => appService, + deps: [AppService], + multi: true, + }, + ], + }); + + const root = document.body.querySelector('app-root')!; + expect(root.textContent).toEqual('app-root content'); + expect(document.body.childNodes.length).toEqual(3); + + appRef.destroy(); + + expect(document.body.querySelector('app-root')).toBeFalsy(); // host element is removed + expect(document.body.childNodes.length).toEqual(2); // other elements are + }), + ); + + it( + 'should clear bootstrapped component contents when async animations are used', + withBody('
before
after
', async () => { + @Component({selector: 'app-root', template: 'app-root content', standalone: true}) + class AppComponent {} + + const appRef = await bootstrapApplication(AppComponent, { + providers: [provideAnimationsAsync()], + }); + + const root = document.body.querySelector('app-root')!; + expect(root.textContent).toEqual('app-root content'); + expect(document.body.childNodes.length).toEqual(3); + + appRef.destroy(); + + expect(document.body.querySelector('app-root')).toBeFalsy(); // host element is removed + expect(document.body.childNodes.length).toEqual(2); // other elements are + }), + ); }); -}); - -describe('destroy', () => { - beforeEach(destroyPlatform); - afterEach(destroyPlatform); - - // See https://github.com/angular/angular/issues/39955 - it('should clear bootstrapped component contents when the `BrowserAnimationsModule` is imported', - withBody('
before
after
', async () => { - @Component({selector: 'app-root', template: 'app-root content'}) - class AppComponent { - } - - @NgModule({ - imports: [BrowserAnimationsModule], - declarations: [AppComponent], - bootstrap: [AppComponent] - }) - class AppModule { - } - - const ngModuleRef = await platformBrowserDynamic().bootstrapModule(AppModule); - - const root = document.body.querySelector('app-root')!; - expect(root.textContent).toEqual('app-root content'); - expect(document.body.childNodes.length).toEqual(3); - - ngModuleRef.destroy(); - - expect(document.body.querySelector('app-root')).toBeFalsy(); // host element is removed - expect(document.body.childNodes.length).toEqual(2); // other elements are preserved - })); - - // See https://github.com/angular/angular/issues/45108 - it('should clear bootstrapped component contents when the animation engine is requested during initialization', - withBody('
before
after
', async () => { - @Injectable({providedIn: 'root'}) - class AppService { - // The `RendererFactory2` is injected here explicitly because we import the - // `BrowserAnimationsModule`. The `RendererFactory2` will be provided with - // `AnimationRendererFactory` which relies on the `AnimationEngine`. We want to ensure that - // `ApplicationRef` is created earlier before the `AnimationEngine`, because previously the - // `AnimationEngine` was created before the `ApplicationRef` (see the link above to the - // original issue). The `ApplicationRef` was created after `APP_INITIALIZER` has been run - // but before the root module is bootstrapped. - constructor(rendererFactory: RendererFactory2) {} - } - - @Component({selector: 'app-root', template: 'app-root content'}) - class AppComponent { - } - - @NgModule({ - imports: [BrowserAnimationsModule], - declarations: [AppComponent], - bootstrap: [AppComponent], - providers: [ - // The `APP_INITIALIZER` token is requested before the root module is bootstrapped, the - // `useFactory` just injects the `AppService` that injects the `RendererFactory2`. - { - provide: APP_INITIALIZER, - useFactory: () => (appService: AppService) => appService, - deps: [AppService], - multi: true - } - ] - }) - class AppModule { - } - - const ngModuleRef = await platformBrowserDynamic().bootstrapModule(AppModule); - - const root = document.body.querySelector('app-root')!; - expect(root.textContent).toEqual('app-root content'); - expect(document.body.childNodes.length).toEqual(3); - - ngModuleRef.destroy(); - - expect(document.body.querySelector('app-root')).toBeFalsy(); // host element is removed - expect(document.body.childNodes.length).toEqual(2); // other elements are preserved - })); - - // See https://github.com/angular/angular/issues/45108 - it('should clear standalone bootstrapped component contents when the animation engine is requested during initialization', - withBody('
before
after
', async () => { - @Injectable({providedIn: 'root'}) - class AppService { - // The `RendererFactory2` is injected here explicitly because we import the - // `BrowserAnimationsModule`. The `RendererFactory2` will be provided with - // `AnimationRendererFactory` which relies on the `AnimationEngine`. We want to ensure - // that `ApplicationRef` is created earlier before the `AnimationEngine`, because - // previously the `AnimationEngine` was created before the `ApplicationRef` (see the link - // above to the original issue). The `ApplicationRef` was created after `APP_INITIALIZER` - // has been run but before the root module is bootstrapped. - constructor(rendererFactory: RendererFactory2) {} - } - - @Component({selector: 'app-root', template: 'app-root content', standalone: true}) - class AppComponent { - } - - const appRef = await bootstrapApplication(AppComponent, { - providers: [ - importProvidersFrom(BrowserAnimationsModule), - // The `APP_INITIALIZER` token is requested before the standalone component is - // bootstrapped, the `useFactory` just injects the `AppService` that injects the - // `RendererFactory2`. - { - provide: APP_INITIALIZER, - useFactory: () => (appService: AppService) => appService, - deps: [AppService], - multi: true - } - ] - }); - - const root = document.body.querySelector('app-root')!; - expect(root.textContent).toEqual('app-root content'); - expect(document.body.childNodes.length).toEqual(3); - - appRef.destroy(); - - expect(document.body.querySelector('app-root')).toBeFalsy(); // host element is removed - expect(document.body.childNodes.length).toEqual(2); // other elements are - })); - - it('should clear bootstrapped component contents when async animations are used', - withBody('
before
after
', async () => { - @Component({selector: 'app-root', template: 'app-root content', standalone: true}) - class AppComponent { - } - - const appRef = - await bootstrapApplication(AppComponent, {providers: [provideAnimationsAsync()]}); - - const root = document.body.querySelector('app-root')!; - expect(root.textContent).toEqual('app-root content'); - expect(document.body.childNodes.length).toEqual(3); - - appRef.destroy(); - - expect(document.body.querySelector('app-root')).toBeFalsy(); // host element is removed - expect(document.body.childNodes.length).toEqual(2); // other elements are - })); -}); })(); @Injectable() @@ -479,13 +515,17 @@ class MockAnimationEngine extends InjectableAnimationEngine { triggers: AnimationTriggerMetadata[] = []; private _capture(name: string, args: any[]) { - const data = this.captures[name] = this.captures[name] || []; + const data = (this.captures[name] = this.captures[name] || []); data.push(args); } override registerTrigger( - componentId: string, namespaceId: string, hostElement: any, name: string, - metadata: AnimationTriggerMetadata): void { + componentId: string, + namespaceId: string, + hostElement: any, + name: string, + metadata: AnimationTriggerMetadata, + ): void { this.triggers.push(metadata); } @@ -503,8 +543,12 @@ class MockAnimationEngine extends InjectableAnimationEngine { } override listen( - namespaceId: string, element: any, eventName: string, eventPhase: string, - callback: (event: any) => any): () => void { + namespaceId: string, + element: any, + eventName: string, + eventPhase: string, + callback: (event: any) => any, + ): () => void { // we don't capture the callback here since the renderer wraps it in a zone this._capture('listen', [element, eventName, eventPhase]); return () => {}; @@ -530,7 +574,6 @@ class ExtendedAnimationRendererFactory extends AnimationRendererFactory { } } - function assertHasParent(element: any, yes: boolean = true) { const parent = element.nativeElement.parentNode; if (yes) { @@ -541,5 +584,5 @@ function assertHasParent(element: any, yes: boolean = true) { } function finishPlayers(players: AnimationPlayer[]) { - players.forEach(player => player.finish()); + players.forEach((player) => player.finish()); } diff --git a/packages/platform-browser/animations/test/noop_animations_module_spec.ts b/packages/platform-browser/animations/test/noop_animations_module_spec.ts index 22c21d86fc1da..3224ee7a03f08 100644 --- a/packages/platform-browser/animations/test/noop_animations_module_spec.ts +++ b/packages/platform-browser/animations/test/noop_animations_module_spec.ts @@ -9,7 +9,11 @@ import {animate, style, transition, trigger} from '@angular/animations'; import {ɵAnimationEngine} from '@angular/animations/browser'; import {Component} from '@angular/core'; import {TestBed} from '@angular/core/testing'; -import {BrowserAnimationsModule, NoopAnimationsModule, provideNoopAnimations} from '@angular/platform-browser/animations'; +import { + BrowserAnimationsModule, + NoopAnimationsModule, + provideNoopAnimations, +} from '@angular/platform-browser/animations'; describe('NoopAnimationsModule', () => { beforeEach(() => { @@ -21,8 +25,9 @@ describe('NoopAnimationsModule', () => { describe('BrowserAnimationsModule with disableAnimations = true', () => { beforeEach(() => { - TestBed.configureTestingModule( - {imports: [BrowserAnimationsModule.withConfig({disableAnimations: true})]}); + TestBed.configureTestingModule({ + imports: [BrowserAnimationsModule.withConfig({disableAnimations: true})], + }); }); noopAnimationTests(); @@ -36,7 +41,6 @@ describe('provideNoopAnimations()', () => { noopAnimationTests(); }); - function noopAnimationTests() { it('should flush and fire callbacks when the zone becomes stable', (done) => { // This test is only meant to be run inside the browser. @@ -48,11 +52,15 @@ function noopAnimationTests() { @Component({ selector: 'my-cmp', template: - '
', - animations: [trigger( - 'myAnimation', - [transition( - '* => state', [style({'opacity': '0'}), animate(500, style({'opacity': '1'}))])])], + '
', + animations: [ + trigger('myAnimation', [ + transition('* => state', [ + style({'opacity': '0'}), + animate(500, style({'opacity': '1'})), + ]), + ]), + ], }) class Cmp { exp: any; @@ -81,57 +89,57 @@ function noopAnimationTests() { }); }); - it('should handle leave animation callbacks even if the element is destroyed in the process', - (async) => { - // This test is only meant to be run inside the browser. - if (isNode) { - async(); - return; - } - - @Component({ - selector: 'my-cmp', - template: - '
', - animations: [trigger( - 'myAnimation', - [transition( - ':leave', [style({'opacity': '0'}), animate(500, style({'opacity': '1'}))])])], - }) - class Cmp { - exp: any; - startEvent: any; - doneEvent: any; - onStart(event: any) { - this.startEvent = event; - } - onDone(event: any) { - this.doneEvent = event; - } - } + it('should handle leave animation callbacks even if the element is destroyed in the process', (async) => { + // This test is only meant to be run inside the browser. + if (isNode) { + async(); + return; + } - TestBed.configureTestingModule({declarations: [Cmp]}); - const engine = TestBed.inject(ɵAnimationEngine); - const fixture = TestBed.createComponent(Cmp); - const cmp = fixture.componentInstance; + @Component({ + selector: 'my-cmp', + template: + '
', + animations: [ + trigger('myAnimation', [ + transition(':leave', [style({'opacity': '0'}), animate(500, style({'opacity': '1'}))]), + ]), + ], + }) + class Cmp { + exp: any; + startEvent: any; + doneEvent: any; + onStart(event: any) { + this.startEvent = event; + } + onDone(event: any) { + this.doneEvent = event; + } + } - cmp.exp = true; - fixture.detectChanges(); - fixture.whenStable().then(() => { - cmp.startEvent = null; - cmp.doneEvent = null; + TestBed.configureTestingModule({declarations: [Cmp]}); + const engine = TestBed.inject(ɵAnimationEngine); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; - cmp.exp = false; - fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(cmp.startEvent.triggerName).toEqual('myAnimation'); - expect(cmp.startEvent.phaseName).toEqual('start'); - expect(cmp.startEvent.toState).toEqual('void'); - expect(cmp.doneEvent.triggerName).toEqual('myAnimation'); - expect(cmp.doneEvent.phaseName).toEqual('done'); - expect(cmp.doneEvent.toState).toEqual('void'); - async(); - }); - }); - }); + cmp.exp = true; + fixture.detectChanges(); + fixture.whenStable().then(() => { + cmp.startEvent = null; + cmp.doneEvent = null; + + cmp.exp = false; + fixture.detectChanges(); + fixture.whenStable().then(() => { + expect(cmp.startEvent.triggerName).toEqual('myAnimation'); + expect(cmp.startEvent.phaseName).toEqual('start'); + expect(cmp.startEvent.toState).toEqual('void'); + expect(cmp.doneEvent.triggerName).toEqual('myAnimation'); + expect(cmp.doneEvent.phaseName).toEqual('done'); + expect(cmp.doneEvent.toState).toEqual('void'); + async(); + }); + }); + }); } diff --git a/packages/platform-browser/src/browser.ts b/packages/platform-browser/src/browser.ts index 4d9278fd69bc5..4c7bc9d739a58 100644 --- a/packages/platform-browser/src/browser.ts +++ b/packages/platform-browser/src/browser.ts @@ -6,8 +6,43 @@ * found in the LICENSE file at https://angular.io/license */ -import {CommonModule, DOCUMENT, XhrFactory, ɵPLATFORM_BROWSER_ID as PLATFORM_BROWSER_ID} from '@angular/common'; -import {APP_ID, ApplicationConfig as ApplicationConfigFromCore, ApplicationModule, ApplicationRef, createPlatformFactory, ErrorHandler, Inject, InjectionToken, ModuleWithProviders, NgModule, NgZone, Optional, PLATFORM_ID, PLATFORM_INITIALIZER, platformCore, PlatformRef, Provider, RendererFactory2, SkipSelf, StaticProvider, Testability, TestabilityRegistry, Type, ɵINJECTOR_SCOPE as INJECTOR_SCOPE, ɵinternalCreateApplication as internalCreateApplication, ɵRuntimeError as RuntimeError, ɵsetDocument, ɵTESTABILITY as TESTABILITY, ɵTESTABILITY_GETTER as TESTABILITY_GETTER} from '@angular/core'; +import { + CommonModule, + DOCUMENT, + XhrFactory, + ɵPLATFORM_BROWSER_ID as PLATFORM_BROWSER_ID, +} from '@angular/common'; +import { + APP_ID, + ApplicationConfig as ApplicationConfigFromCore, + ApplicationModule, + ApplicationRef, + createPlatformFactory, + ErrorHandler, + Inject, + InjectionToken, + ModuleWithProviders, + NgModule, + NgZone, + Optional, + PLATFORM_ID, + PLATFORM_INITIALIZER, + platformCore, + PlatformRef, + Provider, + RendererFactory2, + SkipSelf, + StaticProvider, + Testability, + TestabilityRegistry, + Type, + ɵINJECTOR_SCOPE as INJECTOR_SCOPE, + ɵinternalCreateApplication as internalCreateApplication, + ɵRuntimeError as RuntimeError, + ɵsetDocument, + ɵTESTABILITY as TESTABILITY, + ɵTESTABILITY_GETTER as TESTABILITY_GETTER, +} from '@angular/core'; import {BrowserDomAdapter} from './browser/browser_adapter'; import {BrowserGetTestability} from './browser/testability'; @@ -19,7 +54,6 @@ import {KeyEventsPlugin} from './dom/events/key_events'; import {SharedStylesHost} from './dom/shared_styles_host'; import {RuntimeErrorCode} from './errors'; - /** * Set of config options available during the application bootstrap operation. * @@ -92,7 +126,9 @@ export {ApplicationConfig}; * @publicApi */ export function bootstrapApplication( - rootComponent: Type, options?: ApplicationConfig): Promise { + rootComponent: Type, + options?: ApplicationConfig, +): Promise { return internalCreateApplication({rootComponent, ...createProvidersConfig(options)}); } @@ -114,11 +150,8 @@ export function createApplication(options?: ApplicationConfig) { function createProvidersConfig(options?: ApplicationConfig) { return { - appProviders: [ - ...BROWSER_MODULE_PROVIDERS, - ...(options?.providers ?? []), - ], - platformProviders: INTERNAL_BROWSER_PLATFORM_PROVIDERS + appProviders: [...BROWSER_MODULE_PROVIDERS, ...(options?.providers ?? [])], + platformProviders: INTERNAL_BROWSER_PLATFORM_PROVIDERS, }; } @@ -167,7 +200,7 @@ export const INTERNAL_BROWSER_PLATFORM_PROVIDERS: StaticProvider[] = [ * @publicApi */ export const platformBrowser: (extraProviders?: StaticProvider[]) => PlatformRef = - createPlatformFactory(platformCore, 'browser', INTERNAL_BROWSER_PLATFORM_PROVIDERS); + createPlatformFactory(platformCore, 'browser', INTERNAL_BROWSER_PLATFORM_PROVIDERS); /** * Internal marker to signal whether providers from the `BrowserModule` are already present in DI. @@ -176,7 +209,8 @@ export const platformBrowser: (extraProviders?: StaticProvider[]) => PlatformRef * `BrowserModule` providers without referencing the module itself. */ const BROWSER_MODULE_PROVIDERS_MARKER = new InjectionToken( - (typeof ngDevMode === 'undefined' || ngDevMode) ? 'BrowserModule Providers Marker' : ''); + typeof ngDevMode === 'undefined' || ngDevMode ? 'BrowserModule Providers Marker' : '', +); const TESTABILITY_PROVIDERS = [ { @@ -187,30 +221,33 @@ const TESTABILITY_PROVIDERS = [ { provide: TESTABILITY, useClass: Testability, - deps: [NgZone, TestabilityRegistry, TESTABILITY_GETTER] + deps: [NgZone, TestabilityRegistry, TESTABILITY_GETTER], }, { - provide: Testability, // Also provide as `Testability` for backwards-compatibility. + provide: Testability, // Also provide as `Testability` for backwards-compatibility. useClass: Testability, - deps: [NgZone, TestabilityRegistry, TESTABILITY_GETTER] - } + deps: [NgZone, TestabilityRegistry, TESTABILITY_GETTER], + }, ]; const BROWSER_MODULE_PROVIDERS: Provider[] = [ {provide: INJECTOR_SCOPE, useValue: 'root'}, - {provide: ErrorHandler, useFactory: errorHandler, deps: []}, { + {provide: ErrorHandler, useFactory: errorHandler, deps: []}, + { provide: EVENT_MANAGER_PLUGINS, useClass: DomEventsPlugin, multi: true, - deps: [DOCUMENT, NgZone, PLATFORM_ID] + deps: [DOCUMENT, NgZone, PLATFORM_ID], }, {provide: EVENT_MANAGER_PLUGINS, useClass: KeyEventsPlugin, multi: true, deps: [DOCUMENT]}, - DomRendererFactory2, SharedStylesHost, EventManager, + DomRendererFactory2, + SharedStylesHost, + EventManager, {provide: RendererFactory2, useExisting: DomRendererFactory2}, {provide: XhrFactory, useClass: BrowserXhr, deps: []}, - (typeof ngDevMode === 'undefined' || ngDevMode) ? - {provide: BROWSER_MODULE_PROVIDERS_MARKER, useValue: true} : - [] + typeof ngDevMode === 'undefined' || ngDevMode + ? {provide: BROWSER_MODULE_PROVIDERS_MARKER, useValue: true} + : [], ]; /** @@ -227,13 +264,18 @@ const BROWSER_MODULE_PROVIDERS: Provider[] = [ exports: [CommonModule, ApplicationModule], }) export class BrowserModule { - constructor(@Optional() @SkipSelf() @Inject(BROWSER_MODULE_PROVIDERS_MARKER) - providersAlreadyPresent: boolean|null) { + constructor( + @Optional() + @SkipSelf() + @Inject(BROWSER_MODULE_PROVIDERS_MARKER) + providersAlreadyPresent: boolean | null, + ) { if ((typeof ngDevMode === 'undefined' || ngDevMode) && providersAlreadyPresent) { throw new RuntimeError( - RuntimeErrorCode.BROWSER_MODULE_ALREADY_LOADED, - `Providers from the \`BrowserModule\` have already been loaded. If you need access ` + - `to common directives such as NgIf and NgFor, import the \`CommonModule\` instead.`); + RuntimeErrorCode.BROWSER_MODULE_ALREADY_LOADED, + `Providers from the \`BrowserModule\` have already been loaded. If you need access ` + + `to common directives such as NgIf and NgFor, import the \`CommonModule\` instead.`, + ); } } @@ -250,9 +292,7 @@ export class BrowserModule { static withServerTransition(params: {appId: string}): ModuleWithProviders { return { ngModule: BrowserModule, - providers: [ - {provide: APP_ID, useValue: params.appId}, - ], + providers: [{provide: APP_ID, useValue: params.appId}], }; } } diff --git a/packages/platform-browser/src/browser/browser_adapter.ts b/packages/platform-browser/src/browser/browser_adapter.ts index b56c97c08cf9d..96e5a3f937e0c 100644 --- a/packages/platform-browser/src/browser/browser_adapter.ts +++ b/packages/platform-browser/src/browser/browser_adapter.ts @@ -6,7 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import {ɵparseCookieValue as parseCookieValue, ɵsetRootDomAdapter as setRootDomAdapter} from '@angular/common'; +import { + ɵparseCookieValue as parseCookieValue, + ɵsetRootDomAdapter as setRootDomAdapter, +} from '@angular/common'; import {GenericBrowserDomAdapter} from './generic_browser_adapter'; @@ -56,7 +59,7 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter { } /** @deprecated No longer being used in Ivy code. To be removed in version 14. */ - override getGlobalEventTarget(doc: Document, target: string): EventTarget|null { + override getGlobalEventTarget(doc: Document, target: string): EventTarget | null { if (target === 'window') { return window; } @@ -68,7 +71,7 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter { } return null; } - override getBaseHref(doc: Document): string|null { + override getBaseHref(doc: Document): string | null { const href = getBaseElementHref(); return href == null ? null : relativePath(href); } @@ -78,13 +81,13 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter { override getUserAgent(): string { return window.navigator.userAgent; } - override getCookie(name: string): string|null { + override getCookie(name: string): string | null { return parseCookieValue(document.cookie, name); } } -let baseElement: HTMLElement|null = null; -function getBaseElementHref(): string|null { +let baseElement: HTMLElement | null = null; +function getBaseElementHref(): string | null { baseElement = baseElement || document.querySelector('base'); return baseElement ? baseElement.getAttribute('href') : null; } diff --git a/packages/platform-browser/src/browser/generic_browser_adapter.ts b/packages/platform-browser/src/browser/generic_browser_adapter.ts index b3609eefee9a9..75e5564af4e87 100644 --- a/packages/platform-browser/src/browser/generic_browser_adapter.ts +++ b/packages/platform-browser/src/browser/generic_browser_adapter.ts @@ -8,8 +8,6 @@ import {ɵDomAdapter as DomAdapter} from '@angular/common'; - - /** * Provides DOM operations in any browser environment. * diff --git a/packages/platform-browser/src/browser/meta.ts b/packages/platform-browser/src/browser/meta.ts index a223b99e399b7..21fcac404b019 100644 --- a/packages/platform-browser/src/browser/meta.ts +++ b/packages/platform-browser/src/browser/meta.ts @@ -28,7 +28,7 @@ export type MetaDefinition = { property?: string; scheme?: string; url?: string; -}&{ +} & { // TODO(IgorMinar): this type looks wrong [prop: string]: string; }; @@ -71,7 +71,7 @@ export class Meta { * @returns The existing element with the same attributes and values if found, * the new element if no match is found, or `null` if the tag parameter is not defined. */ - addTag(tag: MetaDefinition, forceCreation: boolean = false): HTMLMetaElement|null { + addTag(tag: MetaDefinition, forceCreation: boolean = false): HTMLMetaElement | null { if (!tag) return null; return this._getOrCreateElement(tag, forceCreation); } @@ -100,7 +100,7 @@ export class Meta { * `"tag_attribute='value string'"`. * @returns The matching element, if any. */ - getTag(attrSelector: string): HTMLMetaElement|null { + getTag(attrSelector: string): HTMLMetaElement | null { if (!attrSelector) return null; return this._doc.querySelector(`meta[${attrSelector}]`) || null; } @@ -126,7 +126,7 @@ export class Meta { * replacement tag. * @return The modified element. */ - updateTag(tag: MetaDefinition, selector?: string): HTMLMetaElement|null { + updateTag(tag: MetaDefinition, selector?: string): HTMLMetaElement | null { if (!tag) return null; selector = selector || this._parseSelector(tag); const meta: HTMLMetaElement = this.getTag(selector)!; @@ -155,14 +155,16 @@ export class Meta { } } - private _getOrCreateElement(meta: MetaDefinition, forceCreation: boolean = false): - HTMLMetaElement { + private _getOrCreateElement( + meta: MetaDefinition, + forceCreation: boolean = false, + ): HTMLMetaElement { if (!forceCreation) { const selector: string = this._parseSelector(meta); // It's allowed to have multiple elements with the same name so it's not enough to // just check that element with the same name already present on the page. We also need to // check if element has tag attributes - const elem = this.getTags(selector).filter(elem => this._containsAttributes(meta, elem))[0]; + const elem = this.getTags(selector).filter((elem) => this._containsAttributes(meta, elem))[0]; if (elem !== undefined) return elem; } const element: HTMLMetaElement = this._dom.createElement('meta') as HTMLMetaElement; @@ -173,8 +175,9 @@ export class Meta { } private _setMetaElementAttributes(tag: MetaDefinition, el: HTMLMetaElement): HTMLMetaElement { - Object.keys(tag).forEach( - (prop: string) => el.setAttribute(this._getMetaKeyMap(prop), tag[prop])); + Object.keys(tag).forEach((prop: string) => + el.setAttribute(this._getMetaKeyMap(prop), tag[prop]), + ); return el; } @@ -185,7 +188,8 @@ export class Meta { private _containsAttributes(tag: MetaDefinition, elem: HTMLMetaElement): boolean { return Object.keys(tag).every( - (key: string) => elem.getAttribute(this._getMetaKeyMap(key)) === tag[key]); + (key: string) => elem.getAttribute(this._getMetaKeyMap(key)) === tag[key], + ); } private _getMetaKeyMap(prop: string): string { @@ -196,6 +200,6 @@ export class Meta { /** * Mapping for MetaDefinition properties with their correct meta attribute names */ -const META_KEYS_MAP: {[prop: string]: string;} = { - httpEquiv: 'http-equiv' +const META_KEYS_MAP: {[prop: string]: string} = { + httpEquiv: 'http-equiv', }; diff --git a/packages/platform-browser/src/browser/testability.ts b/packages/platform-browser/src/browser/testability.ts index b383b514f8aaa..43529610b9a7a 100644 --- a/packages/platform-browser/src/browser/testability.ts +++ b/packages/platform-browser/src/browser/testability.ts @@ -7,7 +7,13 @@ */ import {ɵgetDOM as getDOM} from '@angular/common'; -import {GetTestability, Testability, TestabilityRegistry, ɵglobal as global, ɵRuntimeError as RuntimeError} from '@angular/core'; +import { + GetTestability, + Testability, + TestabilityRegistry, + ɵglobal as global, + ɵRuntimeError as RuntimeError, +} from '@angular/core'; import {RuntimeErrorCode} from '../errors'; @@ -17,9 +23,10 @@ export class BrowserGetTestability implements GetTestability { const testability = registry.findTestabilityInTree(elem, findInAncestors); if (testability == null) { throw new RuntimeError( - RuntimeErrorCode.TESTABILITY_NOT_FOUND, - (typeof ngDevMode === 'undefined' || ngDevMode) && - 'Could not find testability for element.'); + RuntimeErrorCode.TESTABILITY_NOT_FOUND, + (typeof ngDevMode === 'undefined' || ngDevMode) && + 'Could not find testability for element.', + ); } return testability; }; @@ -31,7 +38,7 @@ export class BrowserGetTestability implements GetTestability { const whenAllStable = (callback: () => void) => { const testabilities = global['getAllAngularTestabilities']() as Testability[]; let count = testabilities.length; - const decrement = function() { + const decrement = function () { count--; if (count == 0) { callback(); @@ -48,8 +55,11 @@ export class BrowserGetTestability implements GetTestability { global['frameworkStabilizers'].push(whenAllStable); } - findTestabilityInTree(registry: TestabilityRegistry, elem: any, findInAncestors: boolean): - Testability|null { + findTestabilityInTree( + registry: TestabilityRegistry, + elem: any, + findInAncestors: boolean, + ): Testability | null { if (elem == null) { return null; } diff --git a/packages/platform-browser/src/browser/tools/common_tools.ts b/packages/platform-browser/src/browser/tools/common_tools.ts index 9294008e2d45b..217208728e4b2 100644 --- a/packages/platform-browser/src/browser/tools/common_tools.ts +++ b/packages/platform-browser/src/browser/tools/common_tools.ts @@ -9,7 +9,10 @@ import {ApplicationRef, ComponentRef} from '@angular/core'; export class ChangeDetectionPerfRecord { - constructor(public msPerTick: number, public numTicks: number) {} + constructor( + public msPerTick: number, + public numTicks: number, + ) {} } /** @@ -49,7 +52,7 @@ export class AngularProfiler { } const start = performance.now(); let numTicks = 0; - while (numTicks < 5 || (performance.now() - start) < 500) { + while (numTicks < 5 || performance.now() - start < 500) { this.appRef.tick(); numTicks++; } diff --git a/packages/platform-browser/src/browser/tools/tools.ts b/packages/platform-browser/src/browser/tools/tools.ts index 87191bde486b7..ff5a74872aabe 100644 --- a/packages/platform-browser/src/browser/tools/tools.ts +++ b/packages/platform-browser/src/browser/tools/tools.ts @@ -7,7 +7,9 @@ */ import {ComponentRef} from '@angular/core'; + import {exportNgVar} from '../../dom/util'; + import {AngularProfiler} from './common_tools'; const PROFILER_GLOBAL_NAME = 'profiler'; diff --git a/packages/platform-browser/src/dom/debug/by.ts b/packages/platform-browser/src/dom/debug/by.ts index 6aed2097f83b5..d2910714c2de4 100644 --- a/packages/platform-browser/src/dom/debug/by.ts +++ b/packages/platform-browser/src/dom/debug/by.ts @@ -9,8 +9,6 @@ import {ɵgetDOM as getDOM} from '@angular/common'; import {DebugElement, DebugNode, Predicate, Type} from '@angular/core'; - - /** * Predicates for use with {@link DebugElement}'s query functions. * @@ -39,9 +37,9 @@ export class By { */ static css(selector: string): Predicate { return (debugElement) => { - return debugElement.nativeElement != null ? - elementMatches(debugElement.nativeElement, selector) : - false; + return debugElement.nativeElement != null + ? elementMatches(debugElement.nativeElement, selector) + : false; }; } @@ -60,9 +58,11 @@ export class By { function elementMatches(n: any, selector: string): boolean { if (getDOM().isElementNode(n)) { - return n.matches && n.matches(selector) || - n.msMatchesSelector && n.msMatchesSelector(selector) || - n.webkitMatchesSelector && n.webkitMatchesSelector(selector); + return ( + (n.matches && n.matches(selector)) || + (n.msMatchesSelector && n.msMatchesSelector(selector)) || + (n.webkitMatchesSelector && n.webkitMatchesSelector(selector)) + ); } return false; diff --git a/packages/platform-browser/src/dom/dom_renderer.ts b/packages/platform-browser/src/dom/dom_renderer.ts index 495e46c21ad09..e7cf51f90766e 100644 --- a/packages/platform-browser/src/dom/dom_renderer.ts +++ b/packages/platform-browser/src/dom/dom_renderer.ts @@ -7,7 +7,22 @@ */ import {DOCUMENT, isPlatformServer, ɵgetDOM as getDOM} from '@angular/common'; -import {APP_ID, CSP_NONCE, Inject, Injectable, InjectionToken, NgZone, OnDestroy, PLATFORM_ID, Renderer2, RendererFactory2, RendererStyleFlags2, RendererType2, ViewEncapsulation, ɵRuntimeError as RuntimeError} from '@angular/core'; +import { + APP_ID, + CSP_NONCE, + Inject, + Injectable, + InjectionToken, + NgZone, + OnDestroy, + PLATFORM_ID, + Renderer2, + RendererFactory2, + RendererStyleFlags2, + RendererType2, + ViewEncapsulation, + ɵRuntimeError as RuntimeError, +} from '@angular/core'; import {RuntimeErrorCode} from '../errors'; @@ -41,11 +56,13 @@ const REMOVE_STYLES_ON_COMPONENT_DESTROY_DEFAULT = true; * By default, the value is set to `true`. * @publicApi */ -export const REMOVE_STYLES_ON_COMPONENT_DESTROY = - new InjectionToken(ngDevMode ? 'RemoveStylesOnCompDestroy' : '', { - providedIn: 'root', - factory: () => REMOVE_STYLES_ON_COMPONENT_DESTROY_DEFAULT, - }); +export const REMOVE_STYLES_ON_COMPONENT_DESTROY = new InjectionToken( + ngDevMode ? 'RemoveStylesOnCompDestroy' : '', + { + providedIn: 'root', + factory: () => REMOVE_STYLES_ON_COMPONENT_DESTROY_DEFAULT, + }, +); export function shimContentAttribute(componentShortId: string): string { return CONTENT_ATTR.replace(COMPONENT_REGEX, componentShortId); @@ -56,32 +73,38 @@ export function shimHostAttribute(componentShortId: string): string { } export function shimStylesContent(compId: string, styles: string[]): string[] { - return styles.map(s => s.replace(COMPONENT_REGEX, compId)); + return styles.map((s) => s.replace(COMPONENT_REGEX, compId)); } @Injectable() export class DomRendererFactory2 implements RendererFactory2, OnDestroy { - private readonly rendererByCompId = - new Map(); + private readonly rendererByCompId = new Map< + string, + EmulatedEncapsulationDomRenderer2 | NoneEncapsulationDomRenderer + >(); private readonly defaultRenderer: Renderer2; private readonly platformIsServer: boolean; constructor( - private readonly eventManager: EventManager, - private readonly sharedStylesHost: SharedStylesHost, - @Inject(APP_ID) private readonly appId: string, - @Inject(REMOVE_STYLES_ON_COMPONENT_DESTROY) private removeStylesOnCompDestroy: boolean, - @Inject(DOCUMENT) private readonly doc: Document, - @Inject(PLATFORM_ID) readonly platformId: Object, - readonly ngZone: NgZone, - @Inject(CSP_NONCE) private readonly nonce: string|null = null, + private readonly eventManager: EventManager, + private readonly sharedStylesHost: SharedStylesHost, + @Inject(APP_ID) private readonly appId: string, + @Inject(REMOVE_STYLES_ON_COMPONENT_DESTROY) private removeStylesOnCompDestroy: boolean, + @Inject(DOCUMENT) private readonly doc: Document, + @Inject(PLATFORM_ID) readonly platformId: Object, + readonly ngZone: NgZone, + @Inject(CSP_NONCE) private readonly nonce: string | null = null, ) { this.platformIsServer = isPlatformServer(platformId); - this.defaultRenderer = - new DefaultDomRenderer2(eventManager, doc, ngZone, this.platformIsServer); + this.defaultRenderer = new DefaultDomRenderer2( + eventManager, + doc, + ngZone, + this.platformIsServer, + ); } - createRenderer(element: any, type: RendererType2|null): Renderer2 { + createRenderer(element: any, type: RendererType2 | null): Renderer2 { if (!element || !type) { return this.defaultRenderer; } @@ -118,17 +141,37 @@ export class DomRendererFactory2 implements RendererFactory2, OnDestroy { switch (type.encapsulation) { case ViewEncapsulation.Emulated: renderer = new EmulatedEncapsulationDomRenderer2( - eventManager, sharedStylesHost, type, this.appId, removeStylesOnCompDestroy, doc, - ngZone, platformIsServer); + eventManager, + sharedStylesHost, + type, + this.appId, + removeStylesOnCompDestroy, + doc, + ngZone, + platformIsServer, + ); break; case ViewEncapsulation.ShadowDom: return new ShadowDomRenderer( - eventManager, sharedStylesHost, element, type, doc, ngZone, this.nonce, - platformIsServer); + eventManager, + sharedStylesHost, + element, + type, + doc, + ngZone, + this.nonce, + platformIsServer, + ); default: renderer = new NoneEncapsulationDomRenderer( - eventManager, sharedStylesHost, type, removeStylesOnCompDestroy, doc, ngZone, - platformIsServer); + eventManager, + sharedStylesHost, + type, + removeStylesOnCompDestroy, + doc, + ngZone, + platformIsServer, + ); break; } @@ -153,8 +196,11 @@ class DefaultDomRenderer2 implements Renderer2 { throwOnSyntheticProps = true; constructor( - private readonly eventManager: EventManager, private readonly doc: Document, - private readonly ngZone: NgZone, private readonly platformIsServer: boolean) {} + private readonly eventManager: EventManager, + private readonly doc: Document, + private readonly ngZone: NgZone, + private readonly platformIsServer: boolean, + ) {} destroy(): void {} @@ -203,14 +249,15 @@ class DefaultDomRenderer2 implements Renderer2 { } } - selectRootElement(selectorOrNode: string|any, preserveContent?: boolean): any { - let el: any = typeof selectorOrNode === 'string' ? this.doc.querySelector(selectorOrNode) : - selectorOrNode; + selectRootElement(selectorOrNode: string | any, preserveContent?: boolean): any { + let el: any = + typeof selectorOrNode === 'string' ? this.doc.querySelector(selectorOrNode) : selectorOrNode; if (!el) { throw new RuntimeError( - RuntimeErrorCode.ROOT_NODE_NOT_FOUND, - (typeof ngDevMode === 'undefined' || ngDevMode) && - `The selector "${selectorOrNode}" did not match any elements`); + RuntimeErrorCode.ROOT_NODE_NOT_FOUND, + (typeof ngDevMode === 'undefined' || ngDevMode) && + `The selector "${selectorOrNode}" did not match any elements`, + ); } if (!preserveContent) { el.textContent = ''; @@ -283,8 +330,9 @@ class DefaultDomRenderer2 implements Renderer2 { return; } - (typeof ngDevMode === 'undefined' || ngDevMode) && this.throwOnSyntheticProps && - checkNoSyntheticProp(name, 'property'); + (typeof ngDevMode === 'undefined' || ngDevMode) && + this.throwOnSyntheticProps && + checkNoSyntheticProp(name, 'property'); el[name] = value; } @@ -292,10 +340,14 @@ class DefaultDomRenderer2 implements Renderer2 { node.nodeValue = value; } - listen(target: 'window'|'document'|'body'|any, event: string, callback: (event: any) => boolean): - () => void { - (typeof ngDevMode === 'undefined' || ngDevMode) && this.throwOnSyntheticProps && - checkNoSyntheticProp(event, 'listener'); + listen( + target: 'window' | 'document' | 'body' | any, + event: string, + callback: (event: any) => boolean, + ): () => void { + (typeof ngDevMode === 'undefined' || ngDevMode) && + this.throwOnSyntheticProps && + checkNoSyntheticProp(event, 'listener'); if (typeof target === 'string') { target = getDOM().getGlobalEventTarget(this.doc, target); if (!target) { @@ -304,7 +356,10 @@ class DefaultDomRenderer2 implements Renderer2 { } return this.eventManager.addEventListener( - target, event, this.decoratePreventDefault(callback)) as VoidFunction; + target, + event, + this.decoratePreventDefault(callback), + ) as VoidFunction; } private decoratePreventDefault(eventHandler: Function): Function { @@ -324,9 +379,9 @@ class DefaultDomRenderer2 implements Renderer2 { // Run the event handler inside the ngZone because event handlers are not patched // by Zone on the server. This is required only for tests. - const allowDefaultBehavior = this.platformIsServer ? - this.ngZone.runGuarded(() => eventHandler(event)) : - eventHandler(event); + const allowDefaultBehavior = this.platformIsServer + ? this.ngZone.runGuarded(() => eventHandler(event)) + : eventHandler(event); if (allowDefaultBehavior === false) { event.preventDefault(); } @@ -340,15 +395,14 @@ const AT_CHARCODE = (() => '@'.charCodeAt(0))(); function checkNoSyntheticProp(name: string, nameKind: string) { if (name.charCodeAt(0) === AT_CHARCODE) { throw new RuntimeError( - RuntimeErrorCode.UNEXPECTED_SYNTHETIC_PROPERTY, - `Unexpected synthetic ${nameKind} ${name} found. Please make sure that: + RuntimeErrorCode.UNEXPECTED_SYNTHETIC_PROPERTY, + `Unexpected synthetic ${nameKind} ${name} found. Please make sure that: - Either \`BrowserAnimationsModule\` or \`NoopAnimationsModule\` are imported in your application. - - There is corresponding configuration for the animation named \`${ - name}\` defined in the \`animations\` field of the \`@Component\` decorator (see https://angular.io/api/core/Component#animations).`); + - There is corresponding configuration for the animation named \`${name}\` defined in the \`animations\` field of the \`@Component\` decorator (see https://angular.io/api/core/Component#animations).`, + ); } } - function isTemplateNode(node: any): node is HTMLTemplateElement { return node.tagName === 'TEMPLATE' && node.content !== undefined; } @@ -357,14 +411,14 @@ class ShadowDomRenderer extends DefaultDomRenderer2 { private shadowRoot: any; constructor( - eventManager: EventManager, - private sharedStylesHost: SharedStylesHost, - private hostEl: any, - component: RendererType2, - doc: Document, - ngZone: NgZone, - nonce: string|null, - platformIsServer: boolean, + eventManager: EventManager, + private sharedStylesHost: SharedStylesHost, + private hostEl: any, + component: RendererType2, + doc: Document, + ngZone: NgZone, + nonce: string | null, + platformIsServer: boolean, ) { super(eventManager, doc, ngZone, platformIsServer); this.shadowRoot = (hostEl as any).attachShadow({mode: 'open'}); @@ -410,14 +464,14 @@ class NoneEncapsulationDomRenderer extends DefaultDomRenderer2 { private readonly styles: string[]; constructor( - eventManager: EventManager, - private readonly sharedStylesHost: SharedStylesHost, - component: RendererType2, - private removeStylesOnCompDestroy: boolean, - doc: Document, - ngZone: NgZone, - platformIsServer: boolean, - compId?: string, + eventManager: EventManager, + private readonly sharedStylesHost: SharedStylesHost, + component: RendererType2, + private removeStylesOnCompDestroy: boolean, + doc: Document, + ngZone: NgZone, + platformIsServer: boolean, + compId?: string, ) { super(eventManager, doc, ngZone, platformIsServer); this.styles = compId ? shimStylesContent(compId, component.styles) : component.styles; @@ -441,13 +495,26 @@ class EmulatedEncapsulationDomRenderer2 extends NoneEncapsulationDomRenderer { private hostAttr: string; constructor( - eventManager: EventManager, sharedStylesHost: SharedStylesHost, component: RendererType2, - appId: string, removeStylesOnCompDestroy: boolean, doc: Document, ngZone: NgZone, - platformIsServer: boolean) { + eventManager: EventManager, + sharedStylesHost: SharedStylesHost, + component: RendererType2, + appId: string, + removeStylesOnCompDestroy: boolean, + doc: Document, + ngZone: NgZone, + platformIsServer: boolean, + ) { const compId = appId + '-' + component.id; super( - eventManager, sharedStylesHost, component, removeStylesOnCompDestroy, doc, ngZone, - platformIsServer, compId); + eventManager, + sharedStylesHost, + component, + removeStylesOnCompDestroy, + doc, + ngZone, + platformIsServer, + compId, + ); this.contentAttr = shimContentAttribute(compId); this.hostAttr = shimHostAttribute(compId); } diff --git a/packages/platform-browser/src/dom/events/event_manager.ts b/packages/platform-browser/src/dom/events/event_manager.ts index 053620830e281..736d50d846aff 100644 --- a/packages/platform-browser/src/dom/events/event_manager.ts +++ b/packages/platform-browser/src/dom/events/event_manager.ts @@ -6,8 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ - -import {Inject, Injectable, InjectionToken, NgZone, ɵRuntimeError as RuntimeError} from '@angular/core'; +import { + Inject, + Injectable, + InjectionToken, + NgZone, + ɵRuntimeError as RuntimeError, +} from '@angular/core'; import {RuntimeErrorCode} from '../../errors'; @@ -16,8 +21,9 @@ import {RuntimeErrorCode} from '../../errors'; * * @publicApi */ -export const EVENT_MANAGER_PLUGINS = - new InjectionToken(ngDevMode ? 'EventManagerPlugins' : ''); +export const EVENT_MANAGER_PLUGINS = new InjectionToken( + ngDevMode ? 'EventManagerPlugins' : '', +); /** * An injectable service that provides event management for Angular @@ -33,7 +39,10 @@ export class EventManager { /** * Initializes an instance of the event-manager service. */ - constructor(@Inject(EVENT_MANAGER_PLUGINS) plugins: EventManagerPlugin[], private _zone: NgZone) { + constructor( + @Inject(EVENT_MANAGER_PLUGINS) plugins: EventManagerPlugin[], + private _zone: NgZone, + ) { plugins.forEach((plugin) => { plugin.manager = this; }); @@ -72,9 +81,10 @@ export class EventManager { plugin = plugins.find((plugin) => plugin.supports(eventName)); if (!plugin) { throw new RuntimeError( - RuntimeErrorCode.NO_PLUGIN_FOR_EVENT, - (typeof ngDevMode === 'undefined' || ngDevMode) && - `No event manager plugin found for event ${eventName}`); + RuntimeErrorCode.NO_PLUGIN_FOR_EVENT, + (typeof ngDevMode === 'undefined' || ngDevMode) && + `No event manager plugin found for event ${eventName}`, + ); } this._eventNameToPlugin.set(eventName, plugin); diff --git a/packages/platform-browser/src/dom/events/hammer_gestures.ts b/packages/platform-browser/src/dom/events/hammer_gestures.ts index fe4f6e4cf2788..5ffe38ddbfcb3 100644 --- a/packages/platform-browser/src/dom/events/hammer_gestures.ts +++ b/packages/platform-browser/src/dom/events/hammer_gestures.ts @@ -7,12 +7,18 @@ */ import {DOCUMENT} from '@angular/common'; -import {Inject, Injectable, InjectionToken, NgModule, Optional, Provider, ɵConsole as Console} from '@angular/core'; +import { + Inject, + Injectable, + InjectionToken, + NgModule, + Optional, + Provider, + ɵConsole as Console, +} from '@angular/core'; import {EVENT_MANAGER_PLUGINS, EventManagerPlugin} from './event_manager'; - - /** * Supported HammerJS recognizer event names. */ @@ -52,7 +58,7 @@ const EVENT_NAMES = { 'swipedown': true, // tap 'tap': true, - 'doubletap': true + 'doubletap': true, }; /** @@ -64,7 +70,6 @@ const EVENT_NAMES = { */ export const HAMMER_GESTURE_CONFIG = new InjectionToken('HammerGestureConfig'); - /** * Function that loads HammerJS, returning a promise that is resolved once HammerJs is loaded. * @@ -162,12 +167,14 @@ export class HammerGestureConfig { */ @Injectable() export class HammerGesturesPlugin extends EventManagerPlugin { - private _loaderPromise: Promise|null = null; + private _loaderPromise: Promise | null = null; constructor( - @Inject(DOCUMENT) doc: any, - @Inject(HAMMER_GESTURE_CONFIG) private _config: HammerGestureConfig, private console: Console, - @Optional() @Inject(HAMMER_LOADER) private loader?: HammerLoader|null) { + @Inject(DOCUMENT) doc: any, + @Inject(HAMMER_GESTURE_CONFIG) private _config: HammerGestureConfig, + private console: Console, + @Optional() @Inject(HAMMER_LOADER) private loader?: HammerLoader | null, + ) { super(doc); } @@ -179,8 +186,9 @@ export class HammerGesturesPlugin extends EventManagerPlugin { if (!(window as any).Hammer && !this.loader) { if (typeof ngDevMode === 'undefined' || ngDevMode) { this.console.warn( - `The "${eventName}" event cannot be bound because Hammer.JS is not ` + - `loaded and no custom loader has been specified.`); + `The "${eventName}" event cannot be bound because Hammer.JS is not ` + + `loaded and no custom loader has been specified.`, + ); } return false; } @@ -204,34 +212,35 @@ export class HammerGesturesPlugin extends EventManagerPlugin { cancelRegistration = true; }; - zone.runOutsideAngular( - () => this._loaderPromise! - .then(() => { - // If Hammer isn't actually loaded when the custom loader resolves, give up. - if (!(window as any).Hammer) { - if (typeof ngDevMode === 'undefined' || ngDevMode) { - this.console.warn( - `The custom HAMMER_LOADER completed, but Hammer.JS is not present.`); - } - deregister = () => {}; - return; - } + zone.runOutsideAngular(() => + this._loaderPromise!.then(() => { + // If Hammer isn't actually loaded when the custom loader resolves, give up. + if (!(window as any).Hammer) { + if (typeof ngDevMode === 'undefined' || ngDevMode) { + this.console.warn( + `The custom HAMMER_LOADER completed, but Hammer.JS is not present.`, + ); + } + deregister = () => {}; + return; + } - if (!cancelRegistration) { - // Now that Hammer is loaded and the listener is being loaded for real, - // the deregistration function changes from canceling registration to - // removal. - deregister = this.addEventListener(element, eventName, handler); - } - }) - .catch(() => { - if (typeof ngDevMode === 'undefined' || ngDevMode) { - this.console.warn( - `The "${eventName}" event cannot be bound because the custom ` + - `Hammer.JS loader failed.`); - } - deregister = () => {}; - })); + if (!cancelRegistration) { + // Now that Hammer is loaded and the listener is being loaded for real, + // the deregistration function changes from canceling registration to + // removal. + deregister = this.addEventListener(element, eventName, handler); + } + }).catch(() => { + if (typeof ngDevMode === 'undefined' || ngDevMode) { + this.console.warn( + `The "${eventName}" event cannot be bound because the custom ` + + `Hammer.JS loader failed.`, + ); + } + deregister = () => {}; + }), + ); // Return a function that *executes* `deregister` (and not `deregister` itself) so that we // can change the behavior of `deregister` once the listener is added. Using a closure in @@ -244,8 +253,8 @@ export class HammerGesturesPlugin extends EventManagerPlugin { return zone.runOutsideAngular(() => { // Creating the manager bind events, must be done outside of angular const mc = this._config.buildHammer(element); - const callback = function(eventObj: HammerInput) { - zone.runGuarded(function() { + const callback = function (eventObj: HammerInput) { + zone.runGuarded(function () { handler(eventObj); }); }; @@ -282,10 +291,9 @@ export class HammerGesturesPlugin extends EventManagerPlugin { provide: EVENT_MANAGER_PLUGINS, useClass: HammerGesturesPlugin, multi: true, - deps: [DOCUMENT, HAMMER_GESTURE_CONFIG, Console, [new Optional(), HAMMER_LOADER]] + deps: [DOCUMENT, HAMMER_GESTURE_CONFIG, Console, [new Optional(), HAMMER_LOADER]], }, {provide: HAMMER_GESTURE_CONFIG, useClass: HammerGestureConfig, deps: []}, - ] + ], }) -export class HammerModule { -} +export class HammerModule {} diff --git a/packages/platform-browser/src/dom/events/key_events.ts b/packages/platform-browser/src/dom/events/key_events.ts index 8381b6b5fca6e..bb0da15dc5fc8 100644 --- a/packages/platform-browser/src/dom/events/key_events.ts +++ b/packages/platform-browser/src/dom/events/key_events.ts @@ -31,7 +31,7 @@ const _keyMap: {[k: string]: string} = { 'Down': 'ArrowDown', 'Menu': 'ContextMenu', 'Scroll': 'ScrollLock', - 'Win': 'OS' + 'Win': 'OS', }; /** @@ -41,7 +41,7 @@ const MODIFIER_KEY_GETTERS: {[key: string]: (event: KeyboardEvent) => boolean} = 'alt': (event: KeyboardEvent) => event.altKey, 'control': (event: KeyboardEvent) => event.ctrlKey, 'meta': (event: KeyboardEvent) => event.metaKey, - 'shift': (event: KeyboardEvent) => event.shiftKey + 'shift': (event: KeyboardEvent) => event.shiftKey, }; /** @@ -77,8 +77,11 @@ export class KeyEventsPlugin extends EventManagerPlugin { override addEventListener(element: HTMLElement, eventName: string, handler: Function): Function { const parsedEvent = KeyEventsPlugin.parseEventName(eventName)!; - const outsideHandler = - KeyEventsPlugin.eventCallback(parsedEvent['fullKey'], handler, this.manager.getZone()); + const outsideHandler = KeyEventsPlugin.eventCallback( + parsedEvent['fullKey'], + handler, + this.manager.getZone(), + ); return this.manager.getZone().runOutsideAngular(() => { return getDOM().onAndCancel(element, parsedEvent['domEventName'], outsideHandler); @@ -94,11 +97,11 @@ export class KeyEventsPlugin extends EventManagerPlugin { * @returns an object with the full, normalized string, and the dom event name * or null in the case when the event doesn't match a keyboard event. */ - static parseEventName(eventName: string): {fullKey: string, domEventName: string}|null { + static parseEventName(eventName: string): {fullKey: string; domEventName: string} | null { const parts: string[] = eventName.toLowerCase().split('.'); const domEventName = parts.shift(); - if ((parts.length === 0) || !(domEventName === 'keydown' || domEventName === 'keyup')) { + if (parts.length === 0 || !(domEventName === 'keydown' || domEventName === 'keyup')) { return null; } @@ -110,7 +113,7 @@ export class KeyEventsPlugin extends EventManagerPlugin { parts.splice(codeIX, 1); fullKey = 'code.'; } - MODIFIER_KEYS.forEach(modifierName => { + MODIFIER_KEYS.forEach((modifierName) => { const index: number = parts.indexOf(modifierName); if (index > -1) { parts.splice(index, 1); @@ -127,7 +130,7 @@ export class KeyEventsPlugin extends EventManagerPlugin { // NOTE: Please don't rewrite this as so, as it will break JSCompiler property renaming. // The code must remain in the `result['domEventName']` form. // return {domEventName, fullKey}; - const result: {fullKey: string, domEventName: string} = {} as any; + const result: {fullKey: string; domEventName: string} = {} as any; result['domEventName'] = domEventName; result['fullKey'] = fullKey; return result; @@ -154,11 +157,11 @@ export class KeyEventsPlugin extends EventManagerPlugin { if (keycode == null || !keycode) return false; keycode = keycode.toLowerCase(); if (keycode === ' ') { - keycode = 'space'; // for readability + keycode = 'space'; // for readability } else if (keycode === '.') { - keycode = 'dot'; // because '.' is used as a separator in event names + keycode = 'dot'; // because '.' is used as a separator in event names } - MODIFIER_KEYS.forEach(modifierName => { + MODIFIER_KEYS.forEach((modifierName) => { if (modifierName !== keycode) { const modifierGetter = MODIFIER_KEY_GETTERS[modifierName]; if (modifierGetter(event)) { diff --git a/packages/platform-browser/src/dom/shared_styles_host.ts b/packages/platform-browser/src/dom/shared_styles_host.ts index 57fc6b12e6614..3cb0bb9509c90 100644 --- a/packages/platform-browser/src/dom/shared_styles_host.ts +++ b/packages/platform-browser/src/dom/shared_styles_host.ts @@ -7,7 +7,15 @@ */ import {DOCUMENT, isPlatformServer} from '@angular/common'; -import {APP_ID, CSP_NONCE, Inject, Injectable, OnDestroy, Optional, PLATFORM_ID} from '@angular/core'; +import { + APP_ID, + CSP_NONCE, + Inject, + Injectable, + OnDestroy, + Optional, + PLATFORM_ID, +} from '@angular/core'; /** The style elements attribute name used to set value of `APP_ID` token. */ const APP_ID_ATTRIBUTE_NAME = 'ng-app-id'; @@ -15,20 +23,23 @@ const APP_ID_ATTRIBUTE_NAME = 'ng-app-id'; @Injectable() export class SharedStylesHost implements OnDestroy { // Maps all registered host nodes to a list of style nodes that have been added to the host node. - private readonly styleRef = new Map < string /** Style string */, { - elements: HTMLStyleElement[]; - usage: number - } - > (); + private readonly styleRef = new Map< + string /** Style string */, + { + elements: HTMLStyleElement[]; + usage: number; + } + >(); private readonly hostNodes = new Set(); - private readonly styleNodesInDOM: Map|null; + private readonly styleNodesInDOM: Map | null; private readonly platformIsServer: boolean; constructor( - @Inject(DOCUMENT) private readonly doc: Document, - @Inject(APP_ID) private readonly appId: string, - @Inject(CSP_NONCE) @Optional() private nonce?: string|null, - @Inject(PLATFORM_ID) readonly platformId: object = {}) { + @Inject(DOCUMENT) private readonly doc: Document, + @Inject(APP_ID) private readonly appId: string, + @Inject(CSP_NONCE) @Optional() private nonce?: string | null, + @Inject(PLATFORM_ID) readonly platformId: object = {}, + ) { this.styleNodesInDOM = this.collectServerRenderedStyles(); this.platformIsServer = isPlatformServer(platformId); this.resetHostNodes(); @@ -96,9 +107,10 @@ export class SharedStylesHost implements OnDestroy { styleRef.delete(style); } - private collectServerRenderedStyles(): Map|null { + private collectServerRenderedStyles(): Map | null { const styles = this.doc.head?.querySelectorAll( - `style[${APP_ID_ATTRIBUTE_NAME}="${this.appId}"]`); + `style[${APP_ID_ATTRIBUTE_NAME}="${this.appId}"]`, + ); if (styles?.length) { const styleMap = new Map(); diff --git a/packages/platform-browser/src/dom/util.ts b/packages/platform-browser/src/dom/util.ts index 66d737843c457..4f45affc2d73d 100644 --- a/packages/platform-browser/src/dom/util.ts +++ b/packages/platform-browser/src/dom/util.ts @@ -21,7 +21,7 @@ export function exportNgVar(name: string, value: any): void { // - closure declares globals itself for minified names, which sometimes clobber our `ng` global // - we can't declare a closure extern as the namespace `ng` is already used within Google // for typings for angularJS (via `goog.provide('ng....')`). - const ng = global['ng'] = (global['ng'] as {[key: string]: any} | undefined) || {}; + const ng = (global['ng'] = (global['ng'] as {[key: string]: any} | undefined) || {}); ng[name] = value; } } diff --git a/packages/platform-browser/src/errors.ts b/packages/platform-browser/src/errors.ts index 3a16b76f65db4..d40807b677dcd 100644 --- a/packages/platform-browser/src/errors.ts +++ b/packages/platform-browser/src/errors.ts @@ -27,5 +27,5 @@ export const enum RuntimeErrorCode { SANITIZATION_UNEXPECTED_CTX = 5202, // Animations related errors (5300-5400 range) - ANIMATION_RENDERER_ASYNC_LOADING_FAILURE = 5300 + ANIMATION_RENDERER_ASYNC_LOADING_FAILURE = 5300, } diff --git a/packages/platform-browser/src/hydration.ts b/packages/platform-browser/src/hydration.ts index 0e9f491de3fc9..2382e6759c1b0 100644 --- a/packages/platform-browser/src/hydration.ts +++ b/packages/platform-browser/src/hydration.ts @@ -7,7 +7,19 @@ */ import {HttpTransferCacheOptions, ɵwithHttpTransferCache} from '@angular/common/http'; -import {ENVIRONMENT_INITIALIZER, EnvironmentProviders, inject, makeEnvironmentProviders, NgZone, Provider, ɵConsole as Console, ɵformatRuntimeError as formatRuntimeError, ɵwithDomHydration as withDomHydration, ɵwithEventReplay, ɵwithI18nSupport} from '@angular/core'; +import { + ENVIRONMENT_INITIALIZER, + EnvironmentProviders, + inject, + makeEnvironmentProviders, + NgZone, + Provider, + ɵConsole as Console, + ɵformatRuntimeError as formatRuntimeError, + ɵwithDomHydration as withDomHydration, + ɵwithEventReplay, + ɵwithI18nSupport, +} from '@angular/core'; import {RuntimeErrorCode} from './errors'; @@ -38,8 +50,10 @@ export interface HydrationFeature { * Helper function to create an object that represents a Hydration feature. */ function hydrationFeature( - ɵkind: FeatureKind, ɵproviders: Provider[] = [], - ɵoptions: unknown = {}): HydrationFeature { + ɵkind: FeatureKind, + ɵproviders: Provider[] = [], + ɵoptions: unknown = {}, +): HydrationFeature { return {ɵkind, ɵproviders}; } @@ -49,8 +63,7 @@ function hydrationFeature( * * @publicApi */ -export function withNoHttpTransferCache(): - HydrationFeature { +export function withNoHttpTransferCache(): HydrationFeature { // This feature has no providers and acts as a flag that turns off // HTTP transfer cache (which otherwise is turned on by default). return hydrationFeature(HydrationFeatureKind.NoHttpTransferCache); @@ -65,11 +78,13 @@ export function withNoHttpTransferCache(): * @publicApi */ export function withHttpTransferCacheOptions( - options: HttpTransferCacheOptions, - ): HydrationFeature { + options: HttpTransferCacheOptions, +): HydrationFeature { // This feature has no providers and acts as a flag to pass options to the HTTP transfer cache. return hydrationFeature( - HydrationFeatureKind.HttpTransferCacheOptions, ɵwithHttpTransferCache(options)); + HydrationFeatureKind.HttpTransferCacheOptions, + ɵwithHttpTransferCache(options), + ); } /** @@ -98,25 +113,28 @@ export function withEventReplay(): HydrationFeature { - const ngZone = inject(NgZone); - // Checking `ngZone instanceof NgZone` would be insufficient here, - // because custom implementations might use NgZone as a base class. - if (ngZone.constructor !== NgZone) { - const console = inject(Console); - const message = formatRuntimeError( + return [ + { + provide: ENVIRONMENT_INITIALIZER, + useValue: () => { + const ngZone = inject(NgZone); + // Checking `ngZone instanceof NgZone` would be insufficient here, + // because custom implementations might use NgZone as a base class. + if (ngZone.constructor !== NgZone) { + const console = inject(Console); + const message = formatRuntimeError( RuntimeErrorCode.UNSUPPORTED_ZONEJS_INSTANCE, 'Angular detected that hydration was enabled for an application ' + - 'that uses a custom or a noop Zone.js implementation. ' + - 'This is not yet a fully supported configuration.'); - // tslint:disable-next-line:no-console - console.warn(message); - } + 'that uses a custom or a noop Zone.js implementation. ' + + 'This is not yet a fully supported configuration.', + ); + // tslint:disable-next-line:no-console + console.warn(message); + } + }, + multi: true, }, - multi: true, - }]; + ]; } /** @@ -163,12 +181,14 @@ function provideZoneJsCompatibilityDetector(): Provider[] { * * @publicApi */ -export function provideClientHydration(...features: HydrationFeature[]): - EnvironmentProviders { +export function provideClientHydration( + ...features: HydrationFeature[] +): EnvironmentProviders { const providers: Provider[] = []; const featuresKind = new Set(); - const hasHttpTransferCacheOptions = - featuresKind.has(HydrationFeatureKind.HttpTransferCacheOptions); + const hasHttpTransferCacheOptions = featuresKind.has( + HydrationFeatureKind.HttpTransferCacheOptions, + ); for (const {ɵproviders, ɵkind} of features) { featuresKind.add(ɵkind); @@ -178,19 +198,24 @@ export function provideClientHydration(...features: HydrationFeature` that can be used to store value of type T with `TransferState`. @@ -69,16 +73,45 @@ export const TransferState: {new (): TransferStateFromCore} = TransferStateFromC // The below is a workaround to add a deprecated message. export type StateKey = StateKeyFromCore; -export {ApplicationConfig, bootstrapApplication, BrowserModule, createApplication, platformBrowser, provideProtractorTestingSupport} from './browser'; +export { + ApplicationConfig, + bootstrapApplication, + BrowserModule, + createApplication, + platformBrowser, + provideProtractorTestingSupport, +} from './browser'; export {Meta, MetaDefinition} from './browser/meta'; export {Title} from './browser/title'; export {disableDebugTools, enableDebugTools} from './browser/tools/tools'; export {By} from './dom/debug/by'; export {REMOVE_STYLES_ON_COMPONENT_DESTROY} from './dom/dom_renderer'; export {EVENT_MANAGER_PLUGINS, EventManager, EventManagerPlugin} from './dom/events/event_manager'; -export {HAMMER_GESTURE_CONFIG, HAMMER_LOADER, HammerGestureConfig, HammerLoader, HammerModule} from './dom/events/hammer_gestures'; -export {DomSanitizer, SafeHtml, SafeResourceUrl, SafeScript, SafeStyle, SafeUrl, SafeValue} from './security/dom_sanitization_service'; -export {HydrationFeature, HydrationFeatureKind, provideClientHydration, withEventReplay, withHttpTransferCacheOptions, withI18nSupport, withNoHttpTransferCache} from './hydration'; +export { + HAMMER_GESTURE_CONFIG, + HAMMER_LOADER, + HammerGestureConfig, + HammerLoader, + HammerModule, +} from './dom/events/hammer_gestures'; +export { + DomSanitizer, + SafeHtml, + SafeResourceUrl, + SafeScript, + SafeStyle, + SafeUrl, + SafeValue, +} from './security/dom_sanitization_service'; +export { + HydrationFeature, + HydrationFeatureKind, + provideClientHydration, + withEventReplay, + withHttpTransferCacheOptions, + withI18nSupport, + withNoHttpTransferCache, +} from './hydration'; export * from './private_export'; export {VERSION} from './version'; diff --git a/packages/platform-browser/src/private_export.ts b/packages/platform-browser/src/private_export.ts index 85049685c0408..a0108529d6e70 100644 --- a/packages/platform-browser/src/private_export.ts +++ b/packages/platform-browser/src/private_export.ts @@ -7,7 +7,10 @@ */ export {ɵgetDOM} from '@angular/common'; -export {initDomAdapter as ɵinitDomAdapter, INTERNAL_BROWSER_PLATFORM_PROVIDERS as ɵINTERNAL_BROWSER_PLATFORM_PROVIDERS} from './browser'; +export { + initDomAdapter as ɵinitDomAdapter, + INTERNAL_BROWSER_PLATFORM_PROVIDERS as ɵINTERNAL_BROWSER_PLATFORM_PROVIDERS, +} from './browser'; export {BrowserDomAdapter as ɵBrowserDomAdapter} from './browser/browser_adapter'; export {BrowserGetTestability as ɵBrowserGetTestability} from './browser/testability'; export {DomRendererFactory2 as ɵDomRendererFactory2} from './dom/dom_renderer'; diff --git a/packages/platform-browser/src/security/dom_sanitization_service.ts b/packages/platform-browser/src/security/dom_sanitization_service.ts index 6a30591d5b424..ca9afde9b2ba9 100644 --- a/packages/platform-browser/src/security/dom_sanitization_service.ts +++ b/packages/platform-browser/src/security/dom_sanitization_service.ts @@ -7,7 +7,25 @@ */ import {DOCUMENT} from '@angular/common'; -import {forwardRef, Inject, Injectable, Sanitizer, SecurityContext, ɵ_sanitizeHtml as _sanitizeHtml, ɵ_sanitizeUrl as _sanitizeUrl, ɵallowSanitizationBypassAndThrow as allowSanitizationBypassOrThrow, ɵbypassSanitizationTrustHtml as bypassSanitizationTrustHtml, ɵbypassSanitizationTrustResourceUrl as bypassSanitizationTrustResourceUrl, ɵbypassSanitizationTrustScript as bypassSanitizationTrustScript, ɵbypassSanitizationTrustStyle as bypassSanitizationTrustStyle, ɵbypassSanitizationTrustUrl as bypassSanitizationTrustUrl, ɵBypassType as BypassType, ɵRuntimeError as RuntimeError, ɵunwrapSafeValue as unwrapSafeValue, ɵXSS_SECURITY_URL as XSS_SECURITY_URL} from '@angular/core'; +import { + forwardRef, + Inject, + Injectable, + Sanitizer, + SecurityContext, + ɵ_sanitizeHtml as _sanitizeHtml, + ɵ_sanitizeUrl as _sanitizeUrl, + ɵallowSanitizationBypassAndThrow as allowSanitizationBypassOrThrow, + ɵbypassSanitizationTrustHtml as bypassSanitizationTrustHtml, + ɵbypassSanitizationTrustResourceUrl as bypassSanitizationTrustResourceUrl, + ɵbypassSanitizationTrustScript as bypassSanitizationTrustScript, + ɵbypassSanitizationTrustStyle as bypassSanitizationTrustStyle, + ɵbypassSanitizationTrustUrl as bypassSanitizationTrustUrl, + ɵBypassType as BypassType, + ɵRuntimeError as RuntimeError, + ɵunwrapSafeValue as unwrapSafeValue, + ɵXSS_SECURITY_URL as XSS_SECURITY_URL, +} from '@angular/core'; import {RuntimeErrorCode} from '../errors'; @@ -97,7 +115,7 @@ export abstract class DomSanitizer implements Sanitizer { * For any other security context, this method throws an error if provided * with a plain string. */ - abstract sanitize(context: SecurityContext, value: SafeValue|string|null): string|null; + abstract sanitize(context: SecurityContext, value: SafeValue | string | null): string | null; /** * Bypass security and trust the given value to be safe HTML. Only use this when the bound HTML @@ -150,7 +168,7 @@ export class DomSanitizerImpl extends DomSanitizer { super(); } - override sanitize(ctx: SecurityContext, value: SafeValue|string|null): string|null { + override sanitize(ctx: SecurityContext, value: SafeValue | string | null): string | null { if (value == null) return null; switch (ctx) { case SecurityContext.NONE: @@ -170,9 +188,10 @@ export class DomSanitizerImpl extends DomSanitizer { return unwrapSafeValue(value); } throw new RuntimeError( - RuntimeErrorCode.SANITIZATION_UNSAFE_SCRIPT, - (typeof ngDevMode === 'undefined' || ngDevMode) && - 'unsafe value used in a script context'); + RuntimeErrorCode.SANITIZATION_UNSAFE_SCRIPT, + (typeof ngDevMode === 'undefined' || ngDevMode) && + 'unsafe value used in a script context', + ); case SecurityContext.URL: if (allowSanitizationBypassOrThrow(value, BypassType.Url)) { return unwrapSafeValue(value); @@ -183,14 +202,16 @@ export class DomSanitizerImpl extends DomSanitizer { return unwrapSafeValue(value); } throw new RuntimeError( - RuntimeErrorCode.SANITIZATION_UNSAFE_RESOURCE_URL, - (typeof ngDevMode === 'undefined' || ngDevMode) && - `unsafe value used in a resource URL context (see ${XSS_SECURITY_URL})`); + RuntimeErrorCode.SANITIZATION_UNSAFE_RESOURCE_URL, + (typeof ngDevMode === 'undefined' || ngDevMode) && + `unsafe value used in a resource URL context (see ${XSS_SECURITY_URL})`, + ); default: throw new RuntimeError( - RuntimeErrorCode.SANITIZATION_UNEXPECTED_CTX, - (typeof ngDevMode === 'undefined' || ngDevMode) && - `Unexpected SecurityContext ${ctx} (see ${XSS_SECURITY_URL})`); + RuntimeErrorCode.SANITIZATION_UNEXPECTED_CTX, + (typeof ngDevMode === 'undefined' || ngDevMode) && + `Unexpected SecurityContext ${ctx} (see ${XSS_SECURITY_URL})`, + ); } } diff --git a/packages/platform-browser/test/browser/bootstrap_spec.ts b/packages/platform-browser/test/browser/bootstrap_spec.ts index 6788c7754d0da..b723f7931d06e 100644 --- a/packages/platform-browser/test/browser/bootstrap_spec.ts +++ b/packages/platform-browser/test/browser/bootstrap_spec.ts @@ -8,7 +8,37 @@ import {animate, style, transition, trigger} from '@angular/animations'; import {DOCUMENT, isPlatformBrowser, ɵgetDOM as getDOM} from '@angular/common'; -import {ANIMATION_MODULE_TYPE, APP_INITIALIZER, Compiler, Component, createPlatformFactory, CUSTOM_ELEMENTS_SCHEMA, Directive, ErrorHandler, importProvidersFrom, Inject, inject as _inject, InjectionToken, Injector, LOCALE_ID, NgModule, NgModuleRef, NgZone, OnDestroy, PLATFORM_ID, PLATFORM_INITIALIZER, Provider, provideZoneChangeDetection, Sanitizer, StaticProvider, Testability, TestabilityRegistry, TransferState, Type, VERSION} from '@angular/core'; +import { + ANIMATION_MODULE_TYPE, + APP_INITIALIZER, + Compiler, + Component, + createPlatformFactory, + CUSTOM_ELEMENTS_SCHEMA, + Directive, + ErrorHandler, + importProvidersFrom, + Inject, + inject as _inject, + InjectionToken, + Injector, + LOCALE_ID, + NgModule, + NgModuleRef, + NgZone, + OnDestroy, + PLATFORM_ID, + PLATFORM_INITIALIZER, + Provider, + provideZoneChangeDetection, + Sanitizer, + StaticProvider, + Testability, + TestabilityRegistry, + TransferState, + Type, + VERSION, +} from '@angular/core'; import {ApplicationRef} from '@angular/core/src/application/application_ref'; import {Console} from '@angular/core/src/console'; import {ComponentRef} from '@angular/core/src/linker/component_factory'; @@ -23,8 +53,7 @@ import {expect} from '@angular/platform-browser/testing/src/matchers'; import {bootstrapApplication} from '../../src/browser'; @Component({selector: 'non-existent', template: ''}) -class NonExistentComp { -} +class NonExistentComp {} @Component({selector: 'hello-app', template: '{{greeting}} world!'}) class HelloRootCmp { @@ -61,8 +90,7 @@ class HelloRootCmp4 { } @Directive({selector: 'hello-app'}) -class HelloRootDirectiveIsNotCmp { -} +class HelloRootDirectiveIsNotCmp {} @Component({selector: 'hello-app', template: ''}) class HelloOnDestroyTickCmp implements OnDestroy { @@ -77,8 +105,7 @@ class HelloOnDestroyTickCmp implements OnDestroy { } @Component({selector: 'hello-app', template: 'hello world!'}) -class HelloCmpUsingCustomElement { -} +class HelloCmpUsingCustomElement {} class MockConsole { res: any[][] = []; @@ -87,7 +114,6 @@ class MockConsole { } } - class DummyConsole implements Console { public warnings: string[] = []; @@ -97,23 +123,23 @@ class DummyConsole implements Console { } } - function bootstrap( - cmpType: any, providers: Provider[] = [], platformProviders: StaticProvider[] = [], - imports: Type[] = []): Promise { + cmpType: any, + providers: Provider[] = [], + platformProviders: StaticProvider[] = [], + imports: Type[] = [], +): Promise { @NgModule({ imports: [BrowserModule, ...imports], declarations: [cmpType], bootstrap: [cmpType], providers: providers, - schemas: [CUSTOM_ELEMENTS_SCHEMA] + schemas: [CUSTOM_ELEMENTS_SCHEMA], }) - class TestModule { - } + class TestModule {} return platformBrowserDynamic(platformProviders).bootstrapModule(TestModule); } - describe('bootstrap factory method', () => { let el: HTMLElement; let el2: HTMLElement; @@ -193,8 +219,7 @@ describe('bootstrap factory method', () => { @NgModule({ declarations: [NonStandaloneComp], }) - class NonStandaloneCompModule { - } + class NonStandaloneCompModule {} it('should work for simple standalone components', async () => { await bootstrapApplication(SimpleComp); @@ -249,25 +274,24 @@ describe('bootstrap factory method', () => { expect(el2.innerText).toBe('Hello from Updated SimpleComp2!'); }); - it('should allow bootstrapping multiple standalone components within the same app', - async () => { - const appRef = await bootstrapApplication(SimpleComp); - appRef.bootstrap(SimpleComp2); + it('should allow bootstrapping multiple standalone components within the same app', async () => { + const appRef = await bootstrapApplication(SimpleComp); + appRef.bootstrap(SimpleComp2); - expect(el.innerText).toBe('Hello from SimpleComp!'); - expect(el2.innerText).toBe('Hello from SimpleComp2!'); + expect(el.innerText).toBe('Hello from SimpleComp!'); + expect(el2.innerText).toBe('Hello from SimpleComp2!'); - // Update name in both components. - appRef.components[0].instance.name = 'Updated SimpleComp'; - appRef.components[1].instance.name = 'Updated SimpleComp2'; + // Update name in both components. + appRef.components[0].instance.name = 'Updated SimpleComp'; + appRef.components[1].instance.name = 'Updated SimpleComp2'; - // Run change detection for the app. - appRef.tick(); + // Run change detection for the app. + appRef.tick(); - // Expect both components to be updated, since they belong to the same app. - expect(el.innerText).toBe('Hello from Updated SimpleComp!'); - expect(el2.innerText).toBe('Hello from Updated SimpleComp2!'); - }); + // Expect both components to be updated, since they belong to the same app. + expect(el.innerText).toBe('Hello from Updated SimpleComp!'); + expect(el2.innerText).toBe('Hello from Updated SimpleComp2!'); + }); it('should allow bootstrapping non-standalone components within the same app', async () => { const appRef = await bootstrapApplication(SimpleComp); @@ -292,10 +316,11 @@ describe('bootstrap factory method', () => { }); it('should throw when trying to bootstrap a non-standalone component', async () => { - const msg = 'NG0907: The NonStandaloneComp component is not marked as standalone, ' + - 'but Angular expects to have a standalone component here. Please make sure the ' + - 'NonStandaloneComp component has the `standalone: true` flag in the decorator.'; - let bootstrapError: string|null = null; + const msg = + 'NG0907: The NonStandaloneComp component is not marked as standalone, ' + + 'but Angular expects to have a standalone component here. Please make sure the ' + + 'NonStandaloneComp component has the `standalone: true` flag in the decorator.'; + let bootstrapError: string | null = null; try { await bootstrapApplication(NonStandaloneComp); @@ -311,13 +336,12 @@ describe('bootstrap factory method', () => { standalone: true, selector: '[dir]', }) - class StandaloneDirective { - } + class StandaloneDirective {} - const msg = // - 'NG0906: The StandaloneDirective is not an Angular component, ' + - 'make sure it has the `@Component` decorator.'; - let bootstrapError: string|null = null; + const msg = // + 'NG0906: The StandaloneDirective is not an Angular component, ' + + 'make sure it has the `@Component` decorator.'; + let bootstrapError: string | null = null; try { await bootstrapApplication(StandaloneDirective); @@ -330,10 +354,10 @@ describe('bootstrap factory method', () => { it('should throw when trying to bootstrap a non-annotated class', async () => { class NonAnnotatedClass {} - const msg = // - 'NG0906: The NonAnnotatedClass is not an Angular component, ' + - 'make sure it has the `@Component` decorator.'; - let bootstrapError: string|null = null; + const msg = // + 'NG0906: The NonAnnotatedClass is not an Angular component, ' + + 'make sure it has the `@Component` decorator.'; + let bootstrapError: string | null = null; try { await bootstrapApplication(NonAnnotatedClass); @@ -345,7 +369,7 @@ describe('bootstrap factory method', () => { }); it('should have the TransferState token available', async () => { - let state: TransferState|undefined; + let state: TransferState | undefined; @Component({ selector: 'hello-app', standalone: true, @@ -370,8 +394,11 @@ describe('bootstrap factory method', () => { } bootstrapApplication(SimpleComp, { - providers: [importProvidersFrom(ErrorModule)] - }).then(() => done.fail('Expected bootstrap promised to be rejected'), () => done()); + providers: [importProvidersFrom(ErrorModule)], + }).then( + () => done.fail('Expected bootstrap promised to be rejected'), + () => done(), + ); }); describe('with animations', () => { @@ -379,9 +406,10 @@ describe('bootstrap factory method', () => { standalone: true, selector: 'hello-app', template: - '
Hello from AnimationCmp!
', - animations: - [trigger('myAnimation', [transition('void => *', [style({opacity: 1}), animate(5)])])], + '
Hello from AnimationCmp!
', + animations: [ + trigger('myAnimation', [transition('void => *', [style({opacity: 1}), animate(5)])]), + ], }) class AnimationCmp { renderer = _inject(ANIMATION_MODULE_TYPE, {optional: true}) ?? 'not found'; @@ -398,7 +426,7 @@ describe('bootstrap factory method', () => { const cmp = appRef.components[0].instance; // Wait until animation is completed. - await new Promise(resolve => setTimeout(resolve, 10)); + await new Promise((resolve) => setTimeout(resolve, 10)); expect(cmp.renderer).toBe('BrowserAnimations'); expect(cmp.startEvent.triggerName).toEqual('myAnimation'); @@ -414,7 +442,7 @@ describe('bootstrap factory method', () => { const cmp = appRef.components[0].instance; // Wait until animation is completed. - await new Promise(resolve => setTimeout(resolve, 10)); + await new Promise((resolve) => setTimeout(resolve, 10)); expect(cmp.renderer).toBe('NoopAnimations'); expect(cmp.startEvent.triggerName).toEqual('myAnimation'); @@ -424,47 +452,46 @@ describe('bootstrap factory method', () => { }); }); - it('initializes modules inside the NgZone when using `provideZoneChangeDetection`', - async () => { - let moduleInitialized = false; - @NgModule({}) - class SomeModule { - constructor() { - expect(NgZone.isInAngularZone()).toBe(true); - moduleInitialized = true; - } - } - @Component({ - template: '', - selector: 'hello-app', - imports: [SomeModule], - standalone: true, - }) - class AnimationCmp { - } - - await bootstrapApplication(AnimationCmp, { - providers: [provideZoneChangeDetection({eventCoalescing: true})], - }); - expect(moduleInitialized).toBe(true); - }); + it('initializes modules inside the NgZone when using `provideZoneChangeDetection`', async () => { + let moduleInitialized = false; + @NgModule({}) + class SomeModule { + constructor() { + expect(NgZone.isInAngularZone()).toBe(true); + moduleInitialized = true; + } + } + @Component({ + template: '', + selector: 'hello-app', + imports: [SomeModule], + standalone: true, + }) + class AnimationCmp {} + + await bootstrapApplication(AnimationCmp, { + providers: [provideZoneChangeDetection({eventCoalescing: true})], + }); + expect(moduleInitialized).toBe(true); + }); }); - it('should throw if bootstrapped Directive is not a Component', done => { + it('should throw if bootstrapped Directive is not a Component', (done) => { const logger = new MockConsole(); const errorHandler = new ErrorHandler(); (errorHandler as any)._console = logger as any; - bootstrap(HelloRootDirectiveIsNotCmp, [ - {provide: ErrorHandler, useValue: errorHandler} - ]).catch((error: Error) => { - expect(error).toEqual( - new Error(`HelloRootDirectiveIsNotCmp cannot be used as an entry component.`)); - done(); - }); + bootstrap(HelloRootDirectiveIsNotCmp, [{provide: ErrorHandler, useValue: errorHandler}]).catch( + (error: Error) => { + expect(error).toEqual( + new Error(`HelloRootDirectiveIsNotCmp cannot be used as an entry component.`), + ); + done(); + }, + ); }); it('should have the TransferState token available in NgModule bootstrap', async () => { - let state: TransferState|undefined; + let state: TransferState | undefined; @Component({ selector: 'hello-app', template: '...', @@ -480,24 +507,25 @@ describe('bootstrap factory method', () => { }); it('should retrieve sanitizer', inject([Injector], (injector: Injector) => { - const sanitizer: Sanitizer|null = injector.get(Sanitizer, null); - // We don't want to have sanitizer in DI. We use DI only to overwrite the - // sanitizer, but not for default one. The default one is pulled in by the Ivy - // instructions as needed. - expect(sanitizer).toBe(null); - })); - - it('should throw if no element is found', done => { + const sanitizer: Sanitizer | null = injector.get(Sanitizer, null); + // We don't want to have sanitizer in DI. We use DI only to overwrite the + // sanitizer, but not for default one. The default one is pulled in by the Ivy + // instructions as needed. + expect(sanitizer).toBe(null); + })); + + it('should throw if no element is found', (done) => { const logger = new MockConsole(); const errorHandler = new ErrorHandler(); (errorHandler as any)._console = logger as any; - bootstrap(NonExistentComp, [ - {provide: ErrorHandler, useValue: errorHandler} - ]).then(null, (reason) => { - expect(reason.message).toContain('The selector "non-existent" did not match any elements'); - done(); - return null; - }); + bootstrap(NonExistentComp, [{provide: ErrorHandler, useValue: errorHandler}]).then( + null, + (reason) => { + expect(reason.message).toContain('The selector "non-existent" did not match any elements'); + done(); + return null; + }, + ); }); it('should throw if no provider', async () => { @@ -516,43 +544,43 @@ describe('bootstrap factory method', () => { selector: 'hello-app', template: '', }) - class RootCmp { - } + class RootCmp {} @NgModule({declarations: [CustomCmp], exports: [CustomCmp]}) - class CustomModule { - } + class CustomModule {} - await expectAsync(bootstrap(RootCmp, [{provide: ErrorHandler, useValue: errorHandler}], [], [ - CustomModule - ])).toBeRejected(); + await expectAsync( + bootstrap(RootCmp, [{provide: ErrorHandler, useValue: errorHandler}], [], [CustomModule]), + ).toBeRejected(); }); if (getDOM().supportsDOMEvents) { - it('should forward the error to promise when bootstrap fails', done => { + it('should forward the error to promise when bootstrap fails', (done) => { const logger = new MockConsole(); const errorHandler = new ErrorHandler(); (errorHandler as any)._console = logger as any; - const refPromise = - bootstrap(NonExistentComp, [{provide: ErrorHandler, useValue: errorHandler}]); + const refPromise = bootstrap(NonExistentComp, [ + {provide: ErrorHandler, useValue: errorHandler}, + ]); refPromise.then(null, (reason: any) => { expect(reason.message).toContain('The selector "non-existent" did not match any elements'); done(); }); }); - it('should invoke the default exception handler when bootstrap fails', done => { + it('should invoke the default exception handler when bootstrap fails', (done) => { const logger = new MockConsole(); const errorHandler = new ErrorHandler(); (errorHandler as any)._console = logger as any; - const refPromise = - bootstrap(NonExistentComp, [{provide: ErrorHandler, useValue: errorHandler}]); + const refPromise = bootstrap(NonExistentComp, [ + {provide: ErrorHandler, useValue: errorHandler}, + ]); refPromise.then(null, (reason) => { - expect(logger.res[0].join('#')) - .toContain( - 'ERROR#Error: NG05104: The selector "non-existent" did not match any elements'); + expect(logger.res[0].join('#')).toContain( + 'ERROR#Error: NG05104: The selector "non-existent" did not match any elements', + ); done(); return null; }); @@ -562,10 +590,10 @@ describe('bootstrap factory method', () => { it('should create an injector promise', async () => { const refPromise = bootstrap(HelloRootCmp, testProviders); expect(refPromise).toEqual(jasmine.any(Promise)); - await refPromise; // complete component initialization before switching to the next test + await refPromise; // complete component initialization before switching to the next test }); - it('should set platform name to browser', done => { + it('should set platform name to browser', (done) => { const refPromise = bootstrap(HelloRootCmp, testProviders); refPromise.then((ref) => { expect(isPlatformBrowser(ref.injector.get(PLATFORM_ID))).toBe(true); @@ -573,7 +601,7 @@ describe('bootstrap factory method', () => { }, done.fail); }); - it('should display hello world', done => { + it('should display hello world', (done) => { const refPromise = bootstrap(HelloRootCmp, testProviders); refPromise.then((ref) => { expect(el).toHaveText('hello world!'); @@ -582,26 +610,27 @@ describe('bootstrap factory method', () => { }, done.fail); }); - it('should throw a descriptive error if BrowserModule is installed again via a lazily loaded module', - done => { - @NgModule({imports: [BrowserModule]}) - class AsyncModule { - } - bootstrap(HelloRootCmp, testProviders) - .then((ref: ComponentRef) => { - const compiler: Compiler = ref.injector.get(Compiler); - return compiler.compileModuleAsync(AsyncModule).then(factory => { - expect(() => factory.create(ref.injector)) - .toThrowError( - 'NG05100: Providers from the `BrowserModule` have already been loaded. ' + - 'If you need access to common directives such as NgIf and NgFor, ' + - 'import the `CommonModule` instead.'); - }); - }) - .then(() => done(), err => done.fail(err)); - }); - - it('should support multiple calls to bootstrap', done => { + it('should throw a descriptive error if BrowserModule is installed again via a lazily loaded module', (done) => { + @NgModule({imports: [BrowserModule]}) + class AsyncModule {} + bootstrap(HelloRootCmp, testProviders) + .then((ref: ComponentRef) => { + const compiler: Compiler = ref.injector.get(Compiler); + return compiler.compileModuleAsync(AsyncModule).then((factory) => { + expect(() => factory.create(ref.injector)).toThrowError( + 'NG05100: Providers from the `BrowserModule` have already been loaded. ' + + 'If you need access to common directives such as NgIf and NgFor, ' + + 'import the `CommonModule` instead.', + ); + }); + }) + .then( + () => done(), + (err) => done.fail(err), + ); + }); + + it('should support multiple calls to bootstrap', (done) => { const refPromise1 = bootstrap(HelloRootCmp, testProviders); const refPromise2 = bootstrap(HelloRootCmp2, testProviders); Promise.all([refPromise1, refPromise2]).then((refs) => { @@ -611,15 +640,14 @@ describe('bootstrap factory method', () => { }, done.fail); }); - it('should not crash if change detection is invoked when the root component is disposed', - done => { - bootstrap(HelloOnDestroyTickCmp, testProviders).then((ref) => { - expect(() => ref.destroy()).not.toThrow(); - done(); - }); - }); + it('should not crash if change detection is invoked when the root component is disposed', (done) => { + bootstrap(HelloOnDestroyTickCmp, testProviders).then((ref) => { + expect(() => ref.destroy()).not.toThrow(); + done(); + }); + }); - it('should unregister change detectors when components are disposed', done => { + it('should unregister change detectors when components are disposed', (done) => { bootstrap(HelloRootCmp, testProviders).then((ref) => { const appRef = ref.injector.get(ApplicationRef); ref.destroy(); @@ -628,9 +656,11 @@ describe('bootstrap factory method', () => { }, done.fail); }); - it('should make the provided bindings available to the application component', done => { - const refPromise = - bootstrap(HelloRootCmp3, [testProviders, {provide: 'appBinding', useValue: 'BoundValue'}]); + it('should make the provided bindings available to the application component', (done) => { + const refPromise = bootstrap(HelloRootCmp3, [ + testProviders, + {provide: 'appBinding', useValue: 'BoundValue'}, + ]); refPromise.then((ref) => { expect(ref.injector.get('appBinding')).toEqual('BoundValue'); @@ -638,17 +668,20 @@ describe('bootstrap factory method', () => { }, done.fail); }); - it('should not override locale provided during bootstrap', done => { - const refPromise = - bootstrap(HelloRootCmp, [testProviders], [{provide: LOCALE_ID, useValue: 'fr-FR'}]); + it('should not override locale provided during bootstrap', (done) => { + const refPromise = bootstrap( + HelloRootCmp, + [testProviders], + [{provide: LOCALE_ID, useValue: 'fr-FR'}], + ); - refPromise.then(ref => { + refPromise.then((ref) => { expect(ref.injector.get(LOCALE_ID)).toEqual('fr-FR'); done(); }, done.fail); }); - it('should avoid cyclic dependencies when root component requires Lifecycle through DI', done => { + it('should avoid cyclic dependencies when root component requires Lifecycle through DI', (done) => { const refPromise = bootstrap(HelloRootCmp4, testProviders); refPromise.then((ref) => { @@ -658,19 +691,19 @@ describe('bootstrap factory method', () => { }, done.fail); }); - it('should run platform initializers', done => { + it('should run platform initializers', (done) => { inject([Log], (log: Log) => { const p = createPlatformFactory(platformBrowserDynamic, 'someName', [ {provide: PLATFORM_INITIALIZER, useValue: log.fn('platform_init1'), multi: true}, - {provide: PLATFORM_INITIALIZER, useValue: log.fn('platform_init2'), multi: true} + {provide: PLATFORM_INITIALIZER, useValue: log.fn('platform_init2'), multi: true}, ])(); @NgModule({ imports: [BrowserModule], providers: [ {provide: APP_INITIALIZER, useValue: log.fn('app_init1'), multi: true}, - {provide: APP_INITIALIZER, useValue: log.fn('app_init2'), multi: true} - ] + {provide: APP_INITIALIZER, useValue: log.fn('app_init2'), multi: true}, + ], }) class SomeModule { ngDoBootstrap() {} @@ -687,11 +720,11 @@ describe('bootstrap factory method', () => { it('should not allow provideZoneChangeDetection in bootstrapModule', async () => { @NgModule({imports: [BrowserModule], providers: [provideZoneChangeDetection()]}) - class SomeModule { - } + class SomeModule {} - await expectAsync(platformBrowserDynamic().bootstrapModule(SomeModule)) - .toBeRejectedWithError(/provideZoneChangeDetection.*BootstrapOptions/); + await expectAsync(platformBrowserDynamic().bootstrapModule(SomeModule)).toBeRejectedWithError( + /provideZoneChangeDetection.*BootstrapOptions/, + ); }); it('should register each application with the testability registry', async () => { @@ -706,7 +739,7 @@ describe('bootstrap factory method', () => { expect(registry.findTestabilityInTree(el2)).toEqual(ngModuleRef2.injector.get(Testability)); }); - it('should allow to pass schemas', done => { + it('should allow to pass schemas', (done) => { bootstrap(HelloCmpUsingCustomElement, testProviders).then(() => { expect(el).toHaveText('hello world!'); done(); @@ -745,35 +778,34 @@ describe('bootstrap factory method', () => { } } - it('should be triggered for all bootstrapped components in case change happens in one of them', - done => { - @NgModule({ - imports: [BrowserModule], - declarations: [CompA, CompB], - bootstrap: [CompA, CompB], - schemas: [CUSTOM_ELEMENTS_SCHEMA] - }) - class TestModuleA { - } - platformBrowserDynamic().bootstrapModule(TestModuleA).then((ref) => { - log.length = 0; - (el.querySelectorAll('#button-a')[0]).click(); - expect(log).toContain('CompA:onClick'); - expect(log).toContain('CompA:ngDoCheck'); - expect(log).toContain('CompB:ngDoCheck'); - - log.length = 0; - el2.querySelectorAll('#button-b')[0].click(); - expect(log).toContain('CompB:onClick'); - expect(log).toContain('CompA:ngDoCheck'); - expect(log).toContain('CompB:ngDoCheck'); - - done(); - }, done.fail); - }); - - - it('should work in isolation for each component bootstrapped individually', done => { + it('should be triggered for all bootstrapped components in case change happens in one of them', (done) => { + @NgModule({ + imports: [BrowserModule], + declarations: [CompA, CompB], + bootstrap: [CompA, CompB], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }) + class TestModuleA {} + platformBrowserDynamic() + .bootstrapModule(TestModuleA) + .then((ref) => { + log.length = 0; + el.querySelectorAll('#button-a')[0].click(); + expect(log).toContain('CompA:onClick'); + expect(log).toContain('CompA:ngDoCheck'); + expect(log).toContain('CompB:ngDoCheck'); + + log.length = 0; + el2.querySelectorAll('#button-b')[0].click(); + expect(log).toContain('CompB:onClick'); + expect(log).toContain('CompA:ngDoCheck'); + expect(log).toContain('CompB:ngDoCheck'); + + done(); + }, done.fail); + }); + + it('should work in isolation for each component bootstrapped individually', (done) => { const refPromise1 = bootstrap(CompA); const refPromise2 = bootstrap(CompB); Promise.all([refPromise1, refPromise2]).then((refs) => { diff --git a/packages/platform-browser/test/browser/bootstrap_standalone_spec.ts b/packages/platform-browser/test/browser/bootstrap_standalone_spec.ts index 797ca25d1f470..102aa4ebafab1 100644 --- a/packages/platform-browser/test/browser/bootstrap_standalone_spec.ts +++ b/packages/platform-browser/test/browser/bootstrap_standalone_spec.ts @@ -6,7 +6,17 @@ * found in the LICENSE file at https://angular.io/license */ -import {Component, destroyPlatform, ErrorHandler, Inject, Injectable, InjectionToken, NgModule, NgZone, PlatformRef} from '@angular/core'; +import { + Component, + destroyPlatform, + ErrorHandler, + Inject, + Injectable, + InjectionToken, + NgModule, + NgZone, + PlatformRef, +} from '@angular/core'; import {R3Injector} from '@angular/core/src/di/r3_injector'; import {NoopNgZone} from '@angular/core/src/zone/ng_zone'; import {withBody} from '@angular/private/testing'; @@ -24,59 +34,58 @@ describe('bootstrapApplication for standalone components', () => { } } - it('should create injector where ambient providers shadow explicit providers', - withBody('', async () => { - const testToken = new InjectionToken('test token'); - - @NgModule({ - providers: [ - {provide: testToken, useValue: 'Ambient'}, - ] - }) - class AmbientModule { - } - - @Component({ - selector: 'test-app', - standalone: true, - template: `({{testToken}})`, - imports: [AmbientModule] - }) - class StandaloneCmp { - constructor(@Inject(testToken) readonly testToken: String) {} - } - - const appRef = await bootstrapApplication(StandaloneCmp, { - providers: [ - {provide: testToken, useValue: 'Bootstrap'}, - ] - }); - - appRef.tick(); - - // make sure that ambient providers "shadow" ones explicitly provided during bootstrap - expect(document.body.textContent).toBe('(Ambient)'); - })); - - it('should be able to provide a custom zone implementation in DI', - withBody('', async () => { - @Component({ - selector: 'test-app', - standalone: true, - template: ``, - }) - class StandaloneCmp { - } - - class CustomZone extends NoopNgZone {} - const instance = new CustomZone(); - - const appRef = await bootstrapApplication( - StandaloneCmp, {providers: [{provide: NgZone, useValue: instance}]}); - - appRef.tick(); - expect(appRef.injector.get(NgZone)).toEqual(instance); - })); + it( + 'should create injector where ambient providers shadow explicit providers', + withBody('', async () => { + const testToken = new InjectionToken('test token'); + + @NgModule({ + providers: [{provide: testToken, useValue: 'Ambient'}], + }) + class AmbientModule {} + + @Component({ + selector: 'test-app', + standalone: true, + template: `({{testToken}})`, + imports: [AmbientModule], + }) + class StandaloneCmp { + constructor(@Inject(testToken) readonly testToken: String) {} + } + + const appRef = await bootstrapApplication(StandaloneCmp, { + providers: [{provide: testToken, useValue: 'Bootstrap'}], + }); + + appRef.tick(); + + // make sure that ambient providers "shadow" ones explicitly provided during bootstrap + expect(document.body.textContent).toBe('(Ambient)'); + }), + ); + + it( + 'should be able to provide a custom zone implementation in DI', + withBody('', async () => { + @Component({ + selector: 'test-app', + standalone: true, + template: ``, + }) + class StandaloneCmp {} + + class CustomZone extends NoopNgZone {} + const instance = new CustomZone(); + + const appRef = await bootstrapApplication(StandaloneCmp, { + providers: [{provide: NgZone, useValue: instance}], + }); + + appRef.tick(); + expect(appRef.injector.get(NgZone)).toEqual(instance); + }), + ); /* This test verifies that ambient providers for the standalone component being bootstrapped @@ -87,153 +96,153 @@ describe('bootstrapApplication for standalone components', () => { - application injector (providers specified in the bootstrap options go here); - standalone injector (ambient providers go here); */ - it('should create a standalone injector for standalone components with ambient providers', - withBody('', async () => { - const ambientToken = new InjectionToken('ambient token'); - - @NgModule({ - providers: [ - {provide: ambientToken, useValue: 'Only in AmbientNgModule'}, - ] - }) - class AmbientModule { - } - - @Injectable() - class NeedsAmbientProvider { - constructor(@Inject(ambientToken) readonly ambientToken: String) {} - } - - @Component({ - selector: 'test-app', - template: `({{service.ambientToken}})`, - standalone: true, - imports: [AmbientModule] - }) - class StandaloneCmp { - constructor(readonly service: NeedsAmbientProvider) {} - } - - try { - await bootstrapApplication( - StandaloneCmp, - { - providers: [ - {provide: ErrorHandler, useClass: SilentErrorHandler}, - NeedsAmbientProvider, - ] - }, - ); - - // we expect the bootstrap process to fail since the "NeedsAmbientProvider" service - // (located in the application injector) can't "see" ambient providers (located in a - // standalone injector that is a child of the application injector). - fail('Expected to throw'); - } catch (e: unknown) { - expect(e).toBeInstanceOf(Error); - expect((e as Error).message).toContain('No provider for InjectionToken ambient token!'); - } - })); - - it('should throw if `BrowserModule` is imported in the standalone bootstrap scenario', - withBody('', async () => { - @Component({ - selector: 'test-app', - template: '...', - standalone: true, - imports: [BrowserModule], - }) - class StandaloneCmp { - } - - try { - await bootstrapApplication( - StandaloneCmp, {providers: [{provide: ErrorHandler, useClass: SilentErrorHandler}]}); - - // The `bootstrapApplication` already includes the set of providers from the - // `BrowserModule`, so including the `BrowserModule` again will bring duplicate providers - // and we want to avoid it. - fail('Expected to throw'); - } catch (e: unknown) { - expect(e).toBeInstanceOf(Error); - expect((e as Error).message) - .toContain('NG05100: Providers from the `BrowserModule` have already been loaded.'); - } - })); - - it('should throw if `BrowserModule` is imported indirectly in the standalone bootstrap scenario', - withBody('', async () => { - @NgModule({ - imports: [BrowserModule], - }) - class SomeDependencyModule { - } - - @Component({ - selector: 'test-app', - template: '...', - standalone: true, - imports: [SomeDependencyModule], - }) - class StandaloneCmp { - } - - try { - await bootstrapApplication( - StandaloneCmp, {providers: [{provide: ErrorHandler, useClass: SilentErrorHandler}]}); - - // The `bootstrapApplication` already includes the set of providers from the - // `BrowserModule`, so including the `BrowserModule` again will bring duplicate providers - // and we want to avoid it. - fail('Expected to throw'); - } catch (e: unknown) { - expect(e).toBeInstanceOf(Error); - expect((e as Error).message) - .toContain('NG05100: Providers from the `BrowserModule` have already been loaded.'); - } - })); - - it('should trigger an app destroy when a platform is destroyed', - withBody('', async () => { - let compOnDestroyCalled = false; - let serviceOnDestroyCalled = false; - let injectorOnDestroyCalled = false; - - @Injectable({providedIn: 'root'}) - class ServiceWithOnDestroy { - ngOnDestroy() { - serviceOnDestroyCalled = true; - } - } - - @Component({ - selector: 'test-app', - standalone: true, - template: 'Hello', - }) - class ComponentWithOnDestroy { - constructor(service: ServiceWithOnDestroy) {} - - ngOnDestroy() { - compOnDestroyCalled = true; - } - } - - const appRef = await bootstrapApplication(ComponentWithOnDestroy); - const injector = (appRef as unknown as {injector: R3Injector}).injector; - injector.onDestroy(() => injectorOnDestroyCalled = true); - - expect(document.body.textContent).toBe('Hello'); - - const platformRef = injector.get(PlatformRef); - platformRef.destroy(); - - // Verify the callbacks were invoked. - expect(compOnDestroyCalled).toBe(true); - expect(serviceOnDestroyCalled).toBe(true); - expect(injectorOnDestroyCalled).toBe(true); - - // Make sure the DOM has been cleaned up as well. - expect(document.body.textContent).toBe(''); - })); + it( + 'should create a standalone injector for standalone components with ambient providers', + withBody('', async () => { + const ambientToken = new InjectionToken('ambient token'); + + @NgModule({ + providers: [{provide: ambientToken, useValue: 'Only in AmbientNgModule'}], + }) + class AmbientModule {} + + @Injectable() + class NeedsAmbientProvider { + constructor(@Inject(ambientToken) readonly ambientToken: String) {} + } + + @Component({ + selector: 'test-app', + template: `({{service.ambientToken}})`, + standalone: true, + imports: [AmbientModule], + }) + class StandaloneCmp { + constructor(readonly service: NeedsAmbientProvider) {} + } + + try { + await bootstrapApplication(StandaloneCmp, { + providers: [{provide: ErrorHandler, useClass: SilentErrorHandler}, NeedsAmbientProvider], + }); + + // we expect the bootstrap process to fail since the "NeedsAmbientProvider" service + // (located in the application injector) can't "see" ambient providers (located in a + // standalone injector that is a child of the application injector). + fail('Expected to throw'); + } catch (e: unknown) { + expect(e).toBeInstanceOf(Error); + expect((e as Error).message).toContain('No provider for InjectionToken ambient token!'); + } + }), + ); + + it( + 'should throw if `BrowserModule` is imported in the standalone bootstrap scenario', + withBody('', async () => { + @Component({ + selector: 'test-app', + template: '...', + standalone: true, + imports: [BrowserModule], + }) + class StandaloneCmp {} + + try { + await bootstrapApplication(StandaloneCmp, { + providers: [{provide: ErrorHandler, useClass: SilentErrorHandler}], + }); + + // The `bootstrapApplication` already includes the set of providers from the + // `BrowserModule`, so including the `BrowserModule` again will bring duplicate + // providers and we want to avoid it. + fail('Expected to throw'); + } catch (e: unknown) { + expect(e).toBeInstanceOf(Error); + expect((e as Error).message).toContain( + 'NG05100: Providers from the `BrowserModule` have already been loaded.', + ); + } + }), + ); + + it( + 'should throw if `BrowserModule` is imported indirectly in the standalone bootstrap scenario', + withBody('', async () => { + @NgModule({ + imports: [BrowserModule], + }) + class SomeDependencyModule {} + + @Component({ + selector: 'test-app', + template: '...', + standalone: true, + imports: [SomeDependencyModule], + }) + class StandaloneCmp {} + + try { + await bootstrapApplication(StandaloneCmp, { + providers: [{provide: ErrorHandler, useClass: SilentErrorHandler}], + }); + + // The `bootstrapApplication` already includes the set of providers from the + // `BrowserModule`, so including the `BrowserModule` again will bring duplicate + // providers and we want to avoid it. + fail('Expected to throw'); + } catch (e: unknown) { + expect(e).toBeInstanceOf(Error); + expect((e as Error).message).toContain( + 'NG05100: Providers from the `BrowserModule` have already been loaded.', + ); + } + }), + ); + + it( + 'should trigger an app destroy when a platform is destroyed', + withBody('', async () => { + let compOnDestroyCalled = false; + let serviceOnDestroyCalled = false; + let injectorOnDestroyCalled = false; + + @Injectable({providedIn: 'root'}) + class ServiceWithOnDestroy { + ngOnDestroy() { + serviceOnDestroyCalled = true; + } + } + + @Component({ + selector: 'test-app', + standalone: true, + template: 'Hello', + }) + class ComponentWithOnDestroy { + constructor(service: ServiceWithOnDestroy) {} + + ngOnDestroy() { + compOnDestroyCalled = true; + } + } + + const appRef = await bootstrapApplication(ComponentWithOnDestroy); + const injector = (appRef as unknown as {injector: R3Injector}).injector; + injector.onDestroy(() => (injectorOnDestroyCalled = true)); + + expect(document.body.textContent).toBe('Hello'); + + const platformRef = injector.get(PlatformRef); + platformRef.destroy(); + + // Verify the callbacks were invoked. + expect(compOnDestroyCalled).toBe(true); + expect(serviceOnDestroyCalled).toBe(true); + expect(injectorOnDestroyCalled).toBe(true); + + // Make sure the DOM has been cleaned up as well. + expect(document.body.textContent).toBe(''); + }), + ); }); diff --git a/packages/platform-browser/test/browser/meta_spec.ts b/packages/platform-browser/test/browser/meta_spec.ts index 340aa2a3c9ea1..bde5b2be16e14 100644 --- a/packages/platform-browser/test/browser/meta_spec.ts +++ b/packages/platform-browser/test/browser/meta_spec.ts @@ -145,7 +145,7 @@ describe('Meta service', () => { metaService.addTags([ {name: 'twitter:title', content: 'Content Title'}, - {property: 'og:title', content: 'Content Title'} + {property: 'og:title', content: 'Content Title'}, ]); const twitterMeta = metaService.getTag(nameSelector)!; const fbMeta = metaService.getTag(propertySelector)!; @@ -166,15 +166,14 @@ describe('Meta service', () => { expect(metaService.getTags(selector).length).toEqual(1); }); - it('should not add meta tag if it is already present on the page, even if the first tag with the same name has different other attributes', - () => { - metaService.addTag({name: 'description', content: 'aaa'}); - metaService.addTag({name: 'description', content: 'bbb'}); - metaService.addTag({name: 'description', content: 'aaa'}); - metaService.addTag({name: 'description', content: 'bbb'}); + it('should not add meta tag if it is already present on the page, even if the first tag with the same name has different other attributes', () => { + metaService.addTag({name: 'description', content: 'aaa'}); + metaService.addTag({name: 'description', content: 'bbb'}); + metaService.addTag({name: 'description', content: 'aaa'}); + metaService.addTag({name: 'description', content: 'bbb'}); - expect(metaService.getTags('name="description"').length).toEqual(2); - }); + expect(metaService.getTags('name="description"').length).toEqual(2); + }); it('should add meta tag if it is already present on the page and but has different attr', () => { const selector = 'property="fb:app_id"'; @@ -214,6 +213,6 @@ describe('integration test', () => { }); }); - it('should inject Meta service when using BrowserModule', - () => expect(TestBed.inject(DependsOnMeta).meta).toBeInstanceOf(Meta)); + it('should inject Meta service when using BrowserModule', () => + expect(TestBed.inject(DependsOnMeta).meta).toBeInstanceOf(Meta)); }); diff --git a/packages/platform-browser/test/browser/tools/tools_spec.ts b/packages/platform-browser/test/browser/tools/tools_spec.ts index 4dac6f2b9bcca..3d7e03bd4b4fa 100644 --- a/packages/platform-browser/test/browser/tools/tools_spec.ts +++ b/packages/platform-browser/test/browser/tools/tools_spec.ts @@ -22,13 +22,19 @@ describe('profiler', () => { beforeEach(() => { enableDebugTools({ injector: Injector.create({ - providers: [{ - provide: ApplicationRef, - useValue: jasmine.createSpyObj( - 'ApplicationRef', ['bootstrap', 'tick', 'attachView', 'detachView']), - deps: [] - }] - }) + providers: [ + { + provide: ApplicationRef, + useValue: jasmine.createSpyObj('ApplicationRef', [ + 'bootstrap', + 'tick', + 'attachView', + 'detachView', + ]), + deps: [], + }, + ], + }), } as ComponentRef); }); diff --git a/packages/platform-browser/test/dom/dom_renderer_spec.ts b/packages/platform-browser/test/dom/dom_renderer_spec.ts index ac9d7e8ac87c8..8fbd192921a5c 100644 --- a/packages/platform-browser/test/dom/dom_renderer_spec.ts +++ b/packages/platform-browser/test/dom/dom_renderer_spec.ts @@ -8,10 +8,12 @@ import {Component, Renderer2, ViewEncapsulation} from '@angular/core'; import {ComponentFixture, TestBed} from '@angular/core/testing'; import {By} from '@angular/platform-browser/src/dom/debug/by'; -import {NAMESPACE_URIS, REMOVE_STYLES_ON_COMPONENT_DESTROY} from '@angular/platform-browser/src/dom/dom_renderer'; +import { + NAMESPACE_URIS, + REMOVE_STYLES_ON_COMPONENT_DESTROY, +} from '@angular/platform-browser/src/dom/dom_renderer'; import {expect} from '@angular/platform-browser/testing/src/matchers'; - describe('DefaultDomRendererV2', () => { if (isNode) { // Jasmine will throw if there are no tests. @@ -106,22 +108,21 @@ describe('DefaultDomRendererV2', () => { }); }); - it('should allow to style components with emulated encapsulation and no encapsulation inside of components with shadow DOM', - () => { - const fixture = TestBed.createComponent(SomeApp); - fixture.detectChanges(); + it('should allow to style components with emulated encapsulation and no encapsulation inside of components with shadow DOM', () => { + const fixture = TestBed.createComponent(SomeApp); + fixture.detectChanges(); - const cmp = fixture.debugElement.query(By.css('cmp-shadow')).nativeElement; - const shadow = cmp.shadowRoot.querySelector('.shadow'); + const cmp = fixture.debugElement.query(By.css('cmp-shadow')).nativeElement; + const shadow = cmp.shadowRoot.querySelector('.shadow'); - expect(window.getComputedStyle(shadow).color).toEqual('rgb(255, 0, 0)'); + expect(window.getComputedStyle(shadow).color).toEqual('rgb(255, 0, 0)'); - const emulated = cmp.shadowRoot.querySelector('.emulated'); - expect(window.getComputedStyle(emulated).color).toEqual('rgb(0, 0, 255)'); + const emulated = cmp.shadowRoot.querySelector('.emulated'); + expect(window.getComputedStyle(emulated).color).toEqual('rgb(0, 0, 255)'); - const none = cmp.shadowRoot.querySelector('.none'); - expect(window.getComputedStyle(none).color).toEqual('rgb(0, 255, 0)'); - }); + const none = cmp.shadowRoot.querySelector('.none'); + expect(window.getComputedStyle(none).color).toEqual('rgb(0, 255, 0)'); + }); it('should be able to append children to a