Text manipulation should exclude text rendered using icon-only fonts
authorwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 25 Jun 2020 20:37:34 +0000 (20:37 +0000)
committerwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 25 Jun 2020 20:37:34 +0000 (20:37 +0000)
https://bugs.webkit.org/show_bug.cgi?id=213446
<rdar://problem/63734298>

Reviewed by Myles Maxfield.

Source/WebCore:

See below for more details.

Test: TextManipulation.StartTextManipulationExcludesTextRenderedAsIcons

* editing/TextManipulationController.cpp:
(WebCore::TextManipulationController::shouldExcludeNodeBasedOnStyle):
(WebCore::TextManipulationController::parse):

Augment token exclusion rules so that we're able to exclude certain text nodes from text manipulation, based on
their style (more specifically, based on their font family). Ask the text node's primary font if it is likely to
be used only for rendering icons, and cache the result in TextManipulationController to avoid running the
heuristic an excessive number of times for each font that appears in the document. We currently only run this
for the node's primary font when initiating translation, though we may want to change this in the future so that
node exclusion is recomputed when the primary font of a node changes.

Note that in the case where an icon font fails to load, we won't exclude the text node from translation,
since the primary font will be a fallback. This is intentional, since the node would appear in the document as
normally rendered text (rather than icons), which should be translated.

* editing/TextManipulationController.h:
* platform/graphics/Font.cpp:
(WebCore::Font::isProbablyOnlyUsedToRenderIcons const):
* platform/graphics/Font.h:
* platform/graphics/FontPlatformData.cpp:
(WebCore::FontPlatformData::familyName const):
* platform/graphics/FontPlatformData.h:
* platform/graphics/cocoa/FontCocoa.mm:
(WebCore::Font::isProbablyOnlyUsedToRenderIcons const):

Add a heuristic to detect whether a platform font should be excluded from text manipulation. For now, we only
return true if we suspect that this font is an "icon-only" font (for instance, the "Material Icons" font). We
guess that a font is *probably* an icon-only font if the font supports only characters from the basic or PUA
unicode planes, and the glyph bounds for the set of supported non-control basic latin characters are all empty.

* platform/graphics/cocoa/FontPlatformDataCocoa.mm:
(WebCore::FontPlatformData::familyName const):

Add a platform `familyName()` helper method on `FontPlatformData`. On Cocoa platforms, this wraps a call to
`CTFontCopyFamilyName`.

Tools:

Add a new API test.

* TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj:
* TestWebKitAPI/Tests/WebKitCocoa/Ahem.ttf: Added.
* TestWebKitAPI/Tests/WebKitCocoa/SpaceOnly.otf: Added.
* TestWebKitAPI/Tests/WebKitCocoa/TextManipulation.mm:
(TestWebKitAPI::TEST):

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

15 files changed:
Source/WebCore/ChangeLog
Source/WebCore/PAL/pal/spi/cocoa/CoreTextSPI.h
Source/WebCore/editing/TextManipulationController.cpp
Source/WebCore/editing/TextManipulationController.h
Source/WebCore/platform/graphics/Font.cpp
Source/WebCore/platform/graphics/Font.h
Source/WebCore/platform/graphics/FontPlatformData.cpp
Source/WebCore/platform/graphics/FontPlatformData.h
Source/WebCore/platform/graphics/cocoa/FontCocoa.mm
Source/WebCore/platform/graphics/cocoa/FontPlatformDataCocoa.mm
Tools/ChangeLog
Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj
Tools/TestWebKitAPI/Tests/WebKitCocoa/Ahem.ttf [new file with mode: 0644]
Tools/TestWebKitAPI/Tests/WebKitCocoa/SpaceOnly.otf [new file with mode: 0644]
Tools/TestWebKitAPI/Tests/WebKitCocoa/TextManipulation.mm

