blob: 5a5a60d15b9ac08e48f4329e90fdd407e99ca9f4 [file] [log] [blame]
// 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 <memory>
#include <utility>
#include <vector>
#include "base/memory/raw_ptr.h"
#include "testing/gmock/include/gmock/gmock-matchers.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/accessibility/ax_position.h"
#include "ui/accessibility/ax_tree.h"
#include "ui/accessibility/ax_tree_data.h"
#include "ui/accessibility/ax_tree_id.h"
#include "ui/accessibility/test_ax_tree_manager.h"
namespace ui {
namespace {
class AXComputedNodeDataTest : public ::testing::Test,
public TestAXTreeManager {
public:
AXComputedNodeDataTest();
~AXComputedNodeDataTest() override;
AXComputedNodeDataTest(const AXComputedNodeDataTest& other) = delete;
AXComputedNodeDataTest& operator=(const AXComputedNodeDataTest& other) =
delete;
void SetUp() override;
protected:
// Numbers at the end of variable names indicate their position under the
// root.
AXNodeData root_;
AXNodeData paragraph_0_;
AXNodeData static_text_0_0_ignored_;
AXNodeData paragraph_1_ignored_;
AXNodeData static_text_1_0_;
AXNodeData paragraph_2_ignored_;
AXNodeData link_2_0_ignored_;
AXNodeData static_text_2_0_0_;
AXNodeData static_text_2_0_1_;
raw_ptr<AXNode> root_node_;
};
AXComputedNodeDataTest::AXComputedNodeDataTest() = default;
AXComputedNodeDataTest::~AXComputedNodeDataTest() = default;
void AXComputedNodeDataTest::SetUp() {
// ++kRootWebArea contenteditable
// ++++kParagraph
// ++++++kStaticText IGNORED "i"
// ++++kParagraph IGNORED
// ++++++kStaticText "t_1"
// ++++kParagraph IGNORED
// ++++++kLink IGNORED
// ++++++++kStaticText "s+t++2...0. 0"
// ++++++++kStaticText "s t\n2\r0\r\n1"
root_.id = 1;
paragraph_0_.id = 2;
static_text_0_0_ignored_.id = 3;
paragraph_1_ignored_.id = 4;
static_text_1_0_.id = 5;
paragraph_2_ignored_.id = 6;
link_2_0_ignored_.id = 7;
static_text_2_0_0_.id = 8;
static_text_2_0_1_.id = 9;
root_.role = ax::mojom::Role::kRootWebArea;
root_.AddBoolAttribute(ax::mojom::BoolAttribute::kNonAtomicTextFieldRoot,
true);
root_.child_ids = {paragraph_0_.id, paragraph_1_ignored_.id,
paragraph_2_ignored_.id};
paragraph_0_.role = ax::mojom::Role::kParagraph;
paragraph_0_.AddBoolAttribute(ax::mojom::BoolAttribute::kIsLineBreakingObject,
true);
paragraph_0_.child_ids = {static_text_0_0_ignored_.id};
static_text_0_0_ignored_.role = ax::mojom::Role::kStaticText;
static_text_0_0_ignored_.AddState(ax::mojom::State::kIgnored);
// Ignored text should not appear anywhere and should not be used to separate
// words.
static_text_0_0_ignored_.SetName("i");
paragraph_1_ignored_.role = ax::mojom::Role::kParagraph;
paragraph_1_ignored_.AddState(ax::mojom::State::kIgnored);
paragraph_1_ignored_.AddBoolAttribute(
ax::mojom::BoolAttribute::kIsLineBreakingObject, true);
paragraph_1_ignored_.child_ids = {static_text_1_0_.id};
static_text_1_0_.role = ax::mojom::Role::kStaticText;
// An underscore should separate words.
static_text_1_0_.SetName("t_1");
paragraph_2_ignored_.role = ax::mojom::Role::kParagraph;
paragraph_2_ignored_.AddState(ax::mojom::State::kIgnored);
paragraph_2_ignored_.AddBoolAttribute(
ax::mojom::BoolAttribute::kIsLineBreakingObject, true);
paragraph_2_ignored_.child_ids = {link_2_0_ignored_.id};
link_2_0_ignored_.role = ax::mojom::Role::kLink;
link_2_0_ignored_.AddState(ax::mojom::State::kLinked);
link_2_0_ignored_.AddState(ax::mojom::State::kIgnored);
link_2_0_ignored_.child_ids = {static_text_2_0_0_.id, static_text_2_0_1_.id};
static_text_2_0_0_.role = ax::mojom::Role::kStaticText;
// A series of punctuation marks, or a stretch of whitespace should separate
// words.
static_text_2_0_0_.SetName("s+t++2...0. 0");
static_text_2_0_1_.role = ax::mojom::Role::kStaticText;
// Both a carage return as well as a line break should separate lines, but not
// a space character.
static_text_2_0_1_.SetName("s t\n2\r0\r\n1");
AXTreeUpdate initial_state;
initial_state.root_id = root_.id;
initial_state.nodes = {root_,
paragraph_0_,
static_text_0_0_ignored_,
paragraph_1_ignored_,
static_text_1_0_,
paragraph_2_ignored_,
link_2_0_ignored_,
static_text_2_0_0_,
static_text_2_0_1_};
initial_state.has_tree_data = true;
AXTreeData tree_data;
tree_data.tree_id = AXTreeID::CreateNewAXTreeID();
tree_data.title = "Application";
initial_state.tree_data = tree_data;
auto tree = std::make_unique<AXTree>();
ASSERT_TRUE(tree->Unserialize(initial_state)) << tree->error();
root_node_ = tree->root();
ASSERT_EQ(root_.id, root_node_->id());
// `SetTree` is defined in our `TestAXTreeManager` superclass and it passes
// ownership of the created AXTree to the manager.
SetTree(std::move(tree));
}
} // namespace
using ::testing::ElementsAre;
using ::testing::ElementsAreArray;
using ::testing::SizeIs;
using ::testing::StrEq;
TEST_F(AXComputedNodeDataTest, UnignoredValues) {
const AXNode* paragraph_0_node = root_node_->GetChildAtIndex(0);
const AXNode* paragraph_1_ignored_node = root_node_->GetChildAtIndex(1);
const AXNode* static_text_1_0_node =
paragraph_1_ignored_node->GetChildAtIndex(0);
const AXNode* paragraph_2_ignored_node = root_node_->GetChildAtIndex(2);
const AXNode* link_2_0_ignored_node =
paragraph_2_ignored_node->GetChildAtIndex(0);
const AXNode* static_text_2_0_0_node =
link_2_0_ignored_node->GetChildAtIndex(0);
const AXNode* static_text_2_0_1_node =
link_2_0_ignored_node->GetChildAtIndex(1);
// Perform the checks twice to ensure that caching returns the same values.
for (int i = 0; i < 2; ++i) {
EXPECT_EQ(
0,
root_node_->GetComputedNodeData().GetOrComputeUnignoredIndexInParent());
EXPECT_EQ(0, paragraph_0_node->GetComputedNodeData()
.GetOrComputeUnignoredIndexInParent());
EXPECT_EQ(1, static_text_1_0_node->GetComputedNodeData()
.GetOrComputeUnignoredIndexInParent());
EXPECT_EQ(2, static_text_2_0_0_node->GetComputedNodeData()
.GetOrComputeUnignoredIndexInParent());
EXPECT_EQ(3, static_text_2_0_1_node->GetComputedNodeData()
.GetOrComputeUnignoredIndexInParent());
EXPECT_EQ(
4, root_node_->GetComputedNodeData().GetOrComputeUnignoredChildCount());
EXPECT_EQ(0, paragraph_0_node->GetComputedNodeData()
.GetOrComputeUnignoredChildCount());
EXPECT_EQ(0, static_text_1_0_node->GetComputedNodeData()
.GetOrComputeUnignoredChildCount());
EXPECT_EQ(0, static_text_2_0_0_node->GetComputedNodeData()
.GetOrComputeUnignoredChildCount());
EXPECT_EQ(0, static_text_2_0_1_node->GetComputedNodeData()
.GetOrComputeUnignoredChildCount());
EXPECT_THAT(
root_node_->GetComputedNodeData().GetOrComputeUnignoredChildIDs(),
ElementsAre(paragraph_0_.id, static_text_1_0_.id, static_text_2_0_0_.id,
static_text_2_0_1_.id));
}
}
TEST_F(AXComputedNodeDataTest, HasOrCanComputeAttribute) {
EXPECT_TRUE(root_node_->GetComputedNodeData().HasOrCanComputeAttribute(
ax::mojom::StringAttribute::kValue));
EXPECT_FALSE(root_node_->GetComputedNodeData().HasOrCanComputeAttribute(
ax::mojom::StringAttribute::kHtmlTag));
EXPECT_TRUE(root_node_->GetComputedNodeData().HasOrCanComputeAttribute(
ax::mojom::IntListAttribute::kWordStarts));
EXPECT_FALSE(root_node_->GetComputedNodeData().HasOrCanComputeAttribute(
ax::mojom::IntListAttribute::kLabelledbyIds));
AXNode* paragraph_0_node = root_node_->GetChildAtIndex(0);
EXPECT_FALSE(paragraph_0_node->GetComputedNodeData().HasOrCanComputeAttribute(
ax::mojom::StringAttribute::kValue));
EXPECT_FALSE(paragraph_0_node->GetComputedNodeData().HasOrCanComputeAttribute(
ax::mojom::StringAttribute::kHtmlTag));
EXPECT_TRUE(paragraph_0_node->GetComputedNodeData().HasOrCanComputeAttribute(
ax::mojom::IntListAttribute::kWordStarts));
EXPECT_FALSE(paragraph_0_node->GetComputedNodeData().HasOrCanComputeAttribute(
ax::mojom::IntListAttribute::kLabelledbyIds));
// By removing the `ax::mojom::BoolAttribute::kNonAtomicTextFieldRoot`
// attribute, the root is no longer a content editable.
root_.RemoveBoolAttribute(ax::mojom::BoolAttribute::kNonAtomicTextFieldRoot);
root_.AddIntListAttribute(ax::mojom::IntListAttribute::kLabelledbyIds,
{static_text_0_0_ignored_.id});
paragraph_0_.AddStringAttribute(ax::mojom::StringAttribute::kValue,
"New: \nvalue.");
paragraph_0_.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, "p");
paragraph_0_.AddIntListAttribute(ax::mojom::IntListAttribute::kWordStarts,
{0, 4});
AXTreeUpdate tree_update;
tree_update.root_id = root_.id;
tree_update.nodes = {root_, paragraph_0_};
ASSERT_TRUE(GetTree()->Unserialize(tree_update));
root_node_ = GetTree()->root();
ASSERT_EQ(root_.id, root_node_->id());
// Computing the value attribute is only supported on non-atomic text fields.
EXPECT_FALSE(root_node_->GetComputedNodeData().HasOrCanComputeAttribute(
ax::mojom::StringAttribute::kValue));
EXPECT_FALSE(root_node_->GetComputedNodeData().HasOrCanComputeAttribute(
ax::mojom::StringAttribute::kHtmlTag));
EXPECT_TRUE(root_node_->GetComputedNodeData().HasOrCanComputeAttribute(
ax::mojom::IntListAttribute::kWordStarts));
EXPECT_TRUE(root_node_->GetComputedNodeData().HasOrCanComputeAttribute(
ax::mojom::IntListAttribute::kLabelledbyIds));
paragraph_0_node = root_node_->GetChildAtIndex(0);
// However, for maximum flexibility, if the value attribute is already present
// in the node's data we should use it without checking if the role supports
// it.
EXPECT_TRUE(paragraph_0_node->GetComputedNodeData().HasOrCanComputeAttribute(
ax::mojom::StringAttribute::kValue));
EXPECT_TRUE(paragraph_0_node->GetComputedNodeData().HasOrCanComputeAttribute(
ax::mojom::StringAttribute::kHtmlTag));
EXPECT_TRUE(paragraph_0_node->GetComputedNodeData().HasOrCanComputeAttribute(
ax::mojom::IntListAttribute::kWordStarts));
EXPECT_FALSE(paragraph_0_node->GetComputedNodeData().HasOrCanComputeAttribute(
ax::mojom::IntListAttribute::kLabelledbyIds));
}
TEST_F(AXComputedNodeDataTest, GetOrComputeAttribute) {
// Embedded object behavior is dependant on platform. We manually set it to a
// specific value so that test results are consistent across platforms.
testing::ScopedAXEmbeddedObjectBehaviorSetter embedded_object_behaviour(
AXEmbeddedObjectBehavior::kSuppressCharacter);
// Line breaks should be inserted between each paragraph to mirror how HTML's
// "textContent" works.
EXPECT_THAT(root_node_->GetComputedNodeData().GetOrComputeAttributeUTF8(
ax::mojom::StringAttribute::kValue),
StrEq("\nt_1\ns+t++2...0. 0s t\n2\r0\r\n1"));
EXPECT_THAT(root_node_->GetComputedNodeData().GetOrComputeAttributeUTF8(
ax::mojom::StringAttribute::kHtmlTag),
StrEq(""));
// Boundaries are delimited by a vertical bar, '|'.
// Words: "|t|_|1s|+|t|++|2|...|0|. |0s| |t|\n|2|\r|0|\r\n|1|".
int32_t word_starts[] = {0, 5, 8, 12, 16, 19, 21, 23, 26};
int32_t word_ends[] = {4, 6, 9, 13, 18, 20, 22, 24, 27};
EXPECT_THAT(root_node_->GetComputedNodeData().GetOrComputeAttribute(
ax::mojom::IntListAttribute::kWordStarts),
ElementsAreArray(word_starts));
EXPECT_THAT(root_node_->GetComputedNodeData().GetOrComputeAttribute(
ax::mojom::IntListAttribute::kWordEnds),
ElementsAreArray(word_ends));
// Lines: "|t_1s+t++2...0. 0s t|\n|2|\r|0|\r\n|1|".
int32_t line_starts[] = {0, 21, 23, 26};
int32_t line_ends[] = {21, 23, 26, 27};
EXPECT_THAT(root_node_->GetComputedNodeData().GetOrComputeAttribute(
ax::mojom::IntListAttribute::kLineStarts),
ElementsAreArray(line_starts));
EXPECT_THAT(root_node_->GetComputedNodeData().GetOrComputeAttribute(
ax::mojom::IntListAttribute::kLineEnds),
ElementsAreArray(line_ends));
// Sentences: "|t_1s+t++2...0.| |0s t|\n|2|\r|0|\r\n|1|".
int32_t sentence_starts[] = {0, 21, 23, 26};
int32_t sentence_ends[] = {21, 23, 26, 27};
EXPECT_THAT(root_node_->GetComputedNodeData().GetOrComputeAttribute(
ax::mojom::IntListAttribute::kSentenceStarts),
ElementsAreArray(sentence_starts));
EXPECT_THAT(root_node_->GetComputedNodeData().GetOrComputeAttribute(
ax::mojom::IntListAttribute::kSentenceEnds),
ElementsAreArray(sentence_ends));
EXPECT_THAT(root_node_->GetComputedNodeData().GetOrComputeAttribute(
ax::mojom::IntListAttribute::kLabelledbyIds),
SizeIs(0));
AXNode* paragraph_0_node = root_node_->GetChildAtIndex(0);
EXPECT_THAT(paragraph_0_node->GetComputedNodeData().GetOrComputeAttributeUTF8(
ax::mojom::StringAttribute::kValue),
StrEq(""))
<< "The static text child should be ignored.";
EXPECT_THAT(paragraph_0_node->GetComputedNodeData().GetOrComputeAttributeUTF8(
ax::mojom::StringAttribute::kHtmlTag),
StrEq(""));
// Ignored text produces no boundaries.
EXPECT_THAT(paragraph_0_node->GetComputedNodeData().GetOrComputeAttribute(
ax::mojom::IntListAttribute::kWordStarts),
SizeIs(0));
EXPECT_THAT(paragraph_0_node->GetComputedNodeData().GetOrComputeAttribute(
ax::mojom::IntListAttribute::kWordEnds),
SizeIs(0));
EXPECT_THAT(paragraph_0_node->GetComputedNodeData().GetOrComputeAttribute(
ax::mojom::IntListAttribute::kLineStarts),
SizeIs(0));
EXPECT_THAT(paragraph_0_node->GetComputedNodeData().GetOrComputeAttribute(
ax::mojom::IntListAttribute::kLineEnds),
SizeIs(0));
EXPECT_THAT(paragraph_0_node->GetComputedNodeData().GetOrComputeAttribute(
ax::mojom::IntListAttribute::kSentenceStarts),
SizeIs(0));
EXPECT_THAT(paragraph_0_node->GetComputedNodeData().GetOrComputeAttribute(
ax::mojom::IntListAttribute::kSentenceEnds),
SizeIs(0));
EXPECT_THAT(paragraph_0_node->GetComputedNodeData().GetOrComputeAttribute(
ax::mojom::IntListAttribute::kLabelledbyIds),
SizeIs(0));
// By removing the `ax::mojom::BoolAttribute::kNonAtomicTextFieldRoot`
// attribute, the root is no longer a content editable.
root_.RemoveBoolAttribute(ax::mojom::BoolAttribute::kNonAtomicTextFieldRoot);
root_.AddIntListAttribute(ax::mojom::IntListAttribute::kLabelledbyIds,
{static_text_0_0_ignored_.id});
paragraph_0_.AddStringAttribute(ax::mojom::StringAttribute::kValue,
"New: \nvalue.");
paragraph_0_.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, "p");
// Word starts/ends are intentionally set to the wrong values to ensure that
// `AXNodeData` takes priority over `AXComputedNodeData` if present.
std::vector<int32_t> wrong_word_starts = {1, 5};
std::vector<int32_t> wrong_word_ends = {6, 8};
paragraph_0_.AddIntListAttribute(ax::mojom::IntListAttribute::kWordStarts,
wrong_word_starts);
paragraph_0_.AddIntListAttribute(ax::mojom::IntListAttribute::kWordEnds,
wrong_word_ends);
AXTreeUpdate tree_update;
tree_update.root_id = root_.id;
tree_update.nodes = {root_, paragraph_0_};
ASSERT_TRUE(GetTree()->Unserialize(tree_update));
root_node_ = GetTree()->root();
ASSERT_EQ(root_.id, root_node_->id());
EXPECT_THAT(root_node_->GetComputedNodeData().GetOrComputeAttributeUTF8(
ax::mojom::StringAttribute::kValue),
SizeIs(0))
<< "Computing the value attribute is only supported on non-atomic text "
"fields.";
EXPECT_THAT(root_node_->GetComputedNodeData().GetOrComputeAttributeUTF8(
ax::mojom::StringAttribute::kHtmlTag),
SizeIs(0));
// No change to the various boundaries should have been observed since the
// root's text content hasn't changed.
EXPECT_THAT(root_node_->GetComputedNodeData().GetOrComputeAttribute(
ax::mojom::IntListAttribute::kWordStarts),
ElementsAreArray(word_starts));
EXPECT_THAT(root_node_->GetComputedNodeData().GetOrComputeAttribute(
ax::mojom::IntListAttribute::kWordEnds),
ElementsAreArray(word_ends));
EXPECT_THAT(root_node_->GetComputedNodeData().GetOrComputeAttribute(
ax::mojom::IntListAttribute::kLineStarts),
ElementsAreArray(line_starts));
EXPECT_THAT(root_node_->GetComputedNodeData().GetOrComputeAttribute(
ax::mojom::IntListAttribute::kLineEnds),
ElementsAreArray(line_ends));
EXPECT_THAT(root_node_->GetComputedNodeData().GetOrComputeAttribute(
ax::mojom::IntListAttribute::kSentenceStarts),
ElementsAreArray(sentence_starts));
EXPECT_THAT(root_node_->GetComputedNodeData().GetOrComputeAttribute(
ax::mojom::IntListAttribute::kSentenceEnds),
ElementsAreArray(sentence_ends));
EXPECT_THAT(root_node_->GetComputedNodeData().GetOrComputeAttribute(
ax::mojom::IntListAttribute::kLabelledbyIds),
ElementsAre(static_text_0_0_ignored_.id));
paragraph_0_node = root_node_->GetChildAtIndex(0);
EXPECT_THAT(paragraph_0_node->GetComputedNodeData().GetOrComputeAttributeUTF8(
ax::mojom::StringAttribute::kValue),
StrEq("New: \nvalue."))
<< "For maximum flexibility, if the value attribute is already present "
"in the node's data we should use it without checking if the role "
"supports it.";
EXPECT_THAT(paragraph_0_node->GetComputedNodeData().GetOrComputeAttributeUTF8(
ax::mojom::StringAttribute::kHtmlTag),
StrEq("p"));
// Word starts/ends are intentionally set to the wrong values in `AXNodeData`.
EXPECT_THAT(paragraph_0_node->GetComputedNodeData().GetOrComputeAttribute(
ax::mojom::IntListAttribute::kWordStarts),
ElementsAreArray(wrong_word_starts))
<< "`AXNodeData` should take priority over `AXComputedNodeData`, if "
"present.";
EXPECT_THAT(paragraph_0_node->GetComputedNodeData().GetOrComputeAttribute(
ax::mojom::IntListAttribute::kWordEnds),
ElementsAreArray(wrong_word_ends))
<< "`AXNodeData` should take priority over `AXComputedNodeData`, if "
"present.";
EXPECT_THAT(paragraph_0_node->GetComputedNodeData().GetOrComputeAttribute(
ax::mojom::IntListAttribute::kLineStarts),
ElementsAre());
EXPECT_THAT(paragraph_0_node->GetComputedNodeData().GetOrComputeAttribute(
ax::mojom::IntListAttribute::kLineEnds),
ElementsAre());
EXPECT_THAT(paragraph_0_node->GetComputedNodeData().GetOrComputeAttribute(
ax::mojom::IntListAttribute::kSentenceStarts),
ElementsAre());
EXPECT_THAT(paragraph_0_node->GetComputedNodeData().GetOrComputeAttribute(
ax::mojom::IntListAttribute::kSentenceEnds),
ElementsAre());
EXPECT_THAT(paragraph_0_node->GetComputedNodeData().GetOrComputeAttribute(
ax::mojom::IntListAttribute::kLabelledbyIds),
SizeIs(0));
}
TEST_F(AXComputedNodeDataTest, GetOrComputeTextContent) {
// Embedded object behavior is dependant on platform. We manually set it to a
// specific value so that test results are consistent across platforms.
testing::ScopedAXEmbeddedObjectBehaviorSetter embedded_object_behaviour(
AXEmbeddedObjectBehavior::kSuppressCharacter);
EXPECT_THAT(root_node_->GetComputedNodeData()
.GetOrComputeTextContentWithParagraphBreaksUTF8(),
StrEq("\nt_1\ns+t++2...0. 0s t\n2\r0\r\n1"));
EXPECT_THAT(root_node_->GetComputedNodeData().GetOrComputeTextContentUTF8(),
StrEq("t_1s+t++2...0. 0s t\n2\r0\r\n1"));
EXPECT_EQ(
root_node_->GetComputedNodeData().GetOrComputeTextContentLengthUTF8(),
27);
// Paragraph_0's text is ignored. Ignored text should not be visible.
const AXNode* paragraph_0_node = root_node_->GetChildAtIndex(0);
EXPECT_THAT(paragraph_0_node->GetComputedNodeData()
.GetOrComputeTextContentWithParagraphBreaksUTF8(),
StrEq(""));
EXPECT_THAT(
paragraph_0_node->GetComputedNodeData().GetOrComputeTextContentUTF8(),
StrEq(""));
EXPECT_EQ(paragraph_0_node->GetComputedNodeData()
.GetOrComputeTextContentLengthUTF8(),
0);
// The two incarnations of the "TextContent" methods should behave identically
// when line breaks are manually inserted via e.g. a <br> element in HTML, as
// this case demonstrates.
const AXNode* paragraph_2_ignored_node = root_node_->GetChildAtIndex(2);
EXPECT_THAT(paragraph_2_ignored_node->GetComputedNodeData()
.GetOrComputeTextContentWithParagraphBreaksUTF8(),
StrEq("s+t++2...0. 0s t\n2\r0\r\n1"));
EXPECT_THAT(paragraph_2_ignored_node->GetComputedNodeData()
.GetOrComputeTextContentUTF8(),
StrEq("s+t++2...0. 0s t\n2\r0\r\n1"));
EXPECT_EQ(paragraph_2_ignored_node->GetComputedNodeData()
.GetOrComputeTextContentLengthUTF8(),
24);
}
} // namespace ui