I’m in a situation where I don’t know how to test a VueJS component, I think essentially because both vue and vitest are new to me, but I cannot find any solution googling around.
I have one component that receives some props and two different named slots.
onmounted()
checks if the text in one of this slots "fits" vertically a threshold in px that I set. If not, adds a CSS class to the parent <div>
and shows a button.
What I see in my test is that the height of the elements is always returned as 0
, I suppose because the rendering engine on vitest does not expose/compute element.clientHeight
.
This causes the button whose functionality I need to test to never be rendered.
I tried to change the variable that toggles the button’s visibility from the test, using wrapper.vm.isButtonVisible = true
(isButtonVisible
is a ref
), but without success, I suppose because the script is defined as <script setup>
.
Looks like functions and refs in the component are not accessible from my test suite. Here is a simplified version of the component and the test:
<template>
<div ref="textWrapper" class="detail-summary__text" :class="{'truncated': isButtonVisible}">
<div ref="textContainer" class="detail-summary__inner-text">
<slot name="default"></slot>
</div>
</div>
<div class="detail-summary__button">
<button
v-if="isButtonVisible"
@click="toggleModal"
>Show more</button>
</div>
</template>
<script setup lang="ts">
const textContainer: Ref<DomElement> = ref(null);
const textWrapper: Ref<DomElement> = ref(null);
const textModal: Ref<DomElement> = ref(null);
const isButtonVisible: Ref<boolean> = ref(false);
const isModalOpen: Ref<boolean> = ref(false);
onMounted(() => {
checkContainerHeight();
});
function checkContainerHeight():void {
let textInnerHeight = textHeight.value;
let textWrapperHeight = textHeight.value;
if (textWrapper.value != null && textWrapper.value.clientHeight != null) {
textWrapperHeight = textWrapper.value.clientHeight;
}
if (textContainer.value != null && textContainer.value.clientHeight != null) {
textInnerHeight = textContainer.value.clientHeight;
if (textInnerHeight > textWrapperHeight) {
makeButtonVisible();
}
}
}
function makeButtonVisible(): void {
isButtonVisible.value = true;
}
function toggleModal(): void {
isModalOpen.value = !isModalOpen.value;
}
</script>
I also tried to move isButtonVisible.value = true;
to a function, and call it in the test, but I get no error, and anyway wrapper.html()
does not contain the button, so I guess I cannot access functions either.
Edit (adding a sample test)
In the test, I tried:
it.only('should show the see more button', async () => {
// when
const wrapper = await mount(DetailSummary, {
props: {
[...]
},
slots: {
default: 'text here',
},
stubs: [...],
shallow: true,
});
// then
wrapper.vm.makeButtonVisible() // I see the console that I added to the function
console.log(wrapper.html()); // The snapshot still doesn't show the button
const e = wrapper.findComponent({ name: 'DetailSummary' });
e.vm.makeButtonVisible(); // If I add a console in the function, I see it to be called, even if the linter says that that method does not exist
console.log(wrapper.html()); // The snapshot still doesn't show the button
});
Can someone suggest me how to proceed, or point me to some docs/examples?
2
Answers
I might have found a solution. In the test, I was missing some update to the rendered component.
It now looks like this, and seems to be working. The trick was the
nextTick()
method.I don't know if this is the right/most elegant way, but it seems to be working.
The use of composition API limits a way a component can be tested. As a rule of thumb, it’s supposed to be treated as a graybox and tested as a single unit.
This also creates a problem because there’s no real DOM to test against.
In this case it’s necessary to mock
clientHeight
of multiple elements. The problem is that the elements are available only after the component has been mounted, and Vue test utils don’t provide a way to interfere the natural lifecycle. It’s possible to provide additionalmounted
lifecycle hooks but it’s impossible to guarantee their execution before the component’sonMounted
. Doing this would require to postpone the tested code to make it more testable, which is allowed but can result in unwanted side effects like layout blinking:clientHeight
is readonly but can be overridden in JSDOM with:A less limiting way is to not modify the way
onMounted
works but mock the way fake DOM works to fit the test. SinceclientHeight
is read multiple times on multiple elements, this requires to to mock it in a smarter way to meet the expectations:Then
click
can be triggered onbutton
element to test how it triggers the visibility of a modal.This relies on the convention used with Testing Library, etc that expects unique
data-testid
attributes to be added to tested elements to make the selection of DOM elements unambiguous, e.g.: