BREAKING CHANGE: typescript 3.4 and 3.5 are no longer supported, please update to typescript 3.6 Fixes #32380 PR Close #32946
		
			
				
	
	
		
			662 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			662 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| /**
 | |
|  * @license
 | |
|  * Copyright Google Inc. All Rights Reserved.
 | |
|  *
 | |
|  * Use of this source code is governed by an MIT-style license that can be
 | |
|  * found in the LICENSE file at https://angular.io/license
 | |
|  */
 | |
| 
 | |
| import {DOCUMENT, ɵgetDOM as getDOM} from '@angular/common';
 | |
| import {ResourceLoader} from '@angular/compiler';
 | |
| import {APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, Compiler, CompilerFactory, Component, InjectionToken, LOCALE_ID, NgModule, NgZone, PlatformRef, TemplateRef, Type, ViewChild, ViewContainerRef} from '@angular/core';
 | |
| import {ApplicationRef} from '@angular/core/src/application_ref';
 | |
| import {ErrorHandler} from '@angular/core/src/error_handler';
 | |
| import {ComponentRef} from '@angular/core/src/linker/component_factory';
 | |
| import {getLocaleId} from '@angular/core/src/render3';
 | |
| import {BrowserModule} from '@angular/platform-browser';
 | |
| import {createTemplate, dispatchEvent, getContent} from '@angular/platform-browser/testing/src/browser_util';
 | |
| import {expect} from '@angular/platform-browser/testing/src/matchers';
 | |
| import {onlyInIvy} from '@angular/private/testing';
 | |
| 
 | |
| import {NoopNgZone} from '../src/zone/ng_zone';
 | |
| import {ComponentFixtureNoNgZone, TestBed, async, inject, withModule} from '../testing';
 | |
| 
 | |
| @Component({selector: 'bootstrap-app', template: 'hello'})
 | |
| class SomeComponent {
 | |
| }
 | |
| 
 | |
