In my Nuxt3 project, I have a very basic login page which is SSR like the following:
pages/login.vue
<template>
<form
#submit.prevent="login">
<input
name="email"
v-model="form.email"/>
<input
name="password"
v-model="form.password"/>
<button>login</button>
<form>
</template>
<script setup>
import { useAuth } from '~/store/auth.js';
const auth = useAuth();
const form = ref({email: '', password: ''});
const router = useRouter();
const login = async () => {
useFetch('/api/auth/tokens', {
method: 'POST',
body: form.value
})
.then(({ data }) => {
const { token, user } = data.value;
auth.setUser(user);
auth.setToken(token);
router.push('/profile');
})
}
</script>
First, I try to test it like a SPA page:
import { describe, test, expect } from 'vitest';
import { mount } from '#vue/test-utils';
import LoginPage from '~/pages/login.vue';
describe('login page', async () => {
test('store tokens', async () => {
const wrapper = mount(LoginPage);
const email = 'test#example.com';
const password = 'password';
await wrapper.find('[name="email"]').setValue(email);
await wrapper.find('[name="password"]').setValue(password);
await wrapper.find('form').trigger('submit.prevent');
expect(
wrapper.emitted('submit')[0][0]
).toStrictEqual({
email,
password,
});
// But this does not test the code in `then()` which handled the response and redirect to `/profile`
});
});
Got error:
FAIL tests/pages/login.spec.js > login page > store tokens
ReferenceError: document is not defined
Then, I followed the Nuxt3 testing
import { describe, test, expect } from 'vitest';
import { setup, $fetch } from '#nuxt/test-utils';
import { JSDOM } from 'jsdom';
describe('login page', async () => {
await setup();
test('store tokens', async () => {
const wrapper = (new JSDOM(await $fetch('/login'))).window.document;
const email = 'test#example.com';
const password = 'password';
await wrapper.find('[name="email"]').setValue(email);
await wrapper.find('[name="password"]').setValue(password);
await wrapper.find('form').trigger('submit.prevent');
expect($fetch).toHaveBeenCalledWith('/api/auth/tokens', {
method: 'POST',
body: {
email: 'test#example.com',
password: 'password'
}
});
});
});
But wrapper there is just a jsdom document instance, it can't act like a Vue component.
I wonder:
How to test the user input events?
How to test the code in resovle of useFetch() (in the example, it's the code handling data with pinia)
same question in GitHub
I would unit test the component, so check if inputs work as expected and also if the correct action is triggered on submit. So don't render a whole page, just mount the component in the test.
I would test the login action afterwards separately meaning you mock the data from the API (token, user) and check if they are set correctly to the auth. Plus, if there's a redirect. This should be possible.
You want to test the whole feature (login) which is often referred as "feature testing". But the base is unit tests.
Related
How do I mock authenticate with Nuxt and Cypress?
I have a FastAPI backend that issues a JWT to a frontend NuxtJS application. I want to test the frontend using Cypress. I am battling to mock authenticate.
Here is a simple Cypress test:
// cypress/e2e/user_authentication_test.cy.js
describe("A User logging in", () => {
it.only("can login by supplying the correct credentials", () => {
cy.mockLogin().then(() => {
cy.visit(`${Cypress.env("BASE_URL")}/dashboard`)
.window()
.its("$nuxt.$auth")
.its("loggedIn")
.should("equal", true);
});
});
});
The test above fails at the should assertion, and the user is not redirected.
The mockLogin command is defined as:
// cypress/support/commands.js
Cypress.Commands.add(
'mockLogin',
(username = 'someone', password = 'my_secret_password_123') => {
cy.intercept('POST', 'http://localhost:5000/api/v1/auth/token', {
fixture: 'auth/valid_auth_token.json',
}).as('token_mock')
cy.visit(`${Cypress.env('BASE_URL')}/login`)
cy.get('#login-username').type(username)
cy.get('#login-password').type(`${password}{enter}`)
cy.wait('#token_mock')
}
)
Where valid_auth_token.json contains a JWT.
The actual login is done as follows:
<!-- components/auth/LoginForm.vue -->
<template>
<!-- Login form goes here -->
</template>
<script>
import jwt_decode from 'jwt-decode' // eslint-disable-line camelcase
export default {
name: 'LoginForm',
data() {
return {
username: '',
password: '',
}
},
methods: {
async login() {
const formData = new FormData()
formData.append('username', this.username)
formData.append('password', this.password)
try {
await this.$auth
.loginWith('cookie', {
data: formData,
})
.then((res) => {
const decode = jwt_decode(res.data.access_token) // eslint-disable-line camelcase
this.$auth.setUser(decode)
this.$router.push('/')
})
} catch (error) {
// error handling
}
},
},
}
</script>
I am currently trying to start a Vue app which will contain a user login.
For some reason the I have been struggling with getting this router redirect to work.
The implementation is straight from the vueschool page and specifically aimed at the composition API.
What am I missing here?
Every time I run the registration script it registers the user correctly and logs the error that it can't find the 'push' property on of undefined.
My code completion is telling me it's there, the linting works fine and the IDE Webstorm isn't giving any errors.
<script>
import firebase from "firebase/app";
import "firebase/auth";
import { defineComponent, ref } from "vue";
import { useRouter } from "vue-router";
export default defineComponent({
name: "Register",
setup() {
const form = ref({
email: "",
password: "",
error: "",
});
const pressed = () => {
const user = firebase
.auth()
.createUserWithEmailAndPassword(form.value.email, form.value.password);
user
.then((user) => {
console.log(user);
const router = useRouter();
router.push("/about");
})
.catch((e) => console.log(e.message));
};
return {
form,
pressed,
};
},
});
</script>
Hope it is just something simpel
const router = useRouter(); must be declared in the scope of the setup hook not inside any inner scope :
setup(){
const router = useRouter();
const form = ref({
email: "",
password: "",
error: "",
});
const pressed = () => {
const user = firebase
.auth()
.createUserWithEmailAndPassword(form.value.email, form.value.password);
user
.then((user) => {
console.log(user);
router.push("/about");
})
.catch((e) => console.log(e.message));
};
return {
form,
pressed,
};
},
I have seen similar questions but they dont actually address what am looking for.
so am using using axios globally in app.js for my vue app like window.axios=require('axios')
then in auth.js i have this
export function login(credentials){
return new Promise((res,rej) => {
axios.post('/api/auth/login', credentials)
.then((response) => {
res(response.data);
})
.catch((err) => {
rej("Wrong email or password");
})
});
}
which works fine on the login page
however in my test script
jest.mock("axios", () => ({
post: jest.fn(() => Promise.resolve({data:{first_name:'James','last_name':'Nwachukwu','token':'abc123'}}))
}));
import axios from 'axios'
import {login} from '../helpers/auth'
it("it logs in when data is passed", async () => {
const email='super#gmail.com'
const password='secret';
const result=await login({email,password});
expect(axios.post).toBeCalledWith('/api/auth/login',{"email": "super#gmail.com", "password": "secret"})
expect(result).toEqual({first_name:'James','last_name':'Nwachukwu','token':'abc123'})
})
shows axios is not defined
but if i change auth.js to
import axios from 'axios'
export function login(credentials){
return new Promise((res,rej) => {
axios.post('/api/auth/login', credentials)
.then((response) => {
res(response.data);
})
.catch((err) => {
rej("Wrong email or password");
})
});
}
test passes. how do i run the test without having to import axios on each vue file
I had the same problem just now. I am also including axios via window.axios = require('axios'); in my app.js.
The solution is to set your axios mock on window.axios in your test. So instead of this (incorrect):
axios = {
post: jest.fn().mockName('axiosPost')
}
const wrapper = mount(Component, {
mocks: {
axios: axios
}
})
When your component code calls axios.whatever it is really calling window.axios.whatever (as I understand it), so you need to mirror that in your test environment:
window.axios = {
post: jest.fn().mockName('axiosPost')
}
const wrapper = mount(Component, {
mocks: {
axios: window.axios
}
})
And in your test:
expect(window.axios.post).toHaveBeenCalled()
The above method works fine until you want to chain then to it. In which case you need to set your mock up like this:
window.axios = {
get: jest.fn(() => {
return {
then: jest.fn(() => 'your faked response')
}
}),
}
You don't need to pass it into the component mock though, you can just mount (or shallowMount) the component as usual
I have a login component that's supposed to take user credentials, submit them to the API and based on the response either set JWT in the VueX store or show an error.
In the unit test I'm mocking axios call with a response that's delayed to simulate actual network (using axios-mock-adapter library). Currently, the test is failing, because the test finishes before the promise is resolved despite the fact that I'm using flushPromises() utility.
In fact, I do not see either of the console.log's that are in my login function.
If I remove the delayResponse property from the mocked call, the test passes and i see the 'got data' response in my console log as expected.
What am I missing here? Does anyone have any clue why the flushPromises() just skips over the axios call?
I'm using Vuetify framework, hence the v- tags, although I doubt that makes a difference for the test
Template:
<v-alert
type="error"
:value="!!error"
test-id="LoginError"
v-html="error"
/>
<form
#submit.prevent="login"
test-id="LoginForm"
>
<v-text-field
label="Email Address"
test-id="LoginEmailField"
v-model="credentials.email"
/>
<v-text-field
label="Password"
type="password"
test-id="LoginPasswordField"
v-model="credentials.password"
/>
<v-btn
type="submit"
test-id="LoginSubmitBtn"
>
Login
</v-btn>
</form>
The login function is fairly straight-forward:
async login() {
try {
const response = await axios.post(this.url, this.credentials);
console.log('got response', response);
const token = response.data.payload;
this.$store.dispatch('setUser', { token });
} catch (error) {
console.warn('caught error', error);
this.error = error;
}
}
Test file:
fit('Receives and stores JWT in the VueX store after sending valid credentials', async () => {
const maxios = new AxiosMockAdapter(axios, {delayResponse: 100});
const validData = { email: 'aa#bb.cc', password: '111' };
const validToken = 'valid-token';
maxios.onPost().reply(200, { payload: validToken });
wrapper.find('[test-id="LoginEmailField"]').vm.$emit('input', validData.email);
wrapper.find('[test-id="LoginPasswordField"]').vm.$emit('input', validData.password);
wrapper.find('[test-id="LoginSubmitBtn"]').trigger('click');
await flushPromises();
expect(localStore.state.user.token).toBe(validToken);
});
VueX store:
export const state = {
user: {
token: null,
},
};
const mutations = {
SET_USER: (currentState, user) => {
currentState.user = user;
},
};
const actions = {
setUser: ({ commit }, payload) => {
commit('SET_USER', payload);
},
};
export const mainStore = {
state,
mutations,
actions,
};
export default new Vuex.Store(mainStore);
The response:
expect(received).toBe(expected) // Object.is equality
Expected: "valid-token"
Received: null
Hoping I can explain this clearly and someone has some insight on how I can solve this.
I am trying to enter a input then have a text message delivered to the number that was entered. That simple.
On the homepage, I have an input component with:
<template>
<form class="right-card" #submit.prevent="submit">
<input v-model="search" />
<button class="clear" type="submit" v-on:click="submit"></button>
</form>
</template>
With this function set as a method to pass the param
export default {
data () {
return {
search: ''
}
},
methods: {
submit: function (event) {
this.$router.push(`sms/${this.search}`)
}
}
}
Then I have a /sms page located in pages/sms/_sms.vue which is landed on once the form is submitted
<template>
<div>
<h1>Success Page {{phoneNumber}} {{$route.params}}</h1>
<KeyboardCard/>
</div>
</template>
<script>
import KeyboardCard from '~/components/KeyboardCard.vue'
import axios from '~/plugins/axios'
export default {
asyncData ({ params, error }) {
return axios.get('/api/sms/' + params.sms)
.then((res) => {
console.log(res)
console.log(params)
return { phoneNumber: res.data }
})
.catch((e) => {
error({ statusCode: 404, message: 'Sms not found' })
})
},
components: {
KeyboardCard
}
}
</script>
And finally within api/sms/sms.js I have this on express running.
(note my API keys are replaced with placeholder)
router.get('/sms/:sms', (req, res, next) => {
console.log('express reached')
const accountSid = 'ACCOUNTSIDPLACEHOLDER'
const authToken = 'AUTHTOKENPLACEHOLDER'
const client = require('twilio')(accountSid, authToken)
client.messages.create({
to: '14169190118',
from: '+16477993562',
body: 'This is the ship that made the Kessel Run in 14 parsecs?!'
})
.then((message) => console.log(message.sid))
})
How can I pass the parameter.sms within the to field in my /api/routes/sms.js
Expected: When user enters # into the input how can the api/sms/:sms be called dynamically to the number that was typed in the input component?
Thanks in advance if anyone see's whats going on here :)
Edit: I have my middleware defined in the nuxt.config file, like so:
serverMiddleware: [
// API middleware
'~/api/index.js'
]
and my api/index.js file has:
const express = require('express')
// Create express instnace
const app = express()
// Require API route
const sms = require('./routes/sms')
// Import API Routes
app.use(sms)
// Export the server middleware
module.exports = {
path: '/api',
handler: app
}
I guess this is more an Express.js related question than a Vue.js question.
You can use the passed sms param from your request, like this:
router.get('/sms/:sms', (req, res, next) => {
console.log('express reached')
const accountSid = 'ACCOUNTSIDPLACEHOLDER'
const authToken = 'AUTHTOKENPLACEHOLDER'
const client = require('twilio')(accountSid, authToken)
client.messages.create({
to: req.params.sms,
from: '+16477993562',
body: 'This is the ship that made the Kessel Run in 14 parsecs?!'
})
.then((message) => console.log(message.sid))
})