<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=1137557
-->
<head>
  <title>Test for new API arguments accepting D3E properties</title>
  <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
  <script type="application/javascript;version=1.7" src="inputmethod_common.js"></script>
  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1137557">Mozilla Bug 1137557</a>
<p id="display"></p>
<pre id="test">
<script class="testbody" type="application/javascript;version=1.7">

inputmethod_setup(function() {
  runTest();
});

let gEventDetails = [];
let gCurrentValue = '';
let gTestDescription = '';

let appFrameScript = function appFrameScript() {
  let input = content.document.body.firstElementChild;

  input.focus();

  function sendEventDetail(evt) {
    var eventDetail;

    switch (evt.type) {
      case 'compositionstart':
      case 'compositionupdate':
      case 'compositionend':
        eventDetail = {
          type: evt.type,
          value: input.value,
          data: evt.data
        };
        break;

      case 'input':
        eventDetail = {
          type: evt.type,
          value: input.value
        };
        break;

      default: // keyboard events
        eventDetail = {
          type: evt.type,
          charCode: evt.charCode,
          keyCode: evt.keyCode,
          key: evt.key,
          code: evt.code,
          location: evt.location,
          repeat: evt.repeat,
          value: input.value,
          shift: evt.getModifierState('Shift'),
          capsLock: evt.getModifierState('CapsLock'),
          control: evt.getModifierState('Control'),
          alt: evt.getModifierState('Alt')
        };
        break;
    }

    sendAsyncMessage('test:eventDetail', eventDetail);
  }

  input.addEventListener('compositionstart', sendEventDetail);
  input.addEventListener('compositionupdate', sendEventDetail);
  input.addEventListener('compositionend', sendEventDetail);
  input.addEventListener('input', sendEventDetail);
  input.addEventListener('keydown', sendEventDetail);
  input.addEventListener('keypress', sendEventDetail);
  input.addEventListener('keyup', sendEventDetail);
};

function waitForInputContextChange() {
  return new Promise((resolve) => {
    navigator.mozInputMethod.oninputcontextchange = resolve;
  });
}

function assertEventDetail(expectedDetails, testName) {
  is(gEventDetails.length, expectedDetails.length,
    testName + ' expects ' + expectedDetails.map(d => d.type).join(', ') + ' events, got ' + gEventDetails.map(d => d.type).join(', '));

  expectedDetails.forEach((expectedDetail, j) => {
    for (let key in expectedDetail) {
      is(gEventDetails[j][key], expectedDetail[key],
        testName + ' expects ' + key + ' of ' + gEventDetails[j].type + ' to be equal to ' + expectedDetail[key]);
    }
  });
}

function sendKeyAndAssertResult(testdata) {
  var dict = testdata.dict;
  var testName = gTestDescription + 'sendKey(' + JSON.stringify(dict) + ')';
  var promise = navigator.mozInputMethod.inputcontext.sendKey(dict);

  if (testdata.expectedReject) {
    promise = promise
      .then(() => {
        ok(false, testName + ' should not resolve.');
      }, (e) => {
        ok(true, testName + ' rejects.');
        ok(e instanceof testdata.expectedReject, 'Reject with type.');
      })

    return promise;
  }

  promise = promise
    .then((res) => {
      is(res, true,
        testName + ' should resolve to true.');

      var expectedEventDetail = [];

      var expectedValues = testdata.expectedValues;

      expectedEventDetail.push({
        type: 'keydown',
        key: expectedValues.key,
        charCode: 0,
        code: expectedValues.code || '',
        keyCode: expectedValues.keyCode || 0,
        location: expectedValues.location ? expectedValues.location : 0,
        repeat: expectedValues.repeat || false,
        value: gCurrentValue,
        shift: false,
        capsLock: false,
        control: false,
        alt: false
      });

      if (testdata.expectedKeypress) {
        expectedEventDetail.push({
          type: 'keypress',
          key: expectedValues.key,
          charCode: expectedValues.charCode,
          code: expectedValues.code || '',
          keyCode: expectedValues.charCode ? 0 : expectedValues.keyCode,
          location: expectedValues.location ? expectedValues.location : 0,
          repeat: expectedValues.repeat || false,
          value: gCurrentValue,
          shift: false,
          capsLock: false,
          control: false,
          alt: false
        });
      }

      if (testdata.expectedInput) {
        switch (testdata.expectedInput) {
          case 'Enter':
            gCurrentValue += '\n';
            break;
          case 'Backspace':
            gCurrentValue =
              gCurrentValue.substr(0, gCurrentValue.length - 1);
            break;
          default:
            gCurrentValue += testdata.expectedInput;
            break;
        }

        expectedEventDetail.push({
          type: 'input',
          value: gCurrentValue
        });
      }

      if (!testdata.expectedRepeat) {
        expectedEventDetail.push({
          type: 'keyup',
          key: expectedValues.key,
          charCode: 0,
          code: expectedValues.code || '',
          keyCode: expectedValues.keyCode || 0,
          location: expectedValues.location ? expectedValues.location : 0,
          repeat: expectedValues.repeat || false,
          value: gCurrentValue,
          shift: false,
          capsLock: false,
          control: false,
          alt: false
        });
      }

      assertEventDetail(expectedEventDetail, testName);
      gEventDetails = [];
    }, (e) => {
      ok(false, testName + ' should not reject. ' + e);
    });

  return promise;
}