index f65540f..1307d60 100644 (file)
@@ -1,3 +1,51 @@
+2020-06-25  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        Text manipulation should exclude text rendered using icon-only fonts
+        https://bugs.webkit.org/show_bug.cgi?id=213446
+        <rdar://problem/63734298>
+
+        Reviewed by Myles Maxfield.
+
+        See below for more details.
+
+        Test: TextManipulation.StartTextManipulationExcludesTextRenderedAsIcons
+
+        * editing/TextManipulationController.cpp:
+        (WebCore::TextManipulationController::shouldExcludeNodeBasedOnStyle):
+        (WebCore::TextManipulationController::parse):
+
+        Augment token exclusion rules so that we're able to exclude certain text nodes from text manipulation, based on
+        their style (more specifically, based on their font family). Ask the text node's primary font if it is likely to
+        be used only for rendering icons, and cache the result in TextManipulationController to avoid running the
+        heuristic an excessive number of times for each font that appears in the document. We currently only run this
+        for the node's primary font when initiating translation, though we may want to change this in the future so that
+        node exclusion is recomputed when the primary font of a node changes.
+
+        Note that in the case where an icon font fails to load, we won't exclude the text node from translation,
+        since the primary font will be a fallback. This is intentional, since the node would appear in the document as
+        normally rendered text (rather than icons), which should be translated.
+
+        * editing/TextManipulationController.h:
+        * platform/graphics/Font.cpp:
+        (WebCore::Font::isProbablyOnlyUsedToRenderIcons const):
+        * platform/graphics/Font.h:
+        * platform/graphics/FontPlatformData.cpp:
+        (WebCore::FontPlatformData::familyName const):
+        * platform/graphics/FontPlatformData.h:
+        * platform/graphics/cocoa/FontCocoa.mm:
+        (WebCore::Font::isProbablyOnlyUsedToRenderIcons const):
+
+        Add a heuristic to detect whether a platform font should be excluded from text manipulation. For now, we only
+        return true if we suspect that this font is an "icon-only" font (for instance, the "Material Icons" font). We
+        guess that a font is *probably* an icon-only font if the font supports only characters from the basic or PUA
+        unicode planes, and the glyph bounds for the set of supported non-control basic latin characters are all empty.
+
+        * platform/graphics/cocoa/FontPlatformDataCocoa.mm:
+        (WebCore::FontPlatformData::familyName const):
+
+        Add a platform `familyName()` helper method on `FontPlatformData`. On Cocoa platforms, this wraps a call to
+        `CTFontCopyFamilyName`.
+
 2020-06-25  Kate Cheney  <katherine_cheney@apple.com>
 
         App-bound domain service worker registrations should be limited
index b90d186..213938d 100644 (file)
@@ -101,6 +101,7 @@ CTTypesetterRef CTTypesetterCreateWithUniCharProviderAndOptions(CTUniCharProvide
 bool CTFontGetVerticalGlyphsForCharacters(CTFontRef, const UniChar characters[], CGGlyph glyphs[], CFIndex count);
 void CTFontGetUnsummedAdvancesForGlyphsAndStyle(CTFontRef, CTFontOrientation, CGFontRenderingStyle, const CGGlyph[], CGSize advances[], CFIndex count);
 CTFontDescriptorRef CTFontDescriptorCreateForCSSFamily(CFStringRef cssFamily, CFStringRef language);
+bool CTFontGetGlyphsForCharacterRange(CTFontRef, CGGlyph glyphs[], CFRange);
 
 CTFontDescriptorRef CTFontDescriptorCreateForUIType(CTFontUIFontType, CGFloat size, CFStringRef language);
 CTFontDescriptorRef CTFontDescriptorCreateWithTextStyle(CFStringRef style, CFStringRef size, CFStringRef language);
index 5590850..b5ffb9d 100644 (file)
@@ -37,6 +37,7 @@
 #include "HTMLNames.h"
 #include "HTMLParserIdioms.h"
 #include "InputTypeNames.h"
+#include "NodeRenderStyle.h"
 #include "NodeTraversal.h"
 #include "PseudoElement.h"
 #include "Range.h"
@@ -330,10 +331,33 @@ TextManipulationController::ManipulationUnit TextManipulationController::createU
     return unit;
 }
 
