meteor/meteor

View on GitHub
tools/tool-testing/clients/browserstack/index.js

Summary

Maintainability
A
1 hr
Test Coverage
import Client from '../../client.js';
import configuredClients from "./clients.js";
import { enterJob } from '../../../utils/buildmessage.js';
import { execFileSync } from '../../../utils/processes';
import { ensureDependencies } from '../../../cli/dev-bundle-helpers.js';
import {
  mkdtemp,
  pathJoin,
  readFile,
} from '../../../fs/files';

const NPM_DEPENDENCIES = {
  'selenium-webdriver': '4.1.1',
  'browserstack-local': '1.4.8',
};

const USER = 'dev1141';

// A memoized key from BrowserStackClient._getBrowserStackKey.
let browserStackKey;

export default class BrowserStackClient extends Client {
  constructor(options) {
    super(options);

    enterJob({
      title: 'Installing BrowserStack WebDriver in Meteor tool',
    }, () => {
      ensureDependencies(NPM_DEPENDENCIES);
    });

    this.npmPackageExports = require('selenium-webdriver');

    // Capabilities which are allowed by selenium.
    this.config.seleniumOptions =
      this.config.seleniumOptions || {};

    // Additional capabilities which are unique to BrowserStack.
    this.config.browserStackOptions =
      this.config.browserStackOptions || {};

    this._setName();
  }

  _setName() {
    const name = this.config.seleniumOptions.browserName || "default";
    const version = this.config.seleniumOptions.version || "";
    const device =
      this.config.browserStackOptions.realMobile &&
      this.config.browserStackOptions.device || "";

    this.name = "BrowserStack: " + name +
      (version && ` Version ${version}`) +
      (device && ` (Device: ${device})`);
  }

  connect() {
    const key = BrowserStackClient._getBrowserStackKey();
    if (! key) {
      throw new Error(
        "BrowserStack key not found. Ensure that s3cmd is setup with " +
        "S3 credentials, or set BROWSERSTACK_ACCESS_KEY in your environment.");
    }

    const capabilities = {
      // Authentication
      'browserstack.user': USER,
      'browserstack.key': key,
      // Use the BrowserStackLocal tunnel, to allow BrowserStack to
      // tunnel to the machine this server is running on.
      'browserstack.local': true,

      // Enabled the capturing of "Visual Logs" (i.e. Screenshots).
      'browserstack.debug': true,

      // On browsers that support it, capture the console
      'browserstack.console': 'errors',

      ...this.config.seleniumOptions,
      ...this.config.browserStackOptions,
    };

    const triggerRequest = () => {
      this.driver = new this.npmPackageExports.Builder()
        .usingServer('https://hub-cloud.browserstack.com/wd/hub')
        .withCapabilities(capabilities)
        .build();

      this.driver.get(this.url);
    }

    this._launchBrowserStackTunnel()
      .then(triggerRequest)
      .catch((e) => {
        // In the event of an error, shut down the daemon.
        this.stop();

        throw e;
      });
  }

  stop() {
    this.driver && this.driver.quit();
    this.driver = null;

    this.tunnelProcess && this.tunnelProcess.stop(() => {});
    this.tunnelProcess = null;
  }

  static _getBrowserStackKey() {
    // Use the memoized version, first and foremost.
    if (typeof browserStackKey !== "undefined") {
      return browserStackKey;
    }

    if (process.env.BROWSERSTACK_ACCESS_KEY) {
      return (browserStackKey = process.env.BROWSERSTACK_ACCESS_KEY);
    }

    // Try to get the credentials from S3 with the s3cmd tool.
    const outputDir = pathJoin(mkdtemp(), "key");
      const browserstackKey = "s3://meteor-browserstack-keys/browserstack-key";
    try {
      execFileSync("s3cmd", ["get",
        browserstackKey,
        outputDir
      ]);

      return (browserStackKey = readFile(outputDir, "utf8").trim());
    } catch (e) {
      // A failure is acceptable here; it was just a try.
      console.warn(`Failed to load browserstack key from 
        ${browserstackKey}`, e);
    }

    return (browserStackKey = null);
  }

  _launchBrowserStackTunnel() {
    this.tunnelProcess = new (require('browserstack-local')).Local();

    const options = {
      key: this.constructor._getBrowserStackKey(),
      onlyAutomate: true,
      verbose: true,
      // The ",0" means "SSL off".  It's localhost, after all.
      only: `${this.host},${this.port},0`,
    }

    return new Promise((resolve, reject) => {
      this.tunnelProcess.start(options, (err) => {
        if (err) {
          reject(err);
        } else {
          resolve();
        }
      });
    });
  }

  static prerequisitesMet() {
    return !! this._getBrowserStackKey();
  }

  static pushClients(clients, appConfig) {
    configuredClients.forEach((client) => {
      clients.push(new BrowserStackClient(
        {
          ...appConfig,
          config: {
            seleniumOptions: client.selenium,
            browserStackOptions: client.browserstack,
          },
        },
      ));
    });
  }
}