Commit d39ee097 authored by Alison Maher's avatar Alison Maher Committed by Commit Bot

MoveEndpointByUnit (TextUnit_Page, TextUnit_Document)

This adds an implementation of ITextRangeProvider::MoveEndpointByUnit
for TextUnit_Page and TextUnit_Document.

Behavior for TextUnit_Page is identical to TextUnit_Document, as web
pages are not paginated.

TextUnit_Document behavior is to always move to the beginning or end of
the top-level document. iframes will jump out of the current iframe and
to the end of the top-most document. There currently is buggy behavior
for the iframe case, and a follow up task will address this issue.

Bug: 928948
Change-Id: I87056282dbe037e95a3cd43b29e10a849a9a9a0e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1554132
Commit-Queue: Alison Maher <almaher@microsoft.com>
Reviewed-by: default avatarDominic Mazzoni <dmazzoni@chromium.org>
Cr-Commit-Position: refs/heads/master@{#649157}
parent 15510147
...@@ -115,10 +115,7 @@ void AXPositionTest::SetUp() { ...@@ -115,10 +115,7 @@ void AXPositionTest::SetUp() {
static_text2_.id = STATIC_TEXT2_ID; static_text2_.id = STATIC_TEXT2_ID;
inline_box2_.id = INLINE_BOX2_ID; inline_box2_.id = INLINE_BOX2_ID;
root_.role = ax::mojom::Role::kDialog; root_.role = ax::mojom::Role::kRootWebArea;
root_.AddState(ax::mojom::State::kFocusable);
root_.SetName(std::string("ButtonCheck box") + TEXT_VALUE);
root_.relative_bounds.bounds = gfx::RectF(0, 0, 800, 600);
button_.role = ax::mojom::Role::kButton; button_.role = ax::mojom::Role::kButton;
button_.SetHasPopup(ax::mojom::HasPopup::kMenu); button_.SetHasPopup(ax::mojom::HasPopup::kMenu);
...@@ -1129,6 +1126,114 @@ TEST_F(AXPositionTest, CreatePositionAtEndOfAnchorWithTextPosition) { ...@@ -1129,6 +1126,114 @@ TEST_F(AXPositionTest, CreatePositionAtEndOfAnchorWithTextPosition) {
EXPECT_EQ(ax::mojom::TextAffinity::kDownstream, test_position->affinity()); EXPECT_EQ(ax::mojom::TextAffinity::kDownstream, test_position->affinity());
} }
TEST_F(AXPositionTest, CreatePositionAtStartOfDocumentWithNullPosition) {
TestPositionType null_position = AXNodePosition::CreateNullPosition();
ASSERT_NE(nullptr, null_position);
TestPositionType test_position =
null_position->CreatePositionAtStartOfDocument();
EXPECT_NE(nullptr, test_position);
EXPECT_TRUE(test_position->IsNullPosition());
}
TEST_F(AXPositionTest, CreatePositionAtStartOfDocumentWithTreePosition) {
TestPositionType tree_position = AXNodePosition::CreateTreePosition(
tree_.data().tree_id, root_.id, 0 /* child_index */);
ASSERT_NE(nullptr, tree_position);
TestPositionType test_position =
tree_position->CreatePositionAtStartOfDocument();
EXPECT_NE(nullptr, test_position);
EXPECT_EQ(root_.id, test_position->anchor_id());
tree_position = AXNodePosition::CreateTreePosition(
tree_.data().tree_id, root_.id, 1 /* child_index */);
ASSERT_NE(nullptr, tree_position);
test_position = tree_position->CreatePositionAtStartOfDocument();
EXPECT_NE(nullptr, test_position);
EXPECT_EQ(root_.id, test_position->anchor_id());
tree_position = AXNodePosition::CreateTreePosition(
tree_.data().tree_id, inline_box1_.id, 0 /* child_index */);
ASSERT_NE(nullptr, tree_position);
test_position = tree_position->CreatePositionAtStartOfDocument();
EXPECT_NE(nullptr, test_position);
EXPECT_EQ(root_.id, test_position->anchor_id());
}
TEST_F(AXPositionTest, CreatePositionAtStartOfDocumentWithTextPosition) {
TestPositionType text_position = AXNodePosition::CreateTextPosition(
tree_.data().tree_id, inline_box1_.id, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
TestPositionType test_position =
text_position->CreatePositionAtStartOfDocument();
EXPECT_NE(nullptr, test_position);
EXPECT_EQ(root_.id, test_position->anchor_id());
text_position = AXNodePosition::CreateTextPosition(
tree_.data().tree_id, inline_box1_.id, 1 /* text_offset */,
ax::mojom::TextAffinity::kUpstream);
ASSERT_NE(nullptr, text_position);
test_position = text_position->CreatePositionAtStartOfDocument();
EXPECT_NE(nullptr, test_position);
EXPECT_EQ(root_.id, test_position->anchor_id());
// Affinity should have been reset to the default value.
EXPECT_EQ(ax::mojom::TextAffinity::kDownstream, test_position->affinity());
}
TEST_F(AXPositionTest, CreatePositionAtEndOfDocumentWithNullPosition) {
TestPositionType null_position = AXNodePosition::CreateNullPosition();
ASSERT_NE(nullptr, null_position);
TestPositionType test_position =
null_position->CreatePositionAtEndOfDocument();
EXPECT_NE(nullptr, test_position);
EXPECT_TRUE(test_position->IsNullPosition());
}
TEST_F(AXPositionTest, CreatePositionAtEndOfDocumentWithTreePosition) {
TestPositionType tree_position = AXNodePosition::CreateTreePosition(
tree_.data().tree_id, root_.id, 3 /* child_index */);
ASSERT_NE(nullptr, tree_position);
TestPositionType test_position =
tree_position->CreatePositionAtEndOfDocument();
EXPECT_NE(nullptr, test_position);
EXPECT_EQ(inline_box2_.id, test_position->anchor_id());
tree_position = AXNodePosition::CreateTreePosition(
tree_.data().tree_id, root_.id, 1 /* child_index */);
ASSERT_NE(nullptr, tree_position);
test_position = tree_position->CreatePositionAtEndOfDocument();
EXPECT_NE(nullptr, test_position);
EXPECT_EQ(inline_box2_.id, test_position->anchor_id());
tree_position = AXNodePosition::CreateTreePosition(
tree_.data().tree_id, inline_box1_.id, 0 /* child_index */);
ASSERT_NE(nullptr, tree_position);
test_position = tree_position->CreatePositionAtEndOfDocument();
EXPECT_NE(nullptr, test_position);
EXPECT_EQ(inline_box2_.id, test_position->anchor_id());
}
TEST_F(AXPositionTest, CreatePositionAtEndOfDocumentWithTextPosition) {
TestPositionType text_position = AXNodePosition::CreateTextPosition(
tree_.data().tree_id, inline_box1_.id, 6 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_NE(nullptr, text_position);
TestPositionType test_position =
text_position->CreatePositionAtEndOfDocument();
EXPECT_NE(nullptr, test_position);
EXPECT_EQ(inline_box2_.id, test_position->anchor_id());
text_position = AXNodePosition::CreateTextPosition(
tree_.data().tree_id, inline_box1_.id, 5 /* text_offset */,
ax::mojom::TextAffinity::kUpstream);
ASSERT_NE(nullptr, text_position);
test_position = text_position->CreatePositionAtEndOfDocument();
EXPECT_NE(nullptr, test_position);
EXPECT_EQ(inline_box2_.id, test_position->anchor_id());
// Affinity should have been reset to the default value.
EXPECT_EQ(ax::mojom::TextAffinity::kDownstream, test_position->affinity());
}
TEST_F(AXPositionTest, CreateChildPositionAtWithNullPosition) { TEST_F(AXPositionTest, CreateChildPositionAtWithNullPosition) {
TestPositionType null_position = AXNodePosition::CreateNullPosition(); TestPositionType null_position = AXNodePosition::CreateNullPosition();
ASSERT_NE(nullptr, null_position); ASSERT_NE(nullptr, null_position);
......
...@@ -21,6 +21,7 @@ ...@@ -21,6 +21,7 @@
#include "base/strings/utf_string_conversions.h" #include "base/strings/utf_string_conversions.h"
#include "ui/accessibility/ax_enum_util.h" #include "ui/accessibility/ax_enum_util.h"
#include "ui/accessibility/ax_enums.mojom.h" #include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_role_properties.h"
#include "ui/accessibility/ax_tree_id.h" #include "ui/accessibility/ax_tree_id.h"
namespace ui { namespace ui {
...@@ -330,6 +331,20 @@ class AXPosition { ...@@ -330,6 +331,20 @@ class AXPosition {
return false; return false;
} }
bool AtStartOfDocument() const {
if (IsNullPosition() || !GetAnchor())
return false;
return ui::IsDocument(GetAnchor()->data().role) && AtStartOfAnchor();
}
bool AtEndOfDocument() const {
if (IsNullPosition() || !GetAnchor())
return false;
return CreateNextAnchorPosition()->IsNullPosition() && AtEndOfAnchor();
}
// This method returns a position instead of a node because this allows us to // This method returns a position instead of a node because this allows us to
// return the corresponding text offset or child index in the ancestor that // return the corresponding text offset or child index in the ancestor that
// relates to the current position. // relates to the current position.
...@@ -517,6 +532,44 @@ class AXPosition { ...@@ -517,6 +532,44 @@ class AXPosition {
return CreateNullPosition(); return CreateNullPosition();
} }
AXPositionInstance CreatePositionAtStartOfDocument() const {
if (kind_ == AXPositionKind::NULL_POSITION)
return CreateNullPosition();
AXPositionInstance iterator = Clone();
while (!iterator->IsNullPosition()) {
if (ui::IsDocument(iterator->GetAnchor()->data().role) &&
iterator->CreateParentPosition()->IsNullPosition()) {
return iterator->CreatePositionAtStartOfAnchor();
}
iterator = iterator->CreateParentPosition();
}
return CreateNullPosition();
}
AXPositionInstance CreatePositionAtEndOfDocument() const {
if (kind_ == AXPositionKind::NULL_POSITION)
return CreateNullPosition();
AXPositionInstance iterator = Clone();
while (!iterator->IsNullPosition()) {
if (ui::IsDocument(iterator->GetAnchor()->data().role) &&
iterator->CreateParentPosition()->IsNullPosition()) {
AXPositionInstance tree_position = iterator->AsTreePosition();
DCHECK(tree_position);
while (tree_position->AnchorChildCount()) {
tree_position = tree_position->CreateChildPositionAt(
tree_position->AnchorChildCount() - 1);
}
iterator =
tree_position->AsLeafTextPosition()->CreatePositionAtEndOfAnchor();
return iterator;
}
iterator = iterator->CreateParentPosition();
}
return CreateNullPosition();
}
AXPositionInstance CreateChildPositionAt(int child_index) const { AXPositionInstance CreateChildPositionAt(int child_index) const {
if (IsNullPosition()) if (IsNullPosition())
return CreateNullPosition(); return CreateNullPosition();
......
...@@ -286,7 +286,49 @@ STDMETHODIMP AXPlatformNodeTextRangeProviderWin::MoveEndpointByUnit( ...@@ -286,7 +286,49 @@ STDMETHODIMP AXPlatformNodeTextRangeProviderWin::MoveEndpointByUnit(
int count, int count,
int* units_moved) { int* units_moved) {
WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTRANGE_MOVEENDPOINTBYUNIT); WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTRANGE_MOVEENDPOINTBYUNIT);
return E_NOTIMPL; UIA_VALIDATE_TEXTRANGEPROVIDER_CALL();
*units_moved = 0;
// Per MSDN, MoveEndpointByUnit with zero count has no effect
if (count == 0)
return S_OK;
bool is_start_endpoint = endpoint == TextPatternRangeEndpoint_Start;
AXPositionInstance& position_to_move = is_start_endpoint ? start_ : end_;
AXPositionInstance new_position;
switch (unit) {
case TextUnit_Character:
case TextUnit_Format:
case TextUnit_Word:
case TextUnit_Paragraph:
case TextUnit_Line:
return E_NOTIMPL;
// Page unit is not supported.
// Substituting it by the next larger unit (Document).
case TextUnit_Page:
case TextUnit_Document:
new_position =
MoveEndpointByDocument(position_to_move, count, units_moved);
break;
default:
return UIA_E_NOTSUPPORTED;
}
position_to_move = std::move(new_position);
if (*start_ > *end_) {
// If the start was moved past the end, create a degenerate range
// with the end equal to the start, and vice versa
if (is_start_endpoint)
end_ = start_->Clone();
else
start_ = end_->Clone();
}
return S_OK;
} }
STDMETHODIMP AXPlatformNodeTextRangeProviderWin::MoveEndpointByRange( STDMETHODIMP AXPlatformNodeTextRangeProviderWin::MoveEndpointByRange(
...@@ -403,4 +445,26 @@ ui::AXPlatformNodeWin* AXPlatformNodeTextRangeProviderWin::owner() const { ...@@ -403,4 +445,26 @@ ui::AXPlatformNodeWin* AXPlatformNodeTextRangeProviderWin::owner() const {
return owner_; return owner_;
} }
AXPlatformNodeTextRangeProviderWin::AXPositionInstance
AXPlatformNodeTextRangeProviderWin::MoveEndpointByDocument(
const AXPositionInstance& endpoint,
const int count,
int* units_moved) {
DCHECK_NE(count, 0);
*units_moved = 0;
AXPositionInstance current_endpoint = endpoint->Clone();
const bool forwards = count > 0;
if (forwards && !current_endpoint->AtEndOfDocument()) {
current_endpoint = endpoint->CreatePositionAtEndOfDocument();
*units_moved = 1;
} else if (!forwards && !current_endpoint->AtStartOfDocument()) {
current_endpoint = endpoint->CreatePositionAtStartOfDocument();
*units_moved = -1;
}
return current_endpoint;
}
} // namespace ui } // namespace ui
...@@ -88,6 +88,10 @@ class __declspec(uuid("3071e40d-a10d-45ff-a59f-6e8e1138e2c1")) ...@@ -88,6 +88,10 @@ class __declspec(uuid("3071e40d-a10d-45ff-a59f-6e8e1138e2c1"))
using AXPositionInstance = AXNodePosition::AXPositionInstance; using AXPositionInstance = AXNodePosition::AXPositionInstance;
using AXNodeRange = AXRange<AXNodePosition::AXPositionInstance::element_type>; using AXNodeRange = AXRange<AXNodePosition::AXPositionInstance::element_type>;
AXPositionInstance MoveEndpointByDocument(const AXPositionInstance& endpoint,
const int count,
int* units_moved);
AXNodePosition::AXPositionInstance start_; AXNodePosition::AXPositionInstance start_;
AXNodePosition::AXPositionInstance end_; AXNodePosition::AXPositionInstance end_;
CComPtr<ui::AXPlatformNodeWin> owner_; CComPtr<ui::AXPlatformNodeWin> owner_;
......
...@@ -593,6 +593,131 @@ TEST_F(AXPlatformNodeTextRangeProviderTest, TestITextRangeProviderGetText) { ...@@ -593,6 +593,131 @@ TEST_F(AXPlatformNodeTextRangeProviderTest, TestITextRangeProviderGetText) {
AXNodePosition::SetTreeForTesting(nullptr); AXNodePosition::SetTreeForTesting(nullptr);
} }
TEST_F(AXPlatformNodeTextRangeProviderTest,
TestITextRangeProviderMoveEndpointByDocument) {
ui::AXNodeData text_data;
text_data.id = 2;
text_data.role = ax::mojom::Role::kStaticText;
text_data.SetName("some text");
ui::AXNodeData more_text_data;
more_text_data.id = 3;
more_text_data.role = ax::mojom::Role::kStaticText;
more_text_data.SetName("more text");
ui::AXNodeData even_more_text_data;
even_more_text_data.id = 4;
even_more_text_data.role = ax::mojom::Role::kStaticText;
even_more_text_data.SetName("even more text");
ui::AXNodeData root_data;
root_data.id = 1;
root_data.role = ax::mojom::Role::kRootWebArea;
root_data.child_ids = {2, 3, 4};
ui::AXTreeUpdate update;
ui::AXTreeData tree_data;
tree_data.tree_id = ui::AXTreeID::CreateNewAXTreeID();
update.tree_data = tree_data;
update.has_tree_data = true;
update.root_id = root_data.id;
update.nodes.push_back(root_data);
update.nodes.push_back(text_data);
update.nodes.push_back(more_text_data);
update.nodes.push_back(even_more_text_data);
Init(update);
AXNode* root_node = GetRootNode();
AXNodePosition::SetTreeForTesting(tree_.get());
AXNode* text_node = root_node->children()[1];
ComPtr<IRawElementProviderSimple> text_node_raw =
QueryInterfaceFromNode<IRawElementProviderSimple>(text_node);
ComPtr<ITextProvider> text_provider;
EXPECT_HRESULT_SUCCEEDED(
text_node_raw->GetPatternProvider(UIA_TextPatternId, &text_provider));
// Run the test twice, one for TextUnit_Document and once for TextUnit_Page,
// since they should have identical behavior.
const TextUnit textunit_types[] = {TextUnit_Document, TextUnit_Page};
for (auto& textunit : textunit_types) {
ComPtr<ITextRangeProvider> text_range_provider;
EXPECT_HRESULT_SUCCEEDED(
text_provider->get_DocumentRange(&text_range_provider));
// Verify MoveEndpointByUnit with zero count has no effect
int count;
ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit(
TextPatternRangeEndpoint_End, textunit, /*count*/ 0, &count));
ASSERT_EQ(0, count);
// Move the endpoint to the end of the document. Verify all text content.
ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit(
TextPatternRangeEndpoint_End, textunit, /*count*/ 1, &count));
ASSERT_EQ(1, count);
base::win::ScopedBstr text_content;
ASSERT_HRESULT_SUCCEEDED(
text_range_provider->GetText(-1, text_content.Receive()));
EXPECT_STREQ(L"more texteven more text", text_content);
text_content.Reset();
// Verify no moves occur since the end is already at the end of the document
ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit(
TextPatternRangeEndpoint_End, textunit, /*count*/ 5, &count));
ASSERT_EQ(0, count);
ASSERT_HRESULT_SUCCEEDED(
text_range_provider->GetText(-1, text_content.Receive()));
EXPECT_STREQ(L"more texteven more text", text_content);
text_content.Reset();
// Move the end before the start
ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit(
TextPatternRangeEndpoint_End, textunit, /*count*/ -4, &count));
ASSERT_EQ(-1, count);
ASSERT_HRESULT_SUCCEEDED(
text_range_provider->GetText(-1, text_content.Receive()));
EXPECT_STREQ(L"", text_content);
text_content.Reset();
// Move the end back to the end of the document. The text content
// should now include the entire document since end was previously
// moved before start.
ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit(
TextPatternRangeEndpoint_End, textunit, /*count*/ 1, &count));
ASSERT_EQ(1, count);
ASSERT_HRESULT_SUCCEEDED(
text_range_provider->GetText(-1, text_content.Receive()));
EXPECT_STREQ(L"some textmore texteven more text", text_content);
text_content.Reset();
// Move the start point to the end
ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit(
TextPatternRangeEndpoint_Start, textunit, /*count*/ 3, &count));
ASSERT_EQ(1, count);
ASSERT_HRESULT_SUCCEEDED(
text_range_provider->GetText(-1, text_content.Receive()));
EXPECT_STREQ(L"", text_content);
text_content.Reset();
// Move the start point back to the beginning
ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit(
TextPatternRangeEndpoint_Start, textunit, /*count*/ -3, &count));
ASSERT_EQ(-1, count);
ASSERT_HRESULT_SUCCEEDED(
text_range_provider->GetText(-1, text_content.Receive()));
EXPECT_STREQ(L"some textmore texteven more text", text_content);
text_content.Reset();
}
}
TEST_F(AXPlatformNodeTextRangeProviderTest, TestITextRangeProviderCompare) { TEST_F(AXPlatformNodeTextRangeProviderTest, TestITextRangeProviderCompare) {
ui::AXNodeData text_data; ui::AXNodeData text_data;
text_data.id = 2; text_data.id = 2;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment