Unit test for Svelte on:click event - jestjs

Can anybody point me into right direction?
I'm expecting mock function to be called after click event was fired.
What I've got is:
Expected number of calls: 1
Received number of calls: 0
This are my components along with test file:
EventTestingWrapper.svelte
<script>
export let testComponent
export let clickHandler
</script>
<div data-testid="testing-wrapper">
<svelte:component this={testComponent} on:click={clickHandler} />
</div>
Modal.svelte
<div
class="modal-background"
data-testid="modal-background"
on:click|self={close}
>
lorem
</div>
Modal.test.js
test('trying out test wrapper',()=>{
const clickHandler = jest.fn();
const { getByTestId } = render(EventTestingWrapper, {testComponent: Modal, clickHandler})
const modalBackground = getByTestId('modal-background')
const clickEvent = createEvent.click(modalBackground)
fireEvent(modalBackground, clickEvent);
expect(modalBackground).toBeInTheDocument()
expect(clickHandler).toHaveBeenCalledTimes(1)
})

I don't know exactly if this is what you want, but to pass on:click type event listeners to #testing-library/svelte rendered components you must use the component.$on syntax.
For example:
describe('Button', () => {
it('should render correctly', async () => {
const results = render(Button)
const onClick = jest.fn()
results.component.$on('click', onClick)
const button = results.container.querySelector('button')
expect(button).not.toBeNull()
// Using await when firing events is unique to the svelte testing library because
// we have to wait for the next `tick` so that Svelte flushes all pending state changes.
await fireEvent.click(button as HTMLElement)
expect(results.container).toBeInTheDocument()
expect(onClick.mock.calls.length).toEqual(1)
})
})
This link in the testing-library docs seemed to be quite useful https://sveltesociety.dev/recipes/testing-and-debugging/unit-testing-svelte-component/

This question is about jest but for people coming here in the future looking for a vitest solution, try this:
import Component from '$lib/MyComponent.svelte'
import { expect, test, vi } from 'vitest'
test(`invokes callback functions`, async () => {
const keyup = vi.fn()
const click = vi.fn()
const instance = new Component({
target: document.body,
props: { foo: `bar` },
})
// register callbacks (same as passing them as `on:<event>` props to component
instance.$on(`keyup`, keyup)
instance.$on(`click`, click)
const node = document.querySelector(`.some-css-selector`)
if (!node) throw new Error(`DOM node not found`)
node.dispatchEvent(new KeyboardEvent(`keyup`, { key: `Enter` }))
expect(keyup).toHaveBeenCalledTimes(1)
node.dispatchEvent(new MouseEvent(`click`))
expect(click).toHaveBeenCalledTimes(1)
})

Related

How to mock/simulate a child component event for jest tests in svelte?

