Blue dotted underline with alternatives only shown for last word, gets lost for previ...
authordbates@webkit.org <dbates@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 20 May 2020 00:05:46 +0000 (00:05 +0000)
committerdbates@webkit.org <dbates@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 20 May 2020 00:05:46 +0000 (00:05 +0000)
https://bugs.webkit.org/show_bug.cgi?id=212097
<rdar://problem/61913405>

Reviewed by Darin Adler.

Source/WebCore:

Fix up two cases, <space> is a literal ' ' and | is the position of the caret:
    1. Space inserted after marker, here's what it looks like BEFORE insertion (i.e. endOfFirstWord == startOfSelection):
        hello|

    This case is detected when the end of the next word (relative to caret) would be at the start of the
    word that begins before or on the caret.

    2. Space inserted before marker, here's what it looks like BEFORE insertion (i.e. startOfLastWord == endOfSelection):
        |hello

    This case is detected when the end of the previous word (relative to caret) would be at the end of the
    word that ends after or on the caret.

Note ^^^ example uses a caret, but code is slightly more general and works when the current selection
is a range. Though I didn't explicitly test that because my bug is specific to having a caret selection.

* editing/Editor.cpp:
(WebCore::Editor::insertTextWithoutSendingTextEvent): Do not remove markers if selection starts at the
beginning of a new word. This is detected by looking at the character before the selection start to see
if it is a space or newline. This will be false when there is no preceding character (e.g. start of document),
but that's OK because it doesn't matter what we pass to updateMarkersForWordsAffectedByEditing() - there's
no markers to remove anyway, let alone text for markers to exist in. I don't use isStartOfWord() here
because that would incorrectly return false if the current selection is at the end of a paragraph. I could have
fixed that up by checking isEndOfParagraph() as well, but isStartOfWord() + isEndOfParagraph() is less
efficient than just looking at the previous character directly. So, I did that instead.
(WebCore::Editor::updateMarkersForWordsAffectedByEditing): Save off the original end of the first word and
start of the last word positions before mutating them. Update early return checks to use these saved values
instead of comparing against the start and end of the current selection, which weren't correct. Saved positioned
are aligned by word, but start and end of current selection may NOT be. So, comparison was asymmetric: lhs was
word aligned position, but rhs may not be.

* editing/Editor.h: While I am here, fix up a param name to match what it is called in the .cpp.

* testing/Internals.cpp:
(WebCore::Internals::hasDictationAlternativesMarker): Added
* testing/Internals.h:
* testing/Internals.idl:
Add new functionality for testing purposes.

Tools:

Add tests. As I was writing them I discovered a few bugs in the existing code:
        1. <https://webkit.org/b/212093> REGRESSION (r259930): Dictation marker at start of text is removed when added trailing whitespace is collapsed
        2. <https://webkit.org/b/212098> Inserting a no-break space before or after a marker removes the marker

* TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj:
* TestWebKitAPI/Tests/WebKitCocoa/InsertTextAlternatives.mm: Added.
(TestWebKitAPI::TEST):
(TestWebKitAPI::makeNSStringWithCharacter): Added.

git-svn-id: http://svn.webkit.org/repository/webkit/trunk@261897 268f45cc-cd09-0410-ab3c-d52691b4dbfc

Source/WebCore/ChangeLog
Source/WebCore/editing/Editor.cpp
Source/WebCore/editing/Editor.h
Source/WebCore/testing/Internals.cpp
Source/WebCore/testing/Internals.h
Source/WebCore/testing/Internals.idl
Tools/ChangeLog
Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj
Tools/TestWebKitAPI/Tests/WebKitCocoa/InsertTextAlternatives.mm [new file with mode: 0644]

index a3b6e5c..da3cd2c 100644 (file)
@@ -1,3 +1,51 @@
+2020-05-19  Daniel Bates  <dabates@apple.com>
+
+        Blue dotted underline with alternatives only shown for last word, gets lost for previous insertions
+        https://bugs.webkit.org/show_bug.cgi?id=212097
+        <rdar://problem/61913405>
+
+        Reviewed by Darin Adler.
+
+        Fix up two cases, <space> is a literal ' ' and | is the position of the caret:
+            1. Space inserted after marker, here's what it looks like BEFORE insertion (i.e. endOfFirstWord == startOfSelection):
+                hello|
+
+            This case is detected when the end of the next word (relative to caret) would be at the start of the
+            word that begins before or on the caret.
+
+            2. Space inserted before marker, here's what it looks like BEFORE insertion (i.e. startOfLastWord == endOfSelection):
+                |hello
+
+            This case is detected when the end of the previous word (relative to caret) would be at the end of the
+            word that ends after or on the caret.
+
+        Note ^^^ example uses a caret, but code is slightly more general and works when the current selection
+        is a range. Though I didn't explicitly test that because my bug is specific to having a caret selection.
+
+        * editing/Editor.cpp:
+        (WebCore::Editor::insertTextWithoutSendingTextEvent): Do not remove markers if selection starts at the
+        beginning of a new word. This is detected by looking at the character before the selection start to see
+        if it is a space or newline. This will be false when there is no preceding character (e.g. start of document),
+        but that's OK because it doesn't matter what we pass to updateMarkersForWordsAffectedByEditing() - there's
+        no markers to remove anyway, let alone text for markers to exist in. I don't use isStartOfWord() here
+        because that would incorrectly return false if the current selection is at the end of a paragraph. I could have
+        fixed that up by checking isEndOfParagraph() as well, but isStartOfWord() + isEndOfParagraph() is less
+        efficient than just looking at the previous character directly. So, I did that instead.
+        (WebCore::Editor::updateMarkersForWordsAffectedByEditing): Save off the original end of the first word and
+        start of the last word positions before mutating them. Update early return checks to use these saved values
+        instead of comparing against the start and end of the current selection, which weren't correct. Saved positioned
+        are aligned by word, but start and end of current selection may NOT be. So, comparison was asymmetric: lhs was
+        word aligned position, but rhs may not be.
+
+        * editing/Editor.h: While I am here, fix up a param name to match what it is called in the .cpp.
+
+        * testing/Internals.cpp:
+        (WebCore::Internals::hasDictationAlternativesMarker): Added
+        * testing/Internals.h:
+        * testing/Internals.idl:
+        Add new functionality for testing purposes.
+
+
 2020-05-19  Oriol Brufau  <obrufau@igalia.com>
 
         Fix marginLogicalSizeForChild to check auto margins in the right axis
