Unit Testing VueRouter with the :to Attribute - jestjs

I have a simple test case (Vue 2, Vuetify 2, vue-router, jest, vue-test-utils) where a Vuetify v-btn component is clicked and goes to a new route
<v-btn id="create-plan-btn" :to="{ 'name': 'newPlan', query: { dept: 216001 }}">Create plan</v-btn>
However, I am unable to figure out how to register a click method that captures the :to property's action.
describe('when "Create Plan button is clicked', () => {
const localVue = createLocalVue()
let vuetify
beforeEach(() => {
vuetify = new Vuetify()
})
const $route = { path: '/student/index', name: 'studentLanding' }
const mockRouter = {
to: jest.fn(),
push: jest.fn()
}
const wrapper = shallowMount(StudentLanding, {
localVue,
stubs: {'router-link': RouterLinkStub},
mocks: {
$route: $route,
$router: mockRouter
}
})
it('triggers Vue router call to the new plan page', async () => {
const button = wrapper.find('#create-plan-btn')
expect(button.exists()).toBe(true)
expect(mockRouter.push).toHaveBeenCalledTimes(0)
button.vm.$emit('click') // Vuetify buttons need "vm.$emit" to be triggered, not "trigger()" like a normal HTML button would
await wrapper.vm.$nextTick()
expect(mockRouter.push).toHaveBeenCalledTimes(1)
expect(mockRouter.push).toHaveBeenCalledWith({ name: 'newPlan', query: { dept: 216001 }})
})
})
● when "Create Plan button is clicked › triggers Vue router call to
the new plan page
expect(jest.fn()).toHaveBeenCalledTimes(expected)
Expected number of calls: 1
Received number of calls: 0
Note that changing push to to doesn't make any difference in the above code either.
The test does succeed, however, if I change my to use a #click callback instead:
// Test succeeds with this code
<v-btn id="create-plan-btn" #click="redirect">Create plan</v-btn>
redirect () {
this.$router.push({ name: 'newPlan', query: { dept: 216001 }});
}
Is there a way to modify this test that allows the :to prop to be captured on the click? I've tried to at least make sure the content of the :to prop is visible in the test, but I only ever get [object Object] sent back to me:
expect(button.attributes('to').toString()).toEqual({ name: 'newPlan', query: { dept: 216001 }})
Expected: {"name": "newPlan", "query": {"dept": 216001}}
Received: "[object Object]"

Related

How to update multiple time a discord.js's interaction using setTimeout?