I want to run an isolate test on my svelte parent component (e.g. OrderSearch). Therefore the behavior of the child component (e.g. SearchForm) should be "simulated". The child component throws a search event that is bound in the parent component to initiateSearch.
SearchForm.svelte (Child component - NOT subject of testing - triggering of "submit" should be simulated)
<script>
const dispatchEvent = createEventDispatcher()
const submit = () => {
dispatchEvent('search', {firstName: '42'})
}
</script>
<div on:click="submit">Submit</div>
OrderSearch.svelte (Parent Component - Subject of testing)
<script>
let results = []
const initiateSearch = (e) => {
console.log('initiate search with', e)
// search is started and returns results
results = [{orderId: 'bar'}]
}
</script>
<SearchForm on:search="initiateSearch"></SearchForm>
{#each results as order}
<div data-testid="order">{order.id}</div>
{/each}
My not working approach so far when testing the OrderSearch.svelte in an isolated way:
OrderSearchTest.js
const {getAllByTestId, component} = render(Component)
expect(getAllByTestId('order')).toHaveLength(0)
await component.getSubComponent('SearchForm').dispatchEvent('search', {detail: {orderId: 'jonine'}}
expect(getAllByTestId('order')).toHaveLength(1)
Don't mock the child's event. You don't need to test that the on:<event> directive works, I would assume that Svelte has corresponding tests to ensure that it does. You need to test only that your component responds in the way that it should when the code that is executed on a particular event occurs. So trigger the event using fireEvent and mock or spy on a function, or function(s) called in the event handler.
Here's an example with appropriate changes made to your component:
OrderSearch.svelte
<script>
import http from "path/to/some/http/util"
let results = []
const initiateSearch = async (e) => {
// search is started and returns results
results = await http.fetchSearchResults(e.detail)
}
</script>
<SearchForm on:search="initiateSearch"></SearchForm>
{#each results as order}
<div data-testid="order">{order.id}</div>
{/each}
Then the corresponding test could look like:
import mockHttp from "path/to/some/http/util"
jest.mock("path/to/some/http/util")
...
it("calls search endpoint and renders results after fetch resolves", async () => {
mockHttp.fetchSearchResults.mockResolvedValue([{orderId: 'bar'}])
const { queryByText, getAllByTestId } = render(Component)
const submit = queryByText("Submit")
expect(getAllByTestId('order')).toHaveLength(0)
await fireEvent.click(submit)
expect(getAllByTestId('order')).toHaveLength(1)
})

how to properly handle firebase realtime database listeners with useEffect?

const Messages = (props) => {
const [messages, setMessages] = useState([]);
const { currentChannel, currentUser } = props;
const messagesRef = firebase.database().ref("messages");
useEffect(() => {
if (currentChannel && currentUser) {
const channel = currentChannel.currentChannel;
let loadedMessages = [];
messagesRef.child(channel.id).on("child_added", (snap) => {
loadedMessages.push(snap.val());
});
setMessages(loadedMessages);
}
return () => messagesRef.off();
}, [currentChannel, currentUser, messagesRef]);
const displayMessages = (messages) =>
messages.length > 0 &&
messages.map((message) => (
<Message key={message.timestamp} message={message} user={currentUser} />
));
return (
<>
<MessagesHeader />
<Segment>
<Comment.Group className="messages">
{displayMessages(messages)}
</Comment.Group>
</Segment>
<MessageForm
messagesRef={messagesRef}
currentChannel={currentChannel}
currentUser={currentUser}
/>
</>
);
};
Warning: Maximum update depth exceeded. This can happen when a
component calls setState inside useEffect, but useEffect either
doesn't have a dependency array, or one of the dependencies changes on
every render.
How to prevent this infinite loop?
Every time you render, you're creating a new messagesRef object. It may be dealing with the same spot in the database, but it's a new object. Since messageRef changed, your effect will rerun and that effect will call setMessages with a brand new array each time. Since you set state, the component rerenders, and the loop repeats.
Most of the time, i would recommend having the database ref only exist inside your useEffect, and then removing it from the dependency array. However, it looks like you need to pass the ref as a prop to MessageForm, so that won't work here. Instead, you need to make sure that you only create the database ref once. useMemo is one way to do this:
const messagesRef = useMemo(() => {
return firebase.database().ref("messages");
}, []);

React-testing-library mock function not called

i'm trying to test my component:
in my componet when i click to button i call some function and pass there some id.
my test:
it("should call function on submit click", async () => {
const wrapper = render(<Component />);
const id = "1";
const handleSubmit = jest.fn();
const SubmitBtn = wrapper.getByTestId("submit-btn");
fireEvent.click(SubmitBtn);
await waitFor(() => wrapper);
expect(handleSubmit).toHaveBeenCalled();
});
here i tried:
expect(handleSubmit).toHaveBeenCalled();
expect(handleSubmit).toHaveBeenCalledWith(id);
expect(handleSubmit).toBeCalled();
expect(handleSubmit).toBeCalledWith(id)
tried everything but it's not works..
expect(jest.fn()).toBeCalled()
Expected number of calls: >= 1
Received number of calls: 0
Your mock cannot be called this, because you declare it after having rendered your component. I assume you have a handleSubmit function in your component that gets called on submit click, but when you call :
const handleSubmit = jest.fn();
...the component is already renderer and there is no way you can mock its handleSubmit function.
What I suggest :
Do not try to spy on handleSubmit function, test like a real user
You should not try to test internal component logic, but test what a real user would see : a loading spinner, a confirmation message...This is what promotes RTL, because it will give you confidence in your test, decouple your test from internal implementation (which is a good thing when it comes to React components unit testing), and force you as a developper to think about user first when writing code (thinking how you will test your code using RTL forces you to think about user experience).
In your test you could do this :
// in component
const handleSubmit = () => {
// do things
// say that this boolean controls a message display
setSubmittedMessageDisplay(true);
}
In test
it('should call function on submit click', async () => {
const wrapper = render(<Component />);
const submitBtn = wrapper.getByTestId('submit-btn');
fireEvent.click(submitBtn);
expect(wrapper.getByText('form submitted - controlled by submittedMessageDisplay')).toBeTruthy();
});
Pass the form submit handler down from parent component, mock it
This implies to lift things up to the parent component :
// in parent
const handleSubmit = () => {...}
<ChildWithForm onSubmit={handleSubmit} />
// in child
<Form onSubmit={props.onSubmit}>...</Form>
Then in your test you can do this :
test('submits form correctly', () => {
const mockedHandler = jest.fn();
const wrapper = render(<Child onSubmit={mockedHandler} />);
const submitBtn = wrapper.getByTestId('submit-btn');
fireEvent.click(submitBtn);
expect(mockedHandler).toHaveBeenCalled();
});

How do I make my vue-unit-test trigger a click event on v-btn in Vuetify?

I am creating some unit test for my component, but the test keeps failing, since the button I'm testing
keeps not getting triggerd by a click-event.
I've used the docs as a foundation for my test: https://vuetifyjs.com/sv-SE/getting-started/unit-testing/
I've also tried some of the suggestions mentioned here: https://forum.vuejs.org/t/how-to-trigger-an-onchange-event/11081/4
But it seems like I'm missing something, anyone who can help me out?
My test:
test('If you can click on the Test button', () => {
const wrapper = shallowMount(myComponent, {
localVue,
vuetify,
});
const event = jest.fn();
const button = wrapper.find({name: 'v-btn'})
expect(button.exists()).toBe(true) //this works
wrapper.vm.$on('v-btn:clicked', event)
expect(event).toHaveBeenCalledTimes(0)
button.trigger('click')
expect(event).toHaveBeenCalledTimes(1)
})
myComponent:
<template>
<v-btn class="primary-text" #click.native="methodForTesting($event)">Test</v-btn>
<template>
<script>
methods: {
methodForTesting(){
console.log('button clicked!')
}
</script>
Hope this help, I changed your HTML a bit.
Firstly, I added a <div> and put <v-btn> inside it, this is very
important.
Secondly, I declared a data prop called index which is
initialized in 1.
Thirdly, I used data-testid="button" to identify
it and find it during test.
<template>
<div>
<v-btn data-testid="button" class="primary-text" #click="methodForTesting">Test</v-btn>
</div>
</template>
<script>
export default {
data() {
return {
index: 1
};
},
methods: {
methodForTesting() {
this.index++;
}
}
};
</script>
Now, for the unit test.
The key is to use vm.$emit('click') instead of .trigger('click') since v-btn is a component of Vuetify. If you were using button tag, then you can use .trigger('click').
Also, I changed how jest finds this button.
import Vuetify from 'vuetify'
// Utilities
import { mount, createLocalVue } from '#vue/test-utils'
// Components
import Test from '#/views/Test.vue';
// VARIABLES INITIALIZATION //
const vuetify = new Vuetify()
const localVue = createLocalVue()
// TESTING SECTION //
describe('Testing v-btn component', () => {
it('should trigger methodForTesting', async () => {
const wrapper = mount(Test, {
localVue,
vuetify,
})
const button = wrapper.find('[data-testid="button"]')
expect(button.exists()).toBe(true)
expect(wrapper.vm.$data.index).toBe(1)
button.vm.$emit('click')
await wrapper.vm.$nextTick()
expect(wrapper.vm.$data.index).toBe(2)
})
})
Now, when you are doing a unit test, you should check inputs and outputs. In this case, your input is the click event and your output, is not your method been called, but the data modified or sent by this method. That's why I declared index to see if it changes when you click the button.
Anyway, if you want to check if your method was called, you can use this code instead
describe('Testing v-btn component', () => {
it('should trigger methodForTesting', async () => {
const methodForTesting = jest.fn()
const wrapper = mount(Test, {
localVue,
vuetify,
methods: {
methodForTesting
}
})
const button = wrapper.find('[data-testid="button"]')
expect(button.exists()).toBe(true)
expect(methodForTesting).toHaveBeenCalledTimes(0)
button.vm.$emit('click')
await wrapper.vm.$nextTick()
expect(methodForTesting).toHaveBeenCalledTimes(1)
})
})
But you will receive the next error:
[vue-test-utils]: overwriting methods via the `methods` property is deprecated and will be removed in the next major version. There is no clear migration path for the `methods` property - Vue does not support arbitrarily
replacement of methods, nor should VTU. To stub a complex method extract it from the component and test it in isolation. Otherwise, the suggestion is to rethink those tests.
This is my first post btw

how to render UI with multiplte service calls

I just have a general question for Javascript. If I have to invoke two services for a UI and those two services calls have their own call backs, but UI template has to be rendered only after both the callbacks have finished execution, what should be the best Javascript practice to do it?
invokeServices() {
invokeService1(param1, param2, svcCallback1);
invokeService2(param1, param2, svcCallback2);
//where to render the template???
}
function svcCallback1 (){
//where to render the template???
}
function svcCallback2 (){
//where to render the template???
}
This post might help: Marko vs React: An In-depth Look. Specifically, look for the section on Async for how to use promises and the <await/> tag to delay rendering until all the data is present.
import fsp from 'fs-promise';
$ var filePath = __dirname + '/hello.txt';
$ var readPromise = fsp.readFile(filePath, {encoding: 'utf8'});
<await(helloText from readPromise)>
<p>
${helloText}
</p>
</await>
1. Track Completion of Callbacks
function invokeServicesAndRender() {
let remaining = 2;
let service1Data;
let service2Data;
invokeService1(param1, param2, (err, data) => {
service1Data = data;
if (!--remaining) done();
});
invokeService2(param1, param2, (err, data) => {
service2Data = data;
if (!--remaining) done();
});
function done() {
// all data is here now, we can render the ui
}
}
2. Promisify and use Promise.all
Node has a built-in method to promisify callbacks: util.promisify
const promisify = require('util').promisify;
const promiseService1 = promisify(invokeService1);
const promiseService2 = promisify(invokeService2);
function invokeServicesAndRender() {
Promise.all([
promiseService1(param1, param2),
promiseService2(param1, param2),
]).then(([service1Data, service2Data]) => {
// all data is here now, we can render the ui
});
}
3. If you're using Marko, render immediately and pass promises to the template
I see you tagged this question marko, so I'll mention that with Marko it is recommended to begin rendering immediately and only wait to render chunks that actually need the data. This allows you to flush out content to the user faster.
const promisify = require('util').promisify;
const promiseService1 = promisify(invokeService1);
const promiseService2 = promisify(invokeService2);
function invokeServicesAndRender() {
let service1DataPromise = promiseService1(param1, param2);
let service2DataPromise = promiseService2(param1, param2);
template.render({ service1DataPromise, service2DataPromise });
}
In your template you can use the <await> tag to wait for the data where it is needed:
<h1>Hello World</h1>
<p>this content gets rendered immediately</p>
<await(service1Data from input.service1DataPromise)>
<!-- render content here that needs the service data -->
</await>

Resources