Title: [253860] trunk
Revision
253860
Author
rn...@webkit.org
Date
2019-12-20 18:09:50 -0800 (Fri, 20 Dec 2019)

Log Message

TextManipulationController should respect new token orders
https://bugs.webkit.org/show_bug.cgi?id=205378

Reviewed by Wenson Hsieh.

Source/WebCore:

Updated TextManipulationController::replace to remove all existing content and insert new tokens in the order they appear.

To do this, we first find the common ancestor of all nodes in the paragraph and then remove all nodes in between.

Then we'd insert the node identified by the token identifier and all its ancestors at where they appear. In the case
the same token is used for the second time, we clone its node. For each leaf node, we find the closest ancestor which
had already been inserted by the previous token, and append the leaf node along with its ancestors to it.

I'm expecting to make a lot of refinements & followups to this algorithm in the future but this seems to get basics done.

Tests: TextManipulation.CompleteTextManipulationReplaceSimpleSingleParagraph
       TextManipulation.CompleteTextManipulationDisgardsTokens
       TextManipulation.CompleteTextManipulationReordersContent
       TextManipulation.CompleteTextManipulationCanSplitContent
       TextManipulation.CompleteTextManipulationCanMergeContent
       TextManipulation.CompleteTextManipulationFailWhenContentIsRemoved
       TextManipulation.CompleteTextManipulationFailWhenExcludedContentAppearsMoreThanOnce
       TextManipulation.CompleteTextManipulationPreservesExcludedContent

* editing/TextManipulationController.cpp:
(WebCore::TextManipulationController::didCreateRendererForElement):
(WebCore::TextManipulationController::completeManipulation):
(WebCore::TextManipulationController::replace):

Tools:

Added a bunch of tests for WKTextManipulation.

* TestWebKitAPI/Tests/WebKitCocoa/TextManipulation.mm:
(TextManipulation.CompleteTextManipulationReplaceSimpleSingleParagraph):
(TextManipulation.CompleteTextManipulationDisgardsTokens):
(TextManipulation.CompleteTextManipulationReordersContent):
(TextManipulation.CompleteTextManipulationCanSplitContent):
(TextManipulation.CompleteTextManipulationCanMergeContent):
(TextManipulation.CompleteTextManipulationFailWhenContentIsRemoved):
(TextManipulation.CompleteTextManipulationFailWhenExcludedContentAppearsMoreThanOnce):
(TextManipulation.CompleteTextManipulationPreservesExcludedContent):

Modified Paths

Diff

Modified: trunk/Source/WebCore/ChangeLog (253859 => 253860)


--- trunk/Source/WebCore/ChangeLog	2019-12-21 01:52:11 UTC (rev 253859)
+++ trunk/Source/WebCore/ChangeLog	2019-12-21 02:09:50 UTC (rev 253860)
@@ -1,3 +1,34 @@
+2019-12-20  Ryosuke Niwa  <rn...@webkit.org>
+
+        TextManipulationController should respect new token orders
+        https://bugs.webkit.org/show_bug.cgi?id=205378
+
+        Reviewed by Wenson Hsieh.
+
+        Updated TextManipulationController::replace to remove all existing content and insert new tokens in the order they appear.
+
+        To do this, we first find the common ancestor of all nodes in the paragraph and then remove all nodes in between.
+
+        Then we'd insert the node identified by the token identifier and all its ancestors at where they appear. In the case
+        the same token is used for the second time, we clone its node. For each leaf node, we find the closest ancestor which
+        had already been inserted by the previous token, and append the leaf node along with its ancestors to it.
+
+        I'm expecting to make a lot of refinements & followups to this algorithm in the future but this seems to get basics done.
+
+        Tests: TextManipulation.CompleteTextManipulationReplaceSimpleSingleParagraph
+               TextManipulation.CompleteTextManipulationDisgardsTokens
+               TextManipulation.CompleteTextManipulationReordersContent
+               TextManipulation.CompleteTextManipulationCanSplitContent
+               TextManipulation.CompleteTextManipulationCanMergeContent
+               TextManipulation.CompleteTextManipulationFailWhenContentIsRemoved
+               TextManipulation.CompleteTextManipulationFailWhenExcludedContentAppearsMoreThanOnce
+               TextManipulation.CompleteTextManipulationPreservesExcludedContent
+
+        * editing/TextManipulationController.cpp:
+        (WebCore::TextManipulationController::didCreateRendererForElement):
+        (WebCore::TextManipulationController::completeManipulation):
+        (WebCore::TextManipulationController::replace):
+
 2019-12-20  Sihui Liu  <sihui_...@apple.com>
 
         REGRESSION (r253807): crash in storage/indexeddb/modern/opendatabase-request-private.html

