How to test actions in Flux/React? - reactjs

I'm trying to figure out how to test actions in flux. Stores are simple enough with the provided example, but there seems to be nothing out there for the actions/data/api layer.
In my particular app, I need to pre-process something before posting it to my server. Based on the advice in this post, I decided to implement the async stuff in my actions. What I can't figure out is how to test this preprocessing.
For example in MissionActions.js:
addMissionFromBank: function(bankMission) {
var mission = new Mission({game: GameStore.getGame().resource_uri, order: Constants.MISSION_ORDER_BASE_INT}).convertBankMission(bankMission);
var order = MissionSort.calcOrderBySortMethod(mission, MissionStore.getMissions(), GameStore.getGame().sort_method);
mission['order'] = order;
AppDataController.addMissionFromBank(mission);
},
In this function, I'm converting a stock mission (bankMission) into a regular mission and saving it to a game with the correct order key. Then I'm posting this new regular mission to my server, the callback of which is handled in my MissionStore.
Since this conversion code is important, I want to write a test for it but can't figure out how to do it since there seems to only be examples for testing stores and React components. Any suggestions?

Are you using the flux dispatcher or requiring AppDataController?
Jest will automatically mock modules that you bring in via browserify's require. If you are importing AppDataController via require, then your test might look like this:
jest.dontMock('MissionAction.js') // or path/to/MissionAction.js
describe('#addMissionFromBank', function(){
beforeEach(function(){
MissionAction.addMissionFromBank(exampleMission);
});
it('calls AppDataController.addMissionFromBank', function(){
expect(AppDataController.addMissionFromBank).toBeCalled());
});
});
you want to call the non-mocked method that youre testing and check that the mock is called. to check if its been called at all use
#toBeCalled()
or if you want to check that its called with a specific value (for instance, check that its called with whatever mission evaluates to) use
#toBeCalledWith(value)

You could mock/spyOn AppDataController, and check that it receives a correct mission object. Something like this in jasmine, I'm not sure if it is the same in jest:
spyOn(AppDataController, 'addMissionFromBank');
MissionActions.addMissionFromBank(someBankMission);
expect(AppDataController.addMissionFromBank).toHaveBeenCalledWith(expectedMission);

Related

Test multiple listeners with Jasmine

I need to test every listener in a controller with Jasmine 2.0, really this question is just to reinforce my logic, and perhaps there is a more elegant way to approach testing listeners, or maybe I am being too thorough!
This might be a better question for codereview but I'll leave it here. How to properly test multiple keypress/event listeners in a controller effectively?
it("should trigger the correct actions from key events", function () {
var listenerSpy = jasmine.createSpy('listenerSpy');
angular.forEach(scope.$$listeners, function (fn, eventName) {
listenerSpy(eventName, fn);
expect(listenerSpy).toHaveBeenCalledWith(eventName, fn);
});
});
What you have above is not really testing anything other than JavaScript itself. You are calling a function and then expecting that you just called that function.
A code coverage report would show that the listener function has not executed at all.
Without seeing the code you are testing, I am unable to properly advise on how to structure your test.
There are two possible intentions:
1) Do you want to test that the scope is listening to a set of known elements?
2) Do you want to test the outcome of listener executions?
Usually, it would be best to take path number two, because with it, you also get number one.
Are all of your listeners performing the same action?
If they are, it may make sense to loop through a list of known elements and change them to verify the proper listener execution output.
If the listeners perform differently, each execution's output should be evaluated.

Call translation service from a callback registered in an app.config section

