<!DOCTYPE html>
<html>
<head>
  <title>Test: nsIAccessibleText getText* functions at caret offset</title>

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

  <script type="application/javascript"
          src="chrome://mochikit/content/MochiKit/packed.js"></script>
  <script type="application/javascript"
          src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
  <script type="application/javascript"
          src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>

  <script type="application/javascript"
          src="../common.js"></script>
  <script type="application/javascript"
          src="../role.js"></script>
  <script type="application/javascript"
          src="../states.js"></script>
  <script type="application/javascript"
          src="../events.js"></script>
  <script type="application/javascript"
          src="../text.js"></script>

  <script type="application/javascript">
    //gA11yEventDumpToConsole = true; // debugging

    function traverseTextByLines(aQueue, aID, aLines)
    {
      var wholeText = "";
      for (var i = 0; i < aLines.length ; i++)
        wholeText += aLines[i][0] + aLines[i][1];

      var baseInvokerFunc = synthClick;
      var charIter = new charIterator(wholeText, aLines);
      //charIter.debugOffset = 10; // enable to run tests at given offset only

      while (charIter.next()) {
        aQueue.push(new tmpl_moveTo(aID, baseInvokerFunc, wholeText, charIter));
        baseInvokerFunc = synthRightKey;
      }
    }

    /**
     * Used to get test list for each traversed character.
     */
    function charIterator(aWholeText, aLines)
    {
      this.next = function charIterator_next()
      {
        // Don't increment offset if we are at end of the wrapped line
        // (offset is shared between end of this line and start of next line).
        if (this.mAtWrappedLineEnd) {
          this.mAtWrappedLineEnd = false;
          this.mLine = this.mLine.nextLine;
          return true;
        }

        this.mOffset++;
        if (this.mOffset > aWholeText.length)
          return false;

        var nextLine = this.mLine.nextLine;
        if (!nextLine.isFakeLine() && this.mOffset == nextLine.start) {
          if (nextLine.start == this.mLine.end)
            this.mAtWrappedLineEnd = true;
          else
            this.mLine = nextLine;
        }

        return true;
      }

      Object.defineProperty(this, "offset", { get: function()
        { return this.mOffset; }
      });

      Object.defineProperty(this, "offsetDescr", { get: function()
        {
          return this.mOffset + " offset (" + this.mLine.number + " line, " +
            (this.mOffset - this.mLine.start) + " offset on the line)";
        }
      });

      Object.defineProperty(this, "tests", { get: function()
        {
          // Line boundary tests.
          var cLine = this.mLine;
          var pLine = cLine.prevLine;
          var ppLine = pLine.prevLine;
          var nLine = cLine.nextLine;
          var nnLine = nLine.nextLine;

          var lineTests = [
            [ testTextBeforeOffset, BOUNDARY_LINE_START, pLine.start, cLine.start],
            [ testTextBeforeOffset, BOUNDARY_LINE_END, ppLine.end, pLine.end],
            [ testTextAtOffset, BOUNDARY_LINE_START, cLine.start, nLine.start],
            [ testTextAtOffset, BOUNDARY_LINE_END, pLine.end, cLine.end],
            [ testTextAfterOffset, BOUNDARY_LINE_START, nLine.start, nnLine.start],
            [ testTextAfterOffset, BOUNDARY_LINE_END, cLine.end, nLine.end]
          ];

          // Word boundary tests.
          var cWord = this.mLine.firstWord;
          var nWord = cWord.nextWord, pWord = cWord.prevWord;

          // The current word is a farthest word starting at or after the offset.
          if (this.mOffset >= nWord.start) {
            while (this.mOffset >= nWord.start && !this.mLine.isLastWord(cWord)) {
              cWord = nWord;
              nWord = nWord.nextWord;
            }
            pWord = cWord.prevWord;

          } else if (this.mOffset < cWord.start) {
            while (this.mOffset < cWord.start) {
              cWord = pWord;
              pWord = pWord.prevWord;
            }
            nWord = cWord.nextWord;
          }

          var nnWord = nWord.nextWord, ppWord = pWord.prevWord;

          var isAfterWordEnd =
            this.mOffset > cWord.end || cWord.line != this.mLine;
          var isAtOrAfterWordEnd = (this.mOffset >= cWord.end);
          var useNextWordForAtWordEnd =
            isAtOrAfterWordEnd && this.mOffset != aWholeText.length;

          var wordTests = [
            [ testTextBeforeOffset, BOUNDARY_WORD_START,
              pWord.start, cWord.start ],
            [ testTextBeforeOffset, BOUNDARY_WORD_END,
              (isAfterWordEnd ? pWord : ppWord).end,
              (isAfterWordEnd ? cWord : pWord).end ],
            [ testTextAtOffset, BOUNDARY_WORD_START,
              cWord.start, nWord.start ],
            [ testTextAtOffset, BOUNDARY_WORD_END,
              (useNextWordForAtWordEnd ? cWord : pWord).end,
              (useNextWordForAtWordEnd ? nWord : cWord).end ],
            [ testTextAfterOffset, BOUNDARY_WORD_START,
              nWord.start, nnWord.start ],
            [ testTextAfterOffset, BOUNDARY_WORD_END,
              (isAfterWordEnd ? nWord : cWord).end,
              (isAfterWordEnd ? nnWord : nWord).end ]
          ];

          // Character boundary tests.
          var prevOffset = this.offset > 1 ? this.offset - 1 : 0;
          var nextOffset = this.offset >= aWholeText.length ?
            this.offset : this.offset + 1;
          var nextAfterNextOffset = nextOffset >= aWholeText.length ?
            nextOffset : nextOffset + 1;

          var charTests = [
            [ testTextBeforeOffset, BOUNDARY_CHAR,
              prevOffset, this.offset ],
            [ testTextAtOffset, BOUNDARY_CHAR,
              this.offset,
              this.mAtWrappedLineEnd ? this.offset : nextOffset ],
            [ testTextAfterOffset, BOUNDARY_CHAR,
              this.mAtWrappedLineEnd ? this.offset : nextOffset,
              this.mAtWrappedLineEnd ? nextOffset : nextAfterNextOffset ]
          ];

          return lineTests.concat(wordTests.concat(charTests));
        }
      });

      Object.defineProperty(this, "failures", { get: function()
        {
          if (this.mOffset == this.mLine.start)
            return this.mLine.lineStartFailures;
          if (this.mOffset == this.mLine.end)
            return this.mLine.lineEndFailures;
          return [];
        }
      });

      this.mOffset = -1;
      this.mLine = new line(aWholeText, aLines, 0);
      this.mAtWrappedLineEnd = false;
      this.mWord = this.mLine.firstWord;
    }

    /**
     * A line object. Allows to navigate by lines and by words.
     */
    function line(aWholeText, aLines, aIndex)
    {
      Object.defineProperty(this, "prevLine", { get: function()
        {
          return new line(aWholeText, aLines, aIndex - 1);
        }
      }); 
      Object.defineProperty(this, "nextLine", { get: function()
        {
          return new line(aWholeText, aLines, aIndex + 1);
        }
      });

      Object.defineProperty(this, "start", { get: function()
        {
          if (aIndex < 0)
            return 0;

          if (aIndex >= aLines.length)
            return aWholeText.length;

          return aLines[aIndex][2];
        }
      });
      Object.defineProperty(this, "end", { get: function()
        {
          if (aIndex < 0)
            return 0;

          if (aIndex >= aLines.length)
            return aWholeText.length;

          return aLines[aIndex][3];
        }
      });

      Object.defineProperty(this, "number", { get: function()
        { return aIndex; }
      });
      Object.defineProperty(this, "wholeText", { get: function()
        { return aWholeText; }
      });
      this.isFakeLine = function line_isFakeLine()
      {
        return aIndex < 0 || aIndex >= aLines.length;
      }

      Object.defineProperty(this, "lastWord", { get: function()
        {
          if (aIndex < 0)
            return new word(this, [], -1);
          if (aIndex >= aLines.length)
            return new word(this, [], 0);

          var words = aLines[aIndex][4].words;
          return new word(this, words, words.length - 2);
        }
      });
      Object.defineProperty(this, "firstWord", { get: function()
        {
          if (aIndex < 0)
            return new word(this, [], -1);
          if (aIndex >= aLines.length)
            return new word(this, [], 0);

          var words = aLines[aIndex][4].words;
          return new word(this, words, 0);
        }
      });

      this.isLastWord = function line_isLastWord(aWord)
      {
        var lastWord = this.lastWord;
        return lastWord.start == aWord.start && lastWord.end == aWord.end;
      }

      Object.defineProperty(this, "lineStartFailures", { get: function()
        {
          if (aIndex < 0 || aIndex >= aLines.length)
            return [];

          return aLines[aIndex][4].lsf || [];
        }
      });
      Object.defineProperty(this, "lineEndFailures", { get: function()
        {
          if (aIndex < 0 || aIndex >= aLines.length)
            return [];

          return aLines[aIndex][4].lef || [];
        }
      });
    }

    /**
     * A word object. Allows to navigate by words.
     */
    function word(aLine, aWords, aIndex)
    {
      Object.defineProperty(this, "prevWord", { get: function()
        {
          if (aIndex >= 2)
            return new word(aLine, aWords, aIndex - 2);

          var prevLineLastWord = aLine.prevLine.lastWord;
          if (this.start == prevLineLastWord.start && !this.isFakeStartWord())
            return prevLineLastWord.prevWord;
          return prevLineLastWord;
        }
      });
      Object.defineProperty(this, "nextWord", { get: function()
        {
          if (aIndex + 2 < aWords.length)
            return new word(aLine, aWords, aIndex + 2);

          var nextLineFirstWord = aLine.nextLine.firstWord;
          if (this.end == nextLineFirstWord.end && !this.isFakeEndWord())
            return nextLineFirstWord.nextWord;
          return nextLineFirstWord;
        }
      });

      Object.defineProperty(this, "line", { get: function() { return aLine; } });

      Object.defineProperty(this, "start", { get: function()
        {
          if (this.isFakeStartWord())
            return 0;

          if (this.isFakeEndWord())
            return aLine.end;
           return aWords[aIndex];
        }
      });
      Object.defineProperty(this, "end", { get: function()
        {
          if (this.isFakeStartWord())
            return 0;

          return this.isFakeEndWord() ? aLine.end : aWords[aIndex + 1];
        }
      });

      this.toString = function word_toString()
      {
        var start = this.start, end = this.end;
        return "'" + aLine.wholeText.substring(start, end) +
          "' at [" + start + ", " + end + "]";
      }

      this.isFakeStartWord = function() { return aIndex < 0; }
      this.isFakeEndWord = function() { return aIndex >= aWords.length; }
    }

    /**
     * A template invoker to move through the text.
     */
    function tmpl_moveTo(aID, aInvokerFunc, aWholeText, aCharIter)
    {
      this.offset = aCharIter.offset;

      var checker = new caretMoveChecker(this.offset, aID);
      this.__proto__ = new (aInvokerFunc)(aID, checker);

      this.finalCheck = function genericMoveTo_finalCheck()
      {
        if (this.noTests())
          return;

        for (var i = 0; i < this.tests.length; i++) {
          var func = this.tests[i][0];
          var boundary = this.tests[i][1];
          var startOffset = this.tests[i][2];
          var endOffset = this.tests[i][3];
          var text = aWholeText.substring(startOffset, endOffset);

          var isOk1 = kOk, isOk2 = kOk, isOk3 = kOk;
          for (var fIdx = 0; fIdx < this.failures.length; fIdx++) {
            var failure = this.failures[fIdx];
            if (func.name.indexOf(failure[0]) != -1 && boundary == failure[1]) {
              isOk1 = failure[2];
              isOk2 = failure[3];
              isOk3 = failure[4];
            }
          }

          func.call(null, kCaretOffset, boundary, text, startOffset, endOffset,
                    aID, isOk1, isOk2, isOk3);
        }
      }

      this.getID = function genericMoveTo_getID()
      {
        return "move to " + this.offsetDescr;
      }

      this.noTests = function tmpl_moveTo_noTests()
      {
        return ("debugOffset" in aCharIter) &&
          (aCharIter.debugOffset != this.offset);
      }

      this.offsetDescr = aCharIter.offsetDescr;
      this.tests = this.noTests() ? null : aCharIter.tests;
      this.failures = aCharIter.failures;
    }

    var gQueue = null;
    function doTest()
    {
      gQueue = new eventQueue();

      // __a__w__o__r__d__\n
      //  0  1  2  3  4  5
      // __t__w__o__ (soft line break)
      //  6  7  8  9
      // __w__o__r__d__s
      // 10 11 12 13 14 15

      traverseTextByLines(gQueue, "textarea",
                          [ [ "aword", "\n", 0, 5, { words: [ 0, 5 ] } ],
                            [ "two ", "", 6, 10, { words: [ 6, 9 ] } ],
                            [ "words", "", 10, 15, { words: [ 10, 15 ] } ]
                          ] );

      var line4 = [ // "riend "
        [ "TextBeforeOffset", BOUNDARY_WORD_END, kTodo, kTodo, kTodo ],
        [ "TextAfterOffset", BOUNDARY_WORD_END, kTodo, kTodo, kTodo ]
      ];
      traverseTextByLines(gQueue, "ta_wrapped", 
                          [ [ "hi ", "", 0, 3, { words: [ 0, 2 ] } ],
                            [ "hello ", "", 3, 9, { words: [ 3, 8 ] } ],
                            [ "my ", "", 9, 12, { words: [ 9, 11 ] } ],
                            [ "longf", "", 12, 17, { words: [ 12, 17 ] } ],
                            [ "riend ", "", 17, 23, { words: [ 17, 22 ], lsf: line4 } ],
                            [ "t sq ", "", 23, 28, { words: [ 23, 24, 25, 27 ] } ],
                            [ "t", "", 28, 29, { words: [ 28, 29 ] } ]
                          ] );

      gQueue.invoke(); // will call SimpleTest.finish();
    }

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

  <a target="_blank"
     title="nsIAccessibleText getText related functions tests at caret offset"
     href="https://bugzilla.mozilla.org/show_bug.cgi?id=852021">
   Bug 852021
  </a>
  <p id="display"></p>
  <div id="content" style="display: none"></div>
  <pre id="test">

  <textarea id="textarea" cols="5">aword
two words</textarea>

  <textarea id="ta_wrapped" cols="5">hi hello my longfriend t sq t</textarea>
  </pre>
</body>
</html>