I'm building an E2E test of an Angular application using Protractor. The backend HTTP services are being mocked with $httpBackend. So far, the test looks like this:
describe('foo', function () {
it('bar', function () {
var backendMockModule = function () {
angular
.module('backendMock', [])
.run(['$httpBackend', function ($httpBackend) {
$httpBackend.whenPUT('http://localhost:8080/services/foo/bar')
.respond(function (method, url, data, header) {
return [200, {}, {}];
});
}]);
};
browser.addMockModule('backendMock', backendMockModule);
browser.get('http://localhost:8001/#/foo/bar');
element(by.id('baz')).click();
// here I would like to assert that the Angular app issued a PUT to '/foo/bar' with data = {...}
});
});
The test is a little more elaborated than this, it tests for optimistic update of the interface and other stuff. But I think this is not relevant to the this question, so I removed the other parts. The test in itself is working fine, I'm able to check that the elements on the interface are as expected. What I didn't find out is:
How to assert that the backend HTTP endpoint has been called with the correct data, method, headers, etc?
I have tried to do it like this (adding hasBeenCalled variable):
describe('foo', function () {
it('bar', function () {
var hasBeenCalled = false;
var backendMockModule = function () {
angular
.module('backendMock', [])
.run(['$httpBackend', function ($httpBackend) {
$httpBackend.whenPUT('http://localhost:8080/services/foo/bar')
.respond(function (method, url, data, header) {
hasBeenCalled = true;
return [200, {}, {}];
});
}]);
};
browser.addMockModule('backendMock', backendMockModule);
browser.get('http://localhost:8001/#/foo/bar');
element(by.id('baz')).click();
expect(hasBeenCalled).toBeTruthy();
});
});
But it does not work. I don't know exactly how the Protractor does the testing, but I imagine that it sends a serialized version of the function to the browser in the call addMockModule instead of running the test in the same process as the web page and so I'm not able to share state between the test and the browser (side question: is that correct?).
$httpBackend.flush() is needed before expect(...)...
Related
I have a standard HTTP interceptor as a factory:
angular
.module('app.services')
.factory('HttpInterceptorService', HttpInterceptorService);
function HttpInterceptorService($injector) {
// Callable functions
var service = {
response: response,
responseError: responseError
};
return service;
// Pass through clean response
function response(data) {
return data;
}
// Handle error response
function responseError(rejection) {
// Handle bypass requests
if (angular.isDefined(rejection.config) && rejection.config.bypassInterceptor) {
return rejection;
}
// Get $state via $injector to avoid a circular dependency
var state = $injector.get('$state');
switch (rejection.status) {
case 404:
return state.go('404');
break;
default:
return state.go('error');
}
}
}
In manual testing, I can see this works correctly by redirecting the user to the relevant 404 or error page if an HTTP call returns an error response. The basic principal of this is documented by Angular here: https://docs.angularjs.org/api/ng/service/$http#interceptors
Now I'm trying to write a unit test with Karma & Jasmine to test that the responseError function works correctly. I've checked out this SO answer to help me. My test looks like this:
describe('HttpInterceptorService', function() {
// Bindable members
var $window,
HttpInterceptorService;
// Load module
beforeEach(module('app.services'));
// Set window value
beforeEach(function () {
$window = { location: { href: null } };
module(function($provide) {
$provide.value('$window', $window);
});
});
// Bind references to global variables
beforeEach(inject(function(_HttpInterceptorService_) {
HttpInterceptorService = _HttpInterceptorService_;
}));
// Check service exists with methods
it('Exists with required methods', function() {
expect(HttpInterceptorService).toBeDefined();
expect(angular.isFunction(HttpInterceptorService.response)).toBe(true);
expect(angular.isFunction(HttpInterceptorService.responseError)).toBe(true);
});
// Test 404 HTTP response
describe('When HTTP response 404', function () {
beforeEach(function() {
HttpInterceptorService.responseError({ status: 404 });
});
it('Sets window location', function () {
expect($window.location.href).toBe('/404');
});
});
});
My test passes the Exists with required methods check but fails Sets window location with the following error:
Error: [$injector:unpr] Unknown provider: $stateProvider <- $state
The module doesn't seem to have ui.router module loaded, hence $state service is undefined. This is fine, because real router introduces extra moving parts and is highly undesirable in unit tests.
For functional test it is normal to treat a unit as a blackbox, provide initial conditions and test the results, asserting window.location would be appropriate.
For unit test there's no need to treat a unit as a blackbox, $state service may be stubbed:
var statePromiseMock = {};
beforeEach(module('app.services', {
$state: {
go: jasmine.createSpy().and.returnValue(statePromiseMock)
}
}));
And tested like:
it('...', inject(function (HttpInterceptorService, $state) {
var state404Promise = HttpInterceptorService.responseError({ status: 404 });
expect($state.go).toHaveBeenCalledWith('404');
expect(state404Promise).toBe(statePromiseMock);
...
}))
I.e. it may be something like
describe('HttpInterceptorService', function() {
// Bindable members
var HttpInterceptorService;
var statePromiseMock = {};
beforeEach(module('app.services', {
$state: {
go: jasmine.createSpy().and.returnValue(statePromiseMock)
}
}));
// Bind references to global variables
beforeEach(inject(function(_HttpInterceptorService_) {
HttpInterceptorService = _HttpInterceptorService_;
}));
// Check service exists with methods
it('Exists with required methods', function() {
expect(HttpInterceptorService).toBeDefined();
expect(angular.isFunction(HttpInterceptorService.response)).toBe(true);
expect(angular.isFunction(HttpInterceptorService.responseError)).toBe(true);
});
it('...', inject(function($state) {
var state404Promise = HttpInterceptorService.responseError({
status: 404
});
expect($state.go).toHaveBeenCalledWith('404');
expect(state404Promise).toBe(statePromiseMock);
}))
});
Is there remotely any way to mock any SSE (Server Sent Event) from a Protractor test ?
That means mocking EventSource
Angular controller :
angular.module('app').controller('HomeController', function() {
var monitoringEvents = new window.EventSource('/streams/jobserveur');
monitoringEvents.addEventListener('monitoring-event', function(e) {
var json = JSON.parse(e.data);
...
});
});
Thank you for any insight
I managed to mock EventSource by the solution I mentionned (angular module/protractor addMockModule).
Externalize EventSource calls into a dedicated angular module
angular.module('app.sse', [])
.value('$sse', {
sources : [],
addEventSource : function(name, url) {
this.sources[name] = new window.EventSource(url);
},
addEventListener : function(name, eventName, callback) {
this.sources[name].addEventListener(eventName, callback);
}
});
Referencing the module in the app
angular.module('app', ['app.sse', ...])
Use the $sse module in the app
angular.module('app').controller('HomeController', ['$sse' , function($sse) {
$sse.addEventSource('jobserveur', '/streams/jobserveur');
$sse.addEventListener('jobserveur', 'monitoring-event', function(e) {
var js = JSON.parse(e.data);
}
}]);
From here, make sure your app still work before moving onto the testing
Mock the app.sse module in your test
describe('SSE Fixture', function() {
beforeEach(function() {
browser.addMockModule('app.sse', function() {
angular.module('app.sse', []).value('$sse', {
addEventSource: function(name, url) {
},
addEventListener: function(name, event, callback) {
}
});
});
}
And you're done ! Obviously, the two methods are not implemented here nor is the app.sse module in anyway robust but you get the picture.
Hope it helps anyone
Cheers
I'm using generator-react-webpack to create a React web app. This web app relies on JSON feeds - one of which is hosted on a CDN that does not support JSONP and the CDN url is a subdomain of the webapp. Is there any way to return the JSON data from within the React Component?
Basic React Component:
var AppComponent = React.createClass({
loadData: function() {
jQuery.getJSON(jsonFile.json?callback=?)
.done(function(data) {
console.log(data);
}.bind(this));
},
render: function(){
return ( ... );
}
});
I've tried a few solutions, and have come to the conclusion that I need to define my own callback on the JSON file like so:
JSON:
handleData({
"data": "hello World"
})
Is there a way for the handleData callback to be defined in the react component, or the response accessed from the react component? Any thoughts as to how I can get this to work are much appreciated. Thanks!
This looks like an odd way to do things, especially the part where you're using jQuery. That's a client-side utility to overcome not knowing where everything is and not having direct access to your elements. It makes no sense to use it when you're using React weith Webpack for bundling: React already knows where everything is (using refs) and Webpack means you can just use regular universal Node modules for everything that you need to do.
I'd recommend using something like, using request or a similar universal fetch API:
// loadData.js
var request = require('request');
var loadData = function(urlYouNeed, handler) {
request(urlYouNeed, function(error, response, body) {
if (error) {
return handler(error, false);
}
// do anything processing you need on the body,
var data = process(body);
handler(false, data);
};
So: just a module you can require in any component you define with require('./loadData'). And then in your actual component you do this:
var loadData = require('./loadData');
var AppComponent = React.createClass({
getDefaultProps: function() {
return {
jsonURL: "cdn://whateverjson.json"
};
},
getInitialState: function() {
loadData(this.props.jsonURL, this.updateData);
return {
data: []
}
},
updateData: function(err, data) {
if (err) {
return console.error(err);
}
data = secondaryEnsureRightFormat(data);
this.setState({ data: data });
},
render: function(){
var actualThings = this.state.data.map((entry, pos) => {
return <Whatever content={entry} key={entry.dontUseThePosVariableUpThere}/>
});
return (
<div>
...
{actualThings}
...
</div>
);
}
});
Much cleaner.
If I understand correctly the question, you only have to change your loadData this way :
loadData: function() {
var c = this
jQuery.getJSON(jsonFile.json?callback=?)
.done(function(data) {
c.handleData(data)
});
},
handleData: function(data) {
/* Implement here the function to handle the data */
},
I have a simple component that it's going to fetch data after the component is inserted. It was ok until I run my test. I got this error.
Assertion Failed: You have turned on testing mode, which disabled the run-loop's autorun. You will need to wrap any code with asynchronous side-effects in a run
I understand that the component is fetching data asynchronously but I'm not sure how to solve that in my integration test.
Here's my code
export default Ember.Component.extend({
didInsertElement: function() {
let source = this.get('source');
let url = apiUrl + source;
Ember.$.getJSON(url).then(function(response) {
this.set('data', response.data);
}.bind(this));
},
// something else
};
And this is my test.
moduleForComponent('panel', 'Integration | Component | panel', {
integration: true,
beforeEach () {
this.render(hbs`{{panel}}`);
}
});
test('it has source dropdown', function(assert) {
assert.equal(this.$('select[name="Source"]').length, 1);
});
Without the fetching data bit, the test runs ok.
Try wrapping the getJSON inside an Ember run loop.
So something like this:
var self = this;
Ember.run(function(){
Ember.$.getJSON(url).then(function(response) {
self.set('data', response.data);
});
});
Create a new Promise and then set the response data in the success handler.
The promise returned by Em.$.getJSON and new Ember.RSVP.Promise are different.
let self = this;
let promise = new Ember.RSVP.Promise(function() {
return Ember.$.getJSON(url);
});
promise.then((json) => {
this.set('data', json);
});
I've inherited a Cordova/PhoneGap app running Cordova 3.4. My first task was to implement a Client-Side Routing framework to make it easier to navigate between pages. I chose Flatiron Director as my client-side router, but when I went to implement it I started to get weird functionality out of the app.
My first router setup:
var routing = {
testHandler: function(){
console.log('Route ran');
},
routes: function(){
return {
"/testhandler": testHandler
}
}
};
console.log('Routes added');
The routes are added (at least based on the console output). When I attempt to hit the /testhandler hash, I receive a "Failed to load resource: file:///testhandler" error when I set window.location.hash to "/testhandler". I noticed the "Route ran" statement was never printed.
My next attempt was just using the hashchange event with jQuery.
$(window).on('hashchange', function(){ console.log('Ran'); });
On this attempt, regardless of what I change the hash to, I see the 'Ran' output, but I still receive the "Failed to load resource: " error.
Is this a problem with PhoneGap/Cordova? Or our implementation? Is it just not possible to use client-side routing with Cordova? What am I doing wrong?
I know that this doesn't answer your question directly but you may consider making your own provisional router. This may help you to debug your app and to figure out what's the problem.
Something like this for example:
var router = (function (routes) {
var onRouteChange = function () {
// removes hash from the route
var route = location.hash.slice(1);
if (route in routes) {
routes[route]();
} else {
console.log('Route not defined');
}
};
window.addEventListener('hashchange', onRouteChange, false);
return {
addRoute: function (hashRoute, callback) {
routes[hashRoute] = callback;
},
removeRoute: function (hashRoute) {
delete routes[hashRoute];
}
};
})({
route1: function () {
console.log('Route 1');
document.getElementById('view').innerHTML = '<div><h1>Route 1</h1><p>Para 1</p><p>Para 2</p></div>';
},
route2: function () {
console.log('Route 2');
document.getElementById('view').innerHTML = '<div><h1>Route 1</h1><p>Para 1</p><p>Para 2</p></div>';
}
});