From ember-claude-skills
The Ember 2.x test API — moduleFor, moduleForComponent, moduleForModel, moduleForAcceptance, the global async helpers (visit, click, fillIn), wait(), and the bridge to the modern setupTest/setupRenderingTest API introduced in 3.x. Use when reading or fixing 2.x tests, or when planning the conversion to the modern qunit-based API.
How this skill is triggered — by the user, by Claude, or both
Slash command
/ember-claude-skills:ember-2-testingThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
The 2.x test API was overhauled in 3.x. If you're reading a test file and you see `moduleFor`, `moduleForComponent`, `andThen`, or global async helpers without `await`, you're in the 2.x dialect.
The 2.x test API was overhauled in 3.x. If you're reading a test file and you see moduleFor, moduleForComponent, andThen, or global async helpers without await, you're in the 2.x dialect.
| What you're testing | 2.x helper | 3.x+ replacement |
|---|---|---|
| A unit (service, util, controller class) | moduleFor('service:foo', { ... }) | setupTest(hooks) (then this.owner.lookup('service:foo')) |
| A component (in isolation, with a render context) | moduleForComponent('user-card', { integration: true }) | setupRenderingTest(hooks) |
| A model (Ember Data) | moduleForModel('post', ...) | setupTest(hooks) |
| A user flow (full app boot) | moduleForAcceptance('/posts') | setupApplicationTest(hooks) |
These helpers come from ember-qunit (and ember-cli-qunit/ember-cli-mocha in some 2.x setups).
// tests/acceptance/posts-test.js
import { test } from 'qunit';
import moduleForAcceptance from 'my-app/tests/helpers/module-for-acceptance';
moduleForAcceptance('Acceptance | posts');
test('visiting /posts', function (assert) {
visit('/posts');
andThen(function () {
assert.equal(currentURL(), '/posts');
assert.equal(find('.post').length, 3);
});
});
Key 2.x flavors:
visit, click, fillIn, keyEvent, triggerEvent, find, currentURL, currentRouteName. They are globally injected, not imported.andThen(fn): waits for the test to settle, then runs fn. Each andThen block can issue more async helpers, which are themselves followed by another andThen. This is the 2.x equivalent of await.wait(): returns a promise that resolves when settled. Useful when you need a promise outside andThen.The post-2.x style — await visit('/') — is the same idea, but with native async/await and imports from @ember/test-helpers.
import { moduleForComponent, test } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';
moduleForComponent('user-card', 'Integration | Component | user-card', {
integration: true
});
test('renders the user name', function (assert) {
this.set('user', { name: 'Ada Lovelace' });
this.render(hbs`{{user-card user=user}}`);
assert.equal(this.$('.user-card .name').text().trim(), 'Ada Lovelace');
});
Notes:
integration: true flips the helper into rendering mode. Without it, you get a unit-style stub container with no render context.this.render(hbs\...`)** is the 2.x render call. In 3.x+, it's await render(hbs`...`)from@ember/test-helpers`.this.$(...) is jQuery. qunit-dom (assert.dom(...)) didn't exist yet — assertions are usually assert.equal(this.$('.x').text().trim(), 'expected').{{user-card user=user}}). Angle-bracket invocation worked from 3.4 onward.import { moduleFor, test } from 'ember-qunit';
moduleFor('service:cart', 'Unit | Service | cart', {
// dependencies you need resolved:
needs: ['service:notifications']
});
test('adds an item', function (assert) {
const service = this.subject();
service.add({ id: 1, price: 10 });
assert.equal(service.get('items.length'), 1);
});
needs: [...]: declares which dependencies the resolver should make available. In 3.x+, this dance is replaced by this.owner.register(...) for fakes and automatic resolution of the rest.this.subject(): instantiates the thing under test. In 3.x+, you write this.owner.lookup('service:cart').wait() and pending workwait() from ember-test-helpers resolves when:
jQuery.active).If a test hangs forever in andThen, almost always it's because some async work isn't registered — usually a raw setTimeout, a fetch (jQuery's AJAX is tracked, fetch isn't), or a promise created outside the run loop. Test waiters (Ember.Test.registerWaiter(fn)) let you teach the framework to wait for arbitrary work; modern tests use @ember/test-waiters instead.
Most apps modernize one test file at a time. To make this safe:
ember-qunit@^4 (which supports both moduleFor* and setupTest).moduleForXxx with the matching module(...) { setupXxx(hooks); } block.andThen(...) with await on the preceding helper.this.$(...) with find(...) or assert.dom(...).Example before/after:
// 2.x style
moduleForAcceptance('Acceptance | posts');
test('visiting /posts', function (assert) {
visit('/posts');
andThen(() => assert.equal(currentURL(), '/posts'));
});
// modernized
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { currentURL } from '@ember/test-helpers';
import page from 'my-app/tests/pages/posts';
module('Acceptance | posts', function (hooks) {
setupApplicationTest(hooks);
test('visiting /posts', async function (assert) {
await page.visit();
assert.strictEqual(currentURL(), '/posts');
});
});
Both can coexist in the same suite — convert at your own pace.
ember-test-selectors (the data-test-* strip-on-build addon) was already common in 2.x. If your codebase doesn't use it, adding data-test-* selectors during the migration is one of the higher-leverage things you can do — your tests become independent of the CSS overhaul that almost always accompanies a major upgrade.
ember-cli-mirage was the standard mock-server addon throughout 2.x. The factory/route-handler API has been remarkably stable; in most cases, your 2.x Mirage setup will keep working after the upgrade with only minor tweaks.
| Symptom | Cause | Fix |
|---|---|---|
| Test passes locally, fails in CI | Order-dependent state (a previous test's controller bled in). | Tear down state in afterEach; convert to module + hooks. |
andThen block runs before async work finishes | wait() doesn't see the work because it's outside the run loop. | Wrap in Ember.run(...) or register a test waiter. |
| jQuery DOM lookups break after CSS refactor | Selectors are class-based. | Add data-test-* and select by those. |
this.subject() returns undefined | Missing needs: declaration. | Add the dependency factory name. |
| Acceptance test hangs forever | Unawaited fetch or setTimeout not registered with a waiter. | Register a waiter via Ember.Test.registerWaiter(...). |
moduleFor* as the 2.x setup mechanism.andThen-style tests as the pre-await shape.this.$(...) is jQuery and find(...) is the modern equivalent.module + setupApplicationTest shape.ember-qunit@^4 is in the project's package.json if you plan to mix styles.ember-2-classic-patterns — the 2.x dialect under test.ember-2-to-3-migration — when to convert tests during a migration.ember-testing.Provides CDSS development patterns for drug interaction checking, dose validation, clinical scoring (NEWS2, qSOFA), and alert classification integrated into EMR workflows.
npx claudepluginhub artemgurzhii/ember-claude-skills --plugin ember-claude-skills