Reactive State function unit testing in vue - vue.js

I am very new to unit testing. I have reactive state usage in the component. would like to write unit testing for the component.
The example test component is defined below
<script setup lang="ts">
const state = reactive({
isExpired: true as boolean,
})
</script>
<template>
<div class="container" v-if="state.isExpired === false"></div>
</template>
Here, I want to check whether the container is rendering or not. isExpired value will be dynamically changed.
I have tried the test cases for the component as shared below
describe("Test suit for component",()=>{
let wrapper = shallowMount(TestComponent,{
isExpired : false
});
it("its should display container style",async ()=>{
expect(wrapper.find('.container').exists()).toBeTruthy();
});
});
I am not able to find the relevant unit testing in the online resources. Would like to know how to test the reactive state of component.

Related

watching vueuse's useElementVisibility changes is not working

I have a sample project at https://github.com/eric-g-97477-vue/vue-project
This is a default vue project with vueuse installed.
I modified the script and template part of HelloWorld.vue to be:
<script setup>
import { watch, ref } from "vue";
import { useElementVisibility } from "#vueuse/core";
defineProps({
msg: {
type: String,
required: true,
},
});
const me = ref(null);
const isVisible = useElementVisibility(me);
watch(me, (newValue, oldValue) => {
console.log("visibilityUpdated", newValue, oldValue);
});
</script>
<template>
<div ref="me" class="greetings">
<h1 class="green">{{ msg }}</h1>
<h3>
You’ve successfully created a project with
Vite +
Vue 3.
</h3>
</div>
</template>
and adjusted App.vue so the HelloWorld component could be easily scrolled on or off the screen.
This appears to match the Usage demo code. The primary difference being that I am using <script setup> and the demo code is not. But, perhaps, I need to do things differently as a result...?
The watcher will fire when the app loads and indicates that the HelloWorld component is visible, but it is not. Additionally, regardless if I scroll so the component is visible or not, the watcher does not fire again.
What am I doing wrong?
UPDATE: modified the question based on the discovery that I needed to add ref="me" to the div in the template. Without this, the watcher was never firing.

Call a function from another component using composition API

