I am stuck with Jest testing of file resize/upload logic. This is a simple job to resize uploaded image and add it to the Notes & Attachments based on recordId. It is not a production code - it is created exacly for testing purposes Works fine in developer org. Heroku just receives Base64 string - resizes it - and sends back (no backend code is provided here). So based on Component's js file I have to test:
- onchange={openFileUpload} in lightning-input type="file"
- new FileReader()
- Imperative method call (ImageUpload.uploadFile )
- Notification - toast(title) - which is triggered from then block of the Promise
And all in one file in some sort of chain I guess.
Keeping in mind that I am not a Jest ninja, this task is too much for me. I've done some tests but I am not sure if they are correct. What worries me the most is that the toast is not automatically fired in 'then' block of 'uploadFile' imperative method. I can call it manually but it doesn't make sense. Please take a look.
<template>
<lightning-card title="File Upload LWC" icon-name="action:upload" >
<div>
<lightning-input type="file"
label="Attachment"
accept=".jpg, .png, .gif"
onchange={openFileUpload}
></lightning-input>
</div>
</lightning-card>
</template>
imageUpload.js
==
import { LightningElement } from 'lwc';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import uploadFile from '@salesforce/apex/ImageUpload.uploadFile'
export default class ImageUpload extends LightningElement {
openFileUpload(event) {
if (event?.target.files?.length) {
const recordId = '0010900000c7vKKAAY'
const file = event.target.files[0]
const filename = file.name
// Mozila FileReader
// The FileReader object lets web applications asynchronously read the contents of files (or raw data buffers) stored on the user's computer,
// using File or Blob objects to specify the file or data to read.
const reader = new FileReader() //The readAsDataURL method is used to read the contents of the specified Blob or File
reader.readAsDataURL(file) // Syntax - readAsDataURL(blob)
reader.onload = () => {
var base64 = reader.result.split(',')[1]
uploadFile({ base: base64, name: filename, id: recordId }).then(result => {
if (result === 'ok') {
let title = `${filename} uploaded successfully!`
this.toast(title)
} else {
let title = result
this.toast(title)
}
})
.catch((error) => {
console.log(error)
let title = 'Error'
this.toast(title)
})
}
}
}
toast(title) {
const toastEvent = new ShowToastEvent({
title,
variant: "success"
})
this.dispatchEvent(toastEvent)
}
}
ImageUpload.cls
==
public with sharing class ImageUpload {
@AuraEnabled
public static String uploadFile (String base, String name, String id) {
String content = '{"str": "' + base + '"}';
Http h = new Http();
HttpRequest req = new HttpRequest();
req.setEndpoint( 'https://some-address.herokuapp.com/image' );
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setBody(content);
HttpResponse response = h.send(req);
String jsonRes = response.getBody();
Map<String,Object> responseMap = (Map<String, Object>) JSON.deserializeUntyped(jsonRes);
String base64 = (String)responseMap.get('str');
if (base64 == 'Error') {
return base64;
} else {
ContentVersion cv = createContentVersion( base64, name);
ContentDocumentLink cdl = createContentLink( cv.Id, id);
if (cv == null || cdl == null) {
return 'Error';
}
return 'ok';
}
} // uploadFile
private static ContentVersion createContentVersion(String base64, String filename) {
ContentVersion cv = new ContentVersion();
//EncodingUtil class - https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_classes_restful_encodingUtil.htm
cv.VersionData = EncodingUtil.base64Decode(base64);
cv.Title = filename;
cv.PathOnClient = filename;
try {
insert cv;
return cv;
} catch(DMLException e) {
System.debug(e);
return null;
}
}
private static ContentDocumentLink createContentLink(String contentVersionId, String recordId) {
if (contentVersionId == null || recordId == null) {
return null;
}
ContentDocumentLink cdl = new ContentDocumentLink();
cdl.ContentDocumentId = [SELECT ContentDocumentId FROM ContentVersion WHERE Id =: contentVersionId ].ContentDocumentId;
cdl.LinkedEntityId = recordId;
cdl.ShareType = 'V';
try {
insert cdl;
return cdl;
} catch(DMLException e) {
System.debug(e);
return null;
}
}
}
imageUpload.test.js
==
import ImageUpload from 'c/ImageUpload';
import uploadFile from '@salesforce/apex/ImageUpload.uploadFile'
jest.mock(
'@salesforce/apex/ImageUpload.uploadFile',
() => {
return {
default: jest.fn()
};
},
{ virtual: true }
);
const mockToast = jest.fn();
jest.mock('c/ImageUpload', () => {
return jest.fn().mockImplementation(() => {
return { toast: mockToast };
});
});
describe('c-image-uploaded', () => {
beforeEach(() => {
ImageUpload.mockClear();
mockToast.mockClear();
});
afterEach(() => {
// restore the spy created with spyOn
jest.restoreAllMocks();
while (document.body.firstChild) {
document.body.removeChild(document.body.firstChild);
}
});
async function flushPromises() {
return Promise.resolve();
}
it('FileReader receives Blob', async () => {
let blob = new Blob([""], { type: 'image/png' })
blob.lastModifiedDate = ""
blob.name = "coolImage"
const fileReader = {
readAsDataURL: jest.fn(),
}
jest.spyOn(window, 'FileReader').mockImplementation(() => fileReader)
fileReader.readAsDataURL(blob)
await flushPromises()
expect(fileReader.readAsDataURL).toBeCalledWith(blob);
})
it('Call uploadFile with params', async () => {
const base64 = 'BWpmdYyVL1XcQGZ86EPuuw==' //I used https://generate.plus/en/base64
const recordId = '0010900000c7vKKAAY'
const filename = 'coolImage'
uploadFile({ base: base64, name: filename, id: recordId })
await flushPromises()
expect(uploadFile).toBeCalledWith({ base: base64, name: filename, id: recordId })
expect(uploadFile).toHaveBeenCalledTimes(1)
})
it('kinda toast testing', async () => {
const filename = 'cool image'
uploadFile.mockResolvedValue('ok')
const data = await uploadFile()
let title
if (data === 'ok') {
title = `${filename} uploaded successfully!`
} else {
title = 'some title'
}
mockToast(title)
await flushPromises()
expect(mockToast).toBeCalledWith(`${filename} uploaded successfully!`)
expect(mockToast).toHaveBeenCalledTimes(1)
})
it('resolved promise', async () => {
uploadFile.mockResolvedValue('ok')
const data = await uploadFile()
expect(data).toBe('ok')
// or
await expect(uploadFile()).resolves.toBe('ok') // third call
})
it('rejected promise', async () => {
uploadFile.mockRejectedValue(new Error('Async error message'))
await expect(uploadFile())
.rejects
.toThrow('Async error message');
})
}) // describe