Ember acceptance test not seeing updated DOM - ember.js

I'm still getting into the depths of Ember's acceptance testing. One issue I seem to keep having is the DOM not getting updated after an event. For example, my page has a side menu. A simple toggle changes a property in it's component which then toggles a "hide" class on the menu itself:
Component
import Ember from 'ember';
export default Ember.Component.extend({
menuHidden: true,
actions: {
toggleMenu(){
this.set('menuHidden', !this.get('menuHidden'));
},
}
});
Template
<a id="menu-toggle" class="{{unless menuHidden 'open'}}" {{ action 'toggleMenu' }}>
<span></span><span></span><span></span>
</a>
<div id="menu" class="{{if menuHidden 'hide'}}">
{{#link-to 'dashboard' invokeAction='closeMenu'}}Dashboard{{/link-to}}
{{#each menu as |child|}}
{{menu-child child=child createCase=(action 'createCase') menuHidden=menuHidden}}
{{/each}}
<a href="javascript:void(0)" {{ action 'logout' }}>Logout</a>
</div>
Acceptance Test
import { test } from 'ember-qunit';
import moduleForAcceptance from '../helpers/module-for-acceptance';
moduleForAcceptance('Acceptance | side menu');
test('side menu opens and closes', assert=>{
logIn('my#email.com', 'password');
andThen(()=>{
assert.equal(find('#menu').attr('class'), 'hide', 'Hidden by default');
click('#menu-toggle');
andThen(()=>{
assert.equal(find('#menu').attr('class'), '', 'Now visible');
});
});
});
Now this is running fine in the browser. The test is logging in fine with my custom helper (the menu is only visible when logged in) and if I drop a console.log into toggleMenu() it is being triggered by the test. But it fails the last assert. I've done a console.log on the menu's wrapper's HTML before the last assert, it's still seeing the #menu element with class=hide
Is there something obvious I'm doing wrong? I can't find many examples of people with multiple andThen calls in acceptance tests, so I've tried having it nested - as above - and pulling the second andThen out inline with the first one. No difference.

If you open the developer console in the test browser window, you will likely see this error:
Assertion failed: You have turned on testing mode, which disabled the run-loop's autorun. You will need to wrap any code with asynchronous side-effects in an Ember.run
The following line in your component is considered code with asynchronous side-effects:
this.set('menuHidden', !this.get('menuHidden'));
Instead, for the test to work, you need to manually add the line to the run-loop, which is achieved by adding that line of code in an Ember.run, like so:
import Ember from 'ember';
export default Ember.Component.extend({
menuHidden: true,
actions: {
toggleMenu(){
Ember.run(this, function(){
this.set('menuHidden', !this.get('menuHidden'));
});
},
}
});
This will not affect the actual running code as the operations in your Ember.run will get merged into the main run-loop.
I had a similar issue which I managed to resolve after going through the steps mentioned in here

Related

How to test that an action is fired in a component integration test?

I have a very simple component and I'm trying to test that, when the button is clicked, the appropriate action is triggered. I've followed the docs as closely as I can, and also read several blog posts and SO questions covering the same material, and hence tried several subtly different ways of getting this to work but I just cannot get the test to pass. I've confirmed that it does actually work in the browser. What am I doing wrong?
add-thing.hbs:
{{#if displayForm}}
<form class="form-inline">...form content...</form>
{{else}}
<button {{action 'toggleForm'}} class="btn btn-default add">Add</button>
{{/if}}
add-thing.js:
import Ember from 'ember'
export default Ember.Component.extend({
displayForm: false,
actions: {
toggleForm () {
this.toggleProperty('displayForm')
}
}
})
add-thing-test.js:
import Ember from 'ember'
import { moduleForComponent, test } from 'ember-qunit'
import hbs from 'htmlbars-inline-precompile'
moduleForComponent('add-group', 'Integration | Component | add thing', {
integration: true
})
test('it toggles the form when the Add button is clicked', function (assert) {
assert.expect(1)
this.set('assertCalled', () => { assert.ok(true) })
// Have also tried this.on here instead of this.set
this.render(hbs`{{add-thing toggleForm=(action assertCalled)}}`)
// Have also tried passing in the action like {{add-thing toggleForm='assertCalled'}} as some blog posts suggest
Ember.run(() => document.querySelector('button.add').click())
// Have also tried just a this.$('button.add').click() here
})
Test output:
not ok 16 PhantomJS 2.1 - Integration | Component | add thing: it toggles the form when the Add button is clicked
---
actual: >
null
expected: >
null
stack: >
http://localhost:7357/assets/tests.js:221:24
exports#http://localhost:7357/assets/vendor.js:111:37
requireModule#http://localhost:7357/assets/vendor.js:32:25
require#http://localhost:7357/assets/test-support.js:20229:14
loadModules#http://localhost:7357/assets/test-support.js:20221:21
load#http://localhost:7357/assets/test-support.js:20251:33
http://localhost:7357/assets/test-support.js:7708:22
message: >
Expected 1 assertions, but 0 were run
Log: |
...
Ember: v2.14.0
It looks like you have two different things happening.
this.toggleProperty('displayForm')
that action will toggle displayForm from true to false, but it doesn't "bubble up" or go up anywhere. Clicking the button will fire it and then that is it.
Your test, on the other hand, is looking to see if clicking the button fires an action up to another level.
You can test this by checking if the form exists after clicking the button assert.equal(this.$('form').length, 1);. Or you could change the way the component works, but unless you want the action to bubble up you don't need to go that route.
Which might look something like
toggleForm () {
this.sendAction('toggleForm');
}
or
<button {{action toggleForm}}">Add</button>
(note the no quotes on `toggle form this time, that means call the action that was passed in.)

Ember integration test fails after click event

I have this integration test:
test('can change chord text', function(assert) {
this.render(hbs`{{chart-editor-chord chord=chord}}`);
this.$().click();
assert.ok(!!this.$('.chord-input').length);
});
but the assertion fails, the component template looks like this:
<div {{action 'changeChord'}} class="measure-chord chord-big">
{{#if chord.editing}}
<input type="text" value="{{chord.name}}" class="chord-input">
{{else}}
{{chord.name}}
{{/if}}
</div>
and the component code:
import Ember from 'ember';
export default Ember.Component.extend({
store: Ember.inject.service(),
actions: {
changeChord() {
this.chord.set('editing', true);
}
}
});
I'm updating the chord model in the changeChord() action and it does work if I test in the browser, but the integration test fails. So, does this change in the model have to be rendered synchronously to the template? I tried using wait() in the test but that doesn't make a difference. So how should I test this?
While I'm trying to create a twiddle for you, I found three things:
Where do you create chord mock in your test?
You are not sending event to the correct html component. Use this.$('.measure-chord') or this.$('.chord-big').
Instead of this.chord.set you should use this.get('chord').set. Actually Ember.set(this, 'chord.isEditing', ...) is even better.
And bonus: You don't need a div wrapper, component does this for you.
twiddles:
working copy
without div
It looks like your click helper is clicking the div that your component.js controls instead of the initial div in your template. If you specify the div in your click helper it should work:
this.$('.measure-chord').click();

Ember2.8: Sending an action from a component to the controller

Reading up on the documentation for Ember, I was under the impression that when an action is triggered by a component, it will go up the hierarchy until it hits an action with that name. But here's what's happening right now. I have a game-card component written like so:
game-card.hbs
<div class="flipper">
<div class="front"></div>
<div class="back">
<img {{action "proveImAlive"}} src={{symbol}} />
</div>
</div>
game-card.js
import Ember from 'ember';
export default Ember.Component.extend({
classNames: ['flip-container'],
actions: {
//blank for now because testing for bubbling up
}
});
Now according to what I've read, since game-card.js does not have a 'proveImAlive' action, it will try to bubble up the hierarchy i.e. the controller for the particular route.
play.js (the route /play)
import Ember from 'ember';
export default Ember.Controller.extend({
actions: {
proveImAlive() {
console.log('Im aliiiiveeee');
}
}
});
But when I finally run my application, I get this error:
Uncaught Error: Assertion Failed: <testground#component:game-card::ember483> had no action handler for: proveImAlive
Now my question is twofold:
Why is this error happening?
I want some of my component's actions to bubble up to the route's controller. For example, when a game-card is clicked, i'd like to send the id value (to be implemented) of that card up to the controller so it can store it on an array.
game-card is clicked --> sends value of 1 --> arrayinController.push(1)
How can I achieve this?
First, I'd like to point out that you linked to the documentation of Ember v1.10.0. You should consult the documentation for the version of Ember you are utilizing, which you mention is v2.8.0.
Now according to what I've read, since game-card.js does not have a 'proveImAlive' action, it will try to bubble up the hierarchy i.e. the controller for the particular route.
This isn't quite what happens because components are isolated, so there is no implicit bubbling. When the Guides say "actions sent from components first go to the template's controller" and "it will bubble to the template's route, and then up the route hierarchy" they mean that you have to explicitly send an action up from the Component. If the component is nested inside another component, you have to do this for each layer, until you reach the Controller.
Why is this error happening?
You need to bind the action in the template: {{game-card proveImAlive="proveImAlive"}}
i'd like to send the id value (to be implemented) of that card up to the controller so it can store it on an array.
I am going to be using closure actions for this part of the answer. As mentioned by #kumkanillam, they have better ergonomics, and they are the current proposed way to use actions if you consult the Guides.
I have prepared a Twiddle for you.
a) Initialize array in the controller
export default Ember.Controller.extend({
appName: 'Ember Twiddle',
gameCards: null,
init() {
this.set('gameCards', []);
}
}
b) Implement the action that pushed to the array
export default Ember.Controller.extend({
appName: 'Ember Twiddle',
gameCards: null,
init() {
this.set('gameCards', []);
},
actions: {
proveImAlive(cardNo) {
this.get('gameCards').pushObject(cardNo);
console.log('Im aliiiiveeee - cardNo', cardNo);
}
}
});
c) Bind the closure action down
{{game-card proveImAlive=(action 'proveImAlive')}}
d) Trigger the action passing the arguments
<div class="flipper">
<div class="front"></div>
<div class="back">
<button {{action proveImAlive 1}}> ProveIamAlive</button>
</div>
</div>
You need to explicitly set the action handler:
{{component-name fooAction=fooHandler}}
This is required because it helps keep components modular and reusable. Implicit links could result in a component triggering unintended behavior.
Your code should work, only if you have included game-card component into play.hbs. I doubt the controller for the particular route is not play in your case.
Here is the working-twiddle
Instead of bubbling actions, use closure actions. For better understanding you can go through the below links,
https://dockyard.com/blog/2015/10/29/ember-best-practice-stop-bubbling-and-use-closure-actions
http://miguelcamba.com/blog/2016/01/24/ember-closure-actions-in-depth/
https://emberigniter.com/send-action-does-not-fire/

Integrating ember-modal-dialog inside a site header Ember 2.0

I'm very new to the Ember framework and I have a question regarding getting a modal setup. I have the site-header as a component. When I click a login button I'd like for a modal to popup. I found the Ember Modal Dialog
plugin and was able to set it up so that there is a modal always shown in application.hbs. However, I'm having trouble understanding a couple of things but first here are my files.
application.hbs
{{site-header}}
{{outlet}}
{{#if isShowingModal}}
{{#modal-dialog close="toggleModal"
alignment="center"
translucentOverlay=true}}
Oh hai there!
{{/modal-dialog}}
{{/if}}
{{site-footer}}
site-header.js
import Ember from 'ember';
export default Ember.Component.extend({
isShowingModal: false,
actions: {
toggleModal: function() {
this.toggleProperty('isShowingModal');
}
}
});
So, I have this code for the button in my site-header.hbs:
<li class="login-button"><button class="btn btn-block btn-danger" {{action 'toggleModal'}}>Login</button></li>
As I understand it, action is saying to find toggleModal property in the site-header.js and execute the function above which does the property toggling.
However, how does application.hbs "see" the value of isShowingModal? Can it even see that value since the modal isn't showing up?
When most developers have modals, do they all go inside application.hbs since you want them to appear in the middle of the screen {{outlet}} area? How can you improve the process for including multiple modals?
What changes should I make to make the modal show up when the user clicks a button in the header?
Ok give this a try. In site-header.js
export default Ember.Component.extend({
actions: {
toggleModal() {
this.sendAction('toggleModal');
}
}
});
Application.hbs
{{site-header toggleModal='toggleModal'}}
{{outlet}}
{{#if isShowingModal}}
{{#modal-dialog close="toggleModal"
alignment="center"
translucentOverlay=true}}
Oh hai there!
{{/modal-dialog}}
{{/if}}
{{site-footer}}
The application controller
export default Ember.Controller.extend({
actions: {
toggleModal() {
this.toggleProperty('isShowingModal')
}
}
})
So the action is sent from the site-header component to the site-header component. From there it gets sent to the application controller. In application.hbs, toggleProperty='toggleProperty' connects the action from the component to the controller's action hash. In the controller action hash, it gets handled by toggleModal() and toggles the isShowingModal property. When the modal is closed, ember-modal-dialog fires an close action that is handled by the toggleModal() which again toggles the isShowingModal property.
Application template cannot see the properties of site-header. Only site-header component can see its properties. There are other methods to access them.
It is not necessary to include all modals in application. The plugin will most probably handle the positioning. In your case, you can move the modal code to the site-header component also.
Put the modal code in your site-header template. (or) You can have isShowingModal variable in application, send an action from site-header to application and toggle its value.

Testing an Ember.js component using hasBlock

I have this component set up (stripped down to its minimum):
<a href={{href}}>{{text}}</a>
{{#if template}}
<button {{action "toggleSubmenu"}} class="global-nav-toggle-submenu">
<i class="caret-down"></i>
</button>
{{/if}}
And this test:
test('has a toggle button if a submenu is present', function(assert) {
var component = this.subject({
template: Ember.HTMLBars.compile('<ul class="global-nav-submenu"></ul>')
});
assert.ok(this.$().find('.global-nav-toggle-submenu').length);
});
This runs fine, but I get a deprecation notice from Ember:
Accessing 'template' in <app-name#component:global-nav-item::ember540> is deprecated. To determine if a block was specified to <app-name#component:global-nav-item::ember540> please use '{{#if hasBlock}}' in the components layout.
When I change the template to use hasBlock instead:
<a href={{href}}>{{text}}</a>
{{#if hasBlock}}
<button {{action "toggleSubmenu"}} class="global-nav-toggle-submenu">
<i class="caret-down"></i>
</button>
{{/if}}
The test fails. Logging this.$() to the console shows that the hbs template file seems to be ignoring the template I'm adding programmatically.
In the test, I've tried swapping out template with block, layout, hasBlock, content, etc., with no success. I've tried initializing the subject with hasBlock: true as well.
And the logic runs fine when loading a page in the regular development app that has a block applied:
{{#global-nav-item text="Hello" href="#"}}
Some Content
{{/global-nav-item}}
Is there some other way that I should be setting up my components in unit tests in order to test this logic?
In general you should use the "new" style component integration tests for this type of thing.
See the following resources:
Example from the ember-radio-button addon
Nice blog post - Ember component integration tests
Update: Based on the blog post linked from Robert's answer, here is the new test code in tests/integration/components/global-nav-item-test.js:
import hbs from 'htmlbars-inline-precompile';
import { moduleForComponent, test } from 'ember-qunit';
moduleForComponent('global-nav-item', 'Integration | Component | global-nav-item', {
integration: true
});
test('has a toggle button if a submenu is present', function(assert) {
this.render(hbs`
{{#global-nav-item text="Hello" href="/"}}
<ul class="le-global-nav-submenu"></ul>
{{/global-nav-item}}
`);
assert.ok(this.$('.global-nav-toggle-submenu').length);
});