+bool TextManipulationController::shouldExcludeNodeBasedOnStyle(const Node& node)
+{
+    auto* style = node.renderStyle();
+    if (!style)
+        return false;
+
+    auto& font = style->fontCascade().primaryFont();
+    auto familyName = font.platformData().familyName();
+    if (familyName.isEmpty())
+        return false;
+
+    auto iter = m_cachedFontFamilyExclusionResults.find(familyName);
+    if (iter != m_cachedFontFamilyExclusionResults.end())
+        return iter->value;
+
+    // FIXME: We should reconsider whether a node should be excluded if the primary font
+    // used to render the node changes, since this "icon font" heuristic may return a
+    // different result.
+    bool result = font.isProbablyOnlyUsedToRenderIcons();
+    m_cachedFontFamilyExclusionResults.set(familyName, result);
+    return result;
+}
+
 void TextManipulationController::parse(ManipulationUnit& unit, const String& text, Node& textNode)
 {
     ExclusionRuleMatcher exclusionRuleMatcher(m_exclusionRules);
-    bool isNodeExcluded = exclusionRuleMatcher.isExcluded(&textNode);
+    bool isNodeExcluded = exclusionRuleMatcher.isExcluded(&textNode) || shouldExcludeNodeBasedOnStyle(textNode);
     size_t positionOfLastNonHTMLSpace = WTF::notFound;
     size_t startPositionOfCurrentToken = 0;
     size_t index = 0;
index d6acd3c..9498f62 100644 (file)
@@ -159,6 +159,8 @@ private:
     ManipulationUnit createUnit(const Vector<String>&, Node&);
     void parse(ManipulationUnit&, const String&, Node&);
 
+    bool shouldExcludeNodeBasedOnStyle(const Node&);
+
     void addItem(ManipulationItemData&&);
     void addItemIfPossible(Vector<ManipulationUnit>&&);
     void flushPendingItemsForCallback();
@@ -178,6 +180,8 @@ private:
     WeakHashSet<Element> m_elementsWithNewRenderer;
     WeakHashSet<Element> m_manipulatedElements;
 
+    HashMap<String, bool> m_cachedFontFamilyExclusionResults;
+
     ManipulationItemCallback m_callback;
     Vector<ManipulationItem> m_pendingItemsForCallback;
 
index 8b61fd0..7789306 100644 (file)
@@ -475,6 +475,16 @@ const Font& Font::brokenIdeographFont() const
     return *derivedFontData.brokenIdeographFont;
 }
 
