createAngularJSTestingModule

function

A helper function to use when unit testing AngularJS services that depend upon downgraded Angular services.

See more...

createAngularJSTestingModule(angularModules: any[]): string

Parameters
angularModules any[]

a collection of Angular modules to include in the configuration.

Returns

string

Description

This function returns an AngularJS module that is configured to wire up the AngularJS and Angular injectors without the need to actually bootstrap a hybrid application. This makes it simpler and faster to unit test services.

Use the returned AngularJS module in a call to angular.mocks.module to include this module in the unit test injector.

In the following code snippet, we are configuring the $injector with two modules: The AngularJS ng1AppModule, which is the AngularJS part of our hybrid application and the Ng2AppModule, which is the Angular part.

beforeEach(module(createAngularJSTestingModule([Ng2AppModule])));
beforeEach(module(ng1AppModule.name));

Once this is done we can get hold of services via the AngularJS $injector as normal. Services that are (or have dependencies on) a downgraded Angular service, will be instantiated as needed by the Angular root Injector.

In the following code snippet, heroesService is a downgraded Angular service that we are accessing from AngularJS.

it('should have access to the HeroesService', inject((heroesService: HeroesService) => {
     expect(heroesService).toBeDefined();
   }));

This helper is for testing services not components. For Component testing you must still bootstrap a hybrid app. See UpgradeModule or downgradeModule for more information.

The resulting configuration does not wire up AngularJS digests to Zone hooks. It is the responsibility of the test writer to call $rootScope.$apply, as necessary, to trigger AngularJS handlers of async events from Angular.

The helper sets up global variables to hold the shared Angular and AngularJS injectors.

Here is the example application and its unit tests that use createAngularTestingModule and createAngularJSTestingModule.

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

import {TestBed} from '@angular/core/testing';
import {createAngularJSTestingModule, createAngularTestingModule} from '@angular/upgrade/static/testing';

import {HeroesService, ng1AppModule, Ng2AppModule} from './module';

const {module, inject} = (window as any).angular.mock;

describe('HeroesService (from Angular)', () => {
  beforeEach(() => {
    TestBed.configureTestingModule(
        {imports: [createAngularTestingModule([ng1AppModule.name]), Ng2AppModule]});
  });

  it('should have access to the HeroesService', () => {
    const heroesService = TestBed.inject(HeroesService);
    expect(heroesService).toBeDefined();
  });
});


describe('HeroesService (from AngularJS)', () => {
  beforeEach(module(createAngularJSTestingModule([Ng2AppModule])));
  beforeEach(module(ng1AppModule.name));

  it('should have access to the HeroesService', inject((heroesService: HeroesService) => {
       expect(heroesService).toBeDefined();
     }));
});
/**
 * @license
 * Copyright Google Inc. All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.io/license
 */
import {Component, Directive, ElementRef, EventEmitter, Injectable, Injector, Input, NgModule, Output} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {downgradeComponent, downgradeInjectable, UpgradeComponent, UpgradeModule} from '@angular/upgrade/static';

declare var angular: ng.IAngularStatic;

export interface Hero {
  name: string;
  description: string;
}

export class TextFormatter {
  titleCase(value: string) {
    return value.replace(/((^|\s)[a-z])/g, (_, c) => c.toUpperCase());
  }
}

// This Angular component will be "downgraded" to be used in AngularJS
@Component({
  selector: 'ng2-heroes',
  // This template uses the upgraded `ng1-hero` component
  // Note that because its element is compiled by Angular we must use camelCased attribute names
  template: `<header><ng-content selector="h1"></ng-content></header>
             <ng-content selector=".extra"></ng-content>
             <div *ngFor="let hero of heroes">
               <ng1-hero [hero]="hero" (onRemove)="removeHero.emit(hero)"><strong>Super Hero</strong></ng1-hero>
             </div>
             <button (click)="addHero.emit()">Add Hero</button>`,
})
export class Ng2HeroesComponent {
  @Input() heroes!: Hero[];
  @Output() addHero = new EventEmitter();
  @Output() removeHero = new EventEmitter();
}

// This Angular service will be "downgraded" to be used in AngularJS
@Injectable()
export class HeroesService {
  heroes: Hero[] = [
    {name: 'superman', description: 'The man of steel'},
    {name: 'wonder woman', description: 'Princess of the Amazons'},
    {name: 'thor', description: 'The hammer-wielding god'}
  ];

