[iOS WK2] Web process crashes after changing selection to the end of the document...
authorwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 28 Aug 2017 01:22:55 +0000 (01:22 +0000)
committerwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 28 Aug 2017 01:22:55 +0000 (01:22 +0000)
https://bugs.webkit.org/show_bug.cgi?id=176011
<rdar://problem/32614095>

Reviewed by Ryosuke Niwa.

Source/WebCore:

Adds a null check to visiblePositionForPositionWithOffset. This is a crash point for accessibility codepaths,
since indexForVisiblePosition is not guaranteed to set the given `root` outparam to a non-null value, yet
visiblePositionForIndex requires root to be non-null. This causes a crash when selecting some text, hitting
'Speak', and then changing the selection to somewhere near the end of the document, since accessibility code
will attempt to speak words at an offset past the end of the document. While this is a bug in and of itself, the
web process should still handle this case gracefully and not crash. To fix this, we simply bail and return a
null VisiblePosition if a root container node was not found.

Currently, visiblePositionForPositionWithOffset is implemented twice, in WebCore (AXObjectCache.cpp) and also in
WebKit (WebPageIOS.mm), as identical static functions. This patch moves this helper into Editing.cpp and removes
it from AXObjectCache and WebPageIOS.

Tests: AccessibilityTests.RectsForSpeakingSelectionBasic
       AccessibilityTests.RectsForSpeakingSelectionWithLineWrapping
       AccessibilityTests.RectsForSpeakingSelectionDoNotCrashWhenChangingSelection

* accessibility/AXObjectCache.cpp:
(WebCore::visiblePositionForPositionWithOffset): Deleted.
* editing/Editing.cpp:
(WebCore::visiblePositionForPositionWithOffset):
* editing/Editing.h:

Source/WebKit:

Adds an SPI hook to test accessibility codepaths when speaking selected content. This patch does some minor
refactoring by introducing _accessibilityRetrieveRectsAtSelectionOffset:withText:completionHandler:, which takes
and invokes a completion handler block. The existing _accessibilityRetrieveRectsAtSelectionOffset:withText:
method simply turns around and calls the former variant with `nil` as a completion handler.

* UIProcess/API/Cocoa/WKWebView.mm:
(-[WKWebView _accessibilityRetrieveRectsAtSelectionOffset:withText:completionHandler:]):
* UIProcess/API/Cocoa/WKWebViewPrivate.h:
* UIProcess/ios/WKContentViewInteraction.h:
* UIProcess/ios/WKContentViewInteraction.mm:
(-[WKContentView _accessibilityRetrieveRectsAtSelectionOffset:withText:]):
(-[WKContentView _accessibilityRetrieveRectsAtSelectionOffset:withText:completionHandler:]):
* WebProcess/WebPage/ios/WebPageIOS.mm:
(WebKit::visiblePositionForPositionWithOffset): Deleted.

Tools:

Introduces AccessibilityTests, and adds three new tests that traverse selection-rect-finding codepaths when
speaking selected content. See WebKit and WebCore ChangeLogs for more detail.

* TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj:
* TestWebKitAPI/Tests/ios/AccessibilityTestsIOS.mm: Added.
(-[WKWebView rectsAtSelectionOffset:withText:]):
(checkCGRectValueAtIndex):
(TestWebKitAPI::TEST):

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

13 files changed:
Source/WebCore/ChangeLog
Source/WebCore/accessibility/AXObjectCache.cpp
Source/WebCore/editing/Editing.cpp
Source/WebCore/editing/Editing.h
Source/WebKit/ChangeLog
Source/WebKit/UIProcess/API/Cocoa/WKWebView.mm
Source/WebKit/UIProcess/API/Cocoa/WKWebViewPrivate.h
Source/WebKit/UIProcess/ios/WKContentViewInteraction.h
Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm
Source/WebKit/WebProcess/WebPage/ios/WebPageIOS.mm
Tools/ChangeLog
Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj
Tools/TestWebKitAPI/Tests/ios/AccessibilityTestsIOS.mm [new file with mode: 0644]

