React Testing Library / Jest Update not wrapped in act(...) - testing

I've been getting this error message regarding act while testing.
Warning: An update to EmployeesDashboard inside a test was not
wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
in EmployeesDashboard (at EmployeesDashboard.test.tsx:69)"
I can't figure out why, since it seems I've already wrapped everything in act... What in here should I also be wrapping in act? Any help would be greatly appreciated since I'm fairly new to testing.
import React from 'react';
import { mocked } from 'ts-jest/utils';
import { act } from 'react-dom/test-utils';
import { EmployeesDashboard } from '../EmployeesDashboard';
import {
render,
wait,
waitForElementToBeRemoved,
} from '#testing-library/react';
import { Employee } from '#neurocann-ui/types';
import { getAll } from '#neurocann-ui/api';
jest.mock('#neurocann-ui/api');
const fakeEmployees: Employee[] = [
{
email: 'joni#gmail.com',
phone: '720-555-555',
mailingAddress: {
type: 'home',
streetOne: '390 Blake St',
streetTwo: 'Apt 11',
city: 'Denver',
state: 'CO',
zip: '90203',
},
name: 'Joni Baez',
permissions: ['Employee'],
hireDate: new Date('2020-09-19T05:13:12.923Z'),
},
{
email: 'joni22#gmail.com',
phone: '720-555-555',
mailingAddress: {
type: 'home',
streetOne: '390 Blake St',
streetTwo: 'Apt 11',
city: 'Denver',
state: 'CO',
zip: '90203',
},
name: 'Joni Baez',
permissions: ['Employee'],
hireDate: new Date('2020-09-19T05:13:12.923Z'),
},
];
mocked(getAll).mockImplementation(
(): Promise<any> => {
return Promise.resolve({ data: fakeEmployees });
},
);
describe('EmployeesDashboard', () => {
it('renders progress loader', () => {
act(() => {
render(<EmployeesDashboard />)
});
const employeesDashboard = document.getElementById(
'simple-circular-progress',
);
expect(employeesDashboard).toBeInTheDocument();
expect(employeesDashboard).toMatchSnapshot();
});
it('renders a table', async () => {
await act(() => { render(<EmployeesDashboard />) });
const table = document.getElementById('employees-table');
expect(table).toBeInTheDocument();
});
});

It usually means that you need to wait for something else to show up before expecting the EmployeesDashboard to be in the document. Wait for something else first, maybe the page's header or title. Try:
it('renders progress loader', async () => {
const {findByText} = render(<EmployeesDashboard />)
await findByText("something else");
const employeesDashboard = document.getElementById(
'simple-circular-progress',
);
expect(employeesDashboard).toBeInTheDocument();
expect(employeesDashboard).toMatchSnapshot();
});

Try adding a timer to your mock.
Instead of:
mocked(getAll).mockImplementation(
(): Promise<any> => {
return Promise.resolve({ data: fakeEmployees });
},
);
Try:
mocked(getAll).mockImplementation(() => {
return new Promise(resolve => {
setTimeout(() => resolve(123), 0);
});
});

Related

Jest: How I should change the mock data of Vuex in each test?

