controllers.js

'use strict';

const co = require('co');
const { getByName: getAtom } = require('selenium-atoms');
const { errors } = require('webdriver-dfn-error-code');
const { getErrorByCode } = require('webdriver-dfn-error-code');

const _ = require('./helper');
const logger = require('./logger');

const ELEMENT_OFFSET = 1000;

const implicitWaitForCondition = function(func) {
  return _.waitForCondition(func, this.implicitWaitMs);
};

const sendCommand = async function(type, args) {
  let result = await this.send({
    action: type,
    args: args
  });

  if (typeof result === 'string') {
    try {
      result = JSON.parse(result);
    } catch (e) {
      throw new errors.UnknownError(e.message);
    }
  }
  const code = result.status;
  const value = result.value;

  if (code === 0) {
    return value;
  } else {
    const errorName = getErrorByCode(code);
    const errorMsg = (value && value.message) || result.message;
    throw new errors[errorName](errorMsg);
  }
};

const sendJSCommand = async function(atom, args, inDefaultFrame) {
  let frames = !inDefaultFrame && this.frame ? [this.frame] : [];
  let atomScript = getAtom(atom);
  let script;
  if (frames.length) {
    let elem = getAtom('get_element_from_cache');
    let frame = frames[0];
    script = `(function (window) { var document = window.document;
      return (${atomScript}); })((${elem.toString('utf8')})(${JSON.stringify(
      frame
    )}))`;
  } else {
    script = `(${atomScript})`;
  }
  const command = `${script}(${args.map(JSON.stringify).join(',')})`;
  return await sendCommand.call(this, 'js', command);
};

const convertAtoms2Element = function(atoms) {
  const atomsId = atoms && atoms.ELEMENT;

  if (!atomsId) {
    return null;
  }

  const index = this.atoms.push(atomsId) - 1;

  return {
    ELEMENT: index + ELEMENT_OFFSET
  };
};

const convertElement2Atoms = function(elementId) {
  if (!elementId) {
    return null;
  }

  let atomsId;

  try {
    atomsId = this.atoms[parseInt(elementId, 10) - ELEMENT_OFFSET];
  } catch (e) {
    return null;
  }

  return {
    ELEMENT: atomsId
  };
};

const findElementOrElements = async function(strategy, selector, ctx, many) {
  let result;
  const that = this;

  const atomsElement = convertElement2Atoms.call(this, ctx);

  async function search() {
    result = await sendJSCommand.call(that, `find_element${many ? 's' : ''}`, [
      strategy,
      selector,
      atomsElement
    ]);
    const size = _.size(result);

    if (many) {
      return size >= 0;
    }

    return size > 0;
  }

  try {
    await implicitWaitForCondition.call(this, co.wrap(search));
  } catch (err) {
    result = [];
  }

  if (many) {
    return result.map(convertAtoms2Element.bind(this));
  } else {
    if (!result || _.size(result) === 0) {
      throw new errors.NoSuchElement();
    }
    return convertAtoms2Element.call(this, result);
  }
};

const controllers = {};

/**
 * Change focus to another frame on the page.
 *
 * @module setFrame
 * @param {string} frame Identifier(id/name) for the frame to change focus to
 * @returns {Promise}
 */
controllers.setFrame = async function(frame) {
  if (!frame) {
    this.frame = null;
    logger.debug('Back to default content');
    return null;
  }

  if (frame.ELEMENT) {
    let atomsElement = convertElement2Atoms.call(this, frame.ELEMENT);
    let result = await sendJSCommand.call(this, 'get_frame_window', [
      atomsElement
    ]);
    logger.debug(`Entering into web frame: '${result.WINDOW}'`);
    this.frame = result.WINDOW;
    return null;
  } else {
    let atom = _.isNumber(frame) ? 'frame_by_index' : 'frame_by_id_or_name';
    let result = await sendJSCommand.call(this, atom, [frame]);
    if (!result || !result.WINDOW) {
      throw new errors.NoSuchFrame();
    }
    logger.debug(`Entering into web frame: '${result.WINDOW}'`);
    this.frame = result.WINDOW;
    return null;
  }
};

/**
 * Click on an element.
 *
 * @module click
 * @returns {Promise}
 */
