# Extensions Library

This extension library provides a set of ready to use examples you can add to Virtuoso with a single click.

There is no coding necessary to use any of these extensions, but if you are a developer, you can view the source of each extension and even customize them further or use them as base for making your own extensions.

# Available extensions:

# Making API calls

You can make API calls in Virtuoso by relying on the underlying XHR capabilities of the browser.

Note that in some cases you may wish to disable the Enforce same-origin policy option in your goals' advanced settings, in order to avoid cross-origin issues.

API management capabilities in Virtuoso

Note that most of the use cases below which were designed to call APIs in extensions can now be achieved with Virtuoso's API management.

In this section, we provide examples for making API calls using Virtuoso extensions.

# Make GET call

You can make GET REST API calls, by simply calling the extension API_GET, and supplying the URL as the parameter (you can include any query parameters on the URL as well).

Examples:

 


API_GET("https://reqres.in/api/users?page=2") returning $data
assert $data.page equals "2"
View source
// Last updated: 14/01/2021, 17:44:34 UTC
// Resources:
//   https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js

const makeRequest = async (url, headers = '') => {
  try {
    const { data } = await axios.get(
        url,
        headers ? { headers: JSON.parse(headers) } : {},
    )
    done(JSON.stringify(data))
  } catch (e) {
    doneError(e)
  }
}

if (!url) {
  throw new Error('URL parameter is missing')
}

makeRequest(url, headers)

# Make POST call

You can make POST REST API calls, by simply calling the extension API_POST, supplying the URL as the parameter, and the body of the post request.

Example:

 


API_POST ("https://reqres.in/api/users", '{"name":"Virtuoso", "job":"Intelligent Quality Assistant"}') returning $data
Assert $data.name equals "Virtuoso"

You can even construct the request body JSON using the store command:



 


Store value "Virtuoso" in $request.name
Store value "Intelligent Quality Assistant" in $request.job
API_POST ("https://reqres.in/api/users", $request) returning $data
Assert $data.name equals "Virtuoso"
View source
// Last updated: 14/01/2021, 17:44:45 UTC
// Resources:
//   https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js

const makeRequest = async (url, body, headers = '') => {
  try {
    const { data } = await axios.post(
        url,
        JSON.parse(body),
        headers ? {headers: JSON.parse(headers)} : {},
    )
    done(JSON.stringify(data))
  } catch (e) {
    doneError(e)
  }
}

if (!url) {
  throw new Error('URL parameter is missing')
}

if (!body) {
  throw new Error('body parameter is missing')
}

makeRequest(url, body, headers)

# Make PUT call

You can make PUT REST API calls, by simply calling the extension API_PUT, supplying the URL as the parameter, and the body of the put request.

Example:

 


API_PUT ("https://reqres.in/api/users/2", '{"first_name":"John"}') returning $data
Assert $data.first_name equals "John"

You can even construct the request body JSON using the store command:



 


Store value "John" in $request.first_name
Store value "read" in $request.access
API_PUT ("https://reqres.in/api/users/2", $request) returning $data
Assert $data.first_name equals "John"
View source
// Last updated: 14/01/2021, 17:44:54 UTC
// Resources:
//   https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js

const makeRequest = async (url, body, headers = '') => {
  try {
    const { data } = await axios.put(
        url,
        JSON.parse(body),
        headers ? {headers: JSON.parse(headers)} : {},
    )
    done(JSON.stringify(data))
  } catch (e) {
    doneError(e)
  }
}

if (!url) {
  throw new Error('URL parameter is missing')
}

if (!body) {
  throw new Error('body parameter is missing')
}

makeRequest(url, body, headers)

# Make DELETE call

You can make DELETE REST API calls, by simply calling the extension API_DELETE, and supplying the URL as the parameter.

Example:

 

API_DELETE ("https://reqres.in/api/users/2") returning $data

Delete operation responses

Note that a success delete operation usually returns an empty response with 204 as status code, if the status code differs from 2xx, the test would fail.

View source
// Last updated: 14/01/2021, 17:44:22 UTC
// Resources:
//   https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js

const makeRequest = async (url, headers = '') => {
  try {
    const { data } = await axios.delete(
        url,
        headers ? { headers: JSON.parse(headers) } : {},
    )
    done(JSON.stringify(data))
  } catch (e) {
    doneError(e)
  }
}

if (!url) {
  throw new Error('URL parameter is missing')
}

makeRequest(url, headers)

# Advanced API calls

Some APIs require specific parameters to be passed on the request headers (e.g., authentication, content type, etc). To support this, all above API extensions have an additional optional headers parameter to support this.

Content type example


 

store value '{"content-type":"multipart/form-data"}' in $contentTypeHeader
API_POST ("https://reqres.in/api/users", '{"name":"Virtuoso", "job":"Intelligent Quality Assistant"}', $contentTypeHeader) returning $data

Header authentication example (authentication token stored in $token variable)


 

store value ${'Bearer ' + $token} in $authenticationHeader.Authorization
API_POST ("https://reqres.in/api/users", '{"name":"Virtuoso", "job":"Intelligent Quality Assistant"}', $authenticationHeader) returning $data

# Get computed style

This extension can be used to extract specific computed style values of an element. It can be used with XPath selectors or CSS selectors.

Examples:


 



 


// XPath selector
getComputedStyle("//*[@id='gbw']/div/div[2]", "font-size") returning $fontSize
Assert $fontSize equals "13px"

