Integration tests with Jest, Selenium and BrowserStack - part 2 - multiple browsers
As described in part 1 you can run Jest based tests with BrowserStack service. Now I will show how they can be automatically run against many browsers and OSes.
Parametrized tests
Probably the most simple way to convert single-browser automatic Browserstack test to multi browser/OS is parametrization using enironment variables. Here is how it might be done regarding example test file test/automate.test.js.
Replace the following lines from capabilities object:
  browserName: 'chrome',
  os: 'Windows',with:
  browserName: process.env.BROWSER,
  os: process.env.OS,This is it! Now you may run tests against any browser and OS available in BrowserStack service. Example:
OS=Windows BROWSER=chrome yarn jest ./test/parametrized.test.jsProvided that your parametrized tests file path is: ./test/parametrized.test.js
Running against multiple browsers and OSes might be achieved by chaining execution of commands like:
OS=Windows BROWSER=chrome yarn jest ./test/parametrized.test.js && OS=Windows BROWSER=firefox yarn jest ./test/parametrized.test.jsor (if you don't care about possible previous command errors):
OS=Windows BROWSER=chrome yarn jest ./test/parametrized.test.js; OS=Windows BROWSER=firefox yarn jest ./test/parametrized.test.jsAs you may expect, this is not the most effective method, because the connection to BrowserStack service is opened and closed for each command executed.
Sequential tests
Another option is to write a sequential test wrapper that will run selected tests against browsers defined in the config.json file one after another. It is a bit more complicated as it requires tests code refactoring.
Here is an example content of a config.json:
{
  "browsers": [
    {
      "browserName": "internet explorer",
      "version": "10.0"
    },
    {
      "browserName": "chrome",
      "version": "22.0"
    }
  ],
}The file structure is based on the official example/config.json from https://www.browserstack.com/automate/node#wd. Check the https://www.browserstack.com/automate/capabilities for list of all available keys.
The general idea is to connect to BrowserStackLocal, run tests for all browsers defined inside the config.json file and finally disconnect. To achieve this, we will use the beforeAll and afterAll hooks to handle BrowserStack connection. After establishing a connection with BrowserStackLocal we will loop through all browser versions from config file and run tests for each of them. Here's how it can look.
import webdriver from 'selenium-webdriver';
import browserstack from 'browserstack-local';
const local = new browserstack.Local();
const until = webdriver.until;
const By = webdriver.By;
const config = require('./config.json');
const BrowserStackLocalArgs = {
  key: '',
  onlyAutomate: true,
  folder: __dirname,
};
const start = async () =>
  new Promise((resolve, reject) => {
    local.start(BrowserStackLocalArgs, error => {
      if (error) {
        reject(error);
      }
      resolve();
    });
  });
const stop = async () =>
  new Promise((resolve, reject) => {
    local.stop(function(error) {
      if (error) {
        reject(error);
      }
      resolve();
    });
  });
const getElementById = async (driver, id, timeout = 5000) => {
  const el = await driver.wait(until.elementLocated(By.id(id)), timeout);
  return await driver.wait(until.elementIsVisible(el), timeout);
};
beforeAll(async () => {
  await start();
}, 20000);
afterAll(async () => {
  await stop();
}, 20000);
for (const browser of config.browsers) {
  let driver;
  const capabilities = {
    os: 'Windows',
    build: '0.1.0',
    project: 'jest-selenium-browserstack-example',
    'browserstack.local': true,
    'browserstack.user': '',
    'browserstack.key': BrowserStackLocalArgs.key,
    ...browser,
  };
  describe(
    'selenium webdriver',
    () => {
      beforeAll(async () => {
        driver = await new webdriver.Builder()
          .usingServer('http://hub-cloud.browserstack.com/wd/hub')
          .withCapabilities(capabilities)
          .build();
        console.log('driver start');
      }, 20000);
      afterAll(async () => {
        await driver.quit();
        console.log('driver quit');
      }, 20000);
      describe(`desc ${capabilities.browserName}`, () => {
        test(`test ${capabilities.browserName}`, async () => {
          console.log('tests start');
          await driver.get(
            `http://${
              capabilities['browserstack.user']
            }.browserstack.com/test.html`,
          );
          const btn = await getElementById(driver, 'test-button');
          await btn.click();
          const output = await getElementById(driver, 'output');
          const outputVal = await output.getAttribute('value');
          expect(outputVal).toEqual('Something');
          console.log('tests end');
        });
      });
    },
    30000,
  );
}Although it seems quite easy, you need to take care of several things:
- describecan not use- asyncfunction, so there is no way to prepare- driverinstance to use directly inside- describeblock, it can be used only inside- testbody, which may be asynchronous
- Jest scoping is a topic which is worth refreshing to fully understand what is going on in the presented code https://jestjs.io/docs/en/setup-teardown#scoping
- beforeAlland- afterAllare scoped by- describeblocks and what is more important both of them supports asynchronous function as an argument
- Remember about timeouts, default 5s is often not enough
More details and sample files can be found in this repository https://github.com/jmarceli/jest-selenium-browserstack-example, especially in test/sequential.test.js file.
Sources
https://www.browserstack.com/automate/node https://www.browserstack.com/list-of-browsers-and-platforms?product=automate