Angular RC5 - Overridden component template fails to find input property - templates

I have two components:
IfNodeComponent:
#Component({
template: '<se-dynamic-condition (questionLinkClicked)="onQuestionClicked" [condition]="node.condition"></se-dynamic-condition>',
selector: 'se-node-if'
})
export class NodeIfComponent {
#Input() node: NodeProperties;
onQuestionClicked(event: IQuestionLinkClickEvent): void {
// do stuff
}
}
and DynamicConditionComponent:
#Component({
template: '<p>My original template</p>',
selector: 'se-dynamic-condition'
})
export class DynamicConditionComponent {
#Input() condition: Condition;
#Output() questionLinkClicked = new EventEmitter<IQuestionLinkClickEvent>();
}
I am writing a test to check that the [condition] binding is attached to the se-dynamic-condition component inside the if node template. To do this I am overriding the template of the DynamicConditionComponent to simply be {{condition | json}}. This then allows me to compare the JSON and assert that it is identical to the condition that should be passed in.
Before RC5 I used the OverridingTestComponentBuilder to achieve this. But since I've just upgraded to RC5, I am rewriting that test to use the TestBed instead. This is not going too well. Here is how it looks:
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [NodeIfComponent, DynamicConditionComponent]
});
TestBed.overrideComponent(DynamicConditionComponent, {
set: {
template: '{{condition | json}}'
}
});
fixture = TestBed.createComponent(NodeIfComponent);
component = fixture.componentInstance;
element = fixture.nativeElement;
component.node = {some:'data'};
fixture.detectChanges();
});
it('should display a dynamic condition component and pass the condition to it', () => {
let dc = element.querySelectorAll('se-dynamic-condition');
expect(dc.length).toEqual(1, 'Dynamic condition component is found');
expect(dc[0].innerHTML).toEqual(JSON.stringify({some:'data'}, null, 2));
});
However, running this test fails with the following error:
Can't bind to 'condition' since it isn't a known property of 'se-dynamic-condition'.
If I don't override the template for DynamicConditionComponent, then I don't get the error, but understandably my test fails. And if I remove the property binding from the IfNode template, then I don't get the error, but again, the test fails as expected. The error message points towards the se-dynamic-condition component not being registered in the same module. But it is, and the code works when I run it. It is just the test that is the problem, which doesn't use the module definition anyway. That's what the TestBed.configureTestingModule statement is for.
So it appears that overriding a template also loses the condition property associated with the component.
Am I doing something fundamentally wrong here? Examples I have seen elsewhere of overriding the template work fine, but I haven't seen any that override a template with an input property (and then try to assign a value to that property).
Any help would be greatly appreciated.

This is a bug in RC5 fixed in RC6 by https://github.com/angular/angular/pull/10767
For now as a workaround re-specify the inputs in the override statement.
use
TestBed.overrideComponent(DynamicConditionComponent, {
set: {
template: '{{condition|json}}',
inputs: ['condition'],
}
});

Related

Why won't Trix editor mount in Vue component when running tests with Jest?

