Implement login command and access vuex store - vue.js

I have a login process where after sending a request to the server and getting a response, I do this:
this.$auth.setToken(response.data.token);
this.$store.dispatch("setLoggedUser", {
username: this.form.username
});
Now I'd like to emulate this behavior when testing with cypress, so i don't need to actually login each time I run a test.
So I've created a command:
Cypress.Commands.add("login", () => {
cy
.request({
method: "POST",
url: "http://localhost:8081/api/v1/login",
body: {},
headers: {
Authorization: "Basic " + btoa("administrator:12345678")
}
})
.then(resp => {
window.localStorage.setItem("aq-username", "administrator");
});
});
But I don't know how to emulate the "setLoggedUser" actions, any idea?

In your app code where you create the vuex store, you can conditionally expose it to Cypress:
const store = new Vuex.Store({...})
// Cypress automatically sets window.Cypress by default
if (window.Cypress) {
window.__store__ = store
}
then in your Cypress test code:
cy.visit()
// wait for the store to initialize
cy.window().should('have.property', '__store__')
cy.window().then( win => {
win.__store__.dispatch('myaction')
})
You can add that as another custom command, but ensure you have visited your app first since that vuex store won't exist otherwise.

Step 1: Inside main.js provide the store to Cypress:
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
if (window.Cypress) {
// Add `store` to the window object only when testing with Cypress
window.store = store
}
Step 2: Inside cypress/support/commands.js add a new command:
Cypress.Commands.add('login', function() {
cy.visit('/login') // Load the app in order `cy.window` to work
cy.window().then(window => { // .then() to make cypress wait until window is available
cy.wrap(window.store).as('store') // alias the store (can be accessed like this.store)
cy.request({
method: 'POST',
url: 'https://my-app/api/auth/login',
body: {
email: 'user#gmail.com',
password: 'passowrd'
}
}).then(res => {
// You can access store here
console.log(this.store)
})
})
})
Step 4: Inside cypress/integration create a new test
describe('Test', () => {
beforeEach(function() {
cy.login() // we run our custom command
})
it('passes', function() { // pass function to keep 'this' context
cy.visit('/')
// we have access to this.store in our test
cy.wrap(this.store.state.user).should('be.an', 'object')
})
})

Related

Vue3 / Vuex State is empty when dispatching action inside of lifecycle hook inside of test

We're using the composition API with Vue 3.
We have a Vuex store that, amongst other things, stores the currentUser.
The currentUser can be null or an object { id: 'user-uuid' }.
We're using Vue Test Utils, and they've documented how to use the store inside of tests when using the Composition API. We're using the store without an injection key, and so they document to do it like so:
import { createStore } from 'vuex'
const store = createStore({
// ...
})
const wrapper = mount(App, {
global: {
provide: {
store: store
},
},
})
I have a component and before it is mounted I want to check if I have an access token and no user currently in the store.
If this is the case, we want to fetch the current user (which is an action).
This looks like so:
setup() {
const tokenService = new TokenService();
const store = useStore();
onBeforeMount(async () => {
if (tokenService.getAccessToken() && !store.state.currentUser) {
await store.dispatch(FETCH_CURRENT_USER);
console.log('User: ', store.state.currentUser);
}
});
}
I then have a test for this that looks like this:
it('should fetch the current user if there is an access token and user does not exist', async () => {
localStorage.setItem('access_token', 'le-token');
await shallowMount(App, {
global: {
provide: {
store
}
}
});
expect(store.state.currentUser).toStrictEqual({ id: 'user-uuid' });
});
The test fails, but interestingly, the console log of the currentUser in state is not empty:
console.log src/App.vue:27
User: { id: 'user-uuid' }
Error: expect(received).toStrictEqual(expected) // deep equality
Expected: {"id": "user-uuid"} Received: null
Despite the test failure, this works in the browser correctly.
Interestingly, if I extract the logic to a method on the component and then call that from within the onBeforeMount hook and use the method in my test, it passes:
setup() {
const tokenService = new TokenService();
const store = useStore();
const rehydrateUserState = async () => {
if (tokenService.getAccessToken() && !store.state.currentUser) {
await store.dispatch(FETCH_CURRENT_USER);
console.log('User: ', store.state.currentUser);
}
};
onBeforeMount(async () => {
await rehydrateUserState();
});
return {
rehydrateUserState
};
}
it('should fetch the current user if there is an access token and user does not exist', async () => {
localStorage.setItem('access_token', 'le-token');
await cmp.vm.rehydrateUserState();
expect(store.state.currentUser).toStrictEqual({ id: 'user-uuid' });
});
Any ideas on why this works when extracted to a method but not when inlined into the onBeforeMount hook?

Mock put requests with mock-axios-adapter

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.

Nuxt Ava End-to-End Testing Store Configuration

