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

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();
});
});

Related

beforeRouteUpdate not recognizing function

When I try to call this.getData() in beforeRouteUpdate it just spits out this error
"TypeError: this.getData is not a function"
From looking at other peoples examples this work, but they weren't using async/await.
<script>
export default {
async beforeRouteUpdate(to, from, next) {
await this.getData()
next()
},
data() {
return {
word: null,
}
},
async created() {
await this.getData()
},
methods: {
async getData() {
const resp = await axios.get(
'http://127.0.0.1:8000/api/word/' + this.$route.params.word,
{ validateStatus: false }
)
console.log(resp)
switch (resp.status) {
case 200:
this.word = {
word: resp.data.word,
definition: resp.data.definition,
}
break
case 404:
this.word = null
break
}
},
},
}
</script>
The concept you want use it, is called "prefetch".
It's better use this solution:
beforeRouteEnter(to, from, next) {
axios.get(
'http://127.0.0.1:8000/api/word/' + this.$route.params.word,
{validateStatus: false}
)
.then(resp => {
next()
})
.catch(error => {
})
},
beforeRouteUpdate(to, from, next) {
axios.get(
'http://127.0.0.1:8000/api/word/' + this.$route.params.word,
{validateStatus: false}
)
.then(resp => {
next()
})
.catch(error => {
})
}
NOTE 1: You don't access to this in beforeRouteEnter (to use methods). Because your component doesn't mounted yet.
NOTE 2: To avoid fetch duplication (DRY principle), you can modularize fetching (like vuex actions) and call it.

Nuxt js Router Push Not Working After Delete

I've created a simple CRUD with Nuxt. Data is provided by Lumen. I got a problem with the DELETE, data is deleted but Nuxt does not redirect to the other page.
Here is my script:
<script>
export default {
name: 'EmployeePage',
data() {
return {
fields: ['name','email','image','address'],
emplyees:[],
}
},
mounted() {
this.$axios.get('/employee').then(response => {
this.pegawais = response.data.data
}).catch(error => {
console.log(error.response.data)
})
},
methods: {
async delete(id) {
await this.$axios.delete(`/employee/${id}`).then(response => {
this.$router.push({ name: 'employee' }) <-----this redirect not working
})
}
}
}
</script>
I want Nuxt to redirect to the employee page that display all the data after the deletion.
You should not mix async/await and .then. Use the first approach, that way you will not have the .then callback hell and it will be cleaner overall.
Like this
<script>
export default {
name: 'EmployeePage',
data() {
return {
fields: ['name', 'email', 'image', 'address'],
emplyees: [],
}
},
async mounted() {
try {
const response = await this.$axios.get('/employee')
this.pegawais = response.data.data
} catch (error) {
console.log(error.response.data)
}
},
methods: {
async delete(id) {
await this.$axios.delete(`/employee/${id}`)
await this.$router.push({ name: 'employee' })
},
},
}
</script>
await this.$router.push does not require an await but it's a Promise too, so I'm writing it like that in case you need to call something else afterwards.
this.$axios.$get('/employee') can also be used if you want to remove a .data aka this.pegawais = response.data as shown here.

How can I update the comments without refreshing it?