Modified: trunk/Source/WebCore/editing/TextManipulationController.cpp (253859 => 253860)


--- trunk/Source/WebCore/editing/TextManipulationController.cpp	2019-12-21 01:52:11 UTC (rev 253859)
+++ trunk/Source/WebCore/editing/TextManipulationController.cpp	2019-12-21 02:09:50 UTC (rev 253860)
@@ -30,7 +30,11 @@
 #include "Editing.h"
 #include "ElementAncestorIterator.h"
 #include "EventLoop.h"
+#include "NodeTraversal.h"
+#include "PseudoElement.h"
+#include "Range.h"
 #include "ScriptDisallowedScope.h"
+#include "Text.h"
 #include "TextIterator.h"
 #include "VisibleUnits.h"
 
@@ -176,7 +180,12 @@
 {
     if (m_mutatedElements.computesEmpty())
         scheduleObservartionUpdate();
-    m_mutatedElements.add(element);
+
+    if (is<PseudoElement>(element)) {
+        if (auto* host = downcast<PseudoElement>(element).hostElement())
+            m_mutatedElements.add(*host);
+    } else
+        m_mutatedElements.add(element);
 }
 
 using PositionTuple = std::tuple<RefPtr<Node>, unsigned, unsigned>;
@@ -247,24 +256,40 @@
     if (itemIterator == m_items.end())
         return ManipulationResult::InvalidItem;
 
-    auto didReplace = replace(itemIterator->value, replacementTokens);
-
+    ManipulationItem item;
+    std::exchange(item, itemIterator->value);
     m_items.remove(itemIterator);
 
-    return didReplace;
+    return replace(item, replacementTokens);
 }
 