Given the example official Nuxt end-to-end test example using Ava:
import test from 'ava'
import { Nuxt, Builder } from 'nuxt'
import { resolve } from 'path'
// We keep a reference to Nuxt so we can close
// the server at the end of the test
let nuxt = null
// Init Nuxt.js and start listening on localhost:4000
test.before('Init Nuxt.js', async t => {
const rootDir = resolve(__dirname, '..')
let config = {}
try { config = require(resolve(rootDir, 'nuxt.config.js')) } catch (e) {}
config.rootDir = rootDir // project folder
config.dev = false // production build
config.mode = 'universal' // Isomorphic application
nuxt = new Nuxt(config)
await new Builder(nuxt).build()
nuxt.listen(4000, 'localhost')
})
// Example of testing only generated html
test('Route / exits and render HTML', async t => {
let context = {}
const { html } = await nuxt.renderRoute('/', context)
t.true(html.includes('<h1 class="red">Hello world!</h1>'))
})
// Close the Nuxt server
test.after('Closing server', t => {
nuxt.close()
})
How can you use Nuxt or Builder to configure/access the applications Vuex store? The example Vuex store would look like:
import Vuex from "vuex";
const createStore = () => {
return new Vuex.Store({
state: () => ({
todo: null
}),
mutations: {
receiveTodo(state, todo) {
state.todo = todo;
}
},
actions: {
async nuxtServerInit({ commit }, { app }) {
console.log(app);
const todo = await app.$axios.$get(
"https://jsonplaceholder.typicode.com/todos/1"
);
commit("receiveTodo", todo);
}
}
});
};
export default createStore;
Currently trying to run the provided Ava test, leads to an error attempting to access #nuxtjs/axios method $get:
TypeError {
message: 'Cannot read property \'$get\' of undefined',
}
I'd be able to mock $get and even $axios available on app in Vuex store method nuxtServerInit, I just need to understand how to access app in the test configuration.
Thank you for any help you can provide.
Just encountered this and after digging so many tutorial, I pieced together a solution.
You have essentially import your vuex store into Nuxt when using it programmatically. This is done by:
Importing Nuxt's config file
Adding to the config to turn off everything else but enable store
Load the Nuxt instance and continue your tests
Here's a working code (assuming your ava and dependencies are set up)
// For more info on why this works, check this aweomse guide by this post in getting this working
// https://medium.com/#brandonaaskov/how-to-test-nuxt-stores-with-jest-9a5d55d54b28
import test from 'ava'
import jsdom from 'jsdom'
import { Nuxt, Builder } from 'nuxt'
import nuxtConfig from '../nuxt.config' // your nuxt.config
// these boolean switches turn off the build for all but the store
const resetConfig = {
loading: false,
loadingIndicator: false,
fetch: {
client: false,
server: false
},
features: {
store: true,
layouts: false,
meta: false,
middleware: false,
transitions: false,
deprecations: false,
validate: false,
asyncData: false,
fetch: false,
clientOnline: false,
clientPrefetch: false,
clientUseUrl: false,
componentAliases: false,
componentClientOnly: false
},
build: {
indicator: false,
terser: false
}
}
// We keep a reference to Nuxt so we can close
// the server at the end of the test
let nuxt = null
// Init Nuxt.js and start listening on localhost:5000 BEFORE running your tests. We are combining our config file with our resetConfig using Object.assign into an empty object {}
test.before('Init Nuxt.js', async (t) => {
t.timeout(600000)
const config = Object.assign({}, nuxtConfig, resetConfig, {
srcDir: nuxtConfig.srcDir, // don't worry if its not in your nuxt.config file. it has a default
ignore: ['**/components/**/*', '**/layouts/**/*', '**/pages/**/*']
})
nuxt = new Nuxt(config)
await new Builder(nuxt).build()
nuxt.listen(5000, 'localhost')
})
// Then run our tests using the nuxt we defined initially
test.serial('Route / exists and renders correct HTML', async (t) => {
t.timeout(600000) // Sometimes nuxt's response is slow. We increase the timeont to give it time to render
const context = {}
const { html } = await nuxt.renderRoute('/', context)
t.true(html.includes('preload'))
// t.true(true)
})
test.serial('Route / exits and renders title', async (t) => {
t.timeout(600000)
const { html } = await nuxt.renderRoute('/', {})
const { JSDOM } = jsdom // this was the only way i could get JSDOM to work. normal import threw a functione error
const { document } = (new JSDOM(html)).window
t.true(document.title !== null && document.title !== undefined) // simple test to check if site has a title
})
Doing this should work. HOWEVER, You may still get some errors
✖ Timed out while running tests. If you get this you're mostly out of luck. I thought the problem was with Ava given that it didn't give a descriptive error (and removing any Nuxt method seemed to fix it), but so far even with the above snippet sometimes it works and sometimes it doesn't.
My best guess at this time is that there is a delay on Nuxt's side using either renderRouter or renderAndGetWindow that ava doesn't wait for, but on trying any of these methods ava almost immediately "times out" despite the t.timeout being explicitly set for each test. So far my research has lead me to checking the timeout for renderAndGetWindow (if it exists, but the docs doesn't indicate such).
That's all i've got.

How to use axios in Vue2 project created with vue-cli3

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;

Axios interceptor in vue 2 JS using vuex

I store token after success login call in vuex store like this:
axios.post('/api/auth/doLogin.php', params, axiosConfig)
.then(res => {
console.log(res.data); // token
this.$store.commit('login', res.data);
})
axiosConfig is file where I only set baseURL export default { baseURL: 'http://localhost/obiezaca/v2' } and params is just data sent to backend.
My vuex file looks is:
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {
logged: false,
token: ''
},
mutations: {
login: (state, response) => {
state.logged = true;
state.token = response;
console.log('state updated');
console.log('state.logged flag is: '+state.logged);
console.log('state.token: '+state.token);
},
logout: (state) => {
state.logged = false;
state.token = '';
}
}
});
It is working correctly, I can re-render some of content in my SPA basing on v-if="this.$store.state.logged" for logged user. I'm able to access this.$store.state.logged from any component in my entire app.
Now I want to add my token to every request which call my rest API backend. I've created basic axios http interceptor which looks like this:
import axios from 'axios';
axios.interceptors.request.use(function(config) {
const token = this.$store.state.token;
if(token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
}, function(err) {
return Promise.reject(err);
});
Now I have 2 problems/questions about it.
I know that it is available to use this.$store.state.logged or this.$store.state.token across every component but can I use it same way in single javascript file?
Where should I execute/start my interceptor javascript file? It is independent file which lays in my app main folder but I am not calling it anywhere, in angularJS which I was working before, I had to add $httpProvider.interceptors.push('authInterceptorService'); in config but I don't know how to do same thing in vue architecture. So where should I inject my interceptor?
EDIT
I followed GMaiolo tips I added
import interceptor from './helpers/httpInterceptor.js';
interceptor();
to my main.js file and I refactor my interceptor to this:
import axios from 'axios';
import store from '../store/store';
export default function execute() {
axios.interceptors.request.use(function(config) {
const token = this.$store.state.token;
if(token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
}, function(err) {
return Promise.reject(err);
});
}
Result of this changes is that every already existing backend calls ( GET ) which don't need token to work stopped working but it is logical because I didn't clarified to which request it should add token so it is trying to add it everywhere and in my interceptor something is still wrong and that is why every already exisitng request stopped working.
When I try to do backend POST call in browser console I still get this error:
TypeError: Cannot read property '$store' of undefined
Although I import store to my interceptor file. Any ideas? I can provide some more information if any needed.
I additionally add screenshot of this main, store and interceptor tree structure so you can see that I'm importing fron correct path:
1.
First of all I'd use a Vuex Module as this Login/Session behavior seems to be ideal for a Session module. After that (which is totally optional) you can set up a Getter to avoid accessing the state itself from outside Vuex, you'd would end up with something like this:
state: {
// bear in mind i'm not using a module here for the sake of simplicity
session: {
logged: false,
token: ''
}
},
getters: {
// could use only this getter and use it for both token and logged
session: state => state.session,
// or could have both getters separated
logged: state => state.session.logged,
token: state => state.session.token
},
mutations: {
...
}
With those getters set, you can get the values a bit easier from components. With either using this.$store.getters.logged (or the one you'd want to use) or using the mapGetters helper from Vuex [for more info about this you can check the getters docs]:
import { mapGetters } from 'vuex'
export default {
// ...
computed: {
...mapGetters([
'logged',
'token'
])
}
}
2.
I like to run Axios' interceptors along with Vue instantation in main.js creating, importing and executing an interceptors.js helper. I'd leave an example so you get an idea, but, then again, this is my own preference:
main.js
import Vue from 'vue';
import store from 'Src/store';
import router from 'Src/router';
import App from 'Src/App';
// importing the helper
import interceptorsSetup from 'Src/helpers/interceptors'
// and running it somewhere here
interceptorsSetup()
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
store,
template: '<App/>',
components: { App }
});
interceptors.js
import axios from 'axios';
import store from 'your/store/path/store'
export default function setup() {
axios.interceptors.request.use(function(config) {
const token = store.getters.token;
if(token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
}, function(err) {
return Promise.reject(err);
});
}
And there you'd end up having all the behavior cleanly encapsulated.
I did the same logic. however, I just change the file name. I used axios/index.js but the store is undefined there. so I just change the file name axios/interceptor.js and Don't know store data is accessible look at my below image