Browser testing with browser scripts

You can use Node.js based scripts and Selenium-based APIs to enable Instana browser testing. With predefined global variables and Instana browser testing APIs, you can create and run a browser script test that simulates user interaction in a web browser.

Instana browser script references

To create a browser script test to enable browser testing, you need global variables and modules.

Global variables

The predefined global variables that are outlined in the table accelerate script development.

Name Type Details
$driver @types/selenium-webdriver Type definitions for Selenium WebDriverJS.
$browser ThenableWebDriver Thenable wrapper of a WebDriver client, which provides control over a browser and allows calling WebDriver methods directly on Promise<WebDriver> without calling then.
$secure Dict<string> Variable to access user credentials.
$util UserUtil Utility functions, such as $util.secrets.
$network Network Network utility to set up proxy.
$synthetic Dict<string> Environment variables.
$attributes UserInsight Variable to add and manipulate customized attributes.

The usages and declaration of the predefined global variables are as follows:

$synthetic

You can use $synthetic.var_name to access environment and runtime variables in the script. Some predefined variables are as follows:

  • $synthetic.LOCATION or $synthetic.pop: Used to access the location label, which is the first part in controller.location variable.
  • $synthetic.TEST_ID or $synthetic.id: Used to access the identifier of Synthetic test that is run.
  • $synthetic.TEST_NAME or $synthetic.testName: Used to access test label of Synthetic test that is run.
  • $synthetic.TIME_ZONE or $synthetic.timeZone: Used to access time zone of the PoP running the Synthetic test.
  • $synthetic.JOB_ID or $synthetic.taskId: Used to access the identifier of playback job or task, and it is also the result ID.
  • $synthetic.testType: Used to access the test type.
  • $synthetic.description: Used to access the test description.
  • $synthetic.labels.xxx: Used to access the custom properties defined in the customProperties section of the test definition.

You can also define custom environment variables in the values.yaml file of the helm chart. See the following example:

controller:
  customProperties: "tag1=value1;tag2=value2"

To access these custom variables, use $synthetic.tag1 and $synthetic.tag2 in the script.

To access the custom properties that are defined in customProperties section of test definition and the property value in the script, use $synthetic.labels.xxx. See the following example:

// to print test custom property defined in customProperties of test definition 
console.info('Test custom property -> Team: ' + $synthetic.labels.Team);
console.info('Test custom property -> Purpose: ' + $synthetic.labels.Purpose);

$secure

The Instana browser script supports user credentials to store the secrets securely, such as login username and password. You can use $secure.MY_SECURE_CREDENTIAL to refer to predefined secure credentials in your script.

To create a credential with the Synthetic Open API, complete the following steps:

  1. Make sure that you have proper permission setting.
  2. Use Synthetic OpenAPI or the synctl command to create a credential by passing the credentialName and credentialValue variables.

Then, in your browser script, use $secure.credentialName to refer to the created credential, for example, $secure.password or $secure.username.

Only the $secure.credentialName format is supported. The $secure[credentialName] format is not supported.

$attributes

You can use the $attributes variable to add or get custom attributes to monitoring data. These custom attributes are also stored in the monitoring results with the other default monitoring result attributes.

Instana Synthetic monitoring supports the following APIs for configuring custom attributes:

  • $attributes.set(key, value): Sets the key or value.
  • $attributes.get(key): Returns the value for provided key.
  • $attributes.getKeys(): Returns an array of all keys.
  • $attributes.has(key): Returns true if the key exists.
  • $attributes.unset(key): Removes the specified key or value.
  • $attributes.unsetAll(): Removes all the custom data.

The following example shows the $attributes API:

$attributes.set(xxx, yyy) // sets tag xxx to value yyy
let v = $attributes.get(xxx) // sets variable v to the value of tag xxx

The custom attributes set by $attributes API in the script and custom properties defined by customProperties section of the test definition can be accessed in the test results by synthetic.tags metric, you can use synthetic.tags metric to filter the test results or pass custom value to Smart Alert custom payload.

$network

