chromium/chrome/test/data/chromeos/virtual_keyboard/default_extension/virtual_keyboard_test_base.js

/*
 * Copyright 2013 The Chromium Authors
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

/**
 * Set to true while debugging virtual keyboard tests, for verbose debug output.
 */
var debugging = false;

/**
 * The enumeration of keyset modifiers.
 * @enum {string}
 */
var KeysetModifier =
    {NONE: 'none', SHIFT: 'shift', MORE: 'more', SYMBOL: 'symbol'};

/**
 * Flag values for the shift, control and alt modifiers as defined by
 * EventFlags in "event_constants.h".
 * @type {enum}
 */
var Modifier = {NONE: 0, SHIFT: 2, CONTROL: 4, ALT: 8};

/**
 * Display diagnostic messages when debugging tests.
 * @param {string} Message to conditionally display.
 */
function Debug(message) {
  if (debugging)
    console.log(message);
}

var mockController;

/**
 * Adds mocks for chrome extension API calls.
 *
 * @param {MockController} mockController Controller for validating
 *     calls with expectations.
 */
function mockExtensionApis(mockController) {
  /**
   * Mocks methods within a namespace.
   * @param {string} namespace Dot delimited namespace.
   * @param {Array<string>} methods List of methods names to mock.
   */
  var addMocks = function(namespace, methods) {
    var parts = namespace.split('.');
    var base = window;
    for (var i = 0; i < parts.length; i++) {
      if (!base[parts[i]])
        base[parts[i]] = {};
      base = base[parts[i]];
    }
    methods.forEach(function(m) {
      var fn = base[m] = mockController.createFunctionMock(m);
      fn.functionName = [namespace, m].join('.');
      // Method to arm triggering a callback function with the specified
      // arguments. Skip validation of callbacks.
      fn.setCallbackData = function() {
        fn.callbackData = Array.prototype.slice.call(arguments);
        fn.verifyMock = function() {};
      };
      // TODO(kevers): Add validateCall override that strips functions from the
      // argument signature before calling MockMethod.Prototype.validateCall
    });
  };

  var virtualKeyboardPrivateMethods = [
    'getKeyboardConfig', 'hideKeyboard', 'insertText', 'lockKeyboard',
    'moveCursor', 'sendKeyEvent', 'setMode', 'setKeyboardState',
    'setOccludedBounds', 'setHotrodKeyboard'
  ];

  var inputMethodPrivateMethods =
      ['getCurrentInputMethod', 'getInputMethods', 'setCurrentInputMethod'];

  addMocks('chrome.virtualKeyboardPrivate', virtualKeyboardPrivateMethods);
  addMocks('chrome.inputMethodPrivate', inputMethodPrivateMethods);
  addMocks('chrome.runtime', ['getBackgroundPage']);
  addMocks('chrome.runtime.onMessage', ['addListener']);
  // Ignore calls to addListener. Reevaluate if important to properly track the
  // flow of events.
  chrome.runtime.onMessage.addListener = function() {};

  chrome.i18n = {};
  chrome.i18n.getMessage = function(name) {
    return name;
  };
}

/**
 * Adjust the size and position of the keyboard for testing.
 */
function adjustScreenForTesting() {
  var style = document.body.style;
  style.left = 0;
  style.top = 0;
  style.bottom = 0;
  style.right = 0;
  style.background = 'transparent';
  style.position = 'absolute';

  // Adjust positioning for testing in a non-fullscreen display.
  Object.defineProperty(window.screen, 'width', {
    get: function() {
      return document.body.clientWidth;
    },
    configurable: true
  });
  Object.defineProperty(window.screen, 'height', {
    get: function() {
      return document.body.clientHeight;
    },
    configurable: true
  });
}

/**
 * Create mocks for the virtualKeyboardPrivate API. Any tests that trigger API
 * calls must set expectations for call signatures.
 */
