Vue JS Jest : mount() doesn't render properly - vue.js

MyView.vue :
<template>
<v-container fluid>
<v-row>
<v-col v-for="obj in myObjs" :key="obj.ref">
<MyChild :obj="obj" />
</v-col>
</v-row>
</v-container>
</template>
export default class MyView extends Vue {
myObjs: MyObj[] = []
mounted () {
this.myObjs = await api.getGreatObjs()
// this.myObjs is like expected
}
}
MyView.spec.ts :
beforeEach( () => {
localVue = createLocalVue()
localVue.use(Vuetify)
jest.mock('axios')
jest.spyOn(api, 'getGreatObjs')
.mockImplementation( () => new Promise((resolve, reject) => {
resolve([new MyObj({name: 'toto', value: 45}), ...])
}))
})
it('should render', () => {
wrapper = mount(MyView, {
stubs: ['router-link']
})
expect(wrapper.findAll(MyChild).length).toBe(2)
})
Content of wrapper.html():
<v-container fluid=\"\"><v-container fluid=\"\"><v-row></v-row></v-container></v-container>
I also tried this (MyView.spec.ts):
describe('P1RealTime.vue', () => {
let wrapper: any
let localVue: any
beforeEach( () => {
localVue = createLocalVue()
localVue.use(Vuetify)
})
it('should render', () => {
wrapper = mount(MyView, {
localVue: localVue,
stubs: ['router-link']
})
expect(wrapper.html()).toEqual('')
})
})
Console result :
Expected: ""
Received: "<div class=\"container container--fluid\"><div class=\"container container--fluid\"><div></div></div>"
There is nothing inside "row" (<v-row></v-row>) or "div" (<div></div>) but there should be multiple MyChild ; so, I don't understand.
Could you help me ?

Related

Using Quasar q-select with a filter enabled when options is a json object

I cannot find any examples using composition api for this and could use some direction. I have a q-select which passes options as a prop using a axios request. The data is in this form:
[{description: "Apple Inc.", displaySymbol: "AAPL"}, {description: "Microsoft", displaySymbol: "MSFT"}]
I have about 20000 records in this JSON response. I am able to display it all in a v-select using:
<q-select
class="grey-7"
filled
v-model="addStockSymbol"
use-input
input-debounce="0"
label="Add New Stock Symbol"
:options="stockTickers"
option-label="description"
option-value="displaySymbol"
#blur="addPosition"
#filter="filterFn"
behavior="menu"
>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">
No results
</q-item-section>
</q-item>
</template>
</q-select>
My issue is I do not know how to setup the filter and update function so I can search this. So far I have the code below but the examples on quasar do not use any arrays with objects but rather simple arrays. So I am wondering how do I approach this?
<script>
import {watch, ref, defineComponent,onMounted} from 'vue'
import {usePortfolioStore} from '../stores/portfolio-store'
import {storeToRefs} from 'pinia'
import {finnhubAPI} from 'boot/axios'
export default defineComponent({
name: 'UploadPositions',
components: {
},
setup () {
//v-models
const addStockSymbol = ref('')
const addShareCount = ref('')
const stockTickers = ref([])
const loadData = () => {
finnhubAPI.get('/api/v1/stock/symbol?exchange=US&token=tedkfjdkfdfd')
.then((response) => {
stockTickers.value = response.data
})
.catch(() => {
console.log('API request failed')
})
}
const filterFn = (val, update) => {
if (val === '') {
update(() => {
stockTickers.value =
})
return
}
}
update(() => {
const needle = val.toLowerCase()
this.options = stringOptions.filter(v => v.toLowerCase().indexOf(needle) > -1)
})
//add on mount API request
onMounted(() => {
loadData()
})
return {
addStockSymbol, addShareCount, portfolio, addPosition, deletePosition,
loadData, stockTickers, modifyTickerData, filterFn, update
}
}
})
</script>
Basically you need to store a complete copy of the response data and keep that around, untouched, so that each time the filter function is called you can filter off of that, looking within its objects for the label prop.
When setting up refs:
//v-models
const addStockSymbol = ref('')
const addShareCount = ref('')
const stockTickers = ref([])
const allResponseData= ref([]) // <-- add this one
Then your loadData function:
const loadData = () => {
finnhubAPI.get('/api/v1/stock/symbol?exchange=US&token=cc8ffgiad3iciiq4brf0')
.then((response) => {
const responseData = response.data.map((item) => ({label: item.description, value: item.displaySymbol}));
allResponseData.value = [...responseData];
stockTickers.value = [...responseData];
})
.catch(() => {
console.log('API request failed')
})
}
Then in your filter function:
const filterFn = (val, update, abort) => {
update(() => {
const needle = val.toLowerCase()
stockTickers.value = allResponseData.value.filter(option => {
return option.label.toLowerCase().indexOf(needle) > -1
})
})
}
See it in action:
const { ref } = Vue
const stringOptions = [
{label: 'Google', value: "goog"}, {label:'Facebook',value:'fb'}, {label:'Twitter', value: "twit"},{label: 'Apple', value: 'App'}]
const app = Vue.createApp({
setup () {
const options = ref(stringOptions)
return {
model: ref(null),
options,
filterFn (val, update, abort) {
update(() => {
const needle = val.toLowerCase()
options.value = stringOptions.filter(option => {
return option.label.toLowerCase().indexOf(needle) > -1
})
})
}
}
}
})
app.use(Quasar, { config: {} })
app.mount('#q-app')
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons" rel="stylesheet"/>
<link href="https://cdn.jsdelivr.net/npm/quasar#2.7.7/dist/quasar.min.css" rel="stylesheet"/>
<script src="https://cdn.jsdelivr.net/npm/vue#3/dist/vue.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/quasar#2.7.7/dist/quasar.umd.prod.js"></script>
<!--
Forked from:
https://quasar.dev/vue-components/select#example--basic-filtering
-->
<div id="q-app" style="min-height: 100vh;">
<div class="q-pa-md">
<div class="q-gutter-md row">
<q-select
filled
v-model="model"
use-input
hide-selected
fill-input
input-debounce="0"
:options="options"
#filter="filterFn"
hint="Basic filtering"
style="width: 250px; padding-bottom: 32px"
>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">
No results
</q-item-section>
</q-item>
</template>
</q-select>
</div>
</div>
</div>

