How to test events on the root element of component in vue 2? - unit-testing

I'm writing unit tests for the following component:
<template>
<sub-component
#foo="bar"
/>
</template>
<script>
import SubComponent from './SubComponent';
export default {
name: 'MyComponent',
components: { SubComponent },
methods: {
bar(payload) {
this.$emit('baz', ...payload);
}
}
}
</script>
And the test would be:
import { shallowMount } from '#vue/test-utils';
import _ from 'lodash';
import MyComponent from '../../components/MyComponent';
describe('MyComponent.vue', () => {
let wrapper;
beforeEach(() => {
wrapper = shallowMount(MyComponent);
});
it('should emit baz on subcomponent foo', () => {
const subComp = wrapper.find('sub-component-stub');
expect(subComp.exists()).toBe(true); // passes
subComp.vm.$emit('foo');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted().baz).toBeTruthy(); // does not pass;
// upon logging:
console.log(_.isEqual(wrapper, subComp)); // => true
})
})
})
The example is oversimplified, but the principle here is I want a reusable <sub-component> (a modal) and various functional wrappers around it (related to one particular task the modal type performs) which map additional functionality. I don't want the functionality in the parent components, as it would violate DRY - i'd have to place it in each component containing a particular type of modal.
This would work fine if <sub-component> was not the direct child of <template>. Somehow, it appears wrapper and subComp are hosted on the same element.
How should this be tested properly?

Another possibility it's to find your element in the dom and check the emitted value of your root component.
import { shallowMount } from '#vue/test-utils'
import MyComponent from './MyComponent.vue'
import SubComponent from './SubComponent.vue'
describe('MyComponent', () => {
it('should emit baz on subcomponent foo', () => {
const wrapper = shallowMount(MyComponent)
const subComponent = wrapper.find(SubComponent)
expect(subComponent.exists()).toBe(true)
expect(wrapper.emitted('baz')).toBeUndefined()
subComponent.vm.$emit('foo', ['hello'])
expect(wrapper.emitted('baz')[0]).toEqual(['hello'])
// or expect(wrapper).toEmit('baz', 'hello') cf. below for toEmit
})
})
If you want a custom matcher for Jest:
toEmit(received, eventName, data) {
if (data) {
expect(received.emitted()[eventName][0]).toEqual([data])
} else {
expect(received.emitted()[eventName][0]).toEqual([])
}
return { pass: true }
}

Related

vue.js unit test v-slot with function call

I use bootstrap-vue for the vue.js css framework and decided to test the desired component. This component uses b-table and has a v-slot with a call function.
<template>
<b-table
striped
bordered
:items="items"
:fields="$t('pages.events.show.users.fields')"
>
<template v-slot:cell(name)="{ item }">
<b-avatar :src="item.avatar" class="mr-2" />
<span v-text="item.name" />
</template>
</b-table>
</template>
and I'm writing a simple test for this component:
import { shallowMount } from "#vue/test-utils";
import EventUsersTable from "./EventUsersTable.vue";
/* #region Test setup */
const factory = () => {
return shallowMount(EventUsersTable, {
mocks: {
$t: jest.fn()
},
stubs: {
BTable: true
}
});
};
/* #endregion */
describe("EventUsersTable.vue", () => {
let wrapper;
beforeEach(() => (wrapper = factory()));
test("should render component", () => {
expect(wrapper.html()).toMatchSnapshot();
});
});
and i have error with this content: [Vue warn]: Error in render: "TypeError: Cannot read property 'item' of undefined"
for write test for this component i need fix this problem.
And I have a problem with the vue unit test document, they are very limited and with few examples.
If anyone knows a source that has more examples and scenarios for vue language tests, thank you for introducing it.
After inquiring, I came up with a solution that, using mount and adding the main component, was able to solve my problem.
import { mount } from "#vue/test-utils";
import { BTable } from "bootstrap-vue";
import EventUsersTable from "./EventUsersTable.vue";
/* #region Test setup */
const factory = () => {
return mount(EventUsersTable, {
mocks: {
$t: jest.fn()
},
stubs: {
BTable
}
});
};
/* #endregion */
describe("EventUsersTable.vue", () => {
let wrapper;
beforeEach(() => (wrapper = factory()));
// FIXME: fix this bug for render component
test("should render component", () => {
expect(wrapper.html()).toMatchSnapshot();
});
});

Testing that a method is called when component is mounted

