Implementing Custom JavaScript Actions for Scriptless Testing
The Scriptless Scenario Editor supports several actions which you as a developer can use to execute custom JavaScript code snippets in your GUI Functional Tests:
- Assert Eval action
- Script Eval action
- Store Eval action
The option to execute custom JavaScript code during a test makes Scriptless Testing very extensible and lets advanced users overcome limitations that a purely "scriptless" approach might bring.
All Taurus actions also support ${Taurus Variables}
that you can reference directly in your JavaScript code.
Data Types and Return Statements
All of the actions available in Scriptless GUI Functional Tests run in the context of the web page you are testing, and you can access the document
of the page in order to perform operations. For more information, see JS Document, W3C Schools.
Write actions that are wrapped with a return statement by using immediately invoked function expressions (IIFE).
(function() {
// Code goes here...
})();
IIFEs are a very basic wrapper that create the function and call it right away. The inner function in this example can contain a return statement that is properly handled, and the returned result is propagated.
How Data Types Are Resolved
By default, variables in scripting actions are either of type number or string.
In the following example, you use the storeEval
action to save the number 5 in a variable named sum
.
Depending on how you use this variable sum
, its type is resolved differently. For example, if you use it in a string context
'${sum}' + ' items'
then, after expansion, the variable resolves to a string:
'5' + ' items'
In this case, the strings are concatenated using the plus operator to '5 items'
.
On the other hand, if you have a numeric context
${sum} + 10
then, after expansion, the variable resolves to a number:
5 + 10
And the numbers are added using the plus operator, resulting in 15
.
The same type resolution rule applies to comparison operators.
Working With Complex Data Types
In BlazeMeter, there is no complex data type, instead you serialize JSON data into a string, and then deserialize it back in another code snippet.
In this example, you have used the storeEval
action to create the variable person
, and you serialize the array using the following script:
(function() {
const person = {name: 'John', surname: 'Doe'};
return JSON.stringify(person);
})();
Later on, you can deserialize the data and use the array fields in another action:
(function() {
const person = JSON.parse('${person}');
console.log(person.name); // Will print 'John'
console.log(person.surname); // Will print 'Doe'
})();
JavaScript Actions
ScriptEval
Description
ScriptEval is the basic action for running custom JavaScript code. The action does not return a value. You can consider it equivalent to returning 'void' in programming languages.
Syntax
For ScriptEval, the entire expression is evaluated, but a common best practice is to have the first line be a call to a function that is defined below, or to an anonymous function
myFunction();
function myFunction(){
document.getElementById("buttonID").setAttribute("disabled", true);
//you can add as many instructions here as needed
}
In the simple example above, the custom action sets the disabled
attribute of a button to true so you can verify with another action that the button is no longer clickable.
BlazeMeter evaluates the function myFunction();
which starts executing the instructions.
Usage
Almost all interaction with the page in JavaScript is done through the JS Document (see W3C Schools). In all actions, you can reference ${Taurus Variables}
directly inside the code. These variables may come from a test data parameter or a CSV file, or they may have been set by other actions.
Here is an example where you set the innerText of the button to a string.
myFunction();
function myFunction(){
document.getElementById("buttonID").innerText = "${variableName}";
}
Taurus replaces the variable ${variableName}
with the value just before the step is executed.
StoreEval
Description
StoreEval is the basic action for running custom code, and it stores the return value of the script in a ${Taurus Variable}
so that you can use the output of a script in the rest of your test.
Syntax
For StoreEval, BlazeMeter prefixes your code with an implicit return
call. Otherwise, the syntax is the same as the other actions.
For example, you enter the following code snippet:
myFunction();
function myFunction(){
document.getElementById("buttonID").setAttribute("disabled", true);
//you can add as many instructions here as needed
}
And BlazeMeter prefixes it implicitly with a return
statement:
return myFunction();
function myFunction(){
document.getElementById("buttonID").setAttribute("disabled", true);
}
However, if you add the return
statement yourself and entered the following code:
return myFunction();
function myFunction(){
document.getElementById("buttonID").setAttribute("disabled", true);
}
Then BlazeMeter would treat it as a duplicated return
statement:
return return myFunction();
function myFunction(){
document.getElementById("buttonID").setAttribute("disabled", true);
}
The main thing that’s different for StoreEval is that it takes an extra parameter, the name of the variable to store the result from the script:
Usage
In this example, you store the return value from the script in the variable outputText
. Then, in a laterType action, you use the variable reference ${outputText}
to type the returned value into an address field. Note that when you declare the variable outputText
in the StoreEval Action, you do not wrap it in ${ .. }
characters.
In this example, BlazeMeter executes the first line as return myFunction();
. And inside myFunction() it executes the line:
return document.getElementById("buttonID").innerText;
The function returns the innerText of the button. The action returns the value and stores it in the variable outputText
which you can then reference as ${outputText}
elsewhere.
The following diagram shows where the value is returned (1), where the variable is declared (2), and where the variable is used (3):
AssertEval
Description
AssertEval is a shortcut that evaluates a custom JavaScript code snippet and asserts that the result returns true. If it does not return true, the action is interpreted as an unmet assertion and the test fails.
Syntax & Usage
When myFunction()
is evaluated, it either returns true or false. In this example, the result depends on the presence of a button with a certain text.
myFunction();
function myFunction(){
if(document.getElementById("buttonID").innerText = "Expected Text")
{return true;}
else
{return false;}
}
Simply verifying whether text exists on the web page can more easily be accomplished by the Assert Text action. This is just a trivial example of how a custom JavaScript returning a true or false value lets you pass or fail an assertion.
Worked Examples
The following two examples show how custom JavaScript functions can read any values that are not directly accessible named Objects, and how a they can modify any value while the script is running, for example, to calculate intermediate values or to normalize text.
Example 1: Working With Values From Basic Tables
The web page you want to test contains a regular HTML table. In this first worked example, you want to calculate the sum of the numeric values in the third column of this table so you can compare it in an assertion later:
a | x | 1 |
b | y | 2 |
c | z | 3 |
The following code snippet selects cells from the third column, and then applies a custom calculation script to the cells' innerText contents.
(function() {
const col3 = document.querySelectorAll('table tr td:nth-child(3)');
return Array.from(col3).reduce((res, x) => res + Number(x.innerText), 0);
})();
You use this custom script in a storeEval
action and declare the variable sum
to store the result. This way, you can reference the calculated value later as ${sum}
in another action. In the example below, you use the result in a conditional if-then-else statement. Since the reduce
function returned a number, you can use the result with numeric comparison operators such as if ${sum} > 5
.
Example 2: Verify a Value in a Complex Table
In the second worked example, you want to verify that a particular value in an HTML table is correct. The table is however irregular and columns and values are sometimes included and sometimes omitted, which means the precise position of values in the HTML document changes depending on circumstances. This causes issues with identifying the Object reliably since, if you attempted to reference it like a static object, you would often read the wrong value.
The following screenshot shows an example of a complex irregular table that you may encounter:
Solution: Theory
While the data isn’t in a consistent place each time, you notice an implicit relationship between labels (keys) and values. In this example, MLR Year
is the key and the number 2019
is its value. Similarly, Exclusion Exception
is the key and the text Does not apply
is its value, and so on.
You can use this consistency to identify a particular value in the table with a minimum level of reliability.
Devising a custom function
You want to devise a custom JavaScript function that takes a pair of key and expected value, and returns true if the value for that key found in the table is the expected value, and false otherwise.
More formally:
F(Key,Value) -> True/False
- If the key is in the table:
- If the value for that key is the expected value, return true.
- If the value for that key is not the expected value, return false.
- If the key is not in the table:
- Return false
Locating the data
Before you can start looking for the value in the table, first find the table on the page, and get each of its rows:
function valueVerifier(key, value) { // replace by ${key} and ${value} later
let table = document.getElementsByTagName("table").item(0);
let tableBody = table.getElementsByTagName("tbody").item(0);
let rows = tableBody.getElementsByTagName("tr");
// more code to be added here
}
How does this code snippet work? You declare the function to accept a key and a value as arguments:
function valueVerifier(key, value)
Since you want to find the table, and more specifically its tbody
tag which contains all of the content, your analyze an excerpt from the table HTML:
<table width="100%" cellspacing="1" class="tableRow2" border="2">
<tbody>
<tr valign="top">
<td>Plan Sponsor Name</td>
<td>Smith inc.</td>
</tr>
In this example, there is only one <table>, and if there are more, then the one you are interested in is the first. Adapt this code to identify any particular table, if needed in your usecase.
let table = document.getElementsByTagName("table").item(0);
You capture the HTML DOM Element Object for the first <table> on the page (from the JavaScript DOM Document), getElementsByTagName("table")
gives you the collection of all the elements of the page which have the tag <table>, and item(0)
gives the first item from this collection.
let tableBody = table.getElementsByTagName("tbody").item(0);
The found table has a <tbody> tag which contains all of the rows in <tr> tags, so you get the tbody element. Since it is unusual for tables to have more than one body, you simply get the first, but you could handle multiple bodies here if it was necessary.
You use getElementsByTagName
, but this time you’re selecting only tags inside the table, not inside the entire document, so you write:
table.getElementsByTagName("tbody").item(0);
You store the resulting element object in a variable named tableBody
.
Going back to the HTML excerpt, you see that the <tbody> contains many <tr> rows, which each contain multiple <td> columns. You need to loop over all of the rows to find your target value, so you need to store the rows in a variable:
let rows = tableBody.getElementsByTagName("tr");
Note, here you are not selecting .item(0)
, since you want to search all rows, not just the first.
Searching the value
Now that you have the collection of rows to iterate over, you plan out the main loop:
- For each row, loop over each column.
- For each column, compare the cell's inner text to the ‘key’ value.
- If the key is present, read the next column to the right and compare it to the expected value.
- If the expected value is present, return true and stop searching.
- If the value is not present, continue the search.
- If the key is not present, continue the search.
- If, after you have checked every column and every row without finding the key-value pair, you return false.
The following code sample loops over all rows to search for the target value:
for (let row of rows) {
let cols = row.getElementsByTagName("td");
for (let colIndex = 0; colIndex < cols.length; colIndex++) {
let col = row.getElementsByTagName("td").item(colIndex);
if (col.textContent.includes(key)) {
if (colIndex + 1 < cols.length) {
let nextColVal = row.getElementsByTagName("td").item(colIndex + 1).textContent;
if (nextColVal.includes(value)) { return true; }
}
}
}
}
Let's go through this example step by step.
Looping over rows
Looping over a collection of elements is quite easy with JavaScript:
for (let row of rows) {
// we can do any work here with the 'row'
}
Within each loop, first get a collection of the columns. You do that as before with the following line:
let cols = row.getElementsByTagName("td");
Looping over columns
To loop over the columns, don't use a for (let col of cols)
loop since you will need the index so that you can refer to the next element. There are other ways of doing this, but the following approach is the simplest to understand:
for (let colIndex = 0; colIndex < cols.length; colIndex++) {
// we can do any work with the column and colIndex here
}
Here you have a colIndex
that you can use to access any column: Start with colIndex = 0
and increment it (colIndex++
) until your colIndex
is the same as the number of columns for that particular row (cols.length
).
let col = row.getElementsByTagName("td").item(colIndex);
From here, you can do whatever you need to do with this individual column.
Analyzing the HTML Structure
The analyzed excerpt shows you that the structure of rows in this complex sample table is quite irregular:
<tr valign="top">
<td>Controlling Field Office</td>
<td>121 Atlanta FO</td>
<td>Contract State</td>
<td>TX</td>
<td> </td>
<td> </td>
<td> </td>
<td></td>
</tr>
<tr valign="top">
<td>Employer Class</td>
<td>LIMITED LIABILITY CORP</td>
<td> </td>
</tr>
From the problem statement, you know that if the key is present, its value is always one column to the right of the column the key is in. You see that each <tr> row can have a varying number of <td> columns.
If you search through every row and column, and find the key, you know that the value must be in the next column. If the value matches, you return true; if it doesn’t exist, doesn’t match, or you never find the key, you return false.
If you find the key in a column defined as
let col = row.getElementsByTagName("td").item(colIndex);
then, if the value exists, it must be in a neighboring column defined as:
let nextCol = row.getElementsByTagName("td").item(colIndex + 1)
Here, the cell one to the right is the ‘next’ item in the HTML structure, or colIndex+1
.
Finding the value
Let's look back at the code sample:
for (let row of rows) {
let cols = row.getElementsByTagName("td");
for (let colIndex = 0; colIndex < cols.length; colIndex++) {
let col = row.getElementsByTagName("td").item(colIndex);
if (col.textContent.includes(key)) {
if (colIndex + 1 < cols.length) {
let nextColVal = row.getElementsByTagName("td").item(colIndex + 1).textContent;
if (nextColVal.includes(value)) { return true; }
}
}
}
}
On line 4, you have identified the column cell col
that you are looping over. You ask the question whether the current col
’s text includes the key you are looking for:
if (col.textContent.includes(key)) { ... }
The .textContent
property exists for any HTML Element, and .includes(someString)
is a function of all JavaScript strings. Here you’re verifying whether the key is included somewhere in the text content of this table cell. If the key doesn’t exist, you continue looking at the next column, and so on.
If you do find the key in a particular column, the next question is whether this row has a ‘next’ column:
if (colIndex + 1 < cols.length) { ... }
That is, if the number of columns is greater than the current index, then a column with an index of colIndex + 1
exists, and you can access it:
let nextCol = row.getElementsByTagName("td").item(colIndex + 1)
In the actual code, you save some space by getting the value of the next column in only one line:
let nextColVal = row.getElementsByTagName("td").item(colIndex + 1).textContent;
Finally, you compare this string with your target value, and if it matches, you return true:
if (nextColVal.includes(value)) { return true; }
In all other circumstances, the loop continues, it looks at the next column, and the next row, until the entire table has been checked. If by this point you have not returned true, you return false.
The following code sample shows the complete function:
function valueVerifier(key, value) { // replace by ${key} and ${value} later
let table = document.getElementsByTagName("table").item(0);
let tableBody = table.getElementsByTagName("tbody").item(0);
let rows = tableBody.getElementsByTagName("tr");
for (let row of rows) {
let cols = row.getElementsByTagName("td");
for (let colIndex = 0; colIndex < cols.length; colIndex++) {
let col = row.getElementsByTagName("td").item(colIndex);
if (col.textContent.includes(key)) {
if (colIndex + 1 < cols.length) {
let nextColVal = row.getElementsByTagName("td").item(colIndex + 1).textContent;
if (nextColVal.includes(value)) { return true; }
}
}
}
}
return false;
}
Calling the custom function
The last thing you need to do is call the function with some arguments. For that you add the line console.log(valueVerifier("MLR Year", "2019"));
The following code sample shows the complete function call:
console.log(valueVerifier("MLR Year", "2019"));
function valueVerifier(key, value) { // replace by ${key} and ${value} later
let table = document.getElementsByTagName("table").item(0);
let tableBody = table.getElementsByTagName("tbody").item(0);
let rows = tableBody.getElementsByTagName("tr");
for (let row of rows) {
let cols = row.getElementsByTagName("td");
for (let colIndex = 0; colIndex < cols.length; colIndex++) {
let col = row.getElementsByTagName("td").item(colIndex);
if (col.textContent.includes(key)) {
if (colIndex + 1 < cols.length) {
let nextColVal = row.getElementsByTagName("td").item(colIndex + 1).textContent;
if (nextColVal.includes(value)) { return true; }
}
}
}
}
return false;
}
You don’t technically need the console.log()
, but it will aid you with debugging while you write the script.
Debugging the code snippet
The simplest way to debug a piece of JavaScript like this is by using Chrome Dev Tools on the target web page. For more information, see Chrome DevTools.
To open the Dev Tools, press F12 in Chrome. On the Dev Tools pane, paste the entire code snippet into the Console tab and press enter. It returns true or false depending on whether it found the key value pair in the first table.
If you add the line debugger;
, it triggers a breakpoint in the Chrome debugger. The Chrome Debugger helps you see the exact state of the scope and code at that point, and lets you step through the code line by line.
You can also add console.log(value)
lines to log values to the Chrome console. In this example, you might want to follow the loop execution by printing all non-matching values, like this:
console.log("Did we find the value? " + valueVerifier("MLR Year", "2019"));
function valueVerifier(key, value) {
debugger;
let table = document.getElementsByTagName("table").item(0);
let tableBody = table.getElementsByTagName("tbody").item(0);
let rows = tableBody.getElementsByTagName("tr");
for (let row of rows) {
let cols = row.getElementsByTagName("td")
for (let colIndex = 0; colIndex < cols.length; colIndex++) {
let col = row.getElementsByTagName("td").item(colIndex);
if (col.textContent.includes(key)) {
debugger;
if (colIndex + 1 < cols.length) {
let nextColVal = row.getElementsByTagName("td").item(colIndex + 1).textContent;
if (nextColVal.includes(value)) {
console.log("found target key \"%s\", and value \"%s\"", key, value)
return true;
} else {
console.log("Found a value of \"%s\", instead of \"%s\"",
nextColVal.textContent.trimEnd(), value);
}
}
} else {
console.log("Didn't find the key \"%s\", found \"%s\" instead",
key, colValue.textContent.trimEnd());
}
}
}
console.log("Target value and key not in table")
return false;
}
The debugger;
statement is much more powerful than merely logging values to the console. For more information about breakpoints, see Get Started with Debugging JavaScript in Chrome DevTools.
Running the JavaScript Snippet in a Scriptless Test
Now that you have written your custom code snippet and debugged it, you want to use it in a Scriptless test.
In this case you only care about whether the value is present, so you use an Assert Eval
action. If you cared about what the value was, you could adjust your code and use a Store Eval
action.
In the action, switch to Fullscreen Edit Mode and paste the script:
Make two adjustments to the code:
- Remove the
console.log()
debug line. You want only the true/false result fromvalueVerifier()
function. - Use Taurus variables for values in the
valueVerifier()
function:
valueVerifier("${Key}", "${Value}");
BlazeMeter replaces the Taurus variables ${Key}
and ${Value}
by test data values that you have set earlier in the test, or from a CSV file.
The final snippet in the action looks like this:
Note, you don’t need a return call here, the action returns the result value automatically.
Notes
- The HTML DOM is the context for all scripts, and there is a lot of public information on this and how to use it with JavaScript.
- Writing these scripts is best done in an IDE like VisualStudio Code or alike.
- Debugging these scripts is best done in Chrome with dev tools (F12), and be sure to use the
debugger;
statement. - When using your snippet in Scriptless, remember it will implicitly insert a return call at the start of the code.
- Usually, it is best practice to do most of the work in a function, then calling that function on the first line.
- You can use ${Taurus Variables} directly in the code, but remember these will come in as strings and you should wrap them in "double quotes".
- AssertEval will assert based on the return value, StoreEval will store the returned value, and ScriptEval will run your script but return nothing.
Appendix
Final Code
valueVerifier("${Key}", "${Value}");
function valueVerifier(key, value) {
let table = document.getElementsByTagName("table").item(0);
let tableBody = table.getElementsByTagName("tbody").item(0);
let rows = tableBody.getElementsByTagName("tr");
for (let row of rows) {
let cols = row.getElementsByTagName("td");
for (let colIndex = 0; colIndex < cols.length; colIndex++) {
let col = row.getElementsByTagName("td").item(colIndex);
if (col.textContent.includes(key)) {
if (colIndex + 1 < cols.length) {
let nextColVal = row.getElementsByTagName("td").item(colIndex + 1).textContent;
if (nextColVal.includes(value)) { return true; }
}
}
}
}
return false;
}