Triggered event on b-button doesn't show on wrapper.emitted() - vue.js

I'm new to testing vue components and right now I'm trying to test that clicking a b-button component trigger an event and calls a method.
The test seems to fail because the 'click' event doesn't get triggered.
Also note that i'm using Nuxt.js
The component containing b-button is the following:
<template>
<div>
<b-form-input name="username" type="text" v-model="username"></b-form-input>
<b-form-input name="password" type="password" v-model="password"></b-form-input>
<b-button #click="login">Login</b-button>
</div>
</template>
<script>
import { mapActions } from "vuex";
export default {
data() {
return {
username: "",
password: ""
};
},
methods: {
...mapActions({
dispatchLogin: "auth/login"
}),
login: () => {
const response = this.dispatchLogin(this.username, this.password);
response.then(this.$router.push("/dashboard"));
}
}
};
</script>
And the test is written like that:
test('call login when Login button gets clicked', async () => {
expect.assertions(2);
moxios.stubRequest('/api/auth/login', {
response: 200
});
const store = new Vuex.Store(storeConfig);
const mocks = {
login: jest.fn()
}
const wrapper = shallowMount(login, { localVue, store, mocks });
debugger;
// Note that b-button is stubbed as <button></button>
wrapper.find('button').trigger('click');
await flushPromises()
expect(wrapper.emitted('click')).toHaveLength(1);
expect(auth.actions.login).toHaveBeenCalledTimes(1);
});
The problem is that I can't find any click event in wrapper.emitted().

Related

How to get vuex pagination to be reactive?

I am not sure what the problem is. I am creating a Vue/Vuex pagination system that calls my api to get my list of projects. The page initially loads all the projects in when the page is mounted. The Vuex does the inital call with axios. Vuex finds the projects and the number of pages. Once the user clicks on the pagination that is created with the pagination component, it should automatically change the projects for page 2...3 etc.
The problem I have is that it is not reactive until you press the page number twice. I am using vue 3. I have tried not using Vuex and that was successful. I am trying to create a single store that does all the axios calls. Thanks for all your help!
Vuex store
import { createStore } from 'vuex';
import axios from 'axios';
/**
* Vuex Store
*/
export const store = createStore({
state() {
return {
projects: [],
pagination: {}
}
},
getters: {
projects: state => {
return state.projects;
}
},
actions: {
async getProjects({ commit }, page = 1) {
await axios.get("http://127.0.0.1:8000/api/guest/projects?page=" + page)
.then(response => {
commit('SET_PROJECTS', response.data.data.data);
commit('SET_PAGINATION', {
current_page: response.data.data.pagination.current_page,
first_page_url: response.data.data.pagination.first_page_url,
prev_page_url: response.data.data.pagination.prev_page_url,
next_page_url: response.data.data.pagination.next_page_url,
last_page_url: response.data.data.pagination.last_page_url,
last_page: response.data.data.pagination.last_page,
per_page: response.data.data.pagination.per_page,
total: response.data.data.pagination.total,
path: response.data.data.pagination.path
});
})
.catch((e) => {
console.log(e);
});
}
},
mutations: {
SET_PROJECTS(state, projects) {
state.projects = projects;
},
SET_PAGINATION(state, pagination) {
state.pagination = pagination;
}
},
});
Portfolio Component
<template>
<div>
<div id="portfolio">
<div class="container-fluid mt-5">
<ProjectNav></ProjectNav>
<div class="d-flex flex-wrap overflow-auto justify-content-center mt-5">
<div v-for="project in projects" :key="project.id" class="m-2">
<Project :project="project"></Project>
</div>
</div>
</div>
</div>
<PaginationComponent
:totalPages="totalPages"
#clicked="fetchData"
></PaginationComponent>
</div>
</template>
<script>
import Project from "../projects/Project.vue";
import PaginationComponent from "../pagination/PaginationComponent.vue";
import ProjectNav from "../projectNav/ProjectNav.vue";
/**
* PortfolioComponent is where all the projects are displayed.
*/
export default {
name: "PortfolioComponent",
data() {
return {
location: "portfolio",
projects: []
};
},
components: {
Project,
PaginationComponent,
ProjectNav,
},
mounted() {
this.fetchData(1);
},
computed: {
totalPages() {
return this.$store.state.pagination.last_page;
},
},
methods: {
fetchData(page) {
this.$store.dispatch("getProjects", page);
this.projects = this.$store.getters.projects;
},
},
};
</script>
In your fetchData method you are calling the async action getProjects, but you are not waiting until the returned promise is resolved.
Try to use async and await in your fetchData method.
methods: {
async fetchData(page) {
await this.$store.dispatch("getProjects", page);
this.projects = this.$store.getters.projects;
},
},

vue-test-utils: Unable to detect method call when using #click attribute on child component