I'm currently working on a discordjs v13.6 bot in typescript who post an embed object with the date of day when using /day interaction command.
In a nutshell: I make /day and an embed is posted by my bot with the current date, weather, etc...
It's work fine and I added 3 buttons under the embed:
"reload" button: it will simply update the embed (with current weather forecast).
"previous" button: to update embed's interaction to the previous day.
"next" button: to update embed's interaction to the next day.
My code work as it should like this for the 3 buttons attached to my embed:
import { MessageActionRow, MessageButton } from 'discord.js';
// All customId property are formated like: `{method}#{date}`, ie: `reload#2022-03-10` is the button who'll reload the embed to date 03/10/2022.
export const createNavigationButtons = (date) => (
new MessageActionRow().addComponents([
new MessageButton()
.setCustomId(`prev#${date}`)
.setStyle('SECONDARY')
.setEmoji("946186554517389332"),
new MessageButton()
.setCustomId(`reload#${date}`)
.setStyle('SUCCESS')
.setEmoji("946190012154794055"),
new MessageButton()
.setCustomId(`next#${date}`)
.setStyle('SECONDARY')
.setEmoji("946186699745161296")
])
)
For the logic:
import { ButtonInteraction } from 'discord.js';
import { createEmbed } from "../utils/embed";
import { createNavigationButtons } from "../utils/buttons";
import * as year from "../../resources/year.json";
import * as moment from 'moment';
import { setTimeout as wait } from 'node:timers/promises';
// This function is called in the on('interactionCreate') event, when interaction.isButton() is true
export const button = async (interaction: ButtonInteraction): Promise<void> => {
const [action, date]: string[] = interaction.customId?.split('#');
await interaction.deferUpdate();
await wait(1000);
const newDate: string = {
prev: moment(date, "YYYY-MM-DD").subtract(1, "day").format("YYYY-MM-DD"),
next: moment(date, "YYYY-MM-DD").add( 1, "day").format("YYYY-MM-DD"),
reload: date
}[action];
await interaction.editReply({
embeds: [await createEmbed(year[newDate])],
components: [createNavigationButtons(newDate)]
});
}
It works just as I wished. BUT, everybody can use theses buttons (and I don't want to send /day's answer as ephemeral, I want everybody to see the response). So, if we use /day 2022-03-10 the embed for March 10, 2022. but if the author or someone else (I don't mind) use the button, the embed will be updated with another date (and that's fine by me !). But I want to roll back my embed to the original date few seconds / minutes after the button is pressed.
I tried somes primitive way like the setTimeout like this:
export const button = async (interaction: ButtonInteraction, config: any): Promise<void> => {
// In this test, button's customId are formated like {method}#{date}#{date's origin} (where 'origin' is the original requested's date, optional)
const [action, date, origin]: string[] = interaction.customId?.split('#');
await interaction.deferUpdate();
await wait(1000);
const newDate: string = {
prev: moment(date, "YYYY-MM-DD").subtract(1, "day").format("YYYY-MM-DD"),
next: moment(date, "YYYY-MM-DD").add( 1, "day").format("YYYY-MM-DD"),
reload: date
}[action];
// Here is my setTimeout who is supposed to recursively recall this function with a "reload" and the original date
setTimeout(async () => {
interaction.customId = `reload#${origin ?? date}`;
console.log(interaction.customId);
await button(interaction, config);
}, 5000);
await interaction.editReply({
embeds: [await createEmbed(year[newDate])],
components: [createNavigationButtons(newDate)]
});
};
When I press my buttons with this, it's correctly updated but 5sec (setTimeout's value) after it end up with an error saying:
reload#2022-03-10
/home/toto/tata/node_modules/discord.js/src/structures/interfaces/InteractionResponses.js:180
if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED');
^
Error [INTERACTION_ALREADY_REPLIED]: The reply to this interaction has already been sent or deferred.
at ButtonInteraction.deferUpdate (/home/toto/tata/node_modules/discord.js/src/structures/interfaces/InteractionResponses.js:180:46)
at button (/home/toto/tata/src/services/button.ts:12:21)
at Timeout._onTimeout (/home/toto/tata/src/services/button.ts:28:21)
at listOnTimeout (node:internal/timers:559:17)
at processTimers (node:internal/timers:502:7) {
[Symbol(code)]: 'INTERACTION_ALREADY_REPLIED'
}
I understand that it seems I can't reupdate my interaction with the same token like that, so how shoul I achieve my goal ? may be the setTimeout isn't a propper solution (but it was quite simple to implemant so I tried it first). Any Ideas ?
I successfully reached my objective like this:
// All customId property are formated like: `{method}#{date}#{origin}`, ie: `reload#2022-03-10` is the button who'll reload the embed to date 03/10/2022.
export const createNavigationButtons = (date: string, mode?: boolean) => (
new MessageActionRow().addComponents([
new MessageButton()
.setCustomId(`prev#${date}${!mode ? `#${date}` : ''}`)
.setStyle('SECONDARY')
.setEmoji("946186554517389332"),
new MessageButton()
.setCustomId(`reload#${date}`)
.setStyle('SUCCESS')
.setEmoji("946190012154794055"),
new MessageButton()
.setCustomId(`remind#${date}`)
.setStyle('PRIMARY')
.setEmoji("946192601806155806"),
new MessageButton()
.setCustomId(`next#${date}${!mode ? `#${date}` : ''}`)
.setStyle('SECONDARY')
.setEmoji("946186699745161296")
])
);
export const createButtons = (date, mode?: boolean) => ({
components: [ createNavigationButtons(date, mode) ]
});
export const button = async (interaction: ButtonInteraction, config: any): Promise<void> => {
const [action, date, origin]: string[] = interaction.customId?.split('#');
const newDate: string = {
prev: moment(date, "YYYY-MM-DD").subtract(1, "day").format("YYYY-MM-DD"),
next: moment(date, "YYYY-MM-DD").add( 1, "day").format("YYYY-MM-DD"),
reload: date
}[action];
origin && !interaction.deferred && setTimeout(async () => {
await interaction.editReply({
embeds: [await createEmbed(year[origin], config.server)],
...createButtons(origin)
});
}, 120000);
!interaction.deferred && await interaction.deferUpdate();
await interaction.editReply({
embeds: [await createEmbed(year[newDate], config.server)],
...createButtons(newDate, true)
});
};
In a nutshell, when I first create the embed I place a #{origin} (who's a date), and when I navigate and update my embed with my buttons, I don't send origin (only {action}#{date}, not {action}#{date}#origin by passing true to my createButtons method.

mounted method giving error message - Nuxt.js, Jest

I am new to unit testing and have a question about the mounted method inside the component.
I am testing if the button text is correctly displaying depends on one of the data values, and it passes. However, I have one method in mounted() inside the component and requests API call which is called from nuxt context.
The method is failing and consoling err message from try and catch because looks like it can not find nuxt context inside the test. This is not affecting my test but I wonder if it is fine, or do I need to fix something.
This is my component.
<template>
<button>
{{variations.length > 0 ? 'Select options' : 'add to cart'}}
</button>
</template>
<script>
data() {
return {
variations: [],
}
},
mounted() {
this.getVaridations()
},
methods: {
async getVaridations() {
try {
const variations = await this.$getVatiation.get() // this part is failing and consoling err message from catch
this.variations = variations
} catch (err) {
console.log(err) // consoling as TypeError: Cannot read properties of undefined (reading 'get')
}
},
},
</script>
This is testing
describe('Product.vue', () => {
it('btn display as "Select options" when there is validation', () => {
const wrapper = mount(Product, {})
expect(wrapper.find('.product-btn').text()).toBe('Select options') // This passes
})
})
You can mock any component methods like
import { shallowMount } from "#vue/test-utils";
describe('Product.vue', () => {
it('btn display as "Select options" when there is validation', () => {
const mocks = {
$getVatiation: {
get: () => [] // returns an empty array, change to what you want to return
}
}
const wrapper = shallowMount (Product, {mocks}) // send your mocks as an argument
expect(wrapper.find('.product-btn').text()).toBe('Select options')
})
})

Test case React with Node js

My site is using react and node . For that I created the test case using enzyme at react side and from server site I am using Mocha. Both are working correctly when I am using terminal command (npm test) its showing fail ans success result. But I want to do test case from ui interface .
Basically i want to backed (nodejs) function fronted with one command, Now my test case working on different terminal For example for react I am opened in one terminal and for node side test case I oped another terminal.
this is my node side code :-
const request = require('supertest');
const expect = require('chai').expect;
const req = 'http://localhost:3000';
this.user_id = '';
describe('Register form', function() {
it('User saved successfully in database', function(done) {
request(req)
.post('/users/register')
.set('Accept', 'application/json')
.set('Content-Type', 'application/json')
.send({user:{disabled: false,
email: "dineshsharma.developer#gmail.com",
firstName: "test",
lastName: "test",
password: "24234234ds",
roles: "hr",
status: "verified"}})
.expect(200)
.expect('Content-Type', /json/)
.expect(function(response) {
})
.end(done);
});
});
and this is react side code:-
import React from 'react';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import Select from 'react-select'
import config from 'config';
import { Alert } from 'reactstrap';
import { userActions } from '../../../src/actions';
import Enzyme, {shallow,mount,render,unmount} from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import RegisterPage from '../../../src/features/RegisterPage/RegisterPage';
import { MemoryRouter } from 'react-router-dom';
Enzyme.configure({adapter: new Adapter()});
describe('In register page check react syntex', () => {
it('Should RegisterPage shallow correctly in "debug" mode', () => {
const component = (<RegisterPage debug />);
expect(component).toMatchSnapshot();
});
});
describe('On register page count input filed', () => {
it('Total text filed', () => {
expect(shallow(<RegisterPage />).find('input[type="text"]').length).toEqual(2)
})
it('Total email filed', () => {
expect(shallow(<RegisterPage />).find('input[type="email"]').length).toEqual(1)
})
it('Total checkbox filed', () => {
expect(shallow(<RegisterPage />).find('input[type="checkbox"]').length).toEqual(1)
})
it('Total radio filed', () => {
expect(shallow(<RegisterPage />).find('input[type="radio"]').length).toEqual(2)
})
});
describe('On Register page check firstname , lastname or email address vaildation', () => {
it('Check first name in registerpage ', () => {
const wrapper = mount(<RegisterPage />);
wrapper.find('input[name="firstName"]').simulate('change', {target: {name: 'firstName', value: 'dinesh'}});
expect(wrapper.state().user.firstName).toEqual('dinesh');
})
it('Check lastname in register page ', () => {
const wrapper = mount(<RegisterPage />);
wrapper.find('input[name="lastName"]').simulate('change', {target: {name: 'lastName', value: 'Sharma'}});
expect(wrapper.state().user.lastName).toEqual('Sharma');
})
it('Check email address in register page', () => {
const wrapper = mount(<RegisterPage />);
wrapper.find('input[name="email"]').simulate('change', {target: {name: 'email', value: 'dineshsharma.developer#gmail.com'}});
expect(wrapper.state().user.email).toEqual('dineshsharma.developer#gmail.com');
})
})
If you want to run front end and back end test together in one terminal you can modify your package.json scripts section to look something like this.
{
"scripts": {
"test": "mocha <path to node test files> && jest <path to react test files>"
}
}
Every time you run npm test both sets of tests will run.

Testing and mocking fetch in async useEffect and async Redux-Saga

I'm testing a functional component, that use React-Hooks and Redux-Saga. I can pass parameters in URL for the component, because they are a login page component.
My URL i pass is 'localhost/access/parameter', and when this parameter exists, i need to call a async redux saga, and if the fetch is OK, i put the result in redux-store. When the result is on redux-store, i have a useEffect that verify the result and if is OK, i put her in a input.
I can mock the result with axios, but i'm migrating to use only fetch. i mock the fetch, but when i use
mount(component), provided by enzyme, i do not how to await the redux-saga termine the request and useEffect do your job. I put a console log inside a effect, saga and log the input props to see your value prop, but the value is always empty . I tried to use setImmediate() and process.nextTick().
Links i use to write the code: 1,2, 3
I'm using formik, so they pass some props to me.
My component
const Login = ({
setFieldError, errors, response, fetchDomain, location, values, handleChange, handleBlur, setFieldValue, history,
}) => {
useEffect(() => {
async function fetchUrlDomain() {
const { pathname } = location;
const [, , domain] = pathname.split('/');
if (typeof domain !== 'undefined') {
await fetchDomain(domain);
}
}
fetchUrlDomain();
}, [fetchDomain, location]);
useEffect(() => {
if (typeof response === 'string') {
setFieldError('domain', 'Domain not found');
inputDomain.current.focus();
} else if (Object.keys(response).length > 0) {
setFieldValue('domain', response.Domain);
setFieldError('domain', '');
}
}, [response, setFieldValue, setFieldError]);
return (
<input name="domain" id="domain" value={values.domain} onChange={handleChange} onBlur={handleBlur} type="text" />
);
}
const LoginFormik = withFormik({
mapPropsToValues: () => ({ domain: '' }),
enableReinitialize: false,
validateOnBlur: false,
validateOnChange: false,
})(Login);
const mapStateToProps = () => ({ });
const mapDispatchToProps = dispatch => ({
fetchDomain: (value) => {
dispatch(action({}, constants.RESET_RESPONSE_DOMAIN));
dispatch(action(value, constants.FETCH_DOMAIN_REQUEST));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(LoginFormik);
My Saga
export function* fetchDomain(action) {
const url = yield `${mainUrl}/${action.payload}`;
try {
const response = yield fetch(url).then(res => res.json());
yield put(reduxAction(response , constants.FETCH_DOMAIN_SUCCESS));
} catch (e) {
yield put(reduxAction(e, constants.FETCH_DOMAIN_FAILURE));
}
}
My Reducer
case constants.FETCH_DOMAIN_FAILURE:
return { ...initialState, response: 'Domain not found' };
case constants.FETCH_DOMAIN_SUCCESS: {
const { payload } = action;
return {
...initialState,
id: payload.Id,
apis: payload.Apis,
response: payload,
};
}
case constants.RESET_RESPONSE_DOMAIN:
return { ...initialState };
My Test
it('input with fetch only', (done) => {
const mockSuccessResponse = {
Id: 'fafafafa',
Apis: [],
Domain: 'NAME',
};
const mockJsonPromise = Promise.resolve(mockSuccessResponse);
const mockFetchPromise = Promise.resolve({
json: () => mockJsonPromise,
});
global.fetch = jest.fn().mockImplementation(() => mockFetchPromise);
const wrapper = mount(
<Provider store={store}>
<LoginForm
history={{ push: jest.fn() }}
location={{ pathname: 'localhost/login/Domain' }}
/>
</Provider>,
);
process.nextTick(() => {
const input = wrapper.find('#domain');
console.log(input.props());
expect(input.props().value.toLowerCase()).toBe('name');
global.fetch.mockClear();
done();
});
});
I expect my input have value, but he don't. I tried to use jest-fetch-mock but just don't work, and i want to use native jest methods, no thirty party libraries.
I cannot say what's wrong with your current code. But want to propose different approach instead.
Currently you are testing both redux part and component's one. It contradicts with unit testing strategy when ideally you should mock everything except module under the test.
So I mean if you focus on testing component itself it'd be way easier(less mocks to create) and more readable. For that you need additionally export unwrapped component(Login in your case). Then you can test only its props-vs-render result:
it('calls fetchDomain() with domain part of location', () => {
const fetchDomain = jest.fn();
const location = { pathName: 'example.com/path/sub' }
shallow(<Login fetchDomain={fetchDomain} location={location} />);
expect(fetchDomain).toHaveBeenCalledTimes(1);
expect(fetchDomain).toHaveBeenCalledWith('example.com');
});
it('re-calls fetchDomain() on each change of location prop', () => {
const fetchDomain = jest.fn();
const location = { pathName: 'example.com/path/sub' }
const wrapper = shallow(<Login fetchDomain={fetchDomain} location={location} />);
fetchDomain.mockClear();
wrapper.setProps({ location: { pathName: 'another.org/path' } });
expect(fetchDomain).toHaveBeenCalledTimes(1);
expect(fetchDomain).toHaveBeenCalledWith('another.org');
});
And the same for other cases. See with this approach if you replace redux with direct call to fetch() or whatever, or if you refactor that data to come from parent instead of reading from redux store you will not need to rewrite tests removing mocks to redux. Sure, you will still need to test redux part but it also can be done in isolation.
PS and there is no profit to await fetchDomain(...) in useEffect since you don't use what it returns. await does not work like a pause and that code may rather confuse reader.

Test react-final-form submit

I have started migrating from redux-form to react-final-form to make my bundle smaller. I had couple of tests for my forms and one of them was to test that the correct action was called on form submit. Store action in my test never gets called after switching to react-final-form.
Is there a way ho to test submit function when form is passed as a property.
My test:
it('submits the form', () => {
const wrapper = shallowUntilTarget(<LoginFormContainer store={store} />, 'form');
wrapper.find('form').simulate('submit');
expect(store.getActions()).toEqual(expect.arrayContaining([{ formObj: {}, type: 'PATIENT_LOGIN_REQUEST' }]));
});
shallowUntilTarget renders the actual form through container
Tested component:
class LoginForm extends React.Component<Props> {
submitForm = (values) => {
this.props.dispatch(actions.loginPatient(values));
};
render() {
return (
<Form
onSubmit={this.submitForm}
render={({ handleSubmit }) => (
<form onSubmit={handleSubmit} />
I was not able to test the validation with redux-form, but actually it is much easier to do in react-final-form. The form doesn't not get submitted and fails when validation is not successful. My LoginForm has email and password validation
<Form
onSubmit={this.submitForm}
validate={createValidator({
email: [required, email],
password: [required, minLength('8')],
})}
render={({ handleSubmit }) => (
There could be two tests. One testing failure and one testing successful submit. Both of them have to happened on mounted component.
it('does not submits invalid form ', () => {
const wrapper = mount(<LoginFormContainer store={store} />);
wrapper.find('form').simulate('submit');
expect(store.getState().lastAction).not.toEqual({ payload: {}, type: 'PATIENT_LOGIN_REQUEST' });
});
it('submits valid form', () => {
const wrapper = mount(<LoginFormContainer store={store} />);
const email = wrapper.find('input[name="email"]');
email.instance().value = 'cerny#seznam.cz';
email.simulate('change', email);
const password = wrapper.find('input[name="password"]');
password.instance().value = '12345678';
password.simulate('change', password);
wrapper.find('form').simulate('submit');
expect(store.getState().lastAction).toEqual({
payload: { email: 'cerny#seznam.cz', password: '12345678' },
type: 'PATIENT_LOGIN_REQUEST',
});
});

Resources