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

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)
})
})

Related

Vue received a Component which was made a reactive object

The problem I need to solve: I am writing a little vue-app based on VueJS3.
I got a lot of different sidebars and I need to prevent the case that more than one sidebar is open at the very same time.
To archive this I am following this article.
Now I got a problem:
Vue received a Component which was made a reactive object. This can lead to unnecessary performance overhead, and should be avoided by marking the component with markRaw or using shallowRef instead of ref. (6)
This is my code:
SlideOvers.vue
<template>
<component :is="component" :component="component" v-if="open"/>
</template>
<script>
export default {
name: 'SlideOvers',
computed: {
component() {
return this.$store.state.slideovers.sidebarComponent
},
open () {
return this.$store.state.slideovers.sidebarOpen
},
},
}
</script>
UserSlideOver.vue
<template>
<div>test</div>
</template>
<script>
export default {
name: 'UserSlideOver',
components: {},
computed: {
open () {
return this.$store.state.slideovers.sidebarOpen
},
component () {
return this.$store.state.slideovers.sidebarComponent
}
},
}
</script>
slideovers.js (vuex-store)
import * as types from '../mutation-types'
const state = {
sidebarOpen: false,
sidebarComponent: null
}
const getters = {
sidebarOpen: state => state.sidebarOpen,
sidebarComponent: state => state.sidebarComponent
}
const actions = {
toggleSidebar ({commit, state}, component) {
commit (types.TOGGLE_SIDEBAR)
commit (types.SET_SIDEBAR_COMPONENT, component)
},
closeSidebar ({commit, state}, component) {
commit (types.CLOSE_SIDEBAR)
commit (types.SET_SIDEBAR_COMPONENT, component)
}
}
const mutations = {
[types.TOGGLE_SIDEBAR] (state) {
state.sidebarOpen = !state.sidebarOpen
},
[types.CLOSE_SIDEBAR] (state) {
state.sidebarOpen = false
},
[types.SET_SIDEBAR_COMPONENT] (state, component) {
state.sidebarComponent = component
}
}
export default {
state,
getters,
actions,
mutations
}
App.vue
<template>
<SlideOvers/>
<router-view ref="routerView"/>
</template>
<script>
import SlideOvers from "./SlideOvers";
export default {
name: 'app',
components: {SlideOvers},
};
</script>
And this is how I try to toggle one slideover:
<template>
<router-link
v-slot="{ href, navigate }"
to="/">
<a :href="href"
#click="$store.dispatch ('toggleSidebar', userslideover)">
Test
</a>
</router-link>
</template>
<script>
import {defineAsyncComponent} from "vue";
export default {
components: {
},
data() {
return {
userslideover: defineAsyncComponent(() =>
import('../../UserSlideOver')
),
};
},
};
</script>
Following the recommendation of the warning, use markRaw on the value of usersslideover to resolve the warning:
export default {
data() {
return {
userslideover: markRaw(defineAsyncComponent(() => import('../../UserSlideOver.vue') )),
}
}
}
demo
You can use Object.freeze to get rid of the warning.
If you only use shallowRef f.e., the component will only be mounted once and is not usable in a dynamic component.
<script setup>
import InputField from "src/core/components/InputField.vue";
const inputField = Object.freeze(InputField);
const reactiveComponent = ref(undefined);
setTimeout(function() => {
reactiveComponent.value = inputField;
}, 5000);
setTimeout(function() => {
reactiveComponent.value = undefined;
}, 5000);
setTimeout(function() => {
reactiveComponent.value = inputField;
}, 5000);
</script>
<template>
<component :is="reactiveComponent" />
</template>

[Vue warn]: Property or method "stateSidebar" is not defined on the instance but referenced during render

