Vue 3 composition test method called by child component event - vuejs3

My goal is to test that the method selectVirtualDevices is called when the child component emits the event.
Parent component
<template>
<ChildComponent
#select-devices="selectVirtualDevices"
/>
</template>
<script lang="ts" setup>
const selectVirtualDevices = (devices) => {
some other magic happening here
}
</script
Child component
<script setup lang="ts">
import { computed, ref } from 'vue'
const emit = defineEmits(['selectDevices'])
const selectedDevices = computed({
get() {
return state.selectedDevices
},
set(device) {
emit('selectDevices', devices)
},
})
</script
Test
import { describe, it, expect, vi } from 'vitest'
import { mount } from '#vue/test-utils'
import Parent from './Parent.vue'
import ChildComponent from './ChildComponent.vue'
describe('All Devices', () => {
const wrapper = mount(Parent)
it('should be mounted', () => {
expect(wrapper.html()).toBeTruthy()
})
it('should call selectVirtualDevices', () => {
const spy = vi.spyOn(wrapper.vm, 'selectVirtualDevices')
wrapper.findComponent(ChildComponent).vm.$emit('selectDevices')
expect(spy).toHaveBeenCalled()
})
})
The test fails with:
AssertionError: expected "selectVirtualDevices" to be called at least once
❯ Proxy.methodWrapper ../node_modules/chai/lib/chai/utils/addMethod.js:57:25
❯ apps/line-planning/components/AllDevices.spec.ts:35:16
33| wrapper.findComponent(DevicesDataTable).vm.$emit('selectDevices')
34|
35| expect(spy).toHaveBeenCalled()
| ^
36| })
37| })
What am I doing wrong?

Related

Testing event emitted from child component with Vitest & Vue-Test-Utils