I built a simple Vue component that wraps the Trix editor. I'm trying to write tests for it, but Trix doesn't seem to mount properly and isn't generating a toolbar element like it does in the browser. I'm using Jest test runner.
TrixEdit.vue
<template>
<div ref="trix">
<trix-editor></trix-editor>
</div>
</template>
<script>
import 'trix'
export default {
mounted() {
let el = this.$refs.trix.getElementsByTagName('trix-editor')[0]
// HACK: change the URL field in the link dialog to allow non-urls
let toolbar = this.$refs.trix.getElementsByTagName('trix-toolbar')[0]
toolbar.querySelector('[type=url]').type = 'text'
// insert content
el.editor.insertHTML(this.value)
el.addEventListener('trix-change', e => {
this.$emit('input', e.target.innerHTML)
})
}
}
</script>
TrixEdit.spec.js
import { mount, shallowMount, createLocalVue } from '#vue/test-utils'
import TrixEdit from '#/components/TrixEdit.vue'
const localVue = createLocalVue()
localVue.config.ignoredElements = ['trix-editor']
describe('TrixEdit', () => {
describe('value prop', () => {
it('renders text when value is set', () => {
const wrapper = mount(TrixEdit, {
localVue,
propsData: {
value: 'This is a test'
}
})
expect(wrapper.emitted().input).toEqual('This is a test')
})
})
})
The expect() fails with the following error
Expected value to equal:
"This is a test"
Received:
undefined
at Object.toEqual (tests/unit/TrixEdit.spec.js:19:39)
Why is Trix not initializing in my test?
trix-editor is not mounting mainly because MutationObserver is not supported in JSDOM 11, and attachToDocument was not used. There were several other bugs in the test described below.
GitHub demo w/issues fixed
Missing MutationObserver and window.getSelection
Vue CLI 3.7.0 uses JSDOM 11, which doesn't support MutationObserver, needed by the Custom Elements polyfill to trigger the connectedCallback. That lifecycle hook normally invokes trix-editor's initialization, which would create the trix-toolbar element that your test is trying to query.
Solution 1: In your test, import mutationobserver-shim before TrixEdit.vue, and stub window.getSelection (called by trix and currently not supported by JSDOM):
import 'mutationobserver-shim' // <-- order important
import TrixEdit from '#/components/TrixEdit.vue'
window.getSelection = () => ({})
Solution 2: Do the above in a Jest setup script, configured by setupTestFrameworkScriptFile:
Add the following property to the config object in jest.config.js (or jest in package.json):
setupTestFrameworkScriptFile: '<rootDir>/jest-setup.js',
Add the following code to <rootDir>/jest-setup.js:
import 'mutationobserver-shim'
window.getSelection = () => ({})
Missing attachToDocument
#vue/test-utils does not attach elements to the document by default, so trix-editor does not catch the connectedCallback, which is needed for its initialization.
Solution: Use the attachToDocument option when mounting TrixEdit:
const wrapper = mount(TrixEdit, {
//...
attachToDocument: true, // <-- needed for trix-editor
})
Premature reference to trix-editor's editor
TrixEdit incorrectly assumes that trix-editor is immediately initialized upon mounting, but initialization isn't guaranteed until it fires the trix-initialize event, so accessing trix-editor's internal editor could result in an undefined reference.
Solution: Add an event handler for the trix-initialize event that invokes the initialization code previously in mounted():
<template>
<trix-editor #trix-initialize="onInit" />
</template>
<script>
export default {
methods: {
onInit(e) {
/* initialization code */
}
}
}
</script>
Value set before change listener
The initialization code adds a trix-change-event listener after the value has already been set, missing the event trigger. I assume the intention was to also detect the first initial value setting in order to re-emit it as an input event.
Solution 1: Add event listener first:
<script>
export default {
methods: {
onInit(e) {
//...
el.addEventListener('trix-change', /*...*/)
/* set editor value */
}
}
}
</script>
Solution 2: Use v-on:trix-change="..." (or #trix-change) in the template, which would remove the setup-order problem above:
<template>
<trix-editor #trix-change="onChange" />
</template>
<script>
export default {
methods: {
onChange(e) {
this.$emit('input', e.target.innerHTML)
},
onInit(e) {
//...
/* set editor value */
}
}
}
</script>
Value setting causes error
The initialization code sets the editor's value with the following code, which causes an error in test:
el.editor.insertHTML(this.value) // causes `document.createRange is not a function`
Solution: Use trix-editor's value-accessor, which performs the equivalent action while avoiding this error:
el.value = this.value
I can confirm the problem lies in the not-so-ideal polymer polyfill included inside trix lib. I did an experiment to force apply the polyfill, then I can reproduce the same error TypeError: Cannot read property 'querySelector' of undefined even inside chrome browser env.
Further investigation narrows it down to the MutationObserver behavior difference, but still haven't got to the bottom.
Way to reproduce:
TrixEdit.vue
<template>
<div ref="trix">
<trix-editor></trix-editor>
</div>
</template>
<script>
// force apply polymer polyfill
delete window.customElements;
document.registerElement = undefined;
import "trix";
//...
</script>

How to test VueRouter's beforeRouteEnter using '#vue/test-utils'?

I'm trying to test my 'Container' component which handles a forms logic. It is using vue-router and the vuex store to dispatch actions to get a forms details.
I have the following unit code which isn't working as intended:
it('On route enter, it should dispatch an action to fetch form details', () => {
const getFormDetails = sinon.stub();
const store = new Vuex.Store({
actions: { getFormDetails }
});
const wrapper = shallowMount(MyComponent, { store });
wrapper.vm.$options.beforeRouteEnter[0]();
expect(getFormDetails.called).to.be.true;
});
With the following component (stripped of everything because I don't think its relevant (hopefully):
export default {
async beforeRouteEnter(to, from, next) {
await store.dispatch('getFormDetails');
next();
}
};
I get the following assertion error:
AssertionError: expected false to be true
I'm guessing it is because I am not mounting the router in my test along with a localVue. I tried following the steps but I couldn't seem to get it to invoke the beforeRouteEnter.
Ideally, I would love to inject the router with a starting path and have different tests on route changes. For my use case, I would like to inject different props/dispatch different actions based on the component based on the path of the router.
I'm very new to Vue, so apologies if I'm missing something super obvious and thank you in advance for any help! 🙇🏽
See this doc: https://lmiller1990.github.io/vue-testing-handbook/vue-router.html#component-guards
Based on the doc, your test should look like this:
it('On route enter, it should dispatch an action to fetch form details', async () => {
const getFormDetails = sinon.stub();
const store = new Vuex.Store({
actions: { getFormDetails }
});
const wrapper = shallowMount(MyComponent, { store });
const next = sinon.stub()
MyComponent.beforeRouteEnter.call(wrapper.vm, undefined, undefined, next)
await wrapper.vm.$nextTick()
expect(getFormDetails.called).to.be.true;
expect(next.called).to.be.true
});
A common pattern with beforeRouteEnter is to call methods directly at the instantiated vm instance. The documentation states:
The beforeRouteEnter guard does NOT have access to this, because the guard is called before the navigation is confirmed, thus the new entering component has not even been created yet.
However, you can access the instance by passing a callback to next. The callback will be called when the navigation is confirmed, and the component instance will be passed to the callback as the argument:
beforeRouteEnter (to, from, next) {
next(vm => {
// access to component instance via `vm`
})
}
This is why simply creating a stub or mock callback of next does not work in this case. I solved the problem by using the following parameter for next:
// mount the component
const wrapper = mount(Component, {});
// call the navigation guard manually
Component.beforeRouteEnter.call(wrapper.vm, undefined, undefined, (c) => c(wrapper.vm));
// await
await wrapper.vm.$nextTick();

How to Test a Global Event Bus With Vue Test Utils?

I am trying to learn how to test events emitted through a global Event Bus. Here's the code with some comments in the places I don't know what to do.
// EvtBus.js
import Vue from 'vue';
export const EvtBus = new Vue();
<!-- CouponCode.vue -->
<template>
<div>
<input
class="coupon-code"
type="text"
v-model="code"
#input="validate">
<p v-if="valid">
Coupon Redeemed: {{ message }}
</p>
</div>
</template>
<script>
import { EvtBus } from '../EvtBus.js';
export default {
data () {
return {
code: '',
valid: false,
coupons: [
{
code: '50OFF',
discount: 50,
message: '50% Off!'
},
{
code: 'FREE',
discount: 100,
message: 'Entirely Free!'
}
]
};
},
created () {
EvtBus.$on('coupon-applied', () => {
//console.info('had a coupon applied event on component');
});
},
methods: {
validate () {
// Extract the coupon codes into an array and check if that array
// includes the typed in coupon code.
this.valid = this.coupons.map(coupon => coupon.code).includes(this.code);
if (this.valid) {
this.$emit('applied');
// I NEVER see this on the coupon-code.spec.js
EvtBus.$emit('coupon-applied');
}
}
},
computed: {
message () {
return this.coupons.find(coupon => coupon.code === this.code).message;
}
}
}
</script>
// tests/coupon-code.spec.js
import expect from 'expect';
import { mount } from '#vue/test-utils';
import CouponCode from '../src/components/CouponCode.vue';
import { EvtBus } from '../src/EvtBus.js';
describe('Reminders', () => {
let wrp;
beforeEach(() => {
wrp = mount(CouponCode);
});
it('broadcasts the percentage discount when a valid coupon code is applied', () => {
let code = wrp.find('input.coupon-code');
code.element.value = '50OFF';
code.trigger('input');
console.log(wrp.emitted('applied'));
//
// I NEVER see this on the outpout.
// How can I test it through a global event bus rather than
// an event emitted from the component instance?
//
EvtBus.$on('coupon-applied', () => {
console.log('coupon was applied through event bus');
});
// Passes, but not using EvtBus instance.
expect(wrp.emitted('applied')).toBeTruthy;
});
});
So, my doubt is how to test that the global event bus is emitting and listening to events inside components that use that event bus.
So, is it possible to test the global Event Bus using Vue Test Utils or I should use another approach?
If component is using global EventBus, eg that's imported outside of given component and assigned to window.EventBus, then it's possible to use global Vue instance to redirect $on or $emit events to wrapper's vm instance. That way you can proceed writing tests as if component is emitting via this.$emit instead of EventBus.$emit:
it('clicking "Settings" button emits "openSettings"', () => {
global.EventBus = new Vue();
global.EventBus.$on('openSettings', (data) => {
wrapper.vm.$emit('openSettings', data);
});
// component emits `EventBus.$emit('openSettings')`
expect(wrapper.emitted('openSettings')).toBeTruthy(); // pass
});
Well,
EvtBus.$on('coupon-applied', () => {
console.log('coupon was applied through event bus');
});
This code in your spec file won't be called because the mounted wrp component is not using the same EvtBus you are importing in your spec file above.
What you require to test this is an npm package named inject-loader so that you can provide your own implementation(stub) of the EvtBus dependency of your coupon code component.
Somewhat like this
const couponCodeInjector = require('!!vue-loader?inject!src/views/CouponCode');
const stubbedModules = {
'../EvtBus.js': {
$on : sandbox.spy((evtName, cb) => cb());
}
};
const couponCode = couponCodeInjector(stubbedModules);
and then in your unit test you can assert whether the stubbedModules['../EvtBus.js'].$on has been called or not when code.trigger('input');
PS: I haven't used vue-test-utils. So I don't know exactly how to the stubbing with this npm package.
But the main thing you need to do is to find a way to stub your EvtBus dependency in the CouponCode component in such a way that you can apply a spy on it and check whether that spy has been called or not.
Unit tests should focus on testing a single component in isolation. In this case, you want to test if the event is emitted, since that is the job of CouponCode.vue. Remember, unit tests should focus on testing the smallest units of code, and only test one thing at a time. In this case, we care that the event is emitted -- EventBus.test.js is where we test what happens when the event is emitted.
Noe that toBeTruthy is a function - you need (). expect(wrp.emitted('applied')).toBeTruthy is actually not passing, since you need () - at the moment, it is actually doing nothing -- no assertion is made.
What your assertion should look like is:
expect(wrp.emitted('applied')).toBeTruthy()
You can go one step further, and ensure it was only emitted once by doing something like expect(wrp.emitted().applied.length).toBe(1).
You then test InputBus in isolation, too. If you can post the code for that component, we can work through how to test it.
I worked on a big Vue app recently and contributed a lot to the main repo and documentation, so I'm happy to help out wherever I can.
Let me know if that helps or you need more guidance. If possible, post EventBus.vue as well.
I got the same issue with vue-test-utils and Jest. For me, createLocalVue() of vue-test-utils library fixed the issue. This function creates a local copy of Vue to use when mounting the component. Installing plugins on this copy of Vue prevents polluting the original Vue copy. (https://vue-test-utils.vuejs.org/api/options.html#localvue)
Adding this to your test file will fix the issue:
const EventBus = new Vue();
const GlobalPlugins = {
install(v) {
// Event bus
v.prototype.$bus = EventBus;
},
};
// create a local instance of the global bus
const localVue = createLocalVue();
localVue.use(GlobalPlugins);
jest.mock('#/main', () => ({
$emit: jest.fn(),
}));
Include this in code in your spec file at the very begining.
Note: '#/main' is the file from which you are importing Event Bus.

How do you test Vuejs Components when using Vuex

I am writing unit tests for my vuejs components in a larger application. My application state is all in the vuex store, so almost all of my components pull data from vuex.
I can't find a working example for writing unit test for this. I have found
Where Evan You says:
When unit testing a component in isolation, you can inject a mocked store directly into the component using the store option.
But I can't find a good example of how to test these components. I have tried a bunch of ways. Below is the only way I have seen people do it. stackoverflow question and answer
Basically it looks like the
test.vue:
<template>
<div>test<div>
</template>
<script>
<script>
export default {
name: 'test',
computed: {
name(){
return this.$store.state.test;
}
}
}
</script>
test.spec.js
import ...
const store = new Vuex.Store({
state: {
test: 3
}
});
describe('test.vue', () => {
it('should get test from store', () => {
const parent = new Vue({
template: '<div><test ref="test"></test></div>',
components: { test },
store: store
}).$mount();
expect(parent.$refs.test.name).toBe(3);
});
}
Note the "ref" this example doesn't work without it.
Is this really the right way to do this? It seems like it will get messy fast, because it requires adding the props into the template as a string.
Evan's quote seems to imply that the store can be added directly to the child component (ie not the parent like the example).
How do you do that?
The answer is actually really straightforward but not currently documented.
const propsData = { prop: { id: 1} };
const store = new Vuex.Store({state, getters});
const Constructor = Vue.extend(importedComponent);
const component = new Constructor({ propsData, store });
Note the store passed to the constructor. propsData is currently documented, the "store" option isn't.
Also if you are using Laravel, there are weird problems with the webpack versions you may be running.
The
[vuex] must call Vue.use(Vuex),
Error was caused by useing laravel-elixir-webpack-official.
If you did the fix:
npm install webpack#2.1.0-beta.22 --save-dev
for this https://github.com/JeffreyWay/laravel-elixir-webpack-official/issues/17
Your tests that include Vuex seem to break with the
[vuex] must call Vue.use(Vuex)
even when you have Vue.use(Vuex)

Angular2 Component: Testing form input value change

I have a text input and i'm listening for the changes.
mycomponent.ts
ngOnInit() {
this.searchInput = new Control();
this.searchInput.valueChanges
.distinctUntilChanged()
.subscribe(newValue => this.search(newValue))
}
search(query) {
// do something to search
}
mycomponent.html
<search-box>
<input type="text" [ngFormControl]="searchInput" >
</search-box>
Running the application everything works fine, but i want to unit-test it.
So here's what i tried
mycomponent.spec.ts
beforeEach(done => {
createComponent().then(fix => {
cmpFixture = fix
mockResponse()
instance = cmpFixture.componentInstance
cmpFixture.detectChanges();
done();
})
})
describe('on searching on the list', () => {
let compiled, input
beforeEach(() => {
cmpFixture.detectChanges();
compiled = cmpFixture.debugElement.nativeElement;
spyOn(instance, 'search').and.callThrough()
input = compiled.querySelector('search-box > input')
input.value = 'fake-search-query'
cmpFixture.detectChanges();
})
it('should call the .search() method', () => {
expect(instance.search).toHaveBeenCalled()
})
})
Test fails as the .search() method is not called.
I guess i have to set the value in another way to have the test realize of the change but i really don't know how.
Anyone has ideas?
It might be a little bit late, but it seems that your code is not dispatching input event after setting input element value:
// ...
input.value = 'fake-search-query';
input.dispatchEvent(new Event('input'));
cmpFixture.detectChanges();
// ...
Updating input html field from within an Angular 2 test
Triggering the value change of FormControl is as simple as:
cmpFixture.debugElement.componentInstance.searchInput.setValue(newValue);
Custom component with #input, subscriptions, two way data binding
If you got a custom component you would need further changes in your application to be able to successfully unit test your application
have a look at the gist here this will give you some idea
https://gist.github.com/AikoPath/050ad0ffb91d628d4b10ef81736af386/raw/846c7bcfc54be8cce78eba8d12015bf749b91eee/#ViewChild(ComponentUnderTestComponent).js
More over complete reading over here carefully otherwise you can easily get confused again -
https://betterprogramming.pub/testing-angular-components-with-input-3bd6c07cfaf6