AngularJS: Accessing scope in E2E test - angularjs

I'm trying to access $scope's within an E2E test without success...
As a test I tried this: (My site does not use JQuery..)
The runner has my site in a nested iframe, so I'm accessing it directly, then getting all ng-scopes and trying .scope() on them as in this post and code below...
var frameDocument = document.getElementById('test-frames').children[0].contentDocument;
var scopeElements = frameDocument.getElementsByClassName('ng-scope');
var scopes = [].map.call(scopeElements, function (e) {
return angular.element(e).scope();
});
The above code finds the proper elements, but calling scope() on them returns undefined for each....
Can someone confirm or deny that we can access the scope in E2E? I'd assume there is a way?
Thank-you

Here is my trick based on previous answer.
You can extend it to dynamic scopes. The main part is getting the reference to appWindow from addFutureAction.
//HTML CODE
<body id="main-controller" ng-controller="mainCtrl" ng-init="__init__()">
//Scenario helper.
/*
Run `callback` with scope from `selector`.
*/
angular.scenario.dsl('scope', function() {
return function(selector, callback) {
return this.addFutureAction(
'Executing scope at ' + selector,
function(appWindow, $document, done) {
var body = appWindow.document.getElementById(selector)
var scope = appWindow.angular.element(body).scope()
callback(scope)
scope.$apply()
done(null, 'OK');
})
}
})
//Actual test.
it(
'When alerts are defined, they are displayed.',
function() {
scope('main-controller', function(scope) {
scope.alerts.push({'type': 'error', 'msg': 'Some error.'})
})
expect(element('#alerts').css('display')).toEqual('block')
})

In E2E tests, accessing scope that way is not good option. Instead You can use helper functions like element() to select elements in page, and use expect() to check model data.
What you might need is unit testing. You can access $scope in unit tests easily.
There is a very good guide here: http://www.yearofmoo.com/2013/01/full-spectrum-testing-with-angularjs-and-testacular.html
Also it might be a timing issue, i can reach scopes in testacular runner like this. It runs tests in iframe. To make it work you need to add sleep(3) to your test. But this is very fragile.
setTimeout(function () {
console.log('try to reach frames');
var frame = window.frames[0].window.frames['senario_frame'];
if (!frame) {
console.log('too late');
} else {
var scopeElements = frame.document.getElementsByClassName('ng-scope');
var scopes = [].map.call(scopeElements, function (e) {
return frame.angular.element(e).scope();
});
console.log(scopes);
}
}, 2000);

Related

Jamsine throws error despite same code working in browser

I added this code to my component controller to focus an input and it worked great in the browser but it broke all of my template tests. I thought I could just flush the $timeout and all would be well but it's not.
vm.$onInit = init;
function init(){
focusInput();
}
function focusInput(){
$timeout(function(){
$document[0]
.querySelector('md-autocomplete-wrap')
.querySelector('input')
.focus();
}, 0);
}
However, in my unit test Jasmine is reporting that .querySelector is not available because the result of the first querySelector is null in the test environment.
it('should render', function(){
var wrap, searchBarDirective, $scope;
$scope = $rootScope.$new();
searchBarDirective = $compile(angular.element(template))($scope);
$scope.$digest();
$timeout.flush();
wrap = searchBarDirective.find('md-autocomplete-wrap')[0];
expect(wrap).toBeDefined();
});
It's obvious to me that $document doesn't contain the rendered directive and thus the second querySelector fails. But why doesn't $document contain the directive?
I tried mocking querySelector with spyOn($document[0], "querySelector").and.returnValues($document[0],$document[0]) but that doesn't get me past the focus. Thinking I have lot my way here.
* Revised *
I think that it is important to continue to use $document but I decided to drop the querySelector for the jqLite find method.
function focusInput(){
$timeout(function(){
var input;
try {
// can throw an error if the first find fails
input = $document.find('md-autocomplete').find('input');
}
catch (e) {
angular.noop(e);
}
if(input && angular.isFunction(input.focus)) {
input.focus();
}
}, 0);
}
The test I changed per comments to below. I do have Karma load jquery to make testing easier which allows me to search for :focus
beforeEach(function(){
element = angular.element(template);
$document[0].body.appendChild(element[0]);
$scope = $rootScope.$new();
});
afterEach(function(){
element[0].remove();
});
it('should be focused', function(){
var input, searchBarDirective;
searchBarDirective = $compile(element)($scope);
$scope.$digest();
$timeout.flush();
input = searchBarDirective.find(':focus')[0];
expect(input).toBeDefined();
});
The reason why your querySelector call works in the browser, but not in tests is that you are creating a DOM element with angular.element, but you are never attaching it to the document. There are two ways to address this:
First, you could simply do this. Instead of:
searchBarDirective = $compile(angular.element(template))($scope);
Do:
let element; // declare this in the describe block so it is available later
element = angular.element(template);
document.body.appendChild(element[0]);
searchBarDirective = $compile(element)($scope);
And then do this:
afterEach(() => element[0].remove());
But, that's a bit messy. You should not be manipulating global scope (ie- the document) in your unit tests unless you have to. It would be better in your non-test code to avoid accessing the document and instead access a scope element, or some other DOM element that you can also mock in your tests. This will be a bit harder to do since it may require re-architecting your code a bit. In general though, in order to make modular and testable code, you want to avoid accessing the document object as much as possible.

