<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[284675] trunk</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.webkit.org/projects/webkit/changeset/284675">284675</a></dd>
<dt>Author</dt> <dd>carlosgc@webkit.org</dd>
<dt>Date</dt> <dd>2021-10-22 01:51:03 -0700 (Fri, 22 Oct 2021)</dd>
</dl>

<h3>Log Message</h3>
<pre>[GTK][a11y] Add implementation of text interface when building with ATSPI
https://bugs.webkit.org/show_bug.cgi?id=230258

Reviewed by Adrian Perez de Castro.

Source/WebCore:

* SourcesGTK.txt:
* accessibility/AXObjectCache.cpp:
(WebCore::AXObjectCache::postTextStateChangeNotification):
(WebCore::AXObjectCache::postTextReplacementNotification):
(WebCore::AXObjectCache::postTextReplacementNotificationForTextControl):
(WebCore::AXObjectCache::enqueuePasswordValueChangeNotification):
* accessibility/AXObjectCache.h:
* accessibility/AccessibilityObject.cpp:
(WebCore::AccessibilityObject::textIteratorBehaviorForTextRange const):
* accessibility/AccessibilityObject.h:
* accessibility/AccessibilityRenderObject.cpp:
(WebCore::AccessibilityRenderObject::indexForVisiblePosition const):
* accessibility/atspi/AXObjectCacheAtspi.cpp:
(WebCore::AXObjectCache::postTextStateChangePlatformNotification):
(WebCore::AXObjectCache::postTextReplacementPlatformNotificationForTextControl):
(WebCore::AXObjectCache::postTextReplacementPlatformNotification):
(WebCore::AXObjectCache::nodeTextChangePlatformNotification): Deleted.
* accessibility/atspi/AccessibilityAtspi.cpp:
(WebCore::AccessibilityAtspi::textChanged):
(WebCore::AccessibilityAtspi::textAttributesChanged):
(WebCore::AccessibilityAtspi::textCaretMoved):
(WebCore::AccessibilityAtspi::textSelectionChanged):
* accessibility/atspi/AccessibilityAtspi.h:
* accessibility/atspi/AccessibilityAtspiEnums.h:
* accessibility/atspi/AccessibilityObjectAtspi.cpp:
(WebCore::roleIsTextType):
(WebCore::AccessibilityObjectAtspi::interfacesForObject):
(WebCore::AccessibilityObjectAtspi::path):
(WebCore::AccessibilityObjectAtspi::buildInterfaces const):
* accessibility/atspi/AccessibilityObjectAtspi.h:
* accessibility/atspi/AccessibilityObjectTextAtspi.cpp: Added.
(WebCore::AccessibilityObjectAtspi::atspiBoundaryToTextGranularity):
(WebCore::AccessibilityObjectAtspi::atspiGranularityToTextGranularity):
(WebCore::offsetMapping):
(WebCore::UTF16OffsetToUTF8):
(WebCore::UTF8OffsetToUTF16):
(WebCore::AccessibilityObjectAtspi::text const):
(WebCore::AccessibilityObject::getLengthForTextRange const):
(WebCore::AccessibilityObject::allowsTextRanges const):
(WebCore::AccessibilityObjectAtspi::textInserted):
(WebCore::AccessibilityObjectAtspi::textDeleted):
(WebCore::AccessibilityObjectAtspi::boundaryOffset const):
(WebCore::AccessibilityObjectAtspi::textAtOffset const):
(WebCore::AccessibilityObjectAtspi::characterAtOffset const):
(WebCore::AccessibilityObjectAtspi::boundsForRange const):
(WebCore::AccessibilityObjectAtspi::textExtents const):
(WebCore::AccessibilityObjectAtspi::offsetAtPoint const):
(WebCore::AccessibilityObjectAtspi::boundsForSelection const):
(WebCore::AccessibilityObjectAtspi::selectedRange const):
(WebCore::AccessibilityObjectAtspi::selectionBounds const):
(WebCore::AccessibilityObjectAtspi::setSelectedRange):
(WebCore::AccessibilityObjectAtspi::selectRange):
(WebCore::AccessibilityObjectAtspi::selectionChanged):
(WebCore::AccessibilityObjectAtspi::textAttributes const):
(WebCore::AccessibilityObjectAtspi::textAttributesWithUTF8Offset const):
(WebCore::AccessibilityObjectAtspi::textAttributesChanged):
(WebCore::AccessibilityObjectAtspi::scrollToMakeVisible const):
(WebCore::AccessibilityObjectAtspi::scrollToPoint const):
* editing/atspi/FrameSelectionAtspi.cpp:
(WebCore::FrameSelection::notifyAccessibilityForSelectionChange):
* html/HTMLTextFormControlElement.cpp:
(WebCore::HTMLTextFormControlElement::setInnerTextValue):

Tools:

Add unit tests for the text interface.

