Quellcodebibliothek Statistik Leitseite products/sources/formale Sprachen/C/Firefox/widget/tests/   (Browser von der Mozilla Stiftung Version 136.0.1©)  Datei vom 10.2.2025 mit Größe 503 kB image not shown  

Quelle  window_composition_text_querycontent.xhtml   Sprache: unbekannt

 
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
                 type="text/css"?>
<window title="Testing composition, text and query content events"
  xmlns:html="http://www.w3.org/1999/xhtml"
  xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">

  <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
  <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js" />
  <script src="chrome://mochikit/content/tests/SimpleTest/WindowSnapshot.js" />

  <panel id="panel" hidden="true" orient="vertical">
    <vbox id="vbox">
      <html:textarea id="textbox" cols="20" rows="4" style="font-size: 36px;"/>
    </vbox>
  </panel>

<body  xmlns="http://www.w3.org/1999/xhtml">
<div id="display">
<div id="div" style="margin: 0; padding: 0; font-size: 36px;">Here is a text frame.</div>
<textarea style="margin: 0; font-family: -moz-fixed;" id="textarea" cols="20" rows="4"></textarea><br/>
<iframe id="iframe" width="300" height="150"
        src="data:text/html,<textarea id='textarea' cols='20' rows='4'></textarea>"></iframe><br/>
<iframe id="iframe2" width="300" height="150"
        src="data:text/html,<body onload='document.designMode=%22on%22'>body content</body>"></iframe><br/>
<iframe id="iframe3" width="300" height="150"
        src="data:text/html,<body onload='document.designMode=%22on%22'>body content</body>"></iframe><br/>
<iframe id="iframe4" width="300" height="150"
        src="data:text/html,<div contenteditable id='contenteditable'></div>"></iframe><br/>
<!--
  NOTE: the width for the next two iframes is chosen to be small enough to make
  the Show Password button (for type=password) be outside the viewport so that
  it doesn't affect the rendering compared to the type=text control.
  But still large enough to comfortably fit the input values we test.
-->
<iframe id="iframe5" style="width:10ch" height="50" src="data:text/html,<input id='input'>"></iframe>
<iframe id="iframe6" style="width:10ch" height="50" src="data:text/html,<input id='password' type='password'>"></iframe><br/>
<iframe id="iframe7" width="300" height="150"
        src="data:text/html,<span contenteditable id='contenteditable'></span>"></iframe><br/>
<input id="input" type="text"/><br/>
<input id="password" type="password"/><br/>
</div>
<div id="content" style="display: none">

</div>
<pre id="test">
</pre>
</body>