How can I change selected item in qselect on page load

I use a QSelect Dropdown with some options on my page header, like the following:
<q-select
filled
v-model="model"
use-input
hide-selected
fill-input
input-debounce="0"
:options="options"
#filter="filterFn"
hint="Basic autocomplete"
style="width: 250px; padding-bottom: 32px"
emit-value
map-options
>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">
No results
</q-item-section>
</q-item>
</template>
</q-select>
const stringOptions = [
{label:'Google', value:'g1111111111'}, {label:'Facebook', value:'f2222222222'}, {label:'Twitter', value:'t3333333'}, {label:'Apple', value:'a44444444'}, {label:'Oracle', value:'o555555555'}
]
new Vue({
el: '#q-app',
data () {
return {
model: 'f2222222222',
options: stringOptions
}
},
methods: {
filterFn (val, update, abort) {
update(() => {
const needle = val.toLowerCase()
this.options = stringOptions.filter(v => v.label.toLowerCase().indexOf(needle) > -1)
})
}
}
})
How can I use a method to change the selected value on pageload for example from facebook to google?
I think with something like the following but cant get it working:
mounted: function () {
this.model = 'g1111111111'
},
codepen: https://codepen.io/aquadk/pen/JQbbKw
Thanks
You can use updated method, it called after the data changed and virtual dom is created for that component. Then you can update the value of the model.
const stringOptions = [
{label:'Google', value:'g1111111111'}, {label:'Facebook', value:'f2222222222'}, {label:'Twitter', value:'t3333333'}, {label:'Apple', value:'a44444444'}, {label:'Oracle', value:'o555555555'}
]
new Vue({
el: '#q-app',
data () {
return {
model: 'f2222222222',
options: stringOptions
}
},
methods: {
filterFn (val, update, abort) {
update(() => {
const needle = val.toLowerCase()
this.options = stringOptions.filter(v => v.label.toLowerCase().indexOf(needle) > -1)
})
}
},
updated(){
// Update the value of model
this.model = 'g1111111111';
}
})
The mounted should work, if it's not working the way you expect, try inside-mounted nextTick().
Here is an example with your code:
mounted () {
this.$nextTick(() => {
this.model = 'a44444444'
})
},

Why content of child component with props is not rendered on the page?