I know this seems like a question that would be easy to find, but my code worked some time ago. I am using a Vuex binding to check if my sidebar should be visible or not, so stateSidebar should be set within my entire project.
default.vue
<template>
<div>
<TopNav />
<SidebarAuth v-if="stateSidebar" />
<Nuxt />
</div>
</template>
<script>
import TopNav from './partials/TopNav';
import SidebarAuth from './partials/SidebarAuth';
export default {
components: {
TopNav,
SidebarAuth
},
methods: {
setStateSidebar(event, state) {
this.$store.dispatch('sidebar/setState', state)
}
}
}
</script>
store/sidebar.js
export const state = () => ({
stateSidebar: false
});
export const getters = {
stateSidebar(state) {
return state.stateSidebar;
}
};
export const mutations = {
SET_SIDEBAR_STATE(state, stateSidebar) {
state.stateSidebar = stateSidebar;
}
};
export const actions = {
setState({ commit }, stateSidebar) {
commit('SET_SIDEBAR_STATE', stateSidebar);
},
clearState({ commit }) {
commit('SET_SIDEBAR_STATE', false);
}
};
plugins/mixins/sidebar.js
import Vue from 'vue';
import { mapGetters } from 'vuex';
const Sidebar = {
install(Vue, options) {
Vue.mixin({
computed: {
...mapGetters({
stateSidebar: 'sidebar/stateSidebar'
})
}
})
}
}
Vue.use(Sidebar);
nuxt.config.js
plugins: ["./plugins/mixins/validation", "./plugins/axios", "./plugins/mixins/sidebar"],
If you're creating a mixin, it should be in /mixins
So for example /mixins/my-mixin.js.
export default {
// vuex mixin
}
Then import it like this in default.vue
<script>
import myMixin from '~/mixins/my-mixin`
export default {
mixins: [myMixin],
}
This is not what plugins should be used for tho. And IMO, you should definitively make something simpler and shorter here, with less boilerplate and that will not be deprecated in vue3 (mixins).
This is IMO the recommended way of using it
<template>
<div>
<TopNav />
<SidebarAuth v-if="stateSidebar" />
<Nuxt />
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
computed: {
...mapState('sidebar', ['stateSidebar']) // no need to use object syntax nor a getter since you're just fetching the state here
},
}
</script>
No mixin, no plugin entry.

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');

Why dynamic component is not working in vue3?

Here is a working Vue2 example:
<template>
<div>
<h1>O_o</h1>
<component :is="name"/>
<button #click="onClick">Click me !</button>
</div>
</template>
<script>
export default {
data: () => ({
isShow: false
}),
computed: {
name() {
return this.isShow ? () => import('./DynamicComponent') : '';
}
},
methods: {
onClick() {
this.isShow = true;
}
},
}
</script>
Redone under Vue3 option does not work. No errors occur, but the component does not appear.
<template>
<div>
<h1>O_o</h1>
<component :is="state.name"/>
<button #click="onClick">Click me !</button>
</div>
</template>
<script>
import {ref, reactive, computed} from 'vue'
export default {
setup() {
const state = reactive({
name: computed(() => isShow ? import('./DynamicComponent.vue') : '')
});
const isShow = ref(false);
const onClick = () => {
isShow.value = true;
}
return {
state,
onClick
}
}
}
</script>
Has anyone studied the vue2 beta version? Help me please. Sorry for the clumsy language, I use Google translator.
Leave everything in the template as in Vue2
<template>
<div>
<h1>O_o</h1>
<component :is="name"/>
<button #click="onClick">Click me !</button>
</div>
</template>
Change only in "setup" using defineAsyncComponent
You can learn more about defineAsyncComponent here
https://labs.thisdot.co/blog/async-components-in-vue-3
const isShow = ref(false);
const name = computed (() => isShow.value ? defineAsyncComponent(() => import("./DynamicComponent.vue")) : '')
const onClick = () => {
isShow.value = true;
}
Try this
import DynamicComponent from './DynamicComponent.vue'
export default {
setup() {
const state = reactive({
name: computed(() => isShow ? DynamicComponent : '')
});
...
return {
state,
...
}
}
}
The issue with this seems to be to do with the way we register components when we use the setup script - see the official docs for more info. I've found that you need to register the component globally in order to reference it by string in the template.
For example, for the below Vue component:
<template>
<component :is="item.type" :item="item"></component>
</template>
<script setup lang="ts">
// Where item.type contains the string 'MyComponent'
const props = defineProps<{
item: object
}>()
</script>
We need to register the component in the main.ts, as such:
import { createApp } from 'vue'
import App from './App.vue'
import MyComponent from './MyComponent.vue'
var app = createApp(App);
app.component('MyComponent', MyComponent)
app.mount('#app')
Using 'watch' everything works.
<template>
<component :is="componentPath"/>
</template>
<script lang="ts">
import {defineComponent, ref, watch, SetupContext} from "vue";
export default defineComponent({
props: {
path: {type: String, required: true}
},
setup(props: { path: string }, context: SetupContext) {
const componentPath = ref("");
watch(
() => props.path,
newPath => {
if (newPath !== "")
import("#/" + newPath + ".vue").then(val => {
componentPath.value = val.default;
context.emit("loaded", true);
});
else {
componentPath.value = "";
context.emit("loaded", false);
}
}
);
return {componentPath};
}
});
</script>

