<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <style>
        div.test {
            -webkit-user-modify: read-write;
            padding: 4px;
            border: 1px dashed lightblue;
            margin: 4px 4px 4px 24px;
            outline: none;
            font-family: Lucida Grande;
            counter-increment: test-number;
        }
        div.test:before { content: counter(test-number); position: absolute; left: 8px; font-size: x-small; text-align: right; width: 20px; }
        div.test span { background-color: #def; }
        div.test img { width: 1em; height: 1em; background-color: lightgreen; }
        div.test img + img { background-color: lightblue; }
        div.test div { border: 1px dashed pink; padding: 3px; height: 2em; }
    </style>
    <script>
        var messages = [];

        function log(message)
        {
            messages.push(message);
        }

        function flushLog()
        {
            document.getElementById("console").appendChild(document.createTextNode(messages.join("")));
        }

        function caretCoordinates()
        {
            if (!window.textInputController)
                return { x: 0, y :0 };
            var caretRect = textInputController.firstRectForCharacterRange(textInputController.selectedRange()[0], 0);
            return { x: caretRect[0], y: caretRect[1] };
        }

        function positionsMovingInDirection(sel, direction)
        {
            var positions = [];
            while (true) {
                positions.push({ node: sel.anchorNode, offset: sel.anchorOffset, point: caretCoordinates() });
                sel.modify("move", direction, "character");
                if (positions[positions.length - 1].node == sel.anchorNode && positions[positions.length - 1].offset == sel.anchorOffset)
                    break;
            };
            return positions;
        }

        function fold(string)
        {
            var result = "";
            for (var i = 0; i < string.length; ++i) {
                var char = string.charCodeAt(i);
                if (char >= 0x0660) // Arabic numeral 0
                    char = char - 0x660 + '0'.charCodeAt(0);
                else if (char >= 0x0627) // Alif
                    char = char - 0x0627 + 'A'.charCodeAt(0);
                else if (char >= 0x05d0)
                    char -= 0x058f;
                else if (char == 10) {
                    result += "\\n";
                    continue;
                }
                result += String.fromCharCode(char);
            }
            return result;
        }

        function logPositions(positions)
        {
            for (var i = 0; i < positions.length; ++i) {
                if (i) {
                    if (positions[i].node != positions[i - 1].node)
                        log("]");
                    log(", ");
                }
                if (!i || positions[i].node != positions[i - 1].node)
                    log((positions[i].node instanceof Text ? '"' + fold(positions[i].node.data) + '"' : "<" + positions[i].node.tagName + ">") + "[");
                log(positions[i].offset);
            }
            log("]");
        }

        function checkReverseOrder(positions, reversePositions)
        {
            var mismatch = positions.length != reversePositions.length;
            while (!mismatch) {
                var pos = positions.pop();
                if (!pos)
                    break;
                var reversePos = reversePositions.shift();
                if (pos.node != reversePos.node || pos.offset != reversePos.offset)
                    mismatch = true;
            }

            if (mismatch)
                log("WARNING: Moving to the left did not visit the same positions in reverse order as moving to the right.\n");
        }

        function checkCoordinatesMovingRightDown(positions)
        {
            for (var i = 1; i < positions.length; ++i) {
                if (positions[i].point.y > positions[i - 1].point.y || positions[i].point.x < positions[i - 1].point.x && positions[i].point.y >= positions[i - 1].point.y)
                    log("WARNING: Moved in the wrong direction in step " + i + ": from (" + positions[i - 1].point.x + ", " + positions[i - 1].point.y + ") to (" + positions[i].point.x + ", " + positions[i].point.y + ").\n");
            }
        }

        function checkCoordinatesMovingLeftDown(positions)
        {
            for (var i = 1; i < positions.length; ++i) {
                if (positions[i].point.y > positions[i - 1].point.y || positions[i].point.x > positions[i - 1].point.x && positions[i].point.y >= positions[i - 1].point.y)
                    log("WARNING: Moved in the wrong direction in step " + i + ": from (" + positions[i - 1].point.x + ", " + positions[i - 1].point.y + ") to (" + positions[i].point.x + ", " + positions[i].point.y + ").\n");
            }
        }

        function checkCoordinatesMovingRightUp(positions)
        {
            for (var i = 1; i < positions.length; ++i) {
                if (positions[i].point.y < positions[i - 1].point.y || positions[i].point.x < positions[i - 1].point.x && positions[i].point.y <= positions[i - 1].point.y)
                    log("WARNING: Moved in the wrong direction in step " + i + ": from (" + positions[i - 1].point.x + ", " + positions[i - 1].point.y + ") to (" + positions[i].point.x + ", " + positions[i].point.y + ").\n");
            }
        }

        function checkCoordinatesMovingLeftUp(positions)
        {
            for (var i = 1; i < positions.length; ++i) {
                if (positions[i].point.y < positions[i - 1].point.y || positions[i].point.x > positions[i - 1].point.x && positions[i].point.y <= positions[i - 1].point.y)
                    log("WARNING: Moved in the wrong direction in step " + i + ": from (" + positions[i - 1].point.x + ", " + positions[i - 1].point.y + ") to (" + positions[i].point.x + ", " + positions[i].point.y + ").\n");
            }
        }

        function runTest()
        {
            if (window.internals)
                internals.setContinuousSpellCheckingEnabled(false); // For performance.

            var tests = document.getElementsByClassName("test");
            var sel = getSelection();
            for (var i = 0; i < tests.length; ++i) {
                var positionsMovingRight;
                var positionsMovingLeft;
    
                log("Test " + (i + 1) + ", LTR:\n  Moving right: ");
                sel.setPosition(tests[i], 0);
                positionsMovingInDirection(sel, "forward");
                positionsMovingInDirection(sel, "backward");
                positionsMovingRight = positionsMovingInDirection(sel, "right");
                logPositions(positionsMovingRight);
                log("\n");
                checkCoordinatesMovingRightDown(positionsMovingRight);

                log("  Moving left:  ");
                positionsMovingLeft = positionsMovingInDirection(sel, "left");
                logPositions(positionsMovingLeft);
                log("\n");
                checkCoordinatesMovingLeftUp(positionsMovingLeft);

                checkReverseOrder(positionsMovingLeft, positionsMovingRight);

                tests[i].style.direction = "rtl";

                log("Test " + (i + 1) + ", RTL:\n  Moving left: ");
                sel.setPosition(tests[i], 0);
                positionsMovingInDirection(sel, "forward");
                positionsMovingInDirection(sel, "backward");
                positionsMovingLeft = positionsMovingInDirection(sel, "left");
                logPositions(positionsMovingLeft);
                log("\n");
                checkCoordinatesMovingLeftDown(positionsMovingLeft);

                log("  Moving right:  ");
                positionsMovingRight = positionsMovingInDirection(sel, "right");
                logPositions(positionsMovingRight);
                log("\n");
                checkCoordinatesMovingRightUp(positionsMovingRight);

                checkReverseOrder(positionsMovingLeft, positionsMovingRight);

            }
            document.getElementById("testGroup").style.display = "none";
        }

        onload = function() {
            try {
                runTest();
            } finally {
                flushLog();
            }
        };

        if (window.testRunner)
            testRunner.dumpAsText();
    </script>
</head>
<body>
    <div id="testGroup">

    <div class="test">
        abc
    </div>

    <div class="test">
        אבג
    </div>

    <div class="test"><br>abc
    </div>

    <div class="test"><br>אבג
    </div>

    <div class="test">
        abcאבגdef
    </div>

    <div class="test">
        אבגabcדהו
    </div>

    <div class="test">
        abcאבגדהו
    </div>

    <div class="test">
        אבגabcdef
    </div>

    <div class="test">١٢٣ابة</div>

    <div class="test">ابة١٢٣</div>

    <div class="test">
        <span>abc</span>אבגdef
    </div>

    <div class="test">
        <span>אבג</span>abcדהו
    </div>

    <div class="test">abcאבג123דהוdef
    </div>

    <div class="test">abcאבג123
    </div>

    <div class="test">abcאבג123def
    </div>

    <div class="test">אבג123דהוabcזחט456יכל
    </div>

    <div class="test" style="width: 120px;">
        before    אחרי אנציקלופדיה
    </div>

    <div class="test" style="width: 120px;">
        לפני    after encyclopedia
    </div>

    <div class="test" contenteditable style="width: 120px;">
        before    אחרי אנציקלופדיה
    </div>

    <div class="test" contenteditable style="width: 120px;">
        לפני    after encyclopedia
    </div>

    <div class="test" style="width: 100px;">
        This is יותר צר מיתר the boxes. 
    </div>

    <div contenteditable class="test" style="width: 100px;">
        This is יותר צר מיתר the boxes. 
    </div>

    <div class="test">
        Lorem
        <div></div>
        ipsum
    </div>

    <div class="test">
        צלחת
        <div></div>
        מצנפת
    </div>

    <div class="test">
        abcdefאבג<img>דהו
    </div>

    <div class="test">
        אבגדהוabc<img>def
    </div>

    <div class="test">
        abc<input>אבג<img><img>דהוghi
    </div>

    <div class="test">
        אבג<input>abc<img><img>defדהו
    </div>

    <div class="test">
        abcאבג<span>דהו</span>
    </div>

    <div class="test">
        אבגabc<span>def</span>
    </div>

    <div class="test">
        ab<span>cאבגdef</span>
    </div>

    <div class="test">
        אב<span>גabcדהו</span>
    </div>

    <div class="test">
        abc<span>אבגdef</span>
    </div>

    <div class="test">
        אבג<span>abcדהו</span>
    </div>

    <div class="test">
        abcאdef
    </div>

    <div class="test">
        אבגaדהו
    </div>

    <div class="test">
        abcאבג<span>def</span>
    </div>

    <div class="test">
        אבגabc<span>דהו</span>
    </div>

    <div class="test">
        abcא<span>בגdef</span>
    </div>

    <div class="test">
        אבגa<span>bcדהו</span>
    </div>

    <div class="test" style="white-space: pre;">abc<!-- -->
<!-- -->def</div>

    <div class="test" style="white-space: pre;">אבג<!-- -->
<!-- -->דהו</div>

    <div class="test">
        <span dir="rtl">abcקקק123נננdef</span>
    </div>

    </div>

    <pre id="console"></pre>
</body>