Below is a code for a header and a body (different components). How do you call the continue function of the component 2 and pass a parameter when you are inside component 1, using composition API way...
Component 2:
export default {
setup() {
const continue = (parameter1) => {
// do stuff here
}
return {
continue
}
}
}
One way to solve this is to use events for parent-to-child communication, combined with template refs, from which the child method can be directly called.
In ComponentB.vue, emit an event (e.g., named continue-event) that the parent can listen to. We use a button-click to trigger the event for this example:
<!-- ComponentB.vue -->
<script>
export default {
emits: ['continue-event'],
}
</script>
<template>
<h2>Component B</h2>
<button #click="$emit('continue-event', 'hi')">Trigger continue event</button>
</template>
In the parent, use a template ref on ComponentA.vue to get a reference to it in JavaScript, and create a function (e.g., named myCompContinue) to call the child component's continueFn directly.
<!-- Parent.vue -->
<script>
import { ref } from 'vue'
export default {
⋮
setup() {
const myComp = ref()
const myCompContinue = () => myComp.value.continueFn('hello from B')
return {
myComp,
myCompContinue,
}
},
}
</script>
<template>
<ComponentA ref="myComp" />
⋮
</template>
To link the two components in the parent, use the v-on directive (or # shorthand) to set myCompContinue as the event handler for ComponentB.vue's continue-event, emitted in step 1:
<template>
⋮
<ComponentB #continue-event="myCompContinue" />
</template>
demo
Note: Components written with the Options API (as you are using in the question) by default have their methods and props exposed via template refs, but this is not true for components written with <script setup>. In that case, defineExpose would be needed to expose the desired methods.
It seems like composition API makes everything a lot harder to do with basically no or little benefit. I've recently been porting my app to composition API and it required complete re-architecture, loads of new code and complexity. I really don't get it, seems just like a massive waste of time. Does anyone really think this direction is good ?
Here is how I solved it with script setup syntax:
Parent:
<script setup>
import { ref } from 'vue';
const childComponent = ref(null);
const onSave = () => {
childComponent.value.saveThing();
};
</script>
<template>
<div>
<ChildComponent ref="childComponent" />
<SomeOtherComponent
#save-thing="onSave"
/>
</div>
</template>
ChildComponent:
<script setup>
const saveThing = () => {
// do stuff
};
defineExpose({
saveThing,
});
</script>
It doesn't work without defineExpose. Besides that, the only trick is to create a ref on the component in which you are trying to call a function.
In the above code, it doesn't work to do #save-thing="childComponent.saveThing", and it appears the reason is that the ref is null when the component initially mounts.

Vue3 (Vite) directly access parent data from child component

I have a simple landing page using Vue3 Vite (SSG) without Vuex.
I need to pass a screenWidth value being watched in App.vue to a bunch of child components so that they change images depending on the user's screenWidth.
I could use props to pass this value, but it seems a bit cumbersome to write them for 8 child components, and to use composition data export or provide / inject is definitely overkill.
is there not a way to simply access a parent's data via something like instance.parent (didn't work), $parent.message (Vue2 way), etc from a child component?
// Parent:
data() {
return {
screenWidth: 123
}
}
// Child
<div v-if="$parent.screenWidth > 1200">
img...
</div>
EDIT: Solving this with props for now as no other (working) solution seems to be available in Vite for what used to be easy as pie in Vue2.
EDIT 2: It occurs to me now that using VueUse's built in useWindowSize might have been a good solution here.
Use v-model binding.
Parent component (assuming setup script):
<script setup lang='ts'>
import {ref} from 'vue';
const screenWidth = ref(720);
// use screenWidth as a regulat reactive variable here
</script>
<template>
<Child v-model="screenWidth" />
</template>
Child component:
<script setup lang="ts">
import {ref, watchEffect} from 'vue';
const props = defineProps<{
modelValue: number;
}>();
const emit = defineEmits<{
(e: 'update:modelValue', value: number): void
}>();
const value = ref(props.modelValue);
watchEffect(() => value.value = props.modelValue);
function setValue(newValue: number) {
value.value = key;
emit('update:modelValue', value.value);
}
</script>
<template>
// Use `value` and `setValue` here
</template>

Vue3 Composition API Reusable reactive values unique to calling component

