Using vitest and testing-library is there a way to segregate component renders on a test by test basis? - testing

I have a simple list component written in Vue3 that I am using to learn how to write automated test with Vitest and testing-library. However every test method seems to be rendered together, causing my getByText calls to throw the error TestingLibraryElementError: Found multiple elements with the text: foo.
This is the test I have written:
import { describe, it, expect, test } from 'vitest'
import { render, screen, fireEvent } from '#testing-library/vue'
import TmpList from '../ui/TmpList.vue'
const listItems = ['foo', 'bar']
describe('TmpList', () => {
// Test item-content slot rendering
test('renders item-content slot', () => {
const slotTemplate = `
<template v-slot:item-content="{ item }">
<div> {{ item }} </div>
</template>`;
render(TmpList, { props: { listItems }, slots: { 'item-content': slotTemplate } });
listItems.forEach(li => {
expect(screen.getByText(li)).toBeTruthy();
})
})
// Test list item interaction
test('should select item when clicked and is selectable', async () => {
const slotTemplate = `
<template v-slot:item-content="{ item }">
<div> {{ item }} </div>
</template>`;
render(TmpList, { props: { listItems, selectable: true }, slots: { 'item-content': slotTemplate } });
const firstItem = screen.getByText(listItems[0]);
await fireEvent.click(firstItem);
expect(firstItem.classList).toContain('selected-item')
})
})
The component:
<template>
<ul>
<li v-for="(item, index) in listItems" :key="`list-item-${index}`" #click="onItemClick(index)"
class="rounded mx-2" :class="{
'selected-item bg-secondary-600/20 text-secondary':
selectedIndex == index,
'hover:bg-zinc-200/30': selectable,
}">
<slot name="item-content" :item="item"></slot>
</li>
</ul>
</template>
<script setup lang="ts">
import { computed, ref } from "vue";
export interface Props {
listItems: any[];
selectable?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
selectable: false,
});
const selectedIndex = ref<number>(-1);
const onItemClick = (index: number) => {
if (props.selectable) {
selectedIndex.value = index;
}
};
</script>
This is the full error I get in the terminal:
TestingLibraryElementError: Found multiple elements with the text: foo
Here are the matching elements:
Ignored nodes: comments, script, style
<div>
foo
</div>
Ignored nodes: comments, script, style
<div>
foo
</div>
(If this is intentional, then use the `*AllBy*` variant of the query (like `queryAllByText`, `getAllByText`, or `findAllByText`)).
Ignored nodes: comments, script, style
<body>
<div>
<ul
data-v-96593be0=""
>
<li
class="rounded mx-2"
data-v-96593be0=""
>
<div>
foo
</div>
</li>
<li
class="rounded mx-2"
data-v-96593be0=""
>
<div>
bar
</div>
</li>
</ul>
</div>
<div>
<ul
data-v-96593be0=""
>
<li
class="rounded mx-2 hover:bg-zinc-200/30"
data-v-96593be0=""
>
<div>
foo
</div>
</li>
<li
class="rounded mx-2 hover:bg-zinc-200/30"
data-v-96593be0=""
>
<div>
bar
</div>
</li>
</ul>
</div>
</body>
❯ Object.getElementError node_modules/#testing-library/dom/dist/config.js:37:19
❯ getElementError node_modules/#testing-library/dom/dist/query-helpers.js:20:35
❯ getMultipleElementsFoundError node_modules/#testing-library/dom/dist/query-helpers.js:23:10
❯ node_modules/#testing-library/dom/dist/query-helpers.js:55:13
❯ node_modules/#testing-library/dom/dist/query-helpers.js:95:19
❯ src/components/__tests__/SUList.spec.ts:54:33
52|
53| render(TmpList, { props: { listItems, selectable: true }, slots: { 'item-content': slotTemplate } });
54| const firstItem = screen.getByText(listItems[0]);
| ^
55| await fireEvent.click(firstItem);
56| expect(firstItem.classList).toContain('selected-item')
I know I could use the getAllByText method to query multiple items, but in this test I am expecting only one element to be found. The duplication is related to the rendering in the test, not an issue with the actual component.
Am I doing something wrong when writing the tests? Is there a way to ensure that each render will be executend independetly of renders from other tests?