Angular Testing DOM after update by Factory

Ok, I'm trying to test the outcome of a function that updates the DOM>
I have a directive that loads a template via url.
Then a controller calls a factory method to update the html table with data.
I have the tests showing that I can get the data that is all good.
but how can I test that the updates to the table have taken place?
I am using NodeJS with Karma and Jasmine.
I have followed tutorials on how to load in templates, and I have that working, I can load and access the templates in my test fine.
but when I run the method to update the table, the tests fail.
I'll give an scaled down example of what I'm trying to do. Note, this is just demo code, Not a working app.
Template.
<table><tr><td class="cell1"></td></tr></table>
Directive.
dataTable.directive('dataTable', function () {
return {
restrict: 'E',
templateUrl: 'path/to/template/dataTable.html'
};
});
Controller
dataTable.controller('dataTableController', ['$scope', 'dataTableFactory',
function ($scope, dataTableFactory){
$scope.updateTable = function(){
dataTableFactory.loadData();
// code to load data from dataTableFactory here! //
dataTableFactory.updateTable();
}
}])
Factory
dataTable.factory('dataTableFactory',['$document',function($document){
var _tableData;
return(
"tableData": _tableData,
loadData: function(){
// code to get data and populate _tableData.
}
updateTable: function(){
$document.find('.cell1').append(this.tableData.data);
}
)
}])
Unit Test
describe('dataTable Tests', function () {
var scope, element, $compile, mDataTableFactory, controller, tableData, doc, factory;
beforeEach(module('dataTable'));
beforeEach(module('app.templates')); // setup via ng-html2js
beforeEach(inject(function (_$rootScope_, _$compile_,_$controller_,_dataTableFactory_) {
scope = _$rootScope_.$new();
doc = _$compile_('<flood-guidance></flood-guidance>')(scope);
factory = _dataTableFactory_;
controller = _$controller_('dataTableController', {
$scope: scope,
$element: doc,
dataTableFactory: factory
});
scope.$digest();
}));
it("Template should contain the cell cell1", function(){
expect(doc.find('.cell1').contents().length).toBe(0);
expect(doc.find('.cell1').html()).toBeDefined();
});
// Passes fine, so I know the template loads ok.
it('Should show data in cell1',function(){
factory.tableData = {data: 'someData'};
scope.updateTable();
expect(doc.find('.cell1').contents().length).toBe(1);
expect(doc.find('.cell1').html()).toBe('SomeData');
});
});
});
Test Ouptut
Expected 0 to be 1. Expected '' to be 'someData'.
If I put the updateTable code in to the controller and call the update function there, the test passes, but I'd like to have this in a factory, how can I make this test pass (the app runs and works as expected, I just can't get a working test).
I understand this kind of testing is more focused on the UI and not exactly 'Unit Testing' but is it possible to do this?
So essentially updateTable cannot find the changes performed by factory.tableData. I guess the problem may be due to the way how your factory exposes the _tableData property.
Could you try to modify your factory like this:
dataTable.factory('dataTableFactory',['$document',function($document){
var _tableData;
return(
getTableData: function() { return _tableData; },
setTableData: function(newVal) { _tableData = newVal; },
loadData: function(){
// code to get data and populate _tableData.
}
updateTable: function(){
$document.find('.cell1').append(this.tableData.data);
}
)
}])
and then of course use the setter/getter accordingly. See if it works this way.
OK so I'm still not sure if I fully get your intention but here is a fiddle with my refactored example.
http://jsfiddle.net/ene4jebb/1/
First of all the factory shouldn't touch the DOM, that's the directives responsibility. Thus my rework passes the cellvalue (new scope property) to the directive, which renders it. Now when you call setTableData (which will change _tableData.data) and since in test environment call the $digest loop yourself, the directive will automatically redraw the new stuff.
Controller is kept thin as possible thus only providing a scope property to the factory.
As said not sure if you were after this, but hope it helps. If there are any questions just ask.