I'm trying to test that a method gets called when a component is mounted but it keeps failing with
Expected mock function to have been called one time, but it was called zero times.
Here is the component:
<template>
<b-form-input
class="mr-2 rounded-0"
placeholder="Enter Search term..."
id="input-keyword"
/>
</template>
<script>
export default {
name: 'job-search-test',
methods: {
async searchJobs () {
console.log('Calling Search Jobs from JobsSearchTest')
}
},
mounted () {
this.searchJobs()
}
}
</script>
Here is the test:
import { shallowMount, createLocalVue } from '#vue/test-utils'
import BootstrapVue from 'bootstrap-vue'
import JobSearchTest from '#/components/jobs/JobSearchTest'
const localVue = createLocalVue()
localVue.use(BootstrapVue)
describe('JobsSearchTest.vue', () => {
it('should call searchJobs method when component is mounted', () => {
const methods = {
searchJobs: jest.fn()
}
shallowMount(JobSearchTest, {
mocks: {
methods
},
localVue })
expect(methods.searchJobs).toHaveBeenCalledTimes(1)
})
})
However, the following test passes
import { shallowMount, createLocalVue } from '#vue/test-utils'
import BootstrapVue from 'bootstrap-vue'
import JobSearchTest from '#/components/jobs/JobSearchTest'
const localVue = createLocalVue()
localVue.use(BootstrapVue)
describe('JobsSearchTest.vue', () => {
it('should call searchJobs method when component is mounted', () => {
let searchJobs = jest.fn()
shallowMount(JobSearchTest, {
methods: {
searchJobs
},
localVue })
expect(searchJobs).toHaveBeenCalledTimes(1)
})
})
According to Testing VueJs Applications by Edd Yerburgh one tests a function by stubbing it with a Jest mock the following way
it('should call $bar.start on load', () => {
const $bar = {
start: jest.fn(),
finish: () => {}
}
shallowMount(ItemList, { mocks: $bar })
expect($bar.start).toHaveBeenCalledTimes(1)
})
In my eyes, this is essentially what I am doing in the first test, which fails.
Any help with why this could be happening will be appreciated.
mocks option mocks instance properties. mocks: { methods } assumes that there's methods property in Vue component. Since this.methods.searchJobs() isn't called, the test fails.
It's searchJobs method, the test should be as the working snippet shows:
shallowMount(JobSearchTest, {
methods: {
searchJobs
},
localVue })

How do I test that a Vue component responds to an event emitted by $root?