index 99553de..3e36fa5 100644 (file)
@@ -1270,7 +1270,10 @@ bool Editor::insertTextWithoutSendingTextEvent(const String& text, bool selectIn
     if (!shouldInsertText(text, createLiveRange(selection.toNormalizedRange()).get(), EditorInsertAction::Typed))
         return true;
 
-    updateMarkersForWordsAffectedByEditing(isSpaceOrNewline(text[0]));
+    // FIXME: Should pass false to updateMarkersForWordsAffectedByEditing() to not remove markers if
+    // a leading or trailing no-break space is being inserted. See <https://webkit.org/b/212098>.
+    bool isStartOfNewWord = isSpaceOrNewline(selection.visibleStart().characterBefore());
+    updateMarkersForWordsAffectedByEditing(isSpaceOrNewline(text[0]) || isStartOfNewWord);
 
     bool shouldConsiderApplyingAutocorrection = false;
     if (text == " " || text == "\t")
@@ -3028,12 +3031,15 @@ void Editor::updateMarkersForWordsAffectedByEditing(bool doNotRemoveIfSelectionA
         endOfLastWord = endOfWord(endOfSelection, LeftWordIfOnBoundary);
     }
 
+    auto originalEndOfFirstWord = endOfFirstWord;
+    auto originalStartOfLastWord = startOfLastWord;
+
     // If doNotRemoveIfSelectionAtWordBoundary is true, and first word ends at the start of selection,
     // we choose next word as the first word.
     if (doNotRemoveIfSelectionAtWordBoundary && endOfFirstWord == startOfSelection) {
         startOfFirstWord = nextWordPosition(startOfFirstWord);
         endOfFirstWord = endOfWord(startOfFirstWord, RightWordIfOnBoundary);
-        if (startOfFirstWord == endOfSelection)
+        if (startOfFirstWord == originalStartOfLastWord)
             return;
     }
 
@@ -3042,7 +3048,7 @@ void Editor::updateMarkersForWordsAffectedByEditing(bool doNotRemoveIfSelectionA
     if (doNotRemoveIfSelectionAtWordBoundary && startOfLastWord == endOfSelection) {
         startOfLastWord = previousWordPosition(startOfLastWord);
         endOfLastWord = endOfWord(startOfLastWord, RightWordIfOnBoundary);
-        if (endOfLastWord == startOfSelection)
+        if (endOfLastWord == originalEndOfFirstWord)
             return;
     }
 
index 5137c0d..e97a085 100644 (file)
@@ -479,7 +479,7 @@ public:
     WEBCORE_EXPORT void replaceSelectionWithFragment(DocumentFragment&, SelectReplacement, SmartReplace, MatchStyle, EditAction = EditAction::Insert, MailBlockquoteHandling = MailBlockquoteHandling::RespectBlockquote);
     WEBCORE_EXPORT void replaceSelectionWithText(const String&, SelectReplacement, SmartReplace, EditAction = EditAction::Insert);
     WEBCORE_EXPORT bool selectionStartHasMarkerFor(DocumentMarker::MarkerType, int from, int length) const;
-    void updateMarkersForWordsAffectedByEditing(bool onlyHandleWordsContainingSelection);
+    void updateMarkersForWordsAffectedByEditing(bool doNotRemoveIfSelectionAtWordBoundary);
     void deletedAutocorrectionAtPosition(const Position&, const String& originalString);
     
     WEBCORE_EXPORT void simplifyMarkup(Node* startNode, Node* endNode);
index 3543378..3cf0ec0 100644 (file)
@@ -2356,6 +2356,17 @@ bool Internals::hasAutocorrectedMarker(int from, int length)
     return document->editor().selectionStartHasMarkerFor(DocumentMarker::Autocorrected, from, length);
 }
 
+bool Internals::hasDictationAlternativesMarker(int from, int length)
+{
+    auto* document = contextDocument();
+    if (!document || !document->frame())
+        return false;
+
+    updateEditorUINowIfScheduled();
+
+    return document->frame()->editor().selectionStartHasMarkerFor(DocumentMarker::DictationAlternatives, from, length);
+}
+
 void Internals::setContinuousSpellCheckingEnabled(bool enabled)
 {
     if (!contextDocument() || !contextDocument()->frame())
index 337888c..0b1d0a2 100644 (file)
@@ -349,6 +349,7 @@ public:
     bool hasSpellingMarker(int from, int length);
     bool hasGrammarMarker(int from, int length);
     bool hasAutocorrectedMarker(int from, int length);
+    bool hasDictationAlternativesMarker(int from, int length);
     void setContinuousSpellCheckingEnabled(bool);
     void setAutomaticQuoteSubstitutionEnabled(bool);
     void setAutomaticLinkDetectionEnabled(bool);
index c02b4dd..02a47ed 100644 (file)
@@ -402,6 +402,7 @@ enum CompositingPolicy {
     boolean hasSpellingMarker(long from, long length);
     boolean hasGrammarMarker(long from, long length);
     boolean hasAutocorrectedMarker(long from, long length);
+    boolean hasDictationAlternativesMarker(long from, long length);
     void setContinuousSpellCheckingEnabled(boolean enabled);
     void setAutomaticQuoteSubstitutionEnabled(boolean enabled);
     void setAutomaticLinkDetectionEnabled(boolean enabled);
index 6e26219..0477810 100644 (file)
@@ -1,3 +1,20 @@
+2020-05-19  Daniel Bates  <dabates@apple.com>
+
+        Blue dotted underline with alternatives only shown for last word, gets lost for previous insertions
+        https://bugs.webkit.org/show_bug.cgi?id=212097
+        <rdar://problem/61913405>
+
+        Reviewed by Darin Adler.
+
+        Add tests. As I was writing them I discovered a few bugs in the existing code:
+                1. <https://webkit.org/b/212093> REGRESSION (r259930): Dictation marker at start of text is removed when added trailing whitespace is collapsed
+                2. <https://webkit.org/b/212098> Inserting a no-break space before or after a marker removes the marker
+
+        * TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj:
+        * TestWebKitAPI/Tests/WebKitCocoa/InsertTextAlternatives.mm: Added.
+        (TestWebKitAPI::TEST):
+        (TestWebKitAPI::makeNSStringWithCharacter): Added.
+
 2020-05-19  Sergio Villar Senin  <svillar@igalia.com>
 
         [WPE] Sync jhbuild to flatpak
index b7d0a13..5b48555 100644 (file)
                CEBCA1391E3A807A00C73293 /* page-with-csp-iframe.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = CEBCA1341E3A803400C73293 /* page-with-csp-iframe.html */; };
                CEBCA13A1E3A807A00C73293 /* page-without-csp.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = CEBCA1371E3A803400C73293 /* page-without-csp.html */; };
                CEBCA13B1E3A807A00C73293 /* page-without-csp-iframe.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = CEBCA1361E3A803400C73293 /* page-without-csp-iframe.html */; };
