Nathan Palmer
Nathan Palmer

Reputation: 2030

What is the proper way in ember to use scheduleOnce that is test friendly?

Often, we'll need to schedule an update after rendering completes in reaction to a property change. Here is an example:

  page_changed: function() {
    Ember.run.scheduleOnce('afterRender', this, this.scroll);
  }.observes('current_page')

This doesn't work for testing since there is no runloop at the time scheduleOnce is called. We can simply wrap scheduleOnce in an Ember.run

  page_changed: function() {
    Ember.run(function() {
      Ember.run.scheduleOnce('afterRender', that, that.scroll);
    });
  ).observes('current_page')

..but I'm being told that's not the right way to go about it. I thought I'd reach out and get some other ideas.

For reference here is the issue that I opened up in ember.js#10536

Upvotes: 1

Views: 2841

Answers (3)

Stefan Penner
Stefan Penner

Reputation: 1087

the entry point into your application in the test needs the Ember.run. For example

test('foo', function() {
  user.set('foo', 1); // may have side-affects
});

test('foo', function() {
  Ember.run(function() {
    user.set('foo', 1);
  });
});

Why is this needed? Ember.run wraps the root of all call-stacks that are triggered by click/user-actions/ajax etc. Why? The run-loop is what allows ember to batch

When writing unit-tests, we are "faking" the user or network actions, it isn't obvious to ember what groups of changes you want to "Batch" together.

We can think of Ember.run as a way to create a batch or transactions of changes. Ultimately we use this, to batch DOM reads/writes to interact with the DOM in an ideal way.

Although maybe frustrating until you get the hang of it, it is a great way to create concise and deterministic tests. For example:

test('a series of grouped things', function() { 

  Ember.run(function() {
    component.set('firstName', 'Stef');
    component.set('lastName', 'Penner');
    // DOM isn't updated yet
  });

  // DOM is updated;
  // assert the DOM is updated

   Ember.run(function() {
    component.set('firstName', 'Kris');
    component.set('lastName', 'Selden');
    // DOM isn't updated yet, (its still Stef Penner)
  });

  // DOM is once again updated, its now Kris Selden.
});

Why is this cool? Ember gives us fine-grained control, which lets us not only very easily test aspects of our app in isolation, but also test sequences of what may be user or ajax pushed based actions, without needing to incorporate those aspects.

As time goes on, we hope to improve the test helpers and clarity around this.

Upvotes: 1

Nathan Palmer
Nathan Palmer

Reputation: 2030

Looks like this is the way to do it according to @StefanPenner's comment. Instead of modifying the app code itself just wrap the render call with an Ember.run

test('it renders', function() {
  expect(2);

  var component = this.subject();
  var that = this;

  equal(component._state, 'preRender');

  // Wrapping render in Ember.run
  Ember.run(function() {
    that.render();
  });

  equal(component._state, 'inDOM');
});

Upvotes: 1

GJK
GJK

Reputation: 37389

..but I'm being told that's not the right way to go about it.

The reason is that you need the Ember.run call when the callback is executed, not when it's scheduled. Like this:

Ember.run.scheduleOnce('afterRender', function() {
    Ember.run(function() {
        that.scroll();
    });
});

In your code, Ember.run is being called when the callback is scheduled. This is likely unnecessary as you're probably already in the run loop. My version ensures that when the callback is called, even if the run loop finished long ago, that another run loop is started and that.scroll() is called within the run loop. Does that makes sense?

Upvotes: 0

Related Questions