In vuejs3 app
I read data with axios request from backend API. I see that data are passed to internal
component, but I do not see content of the child component is rendered on the page.
Parent component:
<template>
<div class="row m-0 p-0" v-show="forumCategories.length && isPageLoaded">
<div v-for="(nextActiveForumCategory, index) in forumCategories" :key="nextActiveForumCategory.id" class="col-sm-12 col-md-6 p-2 m-0">
index::{{ index}}
<forum-category-block
:currentLoggedUser="currentLoggedUser"
:nextActiveForumCategory="nextActiveForumCategory"
:index="index"
:is_show_location="true"
></forum-category-block>
</div>
</div>
</template>
<script>
import ForumCategoryBlock from '#/views/forum/ForumCategoryBlock.vue'
import { useStore } from 'vuex'
export default {
name: 'forumsByCategoryPage',
components: {
ForumCategoryBlock,
},
setup () {
const store = useStore()
const orderBy = ref('created_at')
const orderDirection = ref('desc')
const forumsPerPage = ref(20)
const currentPage = ref(1)
let forumsTotalCount = ref(0)
let forumCategories = ref([])
let isPageLoaded = ref(false)
let credentialsConfig = settingCredentialsConfig
const currentLoggedUserToken = computed(
() => {
return store.getters.token
}
)
const currentLoggedUser = computed(
() => {
return store.getters.user
}
)
const forumsByCategoryPageInit = async () => {
loadForums()
}
function loadForums() {
isPageLoaded = false
let credentials = getClone(credentialsConfig)
credentials.headers.Authorization = 'Bearer ' + currentLoggedUserToken.value
let filters = { current_page: currentPage.value, order_by: orderBy.value, order_direction: orderDirection.value }
const apiUrl = process.env.VUE_APP_API_URL
axios.get(apiUrl + '/forums-by-category', filters, credentials)
.then(({ data }) => {
console.log('/forums-by-category data::')
console.log(data)
forumCategories.value = data.forumCategories
forumsTotalCount.value = data.forumsTotalCount
isPageLoaded = true
console.log('++forumCategories::')
console.log(forumCategories)
})
.catch(error => {
console.error(error)
isPageLoaded = true
})
} // loadForums() {
onMounted(forumsByCategoryPageInit)
return {
currentPage, orderBy, orderDirection, isPageLoaded, loadForums, forumCategories, getHeaderIcon, pluralize, forumsTotalCount, forumCategoriesTitle, currentLoggedUser
}
} // setup
</script>
and ForumCategoryBlock.vue:
<template>
<div class="">
<h1>INSIDE</h1>
<fieldset class="bordered" >
<legend class="blocks">Block</legend>
nextActiveForumCategory::{{ nextActiveForumCategory}}<br>
currentLoggedUser::{{ currentLoggedUser}}<br>
index::{{ index }}<br>
</fieldset>
</div>
</template>
<script>
import { computed } from 'vue'
export default {
name: 'forumCategoryBlock',
props: {
currentLoggedUser: {
type: Object,
default: () => {}
},
nextActiveForumCategory: {
type: Object,
default: () => {}
},
index: {
type: Number,
default: () => {}
}
},
setup (props) {
console.log('setup props::')
console.log(props)
const nextActiveForumCategory = computed({
get: () => props.value.nextActiveForumCategory
})
const currentLoggedUser = computed({
get: () => props.value.currentLoggedUser
})
const index = computed({
get: () => props.index
})
return { /* currentLoggedUser, nextActiveForumCategory, index */ }
}
}
</script>
What I see in browser : https://prnt.sc/vh7db9
What is wrong abd how to fix it ?
MODIFIED :
I understood WHERE the error :
<div class="row m-0 p-0" v-show="forumCategories.length && isPageLoaded" style="border: 2px dotted red;">
if to remove 2nd condition && isPageLoaded in a line above I see content.
But looks like that var isPageLoaded is not reactive and I do not see why?
If is declared with ref and is declared in return of setup method.
But looks like as I modify it in loadForums method it does not work in template...
Thanks!
isPageLoaded is losing its reactivity because loadForums() is changing its type from ref to Boolean:
isPageLoaded = true // ❌ no longer a ref
isPageLoaded is a ref, so your code has to access it through its value property. It's probably best to use const instead of let here to avoid this mistake:
const isPageLoaded = ref(false)
isPageLoaded.value = true // ✅

do not mutate vuex store state outside mutation handlers - Vuetify snackbar

I have a snackbar from Vuetify. It's in default.vue and the vuex store controls the v-model, message and color:
DefaultSnackBar.vue
<template>
<v-container>
<v-snackbar
v-model="snackbarProperties.show"
:color="snackbarProperties.color"
timeout="7000"
multi-line
>
{{ snackbarProperties.message }}
<template v-slot:action="{ attrs }">
<v-btn
text
v-bind="attrs"
#click="hideSnackbar"
>
Close
</v-btn>
</template>
</v-snackbar>
</v-container>
</template>
<script>
import { mapActions } from "vuex";
import { mapGetters } from "vuex";
export default {
methods :{
...mapActions("Snackbar",["showSnackbar","hideSnackbar"]),
},
computed: {
...mapGetters("Snackbar",["snackbarProperties"])
},
}
</script>
Snackbar.js
export const state = () => ({
message: "",
color: "",
show: false,
});
export const getters = {
snackbarProperties: state => {
return state;
},
}
export const mutations = {
showSnackbar: (state, payload) => {
state.message = payload.message;
state.color = payload.color;
state.show = true;
},
hideSnackbar: (state) => {
state.message = "";
state.color = ""
state.show = false;
},
}
export const actions = {
showSnackbar({ commit }, payload) {
commit('showSnackbar', payload)
},
hideSnackbar({ commit }) {
commit('hideSnackbar')
}
}
When I call showSnackbar({...}) the bar appears correctly with no errors, but when it disappears (timeout is reached) is get this error and everything crashes
do not mutate vuex store state outside mutation handlers
I think it's because when the bar disappears the component changes the value of the v-model it's attached to but I'm not sure how to work around this.
I found the answer from this vue forum:
Use an action with the setTimeout code in it. Then in the timeout
commit the mutation. Mutations should be synchronous which is why
using a timeout in them is throwing a warning.
I've updated Snackbar.js to suit:
export const state = () => ({
message: "",
color: "",
show: false,
});
export const getters = {
snackbarProperties: state => {
return state;
},
}
export const mutations = {
showSnackbar: (state, payload) => {
state.message = payload.message;
state.color = payload.color;
state.show = true;
},
hideSnackbar: (state) => {
state.message = "";
state.color = ""
state.show = false;
},
}
export const actions = {
showSnackbar({ commit }, payload) {
commit('showSnackbar', payload)
setTimeout(() => {
commit('hideSnackbar')
}, 500);
},
hideSnackbar({ commit }) {
commit('hideSnackbar')
}
}
try this if you need showing multiple
<template>
<div class="text-center">
<v-snackbar
v-for="(snackbar, index) in snackbars.snackbars.filter(
(s) => s.isVisible
)"
:key="snackbar.text + Math.random()"
v-model="snackbar.isVisible"
:color="snackbar.color"
:timeout="-1"
:right="true"
:top="true"
:style="`top: ${index * 60}px`"
>
<v-row no-gutters>
<v-col md="11" sm="11">
{{ snackbar.text }}
</v-col>
<v-col md="1" sm="1">
<v-btn class="mx-2" icon small #click="hideNotify(index)">
<v-icon color="error"> mdi-close </v-icon>
</v-btn>
</v-col>
</v-row>
</v-snackbar>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
computed: {
...mapState({ snackbars: 'notification' }),
},
methods: {
hideNotify(index) {
this.$store.dispatch('notification/HIDE_NOTIFY_WITH_INDEX',index)
},
},
}
</script>
add this into the vuex as notification.js
export const state = () => ({
snackbars: [],
})
export const mutations = {
SET_SNACKBAR(state, snackbar) {
state.snackbars = state.snackbars.concat(snackbar)
},
HIDE_NOTIFY_WITH_INDEX(state,index) {
if (index in state.snackbars) {
state.snackbars.splice(index, 1)
}
},
HIDE_NOTIFY(state) {
state.snackbars = []
},
}
export const actions = {
SET_SNACKBAR({ commit }, snackbar) {
snackbar.isVisible = true
snackbar.color = snackbar.color || 'dark'
commit('SET_SNACKBAR', snackbar)
setTimeout(() => {
commit('HIDE_NOTIFY')
}, 6000)
},
HIDE_NOTIFY_WITH_INDEX({ commit },index) {
commit('HIDE_NOTIFY_WITH_INDEX',index)
},
}