Every render() returns #testing-library's methods (query* /get* /find* ) scoped to the template being rendered.
In other words, they normally require a container parameter, but when returned by render, the container is already set to that particular render's DOM:
it('should select on click', async () => {
const { getByText } = render(TmpList, {
props: { listItems, selectable: true },
slots: { 'item-content': slotTemplate },
})
const firstItem = getByText(listItems[0])
expect(firstItem).not.toHaveClass('selected-item')
await fireEvent.click(firstItem)
expect(firstItem).toHaveClass('selected-item')
})
Notes:
fireEvent is no longer returning a promise in latest versions of #testing-library. If, in the version you're using, still returns a promise, keep the async - only true for #testing-library/react.
you want to get to a point where you no longer need to import screen in your test suite
If you find yourself writing the same selector or the same render parameters multiple times, it might make sense to write a renderComponent helper at the top of your test suite:
describe(`<ListItems />`, () => {
// define TmpList, listItems, slotTemplate
const defaults = {
props: { listItems, selectable: true },
slots: { 'item-content': slotTemplate },
}
const renderComponent = (overrides = {}) => {
// rendered test layout
const rtl = render(TmpList, {
...defaults,
...overrides
})
return {
...rtl,
getFirstItem: () => rtl.getByText(listItems[0]),
}
}
it('should select on click', async () => {
const { getFirstItem } = renderComponent()
expect(getFirstItem()).not.toHaveClass('selected-item')
await fireEvent.click(getFirstItem())
expect(getFirstItem()).toHaveClass('selected-item')
})
it('does something else with different props', () => {
const { getFirstItem } = renderComponent({
props: /* override defaults.props */
})
// expect(getFirstItem()).toBeOhSoSpecial('sigh...')
})
})
Note I'm spreading rtl in the returned value of renderComponent(), so all the get*/find*/query* methods are still available, for the one-off usage, not worth writing a getter for.

Related

Nuxt.js Hackernews API update posts without loading page every minute

I have a nuxt.js project: https://github.com/AzizxonZufarov/newsnuxt2
I need to update posts from API every minute without loading the page:
https://github.com/AzizxonZufarov/newsnuxt2/blob/main/pages/index.vue
How can I do that?
Please help to end the code, I have already written some code for this functionality.
Also I have this button for Force updating. It doesn't work too. It adds posts to previous posts. It is not what I want I need to force update posts when I click it.
This is what I have so far
<template>
<div>
<button class="btn" #click="refresh">Force update</button>
<div class="grid grid-cols-4 gap-5">
<div v-for="s in stories" :key="s">
<StoryCard :story="s" />
</div>
</div>
</div>
</template>
<script>
definePageMeta({
layout: 'stories',
})
export default {
data() {
return {
err: '',
stories: [],
}
},
mounted() {
this.reNew()
},
created() {
/* setInterval(() => {
alert()
stories = []
this.reNew()
}, 60000) */
},
methods: {
refresh() {
stories = []
this.reNew()
},
async reNew() {
await $fetch(
'https://hacker-news.firebaseio.com/v0/topstories.json?print=pretty'
).then((response) => {
const results = response.slice(0, 10)
results.forEach((id) => {
$fetch(
'https://hacker-news.firebaseio.com/v0/item/' +
id +
'.json?print=pretty'
)
.then((response) => {
this.stories.push(response)
})
.catch((err) => {
this.err = err
})
})
})
},
},
}
</script>
<style scoped>
.router-link-exact-active {
color: #12b488;
}
</style>
This is how you efficiently use Nuxt3 with the useLazyAsyncData hook and a setInterval of 60s to fetch the data periodically. On top of using async/await rather than .then.
The refreshData function is also a manual refresh of the data if you need to fetch it again.
We're using useIntervalFn, so please do not forget to install #vueuse/core.
<template>
<div>
<button class="btn" #click="refreshData">Fetch the data manually</button>
<p v-if="error">An error happened: {{ error }}</p>
<div v-else-if="stories" class="grid grid-cols-4 gap-5">
<div v-for="s in stories" :key="s.id">
<p>{{ s.id }}: {{ s.title }}</p>
</div>
</div>
</div>
</template>
<script setup>
import { useIntervalFn } from '#vueuse/core' // VueUse helper, install it
const stories = ref(null)
const { pending, data: fetchedStories, error, refresh } = useLazyAsyncData('topstories', () => $fetch('https://hacker-news.firebaseio.com/v0/topstories.json?print=pretty'))
useIntervalFn(() => {
console.log('refreshing the data again')
refresh() // will call the 'topstories' endpoint, just above
}, 60000) // every 60 000 milliseconds
const responseSubset = computed(() => {
return fetchedStories.value?.slice(0, 10) // optional chaining important here
})
watch(responseSubset, async (newV) => {
if (newV.length) { // not mandatory but in case responseSubset goes null again
stories.value = await Promise.all(responseSubset.value.map(async (id) => await $fetch(`https://hacker-news.firebaseio.com/v0/item/${id}.json?print=pretty`)))
}
})
function refreshData() { refreshNuxtData('topstories') }
</script>

