Vue Test Utils - data does not update after triggering click event - javascript

I have this basic test using Vue Test Utils:
import { mount } from '#vue/test-utils'
const App = {
template: `
<p>Count: {{ count }}</p>
<button #click="handleClick">Increment</button>
`,
data() {
return {
count: 0
}
},
methods: {
handleClick() {
this.count += 1
}
}
}
test('it increments by 1', async () => {
const wrapper = mount(App, {
data() {
return {
count: 0
}
}
})
expect(wrapper.html()).toContain('Count: 0')
await wrapper.find('button').trigger('click')
expect(wrapper.html()).toContain('Count: 1')
})
The test only passes if I either
don't send any custom data to the mount method, or
force a re-render, using wrapper.vm.$forceUpdate() after triggering the event.
However, according to the documentation, shouldn't it just pass as it is already written?

The test is fine, in vue2 you have to add a root to the template. Component template should contain exactly one root element.
<div>
<p>Count: {{ count }}</p>
<button #click="handleClick">Increment</button>
</div>

import { mount } from '#vue/test-utils'
const sleep = (ms: number) => {
return new Promise((resolve) => setTimeout(resolve, ms));
}
const App = {
template: `
<p>Count: {{ count }}</p>
<button #click="handleClick">Increment</button>
`,
data() {
return {
count: 0
}
},
methods: {
handleClick() {
this.count += 1
}
}
}
test('it increments by 1', async () => {
const wrapper = mount(App, {
data() {
return {
count: 0
}
}
})
expect(wrapper.html()).toContain('Count: 0')
await wrapper.find('button').trigger('click')
await wrapper.vm.$nextTick();
await sleep(2000);
expect(wrapper.html()).toContain('Count: 1')
})

Related

Apollo MockedProvider not returning expected data

I wrote a hook that calls apollo useQuery. It's pretty simple:
useDecider:
import { useState } from 'react';
import { useQuery, gql } from '#apollo/client';
export const GET_DECIDER = gql`
query GetDecider($name: [String]!) {
deciders(names: $name) {
decision
name
value
}
}
`;
export const useDecider = name => {
const [enabled, setEnabled] = useState(false);
useQuery(GET_DECIDER, {
variables: {
name
},
onCompleted: data => {
const decision = data?.deciders[0]?.decision;
setEnabled(decision);
},
onError: error => {
return error;
}
});
return {
enabled
};
};
I'm trying to test it now and the MockedProvider is not returning the expected data:
import React from 'react';
import { render, screen } from '#testing-library/react';
import '#testing-library/jest-dom';
import { MockedProvider } from '#apollo/client/testing';
import { useDecider, GET_DECIDER } from './useDecider';
const getMock = (value = false, decider = '') => [
{
request: {
query: GET_DECIDER,
variables: {
name: decider
}
},
result: () => {
console.log('APOLLO RESULT');
return {
data: {
deciders: [
{
decision: value,
name: decider,
value: 10
}
]
}
};
}
}
];
const FakeComponent = ({ decider }) => {
const { enabled } = useDecider(decider);
return <div>{enabled ? 'isEnabled' : 'isDisabled'}</div>;
};
const WrappedComponent = ({ decider, value }) => (
<MockedProvider mocks={getMock(value, decider)} addTypename={false}>
<FakeComponent decider={decider} />
</MockedProvider>
);
describe('useDecider', () => {
it('when decider returns true', () => {
// should return true
render(<WrappedComponent decider="fake_decider" value={true} />);
screen.debug();
const result = screen.getByText('isEnabled');
expect(result).toBeInTheDocument();
});
});
I simplified your hook implementation and put together a working example:
import { useQuery, gql } from "#apollo/client";
export const GET_DECIDER = gql`
query GetDecider($name: [String]!) {
deciders(names: $name) {
decision
name
value
}
}
`;
export const useDecider = (name) => {
const { data } = useQuery(GET_DECIDER, { variables: { name } });
return { enabled: data?.deciders[0]?.decision || false };
};
Note that in the test I also updated your getBy to an await findBy:
describe("useDecider", () => {
it("when decider returns true", async () => {
// should return true
render(<WrappedComponent decider="fake_decider" value={true} />);
screen.debug();
const result = await screen.findByText("isEnabled");
expect(result).toBeInTheDocument();
});
});
This is because you need to wait for your API call to complete before the data will be on the page, hence you would not expect the data to be there on the first render.
From https://www.apollographql.com/docs/react/development-testing/testing/#testing-the-success-state
To test how your component is rendered after its query completes, you
can await a zero-millisecond timeout before performing your checks.
This delays the checks until the next "tick" of the event loop, which
gives MockedProvider an opportunity to populate the mocked result
try adding before your expect call
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});

