Testing Pinia changes in a Vue 3 Component Unit Test - unit-testing

I have a component in my Vue 3 app which displays a checkbox. The checkbox can be manually checked by the user but it can also be checked/unchecked as a result of a Pinia state change. I'm pretty new to Unit Testing but I would assume that a good unit test for this component would include checking whether or not the checkbox reacts to the Pinia state correctly. However, in my Unit Test, when I change the Pinia state, the checkbox value does not change (the component itself works fine, it's only in the Unit Test that this does not work). Does anyone know what I am doing wrong?
As well as calling the store action to update the state I have also tried store.$patch and that doesn't work either.
This is my component:
<template>
<div class="field-checkbox">
<Checkbox role="checkbox" :aria-label="displayName" #change="checkGroupMember()" v-model="checked" :binary="true" />
<label>{{displayName}}</label>
</div>
</template>
<script setup lang="ts">
import { useContactBookStore } from "#/stores/contactBookStore";
import { ref, watch } from "vue";
import { storeToRefs } from "pinia";
const store = useContactBookStore();
const props = defineProps({
groupMember: { type:Object, required: true }
});
const checked = ref(false);
const { getCheckedGroupMembers } = storeToRefs(store)
const displayName = ref(props.groupMember.title + " " + props.groupMember.firstName + " " + props.groupMember.lastName);
// set the initial value of the checkbox
updateCheckBox();
// watch the value of getCheckedGroupMembers in the store and if it
// changes re-evaluate the value of the checkbox
watch(getCheckedGroupMembers , () => {
updateCheckBox();
},{ deep: true })
// when the checkbox is checked/unchecked, run the checkUser method
// in the store
function checkGroupMember() {
const groupMember = {
id:props.groupMember.id,
title:props.groupMember.title,
firstName:props.groupMember.firstName,
lastName:props.groupMember.lastName
}
store.checkGroupMember(groupMember,checked.value);
}
// the checkbox is checked if the user is among the checked users
// in the store
function updateCheckBox() {
const groupMember = {
id: props.groupMember.id,
title: props.groupMember.title,
firstName: props.groupMember.firstName,
lastName: props.groupMember.lastName
}
const exists = getCheckedGroupMembers.value.find((member) => member.id === groupMember.id)
checked.value = !!exists;
}
</script>
and this is my Unit Test:
import { render, screen } from "#testing-library/vue";
import GroupMember from "#/components/ContactBook/GroupMember.vue";
import { describe, it, vi, expect, beforeEach, afterEach } from "vitest";
import { createTestingPinia } from "#pinia/testing";
import PrimeVue from "primevue/config";
import { createPinia, setActivePinia } from "pinia";
import Checkbox from 'primevue/checkbox';
import { useContactBookStore } from "#/stores/contactBookStore";
describe("GroupMember", () => {
const mockUser:GroupMember = {id:"TT001",title:"Mr",firstName:"Ted",lastName:"Tester"}
let mockProps = {groupMember:mockUser};
render(GroupMember, {
props: mockProps,
global: {
components: {Checkbox},
plugins: [PrimeVue,
createTestingPinia({
initialState: {contactBook:{checkedGroupMembers:[mockUser]}},
stubActions: false,
createSpy: vi.fn,
fakeApp:true
}),
],
},
});
setActivePinia(createPinia());
it("Displays the user name in the correct format", async() => {
const displayName = mockProps.groupMember.title + " " + mockProps.groupMember.firstName + " " + mockProps.groupMember.lastName;
screen.getByText(displayName)
});
it("Shows the checkbox initially checked", async() => {
let checkbox:any;
const displayName = mockProps.groupMember.title + " " + mockProps.groupMember.firstName + " " + mockProps.groupMember.lastName;
checkbox = screen.getAllByRole("checkbox", { name: displayName })[1]
expect(checkbox.checked).toBe(true)
});
it("Should display the checkbox as unchecked when the store is updated", async() => {
let checkbox:any;
const displayName = mockProps.groupMember.title + " " + mockProps.groupMember.firstName + " " + mockProps.groupMember.lastName;
checkbox = screen.getAllByRole("checkbox", { name: displayName })[1]
const store = useContactBookStore();
await store.checkGroupMember(mockUser,false);
//await store.$patch({checkedGroupMembers:[]}) // this didn't work either
expect(checkbox.checked).toBe(false)
});
});
this is the error I get when the test runs:
54| expect(checkbox.checked).toBe(false)
| ^
55| });
56| });
- Expected "false"
+ Received "true"

