<html>

<head>
  <title>Text selection testing</title>

  <link rel="stylesheet" type="text/css"
        href="chrome://mochikit/content/tests/SimpleTest/test.css" />

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

  <script type="application/javascript"
          src="../common.js"></script>
  <script type="application/javascript"
          src="../promisified-events.js"></script>

  <script type="application/javascript">
    /**
     * Helper function to test selection bounds.
     * @param {string} aID  The ID to test.
     * @param {nsIAccessibleText} acc  The accessible to test.
     * @param {int} index  The selection's index to test.
     * @param {array} offsets  The start and end offset to test against.
     * @param {string} msgStart  The start of the message to return in test
     *                           messages.
     */
    function testSelectionBounds(aID, acc, index, offsets, msgStart) {
      const [expectedStart, expectedEnd] = offsets;
      const startOffset = {}, endOffset = {};
      acc.getSelectionBounds(index, startOffset, endOffset);

      is(startOffset.value, Math.min(expectedStart, expectedEnd),
         msgStart + ": Wrong start offset for " + aID);
      is(endOffset.value, Math.max(expectedStart, expectedEnd),
         msgStart + ": Wrong end offset for " + aID);
    }

    /**
     * Test adding selections to accessibles.
     * @param {string} aID  The ID of the element to test.
     * @param {array} aSelections  Array of selection start and end indices.
     */
    async function addSelections(aID, aSelections) {
      info("Test adding selections to " + aID);
      const hyperText = getAccessible(aID, [ nsIAccessibleText ]);
      const initialSelectionCount = hyperText.selectionCount;

      // Multiple selection changes will be coalesced, so just listen for one.
      const selectionChange = waitForEvent(EVENT_TEXT_SELECTION_CHANGED, aID);
      for (let [startOffset, endOffset] of aSelections) {
        hyperText.addSelection(startOffset, endOffset);
      }
      await selectionChange;

      is(hyperText.selectionCount,
         aSelections.length + initialSelectionCount,
         "addSelection: Wrong selection count for " + aID);

      for (let i in aSelections) {
        testSelectionBounds(aID, hyperText, initialSelectionCount + i,
                            aSelections[i], "addSelection");
      }

      is(hyperText.caretOffset, aSelections[hyperText.selectionCount -1][1],
         "addSelection: caretOffset not at selection end for " + aID);
    }

    /**
     * Test changing selections in accessibles.
     * @param {string} aID  The ID of the element to test.
     * @param {int} aIndex  The index of the selection to change.
     * @param {array} aSelection  Array of the selection's new start and end
     *                            indices.
     */
    async function changeSelection(aID, aIndex, aSelection) {
      info("Test changing the selection of " + aID + " at index " + aIndex);
      const [startOffset, endOffset] = aSelection;
      const hyperText = getAccessible(aID, [ nsIAccessibleText ]);

      const selectionChanged = waitForEvent(EVENT_TEXT_SELECTION_CHANGED, aID);
      hyperText.setSelectionBounds(aIndex, startOffset, endOffset);
      await selectionChanged;

      testSelectionBounds(aID, hyperText, aIndex,
                          aSelection, "setSelectionBounds");

      is(hyperText.caretOffset, endOffset,
         "setSelectionBounds: caretOffset not at selection end for " + aID);
    }

    /**
     * Test removing all selections from accessibles.
     * @param {string} aID  The ID of the element to test.
     */
    async function removeSelections(aID) {
      info("Testing removal of all selections from " + aID);
      const hyperText = getAccessible(aID, [ nsIAccessibleText ]);

      let selectionsRemoved = waitForEvent(EVENT_TEXT_SELECTION_CHANGED, document);
      const selectionCount = hyperText.selectionCount;
      for (let i = 0; i < selectionCount; i++) {
        hyperText.removeSelection(0);
      }
      await selectionsRemoved;

      is(hyperText.selectionCount, 0,
         "removeSelection: Wrong selection count for " + aID);
    }

    /**
     * Test that changing the DOM selection is reflected in the accessibles.
     * @param {string} aID  The container ID to test in
     * @param {string} aNodeID1  The start node of the selection
     * @param  {int} aNodeOffset1  The offset where the selection should start
     * @param {string} aNodeID2  The node in which the selection should end
     * @param {int} aNodeOffset2  The index at which the selection should end
     * @param {array} aTests  An array of accessibles and their start and end
     *                        offsets to test.
     */
    async function changeDOMSelection(aID, aNodeID1, aNodeOffset1,
                                      aNodeID2, aNodeOffset2,
                                      aTests) {
      info("Test that DOM selection changes are reflected in the accessibles");

      let selectionChanged = waitForEvent(EVENT_TEXT_SELECTION_CHANGED, aID);
      // HyperTextAccessible::GetSelectionDOMRanges ignores hidden selections.
      // Here we may be focusing an editable element (and thus hiding the
      // main document selection), so blur it so that we test what we want to
      // test.
      document.activeElement.blur();

      const sel = window.getSelection();
      const range = document.createRange();
      range.setStart(getNode(aNodeID1), aNodeOffset1);
      range.setEnd(getNode(aNodeID2), aNodeOffset2);
      sel.addRange(range);
      await selectionChanged;

      for (let i = 0; i < aTests.length; i++) {
        const text = getAccessible(aTests[i][0], nsIAccessibleText);
        is(text.selectionCount, 1,
           "setSelectionBounds: Wrong selection count for " + aID);
        testSelectionBounds(aID, text, 0, [aTests[i][1], aTests[i][2]],
                            "setSelectionBounds");
      }
    }

    /**
     * Test  expected and unexpected events for selecting
     * all text and focusing both an input and text area. We expect a caret
     * move, but not a text selection change.
     * @param {string} aID  The ID of the element to test.
     */
    async function eventsForSelectingAllTextAndFocus(aID) {
      info("Test expected caretMove and unexpected textSelection events for " +aID);
      let events = waitForEvents({
        expected: [[EVENT_TEXT_CARET_MOVED, aID]],
        unexpected: [[EVENT_TEXT_SELECTION_CHANGED, aID]]}, aID);
      selectAllTextAndFocus(aID);
      await events;
    }

    /**
     * Do tests
     */

    async function doTests() {
      await addSelections("paragraph", [[1, 3], [6, 10]]);
      await changeSelection("paragraph", 0, [2, 4]);
      await removeSelections("paragraph");

      // reverse selection
      await addSelections("paragraph", [[1, 3], [10, 6]]);
      await removeSelections("paragraph");

      await eventsForSelectingAllTextAndFocus("textbox");
      await changeSelection("textbox", 0, [1, 3]);

      // reverse selection
      await changeSelection("textbox", 0, [3, 1]);

      await eventsForSelectingAllTextAndFocus("textarea");
      await changeSelection("textarea", 0, [1, 3]);

      await changeDOMSelection("c1", "c1_span1", 0, "c1_span2", 0,
                               [["c1", 2, 2]]);
      await changeDOMSelection("c2", "c2", 0, "c2_div2", 1,
                               [["c2", 0, 3], ["c2_div2", 0, 2]]);

      SimpleTest.finish();
    }

    SimpleTest.waitForExplicitFinish();
    addA11yLoadEvent(doTests);
  </script>
</head>

<body>

  <a target="_blank"
     href="https://bugzilla.mozilla.org/show_bug.cgi?id=688126"
     title="nsIAccessibleText::setSelectionBounds doesn't fire text selection changed events in some cases">
    Bug 688126
  </a>
  <a target="_blank"
     href="https://bugzilla.mozilla.org/show_bug.cgi?id=688124"
     title="no text selection changed event when selection is removed">
    Bug 688124
  </a>
  <p id="display"></p>
  <div id="content" style="display: none"></div>
  <pre id="test">
  </pre>

  <p id="paragraph">hello world</p>
  <input id="textbox" value="hello"/>
  <textarea id="textarea">hello</textarea>
  <div id="c1">hi<span id="c1_span1"></span><span id="c1_span2"></span>hi</div>
  <div id="c2">hi<div id="c2_div2">hi</div></div>

</body>
</html>