First, I'm using vuex and axios.
store: commentService.js
components:
CommentBox.vue (Top components)
CommentEnter.vue (Sub components)
This is the logic of the code I wrote.
In the store called commentService.js, there are mutations called commentUpdate.
And There are actions called postComment and getComment.
At this time, In the component called CommentBox dispatches getComment with async created().
Then, in getComment, commentUpdate is commited and executed.
CommentUpdate creates an array of comments inquired by getComment and stores them in a state called commentList.
Then I'll get a commentList with "computed".
CommentEnter, a sub-component, uses the commentList registered as compounded in the CommentBox as a prop.
The code below is commentService.js.
import axios from 'axios'
export default {
namespaced: true,
state: () => ({
comment:'',
commentList: []
}),
mutations: {
commentUpdate(state, payload) {
Object.keys(payload).forEach(key => {
state[key] = payload[key]
})
}
},
actions: {
postComment(state, payload) {
const {id} = payload
axios.post(`http://??.???.???.???:????/api/books/${id}/comments`, {
comment: this.state.comment,
starRate: this.state.starRate
}, {
headers: {
Authorization: `Bearer ` + localStorage.getItem('user-token')
}
})
.then((res) => {
console.log(res)
this.state.comment = ''
this.state.starRate = ''
)
.catch((err) => {
alert('댓글은 한 책당 한 번만 작성할 수 있습니다.')
console.log(err)
this.state.comment = ''
this.state.starRate = ''
})
},
async getComment({commit}, payload) {
const {id} = payload
axios.get(`http://??.???.???.???:????/api/books/${id}/comments`)
.then((res) => {
console.log(res)
const { comment } = res.data.commentMap
commit('commentUpdate', {
commentList: comment
})
})
.catch((err) => {
console.log(err)
commit('commentUpdate', {
commentList: {}
})
})
}
}
}
The code below is CommentBox.vue
computed: {
commentList() {
return this.$store.state.commentService.commentList
}
},
methods: {
async newComment() {
if(this.$store.state.loginService.UserInfoObj.id === '') {
alert('로그인 후 이용할 수 있습니다.')
return
}
this.$store.dispatch('commentService/postComment', {
id: this.$route.params.id,
comment: this.$store.state.comment,
starRate: this.$store.state.starRate
})
}
},
async created() {
this.$store.dispatch('commentService/getComment', {
id: this.$route.params.id
})
}
The code below is CommentEnter.vue
created() {
this.userComment = this.comment
},
props: {
comment: {
type: Object,
default: () => {}
}
},
I asked for a lot of advice.
There were many comments asking for an axios get request after the axios post request was successful.
In fact, I requested an axios get within .then() of the axios post, and the network tab confirmed that the get request occurred normally after the post request.
But it's still not seen immediately when I register a new comment.
I can only see new comments when I refresh it.
How can I make a new comment appear on the screen right away when I register it?
Can't you just call getComment when postComment is finished?
methods: {
async newComment() {
if(this.$store.state.loginService.UserInfoObj.id === '') {
alert('로그인 후 이용할 수 있습니다.')
return
}
this.$store.dispatch('commentService/postComment', {
id: this.$route.params.id,
comment: this.$store.state.comment,
starRate: this.$store.state.starRate
}).then(function() {
this.$store.dispatch('commentService/getComment', {
id: this.$route.params.id
})
})
}
},
}
Or since you're using async:
methods: {
async newComment() {
if(this.$store.state.loginService.UserInfoObj.id === '') {
alert('로그인 후 이용할 수 있습니다.')
return
}
await this.$store.dispatch('commentService/postComment', {
id: this.$route.params.id,
comment: this.$store.state.comment,
starRate: this.$store.state.starRate
})
this.$store.dispatch('commentService/getComment', {
id: this.$route.params.id
})
}
},
}

TypeError when rendering property of Vue-test setData object

I'm running into a strange situation and can't figure out why. Basically in my HTML, if I render 'actor[0]', the test runs fine and the console log shows the entire 'actor' object present in setData
However, if I try to access a property of the 'actor' object, like actor[0].firstname, the test throws a TypeError-can't-read-property-of-undefined.
The weird part is console logging 'wrapper.vm.actor[0].firstname' works fine so it doesn't seem like an async issue.
myapps.spec.js
import { mount } from "#vue/test-utils";
import MyApps from "#/pages/myapps.vue";
import Vuetify from "vuetify";
describe("Testing Myapps", () => {
let vuetify;
beforeEach(() => {
vuetify = new Vuetify();
});
it("Checks SideBarComponent is rendered", async () => {
const wrapper = mount(MyApps, {
// localVue,
vuetify,
mocks: {
$vuetify: { breakpoint: {} }
},
stubs: {
SideBarComponent: true,
FooterComponent: true
}
});
await wrapper.setData({
actor: [
{
firstname: "bob",
lastname: "bob",
group: "actors"
}
]
});
console.log(wrapper.html()); // TypeError: Cannot read property 'first name' of undefined
console.log(wrapper.vm.actor[0].firstname); // "bob" if I set the template back to actor[0] so the test runs
});
});
myapps.vue
<template>
<div>
<v-app>
<v-col cols="3">
<v-btn
text
#click="getAcceptedApplications"
elevation="0"
block
>Accepted {{actor[0].firstname}}</v-btn>
</v-col>
</v-app>
</div>
</template>
<script>
export default {
async asyncData({ params, $axios, store }) {
try {
const body = store.getters.loggedInUser.id;
const [applications, actor] = await Promise.all([
$axios.$get(`/api/v1/apps/`, {
params: {
user: body
}
}),
$axios.$get(`/api/v1/actors/`, {
params: {
user: body
}
})
]);
return { applications, actor };
if (applications.length == 0) {
const hasApps = false;
}
} catch (error) {
if (error.response.status === 403) {
const hasPermission = false;
console.log(hasPermission, "perm");
console.error(error);
return { hasPermission };
}
}
},
data() {
return {
actor: []
};
}
};
</script>
Try not to use setData method, pass data while mounting the component like that:
const wrapper = mount(MyApps, {
vuetify,
mocks: {
$vuetify: { breakpoint: {} }
},
stubs: {
SideBarComponent: true,
FooterComponent: true
}
data: () => ({
actor: [
{
firstname: "bob",
lastname: "bob",
group: "actors"
}
]
})
})

