How to use mocking for test uploading file with jest - vue.js

I am developing a system with nuxt js and jest that in part of that I want to upload an image.
Here's my html code:
<input
id="photo"
ref="photo"
type="file"
name=""
class="form-control d-flex"
#change="uploadPhoto"
>
Here's my uploadPhoto function in nuxt js:
uploadPhoto () {
const file = this.$refs.photo.files[0]
// upload photo
const formData = new FormData()
formData.append('photo', file)
const returnedData = await this.$axios.$post('/api/photo/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
The question is:
How may I mock uploading photo in jest to test my code?
My jest code is something like this:
test('uploading photo test', () => {
wrapper = mount(UploadPhotoComponent, {
stubs: {
NuxtLink: true
},
mocks: {
$auth: {
loggedIn: true,
$storage: {
_state: {
'_token.local': 'api bearer token'
}
}
},
$axios: {
$post: jest.fn(() => {
return Promise.resolve({
status: 200,
message: 'photo was uploaded successfully.',
entire: []
})
})
}
}
})
})
I don't know how to test uploading file in jest using mocks.
Can anyone help me?

Finally, I have realized how to do that. First of all generate an image blob and file:
import fs from 'fs';
import path from 'path';
const dummyImgData = fs.readFileSync(
path.join(__dirname, '../../libs/dummy-img-2.jpeg')
);
const fbParts = [
new Blob([dummyImgData], {
type: 'image/jpeg'
}),
'Same way as you do with blob',
new Uint16Array([999999999999999])
];
const fbImg = new File(fbParts, 'sample.png', {
lastModified: new Date(2020, 1, 1),
type: 'image/jpeg'
});
And after assign it to $refs object:
wrapper.vm.$refs = {
photo: {
files: [
fbImg
]
}
}

Related

Trouble uploading file in Vue frontend

When I try to upload a user photo with Vue, the formData just seems to be an empty object. The Multer middleware then fails because there is no req.file for it read.
User.vue
<form enctype="multipart/form-data">
<div class="pl-3 label">User Photo</div>
<v-file-input
accept='image/*'
label="Upload Photo"
v-model="photo"
></v-file-input>
<v-btn class="mr-5" color="blue lighten-3" #click="submit">
save changes
</v-btn>
export default {
data: () => ({
photo: null
}),
methods: {
submit() {
let formData = new FormData();
if (this.photo) {
formData.append('photo', this.photo)
}
this.$store.dispatch('updateMe', {
photo: formData
});
}
}
};
Vuex Store
export default new Vuex.Store({
state: {
photo: null
},
mutations: {
setUpdatedUser(state, payload) {
state.name = payload.name;
state.email = payload.email;
state.photo = payload.photo;
}
},
actions: {
async updateMe({ commit, getters }, payload) {
let id = localStorage.getItem('userId');
let user = {
photo: payload.photo
};
try {
let token = getters.token;
const response = await axios.patch(
`http://localhost:3000/api/v1/users/updateMe`,
user,
{
headers: { Authorization: 'Bearer ' + token }
}
);
commit('setUpdatedUser', response.data.updatedUser);
} catch (err) {
console.log(err);
}
}
}
User Controller
const multerStorage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'public/users');
},
filename: (req, file, cb) => {
console.log(file)
const ext = file.mimetype.split('/')[1];
cb(null, `user-${req.user.id}-${Date.now()}.${ext}`);
}
});
const multerFilter = (req, file, cb) => {
if (file.mimetype.startsWith('image')) {
cb(null, true);
} else {
cb(new AppError('Not an image. Please upload only images.', 400), false);
}
};
const upload = multer({
storage: multerStorage,
fileFilter: multerFilter
});
exports.uploadUserPhoto = upload.single('photo');
Most other examples I have compared to do not use v-model for the input binding. Does that make a difference?
In an application where I am handling file uploads the files get posted to the node backend.
I also v-model in my component so that shouldn't be the problem.
Frontend method:
upload() {
const data = new FormData()
data.append('file', this.file, this.file.name) // Filename is optional
data.append('additionalInformation', this.additionalInformation)
axios.
post('/api/v1/upload/', data)
.then(response => /* response handling */)
.catch(error => /* error handling */)
}
The node endpoint looks like this:
app.post('/api/v1/upload/', multer().single('file'), (request, response) => {
upload(request.body, request.file, response, config)
})
Reference: FormData.append()

Apollo query inside nuxt.config.js