The right way to draw a Map when data is ready

I need to render a map using Mapbox only when data is ready.
I have the following code in my Vuex store:
/store/index.js
import Vue from "vue";
import Vuex from "vuex";
import _ from "lodash";
import { backendCaller } from "src/core/speakers/backend";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
// Activity
activity: [],
geoIps: [],
},
mutations: {
// Activity
setActivity: (state, value) => {
state.activity = value;
},
setGeoIp: (state, value) => {
state.geoIps.push(value);
},
},
actions: {
// Activity
async FETCH_ACTIVITY({ commit, state }, force = false) {
if (!state.activity.length || force) {
await backendCaller.get("activity").then((response) => {
commit("setActivity", response.data.data);
});
}
},
async FETCH_GEO_IPS({ commit, getters }) {
const geoIpsPromises = getters.activityIps.map(async (activityIp) => {
return await Vue.prototype.$axios
.get(
`http://api.ipstack.com/${activityIp}?access_key=${process.env.IP_STACK_API_KEY}`
)
.then((response) => {
return response.data;
});
});
geoIpsPromises.map((geoIp) => {
return geoIp.then((result) => {
commit("setGeoIp", result);
});
});
},
},
getters: {
activityIps: (state) => {
return _.uniq(state.activity.map((activityRow) => activityRow.ip));
},
},
strict: process.env.DEV,
});
In my App.vue I fetch all APIs requests using an async created method.
App.vue:
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
name: "App",
async created() {
await this.$store.dispatch("FETCH_ACTIVITY");
await this.$store.dispatch("FETCH_GEO_IPS");
},
};
</script>
In my Dashboard component I have a conditional rendering to draw the maps component only when geoIps.length > 0
Dashboard.vue:
<template>
<div v-if="geoIps.length > 0">
<maps-geo-ips-card />
</div>
</template>
<script>
import mapsGeoIpsCard from "components/cards/mapsGeoIpsCard";
export default {
name: "dashboard",
components: {
mapsGeoIpsCard,
},
computed: {
activity() {
return this.$store.state.activity;
},
activityIps() {
return this.$store.getters.activityIps;
},
geoIps() {
return this.$store.state.geoIps;
},
};
</script>
Then I load the Maps component.
<template>
<q-card class="bg-primary APP__card APP__card-highlight">
<q-card-section class="no-padding no-margin">
<div id="map"></div>
</q-card-section>
</q-card>
</template>
<script>
import "mapbox-gl/dist/mapbox-gl.css";
import mapboxgl from "mapbox-gl/dist/mapbox-gl";
export default {
name: "maps-geo-ips-card",
computed: {
geoIps() {
return this.$store.state.geoIps;
},
},
created() {
mapboxgl.accessToken = process.env.MAPBOX_API_KEY;
},
mounted() {
const mapbox = new mapboxgl.Map({
container: "map",
center: [0, 15],
zoom: 1,
});
this.geoIps.map((geoIp) =>
new mapboxgl.Marker()
.setLngLat([geoIp.longitude, geoIp.latitude])
.addTo(mapbox)
);
},
};
</script>
<style>
#map {
height: 500px;
width: 100%;
border-radius: 25px;
overflow: hidden;
}
</style>
The problem is that when the function resolves the first IP address, the map is drawn showing only one address and not all the others like this:
What is the best way to only draw the map when my FETCH_GEO_IPS function has finished?
Thanks in advance
I think the answer lies in this bit of code:
geoIpsPromises.map((geoIp) => {
return geoIp.then((result) => {
commit("setGeoIp", result);
});
});
Your map function loops through every element of the array and commits each IP one by one. So when the first one is committed, your v-if="geoIps.length > 0" is true.
A workaround would be to set a flag only when the IPs are set.
This is a proposed solution:
import Vue from "vue";
import Vuex from "vuex";
import _ from "lodash";
import { backendCaller } from "src/core/speakers/backend";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
// Activity
activity: [],
geoIps: [],
isReady: false
},
mutations: {
// Activity
setActivity: (state, value) => {
state.activity = value;
},
setGeoIp: (state, value) => {
state.geoIps.push(value);
},
setIsReady: (state, value) => {
state.isReady = value;
}
},
actions: {
// Activity
async FETCH_ACTIVITY({ commit, state }, force = false) {
if (!state.activity.length || force) {
await backendCaller.get("activity").then((response) => {
commit("setActivity", response.data.data);
});
}
},
async FETCH_GEO_IPS({ commit, getters }) {
let tofetch = getters.activityIps.length; // get the number of fetch to do
const geoIpsPromises = getters.activityIps.map(async (activityIp) => {
return await Vue.prototype.$axios
.get(
`http://api.ipstack.com/${activityIp}?access_key=${process.env.IP_STACK_API_KEY}`
)
.then((response) => {
return response.data;
});
});
geoIpsPromises.map((geoIp) => {
return geoIp.then((result) => {
commit("setGeoIp", result);
toFetch -= 1; // decrement after each commit
if (toFetch === 0) {
commit("setIsReady", true); // all commits are done
}
});
});
},
},
getters: {
activityIps: (state) => {
return _.uniq(state.activity.map((activityRow) => activityRow.ip));
},
},
strict: process.env.DEV,
});
And in your view:
<template>
<div v-if="isReady">
<maps-geo-ips-card />
</div>
</template>
<script>
import mapsGeoIpsCard from "components/cards/mapsGeoIpsCard";
export default {
name: "dashboard",
components: {
mapsGeoIpsCard,
},
computed: {
activity() {
return this.$store.state.activity;
},
activityIps() {
return this.$store.getters.activityIps;
},
isReady() {
return this.$store.state.isReady;
},
};
</script>

Unable to write a snapshot test of a component that is calling a moment method within the render

I am trying to test the following component:
import React, { Component } from 'react';
import { connect } from 'react-redux'
import moment from 'moment'
import { SingleDatePicker } from 'react-dates'
import MealList from './MealList';
import { capitalize } from '../helpers/helpers'
export class MealSummary extends Component {
state = {
date: moment(),
calendarFocused: false
}
onDateChange = date => {
if (date) this.setState({ date })
}
onFocusChange = ({ focused }) => this.setState({ calendarFocused: focused })
renderMealCategory = (mealCategory) => {
return this.props.meals.filter(meal => meal.mealCategory === mealCategory && meal.date.isSame(this.state.date, 'day'))
}
renderFilteredTotal = (mealCategory) => {
return this.props.meals.filter(meal => meal.mealCategory === mealCategory && meal.date.isSame(this.state.date, 'day'))
.reduce((sum, n) => sum + n.calories, 0)
}
renderTotal = () => (this.props.meals.filter(meal => meal.date.isSame(this.state.date, 'day')).reduce((sum, n) => sum + n.calories, 0))
renderMeals = () => {
const categories = ['breakfast', 'lunch', 'dinner', 'snack']
return categories.map(category => {
return (
<div key={category}>
<h1>{capitalize(category)}</h1>
<MealList meals={this.renderMealCategory(category)} />
<h4>Total: {this.renderFilteredTotal(category)}</h4>
</div>
)
})
}
render() {
return (
<div>
<h1>Summary Page</h1>
<SingleDatePicker
date={this.state.date}
onDateChange={this.onDateChange}
focused={this.state.calendarFocused}
onFocusChange={this.onFocusChange}
numberOfMonths={1}
isOutsideRange={() => false}
id="caloriEat-meal-summary" />
{this.renderMeals()}
<div>
<h1>Total Calories Consumed: {this.renderTotal()}</h1>
</div>
</div>
);
}
}
const mapStateToProps = (state) => ({
meals: state.meals
})
export default connect(mapStateToProps)(MealSummary)
I have written the following failing test:
import React from 'react';
import { shallow } from 'enzyme'
import { MealSummary } from '../../components/MealSummary'
import { meals } from '../fixtures/mealReducer'
describe('<MealSummary />', () => {
test('should render MealSummary', () => {
const renderMealCategory = jest.fn()
const wrapper = shallow(<MealSummary meals={meals} />)
expect(wrapper).toMatchSnapshot()
})
})
I receive the error:
TypeError: meal.date.isSame is not a function
17 |
18 | renderMealCategory = (mealCategory) => {
> 19 | return this.props.meals.filter(meal => meal.mealCategory === mealCategory && meal.date.isSame(this.state.date, 'day'))
| ^
20 | }
21 |
22 | renderFilteredTotal = (mealCategory) => {
The object meals has a meals.date property of which a moment method isSame is called on to compare the date within the state. I have quite a few of these functions within separate calls to render the page correctly. Why is the function not being recognized? I tried passing a mock function for isSame const isSame = jest.fn() and passed it as a prop to MealSummary, but it didn't work. How can I get this test to work correctly with a method that runs from a different (moment) library, so that I can get it to create a snapshot?
You're passing in meals from import { meals } from '../fixtures/mealReducer', we would need to see what is in the reducer, but I am guessing you have something like:
const meals = [{ mealCategory: 'breakfast', date: new Date()}]
when you should have
const meals = [{ mealCategory: 'breakfast', date: moment()}]

Component in use in multiple places all are being updated at the same time

I am new still to Vue I have a component that is used to accept and display files when they are dragged & Dropped onto the component. According to the documentation if I have my attributes in a data function then I can use this component in multiple places and have them update independently of the other component.
This is not the case when I test the component in my add screen the component in the view screen is still updated with the same data. I cannot figure out why this is happening so was wondering if I could get some fresh eyes
to take a look or point me in the right direction on how I can resolve this issue.
this is my ImageViewer code
<template>
<div id="image-panel"
#dragenter="allowDrag"
#mouseenter="toggleSingleViewActionbar('show')"
#mouseleave="toggleSingleViewActionbar('hide')">
<div id="dragDropOverlay" class="drop display-inline align-center"
:class="[isOverlay ? 'overlay' : '']"
#dragleave="retainOverlay"
#dragover.prevent
#drop="onDrop">
<div class="overlay-text">Drag files here</div>
</div>
<single-view v-if="display === 'single'"
:file-list-length="this.fileList.length === 0 ? 0 : this.fileList.length - 1"
:current-position="currentPosition"
:display-type="displayType"
v-on:addToFileList="updateFileList"/>
<grid-view v-else
:file-list-length="this.fileList.length === 0 ? 0 : this.fileList.length - 1"
:current-position="currentPosition"/>
<app-image-section-single-view
v-if="display === 'single'"
:fileList="fileList"
:currentPosition="currentPosition"
:is-overlay="isOverlay"
/>
<app-image-section-grid-view v-else :file-list="fileList"/>
<snack-bar v-if="deleteConfirmation" message="Are you sure you want to delete this file?"></snack-bar>
</div>
</template>
<script>
import ImageSectionSingleView from './ImageSectionSingleView'
import ImageSectionGridView from './ImageSectionGridView.vue'
import { eventBus } from '#/main'
import Singleview from '../Actionbar/Singleview'
import Gridview from '../Actionbar/Gridview'
import SnackBar from '../SnackBar/SnackBar'
import { states } from '../enums/enums'
export default {
data () {
return {
fileList: [],
currentPosition: 0,
display: 'single',
displayType: '',
isOverlay: false,
deleteConfirmation: false
}
},
watch: {
fileList () {
eventBus.$emit('updateFileList', this.fileList)
}
},
methods: {
onDrop (e) {
e.stopPropagation()
e.preventDefault()
if (!this.isOverlay) return
this.updateFileList(e.dataTransfer.files)
this.isOverlay = false
},
allowDrag () {
this.isOverlay = this.$store.getters.appState !== states.view
},
retainOverlay (event) {
if (!this.isOverlay) return
this.isOverlay = !!event.relatedTarget.closest('#dragDropOverlay')
},
getFileList () {
return this.$store.getters.getFileList
},
updateFileList (files) {
this.fileList.push.apply(this.fileList, (
[...files].map((f) => ({
name: f.name,
data: URL.createObjectURL(f),
type: f.type
}))))
this.currentPosition = this.fileList.length - 1
},
getCurrentPosition () {
return this.$store.getters.getCurrentPosition
},
// updateCurrentPosition (position) {
// this.$store.commit('updateCurrentPosition', position)
// },
toggleSingleViewActionbar (action = '') {
this.displayType = action
},
deleteImage (index = -1) {
if (index === -1) {
index = this.currentPosition
}
this.fileList.splice(index, 1)
if (index < this.fileList.length) return
if (this.currentPosition > 0) {
this.currentPosition--
}
},
deleteSelected (indexes) {
for (let i = 0; i < indexes.length; i++) {
this.deleteImage(indexes[i])
}
this.fileList.map((file) => {
file.isVisible = false
})
}
},
created () {
this.fileList = this.getFileList()
this.currentPosition = this.getCurrentPosition()
eventBus.$on('deleteSelectedFiles', (indexes) => {
this.deleteSelected(indexes.sort().reverse())
if (this.fileList.length === 0) {
this.currentPosition = 0
this.display = 'single'
}
})
eventBus.$on('setSelectedFiles', (state) => {
this.fileList.map((file) => {
file.isVisible = state
})
})
eventBus.$on('moveToNextFile', (positionNext) => {
this.currentPosition = positionNext++
})
eventBus.$on('moveToPreviousFile', (positionPrevious) => {
this.currentPosition = positionPrevious--
})
eventBus.$on('confirmDelete', () => {
eventBus.$emit('singleDeleteConfirmation', () => {
})
})
eventBus.$on('confirmationYes', () => {
this.deleteImage()
eventBus.$emit('singleDeleteSnackbarClose')
})
eventBus.$on('confirmationNo', () => {
eventBus.$emit('singleDeleteSnackbarClose')
})
eventBus.$on('switchView', (value) => {
this.display = value
})
eventBus.$on('singleDeleteConfirmation', () => {
this.deleteConfirmation = !this.deleteConfirmation
})
eventBus.$on('singleDeleteSnackbarClose', () => {
this.deleteConfirmation = false
})
},
components: {
appImageSectionSingleView: ImageSectionSingleView,
appImageSectionGridView: ImageSectionGridView,
singleView: Singleview,
gridView: Gridview,
SnackBar: SnackBar
}
}
</script>
and this is where the image/file is displayed
<template>
<div class="display-inline">
<img #dragenter="isOverlay=true" v-if="fileList.length > 0" :src="fileList[currentPosition].data" class="img" />
<img v-else src="../../../src/assets/logo.png" class="img" />
</div>
</template>
<script>
export default {
props: {
fileList: Array,
currentPosition: Number,
fileListLength: Number,
isOverlay: Boolean
}
}
</script>
How can I get it so that It is independently displaying in each of my sections where the component is called ?
Initially the data properties are all independent but then you're assigning this.fileList = this.getFileList(), which is grabbing an array from the store. All components will be sharing that same array in their fileList property. - Comment by skirtle

React + TS: How to call a method from outside of a React Functional Component

Im wondering how I can call a method from outside of a React Functional Component. I wrote the function GetUsedLockers() which gets all the used lockers and returns amount. Now I want to call this function from another another component (OrgLocker.tsx) and display the data from the getUsedLockers() function there.
OrgLockerTables.tsx
const OrgLockerTables: React.FC = () => {
const lockerCall = 'lockers';
const [lockerData, setLockerData] = useState({
id: 0,
guid: "",
is_currently_claimable: false
}[""]);
useEffect(() => {
componentConsole().then((res) => {
setLockerData(res);
})
// eslint-disable-next-line
}, []);
if (!lockerData) return (<div>Loading...</div>);
//function to get all used lockers
function getUsedLockers() {
let amount = 0;
for (let i = 0; i < lockerData.length; i++) {
if (!lockerData.is_currently_claimable) {
amount++;
}
}
console.log('log from getusedlockers, amount: ', amount)
return (amount)
}
// function to get JSON data from the API
function componentConsole(): Promise<any> {
return new Promise<any>((resolve, reject) => {
http.getRequest('/' + lockerCall).then((res) => {
let data = res.data.data;
console.log('data:', data);
resolve(res.data.data);
}).catch((error) => {
console.log(error);
reject();
});
})
}
}
OrgLocker.tsx
import OrgLockerTables from '../tables/orgLockerTables';
const OrgLockers: React.FC = () => {
let lockerTable = new OrgLockerTables();
return (
<div className="main-div-org">
<p>Used</p>
<p>{lockerTable.getUsedLockers()}</p>
</div>
);
}
export default OrgLockers;
When trying to make a call to OrgLockerTables and storing it in the lockerTable let it gives the following error:
Expected 1-2 arguments, but got 0.ts(2554)
Any help would be greatly appreciated!
I've restructured everything making it more understandable, I hope you don't mind according to what I think you want the comment above.
locker-model.ts - The type for the particular data being called back is found
export type Locker = {
id: number;
guid: string;
isCurrentlyClaimable: boolean;
}
locker-business.ts - Where all the business logic is carried out, from the call for data to the calculation based on it
import { Locker } from "./locker-models";
const lockerCall = 'lockers';
const mockedData: Locker[] = [{
id: 0,
guid: "sample",
isCurrentlyClaimable: false,
},
{
id: 1,
guid: "sample2",
isCurrentlyClaimable: true,
},
{
id: 2,
guid: "sample3",
isCurrentlyClaimable: true,
}]
// Mocked function from your backend (componentConsole where you use lockerCall variable)
export const getLockersData = (): Promise<Locker[]> => Promise.resolve(mockedData);
export const getAmount = (lockers: Locker[]): number => {
let amount = 0;
!!lockers ?
lockers.filter(({isCurrentlyClaimable}) => { if(isCurrentlyClaimable) amount++ })
: 0;
return amount;
};
index.tsx - Here are both components that make the call to get the data and render the result you're looking for
import React, { Component } from 'react';
import { Locker } from './locker-models';
import { getLockersData, getAmount } from './locker-business';
import './style.css';
type OrgLockersProps = {
amount: number;
}
const OrgLockers: React.FC<OrgLockersProps> = ({ amount }) => {
return (
<div className="main-div-org">
<p>Lockers used:</p>
<p>{amount}</p>
</div>
);
}
type OrgLockerTableProps = {};
const OrgLockerTable : React.FC<OrgLockerTableProps> = props => {
const [lockerData, setLockerData] = React.useState<Locker[]>([]);
React.useEffect(() => {
getLockersData().then(response => setLockerData(response));
}, []);
const amount = getAmount(lockerData);
return (
<div>
<OrgLockers amount={amount} />
</div>
);
};
You can see the example here
You can create new .js file like Helpers.js and define export function with parameter it like that
export function getUsedLockers(lockerData) {
let amount = 0;
//Check your loop it can be like that
for (let i = 0; i < lockerData.length; i++) {
if (!lockerData[i].is_currently_claimable) {
amount++;
}
}
console.log('log from getusedlockers, amount: ', amount)
return (amount)
}
Then import it where do you want to use.
import {getUsedLockers} from "../Helpers";
And use it like that:
const amount = getUsedLockers(data);

Categories