I want to test if "onLogin" event emitted from child component will trigger "toLogin" function from parent correctly.
Login.vue
<template>
<ChildComponent
ref="child"
#onLogin="toLogin"
/>
</template>
<script>
import { useAuthStore } from "#/stores/AuthStore.js"; //import Pinia Store
import { userLogin } from "#/service/authService.js"; // import axios functions from another js file
import ChildComponent from "#/components/ChildComponent.vue";
export default {
name: "Login",
components: {
ChildComponent,
},
setup() {
const AuthStore = useAuthStore();
const toLogin = async (param) => {
try {
const res = await userLogin (param);
AuthStore.setTokens(res);
} catch (error) {
console.log(error);
}
};
}
</script>
login.spec.js
import { describe, it, expect, vi, beforeAll } from 'vitest';
import { shallowMount, flushPromises } from '#vue/test-utils';
import { createTestingPinia } from "#pinia/testing";
import Login from "#/views/user/Login.vue"
import { useAuthStore } from "#/stores/AuthStore.js";
describe('Login', () => {
let wrapper = null;
beforeAll(() => {
wrapper = shallowMount(Login, {
global: {
plugins: [createTestingPinia({ createSpy: vi.fn })],
},
});
})
it('login by emitted events', async () => {
const AuthStore = useAuthStore();
const loginParam = {
email: 'dummy#email.com',
password: '12345',
};
const spyOnLogin = vi.spyOn(wrapper.vm, 'toLogin');
const spyOnStore = vi.spyOn(AuthStore, 'setTokens');
await wrapper.vm.$refs.child.$emit('onLogin', loginParam);
await wrapper.vm.$nextTick();
await flushPromises();
expect(spyOnLogin).toHaveBeenCalledOnce(); // will not be called
expect(spyOnStore).toHaveBeenCalledOnce(); // will be called once
})
}
I expected both "spyOnLogin" and "spyOnStore" will be called once from emitted event, however, only "spyOnStore" will be called even though "spyOnStore" should only be called after "spyOnLogin" has been triggered.
The error message is:
AssertionError: expected "toLogin" to be called once
❯ src/components/__tests__:136:24
- Expected "1"
+ Received "0"
What do I fail to understand about Vitest & Vue-Test-Utils?
You shouldn't mock your toLogin method because its part of Login component which you are testing. Therefore, instead of expecting if toLogin has been called, you should check if instructions inside are working correctly.
In your case i would only test if after emit, userLogin and AuthStore.setTokens has been called.

How do I use vue-dragscroll in quasar?

I want to use vue-dragscroll in my quasar project. I have created a boot file called scrollbar.js and it looks like this
import { boot } from "quasar/wrappers;"
import { createApp } from "vue";
import { dragscrollNext } from "vue-dragscroll";
import App from "./App.vue";
const app = createApp(App)
app.directive("dragscroll", dragscrollNext);
app.mount("#app");
export default boot(async(/* { app, router, ...} */) => {});
In my quasar.config.js file I did this:
boot: ["scrollbar"],
And in my component I have this:
<script setup>
import useTouch from "src/composables/useTouch.js";
defineProps({
items: Array,
})
const { isTouchDevice } = useTouch();
<script>
<template>
<div v-dragscroll="!isTouchDevice">
<ProductCard v-for="item in items" :key="item.id" :item="item" />
<div>
<template>
The useTouch.js composable looks like this:
export default () => {
const isTouchDevice = computed(
() =>
"ontouchstart" in window ||
navigator.maxTouchPoints > 0 ||
navigator.msMaxTouchPoints > 0
);
return {
isTouchDevice,
};
};
This doesn't work and when I run, it gives a blank page. What am I doing wrong here, how to make this work?

Vue 3 Composition API: Update Child components props dynamically when values update from the parent component

I am trying to update a prop value when the data from the parent component gets updated and passes through the prop. The parent value always updates but does not update or re-renders in the child component when I pass it down. It passes to the prop the first time the child component is accessed but not when the data is updated in the parent component.
Below is the parent component:
<script setup>
import { inject, watchEffect, ref } from "vue";
import ChildComponent from "#/components/ChildComponent.vue"
const { state } = inject("store");
const cart = ref(state.cart);
watchEffect(() => (cart.value = state.cart));
</script>
<template>
<ChildComponent
v-for="(item, index) in cart?.items"
:key="index"
:cartItem="item"
/>
</template>
Below is the child component (only logs on the first load, never loads again):
<script setup>
import { ref, watchEffect } from "vue";
const { cartItem } = defineProps({
cartItem: !Object
});
const item = ref(cartItem);
watchEffect(() => {
console.log(item.value)
});
</script>
I have tried using Watch in many ways but it does not detect the old or the new values. It does not log any outputs
Example child component using watch:
<script setup>
import { ref, watch } from "vue";
const { cartItem } = defineProps({
cartItem: !Object
});
const item = ref(cartItem);
watch(() => item.value, (oldValue, newValue) => {
console.log(oldValue)
console.log(newValue)
});
</script>
I ended up solving the solution by using a v-if to rerender the child component.
<script setup>
import { inject, watchEffect, ref } from "vue";
import ChildComponent from "#/components/ChildComponent.vue"
const { state } = inject("store");
const cart = ref(state.cart);
const render = ref(true);
// Checks when the cart changes from the store
watchEffect(() => {
if(cart.value) {
render.value = true
}
else {
render.value = false
}
};
</script>
<template>
<div v-if="render">
<ChildComponent
v-for="(item, index) in cart?.items"
:key="index"
:cartItem="item"
/>
</div>
</template>
I had the same issue and it was frustrating, sometimes I had to do a workaround to get what I need, but try this inside the child component:
<script>
import { ref, watch } from "vue";
export default {
props: {
cartItem: {
type: !Object,
},
},
setup(props) {
const item = ref(null);
watch(props, () => {
item.value = props.cartItem;
});
return { item }
}
</script>

Use injected variables (nuxt.firebase) in composition api

I'm using the composi api in my Vue project and the nuxt.js firebase module, I would like to call variables injected into modules, such as $ fireAuth, but I didn't find a solution.
Below is a small code training of how I would like it to work:
export default createComponent({
setup(_props, { root }) {
root.$fireAuth= ..
}
}
// or
export default createComponent({
setup(_props, { root , $fireAuth }) {
}
}
I have a work-around for this and it works! (For now.)
Create a dummy component (ex. AppFirebase.vue)
<template></template>
<script lang="ts">
import Vue from "vue";
export default Vue.extend({
created() {
this.$emit("init", this.$fire);
},
});
</script>
Accessing NuxtFireInstance (ex. SomeComponent.vue)
<template>
<fire #init="initFB"></fire>
</template>
<script lang="ts">
import {
defineComponent,
reactive,
} from "#nuxtjs/composition-api";
import fire from "#/components/AppFirebase.vue";
export default defineComponent({
components: { fire },
setup() {
let _fire: any = reactive({});
const initFB = (fire: any) => {
_fire = fire;
};
const signout = async () => {
try {
await _fire.auth.signOut().then(() => {
// do something
});
} catch (error) {
console.log(error);
}
};
return {
initFB,
_fire,
signout,
};
},
});
</script>
Rickroll if you got it working!

Next.js + Redux server side rendering: Has data, but doesn't render on server side

I'm trying to add redux integration to my Next.js app, but I can't get serverside rendering working the way it should. I based my implementation off the official nextjs redux example.
In the end, when the page comes back from the server, the data is present as JSON data in the output, but the actual rendering based on this data did not happen. The weird thing is that before I used redux, the content DID render the way it should.
Naturally, I'm also getting React's checksum warning, indicating that the markup on the server is different.
I have no idea how to make this work properly on the server side. Is there something that I'm missing?
Here's the HTML generated by Next.js:
<h1 data-reactid="3">Test page</h1>
</div></div></div><div id="__next-error"></div></div><div><script>
__NEXT_DATA__ = {"props":{"isServer":true,"store":{},
"initialState":{"authors":{"loading":false,"items":{"4nRpnr66B2CcQ4wsY04CIQ":… }
,"initialProps":{}},"pathname":"/test","query":{},"buildId":1504364251326,"buildStats":null,"assetPrefix":"","nextExport":false,"err":null,"chunks":[]}
module={}
__NEXT_LOADED_PAGES__ = []
__NEXT_LOADED_CHUNKS__ = []
__NEXT_REGISTER_PAGE = function (route, fn) {
__NEXT_LOADED_PAGES__.push({ route: route, fn: fn })
}
__NEXT_REGISTER_CHUNK = function (chunkName, fn) {
__NEXT_LOADED_CHUNKS__.push({ chunkName: chunkName, fn: fn })
}
</script><script async="" id="__NEXT_PAGE__/test" type="text/javascript" src="/_next/1504364251326/page/test"></script><script async="" id="__NEXT_PAGE__/_error" type="text/javascript" src="/_next/1504364251326/page/_error/index.js"></script><div></div><script type="text/javascript" src="/_next/1504364251326/manifest.js"></script><script type="text/javascript" src="/_next/1504364251326/commons.js"></script><script type="text/javascript" src="/_next/1504364251326/main.js"></script></div></body></html>
AS you can see, the initialState value is populated, it contains all the required data, but the DOM still shows empty!.
If I render the dom on the client side, the js picks up the initial content and rerenders the page with the loaded content in place.
Here's my test page JS file:
import React from 'react'
import map from 'lodash.map';
import { initStore } from '../lib/store';
import * as actions from '../lib/actions';
import withRedux from 'next-redux-wrapper';
class IndexPage extends React.PureComponent {
static getInitialProps = ({ store, req }) => Promise.all([
store.dispatch(actions.fetchAll)
]).then( () => ({}) )
render() {
const latestPlants = this.props.plants.latest || [];
return (
<div>
<h1>Test page</h1>
{ map(this.props.plants.items, p => (
<div>{p.fields.name}</div>
))}
</div>
)
}
}
export default withRedux(initStore, data => data, null)(IndexPage)
For whatever it's worth, here's the action that I call above:
export const fetchAll = dispatch => {
dispatch({
type: LOADING_ALL
})
return axios.get('/api/frontpage')
.then( response => {
const data = response.data
dispatch({
type: RESET_AUTHORS,
payload: data.authors
})
dispatch({
type: RESET_PLANTS,
payload: data.plants
})
dispatch({
type: RESET_POSTS,
payload: data.posts
})
});
}
Any help with this would be greatly appreciated, I'm at a loss on how to make this work as expected. Anyone have any leads? Please also comment if there's something I can clarify.
I recommend to split the code in different parts. First, I'll create a store, with something like this:
import { createStore, applyMiddleware } from 'redux';
import thunkMiddleware from 'redux-thunk';
import reducer from './reducers'
export const initStore = (initialState = {}) => {
return createStore(reducer, initialState, applyMiddleware(thunkMiddleware))
}
Then I'll create the store with the types to handle:
const initialState = {
authors: null,
plants: null,
posts: null
}
export default (state = initialState, action) => {
switch (action.type) {
case 'RESET':
return Object.assign({}, state, {
authors: action.authors,
plants: action.plants,
posts: action.posts
})
default:
return state
}
}
In the actions I'll have something like this:
export const fetchAll = dispatch => {
return axios.get('/api/frontpage')
.then( response => {
const data = response.data
dispatch({
type: 'RESET',
authors: data.authors,
plants: data.plants,
posts: data.posts
})
});
}
The index will be something like this:
import React from 'react'
import { initStore } from '../store'
import withRedux from 'next-redux-wrapper'
import Main from '../components'
class Example extends React.Component {
render() {
return (
<div>
<Main />
</div>
)
}
}
export default withRedux(initStore, null)(Example)
And the component Main:
import React, {Component} from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { fetchAll } from '../../actions'
class Data extends Component {
componentWillMount() {
this.props.fetchAll()
}
render() {
const { state } = this.props
return (
<div>
<h1>Test page</h1>
{ map(state.plants.items, p => (
<div>{p.fields.name}</div>
))}
</div>
)
}
}
const mapStateToProps = (state) => {
return {
state
}
}
const mapDistpatchToProps = dispatch => {
return {
fetchAll: bindActionCreators(fetchAll, dispatch)
}
}
export default connect(mapStateToProps, mapDistpatchToProps)(Data)
Make the changes for what you need.
You can check some full examples here:
Form handler
Server Auth

Resources