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:
""
source to share
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
myDirective
√ should 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!');
});
});
source to share
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'
source to share