Vue-test-utils is unable to detect method call when using #click attribute on child component, but is able to detect it when using the #click attribute on a native HTML-element, e.g. a button. Let me demonstrate:
This works:
// Test.vue
<template>
<form #submit.prevent>
<button name="button" type="button" #click="click">Test</button>
</form>
</template>
<script>
import { defineComponent } from 'vue'
export default defineComponent({
name: 'Test',
setup() {
const click = () => {
console.log('Click')
}
return { click }
}
})
</script>
// Test.spec.js
import { mount } from '#vue/test-utils'
import Test from './src/components/Test.vue'
describe('Test.vue', () => {
const wrapper = mount(Test)
if ('detects that method was called', () => {
const spy = spyOn(wrapper.vm, 'click')
wrapper.find('button').trigger('click')
expect(wrapper.vm.click).toHaveBeenCalled() // SUCCESS. Called once
})
})
This does NOT work:
// Test.vue
<template>
<form #submit.prevent>
<ButtonItem #click="click" />
</form>
</template>
<script>
import { defineComponent } from 'vue'
import ButtonItem from './src/components/ButtonItem.vue'
export default defineComponent({
name: 'Test',
components: { ButtonItem },
setup() {
const click = () => {
console.log('Click')
}
return { click }
}
})
</script>
// ButtonItem.vue
<template>
<button type="button">Click</button>
</template>
<script>
import { defineComponent } from 'vue'
export default defineComponent({
name: 'ButtonItem',
})
</script>
// Test.spec.js
import { mount } from '#vue/test-utils'
import Test from './src/components/Test.vue'
import ButtonItem from './src/components/ButtonItem.vue'
describe('Test.vue', () => {
const wrapper = mount(Test)
if ('detects that method was called', () => {
const spy = spyOn(wrapper.vm, 'click')
wrapper.findComponent(ButtonItem).trigger('click')
expect(wrapper.vm.click).toHaveBeenCalled() // FAIL. Called 0 times
})
})
This issue stumbles me. I am not sure what I do wrong. I would be grateful if someone could describe the fault and show me the solution. Thanks!
I haven't run your code to test it yet, but why won't you use a more straightforward solution?
Look out for when the component emits click.
describe('Test.vue', () => {
const wrapper = mount(Test)
it('when clicked on ButtonItem it should be called one time', () => {
const button = wrapper.findComponent(ButtonItem)
button.trigger('click')
expect(wrapper.emitted().click).toBeTruthy()
expect(wrapper.emitted().click.length).toBe(1)
})
})

Vue 3 access child component from slots

I am currently working on a custom validation and would like to, if possible, access a child components and call a method in there.
Form wrapper
<template>
<form #submit.prevent="handleSubmit">
<slot></slot>
</form>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
setup(props, { slots }) {
const validate = (): boolean => {
if (slots.default) {
slots.default().forEach((vNode) => {
if (vNode.props && vNode.props.rules) {
if (vNode.component) {
vNode.component.emit('validate');
}
}
});
}
return false;
};
const handleSubmit = (ev: any): void => {
validate();
};
return {
handleSubmit,
};
},
});
</script>
When I call slot.default() I get proper list of child components and can see their props. However, vNode.component is always null
My code is based from this example but it is for vue 2.
If someone can help me that would be great, or is this even possible to do.
I found another solution, inspired by quasar framework.
Form component provide() bind and unbind function.
bind() push validate function to an array and store in Form component.
Input component inject the bind and unbind function from parent Form component.
run bind() with self validate() function and uid
Form listen submit event from submit button.
run through all those validate() array, if no problem then emit('submit')
Form Component
import {
defineComponent,
onBeforeUnmount,
onMounted,
reactive,
toRefs,
provide
} from "vue";
export default defineComponent({
name: "Form",
emits: ["submit"],
setup(props, { emit }) {
const state = reactive({
validateComponents: []
});
provide("form", {
bind,
unbind
});
onMounted(() => {
state.form.addEventListener("submit", onSubmit);
});
onBeforeUnmount(() => {
state.form.removeEventListener("submit", onSubmit);
});
function bind(component) {
state.validateComponents.push(component);
}
function unbind(uid) {
const index = state.validateComponents.findIndex(c => c.uid === uid);
if (index > -1) {
state.validateComponents.splice(index, 1);
}
}
function validate() {
let valid = true;
for (const component of state.validateComponents) {
const result = component.validate();
if (!result) {
valid = false;
}
}
return valid;
}
function onSubmit() {
const valid = validate();
if (valid) {
emit("submit");
}
}
}
});
Input Component
import { defineComponent } from "vue";
export default defineComponent({
name: "Input",
props: {
rules: {
default: () => [],
type: Array
},
modelValue: {
default: null,
type: String
}
}
setup(props) {
const form = inject("form");
const uid = getCurrentInstance().uid;
onMounted(() => {
form.bind({ validate, uid });
});
onBeforeUnmount(() => {
form.unbind(uid);
});
function validate() {
// validate logic here
let result = true;
props.rules.forEach(rule => {
const value = rule(props.modelValue);
if(!value) result = value;
})
return result;
}
}
});
Usage
<template>
<form #submit="onSubmit">
<!-- rules function -->
<input :rules="[(v) => true]">
<button label="submit form" type="submit">
</form>
</template>
In the link you provided, Linus mentions using $on and $off to do this. These have been removed in Vue 3, but you could use the recommended mitt library.
One way would be to dispatch a submit event to the child components and have them emit a validate event when they receive a submit. But maybe you don't have access to add this to the child components?
JSFiddle Example
<div id="app">
<form-component>
<one></one>
<two></two>
<three></three>
</form-component>
</div>
const emitter = mitt();
const ChildComponent = {
setup(props, { emit }) {
emitter.on('submit', () => {
console.log('Child submit event handler!');
if (props && props.rules) {
emit('validate');
}
});
},
};
function makeChild(name) {
return {
...ChildComponent,
template: `<input value="${name}" />`,
};
}
const formComponent = {
template: `
<form #submit.prevent="handleSubmit">
<slot></slot>
<button type="submit">Submit</button>
</form>
`,
setup() {
const handleSubmit = () => emitter.emit('submit');
return { handleSubmit };
},
};
const app = Vue.createApp({
components: {
formComponent,
one: makeChild('one'),
two: makeChild('two'),
three: makeChild('three'),
}
});
app.mount('#app');

