Unit Testing Angular Directives with Jest

I feel like I'm missing something important in this extremely simplified angular unit test directive:

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 starts, the directive was not linked / compiled / nothing

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

    expect(received).toEqual(expected)

    Expected value to equal:
      "Hi!"
    Received:
      ""

      

+4


source to share


2 answers


Updated answer:

You're right, things don't work as expected when importing just one file.

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

What is happening is that your module variable is angular

not the same as a global variable angular

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 get around this, you just need to use a global variable angular

in your tests.

Csi / __ test __ / all-in-one.test.js

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

/*
Work around Jest 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: (It worked because I accidentally used the global version angular

inside my test.)

Angular module under test is not initializing correctly in your tests.



Your call is beforeEach(app)

wrong.

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

to initialize 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
  myDirectiveshould do something (46ms)

      


For reference, here's the complete app and test:

Csi / application / 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}}'
}))

      

Csi / __ 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!');
    });
});

      

+9


source


A few years later I encountered the same strange behavior and wanted to share what I found.

If you migrate the test with 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 properties from the imported object. That's why angular === window.angular

it matters false

. This also explains why it was angular.mock

not defined, it was not there when I _interopRequireWildcard

made a copy of the module.

Considering there are a couple of additional ways to solve the problem in addition to the accepted answer

Instead of using import * as angular from 'angular'

when using import angular from 'angular'

, this behavior should be avoided as it _interopRequireDefault

does not return another object. (However, if you are using TypeScript, it may not allow types for angular with this method)

Another option is to import angular twice:

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

      

0


source







All Articles