The following APIs are supported in Instana Synthetic monitoring for configuring proxy server:

  • $network.setProxy(string proxy): Sets a proxy server to be used for all requests (HTTP, HTTPS).
  • $network.setProxyForHttp(string proxy): Sets a proxy server to be used for only HTTP requests.
  • $network.setProxyForHttps(string proxy): Sets a proxy server to be used for only HTTPs requests.
  • $network.setProxyPAC(string pac, object auth): Sets a proxy server through a proxy auto-config (PAC) script.
  • $network.setProxyAdvanced(object proxy): Sets the proxy configuration by using the format supported by Chrome/Firefox Extension API for proxying.
  • $network.clearProxy(): Removes proxy configuration.
  • $network.getProxy(): Returns proxy configuration.

$util.secrets

You can use the $util.secrets security API to redact sensitive data.

Before you log in the logging files and send the files to the backend, mask all known sensitive information with the asterisk (*) character.

To redact secrets from URLs, use the $util.secrets.setURLSecretsRegExps command in your script. See the following example:

// to redact 'key', 'password' and 'secret' query parameters in URL
$util.secrets.setURLSecretsRegExps([/key/i, /password/i, /secret/i]);

After you call this API, the URL https://example.com/accounts/status?key=mykey123&secret=mysecret is collected and displayed as https://example.com/accounts/status?key=*&secret=* in the Instana UI.

Instana Synthetic removes all the request and response headers to keep consistent with the Instana current designs.

By default, Instana doesn't collect HTTP headers when it traces HTTP calls to avoid sending sensitive data.

Browser Testing API references

Browser testing supports Selenium-based APIs and more than 30 other extended browser testing APIs. For more information, see API Reference.

Writing a browser script test

You can write a browser script test for the following actions to enable browser testing. The sample codes illustrate how to write a browser script test with commonly used APIs. For more information about the complete sample code, see the Script examples section.

Implementing basic actions

The following sample codes show how to write a browser script test to implement the following basic actions:

  • Prepare headers: To customize request headers before you access websites, use the following command:

    await $browser.addHeader("test", "test");
    await $browser.addHeaders({ "test1": "test1", "test2": "test2" });
    await $browser.deleteHeaders(["test1", "test2"]);
    $browser.getHeaders().forEach((value, key) => console.log(">>>>>>>>>>>>>>>>>>>", "User customized headers: ", key + "=" + value));
    
  • Control access: To control access with the deny and allow list, use the following commands:

    await $browser.addHostnamesToDenylist(["*google*", "*timeanddate*"]);
    await $browser.deleteHostnameFromDenylist("*timeanddate*");
    await $browser.get("https://www.google.com");
    

    When you search google after you run these commands, you can see the message "wwww.google.com is blocked".

  • Open URL: To open URL in browser to access the website, use the following command:

    await $browser.get("http://www.bing.com");
    
  • Set sleep time: To set sleep option in the browser, use the following command. The parameter is the amount of time, in milliseconds, to sleep. In the following example, the browser sleeps for 2 seconds.

    await $browser.sleep(2000);
    
  • Use assertion with page title: Assertion is critical in your tests to determine the test results. To use assertion with page title or page source, use the following code. You can use the library const assert = require("assert").strict; or Chai assertion library const { assert } = require("chai");. Use console.log to log in console output and console log file.

    const { assert } = require("chai");
    
    await $browser.get("http://www.timeanddate.com");
    let page = await $browser.getPageSource();
    assert.isTrue(page.includes("<title>timeanddate.com</title>"));
    console.log(">>>>>>>>>>>>>>>>>>>", "Page title: ", await $browser.getTitle());
    
  • Get session: To obtain information about a session, use the following command:

    let session = await $browser.getSession();
    console.log(">>>>>>>>>>>>>>>>>>>", "Session id: ", session.getId());
    
  • Maximize window size: To maximize the browser window size, use the following command:

    $browser.manage().window().maximize();
    
  • Take screenshot: To take screenshot of the browser, use the following command:

    await $browser.takeScreenshot();
    

