Using Jasmine Unit Testing with front-end JavaScript applications, you can write tests that can interact with the DOM to set up the input, run the test, and verify the results by querying the relevant HTML elements.
For example, the following code snippet will setup the needed DOM elements, call the function that will update the DOM using updateResult()
function, then verify the results by reading the content of the inner text:
function updateResult(result) {
let element = document.getElementById('result');
if (element) {
element.innerText = result;
}
}
describe('updateResult()', function () {
beforeAll(function () {
const element = document.createElement('div');
element.setAttribute('id', 'result');
document.body.appendChild(element);
this.element = element;
});
afterAll(function () {
document.body.removeChild(this.element);
});
it('add result to the dom element', function () {
updateResult('5'); //
expect(this.element.innerText).toBe('5');
});
});
note: the above is a sample based on "leelanarasimha" simple-calculator tutorial.
Now consider a more complex scenario where you need to perform a similar test, however, the test requires complex objects to be available and a relatively large number of nested forms to be visible and filled. In a typical situation, the test is a small one, but in order to reach the point in time to run the test, you need to execute about 5 to 7 steps with complex operations which are outside the scope of the intended function, but that function depends on executing several steps so that you have all needed objects and data structures which are a dependency for the function to be tested.
I am finding it hard to wrap my head around how to use Jasmine Unit testing to write and execute tests for such a complex scenario.
Following is a simple scenario load the Jasmine Spec Runner HTML with the simple JavaScript to run the test engine:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Jasmine Spec Runner v4.4.0</title>
<link rel="shortcut icon" type="image/png" href="lib/jasmine_favicon.png">
<link rel="stylesheet" href="lib/jasmine.css">
<script src="lib/jasmine.js"></script>
<script src="lib/jasmine-html.js"></script>
<script src="lib/boot0.js"></script>
<!-- optional: include a file here that configures the Jasmine env -->
<script src="lib/boot1.js"></script>
<!-- include source files here... -->
<script src="src/calculator.js"></script>
<!-- include spec files here... -->
<script src="spec/calculator.spec.js"></script>
</head>
<body>
</body>
</html>
// calculator.js
let add = (a,b)=>a+b;
//calculator.spec.js
/// <reference path="../src/calculator.js" />
describe("calculator.js", ()=>{
it("should add two numbers", ()=>{
expect(add(1,3)).toBe(4)
})
});
In a typical workflow platform similar to Activity or Camunda, you develop complex forms and attach JavaScipt to add logic to control the Data Entry and perform the needed calculations and processing.
The main problem is that there is a large number of huge javascript programs that were not written with unit testing in mind. In a given scenario, there are 2 or 3 JavaScript functions that will interact with the HTML Form while inputting values, as a result, some REST APIs are invoked on the input event, and data is updated. Then, when clicking a button, another JavaSript function will run to perform the data summarization logic by reading the form data. The form fields are bound to variables that can be accessed using the supplied JavaScript functions to read form data (bound variables). The UI may have multiple nested subforms with repeating instances (data grid). I am thinking it is near impossibility to write JavaScript code to prepare the needed input so that the test will run. So, I am thinking of some alternatives.
Method A:
Method B:
Method C:
...
...
What I am looking for is the following:
How to activate or load Jasmine from the console to kick off the test when it is ready to do so?
Is it possible to save the HTML Forms (the UI in general) and reload it back during the test and how? I can save the form data model in a JSON file and load it during the test.
Any other feedback on the above will be appreciated.
In this attempt, I tried to include the Jasmine Framework script (HTML and JavaScript) within a Form created using a Form Model which is part of a Case Model. I added the links to the script for Jasmine and other specs using HTML Component. It didn't work. I also tried using JavaScript to load the source files and it didn't work either. The Script is loading and I can test the functions in the console, all look fine, but the strange thing nothing was showing on the browser.
<title>Jasmine Spec Runner v4.4.0</title>
<script>
console.log("Start Test Framework");
debugger;
var myScript;
var myElm;
myElm = document.createElement('link');
myElm.setAttribute('rel', 'shortcut icon');
myElm.setAttribute('type', 'image/png');
myElm.setAttribute('href','Jasmine/.test/lib/jasmine_favicon.png');
document.head.appendChild(myElm);
myElm = document.createElement('link');
myElm.setAttribute('rel','stylesheet');
myElm.setAttribute('href','Jasmine/.test/lib/jasmine.css');
document.head.appendChild(myElm);
setTimeout((
)=>{
myScript = document.createElement('script');
myScript.setAttribute('src', 'Jasmine/.test/lib/jasmine.js');
document.head.appendChild(myScript);
}, 200);
setTimeout(()=>{
myScript = document.createElement('script');
myScript.setAttribute('src', 'Jasmine/.test/lib/jasmine-html.js');
document.head.appendChild(myScript);
}, 600);
setTimeout(()=>{
myScript = document.createElement('script');
myScript.setAttribute('src', 'Jasmine/.test/lib/boot0.js');
document.head.appendChild(myScript);
}, 1000);
setTimeout(()=>{
myScript = document.createElement('script');
myScript.setAttribute('src', 'Jasmine/.test/lib/boot1.js');
document.head.appendChild(myScript);
}, 1200);
setTimeout(()=>{
myScript = document.createElement('script');
myScript.setAttribute('src', 'Jasmine/.test/src/Player.js');
document.head.appendChild(myScript);
}, 1600);
setTimeout(()=>{
myScript = document.createElement('script');
myScript.setAttribute('src', 'Jasmine/.test/src/Song.js');
document.head.appendChild(myScript);
}, 2000);
setTimeout(()=>{
myScript = document.createElement('script');
myScript.setAttribute('src', 'Jasmine/.test/spec/SpecHelper.js');
document.head.appendChild(myScript);
}, 2200);
setTimeout(()=>{
myScript = document.createElement('script');
myScript.setAttribute('src', 'Jasmine/.test/spec/PlayerSpec.js');
document.head.appendChild(myScript);
}, 2400);
setTimeout(()=>{
myScript = document.createElement('script');
myScript.setAttribute('src', 'Jasmine/.test/spec/ABC-tests.js');
document.head.appendChild(myScript);
console.log("All scrip loaded.");
}, 2800);
</script>
<h1>Loading Script</h1>
In this attempt, I copied SpecRunner.html
to a folder visible to the web server, and managed to write relatively complex tests, but I had to use Jasmine Spies as I couldn't have access to the Workflow platform JavaScript Objects. I believe this approach is the best approach and manged to get successful results.
//This function is used to spy on a property that doesn't have a GET accessor.
window.spyOnValueProperty = function(obj, prop, accessType = 'get') {
let descriptor = jasmine.util.getPropertyDescriptor(obj, prop)
let value = descriptor.value
let newDescriptor = {
...descriptor,
get: () => value,
set: (newValue) => value = newValue
}
delete newDescriptor.value
delete newDescriptor.writable
Object.defineProperty(obj, prop, newDescriptor)
return spyOnProperty(obj, prop, accessType)
}
describe("ABC-test.js", ()=>{
describe("ABC_RRFCustomeJS.js - General Functions", ()=>{
it("Object.isEmpty() returns true for empty objects", ()=>{
var obj = {};
expect(Object.isEmpty(obj)).toBeTrue();
obj.someProp = "Some Value";
expect(Object.isEmpty(obj)).toBeFalse();
});
})
describe("ABC_RRFCustomeJS.js - Editability of the Subform", ()=>{
describe("None MUL Type", ()=>{
let subform;
let myworkflow;
let myTasks;
let myParentId;
beforeAll(()=>{
myworkflow = {currentState:{
formData:{get:()=>{}, set:(...prms)=>{}},
tempData:{get:()=>{}, set:(...prms)=>{}}
}};
myParentId = '556f9f55-6b45-4f5a-a495-xyz';
let myR360 = {common:{workflowbasepath:"http://workflow-host.dev.local/workflow"}};
window.workflow = window.workflow??null;
//global.workflow = global.workflow??null;
//window.r360 = window.r360??myR360;
myTasks =
[
{
"name": "Confirm receipt of xyz",
"modelId": "GEAR-801d94a7-6a7d-4a87-8d13-xyz",
"criteria": "false"
},
{
"name": "Legal xyz",
"modelId": "GEAR-1bc2dc96-282e-468c-bf3f-xyz",
"criteria": "false"
},
{
"name": "Send Executed xyz",
"modelId": "GEAR-15fb5d54-093d-4437-aeef-xyz",
"criteria": "true"
}
];
spyOnValueProperty(window, "workflow").and.returnValue(myworkflow);
spyOn(workflow.currentState.formData, "get").and.returnValue({
FacilityBookingStatus: "Booked",
root:{
id:myParentId,
MULType:'N/A',
workflowHistory:{isLocked:true}
}
});
spyOn(workflow.currentState.tempData, "set");
subform = Subform.getInstance();
});
it("Create instance of Subform", ()=>{
subform.init();
expect(subform).toEqual(jasmine.any(Object));
expect(subform.tasks).toEqual(myTasks);
});
it("Set Legal/QA Date Task Status Code ALL_ARE_COMPLETED", async ()=>{
let theQuery = `type:TSK parent:${myParentId} modelId:${myTasks[2].modelId} state:completed`;
spyOn(ABCGeneralFunctions, "getWorkObjects").and.returnValue(Promise.resolve(
{
"data": [{"subState": "COMPLETED"}],
"total": 1, "start": 0, "size": 1
}
));
await subform.setStatusCode();
expect(ABCGeneralFunctions.getWorkObjects).toHaveBeenCalledWith(theQuery, `subState`, ``, 50);
expect(workflow.currentState.tempData.set).toHaveBeenCalledWith(CON_LegalQADateTaskStatusCode, CON_SubformERTTaskStatusCodes.ALL_ARE_COMPLETED);
});
it("Editable RT is true", ()=>{
spyOn(workflow.currentState.tempData, "get").and.returnValue({
[CON_LegalQADateTaskStatusCode]:CON_SubformERTTaskStatusCodes.AT_LEAST_ONE_IS_OPEN_OR_NOT_CREATED
});
expect(Subform.editableRT()).toBeTrue();
});
it("Editable RT is false", ()=>{
spyOn(workflow.currentState.tempData, "get").and.returnValue({
[CON_LegalQADateTaskStatusCode]:CON_SubformERTTaskStatusCodes.ALL_ARE_COMPLETED
});
expect(Subform.editableRT()).toBeFalse();
});
it("FEC_FormOnLoad() invoked correctly", async ()=>{
spyOn(Subform.getInstance(), "setStatusCode");
await FEC_FormOnLoad();
expect(workflow.currentState.tempData.set).toHaveBeenCalledWith("Subform_editableRT", Subform.editableRT);
expect(Subform.getInstance().setStatusCode).toHaveBeenCalled();
});
});
describe("MUL Type = 'PrivatePlacementxyz'", ()=>{
let subform;
let myworkflow;
let myTasks;
let myParentId;
beforeAll(()=>{
myworkflow = {currentState:{
formData:{get:()=>{}, set:(...prms)=>{}},
tempData:{get:()=>{}, set:(...prms)=>{}}
}};
myParentId = '556f9f55-6b45-4f5a-a495-xyz';
let myR360 = {common:{workflowbasepath:"http://workflow-host.dev.local/workflow"}};
window.workflow = window.workflow??null;
//global.workflow = global.workflow??null;
//window.r360 = window.r360??myR360;
myTasks =
[
{
"name": "Confirm receipt of xyz",
"modelId": "GEAR-801d94a7-6a7d-4a87-8d13-xyz",
"criteria": "true"
},
{
"name": "Legal xyz",
"modelId": "GEAR-1bc2dc96-282e-468c-bf3f-xyz",
"criteria": "false"
},
{
"name": "Send Executed xyz",
"modelId": "GEAR-15fb5d54-093d-4437-aeef-xyz",
"criteria": "false"
}
];
spyOnValueProperty(window, "workflow").and.returnValue(myworkflow);
spyOn(workflow.currentState.formData, "get").and.returnValue({
FacilityBookingStatus: "Booked",
root:{
id:myParentId,
MULType:'PrivatePlacementxyz',
workflowHistory:{isLocked:true}
}
});
spyOn(workflow.currentState.tempData, "set");
subform = Subform.getInstance();
});
it("Create instance of Subform", ()=>{
subform.init();
expect(subform).toEqual(jasmine.any(Object));
expect(subform.tasks).toEqual(myTasks);
});
it("Set Legal/QA Date Task Status Code ALL_ARE_COMPLETED", async ()=>{
let theQuery = `type:TSK parent:${myParentId} modelId:${myTasks[0].modelId} state:completed`;
spyOn(ABCGeneralFunctions, "getWorkObjects").and.returnValue(Promise.resolve(
{
"data": [{"subState": "COMPLETED"}],
"total": 1, "start": 0, "size": 1
}
));
await subform.setStatusCode();
expect(ABCGeneralFunctions.getWorkObjects).toHaveBeenCalledWith(theQuery, `subState`, ``, 50);
expect(workflow.currentState.tempData.set).toHaveBeenCalledWith(CON_LegalQADateTaskStatusCode, CON_SubformERTTaskStatusCodes.ALL_ARE_COMPLETED);
});
it("Editable RT is true", ()=>{
spyOn(workflow.currentState.tempData, "get").and.returnValue({
[CON_LegalQADateTaskStatusCode]:CON_SubformERTTaskStatusCodes.AT_LEAST_ONE_IS_OPEN_OR_NOT_CREATED
});
expect(Subform.editableRT()).toBeTrue();
});
it("Editable RT is false", ()=>{
spyOn(workflow.currentState.tempData, "get").and.returnValue({
[CON_LegalQADateTaskStatusCode]:CON_SubformERTTaskStatusCodes.ALL_ARE_COMPLETED
});
expect(Subform.editableRT()).toBeFalse();
});
it("FEC_FormOnLoad() invoked correctly", async ()=>{
spyOn(Subform.getInstance(), "setStatusCode");
await FEC_FormOnLoad();
expect(workflow.currentState.tempData.set).toHaveBeenCalledWith("Subform_editableRT", Subform.editableRT);
expect(Subform.getInstance().setStatusCode).toHaveBeenCalled();
});
});
describe("MUL Type = 'Bilateralxyz'", ()=>{
let subform;
let myworkflow;
let myTasks;
let myParentId;
beforeAll(()=>{
myworkflow = {currentState:{
formData:{get:()=>{}, set:(...prms)=>{}},
tempData:{get:()=>{}, set:(...prms)=>{}}
}};
myParentId = '556f9f55-6b45-4f5a-a495-xyz';
let myR360 = {common:{workflowbasepath:"http://workflow-host.dev.local/workflow"}};
window.workflow = window.workflow??null;
//global.workflow = global.workflow??null;
//window.r360 = window.r360??myR360;
myTasks =
[
{
"name": "Confirm receipt of BPA xyz",
"modelId": "GEAR-801d94a7-6a7d-4a87-8d13-xyz",
"criteria": "false"
},
{
"name": "Legal xyz",
"modelId": "GEAR-1bc2dc96-282e-468c-bf3f-xyz",
"criteria": "true"
},
{
"name": "Send Executed xyz",
"modelId": "GEAR-15fb5d54-093d-4437-aeef-xyz",
"criteria": "false"
}
];
spyOnValueProperty(window, "workflow").and.returnValue(myworkflow);
spyOn(workflow.currentState.formData, "get").and.returnValue({
FacilityBookingStatus: "Booked",
root:{
id:myParentId,
MULType:'Bilateralxyz',
workflowHistory:{isLocked:true}
}
});
spyOn(workflow.currentState.tempData, "set");
subform = Subform.getInstance();
});
it("Create instance of Subform", ()=>{
subform.init();
expect(subform).toEqual(jasmine.any(Object));
expect(subform.tasks).toEqual(myTasks);
});
it("Set Legal/QA Date Task Status Code ALL_ARE_COMPLETED", async ()=>{
let theQuery = `type:TSK parent:${myParentId} modelId:${myTasks[1].modelId} state:completed`;
spyOn(ABCGeneralFunctions, "getWorkObjects").and.returnValue(Promise.resolve(
{
"data": [{"subState": "COMPLETED"}],
"total": 1, "start": 0, "size": 1
}
));
await subform.setStatusCode();
expect(ABCGeneralFunctions.getWorkObjects).toHaveBeenCalledWith(theQuery, `subState`, ``, 50);
expect(workflow.currentState.tempData.set).toHaveBeenCalledWith(CON_LegalQADateTaskStatusCode, CON_SubformERTTaskStatusCodes.ALL_ARE_COMPLETED);
});
it("Editable RT is true", ()=>{
spyOn(workflow.currentState.tempData, "get").and.returnValue({
[CON_LegalQADateTaskStatusCode]:CON_SubformERTTaskStatusCodes.AT_LEAST_ONE_IS_OPEN_OR_NOT_CREATED
});
expect(Subform.editableRT()).toBeTrue();
});
it("Editable RT is false", ()=>{
spyOn(workflow.currentState.tempData, "get").and.returnValue({
[CON_LegalQADateTaskStatusCode]:CON_SubformERTTaskStatusCodes.ALL_ARE_COMPLETED
});
expect(Subform.editableRT()).toBeFalse();
});
it("FEC_FormOnLoad() invoked correctly", async ()=>{
spyOn(Subform.getInstance(), "setStatusCode");
await FEC_FormOnLoad();
expect(workflow.currentState.tempData.set).toHaveBeenCalledWith("Subform_editableRT", Subform.editableRT);
expect(Subform.getInstance().setStatusCode).toHaveBeenCalled();
});
});
});
})