I want to set a unit test on a very simple Vue Component with the default Vue Test Utils plugin coupled to Jest Framework.
On a click of a button, the handler calls 2 methods:
emitEvent(): to emit an event (actual target of my test),
effectUI(): for UI effect (using the Web Animations API). This animation is applied on each 'particle' of a 'particles' array. I do not wish to test this part (yet), but this is the one which is problematic.
I works fine when I run the component. No warnings, no errors.
But when I run the test, it passes... with console.error stating that 'particle.animate' is not a function.
I have tried:
first, to do nothing special: since the EffectUI() method has nothing to do with the click event (except they are called by the same handler) so maybe they do...
then, to mock the "animate" function: with no result so far. I assume the issue comes from the Web API method not being recognized. I may be completely wrong.
Code of method called from component click's handler:
effectUI() {
let particles = this.$el.querySelectorAll('span.particle')
particles.forEach(particle => { particle.animate(...) }
}
Code of test file:
import { mount } from '#vue/test-utils'
import ButtonParticles from '#/components/ButtonParticles.vue'
describe('ButtonParticles.vue', () => {
const wrapper = mount(ButtonParticles)
const animStub = jest.fn()
it('should trigger `clicked` event when user clicks on button', () => {
let particles = wrapper.findAll('.particle')
particles.wrappers.forEach(particle => {
particle.animate = animStub
})
wrapper.find('button').trigger('click')
expect(wrapper.emitted().clicked).toBeTruthy()
})
})
Expected results would be to get no console.error
Actual results are: [Vue warn]: Error in v-on handler: "TypeError: particle.animate is not a function" (+ stack trace)
Anyone can help me understand what's happening?
Thanks!
In your test, particle is a wrapper. Try particle.element.animate = animStub
Related
I created a simple ionic app that allows users to book services. The user would select a service category, choose a service(s) then navigate to a form to complete the booking.
I've setup an event bus using tiny emitter since the project uses vue 3 with the composition api. The data emits as expected however when navigating to the booking form the listener is not triggered.
The expected behaviour is to get the selected service(s) and send it along with the rest of the booking info to a REST api.
eventBus.js
import { TinyEmitter } from 'tiny-emitter';
const emitter = new TinyEmitter();
const eventBus = () => {
return { emitter };
};
export default eventBus;
Service.vue
// template
<ion-button routerDirection="forward" routerLink="/booking" #click="sendEvent">Book Now</ion-button>
// script
import eventBus from './eventBus';
export default {
...
setup() {
...
const sendEvent = () => {
eventBus().emitter.emit('selected-service', 100) // the real code emits an array
}
return { sendEvent }
}
}
Booking.vue - Nothing happens in the console log
<script>
...
onMounted(() => {
eventBus().emitter.on('selected-service', (payload) => {
console.log('listener', payload);
})
})
</script>
I know this works in a regular Vue 3 project but I'm not sure why it's not working with ionic.
Things I've tried
Using the native emitter via context as a setup param. https://v3.vuejs.org/guide/composition-api-setup.html#accessing-component-properties
Using the mitt package as described here: Vue 3 Event Bus with Composition API
Emitting the event when the user chooses a service rather than when they click "Book Now"
Calling the listener in setup() directly rather than onMounted.
UPDATE
I noticed the listener gets called when I navigate off the booking page then back to it. So if I go from service details -> booking -> back to service details -> booking it triggers the bus and the payload is captured.
This may be a framework level bug. I've spoken to the Ionic team via twitter and was advised to use query params instead so that's the route I took.
may be rxjs helps you
import { Subject } from 'rxjs';
private newProduct = new Subject<any>();
publishNewProduct() {
this.newProduct.next();
}
subscribeNewProduct(): Subject<any> {
return this.newProduct;
}
Just had the same problem, to make it worse, in my case the bug was intermittent and only present if the dev tools console was closed. Opening the dev tools or using alerts would result in the component being rendered in time to receive the event.. I almost lost my sanity over it.
In my case using a watcher on the a prop using immediate: true was the cleanest solution.
I think this bug is really nasty since global events support have been removed from Vue2 and the Vue3 upgrade docs explicitly suggest to use tiny emitter to achieve it.
This leads to weird behaviors and bugs that are almost impossible to trace back. For this reason, I think the global event pattern should be avoided as much as possible.
As a final note if it can help someone, this is how I ended up being able to use console logs to trace the problem back to the global event bus:
const logs = []
app.config.globalProperties.$log = function () { logs.push(arguments) }
window.viewlogs = () => {
for (let i = 0; i < logs.length; i++) {
console.log.apply(window, logs[i])
}
}
Once the bug occured I could open the dev tools and view logs using window.viewlogs()
Following the documentation, I have written a Vue component containing the following logic:
import debounce from 'lodash/debounce'
export default {
[...]
created () {
this.debouncedOnSubmit = debounce(this.doSubmit, 1000)
},
[...]
The idea behind this is that my form calls the debouncedOnSubmit method upon form submission:
<button
name="order-basket"
type="submit"
#click.prevent="debouncedOnSubmit"
>
Click me!
</button>
Now, that code works fine in my application, as well as in my jest tests. For example, with the vue test utils, I can trigger the click event on that button and the relevant stuff that should happen upon clicking that button can be verified successfully.
However, I get the following annoying warning:
[Vue warn]: Error in v-on handler: "TypeError: _vm.debouncedOnSubmit is not a function"
I partially understand why I get that warning. Indeed, I define this.debouncedOnSubmit in the created() hook. It's pretty likely that Jest does not understand the meaning of that variable, even though the debounce method returns a function.
How can make jest understand that debouncedOnSubmit is a function? What do I need to configure?
I do not want to disable the warnings, as explained here, because I do want to keep a feedback on my doings in my tests. Most of time, the warnings are really helpful and I do not feel comfortable with the idea of switching them off. How can I write my code so that this warning is fixed?
EDIT
changing
#click.prevent="debouncedOnSubmit"
to
#click.prevent="debouncedOnSubmit()"
does not change anything to the problem
moving debouncedOnSubmit from the created() hook to the computed stuff like this
debouncedOnSubmit: debounce(this.doSubmit, 1000)
throws the error
TypeError: Cannot read property 'doSubmit' of undefined
changing
#click.prevent="debouncedOnSubmit"
to
#click.prevent="debouncedOnSubmit()"
and moving debouncedOnSubmit from the created() hook to the computed stuff like this
debouncedOnSubmit () {
const submit = this.doSubmit
return debounce(submit, 1000)
}
still makes my app and tests work, but I still end up with the same warning in my jest tests:
[Vue warn]: Error in v-on handler: "TypeError: _vm.debouncedOnSubmit is not a function"
Use methods property to define your functions
methods: {
debouncedOnSubmit()
}
"TypeError: _vm.debouncedOnSubmit is not a function" means vuejs can not find any function in it's build-in functions with this name.
one thing that you miss is just putting your function in Default Components Options like:
methods, computed, actions
methods: {
debouncedOnSubmit () {
const submit = this.doSubmit
return debounce(submit, 1000)
}
}
then you can call your function without error
I'm having a rough time trying to test if some function is bound to a component after the component has been initialized.
This is my ngOnInit() function:
ngOnInit() {
this.someFunction = this.someFunction.bind(this);
}
And this is the function that I want to bind to the component::
someFunction() {
// this empty function is not called yet but should be bound to the component
}
And this is my before each:
beforeEach(async(() => {
fixture = TestBed.createComponent(ComponentName);
component = fixture.componentInstance;
fixture.detectChanges();
}));
And this is my describe function:
describe('ngOnInit', () => {
it('someFunction has been bound to the component.', () => {
let bindFunctionSpy = spyOn(component.someFunction, 'bind').and.callThrough();
component.ngOnInit();
expect(bindFunctionSpy).toHaveBeenCalledWith(component);
});
});
The problem that I'm facing here is that there is a typescript error in the spyOn function preventing me from compiling the test cases, it says :
error TS2345: Argument of type '"bind"' is not assignable to parameter of type 'never'.
So what exactly am I not doing right here?
The same thing happens if I try spying on any of the prototype functions for a component function like apply or call for example.
Yet if I tried to spy on a prototype function for a component variable like length or toLowerCase it doesn't throw such error!
Another note is that this test actually sometimes gets compiled successfully and actually passes, and sometimes it throws the error while compilingm, but it happens only when I make any random changes like adding a space then saving them so that Karma can detect that a change happened and recompile the tests, but if I closed the terminal and then started it again and ran ng test I get the error again.
The best way to do this is just casting your function to the CallableFunction type.
let bindFunctionSpy = spyOn(component.someFunction as CallableFunction, 'bind').and.callThrough();
You could try
let bindFunctionSpy = spyOn(component.someFunction.prototype, 'bind').and.callThrough();
Try spying on Function.prototype like this -
spyOn(Function.prototype, 'bind');
it worked for me.
I am new to JavaScript testing and currently trying to write some test cases for a store (just an ES6 class) I created. I am using Jest as this is what we usually use for React projects, although here I am not testing a React Component but just a class wrapping a functionality.
The class I am testing extends another class, and has various methods defined in it. I want to test these methods (whether they are called or not), and also whether the properties declared in the class change as and when the corresponding class methods are called.
Now I have read about mocking functions, but from what I understand, they can only do checks like how many times a function is called, but can't replicate the functionality. But in my case, I need the functionality of the methods because I will be checking the class member values these methods change when called.
I am not sure if this is the right approach. Is it wrong to test functions in Jest without mocking? And inferentially, to test the internal workings of functions? When do we mock functions while testing?
The issue I am facing is that the project I am working on is a large one where there are multiple levels of dependencies of classes/functions, and it becomes difficult to test it through Jest as it will need to go through all of them. As I am using alias for file paths in the project, Jest throws errors if it doesn't find any module. I know its possible to use Webpack with Jest, but many of the dependent classes/functions in the code are not in React, and their alias file paths are not maintained by Webpack.
import { getData } from 'service/common/getData';
class Wrapper extends baseClass {
someVariable = false;
payload = null;
changeVariable() {
this.someVariable = true;
}
async getData() {
super.start();
response = await fetchData();
this.payload = response;
super.end();
}
}
This is a small representation of the actual code I have. Can't post the entire class here as I am working on a remote machine. Basically, I want to test whether changeVariable gets called when invoked, and whether it successfully changes someVariable to true when called; and similarly, check the value of payload after network request is complete. Note that fetchData is defined in some other file, but is critical to testing getData method. Also the path used here (service/common/getData) for importing getData is not the absolute path but an alias NOT defined in Webpack, but somewhere else. Jest can't resolve getData because of this. I will not have to worry about this if I mock getData, but then I will not be able to test its functionality I believe.
#maverick It's perfectly okay to test your class methods using jest. Check the code example in the link -
https://repl.it/repls/ClumsyCumbersomeAdware
index.js
class Wrapper {
constructor(){
this.someVariable = false;
}
changeVariable(){
this.someVariable = true;
}
getData(){
return new Promise(resolve => resolve('some data'));
}
}
module.exports = Wrapper;
index.test.js
const Wrapper = require('./index');
const wrapper = new Wrapper();
describe('Wrapper tests', () => {
it('should changeVariable', () => {
wrapper.changeVariable();
expect(wrapper.someVariable).toBe(true);
});
it('should get some data', () => {
wrapper.getData().then( res => expect(res).toBe('some data'));
});
});
This is a very simplistic example and in real life the async calls are much more complicated and dependent of 3rd party libraries or other project modules. In such cases it makes sense to have all the dependencies injected in out class and then mocked individually. For Example -
class GMapService {
constructor(placesApi, directionApi){
this.placesApi = placesApi;
this.directionApi = directionApi;
}
getPlaceDetails(){
this.placesApi.getDetails('NYC');
}
getDirections(){
this.directionApi.getDirections('A', 'B');
}
}
Now you can easily mock placesApi and directionApi, and test them individually without actually requiring Google Map dependencies.
Hope this helps ! 😇
I have a method which uses an ElementRef which is defined below.
#ViewChild('idNaicsRef') idNaicsRef: ElementRef;
ElementRef then sets the focus using .nativeElement.focus().
The method fails while running the spec, saying 'undefined is an object'
Although httpNick's answer should work, I ended up asking an architect on my team about this and he led me to a slightly different solution that may be a bit simpler.
describe(MyComponent.name, () => {
let comp: MyComponent;
describe('myFunction', () => {
it('calls focus', () => {
comp.idNaicsRef = {
nativeElement: jasmine.createSpyObj('nativeElement', ['focus'])
}
comp.myFunction();
expect(comp.idNaicsRef.nativeElement.focus).toHaveBeenCalled();
});
});
This particular example would just test to see if the focus method has been called or not. That's the test that I was interested in when I was testing my method, but you could of course test whatever you wanted. The key is the setup beforehand (which was elusive before it was shown to me).
this should work. this just creates a spy object and then you can populate it with whatever you want, so you could even check if it was called in your unit test.
import createSpyObj = jasmine.createSpyObj;
comp.idNaicsRef = createSpyObj('idNaicsRef', ['nativeElement']);
comp.idNaicsRef.nativeElement = { focus: () => { }};
comp is the reference to the component you are testing.
createSpyObj comes from a jasmine import