When you render or mount a component in a unit test, there is no event loop running, so while you can change the state in the Pinia store, the component does not react to the alteration until "later": import { flushPromises } from '#vue/test-utils'; await flushPromises(), await component.vm.$nextTick or import { nextTick } from 'vue'; await nextTick().
See https://v1.test-utils.vuejs.org/guides/testing-async-components.html where it is described that you can await triggers like clicking on the checkbox, but if the change happens out of band, you don't necessarily have enough awaiting going on.
flushPromises is documented here

Related

vue unit test - data not updated after triggering event ( form submit) as expected

I am testing that form data are sent after submit.
ContactForm.spec.js
import Vue from "vue";
import Vuex from "vuex";
import { mount, shallowMount } from "#vue/test-utils";
import VeeValidate from "vee-validate";
import i18n from "#/locales";
import Vuetify from "vuetify";
import ContactForm from "#/components/Home/ContactForm.vue";
Vue.use(VeeValidate, { errorBagName: "errors" });
Vue.use(Vuetify);
Vue.use(Vuex);
describe("ContactForm.vue", () => {
let store;
let wrapper;
let options;
let input;
const v = new VeeValidate.Validator();
beforeEach(() => {
const el = document.createElement("div");
el.setAttribute("data-app", true);
document.body.appendChild(el);
});
it("should sendMessage - valid form", async () => {
// given
store = new Vuex.Store({
state: {
language: "en",
loading: false
}
})
options = {
sync: false,
provide: {
$validator () {
return new VeeValidate.Validator();
}
},
i18n,
store
};
wrapper = mount(ContactForm, options);
// when
const radioInput = wrapper.findAll('input[type="radio"]');
radioInput.at(1).setChecked(); // input element value is changed, v-model is not
radioInput.at(1).trigger("change"); // v-model updated
input = wrapper.find('input[name="givenName"]');
input.element.value = "John"; // input element value is changed, v-model is not
input.trigger("input"); // v-model updated
input = wrapper.find('input[name="familyName"]');
input.element.value = "Doe"; // input element value is changed, v-model is not
input.trigger("input"); // v-model updated
input = wrapper.find('input[name="email"]');
input.element.value = "john.doe#example.com"; // input element value is changed, v-model is not
input.trigger("input"); // v-model updated
input = wrapper.find('textarea[name="messageContent"]');
input.element.value = "Hello World!"; // input element value is changed, v-model is not
input.trigger("input"); // v-model updated
const contactForm = wrapper.find("form");
contactForm.trigger("submit");
await wrapper.vm.$nextTick();
// then
console.log("DATA: ", wrapper.vm.$data.contactLang);
expect(wrapper.vm.validForm).toEqual(true);
});
});
Validation is successful ( so validForm is set to true in the component )
BUT the test does not pass
console.log
✕ should sendMessage - valid form (476ms)
● ContactForm.vue › should sendMessage - valid form
expect(received).toEqual(expected)
Expected value to equal:
true
Received:
false
The component vue is
ContactForm.vue
<template>
<form id="contactForm" #submit="sendMessage()">
<input v-model="contactLang" type='hidden' data-vv-name="contactLang" v-validate="'required'" name='contactLang'>
<v-layout row wrap align-center>
<v-flex xs12 sm3 md3 lg3>
<v-radio-group row :mandatory="false" v-model="gender" name="gender">
<v-radio :label='genderLabel("f")' value="f" name="female"></v-radio>
<v-radio :label='genderLabel("m")' value="m" name="male"></v-radio>
</v-radio-group>
</v-flex>
<v-flex xs12 sm4 md4 lg4>
<v-text-field
v-model="givenName"
browser-autocomplete="off"
:label="$t('lang.views.home.contactForm.givenName')"
data-vv-name="givenName"
:error-messages="errors.collect('givenName')"
v-validate="'required'"
name="givenName">
</v-text-field>
</v-flex>
<v-flex xs12 sm5 md5 lg5>
<v-text-field
v-model="familyName"
browser-autocomplete="off"
:label="$t('lang.views.home.contactForm.familyName')"
data-vv-name="familyName"
:error-messages="errors.collect('familyName')"
v-validate="'required'"
name="familyName">
</v-text-field>
</v-flex>
</v-layout>
<v-text-field
browser-autocomplete="off"
v-model="email"
:label="$t('lang.views.home.contactForm.email')"
data-vv-name="email"
:error-messages="errors.collect('email')"
v-validate="'required|email'"
name="email">
</v-text-field>
<v-textarea v-model="messageContent" :label="$t('lang.views.home.contactForm.message')" :error-messages="errors.collect('messageContent')" :rules="[(v) => v.length <= 200 || 'Max 200 characters']" :counter="200" v-validate="'required'" data-vv-name="messageContent" name="messageContent"></v-textarea>
<v-btn id="btnClear" round #click.native="clear">{{ $t('lang.views.global.clear') }}</v-btn>
<v-btn round large color="primary" type="submit">{{ $t('lang.views.global.send') }}
<v-icon right>email</v-icon><span slot="loader" class="custom-loader"><v-icon light>cached</v-icon></span>
</v-btn>
</form>
</template>
<script>
import swal from "sweetalert2";
import { mapState } from "vuex";
import appValidationDictionarySetup from "#/locales/appValidationDictionary";
export default {
name: "contactForm",
$_veeValidate: { validator: "new" },
data() {
return {
contactLang: "",
gender: "f",
givenName: "",
familyName: "",
email: "",
messageContent: "",
validForm: false
};
},
...
methods: {
...
sendMessage: function() {
console.log("sendMessage()...");
this.$validator
.validateAll()
.then(isValid => {
console.log("VALIDATION RESULT: ", isValid);
this.validForm = isValid;
if (!isValid) {
console.log("VALIDATION ERROR");
// console.log("Errors: ", this.$validator.errors.items.length);
const alertTitle = this.$validator.dictionary.container[
this.language
].custom.error;
const textMsg = this.$validator.dictionary.container[this.language]
.custom.correct_all;
swal({
title: alertTitle,
text: textMsg,
type: "error",
confirmButtonText: "OK"
});
return;
}
console.log("validation success, form submitted validForm: ", this.validForm);
return;
})
.catch(e => {
// catch error from validateAll() promise
console.log("error validation promise: ", e);
this.validForm = false;
return;
});
},
clear: function() {
this.contactLang = "";
this.gender = "f";
this.givenName = "";
this.familyName = "";
this.email = "";
this.messageContent = "";
this.validForm = false;
this.$validator.reset();
}
},
mounted() {
appValidationDictionarySetup(this.$validator);
this.$validator.localize(this.language);
this.contactLang = this.language;
}
};
</script>
</style>
and the console.log debugging is
console.log src/components/Home/ContactForm.vue:90
sendMessage()...
console.log tests/unit/ContactForm.spec.js:242
DATA: en
console.log src/components/Home/ContactForm.vue:94
VALIDATION RESULT: true
It's weird to see that the DATA ( contactLang ) value is false in the console.log from the spec ... displayed before the validation result
console.log src/components/Home/ContactForm.vue:90
sendMessage()...
console.log tests/unit/ContactForm.spec.js:242
DATA: en
console.log src/components/Home/ContactForm.vue:94
VALIDATION RESULT: true
console.log src/components/Home/ContactForm.vue:112
validation success, form submitted validForm: true
I guess there is async problem ... timout ?
thanks for feedback
SOLVED
It's actually a timeout issue
expect.assertions(1); // Otherwise jest will give a false positive
await contactForm.trigger("submit");
// then
setTimeout(() => {
expect(wrapper.vm.validForm).toEqual(true);
}, 2000);
I propose to use jest faketimers
jest.useFakeTimers()
contactForm.trigger("submit");
await wrapper.vm.$nextTick();
// then
jest.runTimersToTime(2000)
expect(wrapper.vm.validForm).toEqual(true);
I suggest to first make the test fail to avoid false positives
for more information about jest faketimers
https://jestjs.io/docs/en/timer-mocks.html
i did a simple test for my login form of component, in case it helps someone
it("submit form call method login", () => {
const login = jest.fn()
const wrapper = shallowMount(Login, {
methods: {
login
}
})
wrapper.findAll("v-form").at(0).trigger("submit")
expect(login).toBeCalled()
})