Finding and interacting with page elements

  • Wait and find an element on the page: You can use the commands in the following table for specific searches:

    Name Use
    $browser.waitForAndFindElement($driver.By.id("boxyear"), 1000) Wait and find element by ID until the element is visible or timeout value reached
    $browser.findElements($driver.By.css('select')) Search for multiple elements on the page with the CSS selector
    $browser.findElement($driver.By.linkText("About")) Find element by linkText.
    $browser.findElement($driver.By.xpath('//input[@value='f']')) Find element by XPath.
    $browser.$('#boxyear') Find element by ID.
    $browser.$$(".rd-box") Find elements by class.
    elem.$('h2').$('a').getText() Find element by element name.

    The following sample code shows how to wait and find an element on the page:

    // Find element by XPath and wait for the element until specified timeout value reached
    await $browser.waitForAndFindElement($driver.By.xpath(`//textarea[@inputmode='search']`), 10000);
    
    // Find element by id
    let elem = await $browser.waitForAndFindElement($driver.By.id("boxyear"), 1000);
    assert.equal("2022", await elem.getAttribute("value"));
    assert.equal("2022", await $browser.$('#boxyear').getAttribute("value"));
    
    // Find elements by CSS selector
    let selects = await $browser.findElements($driver.By.css('select'));
    assert.equal(2, selects.length);
    
    // Find elements by class
    let elems = await $browser.$$(".rd-box");
    console.log(await elems[0].$('h2').$('a').getText());
    
  • Find elements and perform actions: The following example shows how to find an element by CSS selector and send keys with ActionSequence:

    <input id="sb_form_q" name="q" type="search">
    
    let textarea = await $browser.waitForAndFindElement($driver.By.css('#sb_form_q'), 10000);
    // Actions to send keys
    await $browser
      .actions()
      .move({ origin: textarea })
      .sendKeys("synthetic")
      .pause(5000)
      .sendKeys($driver.Key.ENTER)
      .perform();
    

Applying conditions

  • Wait until title comes condition: To apply wait until the title comes condition, use the following command:

    await $browser.wait($driver.until.titleContains('Year 2022 Calendar'), 10000);
    
  • Wait and poll until timeout condition: To apply wait and poll until timeout condition, use the following commands:

    The following sample is a promise chain invocation that is compatible with an earlier version. Do not use the code in modern JavaScript. Instead, use the code modern asynchronous JavaScript with Async and Await.

    $browser.get("https://www.google.com");
    $browser.wait(function () {
      return $browser.getTitle().then(function (title) {return title.includes("Google");});
    }, 10000).then(function () {
      return $browser.findElement($driver.By.linkText("About")).click();
    }).then(function () {
      return $browser.navigate().back();
    });
    
  • Navigate back: To move back to previously accessed page in the browser, use the following command:

    await $browser.navigate().back();
    
  • Switch to new tabs: To get the current window handle and switch to a new window to open a new page in a new tab and switch back to original window, use the following code:

    console.log("Windows handle: ", await $browser.getWindowHandle(), "Current url: ", await $browser.getCurrentUrl());
    let originalWindow = await $browser.getWindowHandle();
    await $browser.switchTo().newWindow();
    await $browser.get("http://www.timeanddate.com");
    await $browser.switchTo().window(originalWindow);
    await $browser.takeScreenshot();
    

Moving mouse and sending keys

  • Mouse click: To move mouse, use the following command:

    const element = await $browser.waitForAndFindElement(by, timeout);
    const out = $browser.actions().move({origin: element}).press().release().perform();
    
  • Mouse hover: To hover the mouse on a browser element, use the following command:

    const element = await $browser.waitForAndFindElement(by, timeout);
    const out = $browser.actions().move({origin: element, duration: 2000}).perform();
    
  • Send keys: To send keys, use the following command:

    await $browser.actions().sendKeys("synthetic").perform();
    

Handling Shadow DOM elements

To access Shadow DOM elements, you can use the getShadowRoot() function. The new method is supported for Synthetic PoP Helm chart 1.1.1 or later. You must upgrade your Synthetic PoP version to 1.1.1 or later to enable the support for Shadow DOM interaction.

let shadowHost = await $browser.waitForAndFindElement(shadowHostSelector, timeout);
let shadowRoot = await shadowHost.getShadowRoot();
let element = await shadowRoot.findElement(elementSelector);
console.log("element text is:", await element.getText(), ", id is:", await element.getId());

Setting proxy to access websites

  • To set proxy for HTTP and HTTPS requests by using the $network or $browser variables, use the following command. You can set the proxy with string or url.URL.

    await $network.clearProxy();
    await $network.setProxyForHttp(proxyServer);
    website = await accessWebSite();
    checkHostname("www.google.com.hk", website)
    
    await $network.clearProxy();
    let testURL = new URL("http://" + sslProxyServer);
    await $network.setProxyForHttps(testURL);
    website = await accessWebSite();
    checkHostname("www.google.com", website);
    
  • To set proxy for all requests with the bypass list to filter, use the following command. After you run this command in the following example, no proxy is available for www.google.com.

    await $network.clearProxy();
    await $network.setProxy(proxyServer, "www.google.com");
    console.log(">>>>>>>>>>>>>>>>>>>", "Bypass list: ", $network.getProxy().rules.bypassList);
    website = await accessWebSite();
    checkHostname("www.google.com.hk", website);
    