Running Vue 3.2.6 and Vite 2.5.1
I've been experimenting a bit with the new Composition API and trying to figure out some common usecases where it makes sense to use it in favor of the OptionsAPI. One good usecase I immediately identified would be in Modals, the little popups that occur with a warning message or dialogue or whatever else.
In my old Apps, I'd have to create the modal opening logic in every single component where the modal is being called, which lead to a lot of repetition. With the CompAPI, I tried extracting the logic into a simple modal.ts file that exports 2 things, a reactive openModal boolean, and a toggleModal function. It works great! Until I have more than one modal in my app, that is, in which case it'll open every single Modal at once, on top of one another.
As an example setup
modal.ts
import { ref } from "vue";
const openModal = ref(false);
const toggleModal = () => {
openModal.value = !openModal.value;
};
export { openModal, toggleModal };
App.vue
<template>
<Example1 />
<Example2 />
<Example3 />
</template>
Modal.vue
<template>
<div class="modal" #click.self.stop="sendClose">
<slot />
</div>
</template>
<script setup>
const emit = defineEmits(["closeModal"]);
const sendClose = () => {
emit("closeModal");
};
</script>
Example#.vue
Note that each of these are separate components that have the same layout, the only difference being the number
<template>
<h1>Example 1 <span #click="toggleModal">Toggle</span></h1>
<teleport to="body">
<Modal v-if="openModal" #closeModal="toggleModal">
<h1>Modal 1</h1>
</Modal>
</teleport>
</template>
<script setup>
import { openModal, toggleModal } from "#/shared/modal";
import Modal from "#/components/Modal.vue";
</script>
What happens when clicking the toggle span is obvious (in hindsight). It toggles the value of openModal, which will open all 3 modals at once, one on top of the other. The issue is even worse if you try to implement nested Modals, aka logic in one modal that will open up another modal on top of that one.
Am I misunderstanding how to use ref here? Is it even possible for each component to have and keep track of its own version of openModal? Cause the way I've set it up here, it's acting more like a global store, which isn't great for this particular usecase.
The way I imagined this working is that each component would import the reactive openModal value, and keep track of it independently. That way, when one component calls toggleModal, it would only toggle the value inside of the component calling the function.
Is there a way of doing what I originally intended via the Composition API? I feel like the answer is simple but I can't really figure it out.
That is because you are not exporting your composition correctly, resulting in a shared state, since you are exporting the same function and ref to all components. To fix your issue, you should wrap whatever you're exporting in modal.ts in a function, say:
// Wrap in an exported function (you can also do a default export if you want)
export function modalComposition() {
const openModal = ref(false);
const toggleModal = () => {
openModal.value = !openModal.value;
};
return { openModal, toggleModal };
}
And in each component that you plan to use the composition, simply import it, e.g.:
import { modalComposition } from "#/shared/modal";
import Modal from "#/components/Modal.vue";
// By invoking `modalComposition()`, you are no longer passing by reference
// And therefore there is no "shared state"
const { openModal, toggleModal } = modalComposition();
Why does this work?
When you export a function and then invoke it in the setup of every single component, you are ensuring that each component is setup by executing the function, which returns a new ref for every single instance.

Can't stop Vue JS (v2) from reusing components

I have a Vue app that conditionally displays sets of the same component, I'd like to tie in to the created or mounted methods, but Vue keeps re-using earlier components and not triggering the methods. I've tried wrapping the components in keep-alive tags to no effect.
The code below shows what you would expect on the page: changing someVariable to 'test1' or 'test2' shows the relevant components, but the created/mounted methods only fire a maximum of 2 times (e.g. changing someVariable to 'test1' logs creation and mounting of 1 component with the label type1, then changing it to 'test2' only logs 1 more component with the label type3).
Currently using v2.1.10
HTML
<div id="app">
<div v-if="someVariable === 'test1'">
<keep-alive>
<test-component data-type="type1"></test-component>
</keep-alive>
</div>
<div v-if="someVariable === 'test2'">
<keep-alive>
<test-component data-type="type2"></test-component>
</keep-alive>
<keep-alive>
<test-component data-type="type3"></test-component>
</keep-alive>
</div>
</div>
JS/Vue
Vue.component('test-component', {
props: ['data-type'],
template: '<div><p>In the component: {{ dataType }}</p></div>',
created: function () {
console.log('component created: ', this.dataType);
},
mounted: function () {
console.log('component mounted: ', this.dataType);
}
});
var app = new Vue({
el: '#app',
data: {
someVariable: ""
}
});
You should use a watcher on your someVariable instead of trying to hook on created and mounted hooks.
Components are created, and mounted the first time they are visible (rendered). There are NO "shown" or "hidden" hooks.
See https://v2.vuejs.org/v2/guide/computed.html#Watchers:
watch: {
someVariable: function (newValue) {
// test newValue and do what you have to do!
}
}
For your specific example removing keep-alive from the second if should do the trick https://jsfiddle.net/z11fe07p/464/
An interesting thing is that vue seems to re-use the previously rendered component. So if you have one component in the first if when switching to the next if with 2 components it will re-use one component and create a new one for the second component from the block. When getting back to the first if block it will re-use one of the 2 already rendered components.
As mentioned above, a watcher is more suited for such cases, thus getting you rid of handling logic in places where you don't have full control. You can see this tip right here in the docs https://v2.vuejs.org/v2/api/#updated