index ec64ff4d6ccdc88f399ea931a8c1a92edd6ad69f..afc9b0f4127c2b824626768ae0a5d66f3c3aa920 100644 (file)
@@ -1,3 +1,33 @@
+2017-08-27  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [iOS WK2] Web process crashes after changing selection to the end of the document when speaking a selection
+        https://bugs.webkit.org/show_bug.cgi?id=176011
+        <rdar://problem/32614095>
+
+        Reviewed by Ryosuke Niwa.
+
+        Adds a null check to visiblePositionForPositionWithOffset. This is a crash point for accessibility codepaths,
+        since indexForVisiblePosition is not guaranteed to set the given `root` outparam to a non-null value, yet
+        visiblePositionForIndex requires root to be non-null. This causes a crash when selecting some text, hitting
+        'Speak', and then changing the selection to somewhere near the end of the document, since accessibility code
+        will attempt to speak words at an offset past the end of the document. While this is a bug in and of itself, the
+        web process should still handle this case gracefully and not crash. To fix this, we simply bail and return a
+        null VisiblePosition if a root container node was not found.
+
+        Currently, visiblePositionForPositionWithOffset is implemented twice, in WebCore (AXObjectCache.cpp) and also in
+        WebKit (WebPageIOS.mm), as identical static functions. This patch moves this helper into Editing.cpp and removes
+        it from AXObjectCache and WebPageIOS.
+
+        Tests: AccessibilityTests.RectsForSpeakingSelectionBasic
+               AccessibilityTests.RectsForSpeakingSelectionWithLineWrapping
+               AccessibilityTests.RectsForSpeakingSelectionDoNotCrashWhenChangingSelection
+
+        * accessibility/AXObjectCache.cpp:
+        (WebCore::visiblePositionForPositionWithOffset): Deleted.
+        * editing/Editing.cpp:
+        (WebCore::visiblePositionForPositionWithOffset):
+        * editing/Editing.h:
+
 2017-08-27  Devin Rousso  <webkit@devinrousso.com>
 
         Web Inspector: Record actions performed on WebGLRenderingContext
index 78c890af7d6e01f6d2f821f502698c659ef74474..526326d895298712be7cf54f6042026eed52a95c 100644 (file)
@@ -1727,13 +1727,6 @@ RefPtr<Range> AXObjectCache::rangeForNodeContents(Node* node)
     return WTFMove(range);
 }
     
-static VisiblePosition visiblePositionForPositionWithOffset(const VisiblePosition& position, int32_t offset)
-{
-    RefPtr<ContainerNode> root;
-    unsigned startIndex = indexForVisiblePosition(position, root);
-    return visiblePositionForIndex(startIndex + offset, root.get());
-}
-    
 RefPtr<Range> AXObjectCache::rangeMatchesTextNearRange(RefPtr<Range> originalRange, const String& matchText)
 {
     if (!originalRange)
index 574097a6ecd0838531d83bd0f1b4f8684abdbd38..e79b646e228972005e58fe50fbc2953203cb7ae3 100644 (file)
@@ -1092,6 +1092,16 @@ int indexForVisiblePosition(Node& node, const VisiblePosition& visiblePosition,
     return TextIterator::rangeLength(range.ptr(), forSelectionPreservation);
 }
 
+VisiblePosition visiblePositionForPositionWithOffset(const VisiblePosition& position, int offset)
+{
+    RefPtr<ContainerNode> root;
+    unsigned startIndex = indexForVisiblePosition(position, root);
+    if (!root)
+        return { };
+
+    return visiblePositionForIndex(startIndex + offset, root.get());
+}
+
 VisiblePosition visiblePositionForIndex(int index, ContainerNode* scope)
 {
     auto range = TextIterator::rangeFromLocationAndLength(scope, index, 0, true);
index eae48c0bbf23c75a2482c31fd58c1580c1c0e0e2..2b372c0c98f5c67223b77e7386f2af3265599aba 100644 (file)
@@ -146,6 +146,7 @@ int comparePositions(const VisiblePosition&, const VisiblePosition&);
 
 WEBCORE_EXPORT int indexForVisiblePosition(const VisiblePosition&, RefPtr<ContainerNode>& scope);
 int indexForVisiblePosition(Node&, const VisiblePosition&, bool forSelectionPreservation);
+WEBCORE_EXPORT VisiblePosition visiblePositionForPositionWithOffset(const VisiblePosition&, int offset);
 WEBCORE_EXPORT VisiblePosition visiblePositionForIndex(int index, ContainerNode* scope);
 VisiblePosition visiblePositionForIndexUsingCharacterIterator(Node&, int index); // FIXME: Why do we need this version?
 
index 09c3038dafe86cb216f27d64f8f33e6d954207c0..b82b4e0b03ef7f0553d87d676e6b2c0158ba2e6b 100644 (file)
@@ -1,3 +1,26 @@
+2017-08-27  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [iOS WK2] Web process crashes after changing selection to the end of the document when speaking a selection
+        https://bugs.webkit.org/show_bug.cgi?id=176011
+        <rdar://problem/32614095>
+
+        Reviewed by Ryosuke Niwa.
+
+        Adds an SPI hook to test accessibility codepaths when speaking selected content. This patch does some minor
+        refactoring by introducing _accessibilityRetrieveRectsAtSelectionOffset:withText:completionHandler:, which takes
+        and invokes a completion handler block. The existing _accessibilityRetrieveRectsAtSelectionOffset:withText:
+        method simply turns around and calls the former variant with `nil` as a completion handler.
+
+        * UIProcess/API/Cocoa/WKWebView.mm:
+        (-[WKWebView _accessibilityRetrieveRectsAtSelectionOffset:withText:completionHandler:]):
+        * UIProcess/API/Cocoa/WKWebViewPrivate.h:
+        * UIProcess/ios/WKContentViewInteraction.h:
+        * UIProcess/ios/WKContentViewInteraction.mm:
+        (-[WKContentView _accessibilityRetrieveRectsAtSelectionOffset:withText:]):
+        (-[WKContentView _accessibilityRetrieveRectsAtSelectionOffset:withText:completionHandler:]):
+        * WebProcess/WebPage/ios/WebPageIOS.mm:
+        (WebKit::visiblePositionForPositionWithOffset): Deleted.
+
 2017-08-25  Alex Christensen  <achristensen@webkit.org>
 
         Add WKUIDelegatePrivate equivalent of WKPageUIClient's saveDataToFileInDownloadsFolder
index 5e599c0855b52922d58997f03cdef921981c1599..1a1f60d217056b5349eb4cd262c9d8b30b7896ed 100644 (file)
@@ -5401,6 +5401,18 @@ static WebCore::UserInterfaceLayoutDirection toUserInterfaceLayoutDirection(UISe
     } forRequest:infoRequest];
 }
 
