manager.js

/**
 * WebDriver helper.
 *
 * @module WebDriverManager
 */

/**
 * External dependencies
 */
import urljoin from 'url-join';
import webdriver from 'selenium-webdriver';
import chromedriver from 'chromedriver'; // eslint-disable-line no-unused-vars
import proxy from 'selenium-webdriver/proxy';
import firefox from 'selenium-webdriver/firefox';
import chrome from 'selenium-webdriver/chrome';
import SauceLabs from 'saucelabs';
import path from 'path';

const implicitWait = 2000;
const pageLoadWaitMs = 60000;
const chromeUA = 'Mozilla/5.0 (wp-e2e-tests) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36';
const firefoxUA = 'Mozilla/5.0 (wp-e2e-tests) Gecko/20100101 Firefox/46.0';
const saucePreRunScriptURL = 'https://raw.githubusercontent.com/Automattic/wp-e2e-tests/master/fix-saucelabs-etc-hosts.sh';
const saucePreRunWinScriptURL = 'https://raw.githubusercontent.com/Automattic/wp-e2e-tests/master/fix-saucelabs-etc-hosts.bat';

const defaultArgs = {
	baseUrl: 'https://automattic.com',
	resizeBrowserWindow: true,
	useCustomUA: true,
	proxy: 'direct',
	screenshotsDir: path.resolve( process.cwd(), 'screenshots' ),
	headless: false,
};

/**
 * Class representing WebDriver manager.
 */
export default class Manager {
	/**
	 * Creates a manager.
	 *
	 * @param {String} browser - Browser to use. Valid value includes `chrome`
	 *                           and 'firefox'.
	 * @param {object} config  - Manager configuration.
	 */
	constructor( browser = 'chrome', config = {} ) {
		this.browser = browser;
		this.config = Object.assign( defaultArgs, config );

		this.setupDriver();
	}

	/**
	 * Returns instance of WebDriver.
	 *
	 * @example
	 *
	 * import { WebDriverManager } from 'wp-e2e-webdriver';
	 *
	 * const manager = new WebDriverManager( 'chrome' );
	 * const driver = manager.getDriver();
	 *
	 * @return {WebDriver} Instance of WebDriver.
	 */
	getDriver() {
		return this.driver;
	}

	setupDriver() {
		if ( this.config.useSauce ) {
			this.driver = this.buildSauceDriver();
		} else {
			const browserName = this.browser.toLowerCase();
			switch ( browserName ) {
				case 'chrome':
					this.driver = this.buildChromeDriver();
					break;
				case 'firefox':
					this.driver = this.buildFirefoxDriver();
					break;
				default:
					throw new Error(
						`The specified browser: '${ browserName }' in the config` +
						'is not supported. Supported browsers are "chrome" and "firefox"'
					);
			}

			this.browserName = browserName;
		}

		this.driver.manage().timeouts().implicitlyWait( implicitWait );
		this.driver.manage().timeouts().pageLoadTimeout( pageLoadWaitMs );

		if ( this.config.resizeBrowserWindow ) {
			this.resizeBrowser( this.getConfigScreenSize() );
		}
	}

	buildSauceDriver() {
		const caps = this.config.sauceConfig;

		caps.username = this.config.sauceCreds.sauceUsername;
		caps.accessKey = this.config.sauceCreds.sauceAccessKey;
		caps.name = caps.browserName + ' - [' + this.getConfigScreenSize() + ']';
		caps.maxDuration = 2700; // 45 minutes

		let preRunScript = saucePreRunScriptURL;
		if ( caps.platform.match( /Windows/ ) ) {
			preRunScript = saucePreRunWinScriptURL;
		}
		caps.prerun = { executable: preRunScript };

		if ( process.env.CIRCLE_BUILD_NUM ) {
			caps.name += ' - CircleCI Build #' + process.env.CIRCLE_BUILD_NUM;
		}

		global._sauceLabs = new SauceLabs( {
			username: caps.username,
			password: caps.accessKey,
		} );

		const builder = new webdriver.Builder();
		this.browserName = caps.browserName;
		const driver = builder.usingServer( 'http://ondemand.saucelabs.com:80/wd/hub' ).
			withCapabilities( caps ).
			build();

		driver.getSession().then( function( sessionid ) {
			driver.allPassed = true;
			driver.sessionID = sessionid.id_;
		} );

		this.driver = driver;

		return this.driver;
	}

	buildChromeDriver() {
		const builder = new webdriver.Builder();
		builder.setChromeOptions( this.createChromeOptions() );

		return builder.forBrowser( 'chrome' ).
			setLoggingPrefs( this.createLoggingPreference() ).
			build();
	}