<script class="testbody" type="application/javascript">
<![CDATA[

function ok(aCondition, aMessage)
{
  window.arguments[0].SimpleTest.ok(aCondition, aMessage);
}

function is(aLeft, aRight, aMessage)
{
  window.arguments[0].SimpleTest.is(aLeft, aRight, aMessage);
}

function isnot(aLeft, aRight, aMessage)
{
  window.arguments[0].SimpleTest.isnot(aLeft, aRight, aMessage);
}

function isfuzzy(aLeft, aRight, aEpsilon, aMessage) {
  window.arguments[0].SimpleTest.isfuzzy(aLeft, aRight, aEpsilon, aMessage);
}

function todo(aCondition, aMessage)
{
  window.arguments[0].SimpleTest.todo(aCondition, aMessage);
}

function todo_is(aLeft, aRight, aMessage)
{
  window.arguments[0].SimpleTest.todo_is(aLeft, aRight, aMessage);
}

function todo_isnot(aLeft, aRight, aMessage)
{
  window.arguments[0].SimpleTest.todo_isnot(aLeft, aRight, aMessage);
}

function isSimilarTo(aLeft, aRight, aAllowedDifference, aMessage)
{
  if (Math.abs(aLeft - aRight) <= aAllowedDifference) {
    ok(true, aMessage);
  } else {
    ok(false, aMessage + ", got=" + aLeft + ", expected=" + (aRight - aAllowedDifference) + "~" + (aRight + aAllowedDifference));
  }
}

function isGreaterThan(aLeft, aRight, aMessage)
{
  ok(aLeft > aRight, aMessage + ", got=" + aLeft + ", expected minimum value=" + aRight);
}

/**
 * synthesizeSimpleCompositionChange synthesizes a composition which has only
 * one clause and put caret end of it.
 *
 * @param aComposition  string or object.  If string, it's treated as
 *                      composition string whose attribute is
 *                      COMPOSITION_ATTR_RAW_CLAUSE.
 *                      If object, it must have .string whose type is "string".
 *                      Additionally, .attr can be specified if you'd like to
 *                      use the other attribute instead of
 *                      COMPOSITION_ATTR_RAW_CLAUSE.
 */
function synthesizeSimpleCompositionChange(aComposition, aWindow, aCallback) {
  const comp = (() => {
    if (typeof aComposition == "string") {
      return { string: aComposition, attr: COMPOSITION_ATTR_RAW_CLAUSE };
    }
    return {
      string: aComposition.string,
      attr: aComposition.attr === undefined
        ? COMPOSITION_ATTR_RAW_CLAUSE
        : aComposition.attr
    };
  })();
  synthesizeCompositionChange(
    {
      composition: {
        string: comp.string,
        clauses: [
          { length: comp.string.length, attr: comp.attr },
        ],
      },
      caret: { start: comp.string.length, length: 0 },
    },
    aWindow,
    aCallback
  );
}


var div = document.getElementById("div");
var textarea = document.getElementById("textarea");
var panel = document.getElementById("panel");
var textbox = document.getElementById("textbox");
var iframe = document.getElementById("iframe");
var iframe2 = document.getElementById("iframe2");
var iframe3 = document.getElementById("iframe3");
var contenteditable;
var windowOfContenteditable;
var contenteditableBySpan;
var windowOfContenteditableBySpan;
var input = document.getElementById("input");
var password = document.getElementById("password");
var textareaInFrame;

const nsITextInputProcessorCallback = Ci.nsITextInputProcessorCallback;
const nsIInterfaceRequestor = Ci.nsIInterfaceRequestor;
const nsIWebNavigation = Ci.nsIWebNavigation;
const nsIDocShell = Ci.nsIDocShell;
const { AppConstants } = ChromeUtils.importESModule(
  "resource://gre/modules/AppConstants.sys.mjs"
);
const kExpectInputBeforeCompositionEnd = SpecialPowers.getBoolPref("dom.input_events.dispatch_before_compositionend");

function waitForTick() {
  return new Promise(resolve => { SimpleTest.executeSoon(resolve); });
}

async function waitForEventLoops(aTimes)
{
  for (let i = 1; i < aTimes; i++) {
    await waitForTick();
  }
  await new Promise(resolve => { setTimeout(resolve, 20); });
}

function getEditor(aNode)
{
  return aNode.editor;
}

function getHTMLEditorIMESupport(aWindow)
{
  return aWindow.docShell.editor;
}

const kIsWin = (navigator.platform.indexOf("Win") == 0);
const kIsMac = (navigator.platform.indexOf("Mac") == 0);

const kLFLen = (kIsWin && false) ? 2 : 1;
const kLF = (kIsWin && false) ? "\r\n" : "\n";

function checkQueryContentResult(aResult, aMessage)
{
  ok(aResult, aMessage + ": the result is null");
  if (!aResult) {
    return false;
  }
  ok(aResult.succeeded, aMessage + ": the query content failed");
  return aResult.succeeded;
}

function checkContent(aExpectedText, aMessage, aID)
{
  if (!aID) {
    aID = "";
  }
  let textContent = synthesizeQueryTextContent(0, 100);
  if (!checkQueryContentResult(textContent, aMessage +
                               ": synthesizeQueryTextContent " + aID)) {
    return false;
  }
  is(textContent.text, aExpectedText,
     aMessage + ": composition string is wrong " + aID);
  return textContent.text == aExpectedText;
}

function checkContentRelativeToSelection(aRelativeOffset, aLength, aExpectedOffset, aExpectedText, aMessage, aID)
{
  if (!aID) {
    aID = "";
  }
  aMessage += " (aRelativeOffset=" + aRelativeOffset + "): "
  let textContent = synthesizeQueryTextContent(aRelativeOffset, aLength, true);
  if (!checkQueryContentResult(textContent, aMessage +
                               "synthesizeQueryTextContent " + aID)) {
    return false;
  }
  is(textContent.offset, aExpectedOffset,
     aMessage + "offset is wrong " + aID);
  is(textContent.text, aExpectedText,
     aMessage + "text is wrong " + aID);
  return textContent.offset == aExpectedOffset &&
         textContent.text == aExpectedText;
}

function checkSelection(aExpectedOffset, aExpectedText, aMessage, aID)
{
  if (!aID) {
    aID = "";
  }
  let selectedText = synthesizeQuerySelectedText();
  if (!checkQueryContentResult(selectedText, aMessage +
                               ": synthesizeQuerySelectedText " + aID)) {
    return false;
  }
  if (aExpectedOffset === null) {
    is(
      selectedText.notFound,
      true,
      `${aMessage}: selection should not be found ${aID}`
    );
    return selectedText.notFound;
  }

  is(
    selectedText.notFound,
    false,
    `${aMessage}: selection should be found ${aID}`
  );
  if (selectedText.notFound) {
    return false;
  }
  is(
    selectedText.offset,
    aExpectedOffset,
    `${aMessage}: selection offset should be ${aExpectedOffset} ${aID}`
  );
  is(
    selectedText.text,
    aExpectedText,
    `${aMessage}: selected text should be "${aExpectedText}" ${aID}`
  );
  return selectedText.offset == aExpectedOffset &&
         selectedText.text == aExpectedText;
}

function checkIMESelection(
  aSelectionType,
  aExpectedFound,
  aExpectedOffset,
  aExpectedText,
  aMessage,
  aID,
  aToDo = {}
) {
  if (!aID) {
    aID = "";
  }
  aMessage += " (" + aSelectionType + ")";
  let {
    notFound = is,
    offset = is,
    text = is,
  } = aToDo;
  let selectionType = 0;
  switch (aSelectionType) {
    case "RawClause":
      selectionType = QUERY_CONTENT_FLAG_SELECTION_IME_RAWINPUT;
      break;
    case "SelectedRawClause":
      selectionType = QUERY_CONTENT_FLAG_SELECTION_IME_SELECTEDRAWTEXT;
      break;
    case "ConvertedClause":
      selectionType = QUERY_CONTENT_FLAG_SELECTION_IME_CONVERTEDTEXT;
      break;
    case "SelectedClause":
      selectionType = QUERY_CONTENT_FLAG_SELECTION_IME_SELECTEDCONVERTEDTEXT;
      break;
    default:
      ok(false, aMessage + ": invalid selection type, " + aSelectionType);
  }
  isnot(selectionType, 0, aMessage + ": wrong value");
  let selectedText = synthesizeQuerySelectedText(selectionType);
  if (!checkQueryContentResult(selectedText, aMessage +
                               ": synthesizeQuerySelectedText " + aID)) {
    return false;
  }
  notFound(
    selectedText.notFound,
    !aExpectedFound,
    `${aMessage}: selection should ${
      aExpectedFound ? "" : "not"
    } be found ${aID}`);
  if (selectedText.notFound) {
    return selectedText.notFound == !aExpectedFound;
  }

  offset(
    selectedText.offset,
    aExpectedOffset,
    `${aMessage}: selection offset is wrong ${aID}`
  );
  text(
    selectedText.text,
    aExpectedText,
    `${aMessage}: selected text is wrong ${aID}`
  );
  return selectedText.offset == aExpectedOffset &&
         selectedText.text == aExpectedText;
}

function checkRect(aRect, aExpectedRect, aMessage)
{
  is(aRect.left, aExpectedRect.left, aMessage + ": left is wrong");
  is(aRect.top, aExpectedRect.top, aMessage + " top is wrong");
  is(aRect.width, aExpectedRect.width, aMessage + ": width is wrong");
  is(aRect.height, aExpectedRect.height, aMessage + ": height is wrong");
  return aRect.left == aExpectedRect.left &&
         aRect.top == aExpectedRect.top &&
         aRect.width == aExpectedRect.width &&
         aRect.height == aExpectedRect.height;
}

function checkRectFuzzy(aRect, aExpectedRect, aEpsilon, aMessage) {
  isfuzzy(aRect.left, aExpectedRect.left, aEpsilon.left, aMessage + ": left is wrong");
  isfuzzy(aRect.top, aExpectedRect.top, aEpsilon.top, aMessage + " top is wrong");
  isfuzzy(aRect.width, aExpectedRect.width, aEpsilon.width, aMessage + ": width is wrong");
  isfuzzy(aRect.height, aExpectedRect.height, aEpsilon.height, aMessage + ": height is wrong");
  return (aRect.left >= aExpectedRect.left - aEpsilon.left &&
          aRect.left <= aExpectedRect.left + aEpsilon.left) &&
         (aRect.top >= aExpectedRect.top - aEpsilon.top &&
          aRect.top <= aExpectedRect.top + aEpsilon.top) &&
         (aRect.width >= aExpectedRect.width - aEpsilon.width &&
          aRect.width <= aExpectedRect.width + aEpsilon.width) &&
         (aRect.height >= aExpectedRect.height - aEpsilon.height &&
          aRect.height <= aExpectedRect.height + aEpsilon.height);
}

function getRectArray(aQueryTextRectArrayResult) {
  let rects = [];
  for (let i = 0; ; i++) {
    let rect = { left: {}, top: {}, width: {}, height: {} };
    try {
      aQueryTextRectArrayResult.getCharacterRect(i, rect.left, rect.top, rect.width, rect.height);
    } catch (e) {
      break;
    }
    rects.push({
      left: rect.left.value,
      top: rect.top.value,
      width: rect.width.value,
      height: rect.height.value,
    });
  }
  return rects;
}

function checkRectArray(aQueryTextRectArrayResult, aExpectedTextRectArray, aMessage)
{
  for (let i = 1; i < aExpectedTextRectArray.length; ++i) {
    let rect = { left: {}, top: {}, width: {}, height: {} };
    try {
      aQueryTextRectArrayResult.getCharacterRect(i, rect.left, rect.top, rect.width, rect.height);
    } catch (e) {
      ok(false, aMessage + ": failed to retrieve " + i + "th rect (" + e + ")");
      return false;
    }
    function toRect(aRect)
    {
      return { left: aRect.left.value, top: aRect.top.value, width: aRect.width.value, height: aRect.height.value };
    }
    if (!checkRect(toRect(rect), aExpectedTextRectArray[i], aMessage + " " + i + "th rect")) {
      return false;
    }
  }
  return true;
}

function checkRectContainsRect(aRect, aContainer, aMessage)
{
  let container = { left: Math.ceil(aContainer.left),
                    top:  Math.ceil(aContainer.top),
                    width: Math.floor(aContainer.width),
                    height: Math.floor(aContainer.height) };

  let ret = container.left <= aRect.left &&
            container.top <= aRect.top &&
            container.left + container.width >= aRect.left + aRect.width &&
            container.top + container.height >= aRect.top + aRect.height;
  ret = ret && aMessage;
  ok(ret, aMessage + " container={ left=" + container.left + ", top=" +
     container.top + ", width=" + container.width + ", height=" +
     container.height + " } rect={ left=" + aRect.left + ", top=" + aRect.top +
     ", width=" + aRect.width + ", height=" + aRect.height + " }");
  return ret;
}

// eslint-disable-next-line complexity
function runUndoRedoTest()
{
  textarea.value = "";
  textarea.focus();

  // input raw characters
  synthesizeCompositionChange(
    { "composition":
      { "string": "\u306D",
        "clauses":
        [
          { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
        ]
      },
      "caret": { "start": 1, "length": 0 },
      "key": { key: "," },
    });

  synthesizeCompositionChange(
    { "composition":
      { "string": "\u306D\u3053",
        "clauses":
        [
          { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
        ]
      },
      "caret": { "start": 2, "length": 0 },
      "key": { key: "b" },
    });

  // convert
  synthesizeCompositionChange(
    { "composition":
      { "string": "\u732B",
        "clauses":
        [
          { "length": 1,
            "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }
        ]
      },
      "caret": { "start": 1, "length": 0 },
      "key": { key: " " },
    });

  // commit
  synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } });

  // input raw characters
  synthesizeCompositionChange(
    { "composition":
      { "string": "\u307E",
        "clauses":
        [
          { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
        ]
      },
      "caret": { "start": 1, "length": 0 },
      "key": { key: "j" },
    });

  // cancel the composition
  synthesizeComposition({ type: "compositioncommit", data: "", key: { key: "KEY_Escape" } });

  // input raw characters
  synthesizeCompositionChange(
    { "composition":
      { "string": "\u3080",
        "clauses":
        [
          { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
        ]
      },
      "caret": { "start": 1, "length": 0 },
      "key": { key: "]" },
    });

  synthesizeCompositionChange(
    { "composition":
      { "string": "\u3080\u3059",
        "clauses":
        [
          { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
        ]
      },
      "caret": { "start": 2, "length": 0 },
      "key": { key: "r" },
    });

  synthesizeCompositionChange(
    { "composition":
      { "string": "\u3080\u3059\u3081",
        "clauses":
        [
          { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
        ]
      },
      "caret": { "start": 3, "length": 0 },
      "key": { key: "/" },
    });

  // convert
  synthesizeCompositionChange(
    { "composition":
      { "string": "\u5A18",
        "clauses":
        [
          { "length": 1,
            "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }
        ]
      },
      "caret": { "start": 1, "length": 0 },
      "key": { key: " " },
    });

  // commit
  synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter" } });

  sendString(" meant");
  synthesizeKey("KEY_Backspace");
  synthesizeKey("s \"cat-girl\". She is a ");

  // input raw characters
  synthesizeCompositionChange(
    { "composition":
      { "string": "\u3088",
        "clauses":
        [
          { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
        ]
      },
      "caret": { "start": 1, "length": 0 },
      "key": { key: "9" },
    });

  synthesizeCompositionChange(
    { "composition":
      { "string": "\u3088\u3046",
        "clauses":
        [
          { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
        ]
      },
      "caret": { "start": 2, "length": 0 },
      "key": { key: "4" },
    });

  synthesizeCompositionChange(
    { "composition":
      { "string": "\u3088\u3046\u304b",
        "clauses":
        [
          { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
        ]
      },
      "caret": { "start": 3, "length": 0 },
      "key": { key: "t" },
    });

  synthesizeCompositionChange(
    { "composition":
      { "string": "\u3088\u3046\u304b\u3044",
        "clauses":
        [
          { "length": 4, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
        ]
      },
      "caret": { "start": 4, "length": 0 },
      "key": { key: "e" },
    });

  // convert
  synthesizeCompositionChange(
    { "composition":
      { "string": "\u5996\u602a",
        "clauses":
        [
          { "length": 2, "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }
        ]
      },
      "caret": { "start": 2, "length": 0 },
      "key": { key: " " },
    });

  // commit
  synthesizeComposition({ type: "compositioncommitasis", key: { key: "Enter" } });

  synthesizeKey("KEY_Backspace", {repeat: 12});

  let i = 0;
  if (!checkContent("\u732B\u5A18 means \"cat-girl\".",
                    "runUndoRedoTest", "#" + ++i) ||
      !checkSelection(20, "", "runUndoRedoTest", "#" + i)) {
    return;
  }

  synthesizeKey("Z", {accelKey: true});

  if (!checkContent("\u732B\u5A18 means \"cat-girl\". She is a \u5996\u602A",
                    "runUndoRedoTest", "#" + ++i) ||
      !checkSelection(32, "", "runUndoRedoTest", "#" + i)) {
    return;
  }

  synthesizeKey("Z", {accelKey: true});

  if (!checkContent("\u732B\u5A18 means \"cat-girl\". She is a ",
                    "runUndoRedoTest", "#" + ++i) ||
      !checkSelection(30, "", "runUndoRedoTest", "#" + i)) {
    return;
  }

  synthesizeKey("Z", {accelKey: true});

  if (!checkContent("\u732B\u5A18 mean",
                    "runUndoRedoTest", "#" + ++i) ||
      !checkSelection(7, "", "runUndoRedoTest", "#" + i)) {
    return;
  }

  synthesizeKey("Z", {accelKey: true});

  if (!checkContent("\u732B\u5A18 meant",
                    "runUndoRedoTest", "#" + ++i) ||
      !checkSelection(8, "", "runUndoRedoTest", "#" + i)) {
    return;
  }

  synthesizeKey("Z", {accelKey: true});

  if (!checkContent("\u732B\u5A18",
                    "runUndoRedoTest", "#" + ++i) ||
      !checkSelection(2, "", "runUndoRedoTest", "#" + i)) {
    return;
  }

  synthesizeKey("Z", {accelKey: true});

  if (!checkContent("\u732B",
                    "runUndoRedoTest", "#" + ++i) ||
      !checkSelection(1, "", "runUndoRedoTest", "#" + i)) {
    return;
  }

  synthesizeKey("Z", {accelKey: true});

  // XXX this is unexpected behavior, see bug 258291
  if (!checkContent("\u732B",
                    "runUndoRedoTest", "#" + ++i) ||
      !checkSelection(1, "", "runUndoRedoTest", "#" + i)) {
    return;
  }

  synthesizeKey("Z", {accelKey: true});

  if (!checkContent("",
                    "runUndoRedoTest", "#" + ++i) ||
      !checkSelection(0, "", "runUndoRedoTest", "#" + i)) {
    return;
  }

  synthesizeKey("Z", {accelKey: true});

  if (!checkContent("",
                    "runUndoRedoTest", "#" + ++i) ||
      !checkSelection(0, "", "runUndoRedoTest", "#" + i)) {
    return;
  }

  synthesizeKey("Z", {accelKey: true, shiftKey: true});

  if (!checkContent("\u732B",
                    "runUndoRedoTest", "#" + ++i) ||
      !checkSelection(1, "", "runUndoRedoTest", "#" + i)) {
    return;
  }

  synthesizeKey("Z", {accelKey: true, shiftKey: true});

  // XXX this is unexpected behavior, see bug 258291
  if (!checkContent("\u732B",
                    "runUndoRedoTest", "#" + ++i) ||
      !checkSelection(1, "", "runUndoRedoTest", "#" + i)) {
    return;
  }

  synthesizeKey("Z", {accelKey: true, shiftKey: true});

  if (!checkContent("\u732B\u5A18",
                    "runUndoRedoTest", "#" + ++i) ||
      !checkSelection(2, "", "runUndoRedoTest", "#" + i)) {
    return;
  }

  synthesizeKey("Z", {accelKey: true, shiftKey: true});

  if (!checkContent("\u732B\u5A18 meant",
                    "runUndoRedoTest", "#" + ++i) ||
      !checkSelection(8, "", "runUndoRedoTest", "#" + i)) {
    return;
  }

  synthesizeKey("Z", {accelKey: true, shiftKey: true});

  if (!checkContent("\u732B\u5A18 mean",
                    "runUndoRedoTest", "#" + ++i) ||
      !checkSelection(7, "", "runUndoRedoTest", "#" + i)) {
    return;
  }

  synthesizeKey("Z", {accelKey: true, shiftKey: true});

  if (!checkContent("\u732B\u5A18 means \"cat-girl\". She is a ",
                    "runUndoRedoTest", "#" + ++i) ||
      !checkSelection(30, "", "runUndoRedoTest", "#" + i)) {
    return;
  }

  synthesizeKey("Z", {accelKey: true, shiftKey: true});

  if (!checkContent("\u732B\u5A18 means \"cat-girl\". She is a \u5996\u602A",
                    "runUndoRedoTest", "#" + ++i) ||
      !checkSelection(32, "", "runUndoRedoTest", "#" + i)) {
    return;
  }

  synthesizeKey("Z", {accelKey: true, shiftKey: true});

  if (!checkContent("\u732B\u5A18 means \"cat-girl\".",
                    "runUndoRedoTest", "#" + ++i) ||
      !checkSelection(20, "", "runUndoRedoTest", "#" + i)) {
    return;
  }

  synthesizeKey("Z", {accelKey: true, shiftKey: true});

  if (!checkContent("\u732B\u5A18 means \"cat-girl\".",
                    "runUndoRedoTest", "#" + ++i) ||
      !checkSelection(20, "", "runUndoRedoTest", "#" + i)) {
    // eslint-disable-next-line no-useless-return
    return;
  }
}

function checkInputEvent(aEvent, aIsComposing, aInputType, aData, aTargetRanges, aDescription) {
  if (aEvent.type !== "input" && aEvent.type !== "beforeinput") {
    throw new Error(`${aDescription}: "${aEvent.type}" is not InputEvent`);
  }
  ok(InputEvent.isInstance(aEvent), `"${aEvent.type}" event should be dispatched with InputEvent interface: ${aDescription}`);
  let cancelable = aEvent.type === "beforeinput" &&
                   aInputType !== "insertCompositionText" &&
                   aInputType !== "deleteCompositionText";
  is(aEvent.cancelable, cancelable, `"${aEvent.type}" event should ${cancelable ? "be" : "be never"} cancelable: ${aDescription}`);
  is(aEvent.bubbles, true, `"${aEvent.type}" event should always bubble: ${aDescription}`);
  is(aEvent.isComposing, aIsComposing, `isComposing of "${aEvent.type}" event should be ${aIsComposing}: ${aDescription}`);
  is(aEvent.inputType, aInputType, `inputType of "${aEvent.type}" event should be "${aInputType}": ${aDescription}`);
  is(aEvent.data, aData, `data of "${aEvent.type}" event should be ${aData}: ${aDescription}`);
  is(aEvent.dataTransfer, null, `dataTransfer of "${aEvent.type}" event should be null: ${aDescription}`);
  let targetRanges = aEvent.getTargetRanges();
  if (aTargetRanges.length === 0) {
    is(targetRanges.length, 0,
       `getTargetRange() of "${aEvent.type}" event should return empty array: ${aDescription}`);
  } else {
    is(targetRanges.length, aTargetRanges.length,
       `getTargetRange() of "${aEvent.type}" event should return static range array: ${aDescription}`);
    if (targetRanges.length == aTargetRanges.length) {
      for (let i = 0; i < targetRanges.length; i++) {
        is(targetRanges[i].startContainer, aTargetRanges[i].startContainer,
           `startContainer of getTargetRanges()[${i}] of "${aEvent.type}" event does not match: ${aDescription}`);
        is(targetRanges[i].startOffset, aTargetRanges[i].startOffset,
           `startOffset of getTargetRanges()[${i}] of "${aEvent.type}" event does not match: ${aDescription}`);
        is(targetRanges[i].endContainer, aTargetRanges[i].endContainer,
           `endContainer of getTargetRanges()[${i}] of "${aEvent.type}" event does not match: ${aDescription}`);
        is(targetRanges[i].endOffset, aTargetRanges[i].endOffset,
           `endOffset of getTargetRanges()[${i}] of "${aEvent.type}" event does not match: ${aDescription}`);
      }
    }
  }
}

function checkTextInputEvent(aEvent, aData, aDescription) {
  if (aEvent.type !== "textInput") {
    throw new Error(`${aDescription}: "${aEvent.type}" is not TextEvent`);
  }
  ok(TextEvent.isInstance(aEvent), `"${aEvent.type}" event should be dispatched with TextEvent interface: ${aDescription}`);
  is(aEvent.cancelable, true, `"${aEvent.type}" event should be cancelable: ${aDescription}`);
  is(aEvent.bubbles, true, `"${aEvent.type}" event should always bubble: ${aDescription}`);
  is(aEvent.data, aData, `data of "${aEvent.type}" event should be ${aData}: ${aDescription}`);
}

function runCompositionCommitAsIsTest()
{
  textarea.focus();

  let result = [];
  function clearResult()
  {
    result = [];
  }

  function handler(aEvent)
  {
    result.push(aEvent);
  }

  textarea.addEventListener("compositionupdate", handler, true);
  textarea.addEventListener("compositionend", handler, true);
  textarea.addEventListener("beforeinput", handler, true);
  textarea.addEventListener("textInput", handler, true);
  textarea.addEventListener("input", handler, true);
  textarea.addEventListener("text", handler, true);

  // compositioncommitasis with composing string.
  (() => {
    textarea.value = "";
    synthesizeCompositionChange(
      { "composition":
        { "string": "\u3042",
          "clauses":
          [
            { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
          ]
        },
        "caret": { "start": 1, "length": 0 },
        "key": { key: "a" },
      });
    is(textarea.value, "\u3042", "runCompositionCommitAsIsTest: textarea doesn't have composition string #1");

    is(result.findIndex(value => value.type == "textInput"), -1,
      "runCompositionCommitAsIsTest: no textInput event should be fired before commit #1");

    clearResult();
    synthesizeComposition({ type: "compositioncommitasis", key: { key: "Enter" } });

    is(result.length, kExpectInputBeforeCompositionEnd ? 6 : 5,
      `runCompositionCommitAsIsTest: ${kExpectInputBeforeCompositionEnd ? 6 : 5} events should be fired after dispatching compositioncommitasis #1`);
    let index = -1;
    is(result[++index].type, "text",
      "runCompositionCommitAsIsTest: text should be fired after dispatching compositioncommitasis because it's dispatched when there is composing string #1");
    is(result[++index].type, "beforeinput",
      "runCompositionCommitAsIsTest: beforeinput should be fired after dispatching compositioncommitasis because it's dispatched when there is composing string #1");
    checkInputEvent(result[index], true, "insertCompositionText", "\u3042", [],
                    "runCompositionCommitAsIsTest: after dispatching compositioncommitasis #1");
    is(result[++index].type, "textInput",
      "runCompositionCommitAsIsText: textInput should be fired after dispatching compositioncommitasis because it's after the last beforeinput for the composition #1");
    checkTextInputEvent(result[index], "\u3042",
                        "runCompositionCommitAsIsText: after dispatching compositioncommitasis #1");
    if (kExpectInputBeforeCompositionEnd) {
      is(result[++index].type, "input",
        "runCompositionCommitAsIsTest: input should be fired after dispatching compositioncommitasis #1");
      checkInputEvent(result[index], true, "insertCompositionText", "\u3042", [],
                      "runCompositionCommitAsIsTest: after dispatching compositioncommitasis #1");
    }
    is(result[++index].type, "compositionend",
      "runCompositionCommitAsIsTest: compositionend should be fired after dispatching compositioncommitasis #1");
    is(result[++index].type, "input",
      "runCompositionCommitAsIsTest: input should be fired after dispatching compositioncommitasis #1");
    checkInputEvent(result[index], false, "insertCompositionText", "\u3042", [],
                    "runCompositionCommitAsIsTest: after dispatching compositioncommitasis #1");
    is(textarea.value, "\u3042", "runCompositionCommitAsIsTest: textarea doesn't have committed string #1");
  })();

  // compositioncommitasis with committed string.
  textarea.value = "";
  synthesizeCompositionChange(
    { "composition":
      { "string": "\u3042",
        "clauses":
        [
          { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
        ]
      },
      "caret": { "start": 1, "length": 0 },
      "key": { key: "a" },
    });
  is(textarea.value, "\u3042", "runCompositionCommitAsIsTest: textarea doesn't have composition string #2");
  synthesizeCompositionChange(
    { "composition":
      { "string": "\u3042",
        "clauses":
        [
          { "length": 0, "attr": 0 }
        ]
      },
      "caret": { "start": 1, "length": 0 },
      "key": { key: "KEY_Enter", type: "keydown" },
    });
  is(textarea.value, "\u3042", "runCompositionCommitAsIsTest: textarea doesn't have committed string #2");
  isnot(result.findIndex(value => value.type == "textInput"), -1,
        "runCompositionCommitAsIsTest: a textInput event should be fired before commit #2");

  clearResult();
  synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter", type: "keyup" } });

  is(result.length, 2,
     "runCompositionCommitAsIsTest: 2 events should be fired after dispatching compositioncommitasis #2");
  // XXX Do we need a "beforeinput" event here? Not sure.
  is(result[0].type, "compositionend",
     "runCompositionCommitAsIsTest: compositionend should be fired after dispatching compositioncommitasis #2");
  is(result[1].type, "input",
     "runCompositionCommitAsIsTest: input should be fired after dispatching compositioncommitasis #2");
  checkInputEvent(result[1], false, "insertCompositionText", "\u3042", [],
                  "runCompositionCommitAsIsTest: after dispatching compositioncommitasis #2");
  is(textarea.value, "\u3042", "runCompositionCommitAsIsTest: textarea doesn't have committed string #2");

  // compositioncommitasis with committed string.
  textarea.value = "";
  synthesizeCompositionChange(
    { "composition":
      { "string": "\u3042",
        "clauses":
        [
          { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
        ]
      },
      "caret": { "start": 1, "length": 0 },
      "key": { key: "a" },
    });
  is(textarea.value, "\u3042", "runCompositionCommitAsIsTest: textarea doesn't have composition string #3");
  synthesizeCompositionChange(
    { "composition":
      { "string": "",
        "clauses":
        [
          { "length": 0, "attr": 0 }
        ]
      },
      "caret": { "start": 0, "length": 0 },
      "key": { key: "KEY_Escape", type: "keydown" },
    });
  is(textarea.value, "", "runCompositionCommitAsIsTest: textarea has non-empty composition string #3");
  todo_isnot(result.findIndex(value => value.type == "textInput"), -1,
             "runCompositionCommitAsIsTest: a textInput event should be fired immediately before commit #3");

  clearResult();
  synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Escape", type: "keyup" } });

  is(result.length, 2,
     "runCompositionCommitAsIsTest: 2 events should be fired after dispatching compositioncommitasis #3");
  // XXX Do we need a "beforeinput" event here? Not sure.
  is(result[0].type, "compositionend",
     "runCompositionCommitAsIsTest: compositionend shouldn't be fired after dispatching compositioncommitasis #3");
  is(result[1].type, "input",
     "runCompositionCommitAsIsTest: input should be fired after dispatching compositioncommitasis #3");
  checkInputEvent(result[1], false, "insertCompositionText", "", [],
                  "runCompositionCommitAsIsTest: after dispatching compositioncommitasis #3");
  is(textarea.value, "", "runCompositionCommitAsIsTest: textarea doesn't have committed string #3");

  textarea.removeEventListener("compositionupdate", handler, true);
  textarea.removeEventListener("compositionend", handler, true);
  textarea.removeEventListener("beforeinput", handler, true);
  textarea.removeEventListener("textInput", handler, true);
  textarea.removeEventListener("input", handler, true);
  textarea.removeEventListener("text", handler, true);
}

function runCompositionCommitTest()
{
  textarea.focus();

  let result = [];
  function clearResult()
  {
    result = [];
  }

  function handler(aEvent)
  {
    result.push(aEvent);
  }

  textarea.addEventListener("compositionupdate", handler, true);
  textarea.addEventListener("compositionend", handler, true);
  textarea.addEventListener("beforeinput", handler, true);
  textarea.addEventListener("textInput", handler, true);
  textarea.addEventListener("input", handler, true);
  textarea.addEventListener("text", handler, true);

  // compositioncommit with different composing string.
  (() => {
    textarea.value = "";
    synthesizeCompositionChange(
      { "composition":
        { "string": "\u3042",
          "clauses":
          [
            { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
          ]
        },
        "caret": { "start": 1, "length": 0 },
        "key": { key: "a", type: "keydown" },
      });
    is(textarea.value, "\u3042", "runCompositionCommitTest: textarea doesn't have composition string #1");
    is(result.findIndex(value => value.type == "textInput"), -1,
      "runCompositionCommitTest: no textInput event should be fired before commit #1");

    clearResult();
    synthesizeComposition({ type: "compositioncommit", data: "\u3043", key: { key: "a", type: "keyup" } });

    is(result.length, kExpectInputBeforeCompositionEnd ? 7 : 6,
      `runCompositionCommitTest: ${kExpectInputBeforeCompositionEnd ? 7 : 6} events should be fired after dispatching compositioncommit #1`);
    let index = -1;
    is(result[++index].type, "compositionupdate",
      "runCompositionCommitTest: compositionupdate should be fired after dispatching compositioncommit because it's dispatched when there is composing string #1");
    is(result[++index].type, "text",
      "runCompositionCommitTest: text should be fired after dispatching compositioncommit #1");
    is(result[++index].type, "beforeinput",
      "runCompositionCommitTest: beforeinput should be fired after dispatching compositioncommit because it's dispatched when there is composing string #1");
    checkInputEvent(result[index], true, "insertCompositionText", "\u3043", [],
                    "runCompositionCommitTest: after dispatching compositioncommit #1");
    is(result[++index].type, "textInput",
      "runCompositionCommitTest: textInput should be fired after dispatching compositioncommit because the preceding beforeinput is last one for the composition #1");
    checkTextInputEvent(result[index], "\u3043",
                        "runCompositionCommitTest: after dispatching compositioncommit #1");
    if (kExpectInputBeforeCompositionEnd) {
      is(result[++index].type, "input",
        "runCompositionCommitTest: input should be fired after dispatching compositioncommit #1");
      checkInputEvent(result[index], true, "insertCompositionText", "\u3043", [],
                      "runCompositionCommitTest: after dispatching compositioncommit #1");
    }
    is(result[++index].type, "compositionend",
      "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #1");
    is(result[++index].type, "input",
      "runCompositionCommitTest: input should be fired after dispatching compositioncommit #1");
    checkInputEvent(result[index], false, "insertCompositionText", "\u3043", [],
                    "runCompositionCommitTest: after dispatching compositioncommit #1");
    is(textarea.value, "\u3043", "runCompositionCommitTest: textarea doesn't have committed string #1");
  })();

  // compositioncommit with different committed string when there is already committed string
  (() => {
    textarea.value = "";
    clearResult();
    synthesizeCompositionChange(
      { "composition":
        { "string": "\u3042",
          "clauses":
          [
            { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
          ]
        },
        "caret": { "start": 1, "length": 0 },
        "key": { key: "a" },
      });
    is(textarea.value, "\u3042", "runCompositionCommitTest: textarea doesn't have composition string #2");
    synthesizeCompositionChange(
      { "composition":
        { "string": "\u3042",
          "clauses":
          [
            { "length": 0, "attr": 0 }
          ]
        },
        "caret": { "start": 1, "length": 0 },
        "key": { key: "KEY_Enter", type: "keydown" },
      });
    is(textarea.value, "\u3042", "runCompositionCommitTest: textarea doesn't have committed string #2");
    is(result.findIndex(value => value.type == "textInput"), -1,
      "runCompositionCommitTest: no textInput event should be fired before commit #2");

    clearResult();
    synthesizeComposition({ type: "compositioncommit", data: "\u3043", key: { key: "KEY_Enter", type: "keyup" } });

    is(result.length, kExpectInputBeforeCompositionEnd ? 7 : 6,
      `runCompositionCommitTest: ${kExpectInputBeforeCompositionEnd ? 7 : 6} events should be fired after dispatching compositioncommit #2`);
    let index = -1;
    is(result[++index].type, "compositionupdate",
      "runCompositionCommitTest: compositionupdate should be fired after dispatching compositioncommit #2");
    is(result[++index].type, "text",
      "runCompositionCommitTest: text should be fired after dispatching compositioncommit #2");
    is(result[++index].type, "beforeinput",
      "runCompositionCommitTest: beforeinput should be fired after dispatching compositioncommit #2");
    checkInputEvent(result[index], true, "insertCompositionText", "\u3043", [],
                    "runCompositionCommitTest: after dispatching compositioncommit #2");
    is(result[++index].type, "textInput",
      "runCompositionCommitTest: textInput should be fired after dispatching compositioncommit #2");
    checkTextInputEvent(result[index], "\u3043",
                    "runCompositionCommitTest: after dispatching compositioncommit #2");
    if (kExpectInputBeforeCompositionEnd) {
      is(result[++index].type, "input",
        "runCompositionCommitTest: input should be fired after dispatching compositioncommit #2");
      checkInputEvent(result[index], true, "insertCompositionText", "\u3043", [],
                      "runCompositionCommitTest: after dispatching compositioncommit #2");
    }
    is(result[++index].type, "compositionend",
      "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #2");
    is(result[++index].type, "input",
      "runCompositionCommitTest: input should be fired after dispatching compositioncommit #2");
    checkInputEvent(result[index], false, "insertCompositionText", "\u3043", [],
                    "runCompositionCommitTest: after dispatching compositioncommit #2");
    is(textarea.value, "\u3043", "runCompositionCommitTest: textarea doesn't have committed string #2");
  })();

  // compositioncommit with empty composition string.
  (() => {
    textarea.value = "";
    clearResult();
    synthesizeCompositionChange(
      { "composition":
        { "string": "\u3042",
          "clauses":
          [
            { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
          ]
        },
        "caret": { "start": 1, "length": 0 },
        "key": { key: "a" },
      });
    is(textarea.value, "\u3042", "runCompositionCommitTest: textarea doesn't have composition string #3");
    synthesizeCompositionChange(
      { "composition":
        { "string": "",
          "clauses":
          [
            { "length": 0, "attr": 0 }
          ]
        },
        "caret": { "start": 0, "length": 0 },
        "key": { key: "KEY_Enter", type: "keydown" },
      });
    is(textarea.value, "", "runCompositionCommitTest: textarea has non-empty composition string #3");
    is(result.findIndex(value => value.type == "textInput"), -1,
      "runCompositionCommitTest: no textInput event should be fired before commit #3");

    clearResult();
    synthesizeComposition({ type: "compositioncommit", data: "\u3043", key: { key: "KEY_Enter", type: "keyup" } });

    is(result.length, kExpectInputBeforeCompositionEnd ? 7 : 6,
      `runCompositionCommitTest: ${kExpectInputBeforeCompositionEnd ? 7 : 6} events should be fired after dispatching compositioncommit #3`);
    let index = -1;
    is(result[++index].type, "compositionupdate",
      "runCompositionCommitTest: compositionupdate should be fired after dispatching compositioncommit #3");
    is(result[++index].type, "text",
      "runCompositionCommitTest: text should be fired after dispatching compositioncommit #3");
    is(result[++index].type, "beforeinput",
      "runCompositionCommitTest: beforeinput should be fired after dispatching compositioncommit #3");
    checkInputEvent(result[index], true, "insertCompositionText", "\u3043", [],
                    "runCompositionCommitTest: after dispatching compositioncommit #3");
    is(result[++index].type, "textInput",
      "runCompositionCommitTest: textInput should be fired after dispatching compositioncommit #3");
    checkTextInputEvent(result[index], "\u3043",
                        "runCompositionCommitTest: after dispatching compositioncommit #3");
    if (kExpectInputBeforeCompositionEnd) {
      is(result[++index].type, "input",
        "runCompositionCommitTest: input should be fired after dispatching compositioncommit #3");
      checkInputEvent(result[index], true, "insertCompositionText", "\u3043", [],
                      "runCompositionCommitTest: after dispatching compositioncommit #3");
    }
    is(result[++index].type, "compositionend",
      "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #3");
    is(result[++index].type, "input",
      "runCompositionCommitTest: input should be fired after dispatching compositioncommit #3");
    checkInputEvent(result[index], false, "insertCompositionText", "\u3043", [],
                    "runCompositionCommitTest: after dispatching compositioncommit #3");
    is(textarea.value, "\u3043", "runCompositionCommitTest: textarea doesn't have committed string #3");
  })();

  // inserting empty string with simple composition.
  (() => {
    textarea.value = "abc";
    textarea.setSelectionRange(3, 3);
    synthesizeComposition({ type: "compositionstart" });

    clearResult();
    synthesizeComposition({ type: "compositioncommit", data: "" });

    is(result.length, kExpectInputBeforeCompositionEnd ? 6 : 5,
      `runCompositionCommitTest: ${kExpectInputBeforeCompositionEnd ? 6 : 5} events should be fired when inserting empty string with composition`);
    let index = -1;
    is(result[++index].type, "text",
      "runCompositionCommitTest: text should be fired when inserting empty string with composition");
    is(result[++index].type, "beforeinput",
      "runCompositionCommitTest: beforeinput should be fired when inserting empty string with composition");
    checkInputEvent(result[index], true, "insertCompositionText", "", [],
                    "runCompositionCommitTest: when inserting empty string with composition");
    is(result[++index].type, "textInput",
      "runCompositionCommitTest: textInput should be fired when inserting empty string with composition");
    checkTextInputEvent(result[index], "",
                        "runCompositionCommitTest: when inserting empty string with composition");
    if (kExpectInputBeforeCompositionEnd) {
      is(result[++index].type, "input",
        "runCompositionCommitTest: input should be fired when inserting empty string with composition");
      checkInputEvent(result[index], true, "insertCompositionText", "", [],
                      "runCompositionCommitTest: when inserting empty string with composition");
    }
    is(result[++index].type, "compositionend",
      "runCompositionCommitTest: compositionend should be fired when inserting empty string with composition");
    is(result[++index].type, "input",
      "runCompositionCommitTest: input should be fired when inserting empty string with composition");
    checkInputEvent(result[index], false, "insertCompositionText", "", [],
                    "runCompositionCommitTest: when inserting empty string with composition");
    is(textarea.value, "abc",
      "runCompositionCommitTest: textarea should keep original value when inserting empty string with composition");
  })();

  // replacing selection with empty string with simple composition.
  (() => {
    textarea.value = "abc";
    textarea.setSelectionRange(0, 3);
    synthesizeComposition({ type: "compositionstart" });

    clearResult();
    synthesizeComposition({ type: "compositioncommit", data: "" });

    is(result.length, kExpectInputBeforeCompositionEnd ? 6 : 5,
      `runCompositionCommitTest: ${kExpectInputBeforeCompositionEnd ? 6 : 5} events should be fired when replacing with empty string with composition`);
    let index = -1;
    is(result[++index].type, "text",
      "runCompositionCommitTest: text should be fired when replacing with empty string with composition");
    is(result[++index].type, "beforeinput",
      "runCompositionCommitTest: beforeinput should be fired when replacing with empty string with composition");
    checkInputEvent(result[index], true, "insertCompositionText", "", [],
                    "runCompositionCommitTest: when replacing with empty string with composition");
    is(result[++index].type, "textInput",
      "runCompositionCommitTest: textInput should be fired when replacing with empty string with composition");
    checkTextInputEvent(result[index], "",
                        "runCompositionCommitTest: when replacing with empty string with composition");
    if (kExpectInputBeforeCompositionEnd) {
      is(result[++index].type, "input",
        "runCompositionCommitTest: input should be fired when replacing with empty string with composition");
      checkInputEvent(result[index], true, "insertCompositionText", "", [],
                      "runCompositionCommitTest: when replacing with empty string with composition");
    }
    is(result[++index].type, "compositionend",
      "runCompositionCommitTest: compositionend should be fired when replacing with empty string with composition");
    is(result[++index].type, "input",
      "runCompositionCommitTest: input should be fired when replacing with empty string with composition");
    checkInputEvent(result[index], false, "insertCompositionText", "", [],
                    "runCompositionCommitTest: when replacing with empty string with composition");
    is(textarea.value, "",
      "runCompositionCommitTest: textarea should become empty when replacing selection with empty string with composition");
  })();

  // replacing selection with same string with simple composition.
  (() => {
    textarea.value = "abc";
    textarea.setSelectionRange(0, 3);
    synthesizeComposition({ type: "compositionstart" });

    clearResult();
    synthesizeComposition({ type: "compositioncommit", data: "abc" });

    is(result.length, kExpectInputBeforeCompositionEnd ? 7 : 6,
      `runCompositionCommitTest: ${
        kExpectInputBeforeCompositionEnd ? 7 : 6
      } events should be fired when replacing selection with same string with composition`);
    let index = -1;
    is(result[++index].type, "compositionupdate",
      "runCompositionCommitTest: compositionupdate should be fired when replacing selection with same string with composition");
    is(result[++index].type, "text",
      "runCompositionCommitTest: text should be fired when replacing selection with same string with composition");
    is(result[++index].type, "beforeinput",
      "runCompositionCommitTest: beforeinput should be fired when replacing selection with same string with composition");
    checkInputEvent(result[index], true, "insertCompositionText", "abc", [],
                    "runCompositionCommitTest: when replacing selection with same string with composition");
    is(result[++index].type, "textInput",
      "runCompositionCommitTest: textInput should be fired when replacing selection with same string with composition");
    checkTextInputEvent(result[index], "abc",
                        "runCompositionCommitTest: when replacing selection with same string with composition");
    if (kExpectInputBeforeCompositionEnd) {
      is(result[++index].type, "input",
        "runCompositionCommitTest: input should be fired when replacing selection with same string with composition");
      checkInputEvent(result[index], true, "insertCompositionText", "abc", [],
                      "runCompositionCommitTest: when replacing selection with same string with composition");
    }
    is(result[++index].type, "compositionend",
      "runCompositionCommitTest: compositionend should be fired when replacing selection with same string with composition");
    is(result[++index].type, "input",
      "runCompositionCommitTest: input should be fired when replacing selection with same string with composition");
    checkInputEvent(result[index], false, "insertCompositionText", "abc", [],
                    "runCompositionCommitTest: when replacing selection with same string with composition");
    is(textarea.value, "abc",
      "runCompositionCommitTest: textarea should keep same value when replacing selection with same string with composition");
  })();

  // compositioncommit with non-empty composition string.
  (() => {
    textarea.value = "";
    synthesizeCompositionChange(
      { "composition":
        { "string": "\u3042",
          "clauses":
          [
            { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
          ]
        },
        "caret": { "start": 1, "length": 0 },
        "key": { key: "a" },
      });
    is(textarea.value, "\u3042", "runCompositionCommitTest: textarea doesn't have composition string #4");

    clearResult();
    synthesizeComposition({ type: "compositioncommit", data: "", key: { key: "KEY_Enter" } });

    is(result.length, kExpectInputBeforeCompositionEnd ? 7 : 6,
      `runCompositionCommitTest: ${kExpectInputBeforeCompositionEnd ? 7 : 6} events should be fired after dispatching compositioncommit #4`);
    let index = -1;
    is(result[++index].type, "compositionupdate",
      "runCompositionCommitTest: compositionupdate should be fired after dispatching compositioncommit #4");
    is(result[++index].type, "text",
      "runCompositionCommitTest: text should be fired after dispatching compositioncommit #4");
    is(result[++index].type, "beforeinput",
      "runCompositionCommitTest: beforeinput should be fired after dispatching compositioncommit #4");
    checkInputEvent(result[index], true, "insertCompositionText", "", [],
                    "runCompositionCommitTest: after dispatching compositioncommit #4");
    is(result[++index].type, "textInput",
      "runCompositionCommitTest: textInput should be fired after dispatching compositioncommit #4");
    checkTextInputEvent(result[index], "",
                        "runCompositionCommitTest: after dispatching compositioncommit #4");
    if (kExpectInputBeforeCompositionEnd) {
      is(result[++index].type, "input",
        "runCompositionCommitTest: input should be fired after dispatching compositioncommit #4");
      checkInputEvent(result[index], true, "insertCompositionText", "", [],
                      "runCompositionCommitTest: after dispatching compositioncommit #4");
    }
    is(result[++index].type, "compositionend",
      "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #4");
    is(result[++index].type, "input",
      "runCompositionCommitTest: input should be fired after dispatching compositioncommit #4");
    checkInputEvent(result[index], false, "insertCompositionText", "", [],
                    "runCompositionCommitTest: after dispatching compositioncommit #4");
    is(textarea.value, "", "runCompositionCommitTest: textarea should be empty #4");
  })();

  // compositioncommit immediately without compositionstart
  (() => {
    textarea.value = "";

    clearResult();
    synthesizeComposition({ type: "compositioncommit", data: "\u3042", key: { key: "a" } });

    is(result.length, kExpectInputBeforeCompositionEnd ? 7 : 6,
      `runCompositionCommitTest: ${kExpectInputBeforeCompositionEnd ? 7 : 6} events should be fired after dispatching compositioncommit #5`);
    let index = -1;
    is(result[++index].type, "compositionupdate",
      "runCompositionCommitTest: compositionupdate should be fired after dispatching compositioncommit #5");
    is(result[++index].type, "text",
      "runCompositionCommitTest: text should be fired after dispatching compositioncommit #5");
    is(result[++index].type, "beforeinput",
      "runCompositionCommitTest: beforeinput should be fired after dispatching compositioncommit #5");
    checkInputEvent(result[index], true, "insertCompositionText", "\u3042", [],
                    "runCompositionCommitTest: after dispatching compositioncommit #5");
    is(result[++index].type, "textInput",
      "runCompositionCommitTest: textInput should be fired after dispatching compositioncommit #5");
    checkTextInputEvent(result[index], "\u3042",
                        "runCompositionCommitTest: after dispatching compositioncommit #5");
    if (kExpectInputBeforeCompositionEnd) {
      is(result[++index].type, "input",
        "runCompositionCommitTest: input should be fired after dispatching compositioncommit #5");
      checkInputEvent(result[index], true, "insertCompositionText", "\u3042", [],
                      "runCompositionCommitTest: after dispatching compositioncommit #5");
    }
    is(result[++index].type, "compositionend",
      "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #5");
    is(result[++index].type, "input",
      "runCompositionCommitTest: input should be fired after dispatching compositioncommit #5");
    checkInputEvent(result[index], false, "insertCompositionText", "\u3042", [],
                    "runCompositionCommitTest: after dispatching compositioncommit #5");
    is(textarea.value, "\u3042", "runCompositionCommitTest: textarea should be empty #5");
  })();

  // compositioncommit with same composition string.
  (() => {
    textarea.value = "";
    synthesizeCompositionChange(
      { "composition":
        { "string": "\u3042",
          "clauses":
          [
            { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
          ]
        },
        "caret": { "start": 1, "length": 0 },
        "key": { key: "a" },
      });
    is(textarea.value, "\u3042", "runCompositionCommitTest: textarea doesn't have composition string #5");

    clearResult();
    synthesizeComposition({ type: "compositioncommit", data: "\u3042", key: { key: "KEY_Enter" } });

    is(result.length, kExpectInputBeforeCompositionEnd ? 6 : 5,
      `runCompositionCommitTest: ${kExpectInputBeforeCompositionEnd ? 6 : 5} events should be fired after dispatching compositioncommit #6`);
    let index = -1;
    is(result[++index].type, "text",
      "runCompositionCommitTest: text should be fired after dispatching compositioncommit #6");
    is(result[++index].type, "beforeinput",
      "runCompositionCommitTest: beforeinput should be fired after dispatching compositioncommit #6");
    checkInputEvent(result[index], true, "insertCompositionText", "\u3042", [],
                    "runCompositionCommitTest: after dispatching compositioncommit #6");
    is(result[++index].type, "textInput",
      "runCompositionCommitTest: textInput should be fired after dispatching compositioncommit #6");
    checkTextInputEvent(result[index], "\u3042",
                        "runCompositionCommitTest: after dispatching compositioncommit #6");
    if (kExpectInputBeforeCompositionEnd) {
      is(result[++index].type, "input",
        "runCompositionCommitTest: input should be fired after dispatching compositioncommit #6");
      checkInputEvent(result[index], true, "insertCompositionText", "\u3042", [],
                      "runCompositionCommitTest: after dispatching compositioncommit #6");
    }
    is(result[++index].type, "compositionend",
      "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #6");
    is(result[++index].type, "input",
      "runCompositionCommitTest: input should be fired after dispatching compositioncommit #6");
    checkInputEvent(result[index], false, "insertCompositionText", "\u3042", [],
                    "runCompositionCommitTest: after dispatching compositioncommit #6");
    is(textarea.value, "\u3042", "runCompositionCommitTest: textarea should have committed string #6");
  })();

  // compositioncommit with same composition string when there is committed string
  textarea.value = "";
  clearResult();
  synthesizeCompositionChange(
    { "composition":
      { "string": "\u3042",
        "clauses":
        [
          { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
        ]
      },
      "caret": { "start": 1, "length": 0 },
      "key": { key: "a" },
    });
  is(textarea.value, "\u3042", "runCompositionCommitTest: textarea doesn't have composition string #6");

  synthesizeCompositionChange(
    { "composition":
      { "string": "\u3042",
        "clauses":
        [
          { "length": 0, "attr": 0 }
        ]
      },
      "caret": { "start": 1, "length": 0 },
      "key": { key: "KEY_Enter", type: "keydown" },
    });
  is(textarea.value, "\u3042", "runCompositionCommitTest: textarea doesn't have composition string #6");
  todo_isnot(result.findIndex(value => value.type == "textInput"), -1,
             "runCompositionCommitTest: a textInput event should be fired before immediately commit #6");

  clearResult();
  synthesizeComposition({ type: "compositioncommit", data: "\u3042", key: { key: "KEY_Enter", type: "keyup" } });

  is(result.length, 2,
     "runCompositionCommitTest: 2 events should be fired after dispatching compositioncommit #7");
  // XXX Do we need a "beforeinput" event here? Not sure.
  is(result[0].type, "compositionend",
     "runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #7");
  is(result[1].type, "input",
     "runCompositionCommitTest: input should be fired after dispatching compositioncommit #7");
  checkInputEvent(result[1], false, "insertCompositionText", "\u3042", [],
                  "runCompositionCommitTest: after dispatching compositioncommit #7");
  is(textarea.value, "\u3042", "runCompositionCommitTest: textarea should have committed string #6");

  textarea.removeEventListener("compositionupdate", handler, true);
  textarea.removeEventListener("compositionend", handler, true);
  textarea.removeEventListener("beforeinput", handler, true);
  textarea.removeEventListener("textInput", handler, true);
  textarea.removeEventListener("input", handler, true);
  textarea.removeEventListener("text", handler, true);
}

// eslint-disable-next-line complexity
async function runCompositionTest()
{
  textarea.value = "";
  textarea.focus();
  let caretRects = [];

  let caretRect = synthesizeQueryCaretRect(0);
  if (!checkQueryContentResult(caretRect,
        "runCompositionTest: synthesizeQueryCaretRect #0")) {
    return;
  }
  caretRects[0] = caretRect;

  // input first character
  synthesizeCompositionChange(
    { "composition":
      { "string": "\u3089",
        "clauses":
        [
          { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
        ]
      },
      "caret": { "start": 1, "length": 0 },
      "key": { key: "o" },
    });

  if (!checkContent("\u3089", "runCompositionTest", "#1-1") ||
      !checkSelection(1, "", "runCompositionTest", "#1-1")) {
    return;
  }

  caretRect = synthesizeQueryCaretRect(1);
  if (!checkQueryContentResult(caretRect,
        "runCompositionTest: synthesizeQueryCaretRect #1-1")) {
    return;
  }
  caretRects[1] = caretRect;

  // input second character
  synthesizeCompositionChange(
    { "composition":
      { "string": "\u3089\u30FC",
        "clauses":
        [
          { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
        ]
      },
      "caret": { "start": 2, "length": 0 },
      "key": { key: "\\", code: "IntlYen", keyCode: KeyboardEvent.DOM_VK_BACKSLASH },
    });

  if (!checkContent("\u3089\u30FC", "runCompositionTest", "#1-2") ||
      !checkSelection(2, "", "runCompositionTest", "#1-2")) {
    return;
  }

  caretRect = synthesizeQueryCaretRect(2);
  if (!checkQueryContentResult(caretRect,
        "runCompositionTest: synthesizeQueryCaretRect #1-2")) {
    return;
  }
  caretRects[2] = caretRect;

  isnot(caretRects[2].left, caretRects[1].left,
        "runCompositionTest: caret isn't moved (#1-2)");
  is(caretRects[2].top, caretRects[1].top,
     "runCompositionTest: caret is moved to another line (#1-2)");
  is(caretRects[2].width, caretRects[1].width,
     "runCompositionTest: caret width is wrong (#1-2)");
  is(caretRects[2].height, caretRects[1].height,
     "runCompositionTest: caret width is wrong (#1-2)");

  // input third character
  synthesizeCompositionChange(
    { "composition":
      { "string": "\u3089\u30FC\u3081",
        "clauses":
        [
          { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
        ]
      },
      "caret": { "start": 3, "length": 0 },
      "key": { key: "/" },
    });

  if (!checkContent("\u3089\u30FC\u3081", "runCompositionTest", "#1-3") ||
      !checkSelection(3, "", "runCompositionTest", "#1-3")) {
    return;
  }

  caretRect = synthesizeQueryCaretRect(3);
  if (!checkQueryContentResult(caretRect,
        "runCompositionTest: synthesizeQueryCaretRect #1-3")) {
    return;
  }
  caretRects[3] = caretRect;

  isnot(caretRects[3].left, caretRects[2].left,
        "runCompositionTest: caret isn't moved (#1-3)");
  is(caretRects[3].top, caretRects[2].top,
     "runCompositionTest: caret is moved to another line (#1-3)");
  is(caretRects[3].width, caretRects[2].width,
     "runCompositionTest: caret width is wrong (#1-3)");
  is(caretRects[3].height, caretRects[2].height,
     "runCompositionTest: caret height is wrong (#1-3)");

  // moves the caret left
  synthesizeCompositionChange(
    { "composition":
      { "string": "\u3089\u30FC\u3081",
        "clauses":
        [
          { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
        ]
      },
      "caret": { "start": 2, "length": 0 },
      "key": { key: "KEY_ArrowLeft" },
    });

  if (!checkContent("\u3089\u30FC\u3081", "runCompositionTest", "#1-3-1") ||
      !checkSelection(2, "", "runCompositionTest", "#1-3-1")) {
    return;
  }


  caretRect = synthesizeQueryCaretRect(2);
  if (!checkQueryContentResult(caretRect,
        "runCompositionTest: synthesizeQueryCaretRect #1-3-1")) {
    return;
  }

  is(caretRect.left, caretRects[2].left,
     "runCompositionTest: caret rects are different (#1-3-1, left)");
  is(caretRect.top, caretRects[2].top,
     "runCompositionTest: caret rects are different (#1-3-1, top)");
  // by bug 335359, the caret width depends on the right side's character.
  is(caretRect.width, caretRects[2].width + Math.round(window.devicePixelRatio),
     "runCompositionTest: caret rects are different (#1-3-1, width)");
  is(caretRect.height, caretRects[2].height,
     "runCompositionTest: caret rects are different (#1-3-1, height)");

  // moves the caret left
  synthesizeCompositionChange(
    { "composition":
      { "string": "\u3089\u30FC\u3081",
        "clauses":
        [
          { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
        ]
      },
      "caret": { "start": 1, "length": 0 },
      "key": { key: "KEY_ArrowLeft" },
    });

  if (!checkContent("\u3089\u30FC\u3081", "runCompositionTest", "#1-3-2") ||
--> --------------------

--> maximum size reached

--> --------------------

[ Dauer der Verarbeitung: 0.142 Sekunden  (vorverarbeitet)  ]