Unknown custom element: - did you register the component correctly? error on with <nuxt /> component in default.vue Jest

I'm trying to write tests for default.vue file which has the following code:
default.vue
<template>
<div>
<top-nav :class="isSticky ? 'fixed-top stickyAnimate' : ''" />
<main>
<nuxt />
</main>
<footer />
</div>
</template>
<script>
import TopNav from '../components/TopNav.vue';
import Footer from '../components/Footer.vue';
import StickyNavMixin from '../mixins/stickyNavMixin';
export default {
components: {
TopNav,
Footer,
},
mixins: [StickyNavMixin],
data() {
return {
loading: true,
};
},
mounted() {
if (!window.location.hash) {
this.loading = false;
}
},
};
</script>
then my test look like this
default.spec.js
import { createLocalVue, shallowMount } from '#vue/test-utils';
import BootstrapVue from 'bootstrap-vue';
import StickyNavMixin from '../mixins/stickyNavMixin';
import Default from '../layouts/default.vue';
import TopNav from '../components/TopNav.vue';
import Footer from '../components/Footer.vue';
const localVue = createLocalVue();
localVue.use(BootstrapVue);
localVue.mixin(StickyNavMixin);
describe('Default', () => {
let wrapper;
beforeEach(() => {
wrapper = shallowMount(Default, {
localVue,
});
});
test('is a Vue instance', () => {
expect(wrapper.isVueInstance()).toBeTruthy();
});
test('has navbar component', () => {
expect(wrapper.find(TopNav).exists()).toBe(true);
});
});
When I ran this test, I get error says:
[Vue warn]: Unknown custom element: - did you register the component correctly? For
recursive components, make sure to provide the "name" option.found in --->
Please guide me to a right direction. Thank you in advance!
I figured out how to get past that error. I had to just stub it out of the wrapper. You don't have to import Nuxt, just string 'nuxt' will replace it as a stubbed element in the wrapper:
describe('DefaultLayout', () => {
let wrapper;
afterEach(() => {
wrapper.destroy();
});
/** mount **/
test('is a Vue instance', () => {
wrapper = mount(DefaultLayout, {
localVue,
stubs: ['nuxt'],
});
expect(wrapper.isVueInstance()).toBeTruthy();
});
/** shallowMount **/
test('is a Vue instance', () => {
wrapper = shallowMount(DefaultLayout, {
localVue,
stubs: ['nuxt', 'top-nav', 'footer'],
});
expect(wrapper.isVueInstance()).toBeTruthy();
// expect(wrapper.html()).toBe('<div>'); => this is to debug see below for output
});
});
//DEBUG
"<div><top-nav-stub class=\"\"></top-nav-stub> <main><nuxt-stub></nuxt-stub> .
</main> <footer-stub></footer-stub></div>"