The problem
I am trying to test some directives (code for both below). One of them is an "email" (called "epost" in the code(norwegian)) directive. The solution to this should work for all of them, so I am keeping it to this one for now.
Technologies: Angularjs, Jasmine, Requirejs, (grunt & karma running in Chrome)
The directive validates email addresses in two ways; on upshift and on blur. I can test the upshift without problems as you can see in the test below, but I can't figure out how to simulate a blur so the bind('blur') in the directive runs.
What I have done
I have tried to catch the compiled element like this:
elem = angular.element(html);
element = $compile(elem)($scope);
And then in the test i tried several permutations to trigger the blur with a console log just inside the bind function in the directive. None of the below works. It does not trigger.
elem.trigger('blur');
element.trigger('blur');
elem.triggerHandler('blur');
element.triggerHandler('blur');
element.blur();
elem.blur();
I based the injection and setup on this: To test a custom validation angularjs directive
The email directive in angularjs wrapped in requirejs
define(function() {
var Directive = function() {
return {
require: 'ngModel',
link: function(scope, elem, attrs, ctrl) {
var pattern = /^[A-Za-z0-9._%+-]+#[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/;
elem.bind('blur', function() {
scope.$apply(function () {
if (!elem.val() || pattern.test(elem.val())) {
ctrl.$setValidity('epost', true);
} else {
ctrl.$setValidity('epost', false);
}
});
});
ctrl.$parsers.unshift(function(viewValue) {
if (pattern.test(viewValue)) {
ctrl.$setValidity('epost', true);
return viewValue;
} else {
return undefined;
}
});
}
};
};
return Directive;
});
The test (using jasmine and requirejs)
define([
'Angular',
'AngularMocks',
], function () {
describe('Directives', function () {
var $scope;
var form;
beforeEach(module('common'));
beforeEach(function () {
var html = '<form name="form">';
html += '<input type="text" id="epost" name="epost" epost="" ng-model="model.epost"/>';
html += '</form>';
inject(function ($compile, $rootScope) {
$scope = $rootScope.$new();
$scope.model = {
epost: null
};
// Compile the element, run digest cycle
var elem = angular.element(html);
$compile(elem)($scope);
$scope.$digest();
form = $scope.form;
});
});
describe('(epost) Given an input field hooked up with the email directive', function () {
var validEmail = 'a#b.no';
var invalidEmail = 'asdf#asdf';
it('should bind data to model and be valid when email is valid on upshift', function () {
form.epost.$setViewValue(validEmail);
expect($scope.model.epost).toBe(validEmail);
expect(form.epost.$valid).toBe(true);
});
});
});
});
I have been able to figure out where I went wrong after some breakpoint debugging.
The "element" item I get out using the approach described in the top of the question is not actually the directive it self. It's an object which wraps the form and the directive.
Like this
{ 0: // The form
{ 0: // The directive (input element)
{
}
}
}
To actually simulate a blur on the directive it self, I did something like this
var directiveElement = $(element[0][0]);
directiveElement.blur();
After getting the element I wanted, and wrapping it in a jQuery object (may be optional), it worked like a charm. I then used the approach like in the test in the question with $setViewValue and checked the model value like this.
form.epost.$setViewValue('a#b.no');
directiveElement.blur();
expect($scope.model.epost).toBe('a#b.no');
expect($scope.form.epost.$valid).toBeTruthy();
Hope this could be of help to others trying to figure the directive testing out.
I too ran into a similar problem and it mystified me. My solution was to use JQuery to get the input and then use angular.element(input).triggerHandler('blur') to make it work. This is odd to me because I do not have to do this with the click event.
spyOn(controller, 'setRevenueIsInvalid');
var sugarRow = $(element).find('tr#ve_id_5')[0];
var amount = $(sugarRow).find('input.amount')[0];
angular.element(amount).triggerHandler('blur');
expect(controller.setRevenueIsInvalid).toHaveBeenCalled();
Related
I am working with "Smart Table" and will be using their example plugin where a checkbox selects a row in a table: http://lorenzofox3.github.io/smart-table-website/#section-custom
I am writing a unit test for this directive, code below, this is failing. Has anyone written a unit test for this code or could help direct me as to where I am going wrong and if I am actually testing the correct logic?
Directive:
myApp.directive('csSelect', function () {
return {
require: '^stTable',
template: '',
scope: {
row: '=csSelect'
},
link: function (scope, element, attr, ctrl) {
element.bind('change', function (evt) {
scope.$apply(function () {
ctrl.select(scope.row, 'multiple');
});
});
scope.$watch('row.isSelected', function (newValue, oldValue) {
if (newValue === true) {
element.parent().addClass('st-selected');
} else {
element.parent().removeClass('st-selected');
}
});
}
};
});
Unit test:
describe('csSelect',function(){
var scope, element, attr, ctrl;
beforeEach(module('myApp.selectorresult'));
beforeEach(inject(function($rootScope, $compile) {
elm = angular.element(
'<td cs-select="row" class="ng-isolate-scope">' +
'<input type="checkbox">' +
'</td>');
scope = $rootScope;
$compile(elm)(scope);
scope.$digest();
}));
it('should create selectable input',function(){
console.log(elm.find('input'));
var checkbox = elm.find('input');
expect(checkbox.length).toBe(1);
});
});
You need to mock out the stTableController with $controllerProvider before you set up beforeEach(inject...
Check out the test spec for the pagination directive (https://github.com/lorenzofox3/Smart-Table/blob/master/test/spec/stPagination.spec.js), which also requires 'stTable'. It's a good example of how to provide the 'stTableController' with the functions you need from it.
For anyone still having this issue. I hope this helps.
I was struggling with this for ages. I tried mocking the stTableController, I tried adding the vendor files to the karma.conf.js files among other things but could not get any tests to pass.
It seemed that when I removed the require: '^stTable' the tests would pass no problem, but with it in, all tests would fail. I couldn't remove this as this would break my code.
So what I finally found was that all I had to do was add st-table to my element in the spec.js file.
So if my element was
var element = angular.element('<my-component></my-component');
I had to make it
var element = angular.element('<my-component st-table></my-component>');
After that, all tests were passing.
I'm working on an app where text conditionally appears in a ::before pseudo element's content property and is rendered on the page. After a code change caused this important text to accidentally disappear, I wanted to be able to write tests that would capture that error if it happened again, but there are challenges grabbing the content from pseudo-selectors. I was looking for something like:
#scss
.content-div {
&.condition-true {
&:before {
content: "conditional text";
}
}
}
#coffeescript
if #someCondition
$('content-div').addClass('condition-true')
else
$('content-div').removeClass('condition-true')
#spec
context "when true" do
it "should have the conditional text" do
# do thing that makes it true
expect( page ).to have_content("conditional text")
end
end
The solution wasn't so easy, and I thought I'd share here and let others comment, or provide other solutions.
I'm using Capybara 2.3.0 and Poltergeist 1.5.1.
The key was passing a block of code to page.evaluate_script, as well as Javascript's getComputedStyle() function.
content_array = page.evaluate_script <<-SCRIPT.strip.gsub(/\s+/,' ')
(function () {
var elementArray = $('.desired-css-selector');
var contentArray = [];
for (var i = 0, tot=elementArray.length; i < tot; i++) {
var content = window.getComputedStyle( elementArray[i], ':before' ).getPropertyValue('content');
contentArray.push(content);
}
return contentArray;
})()
SCRIPT
content_array.each { |c| c.gsub!(/\A'|'\Z/, '') }
expect( content_array ).to include("conditional text")
UPDATE - SIMPLE EXAMPLE:
I've recently had to do a much simpler version of this:
color = page.evaluate_script <<-SCRIPT
(function () {
var element = document.getElementById('hoverme');
var color = window.getComputedStyle( element, ':hover' ).getPropertyValue('color');
return color;
})()
SCRIPT
I'm trying to setup Karma to test an angular app that I'm building. I've already setup testing around my services and controllers but am finding directives somewhat more complicated to test.
The directive:
angular.module('Directives',[])
.directive('tooltip', function factory(){
return {
link: {
post: function(scope, $element, instanceAttrs, controller){
$element.parent().append('<span class="tooltip-wrapper"><aside class="tooltip-container"></aside></span>');
var $tooltipWrapper = $element.next()
, $tooltip = angular.element($tooltipWrapper.children()[0]);
if(instanceAttrs.icon){
$tooltipWrapper.addClass('help icon standalone');
$element = $tooltipWrapper;
}
if(instanceAttrs.position){
$tooltip.addClass('tooltip-position-' + instanceAttrs.position);
} else {
$tooltip.addClass('tooltip-position-bottom-center');
}
if(instanceAttrs.position === 'right'){
$tooltipWrapper.addClass('tooltip-wrapper-right');
}
if(typeof instanceAttrs.message === 'undefined'){
$tooltip.html(instanceAttrs.$$element[0].childNodes[0].data);
$element.parent().children('tooltip').remove();
}
else {
$tooltip.html(instanceAttrs.message);
}
$tooltip.hide(); // <--- this is where the error gets thrown
$element.on('mouseover', goTooltip);
function goTooltip(){
$tooltip.show();
$element.on('mouseleave', killTooltip);
$element.off('mouseover');
}
function killTooltip(){
$tooltip.hide();
$element.off('mouseleave');
$element.on('mouseover', goTooltip);
}
}
},
scope: false,
replace: false,
restrict:'EA'
};
});
The test:
describe("The tooltip", function(){
var elm
, scope;
beforeEach(module("RPMDirectives"));
beforeEach(inject(function($rootScope, $compile){
elm = angular.element(
'<div>' +
'<a></a>' +
'<tooltip icon="true" position="bottom-center-left">bar</tooltip>' +
'</div>' );
scope = $rootScope;
$compile(elm)(scope);
scope.$digest();
}));
it('should do something', inject(function($compile, $rootScope){
expect(elm.find('aside')).toBe('bar');
}))
});
When I run the test it says that Object [object Object] ($tooltip, indicated by the comment in the directive) has no method hide. The directive works fine when I use it in my browser, and jqlite definitely has a hide method, so I'm at a loss. I'm sure I could get it to work by including jquery in the testing environment, but I'd rather not do that as I'm in the process of removing jquery in favor of Angular in our production app, and proving that scenario here is critical.
Thanks to moral apeman above. It turns out that I was looking at the wrong version of the angular docs and hide is no longer a method of angular.element. Nothing wrong with Jasmine or Karma and testing Angular is still a dream.
This may be a duplicate but I have looked at a lot of other questions here and they usually miss what I am looking for in some way. They mostly talk about a service they created themselves. That I can do and have done. I am trying to override what angular is injecting with my mock. I thought it would be the same but for some reason when I step through the code it is always the angular $cookieStore and not my mock.
I have very limited experience with jasmine and angularjs. I come from a C# background. I usually write unit tests moq (mocking framework for C#). I am use to seeing something like this
[TestClass]
public PageControllerTests
{
private Mock<ICookieStore> mockCookieStore;
private PageController controller;
[TestInitialize]
public void SetUp()
{
mockCookieStore = new Mock<ICookieStore>();
controller = new PageController(mockCookieStore.Object);
}
[TestMethod]
public void GetsCarsFromCookieStore()
{
// Arrange
mockCookieStore.Setup(cs => cs.Get("cars"))
.Return(0);
// Act
controller.SomeMethod();
// Assert
mockCookieStore.VerifyAll();
}
}
I want mock the $cookieStore service which I use in one of my controllers.
app.controller('PageController', ['$scope', '$cookieStore', function($scope, $cookieStore) {
$scope.cars = $cookieStore.get('cars');
if($scope.cars == 0) {
// Do other logic here
.
}
$scope.foo = function() {
.
.
}
}]);
I want to make sure that the $cookieStore.get method is invoked with a 'garage' argument. I also want to be able to control what it gives back. I want it to give back 0 and then my controller must do some other logic.
Here is my test.
describe('Controller: PageController', function () {
var controller,
scope,
cookieStoreSpy;
beforeEach(function () {
cookieStoreSpy = jasmine.createSpyObj('CookieStore', ['get']);
cookieStoreSpy.get.andReturn(function(key) {
switch (key) {
case 'cars':
return 0;
case 'bikes':
return 1;
case 'garage':
return { cars: 0, bikes: 1 };
}
});
module(function($provide) {
$provide.value('$cookieStore', cookieStoreSpy);
});
module('App');
});
beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) {
scope = $rootScope.$new();
controller = $controller;
}));
it('Gets car from cookie', function () {
controller('PageController', { $scope: scope });
expect(cookieStoreSpy.get).toHaveBeenCalledWith('cars');
});
});
This is a solution for the discussion we had in my previous answer.
In my controller I'm using $location.path and $location.search. So to overwrite the $location with my mock I did:
locationMock = jasmine.createSpyObj('location', ['path', 'search']);
locationMock.location = "";
locationMock.path.andCallFake(function(path) {
console.log("### Using location set");
if (typeof path != "undefined") {
console.log("### Setting location: " + path);
this.location = path;
}
return this.location;
});
locationMock.search.andCallFake(function(query) {
console.log("### Using location search mock");
if (typeof query != "undefined") {
console.log("### Setting search location: " + JSON.stringify(query));
this.location = JSON.stringify(query);
}
return this.location;
});
module(function($provide) {
$provide.value('$location', locationMock);
});
I didn't have to inject anything in the $controller. It just worked. Look at the logs:
LOG: '### Using location set'
LOG: '### Setting location: /test'
LOG: '### Using location search mock'
LOG: '### Setting search location: {"limit":"50","q":"ani","tags":[1,2],"category_id":5}'
If you want to check the arguments, spy on the method
// declare the cookieStoreMock globally
var cookieStoreMock;
beforeEach(function() {
cookieStoreMock = {};
cookieStoreMock.get = jasmine.createSpy("cookieStore.get() spy").andCallFake(function(key) {
switch (key) {
case 'cars':
return 0;
case 'bikes':
return 1;
case 'garage':
return {
cars: 0,
bikes: 1
};
}
});
module(function($provide) {
$provide.value('cookieStore', cookieStoreMock);
});
});
And then to test the argument do
expect(searchServiceMock.search).toHaveBeenCalledWith('cars');
Here is an example https://github.com/lucassus/angular-seed/blob/81d820d06e1d00d3bae34b456c0655baa79e51f2/test/unit/controllers/products/index_ctrl_spec.coffee#L3 it's coffeescript code with mocha + sinon.js but the idea is the same.
Basically with the following code snippet you could load a module and substitute its services:
beforeEach(module("myModule", function($provide) {
var stub = xxx; //... create a stub here
$provide.value("myService", stub);
}));
Later in the spec you could inject this stubbed service and do assertions:
it("does something magical", inject(function(myService) {
subject.foo();
expect(myService).toHaveBeenCalledWith("bar");
}));
More details and tips about mocking and testing you could find in this excellent blog post: http://www.yearofmoo.com/2013/09/advanced-testing-and-debugging-in-angularjs.html
Why mock cookieStore when you may use it directly without modification? The code below is a partial unit test for a controller which uses $cookieStore to put and get cookies. If your controller has a method known as "setACookie" that uses $cookieStore.put('cookieName', cookieValue) ... then the test should be able to read the value that was set.
describe('My controller', function() {
var $cookieStore;
describe('MySpecificController', function() {
beforeEach(inject(function(_$httpBackend_, $controller, _$cookieStore_) {
$cookieStore = _$cookieStore_;
// [...] unrelated to cookieStore
}));
it('should be able to reference cookies now', function () {
scope.setACookie();
expect($cookieStore.get('myCookieName')).toBe('setToSomething');
});
});
There isn't much documentation insofar on web-ui testing in Dart. Two methods are available : a) run through Chrome's DumpRenderTree or b) a trick that consists of loading the app as is and running the test code on top of it. For trivial cases, the first option seems to be a bit tedious. So the latter option -- which in my case, doesn't work when it comes to load components.
With the following file structure:
test/
main_test.html
main_test.dart
web/
main.html
app.html
(all the files are listed in this gist)
The following test set hangs on the second step.
main() {
useShadowDom = true;
test('Inline element is initially present.', () {
var story = () => expect(query('#hdr'), isNotNull);
Timer.run(expectAsync0(story));
});
test('Component is loaded.', () {
var story = () => expect(query('#globe'), isNotNull);
Timer.run(expectAsync0(story));
});
}
How could the app component be loaded? More broadly, is there another method of testing web components?
For web-ui test you have to query the shadow dom or the xtag (this) of the webcomponent that you whant to test instead of the "classic" dom.
Based on TodoMVC code sample
With your code:
A working version of this test is :
main() {
useShadowDom = true;
test('Inline element is initially present.', () {
var story = () => expect(query('#hdr'), isNotNull);
Timer.run(expectAsync0(story));
});
test('Component is loaded.', () {
var root = query("span[is=x-app]").shadowRoot;
var story = () => expect(root.query('#globe'), isNotNull);
Timer.run(expectAsync0(story));
});
}
and a test version without expectAsync should be:
main() {
useShadowDom = true;
Timer.run(() {
test('Header element is initially present.', () {
var hdr = query('#hdr');
expect(hdr, isNotNull);
});
test('EchapApp component is loaded.', () {
var root = query("span[is=x-app]").shadowRoot;
var globe = root.query('#globe');
expect(globe, isNotNull);
});
});
}
and finaly a version without shadow dom :
main() {
//useShadowDom = true;
Timer.run(() {
test('Header element is initially present.', () {
var hdr = query('#hdr');
expect(hdr, isNotNull);
});
test('EchapApp component is loaded.', () {
var root = query("span[is=x-app]").xtag;
var globe = root.query('#globe');
expect(globe, isNotNull);
});
});
}
For me this 3 codes are 100% pass on Dartium with
Dart Editor version 0.5.20_r24275
Dart SDK version 0.5.20.4_r24275
You can try using the karma-dart runner: https://github.com/karma-runner/karma-dart
It even has a web components example.
library click_counter_test;
import 'package:unittest/unittest.dart';
import 'dart:html';
import '../web/out/xclickcounter.dart';
main() {
test('CounterComponent.increment', () {
var hello = new DivElement();
var component = new CounterComponent.forElement(hello);
expect(component.count, equals(0));
component.increment();
expect(component.count, equals(1));
});
}
Although it is not Dart specific, you can use Selenium for testing the UI. I believe some members of the Dart team have used Selenium as well to do UI testing.