Carets can split up marriages and families
https://bugs.webkit.org/show_bug.cgi?id=166711
<rdar://problem/29019333>

Reviewed by Alex Christensen.

Source/WTF:

There are four code points which should be allowed to accept emoji modifiers:
- U+1F46A FAMILY
- U+1F46B MAN AND WOMAN HOLDING HANDS
- U+1F46C TWO MEN HOLDING HANDS
- U+1F46D TWO WOMEN HOLDING HANDS

Even though macOS's and iOS's emoji keyboard don't allow users to actually type
these combinations, we may still receive them from other platforms. We should
therefore treat these as joining sequences. Rendering isn't a problem because
the fonts accept the emoji modifiers, but our caret placement code isn't educated
about it. Currently, we treat these emoji groups as ligatures, allowing the caret
to be placed between the two code points, which visually shows as being horizontally
centered in the glyph. Instead, we should treat these code points as accepting
emoji modifiers.

Tests: editing/caret/emoji.html
       editing/caret/ios/emoji.html

* wtf/text/TextBreakIterator.cpp:
(WTF::cursorMovementIterator):

LayoutTests:

AFAICT we don't have a test where we arrow-through a set of emoji. We do
have tests where we backspace-through a set of emoji. Add a new test for
the arrow keys.

* platform/ios/TestExpectations:
* platform/mac/editing/caret/emoji-expected.txt: Added.
* editing/caret/emoji.html: Added.
* editing/caret/ios/emoji-expected.txt: Added.
* editing/caret/ios/emoji.html: Added.

git-svn-id: http://svn.webkit.org/repository/webkit/trunk@210399 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/LayoutTests/editing/caret/ios/emoji.html b/LayoutTests/editing/caret/ios/emoji.html
new file mode 100644
index 0000000..0a8e101
--- /dev/null
+++ b/LayoutTests/editing/caret/ios/emoji.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html> <!-- webkit-test-runner [ useFlexibleViewport=true ] -->
+<html>
+<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
+<head>
+    <script src="../../../resources/js-test-pre.js"></script>
+    <style>
+        body {
+            margin: 0;
+        }
+
+        input {
+            width: 100%;
+            height: 50px;
+            position: absolute;
+            left: 0;
+            top: 0;
+        }
+
+        div {
+            background-image: linear-gradient(0deg, blue, red);
+            height: 4000px;
+        }
+    </style>
+    <script>
+    window.jsTestIsAsync = true;
+
+    function tapInInputScript(tapX, tapY)
+    {
+        return `
+        var caretPositions = [];
+
+        function pressArrow() {
+            uiController.typeCharacterUsingHardwareKeyboard("leftArrow", function() {
+                uiController.doAfterNextStablePresentationUpdate(function() {
+                    var selectionRectLeft = uiController.textSelectionCaretRect.left;
+                    var caretPositionsLength = caretPositions.length;
+                    if (caretPositionsLength == 0 || caretPositions[caretPositionsLength - 1] != selectionRectLeft) {
+                        caretPositions.push(selectionRectLeft);
+                        pressArrow();
+                    } else
+                        uiController.uiScriptComplete(JSON.stringify(caretPositions));
+                });
+            });
+        };
+
+        (function() {
+            uiController.didShowKeyboardCallback = function() {
+                uiController.doAfterNextStablePresentationUpdate(function() {
+                    pressArrow();
+                });
+            };
+            uiController.singleTapAtPoint(${tapX}, ${tapY}, function() { });
+        })()`;
+    }
+
+    var pixelWidth;
+    var currentWidth;
+    function run()
+    {
+        if (!window.testRunner || !testRunner.runUIScript) {
+            description("To manually test, place the caret in the field above and use the arrow keys to make sure the carets don't appear in the middle of characters.");
+            return;
+        }
+
+        testRunner.runUIScript(tapInInputScript(window.innerWidth * 2 / 3, 30), caretPositions => {
+            caretPositions = JSON.parse(caretPositions);
+            pixelWidth = -1;
+            for (var i = 0; i < caretPositions.length - 1; ++i) {
+                currentWidth = caretPositions[i] - caretPositions[i + 1];
+                if (pixelWidth == -1)
+                    pixelWidth = currentWidth;
+                else
+                    shouldBeCloseTo("currentWidth", pixelWidth, 1);
+            }
+            finishJSTest();
+        });
+    }
+    </script>
+</head>
+<body onload=run()>
+    <input value="&#x1f46b;&#x1f46b;&#x1f3fb;&#x1f46b;&#x1f3ff;&#x1f46c;&#x1f46c;&#x1f3fb;&#x1f46c;&#x1f3ff;&#x1f46d;&#x1f46d;&#x1f3fb;&#x1f46d;&#x1f3ff;&#x1f46a;&#x1f46a;&#x1f3fb;&#x1f46a;&#x1f3ff;"></input>
+    <script src="../../../resources/js-test-post.js"></script>
+</body>
+
+</html>