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(), ¤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;
+ 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/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]);