controllers.click = async function(elementId) {
  const atomsElement = convertElement2Atoms.call(this, elementId);
  return await sendJSCommand.call(this, 'click', [atomsElement]);
};

/**
 * Search for an element on the page, starting from the document root.
 * @module findElement
 * @param {string} strategy The type
 * @param {string} using The locator strategy to use.
 * @param {string} value The search target.
 * @returns {Promise.<Element>}
 */
controllers.findElement = async function(strategy, selector, ctx) {
  return await findElementOrElements.call(this, strategy, selector, ctx, false);
};

controllers.findElements = async function(strategy, selector, ctx) {
  return await findElementOrElements.call(this, strategy, selector, ctx, true);
};

/**
 * Returns the visible text for the element.
 *
 * @module getText
 * @returns {Promise.<string>}
 */
controllers.getText = async function(elementId) {
  const atomsElement = convertElement2Atoms.call(this, elementId);
  return await sendJSCommand.call(this, 'get_text', [atomsElement]);
};

/**
 * Clear a TEXTAREA or text INPUT element's value.
 *
 * @module clearText
 * @returns {Promise.<string>}
 */
controllers.clearText = async function(elementId) {
  const atomsElement = convertElement2Atoms.call(this, elementId);
  return await sendJSCommand.call(this, 'clear', [atomsElement]);
};

/**
 * Set element's value.
 *
 * @module setValue
 * @param elementId
 * @param value
 * @returns {Promise.<string>}
 */
controllers.setValue = async function(elementId, value) {
  const atomsElement = convertElement2Atoms.call(this, elementId);
  await sendJSCommand.call(this, 'click', [atomsElement]);
  return await sendJSCommand.call(this, 'type', [atomsElement, value]);
};

/**
 * Determine if an element is currently displayed.
 *
 * @module isDisplayed
 * @returns {Promise.<string>}
 */
controllers.isDisplayed = async function(elementId) {
  const atomsElement = convertElement2Atoms.call(this, elementId);
  return await sendJSCommand.call(this, 'is_displayed', [atomsElement]);
};

/**
 * Get the value of an element's property.
 *
 * @module getProperty
 * @returns {Promise.<string>}
 */
controllers.getProperty = async function(elementId, attrName) {
  const atomsElement = convertElement2Atoms.call(this, elementId);
  return await sendJSCommand.call(this, 'get_attribute_value', [
    atomsElement,
    attrName
  ]);
};

/**
 * Get the current page title.
 *
 * @module title
 * @returns {Promise.<Object>}
 */
controllers.title = async function() {
  return await this.execute('return document.title;');
};

/**
 * Inject a snippet of JavaScript into the page for execution in the context of the currently selected frame.
 *
 * @module execute
 * @param code script
 * @param [args] script argument array
 * @returns {Promise.<string>}
 */
controllers.execute = async function(script, args) {
  if (!args) {
    args = [];
  }

  // args = args.map(arg => {
  //   if (arg.ELEMENT) {
  //     return convertElement2Atoms.call(this, arg.ELEMENT);
  //   } else {
  //     return arg;
  //   }
  // });

  const value = await sendJSCommand.call(
    this,
    'execute_script',
    [script, args],
    true
  );

  if (Array.isArray(value)) {
    return value.map(convertAtoms2Element.bind(this));
  } else {
    return value;
  }
};

/**
 * Retrieve the URL of the current page.
 *
 * @module url
 * @returns {Promise.<string>}
 */
controllers.url = async function() {
  return await sendCommand.call(this, 'url');
};

/**
 * Navigate to a new URL.
 *
 * @module get
 * @param url get a new url.
 * @param options
 *   preserveCookies - whether to preserve cookies.
 * @returns {Promise.<string>}
 */
controllers.get = async function(url, options) {
  this.frame = null;
  return await sendCommand.call(this, 'get', {
    url,
    args: this.args,
    preserveCookies: options ? options.preserveCookies : null
  });
};

/**
 * Navigate forwards in the browser history, if possible.
 *
 * @module forward
 * @returns {Promise.<string>}
 */
controllers.forward = async function() {
  throw new errors.NotImplementedError();
};

/**
 * Navigate backwards in the browser history, if possible.
 *
 * @module back
 * @returns {Promise.<string>}
 */