+               CED73D37246F29FA00DAE327 /* InsertTextAlternatives.mm in Sources */ = {isa = PBXBuildFile; fileRef = CED73D35246F204C00DAE327 /* InsertTextAlternatives.mm */; };
                CEDA12412437C9FB00C28A9E /* editable-region-composited-and-non-composited-overlap.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = CEDA12402437C9EA00C28A9E /* editable-region-composited-and-non-composited-overlap.html */; };
                D34E08761E4E42E1005FF14A /* WKWebViewGetContents.mm in Sources */ = {isa = PBXBuildFile; fileRef = D3BE5E341E4CE85E00FD563A /* WKWebViewGetContents.mm */; };
                DF4B273921A47728009BD1CA /* WKNSDictionaryEmptyDictionaryCrash.mm in Sources */ = {isa = PBXBuildFile; fileRef = DF4B273821A47727009BD1CA /* WKNSDictionaryEmptyDictionaryCrash.mm */; };
                CEBCA1351E3A803400C73293 /* page-with-csp.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = "page-with-csp.html"; sourceTree = "<group>"; };
                CEBCA1361E3A803400C73293 /* page-without-csp-iframe.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = "page-without-csp-iframe.html"; sourceTree = "<group>"; };
                CEBCA1371E3A803400C73293 /* page-without-csp.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = "page-without-csp.html"; sourceTree = "<group>"; };
+               CED73D35246F204C00DAE327 /* InsertTextAlternatives.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = InsertTextAlternatives.mm; sourceTree = "<group>"; };
                CEDA12402437C9EA00C28A9E /* editable-region-composited-and-non-composited-overlap.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = "editable-region-composited-and-non-composited-overlap.html"; path = "ios/editable-region-composited-and-non-composited-overlap.html"; sourceTree = SOURCE_ROOT; };
                D3BE5E341E4CE85E00FD563A /* WKWebViewGetContents.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = WKWebViewGetContents.mm; sourceTree = "<group>"; };
                DC69AA621CF77C6500C6272F /* ScopedLambda.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = ScopedLambda.cpp; sourceTree = "<group>"; };
                                CA97B3922193663B0045DF6F /* IndexedDBUserDelete.mm */,
                                0E404A8A2166DDF8008271BA /* InjectedBundleNodeHandleIsSelectElement.mm */,
                                79C5D430209D768300F1E7CA /* InjectedBundleNodeHandleIsTextField.mm */,
+                               CED73D35246F204C00DAE327 /* InsertTextAlternatives.mm */,
                                2DB0232E1E4E871800707123 /* InteractionDeadlockAfterCrash.mm */,
                                2D116E1223E0CB3900208900 /* iOSMouseSupport.mm */,
                                5C69BDD41F82A7EB000F4F4B /* JavaScriptDuringNavigation.mm */,
                                7CCE7EFC1A411AE600447C4C /* InjectedBundleFrameHitTest.cpp in Sources */,
                                7CCE7EFD1A411AE600447C4C /* InjectedBundleInitializationUserDataCallbackWins.cpp in Sources */,
                                7C83E0B81D0A64BD00FEBCF3 /* InjectedBundleMakeAllShadowRootsOpen.cpp in Sources */,
+                               CED73D37246F29FA00DAE327 /* InsertTextAlternatives.mm in Sources */,
                                7CCE7EC31A411A7E00447C4C /* InspectorBar.mm in Sources */,
                                F44A531121B8990300DBB99C /* InstanceMethodSwizzler.mm in Sources */,
                                7CCE7EDA1A411A8700447C4C /* InstanceMethodSwizzler.mm in Sources */,
diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/InsertTextAlternatives.mm b/Tools/TestWebKitAPI/Tests/WebKitCocoa/InsertTextAlternatives.mm
new file mode 100644 (file)
index 0000000..2df2bdb
--- /dev/null
@@ -0,0 +1,382 @@
+/*
+ * Copyright (C) 2020 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "config.h"
+
+#if PLATFORM(IOS_FAMILY)
+
+#import "PlatformUtilities.h"
+#import "TestInputDelegate.h"
+#import "TestWKWebView.h"
+#import "UIKitSPI.h"
+#import "WKWebViewConfigurationExtras.h"
+#import <WebKit/WKWebViewPrivate.h>
+#import <wtf/unicode/CharacterNames.h>
+
+namespace TestWebKitAPI {
+
+TEST(InsertTextAlternatives, Simple)
+{
+    auto *configuration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"WebProcessPlugInWithInternals" configureJSCForTesting:YES];
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 300, 300) configuration:configuration]);
+    auto inputDelegate = adoptNS([[TestInputDelegate alloc] init]);
+    [inputDelegate setFocusStartsInputSessionPolicyHandler:[] (WKWebView *, id <_WKFocusedElementInfo>) { return _WKFocusStartsInputSessionPolicyAllow; }];
+    [webView _setInputDelegate:inputDelegate.get()];
+
+    [webView synchronouslyLoadHTMLString:@"<body contenteditable='true'>A big&nbsp;</body>"];
+    [webView evaluateJavaScriptAndWaitForInputSessionToChange:@"document.body.focus()"];
+    [webView _synchronouslyExecuteEditCommand:@"MoveToEndOfDocument" argument:nil];
+    [[webView textInputContentView] insertText:@"hello" alternatives:@[@"yellow"] style:UITextAlternativeStyleNone];
+    [webView _synchronouslyExecuteEditCommand:@"MoveToBeginningOfDocument" argument:nil];
+    [webView waitForNextPresentationUpdate];
+
+    EXPECT_FALSE([[webView objectByEvaluatingJavaScript:@"internals.hasDictationAlternativesMarker(0, 1)"] boolValue]); // A
+    EXPECT_FALSE([[webView objectByEvaluatingJavaScript:@"internals.hasDictationAlternativesMarker(2, 3)"] boolValue]); // big
+    EXPECT_FALSE([[webView objectByEvaluatingJavaScript:@"internals.hasDictationAlternativesMarker(0, 6)"] boolValue]); // "A big "
+    EXPECT_TRUE([[webView objectByEvaluatingJavaScript:@"internals.hasDictationAlternativesMarker(6, 5)"] boolValue]); // hello
+}
+
+TEST(InsertTextAlternatives, InsertLeadingSpace)
+{
+    auto *configuration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"WebProcessPlugInWithInternals" configureJSCForTesting:YES];
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 300, 300) configuration:configuration]);
+    auto inputDelegate = adoptNS([[TestInputDelegate alloc] init]);
+    [inputDelegate setFocusStartsInputSessionPolicyHandler:[] (WKWebView *, id <_WKFocusedElementInfo>) { return _WKFocusStartsInputSessionPolicyAllow; }];
+    [webView _setInputDelegate:inputDelegate.get()];
+
+    [webView synchronouslyLoadHTMLString:@"<body contenteditable='true'></body>"];
+    [webView evaluateJavaScriptAndWaitForInputSessionToChange:@"document.body.focus()"];
+    [[webView textInputContentView] insertText:@"hello" alternatives:@[@"yellow"] style:UITextAlternativeStyleNone];
+    [webView waitForNextPresentationUpdate];
+
+    [webView _synchronouslyExecuteEditCommand:@"MoveToBeginningOfDocument" argument:nil];
+    [[webView textInputContentView] insertText:@" "];
+    [webView _synchronouslyExecuteEditCommand:@"MoveToBeginningOfDocument" argument:nil];
+    [webView waitForNextPresentationUpdate];
+
+    EXPECT_FALSE([[webView objectByEvaluatingJavaScript:@"internals.hasDictationAlternativesMarker(0, 1)"] boolValue]); // <space>
+    EXPECT_TRUE([[webView objectByEvaluatingJavaScript:@"internals.hasDictationAlternativesMarker(1, 5)"] boolValue]); // hello
+}
+
+TEST(InsertTextAlternatives, InsertLeadingNewline)
+{
+    auto *configuration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"WebProcessPlugInWithInternals" configureJSCForTesting:YES];
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 300, 300) configuration:configuration]);
+    auto inputDelegate = adoptNS([[TestInputDelegate alloc] init]);
+    [inputDelegate setFocusStartsInputSessionPolicyHandler:[] (WKWebView *, id <_WKFocusedElementInfo>) { return _WKFocusStartsInputSessionPolicyAllow; }];
+    [webView _setInputDelegate:inputDelegate.get()];
+
+    // Set CSS white-space and user-modify so that \n is inserted literally. Otherwise, it would be converted into a <br> if <body>
+    // only had "contenteditable='true'" because it is considered richly editable.
+    [webView synchronouslyLoadHTMLString:@"<body style='white-space: pre; -webkit-user-modify: read-write-plaintext-only'></body>"];
+    [webView evaluateJavaScriptAndWaitForInputSessionToChange:@"document.body.focus()"];
+    [[webView textInputContentView] insertText:@"hello" alternatives:@[@"yellow"] style:UITextAlternativeStyleNone];
+    [webView _synchronouslyExecuteEditCommand:@"MoveToBeginningOfDocument" argument:nil];
+    [[webView textInputContentView] insertText:@"\n"];
+    [webView _synchronouslyExecuteEditCommand:@"MoveToBeginningOfDocument" argument:nil];
+    [webView waitForNextPresentationUpdate];
+
+    EXPECT_FALSE([[webView objectByEvaluatingJavaScript:@"internals.hasDictationAlternativesMarker(0, 1)"] boolValue]); // \n
+    [webView _synchronouslyExecuteEditCommand:@"MoveDown" argument:nil];
+    [webView waitForNextPresentationUpdate];
+    EXPECT_TRUE([[webView objectByEvaluatingJavaScript:@"internals.hasDictationAlternativesMarker(0, 5)"] boolValue]); // hello
+}
+
+static inline NSString *makeNSStringWithCharacter(unichar c)
+{
+    return [NSString stringWithCharacters:&c length:1];
+};
+
+TEST(InsertTextAlternatives, InsertLeadingNoBreakSpace_ExpectedFailure)
+{
+    auto *configuration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"WebProcessPlugInWithInternals" configureJSCForTesting:YES];
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 300, 300) configuration:configuration]);
+    auto inputDelegate = adoptNS([[TestInputDelegate alloc] init]);
+    [inputDelegate setFocusStartsInputSessionPolicyHandler:[] (WKWebView *, id <_WKFocusedElementInfo>) { return _WKFocusStartsInputSessionPolicyAllow; }];
+    [webView _setInputDelegate:inputDelegate.get()];
+
+    [webView synchronouslyLoadHTMLString:@"<body contenteditable='true'></body>"];
+    [webView evaluateJavaScriptAndWaitForInputSessionToChange:@"document.body.focus()"];
+    [[webView textInputContentView] insertText:@"hello" alternatives:@[@"yellow"] style:UITextAlternativeStyleNone];
+    [webView waitForNextPresentationUpdate];
+
+    [webView _synchronouslyExecuteEditCommand:@"MoveToBeginningOfDocument" argument:nil];
+    [[webView textInputContentView] insertText:makeNSStringWithCharacter(noBreakSpace)];
+    [webView _synchronouslyExecuteEditCommand:@"MoveToBeginningOfDocument" argument:nil];
+    [webView waitForNextPresentationUpdate];
+
+    EXPECT_FALSE([[webView objectByEvaluatingJavaScript:@"internals.hasDictationAlternativesMarker(0, 1)"] boolValue]); // <no-break space>
+    // FIXME: Change this to EXPECT_TRUE() once leading no-break spaces do not cause removal of alternatives.
+    // See <https://webkit.org/b/212098>.
+    EXPECT_FALSE([[webView objectByEvaluatingJavaScript:@"internals.hasDictationAlternativesMarker(1, 5)"] boolValue]); // hello
+}
+
+TEST(InsertTextAlternatives, InsertTrailingSpace)
+{
+    auto *configuration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"WebProcessPlugInWithInternals" configureJSCForTesting:YES];
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 300, 300) configuration:configuration]);
+    auto inputDelegate = adoptNS([[TestInputDelegate alloc] init]);
+    [inputDelegate setFocusStartsInputSessionPolicyHandler:[] (WKWebView *, id <_WKFocusedElementInfo>) { return _WKFocusStartsInputSessionPolicyAllow; }];
+    [webView _setInputDelegate:inputDelegate.get()];
+
+    // Set CSS white-space so that ' ' is inserted literally. Otherwise, it would be converted into a no-break space.
+    [webView synchronouslyLoadHTMLString:@"<body contenteditable='true' style='white-space: pre'></body>"];
+    [webView evaluateJavaScriptAndWaitForInputSessionToChange:@"document.body.focus()"];
+    [[webView textInputContentView] insertText:@"hello" alternatives:@[@"yellow"] style:UITextAlternativeStyleNone];
+    [[webView textInputContentView] insertText:@" "];
+    [webView _synchronouslyExecuteEditCommand:@"MoveToBeginningOfDocument" argument:nil];
+    [webView waitForNextPresentationUpdate];
+
+    EXPECT_TRUE([[webView objectByEvaluatingJavaScript:@"internals.hasDictationAlternativesMarker(0, 5)"] boolValue]); // hello
+    EXPECT_FALSE([[webView objectByEvaluatingJavaScript:@"internals.hasDictationAlternativesMarker(5, 1)"] boolValue]); // <space>
+}
+
+TEST(InsertTextAlternatives, InsertTrailingSpaceWhitespaceRebalance_ExpectedFailure)
+{
+    auto *configuration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"WebProcessPlugInWithInternals" configureJSCForTesting:YES];
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 300, 300) configuration:configuration]);
+    auto inputDelegate = adoptNS([[TestInputDelegate alloc] init]);
+    [inputDelegate setFocusStartsInputSessionPolicyHandler:[] (WKWebView *, id <_WKFocusedElementInfo>) { return _WKFocusStartsInputSessionPolicyAllow; }];
+    [webView _setInputDelegate:inputDelegate.get()];
+
+    [webView synchronouslyLoadHTMLString:@"<body contenteditable='true'></body>"];
+    [webView evaluateJavaScriptAndWaitForInputSessionToChange:@"document.body.focus()"];
+    [[webView textInputContentView] insertText:@"hello" alternatives:@[@"yellow"] style:UITextAlternativeStyleNone];
+    [[webView textInputContentView] insertText:@" "];
+    [webView waitForNextPresentationUpdate];
+
+    // FIXME: Change this to EXPECT_TRUE() once <https://webkit.org/b/212093> is fixed.
+    EXPECT_FALSE([[webView objectByEvaluatingJavaScript:@"internals.hasDictationAlternativesMarker(0, 5)"] boolValue]); // hello
+}
+
+TEST(InsertTextAlternatives, InsertTrailingNewline)
+{
+    auto *configuration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"WebProcessPlugInWithInternals" configureJSCForTesting:YES];
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 300, 300) configuration:configuration]);
+    auto inputDelegate = adoptNS([[TestInputDelegate alloc] init]);
+    [inputDelegate setFocusStartsInputSessionPolicyHandler:[] (WKWebView *, id <_WKFocusedElementInfo>) { return _WKFocusStartsInputSessionPolicyAllow; }];
+    [webView _setInputDelegate:inputDelegate.get()];
+
+    // Set CSS white-space and user-modify so that \n is inserted literally. Otherwise, it would be converted into a <br> if <body>
+    // only had "contenteditable='true'" because it is considered richly editable.
+    [webView synchronouslyLoadHTMLString:@"<body style='white-space: pre; -webkit-user-modify: read-write-plaintext-only'></body>"];
+    [webView evaluateJavaScriptAndWaitForInputSessionToChange:@"document.body.focus()"];
+    [[webView textInputContentView] insertText:@"hello" alternatives:@[@"yellow"] style:UITextAlternativeStyleNone];
+    [[webView textInputContentView] insertText:@"\n"];
+    [webView _synchronouslyExecuteEditCommand:@"MoveToBeginningOfDocument" argument:nil];
+    [webView waitForNextPresentationUpdate];
+
+    EXPECT_TRUE([[webView objectByEvaluatingJavaScript:@"internals.hasDictationAlternativesMarker(0, 5)"] boolValue]); // hello
+    [webView _synchronouslyExecuteEditCommand:@"MoveDown" argument:nil];
+    [webView waitForNextPresentationUpdate];
+    EXPECT_FALSE([[webView objectByEvaluatingJavaScript:@"internals.hasDictationAlternativesMarker(0, 1)"] boolValue]); // \n
+}
+
+TEST(InsertTextAlternatives, InsertTrailingNoBreakSpace_ExpectedFailure)
+{
+    auto *configuration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"WebProcessPlugInWithInternals" configureJSCForTesting:YES];
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 300, 300) configuration:configuration]);
+    auto inputDelegate = adoptNS([[TestInputDelegate alloc] init]);
+    [inputDelegate setFocusStartsInputSessionPolicyHandler:[] (WKWebView *, id <_WKFocusedElementInfo>) { return _WKFocusStartsInputSessionPolicyAllow; }];
+    [webView _setInputDelegate:inputDelegate.get()];
+
+    [webView synchronouslyLoadHTMLString:@"<body contenteditable='true'></body>"];
+    [webView evaluateJavaScriptAndWaitForInputSessionToChange:@"document.body.focus()"];
+    [[webView textInputContentView] insertText:@"hello" alternatives:@[@"yellow"] style:UITextAlternativeStyleNone];
+    [[webView textInputContentView] insertText:makeNSStringWithCharacter(noBreakSpace)];
+    [webView _synchronouslyExecuteEditCommand:@"MoveToBeginningOfDocument" argument:nil];
+    [webView waitForNextPresentationUpdate];
+
+    // FIXME: Change first EXPECT to EXPECT_TRUE() once trailing no-break spaces do not cause removal of alternatives.
+    // See <https://webkit.org/b/212098>.
+    EXPECT_FALSE([[webView objectByEvaluatingJavaScript:@"internals.hasDictationAlternativesMarker(0, 5)"] boolValue]); // hello
+    EXPECT_FALSE([[webView objectByEvaluatingJavaScript:@"internals.hasDictationAlternativesMarker(5, 1)"] boolValue]); // <no-break space>
+}
+
+TEST(InsertTextAlternatives, InsertSpaceInMiddle)
+{
+    auto *configuration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"WebProcessPlugInWithInternals" configureJSCForTesting:YES];
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 300, 300) configuration:configuration]);
+    auto inputDelegate = adoptNS([[TestInputDelegate alloc] init]);
+    [inputDelegate setFocusStartsInputSessionPolicyHandler:[] (WKWebView *, id <_WKFocusedElementInfo>) { return _WKFocusStartsInputSessionPolicyAllow; }];
+    [webView _setInputDelegate:inputDelegate.get()];
+
+    [webView synchronouslyLoadHTMLString:@"<body contenteditable='true'></body>"];
+    [webView evaluateJavaScriptAndWaitForInputSessionToChange:@"document.body.focus()"];
+    [[webView textInputContentView] insertText:@"hello" alternatives:@[@"yellow"] style:UITextAlternativeStyleNone];
+    [webView waitForNextPresentationUpdate];
+
+    [webView stringByEvaluatingJavaScript:@"getSelection().setPosition(document.body.firstChild, 2)"];
+    [[webView textInputContentView] insertText:@" "];
+    [webView _synchronouslyExecuteEditCommand:@"MoveToBeginningOfDocument" argument:nil];
+    [webView waitForNextPresentationUpdate];
+
+    EXPECT_FALSE([[webView objectByEvaluatingJavaScript:@"internals.hasDictationAlternativesMarker(0, 2)"] boolValue]); // he
+    EXPECT_FALSE([[webView objectByEvaluatingJavaScript:@"internals.hasDictationAlternativesMarker(2, 1)"] boolValue]); // <space>
+    EXPECT_FALSE([[webView objectByEvaluatingJavaScript:@"internals.hasDictationAlternativesMarker(3, 3)"] boolValue]); // llo
+}
+
+TEST(InsertTextAlternatives, InsertNewlineInMiddle_ExpectedFailure)
+{
+    auto *configuration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"WebProcessPlugInWithInternals" configureJSCForTesting:YES];
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 300, 300) configuration:configuration]);
+    auto inputDelegate = adoptNS([[TestInputDelegate alloc] init]);
+    [inputDelegate setFocusStartsInputSessionPolicyHandler:[] (WKWebView *, id <_WKFocusedElementInfo>) { return _WKFocusStartsInputSessionPolicyAllow; }];
+    [webView _setInputDelegate:inputDelegate.get()];
+
+    // Set CSS white-space and user-modify so that \n is inserted literally. Otherwise, it would be converted into a <br> if <body>
+    // only had "contenteditable='true'" because it is considered richly editable.
+    [webView synchronouslyLoadHTMLString:@"<body style='white-space: pre; -webkit-user-modify: read-write-plaintext-only'></body>"];
+    [webView evaluateJavaScriptAndWaitForInputSessionToChange:@"document.body.focus()"];
+    [[webView textInputContentView] insertText:@"hello" alternatives:@[@"yellow"] style:UITextAlternativeStyleNone];
+    [webView waitForNextPresentationUpdate];
+
+    [webView stringByEvaluatingJavaScript:@"getSelection().setPosition(document.body.firstChild, 2)"];
+    [[webView textInputContentView] insertText:@"\n"];
+    [webView _synchronouslyExecuteEditCommand:@"MoveToBeginningOfDocument" argument:nil];
+    [webView waitForNextPresentationUpdate];
+
+    // FIXME: These results coincidentally match UIKit, but they just feel weird: they are asymmetric to
+    // what happens when a space is inserted in the middle. Marker should be removed when marker is split.
+    EXPECT_TRUE([[webView objectByEvaluatingJavaScript:@"internals.hasDictationAlternativesMarker(0, 2)"] boolValue]); // he
+    [webView _synchronouslyExecuteEditCommand:@"MoveDown" argument:nil];
+    EXPECT_TRUE([[webView objectByEvaluatingJavaScript:@"internals.hasDictationAlternativesMarker(0, 3)"] boolValue]); // llo
+}
+
+TEST(InsertTextAlternatives, InsertNoBreakSpaceInMiddle)
+{
+    auto *configuration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"WebProcessPlugInWithInternals" configureJSCForTesting:YES];
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 300, 300) configuration:configuration]);
+    auto inputDelegate = adoptNS([[TestInputDelegate alloc] init]);
+    [inputDelegate setFocusStartsInputSessionPolicyHandler:[] (WKWebView *, id <_WKFocusedElementInfo>) { return _WKFocusStartsInputSessionPolicyAllow; }];
+    [webView _setInputDelegate:inputDelegate.get()];
+
+    // Set CSS white-space and user-modify so that \n is inserted literally. Otherwise, it would be converted into a <br> if <body>
+    // only had "contenteditable='true'" because it is considered richly editable.
+    [webView synchronouslyLoadHTMLString:@"<body style='white-space: pre; -webkit-user-modify: read-write-plaintext-only'></body>"];
+    [webView evaluateJavaScriptAndWaitForInputSessionToChange:@"document.body.focus()"];
+    [[webView textInputContentView] insertText:@"hello" alternatives:@[@"yellow"] style:UITextAlternativeStyleNone];
+    [webView waitForNextPresentationUpdate];
+
+    [webView stringByEvaluatingJavaScript:@"getSelection().setPosition(document.body.firstChild, 2)"];
+    [[webView textInputContentView] insertText:makeNSStringWithCharacter(noBreakSpace)];
+    [webView _synchronouslyExecuteEditCommand:@"MoveToBeginningOfDocument" argument:nil];
+    [webView waitForNextPresentationUpdate];
+
+    EXPECT_FALSE([[webView objectByEvaluatingJavaScript:@"internals.hasDictationAlternativesMarker(0, 2)"] boolValue]); // he
+    EXPECT_FALSE([[webView objectByEvaluatingJavaScript:@"internals.hasDictationAlternativesMarker(2, 1)"] boolValue]); // <no-break space>
+    EXPECT_FALSE([[webView objectByEvaluatingJavaScript:@"internals.hasDictationAlternativesMarker(3, 3)"] boolValue]); // llo
+}
+
+TEST(InsertTextAlternatives, InsertLeadingNonWhitespaceCharacter)
+{
+    auto *configuration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"WebProcessPlugInWithInternals" configureJSCForTesting:YES];
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 300, 300) configuration:configuration]);
+    auto inputDelegate = adoptNS([[TestInputDelegate alloc] init]);
+    [inputDelegate setFocusStartsInputSessionPolicyHandler:[] (WKWebView *, id <_WKFocusedElementInfo>) { return _WKFocusStartsInputSessionPolicyAllow; }];
+    [webView _setInputDelegate:inputDelegate.get()];
+
+    [webView synchronouslyLoadHTMLString:@"<body contenteditable='true'></body>"];
+    [webView evaluateJavaScriptAndWaitForInputSessionToChange:@"document.body.focus()"];
+    [[webView textInputContentView] insertText:@"hello" alternatives:@[@"yellow"] style:UITextAlternativeStyleNone];
+    [webView waitForNextPresentationUpdate];
+
+    [webView _synchronouslyExecuteEditCommand:@"MoveToBeginningOfDocument" argument:nil];
+    [[webView textInputContentView] insertText:@"a"];
+    [webView _synchronouslyExecuteEditCommand:@"MoveToBeginningOfDocument" argument:nil];
+    [webView waitForNextPresentationUpdate];
+
+    EXPECT_FALSE([[webView objectByEvaluatingJavaScript:@"internals.hasDictationAlternativesMarker(0, 1)"] boolValue]); // a
+    EXPECT_FALSE([[webView objectByEvaluatingJavaScript:@"internals.hasDictationAlternativesMarker(1, 5)"] boolValue]); // hello
+}
+
+TEST(InsertTextAlternatives, InsertTrailingNonWhitespaceCharacter)
+{
+    auto *configuration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"WebProcessPlugInWithInternals" configureJSCForTesting:YES];
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 300, 300) configuration:configuration]);
+    auto inputDelegate = adoptNS([[TestInputDelegate alloc] init]);
+    [inputDelegate setFocusStartsInputSessionPolicyHandler:[] (WKWebView *, id <_WKFocusedElementInfo>) { return _WKFocusStartsInputSessionPolicyAllow; }];
+    [webView _setInputDelegate:inputDelegate.get()];
+
+    [webView synchronouslyLoadHTMLString:@"<body contenteditable='true'></body>"];
+    [webView evaluateJavaScriptAndWaitForInputSessionToChange:@"document.body.focus()"];
+    [[webView textInputContentView] insertText:@"hello" alternatives:@[@"yellow"] style:UITextAlternativeStyleNone];
+    [[webView textInputContentView] insertText:@"a"];
+    [webView _synchronouslyExecuteEditCommand:@"MoveToBeginningOfDocument" argument:nil];
+    [webView waitForNextPresentationUpdate];
+
+    EXPECT_FALSE([[webView objectByEvaluatingJavaScript:@"internals.hasDictationAlternativesMarker(0, 5)"] boolValue]); // hello
+    EXPECT_FALSE([[webView objectByEvaluatingJavaScript:@"internals.hasDictationAlternativesMarker(5, 1)"] boolValue]); // a
+}
+
+TEST(InsertTextAlternatives, InsertNonWhitespaceCharacterInMiddle)
+{
+    auto *configuration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"WebProcessPlugInWithInternals" configureJSCForTesting:YES];
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 300, 300) configuration:configuration]);
+    auto inputDelegate = adoptNS([[TestInputDelegate alloc] init]);
+    [inputDelegate setFocusStartsInputSessionPolicyHandler:[] (WKWebView *, id <_WKFocusedElementInfo>) { return _WKFocusStartsInputSessionPolicyAllow; }];
+    [webView _setInputDelegate:inputDelegate.get()];
+
+    [webView synchronouslyLoadHTMLString:@"<body contenteditable='true'></body>"];
+    [webView evaluateJavaScriptAndWaitForInputSessionToChange:@"document.body.focus()"];
+    [[webView textInputContentView] insertText:@"hello" alternatives:@[@"yellow"] style:UITextAlternativeStyleNone];
+    [webView waitForNextPresentationUpdate];
+
+    [webView stringByEvaluatingJavaScript:@"getSelection().setPosition(document.body.firstChild, 2)"];
+    [[webView textInputContentView] insertText:@"a"];
+    [webView _synchronouslyExecuteEditCommand:@"MoveToBeginningOfDocument" argument:nil];
+    [webView waitForNextPresentationUpdate];
+
+    EXPECT_FALSE([[webView objectByEvaluatingJavaScript:@"internals.hasDictationAlternativesMarker(0, 2)"] boolValue]); // he
+    EXPECT_FALSE([[webView objectByEvaluatingJavaScript:@"internals.hasDictationAlternativesMarker(2, 1)"] boolValue]); // a
+    EXPECT_FALSE([[webView objectByEvaluatingJavaScript:@"internals.hasDictationAlternativesMarker(3, 3)"] boolValue]); // llo
+}
+
+TEST(InsertTextAlternatives, InsertMultipleWordsWithAlternatives)
+{
+    auto *configuration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"WebProcessPlugInWithInternals" configureJSCForTesting:YES];
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 300, 300) configuration:configuration]);
+    auto inputDelegate = adoptNS([[TestInputDelegate alloc] init]);
+    [inputDelegate setFocusStartsInputSessionPolicyHandler:[] (WKWebView *, id <_WKFocusedElementInfo>) { return _WKFocusStartsInputSessionPolicyAllow; }];
+    [webView _setInputDelegate:inputDelegate.get()];
+
+    // Set CSS white-space so that ' ' is inserted literally. Otherwise, it would be converted into a no-break space.
+    [webView synchronouslyLoadHTMLString:@"<body contenteditable='true' style='white-space: pre'></body>"];
+    [webView evaluateJavaScriptAndWaitForInputSessionToChange:@"document.body.focus()"];
+    [[webView textInputContentView] insertText:@"hello" alternatives:@[@"yellow"] style:UITextAlternativeStyleNone];
+    [[webView textInputContentView] insertText:@" "];
+    [[webView textInputContentView] insertText:@"worlds" alternatives:@[@"worm"] style:UITextAlternativeStyleNone];
+    [webView _synchronouslyExecuteEditCommand:@"MoveToBeginningOfDocument" argument:nil];
+    [webView waitForNextPresentationUpdate];
+
+    EXPECT_TRUE([[webView objectByEvaluatingJavaScript:@"internals.hasDictationAlternativesMarker(0, 5)"] boolValue]); // hello
+    EXPECT_FALSE([[webView objectByEvaluatingJavaScript:@"internals.hasDictationAlternativesMarker(5, 1)"] boolValue]); // <space>
+    EXPECT_TRUE([[webView objectByEvaluatingJavaScript:@"internals.hasDictationAlternativesMarker(6, 5)"] boolValue]); // worlds
+}
+
+} // namespace TestWebKitAPI
+
+#endif // PLATFORM(IOS_FAMILY)
+