How can I apply dynamic locale change in AngularJS tests? - javascript

In an AngularJS application using angular-dynamic-locale, I want to test locale-dependent code in different locales.
I am trying to change the locale in an asynchronous setup. In previous Jasmine versions, this was done with a latch function; these are now deprecated and replaced with the done callback.
beforeEach(function (done) {
inject(function ($injector) {
tmhDynamicLocale = $injector.get('tmhDynamicLocale');
console.log('setting locale to French...');
tmhDynamicLocale
.set('fr-fr')
.then(function () {
console.log('locale set.');
done();
})
.catch(function (err) {
done.fail('failed to set locale:', err);
});
});
});
The promise returned by tmhDynamicLocale.set remains pending forever, and the setup times out.
Looking at the innards of tmhDynamicLocale shows that the locale change is scheduled, but never actually applied:
// This line runs
$rootScope.$applyAsync(function() {
// But this callback doesn't
storage[storagePut](storageKey, localeId);
$rootScope.$broadcast('$localeChangeSuccess', localeId, $locale);
deferred.resolve($locale);
});
I have tried calling $browser.defer.flush and digesting/applying the root scope, but the locale change callback is still not executed.
How can I change the locale in this test suite?
Plunker, with logs in tmhDynamicLocale added for clarity.

There doesn't seem to be a clean way to do it, but I've found a hackish workaround.
Why the promise doesn't settle
tmhDynamicLocale loads the locale file via a <script> element. When the load event of this element fires, it schedules the $localeChangeSuccess event and promise resolution.
In production, this works well:
My code calls tmhDynamicLocale.set('fr-fr')
The <script> element is created
The locale file is loaded; load fires
In the load event callback, a new callback is scheduled to be applied asynchronously
The completed-request event also triggers an Angular digest cycle
The digest cycle runs the scheduled callback
This callback resolves the locale change promise
In tests, browser events do not trigger digest cycles, so this happens:
My code calls tmhDynamicLocale.set('fr-fr')
The <script> element is created
Any $timeouts in my test code run here, with an empty $$applyAsyncQueue
The locale file is loaded; load fires
In the load event callback, a new callback is scheduled to be applied asynchronously
My code is not notified of this; it has no way to trigger the new callback, and the promise pends forever
So triggering the promise resolution can't be done inside the Angular digest cycle, or at least I don't see how.
Workaround
Since we can't tell when the callback is scheduled, poll until it is. Unlike $interval, setInterval is not mocked out in tests and allows real polling.
Once the callback is scheduled, $browser.defer.flush will run it, and trigger promise resolution. It throws if there are no deferred tasks to flush, so only call it if the queue is non-empty.
_forceFlushInterval = setInterval(function () {
if ($rootScope.$$applyAsyncQueue.length > 0) {
$browser.defer.flush();
}
});
Conceivably, tasks other than the locale change could be scheduled. So we keep polling until the promise is settled.
tmhDynamicLocale.set('fr-fr')
.then(function () {
clearInterval(_forceFlushInterval);
done();
})
.catch(function () {
clearInterval(_forceFlushInterval);
done.fail();
});
But there are many drawbacks:
Accessing $$private Angular internals is not a good idea, and is likely to break between versions.
Constantly flushing may interfere with other asynchronous setup.
If for some reason the interval doesn't get cleared, it will wreak all kinds of havoc on later tests.
Plunker

Related

Expect jasmine Spy to be called "eventually", before timeout

I've written a lot of asynchronous unit tests lately, using a combination of Angular's fakeAsync, returning Promises from async test body functions, the Jasmine done callback, etc. Generally I've been able to make everything work in a totally deterministic way.
A few parts of my code interact in deeply-tangled ways with 3rd party libraries that are very complex and difficult to mock out. I can't figure out a way to hook an event or generate a Promise that's guaranteed to resolve after this library is finished doing background work, so at the moment my test is stuck using setTimeout:
class MyService {
public async init() {
// Assume library interaction is a lot more complicated to replace with a mock than this would be
this.libraryObject.onError.addEventListener(err => {
this.bannerService.open("Load failed!" + err);
});
// Makes some network calls, etc, that I have no control over
this.libraryObject.loadData();
}
}
it("shows a banner on network error", async done => {
setupLibraryForFailure();
await instance.init();
setTimeout(() => {
expect(banner.open).toHaveBeenCalled();
done();
}, 500); // 500ms is generally enough... on my machine, probably
});
This makes me nervous, especially the magic number in the setTimeout. It also scales poorly, as I'm sure 500ms is far longer than any of my other tests take to complete.
What I think I'd like to do, is be able to tell Jasmine to poll the banner.open spy until it's called, or until the test timeout elapses and the test fails. Then, the test should notice as soon as the error handler is triggered and complete. Is there a better approach, or is this a good idea? Is it a built-in pattern somewhere that I'm not seeing?
I think you can take advantage of callFake, basically calling another function once this function is called.
Something like this:
it("shows a banner on network error", async done => {
setupLibraryForFailure();
// maybe you have already spied on banner open so you have to assign the previous
// spy to a variable and use that variable for the callFake
spyOn(banner, 'open').and.callFake((arg: string) => {
expect(banner.open).toHaveBeenCalled(); // maybe not needed because we are already doing callFake
done(); // call done to let Jasmine know you're done
});
await instance.init();
});
We are setting a spy on banner.open and when it is called, it will call the callback function using the callFake and we call done inside of this callback letting Jasmine know we are done with our assertions.