function runSendKeyAlphabetTests() {
  gTestDescription = 'runSendKeyAlphabetTests(): ';
  var promiseQueue = Promise.resolve();

  // Test the plain alphabets
  var codeA = 'A'.charCodeAt(0);
  for (var i = 0; i < 26; i++) {
    // callbacks in then() are deferred; must only reference these block-scoped
    // variable instead of i.
    let keyCode = codeA + i;
    let code = 'Key' + String.fromCharCode(keyCode);

    [String.fromCharCode(keyCode),
      String.fromCharCode(keyCode).toLowerCase()]
    .forEach((chr) => {
      // Test plain alphabet
      promiseQueue = promiseQueue.then(() => {
        return sendKeyAndAssertResult({
          dict: {
            key: chr
          },
          expectedKeypress: true,
          expectedInput: chr,
          expectedValues: {
            key: chr, code: '',
            keyCode: keyCode,
            charCode: chr.charCodeAt(0)
          }
        });
      });

      // Test plain alphabet with keyCode set
      promiseQueue = promiseQueue.then(() => {
        return sendKeyAndAssertResult({
          dict: {
            key: chr,
            keyCode: keyCode
          },
          expectedKeypress: true,
          expectedInput: chr,
          expectedValues: {
            key: chr, code: '',
            keyCode: keyCode,
            charCode: chr.charCodeAt(0)
          }
        });
      });

      // Test plain alphabet with keyCode set to keyCode + 1,
      // expects keyCode to follow key value and ignore the incorrect value.
      promiseQueue = promiseQueue.then(() => {
        return sendKeyAndAssertResult({
          dict: {
            key: chr,
            keyCode: keyCode + 1
          },
          expectedKeypress: true,
          expectedInput: chr,
          expectedValues: {
            key: chr, code: '',
            keyCode: keyCode,
            charCode: chr.charCodeAt(0)
          }
        });
      });

      // Test plain alphabet with code set
      promiseQueue = promiseQueue.then(() => {
        return sendKeyAndAssertResult({
          dict: {
            key: chr,
            code: code
          },
          expectedKeypress: true,
          expectedInput: chr,
          expectedValues: {
            key: chr, code: code,
            keyCode: keyCode,
            charCode: chr.charCodeAt(0)
          }
        });
      });

      // Test plain alphabet with code set to Digit1,
      // expects keyCode to follow key value.
      promiseQueue = promiseQueue.then(() => {
        return sendKeyAndAssertResult({
          dict: {
            key: chr,
            code: 'Digit1'
          },
          expectedKeypress: true,
          expectedInput: chr,
          expectedValues: {
            key: chr, code: 'Digit1',
            keyCode: keyCode,
            charCode: chr.charCodeAt(0)
          }
        });
      });

      // Test plain alphabet with keyCode set to DOM_VK_1,
      // expects keyCode to follow key value.
      promiseQueue = promiseQueue.then(() => {
        return sendKeyAndAssertResult({
          dict: {
            key: chr,
            keyCode: KeyboardEvent.DOM_VK_1
          },
          expectedKeypress: true,
          expectedInput: chr,
          expectedValues: {
            key: chr, code: '',
            keyCode: keyCode,
            charCode: chr.charCodeAt(0)
          }
        });
      });

      // Test plain alphabet with code set to Digit1
      // and keyCode set to DOM_VK_1,
      // expects keyCode to follow key value.
      promiseQueue = promiseQueue.then(() => {
        return sendKeyAndAssertResult({
          dict: {
            key: chr,
            code: 'Digit1',
            keyCode: KeyboardEvent.DOM_VK_1
          },
          expectedKeypress: true,
          expectedInput: chr,
          expectedValues: {
            key: chr, code: 'Digit1',
            keyCode: keyCode,
            charCode: chr.charCodeAt(0)
          }
        });
      });
    });
  }

  return promiseQueue;
}

function runSendKeyNumberTests() {
  gTestDescription = 'runSendKeyNumberTests(): ';
  var promiseQueue = Promise.resolve();

  // Test numbers
  var code0 = '0'.charCodeAt(0);
  for (var i = 0; i < 10; i++) {
    // callbacks in then() are deferred; must only reference these block-scoped
    // variable instead of i.
    let keyCode = code0 + i;
    let chr = String.fromCharCode(keyCode);

    // Test plain number
    promiseQueue = promiseQueue.then(() => {
      return sendKeyAndAssertResult({
        dict: {
          key: chr
        },
        expectedKeypress: true,
        expectedInput: chr,
        expectedValues: {
          key: chr, code: '',
          keyCode: keyCode,
          charCode: chr.charCodeAt(0)
        }
      });
    });

    // Test plain number with keyCode set
    promiseQueue = promiseQueue.then(() => {
      return sendKeyAndAssertResult({
        dict: {
          key: chr,
          keyCode: keyCode
        },
        expectedKeypress: true,
        expectedInput: chr,
        expectedValues: {
          key: chr, code: '',
          keyCode: keyCode,
          charCode: chr.charCodeAt(0)
        }
      });
    });

    // Test plain number with keyCode set to keyCode + 1,
    // expects keyCode to follow key value and ignore the incorrect value.
    promiseQueue = promiseQueue.then(() => {
      return sendKeyAndAssertResult({
        dict: {
          key: chr,
          keyCode: keyCode + 1
        },
        expectedKeypress: true,
        expectedInput: chr,
        expectedValues: {
          key: chr, code: '',
          keyCode: keyCode,
          charCode: chr.charCodeAt(0)
        }
      });
    });

    // Test plain number with code set
    promiseQueue = promiseQueue.then(() => {
      return sendKeyAndAssertResult({
        dict: {
          key: chr,
          code: 'Digit' + chr
        },
        expectedKeypress: true,
        expectedInput: chr,
        expectedValues: {
          key: chr, code: 'Digit' + chr,
          keyCode: keyCode,
          charCode: chr.charCodeAt(0)
        }
      });
    });

    // Test plain upper caps alphabet with code set to KeyA,
    // expects keyCode to follow key value.
    promiseQueue = promiseQueue.then(() => {
      return sendKeyAndAssertResult({
        dict: {
          key: chr,
          code: 'KeyA'
        },
        expectedKeypress: true,
        expectedInput: chr,
        expectedValues: {
          key: chr, code: 'KeyA',
          keyCode: keyCode,
          charCode: chr.charCodeAt(0)
        }
      });
    });

    // Test plain upper caps alphabet with code set to KeyA,
    // and keyCode set to DOM_VK_A.
    // expects keyCode to follow key value.
    promiseQueue = promiseQueue.then(() => {
      return sendKeyAndAssertResult({
        dict: {
          key: chr,
          code: 'KeyA',
          keyCode: KeyboardEvent.DOM_VK_A
        },
        expectedKeypress: true,
        expectedInput: chr,
        expectedValues: {
          key: chr, code: 'KeyA',
          keyCode: keyCode,
          charCode: chr.charCodeAt(0)
        }
      });
    });
  }

  return promiseQueue;
}

