EmberJS 2.13 unit testing: wrapping synchronous code in a run loop? - ember.js

This is repeated a few times in the current Ember documentation, so I feel like I must be missing something. Let's take the simplest example I found.
Why is the call to levelUp considered asynchronous to warrant wrapping it in the run loop?
incrementProperty is synchronous, and as far as I can tell, so is set (but I could be mistaken here)
player.js
import DS from 'ember-data';
export default DS.Model.extend({
level: DS.attr('number', { defaultValue: 0 }),
levelName: DS.attr('string', { defaultValue: 'Noob' }),
levelUp() {
let newLevel = this.incrementProperty('level');
if (newLevel === 5) {
this.set('levelName', 'Professional');
}
}
});
player-test.js
import { moduleForModel, test } from 'ember-qunit';
import Ember from 'ember';
moduleForModel('player', 'Unit | Model | player', {
// Specify the other units that are required for this test.
needs: []
});
test('should increment level when told to', function(assert) {
// this.subject aliases the createRecord method on the model
const player = this.subject({ level: 4 });
// wrap asynchronous call in run loop
Ember.run(() => player.levelUp());
assert.equal(player.get('level'), 5, 'level gets incremented');
assert.equal(player.get('levelName'), 'Professional', 'new level is called professional');
});

First of all, you are absolutely right. It is not well-described anywhere in the guides.
In testing mode, autorun is disabled. You can read further from the guides about this.
But changing the value in model triggers a run-loop. You can see that at this twiddle. The result is:
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
(By the way, both set and incrementProperty trigger this run-loop as your guess.)
Then here is the run loop source:
DS.attr returns a computed property with set.
The set function triggers an event.
At the end, a run loop is triggered.