I'm trying to make a smart request in nuxt with nuxt-apollo-module in order to grab my routes for the nuxt-sitemaps-module (so I can create my sitemap with them).
I need to make this request from within nuxt.config.js file. I have tried this way with no luck (as app doesn't exist in this context). What would be the right way to do this?
Thanks in advance!
The relevant part of my nuxt.config.js
import gql from 'graphql-tag'
module.exports = {
modules: [
'#nuxtjs/apollo',
'#nuxtjs/sitemap'
],
apollo: {
clientConfigs: {
default: {
httpEndpoint: 'https://example.com/graphql'
}
}
},
sitemap: {
path: '/sitemap.xml',
hostname: 'https://example.com/',
generate: true,
cacheTime: 86400,
trailingSlash: true,
routes: async ({ app }) => {
const myRoutes = ['/one-random-path/']
let client = app.apolloProvider.defaultClient
let myProductsQuery = gql`query {
products {
slug
}
}`
let myBrandsQuery = gql`query {
brands {
slug
}
}`
const myProducts = await client.query({ query: myProductsQuery })
const myBrands = await client.query({ query: myBrandsQuery })
return [myRoutes, ...myProducts, ...myBrands]
}
}
}
I was able to generate it this way, but I'm sure there is a better way.
yarn add node-fetch apollo-boost
sitemap: {
routes: async () => {
const routes = []
const fetch = require("node-fetch")
const { gql } = require("apollo-boost")
const ApolloBoost = require("apollo-boost")
const ApolloClient = ApolloBoost.default
const client = new ApolloClient({
fetch: fetch,
uri: YOUR_API_ENDPOINT,
})
const fetchUsers = gql`
query {
users {
id
}
}
`
const users = await client
.query({
query: fetchUsers,
})
.then((res) => res.data.users)
users.forEach((user) => {
routes.push({
route: `/${user.id}`,
})
})
return routes
},
},
I gave up using Apollo. It was easier to use Axios. Moreover, no nneds to configure #nuxtjs/sitemap :
import axios from 'axios'
sitemap: {
hostname: URL_SITE,
gzip: true,
},
routes() {
return axios({
url: ENDPOINT,
method: 'post',
data: {
query: `
query GET_POSTS {
posts {
nodes {
slug
}
}
}
`,
},
}).then((result) => {
return result.data.data.posts.nodes.map((post) => {
return '/blog/' + post.slug
})
})
}
This was how I was able to do it with authentication. Got round to it by following the documentation here which had a Vue example: https://www.apollographql.com/docs/react/networking/authentication/
Might be a cleaner way to do it with apollo-boost if you can also use auth?
import fetch from 'node-fetch'
import { ApolloClient } from 'apollo-client'
import { HttpLink } from 'apollo-link-http'
import { ApolloLink, concat } from 'apollo-link'
import { InMemoryCache } from 'apollo-cache-inmemory'
import globalQuery from './apollo/queries/global.js'
const httpLink = new HttpLink({ uri: process.env.SCHEMA_URL, fetch })
const authMiddleware = new ApolloLink((operation, forward) => {
// add the authorization to the headers
const token = process.env.STRAPI_API_TOKEN
operation.setContext({
headers: {
authorization: token ? `Bearer ${token}` : 'Bearer',
},
})
return forward(operation)
})
export const apolloClient = new ApolloClient({
link: concat(authMiddleware, httpLink),
cache: new InMemoryCache(),
})
export default async () => {
let global = null
const globalResponse = await apolloClient.query({ query: globalQuery })
if (globalResponse?.data?.global?.data) {
global = globalResponse.data.global.data.attributes
}
console.log('globals:::', global)
}

How pass data from component to external js file

I want to use my component's data within my external JavaScript file, containing my Dropzone configuration. I tried unsuccessfully to use Function.prototype.bind:
export const dropzoneConfig = {
url: api.http.baseUrl + '/files',
thumbnailWidth: 150,
maxFilesize: 5,
acceptedFiles: 'image/*',
addRemoveLinks: true,
sending: function (file, xhr, formData) {
formData.append('type', 'photo');
},
success: function (file, xhr) {
file.id = xhr.data.id;
if (this.entry.files === undefined) {
this.entry.files = [];
}
this.entry.files.push(xhr.data);
this.saveNote();
}.bind(this),
headers: api.http.authHeaders().headers
};
In the code above, this.entry and this.saveNote are unavailable because they're from my Vue component. How do I make them accessible to the external file?
A more general solution would be for the component to pass in a success-event handler that has access to the component's data and methods, as shown below. This solution decouples the configuration from the component's internals.
dropzoneConfig.js:
export const dropzoneConfig = ({ onSuccess }) => ({
//...
success(file, xhr) {
//...
onSuccess(xhr.data)
}
})
App.vue:
<script>
import Dropzone from 'dropzone'
import { dropzoneConfig } from './dropzoneConfig'
export default {
data() {
return {
entry: {
files: []
}
}
},
created() {
Dropzone.options.myComponent = dropzoneConfig({
onSuccess: fileData => this.onDropzoneSuccess(fileData)
})
},
methods: {
saveNote() {
//...
},
onDropzoneSuccess(fileData) {
this.entry.files.push(fileData)
this.saveNote()
}
}
}
</script>

VueJS CKeditor5 upload images

Having trouble with uploading images using CKeditor5 in Vuejs.
First having tried Simple upload Adapter which gave me the following error:
Reason: CKEditorError: ckeditor-duplicated-modules: Some CKEditor 5 modules are duplicated. Read more: https://ckeditor.com/docs/ckeditor5/latest/framework/guides/support/error-codes.html#error-ckeditor-duplicated-modules
I tried making a upload adapter. As a uploadadapter I took the example and modified the url. The uploadadapter.js file looks like the following:
export default class UploadAdapter {
constructor( loader ) {
// The file loader instance to use during the upload.
this.loader = loader;
}
// Starts the upload process.
upload() {
return this.loader.file
.then( file => new Promise( ( resolve, reject ) => {
this._initRequest();
this._initListeners( resolve, reject, file );
this._sendRequest( file );
} ) );
}
// Aborts the upload process.
abort() {
if ( this.xhr ) {
this.xhr.abort();
}
}
// Initializes the XMLHttpRequest object using the URL passed to the constructor.
_initRequest() {
const xhr = this.xhr = new XMLHttpRequest();
xhr.open( 'POST', '<url here>', true );
xhr.responseType = 'json';
}
// Initializes XMLHttpRequest listeners.
_initListeners( resolve, reject, file ) {
const xhr = this.xhr;
const loader = this.loader;
const genericErrorText = `Couldn't upload file: ${ file.name }.`;
xhr.addEventListener( 'error', () => reject( genericErrorText ) );
xhr.addEventListener( 'abort', () => reject() );
xhr.addEventListener( 'load', () => {
const response = xhr.response;
if ( !response || response.error ) {
return reject( response && response.error ? response.error.message : genericErrorText );
}
resolve( {
default: response.url
} );
} );
if ( xhr.upload ) {
xhr.upload.addEventListener( 'progress', evt => {
if ( evt.lengthComputable ) {
loader.uploadTotal = evt.total;
loader.uploaded = evt.loaded;
}
} );
}
}
// Prepares the data and sends the request.
_sendRequest( file ) {
// Prepare the form data.
const data = new FormData();
data.append( 'upload', file );
// Send the request.
this.xhr.send( data );
}
}
The Vue component:
<template>
<form #submit.prevent="store">
<ckeditor
:editor="editor"
v-model="form.content"
:error-messages="errors.content"
:config="editorConfig"
/>
</form>
</template>
<script>
import CKEditor from '#ckeditor/ckeditor5-vue';
import ClassicEditor from '#ckeditor/ckeditor5-build-classic';
import UploadAdapter from '../../UploadAdapter';
export default {
data()
{
return {
form: {
content: null,
},
editor: ClassicEditor,
editorConfig: {
toolbar: [ 'heading', '|', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', '|', 'insertTable', '|', 'imageUpload', 'mediaEmbed', '|', 'undo', 'redo' ],
table: {
toolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ]
},
extraPlugin: [this.uploader],
language: 'nl',
},
}
},
methods: {
store()
{
// Some code
},
uploader(editor)
{
editor.plugins.get( 'FileRepository' ).createUploadAdapter = ( loader ) => {
return new UploadAdapter( loader );
};
},
},
components: {
ckeditor: CKEditor.component
}
}
</script>
However each time when trying to upload a file the following warning is returned:
filerepository-no-upload-adapter: Upload adapter is not defined. Read more: https://ckeditor.com/docs/ckeditor5/latest/framework/guides/support/error-codes.html#error-filerepository-no-upload-adapter
Have looked at the url but it just sends me in circles thus making no progress. What I'm looking for is an example that at least sends a file to the server without errors/ warnings. If the uploadadapter can be scraped and something else except CKfinder can be used that's fine. For now I guess the problem is most likely to be in the Vue component.
use extraPlugins instead of extraPlugin.
moving uploader function to outside of vue component and then using it directly as
extraPlugin: [uploader]
worked for me.

I can't post my file on the server with formData from the state

I'm not able to post a form data on my server files as formData from my store.
Checking the state before submit I can see that image and video properties have formDatas as values, but on submit the params of the request are empty object
<template>
<input type="file" #change="onImageSelected($event)" accept="image/*" >
</template>
methods: {
submit() {
this.$v.$touch()
if (this.$v.$invalid || this.tags.length > 3) {
this.submitStatus = 'ERROR'
} else {
this.submitStatus = 'PENDING'
this.$store.dispatch('exercises/storeExercise')
}
},
onImageSelected(event) {
this.image = event.target.files[0]
if (this.image.size < 8000000) {
const formDataImage = new FormData()
formDataImage.append('image', this.image)
this.$store.dispatch('exercises/commitImageSelected', formDataImage)
}
},
}
const state = {
exercises: [],
tags: [],
selectedExercise: null,
selectedComponent: BeforeSelect
}
const mutations = {
setImageSelected(state, image) {
Vue.set(state.selectedExercise, 'image', image)
},
setVideoSelected(state, video) {
Vue.set(state.selectedExercise, 'video', video)
}
}
const actions = {
commitImageSelected({commit}, image){
commit('setImageSelected', image)
},
commitVideoSelected({commit}, video){
commit('setVideoSelected', video)
},
storeExercise({commit, state}) {
axios.post('exercises', state.selectedExercise)
.then(({data}) => {
commit('setExercises', data)
})
.catch(err => console.log(err))
},
}
Try this axios post in storeExcerise method.
axios.post('exercises',state.selectedExercise,{
headers: { 'Content-Type': undefined},
}).then(function (response) {
if (response.data.ok) {
}
}.bind(this));
This is working fine for me while uploading file to server.