+#if !PLATFORM(COCOA)
+
+bool Font::isProbablyOnlyUsedToRenderIcons() const
+{
+    // FIXME: Not implemented yet.
+    return false;
+}
+
+#endif
+
 #if !LOG_DISABLED
 String Font::description() const
 {
index 1283f82..d40fd3a 100644 (file)
@@ -106,6 +106,8 @@ public:
     const Font* emphasisMarkFont(const FontDescription&) const;
     const Font& brokenIdeographFont() const;
 
+    bool isProbablyOnlyUsedToRenderIcons() const;
+
     const Font* variantFont(const FontDescription& description, FontVariant variant) const
     {
 #if PLATFORM(COCOA)
index 9d287af..d4bc2a4 100644 (file)
@@ -84,4 +84,14 @@ FontPlatformData FontPlatformData::cloneWithSize(const FontPlatformData& source,
 }
 #endif
 
+#if !PLATFORM(COCOA)
+
+String FontPlatformData::familyName() const
+{
+    // FIXME: Not implemented yet.
+    return { };
+}
+
+#endif
+
 }
index d38d4dd..00c4a88 100644 (file)
@@ -155,6 +155,8 @@ public:
     TextRenderingMode textRenderingMode() const { return m_textRenderingMode; }
     bool isForTextCombine() const { return widthVariant() != FontWidthVariant::RegularWidth; } // Keep in sync with callers of FontDescription::setWidthVariant().
 
+    String familyName() const;
+
 #if USE(CAIRO)
     cairo_scaled_font_t* scaledFont() const { return m_scaledFont.get(); }
 #endif
index 14a5a36..8b8c9d6 100644 (file)
@@ -659,4 +659,43 @@ bool Font::platformSupportsCodePoint(UChar32 character, Optional<UChar32> variat
     return CTFontGetGlyphsForCharacters(platformData().ctFont(), codeUnits, glyphs, count);
 }
 
+bool Font::isProbablyOnlyUsedToRenderIcons() const
+{
+    auto platformFont = platformData().font();
+    if (!platformFont)
+        return false;
+
+    // Allow most non-icon fonts to bail early here by testing a single character 'a', without iterating over all basic latin characters.
+    UniChar lowercaseACharacter = 'a';
+    CGGlyph lowercaseAGlyph;
+    if (CTFontGetGlyphsForCharacters(platformFont, &lowercaseACharacter, &lowercaseAGlyph, 1)) {
+        if (!CGRectIsEmpty(CTFontGetBoundingRectsForGlyphs(platformFont, kCTFontOrientationDefault, &lowercaseAGlyph, nullptr, 1)))
+            return false;
+    }
+
+    auto supportedCharacters = adoptCF(CTFontCopyCharacterSet(platformFont));
+    if (CFCharacterSetHasMemberInPlane(supportedCharacters.get(), 1) || CFCharacterSetHasMemberInPlane(supportedCharacters.get(), 2))
+        return false;
+
+    // This encompasses all basic Latin non-control characters.
+    constexpr UniChar firstCharacterToTest = ' ';
+    constexpr UniChar lastCharacterToTest = '~';
+    constexpr auto numberOfCharactersToTest = lastCharacterToTest - firstCharacterToTest + 1;
+
+    Vector<CGGlyph> glyphs;
+    glyphs.fill(0, numberOfCharactersToTest);
+    CTFontGetGlyphsForCharacterRange(platformFont, glyphs.begin(), CFRangeMake(firstCharacterToTest, numberOfCharactersToTest));
+    glyphs.removeAll(0);
+
+    if (glyphs.isEmpty())
+        return false;
+
+    Vector<CGRect> boundingRects;
+    boundingRects.fill(CGRectZero, glyphs.size());
+    CTFontGetBoundingRectsForGlyphs(platformFont, kCTFontOrientationDefault, glyphs.begin(), boundingRects.begin(), glyphs.size());
+    return notFound == boundingRects.findMatching([](auto& rect) {
+        return !CGRectIsEmpty(rect);
+    });
+}
+
 } // namespace WebCore
index 8a639db..72e8251 100644 (file)
@@ -181,4 +181,11 @@ String FontPlatformData::description() const
 
 #endif
 
+String FontPlatformData::familyName() const
+{
+    if (auto platformFont = font())
+        return adoptCF(CTFontCopyFamilyName(platformFont)).get();
+    return { };
+}
+
 } // namespace WebCore
index 3bce6e7..e79c28a 100644 (file)
@@ -1,3 +1,19 @@
+2020-06-25  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        Text manipulation should exclude text rendered using icon-only fonts
+        https://bugs.webkit.org/show_bug.cgi?id=213446
+        <rdar://problem/63734298>
+
+        Reviewed by Myles Maxfield.
+
+        Add a new API test.
+
+        * TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj:
+        * TestWebKitAPI/Tests/WebKitCocoa/Ahem.ttf: Added.
+        * TestWebKitAPI/Tests/WebKitCocoa/SpaceOnly.otf: Added.
+        * TestWebKitAPI/Tests/WebKitCocoa/TextManipulation.mm:
+        (TestWebKitAPI::TEST):
+
 2020-06-25  Aakash Jain  <aakash_jain@apple.com>
 
         [ews] Share more bots between EWS builder and testers
index f1a9949..5e0ca0a 100644 (file)
                F4CD74C620FDACFA00DE3794 /* text-with-async-script.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = F4CD74C520FDACF500DE3794 /* text-with-async-script.html */; };
                F4CD74C920FDB49600DE3794 /* TestURLSchemeHandler.mm in Sources */ = {isa = PBXBuildFile; fileRef = F4CD74C820FDB49600DE3794 /* TestURLSchemeHandler.mm */; };
                F4CF32802366552200D3AD07 /* EnterKeyHintTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = F4CF327F2366552200D3AD07 /* EnterKeyHintTests.mm */; };
