I am using nuxt.js and I´d like to create a folder to put all my services api.
So, I create a services folder called services and I put into it my first service api:
// ClientesService.js in services folder
export default ($axios) => ({
list() {
return $axios.get('clientes')
},
})
Now, in my component, I call the service, but I get an error
TypeError: _services_ClientesService__WEBPACK_IMPORTED_MODULE_12__.default.list is not a function
at _callee2$ (medicos.vue?9853:155:1)
at tryCatch (runtime.js?96cf:63:1)
at Generator.invoke [as _invoke] (runtime.js?96cf:294:1)
at Generator.eval [as next] (runtime.js?96cf:119:1)
at asyncGeneratorStep (asyncToGenerator.js?1da1:3:1)
at _next (asyncToGenerator.js?1da1:25:1)
at eval (asyncToGenerator.js?1da1:32:1)
at new Promise (<anonymous>)
at eval (asyncToGenerator.js?1da1:21:1)
at VueComponent.getEspecialidades (medicos.vue?9853:155:1)
// My component
....
<script>
import ClientesService from '#/services/ClientesService'
export default {
name: 'Medicos',
data: () => ({
loading:false,
especialidades:[]
}),
async mounted() {
await this.getEspecialidades()
},
methods: {
async getEspecialidades() {
this.loading = true
try {
const resp = await ClientesService.list()
this.especialidades = resp.data
} catch (error) {
console.log(error)
} finally {
this.loading = false
}
},
},
}
</script>
I'd recommend create an ApiService that is just an axios instance (where you can define base api url, default headers, interceptors, etc) and use it in all other services. The code should look something like
import axios from 'axios';
const ApiService = new axios.create({
timeout: 30000,
baseURL: 'YOUR_API_URL',
headers: {
buildApplicationVersion: '1.0',
},
});
export default ApiService;
import ApiService from '#/Services/ApiService';
const list = () => {
return ApiService.get('clientes')
}
const CustomerService = {
list
}
export default CustomerService;
Related
I'm using vue2 with composition Api, vuex and apollo client to request a graphql API and I have problems when mocking composable functions with jest
// store-service.ts
export function apolloQueryService(): {
// do some graphql stuff
return { result, loading, error };
}
// store-module.ts
import { apolloQueryService } from 'store-service'
export StoreModule {
state: ()=> ({
result: {}
}),
actions: {
fetchData({commit}) {
const { result, loading, error } = apolloQueryService()
commit('setState', result);
}
},
mutations: {
setState(state, result): {
state.result = result
}
}
}
The Test:
// store-module.spec.ts
import { StoreModule } from store-module.ts
const store = StoreModule
describe('store-module.ts', () => {
beforeEach(() => {
jest.mock('store-service', () => ({
apolloQueryService: jest.fn().mockReturnValue({
result: { value: 'foo' }, loading: false, error: {}
})
}))
})
test('action', async ()=> {
const commit = jest.fn();
await store.actions.fetchData({ commit });
expect(commit).toHaveBeenCalledWith('setData', { value: 'foo' });
})
}
The test fails, because the commit gets called with ('setData', { value: undefined }) which is the result from the original apolloQueryService. My Mock doesn't seem to work. Am I doing something wrong? Appreciate any help, thanks!
Try this :
// store-module.spec.ts
import { StoreModule } from store-module.ts
// first mock the module. use the absolute path to store-service.ts from the project root
jest.mock('store-service');
// then you import the mocked module.
import { apolloQueryService } from 'store-service';
// finally, you add the mock return values for the mock module
apolloQueryService.mockReturnValue({
result: { value: 'foo' }, loading: false, error: {}
});
/* if the import order above creates a problem for you,
you can extract the first step (jest.mock) to an external setup file.
You should do this if you are supposed to mock it in all tests anyway.
https://jestjs.io/docs/configuration#setupfiles-array */
const store = StoreModule
describe('store-module.ts', () => {
test('action', async ()=> {
const commit = jest.fn();
await store.actions.fetchData({ commit });
expect(commit).toHaveBeenCalledWith('setData', { value: 'foo' });
})
}
I am using Nuxt.js and want to test my page which uses asyncData with Jest. I have a factory function to set up my wrapper, but it basically returns a shallowMount.
Expected
When clicking a button I want the function to behave differently depending on the query parameter. When running the test I want to mock this by setting it directly when creating the wrapper (Similar to setting propsData). E.g. const wrapper = factory({ propsData: { myQueryParam: 'some-value' } });
Result
However trying to set propsData still returns undefined: console.log(wrapper.vm.myQueryParam); // undefined while I would expect it to be 'some-value'
Question
Is there a different approach on how I can test this function that relies on query parameters?
Because asyncData is called before Vue is initialised, it means shallowMount doesn't work right out of the box.
Example:
page:
<template>
<div>Your template.</div>
</template>
<script>
export default {
data() {
return {}
},
async asyncData({
params,
error,
$axios
}) {
await $axios.get("something")
}
}
</script>
test:
import { shallowMount } from "#vue/test-utils";
describe('NewsletterConfirm', () => {
const axiosGetMock = jest.fn()
const axiosPostMock = jest.fn()
var getInitialised = async function (thumbprint) {
if (thumbprint == undefined) throw "thumbprint not provided"
let NewsletterConfirm = require('./_thumbprint').default
if (!NewsletterConfirm.asyncData) {
return shallowMount(NewsletterConfirm);
}
let originalData = {}
if (NewsletterConfirm.data != null) {
originalData = NewsletterConfirm.data()
}
const asyncData = await NewsletterConfirm.asyncData({
params: {
thumbprint
},
error: jest.fn(),
$axios: {
get: axiosGetMock,
post: axiosPostMock
}
})
NewsletterConfirm.data = function () {
return {
...originalData,
...asyncData
}
}
return shallowMount(NewsletterConfirm)
}
it('calls axios', async () => {
let result = await getInitialised("thumbprint")
expect(axiosGetMock).toHaveBeenCalledTimes(1)
});
});
Credits to VladDubrovskis for his comment: in this nuxt issue
I have an AuthService that I use in a namespaced store in my Nuxt app. I need to commit mutations from AuthService to the namespaced store but I can't figure out how to import the store into my AuthService.
I've seen examples where the store is imported into the JS file, but the store is explicitly defined in the Vue app. Because I'm using Nuxt with the Module mode for my store, I'm not sure of the root path where I can import my store into the AuthService file. As I understand it, Nuxt handles creating the root store and all the namespaced store behind the scenes when use "Module mode"
My Nuxt store directory includes index.js (which is empty) and auth.js which has the mutations I want to call from AuthService.
auth.js
import AuthService from '../firebase/authService'
const authService = new AuthService()
export const state = () => ({
user: null
})
export const mutations = {
setUser (state, user) {
state.user = user
}
}
export const actions = {
async signUp ({ commit }, payload) {
try {
await authServices.createUser(payload)
return Promise.resolve()
} catch (err) {
const notification = {
duration: 5000,
message: err.message,
type: 'error'
}
commit('ui/activateNotification', notification, { root: true })
return Promise.reject()
}
}
}
authService.js
import { fAuth, fDb } from './config'
// I think I need to import auth store here but I'm not sure how
export default class AuthClient {
async createUser (payload) {
try {
const res = await fAuth.createUserWithEmailAndPassword(payload.email, payload.password)
const { uid } = res.user
const user = {
...payload,
uid
}
await this._createUserDoc(user)
this._initAuthListener()
return Promise.resolve()
} catch (err) {
return Promise.reject(err)
}
}
async _createUserDoc (user) {
await fDb.collection('users').doc(user.uid).set(user)
}
_initAuthListener () {
fAuth.onAuthStateChanged(async (user) => {
try {
if (user) {
const userProfileRef = fDb.collection('users').doc(user.uid)
const userProfileDoc = await userProfileRef.get()
const { uid, userName } = userProfileDoc.data()
// Here is where I want to call a mutation from the auth store
this.store.commit('setUser', {
uid,
userName
})
} else {
this.store.commit('setUser', null)
}
} catch (err) {
console.log(err)
}
})
}
}
I'd like to propose a solution using a plugin.
In the external module (externalModule.js) we define store variable and export an init function that receives Nuxt context as argument. The function assignes the store from context to the variable which can be now used in the module:
let store;
export function init (context) {
store = context.store;
};
(...further business logic using store)
Then in the plugins folder we create a plugin file (let's call it storeInit.js). The file imports the init function from the external module and exports default plugin function required by Nuxt. The function receives context from Nuxt and we call the init function passing the context further:
import { init } from '[pathTo]/externalModule.js';
export default (context, inject) => {
init(context);
};
Then we register the plugin in the nuxt.config.js file:
module.exports = {
...
plugins: [
{ src: '~/plugins/storeInit' }
],
...
}
This way when the app is built by Nuxt and plugins are registered, the context object is passed to the external module and we can use anything from it, among others the store.
In index.js file which is in store folder you need to return store like this
import Vuex from 'vuex'
const createStore = () => {
return new Vuex.Store({
state: {
counter: 0
},
mutations: {
increment (state) {
state.counter++
}
}
})
}
export default createStore
and in your authService.js file you need to import store like this
import $store from '~/store'
by this you will be able to access your store
$store.commit('setUser', null)
I hope this works for you
Important Note: you don't need to install vuex because it is already shipped with nuxtjs
You can access as window.$nuxt.$store
Note: My nuxt version is 2.14.11
I have simple Vue component that fetches API key when it is created and key can be renewed by clicking on button:
<template>
<div>
<div>{{data.api_key}}</div>
<button ref="refresh-trigger" #click="refreshKey()">refresh</button>
</div>
</template>
<script>
export default {
created() {
axios.get(this.url).then((response) => {
this.data = response.data
})
}
methods: {
refreshKey() {
axios.put(this.url).then((response) => {
this.data = response.data
})
},
}
}
</script>
And I want to test it with this code:
import {shallowMount} from '#vue/test-utils';
import axios from 'axios';
import apiPage from '../apiPage';
import MockAdapter from 'axios-mock-adapter';
describe('API page', () => {
it('should renew API key it on refresh', async (done) => {
const flushPromises = () => new Promise(resolve => setTimeout(resolve))
const initialData = {
api_key: 'initial_API_key',
};
const newData = {
api_key: 'new_API_key',
};
const mockAxios = new MockAdapter(axios);
mockAxios.onGet('/someurl.json').replyOnce(200, initialData)
mockAxios.onPut('/someurl.json').replyOnce(200, newData);
const wrapper = shallowMount(api);
expect(wrapper.vm.$data.data.api_key).toBeFalsy();
await flushPromises()
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.$data.data.api_key).toEqual(initialData.api_key);
done()
});
wrapper.find({ref: 'refresh-trigger'}).trigger('click');
wrapper.vm.$nextTick(() => {
console.log(mockAxios.history)
expect(wrapper.vm.$data.data.api_key).toEqual(newData.api_key);
expect(mockAxios.history.get.length).toBe(1);
expect(mockAxios.history.get[1].data).toBe(JSON.stringify(initialData));
expect(mockAxios.history.put.length).toBe(1);
done();
});
})
});
But it turns out only get request is mocked because i receive:
[Vue warn]: Error in nextTick: "Error: expect(received).toEqual(expected)
Difference:
- Expected
+ Received
- new_API_key
+ initial_API_key"
found in
---> <Anonymous>
<Root>
console.error node_modules/vue/dist/vue.runtime.common.dev.js:1883
{ Error: expect(received).toEqual(expected)
Even worse, console.log(mockAxios.history) returns empty put array:
{ get:
[ { transformRequest: [Object],
transformResponse: [Object],
timeout: 0,
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
maxContentLength: -1,
validateStatus: [Function: validateStatus],
headers: [Object],
method: 'get',
url: '/admin/options/api.json',
data: undefined } ],
post: [],
head: [],
delete: [],
patch: [],
put: [],
options: [],
list: [] }
I tried to define mockAxios in describe block, and console.log it after iteration - and it turns out that put request was here. But not when I needed it. :)
What am i doing wrong? Maybe there are some ways to check if created callback was called and all async functions inside it are done? Maybe i'm using axios-mock wrong?
This test code should pass:
import {shallowMount, createLocalVue} from '#vue/test-utils';
import axios from 'axios';
import api from '#/components/api.vue';
import MockAdapter from 'axios-mock-adapter';
describe('API page', () => {
it('should renew API key it on refresh', async () => {
const flushPromises = () => new Promise(resolve => setTimeout(resolve))
const initialData = {
api_key: 'initial_API_key',
};
const newData = {
api_key: 'new_API_key',
};
const mockAxios = new MockAdapter(axios);
const localVue = createLocalVue();
mockAxios
.onGet('/someurl.json').reply(200, initialData)
.onPut('/someurl.json').reply(200, newData);
const wrapper = shallowMount(api, {
localVue,
});
expect(wrapper.vm.$data.data.api_key).toBeFalsy();
await flushPromises();
expect(wrapper.vm.$data.data.api_key).toEqual(initialData.api_key);
wrapper.find({ref: 'refresh-trigger'}).trigger('click');
await flushPromises();
console.log(mockAxios.history);
expect(wrapper.vm.$data.data.api_key).toEqual(newData.api_key);
expect(mockAxios.history.get.length).toBe(1);
expect(mockAxios.history.put.length).toBe(1);
})
});
A few notes:
I prefer to chain the responses on the mockAxios object, that way you can group them by URL so it's clear which endpoint you're mocking:
mockAxios
.onGet('/someurl.json').reply(200, initialData)
.onPut('/someurl.json').reply(200, newData);
mockAxios
.onGet('/anotherUrl.json').reply(200, initialData)
.onPut('/anotherUrl.json').reply(200, newData);
If you want to test that you only made one GET call to the endpoint (with expect(......get.length).toBe(1)) then you should really use reply() instead of replyOnce() and test it the way you're doing it already. The replyOnce() function will remove the handler after replying first time and you'll be getting 404s in your subsequent requests.
mockAxios.history.get[1].data will not contain anything for 3 reasons: GET requests don't have a body (only URL parameters), you only made 1 GET request (here you're checking 2nd GET), and this statement refers to the request that was sent, not data you received.
You're using async/await feature, which means you can take advantage of that for $nextTick: await wrapper.vm.$nextTick(); and drop the done() call all together, but since you already have flushPromises() you might as well use that.
You don't need to test that you received initialData in the 1st call with this line:
expect(mockAxios.history.get[1].data).toBe(JSON.stringify(initialData)); since you're already testing it with expect(...).toEqual(apiKey).
Use createLocalVue() utility to create a local instance of Vue for each mount to avoid contaminating the global Vue instance (useful if you have multiple test groups)
and finally, 7. it's best to break this test up into multiple it statements; unit tests should be microtests, i.e. test a small, clearly identifiable behaviour. Although I didn't break the test up for you so it contains as little changes as possible, I'd highly recommend doing it.
I created a new vue project using the command vue create axe using vue-cli-3.0.016beta. Then installed axios using npm install axios --save. In the main.js file I imported axios as shown below.
import Vue from 'vue'
import App from './App.vue'
import axios from 'axios'
Vue.config.productionTip = false
Vue.use(axios)
new Vue({
render: h => h(App)
}).$mount('#app')
There is not a bit of code change other than this. Still I get an error like the following:
Unhandled promise rejection
TypeError
columnNumber: 7
fileName: "http://localhost:8080/app.js line 1065 > eval"
lineNumber: 57
message: "parsed is undefined"
stack: "isURLSameOrigin#webpack-internal:///./node_modules/axios/lib/helpers/isURLSameOrigin.js:57:7\ndispatchXhrRequest#webpack-internal:///./node_modules/axios/lib/adapters/xhr.js:109:50\nPromise#webpack-internal:///./node_modules/core-js/modules/es6.promise.js:177:7\nxhrAdapter#webpack-internal:///./node_modules/axios/lib/adapters/xhr.js:12:10\ndispatchRequest#webpack-internal:///./node_modules/axios/lib/core/dispatchRequest.js:59:10\nrun#webpack-internal:///./node_modules/core-js/modules/es6.promise.js:75:22\nnotify/<#webpack-internal:///./node_modules/core-js/modules/es6.promise.js:92:30\nflush#webpack-internal:///./node_modules/core-js/modules/_microtask.js:18:9\n"
__proto__: Object { stack: "", … }
I want to axios globally to use interceptors, hence calling it here in main.js. But if I use it in a view-page there is no error!
is this a bug or I'm doing it wrong? Kindly help me to fix this and use axios globally.
Thanks
so the error I see is here
Vue.use(axios)
Vue.use expects a vue installable plugin.
You could have a look at vue-axios
import axios from 'axios'
import VueAxios from 'vue-axios'
Vue.use(VueAxios, axios)
but I would highly discourage it.
It's best to create your own ApiHandler.js file that handles all the remote stuff separately, and you can easily call from anywhere including vue components and vuex.
here is the beginning of my class
<script>
import axios from 'axios';
class ApiHandler{
constructor(apiUrl) {
this.axios = axios;
this.apiUrl = apiUrl || ''; // this line allow passing a custom endpoint for testing
this.config = {
headers: { 'Cache-Control': 'no-cache' }, // can setup to prevent all caching
baseURL: this.apiUrl,
};
}
/**
* #param {Object} payload
* #param {String} payload.username
* #param {String} payload.password
*/
login({ username, password }) {
return new Promise((resolve, reject) => {
this.axios.post('/api/login', { username: username.toLowerCase(), password }, this.config)
.then((response) => {
if (response.code === 200 && response.body && response.body.token) {
resolve(response.body.token);
} else {
reject('Bad Login');
}
})
.catch((err) => {
reject('internal error');
});
});
}
}
</script>
you can then call this from anywhere by...
<script>
import ApiHandler from '../lib/ApiHandler';
const apiRequest = new ApiRequest();
// and then anywhere in the script
let payload = {
username:'someuser',
password:'somepassword',
};
apiRequest.login(payload)
.then(()=>{
// yay - I'm logged in
})
.catch(err => {
// oh oh, display error
})
</script>
this gives you much more flexibility and allows you to separate the remote actions and allows doing first-leg response handling separate of your component, which allows more re-usability.
instead of
Vue.use(axios);
you should
Vue.prototype.$axios = axios;
then you can use it globally
login() {
this.$axios.post('<host>/api/login', data)
.then((res) => { // dosomething })
.catch((err) => { // dosomething });
}
if you want to add globally interceptors with axios, you can
// Add a request interceptor
axios.interceptors.request.use(function (config) {
// Do something before request is sent
return config;
}, function (error) {
// Do something with request error
return Promise.reject(error);
});
// Add a response interceptor
axios.interceptors.response.use(function (response) {
// Do something with response data
return response;
}, function (error) {
// Do something with response error
return Promise.reject(error);
});
// and
Vue.prototype.$axios = axios;