+- (void)_accessibilityRetrieveRectsAtSelectionOffset:(NSInteger)offset withText:(NSString *)text completionHandler:(void (^)(NSArray<NSValue *> *rects))completionHandler
+{
+    [_contentView _accessibilityRetrieveRectsAtSelectionOffset:offset withText:text completionHandler:[capturedCompletionHandler = makeBlockPtr(completionHandler)] (const Vector<WebCore::SelectionRect>& selectionRects) {
+        if (!capturedCompletionHandler)
+            return;
+        auto rectValues = adoptNS([[NSMutableArray alloc] initWithCapacity:selectionRects.size()]);
+        for (auto& selectionRect : selectionRects)
+            [rectValues addObject:[NSValue valueWithCGRect:selectionRect.rect()]];
+        capturedCompletionHandler(rectValues.get());
+    }];
+}
+
 - (CGRect)_contentVisibleRect
 {
     return [self convertRect:[self bounds] toView:self._currentContentView];
index 016f9492334ce363ba05920624a05fe1c0667536..a3afcca2c14c8a2a3ea18395c9ede0dd8081deb7 100644 (file)
@@ -377,6 +377,7 @@ typedef NS_ENUM(NSInteger, _WKImmediateActionType) {
 @property (nonatomic, readonly) CGRect _dragCaretRect WK_API_AVAILABLE(ios(WK_IOS_TBA));
 
 - (void)_requestActivatedElementAtPosition:(CGPoint)position completionBlock:(void (^)(_WKActivatedElementInfo *))block WK_API_AVAILABLE(ios(WK_IOS_TBA));
+- (void)_accessibilityRetrieveRectsAtSelectionOffset:(NSInteger)offset withText:(NSString *)text completionHandler:(void (^)(NSArray<NSValue *> *rects))completionHandler WK_API_AVAILABLE(ios(WK_IOS_TBA));
 
 #endif // TARGET_OS_IPHONE
 
index 0c5300eded6bc5adae05fd7e859bc34b3279dc1e..2de6207f78f334e49f64b11842447570c480e133 100644 (file)
@@ -58,6 +58,7 @@ namespace WebCore {
 class Color;
 class FloatQuad;
 class IntSize;
+class SelectionRect;
 }
 
 #if ENABLE(DRAG_SUPPORT)
@@ -331,6 +332,7 @@ FOR_EACH_WKCONTENTVIEW_ACTION(DECLARE_WKCONTENTVIEW_ACTION_FOR_WEB_VIEW)
 - (NSArray<NSValue *> *)_uiTextSelectionRects;
 - (void)accessibilityRetrieveSpeakSelectionContent;
 - (void)_accessibilityRetrieveRectsEnclosingSelectionOffset:(NSInteger)offset withGranularity:(UITextGranularity)granularity;
+- (void)_accessibilityRetrieveRectsAtSelectionOffset:(NSInteger)offset withText:(NSString *)text completionHandler:(void (^)(const Vector<WebCore::SelectionRect>& rects))completionHandler;
 - (void)_accessibilityRetrieveRectsAtSelectionOffset:(NSInteger)offset withText:(NSString *)text;
 
 @property (nonatomic, readonly) WebKit::InteractionInformationAtPosition currentPositionInformation;
index d1319424fd610f83a1b69d0c1f15cc75a43e7b37..d32f8d41eb6457b0d5d577cc823915601c141c22 100644 (file)
@@ -2279,9 +2279,17 @@ FOR_EACH_WKCONTENTVIEW_ACTION(FORWARD_ACTION_TO_WKWEBVIEW)
 }
 
 - (void)_accessibilityRetrieveRectsAtSelectionOffset:(NSInteger)offset withText:(NSString *)text
+{
+    [self _accessibilityRetrieveRectsAtSelectionOffset:offset withText:text completionHandler:nil];
+}
+
+- (void)_accessibilityRetrieveRectsAtSelectionOffset:(NSInteger)offset withText:(NSString *)text completionHandler:(void (^)(const Vector<SelectionRect>& rects))completionHandler
 {
     RetainPtr<WKContentView> view = self;
-    _page->requestRectsAtSelectionOffsetWithText(offset, text, [view, offset](const Vector<WebCore::SelectionRect>& selectionRects, CallbackBase::Error error) {
+    _page->requestRectsAtSelectionOffsetWithText(offset, text, [view, offset, capturedCompletionHandler = makeBlockPtr(completionHandler)](const Vector<SelectionRect>& selectionRects, CallbackBase::Error error) {
+        if (capturedCompletionHandler)
+            capturedCompletionHandler(selectionRects);
+
         if (error != WebKit::CallbackBase::Error::None)
             return;
         if ([view respondsToSelector:@selector(_accessibilityDidGetSelectionRects:withGranularity:atOffset:)])
index 93863b0517eb14df194a22de57b314b14cb0e5bf..c845869a8e6cdfae3a7340ff731ac47f04a771a6 100644 (file)
@@ -1843,13 +1843,6 @@ void WebPage::moveSelectionByOffset(int32_t offset, CallbackID callbackID)
     send(Messages::WebPageProxy::VoidCallback(callbackID));
 }
 
-static VisiblePosition visiblePositionForPositionWithOffset(const VisiblePosition& position, int32_t offset)
-{
-    RefPtr<ContainerNode> root;
-    unsigned startIndex = indexForVisiblePosition(position, root);
-    return visiblePositionForIndex(startIndex + offset, root.get());
-}
-
 void WebPage::getRectsForGranularityWithSelectionOffset(uint32_t granularity, int32_t offset, CallbackID callbackID)
 {
     Frame& frame = m_page->focusController().focusedOrMainFrame();
index 0e2cea88ee44f01919940a85170cc868791600be..2531cb0dcf0717ed40907f5f96a37340e5ed6c63 100644 (file)
@@ -1,3 +1,20 @@
+2017-08-27  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [iOS WK2] Web process crashes after changing selection to the end of the document when speaking a selection
+        https://bugs.webkit.org/show_bug.cgi?id=176011
+        <rdar://problem/32614095>
+
+        Reviewed by Ryosuke Niwa.
+
+        Introduces AccessibilityTests, and adds three new tests that traverse selection-rect-finding codepaths when
+        speaking selected content. See WebKit and WebCore ChangeLogs for more detail.
+
+        * TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj:
+        * TestWebKitAPI/Tests/ios/AccessibilityTestsIOS.mm: Added.
+        (-[WKWebView rectsAtSelectionOffset:withText:]):
+        (checkCGRectValueAtIndex):
+        (TestWebKitAPI::TEST):
+
 2017-08-25  Eric Carlson  <eric.carlson@apple.com>
 
         Add Logger::logAlways
index e48b285fd39ebccc7271d4ddb5af67d0cccb203f..6b2e51c3e2e86141a4a15257df6fec0684da2476 100644 (file)
@@ -86,6 +86,7 @@
                2E1DFDED1D42A51100714A00 /* large-videos-with-audio.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = 2E1DFDEC1D42A41C00714A00 /* large-videos-with-audio.html */; };
                2E1DFDEF1D42A6F200714A00 /* large-videos-with-audio-autoplay.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = 2E1DFDEE1D42A6EB00714A00 /* large-videos-with-audio-autoplay.html */; };
                2E1DFDF11D42E1E400714A00 /* large-video-seek-to-beginning-and-play-after-ending.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = 2E1DFDF01D42E14400714A00 /* large-video-seek-to-beginning-and-play-after-ending.html */; };
+               2E205BA41F527746005952DD /* AccessibilityTestsIOS.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2E205BA31F527746005952DD /* AccessibilityTestsIOS.mm */; };
                2E54F40D1D7BC84200921ADF /* large-video-mutes-onplaying.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = 2E54F40C1D7BC83900921ADF /* large-video-mutes-onplaying.html */; };
                2E691AEA1D78B53600129407 /* large-videos-paused-video-hides-controls.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = 2E691AE81D78B52B00129407 /* large-videos-paused-video-hides-controls.html */; };
                2E691AEB1D78B53600129407 /* large-videos-playing-video-keeps-controls.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = 2E691AE91D78B52B00129407 /* large-videos-playing-video-keeps-controls.html */; };
                2E1DFDEC1D42A41C00714A00 /* large-videos-with-audio.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = "large-videos-with-audio.html"; sourceTree = "<group>"; };
                2E1DFDEE1D42A6EB00714A00 /* large-videos-with-audio-autoplay.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = "large-videos-with-audio-autoplay.html"; sourceTree = "<group>"; };
                2E1DFDF01D42E14400714A00 /* large-video-seek-to-beginning-and-play-after-ending.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = "large-video-seek-to-beginning-and-play-after-ending.html"; sourceTree = "<group>"; };
+               2E205BA31F527746005952DD /* AccessibilityTestsIOS.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = AccessibilityTestsIOS.mm; sourceTree = "<group>"; };
                2E54F40C1D7BC83900921ADF /* large-video-mutes-onplaying.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = "large-video-mutes-onplaying.html"; sourceTree = "<group>"; };
                2E691AE81D78B52B00129407 /* large-videos-paused-video-hides-controls.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = "large-videos-paused-video-hides-controls.html"; sourceTree = "<group>"; };
                2E691AE91D78B52B00129407 /* large-videos-playing-video-keeps-controls.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = "large-videos-playing-video-keeps-controls.html"; sourceTree = "<group>"; };
                        isa = PBXGroup;
                        children = (
                                A1C4FB6F1BACCEFA003742D0 /* Resources */,