How can I resolve this error when calling v-for?

basically my error is that when I run npm run build on the project it points out that it can't find my manuals list
**npm error:
npm run build
vue-tsc --noEmit && vite build
src/views/ManuaisView.vue:11:80 - error TS2304: Cannot find name 'manuais'.
11 **
Found 1 error.
Code:
<div class="flex flex-wrap -mx-4">
<div class="w-full md:w-1/2 lg:w-1/3 px-4" v-for="(manual, idx) in manuais" :key="manual.key">
<div class="h-full p-8 text-center hover:bg-white rounded-md hover:shadow-xl transition duration-200">
<div class="inline-flex h-16 w-16 mb-6 mx-auto items-center justify-center text-white bg-vermelho rounded-lg">
<i class="far fa-file-pdf fa-2x"></i>
</div>
<h3 class="mb-4 text-xl md:text-2xl leading-tight font-bold">{{manual.nome}}</h3>
<p>{{manual.descritivo}}</p>
<p>{{manual.link}}</p>
</div>
</div>
</div>
</div>
</section>
</template>
<script lang="ts">
import { meudb } from '../db';
var post:any = [];
var items: any;
const postRef = meudb.ref('********').once('value', (snapshot) => {
const documents = snapshot.val();
snapshot.forEach(d => {
post.push({nome:d.val().nome, descritivo:d.val().descritivo, link:d.val().nivel})
});
items = documents
console.log(post);
// do something with documents
});
console.log(postRef);
export default {
data() {
return {
manuais: post,
// manuais: [],
}
},
firebase: {
manuais: {
source: meudb.ref('********'),
asObject: true
}
}
}
</script>
If you want to update the manuais value you should set it to an empty array in its definition (in the data) and update its value in the mounted lifecycle (check documentation here
Example :
export default {
data: () => {
return {
manuais: [],
}
},
mounted() {
var post: any = [];
var items: any;
const postRef = meudb.ref('********').once('value', (snapshot) => {
const documents = snapshot.val();
snapshot.forEach(d => {
post.push({
nome: d.val().nome,
descritivo: d.val().descritivo,
link: d.val().nivel
})
});
items = documents
// do something with documents
});
this.manuais = post; // <-- here updating the value
}
}
Working snippet
Here is a little snippet for you to better undestand how works v for.
In your case, if you apply this logic, your code might work
new Vue({
el: '#app',
data: () => ({
manuais: []
}),
mounted() {
// get data from dataBase here
this.manuais = [{
name: 'foo'
}, {
name: 'bar'
}]
}
})
<script src="https://unpkg.com/vue#2.x/dist/vue.js"></script>
<div id="app">
<div v-for="manual in manuals">{{ manual.name }}</div>
</div>

vuejs for triggering dropdown show

I want the dropdown to be opened when ul is clicked on the menu I want, in the same way, when the 2nd dropdown is clicked, the open dropdown will be closed.
<template>
<li>
<MenuLink
:link="items"
:key="items.name"
#click.stop="clickShow()"
/>
<ul class="children" v-if="hasChild" :class="open ? 'show' : 'hidden'">
<MenuItems
v-for="subItem in items.children"
:key="subItem.name"
:items="subItem"
/>
</ul>
</li>
</template>
<script>
export default {
name: 'MenuItems',
components: {MenuLink},
props: {
items: {type: Object, required: true},
level: {
type: Number,
default: 0
}
},
data() {
return {
childrenCount: 0,
active: false,
show: null,
open: false,
};
},
This is a I found that worked for me.
Also, I used the vue composition api, which I highly recommend using.
<template>
<ul v-if="showDropdownX">
<li>This is X</li>
<li>This is X</li>
<li>This is X</li>
</ul>
<button #click="handleShowX">Toggle dropdown X</button>
<ul v-if="showDropdownY">
<li>This is Y</li>
<li>This is Y</li>
<li>This is Y</li>
</ul>
<button #click="handleShowY">Toggle Dropdown Y</button>
</template>
<script>
import { ref } from "#vue/reactivity";
export default {
setup() {
const showDropdownX = ref(false);
const showDropdownY = ref(false);
const handleShowX = () => {
showDropdownX.value = !showDropdownX.value;
showDropdownY.value = false;
};
const handleShowY = () => {
showDropdownY.value = !showDropdownY.value;
showDropdownX.value = false;
};
return {
showDropdownX,
showDropdownY,
handleShowX,
handleShowY,
};
},
};
</script>
I hope this works for you, I haven't used the exact same code which you used, but I think my example is great to understand the concept of v-if
Edit:
Now the other dropdown will also disappear when you click on one of them.

Vue 3 how to get information about $children

This my old code with VUE 2 in Tabs component:
created() {
this.tabs = this.$children;
}
Tabs:
<Tabs>
<Tab title="tab title">
....
</Tab>
<Tab title="tab title">
....
</Tab>
</Tabs>
VUE 3:
How can I get some information about childrens in Tabs component, using composition API? Get length, iterate over them, and create tabs header, ...etc? Any ideas? (using composition API)
This is my Vue 3 component now. I used provide to get information in child Tab component.
<template>
<div class="tabs">
<div class="tabs-header">
<div
v-for="(tab, index) in tabs"
:key="index"
#click="selectTab(index)"
:class="{'tab-selected': index === selectedIndex}"
class="tab"
>
{{ tab.props.title }}
</div>
</div>
<slot></slot>
</div>
</template>
<script lang="ts">
import {defineComponent, reactive, provide, onMounted, onBeforeMount, toRefs, VNode} from "vue";
interface TabProps {
title: string;
}
export default defineComponent({
name: "Tabs",
setup(_, {slots}) {
const state = reactive({
selectedIndex: 0,
tabs: [] as VNode<TabProps>[],
count: 0
});
provide("TabsProvider", state);
const selectTab = (i: number) => {
state.selectedIndex = i;
};
onBeforeMount(() => {
if (slots.default) {
state.tabs = slots.default().filter((child) => child.type.name === "Tab");
}
});
onMounted(() => {
selectTab(0);
});
return {...toRefs(state), selectTab};
}
});
</script>
Tab component:
<script lang="ts">
export default defineComponent({
name: "Tab",
setup() {
const index = ref(0);
const isActive = ref(false);
const tabs = inject("TabsProvider");
watch(
() => tabs.selectedIndex,
() => {
isActive.value = index.value === tabs.selectedIndex;
}
);
onBeforeMount(() => {
index.value = tabs.count;
tabs.count++;
isActive.value = index.value === tabs.selectedIndex;
});
return {index, isActive};
}
});
</script>
<template>
<div class="tab" v-show="isActive">
<slot></slot>
</div>
</template>
Oh guys, I solved it:
this.$slots.default().filter(child => child.type.name === 'Tab')
To someone wanting whole code:
Tabs.vue
<template>
<div>
<div class="tabs">
<ul>
<li v-for="tab in tabs" :class="{ 'is-active': tab.isActive }">
<a :href="tab.href" #click="selectTab(tab)">{{ tab.name }}</a>
</li>
</ul>
</div>
<div class="tabs-details">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: "Tabs",
data() {
return {tabs: [] };
},
created() {
},
methods: {
selectTab(selectedTab) {
this.tabs.forEach(tab => {
tab.isActive = (tab.name == selectedTab.name);
});
}
}
}
</script>
<style scoped>
</style>
Tab.vue
<template>
<div v-show="isActive"><slot></slot></div>
</template>
<script>
export default {
name: "Tab",
props: {
name: { required: true },
selected: { default: false}
},
data() {
return {
isActive: false
};
},
computed: {
href() {
return '#' + this.name.toLowerCase().replace(/ /g, '-');
}
},
mounted() {
this.isActive = this.selected;
},
created() {
this.$parent.tabs.push(this);
},
}
</script>
<style scoped>
</style>
App.js
<template>
<Tabs>
<Tab :selected="true"
:name="'a'">
aa
</Tab>
<Tab :name="'b'">
bb
</Tab>
<Tab :name="'c'">
cc
</Tab>
</Tabs>
<template/>
If you copy pasted same code as me
then just add to the "tab" component a created method which adds itself to the tabs array of its parent
created() {
this.$parent.tabs.push(this);
},
My solution for scanning children elements (after much sifting through vue code) is this.
export function findChildren(parent, matcher) {
const found = [];
const root = parent.$.subTree;
walk(root, child => {
if (!matcher || matcher.test(child.$options.name)) {
found.push(child);
}
});
return found;
}
function walk(vnode, cb) {
if (!vnode) return;
if (vnode.component) {
const proxy = vnode.component.proxy;
if (proxy) cb(vnode.component.proxy);
walk(vnode.component.subTree, cb);
} else if (vnode.shapeFlag & 16) {
const vnodes = vnode.children;
for (let i = 0; i < vnodes.length; i++) {
walk(vnodes[i], cb);
}
}
}
This will return the child Components. My use for this is I have some generic dialog handling code that searches for child form element components to consult their validity state.
const found = findChildren(this, /^(OSelect|OInput|OInputitems)$/);
const invalid = found.filter(input => !input.checkHtml5Validity());
I made a small improvement to Ingrid Oberbüchler's component as it was not working with hot-reload/dynamic tabs.
in Tab.vue:
onBeforeMount(() => {
// ...
})
onBeforeUnmount(() => {
tabs.count--
})
In Tabs.vue:
const selectTab = // ...
// ...
watch(
() => state.count,
() => {
if (slots.default) {
state.tabs = slots.default().filter((child) => child.type.name === "Tab")
}
}
)
I had the same problem, and after doing so much research and asking myself why they had removed $children, I discovered that they created a better and more elegant alternative.
It's about Dynamic Components. (<component: is =" currentTabComponent "> </component>).
The information I found here:
https://v3.vuejs.org/guide/component-basics.html#dynamic-components
I hope this is useful for you, greetings to all !!
I found this updated Vue3 tutorial Building a Reusable Tabs Component with Vue Slots very helpful with explanations that connected with me.
It uses ref, provide and inject to replace this.tabs = this.$children; with which I was having the same problem.
I had been following the earlier version of the tutorial for building a tabs component (Vue2) that I originally found Creating Your Own Reusable Vue Tabs Component.
With script setup syntax, you can use useSlots: https://vuejs.org/api/sfc-script-setup.html#useslots-useattrs
<script setup>
import { useSlots, ref, computed } from 'vue';
const props = defineProps({
perPage: {
type: Number,
required: true,
},
});
const slots = useSlots();
const amountToShow = ref(props.perPage);
const totalChildrenCount = computed(() => slots.default()[0].children.length);
const childrenToShow = computed(() => slots.default()[0].children.slice(0, amountToShow.value));
</script>
<template>
<component
:is="child"
v-for="(child, index) in childrenToShow"
:key="`show-more-${child.key}-${index}`"
></component>
</template>
A per Vue documentation, supposing you have a default slot under Tabs component, you could have access to the slot´s children directly in the template like so:
// Tabs component
<template>
<div v-if="$slots && $slots.default && $slots.default()[0]" class="tabs-container">
<button
v-for="(tab, index) in getTabs($slots.default()[0].children)"
:key="index"
:class="{ active: modelValue === index }"
#click="$emit('update:model-value', index)"
>
<span>
{{ tab.props.title }}
</span>
</button>
</div>
<slot></slot>
</template>
<script setup>
defineProps({ modelValue: Number })
defineEmits(['update:model-value'])
const getTabs = tabs => {
if (Array.isArray(tabs)) {
return tabs.filter(tab => tab.type.name === 'Tab')
} else {
return []
}
</script>
<style>
...
</style>
And the Tab component could be something like:
// Tab component
<template>
<div v-show="active">
<slot></slot>
</div>
</template>
<script>
export default { name: 'Tab' }
</script>
<script setup>
defineProps({
active: Boolean,
title: String
})
</script>
The implementation should look similar to the following (considering an array of objects, one for each section, with a title and a component):
...
<tabs v-model="active">
<tab
v-for="(section, index) in sections"
:key="index"
:title="section.title"
:active="index === active"
>
<component
:is="section.component"
></component>
</app-tab>
</app-tabs>
...
<script setup>
import { ref } from 'vue'
const active = ref(0)
</script>
Another way is to make use of useSlots as explained in Vue´s documentation (link above).
Based on the answer of #Urkle:
/**
* walks a node down
* #param vnode
* #param cb
*/
export function walk(vnode, cb) {
if (!vnode) return;
if (vnode.component) {
const proxy = vnode.component.proxy;
if (proxy) cb(vnode.component.proxy);
walk(vnode.component.subTree, cb);
} else if (vnode.shapeFlag & 16) {
const vnodes = vnode.children;
for (let i = 0; i < vnodes.length; i++) {
walk(vnodes[i], cb);
}
}
}
Instead of
this.$root.$children.forEach(component => {})
write
walk(this.$root, component => {})
Many thanks #Urkle
In 3.x, the $children property is removed and no longer supported. Instead, if you need to access a child component instance, they recommend using $refs. as a array
https://v3-migration.vuejs.org/breaking-changes/children.html#_2-x-syntax

Rendering component on runtime after Http request in VueJS

I'm trying to conditionally render a component in VueJS after an Http request right away when the application starts. If the response is ok I would like to render component 1, otherwise component 2. I would also like to render the component onClick
App.vue
<template>
<div id="app">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="navbar-collapse" id="navbarsExample05">
<ul class="navbar-nav pl-md-5 ml-auto">
<li v-for="tab in tabs" v-bind:key="tab" v-bind:class="['nav-item nav-link', { active: currentTab === tab }]"
v-on:click="currentTab = tab">
{{ tab }}
</li>
</ul>
</div>
</nav>
<component v-bind:is="currentTabComponent" class="tab"></component>
</div>
</template>
<script>
import Comp1 from '../components/comp1'
import Comp2 from '../components/comp2'
export default {
name: 'App',
components: {
Comp1,
Comp2
},
data: function() {
return {
currentTab: 'Comp2',
tabs: ['Comp1', 'Comp2']
};
},
computed:{
currentTabComponent: function () {
function check(){
fetch('someHttpUrl')
.then(response => response.json())
.then(data => {
resolve('Comp1')
});
.catch(err => {
resolve('Comp2')
})
}
var result = check();
result.then(async function (data) {
return data
})
}
}
}
</script>
When I click on the tab, the right component is loaded. But not when the application starts.
Is there any Vue method to render asynchronous a component?
There is no need to have currentTabComponent computed method.
You can just make your HTTP call and update currentTab when it is done.
Something like this:
mounted() {
fetch('someHttpUrl')
.then(response => response.json())
.then(data => {
this.currentTab = 'Comp1'
});
.catch(err => {
this.currentTab = 'Comp2'
})
}
I also removed the method named check as it seemed redundant.