Unit test on Meteor server side TypeScript

I have a problem with my unit tests on server side.
My test is the next :
import { Meteor } from 'meteor/meteor';
import { Article } from '../../../imports/models/article';
import { Articles } from '../../../imports/collections/articles';
import './articles';
import { Random } from 'meteor/random';
import {Rate} from "../../../imports/models/rate.model";
import { expect, assert } from 'chai';
import {Observable} from "rxjs/Observable";
if (Meteor.isServer) {
describe('Articles', () => {
const userId = Random.id();
beforeEach(() => {
StubCollections.add([Articles]);
StubCollections.stub();
Articles.remove({});
});
it('can delete owned article', async (done) => {
const articleId = await Articles.insert({
title: "string",
content: "string",
owner: userId,
picture_url: "string",
source: "string",
createdAt: new Date()
}).toPromise();
const deleteArticle = Meteor.server.method_handlers["removeArticle"];
// // Run the method with `this` set to the fake invocation
//`enter code here`
const invocation = {userId};
deleteArticle.apply(invocation, [articleId]);
console.log(articleId);
const count = await Articles.find({}).count().toPromise();
// Verify that the method does what we expected
expect(count).equal(0);
StubCollections.restore();
done()
});
});
}
And I can't import stub-collection because typescript not found it.
I have try to had meteor server package on tsconfig.json but I did not suceed.
And when I delete StubCollection I have a Timeout of 2 seconde when "Articles.find({})"
Have you a idea for resolve it ?
My problems is with Article.find({}) result of type Observable with Meteor.observable.
My new test is the next
it('can delete owned article', async done => {
let fixFindToPromise: number = 0;
const articleId = await Articles.insert({
title: "string",
content: "string",
owner: userId,
picture_url: "string",
source: "string",
createdAt: new Date()
}).toPromise();
// console.log('A2', articleId2);
const deleteArticle = Meteor.server.method_handlers["removeArticle"];
// Run the method with `this` set to the fake invocation
deleteArticle.apply({userId}, [articleId]);
// Find the internal implementation of the task method so we can
console.log("ArticleId:", articleId);
Articles.find().subscribe((countLog) => {
fixFindToPromise++;
if (fixFindToPromise == 1 ) {
if ( countLog.length == 0 ) {
done();
} else {
done("Count not correct");
}
}
});
It's possible use find method with promise compatibilty result for better syntax and use assert and expect ?
And exists a project or solution for import package on typescript unit test server side ?
Thank for your response

Testing Vue filters with jest

i'm trying to write a test for one of my filters in my Vue project using jest , can i Test that filter without using it in any component, i mean can i test it as a unit(like a function)? i searched a lot but i couldn't find anything to show me how to write an individual test for a filter in Vue
import Vue from 'vue'
export default function () {
Vue.filter('englishNumber', (value) => {
if (value === '۰') return 0
if (!value) return ''
if (typeof value !== 'string') {
value = value.toString()
}
return value.replace(/[\u06F0-\u06F9]+/g, function (digit) {
let ret = ''
for (let i = 0, len = digit.length; i < len; i++) {
ret += String.fromCharCode(digit.charCodeAt(i) - 1728)
}
return ret
})
})
}
this is the filter i want to test
does anyone know how to write this kind of test ?
If you're writing filters used in multiple components, then it's quite easy to test.
Since Vue.filter simply takes a function, you can just write a test for the function independently of the filter by exporting the definition, like so:
// Define the function independently and export it
export const englishNumber = function (value) {
if (value === '۰') return 0
if (!value) return ''
if (typeof value !== 'string') {
value = value.toString()
}
return value.replace(/[\u06F0-\u06F9]+/g, function (digit) {
let ret = ''
for (let i = 0, len = digit.length; i < len; i++) {
ret += String.fromCharCode(digit.charCodeAt(i) - 1728)
}
return ret
})
}
// Pass the function in to the filter defintion
Vue.filter('englishNumber', englishNumber)
Then in your test file, import the function and test it like you do anything else:
import { englishNumber } from '#/lib/filters.js'
describe("englishNumber") {
it("does whatever") {
expect(englishNumber("actual")).toEqual("expected")
}
}
We can test filters with hand-made components. For example we have a filter that transform date into needed format:
// filters.js
import Vue from "vue";
Vue.filter("dateFormat", (incomingDate) => {
// does the work here
}
So we can test it the following way:
// filters.spec.js
import Vue from "vue";
import "#/components/filters";
import { mount, createLocalVue } from "#vue/test-utils";
const myComponent = Vue.component("myComponent", {
data() {
return {
date: new Date("January 1, 2020 01:10:30")
};
},
template: "<p> {{ date | dateFormat }} </p>"
});
const localVue = createLocalVue();
describe("filter dateFormat", () => {
it("filter transforms date to readable format", () => {
const wrapper = mount(myComponent, {
localVue
});
expect(wrapper.html()).toBe("<p> 01 January 2020 </p>");
});
});
The correct way to unit test a Vue filter would be to declare it locally inside your component (using Vue's API and Vue.filter).
You can then write unit tests inside your Vue component.
You can check this case

Using jest for test react-native - redux app

I am trying to fill a Text Input and verify that the text is filled correctly, accessing the component and getting its value.
I have succeeded in doing so, but without using redux, ie using the native states of react-native. this.state.
Component Code:
//inside constructor
this.state = {
email: ''
}
<TextInput value={this.state.email} onChangeText={(text) => {
console.log('Here change email text!!! ==> ', text);
this.setState({
email: text
})
}} />
Test File Code:
import LoginScreen from '../../App/Containers/LoginScreen' // => connected component.. exported with `export default connect(mapStateToProps, mapDispatchToProps)(LoginScreen)`
import configureStore from 'redux-mock-store'
import { shallow } from 'enzyme'
import Actions, { reducer, INITIAL_STATE } from '../../App/Redux/Reducers/UserReducer'
const initialState = {
user: {
email: 'mockState email',
password: '',
requesting: 0,
userData: null,
loginFinish: false,
errorMessage: null
}
}
const mockStore = configureStore([]);
let store = mockStore(initialState);
const wrapper = shallow(
<LoginScreen/>,
{ context: { store: store } },
);
test('>>>>> LoginScreen component renders correctly', () => {
expect(wrapper.dive()).toMatchSnapshot();
});
test('>>>>> Login button Press', () => {
let render = wrapper.dive();
const textInputProps = render.find('TextInput'); //getting text input from render
console.log(`textInputProps.getNode(1).props.value BEFORE ====>`, textInputProps.getNodes()[0].props.value);
textInputProps.first().simulate('changeText', 'My new value'); // executing onChangeText inside render of component
const textInputProps2 = render.find('TextInput'); //getting text input again for check changes
console.log(`textInputProps2.getNode(1).props.value====>`, textInputProps2.getNodes()[0].props.value);
const state = store.getState(); //verifying internal `initialState`.. NOT CHANGES
console.log('state ===> ', state);
});
I have relied on this link
Running test logs
yarn test v0.24.6
$ jest
PASS Tests/Containers/loginScreenTest.js
✓ >>>>> LoginScreen component renders correctly (282ms)
✓ >>>>> Login button Press (33ms)
console.log Tests/Containers/loginScreenTest.js:60
textInputProps.getNode(1).props.value BEFORE ====>
console.log App/Containers/LoginScreen.js:124
Here change email text!!! ==> My new value
console.log Tests/Containers/loginScreenTest.js:67
textInputProps2.getNode(1).props.value====> My new value => (!!!WORKS!!!)
console.log Tests/Containers/loginScreenTest.js:86
state ===> { user:
{ email: 'mockState email',
password: '',
requesting: 0,
userData: null,
loginFinish: false,
errorMessage: null } }
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 1 passed, 1 total
Time: 2.337s, estimated 3s
Ran all test suites.
✨ Done in 3.10s.
as you can see in the logs textInputProps2.getNode(1).props.value ====> show me the value as expected.
So far so good
Now passing everything to a reducer, with the redux structure, we will see the text input as follows
<TextInput value={this.props.user.email} style={styles.textInputs} placeholder={'Email'} autoCapitalize={'none'} onChangeText={(text) => {
console.log('Here change email text!!! ==> ', text);
this.props.email_typing(text);
}} />
Connected logic
const mapStateToProps = (state) => {
return {
user: state.user
}
}
const mapDispatchToProps = (dispatch) => {
return {
email_typing: (text) => dispatch(UserReducer.email_typing(text)),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(LoginScreen)
My UserReducer File
import { createReducer, createActions } from 'reduxsauce'
import Immutable from 'seamless-immutable'
/* ------------- Types and Action Creators ------------- */
const { Types, Creators } = createActions({
email_typing: ['email'],
})
export const LoginTypes = Types
export default Creators
/* ------------- Initial State ------------- */
export const INITIAL_STATE = Immutable({
email: ''
})
/* ------------- Reducers ------------- */
// state.merge undefined error: https://github.com/infinitered/ignite/pull/20#issuecomment-202550408. Fixed including in Inmutable
export const emailTyping = (state, { email }) => {
console.log('Email Typing changes !!! in original reducer')
return Immutable(state).merge({ email })
}
/* ------------- Hookup Reducers To Types ------------- */
export const reducer = createReducer(INITIAL_STATE, {
[Types.EMAIL_TYPING]: emailTyping,
})
Given this change, the idea is that the initialState within the Test File changes to INITIAL_STATE imported value.
Something like:
const mockStore = configureStore([]);
let store = mockStore(INITIAL_STATE);
but, when i run the test again. Show me the next error:
● >>>>> LoginScreen component renders correctly
TypeError: Cannot read property 'email' of undefined
even if I keep the initialState instead of the INITIAL_STATE, I do not get the above error, but I can not get the text input to take the change.
Running Test Logs
yarn test v0.24.6
$ jest
PASS Tests/Containers/loginScreenTest.js
✓ >>>>> LoginScreen component renders correctly (345ms)
✓ >>>>> Login button Press (24ms)
console.log Tests/Containers/loginScreenTest.js:58
textInputProps.getNode(1).props.value BEFORE ====> mockState email
console.log App/Containers/LoginScreen.js:120
Here change email text!!! ==> My new value
console.log Tests/Containers/loginScreenTest.js:61
textInputProps2.getNode(1).props.value====> mockState email => **(!! HERE !!!, THE VALUE IS BEING THE PREVIOUS ONE AND IGNOR THE CHANGE)**
console.log Tests/Containers/loginScreenTest.js:79
state ===> { user:
{ email: 'mockState email',
password: '',
requesting: 0,
userData: null,
loginFinish: false,
errorMessage: null } }
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 1 passed, 1 total
Time: 2.904s
Ran all test suites.
✨ Done in 3.68s.
Check textInputProps2.getNode(1).props.value====> log to check that this is not useful.
I think that the const initialState declared inside test file It is not being affected by the changes made in the actual reducer when this.props.email_typing(text) action is called;
I have not found the way to connect the actions with the states in the reducer and to be able to load them inside JEST.
I know it's a bit long and I appreciate your time reading it.
I tried to leave it the best explained and as much information as possible.
Thank you very much and I look forward to any response.
I guess you want to do some integration test here. It is possible to achieve what you are trying like that :
import { createStore, combineReducers } from 'redux';
import { reducer } from '.../UserReducer';
// create a real store with the needed reducer(s)
const store = createStore(combineReducers({ user: reducer }));
const wrapper = shallow(
<LoginScreen/>,
{ context: { store } },
);
// ...
test('>>>>> Login button Press', () => {
let render = wrapper.dive();
const textInputProps = render.find('TextInput');
console.log(`value BEFORE ====>`, textInputProps.getNodes()[0].props.value);
textInputProps.first().simulate('changeText', 'My new value');
// Force the component to update after changing state
render = wrapper.update().dive();
const textInputProps2 = render.find('TextInput');
console.log(`value AFTER ====>`, textInputProps2.getNodes()[0].props.value);
const state = store.getState();
console.log('state ===> ', state);
});
I tried with a minimal implementation, here is the console result:
console.log src/Test.test.js:27
value BEFORE ====>
console.log src/Test.test.js:35
value AFTER ====> My new value
console.log src/Test.test.js:38
state ===> { user: { email: 'My new value' } }

How to properly unit test login and local storage

After 3 days researching and not ariving anywhere, I decided to ask here for someone that already have similar experience or can point a better path to follow.
The better SO question I've found was this but left some questions in air: React - how to test form submit?
Since I'm begginer I believe I may getting something wrong, but no sure exactly which. If it's the way I build the components or even test concept itself.
I have the following case:
When a user logins in, it calls API (mock) then save token result (when successful) to localStorage (mock)
When user is already logged in, it gets redirected to homepage
My code until now:
Login Component
class Login extends React.Component {
constructor(props) {
super(props);
this.state = {
email: '',
password: ''
};
this.handleSubmit = this.handleSubmit.bind(this);
this.handleChange = this.handleChange.bind(this);
}
handleSubmit(e) {
e.preventDefault();
this.props.sendLoginRequest(this.state).then(
({data}) => {
console.log(data);
},
(data) => {
console.error(data);
}
);
}
handleChange(e) {
this.setState({ [e.target.name]: e.target.value });
}
render() {
return (
<div id='auth-container' className='login'>
<Form onSubmit={this.handleSubmit}>
<FormGroup controlId='emailaddress'>
<InputGroup bsSize='large'>
<InputGroup.Addon>
<Icon glyph='icon-fontello-mail' />
</InputGroup.Addon>
<FormControl
autoFocus
className='border-focus-blue'
type='email'
placeholder='email#fixdin.com'
name='email'
onChange={this.handleChange}
value={this.state.email} />
</InputGroup>
</FormGroup>
<FormGroup controlId='password'>
<InputGroup bsSize='large'>
<InputGroup.Addon>
<Icon glyph='icon-fontello-key' />
</InputGroup.Addon>
<FormControl
className='border-focus-blue'
type='password'
placeholder='password'
name='password'
onChange={this.handleChange}
value={this.state.password} />
</InputGroup>
</FormGroup>
</Form>
</div>
)
}
}
Login.propTypes = {
sendLoginRequest: React.PropTypes.func.isRequired
}
authAction.js
import createApi from '../services/api';
import { saveToken } from '../services/session';
export function sendLoginRequest(loginData) {
return dispatch => {
const api = createApi();
const loginPromise = api.post('auth/', loginData);
loginPromise.then(
({ data }) => {
saveToken(data.token);
}
);
return loginPromise;
}
}
API..js
import axios from 'axios';
import { isAuthenticated, getToken } from './session';
export const BASE_URL = 'http://localhost:8000/api/v1/';
export default function createAPI() {
let auth = { }
if (isAuthenticated()) {
auth = {
Token: getToken()
}
}
return axios.create({
baseURL: BASE_URL,
auth: auth
});
};
session.js
const TOKEN_KEY = 'token';
export function saveToken(value)
{
localStorage.setItem(TOKEN_KEY, value);
}
export function getToken()
{
return localStorage.getItem(TOKEN_KEY)
}
export function isAuthenticated() {
return getToken() !== null;
}
My test stack is Mocha/Chai/Enzyme/sinon and it's defined
setup.js
var jsdom = require('jsdom');
class LocalStorageMock {
constructor() {
this.store = {};
}
clear() {
this.store = {};
}
getItem(key) {
return this.store[key];
}
setItem(key, value) {
this.store[key] = value.toString();
}
};
if(!global.document) {
global.document = jsdom.jsdom('<!doctype html><html><body></body></html>');
global.window = document.defaultView;
global.navigator = {userAgent: 'node.js'};
global.localStorage = new LocalStorageMock;
}
login-test.js
import React from 'react';
import sinon from 'sinon';
import { mount, shallow } from 'enzyme';
import { expect } from 'chai';
import { Provider } from 'react-redux';
import axios from 'axios'
import moxios from 'moxios'
import store from './../src/store';
import LoginPage from './../src/auth/components/Login';
describe('Login', () => {
beforeEach(function () {
moxios.install(axios)
})
afterEach(function () {
moxios.uninstall(axios)
})
it('should call action on form submit', () => {
const submitRequest = sinon.stub(LoginPage.prototype, 'handleSubmit').returns(true);
const wrapper = mount(<Provider store={store}><LoginPage /></Provider>);
wrapper.find('form').simulate('submit');
expect(submitRequest.called).to.be.true;
submitRequest.restore();
});
it('should save token on succesfull login', () => {
const wrapper = mount(<Provider store={store}><LoginPage /></Provider>);
const emailInput = wrapper.find('input[type="email"]');
const passInput = wrapper.find('input[type="password"]');
const form = wrapper.find('form');
emailInput.value = "valid#email.com";
passInput.value = '123456789';
form.simulate('submit'); // Should I use submit button instead???
moxios.wait(function () {
let request = moxios.requests.mostRecent()
request.respondWith({
status: 200,
response:
{ Token: 'validToken' }
}).then(function () {
expect(localStorage.getItem('Token')).to.equal('validToken');
});
});
});
});
Above test does not pass, since it returns false for submitRequest.called and second test fails with error "Cannot read property 'respondWith' of undefined". I'm not sure how to fix and more, I'm not sure if I idealized it right!!
When doing a lot of research about it, I've seen examples with tests specific for component method call + isolated action test.
So...
When I think about "click login and save token" I'm overthinking a unit test? There's a better way to test things like that? Maybe separate some concerns?
This is the correctly way to test if a form submit invoke its callback? If so, why sinon is not working there?
This is the correctly way to mock + test api call to login and localStorage? If so, why Moxios is not working properly? It keeps giving me that mostRecent() is undefined.
If no, to question 2 and 3, where can I find a valid and working example of how to properly test cited behavior?
Thanks in advance.