Redacting secrets

To redact sensitive data in URL, use the following command. In the following example, the URL is redacted as "url": "https://www.bing.com/?mkt=zh-CN&toHttps=1&redig=*".

$util.secrets.setURLSecretsRegExps([/redig/i]);
console.log(">>>>>>>>>>>>>>>>>>>", "Access bing");
await $browser.get("http://cn.bing.com");
await $browser.wait($driver.until.titleContains('Bing'), 10000);

Managing modules

You can import third-party, core, or optional modules in your browser test script.

Importing optional module

The following example shows the command to import the assertion module chai:

var assert = require('chai').assert;

Supported core modules

Instana browser testing supports Node.js core modules in Node.js v18 or later.

To provide you a more secure runtime, Instana browser testing does not support several built-in modules or APIs, including process, child_process, and fs write or rm operations. If you use these APIs in your test script or bundled scripts, your test result might fail with the following error message. So do not use these APIs in your test scripts.

[SyntheticPoP] [ERROR] process.exit is not allowed. This API is blocked by playback engine.

Supported third-party modules

Instana Synthetic monitoring supports the following third-party modules:

  • @aws-sdk/client-s3 3.626.0
  • atob 2.1.2
  • authenticator 1.1.5
  • basic-ftp 4.6.6
  • btoa 1.2.1
  • chai 4.3.10
  • colors 1.4.0
  • consoleplusplus 1.4.4
  • crypto-js 4.2.0
  • faker 5.5.3
  • got 11.8.6
  • joi 17.7.1
  • js-yaml 4.1.0
  • ldapauth-fork 5.0.2
  • lodash 4.17.21
  • moment 2.29.4
  • net-snmp 3.6.3
  • protocol-buffers 4.2.0
  • q 1.5.1
  • re2 1.18.3
  • ssh2-sftp-client 7.2.3
  • ssl-checker 2.0.7
  • text-encoding 0.7.0
  • thrift 0.14.2
  • totp-generator 0.0.14
  • tough-cookie 4.1.3
  • underscore 1.13.3
  • url-parse 1.5.10
  • urllib 2.41.0
  • uuid 3.4.0
  • validator 13.7.0
  • ws 7.5.10
  • xml2js 0.5.0

Script examples

The following examples include most of the browser testing APIs and complete sample code.

These samples are for demonstration purpose only. You can find the latest examples in the public GitHub repo.

Complete sample code for actions

let click = async (message, by, timeout = 60000) => {
  console.log(`Click on ${message} >> ${by} << `);
  try {
    const element = await $browser.waitForAndFindElement(by, timeout);
    return $browser.actions().move({ origin: element }).click().perform();
  } catch (err) {
    console.error(`\ncatch(click): ${err.message}`);
    throw err;
  }
};

let hover = async (message, by, timeout = 60000) => {
  console.log(`Hover on ${message} >> ${by} << `);
  try {
    const element = await $browser.waitForAndFindElement(by, timeout);
    return $browser.actions().move({ origin: element, duration: 3000 }).perform();
  } catch (err) {
    console.error(`\ncatch(hover): ${err.message}`);
    throw err;
  }
};

(async function () {
  console.log(">>>>>>>>>>>>>>>>>>>", "Access bing page");
  await $browser.get("http://www.bing.com");
  console.log(">>>>>>>>>>>>>>>>>>>", "Actions of search");
  await hover("search button", $driver.By.xpath("//label[@id='search_icon']"), 10000);
  await $browser.takeScreenshot();
  await $browser.actions().sendKeys("synthetic").perform();
  await click("search button", $driver.By.xpath("//label[@id='search_icon']"), 10000);
})();

Complete sample codes for APIs

const { assert } = require("chai");