Vuex update state by using store actions

I have two functions in my store, one that gets data by calling API and one that toggles change on cell "approved". Everything working fine, except that when I toggle this change it happens in database and I get the response that it is done but It doesn't update on UI.
I am confused, what should I do after toggling change to reflect change on UI, should I call my API from .then or should I call action method responsible for getting data from server.
export default {
state: {
drivers: {
allDrivers:[],
driversError:null
},
isLoading: false,
token: localStorage.getItem('token'),
driverApproved: null,
driverNotApproved: null
},
getters: {
driversAreLoading (state) {
return state.isLoading;
},
driverError (state) {
return state.drivers.driversError;
},
getAllDrivers(state){
return state.drivers.allDrivers
}
},
mutations: {
getAllDrivers (state) {
state.isLoading=true;
state.drivers.driversError=null;
},
allDriversAvailable(state,payload){
state.isLoading=false;
state.drivers.allDrivers=payload;
},
allDriversNotAvailable(state,payload){
state.isLoading=false;
state.drivers.driversError=payload;
},
toggleDriverApproval(state){
state.isLoading = true;
},
driverApprovalCompleted(state){
state.isLoading = false;
state.driverApproved = true;
},
driverApprovalError(state){
state.isLoading = false;
state.driverError = true;
}
},
actions: {
allDrivers (context) {
context.commit("getAllDrivers")
return new Promise((res,rej)=>{
http.get('/api/admin/getAllDrivers').then(
response=>{
if (response.data.success){
let data=response.data.data;
data=data.map(function (driver) {
return {
/* response */
};
});
context.commit("allDriversAvailable",data);
res();
}else {
context.commit("allDriversNotAvailable",response.data)
rej()
}
})
.catch(error=>{
context.commit("allDriversNotAvailable",error.data)
rej()
});
});
},
toggleDriverApproval (context, payload){
return new Promise((res, rej)=>{
http.post("/api/admin/toggleDriverApproval",{
driver_id: payload
})
.then( response => {
context.commit('driverApprovalCompleted');
res();
}).catch( error =>{
context.commit('driverApprovalError');
rej()
})
})
}
}
}
and here is the code on the view, I wrote the necessary code for better clarification of the problem
export default {
name: 'Drivers',
data: () => ({
data: [],
allDrivers: [],
driversErrors: []
}),
created() {
this.$store
.dispatch('allDrivers')
.then(() => {
this.data = this.$store.getters.getAllDrivers
})
.catch(() => {
this.errors = this.$store.getters.driverError
})
},
computed: {
isLoading() {
return this.$store.getters.driversAreLoading
}
},
methods: {
verify: function(row) {
console.log(row)
this.$store.dispatch('toggleDriverApproval', row.id).then(() => {
this.data = this.$store.getters.getAllDrivers
console.log('done dis')
})
},
},
}
if I understand your issue, you want the UI displaying your data to change to the updated data after making a post request.
If you are using Vuex you will want to commit a mutation, and use a getter display the data.
I am not sure how your post request is being handled on the server but if successful typically you would send a response back to your front end with the updated data, and commit a mutation with the updated data.
Example:
Make a Post request
toggleDriverApproval (context, payload){
return new Promise((res, rej)=>{
http.post("/api/admin/toggleDriverApproval",{
driver_id: payload
})
.then( response => {
context.commit('driverApprovalCompleted', response.data);
res();
}).catch( error =>{
context.commit('driverApprovalError', error.response.data);
rej()
})
})
}
If succesful commit the mutation
.then( response => {
context.commit('driverApprovalCompleted', response.data);
res();
})
response.data being your data you want to mutate the state with.
Mutation Example:
customMutation(state, data) {
state.driverApproval = data
}
Getter Example:
driver(state) {
return state.driverApproval
}
displaying the getter in a template
<template>
<div v-if="driver">{{driver}}</div>
</template>
<script>
import {mapGetters} from 'vuex'
export default {
name: Example,
computed: {
driver() {
return this.$store.getters.driver
},
// or use mapGetters
...mapGetters(['driver'])
}
}
</script>
more examples can be found at Vuex Docs