-struct DOMChange {
-    Ref<CharacterData> node;
+struct TokenExchangeData {
+    RefPtr<Node> node;
+    String originalContent;
+    bool isExcluded { false };
+    bool isConsumed { false };
+};
+
+struct ReplacementData {
+    Ref<Node> originalNode;
     String newData;
 };
 
+struct NodeInsertion {
+    RefPtr<Node> parentIfDifferentFromCommonAncestor;
+    Ref<Node> child;
+};
+
 auto TextManipulationController::replace(const ManipulationItem& item, const Vector<ManipulationToken>& replacementTokens) -> ManipulationResult
 {
+    if (item.start.isOrphan() || item.end.isOrphan())
+        return ManipulationResult::ContentChanged;
+
     TextIterator iterator { item.start, item.end };
     size_t currentTokenIndex = 0;
-    HashMap<TokenIdentifier, std::pair<RefPtr<Node>, const ManipulationToken*>> tokenToNodeTokenPair;
+    HashMap<TokenIdentifier, TokenExchangeData> tokenExchangeMap;
 
+    RefPtr<Node> commonAncestor;
     while (!iterator.atEnd()) {
         auto string = iterator.text().toString();
         if (currentTokenIndex >= item.tokens.size())
@@ -272,29 +297,109 @@
         auto& currentToken = item.tokens[currentTokenIndex];
         if (iterator.text() != currentToken.content)
             return ManipulationResult::ContentChanged;
-        tokenToNodeTokenPair.set(currentToken.identifier, std::pair<RefPtr<Node>, const ManipulationToken*> { iterator.node(), &currentToken });
+
+        auto currentNode = makeRefPtr(iterator.node());
+        tokenExchangeMap.set(currentToken.identifier, TokenExchangeData { currentNode.copyRef(), currentToken.content, currentToken.isExcluded });
+
+        if (currentNode) {
+            // FIXME: Take care of when currentNode is nullptr.
+            if (!commonAncestor)
+                commonAncestor = currentNode;
+            else if (!currentNode->isDescendantOf(commonAncestor.get())) {
+                commonAncestor = Range::commonAncestorContainer(commonAncestor.get(), currentNode.get());
+                ASSERT(commonAncestor);
+            }
+        }
+
         iterator.advance();
         ++currentTokenIndex;
     }
+    ASSERT(commonAncestor);
 
-    // FIXME: This doesn't preseve the order of the replacement at all.
-    Vector<DOMChange> changes;
+    RefPtr<Node> nodeAfterStart = item.start.computeNodeAfterPosition();
+    if (!nodeAfterStart)
+        nodeAfterStart = item.start.containerNode();
+
+    RefPtr<Node> nodeAfterEnd = item.end.computeNodeAfterPosition();
+    if (!nodeAfterEnd)
+        nodeAfterEnd = NodeTraversal::nextSkippingChildren(*item.end.containerNode());
+
+    HashSet<Ref<Node>> nodesToRemove;
+    for (RefPtr<Node> currentNode = nodeAfterStart; currentNode && currentNode != nodeAfterEnd; currentNode = NodeTraversal::next(*currentNode)) {
+        if (commonAncestor == currentNode)
+            commonAncestor = currentNode->parentNode();
+        nodesToRemove.add(*currentNode);
+    }
+
+    Vector<Ref<Node>> currentElementStack;
+    HashSet<Ref<Node>> reusedOriginalNodes;
+    Vector<NodeInsertion> insertions;
     for (auto& newToken : replacementTokens) {
-        auto it = tokenToNodeTokenPair.find(newToken.identifier);
-        if (it == tokenToNodeTokenPair.end())
+        auto it = tokenExchangeMap.find(newToken.identifier);
+        if (it == tokenExchangeMap.end())
             return ManipulationResult::InvalidToken;
-        auto& oldToken = *it->value.second;
-        if (oldToken.isExcluded)
-            return ManipulationResult::ExclusionViolation;
-        auto* node = it->value.first.get();
-        if (!node || !is<CharacterData>(*node))
-            continue;
-        changes.append({ downcast<CharacterData>(*node), newToken.content });
+
+        auto& exchangeData = it->value;
+
+        RefPtr<Node> contentNode;
+        if (exchangeData.isExcluded) {
+            if (exchangeData.isConsumed)
+                return ManipulationResult::ExclusionViolation;
+            exchangeData.isConsumed = true;
+            if (!newToken.content.isNull() && newToken.content != exchangeData.originalContent)
+                return ManipulationResult::ExclusionViolation;
+            contentNode = Text::create(commonAncestor->document(), exchangeData.originalContent);
+        } else
+            contentNode = Text::create(commonAncestor->document(), newToken.content);
+
+        auto& originalNode = exchangeData.node ? *exchangeData.node : *commonAncestor;
+        RefPtr<ContainerNode> currentNode = is<ContainerNode>(originalNode) ? &downcast<ContainerNode>(originalNode) : originalNode.parentNode();
+
+        Vector<Ref<Node>> currentAncestors;
+        for (; currentNode && currentNode != commonAncestor; currentNode = currentNode->parentNode())
+            currentAncestors.append(*currentNode);
+        currentAncestors.reverse();
+
+        size_t i =0;
+        while (i < currentElementStack.size() && i < currentAncestors.size() && currentElementStack[i].ptr() == currentAncestors[i].ptr())
+            ++i;
+
+        if (i == currentElementStack.size() && i == currentAncestors.size())
+            insertions.append(NodeInsertion { currentElementStack.size() ? currentElementStack.last().ptr() : nullptr, contentNode.releaseNonNull() });
+        else {
+            if (i < currentElementStack.size())
+                currentElementStack.shrink(i);
+            for (;i < currentAncestors.size(); ++i) {
+                Ref<Node> currentNode = currentAncestors[i].copyRef();
+                if (!reusedOriginalNodes.add(currentNode.copyRef()).isNewEntry) {
+                    auto clonedNode = currentNode->cloneNodeInternal(currentNode->document(), Node::CloningOperation::OnlySelf);
+                    if (auto* data = ""
+                        data->eventListenerMap.copyEventListenersNotCreatedFromMarkupToTarget(clonedNode.ptr());
+                    currentNode = WTFMove(clonedNode);
+                }
+
+                insertions.append(NodeInsertion { currentElementStack.size() ? currentElementStack.last().ptr() : nullptr, currentNode.copyRef() });
+                currentElementStack.append(WTFMove(currentNode));
+            }
+            insertions.append(NodeInsertion { currentElementStack.size() ? currentElementStack.last().ptr() : nullptr, contentNode.releaseNonNull() });
+        }
     }
 
-    for (auto& change : changes)
-        change.node->setData(change.newData);
+    Position insertionPoint = item.start;
+    while (insertionPoint.containerNode() != commonAncestor)
+        insertionPoint = positionInParentBeforeNode(insertionPoint.containerNode());
+    ASSERT(!insertionPoint.isNull());
 
+    for (auto& node : nodesToRemove)
+        node->remove();
+
+    for (auto& insertion : insertions) {
+        if (!insertion.parentIfDifferentFromCommonAncestor)
+            insertionPoint.containerNode()->insertBefore(insertion.child, insertionPoint.computeNodeBeforePosition());
+        else
+            insertion.parentIfDifferentFromCommonAncestor->appendChild(insertion.child);
+    }
+
     return ManipulationResult::Success;
 }
 

Modified: trunk/Tools/ChangeLog (253859 => 253860)


--- trunk/Tools/ChangeLog	2019-12-21 01:52:11 UTC (rev 253859)
+++ trunk/Tools/ChangeLog	2019-12-21 02:09:50 UTC (rev 253860)
@@ -1,3 +1,22 @@
+2019-12-20  Ryosuke Niwa  <rn...@webkit.org>
+
+        TextManipulationController should respect new token orders
+        https://bugs.webkit.org/show_bug.cgi?id=205378
+
+        Reviewed by Wenson Hsieh.
+
+        Added a bunch of tests for WKTextManipulation.
+
+        * TestWebKitAPI/Tests/WebKitCocoa/TextManipulation.mm:
+        (TextManipulation.CompleteTextManipulationReplaceSimpleSingleParagraph):
+        (TextManipulation.CompleteTextManipulationDisgardsTokens):
+        (TextManipulation.CompleteTextManipulationReordersContent):
+        (TextManipulation.CompleteTextManipulationCanSplitContent):
+        (TextManipulation.CompleteTextManipulationCanMergeContent):
+        (TextManipulation.CompleteTextManipulationFailWhenContentIsRemoved):
+        (TextManipulation.CompleteTextManipulationFailWhenExcludedContentAppearsMoreThanOnce):
+        (TextManipulation.CompleteTextManipulationPreservesExcludedContent):
+
 2019-12-20  Megan Gardner  <megan_gard...@apple.com>
 
         Paint highlights specified in CSS Highlight API

Modified: trunk/Tools/TestWebKitAPI/Tests/WebKitCocoa/TextManipulation.mm (253859 => 253860)


--- trunk/Tools/TestWebKitAPI/Tests/WebKitCocoa/TextManipulation.mm	2019-12-21 01:52:11 UTC (rev 253859)
+++ trunk/Tools/TestWebKitAPI/Tests/WebKitCocoa/TextManipulation.mm	2019-12-21 02:09:50 UTC (rev 253860)
@@ -450,6 +450,72 @@
     return adoptNS([[_WKTextManipulationItem alloc] initWithIdentifier:itemIdentifier tokens:wkTokens.get()]);
 }
 
+TEST(TextManipulation, CompleteTextManipulationReplaceSimpleSingleParagraph)
+{
+    auto delegate = adoptNS([[TextManipulationDelegate alloc] init]);
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 400, 400)]);
+    [webView _setTextManipulationDelegate:delegate.get()];
+
+    [webView synchronouslyLoadHTMLString:@"<!DOCTYPE html>"
+        "<html><body><p>helllo, wooorld</p></body></html>"];
+
+    done = false;
+    [webView _startTextManipulationsWithConfiguration:nil completion:^{
+        done = true;
+    }];
+    TestWebKitAPI::Util::run(&done);
+
+    auto *items = [delegate items];
+    EXPECT_EQ(items.count, 1UL);
+    EXPECT_EQ(items[0].tokens.count, 1UL);
+    EXPECT_STREQ("helllo, wooorld", items[0].tokens[0].content.UTF8String);
+
+    done = false;
+    [webView _completeTextManipulation:(_WKTextManipulationItem *)createItem(items[0].identifier, {
+        { items[0].tokens[0].identifier, @"hello, world" },
+    }).get() completion:^(BOOL success) {
+        EXPECT_TRUE(success);
+        done = true;
+    }];
+    TestWebKitAPI::Util::run(&done);
+    EXPECT_WK_STREQ("<p>hello, world</p>", [webView stringByEvaluatingJavaScript:@"document.body.innerHTML"]);
+}
+
+TEST(TextManipulation, CompleteTextManipulationDisgardsTokens)
+{
+    auto delegate = adoptNS([[TextManipulationDelegate alloc] init]);
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 400, 400)]);
+    [webView _setTextManipulationDelegate:delegate.get()];
+
+    [webView synchronouslyLoadHTMLString:@"<!DOCTYPE html>"
+        "<html><body><p>hello, <b>world</b>. <i>WebKit</i></p></body></html>"];
+
+    done = false;
+    [webView _startTextManipulationsWithConfiguration:nil completion:^{
+        done = true;
+    }];
+    TestWebKitAPI::Util::run(&done);
+
+    auto *items = [delegate items];
+    EXPECT_EQ(items.count, 1UL);
+    EXPECT_EQ(items[0].tokens.count, 4UL);
+    EXPECT_STREQ("hello, ", items[0].tokens[0].content.UTF8String);
+    EXPECT_STREQ("world", items[0].tokens[1].content.UTF8String);
+    EXPECT_STREQ(". ", items[0].tokens[2].content.UTF8String);
+    EXPECT_STREQ("WebKit", items[0].tokens[3].content.UTF8String);
+
+    done = false;
+    [webView _completeTextManipulation:(_WKTextManipulationItem *)createItem(items[0].identifier, {
+        { items[0].tokens[0].identifier, @"hello, " },
+        { items[0].tokens[3].identifier, @"WebKit" },
+    }).get() completion:^(BOOL success) {
+        EXPECT_TRUE(success);
+        done = true;
+    }];
+    TestWebKitAPI::Util::run(&done);
+    EXPECT_WK_STREQ("<p>hello, <i>WebKit</i></p>", [webView stringByEvaluatingJavaScript:@"document.body.innerHTML"]);
+}
+
 TEST(TextManipulation, CompleteTextManipulationReplaceSimpleParagraphContent)
 {
     auto delegate = adoptNS([[TextManipulationDelegate alloc] init]);
@@ -502,6 +568,114 @@
         [webView stringByEvaluatingJavaScript:@"document.body.innerHTML"]);
 }
 
