Unit testing AngularJS Directives with Jest
Asked Answered
I

2

7

I feel I am missing something crucial in this extremely simplified angular directive unit test:

import * as angular from 'angular'
import 'angular-mocks'

const app = angular.module('my-app', [])

app.directive('myDirective', () => ({
    template: 'this does not work either',
    link: (scope, element) => { // have also tried compile fn
        console.log('This does not log')
        element.html('Hi!')
    }
}))

describe('myDirective', () => {
    var element, scope

    beforeEach(app)

    beforeEach(inject(($rootScope, $compile) => {
        scope = $rootScope.$new()
        element = $compile('<my-directive />')(scope)
        scope.$digest()
    }))

    it('should actually do something', () => {
        expect(element.html()).toEqual('Hi!')
    })
})

When jest runs it appears the directive has not been linked/compiled/whatever

 FAIL  test/HtmlToPlaintextDirective.spec.js
  ● myDirective › should actually do something

    expect(received).toEqual(expected)

    Expected value to equal:
      "Hi!"
    Received:
      ""
Icily answered 30/3, 2017 at 6:56 Comment(0)
P
12

Updated answer:

You're right things don't work as expected when importing everything in a single file.

Digging into things it looks like you're running into some magic that Babel/Jest does to support browser scripts that rely on globals (like AngularJS).

What's happening is that your module's angular variable is not the same as the global angular variable that is visible to angular-mocks.

You can check this by running this at the top of one of your tests:

import * as angular from 'angular'
import 'angular-mocks'

console.log(angular === window.angular); // `false` in Jest!

console.log(angular.mock); // undefined
console.log(window.angular.mock); // `{...}` defined

To work around this you just need to use the global angular variable in your tests.

src/__test__/all-in-one.test.js:

import "angular";
import "angular-mocks";

/*
Work around Jest's window/global mock magic.

Use the global version of `angular` that has been augmented by angular-mocks.
*/
var angular = window.angular;


export var app = angular.module('app', []);

app.directive('myDirective', () => ({
    link: (scope, element) => {
        console.log('This does log');
        scope.content = 'Hi!';
    },
    template: 'content: {{content}}'
}));


describe('myDirective', function(){
    var element;
    var scope;

    beforeEach(function(){
        angular.mock.module(app.name);
    });

    it('should do something', function(){
        inject(function(
            $rootScope,
            $compile
        ){
            scope = $rootScope.$new();
            element = $compile('<my-directive></my-directive>')(scope);
            scope.$digest();
        });

        expect(element.html()).toEqual('content: Hi!');
    });
});

Original answer: (This worked because I was accidentally using the global version of angular inside my test.)

The Angular module under test isn't being initialised correctly in your tests.

Your call to beforeEach(app) isn't correct.

Instead you need to use angular.mock.module("moduleName") to initialise your module.

describe('myDirective', () => {
    var element, scope

    // You need to pass the module name to `angular.mock.module()`
    beforeEach(function(){
        angular.mock.module(app.name);
    });


    // Then you can set up and run your tests as normal:
    beforeEach(inject(($rootScope, $compile) => {
        scope = $rootScope.$new()
        element = $compile('<my-directive></my-directive>')(scope)
        scope.$digest()
    }))

    it('should actually do something', () => {
        expect(element.html()).toEqual('Hi!')
    })
});

And then your test works as expected for me:

 PASS  src\__test__\app.test.js
  myDirective
    √ should do something (46ms)

For reference, here is the full app and test:

src/app/app.module.js:

import * as angular from 'angular'

export var app = angular.module('app', []);

app.directive('myDirective', () => ({
    link: (scope, element) => {
        console.log('This does log');
        scope.content = 'Hi!';
    },
    template: 'content: {{content}}'
}))

src/__test__/app.test.js:

import {app} from "../app/app.module";
import "angular-mocks";

describe('myDirective', function(){
    var element;
    var scope;

    beforeEach(function(){
        angular.mock.module(app.name);
    });

    beforeEach(inject(function(
        $rootScope,
        $compile
    ){
        scope = $rootScope.$new();
        element = $compile('<my-directive></my-directive>')(scope);
        scope.$digest();
    }));

    it('should do something', function(){
        expect(element.html()).toEqual('content: Hi!');
    });
});
Provinciality answered 30/3, 2017 at 7:45 Comment(5)
Thank you so much for your response. Could you please tell me what versions you are running? As I copy-pasted your code exactly and still had my test fail as element.html() === ''. I am using angular: "1.6.3", angular-mocks: "1.6.3", jest: "18.1.0". Edit: upgraded jest to 19.0.2 and same issue. My only difference from your code is merging the two files (removing export+import)Icily
I've updated my answer - you were right about it not working when everything was all in one file.Provinciality
Can anyone show me how to configure jest with angular 1.6? I can't make it working, it seems you get this and started to write tests. Please provide any example repository linkBoggart
@Provinciality could you shed some light how to inject dependencies of module under testing to test file? I am facing the same issue as @captainclam and getting issues with module that uses angular ngSanitize dependency moduleMenken
@Menken probably best to ask a new question with specifics on any errors you're seeing and which dependencies you need to inject/mock. Ping me a link to the question here and I'll be happy to have a look.Provinciality
S
0

I ran into the same baffling behavior a couple years later and I wanted to share what I found

If you transpile the test using babel and look at the imports you will find something similar to the following

var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard");
var angular = _interopRequireWildcard(require("angular"));
require("angular-mocks");

_interopRequireWildcard currently has the following implementation

function _interopRequireWildcard(obj) {
  if (obj && obj.__esModule) {
    return obj;
  } else {
    var newObj = {};

    if (obj != null) {
      for (var key in obj) {
        if (Object.prototype.hasOwnProperty.call(obj, key)) {
          var desc = Object.defineProperty && Object.getOwnPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : {};

          if (desc.get || desc.set) {
            Object.defineProperty(newObj, key, desc);
          } else {
            newObj[key] = obj[key];
          }
        }
      }
    }

    newObj.default = obj;
    return newObj;
  }
}

In short it creates a new object and copies all the properties from the imported object. This is why angular === window.angular is false. It also explains why angular.mock isn't defined, it didn't exist when _interopRequireWildcard made a copy of the module

Given that there are a couple additional ways to solve the problem in addition to the accepted answer

Instead of using import * as angular from 'angular' using import angular from 'angular' should avoid the behavior because _interopRequireDefault does not return a different object. (However, if you are using TypeScript it may not resolve the types for 'angular' correctly with this method)

Another option would be to import angular twice:

import 'angular'
import 'angular-mocks'
import * as angular from 'angular'
Stelly answered 7/5, 2019 at 16:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.