I have a component that listens for an event emitted by the Vue $root instance.
export default {
data() {
return {
name: ''
}
},
methods: {
openModal(name) {
this.name = name
}
},
mounted() {
this.$root.$on('open-modal', name => {
this.openModal(name);
});
}
}
And I have another place the code where I'm calling that event.
this.$root.$emit('open-modal', 'some-name');
How can I write a unit test that calls that event on $root and asserts the event has been called? I'm using Vue test utils https://vue-test-utils.vuejs.org/en/ and can't find a way to call the event.
I tried this but it doesn't work.
it('sets the modal name on the open-modal event', () => {
const wrapper = mount(Modal);
wrapper.vm.$root.$emit('open-modal', 'my-modal')
expect(wrapper.vm.$data.name).to.equal('my-modal');
});
I figured out what was wrong. I was emitting the event correctly. The problem was my component is using VueRouter and calling $router.push() in the openModal method (I left that out of the code example to keep it short). I had to stub VueRouter in my test and everything worked fine. Here's what my test looks like now.
import { shallow, createLocalVue } from 'vue-test-utils';
import VueRouter from 'vue-router';
import Modal from '../cw-modal.vue';
const localVue = createLocalVue();
localVue.use(VueRouter);
describe('cw-modal component', () => {
it('sets visible to true when the "open-modal" even is called with the modalName', () => {
const wrapper = shallow(Modal, {
propsData: {
cwModalName: 'my-modal'
},
localVue,
router: new VueRouter()
});
wrapper.vm.$root.$emit('open-modal', 'my-modal');
expect(wrapper.vm.$data.visible).to.equal(true);
});
}

Angular2 unit test with debounceTime in component "Cannot use setInterval from within an async zone test"

I am trying to test a component that renders background images whenever elements are visible. I detect visibility on scroll an resize events, and to ease the load I'm using debounceTime from RxJS. This however is a problem since the unit test fails with
Failed: Cannot use setInterval from within an async zone test.
I know this is a problem due to the async nature of debounceTime, but I'm not sure how to prevent this / mock debounceTime for the test.
This is the Component
import { Component } from '#angular/core';
import { EventEmitter } from '#angular/core';
import { Observable } from 'rxjs/Observable';
import * as config from '../../config';
import ListComponentBase from '../../../forminputs/ListComponentBase';
#Component({
selector: `list-component`,
template: `
<a
*ngFor="let item of data.list"
class="box"
routerLink="/edit/{{item._id}}">
<div class="name" >{{ item.name }}</div>
<list-item-background
*ngIf="isVisible[item._id]" [mediaObjectId]="item.coverImageUrl"></list-item-background>
</a>
`,
inputs: ['data', 'id'],
outputs: ['filter', 'sorting'],
})
export class ListComponent extends ListComponentBase {
constructor() {
this.isVisible = {};
}
ngOnInit() {
this.check();
if (document.querySelector('nav')){
this.scroll = Observable
.fromEvent(document.querySelector('nav'), 'scroll')
.debounceTime(100).subscribe((event) => {
this.checkVisibility();
});
}
this.resize = Observable
.fromEvent(window, 'resize')
.debounceTime(100).subscribe((event) => {
this.checkVisibility();
});
}
check() {
this.data.list.forEach(item => {
this.isVisible[item._id] = this.isElementInViewport(document.querySelector(`[data-id="${item._id}"]`));
});
}
isElementInViewport(el) {
if (!el) return false;
const rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /*or $(window).height() */
rect.right <= (window.innerWidth || document.documentElement.clientWidth) /*or $(window).width() */
);
}
}
This is the unit test
import { Router } from '#angular/router';
import { TestBed, inject, async } from '#angular/core/testing';
import { StoreModule } from '#ngrx/store';
import RouterLinkMockModule from '../../../testing/RouterLinkMock.module';
import { ListComponent } from './list.component';
import { defaultData, collectionName } from '../../config';
const data = {
sort: initialState.sort,
list: [defaultData, defaultData],
articles: { null: { name: 'test' } },
};
describe(`${collectionName} ListComponent`, () => {
let fixture;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
RouterLinkMockModule,
],
declarations: [
ListComponent,
],
});
});
it('should render 2 items in list', async(inject([Router], (router) => {
fixture = TestBed.createComponent(ListComponent);
fixture.componentInstance.data = data;
fixture.detectChanges();
const el = fixture.debugElement.nativeElement;
expect(el.querySelectorAll('.box').length).toBe(2);
})));
});
You can swap out the default scheduler used by debounceTime with a TestScheduler. It will then no longer schedule future values using setInterval. I am not 100% sure if your test will succeed without the debounce running, otherwise you will have to make a true Rx test in which you let the TestScheduler run in virtual time so it will reach the desired end state as if your debounce had been triggered IRL
I had the very same problem and what helped was using the jasmine's 'done'. Though I must admit I am not familiar with the inject and I do not know what versions of angular, etc. you are running, I am trying to adapt your code, in the way that it worked for me:
...
describe(`${collectionName} ListComponent`, () => {
let fixture;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
RouterLinkMockModule,
],
declarations: [
ListComponent,
],
providers: [
Router //I think you have to do that here if you do not use the inject
]
});
});
it('should render 2 items in list', (done) => {
fixture = TestBed.createComponent(ListComponent);
fixture.componentInstance.data = data;
fixture.detectChanges();
const el = fixture.debugElement.nativeElement;
expect(el.querySelectorAll('.box').length).toBe(2);
done();
});
...
I am not a 100% sure that this will work for you - I have another method that stores all the references I need in a class called 'Page', like it is suggested on the angular.io page for testing. In there I also call create component and I think that is the critical part that should be covered by the 'done'.
Not sure if that is relevant, but for reference: I am using angular 2.3.1 and karma 1.2.0 with jasmine-core 2.5.2

Angular 2 unit testing data passed from parent component to child component