vue.js test-utils Why my onSubmit function mock is not called?

I am trying to test the submit form event using the following spec file :
ContactForm.spec.js
import Vue from "vue";
import Vuex from "vuex";
import { storeMock } from "./mocks.js";
import VeeValidate from "vee-validate";
import i18n from "#/locales";
import Vuetify from "vuetify";
import { mount, shallowMount } from "#vue/test-utils";
import ContactForm from "#/components/Home/ContactForm.vue";
Vue.use(Vuex);
Vue.use(VeeValidate, { errorBagName: "errors" });
Vue.use(Vuetify);
describe("ContactForm.vue", () => {
let wrapper;
let store = new Vuex.Store(storeMock);
const v = new VeeValidate.Validator();
beforeEach(() => {
const el = document.createElement("div");
el.setAttribute("data-app", true);
document.body.appendChild(el);
});
it("submit valid form when click submit", async () => {
// given
const getters = {
language: () => {
return "fr";
},
"authentication/loading": () => {
return false;
}
},
store = new Vuex.Store({ getters });
// const sendMessageMock = jest.fn( () => Promise.resolve() );
const sendMessageMock = jest.fn();
const options = {
sync: false,
store,
provide: () => ({
$validator: v
}),
i18n,
mocks : {
sendMessage: sendMessageMock
}
};
// when
wrapper = shallowMount(ContactForm, options);
const contactForm = wrapper.find('#contactForm');
const submitBtn= wrapper.find('#btnSend');
// console.log(contactForm.html());
// when
contactForm.trigger('submit.prevent');
// then
expect(sendMessageMock.called).toBe(true);
});
});
But this test does not pass ...
console.log
✕ submit valid form when click submit (157ms)
● ContactForm.vue › submit valid form when click submit
expect(received).toBe(expected) // Object.is equality
Expected: true
Received: undefined
Difference:
Comparing two different types of values. Expected boolean but received undefined.
77 | contactForm.trigger('submit.prevent');
78 | // then
> 79 | expect(sendMessageMock.called).toBe(true);
| ^
80 | });
81 | });
82 |
at Object.toBe (tests/unit/ContactForm.spec.js:79:36)
at tryCatch (node_modules/regenerator-runtime/runtime.js:62:40)
at Generator.invoke [as _invoke] (node_modules/regenerator-runtime/runtime.js:296:22)
at Generator.prototype.(anonymous function) [as next] (node_modules/regenerator-runtime/runtime.js:114:21)
at step (node_modules/#babel/runtime/helpers/builtin/asyncToGenerator.js:10:30)
at _next (node_modules/#babel/runtime/helpers/builtin/asyncToGenerator.js:25:9)
at node_modules/#babel/runtime/helpers/builtin/asyncToGenerator.js:32:7
Here is the component vue to be tested
ContactForm.vue
<template>
<form id="contactForm" onSubmit="sendMessage">
<input v-model="language" type='hidden' name='locale'>
<v-layout row wrap align-center>
.../...
</v-layout>
<v-text-field
.../...
</v-text-field>
<v-textarea .../... ></v-textarea>
<v-btn round #click="clear">.../...</v-btn>
<v-btn id="btnSend" round large color="primary" type="submit">Submit</v-btn>
</form>
</template>
<script>
import { mapGetters } from "vuex";
.../...
import router from "#/router";
export default {
name: "contactForm",
data() {
return {
.../...
};
},
computed: {
...mapGetters(["language"]),
...mapGetters("authentication", ["loading"]),
honorificPrefix: function() {
.../...
}
},
watch: {
language(newLanguage) {
.../...
}
},
methods: {
setPrefix: function(value) {
.../...
},
sendMessage: function() {
.../...
},
clear: function() {
.../...
}
},
mounted() {
.../...
}
};
</script>
Feedback welcome ...
SOLVED ...
change the form tag
remove the mocks: block in shallowMount() and use setMethods() to replace the sendMessage() with the sendMessageMock()
it("submit valid form when click submit", async () => {
// given
const getters = {
language: () => {
return "fr";
},
"authentication/loading": () => {
return false;
}
},
store = new Vuex.Store({ getters });
const sendMessageMock = jest.fn();
const options = {
sync: false,
store,
provide: () => ({
$validator: v
}),
i18n
};
// when
wrapper = shallowMount(ContactForm, options);
wrapper.setMethods({ sendMessage: sendMessageMock });
const contactForm = wrapper.find('#contactForm');
// when
contactForm.trigger('submit.prevent');
// then
expect(sendMessageMock).toBeCalled();
});