(async function () {
  /**
   * manipulate headers and retrieve all the customized headers
   */
  await $browser.addHeader("test", "test");
  await $browser.addHeaders({ "test1": "test1", "test2": "test2" });
  await $browser.deleteHeaders(["test1", "test2"]);
  $browser.getHeaders().forEach((value, key) => console.log(">>>>>>>>>>>>>>>>>>>", "User customized headers: ", key + "=" + value));

  /**
   * manipulate deny and allow list
   */
  await $browser.addHostnamesToDenylist(["*google*", "*timeanddate*"]);
  await $browser.deleteHostnameFromDenylist("*timeanddate*");

  /**
   * open url www.bing.com
   * find element by class selector
   * send keys to search Synthetic in the page
   */
  console.log(">>>>>>>>>>>>>>>>>>>", "Access bing page");
  await $browser.get("http://www.bing.com");
  console.log(">>>>>>>>>>>>>>>>>>>", "Actions of search");
  // Find element by XPath and wait for the element until specified timeout value reached
  await $browser.waitForAndFindElement($driver.By.xpath(`//textarea[@inputmode='search']`), 10000);
  // Sleep or pause to wait for specified milliseconds 
  await $browser.sleep(5000);
  // Take a screenshot
  await $browser.takeScreenshot();
  // Actions to send keys
  await $browser.actions().sendKeys("synthetic").sendKeys($driver.Key.ENTER).perform();

  /* Find element by CSS selector and perform actions
  let textarea = await $browser.waitForAndFindElement($driver.By.css('#sb_form_q'), 10000);
  // Actions to send keys
  await $browser
    .actions()
    .move({ origin: textarea })
    .sendKeys("synthetic")
    .pause(5000)
    .sendKeys($driver.Key.ENTER)
    .perform();
  */
  
  /**
   * samples of promise chain invocation, which is not recommended but backwards compatible
   * samples of waiting and polling function with timeout value
   */
  await $browser.deleteHostnameFromDenylist("*google*");
  await $browser.get("https://www.google.com");
  // Call the wait function.
  await $browser.wait(function () {
    return $browser.getTitle().then(function (title) {
      return title.includes("Google");
    });
    //If the condition isn't satisfied within 10000 milliseconds (10 seconds), proceed anyway.
  }, 10000).then(function () {
    return $browser.findElement($driver.By.linkText("About")).click();
  }).then(function () {
    return $browser.navigate().back();
  });

})();

Complete sample code for proxy

Do not run this sample code. You must change the proxy server and assertion.

const url = require("url");
const URL = url.URL;
const { assert } = require("chai");

let website;
let proxyServer = "proxyHost:proxyPort";
let sslProxyServer = "username:password@proxyHost:proxyPort";

async function accessWebSite(url) {
  console.log(">>>>>>>>>>>>>>>>>>>", "Proxy configuration is: ", JSON.stringify($network.getProxy()));
  console.log(">>>>>>>>>>>>>>>>>>>", `Access website ${url}`);
  await $browser.get(url);
  await $browser.findElement($driver.By.name('q')).click();
  await $browser.actions().sendKeys('synthetic').sendKeys($driver.Key.ENTER).perform();
  await $browser.takeScreenshot();
  return $browser.getCurrentUrl();
}

function checkHostname(hostname, website) {
  let actualURL = new URL(website);
  console.log(">>>>>>>>>>>>>>>>>>>", "Hostname is: ", actualURL.hostname);
  assert.equal(actualURL.hostname, hostname);
}

(async function () {
  /**
   * samples of proxy configuration settings with await asynchronous
   * using $network tools
   */
  console.log(">>>>>>>>>>>>>>>>>>>", "Clear proxy configuration");
  await $network.clearProxy();
  console.log(">>>>>>>>>>>>>>>>>>>", "Set proxy configuration for HTTPS request with string");
  await $network.setProxyForHttps(proxyServer);
  website = await accessWebSite("https://www.bing.com");
  checkHostname("www.bing.com", website)

  console.log(">>>>>>>>>>>>>>>>>>>", "Clear proxy configuration");
  await $network.clearProxy();
  console.log(">>>>>>>>>>>>>>>>>>>", "Set proxy configuration for HTTPS request with URL object");
  let testURL = new URL("http://" + sslProxyServer);
  await $network.setProxyForHttps(testURL);
  website = await accessWebSite("https://www.google.com");
  checkHostname("www.google.com", website);


  /**
   * samples of proxy configuration settings with bypass list
   */
  await $network.clearProxy();
  await $network.setProxy(proxyServer, "www.google.com");
  console.log(">>>>>>>>>>>>>>>>>>>", "Proxy config: ", $network.getProxy());
  website = await accessWebSite("https://www.bing.com");
  checkHostname("www.bing.com", website);
})();