function runSendKeyDvorakTests() {
  gTestDescription = 'runSendKeyDvorakTests(): ';
  var promiseQueue = Promise.resolve();

  // Test Dvorak layout emulation
  var qwertyCodeForDvorakKeys = [
    'KeyR', 'KeyT', 'KeyY', 'KeyU', 'KeyI', 'KeyO', 'KeyP',
    'KeyA', 'KeyS', 'KeyD', 'KeyF', 'KeyG',
    'KeyH', 'KeyJ', 'KeyK', 'KeyL', 'Semicolon',
    'KeyX', 'KeyC', 'KeyV', 'KeyB', 'KeyN',
    'KeyM', 'Comma', 'Period', 'Slash'];
  var dvorakKeys = 'PYFGCRL' +
    'AOEUIDHTNS' +
    'QJKXBMWVZ';
  for (var i = 0; i < dvorakKeys.length; i++) {
    // callbacks in then() are deferred; must only reference these block-scoped
    // variable instead of i.
    let keyCode = dvorakKeys.charCodeAt(i);
    let code = qwertyCodeForDvorakKeys[i];

    [dvorakKeys.charAt(i), dvorakKeys.charAt(i).toLowerCase()]
    .forEach((chr) => {
      // Test alphabet with code set to Qwerty code,
      // expects keyCode to follow key value.
      // (This is *NOT* the expected scenario for emulating a Dvorak keyboard,
      //  even though expected results are the same.)
      promiseQueue = promiseQueue.then(() => {
        return sendKeyAndAssertResult({
          dict: {
            key: chr,
            code: code
          },
          expectedKeypress: true,
          expectedInput: chr,
          expectedValues: {
            key: chr, code: code,
            keyCode: keyCode,
            charCode: chr.charCodeAt(0)
          }
        });
      });

      // Test alphabet with code set to Qwerty code and keyCode set,
      // expects keyCode to follow key/keyCode value.
      // (This is the expected scenario for emulating a Dvorak keyboard)
      promiseQueue = promiseQueue.then(() => {
        return sendKeyAndAssertResult({
          dict: {
            key: chr,
            keyCode: keyCode,
            code: code
          },
          expectedKeypress: true,
          expectedInput: chr,
          expectedValues: {
            key: chr, code: code,
            keyCode: keyCode,
            charCode: chr.charCodeAt(0)
          }
        });
      });
    });
  }

  var qwertyCodeForDvorakSymbols = [
    'Minus', 'Equal',
    'KeyQ', 'KeyW', 'KeyE', 'BracketLeft', 'BracketRight', 'Backslash',
    'Quote', 'KeyZ'];

  var shiftDvorakSymbols = '{}\"<>?+|_:';
  var dvorakSymbols = '[]\',./=\\-;';
  var dvorakSymbolsKeyCodes = [
    KeyboardEvent.DOM_VK_OPEN_BRACKET,
    KeyboardEvent.DOM_VK_CLOSE_BRACKET,
    KeyboardEvent.DOM_VK_QUOTE,
    KeyboardEvent.DOM_VK_COMMA,
    KeyboardEvent.DOM_VK_PERIOD,
    KeyboardEvent.DOM_VK_SLASH,
    KeyboardEvent.DOM_VK_EQUALS,
    KeyboardEvent.DOM_VK_BACK_SLASH,
    KeyboardEvent.DOM_VK_HYPHEN_MINUS,
    KeyboardEvent.DOM_VK_SEMICOLON
  ];

  for (var i = 0; i < dvorakSymbols.length; i++) {
    // callbacks in then() are deferred; must only reference these block-scoped
    // variable instead of i.
    let keyCode = dvorakSymbolsKeyCodes[i];
    let code = qwertyCodeForDvorakSymbols[i];

    [dvorakSymbols.charAt(i), shiftDvorakSymbols.charAt(i)]
    .forEach((chr) => {
      // Test symbols with code set to Qwerty code,
      // expects keyCode to be 0.
      // (This is *NOT* the expected scenario for emulating a Dvorak keyboard)
      promiseQueue = promiseQueue.then(() => {
        return sendKeyAndAssertResult({
          dict: {
            key: chr,
            code: code
          },
          expectedKeypress: true,
          expectedInput: chr,
          expectedValues: {
            key: chr, code: code,
            keyCode: 0,
            charCode: chr.charCodeAt(0)
          }
        });
      });

      // Test alphabet with code set to Qwerty code and keyCode set,
      // expects keyCode to follow keyCode value.
      // (This is the expected scenario for emulating a Dvorak keyboard)
      promiseQueue = promiseQueue.then(() => {
        return sendKeyAndAssertResult({
          dict: {
            key: chr,
            keyCode: keyCode,
            code: code
          },
          expectedKeypress: true,
          expectedInput: chr,
          expectedValues: {
            key: chr, code: code,
            keyCode: keyCode,
            charCode: chr.charCodeAt(0)
          }
        });
      });
    });
  }

  return promiseQueue;
}