| {
 | |
|   describe('bootstrap', () => {
 | |
|     let mockConsole: MockConsole;
 | |
| 
 | |
|     beforeEach(() => { mockConsole = new MockConsole(); });
 | |
| 
 | |
|     function createRootEl(selector = 'bootstrap-app') {
 | |
|       const doc = TestBed.inject(DOCUMENT);
 | |
|       const rootEl =
 | |
|           <HTMLElement>getContent(createTemplate(`<${selector}></${selector}>`)).firstChild;
 | |
|       const oldRoots = doc.querySelectorAll(selector);
 | |
|       for (let i = 0; i < oldRoots.length; i++) {
 | |
|         getDOM().remove(oldRoots[i]);
 | |
|       }
 | |
|       doc.body.appendChild(rootEl);
 | |
|     }
 | |
| 
 | |
|     type CreateModuleOptions =
 | |
|         {providers?: any[], ngDoBootstrap?: any, bootstrap?: any[], component?: Type<any>};
 | |
| 
 | |
|     function createModule(providers?: any[]): Type<any>;
 | |
|     function createModule(options: CreateModuleOptions): Type<any>;
 | |
|     function createModule(providersOrOptions: any[] | CreateModuleOptions | undefined): Type<any> {
 | |
|       let options: CreateModuleOptions = {};
 | |
|       if (Array.isArray(providersOrOptions)) {
 | |
|         options = {providers: providersOrOptions};
 | |
|       } else {
 | |
|         options = providersOrOptions || {};
 | |
|       }
 | |
|       const errorHandler = new ErrorHandler();
 | |
|       (errorHandler as any)._console = mockConsole as any;
 | |
| 
 | |
|       const platformModule = getDOM().supportsDOMEvents() ?
 | |
|           BrowserModule :
 | |
|           require('@angular/platform-server').ServerModule;
 | |
| 
 | |
|       @NgModule({
 | |
|         providers: [{provide: ErrorHandler, useValue: errorHandler}, options.providers || []],
 | |
|         imports: [platformModule],
 | |
|         declarations: [options.component || SomeComponent],
 | |
|         entryComponents: [options.component || SomeComponent],
 | |
|         bootstrap: options.bootstrap || []
 | |
|       })
 | |
|       class MyModule {
 | |
|       }
 | |
|       if (options.ngDoBootstrap !== false) {
 | |
|         (<any>MyModule.prototype).ngDoBootstrap = options.ngDoBootstrap || (() => {});
 | |
|       }
 | |
|       return MyModule;
 | |
|     }
 | |
| 
 | |
|     it('should bootstrap a component from a child module',
 | |
|        async(inject([ApplicationRef, Compiler], (app: ApplicationRef, compiler: Compiler) => {
 | |
|          @Component({
 | |
|            selector: 'bootstrap-app',
 | |
|            template: '',
 | |
|          })
 | |
|          class SomeComponent {
 | |
|          }
 | |
| 
 | |
|          const helloToken = new InjectionToken<string>('hello');
 | |
| 
 | |
|          @NgModule({
 | |
|            providers: [{provide: helloToken, useValue: 'component'}],
 | |
|            declarations: [SomeComponent],
 | |
|            entryComponents: [SomeComponent],
 | |
|          })
 | |
|          class SomeModule {
 | |
|          }
 | |
| 
 | |
|          createRootEl();
 | |
|          const modFactory = compiler.compileModuleSync(SomeModule);
 | |
|          const module = modFactory.create(TestBed);
 | |
|          const cmpFactory =
 | |
|              module.componentFactoryResolver.resolveComponentFactory(SomeComponent) !;
 | |
|          const component = app.bootstrap(cmpFactory);
 | |
| 
 | |
|          // The component should see the child module providers
 | |
|          expect(component.injector.get(helloToken)).toEqual('component');
 | |
|        })));
 | |
| 
 | |
|     it('should bootstrap a component with a custom selector',
 | |
|        async(inject([ApplicationRef, Compiler], (app: ApplicationRef, compiler: Compiler) => {
 | |
|          @Component({
 | |
|            selector: 'bootstrap-app',
 | |
|            template: '',
 | |
|          })
 | |
|          class SomeComponent {
 | |
|          }
 | |
| 
 | |
|          const helloToken = new InjectionToken<string>('hello');
 | |
| 
 | |
|          @NgModule({
 | |
|            providers: [{provide: helloToken, useValue: 'component'}],
 | |
|            declarations: [SomeComponent],
 | |
|            entryComponents: [SomeComponent],
 | |
|          })
 | |
|          class SomeModule {
 | |
|          }
 | |
| 
 | |
|          createRootEl('custom-selector');
 | |
|          const modFactory = compiler.compileModuleSync(SomeModule);
 | |
|          const module = modFactory.create(TestBed);
 | |
|          const cmpFactory =
 | |
|              module.componentFactoryResolver.resolveComponentFactory(SomeComponent) !;
 | |
|          const component = app.bootstrap(cmpFactory, 'custom-selector');
 | |
| 
 | |
|          // The component should see the child module providers
 | |
|          expect(component.injector.get(helloToken)).toEqual('component');
 | |
|        })));
 | |
| 
 | |
|     describe('ApplicationRef', () => {
 | |
|       beforeEach(() => { TestBed.configureTestingModule({imports: [createModule()]}); });
 | |
| 
 | |
|       it('should throw when reentering tick', () => {
 | |
|         @Component({template: '{{reenter()}}'})
 | |
|         class ReenteringComponent {
 | |
|           reenterCount = 1;
 | |
|           reenterErr: any;
 | |
| 
 | |
|           constructor(private appRef: ApplicationRef) {}
 | |
| 
 | |
|           reenter() {
 | |
|             if (this.reenterCount--) {
 | |
|               try {
 | |
|                 this.appRef.tick();
 | |
|               } catch (e) {
 | |
|                 this.reenterErr = e;
 | |
|               }
 | |
|             }
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         const fixture = TestBed.configureTestingModule({declarations: [ReenteringComponent]})
 | |
|                             .createComponent(ReenteringComponent);
 | |
|         const appRef = TestBed.inject(ApplicationRef);
 | |
|         appRef.attachView(fixture.componentRef.hostView);
 | |
|         appRef.tick();
 | |
|         expect(fixture.componentInstance.reenterErr.message)
 | |
|             .toBe('ApplicationRef.tick is called recursively');
 | |
|       });
 | |
| 
 | |
|       describe('APP_BOOTSTRAP_LISTENER', () => {
 | |
|         let capturedCompRefs: ComponentRef<any>[];
 | |
|         beforeEach(() => {
 | |
|           capturedCompRefs = [];
 | |
|           TestBed.configureTestingModule({
 | |
|             providers: [{
 | |
|               provide: APP_BOOTSTRAP_LISTENER,
 | |
|               multi: true,
 | |
|               useValue: (compRef: any) => { capturedCompRefs.push(compRef); }
 | |
|             }]
 | |
|           });
 | |
|         });
 | |
| 
 | |
|         it('should be called when a component is bootstrapped',
 | |
|            inject([ApplicationRef], (ref: ApplicationRef) => {
 | |
|              createRootEl();
 | |
|              const compRef = ref.bootstrap(SomeComponent);
 | |
|              expect(capturedCompRefs).toEqual([compRef]);
 | |
|            }));
 | |
|       });
 | |
| 
 | |
|       describe('bootstrap', () => {
 | |
|         it('should throw if an APP_INITIIALIZER is not yet resolved',
 | |
|            withModule(
 | |
|                {
 | |
|                  providers: [
 | |
|                    {provide: APP_INITIALIZER, useValue: () => new Promise(() => {}), multi: true}
 | |
|                  ]
 | |
|                },
 | |
|                inject([ApplicationRef], (ref: ApplicationRef) => {
 | |
|                  createRootEl();
 | |
|                  expect(() => ref.bootstrap(SomeComponent))
 | |
|                      .toThrowError(
 | |
|                          'Cannot bootstrap as there are still asynchronous initializers running. Bootstrap components in the `ngDoBootstrap` method of the root module.');
 | |
|                })));
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     describe('bootstrapModule', () => {
 | |
|       let defaultPlatform: PlatformRef;
 | |
|       beforeEach(inject([PlatformRef], (_platform: PlatformRef) => {
 | |
|         createRootEl();
 | |
|         defaultPlatform = _platform;
 | |
|       }));
 | |
| 
 | |
|       it('should wait for asynchronous app initializers', async(() => {
 | |
|            let resolve: (result: any) => void;
 | |
|            const promise: Promise<any> = new Promise((res) => { resolve = res; });
 | |
|            let initializerDone = false;
 | |
|            setTimeout(() => {
 | |
|              resolve(true);
 | |
|              initializerDone = true;
 | |
|            }, 1);
 | |
| 
 | |
|            defaultPlatform
 | |
|                .bootstrapModule(
 | |
|                    createModule([{provide: APP_INITIALIZER, useValue: () => promise, multi: true}]))
 | |
|                .then(_ => { expect(initializerDone).toBe(true); });
 | |
|          }));
 | |
| 
 | |
|       it('should rethrow sync errors even if the exceptionHandler is not rethrowing', async(() => {
 | |
|            defaultPlatform
 | |
|                .bootstrapModule(createModule(
 | |
|                    [{provide: APP_INITIALIZER, useValue: () => { throw 'Test'; }, multi: true}]))
 | |
|                .then(() => expect(false).toBe(true), (e) => {
 | |
|                  expect(e).toBe('Test');
 | |
|                  // Error rethrown will be seen by the exception handler since it's after
 | |
|                  // construction.
 | |
|                  expect(mockConsole.res[0].join('#')).toEqual('ERROR#Test');
 | |
|                });
 | |
|          }));
 | |
| 
 | |
|       it('should rethrow promise errors even if the exceptionHandler is not rethrowing',
 | |
|          async(() => {
 | |
|            defaultPlatform
 | |
|                .bootstrapModule(createModule([
 | |
|                  {provide: APP_INITIALIZER, useValue: () => Promise.reject('Test'), multi: true}
 | |
|                ]))
 | |
|                .then(() => expect(false).toBe(true), (e) => {
 | |
|                  expect(e).toBe('Test');
 | |
|                  expect(mockConsole.res[0].join('#')).toEqual('ERROR#Test');
 | |
|                });
 | |
|          }));
 | |
| 
 | |
|       it('should throw useful error when ApplicationRef is not configured', async(() => {
 | |
|            @NgModule()
 | |
|            class EmptyModule {
 | |
|            }
 | |
| 
 | |
|            return defaultPlatform.bootstrapModule(EmptyModule)
 | |
|                .then(() => fail('expecting error'), (error) => {
 | |
|                  expect(error.message)
 | |
|                      .toEqual('No ErrorHandler. Is platform module (BrowserModule) included?');
 | |
|                });
 | |
|          }));
 | |
| 
 | |
|       it('should call the `ngDoBootstrap` method with `ApplicationRef` on the main module',
 | |
|          async(() => {
 | |
|            const ngDoBootstrap = jasmine.createSpy('ngDoBootstrap');
 | |
|            defaultPlatform.bootstrapModule(createModule({ngDoBootstrap: ngDoBootstrap}))
 | |
|                .then((moduleRef) => {
 | |
|                  const appRef = moduleRef.injector.get(ApplicationRef);
 | |
|                  expect(ngDoBootstrap).toHaveBeenCalledWith(appRef);
 | |
|                });
 | |
|          }));
 | |
| 
 | |
|       it('should auto bootstrap components listed in @NgModule.bootstrap', async(() => {
 | |
|            defaultPlatform.bootstrapModule(createModule({bootstrap: [SomeComponent]}))
 | |
|                .then((moduleRef) => {
 | |
|                  const appRef: ApplicationRef = moduleRef.injector.get(ApplicationRef);
 | |
|                  expect(appRef.componentTypes).toEqual([SomeComponent]);
 | |
|                });
 | |
|          }));
 | |
| 
 | |
|       it('should error if neither `ngDoBootstrap` nor @NgModule.bootstrap was specified',
 | |
|          async(() => {
 | |
|            defaultPlatform.bootstrapModule(createModule({ngDoBootstrap: false}))
 | |
|                .then(() => expect(false).toBe(true), (e) => {
 | |
|                  const expectedErrMsg =
 | |
|                      `The module MyModule was bootstrapped, but it does not declare "@NgModule.bootstrap" components nor a "ngDoBootstrap" method. Please define one of these.`;
 | |
|                  expect(e.message).toEqual(expectedErrMsg);
 | |
|                  expect(mockConsole.res[0].join('#')).toEqual('ERROR#Error: ' + expectedErrMsg);
 | |
|                });
 | |
|          }));
 | |
| 
 | |
|       it('should add bootstrapped module into platform modules list', async(() => {
 | |
|            defaultPlatform.bootstrapModule(createModule({bootstrap: [SomeComponent]}))
 | |
|                .then(module => expect((<any>defaultPlatform)._modules).toContain(module));
 | |
|          }));
 | |
| 
 | |
|       it('should bootstrap with NoopNgZone', async(() => {
 | |
|            defaultPlatform
 | |
|                .bootstrapModule(createModule({bootstrap: [SomeComponent]}), {ngZone: 'noop'})
 | |
|                .then((module) => {
 | |
|                  const ngZone = module.injector.get(NgZone);
 | |
|                  expect(ngZone instanceof NoopNgZone).toBe(true);
 | |
|                });
 | |
|          }));
 | |
| 
 | |
|       it('should resolve component resources when creating module factory', async() => {
 | |
|         @Component({
 | |
|           selector: 'with-templates-app',
 | |
|           templateUrl: '/test-template.html',
 | |
|         })
 | |
|         class WithTemplateUrlComponent {
 | |
|         }
 | |
| 
 | |
|         const loadResourceSpy = jasmine.createSpy('load resource').and.returnValue('fakeContent');
 | |
|         const testModule = createModule({component: WithTemplateUrlComponent});
 | |
| 
 | |
|         await defaultPlatform.bootstrapModule(testModule, {
 | |
|           providers: [
 | |
|             {provide: ResourceLoader, useValue: {get: loadResourceSpy}},
 | |
|           ]
 | |
|         });
 | |
| 
 | |
|         expect(loadResourceSpy).toHaveBeenCalledTimes(1);
 | |
|         expect(loadResourceSpy).toHaveBeenCalledWith('/test-template.html');
 | |
|       });
 | |
| 
 | |
|       onlyInIvy('We only need to define `LOCALE_ID` for runtime i18n')
 | |
|           .it('should define `LOCALE_ID`', async() => {
 | |
|             @Component({
 | |
|               selector: 'i18n-app',
 | |
|               templateUrl: '',
 | |
|             })
 | |
|             class I18nComponent {
 | |
|             }
 | |
| 
 | |
|             const testModule = createModule(
 | |
|                 {component: I18nComponent, providers: [{provide: LOCALE_ID, useValue: 'ro'}]});
 | |
|             await defaultPlatform.bootstrapModule(testModule);
 | |
| 
 | |
|             expect(getLocaleId()).toEqual('ro');
 | |
|           });
 | |
| 
 | |
|       it('should wait for APP_INITIALIZER to set providers for `LOCALE_ID`', async() => {
 | |
|         let locale: string = '';
 | |
| 
 | |
|         const promise = Promise.resolve().then(() => { locale = 'fr-FR'; });
 | |
| 
 | |
|         const testModule = createModule({
 | |
|           providers: [
 | |
|             {provide: APP_INITIALIZER, useValue: () => promise, multi: true},
 | |
|             {provide: LOCALE_ID, useFactory: () => locale}
 | |
|           ]
 | |
|         });
 | |
|         const app = await defaultPlatform.bootstrapModule(testModule);
 | |
|         expect(app.injector.get(LOCALE_ID)).toEqual('fr-FR');
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     describe('bootstrapModuleFactory', () => {
 | |
|       let defaultPlatform: PlatformRef;
 | |
|       beforeEach(inject([PlatformRef], (_platform: PlatformRef) => {
 | |
|         createRootEl();
 | |
|         defaultPlatform = _platform;
 | |
|       }));
 | |
|       it('should wait for asynchronous app initializers', async(() => {
 | |
|            let resolve: (result: any) => void;
 | |
|            const promise: Promise<any> = new Promise((res) => { resolve = res; });
 | |
|            let initializerDone = false;
 | |
|            setTimeout(() => {
 | |
|              resolve(true);
 | |
|              initializerDone = true;
 | |
|            }, 1);
 | |
| 
 | |
|            const compilerFactory: CompilerFactory =
 | |
|                defaultPlatform.injector.get(CompilerFactory, null) !;
 | |
|            const moduleFactory = compilerFactory.createCompiler().compileModuleSync(
 | |
|                createModule([{provide: APP_INITIALIZER, useValue: () => promise, multi: true}]));
 | |
|            defaultPlatform.bootstrapModuleFactory(moduleFactory).then(_ => {
 | |
|              expect(initializerDone).toBe(true);
 | |
|            });
 | |
|          }));
 | |
| 
 | |
|       it('should rethrow sync errors even if the exceptionHandler is not rethrowing', async(() => {
 | |
|            const compilerFactory: CompilerFactory =
 | |
|                defaultPlatform.injector.get(CompilerFactory, null) !;
 | |
|            const moduleFactory = compilerFactory.createCompiler().compileModuleSync(createModule(
 | |
|                [{provide: APP_INITIALIZER, useValue: () => { throw 'Test'; }, multi: true}]));
 | |
|            expect(() => defaultPlatform.bootstrapModuleFactory(moduleFactory)).toThrow('Test');
 | |
|            // Error rethrown will be seen by the exception handler since it's after
 | |
|            // construction.
 | |
|            expect(mockConsole.res[0].join('#')).toEqual('ERROR#Test');
 | |
|          }));
 | |
| 
 | |
|       it('should rethrow promise errors even if the exceptionHandler is not rethrowing',
 | |
|          async(() => {
 | |
|            const compilerFactory: CompilerFactory =
 | |
|                defaultPlatform.injector.get(CompilerFactory, null) !;
 | |
|            const moduleFactory = compilerFactory.createCompiler().compileModuleSync(createModule(
 | |
|                [{provide: APP_INITIALIZER, useValue: () => Promise.reject('Test'), multi: true}]));
 | |
|            defaultPlatform.bootstrapModuleFactory(moduleFactory)
 | |
|                .then(() => expect(false).toBe(true), (e) => {
 | |
|                  expect(e).toBe('Test');
 | |
|                  expect(mockConsole.res[0].join('#')).toEqual('ERROR#Test');
 | |
|                });
 | |
|          }));
 | |
|     });
 | |
| 
 | |
|     describe('attachView / detachView', () => {
 | |
|       @Component({template: '{{name}}'})
 | |
|       class MyComp {
 | |
|         name = 'Initial';
 | |
|       }
 | |
| 
 | |
|       @Component({template: '<ng-container #vc></ng-container>'})
 | |
|       class ContainerComp {
 | |
|         // TODO(issue/24571): remove '!'.
 | |
|         @ViewChild('vc', {read: ViewContainerRef})
 | |
|         vc !: ViewContainerRef;
 | |
|       }
 | |
| 
 | |
|       @Component({template: '<ng-template #t>Dynamic content</ng-template>'})
 | |
|       class EmbeddedViewComp {
 | |
|         // TODO(issue/24571): remove '!'.
 | |
|         @ViewChild(TemplateRef, {static: true})
 | |
|         tplRef !: TemplateRef<Object>;
 | |
|       }
 | |
| 
 | |
|       beforeEach(() => {
 | |
|         TestBed.configureTestingModule({
 | |
|           declarations: [MyComp, ContainerComp, EmbeddedViewComp],
 | |
|           providers: [{provide: ComponentFixtureNoNgZone, useValue: true}]
 | |
|         });
 | |
|       });
 | |
| 
 | |
|       it('should dirty check attached views', () => {
 | |
|         const comp = TestBed.createComponent(MyComp);
 | |
|         const appRef: ApplicationRef = TestBed.inject(ApplicationRef);
 | |
|         expect(appRef.viewCount).toBe(0);
 | |
| 
 | |
|         appRef.tick();
 | |
|         expect(comp.nativeElement).toHaveText('');
 | |
| 
 | |
|         appRef.attachView(comp.componentRef.hostView);
 | |
|         appRef.tick();
 | |
|         expect(appRef.viewCount).toBe(1);
 | |
|         expect(comp.nativeElement).toHaveText('Initial');
 | |
|       });
 | |
| 
 | |
|       it('should not dirty check detached views', () => {
 | |
|         const comp = TestBed.createComponent(MyComp);
 | |
|         const appRef: ApplicationRef = TestBed.inject(ApplicationRef);
 | |
| 
 | |
|         appRef.attachView(comp.componentRef.hostView);
 | |
|         appRef.tick();
 | |
|         expect(comp.nativeElement).toHaveText('Initial');
 | |
| 
 | |
|         appRef.detachView(comp.componentRef.hostView);
 | |
|         comp.componentInstance.name = 'New';
 | |
|         appRef.tick();
 | |
|         expect(appRef.viewCount).toBe(0);
 | |
|         expect(comp.nativeElement).toHaveText('Initial');
 | |
|       });
 | |
| 
 | |
|       it('should detach attached views if they are destroyed', () => {
 | |
|         const comp = TestBed.createComponent(MyComp);
 | |
|         const appRef: ApplicationRef = TestBed.inject(ApplicationRef);
 | |
| 
 | |
|         appRef.attachView(comp.componentRef.hostView);
 | |
|         comp.destroy();
 | |
| 
 | |
|         expect(appRef.viewCount).toBe(0);
 | |
|       });
 | |
| 
 | |
|       it('should detach attached embedded views if they are destroyed', () => {
 | |
|         const comp = TestBed.createComponent(EmbeddedViewComp);
 | |
|         const appRef: ApplicationRef = TestBed.inject(ApplicationRef);
 | |
| 
 | |
|         const embeddedViewRef = comp.componentInstance.tplRef.createEmbeddedView({});
 | |
| 
 | |
|         appRef.attachView(embeddedViewRef);
 | |
|         embeddedViewRef.destroy();
 | |
| 
 | |
|         expect(appRef.viewCount).toBe(0);
 | |
|       });
 | |
| 
 | |
| 
 | |
|       it('should not allow to attach a view to both, a view container and the ApplicationRef',
 | |
|          () => {
 | |
|            const comp = TestBed.createComponent(MyComp);
 | |
|            let hostView = comp.componentRef.hostView;
 | |
|            const containerComp = TestBed.createComponent(ContainerComp);
 | |
|            containerComp.detectChanges();
 | |
|            const vc = containerComp.componentInstance.vc;
 | |
|            const appRef: ApplicationRef = TestBed.inject(ApplicationRef);
 | |
| 
 | |
|            vc.insert(hostView);
 | |
|            expect(() => appRef.attachView(hostView))
 | |
|                .toThrowError('This view is already attached to a ViewContainer!');
 | |
|            hostView = vc.detach(0) !;
 | |
| 
 | |
|            appRef.attachView(hostView);
 | |
|            expect(() => vc.insert(hostView))
 | |
|                .toThrowError('This view is already attached directly to the ApplicationRef!');
 | |
|          });
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('AppRef', () => {
 | |
|     @Component({selector: 'sync-comp', template: `<span>{{text}}</span>`})
 | |
|     class SyncComp {
 | |
|       text: string = '1';
 | |
|     }
 | |
| 
 | |
|     @Component({selector: 'click-comp', template: `<span (click)="onClick()">{{text}}</span>`})
 | |
|     class ClickComp {
 | |
|       text: string = '1';
 | |
| 
 | |
|       onClick() { this.text += '1'; }
 | |
|     }
 | |
| 
 | |
|     @Component({selector: 'micro-task-comp', template: `<span>{{text}}</span>`})
 | |
|     class MicroTaskComp {
 | |
|       text: string = '1';
 | |
| 
 | |
|       ngOnInit() {
 | |
|         Promise.resolve(null).then((_) => { this.text += '1'; });
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     @Component({selector: 'macro-task-comp', template: `<span>{{text}}</span>`})
 | |
|     class MacroTaskComp {
 | |
|       text: string = '1';
 | |
| 
 | |
|       ngOnInit() {
 | |
|         setTimeout(() => { this.text += '1'; }, 10);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     @Component({selector: 'micro-macro-task-comp', template: `<span>{{text}}</span>`})
 | |
|     class MicroMacroTaskComp {
 | |
|       text: string = '1';
 | |
| 
 | |
|       ngOnInit() {
 | |
|         Promise.resolve(null).then((_) => {
 | |
|           this.text += '1';
 | |
|           setTimeout(() => { this.text += '1'; }, 10);
 | |
|         });
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     @Component({selector: 'macro-micro-task-comp', template: `<span>{{text}}</span>`})
 | |
|     class MacroMicroTaskComp {
 | |
|       text: string = '1';
 | |
| 
 | |
|       ngOnInit() {
 | |
|         setTimeout(() => {
 | |
|           this.text += '1';
 | |
|           Promise.resolve(null).then((_: any) => { this.text += '1'; });
 | |
|         }, 10);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     let stableCalled = false;
 | |
| 
 | |
|     beforeEach(() => {
 | |
|       stableCalled = false;
 | |
|       TestBed.configureTestingModule({
 | |
|         declarations: [
 | |
|           SyncComp, MicroTaskComp, MacroTaskComp, MicroMacroTaskComp, MacroMicroTaskComp, ClickComp
 | |
|         ],
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     afterEach(() => { expect(stableCalled).toBe(true, 'isStable did not emit true on stable'); });
 | |
| 
 | |
|     function expectStableTexts(component: Type<any>, expected: string[]) {
 | |
|       const fixture = TestBed.createComponent(component);
 | |
|       const appRef: ApplicationRef = TestBed.inject(ApplicationRef);
 | |
|       const zone: NgZone = TestBed.inject(NgZone);
 | |
|       appRef.attachView(fixture.componentRef.hostView);
 | |
|       zone.run(() => appRef.tick());
 | |
| 
 | |
|       let i = 0;
 | |
|       appRef.isStable.subscribe({
 | |
|         next: (stable: boolean) => {
 | |
|           if (stable) {
 | |
|             expect(i).toBeLessThan(expected.length);
 | |
|             expect(fixture.nativeElement).toHaveText(expected[i++]);
 | |
|             stableCalled = true;
 | |
|           }
 | |
|         }
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     it('isStable should fire on synchronous component loading',
 | |
|        async(() => { expectStableTexts(SyncComp, ['1']); }));
 | |
| 
 | |
|     it('isStable should fire after a microtask on init is completed',
 | |
|        async(() => { expectStableTexts(MicroTaskComp, ['11']); }));
 | |
| 
 | |
|     it('isStable should fire after a macrotask on init is completed',
 | |
|        async(() => { expectStableTexts(MacroTaskComp, ['11']); }));
 | |
| 
 | |
|     it('isStable should fire only after chain of micro and macrotasks on init are completed',
 | |
|        async(() => { expectStableTexts(MicroMacroTaskComp, ['111']); }));
 | |
| 
 | |
|     it('isStable should fire only after chain of macro and microtasks on init are completed',
 | |
|        async(() => { expectStableTexts(MacroMicroTaskComp, ['111']); }));
 | |
| 
 | |
|     describe('unstable', () => {
 | |
|       let unstableCalled = false;
 | |
| 
 | |
|       afterEach(
 | |
|           () => { expect(unstableCalled).toBe(true, 'isStable did not emit false on unstable'); });
 | |
| 
 | |
|       function expectUnstable(appRef: ApplicationRef) {
 | |
|         appRef.isStable.subscribe({
 | |
|           next: (stable: boolean) => {
 | |
|             if (stable) {
 | |
|               stableCalled = true;
 | |
|             }
 | |
|             if (!stable) {
 | |
|               unstableCalled = true;
 | |
|             }
 | |
|           }
 | |
|         });
 | |
|       }
 | |
| 
 | |
|       it('should be fired after app becomes unstable', async(() => {
 | |
|            const fixture = TestBed.createComponent(ClickComp);
 | |
|            const appRef: ApplicationRef = TestBed.inject(ApplicationRef);
 | |
|            const zone: NgZone = TestBed.inject(NgZone);
 | |
|            appRef.attachView(fixture.componentRef.hostView);
 | |
|            zone.run(() => appRef.tick());
 | |
| 
 | |
|            fixture.whenStable().then(() => {
 | |
|              expectUnstable(appRef);
 | |
|              const element = fixture.debugElement.children[0];
 | |
|              dispatchEvent(element.nativeElement, 'click');
 | |
|            });
 | |
|          }));
 | |
|     });
 | |
|   });
 | |
| }
 | |
| 
 | |
| class MockConsole {
 | |
|   res: any[][] = [];
 | |
|   log(...args: any[]): void {
 | |
|     // Logging from ErrorHandler should run outside of the Angular Zone.
 | |
|     NgZone.assertNotInAngularZone();
 | |
|     this.res.push(args);
 | |
|   }
 | |
|   error(...args: any[]): void {
 | |
|     // Logging from ErrorHandler should run outside of the Angular Zone.
 | |
|     NgZone.assertNotInAngularZone();
 | |
|     this.res.push(args);
 | |
|   }
 | |
| }
 |