Why Async Tests Sometimes Need settled()
The Runloop — Briefly
Ember batches async work into a runloop to coalesce DOM updates. With tracked properties and native async/await you rarely touch it directly.
What you'll learn
- Recognize the schedule and run helpers
- Know when tests need the settled and waitFor helpers
- Prefer later from the runloop over raw setTimeout if you must schedule
The Ember runloop is a scheduler that batches work into queues (actions, routerTransitions, render, afterRender, destroy) so that a burst of updates turns into a single render pass. In Octane, this is mostly invisible.
You Rarely Touch It
With @tracked and native async/await, the framework handles scheduling
for you. The runloop still runs — it just runs behind the scenes.
Where It Surfaces
The runloop becomes visible in tests. Promises must settle before assertions:
import { render, settled, click } from '@ember/test-helpers';
test('it toggles', async function (assert) {
await render(hbs`<Toggle />`);
await click('button');
await settled(); // wait for any pending render + tasks
assert.dom('[data-state]').hasText('on');
}); settled returns when every pending queue is empty. Most test helpers
(click, fillIn, render) call it implicitly.
Scheduling Manually
If you need to defer work, prefer runloop helpers over raw timers — they integrate with tests:
import { later, schedule } from '@ember/runloop';
later(this, () => {
this.poll();
}, 5000);
schedule('afterRender', this, () => {
this.measure();
}); schedule('afterRender', ...) is the canonical way to read DOM measurements
after Ember has flushed renders.
What To Avoid
setTimeout(fn, 0)to “wait for render” — useschedule('afterRender')orawait settled().- Tight polling loops outside the runloop — they don’t pause for renders.
The takeaway: write idiomatic Octane code with tracked state and promises, and the runloop stays out of your way.
ember-concurrency →