First, I made a small Ember app without Ember CLI.
I had this piece of code.
window.MyApp = Ember.Application.create({
ready: function() {
this.register('session:current', MyApp.SessionController, { singleton: true });
this.inject('controller', 'session', 'session:current');
}
});
This worked.
Then I decided to rewrite everything from scratch with Ember CLI.
I edited the file app/app.js and added the ready hook just like in my previous version.
var App = Ember.Application.extend({
modulePrefix: config.modulePrefix,
podModulePrefix: config.podModulePrefix,
Resolver: Resolver,
ready: function() {
this.register('session:current', App.SessionController, { singleton: true });
this.inject('controller', 'session', 'session:current');
}
});
This doesn't work.
The session controller does exist. That's the content of the file app/controllers/session.js
export default Ember.Controller.extend({
isLoggedIn: false,
});
The error message I get is
TypeError: Attempting to register an unknown factory: `session:current`
It appears in the browser.
I googled that message, but I found nothing about dependency injection in Ember CLI.
Any idea?
In ember-cli you can use ember generate service <name of service> and ember generate initializer <name of initializer> to build the stubs to achieve this, which is far better than fiddling about with app.js.
You create a service basically like this:
// app/services/notifications.js
import Ember from 'ember';
export default Ember.Object.extend({
initNotifications: function() {
// setup comes here
}.on('init'),
// Implementation snipped, not relevant to the answer.
});
And the initializer, which injects the service into the component(s) of your application which need it:
// app/initializers/notifications-service.js
import Notifications from '../services/notifications';
export default {
name: 'notification-service',
after: 'auth-service',
initialize: function( container, app ) {
app.register( 'notifications:main', Notifications, { singleton: true } );
app.inject( 'component:system-notifications', 'notificationService', 'service:notifications' );
app.inject( 'service:auth', 'notificationService', 'service:notifications' );
}
};
With that, it becomes available as notificationService on the components specified.
Documentation on the subject of dependency injection in Ember can be found at http://emberjs.com/guides/understanding-ember/dependency-injection-and-service-lookup/
Related
I am building a site using Ember Simple Auth.
I followed these instructions to try and add the current user object to the session and it worked, using this slightly adapted code:
import Ember from 'ember';
import Session from 'simple-auth/session';
export default {
name: "current-user",
before: "simple-auth",
initialize: function(container) {
Session.reopen({
setCurrentUser: function() {
var accessToken = this.get('secure.token');
var _this = this;
if (!Ember.isEmpty(accessToken)) {
return container.lookup('store:main').find('user', 'me').then(function(user) {
_this.set('content.currentUser', user);
});
}
}.observes('secure.token'),
setAccount: function() {
var _this = this;
return container.lookup('store:main').find('account', this.get('content.currentUser.account.content.id')).then(function(account) {
_this.set('content.account', account);
});
}.observes('content.currentUser'),
});
}
};
However, using the latest version of Ember I'm getting the following:
DEPRECATION: lookup was called on a Registry. The initializer API no longer receives a container, and you should use an instanceInitializer to look up objects from the container. See http://emberjs.com/guides/deprecations#toc_deprecate-access-to-instances-in-initializers for more details.
I know that I need to split the above into /app/initializers and /app/instance-initializers (as per the notes here) but I'm not quite sure how to go about it.
Of course, if there is an easier/cleaner way to make the user and account objects available to every route/template I'd love to hear them :)
Thanks
This works for me on:
ember-cli: 0.2.7 (ember: 1.12.0, ember-data: 1.0.0-beta.18)
ember-cli-simple-auth: 0.8.0-beta.3
Note:
ember-data: 1.13. Store is registered in an initializer, should work as is
ember-data: 1.0.0-beta.19. Store is registered in an instance-initializer, some adjustments needed
1) Customize session
//config/environment.js
ENV['simple-auth'] = {
session: 'session:custom',
...
}
//app/sessions/custom.js
import Session from 'simple-auth/session';
export default Session.extend({
// here _store is ember-data store injected by initializer
// why "_store"? because "store" is already used by simple-auth as localStorage
// why initializer? I tried
// _store: Ember.inject.service('store') and got error
currentUser: function() {
var userId = this.get('secure.userId');
if (userId && this.get('isAuthenticated')) {
return this._store.find('user', userId);
}
}.property('secure.userId', 'isAuthenticated')
});
2) Inject store to session by initializer (otherwise find() wouldn't work)
//app/initializers/session-store
export function initialize(container, application) {
application.inject('session:custom', '_store', 'store:main')
// "store:main" is highly dynamic depepeding on ember-data version
// in 1.0.0-beta.19 (June 5, 2015) => "store:application"
// in 1.13 (June 16, 2015) => "service:store"
}
export default {
name: 'session-store',
after: 'ember-data',
initialize: initialize
}
3) In template
{{#if session.isAuthenticated}}
{{session.currentUser.name}}
{{/if}}
Note: this does not relieve you from deprecations generated by ember-simple-auth itself.
First of all you shouldn't reopen the session but use a custom session instead (see this example: https://github.com/simplabs/ember-simple-auth/blob/master/examples/4-authenticated-account.html#L132). Also you you shouldn't only load the current user when the access token is set but when the session is authenticated ('session.get('isAuthenticated')') which makes your code not dependent on the authenticator.
The deprecation warnings regarding the use of the registry in the initializer will go away in ESA 0.9.0 hopefully.
Here's a before and after of an initializer/instance-initializer that I did the other day.
Before
export function initialize( container, application ) {
var session = Ember.Object.create({
user:null,
authorization:null
});
application.register('session:main', session, { instantiate: false });
application.inject('route', 'session', 'session:main');
application.inject('controller', 'session', 'session:main');
application.inject('adapter', 'session', 'session:main');
}
After
export function initialize( instance) {
var session = Ember.Object.create({
user:null,
authorization:null
});
instance.registry.register('session:main', session, { instantiate: false });
instance.registry.injection('route', 'session', 'session:main');
instance.registry.injection('controller', 'session', 'session:main');
instance.registry.injection('adapter', 'session', 'session:main');
}
Ember Data Stuff
Ember Data in the latest iterations should be fetched using store:application
export function initialize(instance) {
var store = instance.container.lookup('store:application');
....
}
export default {
name: 'socket',
initialize: initialize,
after:['ember-data']
};
Quick summary/tldr:
It seems that Ember's container lookup process + Ember-CLI's module resolver doesn't allow manually un-registering a service and then registering a replacement if the original service can be resolved using the resolver (I want to do the method described here, but it doesn't work)
How can I mock an Ember-CLI service in an acceptance test without using a hacky, custom resolver? (example project/acceptance test here)
Detailed explanation + example
Create a new service that is injected into a controller:
ember generate service logger
services/logger.js
export default Ember.Object.extend({
log: function(message){
console.log(message);
}
});
initializers/logger-service.js
export function initialize(container, application) {
application.inject('route', 'loggerService', 'service:logger');
application.inject('controller', 'loggerService', 'service:logger');
}
The service is accessed through its injected name, loggerService, in an action handler on the application controller:
Use the service in a controller
templates/application.hbs
<button id='do-something-button' {{action 'doSomething'}}>Do Something</button>
controllers/application.hs
export default Ember.Controller.extend({
actions: {
doSomething: function(){
// access the injected service
this.loggerService.log('log something');
}
}
});
Attempt to test that this behavior occurs correctly
I created an acceptance test that checks that the button click triggered the service. The intent is to mock out the service and determine if it was called without actually triggering the service's implementation -- this avoids the side-effects of the real service.
ember generate acceptance-test application
tests/acceptance/application-test.js
import Ember from 'ember';
import startApp from '../helpers/start-app';
var application;
var mockLoggerLogCalled;
module('Acceptance: Application', {
setup: function() {
application = startApp();
mockLoggerLogCalled = 0;
var mockLogger = Ember.Object.create({
log: function(m){
mockLoggerLogCalled = mockLoggerLogCalled + 1;
}
});
application.__container__.unregister('service:logger');
application.register('service:logger', mockLogger, {instantiate: false});
},
teardown: function() {
Ember.run(application, 'destroy');
}
});
test('application', function() {
visit('/');
click('#do-something-button');
andThen(function() {
equal(mockLoggerLogCalled, 1, 'log called once');
});
});
This is based on the talk Testing Ember Apps: Managing Dependency by mixonic that recommends unregistering the existing service, then re-registering a mocked version:
application.__container__.unregister('service:logger');
application.register('service:logger', mockLogger, {instantiate: false});
Unfortunately, this does not work with Ember-CLI. The culprit is this line in Ember's container:
function resolve(container, normalizedName) {
// ...
var resolved = container.resolver(normalizedName) || container.registry[normalizedName];
// ...
}
which is part of the container's lookup chain. The issue is that the container's resolve method checks the resolver before checking its internal registry. The application.register command registers our mocked service with the container's registry, but when resolve is called the container checks with the resolver before it queries the registry. Ember-CLI uses a custom resolver to match lookups to modules, which means that it will always resolve the original module and not use the newly registered mock service. The workaround for this looks horrible and involves modifying the resolver to never find the original service's module, which allows the container to use the manually registered mock service.
Modify Resolver to avoid resolving to original service
Using a custom resolver in the test allows the service to be successfully mocked. This works by allowing the resolver to perform normal lookups, but when our service's name is looked up the modified resolver acts like it has no module matching that name. This causes the resolve method to find the manually registered mock service in the container.
var MockResolver = Resolver.extend({
resolveOther: function(parsedName) {
if (parsedName.fullName === "service:logger") {
return undefined;
} else {
return this._super(parsedName);
}
}
});
application = startApp({
Resolver: MockResolver
});
This seems like it shouldn't be necessary and doesn't match the suggested service mocking from the above slides. Is there a better way to mock this service?
The ember-cli project used in this question be found in this example project on github.
Short version of the solution: your registered mock service must have a different service:name than the "real" service you're trying to mock.
Acceptance test:
import Ember from 'ember';
import { module, test } from 'qunit';
import startApp from 'container-doubling/tests/helpers/start-app';
var application;
let speakerMock = Ember.Service.extend({
speak: function() {
console.log("Acceptance Mock!");
}
});
module('Acceptance | acceptance demo', {
beforeEach: function() {
application = startApp();
// the key here is that the registered service:name IS NOT the same as the real service you're trying to mock
// if you inject it as the same service:name, then the real one will take precedence and be loaded
application.register('service:mockSpeaker', speakerMock);
// this should look like your non-test injection, but with the service:name being that of the mock.
// this will make speakerService use your mock
application.inject('component', 'speakerService', 'service:mockSpeaker');
},
afterEach: function() {
Ember.run(application, 'destroy');
}
});
test('visit a route that will trigger usage of the mock service' , function(assert) {
visit('/');
andThen(function() {
assert.equal(currentURL(), '/');
});
});
Integration test (this is what I was originally working on that caused me issues)
import { moduleForComponent, test } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';
import Ember from 'ember';
let speakerMock = Ember.Service.extend({
speak: function() {
console.log("Mock one!");
}
});
moduleForComponent('component-one', 'Integration | Component | component one', {
integration: true,
beforeEach: function() {
// ember 1.13
this.container.register('service:mockspeaker', speakerMock);
this.container.injection('component', 'speakerService', 'service:mockspeaker');
// ember 2.1
//this.container.registry.register('service:mockspeaker', speakerMock);
//this.container.registry.injection('component', 'speakerService', 'service:mockspeaker');
}
});
test('it renders', function(assert) {
assert.expect(1);
this.render(hbs`{{component-one}}`);
assert.ok(true);
});
You can register your mock and inject it instead of the original service.
application.register('service:mockLogger', mockLogger, {
instantiate: false
});
application.inject('route', 'loggerService', 'service:mockLogger');
application.inject('controller', 'loggerService', 'service:mockLogger');
I use this approach for mocking the torii library in my third-party login acceptance tests. I hope there will be a nicer solution in the future.
The existing answers work well, but there's a way that avoids renaming the service and skips the inject.
See https://github.com/ember-weekend/ember-weekend/blob/fb4a02353fbb033daefd258bbc032daf070d17bf/tests/helpers/module-for-acceptance.js#L14 and usage at https://github.com/ember-weekend/ember-weekend/blob/fb4a02353fbb033daefd258bbc032daf070d17bf/tests/acceptance/keyboard-shortcuts-test.js#L13
I'll present it here as an update to the test helper I previously had here, so it's a drop-in replacement, but you may just want to follow the links above instead.
// tests/helpers/override-service.js
// Override a service with a mock/stub service.
// Based on https://github.com/ember-weekend/ember-weekend/blob/fb4a02353fbb033daefd258bbc032daf070d17bf/tests/helpers/module-for-acceptance.js#L14
// e.g. used at https://github.com/ember-weekend/ember-weekend/blob/fb4a02/tests/acceptance/keyboard-shortcuts-test.js#L13
//
// Parameters:
// - newService is the mock object / service stub that will be injected
// - serviceName is the object property being replaced,
// e.g. if you set 'redirector' on a controller you would access it with
// this.get('redirector')
function(app, newService, serviceName) {
const instance = app.__deprecatedInstance__;
const registry = instance.register ? instance : instance.registry;
return registry.register(`service:${serviceName}`, newService);
}
Plus performing the jslint and helper registration steps from https://guides.emberjs.com/v2.5.0/testing/acceptance/#toc_custom-test-helpers
I can then call it like this, in my example stubbing out a redirect (window.location) service, which we want to do because redirecting breaks Testem:
test("testing a redirect's path", function(assert) {
const assertRedirectPerformed = assert.async();
const redirectorMock = Ember.Service.extend({
redirectTo(href) {
assert.equal(href, '/neverwhere');
assertRedirectPerformed();
},
});
overrideService(redirectorMock, 'redirector');
visit('/foo');
click('#bar');
});
Using ember-cli version 0.0.42
Run these commands
ember new myApp
cd myApp
ember generate service serviceOne
ember generate service serviceTwo
ember generate controller application
Make these updates
controllers/application.js
import Ember from 'ember';
export default Ember.Controller.extend({
init: function() {
var a = this.get('serviceOneService.testFunction');
}
});
initializers/service-one-service.js
export default {
name: 'service-one-service',
initialize: function(container, app) {
app.inject('route', 'serviceOneService', 'service:service-one');
app.inject('controller', 'serviceOneService', 'service:service-one');
}
};
services/service-one.js
import Ember from 'ember';
export default Ember.Object.extend({
testFunction: function(){
//How would I make a call to serviceTwo.testFunction here instead of returning 123
return "123"
}
});
services/service-two.js
import Ember from 'ember';
export default Ember.Object.extend({
testFunction: function(){
return "Test function from service 2"
}
});
My question is how to I inject serviceOne into service two and serviceTwo into service one. One I do that how do I access service two from service one. Inside when calling I have an idea of how to injest the service directly into all controllers but when I try and inject into another object (service) inside of testFunction "this" refers to the window and not an ember object.
My first thought was to just inject each service into the overal service namespace but I get this message:
Uncaught Error: Cannot inject a `service:service-one` on other service(s). Register the `service:service-one` as a different type and perform the typeInjection.
You can inject one service into the other by defining an initializer and setting setting the after property to something like after: ['service-one', 'service-two'], which would cause it to run after both of those services have been registered.
The initializer would look something like...
Ember.Application.initializer({
after: ['service one', 'service two'],
name: 'service injections',
initialize: function(container, application){
application.inject('service:service-one', 'serviceTwoService', 'service:service-two');
}
});
Unfortunately if you then try to inject service:service-one into service:service-two you'll get an error stating Maximum call stack size exceeded which happens because the container ends up in a loop while trying lookup all the needed injections.
You can see a working bin here: http://emberjs.jsbin.com/fitaka/1/edit
I am following a Dockyard Tutorial on using ember-cli with rails. This particular section is on basic integration testing. Unfortunately, it doesn't seem to be registering properly (at least I don't think so). The test should fail and say something along the lines of "Expected: 'Welcome to Boston Ember'"; rather, it says "should pass jshint" which it is and is therefore passing. Any idea what I am doing wrong?
tests/integration/landing-page-test.js
import Ember from 'ember';
import startApp from 'bostonember/tests/helpers/start-app';
var App;
module('Integration - Landing Page', {
setup: function() {
App = startApp();
},
teardown: function() {
Ember.run(App, 'destroy');
}
});
test('Should welcome me to Boston Ember', function() {
visit('/').then(function() {
equal(find('h2#title').text(), 'Welcome bloopde bloopasa to Boston Ember');
});
});
tests/helpers/start-app.js
/* global require */
var Application = require('bostonember/app')['default'];
var Router = require('bostonember/router')['default'];
import Ember from 'ember';
export default function startApp(attrs) {
var App;
var attributes = Ember.merge({
// useful Test defaults
rootElement: '#ember-testing',
LOG_ACTIVE_GENERATION:false,
LOG_VIEW_LOOKUPS: false
}, attrs); // but you can override;
Router.reopen({
location: 'none'
});
Ember.run(function(){
App = Application.create(attributes);
App.setupForTesting();
App.injectTestHelpers();
});
App.reset(); // this shouldn't be needed, i want to be able to "start an app at a specific URL"
return App;
}
Will provide additional info upon request. Thank you!
Must have just been a hiccup?
Solution was to recreate the file, same code, but works now.
I usually use Rails for my Ember apps. However this time we opted to decouple the API from the Ember app, and as such I'm trying EmberCLI. So far it's lovely to setup and use. However when using attempting to use fixtures it doesn't load the data.
As listed in this post I am using reopenClass when declaring the fixtures.
If I do not override the model, it does not error but the Ember inspector also shows no data was loaded. If I override my file with:
// routes/campaigns/index.js
export default Ember.Route.extend({
model: function() {
return this.store.find('campaign');
}
});
And visit the /campaigns path then I get the error I get the error Error while loading route: undefined.
From what I can find this seems to happen when Ember cannot find the data.
My router and model with obvious items like export default excluded:
// app/router.js
Router.map(function() {
this.resource('campaigns', function() {
});
});
// models/campaign.js
var Campaign = DS.Model.extend({
name: DS.attr('string')
});
Campaign.reopenClass({
FIXTURES: [
{ "id": 1, "name": "Campaign #1" },
{ "id": 2, "name": "Campaign #2" }
]
});
I have tested the same setup in a Rails app I just made, and it works perfectly. I'd love any insight people could give, as EmberCLI seems lightweight and worth the effort.
Edit: Adding my app.js file to answer question about whether I included DS.FixtureAdapter:
// Import statements
Ember.MODEL_FACTORY_INJECTIONS = true;
var App = Ember.Application.extend({
modulePrefix: 'nala', // TODO: loaded via config
Resolver: Resolver
});
loadInitializers(App, 'nala');
App.ApplicationAdapter = DS.FixtureAdapter({});
export default App;
You need to set up your application adapter located at the filepath adapters/application.js as follows:
export default DS.FixtureAdapter.extend({});
See the first paragraph under ember-cli Naming Conventions. N.B. you won't need to import DS or Ember if you're using ember-cli and have them listed in your .jshintrc file.