function runSendKeyDigitKeySymbolsTests() {
  gTestDescription = 'runSendKeyDigitKeySymbolsTests(): ';
  var promiseQueue = Promise.resolve();

  var digitKeySymbols = ')!@#$%^&*(';
  for (var i = 0; i < digitKeySymbols.length; i++) {
    // callbacks in then() are deferred; must only reference these block-scoped
    // variable instead of i.
    let keyCode = KeyboardEvent['DOM_VK_' + i];
    let chr = digitKeySymbols.charAt(i);
    let code = 'Digit' + i;

    // Test plain symbol
    promiseQueue = promiseQueue.then(() => {
      return sendKeyAndAssertResult({
        dict: {
          key: chr
        },
        expectedKeypress: true,
        expectedInput: chr,
        expectedValues: {
          key: chr, code: '', keyCode: 0,
          charCode: chr.charCodeAt(0)
        }
      });
    });

    // Test plain symbol with keyCode set
    promiseQueue = promiseQueue.then(() => {
      return sendKeyAndAssertResult({
        dict: {
          key: chr,
          keyCode: keyCode
        },
        expectedKeypress: true,
        expectedInput: chr,
        expectedValues: {
          key: chr, code: '',
          keyCode: keyCode,
          charCode: chr.charCodeAt(0)
        }
      });
    });

    // Test plain symbol with code set
    promiseQueue = promiseQueue.then(() => {
      return sendKeyAndAssertResult({
        dict: {
          key: chr,
          code: code
        },
        expectedKeypress: true,
        expectedInput: chr,
        expectedValues: {
          key: chr, code: code,
          keyCode: 0,
          charCode: chr.charCodeAt(0)
        }
      });
    });

    // Test plain symbol with code set to KeyA,
    // expects keyCode to be 0.
    promiseQueue = promiseQueue.then(() => {
      return sendKeyAndAssertResult({
        dict: {
          key: chr,
          code: 'KeyA'
        },
        expectedKeypress: true,
        expectedInput: chr,
        expectedValues: {
          key: chr, code: 'KeyA',
          keyCode: 0,
          charCode: chr.charCodeAt(0)
        }
      });
    });

    // Test plain symbol with keyCode set to DOM_VK_A,
    // expects keyCode to follow the keyCode set.
    promiseQueue = promiseQueue.then(() => {
      return sendKeyAndAssertResult({
        dict: {
          key: chr,
          keyCode: KeyboardEvent.DOM_VK_A
        },
        expectedKeypress: true,
        expectedInput: chr,
        expectedValues: {
          key: chr, code: '',
          keyCode: KeyboardEvent.DOM_VK_A,
          charCode: chr.charCodeAt(0)
        }
      });
    });

    // Test plain symbol with code set to KeyA
    // expects keyCode to follow the keyCode set.
    promiseQueue = promiseQueue.then(() => {
      return sendKeyAndAssertResult({
        dict: {
          key: chr,
          code: 'KeyA',
          keyCode: KeyboardEvent.DOM_VK_A
        },
        expectedKeypress: true,
        expectedInput: chr,
        expectedValues: {
          key: chr, code: 'KeyA',
          keyCode: KeyboardEvent.DOM_VK_A,
          charCode: chr.charCodeAt(0)
        }
      });
    });
  }

  return promiseQueue;
}

function runSendKeyUSKeyboardSymbolsTests() {
  gTestDescription = 'runSendKeyUSKeyboardSymbolsTests(): ';
  var promiseQueue = Promise.resolve();

  // Test printable symbols on US Keyboard
  var symbols = ' ;:=+,<-_.>/?`~[{\\|]}\'\"';
  var symbolKeyCodes = [
    KeyboardEvent.DOM_VK_SPACE,
    KeyboardEvent.DOM_VK_SEMICOLON,
    KeyboardEvent.DOM_VK_SEMICOLON,
    KeyboardEvent.DOM_VK_EQUALS,
    KeyboardEvent.DOM_VK_EQUALS,
    KeyboardEvent.DOM_VK_COMMA,
    KeyboardEvent.DOM_VK_COMMA,
    KeyboardEvent.DOM_VK_HYPHEN_MINUS,
    KeyboardEvent.DOM_VK_HYPHEN_MINUS,
    KeyboardEvent.DOM_VK_PERIOD,
    KeyboardEvent.DOM_VK_PERIOD,
    KeyboardEvent.DOM_VK_SLASH,
    KeyboardEvent.DOM_VK_SLASH,
    KeyboardEvent.DOM_VK_BACK_QUOTE,
    KeyboardEvent.DOM_VK_BACK_QUOTE,
    KeyboardEvent.DOM_VK_OPEN_BRACKET,
    KeyboardEvent.DOM_VK_OPEN_BRACKET,
    KeyboardEvent.DOM_VK_BACK_SLASH,
    KeyboardEvent.DOM_VK_BACK_SLASH,
    KeyboardEvent.DOM_VK_CLOSE_BRACKET,
    KeyboardEvent.DOM_VK_CLOSE_BRACKET,
    KeyboardEvent.DOM_VK_QUOTE,
    KeyboardEvent.DOM_VK_QUOTE
  ];
  var symbolCodes = [
    'Space',
    'Semicolon',
    'Semicolon',
    'Equal',
    'Equal',
    'Comma',
    'Comma',
    'Minus',
    'Minus',
    'Period',
    'Period',
    'Slash',
    'Slash',
    'Backquote',
    'Backquote',
    'BracketLeft',
    'BracketLeft',
    'Backslash',
    'Backslash',
    'BracketRight',
    'BracketRight',
    'Quote',
    'Quote'
  ];
  for (var i = 0; i < symbols.length; i++) {
    // callbacks in then() are deferred; must only reference these block-scoped
    // variable instead of i.
    let keyCode = symbolKeyCodes[i];
    let chr = symbols.charAt(i);
    let code = symbolCodes[i];

    // Test plain symbol
    promiseQueue = promiseQueue.then(() => {
      return sendKeyAndAssertResult({
        dict: {
          key: chr
        },
        expectedKeypress: true,
        expectedInput: chr,
        expectedValues: {
          key: chr, code: '',
          keyCode: 0,
          charCode: chr.charCodeAt(0)
        }
      });
    });

    // Test plain symbol with keyCode set
    promiseQueue = promiseQueue.then(() => {
      return sendKeyAndAssertResult({
        dict: {
          key: chr,
          keyCode: keyCode
        },
        expectedKeypress: true,
        expectedInput: chr,
        expectedValues: {
          key: chr, code: '',
          keyCode: keyCode,
          charCode: chr.charCodeAt(0)
        }
      });
    });

    // Test plain symbol with code set
    // expects keyCode to be 0.
    promiseQueue = promiseQueue.then(() => {
      return sendKeyAndAssertResult({
        dict: {
          key: chr,
          code: code
        },
        expectedKeypress: true,
        expectedInput: chr,
        expectedValues: {
          key: chr, code: code,
          keyCode: 0,
          charCode: chr.charCodeAt(0)
        }
      });
    });

    // Test plain symbol with code set to KeyA,
    // expects keyCode to be 0.
    promiseQueue = promiseQueue.then(() => {
      return sendKeyAndAssertResult({
        dict: {
          key: chr,
          code: 'KeyA'
        },
        expectedKeypress: true,
        expectedInput: chr,
        expectedValues: {
          key: chr, code: 'KeyA',
          keyCode: 0,
          charCode: chr.charCodeAt(0)
        }
      });
    });

    // Test plain symbol with keyCode set to DOM_VK_A,
    // expects keyCode to follow the keyCode set.
    promiseQueue = promiseQueue.then(() => {
      return sendKeyAndAssertResult({
        dict: {
          key: chr,
          keyCode: KeyboardEvent.DOM_VK_A
        },
        expectedKeypress: true,
        expectedInput: chr,
        expectedValues: {
          key: chr, code: '',
          keyCode: KeyboardEvent.DOM_VK_A,
          charCode: chr.charCodeAt(0)
        }
      });
    });

    // Test plain symbol with code set to KeyA
    // expects keyCode to follow the keyCode set.
    promiseQueue = promiseQueue.then(() => {
      return sendKeyAndAssertResult({
        dict: {
          key: chr,
          code: 'KeyA',
          keyCode: KeyboardEvent.DOM_VK_A
        },
        expectedKeypress: true,
        expectedInput: chr,
        expectedValues: {
          key: chr, code: 'KeyA',
          keyCode: KeyboardEvent.DOM_VK_A,
          charCode: chr.charCodeAt(0)
        }
      });
    });
  }

  return promiseQueue;
}