I'm relatively new to AngularJS and the problem I'm facing is one of those "I want to inject a Service into an app.config" type of scenarios, which I realise cannot be done. (I'm comfortable with the different between Service and Provider, and why a Service cannot be injected into a .config.)
What I am trying to accomplish is to use angular-schema-form together with angular-translate such that field titles in generated forms are translated.
There is an issue where someone asks how to do this, and the advice given is to take advantage of angular-schema-form's postProcess, which is a property of the Provider. This callback gives you the form object before it is rendered, giving you the opportunity to manipulate it with user code. Therefore translation could be done within here.
The postProcess method is called on the Provider, so it is done within an app.config:
app.config(function(schemaFormProvider, $translateProvider) {
schemaFormProvider.postProcess(function(form){
// within here I can inspect the form object, find all
// properties whose key is "title", and then perform
// language translation on their values.
So, that is apparently the place where I have an opportunity to manipulate control titles and so on.
Over to the angular-translate library, for me to 'manually' translate strings, I can use the $translate service. This provides both synchronous and asynchronous methods to translate a given key string. The synchronous one is $translate.instant(key).
To glue these two together, what I have tried so far (which does work) is to create a 'bridge' method like this:
var app = angular.module('myApplicationName', ['schemaForm', 'pascalprecht.translate']);
....
app.config(function(schemaFormProvider, $translateProvider) {
schemaFormProvider.postProcess(function(form){
// ... code here which iterates over properties
// and finds all control titles ...
key = app.myTranslate(key);
// ....
}
....
});
app.myTranslate = function (key) {
var service = angular.injector(['ng', 'myApplicationName']).get("$translate");
return service.instant(key);
}
This does work, but it seems ugly and unsafe (as presumably there's no guarantee $translate is ready when the callback is first invoked) and the calls to angular.injector(['ng', 'myApplicationName']).get... are presumably expensive.
Is there a better way, or is this the only way I'm going to get it done, considering the constraints of the libraries I'm working with?
I have also considered an alternative approach altogether, which would be to instead perform the translations on the schema or form objects before they are processed by angular-schema-form. This could be done from within Controllers, eliminating the problem of accessing the $translate service. I may end up going down that route, but it would still be nice to understand the best solution for the above scenario.

how to not duplicate httpBackend code in angular tests?

I have some services that call for data while loading.
so my tests fail because there are unexpected calls that I need to specify in $httpBackend.
this causes a lot of duplicate code in my tests.
part of my attempts to reduce the duplicated code, I decided to add $rootScope.test flag, and if this flag is on those services do not load the data but still I need to duplicate $rootScope.test=true all over the tests.
Is there a way to do this properly in angular tests?
Here is some code
$httpBackend.expectGET('/backend/system/translations/en.json').respond({'angularjs': 'cool'});
$httpBackend.expectGET('/backend/system/translations/he.json').respond({'angularjs': 'cool'});
$httpBackend.expectGET('/backend/system/translations/ru.json').respond({'angularjs': 'cool'});
$httpBackend.expectGET('/backend/system/translations/ar.json').respond({'angularjs': 'cool'});
$httpBackend.expectGET('/translations/general.json').respond({'angularjs': 'cool'});
i18n = $filter('i18n');
Every directive I have with some translation support require these statements per language.
To handle every url in the same way with just one statement try sth like this:
$httpBackend.expectGET(function(url){
return true;
}).respond({'angularjs': 'cool'});
From documentation first argument of expectGET is:
HTTP url or function that receives the url and returns true if the url
match the current definition.
If you want to define response to just translation urls, try sth like this:
$httpBackend.expectGET(function(url){
return url.lastIndexOf("/backend/system/translations/", 0) === 0;
}).respond({'angularjs': 'cool'});
Also, are you sure that you need to use expectGET() instead of whenGET()? If you don't care whether given urls were called or not, how many times and in which order and you just want to define responses, then when... methods are the way to go:
See: "Request Expectations vs Backend Definitions" section of the mentioned doc page.
Backend definitions allow you to define a fake backend for your
application which doesn't assert if a particular request was made or
not, it just returns a trained response if a request is made. The test
will pass whether or not the request gets made during testing.

Angular translate extend existing translations

I am trying to have external modules change my $translateProvider.translation on the main module. see this as a "tranlation plugin" for my app.
it seems like changing translations from the $translate service is not possible.
mymodule.service('MyService', function ($translateProvider) {
var lib = function () {
//EDITED FOR BREVITY
this._registerTranslations = function (ctrl) {
if (!ctrl.i18n) return;
for (var name in ctrl.i18n) {
/////////////////////////////
// THIS IS THE PLACE, OBVIOUSLY PROVIDER IS NOT AVAILABLE!!!!
$translateProvider.translations(name, ctrl.i18n[name]);
//////////////////////////////
}
};
//EDITED FOR BREVITY
};
return new lib();
});
anyone with a bright idea?
So, to answer your question: there's no way to extend existing translations during runtime with $translate service without using asynchronous loading. I wonder why you want to do that anyway, because adding translations in such a way means that they are already there (otherwise you would obviously use asynchronous loading).
Have a look at the Asynchronous loading page. You can create a factory that will load a translation from wherever you want.
I created an Angular constant to hold new translations. If I want to add a new translation, I add it to the constant. Then in my custom loader, I first check the constant to see if the translation exists (either a new one, or an updated one). If so, I load it from the constant. If not, I load it from a .json file (or wherever you load your initial translations from). Use $translate.refresh() to force translations to be reloaded and reevaluated.
Demo here
The demo is pretty simple. You would need to do a little more work if you wanted to just change a subset of the translations, but you get the general idea.
From the AngularJS docs (https://docs.angularjs.org/guide/providers):
You should use the Provider recipe only when you want to expose an API for application-wide configuration that must be made before the application starts. This is usually interesting only for reusable services whose behavior might need to vary slightly between applications.
Providers are to be used with the application's .config function. $translateProvider for configuration, $translate for other services and controllers.

End-to-End testing with Jasmine

I am trying to perform some end-to-end tests of an application written with AngularJS. Currently, I have the following tests setup in e2e.tests.js:
describe('MyApp', function() {
beforeEach(function() {
browser().navigateTo('../index.html');
});
it('should be true', function() {
expect(true).toBe(true);
});
});
Oddly, this basic test fails. The test itself runs. However the results are:
203ms browser navigate to '../index.html'
5ms expect undefined toBe true
http://localhost:81/tests/e2e.tests.js:10:9
expected true but was undefined
Considering "true" is hard-coded, I would expect it to pass this test. I have no idea how true can be undefined. What am I missing?
The thing about Angular's E2E test framework is that looks like Jasmine, but it is not Jasmine. The E2E test code is actually all asynchronous, but it is written in a clever way so that the calls look normal. Most of the calls create asynchronous tasks and Future objects that are tested later. The Future object is kind of like a promise, but it's a little different. It has a value property that it sets when it's ready, and then it calls a done function to move to the next step. In E2E tests, the expect function takes Future objects, not values. You're seeing undefined because expect is testing against future.value, which in this case is true.value, which is undefined.
Try using one of the available selectors that return futures and then test the result. Something like this:
expect(element("html").text()).toMatch("Our App");
The Future objects are not well documented, but you should be able to create a Future manually like this:
var trueFuture = angular.scenario.Future(
"a true value", // name that is displayed in the results
function(callback) { // "behavior" function
callback(null, true); // supposed to call callback with (error, result)
});
expect(trueFuture).toEqual(true);
If you look in the ng-scenario source code, you can see the place where the matcher tests future.value in the angular.scenario.matcher function.
I too have faced the similar problem and here's what I found.
You must be using ng-scenario as your framework with jasmine in config file.
The fact is that expect function in ng-scenario doesn't take any var value or Boolean value. It only takes functions like
expect(browser().location().path()).toContain('some/string')
or some other ng-scenario function like
expect(element('some element').html()).toContain('some String');
Any variable value or Boolean in expect function is undefined.
If you want to use Boolean(true/false) or you want your test to be passed then you have to remove 'ng-scenario' from your framework section of config file.
Try It with only jasmine!

Resources