Can the Web SDK child_changed return a promise?

With the firebase Web SDK you can do:
commentsRef.on('child_added', function(data) {
addCommentElement(postElement, data.key, data.val().text, data.val().author);
});
However I wonder if it's possible to return a Promise instead of the callback above? Just like the following works:
this.productRef.once('value') // Attach listener
.then(result => {
console.log(result.val())
})
Thanks for the awesome work!
Promises are meant to be asynchronous tasks that complete exactly once. So I do something, and then it either gets resolved or rejected, and then that's it.
The child_added event, on the other hand, will fire zero to many times for each time the child changes. This is not suitable for a Promise, and so instead acts as a stream of events, not a single asynchronous task.

Can I resolve a promise from a broadcasted event?

We're using angular legacy (1.5)
I am trying to bulk load some layers using a 3rd party library.
I need to wait till they are loaded before continuing.
So in my get data section, it calls the library and asks it to add data, I start a $q.defer in this section and assign this to a factory level variable.
In the service for the 3rd party lib, I setup a count for requests out and requests in, when they match, the $broadcast and event to tell me its complete.
I then listen ($on) for this event and set the promise to resolved.
however the application doesn't wait for this.
I understand this is a strange one, but what can I do.
Our code is quite involved, so I have tried to create crude example of what we are trying to archive.
function layerFactory($rootScope, $log, $q, DataService) {
var factory = {
getData:getData,
var _dataPromise;
function getData(data){
_getLayerData(data).then(function(){
_processData(data);
});
}
function _getLayerData(data){
_dataPromise = $q.defer();
DataService.getData(data) // Treat DataService as a 3rd party lib, this doesn't return a promise. I have no way of knowing this is complete until a $broadcast is sent.
_dataPromise.promise;
}
$rootScope.$on('dataLoaded', function(){
_dataPromise = $q.resolve();
});
}
return factory;
}
This isn't waiting for the promise to resolve and instead going into the 'then' statement and processing the next function 'too early' I need it to wait till the first function as finished.
Any ideas?
Ok, I couldn't find a way to make this work, so what I did instead was to set a factory level variable (boolean) to indicate when the loading had started, this was then set to false when the $on event was triggered.
In my getLayerData method, I set up an internal to run every 500 ms, inside this interval function I run a check for the loading variable, if false (ie loaded), then return a deferred.resolve() and cancel the interval.

How do I use Mocha/Chai to start checking for DOM elements after a JavaScript Object create it?