function runSendKeyGreekLettersTests() {
  gTestDescription = 'runSendKeyGreekLettersTests(): ';
  var promiseQueue = Promise.resolve();

  // Test Greek letters
  var greekLetters =
    '\u0391\u0392\u0393\u0394\u0395\u0396\u0397\u0398\u0399\u039a\u039b\u039c' +
    '\u039d\u039e\u039f\u03a0\u03a1\u03a3\u03a4\u03a5\u03a6\u03a7\u03a8\u03a9' +
    '\u03b1\u03b2\u03b3\u03b4\u03b5\u03b6\u03b7\u03b8\u03b9\u03ba\u03bb\u03bc' +
    '\u03bd\u03be\u03bf\u03c0\u03c1\u03c3\u03c4\u03c5\u03c6\u03c7\u03c8\u03c9' +
    '\u03c2';
  var greekLettersLayoutMap =
    'ABGDEZHUIKLMNJOPRSTYFXCVABGDEZHUIKLMNJOPRSTYFXCVQ';
  for (var i = 0; i < greekLetters.length; i++) {
    // callbacks in then() are deferred; must only reference these block-scoped
    // variable instead of i.
    let keyCode = greekLettersLayoutMap.charCodeAt(i);
    let chr = greekLetters.charAt(i);
    let code = 'Key' + greekLettersLayoutMap.charAt(i);

    // Test plain alphabet
    promiseQueue = promiseQueue.then(() => {
      return sendKeyAndAssertResult({
        dict: {
          key: chr
        },
        expectedKeypress: true,
        expectedInput: chr,
        expectedValues: {
          key: chr, code: '',
          keyCode: 0,
          charCode: chr.charCodeAt(0)
        }
      });
    });

    // Test plain alphabet with keyCode set
    promiseQueue = promiseQueue.then(() => {
      return sendKeyAndAssertResult({
        dict: {
          key: chr,
          keyCode: keyCode
        },
        expectedKeypress: true,
        expectedInput: chr,
        expectedValues: {
          key: chr, code: '',
          keyCode: keyCode,
          charCode: chr.charCodeAt(0)
        }
      });
    });

    // Test plain alphabet with code set,
    // expects keyCode to be 0.
    promiseQueue = promiseQueue.then(() => {
      return sendKeyAndAssertResult({
        dict: {
          key: chr,
          code: code
        },
        expectedKeypress: true,
        expectedInput: chr,
        expectedValues: {
          key: chr, code: code,
          keyCode: 0,
          charCode: chr.charCodeAt(0)
        }
      });
    });

    // Test plain alphabet with code set to Digit1,
    // expects keyCode to be 0.
    promiseQueue = promiseQueue.then(() => {
      return sendKeyAndAssertResult({
        dict: {
          key: chr,
          code: 'Digit1'
        },
        expectedKeypress: true,
        expectedInput: chr,
        expectedValues: {
          key: chr, code: 'Digit1',
          keyCode: 0,
          charCode: chr.charCodeAt(0)
        }
      });
    });

    // Test plain alphabet with code set to Digit1,
    // and keyCode set to DOM_VK_A.
    // expects keyCode to follow the keyCode set.
    promiseQueue = promiseQueue.then(() => {
      return sendKeyAndAssertResult({
        dict: {
          key: chr,
          code: 'Digit1',
          keyCode: KeyboardEvent.DOM_VK_A
        },
        expectedKeypress: true,
        expectedInput: chr,
        expectedValues: {
          key: chr, code: 'Digit1',
          keyCode: KeyboardEvent.DOM_VK_A,
          charCode: chr.charCodeAt(0)
        }
      });
    });
  }

  return promiseQueue;
}

function runSendKeyEnterTests() {
  gTestDescription = 'runSendKeyEnterTests(): ';
  var promiseQueue = Promise.resolve();

  // Test Enter with code unset
  promiseQueue = promiseQueue.then(() => {
    return sendKeyAndAssertResult({
      dict: {
        key: 'Enter'
      },
      expectedKeypress: true,
      expectedInput: '\n',
      expectedValues: {
        key: 'Enter', code: '',
        keyCode: KeyboardEvent.DOM_VK_RETURN,
        charCode: 0
      }
    });
  });

  // Test Enter with code set
  promiseQueue = promiseQueue.then(() => {
    return sendKeyAndAssertResult({
      dict: {
        key: 'Enter',
        code: 'Enter'
      },
      expectedKeypress: true,
      expectedInput: '\n',
      expectedValues: {
        key: 'Enter', code: 'Enter',
        keyCode: KeyboardEvent.DOM_VK_RETURN,
        charCode: 0
      }
    });
  });

  // Test Enter with keyCode explict set to zero
  promiseQueue = promiseQueue.then(() => {
    return sendKeyAndAssertResult({
      dict: {
        key: 'Enter',
        keyCode: 0
      },
      expectedKeypress: true,
      expectedValues: {
        key: 'Enter', code: '',
        keyCode: 0,
        charCode: 0
      }
    });
  });

  return promiseQueue;
}

