I've got ESA working nicely with Ember 2.0.1 but stumbled on an interesting case whilst testing:
Given the following test:
import Ember from 'ember';
import { module, test } from 'qunit';
import startApp from 'notifier/tests/helpers/start-app';
import Pretender from 'pretender';
import { authenticateSession } from '../../helpers/ember-simple-auth';
let server;
let application;
module('Acceptance | signout', {
beforeEach: function() {
application = startApp();
},
afterEach: function() {
Ember.run(application, 'destroy');
server.shutdown();
}
});
test('successfully sign out and get redirected', function(assert) {
server = new Pretender(function() {
this.post('/oauth/revoke', function() {
return [200, {"Content-Type": "application/json"}];
});
});
authenticateSession(application);
visit('/admin');
click('#sign-out');
andThen(() => {
assert.equal(currentRouteName(), 'users.sign-in');
});
});
The test result is the route never changes. It remains on /admin. This only occurs in testing, it works fine if I manually interact with the app.
The reason this happens is the page never gets reloaded (window.location.reload()) after the session gets invalidated as per https://github.com/simplabs/ember-simple-auth/blob/jj-abrams/addon/mixins/application-route-mixin.js#L99-L101.
Therefore the beforeModel hook in AuthenticatedRouteMixin never get triggered so the test never redirects out of /admin to /users/sign-in.
I get that this happens because you can't run window.location.reload() in testing but I'm not sure what alternative to use. I could override sessionInvalidated() in my application route and just have the app redirect to /users/sign-in when testing but that's no longer actually testing the app I suppose.
Any suggestions?
You cannot actually reload the location in testing mode as that would restart the test suite, thus leading to an infinite loop. You could maybe stub it with sinon and assert that the stub gets called.
Related
I'm running into some issues with the before all hook in ember-mocha (version 0.14.0). Here's an example from the docs that's been slightly modified to include a beforeEach hook:
import { expect } from 'chai';
import { describe, it } from 'mocha';
import { setupApplicationTest } from 'ember-mocha';
import { visit, currentURL } from '#ember/test-helpers';
describe('basic acceptance test', function() {
setupApplicationTest();
beforeEach(async function() {
await visit('index');
});
it('can visit /', async function() {
await visit('/');
expect(currentURL()).to.equal('/');
});
});
The above test runs as expected with no issues. However, when I substitute before for beforeEach I encounter an error:
import { expect } from 'chai';
import { describe, it } from 'mocha';
import { setupApplicationTest } from 'ember-mocha';
import { visit, currentURL } from '#ember/test-helpers';
describe('basic acceptance test', function() {
setupApplicationTest();
before(async function() {
await visit('index');
});
it('can visit /', async function() {
await visit('/');
expect(currentURL()).to.equal('/');
});
});
TypeError: Cannot destructure property `owner` of 'undefined' or 'null'.
at visit (assets/test-support.js:24931:9)
at Context.<anonymous> (assets/tests.js:339:36)
at invoke (assets/test-support.js:22801:21)
at Context.asyncFn (assets/test-support.js:22786:11)
at callFnAsync (assets/test-support.js:14070:8)
at Hook.Runnable.run (assets/test-support.js:14022:7)
at next (assets/test-support.js:14386:10)
at assets/test-support.js:14408:5
at timeslice (assets/test-support.js:9651:27)
Please let me know if any clarification is needed. Thanks in advance for your help!
Thats expected!
before only runs once for all tests.
That means before all beforeEach hooks
However setupApplicationTest utilizes beforeEach to setup the app (and the container), and afterEach to tear it down again.
This means you get a fresh app for all tests.
However you can not really visit anything without an app.
This means for every test you get a new app instance.
This means there is no app for all tests so there is no app that could visit a route.
The same question was asked in the ember discord channel. This answer tries to take the essence of the discussion to archive it on SO.
I have just begun adding ember-intl into an application for which I had working tests. My acceptance tests are still working, but my integration tests on components whose templates are using ember-intl for string translation are failing with:
"No locale defined. Unable to resolve translation:..."
In the ember-intl docs there is a section on Integration Testing, which seems to be out of date:
import hbs from 'htmlbars-inline-precompile';
import wait from 'ember-test-helpers/wait';
import { moduleForComponent, test } from 'ember-qunit';
let service;
moduleForComponent('x-product', 'XProductComponent', {
integration: true,
setup() {
service = this.container.lookup('service:intl');
service.setLocale('en-us');
}
});
test('it renders', function(assert) {
assert.expect(1);
this.render(hbs`{{x-product price=price deadline=deadline}}`);
this.set('price', 1000);
this.set('deadline', new Date());
let output = this.$().text();
assert.ok(output);
});
test('it translates', function(assert) {
assert.expect(1);
/* waits for async behavior (loading translations on app boot) to settle */
return wait().then(() => {
assert.equal(service.t('some.key'), 'Hello world');
});
});
I've looked in the Ember docs and I can see how to stub a service for testing, but not how to just load the service in a test and then work with it.
Instead of using this.container, we now need to use this.owner in the new format tests. Here's a snippet of code showing how to use it in context:
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { find, render } from '#ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
module('Integration | Component | login-form', function(hooks) {
setupRenderingTest(hooks);
let service;
hooks.beforeEach(function() {
service = this.owner.lookup('service:intl');
service.setLocale('en-us');
});
test('it renders', async function(assert) {
await render(hbs`{{login-form}}`);
assert.equal(find('[data-test-login-title]').textContent.trim(), 'Login');
});
});
A PR has been submitted to ember-intl, so hopefully the docs will reflect the latest best-practice soon.
I'm writing a very basic acceptance test against a route that has both admin and non-admin capabilities. My test makes an assertion that if I'm coming to the app for the first time, I don't see logged in capabilities. In my application, I'm using password authentication as follows:
this.get('session').open('firebase', {
provider: 'password',
email: email,
password: password
});
I have found that when I am not authenticated in the app, then run the acceptance test, it passes. However, if I then log in on the app, then run tests, my assertion fails because the session is restored, when I think it shouldn't be. Here's the test:
import { test } from 'qunit';
import moduleForAcceptance from 'app/tests/helpers/module-for-acceptance';
import startApp from '../helpers/start-app';
import destroyApp from '../helpers/destroy-app';
import replaceAppRef from '../helpers/replace-app-ref';
import replaceFirebaseAppService from '../helpers/replace-firebase-app-service';
import stubFirebase from '../helpers/stub-firebase';
import unstubFirebase from '../helpers/unstub-firebase';
import { emptyApplication } from '../helpers/create-test-ref';
moduleForAcceptance('Acceptance | index', {
beforeEach: function() {
stubFirebase();
application = startApp();
replaceFirebaseAppService(application, { });
replaceAppRef(application, emptyApplication());
},
afterEach: function() {
unstubFirebase();
destroyApp(application);
}
});
test('empty app - not authenticated', function(assert) {
visit('/');
andThen(function() {
assert.equal(currentURL(), page.url, 'on the correct page');
// this works if there's no session - fails otherwise
assert.notOk(page.something.isVisible, 'cannot do something');
});
});
I think replaceFirebaseAppService should be overriding the torii-adapter but it doesn't appear to be. Any help would be greatly appreciated.
I'm using:
Ember : 2.7.0
Ember Data : 2.7.0
Firebase : 3.2.1
EmberFire : 2.0.1
jQuery : 2.2.4
Upon looking at Emberfire closer, replaceFirebaseAppService is trying to replace the torii adapter registered at torii-adapter:firebase when it was being registered by my application as torii-adapter:application.
What I ended up doing was basically replicating replaceFirebaseAppService in my own helper:
import stubFirebase from '../helpers/stub-firebase';
import startApp from '../helpers/start-app';
import replaceAppRef from '../helpers/replace-app-ref';
import createOfflineRef from './create-offline-ref';
export default function startFirebaseApp(fixtures = { }) {
stubFirebase();
let application = startApp();
// override default torii-adapter
const mock = { };
application.register('service:firebaseMock', mock, {
instantiate: false,
singleton: true
});
application.inject('torii-provider:application', 'firebaseApp', 'service:firebaseMock');
application.inject('torii-adapter:application', 'firebaseApp', 'service:firebaseMock');
// setup any fixture data and return instance
replaceAppRef(application, createOfflineRef(fixtures));
return application;
}
This prevents the torii-adapter from resolving any session data that I may have from using my application. Then I can use the provided torii helper to mock my session where I need it:
// torii helper
import { stubValidSession } from 'app/tests/helpers/torii';
// mock a valid session
stubValidSession(application, { });
Hope that saves someone else some time.
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');
});
I'm simply trying to write some tests to make sure logging in and out works, including everything that goes with it. Here's what I'm doing so far:
tests/integration/sessions-test.js
import Ember from "ember";
import { test } from 'ember-qunit';
import startApp from '../helpers/start-app';
var App;
module('Integrations: Sessions', {
setup: function() {
App = startApp();
},
teardown: function() {
Ember.run(App, App.destroy);
}
});
test('Unsuccessful Sign In', function() {
expect(3);
visit('/sign-in');
andThen(function() {
fillIn('input#email', 'test#user.com');
fillIn('input#password', 'bad_password');
click('input#submit');
andThen(function() {
equal(currentRouteName(), 'sign-in', 'Unsuccessfull sign in stays on the sign in page.');
ok($('input#email, input#password').hasClass('error'), 'Inputs have a class of "error."');
equal($('input#submit').prop('disabled'), false, 'Submit button is not disabled.');
});
});
});
test('Successful Sign In', function() {
expect(2);
visit('/sign-in');
andThen(function() {
fillIn('input#email', 'test#user.com');
fillIn('input#password', 'password');
click('input#submit');
andThen(function() {
equal(currentRouteName(), 'welcome', 'Successfull sign in redirects to welcome route.');
ok(find('.message').length, "Page contains a list of messages.");
});
});
});
And, here's a trimmed down version of the sign in logic behind the scenes:
app/controllers/sign-in.js
import Ember from 'ember';
export default Ember.Controller.extend({
needs: ['application'],
actions: {
signIn: function() {
var self = this;
var data = this.getProperties('email', 'password');
// Attempt to sign in and handle the response.
var promise = Ember.$.post('/v3/sessions', data);
promise.done(function(response) {
Ember.run(function() {
self.get('controllers.application').set('token', response.access_token);
self.transitionToRoute('welcome');
});
});
...
}
}
});
The "Unsuccessful Sign In" test works just fine. The "Successful Sign In" starts to work, then quits halfway through. It signs in, then redirects correctly. On the welcome page, when it makes a call to get the messages, the node server is responding with Error: Not enough or too many segments and a 500 status. What in the world does that mean and how can I fix it, assuming I don't have any control over the API?
Also, the API is written primarily using Koa and Passport, as far as I know.
Figured it out. Apparently, it was an authentication error, not that you'd ever be able to guess that by the error message.
In the sign in controller, there's a line where I was setting the token property of the application controller. The application controller had an observer to watch that property for changes, then setup the AJAX headers when it changed. Problem is, observes use Ember's run loop, which is disabled while testing.
To fix the issue, I set the AJAX headers there in the sign in controller, just before transitioning to the welcome route.