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):
git-svn-id: http://svn.webkit.org/repository/webkit/trunk@253860 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/Source/WebCore/ChangeLog b/Source/WebCore/ChangeLog
index eb0f9ac..5954018 100644
--- a/Source/WebCore/ChangeLog
+++ b/Source/WebCore/ChangeLog
@@ -1,3 +1,34 @@
+2019-12-20 Ryosuke Niwa <rniwa@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_liu@apple.com>
REGRESSION (r253807): crash in storage/indexeddb/modern/opendatabase-request-private.html
diff --git a/Source/WebCore/editing/TextManipulationController.cpp b/Source/WebCore/editing/TextManipulationController.cpp
index e17bdec..12542a6 100644
--- a/Source/WebCore/editing/TextManipulationController.cpp
+++ b/Source/WebCore/editing/TextManipulationController.cpp
@@ -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,28 +297,108 @@
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(), ¤tToken });
+
+ 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;
- for (auto& newToken : replacementTokens) {
- auto it = tokenToNodeTokenPair.find(newToken.identifier);
- if (it == tokenToNodeTokenPair.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 });
+ 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);
}
- for (auto& change : changes)
- change.node->setData(change.newData);
+ Vector<Ref<Node>> currentElementStack;
+ HashSet<Ref<Node>> reusedOriginalNodes;
+ Vector<NodeInsertion> insertions;
+ for (auto& newToken : replacementTokens) {
+ auto it = tokenExchangeMap.find(newToken.identifier);
+ if (it == tokenExchangeMap.end())
+ return ManipulationResult::InvalidToken;
+
+ 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 = currentNode->eventTargetData())
+ 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() });
+ }
+ }
+
+ 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;
}
diff --git a/Tools/ChangeLog b/Tools/ChangeLog
index 02f9050..7720660 100644
--- a/Tools/ChangeLog
+++ b/Tools/ChangeLog
@@ -1,3 +1,22 @@
+2019-12-20 Ryosuke Niwa <rniwa@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_gardner@apple.com>
Paint highlights specified in CSS Highlight API
diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/TextManipulation.mm b/Tools/TestWebKitAPI/Tests/WebKitCocoa/TextManipulation.mm
index 94f9cdd..46f2323 100644
--- a/Tools/TestWebKitAPI/Tests/WebKitCocoa/TextManipulation.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKitCocoa/TextManipulation.mm
@@ -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=\"https://en.wikipedia.org/wiki/Cat\">cats</a>, <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=\"https://en.wikipedia.org/wiki/Cat\">cat</a></p>",
+ [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]);