function runSendKeyNumpadTests() {
  gTestDescription = 'runSendKeyNumpadTests(): ';
  var promiseQueue = Promise.resolve();

  var tests = [];
  ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
    .forEach(function(key) {
      let charCode = key.charCodeAt(0);

      tests.push({
        dict: {
          key: key,
          code: 'Numpad' + key
        },
        expectedKeypress: true,
        expectedInput: key,
        expectedValues: {
          key: key, code: 'Numpad' + key,
          keyCode: charCode, charCode: charCode,
          location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD
        }
      });
    });

  [['+', 'NumpadAdd'],
   [',', 'NumpadComma'],
   ['.', 'NumpadDecimal'],
   ['.', 'NumpadComma'],  // Locale-specific NumpadComma
   [',', 'NumpadDecimal'],  // Locale-specific NumpadDecimal
   ['/', 'NumpadDivide'],
   ['=', 'NumpadEqual'],
   // ['#', 'NumpadHash'], // Not supported yet.
   ['*', 'NumpadMultiply'],
   ['(', 'NumpadParenLeft'],
   [')', 'NumpadParenRight'],
   // ['*', 'NumpadStar'], // Not supported yet.
   ['-', 'NumpadSubtract']].forEach(function([key, code]) {
    tests.push({
      dict: {
        key: key,
        code: code
      },
      expectedKeypress: true,
      expectedInput: key,
      expectedValues: {
        key: key, code: code, keyCode: 0, charCode: key.charCodeAt(0),
        location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD
      }
    });
  });

  [
    'NumpadComma', // Locale-specific NumpadComma -- outputs nothing
    'NumpadClear',
    'NumpadClearEntry',
    'NumpadMemoryAdd',
    'NumpadMemoryClear',
    'NumpadMemoryRecall',
    'NumpadMemoryStore',
    'NumpadMemorySubtract'
  ].forEach(function(code) {
    tests.push({
      dict: {
        key: 'Unidentified',
        code: code
      },
      expectedKeypress: true,
      expectedValues: {
        key: 'Unidentified', code: code, keyCode: 0, charCode: 0,
        location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD
      }
    });
  });

  tests.push({
    dict: {
      key: 'Enter',
      code: 'NumpadEnter'
    },
    expectedKeypress: true,
    expectedInput: '\n',
    expectedValues: {
      key: 'Enter', code: 'NumpadEnter',
      keyCode: KeyboardEvent.DOM_VK_RETURN, charCode: 0,
      location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD
    }
  });

  tests.push({
    dict: {
      key: 'Backspace',
      code: 'NumpadBackspace'
    },
    expectedKeypress: true,
    expectedInput: 'Backspace', // Special value
    expectedValues: {
      key: 'Backspace', code: 'NumpadBackspace',
      keyCode: KeyboardEvent.DOM_VK_BACK_SPACE, charCode: 0,
      location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD
    }
  });

  tests.forEach((test) => {
    promiseQueue = promiseQueue.then(() => {
      return sendKeyAndAssertResult(test);
    });
  });

  return promiseQueue;
}

function runSendKeyRejectionTests() {
  gTestDescription = 'runSendKeyRejectionTests(): ';
  var promiseQueue = Promise.resolve();

  promiseQueue = promiseQueue.then(() => {
    return sendKeyAndAssertResult({
      dict: undefined,
      expectedReject: TypeError
    });
  });

  return promiseQueue;
}

function setCompositionAndAssertResult(testdata) {
  var dict = testdata.dict;
  var testName;
  var promise;

  if (dict) {
    testName = gTestDescription +
      'setComposition(' + testdata.text +
      ', undefined, undefined, '
      + JSON.stringify(dict) + ')';
    promise = navigator.mozInputMethod.inputcontext
      .setComposition(testdata.text, undefined, undefined, dict);
  } else {
    testName = gTestDescription +
      'setComposition(' + testdata.text + ')';
    promise = navigator.mozInputMethod.inputcontext
      .setComposition(testdata.text);
  }

  if (testdata.expectedReject) {
    promise = promise
      .then(() => {
        ok(false, testName + ' should not resolve.');
      }, (e) => {
        ok(true, testName + ' rejects.');
        ok(e instanceof testdata.expectedReject, 'Reject with type.');
      })

    return promise;
  }

  promise = promise
    .then((res) => {
      is(res, true,
        testName + ' should resolve to true.');

      var expectedEventDetail = [];

      var expectedValues = testdata.expectedValues;

      if (testdata.expectsKeyEvents &&
          (testdata.startsComposition ||
           testdata.dispatchKeyboardEventDuringComposition)) {
        expectedEventDetail.push({
          type: 'keydown',
          key: expectedValues.key,
          charCode: 0,
          code: expectedValues.code || '',
          keyCode: expectedValues.keyCode || 0,
          location: 0,
          repeat: expectedValues.repeat || false,
          value: gCurrentValue,
          shift: false,
          capsLock: false,
          control: false,
          alt: false
        });
      }

      if (testdata.startsComposition) {
        expectedEventDetail.push({
          type: 'compositionstart',
          data: '',
          value: gCurrentValue
        });
      }

      expectedEventDetail.push({
        type: 'compositionupdate',
        data: testdata.text,
        value: gCurrentValue
      });

      expectedEventDetail.push({
        type: 'input',
        value: gCurrentValue += testdata.expectedInput
      });

      if (testdata.expectsKeyEvents &&
          testdata.dispatchKeyboardEventDuringComposition) {
        expectedEventDetail.push({
          type: 'keyup',
          key: expectedValues.key,
          charCode: 0,
          code: expectedValues.code || '',
          keyCode: expectedValues.keyCode || 0,
          location: 0,
          repeat: expectedValues.repeat || false,
          value: gCurrentValue,
          shift: false,
          capsLock: false,
          control: false,
          alt: false
        });
      }

      assertEventDetail(expectedEventDetail, testName);
      gEventDetails = [];
    }, (e) => {
      ok(false, testName + ' should not reject. ' + e);
    });

  return promise;
}