// CSS selector
getComputedStyle("#gbw > div > div:nth-child(2)", "font-family") returning $fontFamily
Assert $fontFamily equals "arial, sans-serif"
View source
// Last updated: 12/05/2020, 07:46:10 UTC
function css( selector, property ) {
  if (!selector) throw new Error('Element selector not provided')
  if (!property) throw new Error('Property name not provided')

  let element = null

  try {
    // Try using XPATH
    element = document.evaluate(selector, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue
  } catch { 
    try {
      // Try using CSS selector if XPATH method failed
      element = document.querySelector(selector)
    } catch { }
  }
  
  // Both XPATH and CSS selector methods failed
  if (!element) throw new Error('Could not find element using provided selector')

  // A CSSStyleDeclaration object containing CSS declaration block of the element
  const computedStyle = window.getComputedStyle( element, null )

  return computedStyle.getPropertyValue( property )
}

return css(selector, property)

# Generate Random Data

This extension enables you to generate random data on demand using the Faker.js library.

You can use various data types, as documented below:
  • address
    • zipCode
    • zipCodeByState
    • city
    • cityPrefix
    • citySuffix
    • cityName
    • streetName
    • streetAddress
    • streetSuffix
    • streetPrefix
    • secondaryAddress
    • county
    • country
    • countryCode
    • state
    • stateAbbr
    • latitude
    • longitude
    • direction
    • cardinalDirection
    • ordinalDirection
    • nearbyGPSCoordinate
    • timeZone
  • animal
    • dog
    • cat
    • snake
    • bear
    • lion
    • cetacean
    • horse
    • bird
    • cow
    • fish
    • crocodilia
    • insect
    • rabbit
    • type
  • commerce
    • color
    • department
    • productName
    • price
    • productAdjective
    • productMaterial
    • product
    • productDescription
  • company
    • suffixes
    • companyName
    • companySuffix
    • catchPhrase
    • bs
    • catchPhraseAdjective
    • catchPhraseDescriptor
    • catchPhraseNoun
    • bsAdjective
    • bsBuzz
    • bsNoun
  • database
    • column
    • type
    • collation
    • engine
  • datatype
    • number
    • float
    • datetime
    • string
    • uuid
    • boolean
    • hexaDecimal
    • json
    • array
  • date
    • past
    • future
    • between
    • betweens
    • recent
    • soon
    • month
    • weekday
  • fake
  • finance
    • account
    • accountName
    • routingNumber
    • mask
    • amount
    • transactionType
    • currencyCode
    • currencyName
    • currencySymbol
    • bitcoinAddress
    • litecoinAddress
    • creditCardNumber
    • creditCardCVV
    • ethereumAddress
    • iban
    • bic
    • transactionDescription
  • git
    • branch
    • commitEntry
    • commitMessage
    • commitSha
    • shortSha
  • hacker
    • abbreviation
    • adjective
    • noun
    • verb
    • ingverb
    • phrase
  • helpers
    • randomize
    • slugify
    • replaceSymbolWithNumber
    • replaceSymbols
    • replaceCreditCardSymbols
    • repeatString
    • regexpStyleStringParse
    • shuffle
    • mustache
    • createCard
    • contextualCard
    • userCard
    • createTransaction
  • image
    • image
    • avatar
    • imageUrl
    • abstract
    • animals
    • business
    • cats
    • city
    • food
    • nightlife
    • fashion
    • people
    • nature
    • sports
    • technics
    • transport
    • dataUri
    • lorempixel
    • unsplash
    • lorempicsum
  • internet
    • avatar
    • email
    • exampleEmail
    • userName
    • protocol
    • httpMethod
    • url
    • domainName
    • domainSuffix
    • domainWord
    • ip
    • ipv6
    • port
    • userAgent
    • color
    • mac
    • password
  • lorem
    • word
    • words
    • sentence
    • slug
    • sentences
    • paragraph
    • paragraphs
    • text
    • lines
  • mersenne
    • rand
    • seed
    • seed_array
  • music
    • genre
  • name
    • firstName
    • lastName
    • middleName
    • findName
    • jobTitle
    • gender
    • prefix
    • suffix
    • title
    • jobDescriptor
    • jobArea
    • jobType
  • phone
    • phoneNumber
    • phoneNumberFormat
    • phoneFormats
  • random
    • number
    • float
    • arrayElement
    • arrayElements
    • objectElement
    • uuid
    • boolean
    • word
    • words
    • image
    • locale
    • alpha
    • alphaNumeric
    • hexaDecimal
  • system
    • fileName
    • commonFileName
    • mimeType
    • commonFileType
    • commonFileExt
    • fileType
    • fileExt
    • directoryPath
    • filePath
    • semver
  • time
    • recent
  • unique
  • vehicle
    • vehicle
    • manufacturer
    • model
    • type
    • fuel
    • vin
    • color
    • vrm
    • bicycle

Examples:

generateRandom("{{name.firstName}} {{name.lastName}}") returning $fullName
generateRandom("{{address.city}}") returning $randomCity
generateRandom("{{lorem.sentence}}") returning $loremIpsum
generateRandom("{{random.number}}") returning $randomNumber
generateRandom("{{datatype.uuid}}") returning $randomUuid

WARNING

The version of the Faker.js library used in this extension is not the latest available, but one that is compatible with the browser environment, as explained in Using external libraries. If you need a more recent version, you can modify the extension by using a different resource.

View source
// Last updated: 18/04/2023, 11:48:42 UTC
// Resources:
//   https://cdn.jsdelivr.net/npm/faker@5.5.3/dist/faker.min.js

try {
  done(faker.fake(type))
} catch (e) {
  doneError(e)
}

# Get query parameter value by name

This extension receives a query parameter name and returns its value. Let's say we are testing an action on the application that should update a query parameter, with this extension we can assert the action is updating the query parameter adding two simple steps after doing the action.

Example usage:


 


navigate to "https://www.google.com/?source=hp"
getQueryParameterByName("source") returning $param
assert $param equals "hp"

In the above example, on line two, this extension extracts the value of source from the current URL and stores it in a variable named $param, so we can use it later, for example, for asserting its value like in line three.

View source
function getQueryParameterByName(name, url) {
    if (!url) url = window.location.href;
    name = name.replace(/[\[\]]/g, '\\$&');
    var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'),
        results = regex.exec(url);
    if (!results) return null;
    if (!results[2]) return '';
    return decodeURIComponent(results[2].replace(/\+/g, ' '));
}

return getQueryParameterByName(name, window.location.href)

# Drag and Drop

Although Virtuoso supports drag and drop (using mouse over "from" then mouse drag to "target"), in few special cases of HTML5 drag and drop implementations, the events fired from browser automation on Virtuoso may not trigger the events the page is expecting.

In these circumstances, you can use this extension to overcome the limitation.

Example using selectors:

dragAndDrop(".from.selector", "#to.selector")

Example using stored elements:

store element details of "Element to drag from" in $from
store element details of "Element to drop to" in $to
dragAndDrop($from, $to)
View source
// Last updated: 13/07/2023, 03:34:14 UTC
/**
 * Use this script providing "src" as stored element details that you want to drag,
 * typically (draggable=true), and providing "dst" as stored element details where you will
 * be dragging "src".
 */

const EVENT_TYPES = {
  DRAG_END: 'dragend',
  DRAG_START: 'dragstart',
  DROP: 'drop'
}

function simulateDragDrop(src, dst) {
  src.focus()
  const event = createCustomEvent(EVENT_TYPES.DRAG_START)
  dispatchEvent(src, EVENT_TYPES.DRAG_START, event)

  const dropEvent = createCustomEvent(EVENT_TYPES.DROP)
  dropEvent.dataTransfer = event.dataTransfer
  dispatchEvent(dst, EVENT_TYPES.DROP, dropEvent)

  const dragEndEvent = createCustomEvent(EVENT_TYPES.DRAG_END)
  dragEndEvent.dataTransfer = event.dataTransfer
  dispatchEvent(src, EVENT_TYPES.DRAG_END, dragEndEvent)
}


function createCustomEvent(type) {
  var event = new CustomEvent("CustomEvent")
  event.initCustomEvent(type, true, true, null)
  event.dataTransfer = {
    data: {
    },
    setData: function (type, val) {
      this.data[type] = val
    },
    getData: function (type) {
      return this.data[type]
    }
  }
  return event
}

function dispatchEvent(node, type, event) {
  if (node.dispatchEvent) {
    return node.dispatchEvent(event)
  }
  if (node.fireEvent) {
    return node.fireEvent("on" + type, event)
  }
}

function getElementByXpath(path) {
  return document.evaluate(path, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
}

function findElement(element) {
  try {
      element = JSON.parse(element)
  } catch(e){}
  var selectors
  if (typeof element === 'string') {
    var type = 'CSS_SELECTOR'
    if (element.startsWith('//*')) {
      type = 'XPATH_ID'
    } else if (element.startsWith('/')) {
      type = 'XPATH'
    }
    selectors = [{ type, value: element }]
  } else {
    selectors = element.selectors
  }
  for (var i in selectors) {
    var selector = selectors[i]
    if(!selector.type) continue
    var el = null
    switch(selector.type) {
      case 'XPATH_ID':
      case 'XPATH':
        el = getElementByXpath(selector.value)
        break
      case 'CSS_SELECTOR':
        el = document.querySelector(selector.value)
        break
      case 'ID':
        el = document.getElementById(selector.value)
        break
    }
    if (el) return el
  }
  return null
}

if (!src) throw new Error('src is required')
if (!dst) throw new Error('dst is required')

var fromElement = findElement(src)
if(!fromElement) throw new Error('Could not find from element: ' + src)
var toElement = findElement(dst)
if(!toElement) throw new Error('Could not find to element: ' + dst)

simulateDragDrop(
  fromElement,
  toElement,
)

# View PDF files

Extensions are very powerful and can perform complex functionality. An example of a complex extension is one which opens a PDF file in the web browser, so that you can see its content and perform assertions.

You can also store PDF file as an environment variable, so you can use it later in your tests.

Parameters:

  • file (required), the URL of the PDF file;
  • password (optional), the password to open the PDF file (if it is encrypted);

If the PDF file is not available as a public link, the first step is to download the PDF file in a test step; for example by clicking on it:



 


click "Download PDF"
Navigate to "https://s3-eu-west-1.amazonaws.com/virtuoso-downloaded-files/test.html"
showPdf($LAST_DOWNLOADED_FILE)
see "Some text on the PDF"

Download file in new tab

As a limitation of our PDF download feature, PDF files opened in a new tab will cause the test step to time out. If possible, trying downloading the file in the current tab (e.g., by removing target="_blank" of a link) or explicitly mark the link as a download (e.g., by appending download='somepdf.pdf' to the anchor tag).

Test steps or extensions can be used to drop the target attribute from the page currently open. For example:



 



// To remove opening in a new window for all links in the current page:
Execute "[...document.querySelectorAll('a[target=\"_blank\"]')].forEach(e=>e.removeAttribute('target'))"

// To only remove on links that contain pdf on the url:
Execute "[...document.querySelectorAll('a[href*=\"pdf\"]')].forEach(e=>e.removeAttribute('target'))"

If the PDF file is available as a public link (e.g., doesn't require a download), first navigate to the same host as the one showing the PDF (e.g., navigate to "https://test.virtuoso.qa" if the file is on https://test.virtuoso.qa/path/file.pdf):


 


Navigate to "https://test.virtuoso.qa"
showPdf("https://test.virtuoso.qa/path/file.pdf")
see "Some text on the PDF"

If the PDF file is password protected, you can pass the password as a parameter:


 


Navigate to "https://test.virtuoso.qa"
showPdf("https://test.virtuoso.qa/path/file.pdf", "password")
see "Some text on the PDF"
View source
// Last updated: 01/05/2025, 07:14:57 UTC
// Resources:
//   https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js
//   https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js

async function showPdf (file, password) {
  document.body.style = "background: grey; padding:5px;"
  document.body.innerHTML = '<div id="pageContainer"></div>'
  var PDF_PATH = file;
  var SVG_NS = 'http://www.w3.org/2000/svg';
  document.title = PDF_PATH

  function buildSVG(viewport, textContent) {
    var svg = document.createElementNS(SVG_NS, 'svg:svg');
    svg.setAttribute('width', viewport.width + 'px');
    svg.setAttribute('height', viewport.height + 'px');
    svg.setAttribute('font-size', 1);

    textContent.items.forEach(function (textItem) {

      if (textItem.str.trim().length === 0){
        return
      }

      var tx = pdfjsLib.Util.transform(
          pdfjsLib.Util.transform(viewport.transform, textItem.transform),
          [1, 0, 0, -1, 0, 0]);
      var style = textContent.styles[textItem.fontName];
      // adding text element
      var text = document.createElementNS(SVG_NS, 'svg:text');
      text.setAttribute('transform', 'matrix(' + tx.join(' ') + ')');
      text.setAttribute('font-family', style.fontFamily);
      text.setAttribute('fill-opacity', 0);
      text.textContent = textItem.str;
      svg.appendChild(text);
    });
    return svg;
  }

  var otherParams = password ? { password } : {}
  var loadingTask = pdfjsLib.getDocument({url: PDF_PATH, ...otherParams });
  loadingTask.promise.then(function (pdfDocument) {
    for (let i = 0; i < pdfDocument.numPages; i++) {
      const div = document.createElement("div")
      div.id = "page-" + i
      div.style = "border: 1px solid black; background: white; margin-bottom: 3px; relative"
      document.getElementById('pageContainer').appendChild(div);
      pdfDocument.getPage(i + 1).then(function (page) {
        const normalViewport = page.getViewport({ scale: 1.0 })
        const viewport = page.getViewport({ scale: window.innerWidth / normalViewport.width });

        const scale = [
          viewport.width / normalViewport.width,
          viewport.height / normalViewport.height
        ]

        page.getTextContent().then(function (textContent) {
          const parentDiv = document.getElementById(div.id)
          parentDiv.style.height = `${viewport.height}px`
          parentDiv.style.width = `${viewport.width}px`

          const pageDiv = document.createElement("div")
          pageDiv.style = "position: relative; height: 100%;";

          parentDiv.appendChild(pageDiv)

          const canvas = document.createElement('canvas')
          canvas.style = "width: 100%; height: 100%; position: absolute; top: 0px; left: 0px; background: white;"
          canvas.height = viewport.height
          canvas.width = viewport.width
          pageDiv.appendChild(canvas)
          page.render({
            canvasContext: canvas.getContext('2d'),
            viewport: viewport
          })

          var svg = buildSVG(viewport, textContent);
          svg.style = "opacity: 1; width: 100%; height: 100%; position: absolute; top: 0px; left: 0px; z-index: 200"
          pageDiv.appendChild(svg);
        }).then(() => {
          page.getAnnotations().then(function (annotations) {
            const pageDiv = document.getElementById(div.id).lastChild
            annotations.forEach(annotation => {

              const link = document.createElement("a")
              link.href = annotation.url
              const button = document.createElement("button")
              const rect = annotation.rect
              link.style = `display: block; position: absolute; left: ${rect[0]*scale[0]}px; z-index:400;
            bottom: ${rect[1]*scale[1]}px;`
              button.style = `opacity: 0; width: ${(rect[2]-rect[0])*scale[0]}px;
              height: ${(rect[3]-rect[1])*scale[1]}px;`
              button.innerText = ""

              link.appendChild(button)
              pageDiv.appendChild(link);
            })
          })
        });
      });
    }
  }).then(done()).catch(error => {
    console.error(error.details)
    doneError(error)
  })
}

try {
  showPdf(file, password)
} catch (error){
  console.error(error.details)
  doneError(error)
}

# Validate that images have alt attributes for accessibility

The W3C Web Content Accessibility Guidelines (WCAG) require that a text alternative should be provided for non-text content unless it is for decoration or formatting. This extension checks that images have an appropriate description for screen readers. If an image does not have an alt text attribute it will be highlighted red on the page and the step will fail. Compliant images will be highlighted with a green border.

Example usage:

Navigate to "https://test.virtuoso.qa"
validateAllImagesAltAttribute
View source
// Last updated: 25/09/2020, 13:48:20 UTC
const badElements = document.evaluate('//img[not(@alt) or @alt=""]', document)


let element = badElements.iterateNext()
let  _throw = false;
let badElementsSaved = []

while(element){
    badElementsSaved.push(element)
    _throw = true
    element = badElements.iterateNext();
}



const goodElements = document.evaluate('//img[@alt and @alt!=""]', document);
let goodElement = goodElements.iterateNext();
let goodElementsSaved = []
while(goodElement){
    goodElementsSaved.push(goodElement);
    goodElement = goodElements.iterateNext()
}

console.log(badElements)
console.log(goodElements)

// Comment or uncomment these to enable/disable highlights of the elements
badElementsSaved.forEach(it => it.style = 'outline:solid red 3px;')
goodElementsSaved.forEach(it => it.style = 'outline:solid green 3px;')


if(_throw){
    console.error("Missing image alt attribute(s)")
    throw new Error("img element(s) " + badElementsSaved.map(it => it.src) + " doesn't have an alt attribute (or it has an empty alt attribute)")
}

# Get accessibility violations

You can fetch the set of accessibility violations on your page (if any) by using the Axe library in your journeys.

Using the extension, you add a test step to collect the violations detected into a variable, which you can then review as part of the side effects, or to add further validations based on them, e.g., to make sure they don't contain a specific violation, or that there are no violations.

Example usage:

getAxeViolations returning $violations
// check that there are no violations
assert ${ $violations.length } equals "0"
View source
// Last updated: 04/03/2021, 15:52:42 UTC
// Resources:
//   https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.1.2/axe.min.js

axe.run().then(results => results.violations).then(JSON.stringify).then(done)

Use this extension when you need to render some valid HTML string into the document.

Parameters:

  • content (required), the valid HTML string you wish to render;
  • elementId (optional), you can customize the element ID of the container where your content will be rendered (e.g., to select that element later easily).

Example:


 

 


store value "<div>Test</div>" in $html
WriteHTML($html)
look for "Test"
WriteHTML($html, "customId")
store element details of "customId"

Note this extension will modify the DOM of your application (during the test session), if you want a blank page to do your tests, you can add this step before using the extension:

navigate to "https://s3-eu-west-1.amazonaws.com/virtuoso-downloaded-files/test.html"
View source
// Last updated: 13/12/2021, 11:34:10 UTC
function write (content, elementId = 'WriteHTMLContent') {
  if (!content) throw new Error('Content is required')
  const container = document.createElement('div')
  container.setAttribute('id', elementId)
  container.innerHTML = content
  document.body.appendChild(container)
}

return write(content, elementId)

# Get and parse XLS sheets

Use this extension to download and parse any XLS sheet to make assertions later on your journey.

Parameters:

  • file (required), the URL of the XLS file;
  • sheetName (optional), the sheet name that should be retrieved (if not provided it will return the first sheet);
  • toHtml (optional), if provided it will return the parsed content as a valid HTML string (e.g., to render it later).

Example returning JSON:


 

 


navigate to "https://s3-eu-west-1.amazonaws.com/virtuoso-downloaded-files/test.html"
getAndParseXLS("url") returning $json
assert $json[0].Name equals "John"
getAndParseXLS("url", "another-sheet") returning $json
assert $json[3].Value equals "100"

Combining this extension with Print HTML content extension you can print the representation of the sheet as a webpage:


 



navigate to "https://s3-eu-west-1.amazonaws.com/virtuoso-downloaded-files/test.html"
getAndParseXLS("url", "sheetName", "true") returning $html
WriteHTML($html)
look for "John"
View source
// Last updated: 26/01/2023, 13:42:33 UTC
// Resources:
//   https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.2/axios.min.js
//   https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js

async function getAndParseXLS(file, sheetName, toHtml = false) {
  const { data } = await axios.get(file, { responseType: 'blob' })

  return new Promise((resolve) => {
    const reader = new FileReader()
    reader.onload = function () {
      const fileData = reader.result
      const wb = XLSX.read(fileData, { type: 'binary' })

      // setting this to true will output HTML instead of JSON
      if (toHtml) {
        resolve(XLSX.utils.sheet_to_html(wb.Sheets[sheetName || wb.SheetNames[0]]))
        return
      }
      
      resolve(XLSX.utils.sheet_to_row_object_array(wb.Sheets[sheetName || wb.SheetNames[0]]))
    }
    reader.readAsBinaryString(data)
  })
}

getAndParseXLS(file, sheetName, toHtml).then(done).catch(doneError)

# OTP Authenticator (2FA)

Many modern applications use the TOTP algorithm as part of 2FA/MFA login flows. This extension enables you to produce the same token that an authenticator app would produce (by default 6-digit tokens).

Parameters:

  • otpKey (required), key to generate OTP (this is secret query parameter in Key Uri Format) if you were presented with a QR code, this equates to the string value of the QR code);

Example:

OTP_Authenticator("lojbhofkt6rzchu7jt25hy62s7vunuu5l5dtwy5pbqrsevft366us5xu") returning $token;

Alternatively you can use environment sensitive variables to store the OTP key and then pass it to the extension:

OTP_Authenticator($otpKey) returning $token;

Using different hash algorithms

The extension generates tokens using the SHA1 hash algorithm, which is the default in most cases. If you need to use a different hash algorithm (e.g., SHA256), you can modify the extension by uncommenting the group of the desired hash.

If you make changes, we recommend renaming the extension to reflect those changes. This will improve the visibility of the algorithm used on your tests (especially in cases where you need to use more than one version of the extension).

View source
// Last updated: 06/06/2024, 13:40:20 UTC
// Resources:
//   https://unpkg.com/@otplib/preset-browser@^12.0.0/buffer.js
//   https://unpkg.com/@otplib/preset-browser@^12.0.0/index.js

// By default this extension uses sha1 as algorithm, if the authenticator needs a different hash uncomment one of the groups below:

// - for sha256
/*
otplib.authenticator.options = {
  algorithm: "sha256"
}
*/

// - for sha512
/*
otplib.authenticator.options = {
  algorithm: "sha512"
}
*/

return otplib.authenticator.generate(secret)

# Wait for element attribute

In dynamic applications, sometimes you need to wait until an element attribute has changed to a desired value (or been removed). For example, you want to make sure that after you click a button, it becomes "disabled" (e.g., it'll have a disabled attribute). This extension helps you easily achieve this.

Parameters:

  • element (required), the target element of this operation, which you either stored from the page (using store element details of ... in $element), or an xpath/css selector string (e.g., #foo .bar);
  • attribute (required), valid HTML attribute of a supplied element you wish to add a wait;
  • value (required), string value to match the attribute's value, the value null means to wait for the attribute to be removed;

Example:

store element details of "Element whose attribute to wait for" in $element
waitForElementAttribute($element, "target", "_blank");

Expecting an attribute to became null:

store element details of "Element whose attribute to wait for" in $element
waitForElementAttribute($element, "target", "null");
View source
// Last updated: 11/06/2021, 08:49:34 UTC
if (!element) throw new Error('No element was supplied')

function getElementByXpath(path) {
  return document.evaluate(path, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
}

function findElement(element) {
  try {
    element = JSON.parse(element)
  } catch (e) { }
  selectors = (typeof element === 'string') ? [{ type: element.startsWith('/') ? 'XPATH' : 'CSS_SELECTOR', value: element }] : element.selectors
  for (var i in selectors) {
    var selector = selectors[i]
    if (!selector.type) continue
    var el = null
    switch (selector.type) {
      case 'XPATH_ID':
      case 'XPATH':
        el = getElementByXpath(selector.value)
        break
      case 'CSS_SELECTOR':
        el = document.querySelector(selector.value)
        break
      case 'ID':
        el = document.getElementById(selector.value)
        break
    }
    if (el) return el
  }
  return null
}

async function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

var MAX_RETRIES = 1000
var SLEEP_INTERVAL = 50 // ms

async function waitForElement(element) {
  let retries = 0
  while (retries < MAX_RETRIES) {
    var el = findElement(element)
    if (el) return el
    retries++
    await sleep(SLEEP_INTERVAL)
  }
  return null
}

async function waitForAttribute(htmlElement, attribute, value) {
  let retries = 0
  while (retries < MAX_RETRIES) {
    var attr = htmlElement[attribute] || htmlElement.getAttribute(attribute)
    if (!attr && (value === undefined || value == 'null' || value === null || (value === "false"))) return true
    if (attr && (value === undefined || attr == value || (value === "true"))) return true
    retries++
    await sleep(SLEEP_INTERVAL)
  }
  return false
}


async function waitForElementAttribute(element, attribute, value) {
  var el = await waitForElement(element)
  if (!el) throw new Error('Element was not found')
  var attr = await waitForAttribute(el, attribute, value)
  if (!attr) throw new Error(`Element attribute '${attribute}' not found, or did not match the required attribute`)
}

waitForElementAttribute(element, attribute, value).then(done).catch(doneError)

# Wait for element to disappear

As part of your testing, you may wish for an element to disappear from the page before you continue (e.g., a loading bar). This extension allows you to waits up to the maximum timeout of an extension (1 minute) for an element to disappear.

Parameters:

  • element (required), the target element of this operation, which can be stored from the page (using store element details of ... in $element)

Example:

store element details of "Element that will disappear" in $element
waitForElementToDisappear($element);
View source
// Last updated: 08/04/2021, 16:03:54 UTC
if (!element) throw new Error('No element was supplied')

function getElementByXpath(path) {
  return document.evaluate(path, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
}

function findElement(element) {
  try {
    element = JSON.parse(element)
  } catch (e) { }
  selectors = (typeof element === 'string') ? [{ type: element.startsWith('/') ? 'XPATH' : 'CSS_SELECTOR', value: element }] : element.selectors
  for (var i in selectors) {
    var selector = selectors[i]
    if (!selector.type) continue
    var el = null
    switch (selector.type) {
      case 'XPATH_ID':
      case 'XPATH':
        el = getElementByXpath(selector.value)
        break
      case 'CSS_SELECTOR':
        el = document.querySelector(selector.value)
        break
      case 'ID':
        el = document.getElementById(selector.value)
        break
    }
    if (el) return el
  }
  return null
}

async function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

var MAX_RETRIES = 1000
var SLEEP_INTERVAL = 50 // ms

function isVisible(e) {
    return !!( e.offsetWidth || e.offsetHeight || e.getClientRects().length );
}

async function waitForElementDisappear(element) {
  let retries = 0
  while (retries < MAX_RETRIES) {
    var el = findElement(element)
    if (!el || !isVisible(el)) return true
    retries++
    await sleep(SLEEP_INTERVAL)
  }
  return false
}

async function waitForElementAttribute(element) {
  var el = await waitForElementDisappear(element)
  if (!el) throw new Error('Element was still visible')
}

waitForElementAttribute(element).then(done).catch(doneError)

# Remove the "target" attribute from an element

In some cases, opening links in a new tab crashes the automation driver due to upstream problems on it (e.g., when opening a PDF in a new tab). These can be solved by removing the target property of the link, triggering the download in the same window.

Parameters:

  • linkElement (required), the target element of this operation which you stored from the page (using store element details of ... in $linkElement);

Example usage:

Store element details of "Open new tab" in $linkElement
removeElementTargetAttribute($linkElement)
View source
// Last updated: 05/09/2022, 13:42:36 UTC
function getElement (selectors) {
  let element = null
  for (const selectorDetails of selectors) {
    switch (selectorDetails.type) {
      case 'CSS_SELECTOR':
        element = document.querySelector(selectorDetails.value)
        break
      case 'XPATH_ID':
      case 'XPATH':
        element = document.evaluate(selectorDetails.value, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue
        break
      case 'ID':
       element = document.getElementById(selectorDetails.value)
        break
      default:
        continue
    }
    // if we found a valid element we don't need to keep searching
    if (element) return element
  }
  throw new Error('Found no suitable selector: ' + JSON.stringify(selectors))
}

const { selectors } = JSON.parse(storedElement)
const element = getElement(selectors)

element.removeAttribute('target')

# Set the value of an input element

While constructing a journey you may face fields that are not updating as expected when interacted with NLP commands, specially when using crossbrowser devices. For example, you used write with an input field but the submit button stays disabled.

When this happens you can store the element details of the input you want to directly set the value, and use this extension to perform that action and trigger any page reactivity associated to that input.

Parameters:

  • storedElement (required), the element to modify, stored into a variable by using a step beforehand: store element details of "element hint" in $elementVariable;
  • value (required), the desired contents of the element's value field, e.g. "#00FF00" for color pickers.

Example usage:

Store element details of "html5colorpicker" in $picker
setValue($picker, "#00FF00")
View source
// Last updated: 21/11/2022, 18:07:27 UTC
function setNativeValue(element, value) {
  const { set: valueSetter } = Object.getOwnPropertyDescriptor(element, 'value') || {};
  const prototype = Object.getPrototypeOf(element);
  const { set: prototypeValueSetter } = Object.getOwnPropertyDescriptor(prototype, 'value') || {};

  if (prototypeValueSetter && valueSetter !== prototypeValueSetter) {
    prototypeValueSetter.call(element, value);
  } else if (valueSetter) {
    valueSetter.call(element, value);
  } else {
    throw new Error('The given element does not have a value setter');
  }
  element.dispatchEvent(new Event('input', { bubbles: true }));
}

function getElement (selectors) {
  let element = null
  for (const selectorDetails of selectors) {
    switch (selectorDetails.type) {
      case 'CSS_SELECTOR':
        element = document.querySelector(selectorDetails.value)
        break
      case 'XPATH':
        element = document.evaluate(selectorDetails.value, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue
        break
      default:
        continue
    }
    // if we found a valid element we don't need to keep searching
    if (element) return element
  }
  throw new Error('Found no suitable selector: ' + JSON.stringify(selectors))
}

const { selectors } = JSON.parse(storedElement)
const element = getElement(selectors)

setNativeValue(element, value)

# Compare two XLS spreadsheets

In some cases, you may need to compare two spreadsheets to check if they are identical. This extension allows you to do that by comparing the number of cells and their contents. if the two spreadsheets are identical, the extension will return a success message. If they are not identical, it will return a failure message with the differences.

Parameters:

  • fileOneUrl (required), the URL of the master Excel file;
  • fileTwoUrl (required), the URL of the another Excel file to be compared against the fileOne;
  • fileOneSheetName (optional), the name of the sheet to be checked in the master file. Defaults to 'Data' if not provided;
  • fileTwoSheetName (optional), the name of the sheet to be checked in the other file. Defaults to 'Data' if not provided; Example usage:
compareSpreadSheets('[file one url]', '[file two url]', '[file one sheet name]', '[file two sheet name]') returning $result
View source
// Last updated: 29/04/2025, 09:03:03 UTC
const errors = [];

async function getFile(fileUrl) {
  const data = await (await fetch(fileUrl)).arrayBuffer();
  return data;
}

function checkCellCount(fileOneData, fileData) {
  if (!fileOneData || !fileData) {
    throw new Error('fileOne or file data is undefined/null');
  }

  const fileOneKeys = Object.keys(fileOneData);
  const fileKeys = Object.keys(fileData);

  for (const key of fileOneKeys) {
    if (!fileOneData[key]) {
      throw new Error('Missing data for key: ' + key);
    }

    if (key.startsWith("!")) continue;

    if (!fileKeys.find((element) => key == element)) {
      console.log("fileOne " + key);
      console.log(fileOneData[key]["v"]);
      errors.push(`fileOne as more cells than fileTwoFile => cell ${key} with value: ${fileOneData[key]["v"]}`);
    }
  }

  for (const key of fileKeys) {
    if (!fileData[key]) {
      throw new Error('Missing data for key: ' + key);
    }

    if (key.startsWith("!")) continue;

    if (!fileOneKeys.find((element) => key == element)) {
      errors.push(`fileTwoFile as more cells than fileOne => cell ${key} with value: ${fileData[key]["v"]}`);
    }
  }
}

function checkCellContent(fileOneData, fileData) {
  if (!fileOneData || !fileData) {
    throw new Error('fileOne or file data is undefined/null');
  }

  const fileOneKeys = Object.keys(fileOneData);

  for (const key of fileOneKeys) {
    if (!fileOneData[key]) {
      throw new Error('Missing data for key: ' + key);
    }

    if (!fileData[key]) {
      throw new Error('Missing data for key: ' + key);
    }

    if (key.startsWith("!")) continue;

    if (fileData[key]["v"] != fileOneData[key]["v"]) {
      errors.push(`fileTwoFile cell ${key} with value: ${fileData[key]["v"]} differs from fileOne with value: ${fileOneData[key]["v"]}`);
    }
  }
}

async function main() {
  const fileOneFile = await getFile(fileOneUrl);
  const fileOne = XLSX.read(fileOneFile, { type: 'buffer' });
  if (!fileOne) {
    throw new Error('Failed to parse fileOne file');
  }

  const fileOneSheetName = 'Data' || fileTwoSheetName;
  const fileOneSheet = fileOne.Sheets[fileOneSheetName];
  if (!fileOneSheet) {
    throw new Error('fileOne missing ' + fileOneSheetName + ' sheet');
  }

  const fileOneData = fileOneSheet;

  const fileTwoFile = await getFile(fileTwoUrl);
  const fileTwo = XLSX.read(fileTwoFile, { type: 'buffer' });
  if (!fileTwo) {
    throw new Error('Failed to parse fileTwo file');
  }
  const fileTwoSheetName = 'Data' || fileOneSheetName ;
  const fileData = fileTwo.Sheets[fileTwoSheetName];
  if (!fileData) {
    throw new Error('File missing Data sheet');
  }

  checkCellCount(fileOneData, fileData);
  checkCellContent(fileOneData, fileData);

  return errors;
}

main().then(done).catch(doneError);

# Calculate date

In certain cases, you may need to calculate a date based on a specific date and time. This extension allows you to do that. You can add or subtract days, months, and years from a date. The extension will return the calculated date in the specified format by you or in the default format (YYYY-MM-DD). Parameters:

  • date (required), the date to be calculated, in the format ISO 8601 ;
  • operation (required), the operation to be performed, which can be add or subtract;
  • value (required), the value to be added or subtracted, which can be a number of days, months, or years;
  • unit (required), the unit of the value, which can be days, weeks, months or years;
  • dateFormat (optional), the format of the date to be returned. If not provided, the default format will be used. for more information about the date format, please refer to moment.js documentation.

Example usage:

calculateDate("12/25/1995", "add", "1", "days", "MM/DD/YYYY") returning $newdate // 12/26/1995
assert $date equals "12/26/1995"
View source
// Last updated: 29/04/2025, 11:25:53 UTC
// Resources:
//   https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.2/moment.min.js
//   https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.31/moment-timezone-with-data.min.js

function manipulateDate(date, operation, value, unit) {
    const momentDate = dateFormat ? moment(date) : moment(date, dateFormat);
    if (operation === "add") {
        return momentDate.add(value, unit).format(dateFormat ? dateFormat : "YYYY-MM-DD");
    } else if (operation === "subtract") {
        return momentDate.subtract(value, unit).format(dateFormat ? dateFormat : "YYYY-MM-DD");
    } else {
        throw new Error("Invalid operation. Use 'add' or 'subtract'.");
    }
}

/**
 * @param date The initial date string, more info visit https://momentjs.com/docs/#/parsing/
 * @param operation The operation to perform (e.g., "add", "subtract")
 * @param unit The unit of time (e.g., "days", "months", "years")
 * @param value The value to add or subtract
 * @param dateFormat optional pramater to specify date format defaults to 'YYYY-MM-DD'
 * @returns The manipulated date as a string
 * @example Execute manipulateDate("2023-01-01", "add", "5", "days") returning $result
 */
return manipulateDate(date, operation, Number(value), unit);

# Perform browser actions

This extension provides a set of browser actions that can be performed in your journeys. It allows you to navigate to a URL, go back or forward in the browser history, reload the current page, and open a URL in a new tab. This extension is useful for automating browser actions that are not directly supported by the Virtuoso NLP.

Parameters:

  • action (required), the action to perform. Valid actions include:
    • blank: Open a new tab with the specified URL.
    • back: Go back in the browser history.
    • forward: Go forward in the browser history.
    • reload: Reload the current page.
    • replace: Replace the current URL with a new one.
    • referrer: Return the referrer URL.
    • current: Return the current URL.
  • url (required for navigate, replace, and open actions), the URL to navigate to, replace with, or open in a new tab.

Example usage:


performBrowserAction using "current" as action returning $status // returns "Current URL: [CURRENT_URL]"
performBrowserAction using "back" as action returning $status // returns "Navigated back."
performBrowserAction using "next" as action returning $status // returns "Navigated forward."

performBrowserAction("blank", "https://rocketshop.virtuoso.qa") returning $status
View source
// Last updated: 29/04/2025, 06:20:39 UTC
// Define navigation functions
const navigationActions = {
    back: () => {
        window.history.back();
        return "Navigated back.";
    },
    forward: () => {
        window.history.forward();
        return "Navigated forward.";
    },
    reload: () => {
        window.location.reload();
        return "Page reloaded.";
    },
    current: () => `Current URL: ${window.location.href}`,
    replace: (url) => {
        if (url) {
            window.location.replace(url);
            return `URL replaced with ${url}.`;
        } else {
            return "Error: URL is required to replace the current URL.";
        }
    },
    referrer: () => `Referrer URL: ${document.referrer}`,
    blank: (url) => {
        if (url) {
            window.open(url, '_blank');
            return `Opened a new tab with given URL`;
        } else {
            return "Error: URL is required to open in a new tab.";
        }
    }
};

// Alias mappings for user-friendly commands
const aliasActions = {
    previous: 'back',
    next: 'forward',
    refresh: 'reload'
};

// Perform browser action based on input
function performBrowserAction(action, url) {
    action = action.toLowerCase();

    // Resolve action alias if any
    action = aliasActions[action] || action;

    // Get the corresponding navigation function
    const navigationAction = navigationActions[action];

    // Execute the navigation function if it exists
    if (navigationAction) {
        return navigationAction(url);
    } else {
        return `Error: Unknown action "${action}".`;
    }
}

return (performBrowserAction(action,url));

# Clean and round number with padding

In some cases, you may need to clean a number string (e.g., '1,469.25456') and round it to a specific number of decimal places (e.g., '1'). This extension helps you achieve that. It removes any non-numeric characters from the string, converts it to a number, and rounds it to the specified number of decimal places. The result is returned as a string with the specified number of decimal places.

Parameters:

  • value (required), the input string that may contain non-numeric characters (e.g., '1,469.25456');
  • roundingLimit (required), the number of decimal places to which the value should be rounded (e.g., '1').

Example usage:

store value "1,469.25456" in $value
store value "1" in $roundingLimit
cleanAndRoundNumberWithPadding($value, $roundingLimit) returning $cleanedValue // 1469.2
View source
// Last updated: 29/04/2025, 06:37:19 UTC
function cleanInput(input) {
  
  return input.replace(/[^0-9.]/g, '');
}

function rounding(value, decimals) {
  return Number(Math.round(value + 'e' + decimals) + 'e-' + decimals);
}  

let cleanedValue = cleanInput(value);

return rounding(parseFloat(cleanedValue), Number(roundingLimit));

# Search value in a JSON

In situations where you need to search for a specific value in a JSON object, this extension can be used. It allows you to search for a key-value pair in a JSON object and returns the matching object(s) if found. This is particularly useful when you have a large JSON object and want to find specific information without manually traversing the entire structure.

Parameters:

  • jsonData (required), the JSON object to search in;
  • searchKey (required), the key to search for in the JSON object;
  • searchValue (required), the value to search for in the JSON object;

Example usage:

store value '{ "personalDetails": {"name":"John", "age":30, "city":"New York"}, "payment" : { "frequency": "monthly", "amount": "8000", "currency": "USD"} }' in $jsonString
store value "name" in $searchKey
store value "John" in $searchValue
searchValueInJson($jsonString, $searchKey, $searchValue) returning $result
assert $result[0].name equals "John"
View source
// Last updated: 30/04/2025, 06:26:31 UTC
function searchNestedJSON(jsonString, searchKey, searchValue) {
    const jsonData = JSON.parse(jsonString);
    const results = [];

    function search(obj) {
        for (const key in obj) {
            if (typeof obj[key] === 'object' && obj[key] !== null) {
                search(obj[key]);
            } else if (key === searchKey && obj[key] ===
                searchValue) {
                results.push(obj);
            }
        }
    }

    search(jsonData);
    return results;
}

try{
  return searchNestedJSON(jsonString, searchKey, searchValue);
} catch(error) {
  throw new Error(error)
}

# Fetch CSV and convert to JSON

In situations where you need to fetch a CSV file and use it in your tests, for example, to validate data or to use it as a data source for your tests (e.g., to validate a table), this extension can be used. It allows you to fetch a CSV file from a URL and convert it into a JSON object for further processing. You can also use the file environment variable to convert the CSV file to JSON.

Parameters:

  • fileUrl (required), the URL of the CSV file to fetch;
  • separator (optional), the separator used in the CSV file (default is comma ,);

Example usage:

store value "https://example.com/data.csv" in $fileUrl
store value "," in $separator
fetchCsvAndConvertToJson($fileUrl, $separator) returning $jsonData
assert $jsonData[0].name equals "John"
View source
// Last updated: 30/04/2025, 13:49:14 UTC
// Resources:
//   https://cdnjs.cloudflare.com/ajax/libs/axios/0.27.2/axios.min.js
//   https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.slim.min.js
//   https://cdnjs.cloudflare.com/ajax/libs/jquery-csv/1.0.21/jquery.csv.min.js

async function fetchCsvAndConvertToJson(){
  try {
    const customSeparator = separator ? separator : ",";
    const { data } = await axios.get(fileUrl)
    const json = $.csv.toObjects(data, { separator: customSeparator })
    done(json)
  } catch(error) {
    doneError(error)
  }
}

fetchCsvAndConvertToJson()
Last Updated: 5/6/2025, 11:08:36 AM