Would I need to use a setTimeout? (And it works when I do). The problem I'm having is that the FeedObject does an asynchronous call, so when a new instance of it is created, it takes sometime before it can inject itself to the DOM.
describe('Default Case', function () {
before(function () {
$divHolder = $('#divHolder');
$divHolder.html('');
var myComponent = new FeedObject({
div: $divHolder[0],
prop: {
id: 'myFeed',
},
client: {}
});
myComponent.createDOMElements();
});
it('should exist', function () {
console.log($divHolder.find('.feed-item')); // This does not exist unless I use a timeout
});
EDIT: Just to clarify, the new FeedObject does an AJAX call.
What you need to determine when new FeedObject is done. Depending on what FeedObject provides, it could mean:
Providing a callback that it called when new FeedObject is done. Call done in the callback.
Listening to an event (most likely a custom jQuery event). Call done in the event handler.
Make the before hook return a promise that exists on the object returned by new FeedObject. For instance, the ready field could contain this promise, and you'd do return myComponent.ready. Mocha will wait for the promise to be resolved. (Remove the done argument from your before hook.)
I strongly suggest that you add one of the facilities above rather than use setTimeout. The problem with setTimeout is that it is always suboptimal: either you wait longer than you need for the DOM to be ready, or you set a timeout which is super close to the real time it take to be ready but in some cases the timeout will be too short (e.g. if your machine happens to have a spike of activity that does not leave enough CPU for the browser running the test code) and the test will fail for bogus reasons.
Well setTimeOut will work (until the server response takes longer than the time you specified and then it won’t work) but it’s definitely not the best or fastest way to do it. When you’re using an async call, you need to define a callback method that runs after the code is finished. The idea of the code should be something like this:
describe('Default Case', function () {
// Your code goes here
$.ajax = function(ajaxOpts)
{
var doneCallback = ajaxOpts.done;
doneCallback(simulatedAjaxResponse);
};
function fetchCallback(user)
{
expect(user.fullName).to.equal("Tomas Jakobsen");
done();
};
fetchCurrentUser(fetchCallback);
});
If you need even more details you could check this very helpful link. Hope that helps!
I would use this async chai plugin. If you are using a promise api (probably the most straight-forward way to handle async testing), this makes testing async ops very simple. You just have to remember to return any invocation that is async (or returns a promise) so chai knows to 'wait' before continuing.
describe(() => {
let sut
beforeEach(() => {
sut = new FeedObject()
return sut.createDomElements() // <- a promise
})
it('should exist', () => {
$('#target').find('.feed-item').should.exist
})
})
Also consider the goals of this testing: why are you doing it? I find that a lot of DOM insert/remove/exists testing is wasted effort. This is particularly true if you are testing a 3rd party library/framework as part of your application code. The burden of proof is on the library/framework to prove it is correct (and most well-written libraries already have a testing suite), not your app. If you are concerned about testing 'did I correctly invoke this 3rd party code' there are better ways to do that without touching the DOM.

What do jasmine runs and waitsFor actually do?

I use jasmine runs and wait to test asynchronous operations. Everything works fine but I'm not quite sure what goes on behind the scenes.
The jasmine documentation states the following example to which I added three log statement.
describe("Asynchronous specs", function() {
var value, flag;
it("should support async execution of test preparation and exepectations", function() {
runs(function() {
flag = false;
value = 0;
setTimeout(function() {
flag = true;
}, 500);
});
waitsFor(function() {
value++;
if(flag) {
console.log("A");
}
return flag;
}, "The Value should be incremented", 750);
console.log("B");
runs(function() {
console.log("C");
expect(value).toBeGreaterThan(0);
});
});
});
});
The first runs and waitsFor are perfectly clear to me. Runs starts an asynchronous operation and waitsFor waits for a condition.
However I do not understand why the second runs does not start until the waitsFor is finished. The waitsFor is not a blocking call.
My guess is that waitsFor implicitly blocks any following runs call until it is finished. Is this so?
My evidence is that the console.log statements output:
B A C
But if waitsFor would really block it should be
A B C
waitsFor does block until the conditions it's waiting for are met or it times out.
From the jasmine docs: "waitsFor() provides a better interface for pausing your spec until some other work has completed. Jasmine will wait until the provided function returns true before continuing with the next block.".
The linked docs also have a slightly clearer example or waitsFor.
EDIT: Ah I see what you mean now. waitsFor won't block JS that isn't wrapped in runs, waitsFor, ect.
What jasmine is doing is taking the function passed to it via runs or waitsFor and if jasmine is not currently waiting, it executes the function immediately. If it is waiting, it doesn't call it until it's finished waiting.
That doesn't stop the console.log as it's been passed to jasmine so jasmine can't prevent it from being executed straight away.
The solution is in the documentation:
Multiple runs() blocks in a spec will run serially. (Jasmine Documentation)
From the site: http://www.htmlgoodies.com/beyond/javascript/test-asynchronous-methods-using-the-jasmine-runs-and-waitfor-methods.html#fbid=mzNDUVfhFXg
Jasmine will call the runs() and waitsFor() methods in the order you
passed them. As soon as the JS parser gets to a waitsFor() method it
will poll it until it returns true and only then will it continue onto
the next runs() method.
Essentially, the runs() and waitsFor() functions stuff an array with their provided functions. The array is then processed by jamine wherein the functions are invoked sequentially. Those functions registered by runs() are expected to perform actual work while those registered by waitsFor() are expected to be 'latch' functions and will be polled (invoked) every 10ms until they return true or the optional registered timeout period expires. If the timeout period expires an error is reported using the optional registered error message; otherwise, the process continues with the next function in the array. Presumably, expects within the runs() can also trigger a failure report (and perhaps even in the latch functions themselves).
The documentation is particularly obtuse on these asynchronous features.

Categories

Resources