Vue Test Utils has an API method called shallowMount() that:
...creates a Wrapper that contains the mounted and rendered Vue component, but with stubbed child components.
I've searched the Vue Test Utils documentation website but failed to find a good explanation of how these stubbed child components behave.
What exactly are these stubbed child components?
Which parts of the Vue component lifecycle do they go through?
Is there a way to pre-program their behavior?
What exactly are stubbed child components?
A stubbed child component is a replacement for a child component rendered by the component under test.
Imagine you have a ParentComponent component that renders a ChildComponent:
const ParentComponent = {
template: `
<div>
<button />
<child-component />
</div>
`,
components: {
ChildComponent
}
}
ChildComponent renders a globally registered component and calls an injected instance method when it's mounted:
const ChildComponent = {
template: `<span><another-component /></span>`,
mounted() {
this.$injectedMethod()
}
}
If you use shallowMount to mount the ParentComponent, Vue Test Utils will render a stub of ChildComponent in place of than the original ChildComponent. The stub component does not render the ChildComponent template, and it doesn't have the mounted lifecycle method.
If you called html on the ParentComponent wrapper, you would see the following output:
const wrapper = shallowMount(ParentComponent)
wrapper.html() // <div><button /><child-component-stub /></div>
The stub looks a bit like this:
const Stub = {
props: originalComonent.props,
render(h) {
return h(tagName, this.$options._renderChildren)
}
}
Because the stub component is created with information from the original component, you can use the original component as a selector:
const wrapper = shallowMount(ParentComponent)
wrapper.find(ChildComponent).props()
Vue is unaware that it's rendering a stubbed component. Vue Test Utils sets it so that when Vue attempts to resolve the component, it will resolve with the stubbed component rather than the original.
Which parts of the Vue component lifecycle do they go through?
Stubs go through all parts of the Vue lifecycle.
Is there a way to pre-program their behavior?
Yes, you can create a custom stub and pass it using the stubs mounting option:
const MyStub = {
template: '<div />',
methods: {
someMethod() {}
}
}
mount(TestComponent, {
stubs: {
'my-stub': MyStub
}
})
You can find more information about stubbed components in this unofficial testing guide for Vue.
https://lmiller1990.github.io/vue-testing-handbook/#what-is-this-guide
In short:
A stub is simply a piece of code that stands in for another.
The Vue Test Utils information also has some information about shallow mount:
https://vue-test-utils.vuejs.org/guides/#common-tips
The Vue Test Utils is lacking quite a bit of context though.
Related
I am working with Ember Octane version, I want to invoke an action in the Route from the child component. The pseudo code is as follows.
**Route**
export default class SomeRouter extends Route {
model() {
return data;
}
#action
refreshRoute() {
this.refresh();
}
}
**SomerRouter.hbs**
<ChildComponent> //Using child component here
**ChildComponent**
export default class ChildComponent extends Component {
#action
revert() {
//How do I invoke the "refreshRoute" on the SomeRouter from here?
}
}
In the revert method of the above child component, "this" refers to the component itself but in the previous version of the ember "this" refers to the router where I could simply call this.refresh(). So how do I achieve this in Ember Octane. Would really appreciate any help.
You dont. This is actually one of the things that are still a bit inconsistent even with octane. Because the bound context of the route template is the Controller, not the route. So you can not access the action with {{this.refreshRoute}}.
To call an action on the Route your best way is to uzilize send. But to do this you need a Controller and define a different action on the Controller:
controllers/some.js:
export default class SomeController extends Controller {
#action
refreshRouteFromController() {
this.send('refreshRoute');
}
}
Now this function you can use from your template:
<ChildComponent #refresh={{this.refreshRouteFromController}}>
And then use it from your component:
revert() {
this.args.refresh();
}
Or directly from a button:
<button {{on "click #refresh}}>...</button>
I am writing an Ember Addon that provides some services that are not exposed through the app/ folder.
// project Foo
// addon/services/foo.js
import Ember from 'ember';
export default Ember.Service.extend({})
The unit test that gets generated by Ember CLI uses moduleFor helper.
// tests/unit/services/foo.js
import { moduleFor, test } from 'ember-qunit';
moduleFor('service:foo', 'Unit | Service | foo', {
// Specify the other units that are required for this test.
// needs: ['service:foo']
});
// Replace this with your real tests.
test('it exists', function(assert) {
let service = this.subject();
assert.ok(service);
});
Problem is that since my FooService is not exposed through the app/ folder, moduleFor helper cannot find it using service:foo name.
What would be the best way to unit test my service here?
I can see three possibilities:
1) add tests/dummy/app/services/foo.js that exports FooService
// tests/dummy/app/services/foo.js
export { default } from 'foo/services/foo.js';
2) create initializers in the dummy app that registers service:foo
// tests/dummy/app/initializers/account-data.js
import FooService from 'foo/services/foo'
export function initialize(application) {
application.register('service:foo', FooService);
}
EDIT
turns out I can't do this. it's not that moduleFor helper is trying to find 'service:foo' but trying to register 'app/services/foo' as 'service:foo'. So registering 'service:foo' ahead of the time does not help.
3) don't use moduleFor helper.
EDIT
By this I meant something like what #Andy Pye's answer. But it would be nice to be able to use moduleFor helper... Especially for models since I need access to store.
EDIT
turns out it gets more complicated for models since I can't create them directly. So if I go with not using moduleFor helper, I need an instead of store object...
I've just had the same (or a very similar) issue. I've found that this seems to work:
// tests/unit/services/foo.js
import { module, test } from 'ember-qunit';
import FooService from 'Foo/services/foo';
module('Unit | Service | foo', {
// Specify the other units that are required for this test.
// needs: ['service:foo']
});
// Replace this with your real tests.
test('it exists', function (assert) {
let service = FooService.create();
assert.ok(service);
});
I'm building a relatively straight-foward comment-list component. I want to pass in the commentable model (say a Post) and have the component take care of creating, editing, deleting comments. Right now I pass around all the various actions and it's been extremely brittle.
How do I create a true instance of an Ember Data model in a component integration test?
My immediate thought was to import the model then .create({}) it but that errors with use this.store.createRecord() instead
/* jshint expr:true */
import { assert } from 'chai';
import { describeComponent, it } from 'ember-mocha';
import hbs from 'htmlbars-inline-precompile';
import Post from 'ownersup-client/post/model';
describeComponent( 'comment-list', 'Integration: CommentListComponent', {
integration: true
},
function() {
it('renders all of the comments', function() {
const model = Post.create({ title: 'title' });
model.get('comments').createRecord({ body: 'One Comment' })
this.render(hbs`{{comment-list model=model}}`);
assert.lengthOf(this.$('.comment-list-item'), 1);
});
}
);
Anyone have any thoughts?
Among all Ember test helpers, the store is only available from moduleForModel.
Here's how this test helper does it (source):
var container = this.container;
var store = container.lookup('service:store') || container.lookup('store:main');
You can do the same inside your test. You can also put it into a helper so that you don't have to copy-paste a lot.
Note that it will only work for an integration test. You can turn any test into integration one by starting the app using the startApp test helper that is bundled with your Ember CLI boilerplate.
The new ember mocha release 0.8.4 contains a new, public way to inject services such as the store. There's a guides section coming soon with an example (see https://github.com/emberjs/guides/blob/master/source/testing/testing-components.md)
essentially, in your beforeEach you want add the following line: this.inject.service('store');, making the store accessible as this.get('store') in your tests.
Here's a link to the pull request for the new change: https://github.com/switchfly/ember-test-helpers/pull/105
General answer for people who are struggled with similar things related to injecting in integration tests.
Everything depends on which version and solutions you have in your project.
When you have an integration test with module
(import { module } from 'ember-qunit';)
you can use this.owner.lookup('service:store') inside your test
for more information, there is a great article from Dockyard
https://dockyard.com/blog/2018/01/11/modern-ember-testing
I'm installing the 'ember-moment' helper and then I'm using it in components. As soon as I do that, those components' tests break saying pretty much nothing:
not ok 120 PhantomJS 1.9 - StatusCardComponent: it renders
---
actual: >
null
message: >
Died on test #2 at http://localhost:7357/assets/test-support.js:2779
at test (http://localhost:7357/assets/test-support.js:1796)
at http://localhost:7357/assets/client.js:11190
at http://localhost:7357/assets/vendor.js:150
at tryFinally (http://localhost:7357/assets/vendor.js:30)
at http://localhost:7357/assets/vendor.js:156
at http://localhost:7357/assets/test-loader.js:29
at http://localhost:7357/assets/test-loader.js:21
at http://localhost:7357/assets/test-loader.js:40
at http://localhost:7357/assets/test-support.js:5545: Assertion Failed: A helper named 'moment' could not be found
Log: >
...
And here is the code of the test itself, it's just the auto generation:
import {
moduleForComponent,
test
} from 'ember-qunit';
moduleForComponent('event-card', {
// Specify the other units that are required for this test
// needs: ['component:foo', 'helper:bar']
});
test('it renders', function(assert) {
assert.expect(2);
// Creates the component instance
var component = this.subject();
assert.equal(component._state, 'preRender');
// Renders the component to the page
this.render();
assert.equal(component._state, 'inDOM');
});
I tried a bunch of ways of adding this helper to the test as a dependency like needs: ['helper:moment'] and adding its initializer signature to the moduleForComponent function's params, but nothing worked and I can't find any information about it. Help is much appreciated.
Edit: According to this PR, you should be able to specify { integration: true } to turn your existing unit test into an integration test, which eliminates the need for needs:.
moduleForComponent('post', { integration: true });
More background can be found here: https://github.com/rwjblue/ember-qunit/issues/108. Essentially, the integration flag disables the isolated container for the test, so that it has access to the app's resolver.
Note: Requires ember-qunit#0.2.11.
Old Answer: Take a look at this PR: https://github.com/switchfly/ember-test-helpers/pull/21 which adds moduleForIntegration. Here's the relavant bit:
This adds a new kind of test module that fills a gap in the current
set of possible tests. So far we have:
• unit tests that are good for testing algorithmic correctness in isolation.
• acceptance tests that are good for testing within a complete
application.
But we're missing the ability to integration test a unit smaller than
whole-application. This PR adds a new TestModuleForIntegration that
lets you drive an integration test with a template. I think this is
often a more appropriate way to test Ember Components than a unit
test, because interesting components tend to have rich dependence on
other components.
You can see it in use in liquid-fire tests:
/* global sinon */
import Ember from "ember";
import moduleForIntegration from "../../helpers/module-for-integration";
import { test } from "ember-qunit";
moduleForIntegration('Integration: liquid-if');
test('it should render', function(assert) {
this.set('person', 'Tom');
this.render(`
{{#liquid-if isReady}}
{{person}} is ready
{{else}}
{{person}} is not ready
{{/liquid-if}}
`); // }}`)
assert.equal(this.$().text().trim(), 'Tom is not ready');
this.set('person', 'Yehuda');
assert.equal(this.$().text().trim(), 'Yehuda is not ready');
this.set('isReady', true);
assert.equal(this.$().text().trim(), 'Yehuda is ready');
});
So this is what ended up working:
import {
moduleForComponent,
test
} from 'ember-qunit';
import Ember from 'ember';
import { initialize } from '../../../../../initializers/ember-moment';
moduleForComponent('event-card', {
// Specify the other units that are required for this test
// needs: ['helper:moment'],
setup: function (container) {
Ember.run(function () {
// these two arguments are not used
// but probably still good to pass them in
// in the event we leverage them in the future
initialize(container);
});
}
});
test('it renders', function (assert) {
assert.expect(2);
// Creates the component instance
var component = this.subject();
assert.equal(component._state, 'preRender');
// Renders the component to the page
this.render();
assert.equal(component._state, 'inDOM');
});
Do you see that line that is commented out that reads:
// needs: ['component:foo', 'helper:bar']
You need to uncomment it out and make it read:
needs: ['helper:moment']
That should help, if your moment helper is registered correctly
I'm using Pikaday to create an Ember Datepicker Component within an Ember CLI project. It seems to be impossible to test user interactions within the component test. Does anyone know how to do this?
For example I'm trying to test that the Pikaday widget is displayed when the input of the component is clicked. The test looks like this:
import { test, moduleForComponent } from 'ember-qunit';
moduleForComponent('datepicker-input');
test('is an input tag', function() {
equal('INPUT', this.$().prop('tagName'));
});
test('clicking the input opens the pikaday dialog', function() {
ok($('.pika-single').hasClass('is-hidden'));
click(this.$().find('input'));
ok(!$('.pika-single').hasClass('is-hidden'));
});
The second tests fails due to ReferenceError: click is not defined. I don't know what I'm doing wrong, as far as I can tell my tests do the same as the example on the Ember.js website: http://emberjs.com/guides/testing/testing-components/#toc_interacting-with-components-in-the-dom
So I'm guessing the problem could be also with Ember CLI. Any help is welcome, I'm open to suggestions how to test the user interactions of an component.
You need to add the component to the DOM.
test('clicking the input opens the pikaday dialog', function() {
$input = this.append();
ok($('.pika-single').hasClass('is-hidden'));
click('#your-input').then(function() {
ok(!$('.pika-single').hasClass('is-hidden'));
});
});
I ended up starting and destroying the app by hand. In addition I also teardown the component after every test by hand. This is necessary until a few upstream changes from the QUnit addon land in Ember CLI.
You can look at the complete test file on GitHub: https://github.com/edgycircle/ember-pikaday/blob/master/tests/unit/components/pikaday-input-test.js
I think you need to call App.setupForTesting, and in older versions of ember, additionally App.injectTestHelpers before the tests are run.
http://emberjs.com/guides/testing/integration/#toc_setup
You also need to call this.append to make the component show up in the DOM.
In what version of Ember was this problem? I've tested with the auto-generated component test almost identically on how I tested on an ember App on an Ember AddOn (ember 2.4.3).
import { moduleForComponent, test } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';
moduleForComponent('image-item', 'Integration | Component | image item', {
integration: true
});
test('it renders an image with alt', function(assert) {
this.set('src', 'image.png');
this.set('alt', 'grandmother');
this.render(hbs`{{image-item src=src alt=alt}}`);
assert.equal(this.$('img').attr('src'), 'image.png');
assert.equal(this.$('img').attr('alt'), 'grandmother');
});