	createChromeOptions() {
		const options = new chrome.Options();
		options.setProxy( this.getProxyType() );
		options.addArguments( '--no-sandbox' );
		if ( process.env.HEADLESS || this.config.headless ) {
			options.addArguments( '--headless' );
		}
		if ( this.config.useCustomUA ) {
			options.addArguments( 'user-agent=' + chromeUA );
		}

		return options;
	}

	buildFirefoxDriver() {
		const builder = new webdriver.Builder();
		builder.setFirefoxOptions( this.createFirefoxOptions() );

		return builder.forBrowser( 'firefox' ).
			setLoggingPrefs( this.createLoggingPreference() ).
			build();
	}

	createFirefoxProfile() {
		const profile = new firefox.Profile();

		profile.setNativeEventsEnabled( true );
		profile.setPreference( 'browser.startup.homepage_override.mstone', 'ignore' );
		profile.setPreference( 'browser.startup.homepage', 'about:blank' );
		profile.setPreference( 'startup.homepage_welcome_url.additional', 'about:blank' );

		if ( this.config.useCustomUA ) {
			profile.setPreference( 'general.useragent.override', firefoxUA );
		}

		return profile;
	}

	createFirefoxOptions() {
		const options = new firefox.Options().setProfile( this.createFirefoxProfile() );
		options.setProxy( this.getProxyType() );

		return options;
	}

	createLoggingPreference() {
		const pref = new webdriver.logging.Preferences();
		pref.setLevel( 'browser', webdriver.logging.Level.SEVERE );

		return pref;
	}

	/**
	 * Get screen size from manager's configuration.
	 *
	 * @return {String} Screen size like 'desktop' or 'mobile'.
	 */
	getConfigScreenSize() {
		let screenSize = this.config.screenSize || process.env.BROWSERSIZE;
		if ( screenSize === undefined || screenSize === '' ) {
			screenSize = 'desktop';
		}
		return screenSize.toLowerCase();
	}

	/**
	 * Get object representation of `screenSize`.
	 *
	 * @param {String} screenSize - Screen size like 'desktop' or 'mobile'.
	 *
	 * @return {object} Object representation of screen size.
	 */
	getScreenSizeAsObject( screenSize ) {
		switch ( screenSize ) {
			case 'mobile':
				return { width: 415, height: 1000 };
			case 'tablet':
				return { width: 1024, height: 1000 };
			case 'desktop':
				return { width: 1440, height: 1000 };
			case 'laptop':
				return { width: 1400, height: 790 };
			default:
				throw new Error(
					'Unsupported screen size specified (' + screenSize + '). ' +
					'Supported values are desktop, tablet and mobile.'
				);
		}
	}

	/**
	 * Resize the browser to `screenSize`.
	 *
	 * @param {String} screenSize - Screen size like 'desktop' or 'mobile'.
	 */
	resizeBrowser( screenSize ) {
		if ( typeof ( screenSize ) === 'string' ) {
			switch ( screenSize.toLowerCase() ) {
				case 'mobile':
					this.driver.manage().window().setSize( 415, 1000 );
					break;
				case 'tablet':
					this.driver.manage().window().setSize( 1024, 1000 );
					break;
				case 'desktop':
					this.driver.manage().window().setSize( 1440, 1000 );
					break;
				case 'laptop':
					this.driver.manage().window().setSize( 1400, 790 );
					break;
				default:
					throw new Error(
						'Unsupported screen size specified (' + screenSize + '). ' +
						'Supported values are desktop, tablet and mobile.'
					);
			}
		} else {
			throw new Error(
				'Unsupported screen size specified (' + screenSize + '). ' +
				'Supported values are desktop, tablet and mobile.' );
		}
	}

	/**
	 * Quit currently running browser.
	 *
	 * @param {Number} waitForMs - Wait time in millisecond before quit.
	 *
	 * @return {Promise} A promise that will be resolved once browser quitted.
	 */
	quitBrowser( waitForMs = 0 ) {
		const driver = this.driver;

		return driver.sleep( waitForMs ).then( () => {
			return driver.quit();
		} );
	}

	getProxyType() {
		const proxyType = this.config.proxy;

		switch ( proxyType.toLowerCase() ) {
			case 'direct':
				return proxy.direct();
			case 'system':
				return proxy.system();
			default:
				throw new Error(
					`Unknown proxy type specified of: '${ proxyType }'. Supported` +
					'values are "direct" or "system"'
				);
		}
	}

	/**
	 * Get base URL from manager's configuration.
	 *
	 * @return {String} Base URL from manager's configuration.
	 */
	getBaseUrl() {
		return this.config.baseUrl;
	}

	/**
	 * Get page url given a `pagePath` without the hostname.
	 *
	 * @param {String} pagePath - Page path.
	 *
	 * @return {String} Full URL. Joined base URL with `pagePath`.
	 */
	getPageUrl( pagePath = '/' ) {
		return urljoin( this.getBaseUrl(), pagePath );
	}
}