  constructor(textFormatter: TextFormatter) {
    // Change all the hero names to title case, using the "upgraded" AngularJS service
    this.heroes.forEach((hero: Hero) => hero.name = textFormatter.titleCase(hero.name));
  }

  addHero() {
    this.heroes =
        this.heroes.concat([{name: 'Kamala Khan', description: 'Epic shape-shifting healer'}]);
  }

  removeHero(hero: Hero) {
    this.heroes = this.heroes.filter((item: Hero) => item !== hero);
  }
}

// This Angular directive will act as an interface to the "upgraded" AngularJS component
@Directive({selector: 'ng1-hero'})
export class Ng1HeroComponentWrapper extends UpgradeComponent {
  // The names of the input and output properties here must match the names of the
  // `<` and `&` bindings in the AngularJS component that is being wrapped
  @Input() hero!: Hero;
  @Output() onRemove!: EventEmitter<void>;

  constructor(elementRef: ElementRef, injector: Injector) {
    // We must pass the name of the directive as used by AngularJS to the super
    super('ng1Hero', elementRef, injector);
  }
}

// This NgModule represents the Angular pieces of the application
@NgModule({
  declarations: [Ng2HeroesComponent, Ng1HeroComponentWrapper],
  providers: [
    HeroesService,
    // Register an Angular provider whose value is the "upgraded" AngularJS service
    {provide: TextFormatter, useFactory: (i: any) => i.get('textFormatter'), deps: ['$injector']}
  ],
  // All components that are to be "downgraded" must be declared as `entryComponents`
  entryComponents: [Ng2HeroesComponent],
  // We must import `UpgradeModule` to get access to the AngularJS core services
  imports: [BrowserModule, UpgradeModule]
})
export class Ng2AppModule {
  constructor(private upgrade: UpgradeModule) {}

  ngDoBootstrap() {
    // We bootstrap the AngularJS app.
    this.upgrade.bootstrap(document.body, [ng1AppModule.name]);
  }
}


// This Angular 1 module represents the AngularJS pieces of the application
export const ng1AppModule: ng.IModule = angular.module('ng1AppModule', []);

// This AngularJS component will be "upgraded" to be used in Angular
ng1AppModule.component('ng1Hero', {
  bindings: {hero: '<', onRemove: '&'},
  transclude: true,
  template: `<div class="title" ng-transclude></div>
             <h2>{{ $ctrl.hero.name }}</h2>
             <p>{{ $ctrl.hero.description }}</p>
             <button ng-click="$ctrl.onRemove()">Remove</button>`
});

// This AngularJS service will be "upgraded" to be used in Angular
ng1AppModule.service('textFormatter', [TextFormatter]);

// Register an AngularJS service, whose value is the "downgraded" Angular injectable.
ng1AppModule.factory('heroesService', downgradeInjectable(HeroesService) as any);

// This directive will act as the interface to the "downgraded" Angular component
ng1AppModule.directive('ng2Heroes', downgradeComponent({component: Ng2HeroesComponent}));

// This is our top level application component
ng1AppModule.component('exampleApp', {
  // We inject the "downgraded" HeroesService into this AngularJS component
  // (We don't need the `HeroesService` type for AngularJS DI - it just helps with TypeScript
  // compilation)
  controller: [
    'heroesService',
    function(heroesService: HeroesService) {
      this.heroesService = heroesService;
    }
  ],
  // This template makes use of the downgraded `ng2-heroes` component
  // Note that because its element is compiled by AngularJS we must use kebab-case attributes
  // for inputs and outputs
  template: `<link rel="stylesheet" href="./styles.css">
          <ng2-heroes [heroes]="$ctrl.heroesService.heroes" (add-hero)="$ctrl.heroesService.addHero()" (remove-hero)="$ctrl.heroesService.removeHero($event)">
            <h1>Heroes</h1>
            <p class="extra">There are {{ $ctrl.heroesService.heroes.length }} heroes.</p>
          </ng2-heroes>`
});


// We bootstrap the Angular module as we would do in a normal Angular app.
// (We are using the dynamic browser platform as this example has not been compiled AOT.)
platformBrowserDynamic().bootstrapModule(Ng2AppModule);

© 2010–2020 Google, Inc.
Licensed under the Creative Commons Attribution License 4.0.
https://v9.angular.io/api/upgrade/static/testing/createAngularJSTestingModule