+                               2E205BA31F527746005952DD /* AccessibilityTestsIOS.mm */,
                                F45B63FC1F19D410009D38B9 /* ActionSheetTests.mm */,
                                F4D4F3B71E4E36E400BB2767 /* DataInteractionTests.mm */,
                                7560917719259C59009EF06E /* MemoryCacheAddImageToCacheIOS.mm */,
                                7CEFA9661AC0B9E200B910FD /* _WKUserContentExtensionStore.mm in Sources */,
                                7CCE7EE41A411AE600447C4C /* AboutBlankLoad.cpp in Sources */,
                                7CCE7EB31A411A7E00447C4C /* AcceptsFirstMouse.mm in Sources */,
+                               2E205BA41F527746005952DD /* AccessibilityTestsIOS.mm in Sources */,
                                F45B63FE1F19D410009D38B9 /* ActionSheetTests.mm in Sources */,
                                37E7DD641EA06FF2009B396D /* AdditionalReadAccessAllowedURLs.mm in Sources */,
                                7A909A7D1D877480007E10F8 /* AffineTransform.cpp in Sources */,
diff --git a/Tools/TestWebKitAPI/Tests/ios/AccessibilityTestsIOS.mm b/Tools/TestWebKitAPI/Tests/ios/AccessibilityTestsIOS.mm
new file mode 100644 (file)
index 0000000..0114266
--- /dev/null
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2017 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "config.h"
+
+#if PLATFORM(IOS) && WK_API_ENABLED
+
+#import "PlatformUtilities.h"
+#import "TestWKWebView.h"
+#import <WebKit/WKWebViewPrivate.h>
+
+@implementation WKWebView (WKAccessibilityTesting)
+- (NSArray<NSValue *> *)rectsAtSelectionOffset:(NSInteger)offset withText:(NSString *)text
+{
+    __block RetainPtr<NSArray> selectionRects;
+    __block bool done = false;
+    [self _accessibilityRetrieveRectsAtSelectionOffset:offset withText:text completionHandler:^(NSArray<NSValue *> *rects) {
+        selectionRects = rects;
+        done = true;
+    }];
+    TestWebKitAPI::Util::run(&done);
+    return selectionRects.autorelease();
+}
+@end
+
+static void checkCGRectValueAtIndex(NSArray<NSValue *> *rectValues, CGRect expectedRect, NSUInteger index)
+{
+    EXPECT_LT(index, rectValues.count);
+    auto observedRect = [rectValues[index] CGRectValue];
+    EXPECT_EQ(expectedRect.origin.x, observedRect.origin.x);
+    EXPECT_EQ(expectedRect.origin.y, observedRect.origin.y);
+    EXPECT_EQ(expectedRect.size.width, observedRect.size.width);
+    EXPECT_EQ(expectedRect.size.height, observedRect.size.height);
+}
+
+namespace TestWebKitAPI {
+
+TEST(AccessibilityTests, RectsForSpeakingSelectionBasic)
+{
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 320, 500)]);
+    [webView synchronouslyLoadHTMLString:@"<meta name='viewport' content='width=device-width,initial-scale=1'><span id='first'>first</span><span id='second'> second</span><br><span id='third'> third</span>"];
+    [webView stringByEvaluatingJavaScript:@"document.execCommand('SelectAll')"];
+
+    checkCGRectValueAtIndex([webView rectsAtSelectionOffset:0 withText:@"first"], CGRectMake(8, 8, 26, 19), 0);
+    checkCGRectValueAtIndex([webView rectsAtSelectionOffset:6 withText:@"second"], CGRectMake(37, 8, 46, 19), 0);
+    checkCGRectValueAtIndex([webView rectsAtSelectionOffset:13 withText:@"third"], CGRectMake(8, 27, 31, 20), 0);
+}
+
+TEST(AccessibilityTests, RectsForSpeakingSelectionWithLineWrapping)
+{
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 320, 500)]);
+    [webView synchronouslyLoadHTMLString:@"<meta name='viewport' content='width=device-width,initial-scale=1'><body style='font-size: 100px; word-wrap: break-word'><span id='text'>abcdefghijklmnopqrstuvwxyz</span></body>"];
+    [webView stringByEvaluatingJavaScript:@"document.execCommand('SelectAll')"];
+
+    NSArray<NSValue *> *rects = [webView rectsAtSelectionOffset:0 withText:@"abcdefghijklmnopqrstuvwxyz"];
+    checkCGRectValueAtIndex(rects, CGRectMake(8, 8, 304, 114), 0);
+    checkCGRectValueAtIndex(rects, CGRectMake(8, 122, 304, 117), 1);
+    checkCGRectValueAtIndex(rects, CGRectMake(8, 239, 304, 117), 2);
+    checkCGRectValueAtIndex(rects, CGRectMake(8, 356, 304, 117), 3);
+    checkCGRectValueAtIndex(rects, CGRectMake(8, 473, 145, 117), 4);
+}
+
+TEST(AccessibilityTests, RectsForSpeakingSelectionDoNotCrashWhenChangingSelection)
+{
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 320, 500)]);
+    [webView synchronouslyLoadHTMLString:@"<meta name='viewport' content='width=device-width,initial-scale=1'><span id='first'>first</span><span id='second'> second</span><br><span id='third'> third</span>"];
+
+    [webView stringByEvaluatingJavaScript:@"getSelection().setBaseAndExtent(third, 0, third, 1)"];
+    EXPECT_EQ(0UL, [webView rectsAtSelectionOffset:13 withText:@"third"].count);
+    EXPECT_WK_STREQ("third", [webView stringByEvaluatingJavaScript:@"getSelection().toString()"]);
+
+    [webView stringByEvaluatingJavaScript:@"getSelection().removeAllRanges()"];
+    EXPECT_EQ(0UL, [webView rectsAtSelectionOffset:13 withText:@"third"].count);
+    EXPECT_WK_STREQ("", [webView stringByEvaluatingJavaScript:@"getSelection().toString()"]);
+}
+
+} // namespace TestWebKitAPI
+
+#endif // PLATFORM(IOS) && WK_API_ENABLED
+