* TestWebKitAPI/Tests/WebKitGtk/TestWebKitAccessibility.cpp:
(AtspiTextRangeDeleter::operator() const):
(AccessibilityTest::startEventMonitor):
(AccessibilityTest::stopEventMonitor):
(AccessibilityTest::findEvent):
(testTextBasic):
(testTextSurrogatePair):
(testTextIterator):
(testTextExtents):
(testTextSelections):
(testTextAttributes):
(testTextStateChanged):
(beforeAll):</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkSourceWebCoreChangeLog">trunk/Source/WebCore/ChangeLog</a></li>
<li><a href="#trunkSourceWebCoreSourcesGTKtxt">trunk/Source/WebCore/SourcesGTK.txt</a></li>
<li><a href="#trunkSourceWebCoreaccessibilityAXObjectCachecpp">trunk/Source/WebCore/accessibility/AXObjectCache.cpp</a></li>
<li><a href="#trunkSourceWebCoreaccessibilityAXObjectCacheh">trunk/Source/WebCore/accessibility/AXObjectCache.h</a></li>
<li><a href="#trunkSourceWebCoreaccessibilityAccessibilityObjectcpp">trunk/Source/WebCore/accessibility/AccessibilityObject.cpp</a></li>
<li><a href="#trunkSourceWebCoreaccessibilityAccessibilityObjecth">trunk/Source/WebCore/accessibility/AccessibilityObject.h</a></li>
<li><a href="#trunkSourceWebCoreaccessibilityAccessibilityRenderObjectcpp">trunk/Source/WebCore/accessibility/AccessibilityRenderObject.cpp</a></li>
<li><a href="#trunkSourceWebCoreaccessibilityatspiAXObjectCacheAtspicpp">trunk/Source/WebCore/accessibility/atspi/AXObjectCacheAtspi.cpp</a></li>
<li><a href="#trunkSourceWebCoreaccessibilityatspiAccessibilityAtspicpp">trunk/Source/WebCore/accessibility/atspi/AccessibilityAtspi.cpp</a></li>
<li><a href="#trunkSourceWebCoreaccessibilityatspiAccessibilityAtspih">trunk/Source/WebCore/accessibility/atspi/AccessibilityAtspi.h</a></li>
<li><a href="#trunkSourceWebCoreaccessibilityatspiAccessibilityAtspiEnumsh">trunk/Source/WebCore/accessibility/atspi/AccessibilityAtspiEnums.h</a></li>
<li><a href="#trunkSourceWebCoreaccessibilityatspiAccessibilityObjectAtspicpp">trunk/Source/WebCore/accessibility/atspi/AccessibilityObjectAtspi.cpp</a></li>
<li><a href="#trunkSourceWebCoreaccessibilityatspiAccessibilityObjectAtspih">trunk/Source/WebCore/accessibility/atspi/AccessibilityObjectAtspi.h</a></li>
<li><a href="#trunkSourceWebCoreeditingatspiFrameSelectionAtspicpp">trunk/Source/WebCore/editing/atspi/FrameSelectionAtspi.cpp</a></li>
<li><a href="#trunkSourceWebCorehtmlHTMLTextFormControlElementcpp">trunk/Source/WebCore/html/HTMLTextFormControlElement.cpp</a></li>
<li><a href="#trunkToolsChangeLog">trunk/Tools/ChangeLog</a></li>
<li><a href="#trunkToolsTestWebKitAPITestsWebKitGtkTestWebKitAccessibilitycpp">trunk/Tools/TestWebKitAPI/Tests/WebKitGtk/TestWebKitAccessibility.cpp</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#trunkSourceWebCoreaccessibilityatspiAccessibilityObjectTextAtspicpp">trunk/Source/WebCore/accessibility/atspi/AccessibilityObjectTextAtspi.cpp</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunkSourceWebCoreChangeLog"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebCore/ChangeLog (284674 => 284675)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebCore/ChangeLog   2021-10-22 08:44:57 UTC (rev 284674)
+++ trunk/Source/WebCore/ChangeLog      2021-10-22 08:51:03 UTC (rev 284675)
</span><span class="lines">@@ -1,3 +1,73 @@
</span><ins>+2021-10-22  Carlos Garcia Campos  <cgarcia@igalia.com>
+
+        [GTK][a11y] Add implementation of text interface when building with ATSPI
+        https://bugs.webkit.org/show_bug.cgi?id=230258
+
+        Reviewed by Adrian Perez de Castro.
+
+        * SourcesGTK.txt:
+        * accessibility/AXObjectCache.cpp:
+        (WebCore::AXObjectCache::postTextStateChangeNotification):
+        (WebCore::AXObjectCache::postTextReplacementNotification):
+        (WebCore::AXObjectCache::postTextReplacementNotificationForTextControl):
+        (WebCore::AXObjectCache::enqueuePasswordValueChangeNotification):
+        * accessibility/AXObjectCache.h:
+        * accessibility/AccessibilityObject.cpp:
+        (WebCore::AccessibilityObject::textIteratorBehaviorForTextRange const):
+        * accessibility/AccessibilityObject.h:
+        * accessibility/AccessibilityRenderObject.cpp:
+        (WebCore::AccessibilityRenderObject::indexForVisiblePosition const):
+        * accessibility/atspi/AXObjectCacheAtspi.cpp:
+        (WebCore::AXObjectCache::postTextStateChangePlatformNotification):
+        (WebCore::AXObjectCache::postTextReplacementPlatformNotificationForTextControl):
+        (WebCore::AXObjectCache::postTextReplacementPlatformNotification):
+        (WebCore::AXObjectCache::nodeTextChangePlatformNotification): Deleted.
+        * accessibility/atspi/AccessibilityAtspi.cpp:
+        (WebCore::AccessibilityAtspi::textChanged):
+        (WebCore::AccessibilityAtspi::textAttributesChanged):
+        (WebCore::AccessibilityAtspi::textCaretMoved):
+        (WebCore::AccessibilityAtspi::textSelectionChanged):
+        * accessibility/atspi/AccessibilityAtspi.h:
+        * accessibility/atspi/AccessibilityAtspiEnums.h:
+        * accessibility/atspi/AccessibilityObjectAtspi.cpp:
+        (WebCore::roleIsTextType):
+        (WebCore::AccessibilityObjectAtspi::interfacesForObject):
+        (WebCore::AccessibilityObjectAtspi::path):
+        (WebCore::AccessibilityObjectAtspi::buildInterfaces const):
+        * accessibility/atspi/AccessibilityObjectAtspi.h:
+        * accessibility/atspi/AccessibilityObjectTextAtspi.cpp: Added.
+        (WebCore::AccessibilityObjectAtspi::atspiBoundaryToTextGranularity):
+        (WebCore::AccessibilityObjectAtspi::atspiGranularityToTextGranularity):
+        (WebCore::offsetMapping):
+        (WebCore::UTF16OffsetToUTF8):
+        (WebCore::UTF8OffsetToUTF16):
+        (WebCore::AccessibilityObjectAtspi::text const):
+        (WebCore::AccessibilityObject::getLengthForTextRange const):
+        (WebCore::AccessibilityObject::allowsTextRanges const):
+        (WebCore::AccessibilityObjectAtspi::textInserted):
+        (WebCore::AccessibilityObjectAtspi::textDeleted):
+        (WebCore::AccessibilityObjectAtspi::boundaryOffset const):
+        (WebCore::AccessibilityObjectAtspi::textAtOffset const):
+        (WebCore::AccessibilityObjectAtspi::characterAtOffset const):
+        (WebCore::AccessibilityObjectAtspi::boundsForRange const):
+        (WebCore::AccessibilityObjectAtspi::textExtents const):
+        (WebCore::AccessibilityObjectAtspi::offsetAtPoint const):
+        (WebCore::AccessibilityObjectAtspi::boundsForSelection const):
+        (WebCore::AccessibilityObjectAtspi::selectedRange const):
+        (WebCore::AccessibilityObjectAtspi::selectionBounds const):
+        (WebCore::AccessibilityObjectAtspi::setSelectedRange):
+        (WebCore::AccessibilityObjectAtspi::selectRange):
+        (WebCore::AccessibilityObjectAtspi::selectionChanged):
+        (WebCore::AccessibilityObjectAtspi::textAttributes const):
+        (WebCore::AccessibilityObjectAtspi::textAttributesWithUTF8Offset const):
+        (WebCore::AccessibilityObjectAtspi::textAttributesChanged):
+        (WebCore::AccessibilityObjectAtspi::scrollToMakeVisible const):
+        (WebCore::AccessibilityObjectAtspi::scrollToPoint const):
+        * editing/atspi/FrameSelectionAtspi.cpp:
+        (WebCore::FrameSelection::notifyAccessibilityForSelectionChange):
+        * html/HTMLTextFormControlElement.cpp:
+        (WebCore::HTMLTextFormControlElement::setInnerTextValue):
+
</ins><span class="cx"> 2021-10-22  Youenn Fablet  <youenn@apple.com>
</span><span class="cx"> 
</span><span class="cx">         Audio over peer connection becomes latent when changing the output
</span></span></pre></div>
<a id="trunkSourceWebCoreSourcesGTKtxt"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebCore/SourcesGTK.txt (284674 => 284675)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebCore/SourcesGTK.txt      2021-10-22 08:44:57 UTC (rev 284674)
+++ trunk/Source/WebCore/SourcesGTK.txt 2021-10-22 08:51:03 UTC (rev 284675)
</span><span class="lines">@@ -42,6 +42,7 @@
</span><span class="cx"> accessibility/atspi/AccessibilityAtspi.cpp
</span><span class="cx"> accessibility/atspi/AccessibilityObjectAtspi.cpp
</span><span class="cx"> accessibility/atspi/AccessibilityObjectComponentAtspi.cpp
</span><ins>+accessibility/atspi/AccessibilityObjectTextAtspi.cpp
</ins><span class="cx"> accessibility/atspi/AccessibilityRootAtspi.cpp
</span><span class="cx"> accessibility/atspi/AXObjectCacheAtspi.cpp
</span><span class="cx"> 
</span></span></pre></div>
<a id="trunkSourceWebCoreaccessibilityAXObjectCachecpp"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebCore/accessibility/AXObjectCache.cpp (284674 => 284675)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebCore/accessibility/AXObjectCache.cpp     2021-10-22 08:44:57 UTC (rev 284674)
+++ trunk/Source/WebCore/accessibility/AXObjectCache.cpp        2021-10-22 08:51:03 UTC (rev 284675)
</span><span class="lines">@@ -1409,7 +1409,7 @@
</span><span class="cx">     if (!node)
</span><span class="cx">         return;
</span><span class="cx"> 
</span><del>-#if PLATFORM(COCOA)
</del><ins>+#if PLATFORM(COCOA) || USE(ATSPI)
</ins><span class="cx">     stopCachingComputedObjectAttributes();
</span><span class="cx"> 
</span><span class="cx">     postTextStateChangeNotification(getOrCreate(node), intent, selection);
</span><span class="lines">@@ -1428,9 +1428,10 @@
</span><span class="cx"> 
</span><span class="cx">     stopCachingComputedObjectAttributes();
</span><span class="cx"> 
</span><del>-#if PLATFORM(COCOA)
</del><ins>+#if PLATFORM(COCOA) || USE(ATSPI)
</ins><span class="cx">     AccessibilityObject* object = getOrCreate(node);
</span><span class="cx">     if (object && object->accessibilityIsIgnored()) {
</span><ins>+#if PLATFORM(COCOA)
</ins><span class="cx">         if (position.atLastEditingPositionForNode()) {
</span><span class="cx">             if (AccessibilityObject* nextSibling = object->nextSiblingUnignored(1))
</span><span class="cx">                 object = nextSibling;
</span><span class="lines">@@ -1438,6 +1439,13 @@
</span><span class="cx">             if (AccessibilityObject* previousSibling = object->previousSiblingUnignored(1))
</span><span class="cx">                 object = previousSibling;
</span><span class="cx">         }
</span><ins>+#elif USE(ATSPI)
+        // ATSPI doesn't expose text nodes, so we need the parent
+        // object which is the one implementing the text interface.
+        auto* parent = object->parentObjectUnignored();
+        if (is<AccessibilityObject>(parent))
+            object = downcast<AccessibilityObject>(parent);
+#endif
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     postTextStateChangeNotification(object, intent, selection);
</span><span class="lines">@@ -1451,7 +1459,7 @@
</span><span class="cx">     AXTRACE("AXObjectCache::postTextStateChangeNotification");
</span><span class="cx">     stopCachingComputedObjectAttributes();
</span><span class="cx"> 
</span><del>-#if PLATFORM(COCOA)
</del><ins>+#if PLATFORM(COCOA) || USE(ATSPI)
</ins><span class="cx">     if (object) {
</span><span class="cx">         if (isPasswordFieldOrContainedByPasswordField(object))
</span><span class="cx">             return;
</span><span class="lines">@@ -1490,7 +1498,7 @@
</span><span class="cx">     stopCachingComputedObjectAttributes();
</span><span class="cx"> 
</span><span class="cx">     AccessibilityObject* object = getOrCreate(node);
</span><del>-#if PLATFORM(COCOA)
</del><ins>+#if PLATFORM(COCOA) || USE(ATSPI)
</ins><span class="cx">     if (object) {
</span><span class="cx">         if (enqueuePasswordValueChangeNotification(object))
</span><span class="cx">             return;
</span><span class="lines">@@ -1525,7 +1533,7 @@
</span><span class="cx">     stopCachingComputedObjectAttributes();
</span><span class="cx"> 
</span><span class="cx">     AccessibilityObject* object = getOrCreate(node);
</span><del>-#if PLATFORM(COCOA)
</del><ins>+#if PLATFORM(COCOA) || USE(ATSPI)
</ins><span class="cx">     if (object) {
</span><span class="cx">         if (enqueuePasswordValueChangeNotification(object))
</span><span class="cx">             return;
</span><span class="lines">@@ -1544,7 +1552,7 @@
</span><span class="cx">     stopCachingComputedObjectAttributes();
</span><span class="cx"> 
</span><span class="cx">     AccessibilityObject* object = getOrCreate(&textControl);
</span><del>-#if PLATFORM(COCOA)
</del><ins>+#if PLATFORM(COCOA) || USE(ATSPI)
</ins><span class="cx">     if (object) {
</span><span class="cx">         if (enqueuePasswordValueChangeNotification(object))
</span><span class="cx">             return;
</span><span class="lines">@@ -1560,6 +1568,7 @@
</span><span class="cx"> 
</span><span class="cx"> bool AXObjectCache::enqueuePasswordValueChangeNotification(AccessibilityObject* object)
</span><span class="cx"> {
</span><ins>+#if PLATFORM(COCOA)
</ins><span class="cx">     if (!isPasswordFieldOrContainedByPasswordField(object))
</span><span class="cx">         return false;
</span><span class="cx"> 
</span><span class="lines">@@ -1575,6 +1584,10 @@
</span><span class="cx">         m_passwordNotificationPostTimer.startOneShot(accessibilityPasswordValueChangeNotificationInterval);
</span><span class="cx"> 
</span><span class="cx">     return true;
</span><ins>+#else
+    UNUSED_PARAM(object);
+    return false;
+#endif
</ins><span class="cx"> }
</span><span class="cx"> 
</span><span class="cx"> void AXObjectCache::frameLoadingEventNotification(Frame* frame, AXLoadingEvent loadingEvent)
</span><span class="lines">@@ -3463,7 +3476,7 @@
</span><span class="cx">         m_cache->stopCachingComputedObjectAttributes();
</span><span class="cx"> }
</span><span class="cx"> 
</span><del>-#if !PLATFORM(COCOA)
</del><ins>+#if !PLATFORM(COCOA) && !USE(ATSPI)
</ins><span class="cx"> AXTextChange AXObjectCache::textChangeForEditType(AXTextEditType type)
</span><span class="cx"> {
</span><span class="cx">     switch (type) {
</span></span></pre></div>
<a id="trunkSourceWebCoreaccessibilityAXObjectCacheh"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebCore/accessibility/AXObjectCache.h (284674 => 284675)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebCore/accessibility/AXObjectCache.h       2021-10-22 08:44:57 UTC (rev 284674)
+++ trunk/Source/WebCore/accessibility/AXObjectCache.h  2021-10-22 08:51:03 UTC (rev 284675)
</span><span class="lines">@@ -398,7 +398,7 @@
</span><span class="cx"> 
</span><span class="cx">     void platformPerformDeferredCacheUpdate();
</span><span class="cx"> 
</span><del>-#if PLATFORM(COCOA)
</del><ins>+#if PLATFORM(COCOA) || USE(ATSPI)
</ins><span class="cx">     void postTextStateChangePlatformNotification(AXCoreObject*, const AXTextStateChangeIntent&, const VisibleSelection&);
</span><span class="cx">     void postTextStateChangePlatformNotification(AccessibilityObject*, AXTextEditType, const String&, const VisiblePosition&);
</span><span class="cx">     void postTextReplacementPlatformNotificationForTextControl(AXCoreObject*, const String& deletedText, const String& insertedText, HTMLTextFormControlElement&);
</span></span></pre></div>
<a id="trunkSourceWebCoreaccessibilityAccessibilityObjectcpp"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebCore/accessibility/AccessibilityObject.cpp (284674 => 284675)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebCore/accessibility/AccessibilityObject.cpp       2021-10-22 08:44:57 UTC (rev 284674)
+++ trunk/Source/WebCore/accessibility/AccessibilityObject.cpp  2021-10-22 08:51:03 UTC (rev 284675)
</span><span class="lines">@@ -3273,8 +3273,8 @@
</span><span class="cx"> TextIteratorBehaviors AccessibilityObject::textIteratorBehaviorForTextRange() const
</span><span class="cx"> {
</span><span class="cx">     TextIteratorBehaviors behaviors { TextIteratorBehavior::IgnoresStyleVisibility };
</span><del>-    
-#if USE(ATK)
</del><ins>+
+#if USE(ATK) || USE(ATSPI)
</ins><span class="cx">     // We need to emit replaced elements for GTK, and present
</span><span class="cx">     // them with the 'object replacement character' (0xFFFC).
</span><span class="cx">     behaviors.add(TextIteratorBehavior::EmitsObjectReplacementCharacters);
</span></span></pre></div>
<a id="trunkSourceWebCoreaccessibilityAccessibilityObjecth"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebCore/accessibility/AccessibilityObject.h (284674 => 284675)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebCore/accessibility/AccessibilityObject.h 2021-10-22 08:44:57 UTC (rev 284674)
+++ trunk/Source/WebCore/accessibility/AccessibilityObject.h    2021-10-22 08:51:03 UTC (rev 284675)
</span><span class="lines">@@ -833,7 +833,7 @@
</span><span class="cx"> inline void AccessibilityObject::detachPlatformWrapper(AccessibilityDetachmentType) { }
</span><span class="cx"> #endif
</span><span class="cx"> 
</span><del>-#if !(ENABLE(ACCESSIBILITY) && USE(ATK))
</del><ins>+#if !(ENABLE(ACCESSIBILITY) && (USE(ATK) || USE(ATSPI)))
</ins><span class="cx"> inline bool AccessibilityObject::allowsTextRanges() const { return true; }
</span><span class="cx"> inline unsigned AccessibilityObject::getLengthForTextRange() const { return text().length(); }
</span><span class="cx"> #endif
</span></span></pre></div>
<a id="trunkSourceWebCoreaccessibilityAccessibilityRenderObjectcpp"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebCore/accessibility/AccessibilityRenderObject.cpp (284674 => 284675)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebCore/accessibility/AccessibilityRenderObject.cpp 2021-10-22 08:44:57 UTC (rev 284674)
+++ trunk/Source/WebCore/accessibility/AccessibilityRenderObject.cpp    2021-10-22 08:51:03 UTC (rev 284675)
</span><span class="lines">@@ -2160,7 +2160,7 @@
</span><span class="cx">     if (!node)
</span><span class="cx">         return 0;
</span><span class="cx"> 
</span><del>-#if USE(ATK)
</del><ins>+#if USE(ATK) || USE(ATSPI)
</ins><span class="cx">     // We need to consider replaced elements for GTK, as they will be
</span><span class="cx">     // presented with the 'object replacement character' (0xFFFC).
</span><span class="cx">     bool forSelectionPreservation = true;
</span></span></pre></div>
<a id="trunkSourceWebCoreaccessibilityatspiAXObjectCacheAtspicpp"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebCore/accessibility/atspi/AXObjectCacheAtspi.cpp (284674 => 284675)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebCore/accessibility/atspi/AXObjectCacheAtspi.cpp  2021-10-22 08:44:57 UTC (rev 284674)
+++ trunk/Source/WebCore/accessibility/atspi/AXObjectCacheAtspi.cpp     2021-10-22 08:51:03 UTC (rev 284675)
</span><span class="lines">@@ -21,6 +21,7 @@
</span><span class="cx"> #include "AXObjectCache.h"
</span><span class="cx"> 
</span><span class="cx"> #if ENABLE(ACCESSIBILITY) && USE(ATSPI)
</span><ins>+#include "AXTextStateChangeIntent.h"
</ins><span class="cx"> #include "AccessibilityObject.h"
</span><span class="cx"> #include "AccessibilityObjectAtspi.h"
</span><span class="cx"> #include "AccessibilityRenderObject.h"
</span><span class="lines">@@ -185,10 +186,95 @@
</span><span class="cx">     }
</span><span class="cx"> }
</span><span class="cx"> 
</span><del>-void AXObjectCache::nodeTextChangePlatformNotification(AccessibilityObject* object, AXTextChange textChange, unsigned offset, const String& text)
</del><ins>+void AXObjectCache::postTextStateChangePlatformNotification(AXCoreObject* coreObject, const AXTextStateChangeIntent&, const VisibleSelection& selection)
</ins><span class="cx"> {
</span><ins>+    RELEASE_ASSERT(isMainThread());
+    if (!coreObject)
+        coreObject = rootWebArea();
+
+    if (!coreObject)
+        return;
+
+    auto* wrapper = coreObject->wrapper();
+    if (!wrapper)
+        return;
+
+    wrapper->selectionChanged(selection);
</ins><span class="cx"> }
</span><span class="cx"> 
</span><ins>+void AXObjectCache::postTextStateChangePlatformNotification(AccessibilityObject* coreObject, AXTextEditType editType, const String& text, const VisiblePosition& position)
+{
+    RELEASE_ASSERT(isMainThread());
+    if (text.isEmpty())
+        return;
+
+    auto* wrapper = coreObject->wrapper();
+    if (!wrapper)
+        return;
+
+    switch (editType) {
+    case AXTextEditTypeDelete:
+    case AXTextEditTypeCut:
+        wrapper->textDeleted(text, position);
+        break;
+    case AXTextEditTypeInsert:
+    case AXTextEditTypeTyping:
+    case AXTextEditTypeDictation:
+    case AXTextEditTypePaste:
+        wrapper->textInserted(text, position);
+        break;
+    case AXTextEditTypeAttributesChange:
+        wrapper->textAttributesChanged();
+        break;
+    case AXTextEditTypeUnknown:
+        break;
+    }
+}
+
+void AXObjectCache::postTextReplacementPlatformNotificationForTextControl(AXCoreObject* coreObject, const String& deletedText, const String& insertedText, HTMLTextFormControlElement&)
+{
+    RELEASE_ASSERT(isMainThread());
+    if (!coreObject)
+        coreObject = rootWebArea();
+
+    if (!coreObject)
+        return;
+
+    if (deletedText.isEmpty() && insertedText.isEmpty())
+        return;
+
+    auto* wrapper = coreObject->wrapper();
+    if (!wrapper)
+        return;
+
+    if (!deletedText.isEmpty())
+        wrapper->textDeleted(deletedText, coreObject->visiblePositionForIndex(0));
+    if (!insertedText.isEmpty())
+        wrapper->textInserted(insertedText, coreObject->visiblePositionForIndex(insertedText.length()));
+}
+
+void AXObjectCache::postTextReplacementPlatformNotification(AXCoreObject* coreObject, AXTextEditType, const String& deletedText, AXTextEditType, const String& insertedText, const VisiblePosition& position)
+{
+    RELEASE_ASSERT(isMainThread());
+    if (!coreObject)
+        coreObject = rootWebArea();
+
+    if (!coreObject)
+        return;
+
+    if (deletedText.isEmpty() && insertedText.isEmpty())
+        return;
+
+    auto* wrapper = coreObject->wrapper();
+    if (!wrapper)
+        return;
+
+    if (!deletedText.isEmpty())
+        wrapper->textDeleted(deletedText, position);
+    if (!insertedText.isEmpty())
+        wrapper->textInserted(insertedText, position);
+}
+
</ins><span class="cx"> void AXObjectCache::frameLoadingEventPlatformNotification(AccessibilityObject* object, AXLoadingEvent loadingEvent)
</span><span class="cx"> {
</span><span class="cx"> }
</span></span></pre></div>
<a id="trunkSourceWebCoreaccessibilityatspiAccessibilityAtspicpp"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebCore/accessibility/atspi/AccessibilityAtspi.cpp (284674 => 284675)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebCore/accessibility/atspi/AccessibilityAtspi.cpp  2021-10-22 08:44:57 UTC (rev 284674)
+++ trunk/Source/WebCore/accessibility/atspi/AccessibilityAtspi.cpp     2021-10-22 08:51:03 UTC (rev 284675)
</span><span class="lines">@@ -167,6 +167,54 @@
</span><span class="cx">     });
</span><span class="cx"> }
</span><span class="cx"> 
</span><ins>+void AccessibilityAtspi::textChanged(AccessibilityObjectAtspi& atspiObject, const char* changeType, CString&& text, unsigned offset, unsigned length)
+{
+    RELEASE_ASSERT(isMainThread());
+    m_queue->dispatch([this, atspiObject = Ref { atspiObject }, changeType = CString(changeType), text = WTFMove(text), offset, length] {
+        if (!m_connection)
+            return;
+
+        g_dbus_connection_emit_signal(m_connection.get(), nullptr, atspiObject->path().utf8().data(), "org.a11y.atspi.Event.Object", "TextChanged",
+            g_variant_new("(siiva{sv})", changeType.data(), offset, length, g_variant_new_string(text.data()), nullptr), nullptr);
+    });
+}
+
+void AccessibilityAtspi::textAttributesChanged(AccessibilityObjectAtspi& atspiObject)
+{
+    RELEASE_ASSERT(isMainThread());
+    m_queue->dispatch([this, atspiObject = Ref { atspiObject }] {
+        if (!m_connection)
+            return;
+
+        g_dbus_connection_emit_signal(m_connection.get(), nullptr, atspiObject->path().utf8().data(), "org.a11y.atspi.Event.Object", "TextAttributesChanged",
+            g_variant_new("(siiva{sv})", "", 0, 0, g_variant_new_string(""), nullptr), nullptr);
+    });
+}
+
+void AccessibilityAtspi::textCaretMoved(AccessibilityObjectAtspi& atspiObject, unsigned caretOffset)
+{
+    RELEASE_ASSERT(isMainThread());
+    m_queue->dispatch([this, atspiObject = Ref { atspiObject }, caretOffset] {
+        if (!m_connection)
+            return;
+
+        g_dbus_connection_emit_signal(m_connection.get(), nullptr, atspiObject->path().utf8().data(), "org.a11y.atspi.Event.Object", "TextCaretMoved",
+            g_variant_new("(siiva{sv})", "", caretOffset, 0, g_variant_new_string(""), nullptr), nullptr);
+    });
+}
+
+void AccessibilityAtspi::textSelectionChanged(AccessibilityObjectAtspi& atspiObject)
+{
+    RELEASE_ASSERT(isMainThread());
+    m_queue->dispatch([this, atspiObject = Ref { atspiObject }] {
+        if (!m_connection)
+            return;
+
+        g_dbus_connection_emit_signal(m_connection.get(), nullptr, atspiObject->path().utf8().data(), "org.a11y.atspi.Event.Object", "TextSelectionChanged",
+            g_variant_new("(siiva{sv})", "", 0, 0, g_variant_new_string(""), nullptr), nullptr);
+    });
+}
+
</ins><span class="cx"> struct RoleNameEntry {
</span><span class="cx">     const char* name;
</span><span class="cx">     const char* localizedName;
</span></span></pre></div>
<a id="trunkSourceWebCoreaccessibilityatspiAccessibilityAtspih"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebCore/accessibility/atspi/AccessibilityAtspi.h (284674 => 284675)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebCore/accessibility/atspi/AccessibilityAtspi.h    2021-10-22 08:44:57 UTC (rev 284674)
+++ trunk/Source/WebCore/accessibility/atspi/AccessibilityAtspi.h       2021-10-22 08:51:03 UTC (rev 284675)
</span><span class="lines">@@ -56,6 +56,11 @@
</span><span class="cx"> 
</span><span class="cx">     void stateChanged(AccessibilityObjectAtspi&, const char*, bool);
</span><span class="cx"> 
</span><ins>+    void textChanged(AccessibilityObjectAtspi&, const char*, CString&&, unsigned, unsigned);
+    void textAttributesChanged(AccessibilityObjectAtspi&);
+    void textCaretMoved(AccessibilityObjectAtspi&, unsigned);
+    void textSelectionChanged(AccessibilityObjectAtspi&);
+
</ins><span class="cx">     static const char* localizedRoleName(AccessibilityRole);
</span><span class="cx"> 
</span><span class="cx"> private:
</span></span></pre></div>
<a id="trunkSourceWebCoreaccessibilityatspiAccessibilityAtspiEnumsh"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebCore/accessibility/atspi/AccessibilityAtspiEnums.h (284674 => 284675)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebCore/accessibility/atspi/AccessibilityAtspiEnums.h       2021-10-22 08:44:57 UTC (rev 284674)
+++ trunk/Source/WebCore/accessibility/atspi/AccessibilityAtspiEnums.h  2021-10-22 08:51:03 UTC (rev 284675)
</span><span class="lines">@@ -258,6 +258,24 @@
</span><span class="cx">     Anywhere
</span><span class="cx"> };
</span><span class="cx"> 
</span><ins>+enum TextBoundaryType {
+    CharBoundary,
+    WordStartBoundary,
+    WordEndBoundary,
+    SentenceStartBoundary,
+    SentenceEndBoundary,
+    LineStartBoundary,
+    LineEndBoundary
+};
+
+enum TextGranularityType {
+    CharGranularity,
+    WordGranularity,
+    SentenceGranularity,
+    LineGranularity,
+    ParagraphGranularity
+};
+
</ins><span class="cx"> } // namespace Atspi
</span><span class="cx"> } // namespace WebCore
</span><span class="cx"> 
</span></span></pre></div>
<a id="trunkSourceWebCoreaccessibilityatspiAccessibilityObjectAtspicpp"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebCore/accessibility/atspi/AccessibilityObjectAtspi.cpp (284674 => 284675)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebCore/accessibility/atspi/AccessibilityObjectAtspi.cpp    2021-10-22 08:44:57 UTC (rev 284674)
+++ trunk/Source/WebCore/accessibility/atspi/AccessibilityObjectAtspi.cpp       2021-10-22 08:51:03 UTC (rev 284675)
</span><span class="lines">@@ -21,6 +21,7 @@
</span><span class="cx"> #include "AccessibilityObjectAtspi.h"
</span><span class="cx"> 
</span><span class="cx"> #if ENABLE(ACCESSIBILITY) && USE(ATSPI)
</span><ins>+#include "AXIsolatedObject.h"
</ins><span class="cx"> #include "AccessibilityAtspiEnums.h"
</span><span class="cx"> #include "AccessibilityObjectInterface.h"
</span><span class="cx"> #include "AccessibilityRootAtspi.h"
</span><span class="lines">@@ -28,6 +29,7 @@
</span><span class="cx"> #include "ElementInlines.h"
</span><span class="cx"> #include "RenderAncestorIterator.h"
</span><span class="cx"> #include "RenderBlock.h"
</span><ins>+#include "RenderObject.h"
</ins><span class="cx"> #include <glib/gi18n-lib.h>
</span><span class="cx"> #include <wtf/MainThread.h>
</span><span class="cx"> #include <wtf/UUID.h>
</span><span class="lines">@@ -39,10 +41,48 @@
</span><span class="cx">     return adoptRef(*new AccessibilityObjectAtspi(coreObject));
</span><span class="cx"> }
</span><span class="cx"> 
</span><ins>+static inline bool roleIsTextType(AccessibilityRole role)
+{
+    return role == AccessibilityRole::Paragraph
+        || role == AccessibilityRole::Heading
+        || role == AccessibilityRole::Div
+        || role == AccessibilityRole::Cell
+        || role == AccessibilityRole::Link
+        || role == AccessibilityRole::WebCoreLink
+        || role == AccessibilityRole::ListItem
+        || role == AccessibilityRole::Pre
+        || role == AccessibilityRole::GridCell
+        || role == AccessibilityRole::TextGroup
+        || role == AccessibilityRole::ApplicationTextGroup
+        || role == AccessibilityRole::ApplicationGroup;
+}
+
</ins><span class="cx"> OptionSet<AccessibilityObjectAtspi::Interface> AccessibilityObjectAtspi::interfacesForObject(AXCoreObject& coreObject)
</span><span class="cx"> {
</span><span class="cx">     OptionSet<Interface> interfaces = { Interface::Accessible, Interface::Component };
</span><span class="cx"> 
</span><ins>+    RenderObject* renderer = coreObject.isAccessibilityRenderObject() ? coreObject.renderer() : nullptr;
+    if (coreObject.roleValue() == AccessibilityRole::StaticText || coreObject.roleValue() == AccessibilityRole::ColorWell)
+        interfaces.add(Interface::Text);
+    else if (coreObject.isTextControl() || coreObject.isNonNativeTextControl())
+        interfaces.add(Interface::Text);
+    else if (!coreObject.isWebArea()) {
+        if (coreObject.roleValue() != AccessibilityRole::Table) {
+            if ((renderer && renderer->childrenInline()) || roleIsTextType(coreObject.roleValue()) || coreObject.isMathToken())
+                interfaces.add(Interface::Text);
+        }
+
+        // Add the Text interface for list items whose first accessible child has a text renderer.
+        if (coreObject.roleValue() == AccessibilityRole::ListItem) {
+            const auto& children = coreObject.children();
+            if (!children.isEmpty()) {
+                auto childInterfaces = interfacesForObject(*children[0]);
+                if (childInterfaces.contains(Interface::Text))
+                    interfaces.add(Interface::Text);
+            }
+        }
+    }
+
</ins><span class="cx">     return interfaces;
</span><span class="cx"> }
</span><span class="cx"> 
</span><span class="lines">@@ -427,6 +467,8 @@
</span><span class="cx">             interfaces.append({ const_cast<GDBusInterfaceInfo*>(&webkit_accessible_interface), &s_accessibleFunctions });
</span><span class="cx">         if (m_interfaces.contains(Interface::Component))
</span><span class="cx">             interfaces.append({ const_cast<GDBusInterfaceInfo*>(&webkit_component_interface), &s_componentFunctions });
</span><ins>+        if (m_interfaces.contains(Interface::Text))
+            interfaces.append({ const_cast<GDBusInterfaceInfo*>(&webkit_text_interface), &s_textFunctions });
</ins><span class="cx">         m_path = atspiRoot->atspi().registerObject(*this, WTFMove(interfaces));
</span><span class="cx">     }
</span><span class="cx"> 
</span><span class="lines">@@ -1040,6 +1082,8 @@
</span><span class="cx">         g_variant_builder_add(builder, "s", webkit_accessible_interface.name);
</span><span class="cx">     if (m_interfaces.contains(Interface::Component))
</span><span class="cx">         g_variant_builder_add(builder, "s", webkit_component_interface.name);
</span><ins>+    if (m_interfaces.contains(Interface::Text))
+        g_variant_builder_add(builder, "s", webkit_text_interface.name);
</ins><span class="cx"> }
</span><span class="cx"> 
</span><span class="cx"> void AccessibilityObjectAtspi::serialize(GVariantBuilder* builder) const
</span></span></pre></div>
<a id="trunkSourceWebCoreaccessibilityatspiAccessibilityObjectAtspih"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebCore/accessibility/atspi/AccessibilityObjectAtspi.h (284674 => 284675)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebCore/accessibility/atspi/AccessibilityObjectAtspi.h      2021-10-22 08:44:57 UTC (rev 284674)
+++ trunk/Source/WebCore/accessibility/atspi/AccessibilityObjectAtspi.h 2021-10-22 08:51:03 UTC (rev 284675)
</span><span class="lines">@@ -44,7 +44,8 @@
</span><span class="cx"> 
</span><span class="cx">     enum class Interface : uint8_t {
</span><span class="cx">         Accessible = 1 << 0,
</span><del>-        Component = 1 << 1
</del><ins>+        Component = 1 << 1,
+        Text = 1 << 2
</ins><span class="cx">     };
</span><span class="cx">     const OptionSet<Interface>& interfaces() const { return m_interfaces; }
</span><span class="cx"> 
</span><span class="lines">@@ -82,6 +83,32 @@
</span><span class="cx">     void scrollToMakeVisible(uint32_t) const;
</span><span class="cx">     void scrollToPoint(const IntPoint&, uint32_t) const;
</span><span class="cx"> 
</span><ins>+    String text() const;
+    enum class TextGranularity {
+        Character,
+        WordStart,
+        WordEnd,
+        SentenceStart,
+        SentenceEnd,
+        LineStart,
+        LineEnd,
+        Paragraph
+    };
+    IntPoint boundaryOffset(unsigned, TextGranularity) const;
+    IntRect boundsForRange(unsigned, unsigned, uint32_t) const;
+    struct TextAttributes {
+        HashMap<String, String> attributes;
+        int startOffset;
+        int endOffset;
+    };
+    TextAttributes textAttributes(std::optional<unsigned> = std::nullopt, bool = false) const;
+    IntPoint selectedRange() const;
+    void setSelectedRange(unsigned, unsigned);
+    void textInserted(const String&, const VisiblePosition&);
+    void textDeleted(const String&, const VisiblePosition&);
+    void textAttributesChanged();
+    void selectionChanged(const VisibleSelection&);
+
</ins><span class="cx"> private:
</span><span class="cx">     explicit AccessibilityObjectAtspi(AXCoreObject*);
</span><span class="cx"> 
</span><span class="lines">@@ -99,10 +126,25 @@
</span><span class="cx">     bool focus() const;
</span><span class="cx">     float opacity() const;
</span><span class="cx"> 
</span><ins>+    static TextGranularity atspiBoundaryToTextGranularity(uint32_t);
+    static TextGranularity atspiGranularityToTextGranularity(uint32_t);
+    CString text(int, int) const;
+    CString textAtOffset(int, TextGranularity, int&, int&) const;
+    int characterAtOffset(int) const;
+    IntRect textExtents(int, int, uint32_t) const;
+    int offsetAtPoint(const IntPoint&, uint32_t) const;
+    IntPoint boundsForSelection(const VisibleSelection&) const;
+    bool selectionBounds(int&, int&) const;
+    bool selectRange(int, int);
+    TextAttributes textAttributesWithUTF8Offset(std::optional<int> = std::nullopt, bool = false) const;
+    bool scrollToMakeVisible(int, int, uint32_t) const;
+    bool scrollToPoint(int, int, uint32_t, int, int) const;
+
</ins><span class="cx">     static OptionSet<Interface> interfacesForObject(AXCoreObject&);
</span><span class="cx"> 
</span><span class="cx">     static GDBusInterfaceVTable s_accessibleFunctions;
</span><span class="cx">     static GDBusInterfaceVTable s_componentFunctions;
</span><ins>+    static GDBusInterfaceVTable s_textFunctions;
</ins><span class="cx"> 
</span><span class="cx">     AXCoreObject* m_axObject { nullptr };
</span><span class="cx">     AXCoreObject* m_coreObject { nullptr };
</span></span></pre></div>
<a id="trunkSourceWebCoreaccessibilityatspiAccessibilityObjectTextAtspicpp"></a>
<div class="addfile"><h4>Added: trunk/Source/WebCore/accessibility/atspi/AccessibilityObjectTextAtspi.cpp (0 => 284675)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebCore/accessibility/atspi/AccessibilityObjectTextAtspi.cpp                                (rev 0)
+++ trunk/Source/WebCore/accessibility/atspi/AccessibilityObjectTextAtspi.cpp   2021-10-22 08:51:03 UTC (rev 284675)
</span><span class="lines">@@ -0,0 +1,1009 @@
</span><ins>+/*
+ * Copyright (C) 2021 Igalia S.L.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public License
+ * along with this library; see the file COPYING.LIB.  If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#include "config.h"
+#include "AccessibilityObjectAtspi.h"
+
+#if ENABLE(ACCESSIBILITY) && USE(ATSPI)
+#include "AccessibilityAtspiEnums.h"
+#include "AccessibilityObjectInterface.h"
+#include "Editing.h"
+#include "PlatformScreen.h"
+#include "SurrogatePairAwareTextIterator.h"
+#include "TextIterator.h"
+#include "VisibleUnits.h"
+
+namespace WebCore {
+
+AccessibilityObjectAtspi::TextGranularity AccessibilityObjectAtspi::atspiBoundaryToTextGranularity(uint32_t boundaryType)
+{
+    switch (boundaryType) {
+    case Atspi::TextBoundaryType::CharBoundary:
+        return TextGranularity::Character;
+    case Atspi::TextBoundaryType::WordStartBoundary:
+        return TextGranularity::WordStart;
+    case Atspi::TextBoundaryType::WordEndBoundary:
+        return TextGranularity::WordEnd;
+    case Atspi::TextBoundaryType::SentenceStartBoundary:
+        return TextGranularity::SentenceStart;
+    case Atspi::TextBoundaryType::SentenceEndBoundary:
+        return TextGranularity::SentenceEnd;
+    case Atspi::TextBoundaryType::LineStartBoundary:
+        return TextGranularity::LineStart;
+    case Atspi::TextBoundaryType::LineEndBoundary:
+        return TextGranularity::LineEnd;
+    }
+    RELEASE_ASSERT_NOT_REACHED();
+}
+
+AccessibilityObjectAtspi::TextGranularity AccessibilityObjectAtspi::atspiGranularityToTextGranularity(uint32_t boundaryType)
+{
+    switch (boundaryType) {
+    case Atspi::TextGranularityType::CharGranularity:
+        return TextGranularity::Character;
+    case Atspi::TextGranularityType::WordGranularity:
+        return TextGranularity::WordStart;
+    case Atspi::TextGranularityType::SentenceGranularity:
+        return TextGranularity::SentenceStart;
+    case Atspi::TextGranularityType::LineGranularity:
+        return TextGranularity::LineStart;
+    case Atspi::TextGranularityType::ParagraphGranularity:
+        return TextGranularity::Paragraph;
+    }
+    RELEASE_ASSERT_NOT_REACHED();
+}
+
+GDBusInterfaceVTable AccessibilityObjectAtspi::s_textFunctions = {
+    // method_call
+    [](GDBusConnection*, const gchar*, const gchar*, const gchar*, const gchar* methodName, GVariant* parameters, GDBusMethodInvocation* invocation, gpointer userData) {
+        RELEASE_ASSERT(!isMainThread());
+        auto atspiObject = Ref { *static_cast<AccessibilityObjectAtspi*>(userData) };
+        atspiObject->updateBackingStore();
+
+        if (!g_strcmp0(methodName, "GetStringAtOffset")) {
+            int offset;
+            uint32_t granularityType;
+            g_variant_get(parameters, "(iu)", &offset, &granularityType);
+            int start = 0, end = 0;
+            auto text = atspiObject->textAtOffset(offset, atspiGranularityToTextGranularity(granularityType), start, end);
+            g_dbus_method_invocation_return_value(invocation, g_variant_new("(sii)", text.isNull() ? "" : text.data(), start, end));
+        } else if (!g_strcmp0(methodName, "GetText")) {
+            int start, end;
+            g_variant_get(parameters, "(ii)", &start, &end);
+            auto text = atspiObject->text(start, end);
+            g_dbus_method_invocation_return_value(invocation, g_variant_new("(s)", text.isNull() ? "" : text.data()));
+        } else if (!g_strcmp0(methodName, "SetCaretOffset")) {
+            int offset;
+            g_variant_get(parameters, "(i)", &offset);
+            g_dbus_method_invocation_return_value(invocation, g_variant_new("(b)", atspiObject->selectRange(offset, offset)));
+        } else if (!g_strcmp0(methodName, "GetTextBeforeOffset")) {
+            g_dbus_method_invocation_return_error_literal(invocation, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED, "");
+        } else if (!g_strcmp0(methodName, "GetTextAtOffset")) {
+            int offset;
+            uint32_t boundaryType;
+            g_variant_get(parameters, "(iu)", &offset, &boundaryType);
+            int start = 0, end = 0;
+            auto text = atspiObject->textAtOffset(offset, atspiBoundaryToTextGranularity(boundaryType), start, end);
+            g_dbus_method_invocation_return_value(invocation, g_variant_new("(sii)", text.isNull() ? "" : text.data(), start, end));
+        } else if (!g_strcmp0(methodName, "GetTextAfterOffset"))
+            g_dbus_method_invocation_return_error_literal(invocation, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED, "");
+        else if (!g_strcmp0(methodName, "GetCharacterAtOffset")) {
+            int offset;
+            g_variant_get(parameters, "(i)", &offset);
+            g_dbus_method_invocation_return_value(invocation, g_variant_new("(i)", atspiObject->characterAtOffset(offset)));
+        } else if (!g_strcmp0(methodName, "GetAttributeValue")) {
+            int offset;
+            const char* name;
+            g_variant_get(parameters, "(i&s)", &offset, &name);
+            auto attributes = atspiObject->textAttributesWithUTF8Offset(offset);
+            g_dbus_method_invocation_return_value(invocation, g_variant_new("(s)", attributes.attributes.get(String::fromUTF8(name)).utf8().data()));
+        } else if (!g_strcmp0(methodName, "GetAttributes")) {
+            int offset;
+            g_variant_get(parameters, "(i)", &offset);
+            auto attributes = atspiObject->textAttributesWithUTF8Offset(offset);
+            GVariantBuilder builder = G_VARIANT_BUILDER_INIT(G_VARIANT_TYPE("a{ss}"));
+            for (const auto& it : attributes.attributes)
+                g_variant_builder_add(&builder, "{ss}", it.key.utf8().data(), it.value.utf8().data());
+            g_dbus_method_invocation_return_value(invocation, g_variant_new("(a{ss}ii)", &builder, attributes.startOffset, attributes.endOffset));
+        } else if (!g_strcmp0(methodName, "GetAttributeRun")) {
+            int offset;
+            gboolean includeDefaults;
+            g_variant_get(parameters, "(ib)", &offset, &includeDefaults);
+            auto attributes = atspiObject->textAttributesWithUTF8Offset(offset, includeDefaults);
+            GVariantBuilder builder = G_VARIANT_BUILDER_INIT(G_VARIANT_TYPE("a{ss}"));
+            for (const auto& it : attributes.attributes)
+                g_variant_builder_add(&builder, "{ss}", it.key.utf8().data(), it.value.utf8().data());
+            g_dbus_method_invocation_return_value(invocation, g_variant_new("(a{ss}ii)", &builder, attributes.startOffset, attributes.endOffset));
+        } else if (!g_strcmp0(methodName, "GetDefaultAttributes") || !g_strcmp0(methodName, "GetDefaultAttributeSet")) {
+            auto attributes = atspiObject->textAttributesWithUTF8Offset();
+            GVariantBuilder builder = G_VARIANT_BUILDER_INIT(G_VARIANT_TYPE("a{ss}"));
+            for (const auto& it : attributes.attributes)
+                g_variant_builder_add(&builder, "{ss}", it.key.utf8().data(), it.value.utf8().data());
+            g_dbus_method_invocation_return_value(invocation, g_variant_new("(a{ss})", &builder));
+        } else if (!g_strcmp0(methodName, "GetCharacterExtents")) {
+            int offset;
+            uint32_t coordinateType;
+            g_variant_get(parameters, "(iu)", &offset, &coordinateType);
+            auto extents = atspiObject->textExtents(offset, offset + 1, coordinateType);
+            g_dbus_method_invocation_return_value(invocation, g_variant_new("(iiii)", extents.x(), extents.y(), extents.width(), extents.height()));
+        } else if (!g_strcmp0(methodName, "GetRangeExtents")) {
+            int start, end;
+            uint32_t coordinateType;
+            g_variant_get(parameters, "(iiu)", &start, &end, &coordinateType);
+            auto extents = atspiObject->textExtents(start, end, coordinateType);
+            g_dbus_method_invocation_return_value(invocation, g_variant_new("(iiii)", extents.x(), extents.y(), extents.width(), extents.height()));
+        } else if (!g_strcmp0(methodName, "GetOffsetAtPoint")) {
+            int x, y;
+            uint32_t coordinateType;
+            g_variant_get(parameters, "(iiu)", &x, &y, &coordinateType);
+            g_dbus_method_invocation_return_value(invocation, g_variant_new("(i)", atspiObject->offsetAtPoint(IntPoint(x, y), coordinateType)));
+        } else if (!g_strcmp0(methodName, "GetNSelections")) {
+            int start, end;
+            g_dbus_method_invocation_return_value(invocation, g_variant_new("(i)", atspiObject->selectionBounds(start, end) && start != end ? 1 : 0));
+        } else if (!g_strcmp0(methodName, "GetSelection")) {
+            int selectionNumber;
+            g_variant_get(parameters, "(i)", &selectionNumber);
+            if (selectionNumber)
+                g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, "Not a valid selection: %d", selectionNumber);
+            else {
+                int start = 0, end = 0;
+                if (atspiObject->selectionBounds(start, end) && start == end)
+                    start = end = 0;
+                g_dbus_method_invocation_return_value(invocation, g_variant_new("(ii)", start, end));
+            }
+        } else if (!g_strcmp0(methodName, "AddSelection")) {
+            int start, end;
+            g_variant_get(parameters, "(ii)", &start, &end);
+            g_dbus_method_invocation_return_value(invocation, g_variant_new("(b)", atspiObject->selectRange(start, end)));
+        } else if (!g_strcmp0(methodName, "SetSelection")) {
+            int start, end, selectionNumber;
+            g_variant_get(parameters, "(iii)", &selectionNumber, &start, &end);
+            if (selectionNumber)
+                g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, "Not a valid selection: %d", selectionNumber);
+            else
+                g_dbus_method_invocation_return_value(invocation, g_variant_new("(b)", atspiObject->selectRange(start, end)));
+        } else if (!g_strcmp0(methodName, "RemoveSelection")) {
+            int selectionNumber;
+            int caretOffset = -1;
+            if (!selectionNumber) {
+                int start, end;
+                if (atspiObject->selectionBounds(start, end))
+                    caretOffset = end;
+            }
+            g_dbus_method_invocation_return_value(invocation, g_variant_new("(b)", caretOffset != -1 ? atspiObject->selectRange(caretOffset, caretOffset) : FALSE));
+        } else if (!g_strcmp0(methodName, "ScrollSubstringTo")) {
+            int start, end;
+            uint32_t scrollType;
+            g_variant_get(parameters, "(iiu)", &start, &end, &scrollType);
+            g_dbus_method_invocation_return_value(invocation, g_variant_new("(b)", atspiObject->scrollToMakeVisible(start, end, scrollType)));
+        } else if (!g_strcmp0(methodName, "ScrollSubstringToPoint")) {
+            int start, end, x, y;
+            uint32_t coordinateType;
+            g_variant_get(parameters, "(iiuii)", &start, &end, &coordinateType, &x, &y);
+            g_dbus_method_invocation_return_value(invocation, g_variant_new("(b)", atspiObject->scrollToPoint(start, end, coordinateType, x, y)));
+        } else if (!g_strcmp0(methodName, "GetBoundedRanges") || !g_strcmp0(methodName, "ScrollSubstringToPoint"))
+            g_dbus_method_invocation_return_error_literal(invocation, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED, "");
+    },
+    // get_property
+    [](GDBusConnection*, const gchar*, const gchar*, const gchar*, const gchar* propertyName, GError** error, gpointer userData) -> GVariant* {
+        RELEASE_ASSERT(!isMainThread());
+        auto atspiObject = Ref { *static_cast<AccessibilityObjectAtspi*>(userData) };
+        atspiObject->updateBackingStore();
+
+        if (!g_strcmp0(propertyName, "CharacterCount"))
+            return g_variant_new_int32(g_utf8_strlen(atspiObject->text().utf8().data(), -1));
+        if (!g_strcmp0(propertyName, "CaretOffset")) {
+            int start = 0, end = 0;
+            return g_variant_new_int32(atspiObject->selectionBounds(start, end) ? end : -1);
+        }
+
+        g_set_error(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "Unknown property '%s'", propertyName);
+        return nullptr;
+    },
+    // set_property,
+    nullptr,
+    // padding
+    nullptr
+};
+
+static Vector<unsigned, 128> offsetMapping(const String& text)
+{
+    if (text.is8Bit())
+        return { };
+
+    Vector<unsigned, 128> offsets;
+    SurrogatePairAwareTextIterator iterator(text.characters16(), 0, text.length(), text.length());
+    UChar32 character;
+    unsigned clusterLength = 0;
+    unsigned i;
+    for (i = 0; iterator.consume(character, clusterLength); iterator.advance(clusterLength), ++i) {
+        for (unsigned j = 0; j < clusterLength; ++j)
+            offsets.append(i);
+    }
+    offsets.append(i++);
+    return offsets;
+}
+
+static inline unsigned UTF16OffsetToUTF8(const Vector<unsigned, 128>& mapping, unsigned offset)
+{
+    return mapping.isEmpty() ? offset : mapping[offset];
+}
+
+static inline unsigned UTF8OffsetToUTF16(const Vector<unsigned, 128>& mapping, unsigned offset)
+{
+    if (mapping.isEmpty())
+        return offset;
+
+    for (unsigned i = offset; i < mapping.size(); ++i) {
+        if (mapping[i] == offset)
+            return i;
+    }
+    return mapping.size();
+}
+
+String AccessibilityObjectAtspi::text() const
+{
+    AXCoreObject* axObject = isMainThread() ? m_coreObject : m_axObject;
+    if (!axObject)
+        return { };
+
+#if ENABLE(INPUT_TYPE_COLOR)
+    if (axObject->roleValue() == AccessibilityRole::ColorWell) {
+        auto color = convertColor<SRGBA<float>>(axObject->colorValue());
+        GUniquePtr<char> colorString(g_strdup_printf("rgb %7.5f %7.5f %7.5f 1", color.red, color.green, color.blue));
+        return String::fromUTF8(colorString.get());
+    }
+#endif
+
+    if (axObject->isTextControl())
+        return axObject->doAXStringForRange({ 0, String::MaxLength });
+
+    auto value = axObject->stringValue();
+    if (!value.isNull())
+        return value;
+
+    if (axObject == m_coreObject)
+        return m_coreObject->textUnderElement(AccessibilityTextUnderElementMode(AccessibilityTextUnderElementMode::TextUnderElementModeIncludeAllChildren));
+
+    return Accessibility::retrieveValueFromMainThread<String>([this]() -> String {
+        if (m_coreObject)
+            m_coreObject->updateBackingStore();
+
+        if (!m_coreObject)
+            return { };
+        return m_coreObject->textUnderElement(AccessibilityTextUnderElementMode(AccessibilityTextUnderElementMode::TextUnderElementModeIncludeAllChildren)).isolatedCopy();
+    });
+}
+
+unsigned AccessibilityObject::getLengthForTextRange() const
+{
+    RELEASE_ASSERT(isMainThread());
+    // FIXME: this should probably be in sync with AccessibilityObjectAtspi::text().
+    unsigned textLength = text().length();
+    if (textLength)
+        return textLength;
+
+    Node* node = this->node();
+    RenderObject* renderer = node ? node->renderer() : nullptr;
+    if (is<RenderText>(renderer))
+        textLength = downcast<RenderText>(*renderer).text().length();
+
+    if (!textLength && allowsTextRanges())
+        textLength = textUnderElement(AccessibilityTextUnderElementMode(AccessibilityTextUnderElementMode::TextUnderElementModeIncludeAllChildren)).length();
+
+    return textLength;
+}
+
+bool AccessibilityObject::allowsTextRanges() const
+{
+    return true;
+}
+
+CString AccessibilityObjectAtspi::text(int startOffset, int endOffset) const
+{
+    RELEASE_ASSERT(!isMainThread());
+    auto utf16Text = text();
+    auto utf8Text = utf16Text.utf8();
+    if (utf8Text.isNull())
+        return { };
+
+    auto length = static_cast<int>(g_utf8_strlen(utf8Text.data(), -1));
+    if (endOffset == -1)
+        endOffset = length;
+
+    if (startOffset < 0 || endOffset < 0)
+        return { };
+
+    if (endOffset <= startOffset)
+        return { };
+
+    if (!startOffset && endOffset == length)
+        return utf8Text;
+
+    GUniquePtr<char> substring(g_utf8_substring(utf8Text.data(), startOffset, endOffset));
+    return substring.get();
+}
+
+void AccessibilityObjectAtspi::textInserted(const String& insertedText, const VisiblePosition& position)
+{
+    RELEASE_ASSERT(isMainThread());
+    if (!m_isRegistered.load())
+        return;
+
+    if (!m_interfaces.contains(Interface::Text))
+        return;
+
+    auto utf16Text = text();
+    auto utf8Text = utf16Text.utf8();
+    auto utf16Offset = m_coreObject->indexForVisiblePosition(position);
+    auto mapping = offsetMapping(utf16Text);
+    auto offset = UTF16OffsetToUTF8(mapping, utf16Offset);
+    auto utf8InsertedText = insertedText.utf8();
+    auto insertedTextLength = g_utf8_strlen(utf8InsertedText.data(), -1);
+    root()->atspi().textChanged(*this, "insert", WTFMove(utf8InsertedText), offset - insertedTextLength, insertedTextLength);
+}
+
+void AccessibilityObjectAtspi::textDeleted(const String& deletedText, const VisiblePosition& position)
+{
+    RELEASE_ASSERT(isMainThread());
+    if (!m_isRegistered.load())
+        return;
+
+    if (!m_interfaces.contains(Interface::Text))
+        return;
+
+    auto utf16Text = text();
+    auto utf8Text = utf16Text.utf8();
+    auto utf16Offset = m_coreObject->indexForVisiblePosition(position);
+    auto mapping = offsetMapping(utf16Text);
+    auto offset = UTF16OffsetToUTF8(mapping, utf16Offset);
+    auto utf8DeletedText = deletedText.utf8();
+    auto deletedTextLength = g_utf8_strlen(utf8DeletedText.data(), -1);
+    root()->atspi().textChanged(*this, "delete", WTFMove(utf8DeletedText), offset, deletedTextLength);
+}
+
+IntPoint AccessibilityObjectAtspi::boundaryOffset(unsigned utf16Offset, TextGranularity granularity) const
+{
+    return Accessibility::retrieveValueFromMainThread<IntPoint>([this, utf16Offset, granularity]() -> IntPoint {
+        if (m_coreObject)
+            m_coreObject->updateBackingStore();
+
+        if (!m_coreObject)
+            return { };
+
+        VisiblePosition offsetPosition = m_coreObject->visiblePositionForIndex(utf16Offset);
+        VisiblePosition startPosition, endPostion;
+        switch (granularity) {
+        case TextGranularity::Character:
+            RELEASE_ASSERT_NOT_REACHED();
+        case TextGranularity::WordStart: {
+            startPosition = isStartOfWord(offsetPosition) && deprecatedIsEditingWhitespace(offsetPosition.characterBefore()) ? offsetPosition : startOfWord(offsetPosition, LeftWordIfOnBoundary);
+            endPostion = nextWordPosition(startPosition);
+            auto positionAfterSpacingAndFollowingWord = nextWordPosition(endPostion);
+            if (positionAfterSpacingAndFollowingWord != endPostion) {
+                auto previousPosition = previousWordPosition(positionAfterSpacingAndFollowingWord);
+                if (previousPosition == startPosition)
+                    endPostion = positionAfterSpacingAndFollowingWord;
+                else
+                    endPostion = previousPosition;
+            }
+            break;
+        }
+        case TextGranularity::WordEnd: {
+            startPosition = previousWordPosition(offsetPosition);
+            auto positionBeforeSpacingAndPreviousWord = previousWordPosition(startPosition);
+            if (positionBeforeSpacingAndPreviousWord != startPosition)
+                startPosition = nextWordPosition(positionBeforeSpacingAndPreviousWord);
+            endPostion = endOfWord(offsetPosition);
+            break;
+        }
+        case TextGranularity::SentenceStart: {
+            startPosition = startOfSentence(offsetPosition);
+            endPostion = endOfSentence(startPosition);
+            if (offsetPosition == endPostion) {
+                startPosition = nextSentencePosition(startPosition);
+                endPostion = endOfSentence(startPosition);
+            }
+            break;
+        }
+        case TextGranularity::SentenceEnd:
+            startPosition = previousSentencePosition(offsetPosition);
+            endPostion = endOfSentence(offsetPosition);
+            break;
+        case TextGranularity::LineStart:
+            startPosition = logicalStartOfLine(offsetPosition);
+            endPostion = nextLinePosition(offsetPosition, 0);
+            break;
+        case TextGranularity::LineEnd:
+            startPosition = logicalStartOfLine(offsetPosition);
+            endPostion = logicalEndOfLine(offsetPosition);
+            break;
+        case TextGranularity::Paragraph:
+            startPosition = startOfParagraph(offsetPosition);
+            endPostion = endOfParagraph(offsetPosition);
+            break;
+        }
+        return { m_coreObject->indexForVisiblePosition(startPosition), m_coreObject->indexForVisiblePosition(endPostion) };
+    });
+}
+
+CString AccessibilityObjectAtspi::textAtOffset(int offset, TextGranularity granularity, int& startOffset, int& endOffset) const
+{
+    RELEASE_ASSERT(!isMainThread());
+    auto utf16Text = text();
+    auto utf8Text = utf16Text.utf8();
+    if (utf8Text.isNull())
+        return { };
+
+    auto length = static_cast<int>(g_utf8_strlen(utf8Text.data(), -1));
+    if (offset < 0 || offset > length)
+        return { };
+
+    if (granularity == TextGranularity::Character) {
+        startOffset = offset;
+        endOffset = std::min(offset + 1, length);
+    } else {
+        auto mapping = offsetMapping(utf16Text);
+        auto utf16Offset = UTF8OffsetToUTF16(mapping, offset);
+        auto boundaryOffset = this->boundaryOffset(utf16Offset, granularity);
+        startOffset = UTF16OffsetToUTF8(mapping, std::max<int>(boundaryOffset.x(), 0));
+        endOffset = UTF16OffsetToUTF8(mapping, std::min<int>(boundaryOffset.y(), utf16Text.length()));
+    }
+
+    GUniquePtr<char> substring(g_utf8_substring(utf8Text.data(), startOffset, endOffset));
+    return substring.get();
+}
+
+int AccessibilityObjectAtspi::characterAtOffset(int offset) const
+{
+    RELEASE_ASSERT(!isMainThread());
+    auto utf8Text = text().utf8();
+    if (utf8Text.isNull())
+        return 0;
+
+    auto length = static_cast<int>(g_utf8_strlen(utf8Text.data(), -1));
+    if (offset < 0 || offset >= length)
+        return 0;
+
+    return g_utf8_get_char(g_utf8_offset_to_pointer(utf8Text.data(), offset));
+}
+
+IntRect AccessibilityObjectAtspi::boundsForRange(unsigned utf16Offset, unsigned length, uint32_t coordinateType) const
+{
+    return Accessibility::retrieveValueFromMainThread<IntRect>([this, utf16Offset, length, coordinateType]() -> IntRect {
+        if (m_coreObject)
+            m_coreObject->updateBackingStore();
+
+        if (!m_coreObject)
+            return { };
+
+        auto extents = m_coreObject->doAXBoundsForRange(PlainTextRange(utf16Offset, length));
+
+        auto* frameView = m_coreObject->documentFrameView();
+        if (!frameView)
+            return extents;
+
+        switch (coordinateType) {
+        case Atspi::CoordinateType::ScreenCoordinates:
+            return frameView->contentsToScreen(extents);
+        case Atspi::CoordinateType::WindowCoordinates:
+            return frameView->contentsToWindow(extents);
+        case Atspi::CoordinateType::ParentCoordinates:
+            return extents;
+        }
+
+        RELEASE_ASSERT_NOT_REACHED();
+    });
+}
+
+IntRect AccessibilityObjectAtspi::textExtents(int startOffset, int endOffset, uint32_t coordinateType) const
+{
+    RELEASE_ASSERT(!isMainThread());
+    auto utf16Text = text();
+    auto utf8Text = utf16Text.utf8();
+    if (utf8Text.isNull())
+        return { };
+
+    auto length = static_cast<int>(g_utf8_strlen(utf8Text.data(), -1));
+    startOffset = std::clamp(startOffset, 0, length);
+    if (endOffset == -1)
+        endOffset = length;
+    else
+        endOffset = std::clamp(endOffset, 0, length);
+    if (endOffset <= startOffset)
+        return { };
+
+    auto mapping = offsetMapping(utf16Text);
+    auto utf16StartOffset = UTF8OffsetToUTF16(mapping, startOffset);
+    auto utf16EndOffset = UTF8OffsetToUTF16(mapping, endOffset);
+    return boundsForRange(utf16StartOffset, utf16EndOffset - utf16StartOffset, coordinateType);
+}
+
+int AccessibilityObjectAtspi::offsetAtPoint(const IntPoint& point, uint32_t coordinateType) const
+{
+    RELEASE_ASSERT(!isMainThread());
+    auto utf16Text = text();
+    auto utf8Text = utf16Text.utf8();
+    if (utf8Text.isNull())
+        return -1;
+
+    auto utf16Offset = Accessibility::retrieveValueFromMainThread<int>([this, &point, coordinateType]() -> int {
+        if (m_coreObject)
+            m_coreObject->updateBackingStore();
+
+        if (!m_coreObject)
+            return -1;
+
+        auto convertedPoint = point;
+        if (auto* frameView = m_coreObject->documentFrameView()) {
+            switch (coordinateType) {
+            case Atspi::CoordinateType::ScreenCoordinates:
+                convertedPoint = frameView->screenToContents(point);
+                break;
+            case Atspi::CoordinateType::WindowCoordinates:
+                convertedPoint = frameView->windowToContents(point);
+                break;
+            case Atspi::CoordinateType::ParentCoordinates:
+                break;
+            }
+        }
+
+        auto position = m_coreObject->visiblePositionForPoint(convertedPoint);
+        if (position.isNull())
+            return -1;
+
+        return m_coreObject->indexForVisiblePosition(position);
+    });
+
+    if (utf16Offset == -1)
+        return -1;
+
+    return UTF16OffsetToUTF8(offsetMapping(utf16Text), utf16Offset);
+}
+
+IntPoint AccessibilityObjectAtspi::boundsForSelection(const VisibleSelection& selection) const
+{
+    RELEASE_ASSERT(isMainThread());
+    if (selection.isNone())
+        return { -1, -1 };
+
+    Node* node = nullptr;
+    if (!m_coreObject->isNativeTextControl())
+        node = m_coreObject->node();
+    else {
+        auto positionInTextControlInnerElement = m_coreObject->visiblePositionForIndex(0);
+        if (auto* innerMostNode = positionInTextControlInnerElement.deepEquivalent().anchorNode())
+            node = innerMostNode->parentNode();
+    }
+    if (!node)
+        return { -1, -1 };
+
+    // We need to limit our search to positions that fall inside the domain of the current object.
+    auto firstValidPosition = firstPositionInOrBeforeNode(node->firstDescendant());
+    auto lastValidPosition = lastPositionInOrAfterNode(node->lastDescendant());
+
+    if (!intersects(makeVisiblePositionRange(makeSimpleRange(firstValidPosition, lastValidPosition)), VisiblePositionRange(selection)))
+        return { -1, -1 };
+
+    // Find the proper range for the selection that falls inside the object.
+    auto nodeRangeStart = std::max(selection.start(), firstValidPosition);
+    auto nodeRangeEnd = std::min(selection.end(), lastValidPosition);
+
+    // Calculate position of the selected range inside the object.
+    auto parentFirstPosition = firstPositionInOrBeforeNode(node);
+    auto rangeInParent = *makeSimpleRange(parentFirstPosition, nodeRangeStart);
+
+    // Set values for start offsets and calculate initial range length.
+    int startOffset = characterCount(rangeInParent, TextIteratorBehavior::EmitsCharactersBetweenAllVisiblePositions);
+    auto nodeRange = *makeSimpleRange(nodeRangeStart, nodeRangeEnd);
+    int rangeLength = characterCount(nodeRange, TextIteratorBehavior::EmitsCharactersBetweenAllVisiblePositions);
+    return { startOffset, startOffset + rangeLength };
+}
+
+IntPoint AccessibilityObjectAtspi::selectedRange() const
+{
+    return Accessibility::retrieveValueFromMainThread<IntPoint>([this]() -> IntPoint {
+        if (m_coreObject)
+            m_coreObject->updateBackingStore();
+
+        if (!m_coreObject)
+            return { -1, -1 };
+
+        return boundsForSelection(m_coreObject->selection());
+    });
+}
+
+bool AccessibilityObjectAtspi::selectionBounds(int& startOffset, int& endOffset) const
+{
+    RELEASE_ASSERT(!isMainThread());
+    auto utf16Text = text();
+    auto utf8Text = utf16Text.utf8();
+    if (utf8Text.isNull())
+        return false;
+
+    auto bounds = selectedRange();
+    if (bounds.x() < 0)
+        return false;
+
+    auto mapping = offsetMapping(utf16Text);
+    startOffset = UTF16OffsetToUTF8(mapping, bounds.x());
+    endOffset = UTF16OffsetToUTF8(mapping, bounds.y());
+
+    auto length = static_cast<int>(g_utf8_strlen(utf8Text.data(), -1));
+    endOffset = std::clamp(endOffset, 0, length);
+    if (endOffset < startOffset) {
+        startOffset = endOffset = 0;
+        return false;
+    }
+
+    return true;
+}
+
+void AccessibilityObjectAtspi::setSelectedRange(unsigned utf16Offset, unsigned length)
+{
+    Accessibility::performFunctionOnMainThread([this, utf16Offset, length] {
+        if (m_coreObject)
+            m_coreObject->updateBackingStore();
+
+        if (!m_coreObject)
+            return;
+
+        auto range = m_coreObject->visiblePositionRangeForRange(PlainTextRange(utf16Offset, length));
+        m_coreObject->setSelectedVisiblePositionRange(range);
+    });
+}
+
+bool AccessibilityObjectAtspi::selectRange(int startOffset, int endOffset)
+{
+    RELEASE_ASSERT(!isMainThread());
+    auto utf16Text = text();
+    auto utf8Text = utf16Text.utf8();
+    if (utf8Text.isNull())
+        return false;
+
+    auto length = static_cast<int>(g_utf8_strlen(utf8Text.data(), -1));
+    startOffset = std::clamp(startOffset, 0, length);
+    if (endOffset == -1)
+        endOffset = length;
+    else
+        endOffset = std::clamp(endOffset, 0, length);
+
+    auto mapping = offsetMapping(utf16Text);
+    auto utf16StartOffset = UTF8OffsetToUTF16(mapping, startOffset);
+    auto utf16EndOffset = startOffset == endOffset ? utf16StartOffset : UTF8OffsetToUTF16(mapping, endOffset);
+    setSelectedRange(utf16StartOffset, utf16EndOffset - utf16StartOffset);
+
+    return true;
+}
+
+void AccessibilityObjectAtspi::selectionChanged(const VisibleSelection& selection)
+{
+    RELEASE_ASSERT(isMainThread());
+    if (!m_isRegistered.load())
+        return;
+
+    if (!m_interfaces.contains(Interface::Text))
+        return;
+
+    if (selection.isNone())
+        return;
+
+    auto utf16Text = text();
+    auto utf8Text = utf16Text.utf8();
+    if (utf8Text.isNull())
+        return;
+
+    auto bounds = boundsForSelection(selection);
+    if (bounds.y() < 0)
+        return;
+
+    auto length = g_utf8_strlen(utf8Text.data(), -1);
+    auto mapping = offsetMapping(utf16Text);
+    auto caretOffset = UTF16OffsetToUTF8(mapping, bounds.y());
+    if (caretOffset <= length)
+        root()->atspi().textCaretMoved(*this, caretOffset);
+
+    if (selection.isRange())
+        root()->atspi().textSelectionChanged(*this);
+}
+
+AccessibilityObjectAtspi::TextAttributes AccessibilityObjectAtspi::textAttributes(std::optional<unsigned> utf16Offset, bool includeDefault) const
+{
+    return Accessibility::retrieveValueFromMainThread<TextAttributes>([this, utf16Offset, includeDefault]() -> TextAttributes {
+        if (m_coreObject)
+            m_coreObject->updateBackingStore();
+
+        if (!m_coreObject || !m_coreObject->renderer() || !m_coreObject->node())
+            return { };
+
+        auto accessibilityTextAttributes = [this](AXCoreObject* axObject, const HashMap<String, String>& defaultAttributes) -> HashMap<String, String> {
+            HashMap<String, String> attributes;
+            auto& style = axObject->renderer()->style();
+
+            auto addAttributeIfNeeded = [&](const String& name, const String& value) {
+                if (defaultAttributes.isEmpty() || defaultAttributes.get(name) != value)
+                    attributes.add(name, value);
+            };
+
+            auto bgColor = style.visitedDependentColor(CSSPropertyBackgroundColor);
+            if (bgColor.isValid() && bgColor.isVisible()) {
+                auto [r, g, b, a] = bgColor.toSRGBALossy<uint8_t>();
+                addAttributeIfNeeded("bg-color"_s, makeString(r, ',', g, ',', b));
+            }
+
+            auto fgColor = style.visitedDependentColor(CSSPropertyColor);
+            if (fgColor.isValid() && fgColor.isVisible()) {
+                auto [r, g, b, a] = fgColor.toSRGBALossy<uint8_t>();
+                addAttributeIfNeeded("fg-color"_s, makeString(r, ',', g, ',', b));
+            }
+
+            addAttributeIfNeeded("family-name"_s, style.fontCascade().firstFamily());
+            addAttributeIfNeeded("size"_s, makeString(std::round(style.computedFontPixelSize() * 72 / WebCore::screenDPI()), "pt"));
+            addAttributeIfNeeded("weight"_s, makeString(static_cast<float>(style.fontCascade().weight())));
+            addAttributeIfNeeded("style"_s, style.fontCascade().italic() ? "italic" : "normal");
+            addAttributeIfNeeded("strikethrough"_s, style.textDecoration() & TextDecoration::LineThrough ? "true" : "false");
+            addAttributeIfNeeded("underline"_s, style.textDecoration() & TextDecoration::Underline ? "single" : "none");
+            addAttributeIfNeeded("invisible"_s, style.visibility() == Visibility::Hidden ? "true" : "false");
+            addAttributeIfNeeded("editable"_s, m_coreObject->canSetValueAttribute() ? "true" : "false");
+            addAttributeIfNeeded("direction"_s, style.direction() == TextDirection::LTR ? "ltr" : "rtl");
+
+            if (!style.textIndent().isUndefined())
+                addAttributeIfNeeded("indent"_s, makeString(valueForLength(style.textIndent(), m_coreObject->size().width()).toInt()));
+
+            switch (style.textAlign()) {
+            case TextAlignMode::Start:
+            case TextAlignMode::End:
+                break;
+            case TextAlignMode::Left:
+            case TextAlignMode::WebKitLeft:
+                addAttributeIfNeeded("justification"_s, "left");
+                break;
+            case TextAlignMode::Right:
+            case TextAlignMode::WebKitRight:
+                addAttributeIfNeeded("justification"_s, "right");
+                break;
+            case TextAlignMode::Center:
+            case TextAlignMode::WebKitCenter:
+                addAttributeIfNeeded("justification"_s, "center");
+                break;
+            case TextAlignMode::Justify:
+                addAttributeIfNeeded("justification"_s, "fill");
+                break;
+            }
+
+            String invalidStatus = m_coreObject->invalidStatus();
+            if (invalidStatus != "false")
+                addAttributeIfNeeded("invalid"_s, invalidStatus);
+
+            String language = m_coreObject->language();
+            if (!language.isEmpty())
+                addAttributeIfNeeded("language"_s, language);
+
+            return attributes;
+        };
+
+        auto defaultAttributes = accessibilityTextAttributes(m_coreObject, { });
+        if (!utf16Offset)
+            return { defaultAttributes, -1, -1 };
+
+        VisiblePosition offsetPosition = m_coreObject->visiblePositionForIndex(*utf16Offset);
+        auto* childNode = offsetPosition.deepEquivalent().deprecatedNode();
+        if (!childNode)
+            return { defaultAttributes, -1, -1 };
+
+        auto* childRenderer = childNode->renderer();
+        if (!childRenderer)
+            return { defaultAttributes, -1, -1 };
+
+        auto* childAxObject = childRenderer->document().axObjectCache()->get(childRenderer);
+        if (!childAxObject || childAxObject == m_coreObject)
+            return { defaultAttributes, -1, -1 };
+
+        auto attributes = accessibilityTextAttributes(childAxObject, defaultAttributes);
+        auto firstValidPosition = firstPositionInOrBeforeNode(m_coreObject->node()->firstDescendant());
+        auto lastValidPosition = lastPositionInOrAfterNode(m_coreObject->node()->lastDescendant());
+
+        auto* startRenderer = childRenderer;
+        auto startPosition = firstPositionInOrBeforeNode(startRenderer->node());
+        for (RenderObject* r = childRenderer->previousInPreOrder(); r && startPosition > firstValidPosition; r = r->previousInPreOrder()) {
+            if (r->firstChildSlow())
+                continue;
+
+            auto childAttributes = accessibilityTextAttributes(r->document().axObjectCache()->get(r), defaultAttributes);
+            if (childAttributes != attributes)
+                break;
+
+            startRenderer = r;
+            startPosition = firstPositionInOrBeforeNode(startRenderer->node());
+        }
+
+        auto* endRenderer = childRenderer;
+        auto endPosition = lastPositionInOrAfterNode(endRenderer->node());
+        for (RenderObject* r = childRenderer->nextInPreOrder(); r && endPosition < lastValidPosition; r = r->nextInPreOrder()) {
+            if (r->firstChildSlow())
+                continue;
+
+            auto childAttributes = accessibilityTextAttributes(r->document().axObjectCache()->get(r), defaultAttributes);
+            if (childAttributes != attributes)
+                break;
+
+            endRenderer = r;
+            endPosition = lastPositionInOrAfterNode(endRenderer->node());
+        }
+
+        if (!includeDefault)
+            return { attributes, m_coreObject->indexForVisiblePosition(startPosition), m_coreObject->indexForVisiblePosition(endPosition) };
+
+        for (const auto& it : attributes)
+            defaultAttributes.set(it.key, it.value);
+
+        return { defaultAttributes, m_coreObject->indexForVisiblePosition(startPosition), m_coreObject->indexForVisiblePosition(endPosition) };
+    });
+}
+
+AccessibilityObjectAtspi::TextAttributes AccessibilityObjectAtspi::textAttributesWithUTF8Offset(std::optional<int> offset, bool includeDefault) const
+{
+    RELEASE_ASSERT(!isMainThread());
+    auto utf16Text = text();
+    auto utf8Text = utf16Text.utf8();
+    if (utf8Text.isNull())
+        return { };
+
+    auto mapping = offsetMapping(utf16Text);
+    std::optional<unsigned> utf16Offset;
+    if (offset) {
+        auto length = static_cast<int>(g_utf8_strlen(utf8Text.data(), -1));
+        if (*offset < 0 || *offset >= length)
+            return { };
+
+        utf16Offset = UTF8OffsetToUTF16(mapping, *offset);
+    }
+
+    auto attributes = textAttributes(utf16Offset, includeDefault);
+    if (attributes.startOffset != -1)
+        attributes.startOffset = UTF16OffsetToUTF8(mapping, attributes.startOffset);
+    if (attributes.endOffset != -1)
+        attributes.endOffset = UTF16OffsetToUTF8(mapping, attributes.endOffset);
+
+    return attributes;
+}
+
+void AccessibilityObjectAtspi::textAttributesChanged()
+{
+    RELEASE_ASSERT(isMainThread());
+    if (!m_isRegistered.load())
+        return;
+
+    if (!m_interfaces.contains(Interface::Text))
+        return;
+
+    root()->atspi().textAttributesChanged(*this);
+}
+
+bool AccessibilityObjectAtspi::scrollToMakeVisible(int startOffset, int endOffset, uint32_t scrollType) const
+{
+    RELEASE_ASSERT(!isMainThread());
+    auto utf16Text = text();
+    auto utf8Text = utf16Text.utf8();
+    if (utf8Text.isNull())
+        return false;
+
+    auto length = static_cast<int>(g_utf8_strlen(utf8Text.data(), -1));
+    if (startOffset < 0 || startOffset > length)
+        return false;
+    if (endOffset < 0 || endOffset > length)
+        return false;
+    if (endOffset < startOffset)
+        std::swap(startOffset, endOffset);
+
+    auto mapping = offsetMapping(utf16Text);
+    auto utf16StartOffset = UTF8OffsetToUTF16(mapping, startOffset);
+    auto utf16EndOffset = UTF8OffsetToUTF16(mapping, endOffset);
+    Accessibility::performFunctionOnMainThread([this, utf16StartOffset, utf16EndOffset, scrollType] {
+        if (m_coreObject)
+            m_coreObject->updateBackingStore();
+
+        if (!m_coreObject || !m_coreObject->renderer())
+            return;
+
+        IntRect rect = m_coreObject->doAXBoundsForRange(PlainTextRange(utf16StartOffset, utf16EndOffset - utf16StartOffset));
+
+        if (m_coreObject->isScrollView()) {
+            if (auto* parent = m_coreObject->parentObject())
+                parent->scrollToMakeVisible();
+        }
+
+        ScrollAlignment alignX;
+        ScrollAlignment alignY;
+        switch (scrollType) {
+        case Atspi::ScrollType::TopLeft:
+            alignX = ScrollAlignment::alignLeftAlways;
+            alignY = ScrollAlignment::alignTopAlways;
+            break;
+        case Atspi::ScrollType::BottomRight:
+            alignX = ScrollAlignment::alignRightAlways;
+            alignY = ScrollAlignment::alignBottomAlways;
+            break;
+        case Atspi::ScrollType::TopEdge:
+        case Atspi::ScrollType::BottomEdge:
+            // Align to a particular edge is not supported, it's always the closest edge.
+            alignX = ScrollAlignment::alignCenterIfNeeded;
+            alignY = ScrollAlignment::alignToEdgeIfNeeded;
+            break;
+        case Atspi::ScrollType::LeftEdge:
+        case Atspi::ScrollType::RightEdge:
+            // Align to a particular edge is not supported, it's always the closest edge.
+            alignX = ScrollAlignment::alignToEdgeIfNeeded;
+            alignY = ScrollAlignment::alignCenterIfNeeded;
+            break;
+        case Atspi::ScrollType::Anywhere:
+            alignX = ScrollAlignment::alignCenterIfNeeded;
+            alignY = ScrollAlignment::alignCenterIfNeeded;
+            break;
+        }
+
+        m_coreObject->renderer()->scrollRectToVisible(rect, false, { SelectionRevealMode::Reveal, alignX, alignY, ShouldAllowCrossOriginScrolling::Yes });
+    });
+
+    return true;
+}
+
+bool AccessibilityObjectAtspi::scrollToPoint(int startOffset, int endOffset, uint32_t coordinateType, int x, int y) const
+{
+    RELEASE_ASSERT(!isMainThread());
+    auto utf16Text = text();
+    auto utf8Text = utf16Text.utf8();
+    if (utf8Text.isNull())
+        return false;
+
+    auto length = static_cast<int>(g_utf8_strlen(utf8Text.data(), -1));
+    if (startOffset < 0 || startOffset > length)
+        return false;
+    if (endOffset < 0 || endOffset > length)
+        return false;
+    if (endOffset < startOffset)
+        std::swap(startOffset, endOffset);
+
+    auto mapping = offsetMapping(utf16Text);
+    auto utf16StartOffset = UTF8OffsetToUTF16(mapping, startOffset);
+    auto utf16EndOffset = UTF8OffsetToUTF16(mapping, endOffset);
+    Accessibility::performFunctionOnMainThread([this, utf16StartOffset, utf16EndOffset, coordinateType, x, y] {
+        if (m_coreObject)
+            m_coreObject->updateBackingStore();
+
+        if (!m_coreObject)
+            return;
+
+        IntPoint point(x, y);
+        if (coordinateType == Atspi::CoordinateType::ScreenCoordinates) {
+            if (auto* frameView = m_coreObject->documentFrameView())
+                point = frameView->contentsToWindow(frameView->screenToContents(point));
+        }
+
+        IntRect rect = m_coreObject->doAXBoundsForRange(PlainTextRange(utf16StartOffset, utf16EndOffset - utf16StartOffset));
+        point.move(-rect.x(), -rect.y());
+        m_coreObject->scrollToGlobalPoint(point);
+    });
+
+    return true;
+}
+
+} // namespace WebCore
+
+#endif // ENABLE(ACCESSIBILITY) && USE(ATSPI)
</ins></span></pre></div>
<a id="trunkSourceWebCoreeditingatspiFrameSelectionAtspicpp"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebCore/editing/atspi/FrameSelectionAtspi.cpp (284674 => 284675)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebCore/editing/atspi/FrameSelectionAtspi.cpp       2021-10-22 08:44:57 UTC (rev 284674)
+++ trunk/Source/WebCore/editing/atspi/FrameSelectionAtspi.cpp  2021-10-22 08:51:03 UTC (rev 284675)
</span><span class="lines">@@ -23,13 +23,20 @@
</span><span class="cx"> #if ENABLE(ACCESSIBILITY) && USE(ATSPI)
</span><span class="cx"> 
</span><span class="cx"> #include "AXObjectCache.h"
</span><ins>+#include "DocumentInlines.h"
</ins><span class="cx"> 
</span><span class="cx"> namespace WebCore {
</span><span class="cx"> 
</span><del>-void FrameSelection::notifyAccessibilityForSelectionChange(const AXTextStateChangeIntent&)
</del><ins>+void FrameSelection::notifyAccessibilityForSelectionChange(const AXTextStateChangeIntent& intent)
</ins><span class="cx"> {
</span><span class="cx">     if (!AXObjectCache::accessibilityEnabled())
</span><span class="cx">         return;
</span><ins>+
+    if (!m_selection.start().isNotNull() || !m_selection.end().isNotNull())
+        return;
+
+    if (AXObjectCache* cache = m_document->existingAXObjectCache())
+        cache->postTextStateChangeNotification(m_selection.start(), intent, m_selection);
</ins><span class="cx"> }
</span><span class="cx"> 
</span><span class="cx"> } // namespace WebCore
</span></span></pre></div>
<a id="trunkSourceWebCorehtmlHTMLTextFormControlElementcpp"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebCore/html/HTMLTextFormControlElement.cpp (284674 => 284675)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebCore/html/HTMLTextFormControlElement.cpp 2021-10-22 08:44:57 UTC (rev 284674)
+++ trunk/Source/WebCore/html/HTMLTextFormControlElement.cpp    2021-10-22 08:51:03 UTC (rev 284675)
</span><span class="lines">@@ -603,7 +603,7 @@
</span><span class="cx">                 innerText->appendChild(HTMLBRElement::create(document()));
</span><span class="cx">         }
</span><span class="cx"> 
</span><del>-#if ENABLE(ACCESSIBILITY) && PLATFORM(COCOA)
</del><ins>+#if ENABLE(ACCESSIBILITY) && (PLATFORM(COCOA) || USE(ATSPI))
</ins><span class="cx">         if (textIsChanged && renderer()) {
</span><span class="cx">             if (AXObjectCache* cache = document().existingAXObjectCache())
</span><span class="cx">                 cache->deferTextReplacementNotificationForTextControl(*this, previousValue);
</span></span></pre></div>
<a id="trunkToolsChangeLog"></a>
<div class="modfile"><h4>Modified: trunk/Tools/ChangeLog (284674 => 284675)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/ChangeLog    2021-10-22 08:44:57 UTC (rev 284674)
+++ trunk/Tools/ChangeLog       2021-10-22 08:51:03 UTC (rev 284675)
</span><span class="lines">@@ -1,3 +1,26 @@
</span><ins>+2021-10-22  Carlos Garcia Campos  <cgarcia@igalia.com>
+
+        [GTK][a11y] Add implementation of text interface when building with ATSPI
+        https://bugs.webkit.org/show_bug.cgi?id=230258
+
+        Reviewed by Adrian Perez de Castro.
+
+        Add unit tests for the text interface.
+
+        * TestWebKitAPI/Tests/WebKitGtk/TestWebKitAccessibility.cpp:
+        (AtspiTextRangeDeleter::operator() const):
+        (AccessibilityTest::startEventMonitor):
+        (AccessibilityTest::stopEventMonitor):
+        (AccessibilityTest::findEvent):
+        (testTextBasic):
+        (testTextSurrogatePair):
+        (testTextIterator):
+        (testTextExtents):
+        (testTextSelections):
+        (testTextAttributes):
+        (testTextStateChanged):
+        (beforeAll):
+
</ins><span class="cx"> 2021-10-22  Kimmo Kinnunen  <kkinnunen@apple.com>
</span><span class="cx"> 
</span><span class="cx">         WebGL low-power and high-performance contexts should use different ANGLE Metal EGLDisplays
</span></span></pre></div>
<a id="trunkToolsTestWebKitAPITestsWebKitGtkTestWebKitAccessibilitycpp"></a>
<div class="modfile"><h4>Modified: trunk/Tools/TestWebKitAPI/Tests/WebKitGtk/TestWebKitAccessibility.cpp (284674 => 284675)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/TestWebKitAPI/Tests/WebKitGtk/TestWebKitAccessibility.cpp    2021-10-22 08:44:57 UTC (rev 284674)
+++ trunk/Tools/TestWebKitAPI/Tests/WebKitGtk/TestWebKitAccessibility.cpp       2021-10-22 08:51:03 UTC (rev 284675)
</span><span class="lines">@@ -34,7 +34,15 @@
</span><span class="cx">     }
</span><span class="cx"> };
</span><span class="cx"> 
</span><ins>+struct AtspiTextRangeDeleter {
+    void operator()(AtspiTextRange* range) const
+    {
+        g_boxed_free(ATSPI_TYPE_TEXT_RANGE, range);
+    }
+};
+
</ins><span class="cx"> using UniqueAtspiEvent = std::unique_ptr<AtspiEvent, AtspiEventDeleter>;
</span><ins>+using UniqueAtspiTextRange = std::unique_ptr<AtspiTextRange, AtspiTextRangeDeleter>;
</ins><span class="cx"> 
</span><span class="cx"> class AccessibilityTest : public WebViewTest {
</span><span class="cx"> public:
</span><span class="lines">@@ -98,9 +106,10 @@
</span><span class="cx">         m_eventSource = nullptr;
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    void startEventMonitor(AtspiAccessible* source, const Vector<const char*>& events)
</del><ins>+    void startEventMonitor(AtspiAccessible* source, Vector<CString>&& events)
</ins><span class="cx">     {
</span><span class="cx">         m_eventMonitor.source = source;
</span><ins>+        m_eventMonitor.eventTypes = WTFMove(events);
</ins><span class="cx">         m_eventMonitor.listener = adoptGRef(atspi_event_listener_new([](AtspiEvent* event, gpointer userData) {
</span><span class="cx">             auto* test = static_cast<AccessibilityTest*>(userData);
</span><span class="cx">             if (event->source == test->m_eventMonitor.source)
</span><span class="lines">@@ -107,8 +116,8 @@
</span><span class="cx">                 test->m_eventMonitor.events.append(static_cast<AtspiEvent*>(g_boxed_copy(ATSPI_TYPE_EVENT, event)));
</span><span class="cx">         }, this, nullptr));
</span><span class="cx"> 
</span><del>-        for (const auto* event : events)
-            atspi_event_listener_register(m_eventMonitor.listener.get(), event, nullptr);
</del><ins>+        for (const auto& event : m_eventMonitor.eventTypes)
+            atspi_event_listener_register(m_eventMonitor.listener.get(), event.data(), nullptr);
</ins><span class="cx">     }
</span><span class="cx"> 
</span><span class="cx">     Vector<UniqueAtspiEvent> stopEventMonitor(unsigned expectedEvents, std::optional<Seconds> timeout = std::nullopt)
</span><span class="lines">@@ -119,10 +128,22 @@
</span><span class="cx">             g_main_context_iteration(nullptr, timeout ? FALSE : TRUE);
</span><span class="cx"> 
</span><span class="cx">         auto events = WTFMove(m_eventMonitor.events);
</span><del>-        m_eventMonitor = { nullptr, { }, nullptr };
</del><ins>+        for (const auto& event : m_eventMonitor.eventTypes)
+            atspi_event_listener_deregister(m_eventMonitor.listener.get(), event.data(), nullptr);
+        m_eventMonitor = { nullptr, { }, { }, nullptr };
</ins><span class="cx">         return events;
</span><span class="cx">     }
</span><span class="cx"> 
</span><ins>+    static AtspiEvent* findEvent(const Vector<UniqueAtspiEvent>& events, const char* eventType)
+    {
+        for (const auto& event : events) {
+            if (!g_strcmp0(event->type, eventType))
+                return event.get();
+        }
+
+        return nullptr;
+    }
+
</ins><span class="cx">     static unsigned stateSetSize(AtspiStateSet* stateSet)
</span><span class="cx">     {
</span><span class="cx">         GArray* states = atspi_state_set_get_states(stateSet);
</span><span class="lines">@@ -136,6 +157,7 @@
</span><span class="cx"> 
</span><span class="cx">     struct {
</span><span class="cx">         GRefPtr<AtspiEventListener> listener;
</span><ins>+        Vector<CString> eventTypes;
</ins><span class="cx">         Vector<UniqueAtspiEvent> events;
</span><span class="cx">         AtspiAccessible* source { nullptr };
</span><span class="cx">     } m_eventMonitor;
</span><span class="lines">@@ -805,6 +827,694 @@
</span><span class="cx"> }
</span><span class="cx"> #endif
</span><span class="cx"> 
</span><ins>+static void testTextBasic(AccessibilityTest* test, gconstpointer)
+{
+    test->showInWindow();
+    test->loadHtml(
+        "<html>"
+        "  <body>"
+        "    <p>This is a line of text</p>"
+        "  </body>"
+        "</html>",
+        nullptr);
+    test->waitUntilLoadFinished();
+
+    auto testApp = test->findTestApplication();
+    g_assert_true(ATSPI_IS_ACCESSIBLE(testApp.get()));
+
+    auto documentWeb = test->findDocumentWeb(testApp.get());
+    g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
+    g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 1);
+
+    auto p = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
+    g_assert_true(ATSPI_IS_TEXT(p.get()));
+    auto length = atspi_text_get_character_count(ATSPI_TEXT(p.get()), nullptr);
+    g_assert_cmpint(length, ==, 22);
+    GUniquePtr<char> text(atspi_text_get_text(ATSPI_TEXT(p.get()), 0, length, nullptr));
+    g_assert_cmpstr(text.get(), ==, "This is a line of text");
+    text.reset(atspi_text_get_text(ATSPI_TEXT(p.get()), 0, -1, nullptr));
+    g_assert_cmpstr(text.get(), ==, "This is a line of text");
+    text.reset(atspi_text_get_text(ATSPI_TEXT(p.get()), 5, 7, nullptr));
+    g_assert_cmpstr(text.get(), ==, "is");
+    text.reset(atspi_text_get_text(ATSPI_TEXT(p.get()), 0, -2, nullptr));
+    g_assert_cmpstr(text.get(), ==, "");
+    text.reset(atspi_text_get_text(ATSPI_TEXT(p.get()), -5, 23, nullptr));
+    g_assert_cmpstr(text.get(), ==, "");
+}
+
+static void testTextSurrogatePair(AccessibilityTest* test, gconstpointer)
+{
+#if USE(ATK)
+    g_test_skip("Surrogate pairs are not correctly handled by WebKit when using ATK");
+    return;
+#endif
+
+    test->showInWindow();
+    test->loadHtml(
+        "<html>"
+        "  <body>"
+        "    <p>This contains a &#x1D306; symbol</p>"
+        "    <input value='This contains a &#x1D306; symbol'/>"
+        "  </body>"
+        "</html>",
+        nullptr);
+    test->waitUntilLoadFinished();
+
+    auto testApp = test->findTestApplication();
+    g_assert_true(ATSPI_IS_ACCESSIBLE(testApp.get()));
+
+    auto documentWeb = test->findDocumentWeb(testApp.get());
+    g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
+    g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 2);
+
+    auto p = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
+    g_assert_true(ATSPI_IS_TEXT(p.get()));
+    auto length = atspi_text_get_character_count(ATSPI_TEXT(p.get()), nullptr);
+    g_assert_cmpint(length, ==, 24);
+    GUniquePtr<char> text(atspi_text_get_text(ATSPI_TEXT(p.get()), 0, length, nullptr));
+    g_assert_cmpstr(text.get(), ==, "This contains a 𝌆 symbol");
+    text.reset(atspi_text_get_text(ATSPI_TEXT(p.get()), 16, 17, nullptr));
+    g_assert_cmpstr(text.get(), ==, "𝌆");
+
+    auto section = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 1, nullptr));
+    g_assert_true(ATSPI_IS_ACCESSIBLE(section.get()));
+    g_assert_cmpint(atspi_accessible_get_child_count(section.get(), nullptr), ==, 1);
+    auto input = adoptGRef(atspi_accessible_get_child_at_index(section.get(), 0, nullptr));
+    g_assert_true(ATSPI_IS_TEXT(input.get()));
+    length = atspi_text_get_character_count(ATSPI_TEXT(input.get()), nullptr);
+    g_assert_cmpint(length, ==, 24);
+    text.reset(atspi_text_get_text(ATSPI_TEXT(input.get()), 0, length, nullptr));
+    g_assert_cmpstr(text.get(), ==, "This contains a 𝌆 symbol");
+    text.reset(atspi_text_get_text(ATSPI_TEXT(input.get()), 16, 17, nullptr));
+    g_assert_cmpstr(text.get(), ==, "𝌆");
+}
+
+static void testTextIterator(AccessibilityTest* test, gconstpointer)
+{
+    test->showInWindow(800, 600);
+    test->loadHtml(
+        "<html>"
+        "  <body>"
+        "    <p>Text of first sentence.   This is the second sentence.<br>And this is the next paragraph.</p>"
+        "  </body>"
+        "</html>",
+        nullptr);
+    test->waitUntilLoadFinished();
+
+    auto testApp = test->findTestApplication();
+    g_assert_true(ATSPI_IS_ACCESSIBLE(testApp.get()));
+
+    auto documentWeb = test->findDocumentWeb(testApp.get());
+    g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
+    g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 1);
+
+    auto p = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
+    g_assert_true(ATSPI_IS_TEXT(p.get()));
+    auto length = atspi_text_get_character_count(ATSPI_TEXT(p.get()), nullptr);
+    g_assert_cmpint(length, ==, 84);
+    GUniquePtr<char> text(atspi_text_get_text(ATSPI_TEXT(p.get()), 0, length, nullptr));
+    g_assert_cmpstr(text.get(), ==, "Text of first sentence. This is the second sentence.\nAnd this is the next paragraph.");
+
+    // Character granularity.
+    UniqueAtspiTextRange range(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 0, ATSPI_TEXT_GRANULARITY_CHAR, nullptr));
+    g_assert_cmpstr(range->content, ==, "T");
+    g_assert_cmpint(range->start_offset, ==, 0);
+    g_assert_cmpint(range->end_offset, ==, 1);
+    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 83, ATSPI_TEXT_GRANULARITY_CHAR, nullptr));
+    g_assert_cmpstr(range->content, ==, ".");
+    g_assert_cmpint(range->start_offset, ==, 83);
+    g_assert_cmpint(range->end_offset, ==, 84);
+    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 84, ATSPI_TEXT_GRANULARITY_CHAR, nullptr));
+    g_assert_cmpstr(range->content, ==, "");
+    g_assert_cmpint(range->start_offset, ==, 84);
+    g_assert_cmpint(range->end_offset, ==, 84);
+    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 85, ATSPI_TEXT_GRANULARITY_CHAR, nullptr));
+    g_assert_cmpstr(range->content, ==, "");
+#if USE(ATSPI)
+    g_assert_cmpint(range->start_offset, ==, 0);
+    g_assert_cmpint(range->end_offset, ==, 0);
+#else
+    // FIXME: ATK returns wrong offsets in this case.
+    g_assert_cmpint(range->start_offset, ==, 84);
+    g_assert_cmpint(range->end_offset, ==, 84);
+#endif
+    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), -1, ATSPI_TEXT_GRANULARITY_CHAR, nullptr));
+    g_assert_cmpstr(range->content, ==, "");
+    g_assert_cmpint(range->start_offset, ==, 0);
+    g_assert_cmpint(range->end_offset, ==, 0);
+
+    // Word granularity.
+    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 0, ATSPI_TEXT_GRANULARITY_WORD, nullptr));
+    g_assert_cmpstr(range->content, ==, "Text ");
+    g_assert_cmpint(range->start_offset, ==, 0);
+    g_assert_cmpint(range->end_offset, ==, 5);
+    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 4, ATSPI_TEXT_GRANULARITY_WORD, nullptr));
+    g_assert_cmpstr(range->content, ==, "Text ");
+    g_assert_cmpint(range->start_offset, ==, 0);
+    g_assert_cmpint(range->end_offset, ==, 5);
+    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 5, ATSPI_TEXT_GRANULARITY_WORD, nullptr));
+    g_assert_cmpstr(range->content, ==, "of ");
+    g_assert_cmpint(range->start_offset, ==, 5);
+    g_assert_cmpint(range->end_offset, ==, 8);
+    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 40, ATSPI_TEXT_GRANULARITY_WORD, nullptr));
+    g_assert_cmpstr(range->content, ==, "second ");
+    g_assert_cmpint(range->start_offset, ==, 36);
+    g_assert_cmpint(range->end_offset, ==, 43);
+    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 16, ATSPI_TEXT_GRANULARITY_WORD, nullptr));
+    g_assert_cmpstr(range->content, ==, "sentence. ");
+    g_assert_cmpint(range->start_offset, ==, 14);
+    g_assert_cmpint(range->end_offset, ==, 24);
+    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 74, ATSPI_TEXT_GRANULARITY_WORD, nullptr));
+    g_assert_cmpstr(range->content, ==, "paragraph.");
+    g_assert_cmpint(range->start_offset, ==, 74);
+    g_assert_cmpint(range->end_offset, ==, 84);
+    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 83, ATSPI_TEXT_GRANULARITY_WORD, nullptr));
+    g_assert_cmpstr(range->content, ==, "paragraph.");
+    g_assert_cmpint(range->start_offset, ==, 74);
+    g_assert_cmpint(range->end_offset, ==, 84);
+    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 80, ATSPI_TEXT_GRANULARITY_WORD, nullptr));
+    g_assert_cmpstr(range->content, ==, "paragraph.");
+    g_assert_cmpint(range->start_offset, ==, 74);
+    g_assert_cmpint(range->end_offset, ==, 84);
+
+    // Sentence granularity.
+    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 0, ATSPI_TEXT_GRANULARITY_SENTENCE, nullptr));
+    g_assert_cmpstr(range->content, ==, "Text of first sentence. ");
+    g_assert_cmpint(range->start_offset, ==, 0);
+    g_assert_cmpint(range->end_offset, ==, 24);
+    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 23, ATSPI_TEXT_GRANULARITY_SENTENCE, nullptr));
+    g_assert_cmpstr(range->content, ==, "Text of first sentence. ");
+    g_assert_cmpint(range->start_offset, ==, 0);
+    g_assert_cmpint(range->end_offset, ==, 24);
+    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 24, ATSPI_TEXT_GRANULARITY_SENTENCE, nullptr));
+    g_assert_cmpstr(range->content, ==, "This is the second sentence.\n");
+    g_assert_cmpint(range->start_offset, ==, 24);
+    g_assert_cmpint(range->end_offset, ==, 53);
+    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 40, ATSPI_TEXT_GRANULARITY_SENTENCE, nullptr));
+    g_assert_cmpstr(range->content, ==, "This is the second sentence.\n");
+    g_assert_cmpint(range->start_offset, ==, 24);
+    g_assert_cmpint(range->end_offset, ==, 53);
+    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 53, ATSPI_TEXT_GRANULARITY_SENTENCE, nullptr));
+    g_assert_cmpstr(range->content, ==, "And this is the next paragraph.");
+    g_assert_cmpint(range->start_offset, ==, 53);
+    g_assert_cmpint(range->end_offset, ==, 84);
+    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 83, ATSPI_TEXT_GRANULARITY_SENTENCE, nullptr));
+    g_assert_cmpstr(range->content, ==, "And this is the next paragraph.");
+    g_assert_cmpint(range->start_offset, ==, 53);
+    g_assert_cmpint(range->end_offset, ==, 84);
+
+    // Line granularity.
+    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 0, ATSPI_TEXT_GRANULARITY_LINE, nullptr));
+    g_assert_cmpstr(range->content, ==, "Text of first sentence. This is the second sentence.\n");
+    g_assert_cmpint(range->start_offset, ==, 0);
+    g_assert_cmpint(range->end_offset, ==, 53);
+    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 83, ATSPI_TEXT_GRANULARITY_LINE, nullptr));
+    g_assert_cmpstr(range->content, ==, "And this is the next paragraph.");
+    g_assert_cmpint(range->start_offset, ==, 53);
+    g_assert_cmpint(range->end_offset, ==, 84);
+
+#if USE(ATSPI) // ATK doesn't implement paragraph granularity.
+    // Paragraph granularity.
+    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 0, ATSPI_TEXT_GRANULARITY_PARAGRAPH, nullptr));
+    g_assert_cmpstr(range->content, ==, "Text of first sentence. This is the second sentence.");
+    g_assert_cmpint(range->start_offset, ==, 0);
+    g_assert_cmpint(range->end_offset, ==, 52);
+    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(p.get()), 83, ATSPI_TEXT_GRANULARITY_PARAGRAPH, nullptr));
+    g_assert_cmpstr(range->content, ==, "And this is the next paragraph.");
+    g_assert_cmpint(range->start_offset, ==, 53);
+    g_assert_cmpint(range->end_offset, ==, 84);
+#endif
+
+    // Using a text control now.
+    test->loadHtml(
+        "<html>"
+        "  <body>"
+        "    <input value='Text of input field'/>"
+        "  </body>"
+        "</html>",
+        nullptr);
+    test->waitUntilLoadFinished();
+
+    documentWeb = test->findDocumentWeb(testApp.get());
+    g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
+    g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 1);
+
+    auto section = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
+    g_assert_true(ATSPI_IS_ACCESSIBLE(section.get()));
+    g_assert_cmpint(atspi_accessible_get_child_count(section.get(), nullptr), ==, 1);
+    auto input = adoptGRef(atspi_accessible_get_child_at_index(section.get(), 0, nullptr));
+    g_assert_true(ATSPI_IS_TEXT(input.get()));
+    length = atspi_text_get_character_count(ATSPI_TEXT(input.get()), nullptr);
+    g_assert_cmpint(length, ==, 19);
+    text.reset(atspi_text_get_text(ATSPI_TEXT(input.get()), 0, length, nullptr));
+    g_assert_cmpstr(text.get(), ==, "Text of input field");
+
+    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(input.get()), 0, ATSPI_TEXT_GRANULARITY_CHAR, nullptr));
+    g_assert_cmpstr(range->content, ==, "T");
+    g_assert_cmpint(range->start_offset, ==, 0);
+    g_assert_cmpint(range->end_offset, ==, 1);
+    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(input.get()), 18, ATSPI_TEXT_GRANULARITY_CHAR, nullptr));
+    g_assert_cmpstr(range->content, ==, "d");
+    g_assert_cmpint(range->start_offset, ==, 18);
+    g_assert_cmpint(range->end_offset, ==, 19);
+
+    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(input.get()), 0, ATSPI_TEXT_GRANULARITY_WORD, nullptr));
+    g_assert_cmpstr(range->content, ==, "Text ");
+    g_assert_cmpint(range->start_offset, ==, 0);
+    g_assert_cmpint(range->end_offset, ==, 5);
+    range.reset(atspi_text_get_string_at_offset(ATSPI_TEXT(input.get()), 16, ATSPI_TEXT_GRANULARITY_WORD, nullptr));
+    g_assert_cmpstr(range->content, ==, "field");
+    g_assert_cmpint(range->start_offset, ==, 14);
+    g_assert_cmpint(range->end_offset, ==, 19);
+}
+
+static void testTextExtents(AccessibilityTest* test, gconstpointer)
+{
+    test->showInWindow();
+    test->loadHtml(
+        "<html>"
+        "  <body>"
+        "    <p>Text</p>"
+        "  </body>"
+        "</html>",
+        nullptr);
+    test->waitUntilLoadFinished();
+
+    auto testApp = test->findTestApplication();
+    g_assert_true(ATSPI_IS_ACCESSIBLE(testApp.get()));
+
+    auto documentWeb = test->findDocumentWeb(testApp.get());
+    g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
+    g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 1);
+
+    auto p = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
+    g_assert_true(ATSPI_IS_TEXT(p.get()));
+    GUniquePtr<AtspiRect> firstCharRect(atspi_text_get_character_extents(ATSPI_TEXT(p.get()), 0, ATSPI_COORD_TYPE_WINDOW, nullptr));
+    g_assert_nonnull(firstCharRect.get());
+    g_assert_cmpuint(firstCharRect->x, >, 0);
+    g_assert_cmpuint(firstCharRect->y, >, 0);
+    g_assert_cmpuint(firstCharRect->width, >, 0);
+    g_assert_cmpuint(firstCharRect->height, >, 0);
+    GUniquePtr<AtspiRect> lastCharRect(atspi_text_get_character_extents(ATSPI_TEXT(p.get()), 3, ATSPI_COORD_TYPE_WINDOW, nullptr));
+    g_assert_nonnull(lastCharRect.get());
+    g_assert_cmpuint(lastCharRect->x, >, firstCharRect->x);
+    g_assert_cmpuint(lastCharRect->y, ==, firstCharRect->y);
+    g_assert_cmpuint(lastCharRect->width, >, 0);
+    g_assert_cmpuint(lastCharRect->height, >, 0);
+    GUniquePtr<AtspiRect> rangeRect(atspi_text_get_range_extents(ATSPI_TEXT(p.get()), 0, 4, ATSPI_COORD_TYPE_WINDOW, nullptr));
+    g_assert_nonnull(rangeRect.get());
+    g_assert_cmpuint(rangeRect->x, ==, firstCharRect->x);
+    g_assert_cmpuint(rangeRect->y, ==, firstCharRect->y);
+    g_assert_cmpuint(rangeRect->width, >, 0);
+    g_assert_cmpuint(rangeRect->height, >, 0);
+#if USE(ATSPI) // Offset at point is broken with ATK.
+    auto offset = atspi_text_get_offset_at_point(ATSPI_TEXT(p.get()), firstCharRect->x, firstCharRect->y, ATSPI_COORD_TYPE_WINDOW, nullptr);
+    g_assert_cmpint(offset, ==, 0);
+    offset = atspi_text_get_offset_at_point(ATSPI_TEXT(p.get()), firstCharRect->x + firstCharRect->width, firstCharRect->y, ATSPI_COORD_TYPE_WINDOW, nullptr);
+    g_assert_cmpint(offset, ==, 1);
+    offset = atspi_text_get_offset_at_point(ATSPI_TEXT(p.get()), lastCharRect->x, lastCharRect->y, ATSPI_COORD_TYPE_WINDOW, nullptr);
+    g_assert_cmpint(offset, ==, 3);
+    offset = atspi_text_get_offset_at_point(ATSPI_TEXT(p.get()), lastCharRect->x + lastCharRect->width, lastCharRect->y, ATSPI_COORD_TYPE_WINDOW, nullptr);
+    g_assert_cmpint(offset, ==, 4);
+#endif
+}
+
+static void testTextSelections(AccessibilityTest* test, gconstpointer)
+{
+    test->showInWindow(800, 600);
+    test->loadHtml(
+        "<html>"
+        "  <body>"
+        "    <p>This is a line of text</p>"
+        "    <input value='This is text input value'/>"
+        "  </body>"
+        "</html>",
+        nullptr);
+    test->waitUntilLoadFinished();
+
+    auto testApp = test->findTestApplication();
+    g_assert_true(ATSPI_IS_ACCESSIBLE(testApp.get()));
+
+    auto documentWeb = test->findDocumentWeb(testApp.get());
+    g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
+    g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 2);
+
+    auto p = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
+    g_assert_true(ATSPI_IS_TEXT(p.get()));
+
+    auto selectionCount = atspi_text_get_n_selections(ATSPI_TEXT(p.get()), nullptr);
+    g_assert_cmpuint(selectionCount, ==, 0);
+    auto caretOffset = atspi_text_get_caret_offset(ATSPI_TEXT(p.get()), nullptr);
+#if USE(ATSPI)
+    g_assert_cmpint(caretOffset, ==, -1);
+#else
+    g_assert_cmpint(caretOffset, ==, 0);
+#endif
+
+    GUniquePtr<AtspiRange> selection(atspi_text_get_selection(ATSPI_TEXT(p.get()), 0, nullptr));
+    g_assert_nonnull(selection.get());
+    g_assert_cmpint(selection->start_offset, ==, 0);
+    g_assert_cmpint(selection->end_offset, ==, 0);
+
+    g_assert_true(atspi_text_set_selection(ATSPI_TEXT(p.get()), 0, 5, 14, nullptr));
+    selectionCount = atspi_text_get_n_selections(ATSPI_TEXT(p.get()), nullptr);
+    g_assert_cmpuint(selectionCount, ==, 1);
+    caretOffset = atspi_text_get_caret_offset(ATSPI_TEXT(p.get()), nullptr);
+    g_assert_cmpint(caretOffset, ==, 14);
+    selection.reset(atspi_text_get_selection(ATSPI_TEXT(p.get()), 0, nullptr));
+    g_assert_nonnull(selection.get());
+    g_assert_cmpint(selection->start_offset, ==, 5);
+    g_assert_cmpint(selection->end_offset, ==, 14);
+    GUniquePtr<char> text(atspi_text_get_text(ATSPI_TEXT(p.get()), selection->start_offset, selection->end_offset, nullptr));
+    g_assert_cmpstr(text.get(), ==, "is a line");
+
+    g_assert_true(atspi_text_remove_selection(ATSPI_TEXT(p.get()), 0, nullptr));
+    selectionCount = atspi_text_get_n_selections(ATSPI_TEXT(p.get()), nullptr);
+    g_assert_cmpuint(selectionCount, ==, 0);
+    caretOffset = atspi_text_get_caret_offset(ATSPI_TEXT(p.get()), nullptr);
+    g_assert_cmpint(caretOffset, ==, 14);
+    g_assert_true(atspi_text_set_caret_offset(ATSPI_TEXT(p.get()), 0, nullptr));
+    caretOffset = atspi_text_get_caret_offset(ATSPI_TEXT(p.get()), nullptr);
+    g_assert_cmpint(caretOffset, ==, 0);
+
+    auto section = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 1, nullptr));
+    g_assert_true(ATSPI_IS_ACCESSIBLE(section.get()));
+    g_assert_cmpint(atspi_accessible_get_child_count(section.get(), nullptr), ==, 1);
+    auto input = adoptGRef(atspi_accessible_get_child_at_index(section.get(), 0, nullptr));
+    g_assert_true(ATSPI_IS_TEXT(input.get()));
+    g_assert_true(atspi_text_set_caret_offset(ATSPI_TEXT(input.get()), 5, nullptr));
+    caretOffset = atspi_text_get_caret_offset(ATSPI_TEXT(input.get()), nullptr);
+    g_assert_cmpint(caretOffset, ==, 5);
+    g_assert_true(atspi_text_set_selection(ATSPI_TEXT(input.get()), 0, 5, 12, nullptr));
+    selectionCount = atspi_text_get_n_selections(ATSPI_TEXT(input.get()), nullptr);
+    g_assert_cmpuint(selectionCount, ==, 1);
+    caretOffset = atspi_text_get_caret_offset(ATSPI_TEXT(input.get()), nullptr);
+    g_assert_cmpint(caretOffset, ==, 12);
+    selection.reset(atspi_text_get_selection(ATSPI_TEXT(input.get()), 0, nullptr));
+    g_assert_nonnull(selection.get());
+    g_assert_cmpint(selection->start_offset, ==, 5);
+    g_assert_cmpint(selection->end_offset, ==, 12);
+    text.reset(atspi_text_get_text(ATSPI_TEXT(input.get()), selection->start_offset, selection->end_offset, nullptr));
+    g_assert_cmpstr(text.get(), ==, "is text");
+}
+
+static void testTextAttributes(AccessibilityTest* test, gconstpointer)
+{
+    test->showInWindow(800, 600);
+    test->loadHtml(
+        "<html>"
+        "  <body>"
+        "    <p>This is a <b>line</b> of text</p>"
+        "  </body>"
+        "</html>",
+        nullptr);
+    test->waitUntilLoadFinished();
+
+    auto testApp = test->findTestApplication();
+    g_assert_true(ATSPI_IS_ACCESSIBLE(testApp.get()));
+
+    auto documentWeb = test->findDocumentWeb(testApp.get());
+    g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
+    g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 1);
+
+    auto p = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
+    g_assert_true(ATSPI_IS_TEXT(p.get()));
+
+    // Including default attributes.
+    int startOffset, endOffset;
+    GRefPtr<GHashTable> attributes = adoptGRef(atspi_text_get_attribute_run(ATSPI_TEXT(p.get()), 0, TRUE, &startOffset, &endOffset, nullptr));
+    g_assert_nonnull(attributes.get());
+    g_assert_cmpuint(g_hash_table_size(attributes.get()), >, 1);
+    g_assert_cmpstr(static_cast<const char*>(g_hash_table_lookup(attributes.get(), "weight")), ==, "400");
+    g_assert_cmpint(startOffset, ==, 0);
+    g_assert_cmpint(endOffset, ==, 10);
+
+    attributes = adoptGRef(atspi_text_get_attribute_run(ATSPI_TEXT(p.get()), 12, TRUE, &startOffset, &endOffset, nullptr));
+    g_assert_nonnull(attributes.get());
+    g_assert_cmpuint(g_hash_table_size(attributes.get()), >, 1);
+    g_assert_cmpstr(static_cast<const char*>(g_hash_table_lookup(attributes.get(), "weight")), ==, "700");
+    GUniquePtr<char> value(atspi_text_get_attribute_value(ATSPI_TEXT(p.get()), 11, const_cast<char*>("weight"), nullptr));
+    g_assert_cmpstr(value.get(), ==, "700");
+    g_assert_cmpint(startOffset, ==, 10);
+    g_assert_cmpint(endOffset, ==, 14);
+
+    attributes = adoptGRef(atspi_text_get_attribute_run(ATSPI_TEXT(p.get()), 16, TRUE, &startOffset, &endOffset, nullptr));
+    g_assert_nonnull(attributes.get());
+    g_assert_cmpuint(g_hash_table_size(attributes.get()), >, 1);
+    g_assert_cmpstr(static_cast<const char*>(g_hash_table_lookup(attributes.get(), "weight")), ==, "400");
+    g_assert_cmpint(startOffset, ==, 14);
+    g_assert_cmpint(endOffset, ==, 22);
+
+    // Without default attributes.
+    attributes = adoptGRef(atspi_text_get_attribute_run(ATSPI_TEXT(p.get()), 0, FALSE, &startOffset, &endOffset, nullptr));
+    g_assert_nonnull(attributes.get());
+    g_assert_cmpuint(g_hash_table_size(attributes.get()), ==, 0);
+    g_assert_cmpint(startOffset, ==, 0);
+    g_assert_cmpint(endOffset, ==, 10);
+
+    attributes = adoptGRef(atspi_text_get_attribute_run(ATSPI_TEXT(p.get()), 12, FALSE, &startOffset, &endOffset, nullptr));
+    g_assert_nonnull(attributes.get());
+    g_assert_cmpuint(g_hash_table_size(attributes.get()), ==, 1);
+    g_assert_cmpstr(static_cast<const char*>(g_hash_table_lookup(attributes.get(), "weight")), ==, "700");
+    g_assert_cmpint(startOffset, ==, 10);
+    g_assert_cmpint(endOffset, ==, 14);
+
+    attributes = adoptGRef(atspi_text_get_attribute_run(ATSPI_TEXT(p.get()), 16, FALSE, &startOffset, &endOffset, nullptr));
+    g_assert_nonnull(attributes.get());
+    g_assert_cmpuint(g_hash_table_size(attributes.get()), ==, 0);
+    g_assert_cmpint(startOffset, ==, 14);
+    g_assert_cmpint(endOffset, ==, 22);
+
+    // Only default attributes.
+    attributes = adoptGRef(atspi_text_get_default_attributes(ATSPI_TEXT(p.get()), nullptr));
+    g_assert_nonnull(attributes.get());
+    g_assert_cmpuint(g_hash_table_size(attributes.get()), >, 1);
+    g_assert_cmpstr(static_cast<const char*>(g_hash_table_lookup(attributes.get(), "weight")), ==, "400");
+
+#if USE(ATSPI)
+    // Atspi implementation handles ranges with the same attributes as one run.
+    test->loadHtml(
+        "<html>"
+        "  <body>"
+        "    <p>This is a <b>li</b><b>ne</b> of text</p>"
+        "  </body>"
+        "</html>",
+        nullptr);
+    test->waitUntilLoadFinished();
+
+    documentWeb = test->findDocumentWeb(testApp.get());
+    g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
+    g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 1);
+
+    p = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
+    g_assert_true(ATSPI_IS_TEXT(p.get()));
+
+    attributes = adoptGRef(atspi_text_get_attribute_run(ATSPI_TEXT(p.get()), 0, TRUE, &startOffset, &endOffset, nullptr));
+    g_assert_nonnull(attributes.get());
+    g_assert_cmpuint(g_hash_table_size(attributes.get()), >, 1);
+    g_assert_cmpstr(static_cast<const char*>(g_hash_table_lookup(attributes.get(), "weight")), ==, "400");
+    g_assert_cmpint(startOffset, ==, 0);
+    g_assert_cmpint(endOffset, ==, 10);
+
+    attributes = adoptGRef(atspi_text_get_attribute_run(ATSPI_TEXT(p.get()), 12, TRUE, &startOffset, &endOffset, nullptr));
+    g_assert_nonnull(attributes.get());
+    g_assert_cmpuint(g_hash_table_size(attributes.get()), >, 1);
+    g_assert_cmpstr(static_cast<const char*>(g_hash_table_lookup(attributes.get(), "weight")), ==, "700");
+    g_assert_cmpint(startOffset, ==, 10);
+    g_assert_cmpint(endOffset, ==, 14);
+
+    attributes = adoptGRef(atspi_text_get_attribute_run(ATSPI_TEXT(p.get()), 16, TRUE, &startOffset, &endOffset, nullptr));
+    g_assert_nonnull(attributes.get());
+    g_assert_cmpuint(g_hash_table_size(attributes.get()), >, 1);
+    g_assert_cmpstr(static_cast<const char*>(g_hash_table_lookup(attributes.get(), "weight")), ==, "400");
+    g_assert_cmpint(startOffset, ==, 14);
+    g_assert_cmpint(endOffset, ==, 22);
+
+#endif
+}
+
+static void testTextStateChanged(AccessibilityTest* test, gconstpointer)
+{
+    test->showInWindow(800, 600);
+    test->loadHtml(
+        "<html>"
+        "  <body>"
+        "    <p>This is a line of text</p>"
+        "    <input value='This is a text field'/>"
+        "    <div contenteditable=true>This is content editable</div>"
+        "  </body>"
+        "</html>",
+        nullptr);
+    test->waitUntilLoadFinished();
+
+    auto testApp = test->findTestApplication();
+    g_assert_true(ATSPI_IS_ACCESSIBLE(testApp.get()));
+
+    auto documentWeb = test->findDocumentWeb(testApp.get());
+    g_assert_true(ATSPI_IS_ACCESSIBLE(documentWeb.get()));
+    g_assert_cmpint(atspi_accessible_get_child_count(documentWeb.get(), nullptr), ==, 3);
+
+    // Text caret moved.
+    auto p = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 0, nullptr));
+    g_assert_true(ATSPI_IS_TEXT(p.get()));
+    test->startEventMonitor(p.get(), { "object:text-caret-moved" });
+    g_assert_true(atspi_text_set_caret_offset(ATSPI_TEXT(p.get()), 10, nullptr));
+    auto events = test->stopEventMonitor(1);
+    g_assert_cmpuint(events.size(), ==, 1);
+    g_assert_cmpstr(events[0]->type, ==, "object:text-caret-moved");
+    g_assert_cmpuint(events[0]->detail1, ==, 10);
+
+    auto section = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 1, nullptr));
+    g_assert_true(ATSPI_IS_ACCESSIBLE(section.get()));
+    g_assert_cmpint(atspi_accessible_get_child_count(section.get(), nullptr), ==, 1);
+    auto input = adoptGRef(atspi_accessible_get_child_at_index(section.get(), 0, nullptr));
+    g_assert_true(ATSPI_IS_TEXT(input.get()));
+    test->startEventMonitor(input.get(), { "object:text-caret-moved" });
+    g_assert_true(atspi_text_set_caret_offset(ATSPI_TEXT(input.get()), 5, nullptr));
+    events = test->stopEventMonitor(1);
+    g_assert_cmpuint(events.size(), ==, 1);
+    g_assert_cmpstr(events[0]->type, ==, "object:text-caret-moved");
+    g_assert_cmpuint(events[0]->detail1, ==, 5);
+
+    auto div = adoptGRef(atspi_accessible_get_child_at_index(documentWeb.get(), 2, nullptr));
+    g_assert_true(ATSPI_IS_TEXT(div.get()));
+    test->startEventMonitor(div.get(), { "object:text-caret-moved" });
+    g_assert_true(atspi_text_set_caret_offset(ATSPI_TEXT(div.get()), 15, nullptr));
+    events = test->stopEventMonitor(1);
+    g_assert_cmpuint(events.size(), ==, 1);
+    g_assert_cmpstr(events[0]->type, ==, "object:text-caret-moved");
+    g_assert_cmpuint(events[0]->detail1, ==, 15);
+
+#if USE(ATSPI) // Selection changed seems to be broken with ATK.
+    // Selection changed.
+    test->startEventMonitor(p.get(), { "object:text-selection-changed", "object:text-caret-moved" });
+    g_assert_true(atspi_text_set_selection(ATSPI_TEXT(p.get()), 0, 10, 15, nullptr));
+    events = test->stopEventMonitor(2);
+    g_assert_cmpuint(events.size(), ==, 2);
+    auto* event = AccessibilityTest::findEvent(events, "object:text-caret-moved");
+    g_assert_nonnull(event);
+    g_assert_cmpuint(event->detail1, ==, 15);
+    g_assert_nonnull(AccessibilityTest::findEvent(events, "object:text-selection-changed"));
+
+    test->startEventMonitor(input.get(), { "object:text-selection-changed", "object:text-caret-moved" });
+    g_assert_true(atspi_text_set_selection(ATSPI_TEXT(input.get()), 0, 5, 10, nullptr));
+    events = test->stopEventMonitor(2);
+    g_assert_cmpuint(events.size(), ==, 2);
+    event = AccessibilityTest::findEvent(events, "object:text-caret-moved");
+    g_assert_nonnull(event);
+    g_assert_cmpuint(event->detail1, ==, 10);
+    g_assert_nonnull(AccessibilityTest::findEvent(events, "object:text-selection-changed"));
+
+    test->startEventMonitor(div.get(), { "object:text-selection-changed", "object:text-caret-moved" });
+    g_assert_true(atspi_text_set_selection(ATSPI_TEXT(div.get()), 0, 15, 18, nullptr));
+    events = test->stopEventMonitor(2);
+    g_assert_cmpuint(events.size(), ==, 2);
+    event = AccessibilityTest::findEvent(events, "object:text-caret-moved");
+    g_assert_nonnull(event);
+    g_assert_cmpuint(event->detail1, ==, 18);
+    g_assert_nonnull(AccessibilityTest::findEvent(events, "object:text-selection-changed"));
+#endif
+
+    // Text changed.
+    g_assert_true(atspi_text_set_caret_offset(ATSPI_TEXT(input.get()), 5, nullptr));
+    test->startEventMonitor(input.get(), { "object:text-changed:insert", "object:text-changed:delete" });
+    test->keyStroke(GDK_KEY_a);
+    events = test->stopEventMonitor(1);
+    g_assert_cmpuint(events.size(), ==, 1);
+    g_assert_cmpstr(events[0]->type, ==, "object:text-changed:insert");
+#if USE(ATSPI)
+    g_assert_cmpuint(events[0]->detail1, ==, 5);
+#endif
+    g_assert_cmpuint(events[0]->detail2, ==, 1);
+    g_assert_true(G_VALUE_HOLDS_STRING(&events[0]->any_data));
+    g_assert_cmpstr(g_value_get_string(&events[0]->any_data), ==, "a");
+    g_assert_true(atspi_text_set_caret_offset(ATSPI_TEXT(input.get()), 0, nullptr));
+    test->startEventMonitor(input.get(), { "object:text-changed:insert", "object:text-changed:delete" });
+    test->keyStroke(GDK_KEY_Delete);
+    events = test->stopEventMonitor(1);
+    g_assert_cmpuint(events.size(), ==, 1);
+    g_assert_cmpstr(events[0]->type, ==, "object:text-changed:delete");
+    g_assert_cmpuint(events[0]->detail1, ==, 0);
+    g_assert_cmpuint(events[0]->detail2, ==, 1);
+    g_assert_true(G_VALUE_HOLDS_STRING(&events[0]->any_data));
+    g_assert_cmpstr(g_value_get_string(&events[0]->any_data), ==, "T");
+
+    g_assert_true(atspi_text_set_caret_offset(ATSPI_TEXT(div.get()), 10, nullptr));
+    test->startEventMonitor(div.get(), { "object:text-changed:insert", "object:text-changed:delete" });
+    test->keyStroke(GDK_KEY_b);
+    events = test->stopEventMonitor(1);
+    g_assert_cmpuint(events.size(), ==, 1);
+    g_assert_cmpstr(events[0]->type, ==, "object:text-changed:insert");
+#if USE(ATSPI)
+    g_assert_cmpuint(events[0]->detail1, ==, 10);
+#endif
+    g_assert_cmpuint(events[0]->detail2, ==, 1);
+    g_assert_true(G_VALUE_HOLDS_STRING(&events[0]->any_data));
+    g_assert_cmpstr(g_value_get_string(&events[0]->any_data), ==, "b");
+    g_assert_true(atspi_text_set_caret_offset(ATSPI_TEXT(div.get()), 15, nullptr));
+    test->startEventMonitor(div.get(), { "object:text-changed:insert", "object:text-changed:delete" });
+    test->keyStroke(GDK_KEY_BackSpace);
+    events = test->stopEventMonitor(1);
+    g_assert_cmpuint(events.size(), ==, 1);
+    g_assert_cmpstr(events[0]->type, ==, "object:text-changed:delete");
+#if USE(ATSPI)
+    g_assert_cmpuint(events[0]->detail1, ==, 14);
+#endif
+    g_assert_cmpuint(events[0]->detail2, ==, 1);
+    g_assert_true(G_VALUE_HOLDS_STRING(&events[0]->any_data));
+    g_assert_cmpstr(g_value_get_string(&events[0]->any_data), ==, "n");
+
+    // Text replaced.
+    g_assert_true(atspi_text_set_selection(ATSPI_TEXT(input.get()), 0, 5, 10, nullptr));
+    test->startEventMonitor(input.get(), { "object:text-changed:insert", "object:text-changed:delete" });
+    test->keyStroke(GDK_KEY_c);
+    events = test->stopEventMonitor(2);
+    g_assert_cmpuint(events.size(), ==, 2);
+    g_assert_cmpstr(events[0]->type, ==, "object:text-changed:delete");
+    g_assert_cmpuint(events[0]->detail1, ==, 6);
+    g_assert_cmpuint(events[0]->detail2, ==, 5);
+    g_assert_true(G_VALUE_HOLDS_STRING(&events[0]->any_data));
+    g_assert_cmpstr(g_value_get_string(&events[0]->any_data), ==, "is a ");
+    g_assert_cmpstr(events[1]->type, ==, "object:text-changed:insert");
+#if USE(ATSPI)
+    g_assert_cmpuint(events[1]->detail1, ==, 5);
+#endif
+    g_assert_cmpuint(events[1]->detail2, ==, 1);
+    g_assert_true(G_VALUE_HOLDS_STRING(&events[1]->any_data));
+    g_assert_cmpstr(g_value_get_string(&events[1]->any_data), ==, "c");
+
+    g_assert_true(atspi_text_set_selection(ATSPI_TEXT(div.get()), 0, 10, 18, nullptr));
+    test->startEventMonitor(div.get(), { "object:text-changed:insert", "object:text-changed:delete" });
+    test->keyStroke(GDK_KEY_Delete);
+    events = test->stopEventMonitor(1);
+    g_assert_cmpuint(events.size(), ==, 1);
+    g_assert_cmpstr(events[0]->type, ==, "object:text-changed:delete");
+#if USE(ATSPI)
+    g_assert_cmpuint(events[0]->detail1, ==, 10);
+#endif
+    g_assert_cmpuint(events[0]->detail2, ==, 8);
+    g_assert_true(G_VALUE_HOLDS_STRING(&events[0]->any_data));
+    g_assert_cmpstr(g_value_get_string(&events[0]->any_data), ==, "bntet ed");
+
+#if USE(ATSPI)
+    // Text input value changed.
+    test->startEventMonitor(input.get(), { "object:text-changed:insert", "object:text-changed:delete" });
+    test->runJavaScriptAndWaitUntilFinished("document.getElementsByTagName('input')[0].value = 'foo';", nullptr);
+    events = test->stopEventMonitor(2);
+    g_assert_cmpuint(events.size(), ==, 2);
+    g_assert_cmpstr(events[0]->type, ==, "object:text-changed:delete");
+    g_assert_cmpuint(events[0]->detail1, ==, 0);
+    g_assert_cmpuint(events[0]->detail2, ==, 16);
+    g_assert_true(G_VALUE_HOLDS_STRING(&events[0]->any_data));
+    g_assert_cmpstr(g_value_get_string(&events[0]->any_data), ==, "his actext field");
+    g_assert_cmpstr(events[1]->type, ==, "object:text-changed:insert");
+    g_assert_cmpuint(events[1]->detail1, ==, 0);
+    g_assert_cmpuint(events[1]->detail2, ==, 3);
+    g_assert_true(G_VALUE_HOLDS_STRING(&events[1]->any_data));
+    g_assert_cmpstr(g_value_get_string(&events[1]->any_data), ==, "foo");
+#endif
+}
+
</ins><span class="cx"> void beforeAll()
</span><span class="cx"> {
</span><span class="cx">     AccessibilityTest::add("WebKitAccessibility", "accessible/basic-hierarchy", testAccessibleBasicHierarchy);
</span><span class="lines">@@ -817,6 +1527,13 @@
</span><span class="cx"> #ifdef ATSPI_SCROLLTYPE_COUNT
</span><span class="cx">     AccessibilityTest::add("WebKitAccessibility", "component/scroll-to", testComponentScrollTo);
</span><span class="cx"> #endif
</span><ins>+    AccessibilityTest::add("WebKitAccessibility", "text/basic", testTextBasic);
+    AccessibilityTest::add("WebKitAccessibility", "text/surrogate-pair", testTextSurrogatePair);
+    AccessibilityTest::add("WebKitAccessibility", "text/iterator", testTextIterator);
+    AccessibilityTest::add("WebKitAccessibility", "text/extents", testTextExtents);
+    AccessibilityTest::add("WebKitAccessibility", "text/selections", testTextSelections);
+    AccessibilityTest::add("WebKitAccessibility", "text/attributes", testTextAttributes);
+    AccessibilityTest::add("WebKitAccessibility", "text/state-changed", testTextStateChanged);
</ins><span class="cx"> }
</span><span class="cx"> 
</span><span class="cx"> void afterAll()
</span></span></pre>
</div>
</div>

</body>
</html>