+               F4CFCDDA249FC9E400527482 /* SpaceOnly.otf in Copy Resources */ = {isa = PBXBuildFile; fileRef = F4CFCDD8249FC9D900527482 /* SpaceOnly.otf */; };
+               F4CFCDDB249FC9E900527482 /* Ahem.ttf in Copy Resources */ = {isa = PBXBuildFile; fileRef = F4CFCDD9249FC9D900527482 /* Ahem.ttf */; };
                F4D2986E20FEE7370092D636 /* RunScriptAfterDocumentLoad.mm in Sources */ = {isa = PBXBuildFile; fileRef = F4D2986D20FEE7370092D636 /* RunScriptAfterDocumentLoad.mm */; };
                F4D4F3B61E4E2BCB00BB2767 /* DragAndDropSimulatorIOS.mm in Sources */ = {isa = PBXBuildFile; fileRef = F4D4F3B41E4E2BCB00BB2767 /* DragAndDropSimulatorIOS.mm */; };
                F4D4F3B91E4E36E400BB2767 /* DragAndDropTestsIOS.mm in Sources */ = {isa = PBXBuildFile; fileRef = F4D4F3B71E4E36E400BB2767 /* DragAndDropTestsIOS.mm */; };
                                55A81800218102210004A39A /* 400x400-green.png in Copy Resources */,
                                379028B914FAC24C007E6B43 /* acceptsFirstMouse.html in Copy Resources */,
                                725C3EF322058A5B007C36FC /* AdditionalSupportedImageTypes.html in Copy Resources */,
+                               F4CFCDDB249FC9E900527482 /* Ahem.ttf in Copy Resources */,
                                1C2B81871C8925A000A5529F /* Ahem.ttf in Copy Resources */,
                                1A63479F183D72A4005B1707 /* all-content-in-one-iframe.html in Copy Resources */,
                                C25CCA0D1E5141840026CB8A /* AllAhem.svg in Copy Resources */,
                                46AE5A3720F9066D00E0873E /* SimpleServiceWorkerRegistrations-4.sqlite3 in Copy Resources */,
                                F4F405BD1D4C0D1C007A9707 /* skinny-autoplaying-video-with-audio.html in Copy Resources */,
                                C01A23F21266156700C9ED55 /* spacebar-scrolling.html in Copy Resources */,
+                               F4CFCDDA249FC9E400527482 /* SpaceOnly.otf in Copy Resources */,
                                E194E1BD177E53C7009C4D4E /* StopLoadingFromDidReceiveResponse.html in Copy Resources */,
                                515BE16F1D428BB100DD7C68 /* StoreBlobToBeDeleted.html in Copy Resources */,
                                9BD6D3A21F7B218300BD4962 /* sunset-in-cupertino-100px.tiff in Copy Resources */,
                F4CD74C820FDB49600DE3794 /* TestURLSchemeHandler.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = TestURLSchemeHandler.mm; sourceTree = "<group>"; };
                F4CDAB3322489FE10057A2D9 /* UserInterfaceSwizzler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UserInterfaceSwizzler.h; sourceTree = "<group>"; };
                F4CF327F2366552200D3AD07 /* EnterKeyHintTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = EnterKeyHintTests.mm; sourceTree = "<group>"; };
+               F4CFCDD8249FC9D900527482 /* SpaceOnly.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = SpaceOnly.otf; sourceTree = "<group>"; };
+               F4CFCDD9249FC9D900527482 /* Ahem.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Ahem.ttf; sourceTree = "<group>"; };
                F4D2986D20FEE7370092D636 /* RunScriptAfterDocumentLoad.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = RunScriptAfterDocumentLoad.mm; sourceTree = "<group>"; };
                F4D4F3B41E4E2BCB00BB2767 /* DragAndDropSimulatorIOS.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = DragAndDropSimulatorIOS.mm; sourceTree = "<group>"; };
                F4D4F3B71E4E36E400BB2767 /* DragAndDropTestsIOS.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = DragAndDropTestsIOS.mm; sourceTree = "<group>"; };
                        children = (
                                55A817FE218101DF0004A39A /* 100x100-red.tga */,
                                55A817FD218101DF0004A39A /* 400x400-green.png */,