#ykaragol is absolutely right about his explanation within his correct answer and I have nothing to add why you need to wrap your code within a run loop; because the source code is there and emberRun.schedule is being called, which requires a run-loop.
What I would like to explain is a bit more about the assertion error you get: "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". That does not directly mean an asynchronous operation (in the sense that an ajax call is made, or a timer is triggered) is in place. We are mostly unaware but; Ember.js does use Ember.run loops and various run queues such as sync, actions, render, afterRender, etc. in order to schedule the effects of our codes so as to optimize the rendering of our application. Even if the code this.set('levelName', 'Professional'); does seem like pretty synchronous; Ember wraps it within a run-loop so that the computed property calculation, or other updates are buffered together in order to prevent multiple rendering (hence decreased performance) of the templates.
I only wished there was better explanation about both run loop, run queues, or how and why to use run loops within tests, but there is not :(

Related

Ember 3.0 acceptance redirect test hangs forever

I have a simple acceptance test written in the modern RFC 268 format for Ember 3.0.
The test is for a page where, if the user is unauthenticated, the URL immediately redirects to /login.
...
module('Acceptance | index', function (hooks) {
setupApplicationTest(hooks);
test('visiting / logged out', async function(assert) {
assert.expect(1);
// Test hangs here forever.
await visit('/');
assert.equal(currentURL(), '/login');
});
});
This test worked great using the older format with moduleForAcceptance.
Unfortunately, this test hangs forever in Ember 3.0. Am I missing something here? Is there a better way to test a redirect?
There are no errors in the console, and the addition of some console.log statements show that the await is where the test hangs.
I found the reason why this was failing. I have an Ember mixin that I use to enhance all of my routes. The mixin checks for whether or not a user is authenticated, and redirects to /login as needed.
export default Mixin.create({
session: service(),
beforeModel() {
this._super(...arguments);
return new EmberPromise((resolve) => {
if (authenticated) {
resolve();
return;
}
this.transitionTo('login');
});
}
});
You'll notice that I am not resolving if authenticated is falsey. That worked fine with my app and the test syntax in 2.18.
The docs say the following regarding that hook I am overriding in my mixin.
returns Any | Promise
if the value returned from this hook is a promise, the transition will pause until the transition resolves. Otherwise, non-promise return values are not utilized in any way.
To me, the bit about "non-promise return values" implies that I should be able to do what I'm doing. Especially considering this worked in 2.18, but I wonder if this was one of those "wow, how did that ever work in the first place" scenarios. Clearly this syntax isn't working in 3.0. since the transition pauses forever when testing.
The answer for me was to ensure I always resolve/reject something. In this case, I had to add an explicit reject() so that the promise chain doesn't hang.
export default Mixin.create({
session: service(),
beforeModel() {
this._super(...arguments);
return new EmberPromise((resolve) => {
if (authenticated) {
resolve();
return;
}
this.transitionTo('login');
reject();
});
}
});
My test was fine. It was the mixin that needed updating in order to work properly with Ember 3.0 and the latest testing syntax.
The issue was not what your beforeModel hook resolves if user is authenticated but that your Promise does not resolve at all. You don't have to return a Promise in beforeModel hook but if you return one it will block the transition until the Promise is resolved. Since it's not clear how ember should react if another transition is called while current transition is blocked (not resolved/rejected promise), resolving or rejecting is correct behavior. Please have in mind that in a Promise return does not have any other meaning than ending your execution. It does not resolve or reject your Promise.
Another reason visit() could hang is that it waits for timers like Ember.run.later() to resolve, causing a non-obvious block somewhere in the application.
AlphaGit on github summarized the issue with an example, saying:
Most of the actions that Ember.testing executes (like visit) will
append to a promise that gets executed action after action in the
right order. In order to pass to the next action, Ember.testing makes
sure that there is nothing pending, so that the step can be considered
complete and move forward.
Along with the things that are tested for, pending AJAX requests are
verified, and also scheduled timers. These timers may arise from, you
guessed it, Ember.run.later calls. If for any reason you would have in
your code periodic Ember.run.later methods (so that one is always
waiting to be excuted), it's likely that you'll face this issue.
I've faced it myself in a similar scenario: My server returns a OAuth
access token with 100 hours until expired, so ember-simpleAuth
registers a call close to the expiration time with Ember.run.later to
refresh the token. This will, however, prevent the test from moving
along. My specific situations has been fixed in further versions but
any similar behavior will reproduce the issue (which is likely a
conclusion of the current design of Ember.testing).
Here are a couple other examples of users running into similar issues:
https://stackoverflow.com/a/58526993/3257984
https://stackoverflow.com/a/27887807/3257984

Ember/emberfire run loop acceptance test

So my acceptance test keeps on destroying itself before my promise finishes. I'm aware that I need to wrap my promise in Ember run loop but I just can't get it to work. Here's how my component looks:
export default Ember.Component.extend({
store: Ember.inject.service(),
didReceiveAttrs() {
this.handleSearchQueryChange();
},
/**
* Search based on the searchQuery
*/
handleSearchQueryChange() {
this.get('store').query('animals', {
orderBy: 'name',
startAt: this.attrs.searchQuery
}).then(searchResults => {
this.set('searchResults', searchResults);
});
}
});
I've already tried wrapping this.handleSearchQueryChange(), this.get('store').query... and this.set('searchResults', searchResults) in a run loop but still, the acceptance test just doesn't wait for store.query to finish.
One thing to note that this store query performs a request on a live Firebase back-end.
I'm currently using Pretender to mock the data and solve this issue. But I'd like to solve it through Ember.run as well. Anyone care to provide a solution?
It sounds like your problem may have the same cause as the errors I've been seeing
tl;dr
To work around this issue, I've been using a custom test waiter. You can install it with ember install ember-cli-test-model-waiter (for Ember v2.0+) and it should just make your test work without any further setup (if not, please file a bug).
Longer answer:
The root cause of this problem is that the ember testing system doesn't know how to handle Firebase's asynchronicity. With most adapters, this isn't a problem, because the testing system instruments AJAX calls and ensures they have completed before proceeding, but this doesn't work with Firebase's websockets communication.
The custom test waiter I mentioned above works by waiting for all models to resolve before proceeding with the test, and so should work with any non-AJAX adapter.

Can't figure out how to wrap function in Ember run loop

I have a component that integrates 2 third-party libraries, imagesLoaded and Isotope.
I get conflicting test failures when running tests in the browser and cli mode. The error is:
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
or
TypeError: 'undefined' is not an object (evaluating 'self.$().isotope')
When I try to wrap callbacks in an ember run loop, they pass in cli-mode, but then fail in browser mode. I can't seem to find the right combination. The issue seems to happen in the callback of imagesLoaded, as if I remove that plugin, it seems to pass fine.
I've tried multiple combinations, but here is my latest code. If anyone has insight on how to properly use the run loop in this component, that would be helpful.
handleLoad: function() {
let self = this;
Ember.run.scheduleOnce('afterRender', this, function(){
self.$().imagesLoaded( function() {
Ember.run(function() {
self.$().isotope({itemSelector: ".card-container"});
self.$().isotope('shuffle');
});
}); // imagesLoaded
}); // Ember.run
}.on('didInsertElement')
You'll have to wrap your existing code in a
Ember.run(function () {
// Ember.run.scheduleOnce('afterRender....
});
as this tells Ember where the run loop starts and ends - in production/development this is done for you by Ember itself, but in testing you have to wrap it.
As an alternative you can start & end a run loop manually by calling it explicitly (see the API Docs):
Ember.run.begin() // <-- tell Ember that your starting a run loop
//Ember.run.scheduleOnce('afterRender', ...
//more ember run loops here
Ember.run.end(); // <-- tell Ember that the run loop ends here

Why does this controller seem to "hold onto state" even between tests?

I'm writing a simple qunit test that has a controller setup w/ a few items in an array
App.UploadController = Ember.Controller.extend({
to_upload: Ember.A([])
});
I add stuff to this "to_upload" array and verify it between tests. One issue I've found that that if I don't "manually clear it" like this in my test "tear down" it holds onto this state (even with an App.reset() being called in each teardown (calling destroy).
App.__container__.lookup("controller:upload").clear_files()
(the clear files is a simple monkey patch I added in my test helper -not production code)
App.UploadController.reopen
clear_files: function() {
this.get('to_upload').clear();
}
Here is my tear down for the qunit tests
module "/upload",
teardown: function() {
App.reset()
App.__container__.lookup("controller:upload").clear_files()
}
It's probably related to the fact that assigning an array to a property in the class definition makes it a class property and not an instance property. I'm not sure if this is documented somewhere, I've read it before a few times though. I'll update with documentation if I can find it.
Until then you can probably prove my theory by modifying the code to look like this.
App.UploadController = Ember.Controller.extend({
init: function(){
this.set('to_upload', Em.A([]));
this._super();
},
to_upload: null
});
You can read about it in step 6 here http://codebrief.com/2012/03/eight-ember-dot-js-gotchas-with-workarounds/
I'm pretty sure this should be put somewhere in the docs, I'm not sure where the most obvious would be though.

Can't get ember bindings to work as documented

I am following the documentation on emberjs.com, but can't get the first bindings example to work.
I created a jsfiddle to demonstrate. What am I missing?
Ember.js uses the concept of a RunLoop to allow bindings, observers and so on.
The problem with the example is that by setting a (bound) property and immediately getting the value via console.log no event is fired which would trigger the RunLoop and therefore synchronize the changes. There are 2 excellent blog posts about the RunLoop: Part 1 and Part 2. Although they target Sproutcore, the concept is about the same for Ember.js.
There are two ways to make your example work.
Force synchronisation via Ember.run.sync()
As the docs state, invoking Ember.run.sync() ... is a useful way to immediately force all bindings in the application to sync. This leaves the code like this, see http://jsfiddle.net/pangratz666/cwR3P/
App = Ember.Application.create({});
App.wife = Ember.Object.create({
householdIncome: 80000
});
App.husband = Ember.Object.create({
householdIncomeBinding: 'App.wife.householdIncome'
});
// force bindings to sync
Ember.run.sync();
console.log(App.husband.get('householdIncome')); // 80000
// Someone gets raise.
App.husband.set('householdIncome', 90000);
// force bindings to sync
Ember.run.sync();
console.log(App.wife.get('householdIncome')); // 90000​
Or the second option is to ...
Show the values in a view
Showing the properties in a view handles all the RunLoop stuff for you, see http://jsfiddle.net/pangratz666/Ub97S/
JavaScript:
App = Ember.Application.create({});
App.wife = Ember.Object.create({
householdIncome: 80000
});
App.husband = Ember.Object.create({
householdIncomeBinding: 'App.wife.householdIncome'
});
// invoke function in 3000ms
Ember.run.later(function() {
// someone gets a raise
App.husband.set('householdIncome', 90000);
}, 3000);​
Handlebars (the view):
<script type="text/x-handlebars" >
Wifes income: {{App.wife.householdIncome}}<br/>
Husbands income: {{App.husband.householdIncome}}
</script>​
You'll need to call Ember.run.sync(); after setting up your bindings to give Ember's run loop a chance to sync before your log statements. This is a handy technique for testing with Ember as well, but isn't typically needed in Ember apps themselves.