nuxtServerInit data not show in page

I try to use nuxtServerInit method.
index.js
import productsService from "../services/productsService";
export const state = () => ({
hotDeals: [],
specialities: []
})
export const mutations = {
SET_SPECIALITIES(state, payload) {
state.specialities = payload;
}
}
export const actions = {
async nuxtServerInit({ dispatch}, ctx) {
try {
await dispatch('fetchSpecialities');
}catch (e) {
console.log(e);
}
},
fetchSpecialities({ commit }) {
productsService.getSpecialities()
.then(response => {
commit('SET_SPECIALITIES', response.data);
});
}
}
component usage
<template>
<v-layout
justify-center
align-center
>
<div>
<v-row >
<span v-for="item in specialities">{{item.productCode}}</span>
</v-row>
</div>
</v-layout>
</template>
<script>
import { mapState } from 'vuex';
export default {
computed: {
...mapState(["specialities"])
}
}
</script>
But it show nonthing on page. If I try to use console.log(state.specialities) in mutation after change state I can see data in web storm console. But in component data is not showing.
i think using watchers will solve your problem
watch: {
specialities(newValue, oldValue) {
console.log(`Updating from ${oldValue} to ${newValue}`);
},
},

Testing two different click events with vue-test-utils

Trying to test whether two methods are called on a click event on two different buttons. The result returns that the mock method on the second event is not called.
The template is the following (JobsSearch.vue):
<template>
<b-input-group class="sm-2 mb-2 mt-2">
<b-form-input
:value="this.searchConfig.Keyword"
#input="this.updateJobsSearchConfig"
class="mr-2 rounded-0"
placeholder="Enter Search term..."
/>
<b-button
#click="searchJobs"
class="rounded-0 search-button"
variant="primary"
>
Search
</b-button>
<b-button
#click="resetFilter"
class="rounded-0 ml-2 reset-button"
variant="primary"
>
Reset
</b-button>
</b-input-group>
</template>
This is my JobsSearch.spec.js
import { shallowMount, createLocalVue } from '#vue/test-utils'
import Vuex from 'vuex'
// BootstrapVue is necessary to recognize the custom HTML elements
import BootstrapVue from 'bootstrap-vue'
import JobsSearch from '#/components/jobs/JobsSearch.vue'
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(BootstrapVue)
describe('JobsSearch.vue', () => {
let state
let store
let wrapper
let searchJobs
let resetFilter
let emitEvents
beforeEach(() => {
state = {
jobs: {
paged: {
size: 100,
page: 1
},
search: {
Keyword: '',
status: [],
ucode: []
}
}
}
store = new Vuex.Store({
state
})
searchJobs = jest.fn()
resetFilter = jest.fn()
emitEvents = jest.fn()
wrapper = shallowMount(JobsSearch, {
methods: {
searchJobs,
resetFilter,
emitEvents
},
localVue,
store })
})
afterEach(() => {
wrapper.destroy()
})
// START: Method tests
it('should call jobsSearch method on search button click event', () => {
wrapper.find('.search-button').trigger('click')
expect(searchJobs).toHaveBeenCalled()
})
it('should call resetFilter method on reset button click event', () => {
wrapper.find('.reset-button').trigger('click')
expect(resetFilter).toHaveBeenCalled()
})
// END: Method tests
})
I expect both searchJobs and resetFilter to be called, however, only the first test passes
Any ideas, please?
It seems like that the resetFilter() method is not stubbed correctly.
Adapt the second test as follows:
it('should call resetFilter method on reset button click event', () => {
const resetFilterStub = jest.fn();
wrapper.setMethods({ resetFilter: resetFilterStub });
wrapper.find('.reset-button').trigger('click')
expect(resetFilter).toHaveBeenCalled()
})