0

Introduces AXComputedNodeData for caching a node's inner text and uses it in AXPosition's grapheme iterator

We are slowly switching to computing some AXNode attributes on the browser side, instead of in Blink.
This is in order to improve performance by sending less data over a process boundary.
For example, if a content editable has a lot of text in it, it is unnecessary to send that text in the form of the
value attribute of the content editable itself, every time a single character in the editable changes.

At first, we will create a class for storing the cached inner text of every AXNode.
Later we will add to that class the value attribute, table information, detected language and possibly hypertext.
Thiese values will be computed on demand.

R=dmazzoni@chromium.org, aleventhal@chromium.org

AX-Relnotes: n/a.
Change-Id: I851c083552788ccc32ebbda0e0db59d2c3364b9c
Bug: 1123141
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2735815
Commit-Queue: Nektarios Paisios <nektar@chromium.org>
Auto-Submit: Nektarios Paisios <nektar@chromium.org>
Reviewed-by: Dominic Mazzoni <dmazzoni@chromium.org>
Cr-Commit-Position: refs/heads/master@{#890881}
This commit is contained in:
Nektarios Paisios
2021-06-09 19:29:15 +00:00
committed by Chromium LUCI CQ
parent 467bf73e50
commit 23304bb6eb
9 changed files with 517 additions and 158 deletions

@ -107,6 +107,8 @@ component("accessibility") {
"ax_active_popup.cc",
"ax_active_popup.h",
"ax_clipping_behavior.h",
"ax_computed_node_data.cc",
"ax_computed_node_data.h",
"ax_coordinate_system.h",
"ax_event_generator.cc",
"ax_event_generator.h",

@ -0,0 +1,117 @@
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/accessibility/ax_computed_node_data.h"
#include "base/logging.h"
#include "base/strings/utf_string_conversions.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_node.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/accessibility/ax_tree_manager.h"
#include "ui/accessibility/ax_tree_manager_map.h"
namespace ui {
AXComputedNodeData::AXComputedNodeData(const AXNode& node) : owner_(&node) {}
AXComputedNodeData::~AXComputedNodeData() = default;
const std::string& AXComputedNodeData::GetOrComputeInnerTextUTF8() const {
if (!inner_text_utf8_) {
VLOG_IF(1, inner_text_utf16_)
<< "Only a single encoding of inner text should be cached.";
inner_text_utf8_ = ComputeInnerTextUTF8();
}
return *inner_text_utf8_;
}
const std::u16string& AXComputedNodeData::GetOrComputeInnerTextUTF16() const {
if (!inner_text_utf16_) {
VLOG_IF(1, inner_text_utf8_)
<< "Only a single encoding of inner text should be cached.";
inner_text_utf16_ = ComputeInnerTextUTF16();
}
return *inner_text_utf16_;
}
int AXComputedNodeData::GetOrComputeInnerTextLengthUTF8() const {
return int{GetOrComputeInnerTextUTF8().length()};
}
int AXComputedNodeData::GetOrComputeInnerTextLengthUTF16() const {
return int{GetOrComputeInnerTextUTF16().length()};
}
std::string AXComputedNodeData::ComputeInnerTextUTF8() const {
DCHECK(owner_);
// If a text field has no descendants, then we compute its inner text from its
// value or its placeholder. Otherwise we prefer to look at its descendant
// text nodes because Blink doesn't always add all trailing white space to the
// value attribute.
const bool is_plain_text_field_without_descendants =
(owner_->data().IsTextField() &&
!owner_->GetUnignoredChildCountCrossingTreeBoundary());
if (is_plain_text_field_without_descendants) {
std::string value =
owner_->data().GetStringAttribute(ax::mojom::StringAttribute::kValue);
// If the value is empty, then there might be some placeholder text in the
// text field, or any other name that is derived from visible contents, even
// if the text field has no children.
if (!value.empty())
return value;
}
// Ordinarily, plain text fields are leaves. We need to exclude them from the
// set of leaf nodes when they expose any descendants. This is because we want
// to compute their inner text from their descendant text nodes as we don't
// always trust the "value" attribute provided by Blink.
const bool is_plain_text_field_with_descendants =
(owner_->data().IsTextField() &&
owner_->GetUnignoredChildCountCrossingTreeBoundary());
if (owner_->IsLeaf() && !is_plain_text_field_with_descendants) {
switch (owner_->data().GetNameFrom()) {
case ax::mojom::NameFrom::kNone:
case ax::mojom::NameFrom::kUninitialized:
// The accessible name is not displayed on screen, e.g. aria-label, or is
// not displayed directly inside the node, e.g. an associated label
// element.
case ax::mojom::NameFrom::kAttribute:
// The node's accessible name is explicitly empty.
case ax::mojom::NameFrom::kAttributeExplicitlyEmpty:
// The accessible name does not represent the entirety of the node's inner
// text, e.g. a table's caption or a figure's figcaption.
case ax::mojom::NameFrom::kCaption:
case ax::mojom::NameFrom::kRelatedElement:
// The accessible name is not displayed directly inside the node but is
// visible via e.g. a tooltip.
case ax::mojom::NameFrom::kTitle:
return std::string();
case ax::mojom::NameFrom::kContents:
// The placeholder text is initially displayed inside the text field and
// takes the place of its value.
case ax::mojom::NameFrom::kPlaceholder:
// The value attribute takes the place of the node's inner text, e.g. the
// value of a submit button is displayed inside the button itself.
case ax::mojom::NameFrom::kValue:
return owner_->data().GetStringAttribute(
ax::mojom::StringAttribute::kName);
}
}
std::string inner_text;
for (auto it = owner_->UnignoredChildrenCrossingTreeBoundaryBegin();
it != owner_->UnignoredChildrenCrossingTreeBoundaryEnd(); ++it) {
inner_text += it->GetInnerText();
}
return inner_text;
}
std::u16string AXComputedNodeData::ComputeInnerTextUTF16() const {
return base::UTF8ToUTF16(ComputeInnerTextUTF8());
}
} // namespace ui

@ -0,0 +1,60 @@
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef UI_ACCESSIBILITY_AX_COMPUTED_NODE_DATA_H_
#define UI_ACCESSIBILITY_AX_COMPUTED_NODE_DATA_H_
#include <string>
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "ui/accessibility/ax_export.h"
namespace ui {
class AXNode;
// Computes and stores information about an `AXNode` that is slow or error-prone
// to compute in the tree's source, e.g. in Blink. This class holds cached
// values that should be re-computed when the associated `AXNode` is in any way
// modified.
class AX_EXPORT AXComputedNodeData final {
public:
explicit AXComputedNodeData(const AXNode& node);
virtual ~AXComputedNodeData();
AXComputedNodeData(const AXComputedNodeData& other) = delete;
AXComputedNodeData& operator=(const AXComputedNodeData& other) = delete;
// Retrieves from the cache or computes the on-screen text that is found
// inside the associated node and all its descendants, caches the result, and
// returns a reference to the cached text.
const std::string& GetOrComputeInnerTextUTF8() const;
const std::u16string& GetOrComputeInnerTextUTF16() const;
// Returns the length of the on-screen text that is found inside the
// associated node and all its descendants. The text is either retrieved from
// the cache, or computed and then cached.
int GetOrComputeInnerTextLengthUTF8() const;
int GetOrComputeInnerTextLengthUTF16() const;
private:
// Computes the on-screen text that is found inside the associated node and
// all its descendants.
std::string ComputeInnerTextUTF8() const;
std::u16string ComputeInnerTextUTF16() const;
// The node that is associated with this instance. Weak, owns us.
const AXNode* const owner_;
// Stores the on-screen text that is found inside the associated node and all
// its descendants.
//
// Only one copy (either UTF8 or UTF16) should be cached as each platform
// should only need one of the encodings.
mutable absl::optional<std::string> inner_text_utf8_;
mutable absl::optional<std::u16string> inner_text_utf16_;
};
} // namespace ui
#endif // UI_ACCESSIBILITY_AX_COMPUTED_NODE_DATA_H_

@ -9,11 +9,13 @@
#include <algorithm>
#include <utility>
#include "base/no_destructor.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "ui/accessibility/ax_computed_node_data.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_hypertext.h"
#include "ui/accessibility/ax_language_detection.h"
@ -609,6 +611,7 @@ void AXNode::SetIndexInParent(size_t index_in_parent) {
}
void AXNode::UpdateUnignoredCachedValues() {
computed_node_data_.reset();
if (!IsIgnored())
UpdateUnignoredCachedValuesRecursive(0);
}
@ -726,7 +729,17 @@ void AXNode::ClearLanguageInfo() {
language_info_.reset();
}
std::u16string AXNode::GetHypertext() const {
const AXComputedNodeData& AXNode::GetComputedNodeData() const {
if (!computed_node_data_)
computed_node_data_ = std::make_unique<AXComputedNodeData>(*this);
return *computed_node_data_;
}
void AXNode::ClearComputedNodeData() {
computed_node_data_.reset();
}
const std::u16string& AXNode::GetHypertext() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
// TODO(nektar): Introduce proper caching of hypertext via
// `AXHypertext::needs_update`.
@ -758,9 +771,8 @@ std::u16string AXNode::GetHypertext() const {
//
// Note that the word "hypertext" comes from the IAccessible2 Standard and
// has nothing to do with HTML.
const std::u16string embedded_character_str(kEmbeddedCharacter);
DCHECK_EQ(static_cast<int>(embedded_character_str.length()),
kEmbeddedCharacterLength);
static const base::NoDestructor<std::u16string> embedded_character_str(
AXNode::kEmbeddedCharacter);
for (size_t i = 0; i < GetUnignoredChildCountCrossingTreeBoundary(); ++i) {
const AXNode* child = GetUnignoredChildAtIndexCrossingTreeBoundary(i);
// Similar to Firefox, we don't expose text nodes in IAccessible2 and ATK
@ -775,7 +787,7 @@ std::u16string AXNode::GetHypertext() const {
character_offset, static_cast<int>(i));
DCHECK(inserted.second) << "An embedded object at " << character_offset
<< " has already been encountered.";
hypertext_.hypertext += embedded_character_str;
hypertext_.hypertext += *embedded_character_str;
}
}
}
@ -807,116 +819,24 @@ const AXHypertext& AXNode::GetOldHypertext() const {
return old_hypertext_;
}
std::string AXNode::GetInnerText() const {
const std::string& AXNode::GetInnerText() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
return GetComputedNodeData().GetOrComputeInnerTextUTF8();
}
// Special case, if a node is hosting another accessibility tree, cross the
// tree boundary and return the inner text that is found in that other tree.
// (A node cannot be hosting an accessibility tree as well as having children
// of its own.)
const AXNode* node = this;
const AXTreeManager* child_tree_manager =
AXTreeManagerMap::GetInstance().GetManagerForChildTree(*node);
if (child_tree_manager) {
node = child_tree_manager->GetRootAsAXNode();
DCHECK(node) << "All child trees should have a non-null rootnode.";
}
// If a text field has no descendants, then we compute its inner text from its
// value or its placeholder. Otherwise we prefer to look at its descendant
// text nodes because Blink doesn't always add all trailing white space to the
// value attribute.
const bool is_atomic_text_field_without_descendants =
(node->data().IsTextField() && !node->GetUnignoredChildCount());
if (is_atomic_text_field_without_descendants) {
std::string value =
node->data().GetStringAttribute(ax::mojom::StringAttribute::kValue);
// If the value is empty, then there might be some placeholder text in the
// text field, or any other name that is derived from visible contents, even
// if the text field has no children.
if (!value.empty())
return value;
}
// Ordinarily, atomic text fields are leaves. We need to exclude them from the
// set of leaf nodes when they expose any descendants. This is because we want
// to compute their inner text from their descendant text nodes as we don't
// always trust the "value" attribute provided by Blink.
const bool is_atomic_text_field_with_descendants =
(node->data().IsTextField() && node->GetUnignoredChildCount());
if (node->IsLeaf() && !is_atomic_text_field_with_descendants) {
switch (node->data().GetNameFrom()) {
case ax::mojom::NameFrom::kNone:
case ax::mojom::NameFrom::kUninitialized:
// The accessible name is not displayed on screen, e.g. aria-label, or is
// not displayed directly inside the node, e.g. an associated label
// element.
case ax::mojom::NameFrom::kAttribute:
// The node's accessible name is explicitly empty.
case ax::mojom::NameFrom::kAttributeExplicitlyEmpty:
// The accessible name does not represent the entirety of the node's inner
// text, e.g. a table's caption or a figure's figcaption.
case ax::mojom::NameFrom::kCaption:
case ax::mojom::NameFrom::kRelatedElement:
// The accessible name is not displayed directly inside the node but is
// visible via e.g. a tooltip.
case ax::mojom::NameFrom::kTitle:
return std::string();
case ax::mojom::NameFrom::kContents:
// The placeholder text is initially displayed inside the text field and
// takes the place of its value.
case ax::mojom::NameFrom::kPlaceholder:
// The value attribute takes the place of the node's inner text, e.g. the
// value of a submit button is displayed inside the button itself.
case ax::mojom::NameFrom::kValue:
return node->data().GetStringAttribute(
ax::mojom::StringAttribute::kName);
}
}
std::string inner_text;
for (auto it = node->UnignoredChildrenBegin();
it != node->UnignoredChildrenEnd(); ++it) {
inner_text += it->GetInnerText();
}
return inner_text;
const std::u16string& AXNode::GetInnerTextUTF16() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
return GetComputedNodeData().GetOrComputeInnerTextUTF16();
}
int AXNode::GetInnerTextLength() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
// This is an optimized version of `AXNode::GetInnerText()`.length(). Instead
// of concatenating the strings in GetInnerText() to then get their length, we
// sum the lengths of the individual strings. This is faster than
// concatenating the strings first and then taking their length, especially
// when the process is recursive.
return GetComputedNodeData().GetOrComputeInnerTextLengthUTF8();
}
// Special case, if a node is hosting another accessibility tree, cross the
// tree boundary and return the inner text that is found in that other tree.
// (A node cannot be hosting an accessibility tree as well as having children
// of its own.)
const AXNode* node = this;
const AXTreeManager* child_tree_manager =
AXTreeManagerMap::GetInstance().GetManagerForChildTree(*node);
if (child_tree_manager) {
node = child_tree_manager->GetRootAsAXNode();
DCHECK(node) << "All child trees should have a non-null rootnode.";
}
const bool is_atomic_text_field_with_descendants =
(node->data().IsTextField() && node->GetUnignoredChildCount());
// Atomic text fields are always leaves so we need to exclude them when
// computing the length of their inner text if that text should be derived
// from their descendant nodes.
if (node->IsLeaf() && !is_atomic_text_field_with_descendants)
return static_cast<int>(node->GetInnerText().length());
int inner_text_length = 0;
for (auto it = node->UnignoredChildrenBegin();
it != node->UnignoredChildrenEnd(); ++it) {
inner_text_length += it->GetInnerTextLength();
}
return inner_text_length;
int AXNode::GetInnerTextLengthUTF16() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
return GetComputedNodeData().GetOrComputeInnerTextLengthUTF16();
}
std::string AXNode::GetLanguage() const {
@ -1583,7 +1503,7 @@ bool AXNode::IsChildOfLeaf() const {
bool AXNode::IsEmptyLeaf() const {
if (!IsLeaf())
return false;
if (GetUnignoredChildCount())
if (GetUnignoredChildCountCrossingTreeBoundary())
return !GetInnerTextLength();
// Text exposed by ignored leaf (text) nodes is not exposed to the platforms'
// accessibility layer, hence such leaf nodes are in effect empty.
@ -1593,7 +1513,7 @@ bool AXNode::IsEmptyLeaf() const {
bool AXNode::IsLeaf() const {
// A node is a leaf if it has no descendants, i.e. if it is at the bottom of
// the tree, regardless whether it is ignored or not.
if (children().empty())
if (!GetChildCountCrossingTreeBoundary())
return true;
// Ignored nodes with any kind of descendants, (ignored or unignored), cannot
@ -1604,7 +1524,7 @@ bool AXNode::IsLeaf() const {
return false;
// An unignored node is a leaf if all of its descendants are ignored.
if (!GetUnignoredChildCount())
if (!GetUnignoredChildCountCrossingTreeBoundary())
return true;
#if defined(OS_WIN)

@ -23,6 +23,7 @@
namespace ui {
class AXComputedNodeData;
class AXTableInfo;
struct AXLanguageInfo;
struct AXTreeData;
@ -63,7 +64,7 @@ class AX_EXPORT AXNode final {
// See AXTree::GetAXTreeID.
virtual AXTreeID GetAXTreeID() const = 0;
// See AXTree::GetTableInfo.
// See `AXTree::GetTableInfo`.
virtual AXTableInfo* GetTableInfo(const AXNode* table_node) const = 0;
// See AXTree::GetFromId.
virtual AXNode* GetFromId(AXNodeID id) const = 0;
@ -244,7 +245,8 @@ class AX_EXPORT AXNode final {
// Set the index in parent, for example if siblings were inserted or deleted.
void SetIndexInParent(size_t index_in_parent);
// Update the unignored index in parent for unignored children.
// When the node's `IsIgnored()` value changes, updates the cached values for
// the unignored index in parent and the unignored child count.
void UpdateUnignoredCachedValues();
// Swap the internal children vector with |children|. This instance
@ -391,7 +393,7 @@ class AX_EXPORT AXNode final {
// ATK and IAccessible2 APIs.
//
// TODO(nektar): Consider changing the return value to std::string.
std::u16string GetHypertext() const;
const std::u16string& GetHypertext() const;
// Temporary method that marks `hypertext_` dirty. This will eventually be
// handled by the AX tree in a followup patch.
@ -407,7 +409,8 @@ class AX_EXPORT AXNode final {
// Only text displayed on screen is included. Text from ARIA and HTML
// attributes that is either not displayed on screen, or outside this node, is
// not returned.
std::string GetInnerText() const;
const std::string& GetInnerText() const;
const std::u16string& GetInnerTextUTF16() const;
// Returns the length of the text (in UTF16 code units) that is found inside
// this node and all its descendants; including text found in embedded
@ -419,6 +422,7 @@ class AX_EXPORT AXNode final {
//
// The length of the text is in UTF8 code units, not in grapheme clusters.
int GetInnerTextLength() const;
int GetInnerTextLengthUTF16() const;
// Returns a string representing the language code.
//
@ -526,6 +530,13 @@ class AX_EXPORT AXNode final {
// Destroy the language info for this node.
void ClearLanguageInfo();
// Get a reference to the cached information stored for this node.
const AXComputedNodeData& GetComputedNodeData() const;
// Clear the cached information stored for this node because it is
// out-of-date.
void ClearComputedNodeData();
// Returns true if node is a group and is a direct descendant of a set-like
// element.
bool IsEmbeddedGroup() const;
@ -650,6 +661,9 @@ class AX_EXPORT AXNode final {
size_t unignored_child_count_ = 0;
AXNode* const parent_;
std::vector<AXNode*> children_;
// Stores information about this node that is immutable and which has been
// computed by the tree's source, such as `content::BlinkAXTreeSource`.
AXNodeData data_;
// See the class comment in "ax_hypertext.h" for an explanation of this
@ -657,6 +671,10 @@ class AX_EXPORT AXNode final {
mutable AXHypertext hypertext_;
mutable AXHypertext old_hypertext_;
// Stores information about this node that can be computed on demand and
// cached.
mutable std::unique_ptr<AXComputedNodeData> computed_node_data_;
// Stores the detected language computed from the node's text.
std::unique_ptr<AXLanguageInfo> language_info_;
};

@ -19,6 +19,7 @@
#include "base/containers/contains.h"
#include "base/containers/stack.h"
#include "base/i18n/break_iterator.h"
#include "base/no_destructor.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
@ -345,7 +346,7 @@ class AXPosition {
if (!IsTextPosition() || text_offset_ > MaxTextOffset())
return str;
const std::u16string text = GetText();
const std::u16string& text = GetText();
DCHECK_GE(text_offset_, 0);
const size_t max_text_offset = text.size();
DCHECK_LE(text_offset_, static_cast<int>(max_text_offset)) << text;
@ -2339,15 +2340,13 @@ class AXPosition {
std::unique_ptr<base::i18n::BreakIterator> grapheme_iterator =
text_position->GetGraphemeIterator();
DCHECK_GE(text_position->text_offset_, 0);
DCHECK_LE(text_position->text_offset_,
static_cast<int>(text_position->name_.length()));
while (!text_position->AtStartOfAnchor() &&
(!gfx::IsValidCodePointIndex(
text_position->name_,
static_cast<size_t>(text_position->text_offset_)) ||
(grapheme_iterator &&
!grapheme_iterator->IsGraphemeBoundary(
static_cast<size_t>(text_position->text_offset_))))) {
DCHECK_LE(text_position->text_offset_, text_position->MaxTextOffset());
while (
!text_position->AtStartOfAnchor() &&
(!gfx::IsValidCodePointIndex(text_position->GetText(),
size_t{text_position->text_offset_}) ||
(grapheme_iterator && !grapheme_iterator->IsGraphemeBoundary(
size_t{text_position->text_offset_})))) {
--text_position->text_offset_;
}
return text_position;
@ -2391,20 +2390,17 @@ class AXPosition {
//
// TODO(nektar): Remove this workaround as soon as the source of the bug
// is identified.
if (text_position->text_offset_ >
static_cast<int>(text_position->name_.length()))
if (text_position->text_offset_ > text_position->MaxTextOffset())
return CreateNullPosition();
DCHECK_GE(text_position->text_offset_, 0);
DCHECK_LE(text_position->text_offset_,
static_cast<int>(text_position->name_.length()));
while (!text_position->AtEndOfAnchor() &&
(!gfx::IsValidCodePointIndex(
text_position->name_,
static_cast<size_t>(text_position->text_offset_)) ||
(grapheme_iterator &&
!grapheme_iterator->IsGraphemeBoundary(
static_cast<size_t>(text_position->text_offset_))))) {
DCHECK_LE(text_position->text_offset_, text_position->MaxTextOffset());
while (
!text_position->AtEndOfAnchor() &&
(!gfx::IsValidCodePointIndex(text_position->GetText(),
size_t{text_position->text_offset_}) ||
(grapheme_iterator && !grapheme_iterator->IsGraphemeBoundary(
size_t{text_position->text_offset_})))) {
++text_position->text_offset_;
}
@ -3828,9 +3824,13 @@ class AXPosition {
// including any text found in descendant text nodes, based on the platform's
// text representation. Some platforms use an embedded object replacement
// character that replaces the text coming from most child nodes.
std::u16string GetText() const {
const std::u16string& GetText() const {
// Note that the use of `base::EmptyString16()` is a special case here. For
// performance reasons `base::EmptyString16()` should only be used when
// returning a const reference to a string and there is an error condition,
// not in any other case when an empty string16 is required.
if (IsNullPosition())
return std::u16string();
return base::EmptyString16();
// Special case, if a position's anchor node has only ignored descendants,
// i.e., it appears to be empty to assistive software, on some platforms we
@ -3838,12 +3838,14 @@ class AXPosition {
// this by adding an embedded object character in the text representation
// used by this class, but we don't expose that character to assistive
// software that tries to retrieve the node's inner text.
static const base::NoDestructor<std::u16string> embedded_character_str(
AXNode::kEmbeddedCharacter);
if (IsEmptyObjectReplacedByCharacter())
return AXNode::kEmbeddedCharacter;
return *embedded_character_str;
switch (g_ax_embedded_object_behavior) {
case AXEmbeddedObjectBehavior::kSuppressCharacter:
return base::UTF8ToUTF16(GetAnchor()->GetInnerText());
return GetAnchor()->GetInnerTextUTF16();
case AXEmbeddedObjectBehavior::kExposeCharacter:
return GetAnchor()->GetHypertext();
}
@ -3896,10 +3898,9 @@ class AXPosition {
switch (g_ax_embedded_object_behavior) {
case AXEmbeddedObjectBehavior::kSuppressCharacter:
// TODO(nektar): Switch to anchor->GetInnerTextLength() after AXPosition
// switches to using UTF8.
return static_cast<int>(
base::UTF8ToUTF16(GetAnchor()->GetInnerText()).length());
// TODO(nektar): Switch to anchor->GetInnerTextLengthUTF8() after
// AXPosition switches to using UTF8.
return GetAnchor()->GetInnerTextLengthUTF16();
case AXEmbeddedObjectBehavior::kExposeCharacter:
return static_cast<int>(GetAnchor()->GetHypertext().length());
}
@ -3977,6 +3978,10 @@ class AXPosition {
if (!IsLeafTextPosition())
return {};
// TODO(nektar): Remove member variable `name_` once hypertext has been
// migrated to AXNode. Currently, hypertext in AXNode gets updated every
// time the `AXNode::GetHypertext()` method is called which erroniously
// invalidates this AXPosition.
name_ = GetText();
auto grapheme_iterator = std::make_unique<base::i18n::BreakIterator>(
name_, base::i18n::BreakIterator::BREAK_CHARACTER);
@ -4032,8 +4037,7 @@ class AXPosition {
int AnchorIndexInParent() const {
// If this is the root tree, the index in parent will be 0.
return GetAnchor() ? static_cast<int>(GetAnchor()->index_in_parent())
: INVALID_INDEX;
return GetAnchor() ? int{GetAnchor()->GetIndexInParent()} : INVALID_INDEX;
}
base::stack<AXNode*> GetAncestorAnchors() const {
@ -4044,9 +4048,6 @@ class AXPosition {
AXNode* current_anchor = GetAnchor();
while (current_anchor) {
anchors.push(current_anchor);
// TODO(nektar): Introduce `AXNode::GetParent()` in order to be able to
// cross tree boundaries and remove
// `AXNode::GetUnignoredParentCrossingTreeBoundaries`.
current_anchor = current_anchor->GetParentCrossingTreeBoundary();
}
@ -4254,7 +4255,8 @@ class AXPosition {
DCHECK(GetAnchor());
return is_last_child &&
GetRole(GetAnchor()->parent()) == ax::mojom::Role::kStaticText;
GetRole(GetAnchor()->GetParentCrossingTreeBoundary()) ==
ax::mojom::Role::kStaticText;
}
// Uses depth-first pre-order traversal.

@ -1136,6 +1136,19 @@ bool AXTree::Unserialize(const AXTreeUpdate& update) {
changes.push_back(AXTreeObserver::Change(node, change));
}
// Clear cached information in `AXComputedNodeData` for every node that has
// been changed in any way, including because of changes to one of its
// descendants.
std::set<AXNodeID> cleared_computed_node_data_ids;
for (AXNodeID node_id : update_state.node_data_changed_ids) {
AXNode* node = GetFromId(node_id);
while (node) {
if (cleared_computed_node_data_ids.insert(node->id()).second)
node->ClearComputedNodeData();
node = node->parent();
}
}
// Update the unignored cached values as necessary, ensuring that we only
// update once for each unignored node.
// If the node is ignored, we must update from an unignored ancestor.
@ -1164,19 +1177,19 @@ bool AXTree::Unserialize(const AXTreeUpdate& update) {
observer.OnTreeDataChanged(this, *update_state.old_tree_data, data_);
}
// Now that the unignored cached values are up to date, update observers to
// Now that the unignored cached values are up to date, notify observers of
// the nodes that were deleted from the tree but not reparented.
for (AXNodeID node_id : update_state.removed_node_ids) {
if (!update_state.IsCreatedNode(node_id))
NotifyNodeHasBeenDeleted(node_id);
}
// Now that the unignored cached values are up to date, update observers to
// Now that the unignored cached values are up to date, notify observers of
// new nodes in the tree.
for (AXNodeID node_id : update_state.new_node_ids)
NotifyNodeHasBeenReparentedOrCreated(GetFromId(node_id), &update_state);
// Now that the unignored cached values are up to date, update observers to
// Now that the unignored cached values are up to date, notify observers of
// node changes.
for (AXNodeID node_data_changed_id : update_state.node_data_changed_ids) {
AXNode* node = GetFromId(node_data_changed_id);

@ -13,6 +13,7 @@
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/test/metrics/histogram_tester.h"
#include "testing/gmock/include/gmock/gmock-matchers.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/accessibility/ax_enum_util.h"
#include "ui/accessibility/ax_node.h"
@ -293,8 +294,27 @@ class TestAXTreeObserver : public AXTreeObserver {
std::vector<std::string> attribute_change_log_;
};
// UTF encodings that are tested by the `AXTreeTestWithMultipleUTFEncodings`
// parameterized tests.
enum class TestEncoding { kUTF8, kUTF16 };
// Fixture for a test that needs to run multiple times with different UTF
// encodings. For example, once with UTF8 encoding and once with UTF16.
class AXTreeTestWithMultipleUTFEncodings
: public ::testing::TestWithParam<TestEncoding> {
public:
AXTreeTestWithMultipleUTFEncodings() = default;
~AXTreeTestWithMultipleUTFEncodings() override = default;
AXTreeTestWithMultipleUTFEncodings(
const AXTreeTestWithMultipleUTFEncodings& other) = delete;
AXTreeTestWithMultipleUTFEncodings& operator=(
const AXTreeTestWithMultipleUTFEncodings& other) = delete;
};
} // namespace
using ::testing::ElementsAre;
// A macro for testing that a absl::optional has both a value and that its value
// is set to a particular expectation.
#define EXPECT_OPTIONAL_EQ(expected, actual) \
@ -2921,7 +2941,7 @@ TEST(AXTreeTest, UnignoredSelection) {
// | |
// 9 16
tree_update.has_tree_data = true;
tree_update.tree_data.tree_id = ui::AXTreeID::CreateNewAXTreeID();
tree_update.tree_data.tree_id = AXTreeID::CreateNewAXTreeID();
tree_update.root_id = 1;
tree_update.nodes.resize(16);
tree_update.nodes[0].id = 1;
@ -3122,9 +3142,9 @@ TEST(AXTreeTest, GetChildrenOrSiblings) {
}
TEST(AXTreeTest, ChildTreeIds) {
ui::AXTreeID tree_id_1 = ui::AXTreeID::CreateNewAXTreeID();
ui::AXTreeID tree_id_2 = ui::AXTreeID::CreateNewAXTreeID();
ui::AXTreeID tree_id_3 = ui::AXTreeID::CreateNewAXTreeID();
AXTreeID tree_id_1 = AXTreeID::CreateNewAXTreeID();
AXTreeID tree_id_2 = AXTreeID::CreateNewAXTreeID();
AXTreeID tree_id_3 = AXTreeID::CreateNewAXTreeID();
AXTreeUpdate initial_state;
initial_state.root_id = 1;
@ -3169,6 +3189,213 @@ TEST(AXTreeTest, ChildTreeIds) {
EXPECT_EQ(0U, child_tree_3_nodes.size());
}
TEST_P(AXTreeTestWithMultipleUTFEncodings, ComputedNodeData) {
// kRootWebArea
// ++kTextField (contenteditable)
// ++++kGenericContainer
// ++++++kStaticText "Line 1"
// ++++++kLineBreak '\n'
// ++++++kStaticText "Line 2"
// ++kParagraph
// ++++kGenericContainer (span) IGNORED
// ++++++kStaticText "span text" IGNORED
// ++++kLink
// ++++++kStaticText "Link text"
AXNodeData root;
root.id = 1;
AXNodeData rich_text_field;
rich_text_field.id = 2;
AXNodeData rich_text_field_text_container;
rich_text_field_text_container.id = 3;
AXNodeData rich_text_field_line_1;
rich_text_field_line_1.id = 4;
AXNodeData rich_text_field_line_break;
rich_text_field_line_break.id = 5;
AXNodeData rich_text_field_line_2;
rich_text_field_line_2.id = 6;
AXNodeData paragraph;
paragraph.id = 7;
AXNodeData paragraph_span;
paragraph_span.id = 8;
AXNodeData paragraph_span_text;
paragraph_span_text.id = 9;
AXNodeData paragraph_link;
paragraph_link.id = 10;
AXNodeData paragraph_link_text;
paragraph_link_text.id = 11;
root.role = ax::mojom::Role::kRootWebArea;
root.child_ids = {rich_text_field.id, paragraph.id};
rich_text_field.role = ax::mojom::Role::kTextField;
rich_text_field.AddState(ax::mojom::State::kEditable);
rich_text_field.AddState(ax::mojom::State::kRichlyEditable);
rich_text_field.AddBoolAttribute(
ax::mojom::BoolAttribute::kNonAtomicTextFieldRoot, true);
rich_text_field.SetName("Rich text field");
rich_text_field.SetValue("Line 1\nLine 2");
rich_text_field.child_ids = {rich_text_field_text_container.id};
rich_text_field_text_container.role = ax::mojom::Role::kGenericContainer;
rich_text_field_text_container.AddState(ax::mojom::State::kIgnored);
rich_text_field_text_container.child_ids = {rich_text_field_line_1.id,
rich_text_field_line_break.id,
rich_text_field_line_2.id};
rich_text_field_line_1.role = ax::mojom::Role::kStaticText;
rich_text_field_line_1.AddState(ax::mojom::State::kEditable);
rich_text_field_line_1.AddState(ax::mojom::State::kRichlyEditable);
rich_text_field_line_1.SetName("Line 1");
rich_text_field_line_break.role = ax::mojom::Role::kLineBreak;
rich_text_field_line_break.AddState(ax::mojom::State::kEditable);
rich_text_field_line_break.AddState(ax::mojom::State::kRichlyEditable);
rich_text_field_line_break.SetName("\n");
rich_text_field_line_2.role = ax::mojom::Role::kStaticText;
rich_text_field_line_2.AddState(ax::mojom::State::kEditable);
rich_text_field_line_2.AddState(ax::mojom::State::kRichlyEditable);
rich_text_field_line_2.SetName("Line 2");
paragraph.role = ax::mojom::Role::kParagraph;
paragraph.child_ids = {paragraph_span.id, paragraph_link.id};
paragraph_span.role = ax::mojom::Role::kGenericContainer;
paragraph_span.AddState(ax::mojom::State::kIgnored);
paragraph_span.child_ids = {paragraph_span_text.id};
paragraph_span_text.role = ax::mojom::Role::kStaticText;
paragraph_span_text.AddState(ax::mojom::State::kIgnored);
paragraph_span_text.SetName("span text");
paragraph_link.role = ax::mojom::Role::kLink;
paragraph_link.AddState(ax::mojom::State::kLinked);
paragraph_link.child_ids = {paragraph_link_text.id};
paragraph_link_text.role = ax::mojom::Role::kStaticText;
paragraph_link_text.SetName("Link text");
AXTreeUpdate update;
update.has_tree_data = true;
update.tree_data.tree_id = AXTreeID::CreateNewAXTreeID();
update.root_id = root.id;
update.nodes = {root,
rich_text_field,
rich_text_field_text_container,
rich_text_field_line_1,
rich_text_field_line_break,
rich_text_field_line_2,
paragraph,
paragraph_span,
paragraph_span_text,
paragraph_link,
paragraph_link_text};
AXTree tree(update);
TestAXTreeObserver test_observer(&tree);
ASSERT_NE(nullptr, tree.root());
ASSERT_EQ(2u, tree.root()->children().size());
if (GetParam() == TestEncoding::kUTF8) {
EXPECT_EQ("Line 1\nLine 2Link text", tree.root()->GetInnerText());
EXPECT_EQ(22, tree.root()->GetInnerTextLength());
} else if (GetParam() == TestEncoding::kUTF16) {
EXPECT_EQ(u"Line 1\nLine 2Link text", tree.root()->GetInnerTextUTF16());
EXPECT_EQ(22, tree.root()->GetInnerTextLengthUTF16());
}
if (GetParam() == TestEncoding::kUTF8) {
EXPECT_EQ("Line 1\nLine 2",
tree.root()->GetChildAtIndex(0)->GetInnerText());
EXPECT_EQ(13, tree.root()->GetChildAtIndex(0)->GetInnerTextLength());
} else if (GetParam() == TestEncoding::kUTF16) {
EXPECT_EQ(u"Line 1\nLine 2",
tree.root()->GetChildAtIndex(0)->GetInnerTextUTF16());
EXPECT_EQ(13, tree.root()->GetChildAtIndex(0)->GetInnerTextLengthUTF16());
}
if (GetParam() == TestEncoding::kUTF8) {
EXPECT_EQ("Link text", tree.root()->GetChildAtIndex(1)->GetInnerText());
EXPECT_EQ(9, tree.root()->GetChildAtIndex(1)->GetInnerTextLength());
} else if (GetParam() == TestEncoding::kUTF16) {
EXPECT_EQ(u"Link text",
tree.root()->GetChildAtIndex(1)->GetInnerTextUTF16());
EXPECT_EQ(9, tree.root()->GetChildAtIndex(1)->GetInnerTextLengthUTF16());
}
//
// Flip the ignored state of the span, the link and the line break, and delete
// the second line in the rich text field, all of which should change their
// cached inner text.
// kRootWebArea
// ++kTextField (contenteditable)
// ++++kGenericContainer
// ++++++kStaticText "Line 1"
// ++++++kLineBreak '\n' IGNORED
// ++kParagraph
// ++++kGenericContainer (span)
// ++++++kStaticText "span text"
// ++++kLink IGNORED
// ++++++kStaticText "Link text"
rich_text_field_line_break.AddState(ax::mojom::State::kIgnored);
paragraph_span.RemoveState(ax::mojom::State::kIgnored);
paragraph_span_text.RemoveState(ax::mojom::State::kIgnored);
// Do not add the ignored state to the link's text on purpose.
paragraph_link.AddState(ax::mojom::State::kIgnored);
rich_text_field_text_container.child_ids = {rich_text_field_line_1.id,
rich_text_field_line_break.id};
AXTreeUpdate update_2;
update_2.node_id_to_clear = rich_text_field_line_2.id;
update_2.nodes = {rich_text_field_text_container, rich_text_field_line_break,
paragraph_span, paragraph_span_text, paragraph_link};
ASSERT_TRUE(tree.Unserialize(update_2)) << tree.error();
ASSERT_EQ(2u, tree.root()->children().size());
if (GetParam() == TestEncoding::kUTF8) {
EXPECT_EQ("Line 1span textLink text", tree.root()->GetInnerText());
EXPECT_EQ(24, tree.root()->GetInnerTextLength());
} else if (GetParam() == TestEncoding::kUTF16) {
EXPECT_EQ(u"Line 1span textLink text", tree.root()->GetInnerTextUTF16());
EXPECT_EQ(24, tree.root()->GetInnerTextLengthUTF16());
}
if (GetParam() == TestEncoding::kUTF8) {
EXPECT_EQ("Line 1", tree.root()->GetChildAtIndex(0)->GetInnerText());
EXPECT_EQ(6, tree.root()->GetChildAtIndex(0)->GetInnerTextLength());
} else if (GetParam() == TestEncoding::kUTF16) {
EXPECT_EQ(u"Line 1", tree.root()->GetChildAtIndex(0)->GetInnerTextUTF16());
EXPECT_EQ(6, tree.root()->GetChildAtIndex(0)->GetInnerTextLengthUTF16());
}
if (GetParam() == TestEncoding::kUTF8) {
EXPECT_EQ("span textLink text",
tree.root()->GetChildAtIndex(1)->GetInnerText());
EXPECT_EQ(18, tree.root()->GetChildAtIndex(1)->GetInnerTextLength());
} else if (GetParam() == TestEncoding::kUTF16) {
EXPECT_EQ(u"span textLink text",
tree.root()->GetChildAtIndex(1)->GetInnerTextUTF16());
EXPECT_EQ(18, tree.root()->GetChildAtIndex(1)->GetInnerTextLengthUTF16());
}
const std::vector<std::string>& change_log =
test_observer.attribute_change_log();
EXPECT_THAT(
change_log,
ElementsAre("ignored changed to true", "ignored changed to false",
"ignored changed to false", "ignored changed to true"));
}
INSTANTIATE_TEST_SUITE_P(MultipleUTFEncodingTest,
AXTreeTestWithMultipleUTFEncodings,
::testing::Values(TestEncoding::kUTF8,
TestEncoding::kUTF16));
// Tests GetPosInSet and GetSetSize return the assigned int attribute values.
TEST(AXTreeTest, SetSizePosInSetAssigned) {
AXTreeUpdate tree_update;

@ -1532,7 +1532,7 @@ TEST_F(AXPlatformNodeTextRangeProviderTest,
// When using heading navigation, the empty objects (see
// AXPosition::IsEmptyObjectReplacedByCharacter for information about empty
// objects) sometimes cause a problem with
// AXPlatformNodeTextRangeProviderWin::ExpandToEnlosingUnit.
// AXPlatformNodeTextRangeProviderWin::ExpandToEnclosingUnit.
// With some specific AXTree (like the one used below), the empty object
// causes ExpandToEnclosingUnit to move the range back on the heading that it
// previously was instead of moving it forward/backward to the next heading.