+TEST(TextManipulation, CompleteTextManipulationReordersContent)
+{
+    auto delegate = adoptNS([[TextManipulationDelegate alloc] init]);
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 400, 400)]);
+    [webView _setTextManipulationDelegate:delegate.get()];
+
+    [webView synchronouslyLoadHTMLString:@"<!DOCTYPE html>"
+        "<html><body><p><a href="" <i>I</i> are</p></body></html>"];
+
+    done = false;
+    [webView _startTextManipulationsWithConfiguration:nil completion:^{
+        done = true;
+    }];
+    TestWebKitAPI::Util::run(&done);
+
+    auto *items = [delegate items];
+    EXPECT_EQ(items.count, 1UL);
+    EXPECT_EQ(items[0].tokens.count, 4UL);
+    EXPECT_STREQ("cats", items[0].tokens[0].content.UTF8String);
+    EXPECT_STREQ(", ", items[0].tokens[1].content.UTF8String);
+    EXPECT_STREQ("I", items[0].tokens[2].content.UTF8String);
+    EXPECT_STREQ(" are", items[0].tokens[3].content.UTF8String);
+
+    done = false;
+    [webView _completeTextManipulation:(_WKTextManipulationItem *)createItem(items[0].identifier, {
+        { items[0].tokens[2].identifier, @"I" },
+        { items[0].tokens[3].identifier, @"'m a " },
+        { items[0].tokens[0].identifier, @"cat" },
+    }).get() completion:^(BOOL success) {
+        EXPECT_TRUE(success);
+        done = true;
+    }];
+    TestWebKitAPI::Util::run(&done);
+    EXPECT_WK_STREQ("<p><i>I</i>'m a <a href=""
+        [webView stringByEvaluatingJavaScript:@"document.body.innerHTML"]);
+}
+
+TEST(TextManipulation, CompleteTextManipulationCanSplitContent)
+{
+    auto delegate = adoptNS([[TextManipulationDelegate alloc] init]);
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 400, 400)]);
+    [webView _setTextManipulationDelegate:delegate.get()];
+
+    [webView synchronouslyLoadHTMLString:@"<!DOCTYPE html>"
+        "<html><body><p id=\"paragraph\"><b class=\"hello-world\">hello world</b> WebKit</p></body></html>"];
+    [webView stringByEvaluatingJavaScript:@"paragraph.firstChild.addEventListener('click', () => window.didClick = true)"];
+
+    done = false;
+    [webView _startTextManipulationsWithConfiguration:nil completion:^{
+        done = true;
+    }];
+    TestWebKitAPI::Util::run(&done);
+
+    auto *items = [delegate items];
+    EXPECT_EQ(items.count, 1UL);
+    EXPECT_EQ(items[0].tokens.count, 2UL);
+    EXPECT_STREQ("hello world", items[0].tokens[0].content.UTF8String);
+    EXPECT_STREQ(" WebKit", items[0].tokens[1].content.UTF8String);
+
+    done = false;
+    [webView _completeTextManipulation:(_WKTextManipulationItem *)createItem(items[0].identifier, {
+        { items[0].tokens[0].identifier, @"hello" },
+        { items[0].tokens[1].identifier, @" WebKit " },
+        { items[0].tokens[0].identifier, @"world" },
+    }).get() completion:^(BOOL success) {
+        EXPECT_TRUE(success);
+        done = true;
+    }];
+    TestWebKitAPI::Util::run(&done);
+    EXPECT_WK_STREQ("<p id=\"paragraph\"><b class=\"hello-world\">hello</b> WebKit <b class=\"hello-world\">world</b></p>",
+        [webView stringByEvaluatingJavaScript:@"document.body.innerHTML"]);
+    EXPECT_TRUE([webView stringByEvaluatingJavaScript:@"didClick = false; paragraph.firstChild.click(); didClick"].boolValue);
+    EXPECT_TRUE([webView stringByEvaluatingJavaScript:@"didClick = false; paragraph.lastChild.click(); didClick"].boolValue);
+}
+
+TEST(TextManipulation, CompleteTextManipulationCanMergeContent)
+{
+    auto delegate = adoptNS([[TextManipulationDelegate alloc] init]);
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 400, 400)]);
+    [webView _setTextManipulationDelegate:delegate.get()];
+
+    [webView synchronouslyLoadHTMLString:@"<!DOCTYPE html><html><body><p><b>hello <i>world</i> WebKit</b></p></body></html>"];
+
+    done = false;
+    [webView _startTextManipulationsWithConfiguration:nil completion:^{
+        done = true;
+    }];
+    TestWebKitAPI::Util::run(&done);
+
+    auto *items = [delegate items];
+    EXPECT_EQ(items.count, 1UL);
+    EXPECT_EQ(items[0].tokens.count, 3UL);
+    EXPECT_STREQ("hello ", items[0].tokens[0].content.UTF8String);
+    EXPECT_STREQ("world", items[0].tokens[1].content.UTF8String);
+    EXPECT_STREQ(" WebKit", items[0].tokens[2].content.UTF8String);
+
+    done = false;
+    [webView _completeTextManipulation:(_WKTextManipulationItem *)createItem(items[0].identifier, {
+        { items[0].tokens[0].identifier, @"hello " },
+        { items[0].tokens[2].identifier, @"world" },
+    }).get() completion:^(BOOL success) {
+        EXPECT_TRUE(success);
+        done = true;
+    }];
+    TestWebKitAPI::Util::run(&done);
+    EXPECT_WK_STREQ("<p><b>hello world</b></p>", [webView stringByEvaluatingJavaScript:@"document.body.innerHTML"]);
+}
+
 TEST(TextManipulation, CompleteTextManipulationFailWhenContentIsChanged)
 {
     auto delegate = adoptNS([[TextManipulationDelegate alloc] init]);
@@ -540,6 +714,38 @@
         [webView stringByEvaluatingJavaScript:@"document.body.innerHTML"]);
 }
 
