I'm writing a script which generates png images from every page of my frontend.
I'm using angular for the UI and capturing the pages with phantom.
The view take a while until angular finish rendering it so I have to wait a little before capturing:
var page = require('webpage').create();
page.open('http://localhost:9000/', function () {
window.setTimeout(function () {
page.render('snapshot.png');
phantom.exit();
}, 2000);
});
I wonder if there is a better way to achieve this. I found angular can emit an event when the page is fully rendered:
$scope.$on('$viewContentLoaded', function () {
// do something
});
And found a way for communicate to phantom with onCallback so I could write something like:
$scope.$on('$viewContentLoaded', function () {
window.callPhantom({ hello: 'world' });
});
Then in other place in phantom script:
page.onCallback = function() {
page.render('snapshot.png');
phantom.exit();
};
But I'm lost in how to inject the angular $viewContentLoaded handle from the phantom script.
I don't know if evaluate/evalueateAsyn are the way to go ...
page.evaluateAsync(function () {
$scope.$on('$viewContentLoaded', function () {
window.callPhantom({ hello: 'world' });
});
});
Maybe I could access the right $scope in some way.
Any ideas?
The associated PhantomJS API is onCallback; you can find the API doc on the wiki.
// in angular
$scope.$on('$viewContentLoaded', function () {
window.callPhantom();
});
// in the phantomjs script
var page = require('webpage').create();
page.onCallback = function() {
page.render('snapshot.png');
phantom.exit();
};
page.open('http://localhost:9000/');
You can get access to the $rootScope by accessing the injector; for example, if you're using the ng-app directive, you can find the element with the directive and call .injector().get("$rootScope") on it. However, I'm not sure if the $viewContentLoaded event will already have fired by then.
Related
I'm trying to use Protractor's addMockModule to insert mock data in my end-2-end test.
My test is supposed to go to a web site, find a button by css-class and click it. The button click calls the function dostuff() in MyService, which fetches data from the backend.
My code so far:
describe('should display validation error', function () {
it('should work', function () {
browser.get('http://my.url');
browser.addMockModule('MyService', function () {
// Fake Service Implementation returning a promise
angular.module('MyService', [])
.value({
dostuff: function () {
return {
then: function (callback) {
var data = { "foo": "bar" };
return callback(data);
}
};
}
});
});
var button = element(by.css('.btn-primary'));
button.click();
browser.sleep(5000);
});
});
The test is accessing the web site and clicking the button. The problem is that real data from the database is displayed, not the mock data.
I followed several threads, like this one: How to mock angular.module('myModule', []).value() in Jasmine/Protractor
However, it seems like the function protractor.getInstance() is deprecated.
Anyone got this working?
Take a look at the unit test for addMockModule(). Try to add the addMockModule statement before you call browser.get()
https://github.com/angular/protractor/blob/673d416b7ef5abd0953da940cfa8cf2a59650df4/spec/basic/mockmodule_spec.js
Really have no idea why this doesn't work. I must be doing something incredibly stupid.
Here is a controller:
angular.module('nightlifeApp')
.controller('TestCtrl', function($scope) {
$scope.testvar = 'before';
setTimeout(function() {
$scope.testvar = 'after';
}, 2000);
});
and here is the view that has this as the controller:
h1(ng-bind='testvar')
h1 {{testvar}}
But neither h1 element ever changes! Any thoughts?
If you're using setTimeout then you manually need to trigger apply. Like
$apply() is used to execute an expression in angular from outside of
the angular framework. (For example from browser DOM events,
setTimeout, XHR or third party libraries). Because we are calling into
the angular framework we need to perform proper scope life cycle of
exception handling, executing watches.
setTimeout(function() {
$scope.$apply(function() {
$scope.testvar = 'after';
});
}, 2000);
In my opinion you should use $timeout service. So it will trigger $apply() automatically. Your code will look like
$timeout(function () {
$scope.testvar = 'after';
}, 2000);
Make sure you've injected $timeout service in your controller. In your HTML you don't need to use ng-bind. You're doing same in controller. Only
<h1> {{testvar}} </h1>
I'm using a module from the UI Boostrap extensions (http://angular-ui.github.io/bootstrap). The module actually serves as a loading dialog and is automatically closed when a set of web service data is returned to my Angular code. As the data on this page is loaded automatically the dialog comes up immediately.
All this works great when I hit the page in question for the the first time or simply refresh it. The problem occurs when I go to a deeper page and then try and navigate back to the original page (with the dialog) via the browser's back button. The dialog never goes away despite all the fact that all the data is returned and the module's dismiss() call has been made.
I've traced this down to the promise to open the dialog appears to be happening after the dismiss call but, again, only when the page is loaded via the back button. The dismiss call never closes anything because it hasn't been added yet (I've confirmed this in the debugger).
The question I have is how could I handle this? Is there a solid way to catch the completion of the page loading via Angular and double check that the dialog closed? Is there a better way via UI Bootstrap's api?
I know this is rather unusual case but any thoughts on it would be great.
Thanks!
#HankScorpio's solution is good, but I think there may be a simplified option now.
There is no need to store the current modal anymore, if you register either a $locationChangeStart or $routeChangeStart listener with $uibModalStack injected and call $uibModalStack.dismissAll(). $locationChangeStart has the benefit of working for both ngRoute and uiRoute.
i.e. If only for the one page, then in your controller you'd have:
angular.module('app')
.controller('ctrl', ['$scope', '$uibModalStack', ctrl]);
function ctrl($scope, $uibModalStack) {
$scope.$on('$locationChangeStart', handleLocationChange);
function handleLocationChange() {
$uibModalStack.dismissAll();
}
}
If you want to do this for all pages then define this in a factory that is always loaded or just an app.run code segment:
angular.module('app')
.run(['$rootScope', '$uibModalStack', setupUibModal]);
setupUibModal($rootScope, $uibModalStack) {
$rootScope.$on('$locationChangeStart', handleLocationChange);
function handleLocationChange() {
$uibModalStack.dismissAll();
}
}
Here is the simple solution when using ui-router for state change
Closing modal popup on the back button click in angularjs
App.run(['$rootScope', '$modalStack', function ($rootScope, $modalStack) {
$rootScope.$on('$stateChangeStart', function (event) {
var top = $modalStack.getTop();
if (top) {
$modalStack.dismiss(top.key);
}
})
}]);
hope this will save lot of time for people who are breaking heads
I've run into this same problem. Here's how I fixed it.
1) Create a service to abstract the opening and closing of a modal and track which one is open (necessary for step 2). Instead of calling $modal.open() directly, call ModalService.open().
Here you go, you can have the one I wrote:
(function () {
'use strict';
var theModule = angular.module('services.modalService', ['ui.bootstrap']);
theModule.factory('ModalService', function ($modal) {
var service = {};
var currentModal;
var clearModal = function () {
currentModal = undefined;
};
service.getCurrentModal = function () {
return currentModal;
};
service.open = function (options) {
currentModal = $modal.open(options);
currentModal.result['finally'](clearModal);
return currentModal;
};
return service;
});
}());
2) In a controller, add an event listener to $routeChangeStart, this event will fire whenever someone hits the back button.
$scope.$on('$routeChangeStart', function(){
var currentModal = ModalService.getCurrentModal();
if(angular.isDefined(currentModal)){
currentModal.dismiss('cancel');
}
});
3) Your modals should now close when a user hits back.
4) Enjoy.
IMPROVEMENT:
I found the answer from HankScorpio to be the best out there. I wanted to include this snippet for those using ui-router and their recommendation for stateful modals.
1) I wanted the result.finally(...) to jump to a parent state;
2) I wanted to control the closing of the modal from $stateProvider config, NOT through rigging a controller and adding a listener to $routeChangeStart
Here is an example of a state that opens (and closes) it's modal:
.state('product.detail', {
url: '/detail/{productId}',
onEnter: /*open-modal logic*/,
onExit: ['ModalService', function (ModalService) { ModalService.close()} ]
})
I made ModalService aware of $state so that the result of closing a modal could jump to a parent view:
a. Add an isStateful flag to modalService.open(...):
service.open = function (options, isStateful) {
currentModal = $uibModal.open(options);
currentModal.result.finally(function () {
clearModal(isStateful);
});
return currentModal;
};
so that clearModal will return to previous state:
var clearModal = function (isStateful) {
currentModal = undefined;
if (isStateful)
$state.go('^');
};
Finally, add the closeModal() function called above (not a "stateful" close, simply a dismissal):
service.close = function() {
if (currentModal) {
currentModal.dismiss().then(function () {
clearModal();
})
}
}
The benefits of this are that back button functionality is controlled at state config level, not through a listener.
When using AngularJS and doing a redirect using $location.path('/path') the new page takes a while to load, especially on mobile.
Is there a way to add a progress bar for loading? Maybe something like YouTube has?
For a progress bar as YouTube has, you can take a look at ngprogress. Then just after the configuration of your app (for example), you can intercept route's events.
And do something like:
app.run(function($rootScope, ngProgress) {
$rootScope.$on('$routeChangeStart', function() {
ngProgress.start();
});
$rootScope.$on('$routeChangeSuccess', function() {
ngProgress.complete();
});
// Do the same with $routeChangeError
});
Since #Luc's anwser ngProgress changed a bit, and now you can only inject ngProgressFactory, that has to be used to create ngProgress instance. Also contrary to #Ketan Patil's answer you should only instantiate ngProgress once:
angular.module('appRoutes', ['ngProgress']).run(function ($rootScope, ngProgressFactory) {
// first create instance when app starts
$rootScope.progressbar = ngProgressFactory.createInstance();
$rootScope.$on("$routeChangeStart", function () {
$rootScope.progressbar.start();
});
$rootScope.$on("$routeChangeSuccess", function () {
$rootScope.progressbar.complete();
});
});
if it is the next route that takes time to load e.g. making ajax call before the controller is run (resolve config on route) then make use of $route service's $routeChangeStart, $routeChangeSuccess and $routeChangeError events.
register a top level controller (outside ng-view) that listens to these events and manages a boolean variable in its $scope.
use this variable with ng-show to overlay a "loading, please wait" div.
if the next route loads fast (i.e. its controller runs quickly) but data that are requested by the controller take a long to load then, i'm afraid, you have to manage the visibility state of spinners in your controller and view.
something like:
$scope.data = null;
$http.get("/whatever").success(function(data) {
$scope.data = data;
});
<div ng-show="data !== null">...</div>
<div ng-show="data === null" class="spinner"></div>
use angular-loading-bar
Standalone demo here ..
https://github.com/danday74/angular-loading-bar-standalone-demo
Here is a working solution which I am using in my application. ngProgress is the best library out there for showing load-bars when changing urls.
Remember to inject the ngProgressFactory instead of ngProgress, as opposed to Luc's solution.
angular.module('appRoutes', []).run(function ($rootScope, ngProgressFactory) {
$rootScope.$on("$routeChangeStart", function () {
$rootScope.progressbar = ngProgressFactory.createInstance();
$rootScope.progressbar.start();
});
$rootScope.$on("$routeChangeSuccess", function () {
$rootScope.progressbar.complete();
});
});
Update Nov-2015 - After analyzing this approach with chrome timings, I have observed that this would not be the correct way for adding a loading bar. Sure, the loading bar will be visible to visitors,but it will not be in sync with actual page load timings.
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);