/**
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.io/license
 */

import {Component, destroyPlatform, Directive, ElementRef, EventEmitter, Inject, Injector, Input, NgModule, NO_ERRORS_SCHEMA, Output, SimpleChanges} from '@angular/core';
import {async, fakeAsync, tick} from '@angular/core/testing';
import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';

import * as angular from '../../../src/common/src/angular1';
import {$EXCEPTION_HANDLER, $SCOPE} from '../../../src/common/src/constants';
import {html, multiTrim, withEachNg1Version} from '../../../src/common/test/helpers/common_test_helpers';
import {downgradeComponent, UpgradeComponent, UpgradeModule} from '../../index';

import {$digest, bootstrap} from './static_test_helpers';


withEachNg1Version(() => {
  describe('upgrade ng1 component', () => {
    beforeEach(() => destroyPlatform());
    afterEach(() => destroyPlatform());

    describe('template/templateUrl', () => {
      it('should support `template` (string)', async(() => {
           // Define `ng1Component`
           const ng1Component: angular.IComponent = {template: 'Hello, Angular!'};

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1'})
           class Ng1ComponentFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({selector: 'ng2', template: '<ng1></ng1>'})
           class Ng2Component {
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1Module', [])
                                 .component('ng1', ng1Component)
                                 .directive('ng2', downgradeComponent({component: Ng2Component}));

           // Define `Ng2Module`
           @NgModule({
             declarations: [Ng1ComponentFacade, Ng2Component],
             entryComponents: [Ng2Component],
             imports: [BrowserModule, UpgradeModule]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2></ng2>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => {
             expect(multiTrim(element.textContent)).toBe('Hello, Angular!');
           });
         }));

      it('should support `template` (function)', async(() => {
           // Define `ng1Component`
           const ng1Component: angular.IComponent = {template: () => 'Hello, Angular!'};

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1'})
           class Ng1ComponentFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({selector: 'ng2', template: '<ng1></ng1>'})
           class Ng2Component {
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1Module', [])
                                 .component('ng1', ng1Component)
                                 .directive('ng2', downgradeComponent({component: Ng2Component}));

           // Define `Ng2Module`
           @NgModule({
             declarations: [Ng1ComponentFacade, Ng2Component],
             entryComponents: [Ng2Component],
             imports: [BrowserModule, UpgradeModule]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2></ng2>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
             expect(multiTrim(element.textContent)).toBe('Hello, Angular!');
           });
         }));

      it('should pass $element to `template` function and not $attrs', async(() => {
           // Define `ng1Component`
           const ng1Component: angular.IComponent = {
             template: ($attrs: angular.IAttributes, $element: angular.IAugmentedJQuery) => {
               expect($attrs).toBeUndefined();
               expect($element).toBeDefined();

               return 'Hello, Angular!';
             }
           };

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1'})
           class Ng1ComponentFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({selector: 'ng2', template: '<ng1></ng1>'})
           class Ng2Component {
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1Module', [])
                                 .component('ng1', ng1Component)
                                 .directive('ng2', downgradeComponent({component: Ng2Component}));

           // Define `Ng2Module`
           @NgModule({
             declarations: [Ng1ComponentFacade, Ng2Component],
             entryComponents: [Ng2Component],
             imports: [BrowserModule, UpgradeModule]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2></ng2>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
             expect(multiTrim(element.textContent)).toBe('Hello, Angular!');
           });
         }));

      it('should support `templateUrl` (string) fetched from `$templateCache`', async(() => {
           // Define `ng1Component`
           const ng1Component: angular.IComponent = {templateUrl: 'ng1.component.html'};

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1'})
           class Ng1ComponentFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({selector: 'ng2', template: '<ng1></ng1>'})
           class Ng2Component {
           }

           // Define `ng1Module`
           const ng1Module =
               angular.module_('ng1Module', [])
                   .component('ng1', ng1Component)
                   .directive('ng2', downgradeComponent({component: Ng2Component}))
                   .run(
                       ($templateCache: angular.ITemplateCacheService) =>
                           $templateCache.put('ng1.component.html', 'Hello, Angular!'));

           // Define `Ng2Module`
           @NgModule({
             declarations: [Ng1ComponentFacade, Ng2Component],
             entryComponents: [Ng2Component],
             imports: [BrowserModule, UpgradeModule]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2></ng2>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
             expect(multiTrim(element.textContent)).toBe('Hello, Angular!');
           });
         }));

      it('should support `templateUrl` (function) fetched from `$templateCache`', async(() => {
           // Define `ng1Component`
           const ng1Component: angular.IComponent = {templateUrl: () => 'ng1.component.html'};

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1'})
           class Ng1ComponentFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({selector: 'ng2', template: '<ng1></ng1>'})
           class Ng2Component {
           }

           // Define `ng1Module`
           const ng1Module =
               angular.module_('ng1Module', [])
                   .component('ng1', ng1Component)
                   .directive('ng2', downgradeComponent({component: Ng2Component}))
                   .run(
                       ($templateCache: angular.ITemplateCacheService) =>
                           $templateCache.put('ng1.component.html', 'Hello, Angular!'));

           // Define `Ng2Module`
           @NgModule({
             declarations: [Ng1ComponentFacade, Ng2Component],
             entryComponents: [Ng2Component],
             imports: [BrowserModule, UpgradeModule]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2></ng2>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
             expect(multiTrim(element.textContent)).toBe('Hello, Angular!');
           });
         }));

      it('should pass $element to `templateUrl` function and not $attrs', async(() => {
           // Define `ng1Component`
           const ng1Component: angular.IComponent = {
             templateUrl: ($attrs: angular.IAttributes, $element: angular.IAugmentedJQuery) => {
               expect($attrs).toBeUndefined();
               expect($element).toBeDefined();

               return 'ng1.component.html';
             }
           };

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1'})
           class Ng1ComponentFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({selector: 'ng2', template: '<ng1></ng1>'})
           class Ng2Component {
           }

           // Define `ng1Module`
           const ng1Module =
               angular.module_('ng1Module', [])
                   .component('ng1', ng1Component)
                   .directive('ng2', downgradeComponent({component: Ng2Component}))
                   .run(
                       ($templateCache: angular.ITemplateCacheService) =>
                           $templateCache.put('ng1.component.html', 'Hello, Angular!'));

           // Define `Ng2Module`
           @NgModule({
             declarations: [Ng1ComponentFacade, Ng2Component],
             entryComponents: [Ng2Component],
             imports: [BrowserModule, UpgradeModule]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2></ng2>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
             expect(multiTrim(element.textContent)).toBe('Hello, Angular!');
           });
         }));

      // NOT SUPPORTED YET
      xit('should support `templateUrl` (string) fetched from the server', fakeAsync(() => {
            // Define `ng1Component`
            const ng1Component: angular.IComponent = {templateUrl: 'ng1.component.html'};

            // Define `Ng1ComponentFacade`
            @Directive({selector: 'ng1'})
            class Ng1ComponentFacade extends UpgradeComponent {
              constructor(elementRef: ElementRef, injector: Injector) {
                super('ng1', elementRef, injector);
              }
            }

            // Define `Ng2Component`
            @Component({selector: 'ng2', template: '<ng1></ng1>'})
            class Ng2Component {
            }

            // Define `ng1Module`
            const ng1Module =
                angular.module_('ng1Module', [])
                    .component('ng1', ng1Component)
                    .directive('ng2', downgradeComponent({component: Ng2Component}))
                    .value(
                        '$httpBackend',
                        (method: string, url: string, post?: any, callback?: Function) =>
                            setTimeout(
                                () => callback!(200, `${method}:${url}`.toLowerCase()), 1000));

            // Define `Ng2Module`
            @NgModule({
              declarations: [Ng1ComponentFacade, Ng2Component],
              entryComponents: [Ng2Component],
              imports: [BrowserModule, UpgradeModule]
            })
            class Ng2Module {
              ngDoBootstrap() {}
            }

            // Bootstrap
            const element = html(`<ng2></ng2>`);

            bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
              tick(500);
              expect(multiTrim(element.textContent)).toBe('');

              tick(500);
              expect(multiTrim(element.textContent)).toBe('get:ng1.component.html');
            });
          }));

      // NOT SUPPORTED YET
      xit('should support `templateUrl` (function) fetched from the server', fakeAsync(() => {
            // Define `ng1Component`
            const ng1Component: angular.IComponent = {templateUrl: () => 'ng1.component.html'};

            // Define `Ng1ComponentFacade`
            @Directive({selector: 'ng1'})
            class Ng1ComponentFacade extends UpgradeComponent {
              constructor(elementRef: ElementRef, injector: Injector) {
                super('ng1', elementRef, injector);
              }
            }

            // Define `Ng2Component`
            @Component({selector: 'ng2', template: '<ng1></ng1>'})
            class Ng2Component {
            }

            // Define `ng1Module`
            const ng1Module =
                angular.module_('ng1Module', [])
                    .component('ng1', ng1Component)
                    .directive('ng2', downgradeComponent({component: Ng2Component}))
                    .value(
                        '$httpBackend',
                        (method: string, url: string, post?: any, callback?: Function) =>
                            setTimeout(
                                () => callback!(200, `${method}:${url}`.toLowerCase()), 1000));

            // Define `Ng2Module`
            @NgModule({
              declarations: [Ng1ComponentFacade, Ng2Component],
              entryComponents: [Ng2Component],
              imports: [BrowserModule, UpgradeModule]
            })
            class Ng2Module {
              ngDoBootstrap() {}
            }

            // Bootstrap
            const element = html(`<ng2></ng2>`);

            bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
              tick(500);
              expect(multiTrim(element.textContent)).toBe('');

              tick(500);
              expect(multiTrim(element.textContent)).toBe('get:ng1.component.html');
            });
          }));

      it('should support empty templates', async(() => {
           // Define `ng1Component`s
           const ng1ComponentA: angular.IComponent = {template: ''};
           const ng1ComponentB: angular.IComponent = {template: () => ''};
           const ng1ComponentC: angular.IComponent = {templateUrl: 'ng1.component.html'};
           const ng1ComponentD: angular.IComponent = {templateUrl: () => 'ng1.component.html'};

           // Define `Ng1ComponentFacade`s
           @Directive({selector: 'ng1A'})
           class Ng1ComponentAFacade extends UpgradeComponent {
             constructor(e: ElementRef, i: Injector) {
               super('ng1A', e, i);
             }
           }
           @Directive({selector: 'ng1B'})
           class Ng1ComponentBFacade extends UpgradeComponent {
             constructor(e: ElementRef, i: Injector) {
               super('ng1B', e, i);
             }
           }
           @Directive({selector: 'ng1C'})
           class Ng1ComponentCFacade extends UpgradeComponent {
             constructor(e: ElementRef, i: Injector) {
               super('ng1C', e, i);
             }
           }
           @Directive({selector: 'ng1D'})
           class Ng1ComponentDFacade extends UpgradeComponent {
             constructor(e: ElementRef, i: Injector) {
               super('ng1D', e, i);
             }
           }

           // Define `Ng2Component`
           @Component({
             selector: 'ng2',
             template: `
               <ng1A>Ignore this</ng1A>
               <ng1B>Ignore this</ng1B>
               <ng1C>Ignore this</ng1C>
               <ng1D>Ignore this</ng1D>
             `
           })
           class Ng2Component {
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1Module', [])
                                 .component('ng1A', ng1ComponentA)
                                 .component('ng1B', ng1ComponentB)
                                 .component('ng1C', ng1ComponentC)
                                 .component('ng1D', ng1ComponentD)
                                 .directive('ng2', downgradeComponent({component: Ng2Component}))
                                 .run(
                                     ($templateCache: angular.ITemplateCacheService) =>
                                         $templateCache.put('ng1.component.html', ''));

           // Define `Ng2Module`
           @NgModule({
             declarations: [
               Ng1ComponentAFacade, Ng1ComponentBFacade, Ng1ComponentCFacade, Ng1ComponentDFacade,
               Ng2Component
             ],
             entryComponents: [Ng2Component],
             imports: [BrowserModule, UpgradeModule],
             schemas: [NO_ERRORS_SCHEMA]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2></ng2>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
             expect(multiTrim(element.textContent)).toBe('');
           });
         }));
    });

    describe('bindings', () => {
      it('should support `@` bindings', fakeAsync(() => {
           let ng2ComponentInstance: Ng2Component;

           // Define `ng1Component`
           const ng1Component: angular.IComponent = {
             template: 'Inside: {{ $ctrl.inputA }}, {{ $ctrl.inputB }}',
             bindings: {inputA: '@inputAttrA', inputB: '@'}
           };

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1'})
           class Ng1ComponentFacade extends UpgradeComponent {
             // TODO(issue/24571): remove '!'.
             @Input('inputAttrA') inputA!: string;
             // TODO(issue/24571): remove '!'.
             @Input() inputB!: string;

             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({
             selector: 'ng2',
             template: `
               <ng1 inputAttrA="{{ dataA }}" inputB="{{ dataB }}"></ng1>
               | Outside: {{ dataA }}, {{ dataB }}
             `
           })
           class Ng2Component {
             dataA = 'foo';
             dataB = 'bar';

             constructor() {
               ng2ComponentInstance = this;
             }
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1Module', [])
                                 .component('ng1', ng1Component)
                                 .directive('ng2', downgradeComponent({component: Ng2Component}));

           // Define `Ng2Module`
           @NgModule({
             declarations: [Ng1ComponentFacade, Ng2Component],
             entryComponents: [Ng2Component],
             imports: [BrowserModule, UpgradeModule]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2></ng2>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => {
             const ng1 = element.querySelector('ng1')!;
             const ng1Controller = angular.element(ng1).controller!('ng1');

             expect(multiTrim(element.textContent)).toBe('Inside: foo, bar | Outside: foo, bar');

             ng1Controller.inputA = 'baz';
             ng1Controller.inputB = 'qux';
             tick();

             expect(multiTrim(element.textContent)).toBe('Inside: baz, qux | Outside: foo, bar');

             ng2ComponentInstance.dataA = 'foo2';
             ng2ComponentInstance.dataB = 'bar2';
             $digest(adapter);
             tick();

             expect(multiTrim(element.textContent))
                 .toBe('Inside: foo2, bar2 | Outside: foo2, bar2');
           });
         }));

      it('should support `<` bindings', fakeAsync(() => {
           let ng2ComponentInstance: Ng2Component;

           // Define `ng1Component`
           const ng1Component: angular.IComponent = {
             template: 'Inside: {{ $ctrl.inputA.value }}, {{ $ctrl.inputB.value }}',
             bindings: {inputA: '<inputAttrA', inputB: '<'}
           };

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1'})
           class Ng1ComponentFacade extends UpgradeComponent {
             // TODO(issue/24571): remove '!'.
             @Input('inputAttrA') inputA!: string;
             // TODO(issue/24571): remove '!'.
             @Input() inputB!: string;

             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({
             selector: 'ng2',
             template: `
               <ng1 [inputAttrA]="dataA" [inputB]="dataB"></ng1>
               | Outside: {{ dataA.value }}, {{ dataB.value }}
             `
           })
           class Ng2Component {
             dataA = {value: 'foo'};
             dataB = {value: 'bar'};

             constructor() {
               ng2ComponentInstance = this;
             }
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1Module', [])
                                 .component('ng1', ng1Component)
                                 .directive('ng2', downgradeComponent({component: Ng2Component}));

           // Define `Ng2Module`
           @NgModule({
             declarations: [Ng1ComponentFacade, Ng2Component],
             entryComponents: [Ng2Component],
             imports: [BrowserModule, UpgradeModule]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2></ng2>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => {
             const ng1 = element.querySelector('ng1')!;
             const ng1Controller = angular.element(ng1).controller!('ng1');

             expect(multiTrim(element.textContent)).toBe('Inside: foo, bar | Outside: foo, bar');

             ng1Controller.inputA = {value: 'baz'};
             ng1Controller.inputB = {value: 'qux'};
             tick();

             expect(multiTrim(element.textContent)).toBe('Inside: baz, qux | Outside: foo, bar');

             ng2ComponentInstance.dataA = {value: 'foo2'};
             ng2ComponentInstance.dataB = {value: 'bar2'};
             $digest(adapter);
             tick();

             expect(multiTrim(element.textContent))
                 .toBe('Inside: foo2, bar2 | Outside: foo2, bar2');
           });
         }));

      it('should support `=` bindings', fakeAsync(() => {
           let ng2ComponentInstance: Ng2Component;

           // Define `ng1Component`
           const ng1Component: angular.IComponent = {
             template: 'Inside: {{ $ctrl.inputA.value }}, {{ $ctrl.inputB.value }}',
             bindings: {inputA: '=inputAttrA', inputB: '='}
           };

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1'})
           class Ng1ComponentFacade extends UpgradeComponent {
             // TODO(issue/24571): remove '!'.
             @Input('inputAttrA') inputA!: string;
             // TODO(issue/24571): remove '!'.
             @Output('inputAttrAChange') inputAChange!: EventEmitter<any>;
             // TODO(issue/24571): remove '!'.
             @Input() inputB!: string;
             // TODO(issue/24571): remove '!'.
             @Output() inputBChange!: EventEmitter<any>;

             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({
             selector: 'ng2',
             template: `
               <ng1 [(inputAttrA)]="dataA" [(inputB)]="dataB"></ng1>
               | Outside: {{ dataA.value }}, {{ dataB.value }}
             `
           })
           class Ng2Component {
             dataA = {value: 'foo'};
             dataB = {value: 'bar'};

             constructor() {
               ng2ComponentInstance = this;
             }
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1Module', [])
                                 .component('ng1', ng1Component)
                                 .directive('ng2', downgradeComponent({component: Ng2Component}));

           // Define `Ng2Module`
           @NgModule({
             declarations: [Ng1ComponentFacade, Ng2Component],
             entryComponents: [Ng2Component],
             imports: [BrowserModule, UpgradeModule]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2></ng2>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => {
             const ng1 = element.querySelector('ng1')!;
             const ng1Controller = angular.element(ng1).controller!('ng1');

             expect(multiTrim(element.textContent)).toBe('Inside: foo, bar | Outside: foo, bar');

             ng1Controller.inputA = {value: 'baz'};
             ng1Controller.inputB = {value: 'qux'};
             tick();

             expect(multiTrim(element.textContent)).toBe('Inside: baz, qux | Outside: baz, qux');

             ng2ComponentInstance.dataA = {value: 'foo2'};
             ng2ComponentInstance.dataB = {value: 'bar2'};
             $digest(adapter);
             tick();

             expect(multiTrim(element.textContent))
                 .toBe('Inside: foo2, bar2 | Outside: foo2, bar2');
           });
         }));

      it('should support `&` bindings', fakeAsync(() => {
           // Define `ng1Component`
           const ng1Component: angular.IComponent = {
             template: 'Inside: -',
             bindings: {outputA: '&outputAttrA', outputB: '&'}
           };

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1'})
           class Ng1ComponentFacade extends UpgradeComponent {
             // TODO(issue/24571): remove '!'.
             @Output('outputAttrA') outputA!: EventEmitter<any>;
             // TODO(issue/24571): remove '!'.
             @Output() outputB!: EventEmitter<any>;

             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({
             selector: 'ng2',
             template: `
               <ng1 (outputAttrA)="dataA = $event" (outputB)="dataB = $event"></ng1>
               | Outside: {{ dataA }}, {{ dataB }}
             `
           })
           class Ng2Component {
             dataA = 'foo';
             dataB = 'bar';
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1Module', [])
                                 .component('ng1', ng1Component)
                                 .directive('ng2', downgradeComponent({component: Ng2Component}));

           // Define `Ng2Module`
           @NgModule({
             declarations: [Ng1ComponentFacade, Ng2Component],
             entryComponents: [Ng2Component],
             imports: [BrowserModule, UpgradeModule]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2></ng2>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
             const ng1 = element.querySelector('ng1')!;
             const ng1Controller = angular.element(ng1).controller!('ng1');

             expect(multiTrim(element.textContent)).toBe('Inside: - | Outside: foo, bar');

             ng1Controller.outputA('baz');
             ng1Controller.outputB('qux');
             tick();

             expect(multiTrim(element.textContent)).toBe('Inside: - | Outside: baz, qux');
           });
         }));

      it('should bind properties, events', fakeAsync(() => {
           // Define `ng1Component`
           const ng1Component: angular.IComponent = {
             template: `
               Hello {{ $ctrl.fullName }};
               A: {{ $ctrl.modelA }};
               B: {{ $ctrl.modelB }};
               C: {{ $ctrl.modelC }}
             `,
             bindings: {fullName: '@', modelA: '<dataA', modelB: '=dataB', modelC: '=', event: '&'},
             controller: function(this: any, $scope: angular.IScope) {
               $scope.$watch('$ctrl.modelB', (v: string) => {
                 if (v === 'Savkin') {
                   this.modelB = 'SAVKIN';
                   this.event('WORKS');

                   // Should not update because `modelA: '<dataA'` is uni-directional.
                   this.modelA = 'VICTOR';

                   // Should not update because `[modelC]` is uni-directional.
                   this.modelC = 'sf';
                 }
               });
             }
           };

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1'})
           class Ng1ComponentFacade extends UpgradeComponent {
             // TODO(issue/24571): remove '!'.
             @Input() fullName!: string;
             @Input('dataA') modelA: any;
             @Input('dataB') modelB: any;
             // TODO(issue/24571): remove '!'.
             @Output('dataBChange') modelBChange!: EventEmitter<any>;
             @Input() modelC: any;
             // TODO(issue/24571): remove '!'.
             @Output() modelCChange!: EventEmitter<any>;
             // TODO(issue/24571): remove '!'.
             @Output() event!: EventEmitter<any>;

             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({
             selector: 'ng2',
             template: `
               <ng1 fullName="{{ last }}, {{ first }}, {{ city }}"
                   [(dataA)]="first" [(dataB)]="last" [modelC]="city"
                   (event)="event = $event">
               </ng1> |
               <ng1 fullName="{{ 'TEST' }}" dataA="First" dataB="Last" modelC="City"></ng1> |
               {{ event }} - {{ last }}, {{ first }}, {{ city }}
             `
           })
           class Ng2Component {
             first = 'Victor';
             last = 'Savkin';
             city = 'SF';
             event = '?';
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1Module', [])
                                 .component('ng1', ng1Component)
                                 .directive('ng2', downgradeComponent({component: Ng2Component}));

           // Define `Ng2Module`
           @NgModule({
             declarations: [Ng1ComponentFacade, Ng2Component],
             entryComponents: [Ng2Component],
             imports: [BrowserModule, UpgradeModule]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2></ng2>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
             expect(multiTrim(element.textContent))
                 .toBe(
                     'Hello Savkin, Victor, SF; A: VICTOR; B: SAVKIN; C: sf | ' +
                     'Hello TEST; A: First; B: Last; C: City | ' +
                     'WORKS - SAVKIN, Victor, SF');

             // Detect changes
             tick();

             expect(multiTrim(element.textContent))
                 .toBe(
                     'Hello SAVKIN, Victor, SF; A: VICTOR; B: SAVKIN; C: sf | ' +
                     'Hello TEST; A: First; B: Last; C: City | ' +
                     'WORKS - SAVKIN, Victor, SF');
           });
         }));

      it('should bind optional properties', fakeAsync(() => {
           // Define `ng1Component`
           const ng1Component: angular.IComponent = {
             template: 'Inside: {{ $ctrl.inputA.value }}, {{ $ctrl.inputB }}',
             bindings:
                 {inputA: '=?inputAttrA', inputB: '=?', outputA: '&?outputAttrA', outputB: '&?'}
           };

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1'})
           class Ng1ComponentFacade extends UpgradeComponent {
             // TODO(issue/24571): remove '!'.
             @Input('inputAttrA') inputA!: string;
             // TODO(issue/24571): remove '!'.
             @Output('inputAttrAChange') inputAChange!: EventEmitter<any>;
             // TODO(issue/24571): remove '!'.
             @Input() inputB!: string;
             // TODO(issue/24571): remove '!'.
             @Output() inputBChange!: EventEmitter<any>;
             // TODO(issue/24571): remove '!'.
             @Output('outputAttrA') outputA!: EventEmitter<any>;
             // TODO(issue/24571): remove '!'.
             @Output() outputB!: EventEmitter<any>;

             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({
             selector: 'ng2',
             template: `
               <ng1 [(inputAttrA)]="dataA" [(inputB)]="dataB.value"></ng1> |
               <ng1 inputB="Bar" (outputAttrA)="dataA = $event"></ng1> |
               <ng1 (outputB)="updateDataB($event)"></ng1> |
               <ng1></ng1> |
               Outside: {{ dataA.value }}, {{ dataB.value }}
             `
           })
           class Ng2Component {
             dataA = {value: 'foo'};
             dataB = {value: 'bar'};

             updateDataB(value: any) {
               this.dataB.value = value;
             }
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1Module', [])
                                 .component('ng1', ng1Component)
                                 .directive('ng2', downgradeComponent({component: Ng2Component}));

           // Define `Ng2Module`
           @NgModule({
             declarations: [Ng1ComponentFacade, Ng2Component],
             entryComponents: [Ng2Component],
             imports: [BrowserModule, UpgradeModule]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2></ng2>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => {
             const ng1s = element.querySelectorAll('ng1')!;
             const ng1Controller0 = angular.element(ng1s[0]).controller!('ng1');
             const ng1Controller1 = angular.element(ng1s[1]).controller!('ng1');
             const ng1Controller2 = angular.element(ng1s[2]).controller!('ng1');

             expect(multiTrim(element.textContent))
                 .toBe(
                     'Inside: foo, bar | Inside: , Bar | Inside: , | Inside: , | Outside: foo, bar');

             ng1Controller0.inputA.value = 'baz';
             ng1Controller0.inputB = 'qux';
             tick();

             expect(multiTrim(element.textContent))
                 .toBe(
                     'Inside: baz, qux | Inside: , Bar | Inside: , | Inside: , | Outside: baz, qux');

             ng1Controller1.outputA({value: 'foo again'});
             ng1Controller2.outputB('bar again');
             $digest(adapter);
             tick();

             expect(ng1Controller0.inputA).toEqual({value: 'foo again'});
             expect(ng1Controller0.inputB).toEqual('bar again');
             expect(multiTrim(element.textContent))
                 .toBe(
                     'Inside: foo again, bar again | Inside: , Bar | Inside: , | Inside: , | ' +
                     'Outside: foo again, bar again');
           });
         }));

      it('should bind properties, events to scope when bindToController is not used',
         fakeAsync(() => {
           // Define `ng1Directive`
           const ng1Directive: angular.IDirective = {
             template: '{{ someText }} - Data: {{ inputA }} - Length: {{ inputA.length }}',
             scope: {inputA: '=', outputA: '&'},
             controller: function(this: any, $scope: angular.IScope) {
               $scope['someText'] = 'ng1';
               this.$scope = $scope;
             }
           };

           // Define `Ng1ComponentFacade`
           @Directive({selector: '[ng1]'})
           class Ng1ComponentFacade extends UpgradeComponent {
             // TODO(issue/24571): remove '!'.
             @Input() inputA!: string;
             // TODO(issue/24571): remove '!'.
             @Output() inputAChange!: EventEmitter<any>;
             // TODO(issue/24571): remove '!'.
             @Output() outputA!: EventEmitter<any>;

             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({
             selector: 'ng2',
             template: `
                <div ng1 [(inputA)]="dataA" (outputA)="dataA.push($event)"></div> |
                {{ someText }} - Data: {{ dataA }} - Length: {{ dataA.length }}
              `
           })
           class Ng2Component {
             someText = 'ng2';
             dataA = [1, 2, 3];
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1Module', [])
                                 .directive('ng1', () => ng1Directive)
                                 .directive('ng2', downgradeComponent({component: Ng2Component}));

           // Define `Ng2Module`
           @NgModule({
             declarations: [Ng1ComponentFacade, Ng2Component],
             entryComponents: [Ng2Component],
             imports: [BrowserModule, UpgradeModule]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2></ng2>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => {
             const ng1 = element.querySelector('[ng1]')!;
             const ng1Controller = angular.element(ng1).controller!('ng1');

             expect(multiTrim(element.textContent))
                 .toBe('ng1 - Data: [1,2,3] - Length: 3 | ng2 - Data: 1,2,3 - Length: 3');

             ng1Controller.$scope.inputA = [4, 5];
             tick();

             expect(multiTrim(element.textContent))
                 .toBe('ng1 - Data: [4,5] - Length: 2 | ng2 - Data: 4,5 - Length: 2');

             ng1Controller.$scope.outputA(6);
             $digest(adapter);
             tick();

             expect(ng1Controller.$scope.inputA).toEqual([4, 5, 6]);
             expect(multiTrim(element.textContent))
                 .toBe('ng1 - Data: [4,5,6] - Length: 3 | ng2 - Data: 4,5,6 - Length: 3');
           });
         }));
    });

    describe('compiling', () => {
      it('should compile the ng1 template in the correct DOM context', async(() => {
           let grandParentNodeName: string;

           // Define `ng1Component`
           const ng1ComponentA: angular.IComponent = {template: 'ng1A(<ng1-b></ng1-b>)'};
           const ng1DirectiveB: angular.IDirective = {
             compile: tElem => {
               grandParentNodeName = tElem.parent!().parent!()[0].nodeName;
               return {};
             }
           };

           // Define `Ng1ComponentAFacade`
           @Directive({selector: 'ng1A'})
           class Ng1ComponentAFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1A', elementRef, injector);
             }
           }

           // Define `Ng2ComponentX`
           @Component({selector: 'ng2-x', template: 'ng2X(<ng1A></ng1A>)'})
           class Ng2ComponentX {
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1', [])
                                 .component('ng1A', ng1ComponentA)
                                 .directive('ng1B', () => ng1DirectiveB)
                                 .directive('ng2X', downgradeComponent({component: Ng2ComponentX}));

           // Define `Ng2Module`
           @NgModule({
             imports: [BrowserModule, UpgradeModule],
             declarations: [Ng1ComponentAFacade, Ng2ComponentX],
             entryComponents: [Ng2ComponentX],
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2-x></ng2-x>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
             expect(grandParentNodeName).toBe('NG2-X');
           });
         }));
    });

    describe('linking', () => {
      it('should run the pre-linking after instantiating the controller', async(() => {
           const log: string[] = [];

           // Define `ng1Directive`
           const ng1Directive: angular.IDirective = {
             template: '',
             link: {pre: () => log.push('ng1-pre')},
             controller: class {
               constructor() {
                 log.push('ng1-ctrl');
               }
             }
           };

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1'})
           class Ng1ComponentFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({selector: 'ng2', template: '<ng1></ng1>'})
           class Ng2Component {
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1', [])
                                 .directive('ng1', () => ng1Directive)
                                 .directive('ng2', downgradeComponent({component: Ng2Component}));

           // Define `Ng2Module`
           @NgModule({
             imports: [BrowserModule, UpgradeModule],
             declarations: [Ng1ComponentFacade, Ng2Component],
             entryComponents: [Ng2Component],
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2></ng2>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
             expect(log).toEqual(['ng1-ctrl', 'ng1-pre']);
           });
         }));

      it('should run the pre-linking function before linking', async(() => {
           const log: string[] = [];

           // Define `ng1Directive`
           const ng1DirectiveA: angular.IDirective = {
             template: '<ng1-b></ng1-b>',
             link: {pre: () => log.push('ng1A-pre')}
           };

           const ng1DirectiveB: angular.IDirective = {link: () => log.push('ng1B-post')};

           // Define `Ng1ComponentAFacade`
           @Directive({selector: 'ng1A'})
           class Ng1ComponentAFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1A', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({selector: 'ng2', template: '<ng1A></ng1A>'})
           class Ng2Component {
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1', [])
                                 .directive('ng1A', () => ng1DirectiveA)
                                 .directive('ng1B', () => ng1DirectiveB)
                                 .directive('ng2', downgradeComponent({component: Ng2Component}));

           // Define `Ng2Module`
           @NgModule({
             imports: [BrowserModule, UpgradeModule],
             declarations: [Ng1ComponentAFacade, Ng2Component],
             entryComponents: [Ng2Component],
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2></ng2>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
             expect(log).toEqual(['ng1A-pre', 'ng1B-post']);
           });
         }));

      it('should run the post-linking function after linking (link: object)', async(() => {
           const log: string[] = [];

           // Define `ng1Directive`
           const ng1DirectiveA: angular.IDirective = {
             template: '<ng1-b></ng1-b>',
             link: {post: () => log.push('ng1A-post')}
           };

           const ng1DirectiveB: angular.IDirective = {link: () => log.push('ng1B-post')};

           // Define `Ng1ComponentAFacade`
           @Directive({selector: 'ng1A'})
           class Ng1ComponentAFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1A', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({selector: 'ng2', template: '<ng1A></ng1A>'})
           class Ng2Component {
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1', [])
                                 .directive('ng1A', () => ng1DirectiveA)
                                 .directive('ng1B', () => ng1DirectiveB)
                                 .directive('ng2', downgradeComponent({component: Ng2Component}));

           // Define `Ng2Module`
           @NgModule({
             imports: [BrowserModule, UpgradeModule],
             declarations: [Ng1ComponentAFacade, Ng2Component],
             entryComponents: [Ng2Component],
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2></ng2>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
             expect(log).toEqual(['ng1B-post', 'ng1A-post']);
           });
         }));

      it('should run the post-linking function after linking (link: function)', async(() => {
           const log: string[] = [];

           // Define `ng1Directive`
           const ng1DirectiveA: angular.IDirective = {
             template: '<ng1-b></ng1-b>',
             link: () => log.push('ng1A-post')
           };

           const ng1DirectiveB: angular.IDirective = {link: () => log.push('ng1B-post')};

           // Define `Ng1ComponentAFacade`
           @Directive({selector: 'ng1A'})
           class Ng1ComponentAFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1A', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({selector: 'ng2', template: '<ng1A></ng1A>'})
           class Ng2Component {
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1', [])
                                 .directive('ng1A', () => ng1DirectiveA)
                                 .directive('ng1B', () => ng1DirectiveB)
                                 .directive('ng2', downgradeComponent({component: Ng2Component}));

           // Define `Ng2Module`
           @NgModule({
             imports: [BrowserModule, UpgradeModule],
             declarations: [Ng1ComponentAFacade, Ng2Component],
             entryComponents: [Ng2Component],
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2></ng2>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
             expect(log).toEqual(['ng1B-post', 'ng1A-post']);
           });
         }));

      it('should run the post-linking function before `$postLink`', async(() => {
           const log: string[] = [];

           // Define `ng1Directive`
           const ng1Directive: angular.IDirective = {
             template: '',
             link: () => log.push('ng1-post'),
             controller: class {
               $postLink() {
                 log.push('ng1-$post');
               }
             }
           };

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1'})
           class Ng1ComponentFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({selector: 'ng2', template: '<ng1></ng1>'})
           class Ng2Component {
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1', [])
                                 .directive('ng1', () => ng1Directive)
                                 .directive('ng2', downgradeComponent({component: Ng2Component}));

           // Define `Ng2Module`
           @NgModule({
             imports: [BrowserModule, UpgradeModule],
             declarations: [Ng1ComponentFacade, Ng2Component],
             entryComponents: [Ng2Component],
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2></ng2>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
             expect(log).toEqual(['ng1-post', 'ng1-$post']);
           });
         }));
    });

    describe('controller', () => {
      it('should support `controllerAs`', async(() => {
           // Define `ng1Directive`
           const ng1Directive: angular.IDirective = {
             template:
                 '{{ vm.scope }}; {{ vm.isClass }}; {{ vm.hasElement }}; {{ vm.isPublished() }}',
             scope: true,
             controllerAs: 'vm',
             controller: class {
               hasElement: string;  // TODO(issue/24571): remove '!'.
               isClass!: string;
               scope: string;

               constructor(public $element: angular.IAugmentedJQuery, $scope: angular.IScope) {
                 this.hasElement = $element[0].nodeName;
                 this.scope = $scope.$parent.$parent === $scope.$root ? 'scope' : 'wrong-scope';

                 this.verifyIAmAClass();
               }

               isPublished() {
                 return this.$element.controller!('ng1') === this ? 'published' : 'not-published';
               }

               verifyIAmAClass() {
                 this.isClass = 'isClass';
               }
             }
           };

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1'})
           class Ng1ComponentFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({selector: 'ng2', template: '<ng1></ng1>'})
           class Ng2Component {
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1Module', [])
                                 .directive('ng1', () => ng1Directive)
                                 .directive('ng2', downgradeComponent({component: Ng2Component}));

           // Define `Ng2Module`
           @NgModule({
             declarations: [Ng1ComponentFacade, Ng2Component],
             entryComponents: [Ng2Component],
             imports: [BrowserModule, UpgradeModule]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2></ng2>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
             expect(multiTrim(element.textContent)).toBe('scope; isClass; NG1; published');
           });
         }));

      it('should support `bindToController` (boolean)', async(() => {
           // Define `ng1Directive`
           const ng1DirectiveA: angular.IDirective = {
             template: 'Scope: {{ title }}; Controller: {{ $ctrl.title }}',
             scope: {title: '@'},
             bindToController: false,
             controllerAs: '$ctrl',
             controller: class {}
           };

           const ng1DirectiveB: angular.IDirective = {
             template: 'Scope: {{ title }}; Controller: {{ $ctrl.title }}',
             scope: {title: '@'},
             bindToController: true,
             controllerAs: '$ctrl',
             controller: class {}
           };

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1A'})
           class Ng1ComponentAFacade extends UpgradeComponent {
             // TODO(issue/24571): remove '!'.
             @Input() title!: string;

             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1A', elementRef, injector);
             }
           }

           @Directive({selector: 'ng1B'})
           class Ng1ComponentBFacade extends UpgradeComponent {
             // TODO(issue/24571): remove '!'.
             @Input() title!: string;

             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1B', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({
             selector: 'ng2',
             template: `
            <ng1A title="WORKS"></ng1A> |
            <ng1B title="WORKS"></ng1B>
          `
           })
           class Ng2Component {
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1Module', [])
                                 .directive('ng1A', () => ng1DirectiveA)
                                 .directive('ng1B', () => ng1DirectiveB)
                                 .directive('ng2', downgradeComponent({component: Ng2Component}));

           // Define `Ng2Module`
           @NgModule({
             declarations: [Ng1ComponentAFacade, Ng1ComponentBFacade, Ng2Component],
             entryComponents: [Ng2Component],
             imports: [BrowserModule, UpgradeModule],
             schemas: [NO_ERRORS_SCHEMA]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2></ng2>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
             expect(multiTrim(element.textContent))
                 .toBe('Scope: WORKS; Controller: | Scope: ; Controller: WORKS');
           });
         }));

      it('should support `bindToController` (object)', async(() => {
           // Define `ng1Directive`
           const ng1Directive: angular.IDirective = {
             template: '{{ $ctrl.title }}',
             scope: {},
             bindToController: {title: '@'},
             controllerAs: '$ctrl',
             controller: class {}
           };

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1'})
           class Ng1ComponentFacade extends UpgradeComponent {
             // TODO(issue/24571): remove '!'.
             @Input() title!: string;

             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({selector: 'ng2', template: '<ng1 title="WORKS"></ng1>'})
           class Ng2Component {
             dataA = 'foo';
             dataB = 'bar';
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1Module', [])
                                 .directive('ng1', () => ng1Directive)
                                 .directive('ng2', downgradeComponent({component: Ng2Component}));

           // Define `Ng2Module`
           @NgModule({
             declarations: [Ng1ComponentFacade, Ng2Component],
             entryComponents: [Ng2Component],
             imports: [BrowserModule, UpgradeModule]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2></ng2>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
             expect(multiTrim(element.textContent)).toBe('WORKS');
           });
         }));

      it('should support `controller` as string', async(() => {
           // Define `ng1Directive`
           const ng1Directive: angular.IDirective = {
             template: '{{ $ctrl.title }} {{ $ctrl.text }}',
             scope: {title: '@'},
             bindToController: true,
             controller: 'Ng1Controller as $ctrl'
           };

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1'})
           class Ng1ComponentFacade extends UpgradeComponent {
             // TODO(issue/24571): remove '!'.
             @Input() title!: string;

             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({selector: 'ng2', template: '<ng1 title="WORKS"></ng1>'})
           class Ng2Component {
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1Module', [])
                                 .controller(
                                     'Ng1Controller',
                                     class {
                                       text = 'GREAT';
                                     })
                                 .directive('ng1', () => ng1Directive)
                                 .directive('ng2', downgradeComponent({component: Ng2Component}));

           // Define `Ng2Module`
           @NgModule({
             declarations: [Ng1ComponentFacade, Ng2Component],
             entryComponents: [Ng2Component],
             imports: [BrowserModule, UpgradeModule]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2></ng2>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
             expect(multiTrim(element.textContent)).toBe('WORKS GREAT');
           });
         }));

      it('should insert the compiled content before instantiating the controller', async(() => {
           let compiledContent: string;
           let getCurrentContent: () => string;

           // Define `ng1Component`
           const ng1Component: angular.IComponent = {
             template: 'Hello, {{ $ctrl.name }}!',
             controller: class {
               name = 'world';

               constructor($element: angular.IAugmentedJQuery) {
                 getCurrentContent = () => $element.text!();
                 compiledContent = getCurrentContent();
               }
             }
           };

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1'})
           class Ng1ComponentFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({selector: 'ng2', template: '<ng1></ng1>'})
           class Ng2Component {
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1Module', [])
                                 .component('ng1', ng1Component)
                                 .directive('ng2', downgradeComponent({component: Ng2Component}));

           // Define `Ng2Module`
           @NgModule({
             imports: [BrowserModule, UpgradeModule],
             declarations: [Ng1ComponentFacade, Ng2Component],
             entryComponents: [Ng2Component]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2></ng2>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
             expect(multiTrim(compiledContent)).toBe('Hello, {{ $ctrl.name }}!');
             expect(multiTrim(getCurrentContent())).toBe('Hello, world!');
           });
         }));
    });

    describe('require', () => {
      // NOT YET SUPPORTED
      xdescribe('in pre-/post-link', () => {
        it('should resolve to its own controller if falsy', async(() => {
             // Define `ng1Directive`
             const ng1Directive: angular.IDirective = {
               template: 'Pre: {{ pre }} | Post: {{ post }}',
               controller: class {
                 value = 'foo';
               },
               link: {
                 pre: function(scope: any, elem: any, attrs: any, ctrl: any) {
                   scope['pre'] = ctrl.value;
                 },
                 post: function(scope: any, elem: any, attrs: any, ctrl: any) {
                   scope['post'] = ctrl.value;
                 }
               }
             };

             // Define `Ng1ComponentFacade`
             @Directive({selector: 'ng1'})
             class Ng1ComponentFacade extends UpgradeComponent {
               constructor(elementRef: ElementRef, injector: Injector) {
                 super('ng1', elementRef, injector);
               }
             }

             // Define `Ng2Component`
             @Component({selector: 'ng2', template: '<ng1></ng1>'})
             class Ng2Component {
             }

             // Define `ng1Module`
             const ng1Module = angular.module_('ng1Module', [])
                                   .directive('ng1', () => ng1Directive)
                                   .directive('ng2', downgradeComponent({component: Ng2Component}));

             // Define `Ng2Module`
             @NgModule({
               declarations: [Ng1ComponentFacade, Ng2Component],
               entryComponents: [Ng2Component],
               imports: [BrowserModule, UpgradeModule]
             })
             class Ng2Module {
               ngDoBootstrap() {}
             }

             // Bootstrap
             const element = html(`<ng2></ng2>`);

             bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
               expect(multiTrim(document.body.textContent)).toBe('Pre: foo | Post: foo');
             });
           }));

        // TODO: Add more tests
      });

      describe('in controller', () => {
        it('should be available to children', async(() => {
             // Define `ng1Component`
             const ng1ComponentA: angular.IComponent = {
               template: '<ng1-b></ng1-b>',
               controller: class {
                 value = 'ng1A';
               }
             };

             const ng1ComponentB: angular.IComponent = {
               template: 'Required: {{ $ctrl.required.value }}',
               require: {required: '^^ng1A'}
             };

             // Define `Ng1ComponentFacade`
             @Directive({selector: 'ng1A'})
             class Ng1ComponentAFacade extends UpgradeComponent {
               constructor(elementRef: ElementRef, injector: Injector) {
                 super('ng1A', elementRef, injector);
               }
             }

             // Define `Ng2Component`
             @Component({selector: 'ng2', template: '<ng1A></ng1A>'})
             class Ng2Component {
             }

             // Define `ng1Module`
             const ng1Module = angular.module_('ng1Module', [])
                                   .component('ng1A', ng1ComponentA)
                                   .component('ng1B', ng1ComponentB)
                                   .directive('ng2', downgradeComponent({component: Ng2Component}));

             // Define `Ng2Module`
             @NgModule({
               declarations: [Ng1ComponentAFacade, Ng2Component],
               entryComponents: [Ng2Component],
               imports: [BrowserModule, UpgradeModule]
             })
             class Ng2Module {
               ngDoBootstrap() {}
             }

             // Bootstrap
             const element = html(`<ng2></ng2>`);

             bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
               expect(multiTrim(element.textContent)).toBe('Required: ng1A');
             });
           }));

        it('should throw if required controller cannot be found', async(() => {
             // Define `ng1Component`
             const ng1ComponentA: angular.IComponent = {require: {foo: 'iDoNotExist'}};
             const ng1ComponentB: angular.IComponent = {require: {foo: '^iDoNotExist'}};
             const ng1ComponentC: angular.IComponent = {require: {foo: '^^iDoNotExist'}};

             // Define `Ng1ComponentFacade`
             @Directive({selector: 'ng1A'})
             class Ng1ComponentAFacade extends UpgradeComponent {
               constructor(elementRef: ElementRef, injector: Injector) {
                 super('ng1A', elementRef, injector);
               }
             }

             @Directive({selector: 'ng1B'})
             class Ng1ComponentBFacade extends UpgradeComponent {
               constructor(elementRef: ElementRef, injector: Injector) {
                 super('ng1B', elementRef, injector);
               }
             }

             @Directive({selector: 'ng1C'})
             class Ng1ComponentCFacade extends UpgradeComponent {
               constructor(elementRef: ElementRef, injector: Injector) {
                 super('ng1C', elementRef, injector);
               }
             }

             // Define `Ng2Component`
             @Component({selector: 'ng2-a', template: '<ng1A></ng1A>'})
             class Ng2ComponentA {
             }

             @Component({selector: 'ng2-b', template: '<ng1B></ng1B>'})
             class Ng2ComponentB {
             }

             @Component({selector: 'ng2-c', template: '<ng1C></ng1C>'})
             class Ng2ComponentC {
             }

             // Define `ng1Module`
             const mockExceptionHandler = jasmine.createSpy($EXCEPTION_HANDLER);
             const ng1Module =
                 angular.module_('ng1Module', [])
                     .component('ng1A', ng1ComponentA)
                     .component('ng1B', ng1ComponentB)
                     .component('ng1C', ng1ComponentC)
                     .directive('ng2A', downgradeComponent({component: Ng2ComponentA}))
                     .directive('ng2B', downgradeComponent({component: Ng2ComponentB}))
                     .directive('ng2C', downgradeComponent({component: Ng2ComponentC}))
                     .value($EXCEPTION_HANDLER, mockExceptionHandler);

             // Define `Ng2Module`
             @NgModule({
               declarations: [
                 Ng1ComponentAFacade, Ng1ComponentBFacade, Ng1ComponentCFacade, Ng2ComponentA,
                 Ng2ComponentB, Ng2ComponentC
               ],
               entryComponents: [Ng2ComponentA, Ng2ComponentB, Ng2ComponentC],
               imports: [BrowserModule, UpgradeModule]
             })
             class Ng2Module {
               ngDoBootstrap() {}
             }

             // Bootstrap
             const elementA = html(`<ng2-a></ng2-a>`);
             const elementB = html(`<ng2-b></ng2-b>`);
             const elementC = html(`<ng2-c></ng2-c>`);

             bootstrap(platformBrowserDynamic(), Ng2Module, elementA, ng1Module).then(() => {
               expect(mockExceptionHandler)
                   .toHaveBeenCalledWith(new Error(
                       'Unable to find required \'iDoNotExist\' in upgraded directive \'ng1A\'.'));
             });

             bootstrap(platformBrowserDynamic(), Ng2Module, elementB, ng1Module).then(() => {
               expect(mockExceptionHandler)
                   .toHaveBeenCalledWith(new Error(
                       'Unable to find required \'^iDoNotExist\' in upgraded directive \'ng1B\'.'));
             });

             bootstrap(platformBrowserDynamic(), Ng2Module, elementC, ng1Module).then(() => {
               expect(mockExceptionHandler)
                   .toHaveBeenCalledWith(new Error(
                       'Unable to find required \'^^iDoNotExist\' in upgraded directive \'ng1C\'.'));
             });
           }));

        it('should not throw if missing required controller is optional', async(() => {
             // Define `ng1Component`
             const ng1Component: angular.IComponent = {
               require: {
                 foo: '?iDoNotExist',
                 bar: '^?iDoNotExist',
                 baz: '?^^iDoNotExist',
               }
             };

             // Define `Ng1ComponentFacade`
             @Directive({selector: 'ng1'})
             class Ng1ComponentFacade extends UpgradeComponent {
               constructor(elementRef: ElementRef, injector: Injector) {
                 super('ng1', elementRef, injector);
               }
             }

             // Define `Ng2Component`
             @Component({selector: 'ng2', template: '<ng1></ng1>'})
             class Ng2Component {
             }

             // Define `ng1Module`
             const mockExceptionHandler = jasmine.createSpy($EXCEPTION_HANDLER);
             const ng1Module = angular.module_('ng1Module', [])
                                   .component('ng1', ng1Component)
                                   .directive('ng2', downgradeComponent({component: Ng2Component}))
                                   .value($EXCEPTION_HANDLER, mockExceptionHandler);

             // Define `Ng2Module`
             @NgModule({
               declarations: [Ng1ComponentFacade, Ng2Component],
               entryComponents: [Ng2Component],
               imports: [BrowserModule, UpgradeModule]
             })
             class Ng2Module {
               ngDoBootstrap() {}
             }

             // Bootstrap
             const element = html(`<ng2></ng2>`);

             bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
               expect(mockExceptionHandler).not.toHaveBeenCalled();
             });
           }));

        it('should assign resolved values to the controller instance (if `require` is not object)',
           async(() => {
             // Define `ng1Component`
             const ng1ComponentA: angular.IComponent = {
               template: 'ng1A(<div><ng2></ng2></div>)',
               controller: class {
                 value = 'A';
               }
             };

             const ng1ComponentB: angular.IComponent = {
               template: `ng1B({{ $ctrl.getProps() }})`,
               require: '^ng1A',
               controller: class {
                 getProps() {
                   // If all goes well, there should be no keys on `this`
                   return Object.keys(this).join(', ');
                 }
               }
             };

             const ng1ComponentC: angular.IComponent = {
               template: `ng1C({{ $ctrl.getProps() }})`,
               require: ['?ng1A', '^ng1A', '^^ng1A', 'ng1C', '^ng1C', '?^^ng1C'],
               controller: class {
                 getProps() {
                   // If all goes well, there should be no keys on `this`
                   return Object.keys(this).join(', ');
                 }
               }
             };

             // Define `Ng1ComponentFacade`
             @Directive({selector: 'ng1B'})
             class Ng1ComponentBFacade extends UpgradeComponent {
               constructor(elementRef: ElementRef, injector: Injector) {
                 super('ng1B', elementRef, injector);
               }
             }

             @Directive({selector: 'ng1C'})
             class Ng1ComponentCFacade extends UpgradeComponent {
               constructor(elementRef: ElementRef, injector: Injector) {
                 super('ng1C', elementRef, injector);
               }
             }

             // Define `Ng2Component`
             @Component(
                 {selector: 'ng2', template: 'ng2(<div><ng1B></ng1B> | <ng1C></ng1C></div>)'})
             class Ng2Component {
             }

             // Define `ng1Module`
             const ng1Module = angular.module_('ng1Module', [])
                                   .component('ng1A', ng1ComponentA)
                                   .component('ng1B', ng1ComponentB)
                                   .component('ng1C', ng1ComponentC)
                                   .directive('ng2', downgradeComponent({component: Ng2Component}));

             // Define `Ng2Module`
             @NgModule({
               declarations: [Ng1ComponentBFacade, Ng1ComponentCFacade, Ng2Component],
               entryComponents: [Ng2Component],
               imports: [BrowserModule, UpgradeModule]
             })
             class Ng2Module {
               ngDoBootstrap() {}
             }

             // Bootstrap
             const element = html(`<ng1-a></ng1-a>`);

             bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
               expect(multiTrim(element.textContent)).toBe('ng1A(ng2(ng1B() | ng1C()))');
             });
           }));

        it('should assign resolved values to the controller instance (if `require` is object)',
           async(() => {
             // Define `ng1Component`
             const ng1ComponentA: angular.IComponent = {
               template: 'ng1A(<div><ng2></ng2></div>)',
               controller: class {
                 value = 'A';
               }
             };

             const ng1ComponentB: angular.IComponent = {
               template: `ng1B(
                 ng1A: {{ $ctrl.ng1ASelf.value }} |
                 ^ng1A: {{ $ctrl.ng1ASelfUp.value }} |
                 ^^ng1A: {{ $ctrl.ng1AParentUp.value }} |
                 ng1B: {{ $ctrl.ng1BSelf.value }} |
                 ^ng1B: {{ $ctrl.ng1BSelfUp.value }} |
                 ^^ng1B: {{ $ctrl.ng1BParentUp.value }}
               )`,
               require: {
                 ng1ASelf: '?ng1A',
                 ng1ASelfUp: '^ng1A',
                 ng1AParentUp: '^^ng1A',
                 ng1BSelf: 'ng1B',
                 ng1BSelfUp: '^ng1B',
                 ng1BParentUp: '?^^ng1B',
               },
               controller: class {
                 value = 'B';
               }
             };

             // Define `Ng1ComponentFacade`
             @Directive({selector: 'ng1B'})
             class Ng1ComponentBFacade extends UpgradeComponent {
               constructor(elementRef: ElementRef, injector: Injector) {
                 super('ng1B', elementRef, injector);
               }
             }

             // Define `Ng2Component`
             @Component({selector: 'ng2', template: 'ng2(<div><ng1B></ng1B></div>)'})
             class Ng2Component {
             }

             // Define `ng1Module`
             const ng1Module = angular.module_('ng1Module', [])
                                   .component('ng1A', ng1ComponentA)
                                   .component('ng1B', ng1ComponentB)
                                   .directive('ng2', downgradeComponent({component: Ng2Component}));

             // Define `Ng2Module`
             @NgModule({
               declarations: [Ng1ComponentBFacade, Ng2Component],
               entryComponents: [Ng2Component],
               imports: [BrowserModule, UpgradeModule]
             })
             class Ng2Module {
               ngDoBootstrap() {}
             }

             // Bootstrap
             const element = html(`<ng1-a></ng1-a>`);

             bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
               expect(multiTrim(element.textContent))
                   .toBe(
                       'ng1A(ng2(ng1B( ng1A: | ^ng1A: A | ^^ng1A: A | ng1B: B | ^ng1B: B | ^^ng1B: )))');
             });
           }));

        it('should assign to controller before calling `$onInit()`', async(() => {
             // Define `ng1Component`
             const ng1ComponentA: angular.IComponent = {
               template: '<ng2></ng2>',
               controller: class {
                 value = 'ng1A';
               }
             };

             const ng1ComponentB: angular.IComponent = {
               template: '$onInit: {{ $ctrl.onInitValue }}',
               require: {required: '^^ng1A'},
               controller: class {
                 $onInit() {
                   const self = this as any;
                   self.onInitValue = self.required.value;
                 }
               }
             };

             // Define `Ng1ComponentFacade`
             @Directive({selector: 'ng1B'})
             class Ng1ComponentBFacade extends UpgradeComponent {
               constructor(elementRef: ElementRef, injector: Injector) {
                 super('ng1B', elementRef, injector);
               }
             }

             // Define `Ng2Component`
             @Component({selector: 'ng2', template: '<ng1B></ng1B>'})
             class Ng2Component {
             }

             // Define `ng1Module`
             const ng1Module = angular.module_('ng1Module', [])
                                   .component('ng1A', ng1ComponentA)
                                   .component('ng1B', ng1ComponentB)
                                   .directive('ng2', downgradeComponent({component: Ng2Component}));

             // Define `Ng2Module`
             @NgModule({
               declarations: [Ng1ComponentBFacade, Ng2Component],
               entryComponents: [Ng2Component],
               imports: [BrowserModule, UpgradeModule]
             })
             class Ng2Module {
               ngDoBootstrap() {}
             }

             // Bootstrap
             const element = html(`<ng1-a></ng1-a>`);

             bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
               expect(multiTrim(element.textContent)).toBe('$onInit: ng1A');
             });
           }));

        it('should use the key as name if the required controller name is omitted', async(() => {
             // Define `ng1Component`
             const ng1ComponentA: angular.IComponent = {
               template: '<ng1-b></ng1-b>',
               controller: class {
                 value = 'A';
               }
             };

             const ng1ComponentB: angular.IComponent = {
               template: '<ng2></ng2>',
               controller: class {
                 value = 'B';
               }
             };

             const ng1ComponentC: angular.IComponent = {
               template:
                   'ng1A: {{ $ctrl.ng1A.value }} | ng1B: {{ $ctrl.ng1B.value }} | ng1C: {{ $ctrl.ng1C.value }}',
               require: {
                 ng1A: '^^',
                 ng1B: '?^',
                 ng1C: '',
               },
               controller: class {
                 value = 'C';
               }
             };

             // Define `Ng1ComponentFacade`
             @Directive({selector: 'ng1C'})
             class Ng1ComponentCFacade extends UpgradeComponent {
               constructor(elementRef: ElementRef, injector: Injector) {
                 super('ng1C', elementRef, injector);
               }
             }

             // Define `Ng2Component`
             @Component({selector: 'ng2', template: '<ng1C></ng1C>'})
             class Ng2Component {
             }

             // Define `ng1Module`
             const ng1Module = angular.module_('ng1Module', [])
                                   .component('ng1A', ng1ComponentA)
                                   .component('ng1B', ng1ComponentB)
                                   .component('ng1C', ng1ComponentC)
                                   .directive('ng2', downgradeComponent({component: Ng2Component}));

             // Define `Ng2Module`
             @NgModule({
               declarations: [Ng1ComponentCFacade, Ng2Component],
               entryComponents: [Ng2Component],
               imports: [BrowserModule, UpgradeModule]
             })
             class Ng2Module {
               ngDoBootstrap() {}
             }

             // Bootstrap
             const element = html('<ng1-a></ng1-a>');

             bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
               expect(multiTrim(element.textContent)).toBe('ng1A: A | ng1B: B | ng1C: C');
             });
           }));
      });
    });

    describe('transclusion', () => {
      it('should support single-slot transclusion', async(() => {
           let ng2ComponentAInstance: Ng2ComponentA;
           let ng2ComponentBInstance: Ng2ComponentB;

           // Define `ng1Component`
           const ng1Component:
               angular.IComponent = {template: 'ng1(<div ng-transclude></div>)', transclude: true};

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1'})
           class Ng1ComponentFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({
             selector: 'ng2A',
             template: 'ng2A(<ng1>{{ value }} | <ng2B *ngIf="showB"></ng2B></ng1>)'
           })
           class Ng2ComponentA {
             value = 'foo';
             showB = false;
             constructor() {
               ng2ComponentAInstance = this;
             }
           }

           @Component({selector: 'ng2B', template: 'ng2B({{ value }})'})
           class Ng2ComponentB {
             value = 'bar';
             constructor() {
               ng2ComponentBInstance = this;
             }
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1Module', [])
                                 .component('ng1', ng1Component)
                                 .directive('ng2A', downgradeComponent({component: Ng2ComponentA}));

           // Define `Ng2Module`
           @NgModule({
             imports: [BrowserModule, UpgradeModule],
             declarations: [Ng1ComponentFacade, Ng2ComponentA, Ng2ComponentB],
             entryComponents: [Ng2ComponentA]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2-a></ng2-a>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => {
             expect(multiTrim(element.textContent)).toBe('ng2A(ng1(foo | ))');

             ng2ComponentAInstance.value = 'baz';
             ng2ComponentAInstance.showB = true;
             $digest(adapter);

             expect(multiTrim(element.textContent)).toBe('ng2A(ng1(baz | ng2B(bar)))');

             ng2ComponentBInstance.value = 'qux';
             $digest(adapter);

             expect(multiTrim(element.textContent)).toBe('ng2A(ng1(baz | ng2B(qux)))');
           });
         }));

      it('should support single-slot transclusion with fallback content', async(() => {
           let ng1ControllerInstances: any[] = [];
           let ng2ComponentInstance: Ng2Component;

           // Define `ng1Component`
           const ng1Component: angular.IComponent = {
             template: 'ng1(<div ng-transclude>{{ $ctrl.value }}</div>)',
             transclude: true,
             controller: class {
               value = 'from-ng1';
               constructor() {
                 ng1ControllerInstances.push(this);
               }
             }
           };

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1'})
           class Ng1ComponentFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({selector: 'ng2', template: 'ng2(<ng1>{{ value }}</ng1> | <ng1></ng1>)'})
           class Ng2Component {
             value = 'from-ng2';
             constructor() {
               ng2ComponentInstance = this;
             }
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1Module', [])
                                 .component('ng1', ng1Component)
                                 .directive('ng2', downgradeComponent({component: Ng2Component}));

           // Define `Ng2Module`
           @NgModule({
             imports: [BrowserModule, UpgradeModule],
             declarations: [Ng1ComponentFacade, Ng2Component],
             entryComponents: [Ng2Component]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2></ng2>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => {
             expect(multiTrim(element.textContent)).toBe('ng2(ng1(from-ng2) | ng1(from-ng1))');

             ng1ControllerInstances.forEach(ctrl => ctrl.value = 'ng1-foo');
             ng2ComponentInstance.value = 'ng2-bar';
             $digest(adapter);

             expect(multiTrim(element.textContent)).toBe('ng2(ng1(ng2-bar) | ng1(ng1-foo))');
           });
         }));

      it('should support multi-slot transclusion', async(() => {
           let ng2ComponentInstance: Ng2Component;

           // Define `ng1Component`
           const ng1Component: angular.IComponent = {
             template:
                 'ng1(x(<div ng-transclude="slotX"></div>) | y(<div ng-transclude="slotY"></div>))',
             transclude: {slotX: 'contentX', slotY: 'contentY'}
           };

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1'})
           class Ng1ComponentFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({
             selector: 'ng2',
             template: `
               ng2(
                 <ng1>
                   <content-x>{{ x }}1</content-x>
                   <content-y>{{ y }}1</content-y>
                   <content-x>{{ x }}2</content-x>
                   <content-y>{{ y }}2</content-y>
                 </ng1>
               )`
           })
           class Ng2Component {
             x = 'foo';
             y = 'bar';
             constructor() {
               ng2ComponentInstance = this;
             }
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1Module', [])
                                 .component('ng1', ng1Component)
                                 .directive('ng2', downgradeComponent({component: Ng2Component}));

           // Define `Ng2Module`
           @NgModule({
             imports: [BrowserModule, UpgradeModule],
             declarations: [Ng1ComponentFacade, Ng2Component],
             entryComponents: [Ng2Component],
             schemas: [NO_ERRORS_SCHEMA]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2></ng2>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => {
             expect(multiTrim(element.textContent, true)).toBe('ng2(ng1(x(foo1foo2)|y(bar1bar2)))');

             ng2ComponentInstance.x = 'baz';
             ng2ComponentInstance.y = 'qux';
             $digest(adapter);

             expect(multiTrim(element.textContent, true)).toBe('ng2(ng1(x(baz1baz2)|y(qux1qux2)))');
           });
         }));

      it('should support default slot (with fallback content)', async(() => {
           let ng1ControllerInstances: any[] = [];
           let ng2ComponentInstance: Ng2Component;

           // Define `ng1Component`
           const ng1Component: angular.IComponent = {
             template: 'ng1(default(<div ng-transclude="">fallback-{{ $ctrl.value }}</div>))',
             transclude: {slotX: 'contentX', slotY: 'contentY'},
             controller: class {
               value = 'ng1';
               constructor() {
                 ng1ControllerInstances.push(this);
               }
             }
           };

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1'})
           class Ng1ComponentFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({
             selector: 'ng2',
             template: `
               ng2(
                 <ng1>
                   ({{ x }})
                   <content-x>ignored x</content-x>
                   {{ x }}-<span>{{ y }}</span>
                   <content-y>ignored y</content-y>
                   <span>({{ y }})</span>
                 </ng1> |
                 <!--
                   Remove any whitespace, because in AngularJS versions prior to 1.6
                   even whitespace counts as transcluded content.
                 -->
                 <ng1><content-x>ignored x</content-x><content-y>ignored y</content-y></ng1>
               )`
           })
           class Ng2Component {
             x = 'foo';
             y = 'bar';
             constructor() {
               ng2ComponentInstance = this;
             }
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1Module', [])
                                 .component('ng1', ng1Component)
                                 .directive('ng2', downgradeComponent({component: Ng2Component}));

           // Define `Ng2Module`
           @NgModule({
             imports: [BrowserModule, UpgradeModule],
             declarations: [Ng1ComponentFacade, Ng2Component],
             entryComponents: [Ng2Component],
             schemas: [NO_ERRORS_SCHEMA]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2></ng2>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => {
             expect(multiTrim(element.textContent, true))
                 .toBe('ng2(ng1(default((foo)foo-bar(bar)))|ng1(default(fallback-ng1)))');

             ng1ControllerInstances.forEach(ctrl => ctrl.value = 'ng1-plus');
             ng2ComponentInstance.x = 'baz';
             ng2ComponentInstance.y = 'qux';
             $digest(adapter);

             expect(multiTrim(element.textContent, true))
                 .toBe('ng2(ng1(default((baz)baz-qux(qux)))|ng1(default(fallback-ng1-plus)))');
           });
         }));

      it('should support optional transclusion slots (with fallback content)', async(() => {
           let ng1ControllerInstances: any[] = [];
           let ng2ComponentInstance: Ng2Component;

           // Define `ng1Component`
           const ng1Component: angular.IComponent = {
             template: `
               ng1(
                x(<div ng-transclude="slotX">{{ $ctrl.x }}</div>) |
                y(<div ng-transclude="slotY">{{ $ctrl.y }}</div>)
               )`,
             transclude: {slotX: '?contentX', slotY: '?contentY'},
             controller: class {
               x = 'ng1X';
               y = 'ng1Y';
               constructor() {
                 ng1ControllerInstances.push(this);
               }
             }
           };

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1'})
           class Ng1ComponentFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({
             selector: 'ng2',
             template: `
               ng2(
                 <ng1><content-x>{{ x }}</content-x></ng1> |
                 <ng1><content-y>{{ y }}</content-y></ng1>
               )`
           })
           class Ng2Component {
             x = 'ng2X';
             y = 'ng2Y';
             constructor() {
               ng2ComponentInstance = this;
             }
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1Module', [])
                                 .component('ng1', ng1Component)
                                 .directive('ng2', downgradeComponent({component: Ng2Component}));

           // Define `Ng2Module`
           @NgModule({
             imports: [BrowserModule, UpgradeModule],
             declarations: [Ng1ComponentFacade, Ng2Component],
             entryComponents: [Ng2Component],
             schemas: [NO_ERRORS_SCHEMA]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2></ng2>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => {
             expect(multiTrim(element.textContent, true))
                 .toBe('ng2(ng1(x(ng2X)|y(ng1Y))|ng1(x(ng1X)|y(ng2Y)))');

             ng1ControllerInstances.forEach(ctrl => {
               ctrl.x = 'ng1X-foo';
               ctrl.y = 'ng1Y-bar';
             });
             ng2ComponentInstance.x = 'ng2X-baz';
             ng2ComponentInstance.y = 'ng2Y-qux';
             $digest(adapter);

             expect(multiTrim(element.textContent, true))
                 .toBe('ng2(ng1(x(ng2X-baz)|y(ng1Y-bar))|ng1(x(ng1X-foo)|y(ng2Y-qux)))');
           });
         }));

      it('should throw if a non-optional slot is not filled', async(() => {
           let errorMessage: string;

           // Define `ng1Component`
           const ng1Component: angular.IComponent = {
             template: '',
             transclude: {slotX: '?contentX', slotY: 'contentY'}
           };

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1'})
           class Ng1ComponentFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({selector: 'ng2', template: '<ng1></ng1>'})
           class Ng2Component {
           }

           // Define `ng1Module`
           const ng1Module =
               angular.module_('ng1Module', [])
                   .value($EXCEPTION_HANDLER, (error: Error) => errorMessage = error.message)
                   .component('ng1', ng1Component)
                   .directive('ng2', downgradeComponent({component: Ng2Component}));

           // Define `Ng2Module`
           @NgModule({
             imports: [BrowserModule, UpgradeModule],
             declarations: [Ng1ComponentFacade, Ng2Component],
             entryComponents: [Ng2Component]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2></ng2>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => {
             expect(errorMessage)
                 .toContain('Required transclusion slot \'slotY\' on directive: ng1');
           });
         }));

      it('should support structural directives in transcluded content', async(() => {
           let ng2ComponentInstance: Ng2Component;

           // Define `ng1Component`
           const ng1Component: angular.IComponent = {
             template:
                 'ng1(x(<div ng-transclude="slotX"></div>) | default(<div ng-transclude=""></div>))',
             transclude: {slotX: 'contentX'}
           };

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1'})
           class Ng1ComponentFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({
             selector: 'ng2',
             template: `
               ng2(
                 <ng1>
                   <content-x><div *ngIf="show">{{ x }}1</div></content-x>
                   <div *ngIf="!show">{{ y }}1</div>
                   <content-x><div *ngIf="!show">{{ x }}2</div></content-x>
                   <div *ngIf="show">{{ y }}2</div>
                 </ng1>
               )`
           })
           class Ng2Component {
             x = 'foo';
             y = 'bar';
             show = true;
             constructor() {
               ng2ComponentInstance = this;
             }
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1Module', [])
                                 .component('ng1', ng1Component)
                                 .directive('ng2', downgradeComponent({component: Ng2Component}));

           // Define `Ng2Module`
           @NgModule({
             imports: [BrowserModule, UpgradeModule],
             declarations: [Ng1ComponentFacade, Ng2Component],
             entryComponents: [Ng2Component],
             schemas: [NO_ERRORS_SCHEMA]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2></ng2>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => {
             expect(multiTrim(element.textContent, true)).toBe('ng2(ng1(x(foo1)|default(bar2)))');

             ng2ComponentInstance.x = 'baz';
             ng2ComponentInstance.y = 'qux';
             ng2ComponentInstance.show = false;
             $digest(adapter);

             expect(multiTrim(element.textContent, true)).toBe('ng2(ng1(x(baz2)|default(qux1)))');

             ng2ComponentInstance.show = true;
             $digest(adapter);

             expect(multiTrim(element.textContent, true)).toBe('ng2(ng1(x(baz1)|default(qux2)))');
           });
         }));
    });

    describe('lifecycle hooks', () => {
      it('should call `$onChanges()` on binding destination (prototype)', fakeAsync(() => {
           const scopeOnChanges = jasmine.createSpy('scopeOnChanges');
           const controllerOnChangesA = jasmine.createSpy('controllerOnChangesA');
           const controllerOnChangesB = jasmine.createSpy('controllerOnChangesB');
           let ng2ComponentInstance: Ng2Component;

           // Define `ng1Directive`
           const ng1DirectiveA: angular.IDirective = {
             template: '',
             scope: {inputA: '<'},
             bindToController: false,
             controllerAs: '$ctrl',
             controller: class {
               $onChanges(changes: SimpleChanges) {
                 controllerOnChangesA(changes);
               }
             }
           };

           const ng1DirectiveB: angular.IDirective = {
             template: '',
             scope: {inputB: '<'},
             bindToController: true,
             controllerAs: '$ctrl',
             controller: class {
               $onChanges(changes: SimpleChanges) {
                 controllerOnChangesB(changes);
               }
             }
           };

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1A'})
           class Ng1ComponentAFacade extends UpgradeComponent {
             @Input() inputA: any;

             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1A', elementRef, injector);
             }
           }

           @Directive({selector: 'ng1B'})
           class Ng1ComponentBFacade extends UpgradeComponent {
             @Input() inputB: any;

             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1B', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({
             selector: 'ng2',
             template: '<ng1A [inputA]="data"></ng1A> | <ng1B [inputB]="data"></ng1B>'
           })
           class Ng2Component {
             data = {foo: 'bar'};

             constructor() {
               ng2ComponentInstance = this;
             }
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1Module', [])
                                 .directive('ng1A', () => ng1DirectiveA)
                                 .directive('ng1B', () => ng1DirectiveB)
                                 .directive('ng2', downgradeComponent({component: Ng2Component}))
                                 .run(($rootScope: angular.IRootScopeService) => {
                                   Object.getPrototypeOf($rootScope)['$onChanges'] = scopeOnChanges;
                                 });

           // Define `Ng2Module`
           @NgModule({
             declarations: [Ng1ComponentAFacade, Ng1ComponentBFacade, Ng2Component],
             entryComponents: [Ng2Component],
             imports: [BrowserModule, UpgradeModule]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2></ng2>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => {
             // Initial change
             expect(scopeOnChanges.calls.count()).toBe(1);
             expect(controllerOnChangesA).not.toHaveBeenCalled();
             expect(controllerOnChangesB.calls.count()).toBe(1);

             expect(scopeOnChanges.calls.argsFor(0)[0]).toEqual({inputA: jasmine.any(Object)});
             expect(scopeOnChanges.calls.argsFor(0)[0].inputA.currentValue).toEqual({foo: 'bar'});
             expect(scopeOnChanges.calls.argsFor(0)[0].inputA.isFirstChange()).toBe(true);
             expect(controllerOnChangesB.calls.argsFor(0)[0].inputB.currentValue).toEqual({
               foo: 'bar'
             });
             expect(controllerOnChangesB.calls.argsFor(0)[0].inputB.isFirstChange()).toBe(true);

             // Change: Re-assign `data`
             ng2ComponentInstance.data = {foo: 'baz'};
             $digest(adapter);
             tick();

             expect(scopeOnChanges.calls.count()).toBe(2);
             expect(controllerOnChangesA).not.toHaveBeenCalled();
             expect(controllerOnChangesB.calls.count()).toBe(2);

             expect(scopeOnChanges.calls.argsFor(1)[0]).toEqual({inputA: jasmine.any(Object)});
             expect(scopeOnChanges.calls.argsFor(1)[0].inputA.previousValue).toEqual({foo: 'bar'});
             expect(scopeOnChanges.calls.argsFor(1)[0].inputA.currentValue).toEqual({foo: 'baz'});
             expect(scopeOnChanges.calls.argsFor(1)[0].inputA.isFirstChange()).toBe(false);
             expect(controllerOnChangesB.calls.argsFor(1)[0].inputB.previousValue).toEqual({
               foo: 'bar'
             });
             expect(controllerOnChangesB.calls.argsFor(1)[0].inputB.currentValue).toEqual({
               foo: 'baz'
             });
             expect(controllerOnChangesB.calls.argsFor(1)[0].inputB.isFirstChange()).toBe(false);

             // No change: Update internal property
             ng2ComponentInstance.data.foo = 'qux';
             $digest(adapter);
             tick();

             expect(scopeOnChanges.calls.count()).toBe(2);
             expect(controllerOnChangesA).not.toHaveBeenCalled();
             expect(controllerOnChangesB.calls.count()).toBe(2);

             // Change: Re-assign `data` (even if it looks the same)
             ng2ComponentInstance.data = {foo: 'qux'};
             $digest(adapter);
             tick();

             expect(scopeOnChanges.calls.count()).toBe(3);
             expect(controllerOnChangesA).not.toHaveBeenCalled();
             expect(controllerOnChangesB.calls.count()).toBe(3);

             expect(scopeOnChanges.calls.argsFor(2)[0]).toEqual({inputA: jasmine.any(Object)});
             expect(scopeOnChanges.calls.argsFor(2)[0].inputA.previousValue).toEqual({foo: 'qux'});
             expect(scopeOnChanges.calls.argsFor(2)[0].inputA.currentValue).toEqual({foo: 'qux'});
             expect(scopeOnChanges.calls.argsFor(2)[0].inputA.isFirstChange()).toBe(false);
             expect(controllerOnChangesB.calls.argsFor(2)[0].inputB.previousValue).toEqual({
               foo: 'qux'
             });
             expect(controllerOnChangesB.calls.argsFor(2)[0].inputB.currentValue).toEqual({
               foo: 'qux'
             });
             expect(controllerOnChangesB.calls.argsFor(2)[0].inputB.isFirstChange()).toBe(false);
           });
         }));

      it('should call `$onChanges()` on binding destination (instance)', fakeAsync(() => {
           const scopeOnChangesA = jasmine.createSpy('scopeOnChangesA');
           const scopeOnChangesB = jasmine.createSpy('scopeOnChangesB');
           const controllerOnChangesA = jasmine.createSpy('controllerOnChangesA');
           const controllerOnChangesB = jasmine.createSpy('controllerOnChangesB');
           let ng2ComponentInstance: Ng2Component;

           // Define `ng1Directive`
           const ng1DirectiveA: angular.IDirective = {
             template: '',
             scope: {inputA: '<'},
             bindToController: false,
             controllerAs: '$ctrl',
             controller: class {
               constructor($scope: angular.IScope) {
                 $scope['$onChanges'] = scopeOnChangesA;
                 (this as any).$onChanges = controllerOnChangesA;
               }
             }
           };

           const ng1DirectiveB: angular.IDirective = {
             template: '',
             scope: {inputB: '<'},
             bindToController: true,
             controllerAs: '$ctrl',
             controller: class {
               constructor($scope: angular.IScope) {
                 $scope['$onChanges'] = scopeOnChangesB;
                 (this as any).$onChanges = controllerOnChangesB;
               }
             }
           };

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1A'})
           class Ng1ComponentAFacade extends UpgradeComponent {
             @Input() inputA: any;

             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1A', elementRef, injector);
             }
           }

           @Directive({selector: 'ng1B'})
           class Ng1ComponentBFacade extends UpgradeComponent {
             @Input() inputB: any;

             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1B', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({
             selector: 'ng2',
             template: '<ng1A [inputA]="data"></ng1A> | <ng1B [inputB]="data"></ng1B>'
           })
           class Ng2Component {
             data = {foo: 'bar'};

             constructor() {
               ng2ComponentInstance = this;
             }
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1Module', [])
                                 .directive('ng1A', () => ng1DirectiveA)
                                 .directive('ng1B', () => ng1DirectiveB)
                                 .directive('ng2', downgradeComponent({component: Ng2Component}));

           // Define `Ng2Module`
           @NgModule({
             declarations: [Ng1ComponentAFacade, Ng1ComponentBFacade, Ng2Component],
             entryComponents: [Ng2Component],
             imports: [BrowserModule, UpgradeModule]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2></ng2>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => {
             // Initial change
             expect(scopeOnChangesA.calls.count()).toBe(1);
             expect(scopeOnChangesB).not.toHaveBeenCalled();
             expect(controllerOnChangesA).not.toHaveBeenCalled();
             expect(controllerOnChangesB.calls.count()).toBe(1);

             expect(scopeOnChangesA.calls.argsFor(0)[0].inputA.currentValue).toEqual({foo: 'bar'});
             expect(scopeOnChangesA.calls.argsFor(0)[0].inputA.isFirstChange()).toBe(true);
             expect(controllerOnChangesB.calls.argsFor(0)[0].inputB.currentValue).toEqual({
               foo: 'bar'
             });
             expect(controllerOnChangesB.calls.argsFor(0)[0].inputB.isFirstChange()).toBe(true);

             // Change: Re-assign `data`
             ng2ComponentInstance.data = {foo: 'baz'};
             $digest(adapter);
             tick();

             expect(scopeOnChangesA.calls.count()).toBe(2);
             expect(scopeOnChangesB).not.toHaveBeenCalled();
             expect(controllerOnChangesA).not.toHaveBeenCalled();
             expect(controllerOnChangesB.calls.count()).toBe(2);

             expect(scopeOnChangesA.calls.argsFor(1)[0].inputA.previousValue).toEqual({foo: 'bar'});
             expect(scopeOnChangesA.calls.argsFor(1)[0].inputA.currentValue).toEqual({foo: 'baz'});
             expect(scopeOnChangesA.calls.argsFor(1)[0].inputA.isFirstChange()).toBe(false);
             expect(controllerOnChangesB.calls.argsFor(1)[0].inputB.previousValue).toEqual({
               foo: 'bar'
             });
             expect(controllerOnChangesB.calls.argsFor(1)[0].inputB.currentValue).toEqual({
               foo: 'baz'
             });
             expect(controllerOnChangesB.calls.argsFor(1)[0].inputB.isFirstChange()).toBe(false);

             // No change: Update internal property
             ng2ComponentInstance.data.foo = 'qux';
             $digest(adapter);
             tick();

             expect(scopeOnChangesA.calls.count()).toBe(2);
             expect(scopeOnChangesB).not.toHaveBeenCalled();
             expect(controllerOnChangesA).not.toHaveBeenCalled();
             expect(controllerOnChangesB.calls.count()).toBe(2);

             // Change: Re-assign `data` (even if it looks the same)
             ng2ComponentInstance.data = {foo: 'qux'};
             $digest(adapter);
             tick();

             expect(scopeOnChangesA.calls.count()).toBe(3);
             expect(scopeOnChangesB).not.toHaveBeenCalled();
             expect(controllerOnChangesA).not.toHaveBeenCalled();
             expect(controllerOnChangesB.calls.count()).toBe(3);

             expect(scopeOnChangesA.calls.argsFor(2)[0].inputA.previousValue).toEqual({foo: 'qux'});
             expect(scopeOnChangesA.calls.argsFor(2)[0].inputA.currentValue).toEqual({foo: 'qux'});
             expect(scopeOnChangesA.calls.argsFor(2)[0].inputA.isFirstChange()).toBe(false);
             expect(controllerOnChangesB.calls.argsFor(2)[0].inputB.previousValue).toEqual({
               foo: 'qux'
             });
             expect(controllerOnChangesB.calls.argsFor(2)[0].inputB.currentValue).toEqual({
               foo: 'qux'
             });
             expect(controllerOnChangesB.calls.argsFor(2)[0].inputB.isFirstChange()).toBe(false);
           });
         }));

      it('should call `$onInit()` on controller', async(() => {
           // Define `ng1Directive`
           const ng1DirectiveA: angular.IDirective = {
             template: 'Called: {{ called }}',
             bindToController: false,
             controller: class {
               constructor(private $scope: angular.IScope) {
                 $scope['called'] = 'no';
               }

               $onInit() {
                 this.$scope['called'] = 'yes';
               }
             }
           };

           const ng1DirectiveB: angular.IDirective = {
             template: 'Called: {{ called }}',
             bindToController: true,
             controller: class {
               constructor($scope: angular.IScope) {
                 $scope['called'] = 'no';
                 (this as any)['$onInit'] = () => $scope['called'] = 'yes';
               }
             }
           };

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1A'})
           class Ng1ComponentAFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1A', elementRef, injector);
             }
           }

           @Directive({selector: 'ng1B'})
           class Ng1ComponentBFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1B', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({selector: 'ng2', template: '<ng1A></ng1A> | <ng1B></ng1B>'})
           class Ng2Component {
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1Module', [])
                                 .directive('ng1A', () => ng1DirectiveA)
                                 .directive('ng1B', () => ng1DirectiveB)
                                 .directive('ng2', downgradeComponent({component: Ng2Component}));

           // Define `Ng2Module`
           @NgModule({
             declarations: [Ng1ComponentAFacade, Ng1ComponentBFacade, Ng2Component],
             entryComponents: [Ng2Component],
             imports: [BrowserModule, UpgradeModule]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2></ng2>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
             expect(multiTrim(element.textContent)).toBe('Called: yes | Called: yes');
           });
         }));

      it('should not call `$onInit()` on scope', async(() => {
           // Define `ng1Directive`
           const ng1DirectiveA: angular.IDirective = {
             template: 'Called: {{ called }}',
             bindToController: false,
             controller: class {
               constructor($scope: angular.IScope) {
                 $scope['called'] = 'no';
                 $scope['$onInit'] = () => $scope['called'] = 'yes';
                 Object.getPrototypeOf($scope)['$onInit'] = () => $scope['called'] = 'yes';
               }
             }
           };

           const ng1DirectiveB: angular.IDirective = {
             template: 'Called: {{ called }}',
             bindToController: true,
             controller: class {
               constructor($scope: angular.IScope) {
                 $scope['called'] = 'no';
                 $scope['$onInit'] = () => $scope['called'] = 'yes';
                 Object.getPrototypeOf($scope)['$onInit'] = () => $scope['called'] = 'yes';
               }
             }
           };

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1A'})
           class Ng1ComponentAFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1A', elementRef, injector);
             }
           }

           @Directive({selector: 'ng1B'})
           class Ng1ComponentBFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1B', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({selector: 'ng2', template: '<ng1A></ng1A> | <ng1B></ng1B>'})
           class Ng2Component {
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1Module', [])
                                 .directive('ng1A', () => ng1DirectiveA)
                                 .directive('ng1B', () => ng1DirectiveB)
                                 .directive('ng2', downgradeComponent({component: Ng2Component}));

           // Define `Ng2Module`
           @NgModule({
             declarations: [Ng1ComponentAFacade, Ng1ComponentBFacade, Ng2Component],
             entryComponents: [Ng2Component],
             imports: [BrowserModule, UpgradeModule]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2></ng2>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
             expect(multiTrim(element.textContent)).toBe('Called: no | Called: no');
           });
         }));

      it('should call `$postLink()` on controller', async(() => {
           // Define `ng1Directive`
           const ng1DirectiveA: angular.IDirective = {
             template: 'Called: {{ called }}',
             bindToController: false,
             controller: class {
               constructor(private $scope: angular.IScope) {
                 $scope['called'] = 'no';
               }

               $postLink() {
                 this.$scope['called'] = 'yes';
               }
             }
           };

           const ng1DirectiveB: angular.IDirective = {
             template: 'Called: {{ called }}',
             bindToController: true,
             controller: class {
               constructor($scope: angular.IScope) {
                 $scope['called'] = 'no';
                 (this as any)['$postLink'] = () => $scope['called'] = 'yes';
               }
             }
           };

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1A'})
           class Ng1ComponentAFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1A', elementRef, injector);
             }
           }

           @Directive({selector: 'ng1B'})
           class Ng1ComponentBFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1B', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({selector: 'ng2', template: '<ng1A></ng1A> | <ng1B></ng1B>'})
           class Ng2Component {
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1Module', [])
                                 .directive('ng1A', () => ng1DirectiveA)
                                 .directive('ng1B', () => ng1DirectiveB)
                                 .directive('ng2', downgradeComponent({component: Ng2Component}));

           // Define `Ng2Module`
           @NgModule({
             declarations: [Ng1ComponentAFacade, Ng1ComponentBFacade, Ng2Component],
             entryComponents: [Ng2Component],
             imports: [BrowserModule, UpgradeModule]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2></ng2>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
             expect(multiTrim(element.textContent)).toBe('Called: yes | Called: yes');
           });
         }));

      it('should not call `$postLink()` on scope', async(() => {
           // Define `ng1Directive`
           const ng1DirectiveA: angular.IDirective = {
             template: 'Called: {{ called }}',
             bindToController: false,
             controller: class {
               constructor($scope: angular.IScope) {
                 $scope['called'] = 'no';
                 $scope['$postLink'] = () => $scope['called'] = 'yes';
                 Object.getPrototypeOf($scope)['$postLink'] = () => $scope['called'] = 'yes';
               }
             }
           };

           const ng1DirectiveB: angular.IDirective = {
             template: 'Called: {{ called }}',
             bindToController: true,
             controller: class {
               constructor($scope: angular.IScope) {
                 $scope['called'] = 'no';
                 $scope['$postLink'] = () => $scope['called'] = 'yes';
                 Object.getPrototypeOf($scope)['$postLink'] = () => $scope['called'] = 'yes';
               }
             }
           };

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1A'})
           class Ng1ComponentAFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1A', elementRef, injector);
             }
           }

           @Directive({selector: 'ng1B'})
           class Ng1ComponentBFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1B', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({selector: 'ng2', template: '<ng1A></ng1A> | <ng1B></ng1B>'})
           class Ng2Component {
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1Module', [])
                                 .directive('ng1A', () => ng1DirectiveA)
                                 .directive('ng1B', () => ng1DirectiveB)
                                 .directive('ng2', downgradeComponent({component: Ng2Component}));

           // Define `Ng2Module`
           @NgModule({
             declarations: [Ng1ComponentAFacade, Ng1ComponentBFacade, Ng2Component],
             entryComponents: [Ng2Component],
             imports: [BrowserModule, UpgradeModule]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2></ng2>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
             expect(multiTrim(element.textContent)).toBe('Called: no | Called: no');
           });
         }));


      it('should call `$doCheck()` on controller', async(() => {
           const controllerDoCheckA = jasmine.createSpy('controllerDoCheckA');
           const controllerDoCheckB = jasmine.createSpy('controllerDoCheckB');

           // Define `ng1Directive`
           const ng1DirectiveA: angular.IDirective = {
             template: 'ng1A',
             bindToController: false,
             controller: class {
               $doCheck() {
                 controllerDoCheckA();
               }
             }
           };

           const ng1DirectiveB: angular.IDirective = {
             template: 'ng1B',
             bindToController: true,
             controller: class {
               constructor() {
                 (this as any)['$doCheck'] = controllerDoCheckB;
               }
             }
           };

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1A'})
           class Ng1ComponentAFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1A', elementRef, injector);
             }
           }

           @Directive({selector: 'ng1B'})
           class Ng1ComponentBFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1B', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({selector: 'ng2', template: '<ng1A></ng1A> | <ng1B></ng1B>'})
           class Ng2Component {
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1Module', [])
                                 .directive('ng1A', () => ng1DirectiveA)
                                 .directive('ng1B', () => ng1DirectiveB)
                                 .directive('ng2', downgradeComponent({component: Ng2Component}));

           // Define `Ng2Module`
           @NgModule({
             declarations: [Ng1ComponentAFacade, Ng1ComponentBFacade, Ng2Component],
             entryComponents: [Ng2Component],
             imports: [BrowserModule, UpgradeModule]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2></ng2>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => {
             // Get to a stable `$digest` state.
             $digest(adapter);

             // Initial change.
             // (Do not use a specific number due to differences between AngularJS 1.5/1.6.)
             expect(controllerDoCheckA.calls.count()).toBeGreaterThan(0);
             expect(controllerDoCheckB.calls.count()).toBeGreaterThan(0);
             controllerDoCheckA.calls.reset();
             controllerDoCheckB.calls.reset();

             // Run a `$digest`
             $digest(adapter);
             expect(controllerDoCheckA.calls.count()).toBe(1);
             expect(controllerDoCheckB.calls.count()).toBe(1);

             // Run another `$digest`
             $digest(adapter);
             expect(controllerDoCheckA.calls.count()).toBe(2);
             expect(controllerDoCheckB.calls.count()).toBe(2);
           });
         }));

      it('should not call `$doCheck()` on scope', async(() => {
           const scopeDoCheck = jasmine.createSpy('scopeDoCheck');

           // Define `ng1Directive`
           const ng1DirectiveA: angular.IDirective = {
             template: 'ng1A',
             bindToController: false,
             controller: class {
               constructor(private $scope: angular.IScope) {
                 $scope['$doCheck'] = scopeDoCheck;
               }
             }
           };

           const ng1DirectiveB: angular.IDirective = {
             template: 'ng1B',
             bindToController: true,
             controller: class {
               constructor(private $scope: angular.IScope) {
                 $scope['$doCheck'] = scopeDoCheck;
               }
             }
           };

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1A'})
           class Ng1ComponentAFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1A', elementRef, injector);
             }
           }

           @Directive({selector: 'ng1B'})
           class Ng1ComponentBFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1B', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({selector: 'ng2', template: '<ng1A></ng1A> | <ng1B></ng1B>'})
           class Ng2Component {
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1Module', [])
                                 .directive('ng1A', () => ng1DirectiveA)
                                 .directive('ng1B', () => ng1DirectiveB)
                                 .directive('ng2', downgradeComponent({component: Ng2Component}));

           // Define `Ng2Module`
           @NgModule({
             declarations: [Ng1ComponentAFacade, Ng1ComponentBFacade, Ng2Component],
             entryComponents: [Ng2Component],
             imports: [BrowserModule, UpgradeModule]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2></ng2>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => {
             // Initial change
             expect(scopeDoCheck).not.toHaveBeenCalled();

             // Run a `$digest`
             $digest(adapter);
             expect(scopeDoCheck).not.toHaveBeenCalled();

             // Run another `$digest`
             $digest(adapter);
             expect(scopeDoCheck).not.toHaveBeenCalled();
           });
         }));


      it('should call `$onDestroy()` on controller', async(() => {
           const controllerOnDestroyA = jasmine.createSpy('controllerOnDestroyA');
           const controllerOnDestroyB = jasmine.createSpy('controllerOnDestroyB');

           // Define `ng1Directive`
           const ng1DirectiveA: angular.IDirective = {
             template: 'ng1A',
             scope: {},
             bindToController: false,
             controllerAs: '$ctrl',
             controller: class {
               $onDestroy() {
                 controllerOnDestroyA();
               }
             }
           };

           const ng1DirectiveB: angular.IDirective = {
             template: 'ng1B',
             scope: {},
             bindToController: true,
             controllerAs: '$ctrl',
             controller: class {
               constructor() {
                 (this as any)['$onDestroy'] = controllerOnDestroyB;
               }
             }
           };

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1A'})
           class Ng1ComponentAFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1A', elementRef, injector);
             }
           }

           @Directive({selector: 'ng1B'})
           class Ng1ComponentBFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1B', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component(
               {selector: 'ng2', template: '<div *ngIf="show"><ng1A></ng1A> | <ng1B></ng1B></div>'})
           class Ng2Component {
             // TODO(issue/24571): remove '!'.
             @Input() show!: boolean;
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1Module', [])
                                 .directive('ng1A', () => ng1DirectiveA)
                                 .directive('ng1B', () => ng1DirectiveB)
                                 .directive('ng2', downgradeComponent({component: Ng2Component}));

           // Define `Ng2Module`
           @NgModule({
             declarations: [Ng1ComponentAFacade, Ng1ComponentBFacade, Ng2Component],
             entryComponents: [Ng2Component],
             imports: [BrowserModule, UpgradeModule]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html('<ng2 [show]="!destroyFromNg2" ng-if="!destroyFromNg1"></ng2>');

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => {
             const $rootScope = adapter.$injector.get('$rootScope') as angular.IRootScopeService;

             expect(multiTrim(document.body.textContent)).toBe('ng1A | ng1B');
             expect(controllerOnDestroyA).not.toHaveBeenCalled();
             expect(controllerOnDestroyB).not.toHaveBeenCalled();

             $rootScope.$apply('destroyFromNg1 = true');

             expect(multiTrim(document.body.textContent)).toBe('');
             expect(controllerOnDestroyA).toHaveBeenCalled();
             expect(controllerOnDestroyB).toHaveBeenCalled();

             controllerOnDestroyA.calls.reset();
             controllerOnDestroyB.calls.reset();
             $rootScope.$apply('destroyFromNg1 = false');

             expect(multiTrim(document.body.textContent)).toBe('ng1A | ng1B');
             expect(controllerOnDestroyA).not.toHaveBeenCalled();
             expect(controllerOnDestroyB).not.toHaveBeenCalled();

             $rootScope.$apply('destroyFromNg2 = true');

             expect(multiTrim(document.body.textContent)).toBe('');
             expect(controllerOnDestroyA).toHaveBeenCalled();
             expect(controllerOnDestroyB).toHaveBeenCalled();
           });
         }));

      it('should not call `$onDestroy()` on scope', async(() => {
           const scopeOnDestroy = jasmine.createSpy('scopeOnDestroy');

           // Define `ng1Directive`
           const ng1DirectiveA: angular.IDirective = {
             template: 'ng1A',
             scope: {},
             bindToController: false,
             controllerAs: '$ctrl',
             controller: class {
               constructor($scope: angular.IScope) {
                 $scope['$onDestroy'] = scopeOnDestroy;
                 Object.getPrototypeOf($scope)['$onDestroy'] = scopeOnDestroy;
               }
             }
           };

           const ng1DirectiveB: angular.IDirective = {
             template: 'ng1B',
             scope: {},
             bindToController: true,
             controllerAs: '$ctrl',
             controller: class {
               constructor($scope: angular.IScope) {
                 $scope['$onDestroy'] = scopeOnDestroy;
                 Object.getPrototypeOf($scope)['$onDestroy'] = scopeOnDestroy;
               }
             }
           };

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1A'})
           class Ng1ComponentAFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1A', elementRef, injector);
             }
           }

           @Directive({selector: 'ng1B'})
           class Ng1ComponentBFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1B', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component(
               {selector: 'ng2', template: '<div *ngIf="show"><ng1A></ng1A> | <ng1B></ng1B></div>'})
           class Ng2Component {
             // TODO(issue/24571): remove '!'.
             @Input() show!: boolean;
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1Module', [])
                                 .directive('ng1A', () => ng1DirectiveA)
                                 .directive('ng1B', () => ng1DirectiveB)
                                 .directive('ng2', downgradeComponent({component: Ng2Component}));

           // Define `Ng2Module`
           @NgModule({
             declarations: [Ng1ComponentAFacade, Ng1ComponentBFacade, Ng2Component],
             entryComponents: [Ng2Component],
             imports: [BrowserModule, UpgradeModule]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html('<ng2 [show]="!destroyFromNg2" ng-if="!destroyFromNg1"></ng2>');

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => {
             const $rootScope = adapter.$injector.get('$rootScope') as angular.IRootScopeService;

             expect(multiTrim(document.body.textContent)).toBe('ng1A | ng1B');
             expect(scopeOnDestroy).not.toHaveBeenCalled();

             $rootScope.$apply('destroyFromNg1 = true');

             expect(multiTrim(document.body.textContent)).toBe('');
             expect(scopeOnDestroy).not.toHaveBeenCalled();

             $rootScope.$apply('destroyFromNg1 = false');

             expect(multiTrim(document.body.textContent)).toBe('ng1A | ng1B');
             expect(scopeOnDestroy).not.toHaveBeenCalled();

             $rootScope.$apply('destroyFromNg2 = true');

             expect(multiTrim(document.body.textContent)).toBe('');
             expect(scopeOnDestroy).not.toHaveBeenCalled();
           });
         }));

      it('should be called in order `$onChanges()` > `$onInit()` > `$doCheck()` > `$postLink()`',
         async(() => {
           // Define `ng1Component`
           const ng1Component: angular.IComponent = {
             // `$doCheck()` will keep getting called as long as the interpolated value keeps
             // changing (by appending `> $doCheck`). Only care about the first 4 values.
             template: '{{ $ctrl.calls.slice(0, 4).join(" > ") }}',
             bindings: {value: '<'},
             controller: class {
               calls: string[] = [];

               $onChanges() {
                 this.calls.push('$onChanges');
               }

               $onInit() {
                 this.calls.push('$onInit');
               }

               $doCheck() {
                 this.calls.push('$doCheck');
               }

               $postLink() {
                 this.calls.push('$postLink');
               }
             }
           };

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1'})
           class Ng1ComponentFacade extends UpgradeComponent {
             @Input() value: any;

             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({selector: 'ng2', template: '<ng1 value="foo"></ng1>'})
           class Ng2Component {
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1Module', [])
                                 .component('ng1', ng1Component)
                                 .directive('ng2', downgradeComponent({component: Ng2Component}));

           // Define `Ng2Module`
           @NgModule({
             declarations: [Ng1ComponentFacade, Ng2Component],
             entryComponents: [Ng2Component],
             imports: [BrowserModule, UpgradeModule]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2></ng2>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
             expect(multiTrim(element.textContent))
                 .toBe('$onChanges > $onInit > $doCheck > $postLink');
           });
         }));
    });

    describe('destroying the upgraded component', () => {
      it('should destroy `$componentScope`', async(() => {
           const scopeDestroyListener = jasmine.createSpy('scopeDestroyListener');
           let ng2ComponentAInstance: Ng2ComponentA;

           // Define `ng1Component`
           const ng1Component: angular.IComponent = {
             controller: class {
               constructor($scope: angular.IScope) {
                 $scope.$on('$destroy', scopeDestroyListener);
               }
             }
           };

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1'})
           class Ng1ComponentFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({selector: 'ng2A', template: '<ng2B *ngIf="!destroyIt"></ng2B>'})
           class Ng2ComponentA {
             destroyIt = false;

             constructor() {
               ng2ComponentAInstance = this;
             }
           }

           @Component({selector: 'ng2B', template: '<ng1></ng1>'})
           class Ng2ComponentB {
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1Module', [])
                                 .component('ng1', ng1Component)
                                 .directive('ng2A', downgradeComponent({component: Ng2ComponentA}));

           // Define `Ng2Module`
           @NgModule({
             declarations: [Ng1ComponentFacade, Ng2ComponentA, Ng2ComponentB],
             entryComponents: [Ng2ComponentA],
             imports: [BrowserModule, UpgradeModule]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2-a></ng2-a>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => {
             expect(scopeDestroyListener).not.toHaveBeenCalled();

             ng2ComponentAInstance.destroyIt = true;
             $digest(adapter);

             expect(scopeDestroyListener).toHaveBeenCalledTimes(1);
           });
         }));

      it('should emit `$destroy` on `$element` and descendants', async(() => {
           const elementDestroyListener = jasmine.createSpy('elementDestroyListener');
           const descendantDestroyListener = jasmine.createSpy('descendantDestroyListener');
           let ng2ComponentAInstance: Ng2ComponentA;

           // Define `ng1Component`
           const ng1Component: angular.IComponent = {
             controller: class {
               constructor($element: angular.IAugmentedJQuery) {
                 $element.on!('$destroy', elementDestroyListener);
                 $element.contents!().on!('$destroy', descendantDestroyListener);
               }
             },
             template: '<div></div>'
           };

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1'})
           class Ng1ComponentFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({selector: 'ng2A', template: '<ng2B *ngIf="!destroyIt"></ng2B>'})
           class Ng2ComponentA {
             destroyIt = false;

             constructor() {
               ng2ComponentAInstance = this;
             }
           }

           @Component({selector: 'ng2B', template: '<ng1></ng1>'})
           class Ng2ComponentB {
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1Module', [])
                                 .component('ng1', ng1Component)
                                 .directive('ng2A', downgradeComponent({component: Ng2ComponentA}));

           // Define `Ng2Module`
           @NgModule({
             declarations: [Ng1ComponentFacade, Ng2ComponentA, Ng2ComponentB],
             entryComponents: [Ng2ComponentA],
             imports: [BrowserModule, UpgradeModule]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2-a></ng2-a>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => {
             expect(elementDestroyListener).not.toHaveBeenCalled();
             expect(descendantDestroyListener).not.toHaveBeenCalled();

             ng2ComponentAInstance.destroyIt = true;
             $digest(adapter);

             expect(elementDestroyListener).toHaveBeenCalledTimes(1);
             expect(descendantDestroyListener).toHaveBeenCalledTimes(1);
           });
         }));

      it('should clear data on `$element` and descendants`', async(() => {
           let ng1ComponentElement: angular.IAugmentedJQuery;
           let ng2ComponentAInstance: Ng2ComponentA;

           // Define `ng1Component`
           const ng1Component: angular.IComponent = {
             controller: class {
               constructor($element: angular.IAugmentedJQuery) {
                 $element.data!('test', 1);
                 $element.contents!().data!('test', 2);

                 ng1ComponentElement = $element;
               }
             },
             template: '<div></div>'
           };

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1'})
           class Ng1ComponentFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({selector: 'ng2A', template: '<ng2B *ngIf="!destroyIt"></ng2B>'})
           class Ng2ComponentA {
             destroyIt = false;

             constructor() {
               ng2ComponentAInstance = this;
             }
           }

           @Component({selector: 'ng2B', template: '<ng1></ng1>'})
           class Ng2ComponentB {
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1Module', [])
                                 .component('ng1', ng1Component)
                                 .directive('ng2A', downgradeComponent({component: Ng2ComponentA}));

           // Define `Ng2Module`
           @NgModule({
             declarations: [Ng1ComponentFacade, Ng2ComponentA, Ng2ComponentB],
             entryComponents: [Ng2ComponentA],
             imports: [BrowserModule, UpgradeModule]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2-a></ng2-a>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => {
             expect(ng1ComponentElement.data!('test')).toBe(1);
             expect(ng1ComponentElement.contents!().data!('test')).toBe(2);

             ng2ComponentAInstance.destroyIt = true;
             $digest(adapter);

             expect(ng1ComponentElement.data!('test')).toBeUndefined();
             expect(ng1ComponentElement.contents!().data!('test')).toBeUndefined();
           });
         }));

      it('should clear dom listeners on `$element` and descendants`', async(() => {
           const elementClickListener = jasmine.createSpy('elementClickListener');
           const descendantClickListener = jasmine.createSpy('descendantClickListener');
           let ng1DescendantElement: angular.IAugmentedJQuery;
           let ng2ComponentAInstance: Ng2ComponentA;

           // Define `ng1Component`
           const ng1Component: angular.IComponent = {
             controller: class {
               constructor($element: angular.IAugmentedJQuery) {
                 ng1DescendantElement = $element.contents!();

                 $element.on!('click', elementClickListener);
                 ng1DescendantElement.on!('click', descendantClickListener);
               }
             },
             template: '<div></div>'
           };

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1'})
           class Ng1ComponentFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({selector: 'ng2A', template: '<ng2B *ngIf="!destroyIt"></ng2B>'})
           class Ng2ComponentA {
             destroyIt = false;

             constructor() {
               ng2ComponentAInstance = this;
             }
           }

           @Component({selector: 'ng2B', template: '<ng1></ng1>'})
           class Ng2ComponentB {
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1Module', [])
                                 .component('ng1', ng1Component)
                                 .directive('ng2A', downgradeComponent({component: Ng2ComponentA}));

           // Define `Ng2Module`
           @NgModule({
             declarations: [Ng1ComponentFacade, Ng2ComponentA, Ng2ComponentB],
             entryComponents: [Ng2ComponentA],
             imports: [BrowserModule, UpgradeModule]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2-a></ng2-a>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => {
             (ng1DescendantElement[0] as HTMLElement).click();
             expect(elementClickListener).toHaveBeenCalledTimes(1);
             expect(descendantClickListener).toHaveBeenCalledTimes(1);

             ng2ComponentAInstance.destroyIt = true;
             $digest(adapter);

             (ng1DescendantElement[0] as HTMLElement).click();
             expect(elementClickListener).toHaveBeenCalledTimes(1);
             expect(descendantClickListener).toHaveBeenCalledTimes(1);
           });
         }));

      it('should clean up `$doCheck()` watchers from the parent scope', async(() => {
           let ng2Component: Ng2Component;

           // Define `ng1Component`
           const ng1Component: angular.IComponent = {
             template: 'ng1',
             controller: class {
               $doCheck() {}
             }
           };

           // Define `Ng1ComponentFacade`
           @Directive({selector: 'ng1'})
           class Ng1ComponentFacade extends UpgradeComponent {
             constructor(elementRef: ElementRef, injector: Injector) {
               super('ng1', elementRef, injector);
             }
           }

           // Define `Ng2Component`
           @Component({selector: 'ng2', template: '<ng1 *ngIf="doShow"></ng1>'})
           class Ng2Component {
             doShow: boolean = false;
             constructor(@Inject($SCOPE) public $scope: angular.IScope) {
               ng2Component = this;
             }
           }

           // Define `ng1Module`
           const ng1Module = angular.module_('ng1Module', [])
                                 .component('ng1', ng1Component)
                                 .directive('ng2', downgradeComponent({component: Ng2Component}));

           // Define `Ng2Module`
           @NgModule({
             imports: [BrowserModule, UpgradeModule],
             declarations: [Ng1ComponentFacade, Ng2Component],
             entryComponents: [Ng2Component]
           })
           class Ng2Module {
             ngDoBootstrap() {}
           }

           // Bootstrap
           const element = html(`<ng2></ng2>`);

           bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => {
             const getWatcherCount: () => number = () =>
                 (ng2Component.$scope as any).$$watchers.length;
             const baseWatcherCount = getWatcherCount();

             expect(multiTrim(document.body.textContent)).toBe('');

             ng2Component.doShow = true;
             $digest(adapter);
             expect(multiTrim(document.body.textContent)).toBe('ng1');
             expect(getWatcherCount()).toBe(baseWatcherCount + 1);

             ng2Component.doShow = false;
             $digest(adapter);
             expect(multiTrim(document.body.textContent)).toBe('');
             expect(getWatcherCount()).toBe(baseWatcherCount);

             ng2Component.doShow = true;
             $digest(adapter);
             expect(multiTrim(document.body.textContent)).toBe('ng1');
             expect(getWatcherCount()).toBe(baseWatcherCount + 1);
           });
         }));
    });

    it('should support ng2 > ng1 > ng2 (no inputs/outputs)', async(() => {
         // Define `ng1Component`
         const ng1Component: angular.IComponent = {template: 'ng1X(<ng2-b></ng2-b>)'};

         // Define `Ng1ComponentFacade`
         @Directive({selector: 'ng1X'})
         class Ng1ComponentFacade extends UpgradeComponent {
           constructor(elementRef: ElementRef, injector: Injector) {
             super('ng1X', elementRef, injector);
           }
         }

         // Define `Ng2Component`
         @Component({selector: 'ng2-a', template: 'ng2A(<ng1X></ng1X>)'})
         class Ng2ComponentA {
         }

         @Component({selector: 'ng2-b', template: 'ng2B'})
         class Ng2ComponentB {
         }

         // Define `ng1Module`
         const ng1Module = angular.module_('ng1', [])
                               .component('ng1X', ng1Component)
                               .directive('ng2A', downgradeComponent({component: Ng2ComponentA}))
                               .directive('ng2B', downgradeComponent({component: Ng2ComponentB}));

         // Define `Ng2Module`
         @NgModule({
           declarations: [Ng1ComponentFacade, Ng2ComponentA, Ng2ComponentB],
           entryComponents: [Ng2ComponentA, Ng2ComponentB],
           imports: [BrowserModule, UpgradeModule],
           schemas: [NO_ERRORS_SCHEMA],
         })
         class Ng2Module {
           ngDoBootstrap() {}
         }

         // Bootstrap
         const element = html(`<ng2-a></ng2-a>`);

         bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
           expect(multiTrim(document.body.textContent)).toBe('ng2A(ng1X(ng2B))');
         });
       }));

    it('should support ng2 > ng1 > ng2 (with inputs/outputs)', fakeAsync(() => {
         let ng2ComponentAInstance: Ng2ComponentA;
         let ng2ComponentBInstance: Ng2ComponentB;
         let ng1ControllerXInstance: Ng1ControllerX;

         // Define `ng1Component`
         class Ng1ControllerX {
           // TODO(issue/24571): remove '!'.
           ng1XInputA!: string;
           ng1XInputB: any;
           ng1XInputC: any;

           constructor() {
             ng1ControllerXInstance = this;
           }
         }
         const ng1Component: angular.IComponent = {
           template: `
              ng1X({{ $ctrl.ng1XInputA }}, {{ $ctrl.ng1XInputB.value }}, {{ $ctrl.ng1XInputC.value }}) |
              <ng2-b
                [ng2-b-input1]="$ctrl.ng1XInputA"
                [ng2-b-input-c]="$ctrl.ng1XInputC.value"
                (ng2-b-output-c)="$ctrl.ng1XInputC = {value: $event}">
              </ng2-b>
            `,
           bindings: {
             ng1XInputA: '@',
             ng1XInputB: '<',
             ng1XInputC: '=',
             ng1XOutputA: '&',
             ng1XOutputB: '&'
           },
           controller: Ng1ControllerX
         };

         // Define `Ng1ComponentFacade`
         @Directive({selector: 'ng1X'})
         class Ng1ComponentXFacade extends UpgradeComponent {
           // TODO(issue/24571): remove '!'.
           @Input() ng1XInputA!: string;
           @Input() ng1XInputB: any;
           @Input() ng1XInputC: any;
           // TODO(issue/24571): remove '!'.
           @Output() ng1XInputCChange!: EventEmitter<any>;
           // TODO(issue/24571): remove '!'.
           @Output() ng1XOutputA!: EventEmitter<any>;
           // TODO(issue/24571): remove '!'.
           @Output() ng1XOutputB!: EventEmitter<any>;

           constructor(elementRef: ElementRef, injector: Injector) {
             super('ng1X', elementRef, injector);
           }
         }

         // Define `Ng2Component`
         @Component({
           selector: 'ng2-a',
           template: `
              ng2A({{ ng2ADataA.value }}, {{ ng2ADataB.value }}, {{ ng2ADataC.value }}) |
              <ng1X
                  ng1XInputA="{{ ng2ADataA.value }}"
                  bind-ng1XInputB="ng2ADataB"
                  [(ng1XInputC)]="ng2ADataC"
                  (ng1XOutputA)="ng2ADataA = $event"
                  on-ng1XOutputB="ng2ADataB.value = $event">
              </ng1X>
            `
         })
         class Ng2ComponentA {
           ng2ADataA = {value: 'foo'};
           ng2ADataB = {value: 'bar'};
           ng2ADataC = {value: 'baz'};

           constructor() {
             ng2ComponentAInstance = this;
           }
         }

         @Component({selector: 'ng2-b', template: 'ng2B({{ ng2BInputA }}, {{ ng2BInputC }})'})
         class Ng2ComponentB {
           @Input('ng2BInput1') ng2BInputA: any;
           @Input() ng2BInputC: any;
           @Output() ng2BOutputC = new EventEmitter();

           constructor() {
             ng2ComponentBInstance = this;
           }
         }

         // Define `ng1Module`
         const ng1Module = angular.module_('ng1', [])
                               .component('ng1X', ng1Component)
                               .directive('ng2A', downgradeComponent({component: Ng2ComponentA}))
                               .directive('ng2B', downgradeComponent({component: Ng2ComponentB}));

         // Define `Ng2Module`
         @NgModule({
           declarations: [Ng1ComponentXFacade, Ng2ComponentA, Ng2ComponentB],
           entryComponents: [Ng2ComponentA, Ng2ComponentB],
           imports: [BrowserModule, UpgradeModule],
           schemas: [NO_ERRORS_SCHEMA],
         })
         class Ng2Module {
           ngDoBootstrap() {}
         }

         // Bootstrap
         const element = html(`<ng2-a></ng2-a>`);

         bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(adapter => {
           // Initial value propagation.
           // (ng2A > ng1X > ng2B)
           expect(multiTrim(document.body.textContent))
               .toBe('ng2A(foo, bar, baz) | ng1X(foo, bar, baz) | ng2B(foo, baz)');

           // Update `ng2BInputA`/`ng2BInputC`.
           // (Should not propagate upwards.)
           ng2ComponentBInstance.ng2BInputA = 'foo2';
           ng2ComponentBInstance.ng2BInputC = 'baz2';
           $digest(adapter);
           tick();

           expect(multiTrim(document.body.textContent))
               .toBe('ng2A(foo, bar, baz) | ng1X(foo, bar, baz) | ng2B(foo2, baz2)');

           // Emit from `ng2BOutputC`.
           // (Should propagate all the way up to `ng1ADataC` and back all the way down to
           // `ng2BInputC`.)
           ng2ComponentBInstance.ng2BOutputC.emit('baz3');
           $digest(adapter);
           tick();

           expect(multiTrim(document.body.textContent))
               .toBe('ng2A(foo, bar, baz3) | ng1X(foo, bar, baz3) | ng2B(foo2, baz3)');

           // Update `ng1XInputA`/`ng1XInputB`.
           // (Should not propagate upwards, only downwards.)
           ng1ControllerXInstance.ng1XInputA = 'foo4';
           ng1ControllerXInstance.ng1XInputB = {value: 'bar4'};
           $digest(adapter);
           tick();

           expect(multiTrim(document.body.textContent))
               .toBe('ng2A(foo, bar, baz3) | ng1X(foo4, bar4, baz3) | ng2B(foo4, baz3)');

           // Update `ng1XInputC`.
           // (Should propagate upwards and downwards.)
           ng1ControllerXInstance.ng1XInputC = {value: 'baz5'};
           $digest(adapter);
           tick();

           expect(multiTrim(document.body.textContent))
               .toBe('ng2A(foo, bar, baz5) | ng1X(foo4, bar4, baz5) | ng2B(foo4, baz5)');

           // Update a property on `ng1XInputC`.
           // (Should propagate upwards and downwards.)
           ng1ControllerXInstance.ng1XInputC.value = 'baz6';
           $digest(adapter);
           tick();

           expect(multiTrim(document.body.textContent))
               .toBe('ng2A(foo, bar, baz6) | ng1X(foo4, bar4, baz6) | ng2B(foo4, baz6)');

           // Emit from `ng1XOutputA`.
           // (Should propagate upwards to `ng1ADataA` and back all the way down to
           // `ng2BInputA`.)
           (ng1ControllerXInstance as any).ng1XOutputA({value: 'foo7'});
           $digest(adapter);
           tick();

           expect(multiTrim(document.body.textContent))
               .toBe('ng2A(foo7, bar, baz6) | ng1X(foo7, bar4, baz6) | ng2B(foo7, baz6)');

           // Emit from `ng1XOutputB`.
           // (Should propagate upwards to `ng1ADataB`, but not downwards,
           //  since `ng1XInputB` has been re-assigned (i.e. `ng2ADataB !== ng1XInputB`).)
           (ng1ControllerXInstance as any).ng1XOutputB('bar8');
           $digest(adapter);
           tick();

           expect(multiTrim(document.body.textContent))
               .toBe('ng2A(foo7, bar8, baz6) | ng1X(foo7, bar4, baz6) | ng2B(foo7, baz6)');

           // Update `ng2ADataA`/`ng2ADataB`/`ng2ADataC`.
           // (Should propagate everywhere.)
           ng2ComponentAInstance.ng2ADataA = {value: 'foo9'};
           ng2ComponentAInstance.ng2ADataB = {value: 'bar9'};
           ng2ComponentAInstance.ng2ADataC = {value: 'baz9'};
           $digest(adapter);
           tick();

           expect(multiTrim(document.body.textContent))
               .toBe('ng2A(foo9, bar9, baz9) | ng1X(foo9, bar9, baz9) | ng2B(foo9, baz9)');
         });
       }));

    it('should support ng2 > ng1 > ng2 > ng1 (with `require`)', async(() => {
         // Define `ng1Component`
         const ng1ComponentA: angular.IComponent = {
           template: 'ng1A(<ng2-b></ng2-b>)',
           controller: class {
             value = 'ng1A';
           }
         };

         const ng1ComponentB: angular.IComponent = {
           template:
               'ng1B(^^ng1A: {{ $ctrl.ng1A.value }} | ?^^ng1B: {{ $ctrl.ng1B.value }} | ^ng1B: {{ $ctrl.ng1BSelf.value }})',
           require: {ng1A: '^^', ng1B: '?^^', ng1BSelf: '^ng1B'},
           controller: class {
             value = 'ng1B';
           }
         };

         // Define `Ng1ComponentFacade`
         @Directive({selector: 'ng1A'})
         class Ng1ComponentAFacade extends UpgradeComponent {
           constructor(elementRef: ElementRef, injector: Injector) {
             super('ng1A', elementRef, injector);
           }
         }

         @Directive({selector: 'ng1B'})
         class Ng1ComponentBFacade extends UpgradeComponent {
           constructor(elementRef: ElementRef, injector: Injector) {
             super('ng1B', elementRef, injector);
           }
         }

         // Define `Ng2Component`
         @Component({selector: 'ng2-a', template: 'ng2A(<ng1A></ng1A>)'})
         class Ng2ComponentA {
         }

         @Component({selector: 'ng2-b', template: 'ng2B(<ng1B></ng1B>)'})
         class Ng2ComponentB {
         }

         // Define `ng1Module`
         const ng1Module = angular.module_('ng1', [])
                               .component('ng1A', ng1ComponentA)
                               .component('ng1B', ng1ComponentB)
                               .directive('ng2A', downgradeComponent({component: Ng2ComponentA}))
                               .directive('ng2B', downgradeComponent({component: Ng2ComponentB}));

         // Define `Ng2Module`
         @NgModule({
           declarations: [Ng1ComponentAFacade, Ng1ComponentBFacade, Ng2ComponentA, Ng2ComponentB],
           entryComponents: [Ng2ComponentA, Ng2ComponentB],
           imports: [BrowserModule, UpgradeModule],
           schemas: [NO_ERRORS_SCHEMA],
         })
         class Ng2Module {
           ngDoBootstrap() {}
         }

         // Bootstrap
         const element = html(`<ng2-a></ng2-a>`);

         bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(() => {
           expect(multiTrim(document.body.textContent))
               .toBe('ng2A(ng1A(ng2B(ng1B(^^ng1A: ng1A | ?^^ng1B: | ^ng1B: ng1B))))');
         });
       }));
  });
});