function endCompositionAndAssertResult(testdata) {
  var dict = testdata.dict;
  var testName;
  var promise;
  if (dict) {
    testName = gTestDescription +
      'endComposition(' + testdata.text + ', ' + JSON.stringify(dict) + ')';
    promise = navigator.mozInputMethod.inputcontext
      .endComposition(testdata.text, dict);
  } else {
    testName = gTestDescription +
      'endComposition(' + testdata.text + ')';
    promise = navigator.mozInputMethod.inputcontext
      .endComposition(testdata.text);
  }

  if (testdata.expectedReject) {
    promise = promise
      .then(() => {
        ok(false, testName + ' should not resolve.');
      }, (e) => {
        ok(true, testName + ' rejects.');
        ok(e instanceof testdata.expectedReject, 'Reject with type.');
      })

    return promise;
  }

  promise = promise
    .then((res) => {
      is(res, true,
        testName + ' should resolve to true.');

      var expectedEventDetail = [];

      var expectedValues = testdata.expectedValues;

      if (testdata.expectsKeyEvents &&
          testdata.dispatchKeyboardEventDuringComposition) {
        expectedEventDetail.push({
          type: 'keydown',
          key: expectedValues.key,
          charCode: 0,
          code: expectedValues.code || '',
          keyCode: expectedValues.keyCode || 0,
          location: 0,
          repeat: expectedValues.repeat || false,
          value: gCurrentValue,
          shift: false,
          capsLock: false,
          control: false,
          alt: false
        });
      }

      expectedEventDetail.push({
        type: 'compositionend',
        data: testdata.text,
        value: gCurrentValue
      });

      expectedEventDetail.push({
        type: 'input',
        value: gCurrentValue
      });

      if (testdata.expectsKeyEvents) {
        expectedEventDetail.push({
          type: 'keyup',
          key: expectedValues.key,
          charCode: 0,
          code: expectedValues.code || '',
          keyCode: expectedValues.keyCode || 0,
          location: 0,
          repeat: expectedValues.repeat || false,
          value: gCurrentValue,
          shift: false,
          capsLock: false,
          control: false,
          alt: false
        });
      }

      assertEventDetail(expectedEventDetail, testName);
      gEventDetails = [];
    }, (e) => {
      ok(false, testName + ' should not reject. ' + e);
    });

  return promise;
}

function runCompositionWithKeyEventTests() {
  var promiseQueue = Promise.resolve();

  [true, false].forEach((dispatchKeyboardEventDuringComposition) => {
    gTestDescription = 'runCompositionWithKeyEventTests() (dispatchKeyboardEvent =' + dispatchKeyboardEventDuringComposition + '): ';

    promiseQueue = promiseQueue
      .then(() => {
        SpecialPowers.setBoolPref(
          'dom.keyboardevent.dispatch_during_composition',
          dispatchKeyboardEventDuringComposition);
      })
      .then(() => {
        return setCompositionAndAssertResult({
          text: 'foo',
          expectsKeyEvents: true,
          startsComposition: true,
          dispatchKeyboardEventDuringComposition: dispatchKeyboardEventDuringComposition,
          expectedInput: 'foo',
          dict: {
            key: 'a',
            code: 'KeyA',
            keyCode: KeyboardEvent.DOM_VK_A
          },
          expectedValues: {
            key: 'a',
            code: 'KeyA',
            keyCode: KeyboardEvent.DOM_VK_A
          }
        });
      })
      .then(() => {
        return setCompositionAndAssertResult({
          text: 'foobar',
          expectsKeyEvents: true,
          startsComposition: false,
          dispatchKeyboardEventDuringComposition: dispatchKeyboardEventDuringComposition,
          expectedInput: 'bar',
          dict: {
            key: 'a',
            code: 'KeyA',
            keyCode: KeyboardEvent.DOM_VK_A
          },
          expectedValues: {
            key: 'a',
            code: 'KeyA',
            keyCode: KeyboardEvent.DOM_VK_A
          }
        });
      })
      .then(() => {
        return endCompositionAndAssertResult({
          text: 'foobar',
          expectsKeyEvents: true,
          dispatchKeyboardEventDuringComposition: dispatchKeyboardEventDuringComposition,
          expectedInput: '',
          dict: {
            key: 'a',
            code: 'KeyA',
            keyCode: KeyboardEvent.DOM_VK_A
          },
          expectedValues: {
            key: 'a',
            code: 'KeyA',
            keyCode: KeyboardEvent.DOM_VK_A
          }
        });
      })
      .then(() => {
        SpecialPowers.clearUserPref(
          'dom.keyboardevent.dispatch_during_composition');
      });
  });

  return promiseQueue;
}

function runCompositionWithoutKeyEventTests() {
  var promiseQueue = Promise.resolve();

  gTestDescription = 'runCompositionWithoutKeyEventTests(): ';

  promiseQueue = promiseQueue
    .then(() => {
      return setCompositionAndAssertResult({
        text: 'foo',
        expectsKeyEvents: false,
        startsComposition: true,
        expectedInput: 'foo'
      });
    })
    .then(() => {
      return setCompositionAndAssertResult({
        text: 'foobar',
        expectsKeyEvents: false,
        startsComposition: false,
        expectedInput: 'bar'
      });
    })
    .then(() => {
      return endCompositionAndAssertResult({
        text: 'foobar',
        expectsKeyEvents: false,
        expectedInput: ''
      });
    });

  return promiseQueue;
}

function keydownAndAssertResult(testdata) {
  var dict = testdata.dict;
  var testName = gTestDescription + 'keydown(' + JSON.stringify(dict) + ')';
  var promise = navigator.mozInputMethod.inputcontext.keydown(dict);

  if (testdata.expectedReject) {
    promise = promise
      .then(() => {
        ok(false, testName + ' should not resolve.');
      }, (e) => {
        ok(true, testName + ' rejects.');
        ok(e instanceof testdata.expectedReject, 'Reject with type.');
      })

    return promise;
  }

  promise = promise
    .then((res) => {
      is(res, true,
        testName + ' should resolve to true.');

      var expectedEventDetail = [];

      var expectedValues = testdata.expectedValues;

      expectedEventDetail.push({
        type: 'keydown',
        key: expectedValues.key,
        charCode: 0,
        code: expectedValues.code || '',
        keyCode: expectedValues.keyCode || 0,
        location: 0,
        repeat: expectedValues.repeat || false,
        value: gCurrentValue,
        shift: false,
        capsLock: false,
        control: false,
        alt: false
      });

      if (testdata.expectedKeypress) {
        expectedEventDetail.push({
          type: 'keypress',
          key: expectedValues.key,
          charCode: expectedValues.charCode,
          code: expectedValues.code || '',
          keyCode: expectedValues.charCode ? 0 : expectedValues.keyCode,
          location: 0,
          repeat: expectedValues.repeat || false,
          value: gCurrentValue,
          shift: false,
          capsLock: false,
          control: false,
          alt: false
        });
      }

      if (testdata.expectedInput) {
        expectedEventDetail.push({
          type: 'input',
          value: gCurrentValue += testdata.expectedInput
        });
      }

      assertEventDetail(expectedEventDetail, testName);
      gEventDetails = [];
    }, (e) => {
      ok(false, testName + ' should not reject. ' + e);
    });

  return promise;
}

