Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular 2: "Unknown Provider: $$angularInjectorProvider" when unit testing with an `UpgradeModule` downgraded service

Tags:

angularjs

Our app currently has some 4,500+ existing Angular 1 tests. We're now incorporating Angular 2 using UpgradeModule to downgrade an Angular 2 service, which is used from our app's run() method. This causes all tests to fail with a Error: [$injector:unpr] Unknown Provider $$angularInjectorProvider error.

I created a plnkr to demonstrate this issue (http://plnkr.co/edit/XuMYBr9xInqq18Kr6EAs?p=preview).

Basically the Angular 1 module looks like this:

// angular1.module.ts

angular.module( 'testApp' )
    .run( [ 'Angular2Service', Angular2Service => {  // injecting downgraded service
        Angular2Service.showTestMessage();
    } ] );

Where Angular2Service is downgraded with:

// app.module.ts

angular.module( 'testApp' )
    .factory( 'Angular2Service', downgradeInjectable( Angular2Service ) );


@NgModule({
    imports: [ BrowserModule, UpgradeModule ],
    providers : [ Angular2Service ]
})
export class AppModule {
    ngDoBootstrap() {}
}

Originally with this so far, I was getting an Unknown Provider error for 'Angular2Service' itself when the tests executed, so I added a beforeEach() that compiles everything in AppModule:

beforeEach( async( () => {
    TestBed.configureTestingModule( {
        imports : [ AppModule ]
    } )
        .compileComponents();
} ) );

And that's where I'm at with the Error: [$injector:unpr] Unknown Provider $$angularInjectorProvider error.

The test itself that is executing is purely an Angular 1 test, which passes if I don't inject Angular2Service into the run() method:

describe( 'testComponent', () => {
    var $compile,
        $scope;

    beforeEach( module( 'testApp' ) );

    beforeEach( inject( $injector => {
        $compile = $injector.get( '$compile' );
        $scope = $injector.get( '$rootScope' ).$new();
    } ) );


    it( 'should show the text "Test Component" in the DOM', () => {
        var element = $compile( '<test-component></test-component>' )( $scope );
        $scope.$apply();

        expect( element[ 0 ].innerHTML ).toBe( 'Test Component' );
    } );

} );

I'm hoping that this can be fixed so that all of our pure Angular 1 tests can continue to work unchanged, but I can update them too if need be. Any help on this would be greatly appreciated!

http://plnkr.co/edit/XuMYBr9xInqq18Kr6EAs?p=preview

like image 526
Greg Jacobs Avatar asked Dec 13 '16 18:12

Greg Jacobs


2 Answers

Another option is to just mock out the service being downgraded. When you have in your angular part something like this:

angular.module('ng1module')
  ).factory('service', downgradeInjectable(MyService) as any);

then you can mock the service with the $provide-syntax as follows:

module(function ($provide) {
        $provide.value('service',{});
})
like image 55
user656449 Avatar answered Oct 27 '22 06:10

user656449


I faced the same problem and came with a solution. It is not the best to be done, but it just solves the problem in the migration phase.

I'm sure that you use upgrade-static.umd.js dist file rather than upgrade.umd.js. The static one is intended to be used with ngc AoT compilation which we should use in production apps. If you dig into its code you can easily figure it defines new angularjs module named $$UpgradeModule which registers a value provider named $$angularInjector (the one in the error above) - this $$angularInjector thing is responsible for injecting Angular modules into angularjs.

The problem

Your old angularjs test don't initiate $$UpgradeModule so its providers are not injectable. Note that angular-mocks (which defines both global module and inject fns) do some patches for jasmine and mocka to provide some injection isolation between suites.

The solution

You simply need to call module('$$UpgradeModule') in the first beforeEach every time you will test a code that will inject (directly or indirectly) the downgraded provider.

However, this is not that simple :). Actually $$UpgradeModule is declared in UpgradeModule.boorstrap function. which means you need to bootstrap the whole app (both angularjs and Angular) before any test (or in more strict form - bootstrap the root module). This is not nice because we want to test modules and providers in complete isolation.

Another solution

An alternative may be to use the UpgradeAdapter defined in upgrade.umd.js. Honestly, I didn't try it - but from the code it seems it can initiate just the modules that we want without bootstrapping the whole app. But I think we may need to redo downgrades in tests using UpgradeAdapter methods to have things work.


EDIT

Example:

To bootstrap, you need to import your app module is SystemJs before importing test files. So ur karma-stest-shim.js will contain something like that

System.import('systemjs.config.js')
  .then(importSystemJsExtras)
  .then(importApp)
  .then(initTestBed)
  .then(initTesting);

function importApp() {
    return Promise.all(
        [
            System.import('app'),
        ]
    )
}

This will do the normal bootstrap for the both versions and make $$upgradeModule defined.

Then in any test that will inject the downgraded provider

beforeEach(function () {
    // the ng1 module for the downgraded provider 
    module('services');
    // the trick is here
    module('$$UpgradeModule');
});
like image 27
Yasser Rabee Avatar answered Oct 27 '22 05:10

Yasser Rabee