controllers.back = async function() {
  this.frame = null;
  return await this.execute('history.back()');
};

/**
 * Get all window handlers.
 *
 * @module back
 * @returns {Promise.<array>}
 */
controllers.getWindows = async function() {
  return await sendCommand.call(this, 'getWindows');
};

controllers.setWindow = async function(windowHandle) {
  return await sendCommand.call(this, 'setWindow', windowHandle);
};

/**
 * Get the size of the specified window.
 *
 * @module setWindowSize
 * @param [handle] window handle to set size for (optional, default: 'current')
 * @returns {Promise.<string>}
 */
controllers.getWindowSize = async function() {
  return await sendCommand.call(this, 'getWindowSize', {});
};

/**
 * Set the size of the specified window.
 *
 * @module setWindowSize
 * @param [handle] window handle to set size for (optional, default: 'current')
 * @returns {Promise.<string>}
 */
controllers.setWindowSize = async function(windowHandle, width, height) {
  return await sendCommand.call(this, 'setWindowSize', {
    windowHandle,
    width,
    height
  });
};

/**
 * Maximize the specified window if not already maximized.
 *
 * @module maximize
 * @param handle window handle
 * @returns {Promise.<string>}
 */
controllers.maximize = async function(windowHandle) {
  return await sendCommand.call(this, 'maximize', {
    windowHandle
  });
};

/**
 * Refresh the current page.
 *
 * @module refresh
 * @returns {Promise.<string>}
 */
controllers.refresh = async function() {
  this.frame = null;
  return await this.execute('location.reload()');
};

/**
 * Get the current page source.
 *
 * @module getSource
 * @returns {Promise.<string>}
 */
controllers.getSource = async function() {
  const cmd = "return document.getElementsByTagName('html')[0].outerHTML";
  return await this.execute(cmd);
};

/**
 * Take a screenshot of the current page.
 *
 * @module getScreenshot
 * @returns {Promise.<string>} The screenshot as a base64 encoded PNG.
 */
controllers.getScreenshot = async function() {
  return await sendCommand.call(this, 'getScreenshot');
};

/**
 * Query the value of an element's computed CSS property.
 *
 * @module getComputedCss
 * @returns {Promise.<string>}
 */
controllers.getComputedCss = async function(elementId, propertyName) {
  return await this.execute(
    'return window.getComputedStyle(arguments[0], null).getPropertyValue(arguments[1]);',
    [convertElement2Atoms.call(this, elementId), propertyName]
  );
};

/**
 * Returns all cookies associated with the address of the current browsing context’s active document.
 *
 * @module getAllCookies
 * @returns {Promise.<string>}
 */
controllers.getAllCookies = async function() {
  return await sendCommand.call(this, 'getAllCookies');
};

/**
 * Returns the cookie with the requested name from the associated cookies in the cookie store of the current browsing context’s active document. If no cookie is found, a no such cookie error is returned.
 *
 * @module getNamedCookie
 * @returns {Promise.<string>}
 */
controllers.getNamedCookie = async function(name) {
  return await sendCommand.call(this, 'getNamedCookie', name);
};

/**
 * Adds a single cookie to the cookie store associated with the active document’s address.
 *
 * @module addCookie
 * @returns {Promise.<string>}
 */
controllers.addCookie = async function(cookie) {
  return await sendCommand.call(this, 'addCookie', cookie);
};

/**
 * Delete either a single cookie by parameter name, or all the cookies associated with the active document’s address if name is undefined.
 *
 * @module deleteCookie
 * @returns {Promise.<string>}
 */
controllers.deleteCookie = async function(name, url) {
  return await sendCommand.call(this, 'deleteCookie', { name, url });
};

/**
 * Delete All Cookies command allows deletion of all cookies associated with the active document’s address.
 *
 * @module deleteAllCookies
 * @returns {Promise.<string>}
 */
controllers.deleteAllCookies = async function() {
  return await sendCommand.call(this, 'deleteAllCookies');
};

/**
 * Clears local storage of current the session.
 *
 * @module clearLocalstorage
 * @returns {Promise.<string>}
 */
controllers.clearLocalstorage = async function() {
  return await sendCommand.call(this, 'clearLocalstorage');
};

module.exports = controllers;