function keyupAndAssertResult(testdata) {
  var dict = testdata.dict;
  var testName = gTestDescription + 'keyup(' + JSON.stringify(dict) + ')';
  var promise = navigator.mozInputMethod.inputcontext.keyup(dict);

  if (testdata.expectedReject) {
    promise = promise
      .then(() => {
        ok(false, testName + ' should not resolve.');
      }, (e) => {
        ok(true, testName + ' rejects.');
        ok(e instanceof testdata.expectedReject, 'Reject with type.');
      })

    return promise;
  }

  promise = promise
    .then((res) => {
      is(res, true,
        testName + ' should resolve to true.');

      var expectedEventDetail = [];

      var expectedValues = testdata.expectedValues;

      expectedEventDetail.push({
        type: 'keyup',
        key: expectedValues.key,
        charCode: 0,
        code: expectedValues.code || '',
        keyCode: expectedValues.keyCode || 0,
        location: 0,
        repeat: expectedValues.repeat || false,
        value: gCurrentValue,
        shift: false,
        capsLock: false,
        control: false,
        alt: false
      });

      assertEventDetail(expectedEventDetail, testName);
      gEventDetails = [];
    }, (e) => {
      ok(false, testName + ' should not reject. ' + e);
    });

  return promise;
}

function runKeyDownUpTests() {
  gTestDescription = 'runKeyDownUpTests(): ';
  var promiseQueue = Promise.resolve();

  let chr = 'a';
  let code = 'KeyA';
  let keyCode = KeyboardEvent.DOM_VK_A;

  promiseQueue = promiseQueue
    .then(() => {
      return keydownAndAssertResult({
        dict: {
          key: chr,
          code: code,
          keyCode: keyCode
        },
        expectedKeypress: true,
        expectedInput: chr,
        expectedValues: {
          key: chr, code: code,
          keyCode: keyCode,
          charCode: chr.charCodeAt(0)
        }
      });
    })
    .then(() => {
      return keyupAndAssertResult({
        dict: {
          key: chr,
          code: code,
          keyCode: keyCode
        },
        expectedValues: {
          key: chr, code: code,
          keyCode: keyCode,
          charCode: chr.charCodeAt(0)
        }
      });
    });

  return promiseQueue;
}

function runKeyDownUpRejectionTests() {
  gTestDescription = 'runKeyDownUpRejectionTests(): ';
  var promiseQueue = Promise.resolve();

  promiseQueue = promiseQueue.then(() => {
    return keydownAndAssertResult({
      dict: undefined,
      expectedReject: TypeError
    });
  });

  promiseQueue = promiseQueue.then(() => {
    return keyupAndAssertResult({
      dict: undefined,
      expectedReject: TypeError
    });
  });

  return promiseQueue;
}

function runRepeatTests() {
  gTestDescription = 'runRepeatTests(): ';
  var promiseQueue = Promise.resolve();

  // Test repeat
  promiseQueue = promiseQueue
    .then(() => {
      return sendKeyAndAssertResult({
        dict: {
          key: 'A',
          repeat: true
        },
        expectedKeypress: true,
        expectedRepeat: true,
        expectedInput: 'A',
        expectedValues: {
          repeat: true,
          key: 'A', code: '',
          keyCode: KeyboardEvent.DOM_VK_A,
          charCode: 'A'.charCodeAt(0)
        }
      });
    })
    .then(() => {
      return keyupAndAssertResult({
        dict: {
          key: 'A'
        },
        expectedKeypress: true,
        expectedRepeat: true,
        expectedInput: 'A',
        expectedValues: {
          key: 'A', code: '',
          keyCode: KeyboardEvent.DOM_VK_A,
          charCode: 'A'.charCodeAt(0)
        }
      });
    });

  return promiseQueue;
}

function runTest() {
  let im = navigator.mozInputMethod;

  // Set current page as an input method.
  SpecialPowers.wrap(im).setActive(true);

  let iframe = document.createElement('iframe');
  iframe.src = 'file_test_bug1137557.html';
  iframe.setAttribute('mozbrowser', true);
  document.body.appendChild(iframe);

  let mm = SpecialPowers.getBrowserFrameMessageManager(iframe);

  iframe.addEventListener('mozbrowserloadend', function() {
    mm.addMessageListener('test:eventDetail', function(msg) {
      gEventDetails.push(msg.data);
    });
    mm.loadFrameScript('data:,(' + encodeURIComponent(appFrameScript.toString()) + ')();', false);
  });

  waitForInputContextChange()
    .then(() => {
      var inputcontext = navigator.mozInputMethod.inputcontext;

      ok(!!inputcontext, 'Receving the first input context');
    })
    .then(() => runSendKeyAlphabetTests())
    .then(() => runSendKeyNumberTests())
    .then(() => runSendKeyDvorakTests())
    .then(() => runSendKeyDigitKeySymbolsTests())
    .then(() => runSendKeyUSKeyboardSymbolsTests())
    .then(() => runSendKeyGreekLettersTests())
    .then(() => runSendKeyEnterTests())
    .then(() => runSendKeyNumpadTests())
    .then(() => runSendKeyRejectionTests())
    .then(() => runCompositionWithKeyEventTests())
    .then(() => runCompositionWithoutKeyEventTests())
    .then(() => runKeyDownUpTests())
    .then(() => runKeyDownUpRejectionTests())
    .then(() => runRepeatTests())
    .catch((err) => {
      console.error(err);
      is(false, err.message);
    })
    .then(() => {
      var p = waitForInputContextChange();

      // Revoke our right from using the IM API.
      SpecialPowers.wrap(im).setActive(false);

      return p;
    })
    .then(() => {
      var inputcontext = navigator.mozInputMethod.inputcontext;

      is(inputcontext, null, 'Receving null input context');

      inputmethod_cleanup();
    })
    .catch((err) => {
      console.error(err);
      is(false, err.message);
    });
}

</script>
</pre>
</body>
</html>