blob: 3e69e24f3b1f3b187d4a513437f2f3c5f1ec3264 [file] [log] [blame]
// Copyright 2016 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 <stdint.h>
#include <algorithm>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "base/bind.h"
#include "base/callback.h"
#include "base/strings/string16.h"
#include "base/strings/utf_string_conversions.h"
#include "testing/gtest/include/gtest/gtest.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_node_position.h"
#include "ui/accessibility/ax_range.h"
#include "ui/accessibility/ax_tree.h"
#include "ui/accessibility/ax_tree_data.h"
#include "ui/accessibility/ax_tree_id.h"
#include "ui/accessibility/ax_tree_update.h"
#include "ui/accessibility/test_ax_tree_manager.h"
namespace ui {
using TestPositionType = std::unique_ptr<AXPosition<AXNodePosition, AXNode>>;
using TestPositionRange = AXRange<AXPosition<AXNodePosition, AXNode>>;
namespace {
constexpr AXNode::AXID ROOT_ID = 1;
constexpr AXNode::AXID BUTTON_ID = 2;
constexpr AXNode::AXID CHECK_BOX_ID = 3;
constexpr AXNode::AXID TEXT_FIELD_ID = 4;
constexpr AXNode::AXID STATIC_TEXT1_ID = 5;
constexpr AXNode::AXID INLINE_BOX1_ID = 6;
constexpr AXNode::AXID LINE_BREAK_ID = 7;
constexpr AXNode::AXID STATIC_TEXT2_ID = 8;
constexpr AXNode::AXID INLINE_BOX2_ID = 9;
// A group of basic and extended characters.
constexpr const wchar_t* kGraphemeClusters[] = {
// The English word "hey" consisting of four ASCII characters.
L"h",
L"e",
L"y",
// A Hindi word (which means "Hindi") consisting of two Devanagari
// grapheme clusters.
L"\x0939\x093F",
L"\x0928\x094D\x0926\x0940",
// A Thai word (which means "feel") consisting of three Thai grapheme
// clusters.
L"\x0E23\x0E39\x0E49",
L"\x0E2A\x0E36",
L"\x0E01",
};
class AXPositionTest : public testing::Test, public TestAXTreeManager {
public:
AXPositionTest() = default;
~AXPositionTest() override = default;
protected:
static const char* TEXT_VALUE;
void SetUp() override;
// Creates a document with three pages, adding any extra information to this
// basic document structure that has been provided as arguments.
std::unique_ptr<AXTree> CreateMultipageDocument(
AXNodeData& root_data,
AXNodeData& page_1_data,
AXNodeData& page_1_text_data,
AXNodeData& page_2_data,
AXNodeData& page_2_text_data,
AXNodeData& page_3_data,
AXNodeData& page_3_text_data) const;
// Creates a document with three static text objects each containing text in a
// different language.
std::unique_ptr<AXTree> CreateMultilingualDocument(
std::vector<int>* text_offsets) const;
void AssertTextLengthEquals(const AXTree* tree,
AXNode::AXID node_id,
int expected_text_length) const;
// Creates a new AXTree from a vector of nodes.
// Assumes the first node in the vector is the root.
std::unique_ptr<AXTree> CreateAXTree(
const std::vector<AXNodeData>& nodes) const;
AXNodeData root_;
AXNodeData button_;
AXNodeData check_box_;
AXNodeData text_field_;
AXNodeData static_text1_;
AXNodeData line_break_;
AXNodeData static_text2_;
AXNodeData inline_box1_;
AXNodeData inline_box2_;
private:
DISALLOW_COPY_AND_ASSIGN(AXPositionTest);
};
// Used by AXPositionExpandToEnclosingTextBoundaryTestWithParam.
//
// Every test instance starts from a pre-determined position and calls the
// ExpandToEnclosingTextBoundary method with the arguments provided in this
// struct.
struct ExpandToEnclosingTextBoundaryTestParam {
ExpandToEnclosingTextBoundaryTestParam() = default;
// Required by GTest framework.
ExpandToEnclosingTextBoundaryTestParam(
const ExpandToEnclosingTextBoundaryTestParam& other) = default;
ExpandToEnclosingTextBoundaryTestParam& operator=(
const ExpandToEnclosingTextBoundaryTestParam& other) = default;
~ExpandToEnclosingTextBoundaryTestParam() = default;
// The text boundary to expand to.
ax::mojom::TextBoundary boundary;
// Determines how to expand to the enclosing range when the starting position
// is already at a text boundary.
AXRangeExpandBehavior expand_behavior;
// The text position that should be returned for the anchor of the range.
std::string expected_anchor_position;
// The text position that should be returned for the focus of the range.
std::string expected_focus_position;
};
// This is a fixture for a set of parameterized tests that test the
// |ExpandToEnclosingTextBoundary| method with all possible input arguments.
class AXPositionExpandToEnclosingTextBoundaryTestWithParam
: public AXPositionTest,
public testing::WithParamInterface<
ExpandToEnclosingTextBoundaryTestParam> {
public:
AXPositionExpandToEnclosingTextBoundaryTestWithParam() = default;
~AXPositionExpandToEnclosingTextBoundaryTestWithParam() override = default;
DISALLOW_COPY_AND_ASSIGN(
AXPositionExpandToEnclosingTextBoundaryTestWithParam);
};
// Used by AXPositionCreatePositionAtTextBoundaryTestWithParam.
//
// Every test instance starts from a pre-determined position and calls the
// CreatePositionAtTextBoundary method with the arguments provided in this
// struct.
struct CreatePositionAtTextBoundaryTestParam {
CreatePositionAtTextBoundaryTestParam() = default;
// Required by GTest framework.
CreatePositionAtTextBoundaryTestParam(
const CreatePositionAtTextBoundaryTestParam& other) = default;
CreatePositionAtTextBoundaryTestParam& operator=(
const CreatePositionAtTextBoundaryTestParam& other) = default;
~CreatePositionAtTextBoundaryTestParam() = default;
// The text boundary to move to.
ax::mojom::TextBoundary boundary;
// The direction to move to.
ax::mojom::MoveDirection direction;
// What to do when the starting position is already at a text boundary, or
// when the movement operation will cause us to cross the starting object's
// boundary.
AXBoundaryBehavior boundary_behavior;
// The text position that should be returned, if the method was called on a
// text position instance.
std::string expected_text_position;
};
// This is a fixture for a set of parameterized tests that test the
// |CreatePositionAtTextBoundary| method with all possible input arguments.
class AXPositionCreatePositionAtTextBoundaryTestWithParam
: public AXPositionTest,
public testing::WithParamInterface<
CreatePositionAtTextBoundaryTestParam> {
public:
AXPositionCreatePositionAtTextBoundaryTestWithParam() = default;
~AXPositionCreatePositionAtTextBoundaryTestWithParam() override = default;
DISALLOW_COPY_AND_ASSIGN(AXPositionCreatePositionAtTextBoundaryTestWithParam);
};
// Used by |AXPositionTextNavigationTestWithParam|.
//
// The test starts from a pre-determined position and repeats a text navigation
// operation, such as |CreateNextWordStartPosition|, until it runs out of
// expectations.
struct TextNavigationTestParam {
TextNavigationTestParam() = default;
// Required by GTest framework.
TextNavigationTestParam(const TextNavigationTestParam& other) = default;
TextNavigationTestParam& operator=(const TextNavigationTestParam& other) =
default;
~TextNavigationTestParam() = default;
// Stores the method that should be called repeatedly by the test to create
// the next position.
base::RepeatingCallback<TestPositionType(const TestPositionType&)> TestMethod;
// The node at which the test should start.
AXNode::AXID start_node_id;
// The text offset at which the test should start.
int start_offset;
// A list of positions that should be returned from the method being tested,
// in stringified form.
std::vector<std::string> expectations;
};
// This is a fixture for a set of parameterized tests that ensure that text
// navigation operations, such as |CreateNextWordStartPosition|, work properly.
//
// Starting from a given position, test instances call a given text navigation
// method repeatedly and compare the return values to a set of expectations.
//
// TODO(nektar): Only text positions are tested for now.
class AXPositionTextNavigationTestWithParam
: public AXPositionTest,
public testing::WithParamInterface<TextNavigationTestParam> {
public:
AXPositionTextNavigationTestWithParam() = default;
~AXPositionTextNavigationTestWithParam() override = default;
DISALLOW_COPY_AND_ASSIGN(AXPositionTextNavigationTestWithParam);
};
const char* AXPositionTest::TEXT_VALUE = "Line 1\nLine 2";
void AXPositionTest::SetUp() {
// Most tests use kSuppressCharacter behavior.
g_ax_embedded_object_behavior = AXEmbeddedObjectBehavior::kSuppressCharacter;
// root_
// |
// +------------+-----------+
// | | |
// button_ check_box_ text_field_
// |
// +-----------+------------+
// | | |
// static_text1_ line_break_ static_text2_
// | |
// inline_box1_ inline_box2_
root_.id = ROOT_ID;
button_.id = BUTTON_ID;
check_box_.id = CHECK_BOX_ID;
text_field_.id = TEXT_FIELD_ID;
static_text1_.id = STATIC_TEXT1_ID;
inline_box1_.id = INLINE_BOX1_ID;
line_break_.id = LINE_BREAK_ID;
static_text2_.id = STATIC_TEXT2_ID;
inline_box2_.id = INLINE_BOX2_ID;
root_.role = ax::mojom::Role::kRootWebArea;
root_.AddBoolAttribute(ax::mojom::BoolAttribute::kIsLineBreakingObject, true);
button_.role = ax::mojom::Role::kButton;
button_.AddBoolAttribute(ax::mojom::BoolAttribute::kIsLineBreakingObject,
true);
button_.SetHasPopup(ax::mojom::HasPopup::kMenu);
button_.SetName("Button");
// Name is not visible in the tree's text representation, i.e. it may be
// coming from an aria-label.
button_.SetNameFrom(ax::mojom::NameFrom::kAttribute);
button_.relative_bounds.bounds = gfx::RectF(20, 20, 200, 30);
root_.child_ids.push_back(button_.id);
check_box_.role = ax::mojom::Role::kCheckBox;
check_box_.AddBoolAttribute(ax::mojom::BoolAttribute::kIsLineBreakingObject,
true);
check_box_.SetCheckedState(ax::mojom::CheckedState::kTrue);
check_box_.SetName("Check box");
// Name is not visible in the tree's text representation, i.e. it may be
// coming from an aria-label.
check_box_.SetNameFrom(ax::mojom::NameFrom::kAttribute);
check_box_.relative_bounds.bounds = gfx::RectF(20, 50, 200, 30);
root_.child_ids.push_back(check_box_.id);
text_field_.role = ax::mojom::Role::kTextField;
text_field_.AddBoolAttribute(ax::mojom::BoolAttribute::kIsLineBreakingObject,
true);
text_field_.AddState(ax::mojom::State::kEditable);
text_field_.SetValue(TEXT_VALUE);
text_field_.AddIntListAttribute(
ax::mojom::IntListAttribute::kCachedLineStarts,
std::vector<int32_t>{0, 7});
text_field_.child_ids.push_back(static_text1_.id);
text_field_.child_ids.push_back(line_break_.id);
text_field_.child_ids.push_back(static_text2_.id);
root_.child_ids.push_back(text_field_.id);
static_text1_.role = ax::mojom::Role::kStaticText;
static_text1_.AddState(ax::mojom::State::kEditable);
static_text1_.SetName("Line 1");
static_text1_.child_ids.push_back(inline_box1_.id);
static_text1_.AddIntAttribute(
ax::mojom::IntAttribute::kTextStyle,
static_cast<int32_t>(ax::mojom::TextStyle::kBold));
inline_box1_.role = ax::mojom::Role::kInlineTextBox;
inline_box1_.AddState(ax::mojom::State::kEditable);
inline_box1_.SetName("Line 1");
inline_box1_.AddIntListAttribute(ax::mojom::IntListAttribute::kWordStarts,
std::vector<int32_t>{0, 5});
inline_box1_.AddIntListAttribute(ax::mojom::IntListAttribute::kWordEnds,
std::vector<int32_t>{4, 6});
inline_box1_.AddIntAttribute(ax::mojom::IntAttribute::kNextOnLineId,
line_break_.id);
line_break_.role = ax::mojom::Role::kLineBreak;
line_break_.AddBoolAttribute(ax::mojom::BoolAttribute::kIsLineBreakingObject,
true);
line_break_.AddState(ax::mojom::State::kEditable);
line_break_.SetName("\n");
line_break_.AddIntAttribute(ax::mojom::IntAttribute::kPreviousOnLineId,
inline_box1_.id);
static_text2_.role = ax::mojom::Role::kStaticText;
static_text2_.AddState(ax::mojom::State::kEditable);
static_text2_.SetName("Line 2");
static_text2_.child_ids.push_back(inline_box2_.id);
static_text2_.AddFloatAttribute(ax::mojom::FloatAttribute::kFontSize, 1.0f);
inline_box2_.role = ax::mojom::Role::kInlineTextBox;
inline_box2_.AddState(ax::mojom::State::kEditable);
inline_box2_.SetName("Line 2");
inline_box2_.AddIntListAttribute(ax::mojom::IntListAttribute::kWordStarts,
std::vector<int32_t>{0, 5});
inline_box2_.AddIntListAttribute(ax::mojom::IntListAttribute::kWordEnds,
std::vector<int32_t>{4, 6});
AXTreeUpdate initial_state;
initial_state.root_id = 1;
initial_state.nodes = {root_, button_, check_box_,
text_field_, static_text1_, inline_box1_,
line_break_, static_text2_, inline_box2_};
initial_state.has_tree_data = true;
initial_state.tree_data.tree_id = AXTreeID::CreateNewAXTreeID();
initial_state.tree_data.title = "Dialog title";
// "SetTree" is defined in "TestAXTreeManager" and it passes ownership of the
// created AXTree to the manager.
SetTree(std::make_unique<AXTree>(initial_state));
}
std::unique_ptr<AXTree> AXPositionTest::CreateMultipageDocument(
AXNodeData& root_data,
AXNodeData& page_1_data,
AXNodeData& page_1_text_data,
AXNodeData& page_2_data,
AXNodeData& page_2_text_data,
AXNodeData& page_3_data,
AXNodeData& page_3_text_data) const {
root_data.id = 1;
root_data.role = ax::mojom::Role::kDocument;
page_1_data.id = 2;
page_1_data.role = ax::mojom::Role::kRegion;
page_1_data.AddBoolAttribute(ax::mojom::BoolAttribute::kIsPageBreakingObject,
true);
page_1_text_data.id = 3;
page_1_text_data.role = ax::mojom::Role::kStaticText;
page_1_text_data.SetName("some text on page 1");
page_1_text_data.AddBoolAttribute(
ax::mojom::BoolAttribute::kIsLineBreakingObject, true);
page_1_data.child_ids = {3};
page_2_data.id = 4;
page_2_data.role = ax::mojom::Role::kRegion;
page_2_data.AddBoolAttribute(ax::mojom::BoolAttribute::kIsPageBreakingObject,
true);
page_2_text_data.id = 5;
page_2_text_data.role = ax::mojom::Role::kStaticText;
page_2_text_data.SetName("some text on page 2");
page_2_text_data.AddIntAttribute(
ax::mojom::IntAttribute::kTextStyle,
static_cast<int32_t>(ax::mojom::TextStyle::kBold));
page_2_data.child_ids = {5};
page_3_data.id = 6;
page_3_data.role = ax::mojom::Role::kRegion;
page_3_data.AddBoolAttribute(ax::mojom::BoolAttribute::kIsPageBreakingObject,
true);
page_3_text_data.id = 7;
page_3_text_data.role = ax::mojom::Role::kStaticText;
page_3_text_data.SetName("some more text on page 3");
page_3_data.child_ids = {7};
root_data.child_ids = {2, 4, 6};
return CreateAXTree({root_data, page_1_data, page_1_text_data, page_2_data,
page_2_text_data, page_3_data, page_3_text_data});
}
std::unique_ptr<AXTree> AXPositionTest::CreateMultilingualDocument(
std::vector<int>* text_offsets) const {
EXPECT_NE(nullptr, text_offsets);
text_offsets->push_back(0);
base::string16 english_text;
for (int i = 0; i < 3; ++i) {
base::string16 grapheme = base::WideToUTF16(kGraphemeClusters[i]);
EXPECT_EQ(1u, grapheme.length())
<< "All English characters should be one UTF16 code unit in length.";
text_offsets->push_back(text_offsets->back() + int{grapheme.length()});
english_text.append(grapheme);
}
base::string16 hindi_text;
for (int i = 3; i < 5; ++i) {
base::string16 grapheme = base::WideToUTF16(kGraphemeClusters[i]);
EXPECT_LE(2u, grapheme.length()) << "All Hindi characters should be two "
"or more UTF16 code units in length.";
text_offsets->push_back(text_offsets->back() + int{grapheme.length()});
hindi_text.append(grapheme);
}
base::string16 thai_text;
for (int i = 5; i < 8; ++i) {
base::string16 grapheme = base::WideToUTF16(kGraphemeClusters[i]);
EXPECT_LT(0u, grapheme.length())
<< "One of the Thai characters should be one UTF16 code unit, "
"whilst others should be two or more.";
text_offsets->push_back(text_offsets->back() + int{grapheme.length()});
thai_text.append(grapheme);
}
AXNodeData root_data;
root_data.id = 1;
root_data.role = ax::mojom::Role::kRootWebArea;
AXNodeData text_data1;
text_data1.id = 2;
text_data1.role = ax::mojom::Role::kStaticText;
text_data1.SetName(english_text);
AXNodeData text_data2;
text_data2.id = 3;
text_data2.role = ax::mojom::Role::kStaticText;
text_data2.SetName(hindi_text);
AXNodeData text_data3;
text_data3.id = 4;
text_data3.role = ax::mojom::Role::kStaticText;
text_data3.SetName(thai_text);
root_data.child_ids = {text_data1.id, text_data2.id, text_data3.id};
return CreateAXTree({root_data, text_data1, text_data2, text_data3});
}
void AXPositionTest::AssertTextLengthEquals(const AXTree* tree,
AXNode::AXID node_id,
int expected_text_length) const {
TestPositionType text_position = AXNodePosition::CreateTextPosition(
tree->data().tree_id, node_id, 0 /* text_offset */,
ax::mojom::TextAffinity::kUpstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
ASSERT_EQ(expected_text_length, text_position->MaxTextOffset());
ASSERT_EQ(expected_text_length,
static_cast<int>(text_position->GetText().length()));
}
std::unique_ptr<AXTree> AXPositionTest::CreateAXTree(
const std::vector<AXNodeData>& nodes) const {
EXPECT_FALSE(nodes.empty());
AXTreeUpdate update;
update.tree_data.tree_id = AXTreeID::CreateNewAXTreeID();
update.has_tree_data = true;
update.root_id = nodes[0].id;
update.nodes = nodes;
return std::make_unique<AXTree>(update);
}
} // namespace
TEST_F(AXPositionTest, Clone) {
TestPositionType null_position = AXNodePosition::CreateNullPosition();
ASSERT_NE(nullptr, null_position);
TestPositionType copy_position = null_position->Clone();
ASSERT_NE(nullptr, copy_position);
EXPECT_TRUE(copy_position->IsNullPosition());
TestPositionType tree_position = AXNodePosition::CreateTreePosition(
GetTreeID(), root_.id, 1 /* child_index */);
ASSERT_NE(nullptr, tree_position);
copy_position = tree_position->Clone();
ASSERT_NE(nullptr, copy_position);
EXPECT_TRUE(copy_position->IsTreePosition());
EXPECT_EQ(root_.id, copy_position->anchor_id());
EXPECT_EQ(1, copy_position->child_index());
EXPECT_EQ(AXNodePosition::INVALID_OFFSET, copy_position->text_offset());
tree_position = AXNodePosition::CreateTreePosition(
GetTreeID(), root_.id, AXNodePosition::BEFORE_TEXT);
ASSERT_NE(nullptr, tree_position);
copy_position = tree_position->Clone();
ASSERT_NE(nullptr, copy_position);
EXPECT_TRUE(copy_position->IsTreePosition());
EXPECT_EQ(root_.id, copy_position->anchor_id());
EXPECT_EQ(AXNodePosition::BEFORE_TEXT, copy_position->child_index());
EXPECT_EQ(AXNodePosition::INVALID_OFFSET, copy_position->text_offset());
TestPositionType text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), text_field_.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kUpstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
copy_position = text_position->Clone();
ASSERT_NE(nullptr, copy_position);
EXPECT_TRUE(copy_position->IsTextPosition());
EXPECT_EQ(text_field_.id, copy_position->anchor_id());
EXPECT_EQ(0, copy_position->text_offset());
EXPECT_EQ(ax::mojom::TextAffinity::kUpstream, copy_position->affinity());
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), text_field_.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
copy_position = text_position->Clone();
ASSERT_NE(nullptr, copy_position);
EXPECT_TRUE(copy_position->IsTextPosition());
EXPECT_EQ(text_field_.id, copy_position->anchor_id());
EXPECT_EQ(0, copy_position->text_offset());
EXPECT_EQ(ax::mojom::TextAffinity::kDownstream, copy_position->affinity());
EXPECT_EQ(AXNodePosition::INVALID_INDEX, copy_position->child_index());
}
TEST_F(AXPositionTest, Serialize) {
TestPositionType null_position = AXNodePosition::CreateNullPosition();
ASSERT_NE(nullptr, null_position);
TestPositionType copy_position =
AXNodePosition::Unserialize(null_position->Serialize());
ASSERT_NE(nullptr, copy_position);
EXPECT_TRUE(copy_position->IsNullPosition());
TestPositionType tree_position = AXNodePosition::CreateTreePosition(
GetTreeID(), root_.id, 1 /* child_index */);
ASSERT_NE(nullptr, tree_position);
copy_position = AXNodePosition::Unserialize(tree_position->Serialize());
ASSERT_NE(nullptr, copy_position);
EXPECT_TRUE(copy_position->IsTreePosition());
EXPECT_EQ(root_.id, copy_position->anchor_id());
EXPECT_EQ(1, copy_position->child_index());
EXPECT_EQ(AXNodePosition::INVALID_OFFSET, copy_position->text_offset());
tree_position = AXNodePosition::CreateTreePosition(
GetTreeID(), root_.id, AXNodePosition::BEFORE_TEXT);
ASSERT_NE(nullptr, tree_position);
copy_position = AXNodePosition::Unserialize(tree_position->Serialize());
ASSERT_NE(nullptr, copy_position);
EXPECT_TRUE(copy_position->IsTreePosition());
EXPECT_EQ(root_.id, copy_position->anchor_id());
EXPECT_EQ(AXNodePosition::BEFORE_TEXT, copy_position->child_index());
EXPECT_EQ(AXNodePosition::INVALID_OFFSET, copy_position->text_offset());
TestPositionType text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), text_field_.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kUpstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
copy_position = AXNodePosition::Unserialize(text_position->Serialize());
ASSERT_NE(nullptr, copy_position);
EXPECT_TRUE(copy_position->IsTextPosition());
EXPECT_EQ(text_field_.id, copy_position->anchor_id());
EXPECT_EQ(0, copy_position->text_offset());
EXPECT_EQ(ax::mojom::TextAffinity::kUpstream, copy_position->affinity());
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), text_field_.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
copy_position = AXNodePosition::Unserialize(text_position->Serialize());
ASSERT_NE(nullptr, copy_position);
EXPECT_TRUE(copy_position->IsTextPosition());
EXPECT_EQ(text_field_.id, copy_position->anchor_id());
EXPECT_EQ(0, copy_position->text_offset());
EXPECT_EQ(ax::mojom::TextAffinity::kDownstream, copy_position->affinity());
EXPECT_EQ(AXNodePosition::INVALID_INDEX, copy_position->child_index());
}
TEST_F(AXPositionTest, ToString) {
AXNodeData root_data;
root_data.id = 1;
root_data.role = ax::mojom::Role::kRootWebArea;
AXNodeData static_text_data_1;
static_text_data_1.id = 2;
static_text_data_1.role = ax::mojom::Role::kStaticText;
static_text_data_1.SetName("some text");
AXNodeData static_text_data_2;
static_text_data_2.id = 3;
static_text_data_2.role = ax::mojom::Role::kStaticText;
static_text_data_2.SetName(base::WideToUTF16(L"\xfffc"));
AXNodeData static_text_data_3;
static_text_data_3.id = 4;
static_text_data_3.role = ax::mojom::Role::kStaticText;
static_text_data_3.SetName("more text");
root_data.child_ids = {static_text_data_1.id, static_text_data_2.id,
static_text_data_3.id};
SetTree(CreateAXTree(
{root_data, static_text_data_1, static_text_data_2, static_text_data_3}));
TestPositionType text_position_1 = AXNodePosition::CreateTextPosition(
GetTreeID(), root_data.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_TRUE(text_position_1->IsTextPosition());
EXPECT_EQ(
"TextPosition anchor_id=1 text_offset=0 affinity=downstream "
"annotated_text=<s>ome text\xEF\xBF\xBCmore text",
text_position_1->ToString());
TestPositionType text_position_2 = AXNodePosition::CreateTextPosition(
GetTreeID(), root_data.id, 5 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_TRUE(text_position_2->IsTextPosition());
EXPECT_EQ(
"TextPosition anchor_id=1 text_offset=5 affinity=downstream "
"annotated_text=some <t>ext\xEF\xBF\xBCmore text",
text_position_2->ToString());
TestPositionType text_position_3 = AXNodePosition::CreateTextPosition(
GetTreeID(), root_data.id, 9 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_TRUE(text_position_3->IsTextPosition());
EXPECT_EQ(
"TextPosition anchor_id=1 text_offset=9 affinity=downstream "
"annotated_text=some text<\xEF\xBF\xBC>more text",
text_position_3->ToString());
TestPositionType text_position_4 = AXNodePosition::CreateTextPosition(
GetTreeID(), root_data.id, 10 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_TRUE(text_position_4->IsTextPosition());
EXPECT_EQ(
"TextPosition anchor_id=1 text_offset=10 affinity=downstream "
"annotated_text=some text\xEF\xBF\xBC<m>ore text",
text_position_4->ToString());
TestPositionType text_position_5 = AXNodePosition::CreateTextPosition(
GetTreeID(), root_data.id, 19 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_TRUE(text_position_5->IsTextPosition());
EXPECT_EQ(
"TextPosition anchor_id=1 text_offset=19 affinity=downstream "
"annotated_text=some text\xEF\xBF\xBCmore text<>",
text_position_5->ToString());
TestPositionType text_position_6 = AXNodePosition::CreateTextPosition(
GetTreeID(), static_text_data_2.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_TRUE(text_position_6->IsTextPosition());
EXPECT_EQ(
"TextPosition anchor_id=3 text_offset=0 affinity=downstream "
"annotated_text=<\xEF\xBF\xBC>",
text_position_6->ToString());
TestPositionType text_position_7 = AXNodePosition::CreateTextPosition(
GetTreeID(), static_text_data_2.id, 1 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_TRUE(text_position_7->IsTextPosition());
EXPECT_EQ(
"TextPosition anchor_id=3 text_offset=1 affinity=downstream "
"annotated_text=\xEF\xBF\xBC<>",
text_position_7->ToString());
TestPositionType text_position_8 = AXNodePosition::CreateTextPosition(
GetTreeID(), static_text_data_3.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_TRUE(text_position_8->IsTextPosition());
EXPECT_EQ(
"TextPosition anchor_id=4 text_offset=0 affinity=downstream "
"annotated_text=<m>ore text",
text_position_8->ToString());
TestPositionType text_position_9 = AXNodePosition::CreateTextPosition(
GetTreeID(), static_text_data_3.id, 5 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_TRUE(text_position_9->IsTextPosition());
EXPECT_EQ(
"TextPosition anchor_id=4 text_offset=5 affinity=downstream "
"annotated_text=more <t>ext",
text_position_9->ToString());
TestPositionType text_position_10 = AXNodePosition::CreateTextPosition(
GetTreeID(), static_text_data_3.id, 9 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_TRUE(text_position_10->IsTextPosition());
EXPECT_EQ(
"TextPosition anchor_id=4 text_offset=9 affinity=downstream "
"annotated_text=more text<>",
text_position_10->ToString());
}
TEST_F(AXPositionTest, IsIgnored) {
EXPECT_FALSE(AXNodePosition::CreateNullPosition()->IsIgnored());
// We now need to update the tree structure to test ignored tree and text
// positions.
AXNodeData root_data;
root_data.id = 1;
root_data.role = ax::mojom::Role::kRootWebArea;
AXNodeData static_text_data_1;
static_text_data_1.id = 2;
static_text_data_1.role = ax::mojom::Role::kStaticText;
static_text_data_1.SetName("One");
AXNodeData inline_box_data_1;
inline_box_data_1.id = 3;
inline_box_data_1.role = ax::mojom::Role::kInlineTextBox;
inline_box_data_1.SetName("One");
inline_box_data_1.AddState(ax::mojom::State::kIgnored);
AXNodeData container_data;
container_data.id = 4;
container_data.role = ax::mojom::Role::kGenericContainer;
container_data.AddState(ax::mojom::State::kIgnored);
AXNodeData static_text_data_2;
static_text_data_2.id = 5;
static_text_data_2.role = ax::mojom::Role::kStaticText;
static_text_data_2.SetName("Two");
AXNodeData inline_box_data_2;
inline_box_data_2.id = 6;
inline_box_data_2.role = ax::mojom::Role::kInlineTextBox;
inline_box_data_2.SetName("Two");
static_text_data_1.child_ids = {inline_box_data_1.id};
container_data.child_ids = {static_text_data_2.id};
static_text_data_2.child_ids = {inline_box_data_2.id};
root_data.child_ids = {static_text_data_1.id, container_data.id};
SetTree(
CreateAXTree({root_data, static_text_data_1, inline_box_data_1,
container_data, static_text_data_2, inline_box_data_2}));
//
// Text positions.
//
TestPositionType text_position_1 = AXNodePosition::CreateTextPosition(
GetTreeID(), root_data.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_TRUE(text_position_1->IsTextPosition());
// Since the leaf node containing the text that is pointed to is ignored, this
// position should be ignored.
EXPECT_TRUE(text_position_1->IsIgnored());
// Create a text position before the letter "e" in "One".
TestPositionType text_position_2 = AXNodePosition::CreateTextPosition(
GetTreeID(), root_data.id, 2 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_TRUE(text_position_2->IsTextPosition());
// Same as above.
EXPECT_TRUE(text_position_2->IsIgnored());
// Create a text position before the letter "T" in "Two".
TestPositionType text_position_3 = AXNodePosition::CreateTextPosition(
GetTreeID(), root_data.id, 3 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_TRUE(text_position_3->IsTextPosition());
// Since the leaf node containing the text that is pointed to is not ignored,
// but only a generic container that is in between this position and the leaf
// node, this position should not be ignored.
EXPECT_FALSE(text_position_3->IsIgnored());
// Create a text position before the letter "w" in "Two".
TestPositionType text_position_4 = AXNodePosition::CreateTextPosition(
GetTreeID(), root_data.id, 4 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_TRUE(text_position_4->IsTextPosition());
// Same as above.
EXPECT_FALSE(text_position_4->IsIgnored());
// But a text position on the ignored generic container itself, should be
// ignored.
TestPositionType text_position_5 = AXNodePosition::CreateTextPosition(
GetTreeID(), container_data.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_TRUE(text_position_5->IsTextPosition());
EXPECT_TRUE(text_position_5->IsIgnored());
// Whilst a text position on its static text child should not be ignored since
// there is nothing ignore below the generic container.
TestPositionType text_position_6 = AXNodePosition::CreateTextPosition(
GetTreeID(), static_text_data_2.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_TRUE(text_position_6->IsTextPosition());
EXPECT_FALSE(text_position_6->IsIgnored());
// A text position on an ignored leaf node should be ignored.
TestPositionType text_position_7 = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_box_data_1.id, 1 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_TRUE(text_position_7->IsTextPosition());
EXPECT_TRUE(text_position_7->IsIgnored());
//
// Tree positions.
//
// A "before children" position on the root should not be ignored, despite the
// fact that the leaf equivalent position is, because we can always adjust to
// an unignored position if asked to find the leaf equivalent unignored
// position.
TestPositionType tree_position_1 = AXNodePosition::CreateTreePosition(
GetTreeID(), root_data.id, 0 /* child_index */);
ASSERT_TRUE(tree_position_1->IsTreePosition());
EXPECT_FALSE(tree_position_1->IsIgnored());
// A tree position pointing to an ignored child node should be ignored.
TestPositionType tree_position_2 = AXNodePosition::CreateTreePosition(
GetTreeID(), root_data.id, 1 /* child_index */);
ASSERT_TRUE(tree_position_2->IsTreePosition());
EXPECT_TRUE(tree_position_2->IsIgnored());
// An "after text" tree position on an ignored leaf node should be ignored.
TestPositionType tree_position_3 = AXNodePosition::CreateTreePosition(
GetTreeID(), inline_box_data_1.id, 0 /* child_index */);
ASSERT_TRUE(tree_position_3->IsTreePosition());
EXPECT_TRUE(tree_position_3->IsIgnored());
// A "before text" tree position on an ignored leaf node should be ignored.
TestPositionType tree_position_4 = AXNodePosition::CreateTreePosition(
GetTreeID(), inline_box_data_1.id, AXNodePosition::BEFORE_TEXT);
ASSERT_TRUE(tree_position_4->IsTreePosition());
EXPECT_TRUE(tree_position_4->IsIgnored());
// An "after children" tree position on the root node, where the last child is
// ignored, should be ignored.
TestPositionType tree_position_5 = AXNodePosition::CreateTreePosition(
GetTreeID(), root_data.id, 2 /* child_index */);
ASSERT_TRUE(tree_position_5->IsTreePosition());
EXPECT_TRUE(tree_position_5->IsIgnored());
// A "before text" position on an unignored node should not be ignored.
TestPositionType tree_position_6 = AXNodePosition::CreateTreePosition(
GetTreeID(), static_text_data_1.id, AXNodePosition::BEFORE_TEXT);
ASSERT_TRUE(tree_position_6->IsTreePosition());
EXPECT_FALSE(tree_position_6->IsIgnored());
}
TEST_F(AXPositionTest, GetTextFromNullPosition) {
TestPositionType text_position = AXNodePosition::CreateNullPosition();
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsNullPosition());
ASSERT_EQ(base::WideToUTF16(L""), text_position->GetText());
}
TEST_F(AXPositionTest, GetTextFromRoot) {
TestPositionType text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), root_.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kUpstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
ASSERT_EQ(base::WideToUTF16(L"Line 1\nLine 2"), text_position->GetText());
}
TEST_F(AXPositionTest, GetTextFromButton) {
TestPositionType text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), button_.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kUpstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
ASSERT_EQ(base::WideToUTF16(L""), text_position->GetText());
}
TEST_F(AXPositionTest, GetTextFromCheckbox) {
TestPositionType text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), check_box_.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kUpstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
ASSERT_EQ(base::WideToUTF16(L""), text_position->GetText());
}
TEST_F(AXPositionTest, GetTextFromTextField) {
TestPositionType text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), text_field_.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kUpstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
ASSERT_EQ(base::WideToUTF16(L"Line 1\nLine 2"), text_position->GetText());
}
TEST_F(AXPositionTest, GetTextFromStaticText) {
TestPositionType text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), static_text1_.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kUpstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
ASSERT_EQ(base::WideToUTF16(L"Line 1"), text_position->GetText());
}
TEST_F(AXPositionTest, GetTextFromInlineTextBox) {
TestPositionType text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_box1_.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kUpstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
ASSERT_EQ(base::WideToUTF16(L"Line 1"), text_position->GetText());
}
TEST_F(AXPositionTest, GetTextFromLineBreak) {
TestPositionType text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), line_break_.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kUpstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
ASSERT_EQ(base::WideToUTF16(L"\n"), text_position->GetText());
}
TEST_F(AXPositionTest, GetMaxTextOffsetFromNullPosition) {
TestPositionType text_position = AXNodePosition::CreateNullPosition();
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsNullPosition());
ASSERT_EQ(AXNodePosition::INVALID_OFFSET, text_position->MaxTextOffset());
}
TEST_F(AXPositionTest, GetMaxTextOffsetFromRoot) {
TestPositionType text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), root_.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kUpstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
ASSERT_EQ(13, text_position->MaxTextOffset());
}
TEST_F(AXPositionTest, GetMaxTextOffsetFromButton) {
TestPositionType text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), button_.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kUpstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
ASSERT_EQ(0, text_position->MaxTextOffset());
}
TEST_F(AXPositionTest, GetMaxTextOffsetFromCheckbox) {
TestPositionType text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), check_box_.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kUpstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
ASSERT_EQ(0, text_position->MaxTextOffset());
}
TEST_F(AXPositionTest, GetMaxTextOffsetFromTextfield) {
TestPositionType text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), text_field_.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kUpstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
ASSERT_EQ(13, text_position->MaxTextOffset());
}
TEST_F(AXPositionTest, GetMaxTextOffsetFromStaticText) {
TestPositionType text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), static_text1_.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kUpstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
ASSERT_EQ(6, text_position->MaxTextOffset());
}
TEST_F(AXPositionTest, GetMaxTextOffsetFromInlineTextBox) {
TestPositionType text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_box1_.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kUpstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
ASSERT_EQ(6, text_position->MaxTextOffset());
}
TEST_F(AXPositionTest, GetMaxTextOffsetFromLineBreak) {
TestPositionType text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), line_break_.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kUpstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
ASSERT_EQ(1, text_position->MaxTextOffset());
}
TEST_F(AXPositionTest, GetMaxTextOffsetUpdate) {
AXNodeData root_data;
root_data.id = 1;
root_data.role = ax::mojom::Role::kRootWebArea;
AXNodeData text_field_data;
text_field_data.id = 2;
text_field_data.role = ax::mojom::Role::kTextField;
text_field_data.SetName("some text");
text_field_data.SetNameFrom(ax::mojom::NameFrom::kPlaceholder);
AXNodeData text_data;
text_data.id = 3;
text_data.role = ax::mojom::Role::kStaticText;
text_data.SetName("more text");
text_data.SetNameFrom(ax::mojom::NameFrom::kContents);
root_data.child_ids = {text_field_data.id, text_data.id};
SetTree(CreateAXTree({root_data, text_field_data, text_data}));
AssertTextLengthEquals(GetTree(), text_field_data.id, 9);
AssertTextLengthEquals(GetTree(), text_data.id, 9);
AssertTextLengthEquals(GetTree(), root_data.id, 18);
// Update the placeholder text.
text_field_data.SetName("Adjusted line 1");
SetTree(CreateAXTree({root_data, text_field_data, text_data}));
AssertTextLengthEquals(GetTree(), text_field_data.id, 15);
AssertTextLengthEquals(GetTree(), text_data.id, 9);
AssertTextLengthEquals(GetTree(), root_data.id, 24);
// Value should override name in text fields.
text_field_data.SetValue("Value should override name");
SetTree(CreateAXTree({root_data, text_field_data, text_data}));
AssertTextLengthEquals(GetTree(), text_field_data.id, 26);
AssertTextLengthEquals(GetTree(), text_data.id, 9);
AssertTextLengthEquals(GetTree(), root_data.id, 35);
// An empty value should fall back to placeholder text.
text_field_data.SetValue("");
SetTree(CreateAXTree({root_data, text_field_data, text_data}));
AssertTextLengthEquals(GetTree(), text_field_data.id, 15);
AssertTextLengthEquals(GetTree(), text_data.id, 9);
AssertTextLengthEquals(GetTree(), root_data.id, 24);
}
TEST_F(AXPositionTest, GetMaxTextOffsetAndGetTextWithGeneratedContent) {
// ++1 kRootWebArea
// ++++2 kTextField
// ++++++3 kStaticText
// ++++++++4 kInlineTextBox
// ++++++5 kStaticText
// ++++++++6 kInlineTextBox
AXNodeData root_1;
AXNodeData text_field_2;
AXNodeData static_text_3;
AXNodeData inline_box_4;
AXNodeData static_text_5;
AXNodeData inline_box_6;
root_1.id = 1;
text_field_2.id = 2;
static_text_3.id = 3;
inline_box_4.id = 4;
static_text_5.id = 5;
inline_box_6.id = 6;
root_1.role = ax::mojom::Role::kRootWebArea;
root_1.child_ids = {text_field_2.id};
text_field_2.role = ax::mojom::Role::kTextField;
text_field_2.SetValue("3.14");
text_field_2.child_ids = {static_text_3.id, static_text_5.id};
static_text_3.role = ax::mojom::Role::kStaticText;
static_text_3.SetName("Placeholder from generated content");
static_text_3.child_ids = {inline_box_4.id};
inline_box_4.role = ax::mojom::Role::kInlineTextBox;
inline_box_4.SetName("Placeholder from generated content");
static_text_5.role = ax::mojom::Role::kStaticText;
static_text_5.SetName("3.14");
static_text_5.child_ids = {inline_box_6.id};
inline_box_6.role = ax::mojom::Role::kInlineTextBox;
inline_box_6.SetName("3.14");
SetTree(CreateAXTree({root_1, text_field_2, static_text_3, inline_box_4,
static_text_5, inline_box_6}));
TestPositionType text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), text_field_2.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
EXPECT_TRUE(text_position->IsTextPosition());
EXPECT_EQ(38, text_position->MaxTextOffset());
EXPECT_EQ(base::WideToUTF16(L"Placeholder from generated content3.14"),
text_position->GetText());
}
TEST_F(AXPositionTest, AtStartOfAnchorWithNullPosition) {
TestPositionType null_position = AXNodePosition::CreateNullPosition();
ASSERT_NE(nullptr, null_position);
EXPECT_FALSE(null_position->AtStartOfAnchor());
}
TEST_F(AXPositionTest, AtStartOfAnchorWithTreePosition) {
TestPositionType tree_position = AXNodePosition::CreateTreePosition(
GetTreeID(), root_.id, 0 /* child_index */);
ASSERT_NE(nullptr, tree_position);
EXPECT_TRUE(tree_position->AtStartOfAnchor());
tree_position = AXNodePosition::CreateTreePosition(GetTreeID(), root_.id,
1 /* child_index */);
ASSERT_NE(nullptr, tree_position);
EXPECT_FALSE(tree_position->AtStartOfAnchor());
tree_position = AXNodePosition::CreateTreePosition(GetTreeID(), root_.id,
3 /* child_index */);
ASSERT_NE(nullptr, tree_position);
EXPECT_FALSE(tree_position->AtStartOfAnchor());
// A "before text" position.
tree_position = AXNodePosition::CreateTreePosition(
GetTreeID(), inline_box1_.id, AXNodePosition::BEFORE_TEXT);
ASSERT_NE(nullptr, tree_position);
EXPECT_TRUE(tree_position->AtStartOfAnchor());
// An "after text" position.
tree_position = AXNodePosition::CreateTreePosition(
GetTreeID(), inline_box1_.id, 0 /* child_index */);
ASSERT_NE(nullptr, tree_position);
EXPECT_FALSE(tree_position->AtStartOfAnchor());
}
TEST_F(AXPositionTest, AtStartOfAnchorWithTextPosition) {
TestPositionType text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_box1_.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kUpstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
EXPECT_TRUE(text_position->AtStartOfAnchor());
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_box1_.id, 1 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
EXPECT_FALSE(text_position->AtStartOfAnchor());
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_box1_.id, 6 /* text_offset */,
ax::mojom::TextAffinity::kUpstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
EXPECT_FALSE(text_position->AtStartOfAnchor());
}
TEST_F(AXPositionTest, AtEndOfAnchorWithNullPosition) {
TestPositionType null_position = AXNodePosition::CreateNullPosition();
ASSERT_NE(nullptr, null_position);
EXPECT_FALSE(null_position->AtEndOfAnchor());
}
TEST_F(AXPositionTest, AtEndOfAnchorWithTreePosition) {
TestPositionType tree_position = AXNodePosition::CreateTreePosition(
GetTreeID(), root_.id, 3 /* child_index */);
ASSERT_NE(nullptr, tree_position);
EXPECT_TRUE(tree_position->AtEndOfAnchor());
tree_position = AXNodePosition::CreateTreePosition(GetTreeID(), root_.id,
2 /* child_index */);
ASSERT_NE(nullptr, tree_position);
EXPECT_FALSE(tree_position->AtEndOfAnchor());
tree_position = AXNodePosition::CreateTreePosition(GetTreeID(), root_.id,
0 /* child_index */);
ASSERT_NE(nullptr, tree_position);
EXPECT_FALSE(tree_position->AtEndOfAnchor());
}
TEST_F(AXPositionTest, AtEndOfAnchorWithTextPosition) {
TestPositionType text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_box1_.id, 6 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
EXPECT_TRUE(text_position->AtEndOfAnchor());
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_box1_.id, 5 /* text_offset */,
ax::mojom::TextAffinity::kUpstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
EXPECT_FALSE(text_position->AtEndOfAnchor());
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_box1_.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
EXPECT_FALSE(text_position->AtEndOfAnchor());
}
TEST_F(AXPositionTest, AtStartOfLineWithTextPosition) {
// An upstream affinity should not affect the outcome since there is no soft
// line break.
TestPositionType text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_box1_.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kUpstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
EXPECT_TRUE(text_position->AtStartOfLine());
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_box1_.id, 1 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
EXPECT_FALSE(text_position->AtStartOfLine());
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), line_break_.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
EXPECT_FALSE(text_position->AtStartOfLine());
// An "after text" position anchored at the line break should be equivalent to
// a "before text" position at the start of the next line.
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), line_break_.id, 1 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
EXPECT_TRUE(text_position->AtStartOfLine());
// An upstream affinity should not affect the outcome since there is no soft
// line break.
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_box2_.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kUpstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
EXPECT_TRUE(text_position->AtStartOfLine());
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_box2_.id, 1 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
EXPECT_FALSE(text_position->AtStartOfLine());
}
TEST_F(AXPositionTest, AtStartOfLineStaticTextExtraPrecedingSpace) {
// Consider the following web content:
// <style>
// .required-label::after {
// content: " *";
// }
// </style>
// <label class="required-label">Required </label>
//
// Which has the following AXTree, where the static text (#3)
// contains an extra preceding space compared to its inline text (#4).
// ++1 kRootWebArea
// ++++2 kLabelText
// ++++++3 kStaticText name=" *"
// ++++++++4 kInlineTextBox name="*"
// This test ensures that this difference between static text and its inline
// text box does not cause a hang when AtStartOfLine is called on static text
// with text position " <*>".
AXNodeData root;
root.id = 1;
root.role = ax::mojom::Role::kRootWebArea;
// "kIsLineBreakingObject" is not strictly necessary but is added for
// completeness.
root.AddBoolAttribute(ax::mojom::BoolAttribute::kIsLineBreakingObject, true);
AXNodeData label_text;
label_text.id = 2;
label_text.role = ax::mojom::Role::kLabelText;
AXNodeData static_text1;
static_text1.id = 3;
static_text1.role = ax::mojom::Role::kStaticText;
static_text1.SetName(" *");
AXNodeData inline_text1;
inline_text1.id = 4;
inline_text1.role = ax::mojom::Role::kInlineTextBox;
inline_text1.SetName("*");
static_text1.child_ids = {inline_text1.id};
root.child_ids = {static_text1.id};
SetTree(CreateAXTree({root, static_text1, inline_text1}));
// Calling AtStartOfLine on |static_text1| with position " <*>",
// text_offset_=1, should not get into an infinite loop; it should be
// guaranteed to terminate.
TestPositionType text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), static_text1.id, 1 /* child_index */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_FALSE(text_position->AtStartOfLine());
}
TEST_F(AXPositionTest, AtEndOfLineWithTextPosition) {
TestPositionType text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_box1_.id, 5 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
EXPECT_FALSE(text_position->AtEndOfLine());
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_box1_.id, 6 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
EXPECT_TRUE(text_position->AtEndOfLine());
// A "before text" position anchored at the line break should visually be the
// same as a text position at the end of the previous line.
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), line_break_.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
EXPECT_TRUE(text_position->AtEndOfLine());
// The following position comes after the soft line break, so it should not be
// marked as the end of the line.
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), line_break_.id, 1 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
EXPECT_FALSE(text_position->AtEndOfLine());
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_box2_.id, 5 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
EXPECT_FALSE(text_position->AtEndOfLine());
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_box2_.id, 6 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
EXPECT_TRUE(text_position->AtEndOfLine());
}
TEST_F(AXPositionTest, AtStartOfBlankLine) {
// Modify the test tree so that the line break will appear on a line of its
// own, i.e. as creating a blank line.
inline_box1_.RemoveIntAttribute(ax::mojom::IntAttribute::kNextOnLineId);
line_break_.RemoveIntAttribute(ax::mojom::IntAttribute::kPreviousOnLineId);
AXTreeUpdate update;
update.nodes = {inline_box1_, line_break_};
ASSERT_TRUE(GetTree()->Unserialize(update));
TestPositionType tree_position = AXNodePosition::CreateTreePosition(
GetTreeID(), text_field_.id, 1 /* child_index */);
ASSERT_NE(nullptr, tree_position);
ASSERT_TRUE(tree_position->IsTreePosition());
EXPECT_TRUE(tree_position->AtStartOfLine());
TestPositionType text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), line_break_.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
EXPECT_TRUE(text_position->AtStartOfLine());
// A text position after a blank line should be equivalent to a "before text"
// position at the line that comes after it.
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), line_break_.id, 1 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
EXPECT_TRUE(text_position->AtStartOfLine());
}
TEST_F(AXPositionTest, AtEndOfBlankLine) {
// Modify the test tree so that the line break will appear on a line of its
// own, i.e. as creating a blank line.
inline_box1_.RemoveIntAttribute(ax::mojom::IntAttribute::kNextOnLineId);
line_break_.RemoveIntAttribute(ax::mojom::IntAttribute::kPreviousOnLineId);
AXTreeUpdate update;
update.nodes = {inline_box1_, line_break_};
ASSERT_TRUE(GetTree()->Unserialize(update));
TestPositionType tree_position = AXNodePosition::CreateTreePosition(
GetTreeID(), text_field_.id, 1 /* child_index */);
ASSERT_NE(nullptr, tree_position);
ASSERT_TRUE(tree_position->IsTreePosition());
EXPECT_FALSE(tree_position->AtEndOfLine());
TestPositionType text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), line_break_.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
EXPECT_FALSE(text_position->AtEndOfLine());
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), line_break_.id, 1 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
EXPECT_TRUE(text_position->AtEndOfLine());
}
TEST_F(AXPositionTest, AtStartAndEndOfLineWhenAtEndOfTextSpan) {
// This test ensures that the "AtStartOfLine" and the "AtEndOfLine" methods
// return false and true respectively when we are at the end of a text span.
//
// A text span is defined by a series of inline text boxes that make up a
// single static text object. Lines always end at the end of static text
// objects, so there would never arise a situation when a position at the end
// of a text span would be at start of line. It should always be at end of
// line. On the contrary, if a position is at the end of an inline text box
// and the equivalent parent position is in the middle of a static text
// object, then the position would sometimes be at start of line, i.e., when
// the inline text box contains only white space that is used to separate
// lines in the case of lines being wrapped by a soft line break.
//
// Example accessibility tree:
// 0:kRootWebArea
// ++1:kStaticText "Hello testing "
// ++++2:kInlineTextBox "Hello" kNextOnLine=2
// ++++3:kInlineTextBox " " kPreviousOnLine=2
// ++++4:kInlineTextBox "testing" kNextOnLine=5
// ++++5:kInlineTextBox " " kPreviousOnLine=4
// ++6:kStaticText "here."
// ++++7:kInlineTextBox "here."
//
// Resulting text representation:
// "Hello<soft_line_break>testing <hard_line_break>here."
// Notice the extra space after the word "testing". This is not a line break.
// The hard line break is caused by the presence of the second static text
// object.
//
// A position at the end of inline text box 3 should be at start of line,
// whilst a position at the end of inline text box 5 should not.
AXNodeData root_data;
root_data.id = 1;
root_data.role = ax::mojom::Role::kRootWebArea;
// "kIsLineBreakingObject" is not strictly necessary but is added for
// completeness.
root_data.AddBoolAttribute(ax::mojom::BoolAttribute::kIsLineBreakingObject,
true);
AXNodeData static_text_data_1;
static_text_data_1.id = 2;
static_text_data_1.role = ax::mojom::Role::kStaticText;
static_text_data_1.SetName("Hello testing ");
AXNodeData inline_box_data_1;
inline_box_data_1.id = 3;
inline_box_data_1.role = ax::mojom::Role::kInlineTextBox;
inline_box_data_1.SetName("hello");
AXNodeData inline_box_data_2;
inline_box_data_2.id = 4;
inline_box_data_2.role = ax::mojom::Role::kInlineTextBox;
inline_box_data_1.AddIntAttribute(ax::mojom::IntAttribute::kNextOnLineId,
inline_box_data_2.id);
inline_box_data_2.AddIntAttribute(ax::mojom::IntAttribute::kPreviousOnLineId,
inline_box_data_1.id);
// The name is a space character that we assume it turns into a soft line
// break by the layout engine.
inline_box_data_2.SetName(" ");
AXNodeData inline_box_data_3;
inline_box_data_3.id = 5;
inline_box_data_3.role = ax::mojom::Role::kInlineTextBox;
inline_box_data_3.SetName("testing");
AXNodeData inline_box_data_4;
inline_box_data_4.id = 6;
inline_box_data_4.role = ax::mojom::Role::kInlineTextBox;
inline_box_data_3.AddIntAttribute(ax::mojom::IntAttribute::kNextOnLineId,
inline_box_data_4.id);
inline_box_data_4.AddIntAttribute(ax::mojom::IntAttribute::kPreviousOnLineId,
inline_box_data_3.id);
inline_box_data_4.SetName(" "); // Just a space character - not a line break.
AXNodeData static_text_data_2;
static_text_data_2.id = 7;
static_text_data_2.role = ax::mojom::Role::kStaticText;
static_text_data_2.SetName("here.");
AXNodeData inline_box_data_5;
inline_box_data_5.id = 8;
inline_box_data_5.role = ax::mojom::Role::kInlineTextBox;
inline_box_data_5.SetName("here.");
static_text_data_1.child_ids = {inline_box_data_1.id, inline_box_data_2.id,
inline_box_data_3.id, inline_box_data_4.id};
static_text_data_2.child_ids = {inline_box_data_5.id};
root_data.child_ids = {static_text_data_1.id, static_text_data_2.id};
SetTree(CreateAXTree({root_data, static_text_data_1, inline_box_data_1,
inline_box_data_2, inline_box_data_3, inline_box_data_4,
static_text_data_2, inline_box_data_5}));
// An "after text" tree position - after the soft line break.
TestPositionType tree_position = AXNodePosition::CreateTreePosition(
GetTreeID(), inline_box_data_2.id, 0 /* child_index */);
ASSERT_NE(nullptr, tree_position);
ASSERT_TRUE(tree_position->IsTreePosition());
EXPECT_TRUE(tree_position->AtStartOfLine());
EXPECT_FALSE(tree_position->AtEndOfLine());
// An "after text" tree position - after the space character and before the
// hard line break caused by the second static text object.
tree_position = AXNodePosition::CreateTreePosition(
GetTreeID(), inline_box_data_4.id, 0 /* child_index */);
ASSERT_NE(nullptr, tree_position);
ASSERT_TRUE(tree_position->IsTreePosition());
EXPECT_FALSE(tree_position->AtStartOfLine());
EXPECT_TRUE(tree_position->AtEndOfLine());
TestPositionType text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_box_data_2.id, 1 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
EXPECT_TRUE(text_position->AtStartOfLine());
EXPECT_FALSE(text_position->AtEndOfLine());
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_box_data_4.id, 1 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
EXPECT_FALSE(text_position->AtStartOfLine());
EXPECT_TRUE(text_position->AtEndOfLine());
}
TEST_F(AXPositionTest, AtStartAndEndOfLineInsideTextField) {
// This test ensures that "AtStart/EndOfLine" methods work properly when at
// the start or end of a text field.
//
// We setup a test tree with two text fields. The first one has one line of
// text, and the second one three. There are inline text boxes containing only
// white space at the start and end of both text fields, which is a valid
// AXTree that might be generated by our renderer.
AXNodeData root_data;
root_data.id = 1;
root_data.role = ax::mojom::Role::kRootWebArea;
// "kIsLineBreakingObject" is not strictly necessary but is added for
// completeness.
root_data.AddBoolAttribute(ax::mojom::BoolAttribute::kIsLineBreakingObject,
true);
AXNodeData text_field_data_1;
text_field_data_1.id = 2;
text_field_data_1.role = ax::mojom::Role::kTextField;
// "kIsLineBreakingObject" and the "kEditable" state are not strictly
// necessary but are added for completeness.
text_field_data_1.AddBoolAttribute(
ax::mojom::BoolAttribute::kIsLineBreakingObject, true);
text_field_data_1.AddState(ax::mojom::State::kEditable);
// Notice that there is one space at the start and one at the end of the text
// field's value.
text_field_data_1.SetValue(" Text field one ");
AXNodeData static_text_data_1;
static_text_data_1.id = 3;
static_text_data_1.role = ax::mojom::Role::kStaticText;
static_text_data_1.SetName(" Text field one ");
AXNodeData inline_box_data_1;
inline_box_data_1.id = 4;
inline_box_data_1.role = ax::mojom::Role::kInlineTextBox;
inline_box_data_1.SetName(" ");
AXNodeData inline_box_data_2;
inline_box_data_2.id = 5;
inline_box_data_2.role = ax::mojom::Role::kInlineTextBox;
inline_box_data_1.AddIntAttribute(ax::mojom::IntAttribute::kNextOnLineId,
inline_box_data_2.id);
inline_box_data_2.AddIntAttribute(ax::mojom::IntAttribute::kPreviousOnLineId,
inline_box_data_1.id);
inline_box_data_2.SetName("Text field one");
AXNodeData inline_box_data_3;
inline_box_data_3.id = 6;
inline_box_data_3.role = ax::mojom::Role::kInlineTextBox;
inline_box_data_2.AddIntAttribute(ax::mojom::IntAttribute::kNextOnLineId,
inline_box_data_3.id);
inline_box_data_3.AddIntAttribute(ax::mojom::IntAttribute::kPreviousOnLineId,
inline_box_data_2.id);
inline_box_data_3.SetName(" ");
AXNodeData text_field_data_2;
text_field_data_2.id = 7;
text_field_data_2.role = ax::mojom::Role::kTextField;
// "kIsLineBreakingObject" and the "kEditable" state are not strictly
// necessary but are added for completeness.
text_field_data_2.AddBoolAttribute(
ax::mojom::BoolAttribute::kIsLineBreakingObject, true);
text_field_data_1.AddState(ax::mojom::State::kEditable);
// Notice that there are three lines, the first and the last one include only
// a single space.
text_field_data_2.SetValue(" Text field two ");
AXNodeData static_text_data_2;
static_text_data_2.id = 8;
static_text_data_2.role = ax::mojom::Role::kStaticText;
static_text_data_2.SetName(" Text field two ");
AXNodeData inline_box_data_4;
inline_box_data_4.id = 9;
inline_box_data_4.role = ax::mojom::Role::kInlineTextBox;
inline_box_data_4.SetName(" ");
AXNodeData inline_box_data_5;
inline_box_data_5.id = 10;
inline_box_data_5.role = ax::mojom::Role::kInlineTextBox;
inline_box_data_5.SetName("Text field two");
AXNodeData inline_box_data_6;
inline_box_data_6.id = 11;
inline_box_data_6.role = ax::mojom::Role::kInlineTextBox;
inline_box_data_6.SetName(" ");
static_text_data_1.child_ids = {inline_box_data_1.id, inline_box_data_2.id,
inline_box_data_3.id};
static_text_data_2.child_ids = {inline_box_data_4.id, inline_box_data_5.id,
inline_box_data_6.id};
text_field_data_1.child_ids = {static_text_data_1.id};
text_field_data_2.child_ids = {static_text_data_2.id};
root_data.child_ids = {text_field_data_1.id, text_field_data_2.id};
SetTree(
CreateAXTree({root_data, text_field_data_1, static_text_data_1,
inline_box_data_1, inline_box_data_2, inline_box_data_3,
text_field_data_2, static_text_data_2, inline_box_data_4,
inline_box_data_5, inline_box_data_6}));
TestPositionType tree_position = AXNodePosition::CreateTreePosition(
GetTreeID(), text_field_data_1.id, 0 /* child_index */);
ASSERT_NE(nullptr, tree_position);
ASSERT_TRUE(tree_position->IsTreePosition());
EXPECT_TRUE(tree_position->AtStartOfLine());
EXPECT_FALSE(tree_position->AtEndOfLine());
tree_position = AXNodePosition::CreateTreePosition(
GetTreeID(), text_field_data_1.id, 1 /* child_index */);
ASSERT_NE(nullptr, tree_position);
ASSERT_TRUE(tree_position->IsTreePosition());
EXPECT_FALSE(tree_position->AtStartOfLine());
EXPECT_TRUE(tree_position->AtEndOfLine());
tree_position = AXNodePosition::CreateTreePosition(
GetTreeID(), text_field_data_2.id, 0 /* child_index */);
ASSERT_NE(nullptr, tree_position);
ASSERT_TRUE(tree_position->IsTreePosition());
EXPECT_TRUE(tree_position->AtStartOfLine());
EXPECT_FALSE(tree_position->AtEndOfLine());
tree_position = AXNodePosition::CreateTreePosition(
GetTreeID(), text_field_data_2.id, 1 /* child_index */);
ASSERT_NE(nullptr, tree_position);
ASSERT_TRUE(tree_position->IsTreePosition());
EXPECT_FALSE(tree_position->AtStartOfLine());
EXPECT_TRUE(tree_position->AtEndOfLine());
TestPositionType text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), text_field_data_1.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
EXPECT_TRUE(text_position->AtStartOfLine());
EXPECT_FALSE(text_position->AtEndOfLine());
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), text_field_data_1.id, 16 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
EXPECT_FALSE(text_position->AtStartOfLine());
EXPECT_TRUE(text_position->AtEndOfLine());
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), text_field_data_2.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
EXPECT_TRUE(text_position->AtStartOfLine());
EXPECT_FALSE(text_position->AtEndOfLine());
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), text_field_data_2.id, 16 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
EXPECT_FALSE(text_position->AtStartOfLine());
EXPECT_TRUE(text_position->AtEndOfLine());
}
TEST_F(AXPositionTest, AtStartOfParagraphWithTextPosition) {
// An upstream affinity should not affect the outcome since there is no soft
// line break.
TestPositionType text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_box1_.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kUpstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
EXPECT_TRUE(text_position->AtStartOfParagraph());
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_box1_.id, 1 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
EXPECT_FALSE(text_position->AtStartOfParagraph());
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), line_break_.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
EXPECT_FALSE(text_position->AtStartOfParagraph());
// An "after text" position anchored at the line break should not be the same
// as a text position at the start of the next paragraph because in practice
// they should have resulted from two different ancestor positions. The former
// should have been an upstream position, whilst the latter a downstream one.
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), line_break_.id, 1 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
EXPECT_FALSE(text_position->AtStartOfParagraph());
// An upstream affinity should not affect the outcome since there is no soft
// line break.
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_box2_.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kUpstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
EXPECT_TRUE(text_position->AtStartOfParagraph());
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_box2_.id, 1 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
EXPECT_FALSE(text_position->AtStartOfParagraph());
}
TEST_F(AXPositionTest, AtEndOfParagraphWithTextPosition) {
// End of |inline_box1_| is not the end of paragraph since it's
// followed by a whitespace-only line breaking object
TestPositionType text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_box1_.id, 6 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
EXPECT_FALSE(text_position->AtEndOfParagraph());
// The start of |line_break_| is not the end of paragraph since it's
// not the end of its anchor.
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), line_break_.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
EXPECT_FALSE(text_position->AtEndOfParagraph());
// The end of |line_break_| is the end of paragraph since it's
// a line breaking object without additional trailing whitespace.
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), line_break_.id, 1 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
EXPECT_TRUE(text_position->AtEndOfParagraph());
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_box2_.id, 5 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
EXPECT_FALSE(text_position->AtEndOfParagraph());
// The end of |inline_box2_| is the end of paragraph since it's
// followed by the end of document.
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_box2_.id, 6 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
ASSERT_TRUE(text_position->IsTextPosition());
EXPECT_TRUE(text_position->AtEndOfParagraph());
}
TEST_F(AXPositionTest, ParagraphEdgesWithPreservedNewLine) {
// This test ensures that "At{Start|End}OfParagraph" work correctly when a
// text position is on a preserved newline character.
//
// Newline characters are used to separate paragraphs. If there is a series of
// newline characters, a paragraph should start after the last newline
// character.
// ++1 kRootWebArea isLineBreakingObject
// ++++2 kStaticText "some text"
// ++++++3 kInlineTextBox "some text"
// ++++4 kGenericContainer isLineBreakingObject
// ++++++5 kStaticText "\nmore text"
// ++++++++6 kInlineTextBox "\n" isLineBreakingObject
// ++++++++7 kInlineTextBox "more text"
AXNodeData root_data;
root_data.id = 1;
root_data.role = ax::mojom::Role::kRootWebArea;
root_data.AddBoolAttribute(ax::mojom::BoolAttribute::kIsLineBreakingObject,
true);
AXNodeData static_text_data_1;
static_text_data_1.id = 2;
static_text_data_1.role = ax::mojom::Role::kStaticText;
static_text_data_1.SetName("some text");
AXNodeData some_text_data;
some_text_data.id = 3;
some_text_data.role = ax::mojom::Role::kInlineTextBox;
some_text_data.SetName("some text");
AXNodeData container_data;
container_data.id = 4;
container_data.role = ax::mojom::Role::kGenericContainer;
container_data.AddBoolAttribute(
ax::mojom::BoolAttribute::kIsLineBreakingObject, true);
AXNodeData static_text_data_2;
static_text_data_2.id = 5;
static_text_data_2.role = ax::mojom::Role::kStaticText;
static_text_data_2.SetName("\nmore text");
AXNodeData preserved_newline_data;
preserved_newline_data.id = 6;
preserved_newline_data.role = ax::mojom::Role::kInlineTextBox;
preserved_newline_data.SetName("\n");
preserved_newline_data.AddBoolAttribute(
ax::mojom::BoolAttribute::kIsLineBreakingObject, true);
AXNodeData more_text_data;
more_text_data.id = 7;
more_text_data.role = ax::mojom::Role::kInlineTextBox;
more_text_data.SetName("more text");
static_text_data_1.child_ids = {some_text_data.id};
container_data.child_ids = {static_text_data_2.id};
static_text_data_2.child_ids = {preserved_newline_data.id, more_text_data.id};
root_data.child_ids = {static_text_data_1.id, container_data.id};
SetTree(CreateAXTree({root_data, static_text_data_1, some_text_data,
container_data, static_text_data_2,
preserved_newline_data, more_text_data}));
// Text position "some tex<t>\nmore text".
TestPositionType text_position1 = AXNodePosition::CreateTextPosition(
GetTreeID(), root_data.id, 8 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
EXPECT_FALSE(text_position1->AtEndOfParagraph());
EXPECT_FALSE(text_position1->AtStartOfParagraph());
// Text position "some text<\n>more text".
TestPositionType text_position2 = AXNodePosition::CreateTextPosition(
GetTreeID(), root_data.id, 9 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
EXPECT_FALSE(text_position2->AtEndOfParagraph());
EXPECT_FALSE(text_position2->AtStartOfParagraph());
// Text position "some text<\n>more text".
TestPositionType text_position3 = AXNodePosition::CreateTextPosition(
GetTreeID(), root_data.id, 9 /* text_offset */,
ax::mojom::TextAffinity::kUpstream);
EXPECT_FALSE(text_position3->AtEndOfParagraph());
EXPECT_FALSE(text_position3->AtStartOfParagraph());
// Text position "some text\n<m>ore text".
TestPositionType text_position4 = AXNodePosition::CreateTextPosition(
GetTreeID(), root_data.id, 10 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
EXPECT_FALSE(text_position4->AtEndOfParagraph());
EXPECT_TRUE(text_position4->AtStartOfParagraph());
// Text position "some text\n<m>ore text".
TestPositionType text_position5 = AXNodePosition::CreateTextPosition(
GetTreeID(), root_data.id, 10 /* text_offset */,
ax::mojom::TextAffinity::kUpstream);
EXPECT_TRUE(text_position5->AtEndOfParagraph());
EXPECT_FALSE(text_position5->AtStartOfParagraph());
// Text position "<\n>more text".
TestPositionType text_position6 = AXNodePosition::CreateTextPosition(
GetTreeID(), container_data.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
EXPECT_FALSE(text_position6->AtEndOfParagraph());
EXPECT_FALSE(text_position6->AtStartOfParagraph());
// Text position "\n<m>ore text".
TestPositionType text_position7 = AXNodePosition::CreateTextPosition(
GetTreeID(), container_data.id, 1 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
EXPECT_FALSE(text_position7->AtEndOfParagraph());
EXPECT_TRUE(text_position7->AtStartOfParagraph());
// Text position "\n<m>ore text".
TestPositionType text_position8 = AXNodePosition::CreateTextPosition(
GetTreeID(), container_data.id, 1 /* text_offset */,
ax::mojom::TextAffinity::kUpstream);
EXPECT_TRUE(text_position8->AtEndOfParagraph());
EXPECT_FALSE(text_position8->AtStartOfParagraph());
// Text position "\n<m>ore text".
TestPositionType text_position9 = AXNodePosition::CreateTextPosition(
GetTreeID(), static_text_data_2.id, 1 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
EXPECT_FALSE(text_position9->AtEndOfParagraph());
EXPECT_TRUE(text_position9->AtStartOfParagraph());
// Text position "\n<m>ore text".
TestPositionType text_position10 = AXNodePosition::CreateTextPosition(
GetTreeID(), static_text_data_2.id, 1 /* text_offset */,
ax::mojom::TextAffinity::kUpstream);
EXPECT_TRUE(text_position10->AtEndOfParagraph());
EXPECT_FALSE(text_position10->AtStartOfParagraph());
TestPositionType text_position11 = AXNodePosition::CreateTextPosition(
GetTreeID(), preserved_newline_data.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
EXPECT_FALSE(text_position11->AtEndOfParagraph());
EXPECT_FALSE(text_position11->AtStartOfParagraph());
TestPositionType text_position12 = AXNodePosition::CreateTextPosition(
GetTreeID(), preserved_newline_data.id, 1 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
EXPECT_TRUE(text_position12->AtEndOfParagraph());
EXPECT_FALSE(text_position12->AtStartOfParagraph());
TestPositionType text_position13 = AXNodePosition::CreateTextPosition(
GetTreeID(), more_text_data.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
EXPECT_FALSE(text_position13->AtEndOfParagraph());
EXPECT_TRUE(text_position13->AtStartOfParagraph());
TestPositionType text_position14 = AXNodePosition::CreateTextPosition(
GetTreeID(), more_text_data.id, 1 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
EXPECT_FALSE(text_position14->AtEndOfParagraph());
EXPECT_FALSE(text_position14->AtStartOfParagraph());
}
TEST_F(
AXPositionTest,
PreviousParagraphEndStopAtAnchorBoundaryWithConsecutiveParentChildLineBreakingObjects) {
// This test updates the tree structure to test a specific edge case -
// CreatePreviousParagraphEndPosition(), stopping at an anchor boundary,
// with consecutive parent-child line breaking objects.
// ++1 rootWebArea
// ++++2 staticText name="first"
// ++++3 genericContainer isLineBreakingObject
// ++++++4 genericContainer isLineBreakingObject
// ++++++5 staticText name="second"
AXNodeData root_data;
root_data.id = 1;
root_data.role = ax::mojom::Role::kRootWebArea;
AXNodeData static_text_data_a;
static_text_data_a.id = 2;
static_text_data_a.role = ax::mojom::Role::kStaticText;
static_text_data_a.SetName("first");
AXNodeData container_data_a;
container_data_a.id = 3;
container_data_a.role = ax::mojom::Role::kGenericContainer;
container_data_a.AddBoolAttribute(
ax::mojom::BoolAttribute::kIsLineBreakingObject, true);
AXNodeData container_data_b;
container_data_b.id = 4;
container_data_b.role = ax::mojom::Role::kGenericContainer;
container_data_b.AddBoolAttribute(
ax::mojom::BoolAttribute::kIsLineBreakingObject, true);
AXNodeData static_text_data_b;
static_text_data_b.id = 5;
static_text_data_b.role = ax::mojom::Role::kStaticText;
static_text_data_b.SetName("second");
root_data.child_ids = {static_text_data_a.id, container_data_a.id};
container_data_a.child_ids = {container_data_b.id, static_text_data_b.id};
SetTree(CreateAXTree({root_data, static_text_data_a, container_data_a,
container_data_b, static_text_data_b}));
TestPositionType test_position = AXNodePosition::CreateTextPosition(
GetTreeID(), root_data.id, 11 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
test_position = test_position->CreatePreviousParagraphEndPosition(
AXBoundaryBehavior::StopAtAnchorBoundary);
EXPECT_TRUE(test_position->IsTextPosition());
EXPECT_EQ(root_data.id, test_position->anchor_id());
EXPECT_EQ(5, test_position->text_offset());
}
TEST_F(AXPositionTest, AtStartOrEndOfParagraphOnAListMarker) {
// "AtStartOfParagraph" should return true before a list marker, either a
// Legacy Layout or an NG Layout one. It should return false on the next
// sibling of the list marker, i.e., before the list item's actual text
// contents.
//
// There are two list markers in the following test tree. The first one is a
// Legacy Layout one and the second an NG Layout one.
// ++1 kRootWebArea
// ++++2 kStaticText "Before list."
// ++++++3 kInlineTextBox "Before list."
// ++++4 kList
// ++++++5 kListItem
// ++++++++6 kListMarker
// ++++++++++7 kStaticText "1. "
// ++++++++++++8 kInlineTextBox "1. "
// ++++++++9 kStaticText "First item."
// ++++++++++10 kInlineTextBox "First item."
// ++++++11 kListItem
// ++++++++12 kListMarker "2. "
// ++++++++13 kStaticText "Second item."
// ++++++++++14 kInlineTextBox "Second item."
// ++15 kStaticText "After list."
// ++++16 kInlineTextBox "After list."
AXNodeData root;
AXNodeData list;
AXNodeData list_item1;
AXNodeData list_item2;
AXNodeData list_marker_legacy;
AXNodeData list_marker_ng;
AXNodeData static_text1;
AXNodeData static_text2;
AXNodeData static_text3;
AXNodeData static_text4;
AXNodeData static_text5;
AXNodeData inline_box1;
AXNodeData inline_box2;
AXNodeData inline_box3;
AXNodeData inline_box4;
AXNodeData inline_box5;
root.id = 1;
static_text1.id = 2;
inline_box1.id = 3;
list.id = 4;
list_item1.id = 5;
list_marker_legacy.id = 6;
static_text2.id = 7;
inline_box2.id = 8;
static_text3.id = 9;
inline_box3.id = 10;
list_item2.id = 11;
list_marker_ng.id = 12;
static_text4.id = 13;
inline_box4.id = 14;
static_text5.id = 15;
inline_box5.id = 16;
root.role = ax::mojom::Role::kRootWebArea;
root.child_ids = {static_text1.id, list.id, static_text5.id};
root.AddBoolAttribute(ax::mojom::BoolAttribute::kIsLineBreakingObject, true);
static_text1.role = ax::mojom::Role::kStaticText;
static_text1.child_ids = {inline_box1.id};
static_text1.SetName("Before list.");
inline_box1.role = ax::mojom::Role::kInlineTextBox;
inline_box1.SetName("Before list.");
list.role = ax::mojom::Role::kList;
list.child_ids = {list_item1.id, list_item2.id};
list_item1.role = ax::mojom::Role::kListItem;
list_item1.child_ids = {list_marker_legacy.id, static_text3.id};
list_item1.AddBoolAttribute(ax::mojom::BoolAttribute::kIsLineBreakingObject,
true);
list_marker_legacy.role = ax::mojom::Role::kListMarker;
list_marker_legacy.child_ids = {static_text2.id};
static_text2.role = ax::mojom::Role::kStaticText;
static_text2.child_ids = {inline_box2.id};
static_text2.SetName("1. ");
inline_box2.role = ax::mojom::Role::kInlineTextBox;
inline_box2.SetName("1. ");
inline_box2.AddIntAttribute(ax::mojom::IntAttribute::kNextOnLineId,
inline_box3.id);
static_text3.role = ax::mojom::Role::kStaticText;
static_text3.child_ids = {inline_box3.id};
static_text3.SetName("First item.");
inline_box3.role = ax::mojom::Role::kInlineTextBox;
inline_box3.SetName("First item.");
inline_box3.AddIntAttribute(ax::mojom::IntAttribute::kPreviousOnLineId,
inline_box2.id);
list_item2.role = ax::mojom::Role::kListItem;
list_item2.child_ids = {list_marker_ng.id, static_text4.id};
list_item2.AddBoolAttribute(ax::mojom::BoolAttribute::kIsLineBreakingObject,
true);
list_marker_ng.role = ax::mojom::Role::kListMarker;
list_marker_ng.SetName("2. ");
list_marker_ng.SetNameFrom(ax::mojom::NameFrom::kContents);
list_marker_ng.AddIntAttribute(ax::mojom::IntAttribute::kNextOnLineId,
inline_box4.id);
static_text4.role = ax::mojom::Role::kStaticText;
static_text4.child_ids = {inline_box4.id};
static_text4.SetName("Second item.");
inline_box4.role = ax::mojom::Role::kInlineTextBox;
inline_box4.SetName("Second item.");
inline_box4.AddIntAttribute(ax::mojom::IntAttribute::kPreviousOnLineId,
list_marker_ng.id);
static_text5.role = ax::mojom::Role::kStaticText;
static_text5.child_ids = {inline_box5.id};
static_text5.SetName("After list.");
inline_box5.role = ax::mojom::Role::kInlineTextBox;
inline_box5.SetName("After list.");
SetTree(CreateAXTree({root, static_text1, inline_box1, list, list_item1,
list_marker_legacy, static_text2, inline_box2,
static_text3, inline_box3, list_item2, list_marker_ng,
static_text4, inline_box4, static_text5, inline_box5}));
// A text position after the text "Before list.". It should not be equivalent
// to a position that is before the list itself, or before the first list
// bullet / item.
TestPositionType text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), static_text1.id, 12 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
EXPECT_FALSE(text_position->AtStartOfParagraph());
EXPECT_TRUE(text_position->AtEndOfParagraph());
// A text position after the text "Before list.". It should not be equivalent
// to a position that is before the list itself, or before the first list
// bullet / item.
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_box1.id, 12 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
EXPECT_FALSE(text_position->AtStartOfParagraph());
EXPECT_TRUE(text_position->AtEndOfParagraph());
// A text position before the list.
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), list.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
EXPECT_TRUE(text_position->AtStartOfParagraph());
EXPECT_FALSE(text_position->AtEndOfParagraph());
// A downstream text position after the list. It should resolve to a leaf
// position before the paragraph that comes after the list, so it should be
// "AtStartOfParagraph".
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), list.id, 14 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
EXPECT_TRUE(text_position->AtStartOfParagraph());
EXPECT_FALSE(text_position->AtEndOfParagraph());
// An upstream text position after the list. It should be "AtEndOfParagraph".
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), list.id, 14 /* text_offset */,
ax::mojom::TextAffinity::kUpstream);
ASSERT_NE(nullptr, text_position);
EXPECT_FALSE(text_position->AtStartOfParagraph());
EXPECT_TRUE(text_position->AtEndOfParagraph());
// A text position before the first list bullet (the Legacy Layout one).
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), list_marker_legacy.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
EXPECT_TRUE(text_position->AtStartOfParagraph());
EXPECT_FALSE(text_position->AtEndOfParagraph());
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), list_marker_legacy.id, 1 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
EXPECT_FALSE(text_position->AtStartOfParagraph());
EXPECT_FALSE(text_position->AtEndOfParagraph());
// A text position before the first list bullet (the Legacy Layout one).
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), static_text2.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
EXPECT_TRUE(text_position->AtStartOfParagraph());
EXPECT_FALSE(text_position->AtEndOfParagraph());
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), static_text2.id, 2 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
EXPECT_FALSE(text_position->AtStartOfParagraph());
EXPECT_FALSE(text_position->AtEndOfParagraph());
// A text position before the first list bullet (the Legacy Layout one).
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_box2.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
EXPECT_TRUE(text_position->AtStartOfParagraph());
EXPECT_FALSE(text_position->AtEndOfParagraph());
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_box2.id, 3 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
EXPECT_FALSE(text_position->AtStartOfParagraph());
EXPECT_FALSE(text_position->AtEndOfParagraph());
// A text position before the second list bullet (the NG Layout one).
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), list_marker_ng.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
EXPECT_TRUE(text_position->AtStartOfParagraph());
EXPECT_FALSE(text_position->AtEndOfParagraph());
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), list_marker_ng.id, 3 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
EXPECT_FALSE(text_position->AtStartOfParagraph());
EXPECT_FALSE(text_position->AtEndOfParagraph());
// A text position before the text contents of the first list item - not the
// bullet.
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), static_text3.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
EXPECT_FALSE(text_position->AtStartOfParagraph());
EXPECT_FALSE(text_position->AtEndOfParagraph());
// A text position before the text contents of the first list item - not the
// bullet.
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_box3.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
EXPECT_FALSE(text_position->AtStartOfParagraph());
EXPECT_FALSE(text_position->AtEndOfParagraph());
// A text position after the text contents of the first list item.
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), static_text3.id, 11 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
EXPECT_FALSE(text_position->AtStartOfParagraph());
EXPECT_TRUE(text_position->AtEndOfParagraph());
// A text position after the text contents of the first list item.
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_box3.id, 11 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
EXPECT_FALSE(text_position->AtStartOfParagraph());
EXPECT_TRUE(text_position->AtEndOfParagraph());
// A text position before the text contents of the second list item - not the
// bullet.
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), static_text4.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
EXPECT_FALSE(text_position->AtStartOfParagraph());
EXPECT_FALSE(text_position->AtEndOfParagraph());
// A text position before the text contents of the second list item - not the
// bullet.
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_box4.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
EXPECT_FALSE(text_position->AtStartOfParagraph());
EXPECT_FALSE(text_position->AtEndOfParagraph());
// A text position after the text contents of the second list item.
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), static_text4.id, 12 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
EXPECT_FALSE(text_position->AtStartOfParagraph());
EXPECT_TRUE(text_position->AtEndOfParagraph());
// A text position after the text contents of the second list item.
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_box4.id, 12 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
EXPECT_FALSE(text_position->AtStartOfParagraph());
EXPECT_TRUE(text_position->AtEndOfParagraph());
// A text position before the text "After list.".
text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_box5.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
EXPECT_TRUE(text_position->AtStartOfParagraph());
EXPECT_FALSE(text_position->AtEndOfParagraph());
}
TEST_F(AXPositionTest,
AtStartOrEndOfParagraphWithLeadingAndTrailingDocumentWhitespace) {
// This test ensures that "At{Start|End}OfParagraph" work correctly when a
// text position is on a preserved newline character.
//
// Newline characters are used to separate paragraphs. If there is a series of
// newline characters, a paragraph should start after the last newline
// character.
// ++1 kRootWebArea isLineBreakingObject
// ++++2 kGenericContainer isLineBreakingObject
// ++++++3 kStaticText "\n"
// ++++++++4 kInlineTextBox "\n" isLineBreakingObject
// ++++5 kGenericContainer isLineBreakingObject
// ++++++6 kStaticText "some text"
// ++++++++7 kInlineTextBox "some"
// ++++++++8 kInlineTextBox " "
// ++++++++9 kInlineTextBox "text"
// ++++10 kGenericContainer isLineBreakingObject
// ++++++11 kStaticText "\n"
// ++++++++12 kInlineTextBox "\n" isLineBreakingObject
AXNodeData root_data;
root_data.id = 1;
root_data.role = ax::mojom::Role::kRootWebArea;
root_data.AddBoolAttribute(ax::mojom::BoolAttribute::kIsLineBreakingObject,
true);
AXNodeData container_data_a;
container_data_a.id = 2;
container_data_a.role = ax::mojom::Role::kGenericContainer;
container_data_a.AddBoolAttribute(
ax::mojom::BoolAttribute::kIsLineBreakingObject, true);
AXNodeData static_text_data_a;
static_text_data_a.id = 3;
static_text_data_a.role = ax::mojom::Role::kStaticText;
static_text_data_a.SetName("\n");
AXNodeData inline_text_data_a;
inline_text_data_a.id = 4;
inline_text_data_a.role = ax::mojom::Role::kInlineTextBox;
inline_text_data_a.SetName("\n");
inline_text_data_a.AddBoolAttribute(
ax::mojom::BoolAttribute::kIsLineBreakingObject, true);
AXNodeData container_data_b;
container_data_b.id = 5;
container_data_b.role = ax::mojom::Role::kGenericContainer;
container_data_b.AddBoolAttribute(
ax::mojom::BoolAttribute::kIsLineBreakingObject, true);
AXNodeData static_text_data_b;
static_text_data_b.id = 6;
static_text_data_b.role = ax::mojom::Role::kStaticText;
static_text_data_b.SetName("some text");
AXNodeData inline_text_data_b_1;
inline_text_data_b_1.id = 7;
inline_text_data_b_1.role = ax::mojom::Role::kInlineTextBox;
inline_text_data_b_1.SetName("some");
AXNodeData inline_text_data_b_2;
inline_text_data_b_2.id = 8;
inline_text_data_b_2.role = ax::mojom::Role::kInlineTextBox;
inline_text_data_b_2.SetName(" ");
AXNodeData inline_text_data_b_3;
inline_text_data_b_3.id = 9;
inline_text_data_b_3.role = ax::mojom::Role::kInlineTextBox;
inline_text_data_b_3.SetName("text");
AXNodeData container_data_c;
container_data_c.id = 10;
container_data_c.role = ax::mojom::Role::kGenericContainer;
container_data_c.AddBoolAttribute(
ax::mojom::BoolAttribute::kIsLineBreakingObject, true);
AXNodeData static_text_data_c;
static_text_data_c.id = 11;
static_text_data_c.role = ax::mojom::Role::kStaticText;
static_text_data_c.SetName("\n");
AXNodeData inline_text_data_c;
inline_text_data_c.id = 12;
inline_text_data_c.role = ax::mojom::Role::kInlineTextBox;
inline_text_data_c.SetName("\n");
inline_text_data_c.AddBoolAttribute(
ax::mojom::BoolAttribute::kIsLineBreakingObject, true);
root_data.child_ids = {container_data_a.id, container_data_b.id,
container_data_c.id};
container_data_a.child_ids = {static_text_data_a.id};
static_text_data_a.child_ids = {inline_text_data_a.id};
container_data_b.child_ids = {static_text_data_b.id};
static_text_data_b.child_ids = {inline_text_data_b_1.id,
inline_text_data_b_2.id,
inline_text_data_b_3.id};
container_data_c.child_ids = {static_text_data_c.id};
static_text_data_c.child_ids = {inline_text_data_c.id};
SetTree(CreateAXTree(
{root_data, container_data_a, container_data_b, container_data_c,
static_text_data_a, static_text_data_b, static_text_data_c,
inline_text_data_a, inline_text_data_b_1, inline_text_data_b_2,
inline_text_data_b_3, inline_text_data_c}));
// Before the first "\n".
TestPositionType text_position1 = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_text_data_a.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
EXPECT_FALSE(text_position1->AtEndOfParagraph());
EXPECT_TRUE(text_position1->AtStartOfParagraph());
// After the first "\n".
//
// Since the position is an "after text" position, it is similar to pressing
// the End key, (or Cmd-Right on Mac), while the caret is on the line break,
// so it should not be "AtStartOfParagraph".
TestPositionType text_position2 = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_text_data_a.id, 1 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
EXPECT_TRUE(text_position2->AtEndOfParagraph());
EXPECT_FALSE(text_position2->AtStartOfParagraph());
// Before "some".
TestPositionType text_position3 = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_text_data_b_1.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
EXPECT_FALSE(text_position3->AtEndOfParagraph());
EXPECT_TRUE(text_position3->AtStartOfParagraph());
// After "some".
TestPositionType text_position4 = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_text_data_b_1.id, 4 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
EXPECT_FALSE(text_position4->AtEndOfParagraph());
EXPECT_FALSE(text_position4->AtStartOfParagraph());
// Before " ".
TestPositionType text_position5 = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_text_data_b_2.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
EXPECT_FALSE(text_position5->AtEndOfParagraph());
EXPECT_FALSE(text_position5->AtStartOfParagraph());
// After " ".
TestPositionType text_position6 = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_text_data_b_2.id, 1 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
EXPECT_FALSE(text_position6->AtEndOfParagraph());
EXPECT_FALSE(text_position6->AtStartOfParagraph());
// Before "text".
TestPositionType text_position7 = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_text_data_b_3.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
EXPECT_FALSE(text_position7->AtEndOfParagraph());
EXPECT_FALSE(text_position7->AtStartOfParagraph());
// After "text".
TestPositionType text_position8 = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_text_data_b_3.id, 4 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
EXPECT_FALSE(text_position8->AtEndOfParagraph());
EXPECT_FALSE(text_position8->AtStartOfParagraph());
// Before the second "\n".
TestPositionType text_position9 = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_text_data_c.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
EXPECT_FALSE(text_position9->AtEndOfParagraph());
EXPECT_FALSE(text_position9->AtStartOfParagraph());
// After the second "\n".
TestPositionType text_position10 = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_text_data_c.id, 1 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
EXPECT_TRUE(text_position10->AtEndOfParagraph());
EXPECT_FALSE(text_position10->AtStartOfParagraph());
}
TEST_F(AXPositionTest, AtStartOrEndOfParagraphWithIgnoredNodes) {
// This test ensures that "At{Start|End}OfParagraph" work correctly when there
// are ignored nodes present near a paragraph boundary.
//
// An ignored node that is between a given position and a paragraph boundary
// should not be taken into consideration. The position should be interpreted
// as being on the boundary.
// ++1 kRootWebArea isLineBreakingObject