+TEST(TextManipulation, CompleteTextManipulationFailWhenContentIsRemoved)
+{
+    auto delegate = adoptNS([[TextManipulationDelegate alloc] init]);
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 400, 400)]);
+    [webView _setTextManipulationDelegate:delegate.get()];
+
+    [webView synchronouslyLoadHTMLString:@"<!DOCTYPE html><html><body><p>hello, world</p></body></html>"];
+
+    done = false;
+    [webView _startTextManipulationsWithConfiguration:nil completion:^{
+        done = true;
+    }];
+    TestWebKitAPI::Util::run(&done);
+
+    auto *items = [delegate items];
+    EXPECT_EQ(items.count, 1UL);
+    EXPECT_EQ(items[0].tokens.count, 1UL);
+    EXPECT_STREQ("hello, world", items[0].tokens[0].content.UTF8String);
+
+    [webView stringByEvaluatingJavaScript:@"document.body.innerHTML = 'new content'"];
+
+    done = false;
+    [webView _completeTextManipulation:(_WKTextManipulationItem *)createItem(items[0].identifier, {
+        { items[0].tokens[0].identifier, @"hey" },
+    }).get() completion:^(BOOL success) {
+        EXPECT_FALSE(success);
+        done = true;
+    }];
+    TestWebKitAPI::Util::run(&done);
+    EXPECT_WK_STREQ("new content", [webView stringByEvaluatingJavaScript:@"document.body.innerHTML"]);
+}
+
 TEST(TextManipulation, CompleteTextManipulationFailWhenDocumentHasBeenNavigatedAway)
 {
     auto delegate = adoptNS([[TextManipulationDelegate alloc] init]);
@@ -593,7 +799,7 @@
 
     RetainPtr<_WKTextManipulationConfiguration> configuration = adoptNS([[_WKTextManipulationConfiguration alloc] init]);
     [configuration setExclusionRules:@[
-        [[[_WKTextManipulationExclusionRule alloc] initExclusion:(BOOL)YES forElement:@"p"] autorelease],
+        [[[_WKTextManipulationExclusionRule alloc] initExclusion:(BOOL)YES forElement:@"em"] autorelease],
     ]];
 
     done = false;
@@ -610,6 +816,7 @@
 
     done = false;
     [webView _completeTextManipulation:(_WKTextManipulationItem *)createItem(items[0].identifier, {
+        { items[0].tokens[0].identifier, @"Hello," },
         { items[0].tokens[1].identifier, @"WebKit" },
     }).get() completion:^(BOOL success) {
         EXPECT_FALSE(success);
@@ -620,6 +827,84 @@
     EXPECT_WK_STREQ("<p>hi, <em>WebKitten</em></p>", [webView stringByEvaluatingJavaScript:@"document.body.innerHTML"]);
 }
 
+TEST(TextManipulation, CompleteTextManipulationFailWhenExcludedContentAppearsMoreThanOnce)
+{
+    auto delegate = adoptNS([[TextManipulationDelegate alloc] init]);
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 400, 400)]);
+    [webView _setTextManipulationDelegate:delegate.get()];
+
+    [webView synchronouslyLoadTestPageNamed:@"simple"];
+    [webView stringByEvaluatingJavaScript:@"document.body.innerHTML = '<p>hi, <em>WebKitten</em></p>'"];
+
+    RetainPtr<_WKTextManipulationConfiguration> configuration = adoptNS([[_WKTextManipulationConfiguration alloc] init]);
+    [configuration setExclusionRules:@[
+        [[[_WKTextManipulationExclusionRule alloc] initExclusion:(BOOL)YES forElement:@"em"] autorelease],
+    ]];
+
+    done = false;
+    [webView _startTextManipulationsWithConfiguration:configuration.get() completion:^{
+        done = true;
+    }];
+    TestWebKitAPI::Util::run(&done);
+
+    auto *items = [delegate items];
+    EXPECT_EQ(items.count, 1UL);
+    EXPECT_EQ(items[0].tokens.count, 2UL);
+    EXPECT_STREQ("hi, ", items[0].tokens[0].content.UTF8String);
+    EXPECT_STREQ("WebKitten", items[0].tokens[1].content.UTF8String);
+
+    done = false;
+    [webView _completeTextManipulation:(_WKTextManipulationItem *)createItem(items[0].identifier, {
+        { items[0].tokens[1].identifier, nil },
+        { items[0].tokens[0].identifier, @"Hello," },
+        { items[0].tokens[1].identifier, nil },
+    }).get() completion:^(BOOL success) {
+        EXPECT_FALSE(success);
+        done = true;
+    }];
+
+    TestWebKitAPI::Util::run(&done);
+    EXPECT_WK_STREQ("<p>hi, <em>WebKitten</em></p>", [webView stringByEvaluatingJavaScript:@"document.body.innerHTML"]);
+}
+
+TEST(TextManipulation, CompleteTextManipulationPreservesExcludedContent)
+{
+    auto delegate = adoptNS([[TextManipulationDelegate alloc] init]);
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 400, 400)]);
+    [webView _setTextManipulationDelegate:delegate.get()];
+
+    [webView synchronouslyLoadHTMLString:@"<!DOCTYPE html><html><body><p>hi, <em>WebKitten</em></p></body></html>"];
+
+    RetainPtr<_WKTextManipulationConfiguration> configuration = adoptNS([[_WKTextManipulationConfiguration alloc] init]);
+    [configuration setExclusionRules:@[
+        [[[_WKTextManipulationExclusionRule alloc] initExclusion:(BOOL)YES forElement:@"em"] autorelease],
+    ]];
+
+    done = false;
+    [webView _startTextManipulationsWithConfiguration:configuration.get() completion:^{
+        done = true;
+    }];
+    TestWebKitAPI::Util::run(&done);
+
+    auto *items = [delegate items];
+    EXPECT_EQ(items.count, 1UL);
+    EXPECT_EQ(items[0].tokens.count, 2UL);
+    EXPECT_STREQ("hi, ", items[0].tokens[0].content.UTF8String);
+    EXPECT_STREQ("WebKitten", items[0].tokens[1].content.UTF8String);
+
+    done = false;
+    [webView _completeTextManipulation:(_WKTextManipulationItem *)createItem(items[0].identifier, {
+        { items[0].tokens[0].identifier, @"Hello, " },
+        { items[0].tokens[1].identifier, nil },
+    }).get() completion:^(BOOL success) {
+        EXPECT_TRUE(success);
+        done = true;
+    }];
+
+    TestWebKitAPI::Util::run(&done);
+    EXPECT_WK_STREQ("<p>Hello, <em>WebKitten</em></p>", [webView stringByEvaluatingJavaScript:@"document.body.innerHTML"]);
+}
+
 TEST(TextManipulation, TextManipulationTokenDebugDescription)
 {
     auto token = adoptNS([[_WKTextManipulationToken alloc] init]);
_______________________________________________
webkit-changes mailing list
webkit-changes@lists.webkit.org
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to