How to access/update $rootScope from outside Angular

My application initializes an object graph in $rootScope, like this ...
var myApp = angular.module('myApp', []);
myApp.run(function ($rootScope) {
$rootScope.myObject = { value: 1 };
});
... and then consumes data from that object graph (1-way binding only), like this ...
<p>The value is: {{myObject.value}}</p>
This works fine, but if I subsequently (after page rendering has completed) try to update the $rootScope and replace the original object with a new one, it is ignored. I initially assumed that this was because AngularJS keeps a reference to the original object, even though I have replaced it.
However, if I wrap the the consuming HTML in a controller, I am able to repeatedly update its scope in the intended manner and the modifications are correctly reflected in the page.
myApp.controller('MyController', function ($scope, $timeout) {
$scope.myObject = { value: 3 };
$timeout(function() {
$scope.myObject = { value: 4 };
$timeout(function () {
$scope.myObject = { value: 5 };
}, 1000);
}, 1000);
});
Is there any way to accomplish this via the $rootScope, or can it only be done inside a controller? Also, is there a more recommended pattern for implementing such operations? Specifically, I need a way to replace complete object graphs that are consumed by AngularJS from outside of AngularJS code.
Thanks, in advance, for your suggestions,
Tim
Edit: As suggested in comments, I have tried executing the change inside $apply, but it doesn't help:
setTimeout(function() {
var injector = angular.injector(["ng", "myApp"]);
var rootScope = injector.get("$rootScope");
rootScope.$apply(function () {
rootScope.myObject = { value: 6 };
});
console.log("rootScope updated");
}, 5000);
Except for very, very rare cases or debugging purposes, doing this is just BAD practice (or an indication of BAD application design)!
For the very, very rare cases (or debugging), you can do it like this:
Access an element that you know is part of the app and wrap it as a jqLite/jQuery element.
Get the element's Scope and then the $rootScope by accessing .scope().$root. (There are other ways as well.)
Do whatever you do, but wrap it in $rootScope.$apply(), so Angular will know something is going on and do its magic.
E.g.:
function badPractice() {
var $body = angular.element(document.body); // 1
var $rootScope = $body.scope().$root; // 2
$rootScope.$apply(function () { // 3
$rootScope.someText = 'This is BAD practice :(';
});
}
See, also, this short demo.
EDIT
Angular 1.3.x introduced an option to disable debug-info from being attached to DOM elements (including the scope): $compileProvider.debugInfoEnabled()
It is advisable to disable debug-info in production (for performance's sake), which means that the above method would not work any more.
If you just want to debug a live (production) instance, you can call angular.reloadWithDebugInfo(), which will reload the page with debug-info enabled.
Alternatively, you can go with Plan B (accessing the $rootScope through an element's injector):
function badPracticePlanB() {
var $body = angular.element(document.body); // 1
var $rootScope = $body.injector().get('$rootScope'); // 2b
$rootScope.$apply(function () { // 3
$rootScope.someText = 'This is BAD practice too :(';
});
}
After you update the $rootScope call $rootScope.$apply() to update the bindings.
Think of modifying the scopes as an atomic operation and $apply() commits those changes.
If you want to update root scope's object, inject $rootScope into your controller:
myApp.controller('MyController', function ($scope, $timeout, $rootScope) {
$rootScope.myObject = { value: 3 };
$timeout(function() {
$rootScope.myObject = { value: 4 };
$timeout(function () {
$rootScope.myObject = { value: 5 };
}, 1000);
}, 1000);
});
Demo fiddle

Why is angular $apply required here?

Please consider the following angularjs code for a controller:
(function (app) {
var controller = function ($scope, $state, datacontext) {
$scope.$parent.manageTitle = "Account Management";
$scope.accounts = [];
var init = function () {
getRecords();
};
var getRecords = function () {
return datacontext.getAccounts().then(function (data) {
$scope.$apply(function () {
$scope.accounts = data;
});
});
};
init();
};
app.controller("accountsCtrl", ["$scope", "$state", "datacontext", controller]);
})(angular.module("app"));
Removing the $scope.$apply wrapper and leaving just the "$scope.accounts = data" in the getRecords method breaks the code. The data is retrieved but the ng-repeat directive in the html is not automatically updated. I'm trying to get my arms around the entire $apply/$digest model, but it sure seems to be that the $apply should NOT be required in this case.
Am I doing something wrong?
Thanks.
<------------------------------------------ EDIT ---------------------------------------->
Ok, thanks for the responses. Here is the datacontext. It uses Breeze. I still can't figure out what the problem is - - I just don't see why $apply is required in the code, above.
(function (app) {
var datacontext = function () {
'use strict';
breeze.config.initializeAdapterInstance('modelLibrary', 'backingStore', true);
breeze.config.initializeAdapterInstance("ajax", "angular", true);
breeze.NamingConvention.camelCase.setAsDefault();
var service;
var manager = new breeze.EntityManager('api/ProximityApi');
var entityQuery = breeze.EntityQuery;
var queryFailed = function (error) {
};
var querySuccess = function (data) {
return data.results;
};
var getAccounts = function () {
var orderBy = 'accountName';
return entityQuery.from('Accounts')
.select('id, accountName')
.orderBy(orderBy)
.using(manager)
.execute()
.then(querySuccess, queryFailed);
};
service = {
getAccounts: getAccounts
};
return service;
};
app.factory('datacontext', [datacontext]);
})(angular.module('app'));
Thanks again!
Thanks for your answers. Jared - you're right on the money. By default, Breeze does not use angular $q promises, but uses third-party Q.js promises instead. Therefore, I needed $apply to synchronize the VM to the view. Recently however, the Breeze folks created angular.breeze.js, which allows the Breeze code to use angular promises, instead. By including the angular.breeze module in the application, all Breeze code will use native angular promises and $http instead.
This solved my problem and I could remove the $apply call.
See: http://www.breezejs.com/documentation/breeze-angular-service
The reason that you need to use the $apply function is the result of using Breeze to to return the data. the $apply function is used to get angular to run a digest on all the internal watches and update the scope accordingly. This is not needed when all changes occur in the angular scope as it does this digest automatically. In your code, because you are using Breeze the changes are taking place outside the angular scope, thus you will need to get angular to manually run the digest, and this is true for anything that takes place out side of angular (jQuery, other frameworks ect...). It is true that Breeze is using promises to update the data, however Angular does not know how to handle the changes after the promise returns because it is out side the scope. If you were using an angular service with promises then the view would be updated automatically. If your code is working correctly as is then it would be the correct way to use $apply in this way.
The only thing I might suggest is to change the way you are calling the apply to make sure that it will only run if another digest is not currently in progress as this can cause digest errors. I suggest you call the function as such:
if(!$scope.$$phase){$scope.$apply(function () {
$scope.accounts = data;
});
Or the other option would be to write a custom wrapper around the $apply function like this SafeApply

How should I access an element's angularjs $ngModelController in a jasmine unit test?

I'm currently using directiveElement.data("$ngModelController") to get access to the element's $ngModelController, as in the following example.
describe("directiveElement", function () {
it("should do something with ngModelController", inject(function($compile, $rootScope) {
var directiveElement = $compile("<input ng-model="myNgModel" customDirective type="text"></input>")($rootScope);
$rootScope.$digest();
var ngModelCtrl = directiveElement.data("$ngModelController");
ngModelCtrl.$modelValue = "12345";
// do rest of test
}));
});
However, I want to know if there is a better to access the $ngModelController, or if accessing the $ngModelController is a bad idea?
You could also do directiveElement.controller('ngModel').
I certainly think there are legitimate testing reasons why you would want a handle on this, though the more common way is to get a handle on it through the form (eg. https://github.com/angular/angular.js/blob/master/test/ng/directive/formSpec.js)

Resources