function setUp() {
  mockController = new MockController();

  // It is not safe to install the mockTimer during setUp, as intialization of
  // the keyboard uses polling to determine when loading is complete. Instead,
  // install the mockTimer as needed once a test is initiated and unintall on
  // completion of the test.

  mockExtensionApis(mockController);

  adjustScreenForTesting();

  var validateSendCall = function(index, expected, observed) {
    // Only consider the first argument (VirtualKeyEvent) for the validation of
    // sendKeyEvent calls.
    var expectedEvent = expected[0];
    var observedEvent = observed[0];
    assertEquals(
        expectedEvent.type, observedEvent.type, 'Mismatched event types.');
    assertEquals(
        expectedEvent.charValue, observedEvent.charValue,
        'Mismatched unicode values for character.');
    assertEquals(
        expectedEvent.keyCode, observedEvent.keyCode, 'Mismatched key codes.');
    assertEquals(
        expectedEvent.modifiers, observedEvent.modifiers,
        'Mismatched states for modifiers.');
  };
  chrome.virtualKeyboardPrivate.sendKeyEvent.validateCall = validateSendCall;

  var validateLockKeyboard = function(index, expected, observed) {
    assertEquals(
        expected[0], observed[0], 'Mismatched keyboard lock/unlock state.');
  };
  chrome.virtualKeyboardPrivate.lockKeyboard.validateCall =
      validateLockKeyboard;

  chrome.virtualKeyboardPrivate.hideKeyboard.validateCall = function() {
    // hideKeyboard has one optional argument for error logging that does not
    // matter for the purpose of validating the call.
  };

  // Set data to be provided to callbacks in response to API calls.
  // TODO(kevers): Provide mechanism to override these values for individual
  // tests as needed.
  chrome.virtualKeyboardPrivate.getKeyboardConfig.setCallbackData({
    layout: 'qwerty',
    a11ymode: false,
    experimental: false,
    features: [],
  });

  chrome.inputMethodPrivate.getCurrentInputMethod.setCallbackData('us:en');

  // Set an empty list. Tests that care about input methods in the menu will
  // need to call this again with their own list of input methods.
  chrome.inputMethodPrivate.getInputMethods.setCallbackData([]);

  chrome.runtime.getBackgroundPage.setCallbackData(undefined);

  // TODO(kevers): Mock additional extension API calls as required.
}

/**
 * Verify that API calls match expectations.
 */
function tearDown() {
  mockController.verifyMocks();
  mockController.reset();
}


// ------------------- Helper Functions -------------------------

/**
 * Runs a test asynchronously once keyboard loading is complete.
 * @param {Function} runTestCallback Asynchronous test function.
 * @param {Object=} opt_config Optional configuration for the keyboard.
 */
function onKeyboardReady(runTestCallback, opt_config) {
  var default_config = {
    keyset: 'us.compact.qwerty',
    languageCode: 'en',
    passwordLayout: 'us',
    name: 'English'
  };
  var config = opt_config || default_config;
  var options = config.options || {};
  chrome.virtualKeyboardPrivate.keyboardLoaded = function() {
    runTestCallback();
  };
  window.initializeVirtualKeyboard(
      config.keyset, config.languageCode, config.passwordLayout, config.name,
      options);
}

/**
 * Defers continuation of a test until one or more keysets are loaded.
 * @param {string|Array<string>} keyset Name of the target keyset or list of
 *     keysets.
 * @param {Function} continueTestCallback Callback function to invoke in order
 *     to resume the test.
 */
function onKeysetsReady(keyset, continueTestCallback) {
  if (keyset instanceof Array) {
    var blocked = false;
    keyset.forEach(function(id) {
      if (!(id in controller.container_.keysetViewMap))
        blocked = true;
    });
    if (!blocked) {
      continueTestCallback();
      return;
    }
  } else if (keyset in controller.container_.keysetViewMap) {
    continueTestCallback();
    return;
  }
  setTimeout(function() {
    onKeysetsReady(keyset, continueTestCallback);
  }, 100);
}

/**
 * Mocks a touch event targeted on a key.
 * @param {!Element} key .
 * @param {string} eventType .
 */
function mockTouchEvent(key, eventType) {
  var rect = key.getBoundingClientRect();
  var x = rect.left + rect.width / 2;
  var y = rect.top + rect.height / 2;
  var e = document.createEvent('UIEvent');
  e.initUIEvent(eventType, true, true);
  e.touches = [{pageX: x, pageY: y}];
  e.target = key;
  return key.dispatchEvent(e);
}

/**
 * Simulates tapping on a key.
 * @param {!Element} key .
 */
function mockTap(key) {
  mockTouchEvent(key, 'touchstart');
  mockTouchEvent(key, 'touchend');
}

/**
 * Returns the active keyboard view.
 * @return {!HTMLElement}
 */
function getActiveView() {
  var container = document.querySelector('.inputview-container');
  var views = container.querySelectorAll('.inputview-view');
  for (var i = 0; i < views.length; i++) {
    var display = views[i].style.display;
    if (!display || display != 'none') {
      return views[i];
    }
  }
}