+                               F4CFCDD9249FC9D900527482 /* Ahem.ttf */,
                                C25CCA0C1E5140E50026CB8A /* AllAhem.svg */,
                                F4A9202E1FEE34C800F59590 /* apple-data-url.html */,
                                A1798B8A2243611A000764BD /* apple-pay-active-session.html */,
                                C9B4AD291ECA6EA500F5FEA0 /* silence-long.m4a */,
                                4656A75720F9054F0002E21F /* SimpleServiceWorkerRegistrations-4.sqlite3 */,
                                F4F405BB1D4C0CF8007A9707 /* skinny-autoplaying-video-with-audio.html */,
+                               F4CFCDD8249FC9D900527482 /* SpaceOnly.otf */,
                                515BE16E1D4288FF00DD7C68 /* StoreBlobToBeDeleted.html */,
                                9BD6D3A11F7B202100BD4962 /* sunset-in-cupertino-100px.tiff */,
                                9BD6D3A01F7B202000BD4962 /* sunset-in-cupertino-200px.png */,
diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/Ahem.ttf b/Tools/TestWebKitAPI/Tests/WebKitCocoa/Ahem.ttf
new file mode 100644 (file)
index 0000000..ac81cb0
Binary files /dev/null and b/Tools/TestWebKitAPI/Tests/WebKitCocoa/Ahem.ttf differ
diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/SpaceOnly.otf b/Tools/TestWebKitAPI/Tests/WebKitCocoa/SpaceOnly.otf
new file mode 100644 (file)
index 0000000..5b1343c
Binary files /dev/null and b/Tools/TestWebKitAPI/Tests/WebKitCocoa/SpaceOnly.otf differ
index bcde0f1..9712fd6 100644 (file)
@@ -1002,6 +1002,52 @@ TEST(TextManipulation, StartTextManipulationExtractsValuesByNode)
     EXPECT_STREQ("five", items[1].tokens[1].content.UTF8String);
 }
 
+TEST(TextManipulation, StartTextManipulationExcludesTextRenderedAsIcons)
+{
+    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>"
+        "<head>"
+        "<style>"
+        "@font-face {"
+        "    font-family: Ahem;"
+        "    src: url(Ahem.ttf);"
+        "}"
+        "@font-face {"
+        "    font-family: SpaceOnly;"
+        "    src: url(SpaceOnly.otf);"
+        "}"
+        ".Ahem { font-family: Ahem; }"
+        ".SpaceOnly { font-family: SpaceOnly; }"
+        ".Missing { font-family: Missing; }"
+        ".SystemUI { font-family: system-ui; }"
+        "</style>"
+        "</head>"
+        "<body>"
+        "<span class='Ahem'>one</span><span class='SpaceOnly'>two</span><span class='Missing'>three</span><span class='SystemUI'>four</span>"
+        "</body>"];
+
+    done = false;
+    [webView _startTextManipulationsWithConfiguration:nil completion:^{
+        done = true;
+    }];
+    TestWebKitAPI::Util::run(&done);
+
+    auto items = [delegate items];
+    EXPECT_EQ(items.count, 1UL);
+    auto item = items.firstObject;
+    EXPECT_EQ(item.tokens.count, 4UL);
+    EXPECT_WK_STREQ(item.tokens[0].content, "one");
+    EXPECT_FALSE(item.tokens[0].isExcluded);
+    EXPECT_WK_STREQ(item.tokens[1].content, "two");
+    EXPECT_TRUE(item.tokens[1].isExcluded);
+    EXPECT_WK_STREQ(item.tokens[2].content, "three");
+    EXPECT_FALSE(item.tokens[2].isExcluded);
+    EXPECT_WK_STREQ(item.tokens[3].content, "four");
+    EXPECT_FALSE(item.tokens[3].isExcluded);
+}
+
 struct Token {
     NSString *identifier;
     NSString *content;