I have a question about the way I've seen (the very few) examples of testing of data passed down from a parent component into a child component. Currently, in the Angular2 docs, they're testing to see if data has been passed down from a parent component to a child by inspecting the dom values of the child component. The issue that I have with this approach is that it forces the parent's spec to know the html structure of the child component. The parent component's job is just to pass data into the child. An example...
I have a Story Component as follows:
'use strict';
import {Component, OnInit, Input} from '#angular/core';
import {StoryService} from '../../services/story.service';
import {StoryModel} from '../../models/story-model';
import {AlbumCover} from './album-cover/album-cover';
import {Author} from "./author/author";
import {StoryDuration} from "./story-duration/story-duration";
#Component({
selector: 'story',
templateUrl: 'build/components/story/story.html',
providers: [StoryService],
directives: [AlbumCover, Author, StoryDuration]
})
export class Story implements OnInit {
#Input('id') id:number;
public story:StoryModel;
constructor(private storyService:StoryService) {}
ngOnInit() {
this.getStory();
}
private getStory() {
this.storyService.getStory(this.id).subscribe(story => this.story = story);
}
}
Notice how it has an AlbumCover Component dependency in the directives array in the #Component decorator.
Here is my Story template:
<div *ngIf="story">
<album-cover [image]="story.albumCover" [title]="story.title"></album-cover>
<div class="author-duration-container">
<author [avatar]="story.author.avatar" [name]="story.author.name"></author>
<story-duration [word-count]="story.wordCount"></story-duration>
</div>
</div>
Notice the <album-cover [image]="story.albumCover" [title]="story.title"></album-cover> line where I'm binding the story.albumCover from the Story controller to the image property of the AlbumCover. This is all working perfectly. Now for the test:
import {provide} from '#angular/core';
import {beforeEach, beforeEachProviders, describe, expect, injectAsync, it, setBaseTestProviders, resetBaseTestProviders} from '#angular/core/testing';
import {HTTP_PROVIDERS} from '#angular/http';
import {BROWSER_APP_DYNAMIC_PROVIDERS} from "#angular/platform-browser-dynamic";
import {TEST_BROWSER_STATIC_PLATFORM_PROVIDERS, ADDITIONAL_TEST_BROWSER_PROVIDERS} from '#angular/platform-browser/testing';
import {ComponentFixture, TestComponentBuilder} from '#angular/compiler/testing';
import {Observable} from 'rxjs/Observable';
// TODO: this pattern of importing 'of' can probably go away once rxjs is fixed
// https://github.com/ReactiveX/rxjs/issues/1713
import 'rxjs/add/observable/of';
resetBaseTestProviders();
setBaseTestProviders(
TEST_BROWSER_STATIC_PLATFORM_PROVIDERS,
[BROWSER_APP_DYNAMIC_PROVIDERS, ADDITIONAL_TEST_BROWSER_PROVIDERS]
);
import {Story} from './story';
import {StoryModel} from '../../models/story-model';
import {StoryService} from '../../services/story.service';
var mockStory = {
id: 1,
title: 'Benefit',
albumCover: 'images/placeholders/story-4.jpg',
author: {
id: 2,
name: 'Brett Beach',
avatar: 'images/placeholders/author-1.jpg'
},
wordCount: 4340,
content: '<p>This is going to be a great book! I <strong>swear!</strong></p>'
};
class MockStoryService {
public getStory(id):Observable<StoryModel> {
return Observable.of(mockStory);
}
}
describe('Story', () => {
var storyFixture,
story,
storyEl;
beforeEachProviders(() => [
HTTP_PROVIDERS
]);
beforeEach(injectAsync([TestComponentBuilder], (tcb:TestComponentBuilder) => {
return tcb
.overrideProviders(Story, [
provide(StoryService, {
useClass: MockStoryService
})
])
.createAsync(Story)
.then((componentFixture:ComponentFixture<Story>) => {
storyFixture = componentFixture;
story = componentFixture.componentInstance;
storyEl = componentFixture.nativeElement;
componentFixture.detectChanges();
});
}));
describe(`ngOnInit`, () => {
describe(`storyService.getStory`, () => {
it(`should be called, and on success, set this.story`, () => {
spyOn(story.storyService, 'getStory').and.callThrough();
story.ngOnInit();
expect(story.storyService.getStory).toHaveBeenCalled();
expect(story.story.title).toBe('Benefit');
});
});
});
it('should not show the story component if story does not exist', () => {
story.story = null;
storyFixture.detectChanges();
expect(storyEl.children.length).toBe(0);
});
it('should show the story component if story exists', () => {
story.story = mockStory;
storyFixture.detectChanges();
expect(storyEl.children.length).not.toBe(0);
});
describe('story components', () => {
beforeEach(() => {
story.story = mockStory;
storyFixture.detectChanges();
});
describe('album cover', () => {
var element,
img;
beforeEach(() => {
element = storyEl.querySelector('album-cover');
img = element.querySelector('img');
});
it(`should be passed the story albumCover and title to the album cover component`, () => {
expect(img.attributes.src.value).toBe(mockStory.albumCover);
expect(img.attributes.alt.value).toBe(mockStory.title);
});
});
describe('author', () => {
var element,
img,
nameEl;
beforeEach(() => {
element = storyEl.querySelector('author');
img = element.querySelector('img');
nameEl = element.querySelector('.name');
});
it(`should be passed the author name and avatar`, () => {
expect(img.attributes.src.value).toBe(story.story.author.avatar);
expect(img.attributes.alt.value).toBe(story.story.author.name);
expect(nameEl.innerText).toBe(story.story.author.name);
});
});
describe('story duration', () => {
var element;
beforeEach(() => {
element = storyEl.querySelector('.story-duration');
});
it(`should be passed the word count to generate the total read time`, () => {
story.story.wordCount = 234234;
storyFixture.detectChanges();
expect(element.innerText).toBe(`852 min read`);
});
});
});
});
Look at my describe('album cover'.... The way I'm passing this expectation is that I'm finding the <album-cover> element, then finding the <img> tag inside of it, then checking the <img>'s DOM attributes. To me, this expection should be inside of the album-cover.spec.ts - NOT the story.spec.ts.
My question is: is there a way to test if a parent component passed data into a child component without relying on reading dom values?
You can use overrideTemplate to pass a view just for the test.
return tcb
.overrideTemplate(AlbumCover, '<div>{{valueFromParent}}</div>')
.overrideProviders(Story, [