/**
 * Finds the ancestor element corresponding the the soft key view.
 * @param {Element} source .
 * @return {?Element} .
 */
function getSoftKeyView(source) {
  var parent = source.parentElement;
  while (parent && !parent.classList.contains('inputview-skv')) {
    parent = parent.parentElement;
  }
  return parent;
}

/**
 * Locates a key by label.
 * @param {string} label The label on the key.  If the key has multiple labels,
 *    |label| can match any of them.
 * @param {string=} opt_rowId Optional ID of the row containing the key.
 * @returns {?Element} .
 */
function findKey(label, opt_rowId) {
  var view = getActiveView();
  assertTrue(!!view, 'Unable to find active keyboard view');
  if (opt_rowId) {
    view = view.querySelector('#' + opt_rowId);
    assertTrue(!!view, 'Unable to locate row ' + opt_rowId);
  }
  var candidates = view.querySelectorAll('.inputview-ch');
  // Compact layouts use a different naming convention.
  if (candidates.length == 0)
    candidates = view.querySelectorAll('.inputview-special-key-name');
  for (var i = 0; i < candidates.length; i++) {
    if (candidates[i].textContent == label)
      return candidates[i];
  }
  assertTrue(false, 'Cannot find key labeled \'' + label + '\'');
}

/**
 * Locates a key with matching ID. Note that IDs are not necessarily unique
 * across keysets; however, it is unique within the active layout.
 */
function findKeyById(label) {
  var view = getActiveView();
  assertTrue(!!view, 'Unable to find active keyboard view');
  var key = view.querySelector('#' + label);
  assertTrue(!!key, 'Cannot find key with ID ' + label);
  return key;
}

/**
 * Verifies if a key contains a matching label.
 * @param {Element} key .
 * @param {string} label .
 * @return {boolean} .
 */
function hasLabel(key, label) {
  var characters = key.querySelectorAll('.inputview-ch');
  // Compact layouts represent keys differently.
  if (characters.length == 0)
    characters = key.querySelectorAll('.inputview-special-key-name');
  for (var i = 0; i < characters.length; i++) {
    if (characters[i].textContent == label)
      return true;
  }
  return false;
}

/**
 * Mock typing of basic keys. Each keystroke should trigger a pair of
 * API calls to send viritual key events.
 * @param {string} label The character being typed.
 * @param {number} keyCode The legacy key code for the character.
 * @param {number} modifiers Indicates which if any of the shift, control and
 *     alt keys are being virtually pressed.
 * @param {number=} opt_unicode Optional unicode value for the character. Only
 *     required if it cannot be directly calculated from the label.
 */
function mockTypeCharacter(label, keyCode, modifiers, opt_unicode) {
  var key = label.length == 1 ? findKey(label) : findKeyById(label);
  // opt_unicode is allowed to be 0.
  var unicodeValue = opt_unicode;
  if (unicodeValue === undefined)
    unicodeValue = label.charCodeAt(0);
  var send = chrome.virtualKeyboardPrivate.sendKeyEvent;
  send.addExpectation({
    type: 'keydown',
    charValue: unicodeValue,
    keyCode: keyCode,
    modifiers: modifiers
  });
  send.addExpectation({
    type: 'keyup',
    charValue: unicodeValue,
    keyCode: keyCode,
    modifiers: modifiers
  });
  mockTap(key);
}

/**
 * Emulates a user triggering a keyset modifier.
 * @param {!KeysetModifier} The modifier to apply.
 */
function applyKeysetModifier(modifier) {
  if (modifier == KeysetModifier.NONE) {
    return;
  }
  var activeView = getActiveView();
  if (modifier == KeysetModifier.SHIFT) {
    // Set state of shift key.
    var leftShift = activeView.querySelector('#ShiftLeft');
    assertTrue(!!leftShift, 'Unable to find left shift key');
    var currentShiftState =
        !!leftShift.querySelector('.inputview-special-key-highlight');
    if (!currentShiftState) {
      mockTap(leftShift);
    }
  } else if (modifier == KeysetModifier.SYMBOL) {
    var switchKey = findKey('?123', 'spaceKeyrow');
    assertTrue(!!switchKey, 'Unable to find symbol transition key');
    // Switch to symbol keyset.
    mockTap(switchKey);
  } else {
    var switchKey = findKey('~[<', 'row3');
    assertTrue(!!switchKey, 'Unable to find more transition key');
    // Switch to more keyset.
    mockTap(switchKey);
  }
}