I've been working in a test where I need the data from Vuex. However, I'm having some problems, I need to change that data in each test in order to test the functionality of the component.
Here is my component:
<template>
<div id="cb-items-displayer" #click="textClick">
<span>(</span>
<p>{{ text }}</p>
<span>)</span>
</div>
</template>
<script lang="ts" setup>
import { capitalize } from '#/utils/capitalize'
import { ItemsDisplayer } from '#/models/ItemsDisplayer'
import { computed, PropType } from 'vue'
import { useStore } from 'vuex'
const store = useStore()
const props = defineProps({
type: {
type: String,
default: '',
},
menuType: {
type: String,
default: '',
},
items: {
type: Array as PropType<ItemsDisplayer[]>,
default: () => [],
}
})
const emit = defineEmits<{
(event: 'textClicked'): void
}>()
const text = computed(() => {
const param = props.menuType === 'radio' ? 'One' : 'Many'
console.log( "TYPEEE ", props.type, " ", param )
const itemsIds = store.getters['filters/get' + capitalize(props.type) + param]
console.log("ITEMSSS", JSON.stringify(itemsIds))
return getTextToShow(itemsIds)
})
const getTextToShow = (itemsIds: string) => {
//TODO - improve it
if (itemsIds === 'all') {
return 'all'
} else if (itemsIds.length === 0) {
return '-'
} else if (itemsIds.length === 1) {
return getName(itemsIds[0], props.items)
} else {
return itemsIds.length
}
}
const textClick = () => {
emit('textClicked')
}
const getName = (id: string, items: ItemsDisplayer[]) => {
const found: ItemsDisplayer = items.find((x) => x.id! === id) as ItemsDisplayer
console.log("GETNAME ", found.name)
return found?.name
}
</script>
And this is the test:
import { render, screen, click, waitFor } from '#tests/app-test-utils'
import ItemsDisplayer from './ItemsDisplayer.vue'
import { capitalize } from '#/utils/capitalize'
let mockStoreCommit: jest.Mock
jest.mock('vuex', () => ({
...jest.requireActual('vuex'),
useStore: () => ({
getters: {
[`filters/get${capitalize('categories')}Many`]: [],
},
commit: mockStoreCommit,
}),
}))
describe('ItemsDisplayer', () => {
beforeEach(() => {
mockStoreCommit = jest.fn()
render(
ItemsDisplayer,
{},
{
props: {
type: 'categories',
menuType: 'checkbox',
items: [
{
box_templates:"",
id:"1",
name:"Culture"
},
{
box_templates:"",
id:"2",
name:"Economy"
},
{
box_templates:"",
id:"3",
name:"Education"
}
]},
}
)
})
it('renders the component', async() => {
await screen.getByText('-')
})
it('renders the component with one item', async() => {
//DON'T WORK HERE THERE SHOULD BE A CHANGE OF DATA IN THE MOCKED STORE IN ORDER TO WORK
await screen.getByText('Culture')
})
})
My problem is that I need to change the value of [filters/get${capitalize('categories')}Many] to ["1"] in the second test.
I tried several things in order to change the mocked data but they don't work. How can I change the mocked store data in each test?
Thanks!
You can achieve this by lazy loading your vue component:
Add jest.resetModules(); in the beforeEach to reset all of the imported modules before each test so they can be re-evaluated and re-mocked:
beforeEach(() => {
jest.resetModules();
In each unit test, you will first need to import the vue component using the require syntax as follows:
const ItemsDisplayer = require('./ItemsDisplayer.vue').default;
Then add the mock directly after the import with the [`filters/get${capitalize('categories')}Many`] value being set to whatever you want it to be:
jest.mock('vuex', () => ({
...jest.requireActual('vuex'),
useStore: () => ({
getters: {
[`filters/get${capitalize('categories')}Many`]: ["1"],
},
commit: mockStoreCommit,
}),
}));
I have noticed that you do your rendering in the beforeEach. Unfortunately because you import and mock your modules during the test, the rendering will need to be done after these have taken place - hence you will need to either move that logic into your unit test or extract it into another function which can be called from within the unit test.
Each unit test should look something like this:
it('renders the component', async() => {
const ItemsDisplayer = require('./ItemsDisplayer.vue').default;
jest.mock('vuex', () => ({
...jest.requireActual('vuex'),
useStore: () => ({
getters: {
[`filters/get${capitalize('categories')}Many`]: ["1"],
},
commit: mockStoreCommit,
}),
}));
// beforeEach logic here or a call to a function that contains it
await screen.getByText('-')
})

Testing AsyncStorage with Redux Thunk

When I try to test if a Cat is added correctly to the Async Storage I am receiving null. How can I solve this? In the app is working correctly. I am using React Native, React-Native-Async-Storage, Redux Thunk and Redux Persist.
REDUCER
export const INITIAL_STATE: CatsState = {
cats: [],
selectedCat: null
}
const catsReducer = (state = INITIAL_STATE, action: CatsAction): CatsState => {
switch (action.type) {
case CatsActionTypes.SET_CATS:
return { ...state, cats: action.payload }
case CatsActionTypes.SELECT_CAT:
return { ...state, selectedCat: action.payload }
default:
return state
}
}
ACTION
export const addCat = ({ cat }: AddCatData): ThunkAction<void, RootState, null, CatsAction> => {
return async (dispatch: Dispatch<CatsAction>) => {
try {
const response = await AsyncStorage.getItem(STORAGE_KEYS.cats)
const cats: Cat[] = response ? JSON.parse(response) : []
cats.push(cat)
await AsyncStorage.setItem(STORAGE_KEYS.cats, JSON.stringify(cats))
dispatch({ type: CatsActionTypes.SET_CATS, payload: cats })
} catch (error) {
console.log(error)
}
}
}
TEST
beforeEach(async () => await AsyncStorage.clear())
describe('add cat', () => {
it('persist cat correctly into local storage', async () => {
const cat: Cat = {
id: '1',
name: 'Cat',
breed: 'Breed',
age: 1,
gender: 'male',
weight: 5,
size: 'Big',
color: 'Brown',
mood: 'Angry',
description: 'Cat description',
friendly: 3,
liked: true
}
addCat({ cat })
const response = await AsyncStorage.getItem(STORAGE_KEYS.cats)
const cats: Cat[] = JSON.parse(response)
const expectedArray = { cats: [cat] }
expect(cats).toStrictEqual(expectedArray)
})
})
Thank you
Use official documentation react-native-async-storage package: https://react-native-async-storage.github.io/async-storage/docs/advanced/jest
UPDATED:
For you case you need do several steps:
#1 Mock your data
const CAT_MOCK = {
id: '1',
name: 'Cat',
}
#2 Mock Storage
const STORAGE_MOCK = {
[STORAGE_KEYS.cats]: [CAT_MOCK]
}
#3 Mock library for your case
jest.mock('#react-native-async-storage', () => ({
getItem: jest.fn((item) => {
return new Promise((resolve, reject) => {
resolve(STORAGE_MOCK[item]);
});
}),
}));

Cannot read property state

I try to test this action:
const getGameList = function(context) {
if(context.state.user.id){
let request_body = {
user_id : context.state.user.id
}
axios.post(`api/game_list_of_user`,request_body).then(response => {
context.commit('UpdateGameList',response.data);
}).catch(error => console.log(error));
}
};
My action is to get the list of game for a specific user.
This action has:
as input my user id .
as output my game of list.
My test:
import actions from '#/store/actions'
import state from '#/store/state'
import store from '#/store'
import axios from 'axios'
jest.mock('axios');
describe('getGameList', () => {
test('Success: should return the game list of the user and update gameList in the store', () => {
const state = { user: {id: 1} };
const mockFunction = jest.fn();
const response = {
data: [
{ id:1, name:"game_name1" },
{ id:2, name:"game_name2" }
]
};
axios.post.mockResolvedValue(response);
actions.getGameList({ mockFunction },{ state });
//expect(mockFunction).toHaveBeenCalledTimes(1);
//expect(mockFunction).toHaveBeenCalledWith('UpdateGameList',response.data);
});
test('Error: an error occurred', () => {
const errorMessage = 'Error';
axios.get.mockImplementationOnce(() =>
Promise.reject(new Error(errorMessage))
);
});
});
I declare my state (with my user id).
I declare my expected response
from my request (the game list = response.data).
I use jest.fn() to mock the function. (Should I do that ?)
I got this error:
I want to check:
My request has been called
The response of my request matches with my expected response
My mutation is then called
How can I solve that error?
Edit1: my test
jest.mock('axios');
describe('getGameList', () => {
test('Success: should return the game list of the user and update gameList in the store', () => {
const context = {
state : {
user: {
id: 1
}
}
};
const mockFunction = jest.fn();
const response = {
data: [
{ id:1, name:"game_name1" },
{ id:2, name:"game_name2" }
]
};
axios.post.mockResolvedValue(response);
actions.getGameList({ mockFunction, context });
expect({ mockFunction, context }).toHaveBeenCalledTimes(1);
expect(mockFunction).toHaveBeenCalledWith('UpdateGameList',response.data);
});
test('Error: an error occurred', () => {
const errorMessage = 'Error';
axios.get.mockImplementationOnce(() =>
Promise.reject(new Error(errorMessage))
);
});
});
this is my solution:
import actions from '#/store/actions'
import mutations from '#/store/mutations'
import state from '#/store/state'
import store from '#/store'
import axios from 'axios'
let url = ''
let body = {}
jest.mock("axios", () => ({
post: jest.fn((_url, _body) => {
return new Promise((resolve) => {
url = _url
body = _body
resolve(true)
})
})
}))
//https://medium.com/techfides/a-beginner-friendly-guide-to-unit-testing-the-vue-js-application-28fc049d0c78
//https://www.robinwieruch.de/axios-jest
//https://lmiller1990.github.io/vue-testing-handbook/vuex-actions.html#testing-actions
describe('getGameList', () => {
test('Success: should return the game list of the user and update gameList in the store', async () => {
const context= {
state: {
user: {
id:1
}
},
commit: jest.fn()
}
const response = {
data: [
{ id:1, name:"game_name1" },
{ id:2, name:"game_name2" }
]
};
axios.post.mockResolvedValue(response) //OR axios.post.mockImplementationOnce(() => Promise.resolve(response));
await actions.getGameList(context)
expect(axios.post).toHaveBeenCalledWith("api/game_list_of_user",{"user_id":1});
expect(axios.post).toHaveBeenCalledTimes(1)
expect(context.commit).toHaveBeenCalledWith("UpdateGameList", response.data)
});
test('Error: an error occurred', () => {
const errorMessage = 'Error';
axios.post.mockImplementationOnce(() =>
Promise.reject(new Error(errorMessage))
);
});
});

Nuxt asyncData result is undefined if using global mixin head() method

I'm would like to get titles for my pages dynamically in Nuxt.js in one place.
For that I've created a plugin, which creates global mixin which requests title from server for every page. I'm using asyncData for that and put the response into storage, because SSR is important here.
To show the title on the page I'm using Nuxt head() method and store getter, but it always returns undefined.
If I place this getter on every page it works well, but I would like to define it only once in the plugin.
Is that a Nuxt bug or I'm doing something wrong?
Here's the plugin I wrote:
import Vue from 'vue'
import { mapGetters } from "vuex";
Vue.mixin({
async asyncData({ context, route, store, error }) {
const meta = await store.dispatch('pageMeta/setMetaFromServer', { path: route.path })
return {
pageMetaTitle: meta
}
},
...mapGetters('pageMeta', ['getTitle']),
head() {
return {
title: this.getTitle, // undefined
// title: this.pageMetaTitle - still undefined
};
},
})
I would like to set title in plugin correctly, now it's undefined
Update:
Kinda solved it by using getter and head() in global layout:
computed: {
...mapGetters('pageMeta', ['getTitle']),
}
head() {
return {
title: this.getTitle,
};
},
But still is there an option to use it only in the plugin?
Update 2
Here's the code of setMetaFromServer action
import SeoPagesConnector from '../../connectors/seoPages/v1/seoPagesConnector';
const routesMeta = [
{
path: '/private/kredity',
dynamic: true,
data: {
title: 'TEST 1',
}
},
{
path: '/private/kreditnye-karty',
dynamic: false,
data: {
title: 'TEST'
}
}
];
const initialState = () => ({
title: 'Юником 24',
description: '',
h1: '',
h2: '',
h3: '',
h4: '',
h5: '',
h6: '',
content: '',
meta_robots_content: '',
og_description: '',
og_image: '',
og_title: '',
url: '',
url_canonical: '',
});
export default {
state: initialState,
namespaced: true,
getters: {
getTitle: state => state.title,
getDescription: state => state.description,
getH1: state => state.h1,
},
mutations: {
SET_META_FIELDS(state, { data }) {
if (data) {
Object.entries(data).forEach(([key, value]) => {
state[key] = value;
})
}
},
},
actions: {
async setMetaFromServer(info, { path }) {
const routeMeta = routesMeta.find(route => route.path === path);
let dynamicMeta;
if (routeMeta) {
if (!routeMeta.dynamic) {
info.commit('SET_META_FIELDS', routeMeta);
} else {
try {
dynamicMeta = await new SeoPagesConnector(this.$axios).getSeoPage({ path })
info.commit('SET_META_FIELDS', dynamicMeta);
return dynamicMeta && dynamicMeta.data;
} catch (e) {
info.commit('SET_META_FIELDS', routeMeta);
return routeMeta && routeMeta.data;
}
}
} else {
info.commit('SET_META_FIELDS', { data: initialState() });
return { data: initialState() };
}
return false;
},
}
}

How can I test data returned from an ajax call in mounted is correctly rendered?

I have a component (simplified)
<template>
<div v-if="account">
<h1 v-text="accountName"></h1>
</div>
</template>
<script>
import repo from '../../repo';
export default {
data() {
return {
account: {}
}
},
mounted() {
return this.load();
},
computed: {
accountName: function () {
return this.account.forename + ' ' + this.account.surname;
}
},
methods: {
load() {
return repo
.get(repo.accounts, {
params: {
id: this.$route.params.id
}
})
.then((response) => {
console.log(response.data);
this.account = response.data;
this.validateObj = this.account;
}, (error) => {
switch (error.response.status) {
case 403:
this.$router.push({name: '403'});
break;
default:
this.$refs['generic_modal'].open(error.message);
}
});
}
}
}
</script>
Which on mount, calls an API, gets the returned data, and renders the forename and surname.
I'm trying to write a mocha test to check that this works. I can do it using a timeout.
it('mounts', done => {
const $route = {
params: {
id: 1
}
};
mock.onGet('/api/accounts/1').reply(200, {
forename: 'Tom',
surname: 'Hart'
});
const wrapper = shallowMount(AccountsEdit, {
i18n,
mocks: {
$route
}
});
setTimeout(a => {
expect(wrapper.html()).toContain('Tom Hart');
done();
}, 1900);
});
But I wondered is there a better way? I was hoping to hook into the axios.get call, and run the check once that's finished, however, I can't seem to figure out how to do it.
EDIT: I tried using $nextTick, however, that didn't work either
wrapper.vm.$nextTick(() => {
expect(wrapper.html()).toContain('Tom Hart');
done();
});
{ Error: expect(received).toContain(expected) // indexOf
Expected substring: "Tom Hart"
Received string: "<div><h1>undefined undefined</h1></div>"
at VueComponent.<anonymous> (/home/tom/Dev/V6/Admin/.tmp/mocha-webpack/1554202885644/tests/Javascript/Components/Pages/account-edit.spec.js:37:1)
at Array.<anonymous> (/home/tom/Dev/V6/Admin/node_modules/vue/dist/vue.runtime.common.dev.js:1976:12)
at flushCallbacks (/home/tom/Dev/V6/Admin/node_modules/vue/dist/vue.runtime.common.dev.js:1902:14)
matcherResult: { message: [Function: message], pass: false } }
{ forename: 'Tom', surname: 'Hart' }
1) mounts
0 passing (2s)
1 failing
1) Accounts Edit Page
mounts:
Error: Timeout of 2000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. (/home/tom/Dev/V6/Admin/.tmp/mocha-webpack/1554202885644/bundle.js)
EDIT 2: It seems just as a test, chaining $nextTick eventually works, so I guess something else is causing ticks before my call returns? Is there anyway to tell what caused a tick to happen?
wrapper.vm.$nextTick(() => {
wrapper.vm.$nextTick(() => {
wrapper.vm.$nextTick(() => {
wrapper.vm.$nextTick(() => {
wrapper.vm.$nextTick(() => {
wrapper.vm.$nextTick(() => {
expect(wrapper.find('h1').html()).toContain('Tom Hart');
done();
});
});
});
});
});
});
Hey we had similar problem and found this library:
https://www.npmjs.com/package/flush-promises
Which allow to us wait all promises before continue testing.
Try to do something like this:
const flushPromises = require('flush-promises');
it('mounts', (done) => {
const $route = {
params: {
id: 1
}
};
mock.onGet('/api/accounts/1').reply(200, {
forename: 'Tom',
surname: 'Hart'
});
const wrapper = shallowMount(AccountsEdit, {
i18n,
mocks: {
$route
}
});
flushPromises().then(() => {
expect(wrapper.html()).toContain('Tom Hart');
done();
});
});