diff --git a/ci/licenses_golden/excluded_files b/ci/licenses_golden/excluded_files index 7147bcc6eb607..b5892700c70ad 100644 --- a/ci/licenses_golden/excluded_files +++ b/ci/licenses_golden/excluded_files @@ -358,6 +358,8 @@ ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_base_unittest.cc ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_mac_unittest.h ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_mac_unittest.mm +../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_textprovider_win_unittest.cc +../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_textrangeprovider_win_unittest.cc ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_unittest.cc ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_unittest.h ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_win_unittest.cc diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 5a7f8c8018777..ea6b7262c917f 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -6054,6 +6054,11 @@ ORIGIN: ../../../flutter/third_party/accessibility/ax/platform/ax_fragment_root_ ORIGIN: ../../../flutter/third_party/accessibility/ax/platform/ax_fragment_root_win.h + ../../../flutter/third_party/accessibility/LICENSE ORIGIN: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_delegate_utils_win.cc + ../../../flutter/third_party/accessibility/LICENSE ORIGIN: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_delegate_utils_win.h + ../../../flutter/third_party/accessibility/LICENSE +ORIGIN: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_textprovider_win.cc + ../../../flutter/third_party/accessibility/LICENSE +ORIGIN: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_textprovider_win.h + ../../../flutter/third_party/accessibility/LICENSE +ORIGIN: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_textrangeprovider_win.cc + ../../../flutter/third_party/accessibility/LICENSE +ORIGIN: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_textrangeprovider_win.h + ../../../flutter/third_party/accessibility/LICENSE +ORIGIN: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_tree_manager.h + ../../../flutter/third_party/accessibility/LICENSE ORIGIN: ../../../flutter/third_party/accessibility/base/win/scoped_safearray.h + ../../../flutter/third_party/accessibility/LICENSE TYPE: LicenseType.bsd FILE: ../../../flutter/third_party/accessibility/ax/ax_active_popup.cc @@ -6073,6 +6078,11 @@ FILE: ../../../flutter/third_party/accessibility/ax/platform/ax_fragment_root_wi FILE: ../../../flutter/third_party/accessibility/ax/platform/ax_fragment_root_win.h FILE: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_delegate_utils_win.cc FILE: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_delegate_utils_win.h +FILE: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_textprovider_win.cc +FILE: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_textprovider_win.h +FILE: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_textrangeprovider_win.cc +FILE: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_textrangeprovider_win.h +FILE: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_tree_manager.h FILE: ../../../flutter/third_party/accessibility/base/win/scoped_safearray.h ---------------------------------------------------------------------------------------------------- Copyright 2019 The Chromium Authors. All rights reserved. diff --git a/third_party/accessibility/BUILD.gn b/third_party/accessibility/BUILD.gn index eb217af52fe73..19ba407c38220 100644 --- a/third_party/accessibility/BUILD.gn +++ b/third_party/accessibility/BUILD.gn @@ -91,6 +91,8 @@ if (enable_unittests) { if (is_win) { sources += [ "ax/platform/ax_fragment_root_win_unittest.cc", + "ax/platform/ax_platform_node_textprovider_win_unittest.cc", + "ax/platform/ax_platform_node_textrangeprovider_win_unittest.cc", "ax/platform/ax_platform_node_win_unittest.cc", "base/win/dispatch_stub.cc", "base/win/dispatch_stub.h", diff --git a/third_party/accessibility/ax/BUILD.gn b/third_party/accessibility/ax/BUILD.gn index 3c234b72344a7..d03e3cde122de 100644 --- a/third_party/accessibility/ax/BUILD.gn +++ b/third_party/accessibility/ax/BUILD.gn @@ -17,6 +17,7 @@ source_set("ax") { "platform/ax_platform_node_delegate.h", "platform/ax_platform_node_delegate_base.cc", "platform/ax_platform_node_delegate_base.h", + "platform/ax_platform_tree_manager.h", "platform/ax_unique_id.cc", "platform/ax_unique_id.h", "platform/compute_attributes.cc", @@ -92,6 +93,10 @@ source_set("ax") { "platform/ax_fragment_root_win.h", "platform/ax_platform_node_delegate_utils_win.cc", "platform/ax_platform_node_delegate_utils_win.h", + "platform/ax_platform_node_textprovider_win.cc", + "platform/ax_platform_node_textprovider_win.h", + "platform/ax_platform_node_textrangeprovider_win.cc", + "platform/ax_platform_node_textrangeprovider_win.h", "platform/ax_platform_node_win.cc", "platform/ax_platform_node_win.h", "platform/uia_registrar_win.cc", diff --git a/third_party/accessibility/ax/ax_node.cc b/third_party/accessibility/ax/ax_node.cc index 96491771482ae..a58d1913efe93 100644 --- a/third_party/accessibility/ax/ax_node.cc +++ b/third_party/accessibility/ax/ax_node.cc @@ -11,6 +11,8 @@ #include "ax_role_properties.h" #include "ax_table_info.h" #include "ax_tree.h" +#include "ax_tree_manager.h" +#include "ax_tree_manager_map.h" #include "base/color_utils.h" #include "base/string_utils.h" @@ -1197,6 +1199,30 @@ bool AXNode::IsEmbeddedGroup() const { return ui::IsSetLike(parent()->data().role); } +AXNode* AXNode::GetLowestPlatformAncestor() const { + AXNode* current_node = const_cast(this); + AXNode* lowest_unignored_node = current_node; + for (; lowest_unignored_node && lowest_unignored_node->IsIgnored(); + lowest_unignored_node = lowest_unignored_node->parent()) { + } + + // `highest_leaf_node` could be nullptr. + AXNode* highest_leaf_node = lowest_unignored_node; + // For the purposes of this method, a leaf node does not include leaves in the + // internal accessibility tree, only in the platform exposed tree. + for (AXNode* ancestor_node = lowest_unignored_node; ancestor_node; + ancestor_node = ancestor_node->GetUnignoredParent()) { + if (ancestor_node->IsLeaf()) + highest_leaf_node = ancestor_node; + } + if (highest_leaf_node) + return highest_leaf_node; + + if (lowest_unignored_node) + return lowest_unignored_node; + return current_node; +} + AXNode* AXNode::GetTextFieldAncestor() const { AXNode* parent = GetUnignoredParent(); @@ -1210,4 +1236,69 @@ AXNode* AXNode::GetTextFieldAncestor() const { return nullptr; } +bool AXNode::IsDescendantOfCrossingTreeBoundary(const AXNode* ancestor) const { + if (!ancestor) + return false; + if (this == ancestor) + return true; + if (const AXNode* parent = GetParentCrossingTreeBoundary()) + return parent->IsDescendantOfCrossingTreeBoundary(ancestor); + return false; +} + +AXNode* AXNode::GetParentCrossingTreeBoundary() const { + BASE_DCHECK(!tree_->GetTreeUpdateInProgressState()); + if (parent_) + return parent_; + const AXTreeManager* manager = + AXTreeManagerMap::GetInstance().GetManager(tree_->GetAXTreeID()); + if (manager) + return manager->GetParentNodeFromParentTreeAsAXNode(); + return nullptr; +} + +AXTree::Selection AXNode::GetUnignoredSelection() const { + BASE_DCHECK(tree()) + << "Cannot retrieve the current selection if the node is not " + "attached to an accessibility tree.\n" + << *this; + AXTree::Selection selection = tree()->GetUnignoredSelection(); + + // "selection.anchor_offset" and "selection.focus_ofset" might need to be + // adjusted if the anchor or the focus nodes include ignored children. + // + // TODO(nektar): Move this logic into its own "AXSelection" class and cache + // the result for faster reuse. + const AXNode* anchor = tree()->GetFromId(selection.anchor_object_id); + if (anchor && !anchor->IsLeaf()) { + BASE_DCHECK(selection.anchor_offset >= 0); + if (static_cast(selection.anchor_offset) < + anchor->children().size()) { + const AXNode* anchor_child = anchor->children()[selection.anchor_offset]; + BASE_DCHECK(anchor_child); + selection.anchor_offset = + static_cast(anchor_child->GetUnignoredIndexInParent()); + } else { + selection.anchor_offset = + static_cast(anchor->GetUnignoredChildCount()); + } + } + + const AXNode* focus = tree()->GetFromId(selection.focus_object_id); + if (focus && !focus->IsLeaf()) { + BASE_DCHECK(selection.focus_offset >= 0); + if (static_cast(selection.focus_offset) < + focus->children().size()) { + const AXNode* focus_child = focus->children()[selection.focus_offset]; + BASE_DCHECK(focus_child); + selection.focus_offset = + static_cast(focus_child->GetUnignoredIndexInParent()); + } else { + selection.focus_offset = + static_cast(focus->GetUnignoredChildCount()); + } + } + return selection; +} + } // namespace ui diff --git a/third_party/accessibility/ax/ax_node.h b/third_party/accessibility/ax/ax_node.h index 0cae10f933da5..45fdb42e1e405 100644 --- a/third_party/accessibility/ax/ax_node.h +++ b/third_party/accessibility/ax/ax_node.h @@ -120,6 +120,10 @@ class AX_EXPORT AXNode final { size_t GetUnignoredChildCount() const; AXNode* GetUnignoredChildAtIndex(size_t index) const; AXNode* GetUnignoredParent() const; + // Gets the unignored selection from the accessibility tree, meaning the + // selection whose endpoints are on unignored nodes. (An "ignored" node is a + // node that is not exposed to platform APIs: See `IsIgnored`.) + OwnerTree::Selection GetUnignoredSelection() const; size_t GetUnignoredIndexInParent() const; size_t GetIndexInParent() const; AXNode* GetFirstUnignoredChild() const; @@ -191,6 +195,9 @@ class AX_EXPORT AXNode final { // Return true if this object is equal to or a descendant of |ancestor|. bool IsDescendantOf(const AXNode* ancestor) const; + bool IsDescendantOfCrossingTreeBoundary(const AXNode* ancestor) const; + AXNode* GetParentCrossingTreeBoundary() const; + // Gets the text offsets where new lines start either from the node's data or // by computing them and caching the result. std::vector GetOrComputeLineStartOffsets(); @@ -436,6 +443,12 @@ class AX_EXPORT AXNode final { // Finds and returns a pointer to ordered set containing node. AXNode* GetOrderedSet() const; + // If this node is exposed to the platform's accessibility layer, returns this + // node. Otherwise, returns the lowest ancestor that is exposed to the + // platform. (See `IsLeaf` and `IsIgnored` for information on what is + // exposed to platform APIs.) + AXNode* GetLowestPlatformAncestor() const; + private: // Computes the text offset where each line starts by traversing all child // leaf nodes. diff --git a/third_party/accessibility/ax/ax_node_position.cc b/third_party/accessibility/ax/ax_node_position.cc index 41308933f3090..733e5b093c3d7 100644 --- a/third_party/accessibility/ax/ax_node_position.cc +++ b/third_party/accessibility/ax/ax_node_position.cc @@ -20,6 +20,16 @@ AXEmbeddedObjectBehavior g_ax_embedded_object_behavior = AXEmbeddedObjectBehavior::kSuppressCharacter; #endif // defined(OS_WIN) +ScopedAXEmbeddedObjectBehaviorSetter::ScopedAXEmbeddedObjectBehaviorSetter( + AXEmbeddedObjectBehavior behavior) { + prev_behavior_ = g_ax_embedded_object_behavior; + g_ax_embedded_object_behavior = behavior; +} + +ScopedAXEmbeddedObjectBehaviorSetter::~ScopedAXEmbeddedObjectBehaviorSetter() { + g_ax_embedded_object_behavior = prev_behavior_; +} + // static AXNodePosition::AXPositionInstance AXNodePosition::CreatePosition( const AXNode& node, @@ -205,8 +215,9 @@ std::u16string AXNodePosition::GetText() const { ax::mojom::StringAttribute::kName); } - for (int i = 0; i < AnchorChildCount(); ++i) + for (int i = 0; i < AnchorChildCount(); ++i) { text += CreateChildPositionAt(i)->GetText(); + } return text; } @@ -263,8 +274,9 @@ int AXNodePosition::MaxTextOffset() const { } int text_length = 0; - for (int i = 0; i < AnchorChildCount(); ++i) + for (int i = 0; i < AnchorChildCount(); ++i) { text_length += CreateChildPositionAt(i)->MaxTextOffset(); + } return text_length; } @@ -286,9 +298,10 @@ bool AXNodePosition::IsInLineBreakingObject() const { if (IsNullPosition()) return false; BASE_DCHECK(GetAnchor()); - return GetAnchor()->data().GetBoolAttribute( - ax::mojom::BoolAttribute::kIsLineBreakingObject) && - !GetAnchor()->IsInListMarker(); + return (GetAnchor()->data().GetBoolAttribute( + ax::mojom::BoolAttribute::kIsLineBreakingObject) && + !GetAnchor()->IsInListMarker()) || + GetAnchor()->data().role == ax::mojom::Role::kLineBreak; } ax::mojom::Role AXNodePosition::GetAnchorRole() const { diff --git a/third_party/accessibility/ax/ax_position.h b/third_party/accessibility/ax/ax_position.h index bf05933828ec6..c426b46047af4 100644 --- a/third_party/accessibility/ax/ax_position.h +++ b/third_party/accessibility/ax/ax_position.h @@ -105,6 +105,16 @@ enum class AXEmbeddedObjectBehavior { // overridden for testing. AX_EXPORT extern AXEmbeddedObjectBehavior g_ax_embedded_object_behavior; +class AX_EXPORT ScopedAXEmbeddedObjectBehaviorSetter { + public: + explicit ScopedAXEmbeddedObjectBehaviorSetter( + AXEmbeddedObjectBehavior behavior); + ~ScopedAXEmbeddedObjectBehaviorSetter(); + + private: + AXEmbeddedObjectBehavior prev_behavior_; +}; + // Forward declarations. template class AXPosition; @@ -324,8 +334,9 @@ class AXPosition { BASE_DCHECK(GetAnchor()); // If this position is anchored to an ignored node, then consider this // position to be ignored. - if (GetAnchor()->IsIgnored()) + if (GetAnchor()->IsIgnored()) { return true; + } switch (kind_) { case AXPositionKind::NULL_POSITION: @@ -372,8 +383,9 @@ class AXPosition { // If the corresponding leaf position is ignored, the current text // offset will point to ignored text. Therefore, consider this position // to be ignored. - if (!IsLeaf()) + if (!IsLeaf()) { return AsLeafTreePosition()->IsIgnored(); + } return false; } } @@ -417,8 +429,9 @@ class AXPosition { (child_index_ >= 0 && child_index_ <= AnchorChildCount())) && !IsInDescendantOfEmptyObject(); case AXPositionKind::TEXT_POSITION: - if (!GetAnchor() || IsInDescendantOfEmptyObject()) + if (!GetAnchor() || IsInDescendantOfEmptyObject()) { return false; + } // For performance reasons we skip any validation of the text offset // that involves retrieving the anchor's text, if the offset is set to @@ -1029,8 +1042,9 @@ class AXPosition { const AXNodeType* ancestor_anchor, ax::mojom::MoveDirection move_direction = ax::mojom::MoveDirection::kForward) const { - if (!ancestor_anchor) + if (!ancestor_anchor) { return CreateNullPosition(); + } AXPositionInstance ancestor_position = Clone(); while (!ancestor_position->IsNullPosition() && @@ -1285,8 +1299,9 @@ class AXPosition { } AXPositionInstance AsLeafTextPosition() const { - if (IsNullPosition() || IsLeaf()) + if (IsNullPosition() || IsLeaf()) { return AsTextPosition(); + } // Adjust the text offset. // No need to check for "before text" positions here because they are only @@ -1316,7 +1331,7 @@ class AXPosition { child_position->affinity_ = ax::mojom::TextAffinity::kUpstream; break; } - child_position = text_position->CreateChildPositionAt(i); + child_position = std::move(text_position->CreateChildPositionAt(i)); adjusted_offset -= max_text_offset_in_parent; } @@ -1902,7 +1917,7 @@ class AXPosition { // the same as the one that would have been computed if the original // position were at the start of the inline text box for "Line two". const int max_text_offset = MaxTextOffset(); - const int max_text_offset_in_parent = + int max_text_offset_in_parent = IsEmbeddedObjectInParent() ? 1 : max_text_offset; int parent_offset = AnchorTextOffsetInParent(); ax::mojom::TextAffinity parent_affinity = affinity_; @@ -1935,6 +1950,14 @@ class AXPosition { parent_affinity = ax::mojom::TextAffinity::kDownstream; } + // This dummy position serves to retrieve the max text offset of the + // anchor-node in which we want to create the parent position. + AXPositionInstance dummy_position = + CreateTextPosition(tree_id, parent_id, 0, parent_affinity); + max_text_offset_in_parent = dummy_position->MaxTextOffset(); + if (parent_offset > max_text_offset_in_parent) { + parent_offset = max_text_offset_in_parent; + } AXPositionInstance parent_position = CreateTextPosition( tree_id, parent_id, parent_offset, parent_affinity); @@ -2061,6 +2084,7 @@ class AXPosition { BASE_DCHECK(text_position->text_offset_ >= 0); return text_position; } + text_position = text_position->CreateNextLeafTextPosition(); while (!text_position->IsNullPosition() && (text_position->IsIgnored() || !text_position->MaxTextOffset())) { diff --git a/third_party/accessibility/ax/ax_range.h b/third_party/accessibility/ax/ax_range.h index d7c97687f9f80..79bcc9f1fbd78 100644 --- a/third_party/accessibility/ax/ax_range.h +++ b/third_party/accessibility/ax/ax_range.h @@ -11,6 +11,7 @@ #include #include +#include "ax_clipping_behavior.h" #include "ax_enums.h" #include "ax_offscreen_result.h" #include "ax_role_properties.h" @@ -35,6 +36,7 @@ class AXRangeRectDelegate { AXNode::AXID node_id, int start_offset, int end_offset, + ui::AXClippingBehavior clipping_behavior, AXOffscreenResult* offscreen_result) = 0; virtual gfx::Rect GetBoundsRect(AXTreeID tree_id, AXNode::AXID node_id, @@ -392,7 +394,8 @@ class AXRange { current_line_start->tree_id(), current_line_start->anchor_id(), current_line_start->text_offset(), - current_line_end->text_offset(), &offscreen_result) + current_line_end->text_offset(), + ui::AXClippingBehavior::kUnclipped, &offscreen_result) : delegate->GetBoundsRect(current_line_start->tree_id(), current_line_start->anchor_id(), &offscreen_result); diff --git a/third_party/accessibility/ax/ax_range_unittest.cc b/third_party/accessibility/ax/ax_range_unittest.cc index 06cc8f8087e02..01d9e2404f16f 100644 --- a/third_party/accessibility/ax/ax_range_unittest.cc +++ b/third_party/accessibility/ax/ax_range_unittest.cc @@ -68,6 +68,7 @@ class TestAXRangeScreenRectDelegate : public AXRangeRectDelegate { AXNode::AXID node_id, int start_offset, int end_offset, + ui::AXClippingBehavior clipping_behavior, AXOffscreenResult* offscreen_result) override { if (tree_manager_->GetTreeID() != tree_id) return gfx::Rect(); @@ -80,7 +81,7 @@ class TestAXRangeScreenRectDelegate : public AXRangeRectDelegate { TestAXNodeHelper::GetOrCreate(tree_manager_->GetTree(), node); return wrapper->GetInnerTextRangeBoundsRect( start_offset, end_offset, AXCoordinateSystem::kScreenDIPs, - AXClippingBehavior::kClipped, offscreen_result); + clipping_behavior, offscreen_result); } gfx::Rect GetBoundsRect(AXTreeID tree_id, diff --git a/third_party/accessibility/ax/ax_tree_manager.h b/third_party/accessibility/ax/ax_tree_manager.h index 7a3c3bb72ed41..3b4476ae7323c 100644 --- a/third_party/accessibility/ax/ax_tree_manager.h +++ b/third_party/accessibility/ax/ax_tree_manager.h @@ -7,6 +7,7 @@ #include "ax_export.h" #include "ax_node.h" +#include "ax_tree.h" #include "ax_tree_id.h" namespace ui { @@ -16,6 +17,8 @@ namespace ui { // trees). class AX_EXPORT AXTreeManager { public: + virtual ~AXTreeManager() = default; + // Returns the AXNode with the given |node_id| from the tree that has the // given |tree_id|. This allows for callers to access nodes outside of their // own tree. Returns nullptr if |tree_id| or |node_id| is not found. @@ -40,6 +43,8 @@ class AX_EXPORT AXTreeManager { // hosts the current tree. Returns nullptr if this tree doesn't have a parent // tree. virtual AXNode* GetParentNodeFromParentTreeAsAXNode() const = 0; + + virtual AXTree* GetTree() const = 0; }; } // namespace ui diff --git a/third_party/accessibility/ax/platform/ax_platform_node_delegate.h b/third_party/accessibility/ax/platform/ax_platform_node_delegate.h index 2bf9c2ff48c8f..4a1bdc5c3e83e 100644 --- a/third_party/accessibility/ax/platform/ax_platform_node_delegate.h +++ b/third_party/accessibility/ax/platform/ax_platform_node_delegate.h @@ -416,6 +416,14 @@ class AX_EXPORT AXPlatformNodeDelegate { // element. The default value should be false if not in testing mode. virtual bool ShouldIgnoreHoveredStateForTesting() = 0; + // If this object is exposed to the platform's accessibility layer, returns + // this object. Otherwise, returns the platform leaf or lowest unignored + // ancestor under which this object is found. + // + // (An ignored node means that the node should not be exposed to platform + // APIs: See `IsIgnored`.) + virtual gfx::NativeViewAccessible GetLowestPlatformAncestor() const = 0; + // Creates a string representation of this delegate's data. std::string ToString() { return GetData().ToString(); } diff --git a/third_party/accessibility/ax/platform/ax_platform_node_delegate_base.cc b/third_party/accessibility/ax/platform/ax_platform_node_delegate_base.cc index f91d4935bc038..501c75096fa10 100644 --- a/third_party/accessibility/ax/platform/ax_platform_node_delegate_base.cc +++ b/third_party/accessibility/ax/platform/ax_platform_node_delegate_base.cc @@ -87,6 +87,32 @@ gfx::NativeViewAccessible AXPlatformNodeDelegateBase::GetParent() { return nullptr; } +gfx::NativeViewAccessible +AXPlatformNodeDelegateBase::GetLowestPlatformAncestor() const { + AXPlatformNodeDelegateBase* current_delegate = + const_cast(this); + AXPlatformNodeDelegateBase* lowest_unignored_delegate = current_delegate; + + // `highest_leaf_delegate` could be nullptr. + AXPlatformNodeDelegateBase* highest_leaf_delegate = lowest_unignored_delegate; + // For the purposes of this method, a leaf node does not include leaves in the + // internal accessibility tree, only in the platform exposed tree. + for (AXPlatformNodeDelegateBase* ancestor_delegate = + lowest_unignored_delegate; + ancestor_delegate; + ancestor_delegate = static_cast( + ancestor_delegate->GetParentDelegate())) { + if (ancestor_delegate->IsLeaf()) + highest_leaf_delegate = ancestor_delegate; + } + if (highest_leaf_delegate) + return highest_leaf_delegate->GetNativeViewAccessible(); + + if (lowest_unignored_delegate) + return lowest_unignored_delegate->GetNativeViewAccessible(); + return current_delegate->GetNativeViewAccessible(); +} + int AXPlatformNodeDelegateBase::GetChildCount() const { return 0; } diff --git a/third_party/accessibility/ax/platform/ax_platform_node_delegate_base.h b/third_party/accessibility/ax/platform/ax_platform_node_delegate_base.h index 58e8d086d95b3..c51decc3640f2 100644 --- a/third_party/accessibility/ax/platform/ax_platform_node_delegate_base.h +++ b/third_party/accessibility/ax/platform/ax_platform_node_delegate_base.h @@ -51,6 +51,14 @@ class AX_EXPORT AXPlatformNodeDelegateBase : public AXPlatformNodeDelegate { // be a native accessible object implemented by another class. gfx::NativeViewAccessible GetParent() override; + // If this object is exposed to the platform's accessibility layer, returns + // this object. Otherwise, returns the platform leaf or lowest unignored + // ancestor under which this object is found. + // + // (An ignored node means that the node should not be exposed to platform + // APIs: See `IsIgnored`.) + gfx::NativeViewAccessible GetLowestPlatformAncestor() const override; + // Get the index in parent. Typically this is the AXNode's index_in_parent_. int GetIndexInParent() override; diff --git a/third_party/accessibility/ax/platform/ax_platform_node_textprovider_win.cc b/third_party/accessibility/ax/platform/ax_platform_node_textprovider_win.cc new file mode 100644 index 0000000000000..7c373df99ee1a --- /dev/null +++ b/third_party/accessibility/ax/platform/ax_platform_node_textprovider_win.cc @@ -0,0 +1,354 @@ +// Copyright 2019 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 "ax/platform/ax_platform_node_textprovider_win.h" + +#include + +#include "base/win/scoped_safearray.h" + +#include "ax/platform/ax_platform_node_textrangeprovider_win.h" + +#define UIA_VALIDATE_TEXTPROVIDER_CALL() \ + if (!owner()->GetDelegate()) \ + return UIA_E_ELEMENTNOTAVAILABLE; +#define UIA_VALIDATE_TEXTPROVIDER_CALL_1_ARG(arg) \ + if (!owner()->GetDelegate()) \ + return UIA_E_ELEMENTNOTAVAILABLE; \ + if (!arg) \ + return E_INVALIDARG; + +namespace ui { + +AXPlatformNodeTextProviderWin::AXPlatformNodeTextProviderWin() {} + +AXPlatformNodeTextProviderWin::~AXPlatformNodeTextProviderWin() {} + +// static +AXPlatformNodeTextProviderWin* AXPlatformNodeTextProviderWin::Create( + AXPlatformNodeWin* owner) { + CComObject* text_provider = nullptr; + if (SUCCEEDED(CComObject::CreateInstance( + &text_provider))) { + BASE_DCHECK(text_provider); + text_provider->owner_ = owner; + text_provider->AddRef(); + return text_provider; + } + + return nullptr; +} + +// static +void AXPlatformNodeTextProviderWin::CreateIUnknown(AXPlatformNodeWin* owner, + IUnknown** unknown) { + Microsoft::WRL::ComPtr text_provider( + Create(owner)); + if (text_provider) + *unknown = text_provider.Detach(); +} + +// +// ITextProvider methods. +// + +HRESULT AXPlatformNodeTextProviderWin::GetSelection(SAFEARRAY** selection) { + UIA_VALIDATE_TEXTPROVIDER_CALL(); + + *selection = nullptr; + + AXPlatformNodeDelegate* delegate = owner()->GetDelegate(); + AXTree::Selection unignored_selection = delegate->GetUnignoredSelection(); + + AXPlatformNode* anchor_object = + delegate->GetFromNodeID(unignored_selection.anchor_object_id); + AXPlatformNode* focus_object = + delegate->GetFromNodeID(unignored_selection.focus_object_id); + + // anchor_offset corresponds to the selection start index + // and focus_offset is where the selection ends. + auto start_offset = unignored_selection.anchor_offset; + auto end_offset = unignored_selection.focus_offset; + + // If there's no selected object, return success and don't fill the SAFEARRAY. + if (!anchor_object || !focus_object) + return S_OK; + + AXNodePosition::AXPositionInstance start = + anchor_object->GetDelegate()->CreateTextPositionAt(start_offset); + AXNodePosition::AXPositionInstance end = + focus_object->GetDelegate()->CreateTextPositionAt(end_offset); + + BASE_DCHECK(!start->IsNullPosition()); + BASE_DCHECK(!end->IsNullPosition()); + + // Reverse start and end if the selection goes backwards + if (*start > *end) + std::swap(start, end); + + Microsoft::WRL::ComPtr text_range_provider = + AXPlatformNodeTextRangeProviderWin::CreateTextRangeProvider( + std::move(start), std::move(end)); + if (&text_range_provider == nullptr) + return E_OUTOFMEMORY; + + // Since we don't support disjoint text ranges, the SAFEARRAY returned + // will always have one element + base::win::ScopedSafearray selections_to_return( + SafeArrayCreateVector(VT_UNKNOWN /* element type */, 0 /* lower bound */, + 1 /* number of elements */)); + + if (!selections_to_return.Get()) + return E_OUTOFMEMORY; + + LONG index = 0; + HRESULT hr = SafeArrayPutElement(selections_to_return.Get(), &index, + text_range_provider.Get()); + BASE_DCHECK(SUCCEEDED(hr)); + + // Since BASE_DCHECK only happens in debug builds, return immediately to + // ensure that we're not leaking the SAFEARRAY on release builds + if (FAILED(hr)) + return E_FAIL; + + *selection = selections_to_return.Release(); + + return S_OK; +} + +HRESULT AXPlatformNodeTextProviderWin::GetVisibleRanges( + SAFEARRAY** visible_ranges) { + UIA_VALIDATE_TEXTPROVIDER_CALL(); + + const AXPlatformNodeDelegate* delegate = owner()->GetDelegate(); + + // Get the Clipped Frame Bounds of the current node, not from the root, + // so if this node is wrapped with overflow styles it will have the + // correct bounds + const gfx::Rect frame_rect = delegate->GetBoundsRect( + AXCoordinateSystem::kFrame, AXClippingBehavior::kClipped); + + const auto start = delegate->CreateTextPositionAt(0); + const auto end = start->CreatePositionAtEndOfAnchor(); + BASE_DCHECK(start->GetAnchor() == end->GetAnchor()); + + // SAFEARRAYs are not dynamic, so fill the visible ranges in a vector + // and then transfer to an appropriately-sized SAFEARRAY + std::vector> ranges; + + auto current_line_start = start->Clone(); + while (!current_line_start->IsNullPosition() && *current_line_start < *end) { + auto current_line_end = current_line_start->CreateNextLineEndPosition( + AXBoundaryBehavior::CrossBoundary); + if (current_line_end->IsNullPosition() || *current_line_end > *end) + current_line_end = end->Clone(); + + gfx::Rect current_rect = delegate->GetInnerTextRangeBoundsRect( + current_line_start->text_offset(), current_line_end->text_offset(), + AXCoordinateSystem::kFrame, AXClippingBehavior::kUnclipped); + + if (frame_rect.Contains(current_rect)) { + Microsoft::WRL::ComPtr text_range_provider = + AXPlatformNodeTextRangeProviderWin::CreateTextRangeProvider( + current_line_start->Clone(), current_line_end->Clone()); + + ranges.emplace_back(text_range_provider); + } + + current_line_start = current_line_start->CreateNextLineStartPosition( + AXBoundaryBehavior::CrossBoundary); + } + + base::win::ScopedSafearray scoped_visible_ranges( + SafeArrayCreateVector(VT_UNKNOWN /* element type */, 0 /* lower bound */, + ranges.size() /* number of elements */)); + + if (!scoped_visible_ranges.Get()) + return E_OUTOFMEMORY; + + LONG index = 0; + for (Microsoft::WRL::ComPtr& current_provider : ranges) { + HRESULT hr = SafeArrayPutElement(scoped_visible_ranges.Get(), &index, + current_provider.Get()); + BASE_DCHECK(SUCCEEDED(hr)); + + // Since BASE_DCHECK only happens in debug builds, return immediately to + // ensure that we're not leaking the SAFEARRAY on release builds + if (FAILED(hr)) + return E_FAIL; + + ++index; + } + + *visible_ranges = scoped_visible_ranges.Release(); + + return S_OK; +} + +HRESULT AXPlatformNodeTextProviderWin::RangeFromChild( + IRawElementProviderSimple* child, + ITextRangeProvider** range) { + UIA_VALIDATE_TEXTPROVIDER_CALL_1_ARG(child); + + *range = nullptr; + + Microsoft::WRL::ComPtr child_platform_node; + if (!SUCCEEDED(child->QueryInterface(IID_PPV_ARGS(&child_platform_node)))) + return UIA_E_INVALIDOPERATION; + + if (!owner()->IsDescendant(child_platform_node.Get())) + return E_INVALIDARG; + + *range = GetRangeFromChild(owner(), child_platform_node.Get()); + + return S_OK; +} + +HRESULT AXPlatformNodeTextProviderWin::RangeFromPoint( + UiaPoint uia_point, + ITextRangeProvider** range) { + UIA_VALIDATE_TEXTPROVIDER_CALL(); + *range = nullptr; + + gfx::Point point(uia_point.x, uia_point.y); + // Retrieve the closest accessibility node. No coordinate unit conversion is + // needed, hit testing input is also in screen coordinates. + + AXPlatformNodeWin* nearest_node = + static_cast(owner()->NearestLeafToPoint(point)); + BASE_DCHECK(nearest_node); + BASE_DCHECK(nearest_node->IsLeaf()); + + AXNodePosition::AXPositionInstance start, end; + start = nearest_node->GetDelegate()->CreateTextPositionAt( + nearest_node->NearestTextIndexToPoint(point)); + BASE_DCHECK(!start->IsNullPosition()); + end = start->Clone(); + + *range = AXPlatformNodeTextRangeProviderWin::CreateTextRangeProvider( + std::move(start), std::move(end)); + return S_OK; +} + +HRESULT AXPlatformNodeTextProviderWin::get_DocumentRange( + ITextRangeProvider** range) { + UIA_VALIDATE_TEXTPROVIDER_CALL(); + + // Get range from child, where child is the current node. In other words, + // getting the text range of the current owner AxPlatformNodeWin node. + *range = GetRangeFromChild(owner(), owner()); + + return S_OK; +} + +HRESULT AXPlatformNodeTextProviderWin::get_SupportedTextSelection( + enum SupportedTextSelection* text_selection) { + UIA_VALIDATE_TEXTPROVIDER_CALL(); + + *text_selection = SupportedTextSelection_Single; + return S_OK; +} + +// +// ITextEditProvider methods. +// + +HRESULT AXPlatformNodeTextProviderWin::GetActiveComposition( + ITextRangeProvider** range) { + UIA_VALIDATE_TEXTPROVIDER_CALL(); + + *range = nullptr; + return GetTextRangeProviderFromActiveComposition(range); +} + +HRESULT AXPlatformNodeTextProviderWin::GetConversionTarget( + ITextRangeProvider** range) { + UIA_VALIDATE_TEXTPROVIDER_CALL(); + + *range = nullptr; + return GetTextRangeProviderFromActiveComposition(range); +} + +ITextRangeProvider* AXPlatformNodeTextProviderWin::GetRangeFromChild( + ui::AXPlatformNodeWin* ancestor, + ui::AXPlatformNodeWin* descendant) { + BASE_DCHECK(ancestor); + BASE_DCHECK(descendant); + BASE_DCHECK(descendant->GetDelegate()); + BASE_DCHECK(ancestor->IsDescendant(descendant)); + + // Start and end should be leaf text positions that span the beginning and end + // of text content within a node. The start position should be the directly + // first child and the end position should be the deepest last child node. + AXNodePosition::AXPositionInstance start = + descendant->GetDelegate()->CreateTextPositionAt(0)->AsLeafTextPosition(); + + AXNodePosition::AXPositionInstance end; + if (descendant->GetChildCount() == 0) { + end = descendant->GetDelegate() + ->CreateTextPositionAt(0) + ->CreatePositionAtEndOfAnchor() + ->AsLeafTextPosition(); + } else { + AXPlatformNodeBase* deepest_last_child = descendant->GetLastChild(); + while (deepest_last_child && deepest_last_child->GetChildCount() > 0) + deepest_last_child = deepest_last_child->GetLastChild(); + + end = deepest_last_child->GetDelegate() + ->CreateTextPositionAt(0) + ->CreatePositionAtEndOfAnchor() + ->AsLeafTextPosition(); + } + + return AXPlatformNodeTextRangeProviderWin::CreateTextRangeProvider( + std::move(start), std::move(end)); +} + +ITextRangeProvider* AXPlatformNodeTextProviderWin::CreateDegenerateRangeAtStart( + ui::AXPlatformNodeWin* node) { + BASE_DCHECK(node); + BASE_DCHECK(node->GetDelegate()); + + // Create a degenerate range positioned at the node's start. + AXNodePosition::AXPositionInstance start, end; + start = node->GetDelegate()->CreateTextPositionAt(0)->AsLeafTextPosition(); + end = start->Clone(); + return AXPlatformNodeTextRangeProviderWin::CreateTextRangeProvider( + std::move(start), std::move(end)); +} + +ui::AXPlatformNodeWin* AXPlatformNodeTextProviderWin::owner() const { + return owner_.Get(); +} + +HRESULT +AXPlatformNodeTextProviderWin::GetTextRangeProviderFromActiveComposition( + ITextRangeProvider** range) { + *range = nullptr; + // We fetch the start and end offset of an active composition only if + // this object has focus and TSF is in composition mode. + // The offsets here refer to the character positions in a plain text + // view of the DOM tree. Ex: if the active composition in an element + // has "abc" then the range will be (0,3) in both TSF and accessibility + if ((AXPlatformNode::FromNativeViewAccessible( + owner()->GetDelegate()->GetFocus()) == + static_cast(owner())) && + owner()->HasActiveComposition()) { + gfx::Range active_composition_offset = + owner()->GetActiveCompositionOffsets(); + AXNodePosition::AXPositionInstance start = + owner()->GetDelegate()->CreateTextPositionAt( + /*offset*/ active_composition_offset.start()); + AXNodePosition::AXPositionInstance end = + owner()->GetDelegate()->CreateTextPositionAt( + /*offset*/ active_composition_offset.end()); + + *range = AXPlatformNodeTextRangeProviderWin::CreateTextRangeProvider( + std::move(start), std::move(end)); + } + + return S_OK; +} + +} // namespace ui diff --git a/third_party/accessibility/ax/platform/ax_platform_node_textprovider_win.h b/third_party/accessibility/ax/platform/ax_platform_node_textprovider_win.h new file mode 100644 index 0000000000000..9dbb4f72ab53d --- /dev/null +++ b/third_party/accessibility/ax/platform/ax_platform_node_textprovider_win.h @@ -0,0 +1,82 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UI_ACCESSIBILITY_PLATFORM_AX_PLATFORM_NODE_TEXTPROVIDER_WIN_H_ +#define UI_ACCESSIBILITY_PLATFORM_AX_PLATFORM_NODE_TEXTPROVIDER_WIN_H_ + +#include +#include + +#include + +#include "ax/ax_node_position.h" +#include "ax/platform/ax_platform_node_win.h" + +namespace ui { + +class AX_EXPORT __declspec(uuid("3e1c192b-4348-45ac-8eb6-4b58eeb3dcca")) + AXPlatformNodeTextProviderWin + : public CComObjectRootEx, + public ITextEditProvider { + public: + BEGIN_COM_MAP(AXPlatformNodeTextProviderWin) + COM_INTERFACE_ENTRY(ITextProvider) + COM_INTERFACE_ENTRY(ITextEditProvider) + COM_INTERFACE_ENTRY(AXPlatformNodeTextProviderWin) + END_COM_MAP() + + AXPlatformNodeTextProviderWin(); + ~AXPlatformNodeTextProviderWin(); + + static AXPlatformNodeTextProviderWin* Create(AXPlatformNodeWin* owner); + static void CreateIUnknown(AXPlatformNodeWin* owner, IUnknown** unknown); + + // + // ITextProvider methods. + // + + IFACEMETHODIMP GetSelection(SAFEARRAY** selection) override; + + IFACEMETHODIMP GetVisibleRanges(SAFEARRAY** visible_ranges) override; + + IFACEMETHODIMP RangeFromChild(IRawElementProviderSimple* child, + ITextRangeProvider** range) override; + + IFACEMETHODIMP RangeFromPoint(UiaPoint point, + ITextRangeProvider** range) override; + + IFACEMETHODIMP get_DocumentRange(ITextRangeProvider** range) override; + + IFACEMETHODIMP get_SupportedTextSelection( + enum SupportedTextSelection* text_selection) override; + + // + // ITextEditProvider methods. + // + + IFACEMETHODIMP GetActiveComposition(ITextRangeProvider** range) override; + + IFACEMETHODIMP GetConversionTarget(ITextRangeProvider** range) override; + + // ITextProvider supporting methods. + + static ITextRangeProvider* GetRangeFromChild( + ui::AXPlatformNodeWin* ancestor, + ui::AXPlatformNodeWin* descendant); + + // Create a dengerate text range at the start of the specified node. + static ITextRangeProvider* CreateDegenerateRangeAtStart( + ui::AXPlatformNodeWin* node); + + private: + friend class AXPlatformNodeTextProviderTest; + ui::AXPlatformNodeWin* owner() const; + HRESULT GetTextRangeProviderFromActiveComposition(ITextRangeProvider** range); + + Microsoft::WRL::ComPtr owner_; +}; + +} // namespace ui + +#endif // UI_ACCESSIBILITY_PLATFORM_AX_PLATFORM_NODE_TEXTPROVIDER_WIN_H_ diff --git a/third_party/accessibility/ax/platform/ax_platform_node_textprovider_win_unittest.cc b/third_party/accessibility/ax/platform/ax_platform_node_textprovider_win_unittest.cc new file mode 100644 index 0000000000000..3eb027f725353 --- /dev/null +++ b/third_party/accessibility/ax/platform/ax_platform_node_textprovider_win_unittest.cc @@ -0,0 +1,951 @@ +// Copyright 2019 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 "ax/platform/ax_platform_node_win_unittest.h" + +#include +#include + +#include + +#include "ax/ax_action_data.h" +#include "ax/platform/ax_fragment_root_win.h" +#include "ax/platform/ax_platform_node_textprovider_win.h" +#include "ax/platform/ax_platform_node_textrangeprovider_win.h" +#include "ax/platform/test_ax_node_wrapper.h" +#include "base/win/scoped_bstr.h" +#include "base/win/scoped_safearray.h" + +#include "flutter/fml/logging.h" +#include "flutter/fml/platform/win/wstring_conversion.h" + +using Microsoft::WRL::ComPtr; + +namespace ui { + +// Helper macros for UIAutomation HRESULT expectations +#define EXPECT_UIA_INVALIDOPERATION(expr) \ + EXPECT_EQ(static_cast(UIA_E_INVALIDOPERATION), (expr)) +#define EXPECT_INVALIDARG(expr) \ + EXPECT_EQ(static_cast(E_INVALIDARG), (expr)) + +class AXPlatformNodeTextProviderTest : public AXPlatformNodeWinTest { + public: + AXPlatformNodeTextProviderTest() = default; + ~AXPlatformNodeTextProviderTest() override = default; + AXPlatformNodeTextProviderTest(const AXPlatformNodeTextProviderTest&) = + delete; + AXPlatformNodeTextProviderTest& operator=( + const AXPlatformNodeTextProviderTest&) = delete; + + protected: + void SetOwner(AXPlatformNodeWin* owner, + ITextRangeProvider* destination_range) { + ComPtr destination_provider = destination_range; + ComPtr destination_provider_interal; + + destination_provider->QueryInterface( + IID_PPV_ARGS(&destination_provider_interal)); + destination_provider_interal->SetOwnerForTesting(owner); + } + AXPlatformNodeWin* GetOwner( + const AXPlatformNodeTextProviderWin* text_provider) { + return text_provider->owner_.Get(); + } + const AXNodePosition::AXPositionInstance& GetStart( + const AXPlatformNodeTextRangeProviderWin* text_range) { + return text_range->start(); + } + const AXNodePosition::AXPositionInstance& GetEnd( + const AXPlatformNodeTextRangeProviderWin* text_range) { + return text_range->end(); + } +}; + +TEST_F(AXPlatformNodeTextProviderTest, + DISABLED_CreateDegenerateRangeFromStart) { + AXNodeData text1_data; + text1_data.id = 3; + text1_data.role = ax::mojom::Role::kStaticText; + text1_data.SetName("some text"); + + AXNodeData text2_data; + text2_data.id = 4; + text2_data.role = ax::mojom::Role::kStaticText; + text2_data.SetName("more text"); + + AXNodeData link_data; + link_data.id = 2; + link_data.role = ax::mojom::Role::kLink; + link_data.child_ids = {3, 4}; + + AXNodeData root_data; + root_data.id = 1; + root_data.role = ax::mojom::Role::kStaticText; + root_data.SetName("Document"); + root_data.child_ids = {2}; + + AXTreeUpdate update; + AXTreeData tree_data; + tree_data.tree_id = AXTreeID::CreateNewAXTreeID(); + update.tree_data = tree_data; + update.has_tree_data = true; + update.root_id = root_data.id; + update.nodes = {root_data, link_data, text1_data, text2_data}; + + Init(update); + AXNode* root_node = GetRootAsAXNode(); + AXNode* link_node = root_node->children()[0]; + AXNode* text2_node = link_node->children()[1]; + AXPlatformNodeWin* owner = + static_cast(AXPlatformNodeFromNode(root_node)); + BASE_DCHECK(owner); + + ComPtr root_node_raw = + QueryInterfaceFromNode(root_node); + ComPtr link_node_raw = + QueryInterfaceFromNode(link_node); + ComPtr text2_node_raw = + QueryInterfaceFromNode(text2_node); + + ComPtr root_platform_node; + EXPECT_HRESULT_SUCCEEDED( + root_node_raw->QueryInterface(IID_PPV_ARGS(&root_platform_node))); + ComPtr link_platform_node; + EXPECT_HRESULT_SUCCEEDED( + link_node_raw->QueryInterface(IID_PPV_ARGS(&link_platform_node))); + ComPtr text2_platform_node; + EXPECT_HRESULT_SUCCEEDED( + text2_node_raw->QueryInterface(IID_PPV_ARGS(&text2_platform_node))); + + // Degenerate range created on root node should be: + // <>some textmore text + ComPtr text_range_provider = + AXPlatformNodeTextProviderWin::CreateDegenerateRangeAtStart( + root_platform_node.Get()); + SetOwner(owner, text_range_provider.Get()); + base::win::ScopedBstr text_content; + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetText(-1, text_content.Receive())); + EXPECT_EQ(0, wcscmp(text_content.Get(), L"")); + + ComPtr actual_range; + text_range_provider->QueryInterface(IID_PPV_ARGS(&actual_range)); + AXNodePosition::AXPositionInstance expected_start, expected_end; + expected_start = root_platform_node->GetDelegate()->CreateTextPositionAt(0); + expected_end = expected_start->Clone(); + EXPECT_EQ(*GetStart(actual_range.Get()), *expected_start); + EXPECT_EQ(*GetEnd(actual_range.Get()), *expected_end); + text_content.Release(); + + // Degenerate range created on link node should be: + // <>some textmore text + text_range_provider = + AXPlatformNodeTextProviderWin::CreateDegenerateRangeAtStart( + link_platform_node.Get()); + SetOwner(owner, text_range_provider.Get()); + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetText(-1, text_content.Receive())); + EXPECT_EQ(0, wcscmp(text_content.Get(), L"")); + text_range_provider->QueryInterface(IID_PPV_ARGS(&actual_range)); + EXPECT_EQ(*GetStart(actual_range.Get()), *expected_start); + EXPECT_EQ(*GetEnd(actual_range.Get()), *expected_end); + text_content.Release(); + + // Degenerate range created on more text node should be: + // some text<>more text + text_range_provider = + AXPlatformNodeTextProviderWin::CreateDegenerateRangeAtStart( + text2_platform_node.Get()); + SetOwner(owner, text_range_provider.Get()); + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetText(-1, text_content.Receive())); + EXPECT_EQ(0, wcscmp(text_content.Get(), L"")); + text_range_provider->QueryInterface(IID_PPV_ARGS(&actual_range)); + expected_start = text2_platform_node->GetDelegate()->CreateTextPositionAt(0); + expected_end = expected_start->Clone(); + EXPECT_EQ(*GetStart(actual_range.Get()), *expected_start); + EXPECT_EQ(*GetEnd(actual_range.Get()), *expected_end); + text_content.Release(); +} + +TEST_F(AXPlatformNodeTextProviderTest, DISABLED_ITextProviderRangeFromChild) { + AXNodeData text_data; + text_data.id = 2; + text_data.role = ax::mojom::Role::kStaticText; + text_data.SetName("some text"); + + AXNodeData empty_text_data; + empty_text_data.id = 3; + empty_text_data.role = ax::mojom::Role::kStaticText; + + AXNodeData root_data; + root_data.id = 1; + root_data.role = ax::mojom::Role::kStaticText; + root_data.SetName("Document"); + root_data.child_ids.push_back(2); + root_data.child_ids.push_back(3); + + AXTreeUpdate update; + AXTreeData tree_data; + tree_data.tree_id = 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(empty_text_data); + + Init(update); + + AXNode* root_node = GetRootAsAXNode(); + AXNode* text_node = root_node->children()[0]; + AXNode* empty_text_node = root_node->children()[1]; + AXPlatformNodeWin* owner = + static_cast(AXPlatformNodeFromNode(root_node)); + BASE_DCHECK(owner); + + ComPtr root_node_raw = + QueryInterfaceFromNode(root_node); + ComPtr text_node_raw = + QueryInterfaceFromNode(text_node); + ComPtr empty_text_node_raw = + QueryInterfaceFromNode(empty_text_node); + + // Call RangeFromChild on the root with the text child passed in. + ComPtr text_provider; + EXPECT_HRESULT_SUCCEEDED( + root_node_raw->GetPatternProvider(UIA_TextPatternId, &text_provider)); + + ComPtr text_range_provider; + EXPECT_HRESULT_SUCCEEDED( + text_provider->RangeFromChild(text_node_raw.Get(), &text_range_provider)); + SetOwner(owner, text_range_provider.Get()); + + base::win::ScopedBstr text_content; + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetText(-1, text_content.Receive())); + EXPECT_EQ(0, wcscmp(text_content.Get(), L"some text")); + + // Now test that the reverse relation doesn't return a valid + // ITextRangeProvider, and instead returns E_INVALIDARG. + EXPECT_HRESULT_SUCCEEDED( + text_node_raw->GetPatternProvider(UIA_TextPatternId, &text_provider)); + + EXPECT_INVALIDARG( + text_provider->RangeFromChild(root_node_raw.Get(), &text_range_provider)); + + // Now test that a child with no text returns a degenerate range. + EXPECT_HRESULT_SUCCEEDED( + root_node_raw->GetPatternProvider(UIA_TextPatternId, &text_provider)); + + EXPECT_HRESULT_SUCCEEDED(text_provider->RangeFromChild( + empty_text_node_raw.Get(), &text_range_provider)); + SetOwner(owner, text_range_provider.Get()); + + base::win::ScopedBstr empty_text_content; + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetText(-1, empty_text_content.Receive())); + EXPECT_EQ(0, wcscmp(empty_text_content.Get(), L"")); + + // Test that passing in an object from a different instance of + // IRawElementProviderSimple than that of the valid text provider + // returns UIA_E_INVALIDOPERATION. + ComPtr other_root_node_raw; + MockIRawElementProviderSimple::CreateMockIRawElementProviderSimple( + &other_root_node_raw); + + EXPECT_HRESULT_SUCCEEDED( + root_node_raw->GetPatternProvider(UIA_TextPatternId, &text_provider)); + + EXPECT_UIA_INVALIDOPERATION(text_provider->RangeFromChild( + other_root_node_raw.Get(), &text_range_provider)); +} + +TEST_F(AXPlatformNodeTextProviderTest, + DISABLED_ITextProviderRangeFromChildMultipleChildren) { + const int ROOT_ID = 1; + const int DIALOG_ID = 2; + const int DIALOG_LABEL_ID = 3; + const int DIALOG_DESCRIPTION_ID = 4; + const int BUTTON_ID = 5; + const int BUTTON_IMG_ID = 6; + const int BUTTON_TEXT_ID = 7; + const int DIALOG_DETAIL_ID = 8; + + AXNodeData root; + root.id = ROOT_ID; + root.role = ax::mojom::Role::kStaticText; + root.SetName("Document"); + root.child_ids = {DIALOG_ID}; + + AXNodeData dialog; + dialog.id = DIALOG_ID; + dialog.role = ax::mojom::Role::kDialog; + dialog.child_ids = {DIALOG_LABEL_ID, DIALOG_DESCRIPTION_ID, BUTTON_ID, + DIALOG_DETAIL_ID}; + + AXNodeData dialog_label; + dialog_label.id = DIALOG_LABEL_ID; + dialog_label.role = ax::mojom::Role::kStaticText; + dialog_label.SetName("Dialog label."); + + AXNodeData dialog_description; + dialog_description.id = DIALOG_DESCRIPTION_ID; + dialog_description.role = ax::mojom::Role::kStaticText; + dialog_description.SetName("Dialog description."); + + AXNodeData button; + button.id = BUTTON_ID; + button.role = ax::mojom::Role::kButton; + button.child_ids = {BUTTON_IMG_ID, BUTTON_TEXT_ID}; + + AXNodeData button_img; + button_img.id = BUTTON_IMG_ID; + button_img.role = ax::mojom::Role::kImage; + + AXNodeData button_text; + button_text.id = BUTTON_TEXT_ID; + button_text.role = ax::mojom::Role::kStaticText; + button_text.SetName("ok."); + + AXNodeData dialog_detail; + dialog_detail.id = DIALOG_DETAIL_ID; + dialog_detail.role = ax::mojom::Role::kStaticText; + dialog_detail.SetName("Some more detail about dialog."); + + AXTreeUpdate update; + AXTreeData tree_data; + tree_data.tree_id = AXTreeID::CreateNewAXTreeID(); + update.tree_data = tree_data; + update.has_tree_data = true; + update.root_id = ROOT_ID; + update.nodes = {root, dialog, dialog_label, dialog_description, + button, button_img, button_text, dialog_detail}; + + Init(update); + + AXNode* root_node = GetRootAsAXNode(); + AXNode* dialog_node = root_node->children()[0]; + AXPlatformNodeWin* owner = + static_cast(AXPlatformNodeFromNode(root_node)); + BASE_DCHECK(owner); + + ComPtr root_node_raw = + QueryInterfaceFromNode(root_node); + ComPtr dialog_node_raw = + QueryInterfaceFromNode(dialog_node); + + // Call RangeFromChild on the root with the dialog child passed in. + ComPtr text_provider; + EXPECT_HRESULT_SUCCEEDED( + root_node_raw->GetPatternProvider(UIA_TextPatternId, &text_provider)); + + ComPtr text_range_provider; + EXPECT_HRESULT_SUCCEEDED(text_provider->RangeFromChild(dialog_node_raw.Get(), + &text_range_provider)); + SetOwner(owner, text_range_provider.Get()); + + base::win::ScopedBstr text_content; + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetText(-1, text_content.Receive())); + EXPECT_EQ(base::WideToUTF16(text_content.Get()), + u"Dialog label.Dialog description." + kEmbeddedCharacterAsString + + u"ok.Some more detail " + u"about dialog."); + + // Check the reverse relationship that GetEnclosingElement on the text range + // gives back the dialog. + ComPtr enclosing_element; + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetEnclosingElement(&enclosing_element)); + EXPECT_EQ(enclosing_element.Get(), dialog_node_raw.Get()); +} + +TEST_F(AXPlatformNodeTextProviderTest, DISABLED_NearestTextIndexToPoint) { + AXNodeData text_data; + text_data.id = 2; + text_data.role = ax::mojom::Role::kInlineTextBox; + text_data.SetName("text"); + // spacing: "t-e-x---t-" + text_data.AddIntListAttribute(ax::mojom::IntListAttribute::kCharacterOffsets, + {2, 4, 8, 10}); + + AXNodeData root_data; + root_data.id = 1; + root_data.role = ax::mojom::Role::kRootWebArea; + root_data.relative_bounds.bounds = gfx::RectF(1, 1, 2, 2); + root_data.child_ids.push_back(2); + + Init(root_data, text_data); + + AXNode* root_node = GetRootAsAXNode(); + AXNode* text_node = root_node->children()[0]; + + struct NearestTextIndexTestData { + AXNode* node; + struct point_offset_expected_index_pair { + int point_offset_x; + int expected_index; + }; + std::vector test_data; + }; + NearestTextIndexTestData nodes[] = { + {text_node, + {{0, 0}, {2, 0}, {3, 1}, {4, 1}, {5, 2}, {8, 2}, {9, 3}, {10, 3}}}, + {root_node, + {{0, 0}, {2, 0}, {3, 0}, {4, 0}, {5, 0}, {8, 0}, {9, 0}, {10, 0}}}}; + for (auto data : nodes) { + if (!data.node->IsText() && !data.node->data().IsTextField()) { + continue; + } + ComPtr element_provider = + QueryInterfaceFromNode(data.node); + ComPtr text_provider; + EXPECT_HRESULT_SUCCEEDED(element_provider->GetPatternProvider( + UIA_TextPatternId, &text_provider)); + // get internal implementation to access helper for testing + ComPtr platform_text_provider; + EXPECT_HRESULT_SUCCEEDED( + text_provider->QueryInterface(IID_PPV_ARGS(&platform_text_provider))); + + ComPtr platform_node; + EXPECT_HRESULT_SUCCEEDED( + element_provider->QueryInterface(IID_PPV_ARGS(&platform_node))); + + for (auto pair : data.test_data) { + EXPECT_EQ(pair.expected_index, platform_node->NearestTextIndexToPoint( + gfx::Point(pair.point_offset_x, 0))); + } + } +} + +TEST_F(AXPlatformNodeTextProviderTest, DISABLED_ITextProviderDocumentRange) { + AXNodeData text_data; + text_data.id = 2; + text_data.role = ax::mojom::Role::kStaticText; + text_data.SetName("some text"); + + AXNodeData root_data; + root_data.id = 1; + root_data.role = ax::mojom::Role::kStaticText; + root_data.SetName("Document"); + root_data.child_ids.push_back(2); + + Init(root_data, text_data); + + ComPtr root_node = + GetRootIRawElementProviderSimple(); + + ComPtr text_provider; + EXPECT_HRESULT_SUCCEEDED( + root_node->GetPatternProvider(UIA_TextPatternId, &text_provider)); + + ComPtr text_range_provider; + EXPECT_HRESULT_SUCCEEDED( + text_provider->get_DocumentRange(&text_range_provider)); +} + +TEST_F(AXPlatformNodeTextProviderTest, + DISABLED_ITextProviderDocumentRangeTrailingIgnored) { + // ++1 root + // ++++2 kGenericContainer + // ++++++3 kStaticText "Hello" + // ++++4 kGenericContainer + // ++++++5 kGenericContainer + // ++++++++6 kStaticText "3.14" + // ++++7 kGenericContainer (ignored) + // ++++++8 kGenericContainer (ignored) + // ++++++++9 kStaticText "ignored" + AXNodeData root_1; + AXNodeData gc_2; + AXNodeData static_text_3; + AXNodeData gc_4; + AXNodeData gc_5; + AXNodeData static_text_6; + AXNodeData gc_7_ignored; + AXNodeData gc_8_ignored; + AXNodeData static_text_9_ignored; + + root_1.id = 1; + gc_2.id = 2; + static_text_3.id = 3; + gc_4.id = 4; + gc_5.id = 5; + static_text_6.id = 6; + gc_7_ignored.id = 7; + gc_8_ignored.id = 8; + static_text_9_ignored.id = 9; + + root_1.role = ax::mojom::Role::kRootWebArea; + root_1.child_ids = {gc_2.id, gc_4.id, gc_7_ignored.id}; + root_1.SetName("Document"); + + gc_2.role = ax::mojom::Role::kGenericContainer; + gc_2.AddIntListAttribute(ax::mojom::IntListAttribute::kLabelledbyIds, + {static_text_3.id}); + gc_2.child_ids = {static_text_3.id}; + + static_text_3.role = ax::mojom::Role::kStaticText; + static_text_3.SetName("Hello"); + + gc_4.role = ax::mojom::Role::kGenericContainer; + gc_4.AddIntListAttribute(ax::mojom::IntListAttribute::kLabelledbyIds, + {gc_5.id}); + gc_4.child_ids = {gc_5.id}; + + gc_5.role = ax::mojom::Role::kGenericContainer; + gc_5.child_ids = {static_text_6.id}; + + static_text_6.role = ax::mojom::Role::kStaticText; + static_text_6.SetName("3.14"); + + gc_7_ignored.role = ax::mojom::Role::kGenericContainer; + gc_7_ignored.child_ids = {gc_8_ignored.id}; + gc_7_ignored.AddState(ax::mojom::State::kIgnored); + + gc_8_ignored.role = ax::mojom::Role::kGenericContainer; + gc_8_ignored.child_ids = {static_text_9_ignored.id}; + gc_8_ignored.AddState(ax::mojom::State::kIgnored); + + static_text_9_ignored.role = ax::mojom::Role::kStaticText; + static_text_9_ignored.SetName("ignored"); + static_text_9_ignored.AddState(ax::mojom::State::kIgnored); + + AXTreeUpdate update; + AXTreeData tree_data; + tree_data.tree_id = AXTreeID::CreateNewAXTreeID(); + update.tree_data = tree_data; + update.has_tree_data = true; + update.root_id = root_1.id; + update.nodes = {root_1, gc_2, static_text_3, + gc_4, gc_5, static_text_6, + gc_7_ignored, gc_8_ignored, static_text_9_ignored}; + + Init(update); + + ComPtr root_node = + GetRootIRawElementProviderSimple(); + + ComPtr text_provider; + EXPECT_HRESULT_SUCCEEDED( + root_node->GetPatternProvider(UIA_TextPatternId, &text_provider)); + + ComPtr text_range_provider; + EXPECT_HRESULT_SUCCEEDED( + text_provider->get_DocumentRange(&text_range_provider)); + + ComPtr text_range; + text_range_provider->QueryInterface(IID_PPV_ARGS(&text_range)); + + ComPtr root_text_provider; + EXPECT_HRESULT_SUCCEEDED( + root_node->GetPatternProvider(UIA_TextPatternId, &root_text_provider)); + ComPtr root_platform_node; + root_text_provider->QueryInterface(IID_PPV_ARGS(&root_platform_node)); + AXPlatformNodeWin* owner = GetOwner(root_platform_node.Get()); + + AXNodePosition::AXPositionInstance expected_start = + owner->GetDelegate()->CreateTextPositionAt(0)->AsLeafTextPosition(); + AXNodePosition::AXPositionInstance expected_end = + owner->GetDelegate() + ->CreateTextPositionAt(0) + ->CreatePositionAtEndOfAnchor(); + expected_end = expected_end->AsLeafTextPosition(); + EXPECT_EQ(*GetStart(text_range.Get()), *expected_start); + EXPECT_EQ(*GetEnd(text_range.Get()), *expected_end); +} + +TEST_F(AXPlatformNodeTextProviderTest, + DISABLED_ITextProviderDocumentRangeNested) { + AXNodeData text_data; + text_data.id = 3; + text_data.role = ax::mojom::Role::kStaticText; + text_data.SetName("some text"); + + AXNodeData paragraph_data; + paragraph_data.id = 2; + paragraph_data.role = ax::mojom::Role::kParagraph; + paragraph_data.child_ids.push_back(3); + + AXNodeData root_data; + root_data.id = 1; + root_data.role = ax::mojom::Role::kStaticText; + root_data.SetName("Document"); + root_data.child_ids.push_back(2); + + Init(root_data, paragraph_data, text_data); + + ComPtr root_node = + GetRootIRawElementProviderSimple(); + + ComPtr text_provider; + EXPECT_HRESULT_SUCCEEDED( + root_node->GetPatternProvider(UIA_TextPatternId, &text_provider)); + + ComPtr text_range_provider; + EXPECT_HRESULT_SUCCEEDED( + text_provider->get_DocumentRange(&text_range_provider)); +} + +TEST_F(AXPlatformNodeTextProviderTest, + DISABLED_ITextProviderSupportedSelection) { + AXNodeData text_data; + text_data.id = 2; + text_data.role = ax::mojom::Role::kStaticText; + text_data.SetName("some text"); + + AXNodeData root_data; + root_data.id = 1; + root_data.role = ax::mojom::Role::kStaticText; + root_data.SetName("Document"); + root_data.child_ids.push_back(2); + + Init(root_data, text_data); + + ComPtr root_node = + GetRootIRawElementProviderSimple(); + + ComPtr text_provider; + EXPECT_HRESULT_SUCCEEDED( + root_node->GetPatternProvider(UIA_TextPatternId, &text_provider)); + + SupportedTextSelection text_selection_mode; + EXPECT_HRESULT_SUCCEEDED( + text_provider->get_SupportedTextSelection(&text_selection_mode)); + EXPECT_EQ(text_selection_mode, SupportedTextSelection_Single); +} + +TEST_F(AXPlatformNodeTextProviderTest, DISABLED_ITextProviderGetSelection) { + AXNodeData text_data; + text_data.id = 2; + text_data.role = ax::mojom::Role::kStaticText; + text_data.SetName("some text"); + + AXNodeData textbox_data; + textbox_data.id = 3; + textbox_data.role = ax::mojom::Role::kInlineTextBox; + textbox_data.SetName("textbox text"); + textbox_data.AddState(ax::mojom::State::kEditable); + + AXNodeData nonatomic_textfield_data; + nonatomic_textfield_data.id = 4; + nonatomic_textfield_data.role = ax::mojom::Role::kGroup; + nonatomic_textfield_data.child_ids = {5}; + + AXNodeData text_child_data; + text_child_data.id = 5; + text_child_data.role = ax::mojom::Role::kStaticText; + text_child_data.SetName("text"); + + AXNodeData root_data; + root_data.id = 1; + root_data.role = ax::mojom::Role::kStaticText; + root_data.SetName("Document"); + root_data.child_ids = {2, 3, 4}; + + AXTreeUpdate update; + AXTreeData tree_data; + tree_data.tree_id = AXTreeID::CreateNewAXTreeID(); + update.tree_data = tree_data; + update.has_tree_data = true; + update.root_id = root_data.id; + update.nodes = {root_data, text_data, textbox_data, nonatomic_textfield_data, + text_child_data}; + Init(update); + + ComPtr root_node = + GetRootIRawElementProviderSimple(); + + ComPtr root_text_provider; + EXPECT_HRESULT_SUCCEEDED( + root_node->GetPatternProvider(UIA_TextPatternId, &root_text_provider)); + + base::win::ScopedSafearray selections; + root_text_provider->GetSelection(selections.Receive()); + ASSERT_EQ(nullptr, selections.Get()); + + ComPtr root_platform_node; + root_text_provider->QueryInterface(IID_PPV_ARGS(&root_platform_node)); + + AXPlatformNodeWin* owner = GetOwner(root_platform_node.Get()); + AXTreeData& selected_tree_data = + const_cast(owner->GetDelegate()->GetTreeData()); + selected_tree_data.sel_focus_object_id = 2; + selected_tree_data.sel_anchor_object_id = 2; + selected_tree_data.sel_anchor_offset = 0; + selected_tree_data.sel_focus_offset = 4; + + root_text_provider->GetSelection(selections.Receive()); + ASSERT_NE(nullptr, selections.Get()); + + LONG ubound; + EXPECT_HRESULT_SUCCEEDED(SafeArrayGetUBound(selections.Get(), 1, &ubound)); + EXPECT_EQ(0, ubound); + LONG lbound; + EXPECT_HRESULT_SUCCEEDED(SafeArrayGetLBound(selections.Get(), 1, &lbound)); + EXPECT_EQ(0, lbound); + + LONG index = 0; + ComPtr text_range_provider; + EXPECT_HRESULT_SUCCEEDED(SafeArrayGetElement( + selections.Get(), &index, static_cast(&text_range_provider))); + SetOwner(owner, text_range_provider.Get()); + + base::win::ScopedBstr text_content; + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetText(-1, text_content.Receive())); + EXPECT_EQ(0, wcscmp(text_content.Get(), L"some")); + text_content.Reset(); + selections.Reset(); + text_range_provider.Reset(); + + // Verify that start and end are appropriately swapped when sel_anchor_offset + // is greater than sel_focus_offset + selected_tree_data.sel_focus_object_id = 2; + selected_tree_data.sel_anchor_object_id = 2; + selected_tree_data.sel_anchor_offset = 4; + selected_tree_data.sel_focus_offset = 0; + + root_text_provider->GetSelection(selections.Receive()); + ASSERT_NE(nullptr, selections.Get()); + + EXPECT_HRESULT_SUCCEEDED(SafeArrayGetUBound(selections.Get(), 1, &ubound)); + EXPECT_EQ(0, ubound); + EXPECT_HRESULT_SUCCEEDED(SafeArrayGetLBound(selections.Get(), 1, &lbound)); + EXPECT_EQ(0, lbound); + + EXPECT_HRESULT_SUCCEEDED(SafeArrayGetElement( + selections.Get(), &index, static_cast(&text_range_provider))); + SetOwner(owner, text_range_provider.Get()); + + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetText(-1, text_content.Receive())); + EXPECT_EQ(0, wcscmp(text_content.Get(), L"some")); + text_content.Reset(); + selections.Reset(); + text_range_provider.Reset(); + + // Verify that text ranges at an insertion point returns a degenerate (empty) + // text range via textbox with sel_anchor_offset equal to sel_focus_offset + selected_tree_data.sel_focus_object_id = 3; + selected_tree_data.sel_anchor_object_id = 3; + selected_tree_data.sel_anchor_offset = 1; + selected_tree_data.sel_focus_offset = 1; + + AXNode* text_edit_node = GetRootAsAXNode()->children()[1]; + + ComPtr text_edit_com = + QueryInterfaceFromNode(text_edit_node); + + ComPtr text_edit_provider; + EXPECT_HRESULT_SUCCEEDED(text_edit_com->GetPatternProvider( + UIA_TextPatternId, &text_edit_provider)); + + selections.Reset(); + EXPECT_HRESULT_SUCCEEDED( + text_edit_provider->GetSelection(selections.Receive())); + EXPECT_NE(nullptr, selections.Get()); + + EXPECT_HRESULT_SUCCEEDED(SafeArrayGetUBound(selections.Get(), 1, &ubound)); + EXPECT_EQ(0, ubound); + EXPECT_HRESULT_SUCCEEDED(SafeArrayGetLBound(selections.Get(), 1, &lbound)); + EXPECT_EQ(0, lbound); + + ComPtr text_edit_range_provider; + EXPECT_HRESULT_SUCCEEDED( + SafeArrayGetElement(selections.Get(), &index, + static_cast(&text_edit_range_provider))); + SetOwner(owner, text_edit_range_provider.Get()); + EXPECT_HRESULT_SUCCEEDED( + text_edit_range_provider->GetText(-1, text_content.Receive())); + EXPECT_EQ(0U, text_content.Length()); + text_content.Reset(); + selections.Reset(); + text_edit_range_provider.Reset(); + + // Verify selections that span multiple nodes + selected_tree_data.sel_focus_object_id = 2; + selected_tree_data.sel_focus_offset = 0; + selected_tree_data.sel_anchor_object_id = 3; + selected_tree_data.sel_anchor_offset = 12; + + root_text_provider->GetSelection(selections.Receive()); + ASSERT_NE(nullptr, selections.Get()); + + EXPECT_HRESULT_SUCCEEDED(SafeArrayGetUBound(selections.Get(), 1, &ubound)); + EXPECT_EQ(0, ubound); + EXPECT_HRESULT_SUCCEEDED(SafeArrayGetLBound(selections.Get(), 1, &lbound)); + EXPECT_EQ(0, lbound); + + EXPECT_HRESULT_SUCCEEDED(SafeArrayGetElement( + selections.Get(), &index, static_cast(&text_range_provider))); + + SetOwner(owner, text_range_provider.Get()); + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetText(-1, text_content.Receive())); + EXPECT_EQ(0, wcscmp(text_content.Get(), L"some texttextbox text")); + text_content.Reset(); + selections.Reset(); + text_range_provider.Reset(); + + // Verify SAFEARRAY value for degenerate selection. + selected_tree_data.sel_focus_object_id = 2; + selected_tree_data.sel_anchor_object_id = 2; + selected_tree_data.sel_anchor_offset = 1; + selected_tree_data.sel_focus_offset = 1; + + root_text_provider->GetSelection(selections.Receive()); + ASSERT_NE(nullptr, selections.Get()); + + EXPECT_HRESULT_SUCCEEDED(SafeArrayGetUBound(selections.Get(), 1, &ubound)); + EXPECT_EQ(0, ubound); + EXPECT_HRESULT_SUCCEEDED(SafeArrayGetLBound(selections.Get(), 1, &lbound)); + EXPECT_EQ(0, lbound); + + EXPECT_HRESULT_SUCCEEDED(SafeArrayGetElement( + selections.Get(), &index, static_cast(&text_range_provider))); + + SetOwner(owner, text_range_provider.Get()); + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetText(-1, text_content.Receive())); + EXPECT_EQ(0, wcscmp(text_content.Get(), L"")); + text_content.Reset(); + selections.Reset(); + text_range_provider.Reset(); + + // Removed testing logic for non-atomic text fields as we do not have this + // role. + + // Now delete the tree (which will delete the associated elements) and verify + // that UIA_E_ELEMENTNOTAVAILABLE is returned when calling GetSelection on + // a dead element + DestroyTree(); + + EXPECT_EQ(static_cast(UIA_E_ELEMENTNOTAVAILABLE), + text_edit_provider->GetSelection(selections.Receive())); +} + +TEST_F(AXPlatformNodeTextProviderTest, + DISABLED_ITextProviderGetActiveComposition) { + AXNodeData text_data; + text_data.id = 2; + text_data.role = ax::mojom::Role::kStaticText; + text_data.SetName("some text"); + + AXNodeData root_data; + root_data.id = 1; + root_data.role = ax::mojom::Role::kStaticText; + root_data.SetName("Document"); + root_data.child_ids.push_back(2); + + AXTreeUpdate update; + AXTreeData tree_data; + tree_data.tree_id = 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); + Init(update); + + ComPtr root_node = + GetRootIRawElementProviderSimple(); + + ComPtr root_text_provider; + EXPECT_HRESULT_SUCCEEDED( + root_node->GetPatternProvider(UIA_TextPatternId, &root_text_provider)); + + ComPtr root_text_edit_provider; + EXPECT_HRESULT_SUCCEEDED(root_node->GetPatternProvider( + UIA_TextEditPatternId, &root_text_edit_provider)); + + ComPtr text_range_provider; + root_text_edit_provider->GetActiveComposition(&text_range_provider); + ASSERT_EQ(nullptr, text_range_provider); + + ComPtr root_platform_node; + root_text_provider->QueryInterface(IID_PPV_ARGS(&root_platform_node)); + + AXActionData action_data; + action_data.action = ax::mojom::Action::kFocus; + action_data.target_node_id = 1; + AXPlatformNodeWin* owner = GetOwner(root_platform_node.Get()); + owner->GetDelegate()->AccessibilityPerformAction(action_data); + const std::u16string active_composition_text = u"a"; + owner->OnActiveComposition(gfx::Range(0, 1), active_composition_text, false); + + root_text_edit_provider->GetActiveComposition(&text_range_provider); + ASSERT_NE(nullptr, text_range_provider); + ComPtr actual_range; + AXNodePosition::AXPositionInstance expected_start = + owner->GetDelegate()->CreateTextPositionAt(0); + AXNodePosition::AXPositionInstance expected_end = + owner->GetDelegate()->CreateTextPositionAt(1); + text_range_provider->QueryInterface(IID_PPV_ARGS(&actual_range)); + EXPECT_EQ(*GetStart(actual_range.Get()), *expected_start); + EXPECT_EQ(*GetEnd(actual_range.Get()), *expected_end); +} + +TEST_F(AXPlatformNodeTextProviderTest, + DISABLED_ITextProviderGetConversionTarget) { + AXNodeData text_data; + text_data.id = 2; + text_data.role = ax::mojom::Role::kStaticText; + text_data.SetName("some text"); + + AXNodeData root_data; + root_data.id = 1; + root_data.role = ax::mojom::Role::kStaticText; + root_data.SetName("Document"); + root_data.child_ids.push_back(2); + + AXTreeUpdate update; + AXTreeData tree_data; + tree_data.tree_id = 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); + Init(update); + + ComPtr root_node = + GetRootIRawElementProviderSimple(); + + ComPtr root_text_provider; + EXPECT_HRESULT_SUCCEEDED( + root_node->GetPatternProvider(UIA_TextPatternId, &root_text_provider)); + + ComPtr root_text_edit_provider; + EXPECT_HRESULT_SUCCEEDED(root_node->GetPatternProvider( + UIA_TextEditPatternId, &root_text_edit_provider)); + + ComPtr text_range_provider; + root_text_edit_provider->GetConversionTarget(&text_range_provider); + ASSERT_EQ(nullptr, text_range_provider); + + ComPtr root_platform_node; + root_text_provider->QueryInterface(IID_PPV_ARGS(&root_platform_node)); + + AXActionData action_data; + action_data.action = ax::mojom::Action::kFocus; + action_data.target_node_id = 1; + AXPlatformNodeWin* owner = GetOwner(root_platform_node.Get()); + owner->GetDelegate()->AccessibilityPerformAction(action_data); + const std::u16string active_composition_text = u"a"; + owner->OnActiveComposition(gfx::Range(0, 1), active_composition_text, false); + + root_text_edit_provider->GetConversionTarget(&text_range_provider); + ASSERT_NE(nullptr, text_range_provider); + ComPtr actual_range; + AXNodePosition::AXPositionInstance expected_start = + owner->GetDelegate()->CreateTextPositionAt(0); + AXNodePosition::AXPositionInstance expected_end = + owner->GetDelegate()->CreateTextPositionAt(1); + text_range_provider->QueryInterface(IID_PPV_ARGS(&actual_range)); + EXPECT_EQ(*GetStart(actual_range.Get()), *expected_start); + EXPECT_EQ(*GetEnd(actual_range.Get()), *expected_end); +} + +} // namespace ui diff --git a/third_party/accessibility/ax/platform/ax_platform_node_textrangeprovider_win.cc b/third_party/accessibility/ax/platform/ax_platform_node_textrangeprovider_win.cc new file mode 100644 index 0000000000000..2c7cca0e2e81b --- /dev/null +++ b/third_party/accessibility/ax/platform/ax_platform_node_textrangeprovider_win.cc @@ -0,0 +1,1702 @@ +// Copyright 2019 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 "ax/platform/ax_platform_node_textrangeprovider_win.h" + +#include +#include + +#include "ax/ax_action_data.h" +#include "ax/ax_range.h" +#include "ax/platform/ax_platform_node_delegate.h" +#include "ax/platform/ax_platform_node_win.h" +#include "ax/platform/ax_platform_tree_manager.h" +#include "base/win/variant_vector.h" + +#define UIA_VALIDATE_TEXTRANGEPROVIDER_CALL() \ + if (!GetOwner() || !GetOwner()->GetDelegate() || !start() || \ + !start()->GetAnchor() || !end() || !end()->GetAnchor()) \ + return UIA_E_ELEMENTNOTAVAILABLE; \ + SetStart(start()->AsValidPosition()); \ + SetEnd(end()->AsValidPosition()); +#define UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_IN(in) \ + if (!GetOwner() || !GetOwner()->GetDelegate() || !start() || \ + !start()->GetAnchor() || !end() || !end()->GetAnchor()) \ + return UIA_E_ELEMENTNOTAVAILABLE; \ + if (!in) \ + return E_POINTER; \ + SetStart(start()->AsValidPosition()); \ + SetEnd(end()->AsValidPosition()); +#define UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_OUT(out) \ + if (!GetOwner() || !GetOwner()->GetDelegate() || !start() || \ + !start()->GetAnchor() || !end() || !end()->GetAnchor()) \ + return UIA_E_ELEMENTNOTAVAILABLE; \ + if (!out) \ + return E_POINTER; \ + *out = {}; \ + SetStart(start()->AsValidPosition()); \ + SetEnd(end()->AsValidPosition()); +#define UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_IN_1_OUT(in, out) \ + if (!GetOwner() || !GetOwner()->GetDelegate() || !start() || \ + !start()->GetAnchor() || !end() || !end()->GetAnchor()) \ + return UIA_E_ELEMENTNOTAVAILABLE; \ + if (!in || !out) \ + return E_POINTER; \ + *out = {}; \ + SetStart(start()->AsValidPosition()); \ + SetEnd(end()->AsValidPosition()); +// Validate bounds calculated by AXPlatformNodeDelegate. Degenerate bounds +// indicate the interface is not yet supported on the platform. +#define UIA_VALIDATE_BOUNDS(bounds) \ + if (bounds.OffsetFromOrigin().IsZero() && bounds.IsEmpty()) \ + return UIA_E_NOTSUPPORTED; + +namespace ui { + +class AXRangePhysicalPixelRectDelegate : public AXRangeRectDelegate { + public: + explicit AXRangePhysicalPixelRectDelegate( + AXPlatformNodeTextRangeProviderWin* host) + : host_(host) {} + + gfx::Rect GetInnerTextRangeBoundsRect( + AXTreeID tree_id, + AXNode::AXID node_id, + int start_offset, + int end_offset, + ui::AXClippingBehavior clipping_behavior, + AXOffscreenResult* offscreen_result) override { + AXPlatformNodeDelegate* delegate = host_->GetDelegate(tree_id, node_id); + BASE_DCHECK(delegate); + return delegate->GetInnerTextRangeBoundsRect( + start_offset, end_offset, ui::AXCoordinateSystem::kScreenPhysicalPixels, + clipping_behavior, offscreen_result); + } + + gfx::Rect GetBoundsRect(AXTreeID tree_id, + AXNode::AXID node_id, + AXOffscreenResult* offscreen_result) override { + AXPlatformNodeDelegate* delegate = host_->GetDelegate(tree_id, node_id); + BASE_DCHECK(delegate); + return delegate->GetBoundsRect( + ui::AXCoordinateSystem::kScreenPhysicalPixels, + ui::AXClippingBehavior::kClipped, offscreen_result); + } + + private: + AXPlatformNodeTextRangeProviderWin* host_; +}; + +AXPlatformNodeTextRangeProviderWin::AXPlatformNodeTextRangeProviderWin() {} + +AXPlatformNodeTextRangeProviderWin::~AXPlatformNodeTextRangeProviderWin() {} + +ITextRangeProvider* AXPlatformNodeTextRangeProviderWin::CreateTextRangeProvider( + AXPositionInstance start, + AXPositionInstance end) { + CComObject* text_range_provider = nullptr; + if (SUCCEEDED(CComObject::CreateInstance( + &text_range_provider))) { + BASE_DCHECK(text_range_provider); + text_range_provider->SetStart(std::move(start)); + text_range_provider->SetEnd(std::move(end)); + text_range_provider->AddRef(); + return text_range_provider; + } + + return nullptr; +} + +ITextRangeProvider* +AXPlatformNodeTextRangeProviderWin::CreateTextRangeProviderForTesting( + AXPlatformNodeWin* owner, + AXPositionInstance start, + AXPositionInstance end) { + Microsoft::WRL::ComPtr text_range_provider = + CreateTextRangeProvider(start->Clone(), end->Clone()); + Microsoft::WRL::ComPtr + text_range_provider_win; + if (SUCCEEDED(text_range_provider->QueryInterface( + IID_PPV_ARGS(&text_range_provider_win)))) { + text_range_provider_win->SetOwnerForTesting(owner); // IN-TEST + return text_range_provider_win.Get(); + } + + return nullptr; +} + +// +// ITextRangeProvider methods. +// +HRESULT AXPlatformNodeTextRangeProviderWin::Clone(ITextRangeProvider** clone) { + UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_OUT(clone); + + *clone = CreateTextRangeProvider(start()->Clone(), end()->Clone()); + return S_OK; +} + +HRESULT AXPlatformNodeTextRangeProviderWin::Compare(ITextRangeProvider* other, + BOOL* result) { + UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_IN_1_OUT(other, result); + + Microsoft::WRL::ComPtr other_provider; + if (other->QueryInterface(IID_PPV_ARGS(&other_provider)) != S_OK) + return UIA_E_INVALIDOPERATION; + + if (*start() == *(other_provider->start()) && + *end() == *(other_provider->end())) { + *result = TRUE; + } + return S_OK; +} + +HRESULT AXPlatformNodeTextRangeProviderWin::CompareEndpoints( + TextPatternRangeEndpoint this_endpoint, + ITextRangeProvider* other, + TextPatternRangeEndpoint other_endpoint, + int* result) { + UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_IN_1_OUT(other, result); + + Microsoft::WRL::ComPtr other_provider; + if (other->QueryInterface(IID_PPV_ARGS(&other_provider)) != S_OK) + return UIA_E_INVALIDOPERATION; + + const AXPositionInstance& this_provider_endpoint = + (this_endpoint == TextPatternRangeEndpoint_Start) ? start() : end(); + const AXPositionInstance& other_provider_endpoint = + (other_endpoint == TextPatternRangeEndpoint_Start) + ? other_provider->start() + : other_provider->end(); + + std::optional comparison = + this_provider_endpoint->CompareTo(*other_provider_endpoint); + if (!comparison) + return UIA_E_INVALIDOPERATION; + + if (comparison.value() < 0) + *result = -1; + else if (comparison.value() > 0) + *result = 1; + else + *result = 0; + return S_OK; +} + +HRESULT AXPlatformNodeTextRangeProviderWin::ExpandToEnclosingUnit( + TextUnit unit) { + return ExpandToEnclosingUnitImpl(unit); +} + +HRESULT AXPlatformNodeTextRangeProviderWin::ExpandToEnclosingUnitImpl( + TextUnit unit) { + UIA_VALIDATE_TEXTRANGEPROVIDER_CALL(); + { + AXPositionInstance normalized_start = start()->Clone(); + AXPositionInstance normalized_end = end()->Clone(); + NormalizeTextRange(normalized_start, normalized_end); + SetStart(std::move(normalized_start)); + SetEnd(std::move(normalized_end)); + } + + // Determine if start is on a boundary of the specified TextUnit, if it is + // not, move backwards until it is. Move the end forwards from start until it + // is on the next TextUnit boundary, if one exists. + switch (unit) { + case TextUnit_Character: { + // For characters, the start endpoint will always be on a TextUnit + // boundary, thus we only need to move the end position. + AXPositionInstance end_backup = end()->Clone(); + SetEnd(start()->CreateNextCharacterPosition( + AXBoundaryBehavior::CrossBoundary)); + + if (end()->IsNullPosition()) { + // The previous could fail if the start is at the end of the last anchor + // of the tree, try expanding to the previous character instead. + AXPositionInstance start_backup = start()->Clone(); + SetStart(start()->CreatePreviousCharacterPosition( + AXBoundaryBehavior::CrossBoundary)); + + if (start()->IsNullPosition()) { + // Text representation is empty, undo everything and exit. + SetStart(std::move(start_backup)); + SetEnd(std::move(end_backup)); + return S_OK; + } + SetEnd(start()->CreateNextCharacterPosition( + AXBoundaryBehavior::CrossBoundary)); + BASE_DCHECK(!end()->IsNullPosition()); + } + + AXPositionInstance normalized_start = start()->Clone(); + AXPositionInstance normalized_end = end()->Clone(); + NormalizeTextRange(normalized_start, normalized_end); + SetStart(std::move(normalized_start)); + SetEnd(std::move(normalized_end)); + break; + } + case TextUnit_Format: + SetStart(start()->CreatePreviousFormatStartPosition( + AXBoundaryBehavior::StopAtAnchorBoundary)); + SetEnd(start()->CreateNextFormatEndPosition( + AXBoundaryBehavior::StopAtAnchorBoundary)); + break; + case TextUnit_Word: { + AXPositionInstance start_backup = start()->Clone(); + SetStart(start()->CreatePreviousWordStartPosition( + AXBoundaryBehavior::StopAtAnchorBoundary)); + + // Since start_ is already located at a word boundary, we need to cross it + // in order to move to the next one. Because Windows ATs behave + // undesirably when the start and end endpoints are not in the same anchor + // (for character and word navigation), stop at anchor boundary. + SetEnd(start()->CreateNextWordStartPosition( + AXBoundaryBehavior::StopAtAnchorBoundary)); + break; + } + case TextUnit_Line: + // Walk backwards to the previous line start (but don't walk backwards + // if we're already at the start of a line). The previous line start can + // occur in a different node than where `start` is currently pointing, so + // use kStopAtLastAnchorBoundary, which will stop at the tree boundary if + // no previous line start is found. + SetStart(start()->CreateBoundaryStartPosition( + AXBoundaryBehavior::StopIfAlreadyAtBoundary, + ax::mojom::MoveDirection::kBackward, &AtStartOfLinePredicate, + &AtEndOfLinePredicate)); + // From the start we just walked backwards to, walk forwards to the line + // end position. + SetEnd(start()->CreateBoundaryEndPosition( + AXBoundaryBehavior::StopAtLastAnchorBoundary, + ax::mojom::MoveDirection::kForward, &AtStartOfLinePredicate, + &AtEndOfLinePredicate)); + break; + case TextUnit_Paragraph: + SetStart(start()->CreatePreviousParagraphStartPosition( + AXBoundaryBehavior::StopIfAlreadyAtBoundary)); + SetEnd(start()->CreateNextParagraphStartPosition( + AXBoundaryBehavior::StopAtLastAnchorBoundary)); + break; + case TextUnit_Page: { + // Per UIA spec, if the document containing the current range doesn't + // support pagination, default to document navigation. + const AXNode* common_anchor = start()->LowestCommonAnchor(*end()); + if (common_anchor->tree()->HasPaginationSupport()) { + SetStart(start()->CreatePreviousPageStartPosition( + AXBoundaryBehavior::StopAtAnchorBoundary)); + SetEnd(start()->CreateNextPageEndPosition( + AXBoundaryBehavior::StopAtAnchorBoundary)); + break; + } + } + [[fallthrough]]; + case TextUnit_Document: + SetStart( + start()->CreatePositionAtStartOfDocument()->AsLeafTextPosition()); + SetEnd(start()->CreatePositionAtEndOfDocument()); + break; + default: + return UIA_E_NOTSUPPORTED; + } + BASE_DCHECK(!start()->IsNullPosition()); + BASE_DCHECK(!end()->IsNullPosition()); + return S_OK; +} + +HRESULT AXPlatformNodeTextRangeProviderWin::FindAttribute( + TEXTATTRIBUTEID text_attribute_id, + VARIANT attribute_val, + BOOL is_backward, + ITextRangeProvider** result) { + // Algorithm description: + // Performs linear search. Expand forward or backward to fetch the first + // instance of a sub text range that matches the attribute and its value. + // |is_backward| determines the direction of our search. + // |is_backward=true|, we search from the end of this text range to its + // beginning. + // |is_backward=false|, we search from the beginning of this text range to its + // end. + // + // 1. Iterate through the vector of AXRanges in this text range in the + // direction denoted by |is_backward|. + // 2. The |matched_range| is initially denoted as null since no range + // currently matches. We initialize |matched_range| to non-null value when + // we encounter the first AXRange instance that matches in attribute and + // value. We then set the |matched_range_start| to be the start (anchor) of + // the current AXRange, and |matched_range_end| to be the end (focus) of + // the current AXRange. + // 3. If the current AXRange we are iterating on continues to match attribute + // and value, we extend |matched_range| in one of the two following ways: + // - If |is_backward=true|, we extend the |matched_range| by moving + // |matched_range_start| backward. We do so by setting + // |matched_range_start| to the start (anchor) of the current AXRange. + // - If |is_backward=false|, we extend the |matched_range| by moving + // |matched_range_end| forward. We do so by setting |matched_range_end| + // to the end (focus) of the current AXRange. + // 4. We found a match when the current AXRange we are iterating on does not + // match the attribute and value and there is a previously matched range. + // The previously matched range is the final match we found. + UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_OUT(result); + // Use a cloned range so that FindAttribute does not introduce side-effects + // while normalizing the original range. + AXPositionInstance normalized_start = start()->Clone(); + AXPositionInstance normalized_end = end()->Clone(); + NormalizeTextRange(normalized_start, normalized_end); + + *result = nullptr; + AXPositionInstance matched_range_start = nullptr; + AXPositionInstance matched_range_end = nullptr; + + std::vector anchors; + AXNodeRange range(normalized_start->Clone(), normalized_end->Clone()); + for (AXNodeRange leaf_text_range : range) + anchors.emplace_back(std::move(leaf_text_range)); + + auto expand_match = [&matched_range_start, &matched_range_end, is_backward]( + auto& current_start, auto& current_end) { + // The current AXRange has the attribute and its value that we are looking + // for, we expand the matched text range if a previously matched exists, + // otherwise initialize a newly matched text range. + if (matched_range_start != nullptr && matched_range_end != nullptr) { + // Continue expanding the matched text range forward/backward based on + // the search direction. + if (is_backward) + matched_range_start = current_start->Clone(); + else + matched_range_end = current_end->Clone(); + } else { + // Initialize the matched text range. The first AXRange instance that + // matches the attribute and its value encountered. + matched_range_start = current_start->Clone(); + matched_range_end = current_end->Clone(); + } + }; + + HRESULT hr_result = + is_backward + ? FindAttributeRange(text_attribute_id, attribute_val, + anchors.crbegin(), anchors.crend(), expand_match) + : FindAttributeRange(text_attribute_id, attribute_val, + anchors.cbegin(), anchors.cend(), expand_match); + if (FAILED(hr_result)) + return E_FAIL; + + if (matched_range_start != nullptr && matched_range_end != nullptr) + *result = CreateTextRangeProvider(std::move(matched_range_start), + std::move(matched_range_end)); + return S_OK; +} + +template +HRESULT AXPlatformNodeTextRangeProviderWin::FindAttributeRange( + const TEXTATTRIBUTEID text_attribute_id, + VARIANT attribute_val, + const AnchorIterator first, + const AnchorIterator last, + ExpandMatchLambda expand_match) { + AXPlatformNodeWin* current_platform_node; + bool is_match_found = false; + + for (auto it = first; it != last; ++it) { + const auto& current_start = it->anchor(); + const auto& current_end = it->focus(); + + BASE_DCHECK(current_start->GetAnchor() == current_end->GetAnchor()); + + AXPlatformNodeDelegate* delegate = GetDelegate(current_start); + BASE_DCHECK(delegate); + + current_platform_node = static_cast( + delegate->GetFromNodeID(current_start->GetAnchor()->id())); + + base::win::VariantVector current_attribute_value; + if (FAILED(current_platform_node->GetTextAttributeValue( + text_attribute_id, current_start->text_offset(), + current_end->text_offset(), ¤t_attribute_value))) { + return E_FAIL; + } + + if (!current_attribute_value.Compare(attribute_val)) { + // When we encounter an AXRange instance that matches the attribute + // and its value which we are looking for and no previously matched text + // range exists, we expand or initialize the matched range. + is_match_found = true; + expand_match(current_start, current_end); + } else if (is_match_found) { + // When we encounter an AXRange instance that does not match the attribute + // and its value which we are looking for and a previously matched text + // range exists, the previously matched text range is the result we found. + break; + } + } + return S_OK; +} + +static bool StringSearch(const std::u16string& search_string, + const std::u16string& find_in, + size_t* find_start, + size_t* find_length, + bool ignore_case, + bool backwards) { + // TODO(schectman) Respect ignore_case/i18n. + // https://github.com/flutter/flutter/issues/117013 + size_t match_pos; + if (backwards) { + match_pos = find_in.rfind(search_string); + } else { + match_pos = find_in.find(search_string); + } + if (match_pos == std::u16string::npos) { + return false; + } + *find_start = match_pos; + *find_length = search_string.length(); + return true; +} + +HRESULT AXPlatformNodeTextRangeProviderWin::FindText( + BSTR string, + BOOL backwards, + BOOL ignore_case, + ITextRangeProvider** result) { + UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_IN_1_OUT(string, result); + // On Windows, there's a dichotomy in the definition of a text offset in a + // text position between different APIs: + // - on UIA, a text offset translates to the offset in the text itself + // - on IA2, it translates to the offset in the hypertext + // + // All unignored non-text nodes are represented with an "embedded object + // character" in their parent's text representation on IA2, but aren't on UIA. + // This leads to different expected MaxTextOffset values for a same text + // position. If `string` is found in the text represented by the start/end + // endpoints, we'll create text positions in the least common ancestor, use + // the flat text representation's offsets of found string, then convert the + // positions to leaf. If 'embedded object characters' are considered, instead + // of the flat text representation, this falls apart. + // + // Whether we expose embedded object characters for nodes is managed by the + // |g_ax_embedded_object_behavior| global variable set in ax_node_position.cc. + // When on Windows, this variable is always set to kExposeCharacter... which + // is incorrect if we run UIA-specific code. To avoid problems caused by that, + // we use the following ScopedAXEmbeddedObjectBehaviorSetter to modify the + // value of the global variable to what is really expected on UIA. + ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior( + AXEmbeddedObjectBehavior::kSuppressCharacter); + + std::u16string search_string = base::WideToUTF16(string); + if (search_string.length() <= 0) + return E_INVALIDARG; + + size_t appended_newlines_count = 0; + std::u16string text_range = GetString(-1, &appended_newlines_count); + size_t find_start; + size_t find_length; + if (StringSearch(search_string, text_range, &find_start, &find_length, + ignore_case, backwards) && + find_length > appended_newlines_count) { + // TODO(https://crbug.com/1023599): There is a known issue here related to + // text searches of a |string| starting and ending with a "\n", e.g. + // "\nsometext" or "sometext\n" if the newline is computed from a line + // breaking object. FindText() is rarely called, and when it is, it's not to + // look for a string starting or ending with a newline. This may change + // someday, and if so, we'll have to address this issue. + const AXNode* common_anchor = start()->LowestCommonAnchor(*end()); + AXPositionInstance start_ancestor_position = + start()->CreateAncestorPosition(common_anchor, + ax::mojom::MoveDirection::kForward); + BASE_DCHECK(!start_ancestor_position->IsNullPosition()); + AXPositionInstance end_ancestor_position = end()->CreateAncestorPosition( + common_anchor, ax::mojom::MoveDirection::kForward); + BASE_DCHECK(!end_ancestor_position->IsNullPosition()); + const AXNode* anchor = start_ancestor_position->GetAnchor(); + BASE_DCHECK(anchor); + const int start_offset = + start_ancestor_position->text_offset() + find_start; + const int end_offset = start_offset + find_length - appended_newlines_count; + const int max_end_offset = end_ancestor_position->text_offset(); + BASE_DCHECK(start_offset <= end_offset && end_offset <= max_end_offset); + + AXPositionInstance start = + ui::AXNodePosition::CreateTextPosition( + anchor->tree()->GetAXTreeID(), anchor->id(), start_offset, + ax::mojom::TextAffinity::kDownstream) + ->AsLeafTextPosition(); + AXPositionInstance end = + ui::AXNodePosition::CreateTextPosition( + anchor->tree()->GetAXTreeID(), anchor->id(), end_offset, + ax::mojom::TextAffinity::kDownstream) + ->AsLeafTextPosition(); + + *result = CreateTextRangeProvider(start->Clone(), end->Clone()); + } + return S_OK; +} + +HRESULT AXPlatformNodeTextRangeProviderWin::GetAttributeValue( + TEXTATTRIBUTEID attribute_id, + VARIANT* value) { + UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_OUT(value); + + base::win::VariantVector attribute_value; + + // When the range spans only a generated newline (a generated newline is not + // part of a node, but rather introduced by AXRange::GetText when at a + // paragraph boundary), it doesn't make sense to return the readonly value of + // the start or end anchor since the newline character is not part of any of + // those nodes. Thus, this attribute value is independent from these nodes. + // + // Instead, we should return the readonly attribute value of the common anchor + // for these two endpoints since the newline character has more in common with + // its ancestor than its siblings. Important: This might not be true for all + // attributes, but it appears to be reasonable enough for the readonly one. + // + // To determine if the range encompasses *only* a generated newline, we need + // to validate that both the start and end endpoints are around the same + // paragraph boundary. + if (attribute_id == UIA_IsReadOnlyAttributeId && + start()->anchor_id() != end()->anchor_id() && + start()->AtEndOfParagraph() && end()->AtStartOfParagraph() && + *start()->CreateNextCharacterPosition( + AXBoundaryBehavior::CrossBoundary) == *end()) { + AXPlatformNodeWin* common_anchor = GetLowestAccessibleCommonPlatformNode(); + BASE_DCHECK(common_anchor); + + HRESULT hr = common_anchor->GetTextAttributeValue( + attribute_id, std::nullopt, std::nullopt, &attribute_value); + + if (FAILED(hr)) + return E_FAIL; + + *value = attribute_value.ReleaseAsScalarVariant(); + return S_OK; + } + + // Use a cloned range so that GetAttributeValue does not introduce + // side-effects while normalizing the original range. + AXPositionInstance normalized_start = start()->Clone(); + AXPositionInstance normalized_end = end()->Clone(); + NormalizeTextRange(normalized_start, normalized_end); + + // The range is inclusive, so advance our endpoint to the next position + const auto end_leaf_text_position = normalized_end->AsLeafTextPosition(); + auto end = end_leaf_text_position->CreateNextAnchorPosition(); + + // Iterate over anchor positions + for (auto it = normalized_start->AsLeafTextPosition(); + it->anchor_id() != end->anchor_id() || it->tree_id() != end->tree_id(); + it = it->CreateNextAnchorPosition()) { + // If the iterator creates a null position, then it has likely overrun the + // range, return failure. This is unexpected but may happen if the range + // became inverted. + BASE_DCHECK(!it->IsNullPosition()); + if (it->IsNullPosition()) + return E_FAIL; + + AXPlatformNodeDelegate* delegate = GetDelegate(it.get()); + BASE_DCHECK(it && delegate); + + AXPlatformNodeWin* platform_node = static_cast( + delegate->GetFromNodeID(it->anchor_id())); + BASE_DCHECK(platform_node); + + // Only get attributes for nodes in the tree. Exclude descendants of leaves + // and ignored objects. + platform_node = static_cast( + AXPlatformNode::FromNativeViewAccessible( + platform_node->GetDelegate()->GetLowestPlatformAncestor())); + BASE_DCHECK(platform_node); + + base::win::VariantVector current_value; + const bool at_end_leaf_text_anchor = + it->anchor_id() == end_leaf_text_position->anchor_id() && + it->tree_id() == end_leaf_text_position->tree_id(); + const std::optional start_offset = + it->IsTextPosition() ? std::make_optional(it->text_offset()) + : std::nullopt; + const std::optional end_offset = + at_end_leaf_text_anchor + ? std::make_optional(end_leaf_text_position->text_offset()) + : std::nullopt; + HRESULT hr = platform_node->GetTextAttributeValue( + attribute_id, start_offset, end_offset, ¤t_value); + if (FAILED(hr)) + return E_FAIL; + + if (attribute_value.Type() == VT_EMPTY) { + attribute_value = std::move(current_value); + } else if (attribute_value != current_value) { + V_VT(value) = VT_UNKNOWN; + return ::UiaGetReservedMixedAttributeValue(&V_UNKNOWN(value)); + } + } + + if (ShouldReleaseTextAttributeAsSafearray(attribute_id, attribute_value)) + *value = attribute_value.ReleaseAsSafearrayVariant(); + else + *value = attribute_value.ReleaseAsScalarVariant(); + return S_OK; +} + +HRESULT AXPlatformNodeTextRangeProviderWin::GetBoundingRectangles( + SAFEARRAY** screen_physical_pixel_rectangles) { + UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_OUT(screen_physical_pixel_rectangles); + + *screen_physical_pixel_rectangles = nullptr; + AXNodeRange range(start()->Clone(), end()->Clone()); + AXRangePhysicalPixelRectDelegate rect_delegate(this); + std::vector rects = range.GetRects(&rect_delegate); + + // 4 array items per rect: left, top, width, height + SAFEARRAY* safe_array = SafeArrayCreateVector( + VT_R8 /* element type */, 0 /* lower bound */, rects.size() * 4); + + if (!safe_array) + return E_OUTOFMEMORY; + + if (rects.size() > 0) { + double* double_array = nullptr; + HRESULT hr = SafeArrayAccessData(safe_array, + reinterpret_cast(&double_array)); + + if (SUCCEEDED(hr)) { + for (size_t rect_index = 0; rect_index < rects.size(); rect_index++) { + const gfx::Rect& rect = rects[rect_index]; + double_array[rect_index * 4] = rect.x(); + double_array[rect_index * 4 + 1] = rect.y(); + double_array[rect_index * 4 + 2] = rect.width(); + double_array[rect_index * 4 + 3] = rect.height(); + } + hr = SafeArrayUnaccessData(safe_array); + } + + if (FAILED(hr)) { + BASE_DCHECK(safe_array); + SafeArrayDestroy(safe_array); + return E_FAIL; + } + } + + *screen_physical_pixel_rectangles = safe_array; + return S_OK; +} + +HRESULT AXPlatformNodeTextRangeProviderWin::GetEnclosingElement( + IRawElementProviderSimple** element) { + UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_OUT(element); + + AXPlatformNodeWin* enclosing_node = GetLowestAccessibleCommonPlatformNode(); + if (!enclosing_node) + return UIA_E_ELEMENTNOTAVAILABLE; + + enclosing_node->GetNativeViewAccessible()->QueryInterface( + IID_PPV_ARGS(element)); + + BASE_DCHECK(*element); + return S_OK; +} + +HRESULT AXPlatformNodeTextRangeProviderWin::GetText(int max_count, BSTR* text) { + UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_OUT(text); + + // -1 is a valid value that signifies that the caller wants complete text. + // Any other negative value is an invalid argument. + if (max_count < -1) + return E_INVALIDARG; + + std::wstring full_text = base::UTF16ToWide(GetString(max_count)); + if (!full_text.empty()) { + size_t length = full_text.length(); + + if (max_count != -1 && max_count < static_cast(length)) + *text = SysAllocStringLen(full_text.c_str(), max_count); + else + *text = SysAllocStringLen(full_text.c_str(), length); + } else { + *text = SysAllocString(L""); + } + return S_OK; +} + +HRESULT AXPlatformNodeTextRangeProviderWin::Move(TextUnit unit, + int count, + int* units_moved) { + UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_OUT(units_moved); + + // Per MSDN, move with zero count has no effect. + if (count == 0) + return S_OK; + + // Save a clone of start and end, in case one of the moves fails. + auto start_backup = start()->Clone(); + auto end_backup = end()->Clone(); + bool is_degenerate_range = (*start() == *end()); + + // Move the start of the text range forward or backward in the document by the + // requested number of text unit boundaries. + int start_units_moved = 0; + HRESULT hr = MoveEndpointByUnitImpl(TextPatternRangeEndpoint_Start, unit, + count, &start_units_moved); + + bool succeeded_move = SUCCEEDED(hr) && start_units_moved != 0; + if (succeeded_move) { + SetEnd(start()->Clone()); + if (!is_degenerate_range) { + bool forwards = count > 0; + if (forwards && start()->AtEndOfDocument()) { + // The start is at the end of the document, so move the start backward + // by one text unit to expand the text range from the degenerate range + // state. + int current_start_units_moved = 0; + hr = MoveEndpointByUnitImpl(TextPatternRangeEndpoint_Start, unit, -1, + ¤t_start_units_moved); + start_units_moved -= 1; + succeeded_move = SUCCEEDED(hr) && current_start_units_moved == -1 && + start_units_moved > 0; + } else { + // The start is not at the end of the document, so move the endpoint + // forward by one text unit to expand the text range from the degenerate + // state. + int end_units_moved = 0; + hr = MoveEndpointByUnitImpl(TextPatternRangeEndpoint_End, unit, 1, + &end_units_moved); + succeeded_move = SUCCEEDED(hr) && end_units_moved == 1; + } + + // Because Windows ATs behave undesirably when the start and end endpoints + // are not in the same anchor (for character and word navigation), make + // sure to bring back the end endpoint to the end of the start's anchor. + if (start()->anchor_id() != end()->anchor_id() && + (unit == TextUnit_Character || unit == TextUnit_Word)) { + ExpandToEnclosingUnitImpl(unit); + } + } + } + + if (!succeeded_move) { + SetStart(std::move(start_backup)); + SetEnd(std::move(end_backup)); + start_units_moved = 0; + if (!SUCCEEDED(hr)) + return hr; + } + + *units_moved = start_units_moved; + return S_OK; +} + +HRESULT AXPlatformNodeTextRangeProviderWin::MoveEndpointByUnit( + TextPatternRangeEndpoint endpoint, + TextUnit unit, + int count, + int* units_moved) { + return MoveEndpointByUnitImpl(endpoint, unit, count, units_moved); +} + +HRESULT AXPlatformNodeTextRangeProviderWin::MoveEndpointByUnitImpl( + TextPatternRangeEndpoint endpoint, + TextUnit unit, + int count, + int* units_moved) { + UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_OUT(units_moved); + + // Per MSDN, MoveEndpointByUnit with zero count has no effect. + if (count == 0) { + *units_moved = 0; + return S_OK; + } + + bool is_start_endpoint = endpoint == TextPatternRangeEndpoint_Start; + AXPositionInstance position_to_move = + is_start_endpoint ? start()->Clone() : end()->Clone(); + + AXPositionInstance new_position; + // TODO(schectman): TextUnit_Format implementation. + // https://github.com/flutter/flutter/issues/117792 + switch (unit) { + case TextUnit_Character: + new_position = + MoveEndpointByCharacter(position_to_move, count, units_moved); + break; + case TextUnit_Word: + new_position = MoveEndpointByWord(position_to_move, count, units_moved); + break; + case TextUnit_Line: + new_position = MoveEndpointByLine(position_to_move, is_start_endpoint, + count, units_moved); + break; + case TextUnit_Paragraph: + new_position = MoveEndpointByParagraph( + position_to_move, is_start_endpoint, count, units_moved); + break; + case TextUnit_Page: + new_position = MoveEndpointByPage(position_to_move, is_start_endpoint, + count, units_moved); + break; + case TextUnit_Document: + new_position = + MoveEndpointByDocument(position_to_move, count, units_moved); + break; + default: + return UIA_E_NOTSUPPORTED; + } + if (is_start_endpoint) + SetStart(std::move(new_position)); + else + SetEnd(std::move(new_position)); + + // If the start was moved past the end, create a degenerate range with the end + // equal to the start; do the equivalent if the end moved past the start. + std::optional endpoint_comparison = + AXNodeRange::CompareEndpoints(start().get(), end().get()); + BASE_DCHECK(endpoint_comparison.has_value()); + + if (endpoint_comparison.value_or(0) > 0) { + if (is_start_endpoint) + SetEnd(start()->Clone()); + else + SetStart(end()->Clone()); + } + return S_OK; +} + +HRESULT AXPlatformNodeTextRangeProviderWin::MoveEndpointByRange( + TextPatternRangeEndpoint this_endpoint, + ITextRangeProvider* other, + TextPatternRangeEndpoint other_endpoint) { + UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_IN(other); + + Microsoft::WRL::ComPtr other_provider; + if (other->QueryInterface(IID_PPV_ARGS(&other_provider)) != S_OK) + return UIA_E_INVALIDOPERATION; + + const AXPositionInstance& other_provider_endpoint = + (other_endpoint == TextPatternRangeEndpoint_Start) + ? other_provider->start() + : other_provider->end(); + + if (this_endpoint == TextPatternRangeEndpoint_Start) { + SetStart(other_provider_endpoint->Clone()); + if (*start() > *end()) + SetEnd(start()->Clone()); + } else { + SetEnd(other_provider_endpoint->Clone()); + if (*start() > *end()) + SetStart(end()->Clone()); + } + return S_OK; +} + +HRESULT AXPlatformNodeTextRangeProviderWin::Select() { + UIA_VALIDATE_TEXTRANGEPROVIDER_CALL(); + + AXPositionInstance selection_start = start()->Clone(); + AXPositionInstance selection_end = end()->Clone(); + + // Blink only supports selections within a single tree. So if start_ and end_ + // are in different trees, we can't directly pass them to the render process + // for selection. + if (selection_start->tree_id() != selection_end->tree_id()) { + // Prioritize the end position's tree, as a selection's focus object is the + // end of a selection. + selection_start = selection_end->CreatePositionAtStartOfAXTree(); + } + + BASE_DCHECK(!selection_start->IsNullPosition()); + BASE_DCHECK(!selection_end->IsNullPosition()); + BASE_DCHECK(selection_start->tree_id() == selection_end->tree_id()); + + // TODO(crbug.com/1124051): Blink does not support selection on the list + // markers. So if |selection_start| or |selection_end| are in list markers, we + // don't perform selection and return success. Remove this check once this bug + // is fixed. + if (selection_start->GetAnchor()->IsInListMarker() || + selection_end->GetAnchor()->IsInListMarker()) { + return S_OK; + } + + AXPlatformNodeDelegate* delegate = + GetDelegate(selection_start->tree_id(), selection_start->anchor_id()); + BASE_DCHECK(delegate); + + AXNodeRange new_selection_range(std::move(selection_start), + std::move(selection_end)); + RemoveFocusFromPreviousSelectionIfNeeded(new_selection_range); + + AXActionData action_data; + action_data.anchor_node_id = new_selection_range.anchor()->anchor_id(); + action_data.anchor_offset = new_selection_range.anchor()->text_offset(); + action_data.focus_node_id = new_selection_range.focus()->anchor_id(); + action_data.focus_offset = new_selection_range.focus()->text_offset(); + action_data.action = ax::mojom::Action::kSetSelection; + + delegate->AccessibilityPerformAction(action_data); + return S_OK; +} + +HRESULT AXPlatformNodeTextRangeProviderWin::AddToSelection() { + // Blink does not support disjoint text selections. + return UIA_E_INVALIDOPERATION; +} + +HRESULT +AXPlatformNodeTextRangeProviderWin::RemoveFromSelection() { + // Blink does not support disjoint text selections. + return UIA_E_INVALIDOPERATION; +} + +HRESULT AXPlatformNodeTextRangeProviderWin::ScrollIntoView(BOOL align_to_top) { + UIA_VALIDATE_TEXTRANGEPROVIDER_CALL(); + + const AXPositionInstance start_common_ancestor = + start()->LowestCommonAncestor(*end()); + const AXPositionInstance end_common_ancestor = + end()->LowestCommonAncestor(*start()); + if (start_common_ancestor->IsNullPosition() || + end_common_ancestor->IsNullPosition()) { + return E_INVALIDARG; + } + + const AXNode* common_ancestor_anchor = start_common_ancestor->GetAnchor(); + BASE_DCHECK(common_ancestor_anchor == end_common_ancestor->GetAnchor()); + + const AXTreeID common_ancestor_tree_id = start_common_ancestor->tree_id(); + const AXPlatformNodeDelegate* root_delegate = + GetRootDelegate(common_ancestor_tree_id); + BASE_DCHECK(root_delegate); + const gfx::Rect root_frame_bounds = root_delegate->GetBoundsRect( + AXCoordinateSystem::kFrame, AXClippingBehavior::kUnclipped); + UIA_VALIDATE_BOUNDS(root_frame_bounds); + + const AXPlatformNode* common_ancestor_platform_node = + GetOwner()->GetDelegate()->GetFromTreeIDAndNodeID( + common_ancestor_tree_id, common_ancestor_anchor->id()); + BASE_DCHECK(common_ancestor_platform_node); + AXPlatformNodeDelegate* common_ancestor_delegate = + common_ancestor_platform_node->GetDelegate(); + BASE_DCHECK(common_ancestor_delegate); + const gfx::Rect text_range_container_frame_bounds = + common_ancestor_delegate->GetBoundsRect(AXCoordinateSystem::kFrame, + AXClippingBehavior::kUnclipped); + UIA_VALIDATE_BOUNDS(text_range_container_frame_bounds); + + gfx::Point target_point; + if (align_to_top) { + target_point = gfx::Point(root_frame_bounds.x(), root_frame_bounds.y()); + } else { + target_point = + gfx::Point(root_frame_bounds.x(), + root_frame_bounds.y() + root_frame_bounds.height()); + } + + if ((align_to_top && start()->GetAnchor()->IsText()) || + (!align_to_top && end()->GetAnchor()->IsText())) { + const gfx::Rect text_range_frame_bounds = + common_ancestor_delegate->GetInnerTextRangeBoundsRect( + start_common_ancestor->text_offset(), + end_common_ancestor->text_offset(), AXCoordinateSystem::kFrame, + AXClippingBehavior::kUnclipped); + UIA_VALIDATE_BOUNDS(text_range_frame_bounds); + + if (align_to_top) { + target_point.Offset(0, -(text_range_container_frame_bounds.height() - + text_range_frame_bounds.height())); + } else { + target_point.Offset(0, -text_range_frame_bounds.height()); + } + } else { + if (!align_to_top) + target_point.Offset(0, -text_range_container_frame_bounds.height()); + } + + const gfx::Rect root_screen_bounds = root_delegate->GetBoundsRect( + AXCoordinateSystem::kScreenDIPs, AXClippingBehavior::kUnclipped); + UIA_VALIDATE_BOUNDS(root_screen_bounds); + target_point += root_screen_bounds.OffsetFromOrigin(); + + AXActionData action_data; + action_data.action = ax::mojom::Action::kScrollToPoint; + action_data.target_node_id = common_ancestor_anchor->id(); + action_data.target_point = target_point; + if (!common_ancestor_delegate->AccessibilityPerformAction(action_data)) + return E_FAIL; + return S_OK; +} + +// This function is expected to return a subset of the *direct* children of the +// common ancestor node. The subset should only include the direct children +// included - fully or partially - in the range. +HRESULT AXPlatformNodeTextRangeProviderWin::GetChildren(SAFEARRAY** children) { + UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_OUT(children); + std::vector descendants; + + AXPlatformNodeWin* start_anchor = + GetPlatformNodeFromAXNode(start()->GetAnchor()); + AXPlatformNodeWin* end_anchor = GetPlatformNodeFromAXNode(end()->GetAnchor()); + AXPlatformNodeWin* common_anchor = GetLowestAccessibleCommonPlatformNode(); + if (!common_anchor || !start_anchor || !end_anchor) + return UIA_E_ELEMENTNOTAVAILABLE; + + SAFEARRAY* safe_array = SafeArrayCreateVector(VT_UNKNOWN, 0, 0); + + // TODO(schectman): Implement GetUIADirectChildrenInRange for + // FlutterPlatformNodeDelegate + + *children = safe_array; + return S_OK; +} + +// static +bool AXPlatformNodeTextRangeProviderWin::AtStartOfLinePredicate( + const AXPositionInstance& position) { + return !position->IsIgnored() && position->AtStartOfAnchor() && + (position->AtStartOfLine() || position->AtStartOfInlineBlock()); +} + +// static +bool AXPlatformNodeTextRangeProviderWin::AtEndOfLinePredicate( + const AXPositionInstance& position) { + return !position->IsIgnored() && position->AtEndOfAnchor() && + (position->AtEndOfLine() || position->AtStartOfInlineBlock()); +} + +// static +AXPlatformNodeTextRangeProviderWin::AXPositionInstance +AXPlatformNodeTextRangeProviderWin::GetNextTextBoundaryPosition( + const AXPositionInstance& position, + ax::mojom::TextBoundary boundary_type, + AXBoundaryBehavior options, + ax::mojom::MoveDirection boundary_direction) { + // Override At[Start|End]OfLinePredicate for behavior specific to UIA. + BASE_DCHECK(boundary_type != ax::mojom::TextBoundary::kNone); + switch (boundary_type) { + case ax::mojom::TextBoundary::kLineStart: + return position->CreateBoundaryStartPosition(options, boundary_direction, + &AtStartOfLinePredicate, + &AtEndOfLinePredicate); + case ax::mojom::TextBoundary::kLineEnd: + return position->CreateBoundaryEndPosition(options, boundary_direction, + &AtStartOfLinePredicate, + &AtEndOfLinePredicate); + default: + return position->CreatePositionAtTextBoundary( + boundary_type, boundary_direction, options); + } +} + +std::u16string AXPlatformNodeTextRangeProviderWin::GetString( + int max_count, + size_t* appended_newlines_count) { + AXNodeRange range(start()->Clone(), end()->Clone()); + return range.GetText(AXTextConcatenationBehavior::kAsTextContent, max_count, + false, appended_newlines_count); +} + +AXPlatformNodeWin* AXPlatformNodeTextRangeProviderWin::GetOwner() const { + // Unit tests can't call |GetPlatformNodeFromTree|, so they must provide an + // owner node. + if (owner_for_test_.Get()) + return owner_for_test_.Get(); + + const AXPositionInstance& position = + !start()->IsNullPosition() ? start() : end(); + // If start and end are both null, there's no owner. + if (position->IsNullPosition()) + return nullptr; + + const AXNode* anchor = position->GetAnchor(); + BASE_DCHECK(anchor); + const AXTreeManager* tree_manager = + AXTreeManagerMap::GetInstance().GetManager(anchor->tree()->GetAXTreeID()); + BASE_DCHECK(tree_manager); + const AXPlatformTreeManager* platform_tree_manager = + static_cast(tree_manager); + return static_cast( + platform_tree_manager->GetPlatformNodeFromTree(*anchor)); +} + +AXPlatformNodeDelegate* AXPlatformNodeTextRangeProviderWin::GetDelegate( + const AXPositionInstanceType* position) const { + return GetDelegate(position->tree_id(), position->anchor_id()); +} + +AXPlatformNodeDelegate* AXPlatformNodeTextRangeProviderWin::GetDelegate( + const AXTreeID tree_id, + const AXNode::AXID node_id) const { + AXPlatformNode* platform_node = + GetOwner()->GetDelegate()->GetFromTreeIDAndNodeID(tree_id, node_id); + if (!platform_node) + return nullptr; + + return platform_node->GetDelegate(); +} + +AXPlatformNodeTextRangeProviderWin::AXPositionInstance +AXPlatformNodeTextRangeProviderWin::MoveEndpointByCharacter( + const AXPositionInstance& endpoint, + const int count, + int* units_moved) { + return MoveEndpointByUnitHelper(std::move(endpoint), + ax::mojom::TextBoundary::kCharacter, count, + units_moved); +} + +AXPlatformNodeTextRangeProviderWin::AXPositionInstance +AXPlatformNodeTextRangeProviderWin::MoveEndpointByWord( + const AXPositionInstance& endpoint, + const int count, + int* units_moved) { + return MoveEndpointByUnitHelper(std::move(endpoint), + ax::mojom::TextBoundary::kWordStart, count, + units_moved); +} + +AXPlatformNodeTextRangeProviderWin::AXPositionInstance +AXPlatformNodeTextRangeProviderWin::MoveEndpointByLine( + const AXPositionInstance& endpoint, + bool is_start_endpoint, + const int count, + int* units_moved) { + return MoveEndpointByUnitHelper(std::move(endpoint), + is_start_endpoint + ? ax::mojom::TextBoundary::kLineStart + : ax::mojom::TextBoundary::kLineEnd, + count, units_moved); +} + +AXPlatformNodeTextRangeProviderWin::AXPositionInstance +AXPlatformNodeTextRangeProviderWin::MoveEndpointByParagraph( + const AXPositionInstance& endpoint, + const bool is_start_endpoint, + const int count, + int* units_moved) { + return MoveEndpointByUnitHelper(std::move(endpoint), + is_start_endpoint + ? ax::mojom::TextBoundary::kParagraphStart + : ax::mojom::TextBoundary::kParagraphEnd, + count, units_moved); +} + +AXPlatformNodeTextRangeProviderWin::AXPositionInstance +AXPlatformNodeTextRangeProviderWin::MoveEndpointByPage( + const AXPositionInstance& endpoint, + const bool is_start_endpoint, + const int count, + int* units_moved) { + // Per UIA spec, if the document containing the current endpoint doesn't + // support pagination, default to document navigation. + // + // Note that the "ax::mojom::MoveDirection" should not matter when calculating + // the ancestor position for use when navigating by page or document, so we + // use a backward direction as the default. + AXPositionInstance common_ancestor = start()->LowestCommonAncestor(*end()); + if (!common_ancestor->GetAnchor()->tree()->HasPaginationSupport()) + return MoveEndpointByDocument(std::move(endpoint), count, units_moved); + + return MoveEndpointByUnitHelper(std::move(endpoint), + is_start_endpoint + ? ax::mojom::TextBoundary::kPageStart + : ax::mojom::TextBoundary::kPageEnd, + count, units_moved); +} + +AXPlatformNodeTextRangeProviderWin::AXPositionInstance +AXPlatformNodeTextRangeProviderWin::MoveEndpointByDocument( + const AXPositionInstance& endpoint, + const int count, + int* units_moved) { + BASE_DCHECK(count != 0); + + if (count < 0) { + *units_moved = !endpoint->AtStartOfDocument() ? -1 : 0; + + return endpoint->CreatePositionAtStartOfDocument(); + } + *units_moved = !endpoint->AtEndOfDocument() ? 1 : 0; + return endpoint->CreatePositionAtEndOfDocument(); +} + +AXPlatformNodeTextRangeProviderWin::AXPositionInstance +AXPlatformNodeTextRangeProviderWin::MoveEndpointByUnitHelper( + const AXPositionInstance& endpoint, + const ax::mojom::TextBoundary boundary_type, + const int count, + int* units_moved) { + BASE_DCHECK(count != 0); + const ax::mojom::MoveDirection boundary_direction = + (count > 0) ? ax::mojom::MoveDirection::kForward + : ax::mojom::MoveDirection::kBackward; + + const AXNode* initial_endpoint = endpoint->GetAnchor(); + + // Most of the methods used to create the next/previous position go back and + // forth creating a leaf text position and rooting the result to the original + // position's anchor; avoid this by normalizing to a leaf text position. + AXPositionInstance current_endpoint = endpoint->AsLeafTextPosition(); + AXPositionInstance next_endpoint = GetNextTextBoundaryPosition( + current_endpoint, boundary_type, + AXBoundaryBehavior::StopAtLastAnchorBoundary, boundary_direction); + BASE_DCHECK(next_endpoint->IsLeafTextPosition()); + + bool is_ignored_for_text_navigation = false; + int iteration = 0; + // Since AXBoundaryBehavior::kStopAtLastAnchorBoundary forces the next + // text boundary position to be different than the input position, the + // only case where these are equal is when they're already located at the + // last anchor boundary. In such case, there is no next position to move + // to. + while (iteration < std::abs(count) && + !(next_endpoint->GetAnchor() == current_endpoint->GetAnchor() && + *next_endpoint == *current_endpoint)) { + is_ignored_for_text_navigation = false; + current_endpoint = std::move(next_endpoint); + + next_endpoint = GetNextTextBoundaryPosition( + current_endpoint, boundary_type, + AXBoundaryBehavior::StopAtLastAnchorBoundary, boundary_direction); + BASE_DCHECK(next_endpoint->IsLeafTextPosition()); + + // Loop until we're not on a position that is ignored for text navigation. + // There is one exception for character navigation - since the ignored + // anchor is represented by an embedded object character, we allow + // navigation by character for consistency (i.e. you should be able to + // move by character the same number of characters that are represented by + // the ranges flat string buffer). + is_ignored_for_text_navigation = + boundary_type != ax::mojom::TextBoundary::kCharacter && + current_endpoint->GetAnchor()->data().role != + ax::mojom::Role::kSplitter; + if (!is_ignored_for_text_navigation) + iteration++; + } + + *units_moved = (count > 0) ? iteration : -iteration; + + if (is_ignored_for_text_navigation && + initial_endpoint != current_endpoint->GetAnchor()) { + // If the last node in the tree is ignored for text navigation, we + // should still be able to return an endpoint located on that node. We + // also need to ensure that the value of |units_moved| is accurate. + *units_moved += (count > 0) ? 1 : -1; + } + + return current_endpoint; +} + +void AXPlatformNodeTextRangeProviderWin::NormalizeTextRange( + AXPositionInstance& start, + AXPositionInstance& end) { + if (!start->IsValid() || !end->IsValid()) + return; + + // If either endpoint is anchored to an ignored node, + // first snap them both to be unignored positions. + NormalizeAsUnignoredTextRange(start, end); + + bool is_degenerate = *start == *end; + AXPositionInstance normalized_start = + is_degenerate ? start->Clone() + : start->AsLeafTextPositionBeforeCharacter(); + + // For a degenerate range, the |end_| will always be the same as the + // normalized start, so there's no need to compute the normalized end. + // However, a degenerate range might go undetected if there's an ignored node + // (or many) between the two endpoints. For this reason, we need to + // compare the |end_| with both the |start_| and the |normalized_start|. + is_degenerate = is_degenerate || *normalized_start == *end; + AXPositionInstance normalized_end = + is_degenerate ? normalized_start->Clone() + : end->AsLeafTextPositionAfterCharacter(); + + if (!normalized_start->IsNullPosition() && + !normalized_end->IsNullPosition()) { + start = std::move(normalized_start); + end = std::move(normalized_end); + } + + BASE_DCHECK(*start <= *end); +} + +// static +void AXPlatformNodeTextRangeProviderWin::NormalizeAsUnignoredPosition( + AXPositionInstance& position) { + if (position->IsNullPosition() || !position->IsValid()) + return; + + if (position->IsIgnored()) { + AXPositionInstance normalized_position = position->AsUnignoredPosition( + AXPositionAdjustmentBehavior::kMoveForward); + if (normalized_position->IsNullPosition()) { + normalized_position = position->AsUnignoredPosition( + AXPositionAdjustmentBehavior::kMoveBackward); + } + + if (!normalized_position->IsNullPosition()) + position = std::move(normalized_position); + } + BASE_DCHECK(!position->IsNullPosition()); +} + +// static +void AXPlatformNodeTextRangeProviderWin::NormalizeAsUnignoredTextRange( + AXPositionInstance& start, + AXPositionInstance& end) { + if (!start->IsValid() || !end->IsValid()) + return; + + if (!start->IsIgnored() && !end->IsIgnored()) + return; + NormalizeAsUnignoredPosition(start); + NormalizeAsUnignoredPosition(end); + BASE_DCHECK(*start <= *end); +} + +AXPlatformNodeDelegate* AXPlatformNodeTextRangeProviderWin::GetRootDelegate( + const ui::AXTreeID tree_id) { + const AXTreeManager* ax_tree_manager = + AXTreeManagerMap::GetInstance().GetManager(tree_id); + BASE_DCHECK(ax_tree_manager); + AXNode* root_node = ax_tree_manager->GetRootAsAXNode(); + const AXPlatformNode* root_platform_node = + GetOwner()->GetDelegate()->GetFromTreeIDAndNodeID(tree_id, + root_node->id()); + BASE_DCHECK(root_platform_node); + return root_platform_node->GetDelegate(); +} + +void AXPlatformNodeTextRangeProviderWin::SetStart( + AXPositionInstance new_start) { + endpoints_.SetStart(std::move(new_start)); +} + +void AXPlatformNodeTextRangeProviderWin::SetEnd(AXPositionInstance new_end) { + endpoints_.SetEnd(std::move(new_end)); +} + +void AXPlatformNodeTextRangeProviderWin::SetOwnerForTesting( + AXPlatformNodeWin* owner) { + owner_for_test_ = owner; +} + +AXNode* AXPlatformNodeTextRangeProviderWin::GetSelectionCommonAnchor() { + AXPlatformNodeDelegate* delegate = GetOwner()->GetDelegate(); + AXTree::Selection unignored_selection = delegate->GetUnignoredSelection(); + AXPlatformNode* anchor_object = + delegate->GetFromNodeID(unignored_selection.anchor_object_id); + AXPlatformNode* focus_object = + delegate->GetFromNodeID(unignored_selection.focus_object_id); + + if (!anchor_object || !focus_object) + return nullptr; + + AXNodePosition::AXPositionInstance start = + anchor_object->GetDelegate()->CreateTextPositionAt( + unignored_selection.anchor_offset); + AXNodePosition::AXPositionInstance end = + focus_object->GetDelegate()->CreateTextPositionAt( + unignored_selection.focus_offset); + + return start->LowestCommonAnchor(*end); +} + +// When the current selection is inside a focusable element, the DOM focused +// element will correspond to this element. When we update the selection to be +// on a different element that is not focusable, the new selection won't be +// applied unless we remove the DOM focused element. For example, with Narrator, +// if we move by word from a text field (focusable) to a static text (not +// focusable), the selection will stay on the text field because the DOM focused +// element will still be the text field. To avoid that, we need to remove the +// focus from this element. Since |ax::mojom::Action::kBlur| is not implemented, +// we perform a |ax::mojom::Action::focus| action on the root node. The result +// is the same. +void AXPlatformNodeTextRangeProviderWin:: + RemoveFocusFromPreviousSelectionIfNeeded(const AXNodeRange& new_selection) { + const AXNode* old_selection_node = GetSelectionCommonAnchor(); + const AXNode* new_selection_node = + new_selection.anchor()->LowestCommonAnchor(*new_selection.focus()); + + if (!old_selection_node) + return; + + if (!new_selection_node || + (old_selection_node->data().HasState(ax::mojom::State::kFocusable) && + !new_selection_node->data().HasState(ax::mojom::State::kFocusable))) { + AXPlatformNodeDelegate* root_delegate = + GetRootDelegate(old_selection_node->tree()->GetAXTreeID()); + BASE_DCHECK(root_delegate); + + AXActionData focus_action; + focus_action.action = ax::mojom::Action::kFocus; + root_delegate->AccessibilityPerformAction(focus_action); + } +} + +AXPlatformNodeWin* +AXPlatformNodeTextRangeProviderWin::GetPlatformNodeFromAXNode( + const AXNode* node) const { + if (!node) + return nullptr; + + // TODO(kschmi): Update to use AXTreeManager. + AXPlatformNodeWin* platform_node = + static_cast(AXPlatformNode::FromNativeViewAccessible( + GetDelegate(node->tree()->GetAXTreeID(), node->id()) + ->GetNativeViewAccessible())); + BASE_DCHECK(platform_node); + + return platform_node; +} + +AXPlatformNodeWin* +AXPlatformNodeTextRangeProviderWin::GetLowestAccessibleCommonPlatformNode() + const { + AXNode* common_anchor = start()->LowestCommonAnchor(*end()); + if (!common_anchor) + return nullptr; + + return GetPlatformNodeFromAXNode(common_anchor)->GetLowestAccessibleElement(); +} + +// static +bool AXPlatformNodeTextRangeProviderWin::TextAttributeIsArrayType( + TEXTATTRIBUTEID attribute_id) { + // https://docs.microsoft.com/en-us/windows/win32/winauto/uiauto-textattribute-ids + return attribute_id == UIA_AnnotationObjectsAttributeId || + attribute_id == UIA_AnnotationTypesAttributeId || + attribute_id == UIA_TabsAttributeId; +} + +// static +bool AXPlatformNodeTextRangeProviderWin::TextAttributeIsUiaReservedValue( + const base::win::VariantVector& vector) { + // Reserved values are always IUnknown. + if (vector.Type() != VT_UNKNOWN) + return false; + + base::win::ScopedVariant mixed_attribute_value_variant; + { + Microsoft::WRL::ComPtr mixed_attribute_value; + HRESULT hr = ::UiaGetReservedMixedAttributeValue(&mixed_attribute_value); + BASE_DCHECK(SUCCEEDED(hr)); + mixed_attribute_value_variant.Set(mixed_attribute_value.Get()); + } + + base::win::ScopedVariant not_supported_value_variant; + { + Microsoft::WRL::ComPtr not_supported_value; + HRESULT hr = ::UiaGetReservedNotSupportedValue(¬_supported_value); + BASE_DCHECK(SUCCEEDED(hr)); + not_supported_value_variant.Set(not_supported_value.Get()); + } + + return !vector.Compare(mixed_attribute_value_variant) || + !vector.Compare(not_supported_value_variant); +} + +// static +bool AXPlatformNodeTextRangeProviderWin::ShouldReleaseTextAttributeAsSafearray( + TEXTATTRIBUTEID attribute_id, + const base::win::VariantVector& attribute_value) { + // |vector| may be pre-populated with a UIA reserved value. In such a case, we + // must release as a scalar variant. + return TextAttributeIsArrayType(attribute_id) && + !TextAttributeIsUiaReservedValue(attribute_value); +} + +AXPlatformNodeTextRangeProviderWin::TextRangeEndpoints::TextRangeEndpoints() { + start_ = AXNodePosition::CreateNullPosition(); + end_ = AXNodePosition::CreateNullPosition(); +} + +AXPlatformNodeTextRangeProviderWin::TextRangeEndpoints::~TextRangeEndpoints() { + SetStart(AXNodePosition::CreateNullPosition()); + SetEnd(AXNodePosition::CreateNullPosition()); +} + +void AXPlatformNodeTextRangeProviderWin::TextRangeEndpoints::SetStart( + AXPositionInstance new_start) { + bool did_tree_change = start_->tree_id() != new_start->tree_id(); + // TODO(bebeaudr): We can't use IsNullPosition() here because of + // https://crbug.com/1152939. Once this is fixed, we can go back to + // IsNullPosition(). + if (did_tree_change && start_->kind() != AXPositionKind::NULL_POSITION && + start_->tree_id() != end_->tree_id()) { + RemoveObserver(start_->tree_id()); + } + + start_ = std::move(new_start); + + if (did_tree_change && !start_->IsNullPosition() && + start_->tree_id() != end_->tree_id()) { + AddObserver(start_->tree_id()); + } +} + +void AXPlatformNodeTextRangeProviderWin::TextRangeEndpoints::SetEnd( + AXPositionInstance new_end) { + bool did_tree_change = end_->tree_id() != new_end->tree_id(); + // TODO(bebeaudr): We can't use IsNullPosition() here because of + // https://crbug.com/1152939. Once this is fixed, we can go back to + // IsNullPosition(). + if (did_tree_change && end_->kind() != AXPositionKind::NULL_POSITION && + end_->tree_id() != start_->tree_id()) { + RemoveObserver(end_->tree_id()); + } + + end_ = std::move(new_end); + + if (did_tree_change && !end_->IsNullPosition() && + start_->tree_id() != end_->tree_id()) { + AddObserver(end_->tree_id()); + } +} + +void AXPlatformNodeTextRangeProviderWin::TextRangeEndpoints::AddObserver( + const AXTreeID tree_id) { + AXTreeManager* ax_tree_manager = + AXTreeManagerMap::GetInstance().GetManager(tree_id); + BASE_DCHECK(ax_tree_manager); + ax_tree_manager->GetTree()->AddObserver(this); +} + +void AXPlatformNodeTextRangeProviderWin::TextRangeEndpoints::RemoveObserver( + const AXTreeID tree_id) { + AXTreeManager* ax_tree_manager = + AXTreeManagerMap::GetInstance().GetManager(tree_id); + if (ax_tree_manager) + ax_tree_manager->GetTree()->RemoveObserver(this); +} + +// Ensures that our endpoints are located on non-deleted nodes (step 1, case A +// and B). See comment in header file for more details. +void AXPlatformNodeTextRangeProviderWin::TextRangeEndpoints:: + OnSubtreeWillBeDeleted(AXTree* tree, AXNode* node) { + // If an endpoint is on a node that is included in a subtree that is about to + // be deleted, move endpoint up to the parent of the deleted subtree's root + // since we want to ensure that the endpoints of a text range provider are + // always valid positions. Otherwise, the range will be stuck on nodes that + // don't exist anymore. + BASE_DCHECK(tree); + BASE_DCHECK(node); + BASE_DCHECK(tree->GetAXTreeID() == node->tree()->GetAXTreeID()); + + AdjustEndpointForSubtreeDeletion(tree, node, true /* is_start_endpoint */); + AdjustEndpointForSubtreeDeletion(tree, node, false /* is_start_endpoint */); +} + +void AXPlatformNodeTextRangeProviderWin::TextRangeEndpoints:: + AdjustEndpointForSubtreeDeletion(AXTree* tree, + const AXNode* const node, + bool is_start_endpoint) { + AXPositionInstance endpoint = + is_start_endpoint ? start_->Clone() : end_->Clone(); + if (tree->GetAXTreeID() != endpoint->tree_id()) + return; + + // When the subtree of the root node will be deleted, we can be certain that + // our endpoint should be invalidated. We know it's the root node when the + // node doesn't have a parent. + AXNode* endpoint_anchor = endpoint->GetAnchor(); + if (!node->parent() || !endpoint_anchor) { + is_start_endpoint ? SetStart(AXNodePosition::CreateNullPosition()) + : SetEnd(AXNodePosition::CreateNullPosition()); + return; + } + + DeletionOfInterest deletion_of_interest = {tree->GetAXTreeID(), node->id()}; + + // If the root of subtree being deleted is a child of the anchor of the + // endpoint, ensure `AXPosition::AsValidPosition` is called after the node is + // deleted so that the index doesn't go out of bounds of the child array. + if (endpoint->kind() == AXPositionKind::TREE_POSITION && + endpoint_anchor == node->parent()) { + if (is_start_endpoint) + validation_necessary_for_start_ = deletion_of_interest; + else + validation_necessary_for_end_ = deletion_of_interest; + return; + } + + // Fast check for the common case - there are many tree updates and the + // endpoints probably are not in the deleted subtree. Note that + // CreateAncestorPosition/GetParentPosition can be expensive for text + // positions. + if (!endpoint_anchor->IsDescendantOfCrossingTreeBoundary(node)) + return; + + AXPositionInstance new_endpoint = endpoint->CreateAncestorPosition( + node, ax::mojom::MoveDirection::kForward); + + // Obviously, we want the position to be on the parent of |node| and not on + // |node| itself since it's about to be deleted. + new_endpoint = new_endpoint->CreateParentPosition(); + AXPositionInstance other_endpoint = + is_start_endpoint ? end_->Clone() : start_->Clone(); + + // Convert |new_endpoint| and |other_endpoint| to unignored positions to avoid + // AXPosition::SlowCompareTo in the < operator below. + NormalizeAsUnignoredPosition(new_endpoint); + NormalizeAsUnignoredPosition(other_endpoint); + BASE_DCHECK(!new_endpoint->IsIgnored()); + BASE_DCHECK(!other_endpoint->IsIgnored()); + + // If after all the above operations we're still left with a new endpoint that + // is a descendant of the subtree root being deleted, just point at a null + // position and don't crash later on. This can happen when the entire parent + // chain of the subtree is ignored. + endpoint_anchor = new_endpoint->GetAnchor(); + if (!endpoint_anchor || + endpoint_anchor->IsDescendantOfCrossingTreeBoundary(node)) + new_endpoint = AXNodePosition::CreateNullPosition(); + + // Create a degenerate range at the new position if we have an inverted range + // - which occurs when the |end_| comes before the |start_|. This could have + // happened due to the new endpoint walking forwards or backwards when + // normalizing above. If we don't set the opposite endpoint to something that + // we know will be safe (i.e. not in a deleted subtree) we'll crash later on + // when trying to create a valid position. + if (is_start_endpoint) { + if (*other_endpoint < *new_endpoint) + SetEnd(new_endpoint->Clone()); + + SetStart(std::move(new_endpoint)); + validation_necessary_for_start_ = deletion_of_interest; + } else { + if (*new_endpoint < *other_endpoint) + SetStart(new_endpoint->Clone()); + + SetEnd(std::move(new_endpoint)); + validation_necessary_for_end_ = deletion_of_interest; + } +} + +// Ensures that our endpoints are always valid (step 2, all scenarios). See +// comment in header file for more details. +void AXPlatformNodeTextRangeProviderWin::TextRangeEndpoints::OnNodeDeleted( + AXTree* tree, + AXNode::AXID node_id) { + BASE_DCHECK(tree); + + if (validation_necessary_for_start_.has_value() && + validation_necessary_for_start_->tree_id == tree->GetAXTreeID() && + validation_necessary_for_start_->node_id == node_id) { + if (!start_->IsNullPosition() && start_->GetAnchor()->data().id != 0) + SetStart(start_->AsValidPosition()); + else + SetStart(AXNodePosition::CreateNullPosition()); + + validation_necessary_for_start_ = std::nullopt; + } + + if (validation_necessary_for_end_.has_value() && + validation_necessary_for_end_->tree_id == tree->GetAXTreeID() && + validation_necessary_for_end_->node_id == node_id) { + if (!end_->IsNullPosition() && end_->GetAnchor()->data().id != 0) + SetEnd(end_->AsValidPosition()); + else + SetEnd(AXNodePosition::CreateNullPosition()); + + validation_necessary_for_end_ = std::nullopt; + } +} + +} // namespace ui diff --git a/third_party/accessibility/ax/platform/ax_platform_node_textrangeprovider_win.h b/third_party/accessibility/ax/platform/ax_platform_node_textrangeprovider_win.h new file mode 100644 index 0000000000000..78b6aa75ff444 --- /dev/null +++ b/third_party/accessibility/ax/platform/ax_platform_node_textrangeprovider_win.h @@ -0,0 +1,285 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UI_ACCESSIBILITY_PLATFORM_AX_PLATFORM_NODE_TEXTRANGEPROVIDER_WIN_H_ +#define UI_ACCESSIBILITY_PLATFORM_AX_PLATFORM_NODE_TEXTRANGEPROVIDER_WIN_H_ + +#include +#include + +#include + +#include "ax/ax_node_position.h" +#include "ax/ax_tree_observer.h" +#include "ax/platform/ax_platform_node_delegate.h" +#include "ax/platform/ax_platform_node_win.h" + +namespace ui { + +class AX_EXPORT __declspec(uuid("3071e40d-a10d-45ff-a59f-6e8e1138e2c1")) + AXPlatformNodeTextRangeProviderWin + : public CComObjectRootEx, + public ITextRangeProvider { + public: + BEGIN_COM_MAP(AXPlatformNodeTextRangeProviderWin) + COM_INTERFACE_ENTRY(ITextRangeProvider) + COM_INTERFACE_ENTRY(AXPlatformNodeTextRangeProviderWin) + END_COM_MAP() + + AXPlatformNodeTextRangeProviderWin(); + ~AXPlatformNodeTextRangeProviderWin(); + + static ITextRangeProvider* CreateTextRangeProvider( + AXNodePosition::AXPositionInstance start, + AXNodePosition::AXPositionInstance end); + + // Creates an instance of the class for unit tests, where AXPlatformNodes + // cannot be queried automatically from endpoints. + static ITextRangeProvider* CreateTextRangeProviderForTesting( + AXPlatformNodeWin* owner, + AXNodePosition::AXPositionInstance start, + AXNodePosition::AXPositionInstance end); + + // + // ITextRangeProvider methods. + // + + IFACEMETHODIMP Clone(ITextRangeProvider** clone) override; + IFACEMETHODIMP Compare(ITextRangeProvider* other, BOOL* result) override; + IFACEMETHODIMP + CompareEndpoints(TextPatternRangeEndpoint this_endpoint, + ITextRangeProvider* other, + TextPatternRangeEndpoint other_endpoint, + int* result) override; + IFACEMETHODIMP ExpandToEnclosingUnit(TextUnit unit) override; + IFACEMETHODIMP + FindAttribute(TEXTATTRIBUTEID attribute_id, + VARIANT attribute_val, + BOOL is_backward, + ITextRangeProvider** result) override; + IFACEMETHODIMP + FindText(BSTR string, + BOOL backwards, + BOOL ignore_case, + ITextRangeProvider** result) override; + IFACEMETHODIMP GetAttributeValue(TEXTATTRIBUTEID attribute_id, + VARIANT* value) override; + IFACEMETHODIMP + GetBoundingRectangles(SAFEARRAY** screen_physical_pixel_rectangles) override; + IFACEMETHODIMP + GetEnclosingElement(IRawElementProviderSimple** element) override; + IFACEMETHODIMP GetText(int max_count, BSTR* text) override; + IFACEMETHODIMP Move(TextUnit unit, int count, int* units_moved) override; + IFACEMETHODIMP + MoveEndpointByUnit(TextPatternRangeEndpoint endpoint, + TextUnit unit, + int count, + int* units_moved) override; + IFACEMETHODIMP + MoveEndpointByRange(TextPatternRangeEndpoint this_endpoint, + ITextRangeProvider* other, + TextPatternRangeEndpoint other_endpoint) override; + IFACEMETHODIMP Select() override; + IFACEMETHODIMP AddToSelection() override; + IFACEMETHODIMP RemoveFromSelection() override; + IFACEMETHODIMP ScrollIntoView(BOOL align_to_top) override; + IFACEMETHODIMP GetChildren(SAFEARRAY** children) override; + + AXPlatformNodeWin* GetOwner() const; + void SetOwnerForTesting(AXPlatformNodeWin* owner); + + private: + using AXPositionInstance = AXNodePosition::AXPositionInstance; + using AXPositionInstanceType = typename AXPositionInstance::element_type; + using AXNodeRange = AXRange; + + friend class AXPlatformNodeTextRangeProviderTest; + friend class AXPlatformNodeTextProviderTest; + friend class AXRangePhysicalPixelRectDelegate; + + static bool AtStartOfLinePredicate(const AXPositionInstance& position); + static bool AtEndOfLinePredicate(const AXPositionInstance& position); + + static AXPositionInstance GetNextTextBoundaryPosition( + const AXPositionInstance& position, + ax::mojom::TextBoundary boundary_type, + AXBoundaryBehavior options, + ax::mojom::MoveDirection boundary_direction); + + // Prefer these *Impl methods when functionality is needed internally. We + // should avoid calling external APIs internally as it will cause the + // histograms to become innaccurate. + HRESULT MoveEndpointByUnitImpl(TextPatternRangeEndpoint endpoint, + TextUnit unit, + int count, + int* units_moved); + + IFACEMETHODIMP ExpandToEnclosingUnitImpl(TextUnit unit); + + std::u16string GetString(int max_count, + size_t* appended_newlines_count = nullptr); + const AXPositionInstance& start() const { return endpoints_.GetStart(); } + const AXPositionInstance& end() const { return endpoints_.GetEnd(); } + AXPlatformNodeDelegate* GetDelegate( + const AXPositionInstanceType* position) const; + AXPlatformNodeDelegate* GetDelegate(const AXTreeID tree_id, + const AXNode::AXID node_id) const; + + template + HRESULT FindAttributeRange(const TEXTATTRIBUTEID text_attribute_id, + VARIANT attribute_val, + const AnchorIterator first, + const AnchorIterator last, + ExpandMatchLambda expand_match); + + AXPositionInstance MoveEndpointByCharacter(const AXPositionInstance& endpoint, + const int count, + int* units_moved); + AXPositionInstance MoveEndpointByWord(const AXPositionInstance& endpoint, + const int count, + int* units_moved); + AXPositionInstance MoveEndpointByLine(const AXPositionInstance& endpoint, + bool is_start_endpoint, + const int count, + int* units_moved); + AXPositionInstance MoveEndpointByParagraph(const AXPositionInstance& endpoint, + const bool is_start_endpoint, + const int count, + int* units_moved); + AXPositionInstance MoveEndpointByPage(const AXPositionInstance& endpoint, + const bool is_start_endpoint, + const int count, + int* units_moved); + AXPositionInstance MoveEndpointByDocument(const AXPositionInstance& endpoint, + const int count, + int* units_moved); + + AXPositionInstance MoveEndpointByUnitHelper( + const AXPositionInstance& endpoint, + const ax::mojom::TextBoundary boundary_type, + const int count, + int* units_moved); + + // A text range normalization is necessary to prevent a |start_| endpoint to + // be positioned at the end of an anchor when it can be at the start of the + // next anchor. After normalization, it is guaranteed that: + // * both endpoints passed by parameter are always positioned on unignored + // anchors; + // * both endpoints passed by parameter are never between a grapheme cluster; + // * if the endpoints passed by parameter create a degenerate range, both + // endpoints are on the same anchor. + // Normalization never updates the internal endpoints directly. Instead, it + // normalizes the endpoints passed by parameter. + void NormalizeTextRange(AXPositionInstance& start, AXPositionInstance& end); + static void NormalizeAsUnignoredPosition(AXPositionInstance& position); + static void NormalizeAsUnignoredTextRange(AXPositionInstance& start, + AXPositionInstance& end); + + AXPlatformNodeDelegate* GetRootDelegate(const ui::AXTreeID tree_id); + AXNode* GetSelectionCommonAnchor(); + void RemoveFocusFromPreviousSelectionIfNeeded( + const AXNodeRange& new_selection); + AXPlatformNodeWin* GetPlatformNodeFromAXNode(const AXNode* node) const; + AXPlatformNodeWin* GetLowestAccessibleCommonPlatformNode() const; + bool HasTextRangeOrSelectionInAtomicTextField( + const AXPositionInstance& start_position, + const AXPositionInstance& end_position) const; + + void SetStart(AXPositionInstance start); + void SetEnd(AXPositionInstance end); + + static bool TextAttributeIsArrayType(TEXTATTRIBUTEID attribute_id); + static bool TextAttributeIsUiaReservedValue( + const base::win::VariantVector& vector); + static bool ShouldReleaseTextAttributeAsSafearray( + TEXTATTRIBUTEID attribute_id, + const base::win::VariantVector& vector); + + Microsoft::WRL::ComPtr owner_for_test_; + + // The TextRangeEndpoints class has the responsibility of keeping the + // endpoints of the range valid or nullify them when it can't find a valid + // alternative. + // + // An endpoint can become invalid when + // A. the node it's on gets deleted, + // B. when an ancestor node gets deleted, deleting the subtree our endpoint + // is on, or + // C. when a descendant node gets deleted, potentially rendering the + // position invalid due to a smaller MaxTextOffset value (for a text + // position) or fewer child nodes (for a tree position). + // + // In all cases, our approach to resolve the endpoints to valid positions + // takes two steps: + // 1. Move the endpoint to an equivalent ancestor position before the node + // gets deleted - we can't move the position once the node it's on is + // deleted since this position would already be considered invalid. + // 2. Call AsValidPosition on that new position once the node is deleted - + // calling this function before the node gets deleted wouldn't do much + // since our position would still be considered valid at this point. + // + // Because AsValidPosition can potentially be expensive, we only want to run + // it when necessary. For this reason, we store the node ID and tree ID that + // causes the first step to happen and only run the second step in + // OnNodeDeleted for the corresponding node deletion. When OnNodeDeleted is + // called, the |start_| and |end_| endpoints have already been moved up to an + // ancestor that is still part of the tree. This is to ensure that we don't + // have to read the node/tree structure of the deleted node in that function - + // which would likely result in a crash. + // + // Both scenarios A and B are fixed by this approach (by the implementation of + // OnSubtreeWillBeDeleted), but we still have work to do to fix scenario C. + // This case, in theory, would only require the second step to ensure that the + // position is always valid but computing whether node is part of the subtree + // of the endpoint we're on would be very expensive. Furthermore, because the + // endpoints are generally on leaf nodes, the scenario is unlikely - we + // haven't heard of issues caused by this scenario yet. Eventually, we might + // be able to scope the fix to specific use cases, like when the range is on + // UIA embedded object (e.g. button, select, etc.) + // + // *** + // + // Why we can't use a ScopedObserver here: + // We tried using a ScopedObserver instead of a simple observer in this case, + // but there appears to be a problem with the lifetime of the referenced + // AXTreeManager in the ScopedObserver. The AXTreeManager can get deleted + // before the TextRangeEndpoints does, so when the destructor of the + // ScopedObserver calls ScopedObserver::RemoveAll on an already deleted + // AXTreeManager, it crashes. + class TextRangeEndpoints : public AXTreeObserver { + public: + TextRangeEndpoints(); + ~TextRangeEndpoints() override; + const AXPositionInstance& GetStart() const { return start_; } + const AXPositionInstance& GetEnd() const { return end_; } + void SetStart(AXPositionInstance new_start); + void SetEnd(AXPositionInstance new_end); + + void AddObserver(const AXTreeID tree_id); + void RemoveObserver(const AXTreeID tree_id); + void OnSubtreeWillBeDeleted(AXTree* tree, AXNode* node) override; + void OnNodeDeleted(AXTree* tree, AXNode::AXID node_id) override; + + private: + struct DeletionOfInterest { + AXTreeID tree_id; + AXNode::AXID node_id; + }; + + void AdjustEndpointForSubtreeDeletion(AXTree* tree, + const AXNode* const node, + bool is_start_endpoint); + + AXPositionInstance start_; + AXPositionInstance end_; + + std::optional validation_necessary_for_start_; + std::optional validation_necessary_for_end_; + }; + TextRangeEndpoints endpoints_; +}; + +} // namespace ui + +#endif // UI_ACCESSIBILITY_PLATFORM_AX_PLATFORM_NODE_TEXTRANGEPROVIDER_WIN_H_ diff --git a/third_party/accessibility/ax/platform/ax_platform_node_textrangeprovider_win_unittest.cc b/third_party/accessibility/ax/platform/ax_platform_node_textrangeprovider_win_unittest.cc new file mode 100644 index 0000000000000..40bcc10bf5243 --- /dev/null +++ b/third_party/accessibility/ax/platform/ax_platform_node_textrangeprovider_win_unittest.cc @@ -0,0 +1,7565 @@ +// Copyright 2019 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 "ax/platform/ax_platform_node_win_unittest.h" + +#include +#include + +#include +#include + +#include "ax/ax_tree.h" +#include "ax/platform/ax_fragment_root_win.h" +#include "ax/platform/ax_platform_node_textrangeprovider_win.h" +#include "base/win/atl.h" +#include "base/win/scoped_bstr.h" +#include "base/win/scoped_safearray.h" +#include "base/win/scoped_variant.h" + +using Microsoft::WRL::ComPtr; + +namespace ui { + +// Helper macros for UIAutomation HRESULT expectations +#define EXPECT_UIA_ELEMENTNOTAVAILABLE(expr) \ + EXPECT_EQ(static_cast(UIA_E_ELEMENTNOTAVAILABLE), (expr)) +#define EXPECT_UIA_INVALIDOPERATION(expr) \ + EXPECT_EQ(static_cast(UIA_E_INVALIDOPERATION), (expr)) +#define EXPECT_UIA_ELEMENTNOTENABLED(expr) \ + EXPECT_EQ(static_cast(UIA_E_ELEMENTNOTENABLED), (expr)) +#define EXPECT_UIA_NOTSUPPORTED(expr) \ + EXPECT_EQ(static_cast(UIA_E_NOTSUPPORTED), (expr)) + +#define ASSERT_UIA_ELEMENTNOTAVAILABLE(expr) \ + ASSERT_EQ(static_cast(UIA_E_ELEMENTNOTAVAILABLE), (expr)) +#define ASSERT_UIA_INVALIDOPERATION(expr) \ + ASSERT_EQ(static_cast(UIA_E_INVALIDOPERATION), (expr)) +#define ASSERT_UIA_ELEMENTNOTENABLED(expr) \ + ASSERT_EQ(static_cast(UIA_E_ELEMENTNOTENABLED), (expr)) +#define ASSERT_UIA_NOTSUPPORTED(expr) \ + ASSERT_EQ(static_cast(UIA_E_NOTSUPPORTED), (expr)) + +#define EXPECT_UIA_GETPROPERTYVALUE_EQ(node, property_id, expected) \ + { \ + base::win::ScopedVariant expectedVariant(expected); \ + ASSERT_EQ(VT_BSTR, expectedVariant.type()); \ + ASSERT_NE(nullptr, expectedVariant.ptr()->bstrVal); \ + base::win::ScopedVariant actual; \ + ASSERT_HRESULT_SUCCEEDED( \ + node->GetPropertyValue(property_id, actual.Receive())); \ + ASSERT_EQ(VT_BSTR, actual.type()); \ + ASSERT_NE(nullptr, actual.ptr()->bstrVal); \ + EXPECT_STREQ(expectedVariant.ptr()->bstrVal, actual.ptr()->bstrVal); \ + } + +#define EXPECT_UIA_ELEMENT_ARRAY_BSTR_EQ(array, element_test_property_id, \ + expected_property_values) \ + { \ + ASSERT_EQ(1u, SafeArrayGetDim(array)); \ + LONG array_lower_bound; \ + ASSERT_HRESULT_SUCCEEDED( \ + SafeArrayGetLBound(array, 1, &array_lower_bound)); \ + LONG array_upper_bound; \ + ASSERT_HRESULT_SUCCEEDED( \ + SafeArrayGetUBound(array, 1, &array_upper_bound)); \ + IUnknown** array_data; \ + ASSERT_HRESULT_SUCCEEDED( \ + ::SafeArrayAccessData(array, reinterpret_cast(&array_data))); \ + size_t count = array_upper_bound - array_lower_bound + 1; \ + ASSERT_EQ(expected_property_values.size(), count); \ + for (size_t i = 0; i < count; ++i) { \ + ComPtr element; \ + ASSERT_HRESULT_SUCCEEDED( \ + array_data[i]->QueryInterface(IID_PPV_ARGS(&element))); \ + EXPECT_UIA_GETPROPERTYVALUE_EQ(element, element_test_property_id, \ + expected_property_values[i].c_str()); \ + } \ + ASSERT_HRESULT_SUCCEEDED(::SafeArrayUnaccessData(array)); \ + } + +#define EXPECT_UIA_SAFEARRAY_EQ(safearray, expected_property_values) \ + { \ + using T = typename decltype(expected_property_values)::value_type; \ + EXPECT_EQ(sizeof(T), ::SafeArrayGetElemsize(safearray)); \ + EXPECT_EQ(1u, SafeArrayGetDim(safearray)); \ + LONG array_lower_bound; \ + EXPECT_HRESULT_SUCCEEDED( \ + SafeArrayGetLBound(safearray, 1, &array_lower_bound)); \ + LONG array_upper_bound; \ + EXPECT_HRESULT_SUCCEEDED( \ + SafeArrayGetUBound(safearray, 1, &array_upper_bound)); \ + const size_t count = array_upper_bound - array_lower_bound + 1; \ + EXPECT_EQ(expected_property_values.size(), count); \ + if (sizeof(T) == ::SafeArrayGetElemsize(safearray) && \ + count == expected_property_values.size()) { \ + T* array_data; \ + EXPECT_HRESULT_SUCCEEDED(::SafeArrayAccessData( \ + safearray, reinterpret_cast(&array_data))); \ + for (size_t i = 0; i < count; ++i) { \ + EXPECT_EQ(array_data[i], expected_property_values[i]); \ + } \ + EXPECT_HRESULT_SUCCEEDED(::SafeArrayUnaccessData(safearray)); \ + } \ + } + +#define EXPECT_UIA_TEXTATTRIBUTE_EQ(provider, attribute, variant) \ + { \ + base::win::ScopedVariant scoped_variant; \ + EXPECT_HRESULT_SUCCEEDED( \ + provider->GetAttributeValue(attribute, scoped_variant.Receive())); \ + EXPECT_EQ(0, scoped_variant.Compare(variant)); \ + } + +#define EXPECT_UIA_TEXTATTRIBUTE_MIXED(provider, attribute) \ + { \ + ComPtr expected_mixed; \ + EXPECT_HRESULT_SUCCEEDED( \ + ::UiaGetReservedMixedAttributeValue(&expected_mixed)); \ + base::win::ScopedVariant scoped_variant; \ + EXPECT_HRESULT_SUCCEEDED( \ + provider->GetAttributeValue(attribute, scoped_variant.Receive())); \ + EXPECT_EQ(VT_UNKNOWN, scoped_variant.type()); \ + EXPECT_EQ(expected_mixed.Get(), V_UNKNOWN(scoped_variant.ptr())); \ + } + +#define EXPECT_UIA_TEXTATTRIBUTE_NOTSUPPORTED(provider, attribute) \ + { \ + ComPtr expected_notsupported; \ + EXPECT_HRESULT_SUCCEEDED( \ + ::UiaGetReservedNotSupportedValue(&expected_notsupported)); \ + base::win::ScopedVariant scoped_variant; \ + EXPECT_HRESULT_SUCCEEDED( \ + provider->GetAttributeValue(attribute, scoped_variant.Receive())); \ + EXPECT_EQ(VT_UNKNOWN, scoped_variant.type()); \ + EXPECT_EQ(expected_notsupported.Get(), V_UNKNOWN(scoped_variant.ptr())); \ + } + +#define EXPECT_UIA_TEXTRANGE_EQ(provider, expected_content) \ + { \ + base::win::ScopedBstr provider_content; \ + EXPECT_HRESULT_SUCCEEDED( \ + provider->GetText(-1, provider_content.Receive())); \ + EXPECT_STREQ(expected_content, provider_content.Get()); \ + } + +#define EXPECT_UIA_FIND_TEXT(text_range_provider, search_term, ignore_case, \ + owner) \ + { \ + base::win::ScopedBstr find_string(search_term); \ + ComPtr text_range_provider_found; \ + EXPECT_HRESULT_SUCCEEDED(text_range_provider->FindText( \ + find_string.Get(), false, ignore_case, &text_range_provider_found)); \ + if (text_range_provider_found == nullptr) { \ + EXPECT_TRUE(false); \ + } else { \ + SetOwner(owner, text_range_provider_found.Get()); \ + base::win::ScopedBstr found_content; \ + EXPECT_HRESULT_SUCCEEDED( \ + text_range_provider_found->GetText(-1, found_content.Receive())); \ + if (ignore_case) \ + EXPECT_EQ(0, _wcsicmp(found_content.Get(), find_string.Get())); \ + else \ + EXPECT_EQ(0, wcscmp(found_content.Get(), find_string.Get())); \ + } \ + } + +#define EXPECT_UIA_FIND_TEXT_NO_MATCH(text_range_provider, search_term, \ + ignore_case, owner) \ + { \ + base::win::ScopedBstr find_string(search_term); \ + ComPtr text_range_provider_found; \ + EXPECT_HRESULT_SUCCEEDED(text_range_provider->FindText( \ + find_string.Get(), false, ignore_case, &text_range_provider_found)); \ + EXPECT_EQ(nullptr, text_range_provider_found); \ + } + +#define EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT(text_range_provider, endpoint, unit, \ + count, expected_text, expected_count) \ + { \ + int result_count; \ + EXPECT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( \ + endpoint, unit, count, &result_count)); \ + EXPECT_EQ(expected_count, result_count); \ + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, expected_text); \ + } + +#define EXPECT_UIA_MOVE(text_range_provider, unit, count, expected_text, \ + expected_count) \ + { \ + int result_count; \ + EXPECT_HRESULT_SUCCEEDED( \ + text_range_provider->Move(unit, count, &result_count)); \ + EXPECT_EQ(expected_count, result_count); \ + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, expected_text); \ + } + +#define EXPECT_ENCLOSING_ELEMENT(ax_node_given, ax_node_expected) \ + { \ + ComPtr text_range_provider; \ + GetTextRangeProviderFromTextNode(text_range_provider, ax_node_given); \ + ComPtr enclosing_element; \ + ASSERT_HRESULT_SUCCEEDED( \ + text_range_provider->GetEnclosingElement(&enclosing_element)); \ + ComPtr expected_text_provider = \ + QueryInterfaceFromNode(ax_node_expected); \ + EXPECT_EQ(expected_text_provider.Get(), enclosing_element.Get()); \ + } + +#define DCHECK_EQ(a, b) BASE_DCHECK((a) == (b)) + +static AXNodePosition::AXPositionInstance CreateTextPosition( + const AXNode& anchor, + int text_offset, + ax::mojom::TextAffinity affinity) { + return AXNodePosition::CreateTextPosition(anchor.tree()->GetAXTreeID(), + anchor.id(), text_offset, affinity); +} + +class AXPlatformNodeTextRangeProviderTest : public ui::AXPlatformNodeWinTest { + public: + const AXNodePosition::AXPositionInstance& GetStart( + const AXPlatformNodeTextRangeProviderWin* text_range) { + return text_range->start(); + } + + const AXNodePosition::AXPositionInstance& GetEnd( + const AXPlatformNodeTextRangeProviderWin* text_range) { + return text_range->end(); + } + + ui::AXPlatformNodeWin* GetOwner( + const AXPlatformNodeTextRangeProviderWin* text_range) { + return text_range->GetOwner(); + } + + void CopyOwnerToClone(ITextRangeProvider* source_range, + ITextRangeProvider* destination_range) { + ComPtr source_provider = source_range; + ComPtr destination_provider = destination_range; + + ComPtr source_provider_internal; + ComPtr destination_provider_internal; + + source_provider->QueryInterface(IID_PPV_ARGS(&source_provider_internal)); + destination_provider->QueryInterface( + IID_PPV_ARGS(&destination_provider_internal)); + destination_provider_internal->SetOwnerForTesting( + source_provider_internal->GetOwner()); + } + + void SetOwner(AXPlatformNodeWin* owner, + ITextRangeProvider* destination_range) { + ComPtr destination_provider_internal; + auto as = + static_cast(destination_range); + destination_range->QueryInterface( + IID_PPV_ARGS(&destination_provider_internal)); + destination_provider_internal->SetOwnerForTesting(owner); + } + + void NormalizeTextRange(AXPlatformNodeTextRangeProviderWin* text_range, + AXNodePosition::AXPositionInstance& start, + AXNodePosition::AXPositionInstance& end) { + DCHECK_EQ(*GetStart(text_range), *start); + DCHECK_EQ(*GetEnd(text_range), *end); + text_range->NormalizeTextRange(start, end); + } + + void GetTextRangeProviderFromTextNode( + ComPtr& text_range_provider, + ui::AXNode* text_node) { + ComPtr provider_simple = + QueryInterfaceFromNode(text_node); + ASSERT_NE(nullptr, provider_simple.Get()); + + ComPtr text_provider; + EXPECT_HRESULT_SUCCEEDED( + provider_simple->GetPatternProvider(UIA_TextPatternId, &text_provider)); + ASSERT_NE(nullptr, text_provider.Get()); + + EXPECT_HRESULT_SUCCEEDED( + text_provider->get_DocumentRange(&text_range_provider)); + ASSERT_NE(nullptr, text_range_provider.Get()); + + ComPtr text_range_provider_interal; + EXPECT_HRESULT_SUCCEEDED(text_range_provider->QueryInterface( + IID_PPV_ARGS(&text_range_provider_interal))); + AXPlatformNode* ax_platform_node = AXPlatformNodeFromNode(text_node); + ASSERT_NE(ax_platform_node, nullptr); + text_range_provider_interal->SetOwnerForTesting( + static_cast(ax_platform_node)); + } + + void CreateTextRangeProviderWin( + ComPtr& text_range_provider_win, + AXPlatformNodeWin* owner, + const AXNode* start_anchor, + int start_offset, + ax::mojom::TextAffinity start_affinity, + const AXNode* end_anchor, + int end_offset, + ax::mojom::TextAffinity end_affinity) { + AXNodePosition::AXPositionInstance range_start = + CreateTextPosition(*start_anchor, start_offset, start_affinity); + AXNodePosition::AXPositionInstance range_end = + CreateTextPosition(*end_anchor, end_offset, end_affinity); + + ComPtr text_range_provider = + AXPlatformNodeTextRangeProviderWin::CreateTextRangeProviderForTesting( + owner, std::move(range_start), std::move(range_end)); + + text_range_provider->QueryInterface(IID_PPV_ARGS(&text_range_provider_win)); + } + + void ComputeWordBoundariesOffsets(const std::string& text, + std::vector& word_start_offsets, + std::vector& word_end_offsets) { + char previous_char = ' '; + word_start_offsets = std::vector(); + for (size_t i = 0; i < text.size(); ++i) { + if (previous_char == ' ' && text[i] != ' ') + word_start_offsets.push_back(i); + previous_char = text[i]; + } + + previous_char = ' '; + word_end_offsets = std::vector(); + for (size_t i = text.size(); i > 0; --i) { + if (previous_char == ' ' && text[i - 1] != ' ') + word_end_offsets.push_back(i); + previous_char = text[i - 1]; + } + std::reverse(word_end_offsets.begin(), word_end_offsets.end()); + } + + AXTreeUpdate BuildTextDocument( + const std::vector& text_nodes_content, + bool build_word_boundaries_offsets = false, + bool place_text_on_one_line = false) { + int current_id = 0; + AXNodeData root_data; + root_data.id = ++current_id; + root_data.role = ax::mojom::Role::kRootWebArea; + + AXTreeUpdate update; + update.tree_data.tree_id = ui::AXTreeID::CreateNewAXTreeID(); + update.has_tree_data = true; + + for (const std::string& text_content : text_nodes_content) { + AXNodeData static_text_data; + static_text_data.id = ++current_id; + static_text_data.role = ax::mojom::Role::kStaticText; + static_text_data.SetName(text_content); + root_data.child_ids.push_back(static_text_data.id); + + AXNodeData inline_box_data; + inline_box_data.id = ++current_id; + inline_box_data.role = ax::mojom::Role::kInlineTextBox; + inline_box_data.SetName(text_content); + static_text_data.child_ids = {inline_box_data.id}; + + if (build_word_boundaries_offsets) { + std::vector word_end_offsets; + std::vector word_start_offsets; + ComputeWordBoundariesOffsets(text_content, word_start_offsets, + word_end_offsets); + inline_box_data.AddIntListAttribute( + ax::mojom::IntListAttribute::kWordStarts, word_start_offsets); + inline_box_data.AddIntListAttribute( + ax::mojom::IntListAttribute::kWordEnds, word_end_offsets); + } + + if (place_text_on_one_line && !update.nodes.empty()) { + AXNodeData* previous_inline_box_data = &update.nodes.back(); + static_text_data.AddIntAttribute( + ax::mojom::IntAttribute::kPreviousOnLineId, + previous_inline_box_data->id); + inline_box_data.AddIntAttribute( + ax::mojom::IntAttribute::kPreviousOnLineId, + previous_inline_box_data->id); + previous_inline_box_data->AddIntAttribute( + ax::mojom::IntAttribute::kNextOnLineId, inline_box_data.id); + } + + update.nodes.push_back(static_text_data); + update.nodes.push_back(inline_box_data); + } + + update.nodes.insert(update.nodes.begin(), root_data); + update.root_id = root_data.id; + return update; + } + + ui::AXTreeUpdate BuildAXTreeForBoundingRectangles() { + // AXTree content: + // Line 1
Line 2 + ui::AXNodeData root; + ui::AXNodeData button; + ui::AXNodeData check_box; + ui::AXNodeData text_field; + ui::AXNodeData static_text1; + ui::AXNodeData line_break; + ui::AXNodeData static_text2; + ui::AXNodeData inline_box1; + ui::AXNodeData inline_box2; + ui::AXNodeData inline_box_line_break; + + const int ROOT_ID = 1; + const int BUTTON_ID = 2; + const int CHECK_BOX_ID = 3; + const int TEXT_FIELD_ID = 4; + const int STATIC_TEXT1_ID = 5; + const int INLINE_BOX1_ID = 6; + const int LINE_BREAK_ID = 7; + const int INLINE_BOX_LINE_BREAK_ID = 8; + const int STATIC_TEXT2_ID = 9; + const int INLINE_BOX2_ID = 10; + + 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; + inline_box_line_break.id = INLINE_BOX_LINE_BREAK_ID; + static_text2.id = STATIC_TEXT2_ID; + inline_box2.id = INLINE_BOX2_ID; + + std::string LINE_1_TEXT = "Line 1"; + std::string LINE_2_TEXT = "Line 2"; + std::string LINE_BREAK_TEXT = "\n"; + std::string ALL_TEXT = LINE_1_TEXT + LINE_BREAK_TEXT + LINE_2_TEXT; + std::string BUTTON_TEXT = "Button"; + std::string CHECKBOX_TEXT = "Check box"; + + root.role = ax::mojom::Role::kRootWebArea; + + button.role = ax::mojom::Role::kButton; + button.SetHasPopup(ax::mojom::HasPopup::kMenu); + button.SetName(BUTTON_TEXT); + button.SetValue(BUTTON_TEXT); + button.relative_bounds.bounds = gfx::RectF(20, 20, 200, 30); + button.AddIntAttribute(ax::mojom::IntAttribute::kNextOnLineId, + check_box.id); + root.child_ids.push_back(button.id); + + check_box.role = ax::mojom::Role::kCheckBox; + check_box.SetCheckedState(ax::mojom::CheckedState::kTrue); + check_box.SetName(CHECKBOX_TEXT); + check_box.relative_bounds.bounds = gfx::RectF(20, 50, 200, 30); + check_box.AddIntAttribute(ax::mojom::IntAttribute::kPreviousOnLineId, + button.id); + root.child_ids.push_back(check_box.id); + + text_field.role = ax::mojom::Role::kTextField; + text_field.AddState(ax::mojom::State::kEditable); + text_field.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, + "input"); + text_field.AddStringAttribute(ax::mojom::StringAttribute::kInputType, + "text"); + text_field.SetValue(ALL_TEXT); + text_field.AddIntListAttribute( + ax::mojom::IntListAttribute::kCachedLineStarts, + std::vector{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_TEXT); + static_text1.child_ids.push_back(inline_box1.id); + + inline_box1.role = ax::mojom::Role::kInlineTextBox; + inline_box1.AddState(ax::mojom::State::kEditable); + inline_box1.SetName(LINE_1_TEXT); + inline_box1.relative_bounds.bounds = gfx::RectF(220, 20, 100, 30); + std::vector character_offsets1; + // The width of each character is 5px. + character_offsets1.push_back(225); // "L" {220, 20, 5x30} + character_offsets1.push_back(230); // "i" {225, 20, 5x30} + character_offsets1.push_back(235); // "n" {230, 20, 5x30} + character_offsets1.push_back(240); // "e" {235, 20, 5x30} + character_offsets1.push_back(245); // " " {240, 20, 5x30} + character_offsets1.push_back(250); // "1" {245, 20, 5x30} + inline_box1.AddIntListAttribute( + ax::mojom::IntListAttribute::kCharacterOffsets, character_offsets1); + inline_box1.AddIntListAttribute(ax::mojom::IntListAttribute::kWordStarts, + std::vector{0, 5}); + inline_box1.AddIntListAttribute(ax::mojom::IntListAttribute::kWordEnds, + std::vector{4, 6}); + inline_box1.AddIntAttribute(ax::mojom::IntAttribute::kNextOnLineId, + line_break.id); + + line_break.role = ax::mojom::Role::kLineBreak; + line_break.AddState(ax::mojom::State::kEditable); + line_break.SetName(LINE_BREAK_TEXT); + line_break.relative_bounds.bounds = gfx::RectF(250, 20, 0, 30); + line_break.AddIntAttribute(ax::mojom::IntAttribute::kPreviousOnLineId, + inline_box1.id); + line_break.child_ids.push_back(inline_box_line_break.id); + + inline_box_line_break.role = ax::mojom::Role::kInlineTextBox; + inline_box_line_break.AddBoolAttribute( + ax::mojom::BoolAttribute::kIsLineBreakingObject, true); + inline_box_line_break.SetName(LINE_BREAK_TEXT); + inline_box_line_break.relative_bounds.bounds = gfx::RectF(250, 20, 0, 30); + inline_box_line_break.AddIntListAttribute( + ax::mojom::IntListAttribute::kCharacterOffsets, {0}); + inline_box_line_break.AddIntListAttribute( + ax::mojom::IntListAttribute::kWordStarts, std::vector{0}); + inline_box_line_break.AddIntListAttribute( + ax::mojom::IntListAttribute::kWordEnds, std::vector{0}); + + static_text2.role = ax::mojom::Role::kStaticText; + static_text2.AddState(ax::mojom::State::kEditable); + static_text2.SetName(LINE_2_TEXT); + static_text2.child_ids.push_back(inline_box2.id); + + inline_box2.role = ax::mojom::Role::kInlineTextBox; + inline_box2.AddState(ax::mojom::State::kEditable); + inline_box2.SetName(LINE_2_TEXT); + inline_box2.relative_bounds.bounds = gfx::RectF(220, 50, 100, 30); + std::vector character_offsets2; + // The width of each character is 7 px. + character_offsets2.push_back(227); // "L" {220, 50, 7x30} + character_offsets2.push_back(234); // "i" {227, 50, 7x30} + character_offsets2.push_back(241); // "n" {234, 50, 7x30} + character_offsets2.push_back(248); // "e" {241, 50, 7x30} + character_offsets2.push_back(255); // " " {248, 50, 7x30} + character_offsets2.push_back(262); // "2" {255, 50, 7x30} + inline_box2.AddIntListAttribute( + ax::mojom::IntListAttribute::kCharacterOffsets, character_offsets2); + inline_box2.AddIntListAttribute(ax::mojom::IntListAttribute::kWordStarts, + std::vector{0, 5}); + inline_box2.AddIntListAttribute(ax::mojom::IntListAttribute::kWordEnds, + std::vector{4, 6}); + + AXTreeUpdate update; + update.has_tree_data = true; + update.root_id = ROOT_ID; + update.nodes = { + root, button, check_box, text_field, + static_text1, inline_box1, line_break, inline_box_line_break, + static_text2, inline_box2}; + update.tree_data.tree_id = ui::AXTreeID::CreateNewAXTreeID(); + return update; + } + + const std::wstring tree_for_move_full_text = + L"First line of text\nStandalone line\n" + L"bold text\nParagraph 1\nParagraph 2"; + + ui::AXTreeUpdate BuildAXTreeForMove() { + ui::AXNodeData group1_data; + group1_data.id = 2; + group1_data.role = ax::mojom::Role::kGenericContainer; + group1_data.AddBoolAttribute( + ax::mojom::BoolAttribute::kIsLineBreakingObject, true); + + ui::AXNodeData text_data; + text_data.id = 3; + text_data.role = ax::mojom::Role::kStaticText; + std::string text_content = "First line of text"; + text_data.SetName(text_content); + std::vector word_end_offsets; + std::vector word_start_offsets; + ComputeWordBoundariesOffsets(text_content, word_start_offsets, + word_end_offsets); + text_data.AddIntListAttribute(ax::mojom::IntListAttribute::kWordStarts, + word_start_offsets); + text_data.AddIntListAttribute(ax::mojom::IntListAttribute::kWordEnds, + word_end_offsets); + group1_data.child_ids = {text_data.id}; + + ui::AXNodeData group2_data; + group2_data.id = 4; + group2_data.role = ax::mojom::Role::kGenericContainer; + + ui::AXNodeData line_break1_data; + line_break1_data.id = 5; + line_break1_data.role = ax::mojom::Role::kLineBreak; + line_break1_data.AddBoolAttribute( + ax::mojom::BoolAttribute::kIsLineBreakingObject, true); + line_break1_data.SetName("\n"); + + ui::AXNodeData standalone_text_data; + standalone_text_data.id = 6; + standalone_text_data.role = ax::mojom::Role::kStaticText; + text_content = "Standalone line"; + standalone_text_data.SetName(text_content); + ComputeWordBoundariesOffsets(text_content, word_start_offsets, + word_end_offsets); + standalone_text_data.AddIntListAttribute( + ax::mojom::IntListAttribute::kWordStarts, word_start_offsets); + standalone_text_data.AddIntListAttribute( + ax::mojom::IntListAttribute::kWordEnds, word_end_offsets); + + ui::AXNodeData line_break2_data; + line_break2_data.id = 7; + line_break2_data.role = ax::mojom::Role::kLineBreak; + line_break2_data.AddBoolAttribute( + ax::mojom::BoolAttribute::kIsLineBreakingObject, true); + line_break2_data.SetName("\n"); + + group2_data.child_ids = {line_break1_data.id, standalone_text_data.id, + line_break2_data.id}; + standalone_text_data.AddIntAttribute(ax::mojom::IntAttribute::kNextOnLineId, + line_break2_data.id); + line_break2_data.AddIntAttribute(ax::mojom::IntAttribute::kPreviousOnLineId, + standalone_text_data.id); + + ui::AXNodeData bold_text_data; + bold_text_data.id = 8; + bold_text_data.role = ax::mojom::Role::kStaticText; + bold_text_data.AddIntAttribute( + ax::mojom::IntAttribute::kTextStyle, + static_cast(ax::mojom::TextStyle::kBold)); + text_content = "bold text"; + bold_text_data.SetName(text_content); + ComputeWordBoundariesOffsets(text_content, word_start_offsets, + word_end_offsets); + bold_text_data.AddIntListAttribute(ax::mojom::IntListAttribute::kWordStarts, + word_start_offsets); + bold_text_data.AddIntListAttribute(ax::mojom::IntListAttribute::kWordEnds, + word_end_offsets); + + ui::AXNodeData paragraph1_data; + paragraph1_data.id = 9; + paragraph1_data.role = ax::mojom::Role::kParagraph; + paragraph1_data.AddBoolAttribute( + ax::mojom::BoolAttribute::kIsLineBreakingObject, true); + + ui::AXNodeData paragraph1_text_data; + paragraph1_text_data.id = 10; + paragraph1_text_data.role = ax::mojom::Role::kStaticText; + text_content = "Paragraph 1"; + paragraph1_text_data.SetName(text_content); + ComputeWordBoundariesOffsets(text_content, word_start_offsets, + word_end_offsets); + paragraph1_text_data.AddIntListAttribute( + ax::mojom::IntListAttribute::kWordStarts, word_start_offsets); + paragraph1_text_data.AddIntListAttribute( + ax::mojom::IntListAttribute::kWordEnds, word_end_offsets); + paragraph1_data.AddBoolAttribute( + ax::mojom::BoolAttribute::kIsLineBreakingObject, true); + + ui::AXNodeData ignored_text_data; + ignored_text_data.id = 11; + ignored_text_data.role = ax::mojom::Role::kStaticText; + ignored_text_data.AddState(ax::mojom::State::kIgnored); + text_content = "ignored text"; + ignored_text_data.SetName(text_content); + + paragraph1_data.child_ids = {paragraph1_text_data.id, ignored_text_data.id}; + + ui::AXNodeData paragraph2_data; + paragraph2_data.id = 12; + paragraph2_data.role = ax::mojom::Role::kParagraph; + paragraph2_data.AddBoolAttribute( + ax::mojom::BoolAttribute::kIsLineBreakingObject, true); + + ui::AXNodeData paragraph2_text_data; + paragraph2_text_data.id = 13; + paragraph2_text_data.role = ax::mojom::Role::kStaticText; + text_content = "Paragraph 2"; + paragraph2_text_data.SetName(text_content); + ComputeWordBoundariesOffsets(text_content, word_start_offsets, + word_end_offsets); + paragraph2_text_data.AddIntListAttribute( + ax::mojom::IntListAttribute::kWordStarts, word_start_offsets); + paragraph2_text_data.AddIntListAttribute( + ax::mojom::IntListAttribute::kWordEnds, word_end_offsets); + paragraph1_data.AddBoolAttribute( + ax::mojom::BoolAttribute::kIsLineBreakingObject, true); + paragraph2_data.child_ids = {paragraph2_text_data.id}; + + ui::AXNodeData root_data; + root_data.id = 1; + root_data.role = ax::mojom::Role::kRootWebArea; + root_data.child_ids = {group1_data.id, group2_data.id, bold_text_data.id, + paragraph1_data.id, paragraph2_data.id}; + + ui::AXTreeUpdate update; + update.has_tree_data = true; + update.root_id = root_data.id; + update.nodes = {root_data, group1_data, + text_data, group2_data, + line_break1_data, standalone_text_data, + line_break2_data, bold_text_data, + paragraph1_data, paragraph1_text_data, + ignored_text_data, paragraph2_data, + paragraph2_text_data}; + update.tree_data.tree_id = ui::AXTreeID::CreateNewAXTreeID(); + return update; + } + + AXTreeUpdate BuildAXTreeForMoveByFormat() { + // 1 + // | + // ------------------------------------- + // | | | | | | | + // 2 4 8 10 12 14 16 + // | | | | | | | + // | --------- | | | | | + // | | | | | | | | | + // 3 5 6 7 9 11 13 15 17 + + AXNodeData group1_data; + group1_data.id = 2; + group1_data.role = ax::mojom::Role::kGenericContainer; + group1_data.AddStringAttribute(ax::mojom::StringAttribute::kFontFamily, + "test font"); + group1_data.AddBoolAttribute( + ax::mojom::BoolAttribute::kIsLineBreakingObject, true); + + AXNodeData text_data; + text_data.id = 3; + text_data.role = ax::mojom::Role::kStaticText; + text_data.SetName("Text with formatting"); + group1_data.child_ids = {text_data.id}; + + AXNodeData group2_data; + group2_data.id = 4; + group2_data.role = ax::mojom::Role::kGenericContainer; + group2_data.AddBoolAttribute( + ax::mojom::BoolAttribute::kIsLineBreakingObject, true); + + AXNodeData line_break1_data; + line_break1_data.id = 5; + line_break1_data.role = ax::mojom::Role::kLineBreak; + line_break1_data.SetName("\n"); + + AXNodeData standalone_text_data; + standalone_text_data.id = 6; + standalone_text_data.role = ax::mojom::Role::kStaticText; + standalone_text_data.SetName("Standalone line with no formatting"); + + AXNodeData line_break2_data; + line_break2_data.id = 7; + line_break2_data.role = ax::mojom::Role::kLineBreak; + line_break2_data.SetName("\n"); + + group2_data.child_ids = {line_break1_data.id, standalone_text_data.id, + line_break2_data.id}; + + AXNodeData group3_data; + group3_data.id = 8; + group3_data.role = ax::mojom::Role::kGenericContainer; + group3_data.AddIntAttribute( + ax::mojom::IntAttribute::kTextStyle, + static_cast(ax::mojom::TextStyle::kBold)); + group3_data.AddBoolAttribute( + ax::mojom::BoolAttribute::kIsLineBreakingObject, true); + + AXNodeData bold_text_data; + bold_text_data.id = 9; + bold_text_data.role = ax::mojom::Role::kStaticText; + bold_text_data.SetName("bold text"); + group3_data.child_ids = {bold_text_data.id}; + + AXNodeData paragraph1_data; + paragraph1_data.id = 10; + paragraph1_data.role = ax::mojom::Role::kParagraph; + paragraph1_data.AddIntAttribute(ax::mojom::IntAttribute::kColor, 100); + paragraph1_data.AddBoolAttribute( + ax::mojom::BoolAttribute::kIsLineBreakingObject, true); + + AXNodeData paragraph1_text_data; + paragraph1_text_data.id = 11; + paragraph1_text_data.role = ax::mojom::Role::kStaticText; + paragraph1_text_data.SetName("Paragraph 1"); + paragraph1_data.child_ids = {paragraph1_text_data.id}; + + AXNodeData paragraph2_data; + paragraph2_data.id = 12; + paragraph2_data.role = ax::mojom::Role::kParagraph; + paragraph2_data.AddFloatAttribute(ax::mojom::FloatAttribute::kFontSize, + 1.0f); + paragraph2_data.AddBoolAttribute( + ax::mojom::BoolAttribute::kIsLineBreakingObject, true); + + AXNodeData paragraph2_text_data; + paragraph2_text_data.id = 13; + paragraph2_text_data.role = ax::mojom::Role::kStaticText; + paragraph2_text_data.SetName("Paragraph 2"); + paragraph2_data.child_ids = {paragraph2_text_data.id}; + + AXNodeData paragraph3_data; + paragraph3_data.id = 14; + paragraph3_data.role = ax::mojom::Role::kParagraph; + paragraph3_data.AddFloatAttribute(ax::mojom::FloatAttribute::kFontSize, + 1.0f); + paragraph3_data.AddBoolAttribute( + ax::mojom::BoolAttribute::kIsLineBreakingObject, true); + + AXNodeData paragraph3_text_data; + paragraph3_text_data.id = 15; + paragraph3_text_data.role = ax::mojom::Role::kStaticText; + paragraph3_text_data.SetName("Paragraph 3"); + paragraph3_data.child_ids = {paragraph3_text_data.id}; + + AXNodeData paragraph4_data; + paragraph4_data.id = 16; + paragraph4_data.role = ax::mojom::Role::kParagraph; + paragraph4_data.AddFloatAttribute(ax::mojom::FloatAttribute::kFontSize, + 2.0f); + paragraph4_data.AddBoolAttribute( + ax::mojom::BoolAttribute::kIsLineBreakingObject, true); + + AXNodeData paragraph4_text_data; + paragraph4_text_data.id = 17; + paragraph4_text_data.role = ax::mojom::Role::kStaticText; + paragraph4_text_data.SetName("Paragraph 4"); + paragraph4_data.child_ids = {paragraph4_text_data.id}; + + AXNodeData root_data; + root_data.id = 1; + root_data.role = ax::mojom::Role::kRootWebArea; + root_data.child_ids = {group1_data.id, group2_data.id, + group3_data.id, paragraph1_data.id, + paragraph2_data.id, paragraph3_data.id, + paragraph4_data.id}; + + AXTreeUpdate update; + update.has_tree_data = true; + update.root_id = root_data.id; + update.nodes = {root_data, + group1_data, + text_data, + group2_data, + line_break1_data, + standalone_text_data, + line_break2_data, + group3_data, + bold_text_data, + paragraph1_data, + paragraph1_text_data, + paragraph2_data, + paragraph2_text_data, + paragraph3_data, + paragraph3_text_data, + paragraph4_data, + paragraph4_text_data}; + update.tree_data.tree_id = ui::AXTreeID::CreateNewAXTreeID(); + return update; + } + + void ExpectPositionsEqual(const AXNodePosition::AXPositionInstance& a, + const AXNodePosition::AXPositionInstance& b) { + EXPECT_EQ(*a, *b); + EXPECT_EQ(a->anchor_id(), b->anchor_id()); + EXPECT_EQ(a->text_offset(), b->text_offset()); + } +}; + +class MockAXPlatformNodeTextRangeProviderWin + : public CComObjectRootEx, + public ITextRangeProvider { + public: + BEGIN_COM_MAP(MockAXPlatformNodeTextRangeProviderWin) + COM_INTERFACE_ENTRY(ITextRangeProvider) + END_COM_MAP() + + MockAXPlatformNodeTextRangeProviderWin() {} + ~MockAXPlatformNodeTextRangeProviderWin() {} + + static HRESULT CreateMockTextRangeProvider(ITextRangeProvider** provider) { + CComObject* text_range_provider = + nullptr; + HRESULT hr = + CComObject::CreateInstance( + &text_range_provider); + if (SUCCEEDED(hr)) { + *provider = text_range_provider; + } + + return hr; + } + + // + // ITextRangeProvider methods. + // + IFACEMETHODIMP Clone(ITextRangeProvider** clone) override { + return E_NOTIMPL; + } + + IFACEMETHODIMP Compare(ITextRangeProvider* other, BOOL* result) override { + return E_NOTIMPL; + } + + IFACEMETHODIMP CompareEndpoints(TextPatternRangeEndpoint this_endpoint, + ITextRangeProvider* other, + TextPatternRangeEndpoint other_endpoint, + int* result) override { + return E_NOTIMPL; + } + + IFACEMETHODIMP ExpandToEnclosingUnit(TextUnit unit) override { + return E_NOTIMPL; + } + + IFACEMETHODIMP FindAttribute(TEXTATTRIBUTEID attribute_id, + VARIANT val, + BOOL backward, + ITextRangeProvider** result) override { + return E_NOTIMPL; + } + + IFACEMETHODIMP FindText(BSTR string, + BOOL backwards, + BOOL ignore_case, + ITextRangeProvider** result) override { + return E_NOTIMPL; + } + + IFACEMETHODIMP GetAttributeValue(TEXTATTRIBUTEID attribute_id, + VARIANT* value) override { + return E_NOTIMPL; + } + + IFACEMETHODIMP GetBoundingRectangles(SAFEARRAY** rectangles) override { + return E_NOTIMPL; + } + + IFACEMETHODIMP GetEnclosingElement( + IRawElementProviderSimple** element) override { + return E_NOTIMPL; + } + + IFACEMETHODIMP GetText(int max_count, BSTR* text) override { + return E_NOTIMPL; + } + + IFACEMETHODIMP Move(TextUnit unit, int count, int* units_moved) override { + return E_NOTIMPL; + } + + IFACEMETHODIMP MoveEndpointByUnit(TextPatternRangeEndpoint endpoint, + TextUnit unit, + int count, + int* units_moved) override { + return E_NOTIMPL; + } + + IFACEMETHODIMP MoveEndpointByRange( + TextPatternRangeEndpoint this_endpoint, + ITextRangeProvider* other, + TextPatternRangeEndpoint other_endpoint) override { + return E_NOTIMPL; + } + + IFACEMETHODIMP Select() override { return E_NOTIMPL; } + + IFACEMETHODIMP AddToSelection() override { return E_NOTIMPL; } + + IFACEMETHODIMP RemoveFromSelection() override { return E_NOTIMPL; } + + IFACEMETHODIMP ScrollIntoView(BOOL align_to_top) override { + return E_NOTIMPL; + } + + IFACEMETHODIMP GetChildren(SAFEARRAY** children) override { + return E_NOTIMPL; + } +}; + +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderClone) { + Init(BuildTextDocument({"some text"})); + + ComPtr text_range_provider; + GetTextRangeProviderFromTextNode(text_range_provider, + GetRootAsAXNode()->children()[0]); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"some text"); + + ComPtr text_range_provider_clone; + text_range_provider->Clone(&text_range_provider_clone); + CopyOwnerToClone(text_range_provider.Get(), text_range_provider_clone.Get()); + ComPtr original_range; + ComPtr clone_range; + + text_range_provider->QueryInterface(IID_PPV_ARGS(&original_range)); + text_range_provider_clone->QueryInterface(IID_PPV_ARGS(&clone_range)); + + EXPECT_EQ(*GetStart(original_range.Get()), *GetStart(clone_range.Get())); + EXPECT_EQ(*GetEnd(original_range.Get()), *GetEnd(clone_range.Get())); + EXPECT_EQ(GetOwner(original_range.Get()), GetOwner(clone_range.Get())); + + // Clear original text range provider. + text_range_provider.Reset(); + EXPECT_EQ(nullptr, text_range_provider.Get()); + + // Ensure the clone still works correctly. + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider_clone, L"some text"); +} + +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderCompareEndpoints) { + Init(BuildTextDocument({"some text", "more text"}, + false /* build_word_boundaries_offsets */, + true /* place_text_on_one_line */)); + + AXNode* root_node = GetRootAsAXNode(); + + // Get the textRangeProvider for the document, + // which contains text "some textmore text". + ComPtr document_text_range_provider; + GetTextRangeProviderFromTextNode(document_text_range_provider, root_node); + + // Get the textRangeProvider for "some text". + ComPtr text_range_provider; + GetTextRangeProviderFromTextNode(text_range_provider, + root_node->children()[0]); + + // Get the textRangeProvider for "more text". + ComPtr more_text_range_provider; + GetTextRangeProviderFromTextNode(more_text_range_provider, + root_node->children()[1]); + + // Compare the endpoints of the document which contains "some textmore text". + int result; + EXPECT_HRESULT_SUCCEEDED(document_text_range_provider->CompareEndpoints( + TextPatternRangeEndpoint_Start, document_text_range_provider.Get(), + TextPatternRangeEndpoint_Start, &result)); + EXPECT_EQ(0, result); + + EXPECT_HRESULT_SUCCEEDED(document_text_range_provider->CompareEndpoints( + TextPatternRangeEndpoint_End, document_text_range_provider.Get(), + TextPatternRangeEndpoint_End, &result)); + EXPECT_EQ(0, result); + + EXPECT_HRESULT_SUCCEEDED(document_text_range_provider->CompareEndpoints( + TextPatternRangeEndpoint_Start, document_text_range_provider.Get(), + TextPatternRangeEndpoint_End, &result)); + EXPECT_EQ(-1, result); + + EXPECT_HRESULT_SUCCEEDED(document_text_range_provider->CompareEndpoints( + TextPatternRangeEndpoint_End, document_text_range_provider.Get(), + TextPatternRangeEndpoint_Start, &result)); + EXPECT_EQ(1, result); + + // Compare the endpoints of "some text" and "more text". The position at the + // end of "some text" is logically equivalent to the position at the start of + // "more text". + EXPECT_HRESULT_SUCCEEDED(text_range_provider->CompareEndpoints( + TextPatternRangeEndpoint_Start, more_text_range_provider.Get(), + TextPatternRangeEndpoint_Start, &result)); + EXPECT_EQ(-1, result); + + EXPECT_HRESULT_SUCCEEDED(text_range_provider->CompareEndpoints( + TextPatternRangeEndpoint_End, more_text_range_provider.Get(), + TextPatternRangeEndpoint_Start, &result)); + EXPECT_EQ(0, result); + + // Compare the endpoints of "some text" with those of the entire document. The + // position at the start of "some text" is logically equivalent to the + // position at the start of the document. + EXPECT_HRESULT_SUCCEEDED(text_range_provider->CompareEndpoints( + TextPatternRangeEndpoint_Start, document_text_range_provider.Get(), + TextPatternRangeEndpoint_Start, &result)); + EXPECT_EQ(0, result); + + EXPECT_HRESULT_SUCCEEDED(text_range_provider->CompareEndpoints( + TextPatternRangeEndpoint_End, document_text_range_provider.Get(), + TextPatternRangeEndpoint_End, &result)); + EXPECT_EQ(-1, result); + + // Compare the endpoints of "more text" with those of the entire document. + EXPECT_HRESULT_SUCCEEDED(more_text_range_provider->CompareEndpoints( + TextPatternRangeEndpoint_Start, document_text_range_provider.Get(), + TextPatternRangeEndpoint_Start, &result)); + EXPECT_EQ(1, result); + + EXPECT_HRESULT_SUCCEEDED(more_text_range_provider->CompareEndpoints( + TextPatternRangeEndpoint_End, document_text_range_provider.Get(), + TextPatternRangeEndpoint_End, &result)); + EXPECT_EQ(0, result); +} + +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderExpandToEnclosingCharacter) { + ui::AXTreeUpdate update = BuildTextDocument({"some text", "more text"}); + Init(update); + AXNode* root_node = GetRootAsAXNode(); + + ComPtr text_range_provider; + GetTextRangeProviderFromTextNode(text_range_provider, root_node); + ASSERT_HRESULT_SUCCEEDED( + text_range_provider->ExpandToEnclosingUnit(TextUnit_Character)); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"s"); + + int count; + ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_End, TextUnit_Character, /*count*/ 2, &count)); + ASSERT_EQ(2, count); + ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_Start, TextUnit_Character, /*count*/ 1, &count)); + ASSERT_EQ(1, count); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"om"); + + ASSERT_HRESULT_SUCCEEDED( + text_range_provider->ExpandToEnclosingUnit(TextUnit_Character)); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"o"); + + ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_End, TextUnit_Character, /*count*/ 9, &count)); + ASSERT_EQ(9, count); + ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_Start, TextUnit_Character, /*count*/ 8, &count)); + ASSERT_EQ(8, count); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"mo"); + + ASSERT_HRESULT_SUCCEEDED( + text_range_provider->ExpandToEnclosingUnit(TextUnit_Character)); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"m"); + + // Move the start and end to the end of the document. + // Expand to enclosing unit should never return a null position. + ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_End, TextUnit_Character, /*count*/ 9, &count)); + ASSERT_EQ(8, count); + ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_Start, TextUnit_Character, /*count*/ 9, &count)); + ASSERT_EQ(9, count); + + ASSERT_HRESULT_SUCCEEDED( + text_range_provider->ExpandToEnclosingUnit(TextUnit_Character)); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"t"); + + // Move both endpoints to the position before the start of the "more text" + // anchor. Then, force the start to be on the position after the end of + // "some text" by moving one character backward and one forward. + ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_End, TextUnit_Character, /*count*/ -9, &count)); + ASSERT_EQ(-9, count); + ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_Start, TextUnit_Character, /*count*/ -1, + &count)); + ASSERT_EQ(-1, count); + ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_Start, TextUnit_Character, /*count*/ 1, &count)); + ASSERT_EQ(1, count); + ASSERT_HRESULT_SUCCEEDED( + text_range_provider->ExpandToEnclosingUnit(TextUnit_Character)); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"m"); + + // Check that the enclosing element of the range matches ATs expectations. + ComPtr more_text_provider = + QueryInterfaceFromNode( + root_node->children()[1]->children()[0]); + ComPtr enclosing_element; + ASSERT_HRESULT_SUCCEEDED( + text_range_provider->GetEnclosingElement(&enclosing_element)); + EXPECT_EQ(more_text_provider.Get(), enclosing_element.Get()); +} + +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderExpandToEnclosingWord) { + Init(BuildTextDocument({"some text", "definitely not text"}, + /*build_word_boundaries_offsets*/ true)); + + ComPtr text_range_provider; + GetTextRangeProviderFromTextNode(text_range_provider, + GetRootAsAXNode()->children()[1]); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"definitely not text"); + + // Start endpoint is already on a word's start boundary. + ASSERT_HRESULT_SUCCEEDED( + text_range_provider->ExpandToEnclosingUnit(TextUnit_Word)); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"definitely "); + + // Start endpoint is between a word's start and end boundaries. + int count; + ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_Start, TextUnit_Character, /*count*/ -2, + &count)); + ASSERT_EQ(-2, count); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"xtdefinitely "); + + ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_End, TextUnit_Character, /*count*/ 4, &count)); + ASSERT_EQ(4, count); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"xtdefinitely not "); + + ASSERT_HRESULT_SUCCEEDED( + text_range_provider->ExpandToEnclosingUnit(TextUnit_Word)); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"text"); + + // Start endpoint is on a word's end boundary. + ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_Start, TextUnit_Character, /*count*/ 18, + &count)); + ASSERT_EQ(18, count); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L""); + + ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_End, TextUnit_Character, /*count*/ 1, &count)); + ASSERT_EQ(1, count); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L" "); + + ASSERT_HRESULT_SUCCEEDED( + text_range_provider->ExpandToEnclosingUnit(TextUnit_Word)); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"not "); +} + +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderExpandToEnclosingLine) { + Init(BuildTextDocument({"line #1", "maybe line #1?", "not line #1"})); + + ComPtr text_range_provider; + GetTextRangeProviderFromTextNode(text_range_provider, + GetRootAsAXNode()->children()[0]); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"line #1"); + + // Start endpoint is already on a line's start boundary. + int count; + ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_End, TextUnit_Character, /*count*/ -11, &count)); + ASSERT_EQ(-7, count); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L""); + + ASSERT_HRESULT_SUCCEEDED( + text_range_provider->ExpandToEnclosingUnit(TextUnit_Line)); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"line #1"); + + // Start endpoint is between a line's start and end boundaries. + ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_Start, TextUnit_Character, /*count*/ 13, + &count)); + ASSERT_EQ(13, count); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L""); + + ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_End, TextUnit_Character, /*count*/ 4, &count)); + ASSERT_EQ(4, count); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"line"); + + ASSERT_HRESULT_SUCCEEDED( + text_range_provider->ExpandToEnclosingUnit(TextUnit_Line)); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"maybe line #1?"); + + // Start endpoint is on a line's end boundary. + ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_Start, TextUnit_Character, /*count*/ 29, + &count)); + ASSERT_EQ(25, count); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L""); + + ASSERT_HRESULT_SUCCEEDED( + text_range_provider->ExpandToEnclosingUnit(TextUnit_Line)); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"not line #1"); +} + +// TODO(schectman) https://github.com/flutter/flutter/issues/117012 +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderExpandToEnclosingParagraph) { + Init(BuildAXTreeForMove()); + AXNode* root_node = GetRootAsAXNode(); + + ComPtr text_range_provider; + GetTextRangeProviderFromTextNode(text_range_provider, root_node); + + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, + /*expected_text*/ tree_for_move_full_text.data()); + + // Start endpoint is already on a paragraph's start boundary. + // + // Note that there are 5 paragraphs, not 6, because the line break element + // between the first and second paragraph is merged in the text of the first + // paragraph. This is standard UIA behavior which merges any trailing + // whitespace with the previous paragraph. + int count; + ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_End, TextUnit_Paragraph, /*count*/ -5, &count)); + EXPECT_EQ(-5, count); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L""); + + ASSERT_HRESULT_SUCCEEDED( + text_range_provider->ExpandToEnclosingUnit(TextUnit_Paragraph)); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"First line of text\n"); + + // Moving the start by two lines will create a degenerate range positioned + // at the next paragraph (skipping the newline). + ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_Start, TextUnit_Line, /*count*/ 2, &count)); + EXPECT_EQ(2, count); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L""); + + ASSERT_HRESULT_SUCCEEDED( + text_range_provider->ExpandToEnclosingUnit(TextUnit_Paragraph)); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"Standalone line\n"); + + // Move to the next paragraph via MoveEndpointByUnit (line), then move to + // the middle of the paragraph via Move (word), then expand by paragraph. + ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_Start, TextUnit_Line, /*count*/ 1, &count)); + EXPECT_EQ(1, count); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L""); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Word, + /*count*/ 1, + /*expected_text*/ + L"", + /*expected_count*/ 1); + ASSERT_HRESULT_SUCCEEDED( + text_range_provider->ExpandToEnclosingUnit(TextUnit_Paragraph)); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"bold text\n"); + + // Create a degenerate range at the end of the document, then expand by + // paragraph. + ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_Start, TextUnit_Document, /*count*/ 1, &count)); + EXPECT_EQ(1, count); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L""); + ASSERT_HRESULT_SUCCEEDED( + text_range_provider->ExpandToEnclosingUnit(TextUnit_Paragraph)); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"Paragraph 2"); +} + +// TODO(schectman) https://github.com/flutter/flutter/issues/117012 +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderExpandToEnclosingFormat) { + Init(BuildAXTreeForMoveByFormat()); + AXNode* root_node = GetRootAsAXNode(); + ComPtr text_range_provider; + GetTextRangeProviderFromTextNode(text_range_provider, root_node); + ComPtr text_range_provider_internal; + ASSERT_HRESULT_SUCCEEDED(text_range_provider->QueryInterface( + IID_PPV_ARGS(&text_range_provider_internal))); + + EXPECT_UIA_TEXTRANGE_EQ( + text_range_provider, + L"Text with formatting\nStandalone line with no formatting\nbold " + L"text\nParagraph 1\nParagraph 2\nParagraph 3\nParagraph 4"); + + // https://docs.microsoft.com/en-us/windows/win32/api/uiautomationclient/nf-uiautomationclient-iuiautomationtextrange-expandtoenclosingunit + // Consider two consecutive text units A and B. + // The documentation illustrates 9 cases, but cases 1 and 9 are equivalent. + // In each case, the expected output is a range from start of A to end of A. + + // Create a range encompassing nodes 11-15 which will serve as text units A + // and B for this test. + ComPtr units_a_b_provider; + ASSERT_HRESULT_SUCCEEDED(text_range_provider->Clone(&units_a_b_provider)); + CopyOwnerToClone(text_range_provider.Get(), units_a_b_provider.Get()); + + int count; + ASSERT_HRESULT_SUCCEEDED(units_a_b_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_Start, TextUnit_Line, /*count*/ 5, &count)); + ASSERT_EQ(5, count); + ASSERT_HRESULT_SUCCEEDED(units_a_b_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_End, TextUnit_Line, /*count*/ -1, &count)); + ASSERT_EQ(-1, count); + EXPECT_UIA_TEXTRANGE_EQ(units_a_b_provider, + L"Paragraph 1\nParagraph 2\nParagraph 3"); + + // Create a range encompassing node 11 which will serve as our expected + // value of a range from start of A to end of A. + ComPtr unit_a_provider; + ASSERT_HRESULT_SUCCEEDED(units_a_b_provider->Clone(&unit_a_provider)); + CopyOwnerToClone(units_a_b_provider.Get(), unit_a_provider.Get()); + ASSERT_HRESULT_SUCCEEDED(unit_a_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_End, TextUnit_Line, /*count*/ -2, &count)); + ASSERT_EQ(-2, count); + EXPECT_UIA_TEXTRANGE_EQ(unit_a_provider, L"Paragraph 1"); + + // Case 1: Degenerate range at start of A. + { + SCOPED_TRACE("Case 1: Degenerate range at start of A."); + ComPtr test_case_provider; + ASSERT_HRESULT_SUCCEEDED(unit_a_provider->Clone(&test_case_provider)); + CopyOwnerToClone(unit_a_provider.Get(), test_case_provider.Get()); + ASSERT_HRESULT_SUCCEEDED(test_case_provider->MoveEndpointByRange( + TextPatternRangeEndpoint_End, test_case_provider.Get(), + TextPatternRangeEndpoint_Start)); + EXPECT_UIA_TEXTRANGE_EQ(test_case_provider, L""); + + ASSERT_HRESULT_SUCCEEDED( + test_case_provider->ExpandToEnclosingUnit(TextUnit_Format)); + BOOL are_same; + ASSERT_HRESULT_SUCCEEDED( + test_case_provider->Compare(unit_a_provider.Get(), &are_same)); + EXPECT_TRUE(are_same); + } + + // Case 2: Range from start of A to middle of A. + { + SCOPED_TRACE("Case 2: Range from start of A to middle of A."); + ComPtr test_case_provider; + ASSERT_HRESULT_SUCCEEDED(unit_a_provider->Clone(&test_case_provider)); + CopyOwnerToClone(unit_a_provider.Get(), test_case_provider.Get()); + ASSERT_HRESULT_SUCCEEDED(test_case_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_End, TextUnit_Character, /*count*/ -7, + &count)); + ASSERT_EQ(-7, count); + EXPECT_UIA_TEXTRANGE_EQ(test_case_provider, L"Para"); + + ASSERT_HRESULT_SUCCEEDED( + test_case_provider->ExpandToEnclosingUnit(TextUnit_Format)); + BOOL are_same; + ASSERT_HRESULT_SUCCEEDED( + test_case_provider->Compare(unit_a_provider.Get(), &are_same)); + EXPECT_TRUE(are_same); + } + + // Case 3: Range from start of A to end of A. + { + SCOPED_TRACE("Case 3: Range from start of A to end of A."); + ComPtr test_case_provider; + ASSERT_HRESULT_SUCCEEDED(unit_a_provider->Clone(&test_case_provider)); + CopyOwnerToClone(unit_a_provider.Get(), test_case_provider.Get()); + EXPECT_UIA_TEXTRANGE_EQ(test_case_provider, L"Paragraph 1"); + + ASSERT_HRESULT_SUCCEEDED( + test_case_provider->ExpandToEnclosingUnit(TextUnit_Format)); + BOOL are_same; + ASSERT_HRESULT_SUCCEEDED( + test_case_provider->Compare(unit_a_provider.Get(), &are_same)); + EXPECT_TRUE(are_same); + } + + // Case 4: Range from start of A to middle of B. + { + SCOPED_TRACE("Case 4: Range from start of A to middle of B."); + ComPtr test_case_provider; + ASSERT_HRESULT_SUCCEEDED(unit_a_provider->Clone(&test_case_provider)); + CopyOwnerToClone(unit_a_provider.Get(), test_case_provider.Get()); + ASSERT_HRESULT_SUCCEEDED(test_case_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_End, TextUnit_Character, /*count*/ 5, &count)); + ASSERT_EQ(5, count); + EXPECT_UIA_TEXTRANGE_EQ(test_case_provider, L"Paragraph 1\nPara"); + + ASSERT_HRESULT_SUCCEEDED( + test_case_provider->ExpandToEnclosingUnit(TextUnit_Format)); + BOOL are_same; + ASSERT_HRESULT_SUCCEEDED( + test_case_provider->Compare(unit_a_provider.Get(), &are_same)); + EXPECT_TRUE(are_same); + } + + // Case 5: Degenerate range in middle of A. + { + SCOPED_TRACE("Case 5: Degenerate range in middle of A."); + ComPtr test_case_provider; + ASSERT_HRESULT_SUCCEEDED(unit_a_provider->Clone(&test_case_provider)); + CopyOwnerToClone(unit_a_provider.Get(), test_case_provider.Get()); + ASSERT_HRESULT_SUCCEEDED(test_case_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_Start, TextUnit_Character, /*count*/ 4, + &count)); + ASSERT_EQ(4, count); + ASSERT_HRESULT_SUCCEEDED(test_case_provider->MoveEndpointByRange( + TextPatternRangeEndpoint_End, test_case_provider.Get(), + TextPatternRangeEndpoint_Start)); + EXPECT_UIA_TEXTRANGE_EQ(test_case_provider, L""); + + ASSERT_HRESULT_SUCCEEDED( + test_case_provider->ExpandToEnclosingUnit(TextUnit_Format)); + BOOL are_same; + ASSERT_HRESULT_SUCCEEDED( + test_case_provider->Compare(unit_a_provider.Get(), &are_same)); + EXPECT_TRUE(are_same); + } + + // Case 6: Range from middle of A to middle of A. + { + SCOPED_TRACE("Case 6: Range from middle of A to middle of A."); + ComPtr test_case_provider; + ASSERT_HRESULT_SUCCEEDED(unit_a_provider->Clone(&test_case_provider)); + CopyOwnerToClone(unit_a_provider.Get(), test_case_provider.Get()); + ASSERT_HRESULT_SUCCEEDED(test_case_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_Start, TextUnit_Character, /*count*/ 4, + &count)); + ASSERT_EQ(4, count); + ASSERT_HRESULT_SUCCEEDED(test_case_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_End, TextUnit_Character, /*count*/ -2, + &count)); + ASSERT_EQ(-2, count); + EXPECT_UIA_TEXTRANGE_EQ(test_case_provider, L"graph"); + + ASSERT_HRESULT_SUCCEEDED( + test_case_provider->ExpandToEnclosingUnit(TextUnit_Format)); + BOOL are_same; + ASSERT_HRESULT_SUCCEEDED( + test_case_provider->Compare(unit_a_provider.Get(), &are_same)); + EXPECT_TRUE(are_same); + } + + // Case 7: Range from middle of A to end of A. + { + SCOPED_TRACE("Case 7: Range from middle of A to end of A."); + ComPtr test_case_provider; + ASSERT_HRESULT_SUCCEEDED(unit_a_provider->Clone(&test_case_provider)); + CopyOwnerToClone(unit_a_provider.Get(), test_case_provider.Get()); + ASSERT_HRESULT_SUCCEEDED(test_case_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_Start, TextUnit_Character, /*count*/ 4, + &count)); + ASSERT_EQ(4, count); + EXPECT_UIA_TEXTRANGE_EQ(test_case_provider, L"graph 1"); + + ASSERT_HRESULT_SUCCEEDED( + test_case_provider->ExpandToEnclosingUnit(TextUnit_Format)); + BOOL are_same; + ASSERT_HRESULT_SUCCEEDED( + test_case_provider->Compare(unit_a_provider.Get(), &are_same)); + EXPECT_TRUE(are_same); + } + + // Case 8: Range from middle of A to middle of B. + { + SCOPED_TRACE("Case 8: Range from middle of A to middle of B."); + ComPtr test_case_provider; + ASSERT_HRESULT_SUCCEEDED(unit_a_provider->Clone(&test_case_provider)); + CopyOwnerToClone(unit_a_provider.Get(), test_case_provider.Get()); + ASSERT_HRESULT_SUCCEEDED(test_case_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_Start, TextUnit_Character, /*count*/ 5, + &count)); + ASSERT_EQ(5, count); + ASSERT_HRESULT_SUCCEEDED(test_case_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_End, TextUnit_Character, /*count*/ 5, &count)); + ASSERT_EQ(5, count); + EXPECT_UIA_TEXTRANGE_EQ(test_case_provider, L"raph 1\nPara"); + + ASSERT_HRESULT_SUCCEEDED( + test_case_provider->ExpandToEnclosingUnit(TextUnit_Format)); + BOOL are_same; + ASSERT_HRESULT_SUCCEEDED( + test_case_provider->Compare(unit_a_provider.Get(), &are_same)); + EXPECT_TRUE(are_same); + } +} + +// TODO(schectman) https://github.com/flutter/flutter/issues/117012 +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderExpandToEnclosingFormatWithEmptyObjects) { + // This test updates the tree structure to test a specific edge case. + // + // When using heading navigation, the empty objects (see + // AXPosition::IsEmptyObjectReplacedByCharacter for information about empty + // objects) sometimes cause a problem with + // AXPlatformNodeTextRangeProviderWin::ExpandToEnclosingUnit. + // With some specific AXTree (like the one used below), the empty object + // causes ExpandToEnclosingUnit to move the range back on the heading that it + // previously was instead of moving it forward/backward to the next heading. + // To avoid this, empty objects are always marked as format boundaries. + // + // The issue normally occurs when a heading is directly followed by an ignored + // empty object, itself followed by an unignored empty object. + // + // ++1 kRootWebArea + // ++++2 kHeading + // ++++++3 kStaticText + // ++++++++4 kInlineTextBox + // ++++5 kGenericContainer ignored + // ++++6 kButton + ui::AXNodeData root_1; + ui::AXNodeData heading_2; + ui::AXNodeData static_text_3; + ui::AXNodeData inline_box_4; + ui::AXNodeData generic_container_5; + ui::AXNodeData button_6; + + root_1.id = 1; + heading_2.id = 2; + static_text_3.id = 3; + inline_box_4.id = 4; + generic_container_5.id = 5; + button_6.id = 6; + + root_1.role = ax::mojom::Role::kRootWebArea; + root_1.child_ids = {heading_2.id, generic_container_5.id, button_6.id}; + + heading_2.role = ax::mojom::Role::kHeading; + heading_2.child_ids = {static_text_3.id}; + + static_text_3.role = ax::mojom::Role::kStaticText; + static_text_3.child_ids = {inline_box_4.id}; + static_text_3.SetName("3.14"); + + inline_box_4.role = ax::mojom::Role::kInlineTextBox; + inline_box_4.SetName("3.14"); + + generic_container_5.role = ax::mojom::Role::kGenericContainer; + generic_container_5.AddBoolAttribute( + ax::mojom::BoolAttribute::kIsLineBreakingObject, true); + generic_container_5.AddState(ax::mojom::State::kIgnored); + + button_6.role = ax::mojom::Role::kButton; + + 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_1.id; + update.nodes.push_back(root_1); + update.nodes.push_back(heading_2); + update.nodes.push_back(static_text_3); + update.nodes.push_back(inline_box_4); + update.nodes.push_back(generic_container_5); + update.nodes.push_back(button_6); + + Init(update); + + AXNode* root_node = GetRootAsAXNode(); + ComPtr text_range_provider; + GetTextRangeProviderFromTextNode(text_range_provider, root_node); + + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"3.14\xFFFC"); + + // Create a degenerate range positioned at the boundary between nodes 4 and 6, + // e.g., "3.14<>" and "<\xFFFC>" (because node 5 is ignored). + int count; + ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_Start, TextUnit_Character, /*count*/ 5, &count)); + ASSERT_EQ(5, count); + ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_End, TextUnit_Character, /*count*/ -1, &count)); + ASSERT_EQ(-1, count); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L""); + + // ExpandToEnclosingUnit should move the range to the next non-ignored empty + // object (i.e, node 6), and not at the beginning of node 4. + ASSERT_HRESULT_SUCCEEDED( + text_range_provider->ExpandToEnclosingUnit(TextUnit_Format)); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"\xFFFC"); +} + +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderExpandToEnclosingDocument) { + Init(BuildTextDocument({"some text", "more text", "even more text"})); + + AXNode* root_node = GetRootAsAXNode(); + AXNode* text_node = root_node->children()[0]; + AXNode* more_text_node = root_node->children()[1]; + AXNode* even_more_text_node = root_node->children()[2]; + + // 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}; + ComPtr text_range_provider; + + for (auto& textunit : textunit_types) { + GetTextRangeProviderFromTextNode(text_range_provider, text_node); + ASSERT_HRESULT_SUCCEEDED( + text_range_provider->ExpandToEnclosingUnit(textunit)); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, + L"some textmore texteven more text"); + + GetTextRangeProviderFromTextNode(text_range_provider, more_text_node); + ASSERT_HRESULT_SUCCEEDED( + text_range_provider->ExpandToEnclosingUnit(textunit)); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, + L"some textmore texteven more text"); + + GetTextRangeProviderFromTextNode(text_range_provider, even_more_text_node); + ASSERT_HRESULT_SUCCEEDED( + text_range_provider->ExpandToEnclosingUnit(textunit)); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, + L"some textmore texteven more text"); + } +} + +// TODO(schectman) Why should this be ignored? +// https://github.com/flutter/flutter/issues/117012 +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderIgnoredForTextNavigation) { + // ++1 kRootWebArea + // ++++2 kStaticText + // ++++++3 kInlineTextBox foo + // ++++4 kSplitter + // ++++5 kStaticText + // ++++++6 kInlineTextBox bar + // ++++7 genericContainer + // ++++8 kStaticText + // ++++++9 kInlineTextBox baz + ui::AXNodeData root_1; + ui::AXNodeData static_text_2; + ui::AXNodeData inline_box_3; + ui::AXNodeData splitter_4; + ui::AXNodeData static_text_5; + ui::AXNodeData inline_box_6; + ui::AXNodeData generic_container_7; + ui::AXNodeData static_text_8; + ui::AXNodeData inline_box_9; + + root_1.id = 1; + static_text_2.id = 2; + inline_box_3.id = 3; + splitter_4.id = 4; + static_text_5.id = 5; + inline_box_6.id = 6; + generic_container_7.id = 7; + static_text_8.id = 8; + inline_box_9.id = 9; + + root_1.role = ax::mojom::Role::kRootWebArea; + root_1.child_ids = {static_text_2.id, splitter_4.id, static_text_5.id, + generic_container_7.id, static_text_8.id}; + + static_text_2.role = ax::mojom::Role::kStaticText; + static_text_2.child_ids = {inline_box_3.id}; + static_text_2.SetName("foo"); + + inline_box_3.role = ax::mojom::Role::kInlineTextBox; + inline_box_3.SetName("foo"); + + splitter_4.role = ax::mojom::Role::kSplitter; + splitter_4.AddBoolAttribute(ax::mojom::BoolAttribute::kIsLineBreakingObject, + true); + + static_text_5.role = ax::mojom::Role::kStaticText; + static_text_5.child_ids = {inline_box_6.id}; + static_text_5.SetName("bar"); + + inline_box_6.role = ax::mojom::Role::kInlineTextBox; + inline_box_6.SetName("bar"); + + generic_container_7.role = ax::mojom::Role::kGenericContainer; + generic_container_7.AddBoolAttribute( + ax::mojom::BoolAttribute::kIsLineBreakingObject, true); + + static_text_8.role = ax::mojom::Role::kStaticText; + static_text_8.child_ids = {inline_box_9.id}; + static_text_8.SetName("bar"); + + inline_box_9.role = ax::mojom::Role::kInlineTextBox; + inline_box_9.SetName("baz"); + + 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_1.id; + update.nodes = { + root_1, static_text_2, inline_box_3, splitter_4, + static_text_5, inline_box_6, generic_container_7, static_text_8, + inline_box_9}; + + Init(update); + + AXNode* root_node = GetRootAsAXNode(); + ComPtr text_range_provider; + GetTextRangeProviderFromTextNode(text_range_provider, root_node); + + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, + L"foo\n\xFFFC\nbar\n\xFFFC\nbaz"); + + int count; + ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_Start, TextUnit_Paragraph, /*count*/ 1, &count)); + ASSERT_EQ(1, count); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"bar\n\xFFFC\nbaz"); + + ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_Start, TextUnit_Paragraph, /*count*/ 1, &count)); + ASSERT_EQ(1, count); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"baz"); +} + +// TODO(schectman) Segfault after test completes. +// Why? https://github.com/flutter/flutter/issues/117012 +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderInvalidCalls) { + // Test for when a text range provider is invalid. Because no ax tree is + // available, the anchor is invalid, so the text range provider fails the + // validate call. + { + Init(BuildTextDocument({})); + + ComPtr text_range_provider; + GetTextRangeProviderFromTextNode(text_range_provider, GetRootAsAXNode()); + + DestroyTree(); + ComPtr text_range_provider_clone; + EXPECT_UIA_ELEMENTNOTAVAILABLE( + text_range_provider->Clone(&text_range_provider_clone)); + + BOOL compare_result; + EXPECT_UIA_ELEMENTNOTAVAILABLE(text_range_provider->Compare( + text_range_provider.Get(), &compare_result)); + + int compare_endpoints_result; + EXPECT_UIA_ELEMENTNOTAVAILABLE(text_range_provider->CompareEndpoints( + TextPatternRangeEndpoint_Start, text_range_provider.Get(), + TextPatternRangeEndpoint_Start, &compare_endpoints_result)); + + VARIANT attr_val; + V_VT(&attr_val) = VT_BOOL; + V_BOOL(&attr_val) = VARIANT_TRUE; + ComPtr matched_range_provider; + EXPECT_UIA_ELEMENTNOTAVAILABLE(text_range_provider->FindAttribute( + UIA_IsHiddenAttributeId, attr_val, true, &matched_range_provider)); + + EXPECT_UIA_ELEMENTNOTAVAILABLE(text_range_provider->MoveEndpointByRange( + TextPatternRangeEndpoint_Start, text_range_provider.Get(), + TextPatternRangeEndpoint_Start)); + + EXPECT_UIA_ELEMENTNOTAVAILABLE(text_range_provider->Select()); + } + + // Test for when this provider is valid, but the other provider is not an + // instance of AXPlatformNodeTextRangeProviderWin, so no operation can be + // performed on the other provider. + { + Init(BuildTextDocument({})); + + ComPtr this_provider; + GetTextRangeProviderFromTextNode(this_provider, GetRootAsAXNode()); + + ComPtr other_provider_different_type; + MockAXPlatformNodeTextRangeProviderWin::CreateMockTextRangeProvider( + &other_provider_different_type); + + BOOL compare_result; + EXPECT_UIA_INVALIDOPERATION(this_provider->Compare( + other_provider_different_type.Get(), &compare_result)); + + int compare_endpoints_result; + EXPECT_UIA_INVALIDOPERATION(this_provider->CompareEndpoints( + TextPatternRangeEndpoint_Start, other_provider_different_type.Get(), + TextPatternRangeEndpoint_Start, &compare_endpoints_result)); + + EXPECT_UIA_INVALIDOPERATION(this_provider->MoveEndpointByRange( + TextPatternRangeEndpoint_Start, other_provider_different_type.Get(), + TextPatternRangeEndpoint_Start)); + } +} + +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderGetText) { + Init(BuildTextDocument({"some text", "more text"})); + + AXNode* root_node = GetRootAsAXNode(); + AXNode* text_node = root_node->children()[0]; + + ComPtr text_range_provider; + GetTextRangeProviderFromTextNode(text_range_provider, text_node); + + base::win::ScopedBstr text_content; + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetText(-1, text_content.Receive())); + EXPECT_STREQ(text_content.Get(), L"some text"); + text_content.Reset(); + + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetText(4, text_content.Receive())); + EXPECT_STREQ(text_content.Get(), L"some"); + text_content.Reset(); + + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetText(0, text_content.Receive())); + EXPECT_STREQ(text_content.Get(), L""); + text_content.Reset(); + + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetText(9, text_content.Receive())); + EXPECT_STREQ(text_content.Get(), L"some text"); + text_content.Reset(); + + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetText(10, text_content.Receive())); + EXPECT_STREQ(text_content.Get(), L"some text"); + text_content.Reset(); + + EXPECT_HRESULT_FAILED(text_range_provider->GetText(-1, nullptr)); + + EXPECT_HRESULT_FAILED( + text_range_provider->GetText(-2, text_content.Receive())); + text_content.Reset(); + + ComPtr document_textrange; + GetTextRangeProviderFromTextNode(document_textrange, root_node); + + EXPECT_HRESULT_SUCCEEDED( + document_textrange->GetText(-1, text_content.Receive())); + EXPECT_STREQ(text_content.Get(), L"some textmore text"); + text_content.Reset(); +} + +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderMoveCharacter) { + Init(BuildAXTreeForMove()); + AXNode* root_node = GetRootAsAXNode(); + + ComPtr text_range_provider; + GetTextRangeProviderFromTextNode(text_range_provider, root_node); + + // Moving by 0 should have no effect. + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Character, /*count*/ 0, + /*expected_text*/ + L"First line of text\nStandalone line\n" + L"bold textParagraph 1Paragraph 2", + /*expected_count*/ 0); + + // Move forward. + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Character, + /*count*/ 1, + /*expected_text*/ L"i", + /*expected_count*/ 1); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Character, + /*count*/ 18, + /*expected_text*/ L"S", + /*expected_count*/ 18); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Character, + /*count*/ 16, + /*expected_text*/ L"b", + /*expected_count*/ 16); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Character, + /*count*/ 60, + /*expected_text*/ L"2", + /*expected_count*/ 30); + + // Trying to move past the last character should have no effect. + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Character, + /*count*/ 1, + /*expected_text*/ L"2", + /*expected_count*/ 0); + + // Move backward. + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Character, + /*count*/ -2, + /*expected_text*/ L"h", + /*expected_count*/ -2); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Character, + /*count*/ -9, + /*expected_text*/ L"1", + /*expected_count*/ -9); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Character, + /*count*/ -60, + /*expected_text*/ L"F", + /*expected_count*/ -54); + + // Moving backward by any number of characters at the start of document + // should have no effect. + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Character, + /*count*/ -1, + /*expected_text*/ + L"F", + /*expected_count*/ 0); + + // Degenerate range moves. + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_End, TextUnit_Character, + /*count*/ -1, + /*expected_text*/ L"", + /*expected_count*/ -1); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Character, + /*count*/ 4, + /*expected_text*/ L"", + /*expected_count*/ 4); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Character, + /*count*/ 70, + /*expected_text*/ L"", + /*expected_count*/ 62); + + // Trying to move past the last character should have no effect. + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Character, + /*count*/ 70, + /*expected_text*/ L"", + /*expected_count*/ 0); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Character, + /*count*/ -2, + /*expected_text*/ L"", + /*expected_count*/ -2); +} + +// TODO(schectman) https://github.com/flutter/flutter/issues/117012 +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderMoveFormat) { + Init(BuildAXTreeForMoveByFormat()); + AXNode* root_node = GetRootAsAXNode(); + + ComPtr text_range_provider; + GetTextRangeProviderFromTextNode(text_range_provider, root_node); + + // Moving by 0 should have no effect. + EXPECT_UIA_MOVE( + text_range_provider, TextUnit_Format, + /*count*/ 0, + /*expected_text*/ + L"Text with formatting\nStandalone line with no formatting\nbold " + L"text\nParagraph 1\nParagraph 2\nParagraph 3\nParagraph 4", + /*expected_count*/ 0); + + // Move forward. + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Format, + /*count*/ 1, + /*expected_text*/ L"\nStandalone line with no formatting\n", + /*expected_count*/ 1); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Format, + /*count*/ 2, + /*expected_text*/ L"Paragraph 1", + /*expected_count*/ 2); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Format, + /*count*/ 1, + /*expected_text*/ L"Paragraph 2\nParagraph 3", + /*expected_count*/ 1); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Format, + /*count*/ 1, + /*expected_text*/ L"Paragraph 4", + /*expected_count*/ 1); + + // Trying to move past the last format should have no effect. + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Format, + /*count*/ 1, + /*expected_text*/ L"Paragraph 4", + /*expected_count*/ 0); + + // Move backward. + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Format, + /*count*/ -3, + /*expected_text*/ L"bold text", + /*expected_count*/ -3); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Format, + /*count*/ -1, + /*expected_text*/ L"\nStandalone line with no formatting\n", + /*expected_count*/ -1); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Format, + /*count*/ -1, + /*expected_text*/ L"Text with formatting", + /*expected_count*/ -1); + + // Moving backward by any number of formats at the start of document + // should have no effect. + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Format, + /*count*/ -1, + /*expected_text*/ + L"Text with formatting", + /*expected_count*/ 0); + + // Test degenerate range creation at the beginning of the document. + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_End, TextUnit_Format, + /*count*/ -1, + /*expected_text*/ L"", + /*expected_count*/ -1); + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_End, TextUnit_Format, + /*count*/ 1, + /*expected_text*/ L"Text with formatting", + /*expected_count*/ 1); + + // Test degenerate range creation at the end of the document. + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Format, + /*count*/ 5, + /*expected_text*/ L"Paragraph 4", + /*expected_count*/ 5); + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_Start, TextUnit_Format, + /*count*/ 1, + /*expected_text*/ L"", + /*expected_count*/ 1); + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_Start, TextUnit_Format, + /*count*/ -1, + /*expected_text*/ L"Paragraph 4", + /*expected_count*/ -1); + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_Start, TextUnit_Format, + /*count*/ 1, + /*expected_text*/ L"", + /*expected_count*/ 1); + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_Start, TextUnit_Format, + /*count*/ -1, + /*expected_text*/ L"Paragraph 4", + /*expected_count*/ -1); + + // Degenerate range moves. + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Format, + /*count*/ -5, + /*expected_text*/ L"Text with formatting", + /*expected_count*/ -5); + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_End, TextUnit_Format, + /*count*/ -1, + /*expected_text*/ L"", + /*expected_count*/ -1); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Format, + /*count*/ 3, + /*expected_text*/ L"", + /*expected_count*/ 3); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Format, + /*count*/ 70, + /*expected_text*/ L"", + /*expected_count*/ 3); + + // Trying to move past the last format should have no effect. + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Format, + /*count*/ 70, + /*expected_text*/ L"", + /*expected_count*/ 0); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Format, + /*count*/ -2, + /*expected_text*/ L"", + /*expected_count*/ -2); +} + +// TODO(schectman) https://github.com/flutter/flutter/issues/117012 +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderMoveWord) { + Init(BuildAXTreeForMove()); + AXNode* root_node = GetRootAsAXNode(); + + ComPtr text_range_provider; + GetTextRangeProviderFromTextNode(text_range_provider, root_node); + + // Moving by 0 should have no effect. + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Word, /*count*/ 0, + /*expected_text*/ tree_for_move_full_text.data(), + /*expected_count*/ 0); + + // Move forward. + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Word, + /*count*/ 1, + /*expected_text*/ L"line ", + /*expected_count*/ 1); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Word, + /*count*/ 2, + /*expected_text*/ L"text", + /*expected_count*/ 2); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Word, + /*count*/ 2, + /*expected_text*/ L"line", + /*expected_count*/ 2); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Word, + /*count*/ 3, + /*expected_text*/ L"Paragraph ", + /*expected_count*/ 3); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Word, + /*count*/ 6, + /*expected_text*/ L"2", + /*expected_count*/ 3); + + // Trying to move past the last word should have no effect. + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Word, + /*count*/ 1, + /*expected_text*/ L"2", + /*expected_count*/ 0); + + // Move backward. + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Word, + /*count*/ -3, + /*expected_text*/ L"Paragraph ", + /*expected_count*/ -3); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Word, + /*count*/ -3, + /*expected_text*/ L"line", + /*expected_count*/ -3); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Word, + /*count*/ -2, + /*expected_text*/ L"text", + /*expected_count*/ -2); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Word, + /*count*/ -6, + /*expected_text*/ L"First ", + /*expected_count*/ -3); + + // Moving backward by any number of words at the start of document + // should have no effect. + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Word, + /*count*/ -20, + /*expected_text*/ L"First ", + /*expected_count*/ 0); + + // Degenerate range moves. + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT(text_range_provider, + TextPatternRangeEndpoint_End, TextUnit_Word, + /*count*/ -1, + /*expected_text*/ L"", + /*expected_count*/ -1); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Word, + /*count*/ 4, + /*expected_text*/ L"", + /*expected_count*/ 4); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Word, + /*count*/ 70, + /*expected_text*/ L"", + /*expected_count*/ 8); + + // Trying to move past the last word should have no effect. + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Word, + /*count*/ 70, + /*expected_text*/ L"", + /*expected_count*/ 0); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Word, + /*count*/ -2, + /*expected_text*/ L"", + /*expected_count*/ -2); +} + +// TODO(schectman) https://github.com/flutter/flutter/issues/117012 +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderMoveLine) { + Init(BuildAXTreeForMove()); + AXNode* root_node = GetRootAsAXNode(); + + ComPtr text_range_provider; + GetTextRangeProviderFromTextNode(text_range_provider, root_node); + + // Moving by 0 should have no effect. + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Line, /*count*/ 0, + /*expected_text*/ tree_for_move_full_text.data(), + /*expected_count*/ 0); + + // Move forward. + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Line, + /*count*/ 2, + /*expected_text*/ L"Standalone line", + /*expected_count*/ 2); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Line, + /*count*/ 1, + /*expected_text*/ L"bold text", + /*expected_count*/ 1); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Line, + /*count*/ 10, + /*expected_text*/ L"Paragraph 2", + /*expected_count*/ 2); + + // Trying to move past the last line should have no effect. + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Line, + /*count*/ 1, + /*expected_text*/ L"Paragraph 2", + /*expected_count*/ 0); + + // Move backward. + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Line, + /*count*/ -1, + /*expected_text*/ L"Paragraph 1", + /*expected_count*/ -1); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Line, + /*count*/ -5, + /*expected_text*/ L"First line of text", + /*expected_count*/ -4); + + // Moving backward by any number of lines at the start of document + // should have no effect. + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Line, + /*count*/ -20, + /*expected_text*/ L"First line of text", + /*expected_count*/ 0); + + // Degenerate range moves. + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT(text_range_provider, + TextPatternRangeEndpoint_End, TextUnit_Line, + /*count*/ -1, + /*expected_text*/ L"", + /*expected_count*/ -1); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Line, + /*count*/ 4, + /*expected_text*/ L"", + /*expected_count*/ 4); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Line, + /*count*/ 70, + /*expected_text*/ L"", + /*expected_count*/ 2); + + // Trying to move past the last line should have no effect. + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Line, + /*count*/ 70, + /*expected_text*/ L"", + /*expected_count*/ 0); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Line, + /*count*/ -2, + /*expected_text*/ L"", + /*expected_count*/ -2); +} + +// TODO(schectman) https://github.com/flutter/flutter/issues/117012 +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderMoveParagraph) { + Init(BuildAXTreeForMove()); + AXNode* root_node = GetRootAsAXNode(); + + ComPtr text_range_provider; + GetTextRangeProviderFromTextNode(text_range_provider, root_node); + + // Moving by 0 should have no effect. + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Paragraph, /*count*/ 0, + /*expected_text*/ tree_for_move_full_text.data(), + /*expected_count*/ 0); + + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_End, TextUnit_Paragraph, + /*count*/ -4, + /*expected_text*/ L"First line of text\n", + /*expected_count*/ -4); + + // The first line break does not create an empty paragraph because even though + // it is in a block element (i.e. a kGenericContainer) of its own which is a + // line breaking object, it merges with the previous paragraph. This is + // standard UIA behavior which merges any trailing whitespace with the + // previous paragraph. + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_End, TextUnit_Paragraph, + /*count*/ -1, + /*expected_text*/ L"", + /*expected_count*/ -1); + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_End, TextUnit_Paragraph, + /*count*/ 1, + /*expected_text*/ L"First line of text\n", + /*expected_count*/ 1); + + // + // Move forward. + // + + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Paragraph, + /*count*/ 1, + /*expected_text*/ L"Standalone line\n", + /*expected_count*/ 1); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Paragraph, + /*count*/ 1, + /*expected_text*/ L"bold text\n", + /*expected_count*/ 1); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Paragraph, + /*count*/ 1, + /*expected_text*/ L"Paragraph 1\n", + /*expected_count*/ 1); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Paragraph, + /*count*/ 1, + /*expected_text*/ L"Paragraph 2", + /*expected_count*/ 1); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Paragraph, + /*count*/ 2, + /*expected_text*/ L"Paragraph 2", + /*expected_count*/ 0); + + // Trying to move past the last paragraph should have no effect. + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Paragraph, + /*count*/ 1, + /*expected_text*/ L"Paragraph 2", + /*expected_count*/ 0); + + // + // Move backward. + // + + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Paragraph, + /*count*/ -1, + /*expected_text*/ L"Paragraph 1\n", + /*expected_count*/ -1); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Paragraph, + /*count*/ -1, + /*expected_text*/ L"bold text\n", + /*expected_count*/ -1); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Paragraph, + /*count*/ -1, + /*expected_text*/ L"Standalone line\n", + /*expected_count*/ -1); + // The first line break creates an empty paragraph because it is in a block + // element (i.e. a kGenericContainer) of its own which is a line breaking + // object. It's like having a
element wrapped inside a
. + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Paragraph, + /*count*/ -1, + /*expected_text*/ L"First line of text\n", + /*expected_count*/ -1); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Paragraph, + /*count*/ -1, + /*expected_text*/ L"First line of text\n", + /*expected_count*/ 0); + + // Moving backward by any number of paragraphs at the start of document + // should have no effect. + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Paragraph, + /*count*/ -1, + /*expected_text*/ L"First line of text\n", + /*expected_count*/ 0); + + // Test degenerate range creation at the beginning of the document. + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_End, TextUnit_Paragraph, + /*count*/ -1, + /*expected_text*/ L"", + /*expected_count*/ -1); + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_End, TextUnit_Paragraph, + /*count*/ 1, + /*expected_text*/ L"First line of text\n", + /*expected_count*/ 1); + + // Test degenerate range creation at the end of the document. + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Paragraph, + /*count*/ 5, + /*expected_text*/ L"Paragraph 2", + /*expected_count*/ 4); + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_Start, TextUnit_Paragraph, + /*count*/ 1, + /*expected_text*/ L"", + /*expected_count*/ 1); + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_Start, TextUnit_Paragraph, + /*count*/ -1, + /*expected_text*/ L"Paragraph 2", + /*expected_count*/ -1); + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_Start, TextUnit_Paragraph, + /*count*/ 1, + /*expected_text*/ L"", + /*expected_count*/ 1); + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_Start, TextUnit_Paragraph, + /*count*/ -1, + /*expected_text*/ L"Paragraph 2", + /*expected_count*/ -1); + + // + // Degenerate range moves. + // + + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Paragraph, + /*count*/ -6, + /*expected_text*/ L"First line of text\n", + /*expected_count*/ -4); + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_End, TextUnit_Paragraph, + /*count*/ -1, + /*expected_text*/ L"", + /*expected_count*/ -1); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Paragraph, + /*count*/ 3, + /*expected_text*/ L"", + /*expected_count*/ 3); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Paragraph, + /*count*/ 70, + /*expected_text*/ L"", + /*expected_count*/ 2); + + // Trying to move past the last paragraph should have no effect. + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Paragraph, + /*count*/ 70, + /*expected_text*/ L"", + /*expected_count*/ 0); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Paragraph, + /*count*/ -2, + /*expected_text*/ L"", + /*expected_count*/ -2); +} + +// TODO(schectman) https://github.com/flutter/flutter/issues/117012 +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderMoveDocument) { + Init(BuildAXTreeForMove()); + AXNode* root_node = GetRootAsAXNode(); + + ComPtr text_range_provider; + GetTextRangeProviderFromTextNode(text_range_provider, root_node); + + // Moving by 0 should have no effect. + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Document, /*count*/ 0, + /*expected_text*/ tree_for_move_full_text.data(), + /*expected_count*/ 0); + + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Document, /*count*/ -1, + /*expected_text*/ tree_for_move_full_text.data(), + /*expected_count*/ 0); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Document, /*count*/ 2, + /*expected_text*/ tree_for_move_full_text.data(), + /*expected_count*/ 0); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Page, /*count*/ 1, + /*expected_text*/ tree_for_move_full_text.data(), + /*expected_count*/ 0); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Page, /*count*/ -1, + /*expected_text*/ tree_for_move_full_text.data(), + /*expected_count*/ 0); + + // Degenerate range moves. + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_End, TextUnit_Document, + /*count*/ -2, + /*expected_text*/ L"", + /*expected_count*/ -1); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Page, + /*count*/ 4, + /*expected_text*/ L"", + /*expected_count*/ 1); + + // Trying to move past the last character should have no effect. + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Document, + /*count*/ 1, + /*expected_text*/ L"", + /*expected_count*/ 0); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Page, + /*count*/ -2, + /*expected_text*/ L"", + /*expected_count*/ -1); + EXPECT_UIA_MOVE(text_range_provider, TextUnit_Document, + /*count*/ -1, + /*expected_text*/ L"", + /*expected_count*/ 0); +} + +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderMove) { + Init(BuildAXTreeForMove()); + AXNode* root_node = GetRootAsAXNode(); + + ComPtr text_range_provider; + GetTextRangeProviderFromTextNode(text_range_provider, root_node); + + // TODO(https://crbug.com/928948): test intermixed unit types +} + +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderMoveEndpointByDocument) { + Init(BuildTextDocument({"some text", "more text", "even more text"})); + AXNode* text_node = GetRootAsAXNode()->children()[1]; + + // 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}; + ComPtr text_range_provider; + + for (auto& textunit : textunit_types) { + GetTextRangeProviderFromTextNode(text_range_provider, text_node); + + // Verify MoveEndpointByUnit with zero count has no effect + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT(text_range_provider, + TextPatternRangeEndpoint_End, textunit, + /*count*/ 0, + /*expected_text*/ L"more text", + /*expected_count*/ 0); + + // Move the endpoint to the end of the document. Verify all text content. + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_End, textunit, + /*count*/ 1, + /*expected_text*/ L"more texteven more text", + /*expected_count*/ 1); + + // Verify no moves occur since the end is already at the end of the document + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_End, textunit, + /*count*/ 5, + /*expected_text*/ L"more texteven more text", + /*expected_count*/ 0); + + // Move the end before the start + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT(text_range_provider, + TextPatternRangeEndpoint_End, textunit, + /*count*/ -4, + /*expected_text*/ L"", + /*expected_count*/ -1); + + // 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. + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_End, textunit, + /*count*/ 1, + /*expected_text*/ L"some textmore texteven more text", + /*expected_count*/ 1); + + // Move the start point to the end + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT(text_range_provider, + TextPatternRangeEndpoint_Start, textunit, + /*count*/ 3, + /*expected_text*/ L"", + /*expected_count*/ 1); + + // Move the start point back to the beginning + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_Start, textunit, + /*count*/ -3, + /*expected_text*/ L"some textmore texteven more text", + /*expected_count*/ -1); + } +} + +// TODO(schectman) We are probably not accounting for multibyte characters +// properly yet. https://github.com/flutter/flutter/issues/117012 +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderMoveEndpointByCharacterMultilingual) { + // The English string has three characters, each 8 bits in length. + const std::string english = "hey"; + + // The Hindi string has two characters, the first one 32 bits and the second + // 64 bits in length. It is formatted in UTF16. + const std::string hindi = + base::UTF16ToUTF8(u"\x0939\x093F\x0928\x094D\x0926\x0940"); + + // The Thai string has three characters, the first one 48, the second 32 and + // the last one 16 bits in length. It is formatted in UTF16. + const std::string thai = + base::UTF16ToUTF8(u"\x0E23\x0E39\x0E49\x0E2A\x0E36\x0E01"); + + Init(BuildTextDocument({english, hindi, thai})); + + ComPtr text_range_provider; + GetTextRangeProviderFromTextNode(text_range_provider, + GetRootAsAXNode()->children()[0]); + + // Verify MoveEndpointByUnit with zero count has no effect + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"hey"); + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_Start, TextUnit_Character, + /*count*/ 0, + /*expected_text*/ L"hey", + /*expected_count*/ 0); + + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_Start, TextUnit_Character, + /*count*/ 1, + /*expected_text*/ L"ey", + /*expected_count*/ 1); + + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_End, TextUnit_Character, + /*count*/ -1, + /*expected_text*/ L"e", + /*expected_count*/ -1); + + // Move end into the adjacent node. + // + // The first character of the second node is 32 bits in length. + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_End, TextUnit_Character, + /*count*/ 2, + /*expected_text*/ L"ey\x0939\x093F", + /*expected_count*/ 2); + + // The second character of the second node is 64 bits in length. + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_End, TextUnit_Character, + /*count*/ 1, + /*expected_text*/ L"ey\x939\x93F\x928\x94D\x926\x940", + /*expected_count*/ 1); + + // Move start into the adjacent node as well. + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_Start, TextUnit_Character, + /*count*/ 2, + /*expected_text*/ L"\x939\x93F\x928\x94D\x926\x940", + /*expected_count*/ 2); + + // Move end into the last node. + // + // The first character of the last node is 48 bits in length. + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_End, TextUnit_Character, + /*count*/ 1, + /*expected_text*/ L"\x939\x93F\x928\x94D\x926\x940\xE23\xE39\xE49", + /*expected_count*/ 1); + + // Move end back into the second node and then into the last node again. + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_End, TextUnit_Character, + /*count*/ -2, + /*expected_text*/ L"\x939\x93F", + /*expected_count*/ -2); + + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_End, TextUnit_Character, + /*count*/ 3, + /*expected_text*/ + L"\x939\x93F\x928\x94D\x926\x940\xE23\xE39\xE49\xE2A\xE36", + /*expected_count*/ 3); + + // The last character of the last node is only 16 bits in length. + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_End, TextUnit_Character, + /*count*/ 1, + /*expected_text*/ + L"\x939\x93F\x928\x94D\x926\x940\xE23\xE39\xE49\xE2A\xE36\xE01", + /*expected_count*/ 1); + + // Move start into the last node. + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_Start, TextUnit_Character, + /*count*/ 3, + /*expected_text*/ L"\x0E2A\x0E36\x0E01", + /*expected_count*/ 3); + + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_Start, TextUnit_Character, + /*count*/ -1, + /*expected_text*/ L"\x0E23\x0E39\x0E49\x0E2A\x0E36\x0E01", + /*expected_count*/ -1); +} + +// TODO(schectman) https://github.com/flutter/flutter/issues/117012 +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderMoveEndpointByWord) { + Init(BuildTextDocument({"some text", "more text", "even more text"}, + /*build_word_boundaries_offsets*/ true)); + + ComPtr text_range_provider; + GetTextRangeProviderFromTextNode(text_range_provider, + GetRootAsAXNode()->children()[1]); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"more text"); + + // Moving with zero count does not alter the range. + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT(text_range_provider, + TextPatternRangeEndpoint_End, TextUnit_Word, + /*count*/ 0, + /*expected_text*/ L"more text", + /*expected_count*/ 0); + + // Moving the start forward and backward. + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_Start, TextUnit_Word, + /*count*/ 1, + /*expected_text*/ L"text", + /*expected_count*/ 1); + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_Start, TextUnit_Word, + /*count*/ -1, + /*expected_text*/ L"more text", + /*expected_count*/ -1); + + // Moving the end backward and forward. + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT(text_range_provider, + TextPatternRangeEndpoint_End, TextUnit_Word, + /*count*/ -1, + /*expected_text*/ L"more ", + /*expected_count*/ -1); + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT(text_range_provider, + TextPatternRangeEndpoint_End, TextUnit_Word, + /*count*/ 1, + /*expected_text*/ L"more text", + /*expected_count*/ 1); + + // Moving the start past the end, then reverting. + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_Start, TextUnit_Word, + /*count*/ 3, + /*expected_text*/ L"", + /*expected_count*/ 3); + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_Start, TextUnit_Word, + /*count*/ -3, + /*expected_text*/ L"more texteven ", + /*expected_count*/ -3); + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT(text_range_provider, + TextPatternRangeEndpoint_End, TextUnit_Word, + /*count*/ -1, + /*expected_text*/ L"more text", + /*expected_count*/ -1); + + // Moving the end past the start, then reverting. + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT(text_range_provider, + TextPatternRangeEndpoint_End, TextUnit_Word, + /*count*/ -3, + /*expected_text*/ L"", + /*expected_count*/ -3); + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT(text_range_provider, + TextPatternRangeEndpoint_End, TextUnit_Word, + /*count*/ 3, + /*expected_text*/ L"textmore text", + /*expected_count*/ 3); + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_Start, TextUnit_Word, + /*count*/ 1, + /*expected_text*/ L"more text", + /*expected_count*/ 1); + + // Moving the endpoints further than both ends of the document. + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT(text_range_provider, + TextPatternRangeEndpoint_End, TextUnit_Word, + /*count*/ 5, + /*expected_text*/ L"more texteven more text", + /*expected_count*/ 3); + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_Start, TextUnit_Word, + /*count*/ 6, + /*expected_text*/ L"", + /*expected_count*/ 5); + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_Start, TextUnit_Word, + /*count*/ -8, + /*expected_text*/ L"some textmore texteven more text", + /*expected_count*/ -7); + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT(text_range_provider, + TextPatternRangeEndpoint_End, TextUnit_Word, + /*count*/ -8, + /*expected_text*/ L"", + /*expected_count*/ -7); +} + +// TODO(schectman) https://github.com/flutter/flutter/issues/117012 +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderMoveEndpointByLine) { + Init(BuildTextDocument({"0", "1", "2", "3", "4", "5", "6"})); + + ComPtr text_range_provider; + GetTextRangeProviderFromTextNode(text_range_provider, + GetRootAsAXNode()->children()[3]); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"3"); + + // Moving with zero count does not alter the range. + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT(text_range_provider, + TextPatternRangeEndpoint_End, TextUnit_Line, + /*count*/ 0, + /*expected_text*/ L"3", + /*expected_count*/ 0); + + // Moving the start backward and forward. + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_Start, TextUnit_Line, + /*count*/ -2, + /*expected_text*/ L"123", + /*expected_count*/ -2); + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_Start, TextUnit_Line, + /*count*/ 1, + /*expected_text*/ L"23", + /*expected_count*/ 1); + + // Moving the end forward and backward. + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT(text_range_provider, + TextPatternRangeEndpoint_End, TextUnit_Line, + /*count*/ 3, + /*expected_text*/ L"23456", + /*expected_count*/ 3); + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT(text_range_provider, + TextPatternRangeEndpoint_End, TextUnit_Line, + /*count*/ -2, + /*expected_text*/ L"234", + /*expected_count*/ -2); + + // Moving the end past the start and vice versa. + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT(text_range_provider, + TextPatternRangeEndpoint_End, TextUnit_Line, + /*count*/ -4, + /*expected_text*/ L"", + /*expected_count*/ -4); + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_Start, TextUnit_Line, + /*count*/ -1, + /*expected_text*/ L"0", + /*expected_count*/ -1); + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_Start, TextUnit_Line, + /*count*/ 6, + /*expected_text*/ L"", + /*expected_count*/ 6); + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_Start, TextUnit_Line, + /*count*/ -6, + /*expected_text*/ L"012345", + /*expected_count*/ -6); + + // Moving the endpoints further than both ends of the document. + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT(text_range_provider, + TextPatternRangeEndpoint_End, TextUnit_Line, + /*count*/ -13, + /*expected_text*/ L"", + /*expected_count*/ -6); + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT(text_range_provider, + TextPatternRangeEndpoint_End, TextUnit_Line, + /*count*/ 11, + /*expected_text*/ L"0123456", + /*expected_count*/ 7); + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_Start, TextUnit_Line, + /*count*/ 9, + /*expected_text*/ L"", + /*expected_count*/ 7); + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_Start, TextUnit_Line, + /*count*/ -7, + /*expected_text*/ L"0123456", + /*expected_count*/ -7); +} + +// TODO(schectman) https://github.com/flutter/flutter/issues/117012 +// Verify that the endpoint can move past an empty text field. +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderMoveEndpointByUnitTextField) { + // An empty text field should also be a character, word, and line boundary. + ui::AXNodeData root_data; + root_data.id = 1; + root_data.role = ax::mojom::Role::kRootWebArea; + + ui::AXNodeData group1_data; + group1_data.id = 2; + group1_data.role = ax::mojom::Role::kGenericContainer; + + ui::AXNodeData text_data; + text_data.id = 3; + text_data.role = ax::mojom::Role::kStaticText; + std::string text_content = "some text"; + text_data.SetName(text_content); + std::vector word_start_offsets, word_end_offsets; + ComputeWordBoundariesOffsets(text_content, word_start_offsets, + word_end_offsets); + text_data.AddIntListAttribute(ax::mojom::IntListAttribute::kWordStarts, + word_start_offsets); + text_data.AddIntListAttribute(ax::mojom::IntListAttribute::kWordEnds, + word_end_offsets); + + ui::AXNodeData text_input_data; + text_input_data.id = 4; + text_input_data.role = ax::mojom::Role::kTextField; + text_input_data.AddState(ax::mojom::State::kEditable); + text_input_data.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, + "input"); + text_input_data.AddStringAttribute(ax::mojom::StringAttribute::kInputType, + "text"); + + ui::AXNodeData group2_data; + group2_data.id = 5; + group2_data.role = ax::mojom::Role::kGenericContainer; + + ui::AXNodeData more_text_data; + more_text_data.id = 6; + more_text_data.role = ax::mojom::Role::kStaticText; + text_content = "more text"; + more_text_data.SetName(text_content); + ComputeWordBoundariesOffsets(text_content, word_start_offsets, + word_end_offsets); + more_text_data.AddIntListAttribute(ax::mojom::IntListAttribute::kWordStarts, + word_start_offsets); + more_text_data.AddIntListAttribute(ax::mojom::IntListAttribute::kWordEnds, + word_end_offsets); + + ui::AXNodeData empty_text_data; + empty_text_data.id = 7; + empty_text_data.role = ax::mojom::Role::kStaticText; + empty_text_data.AddState(ax::mojom::State::kEditable); + text_content = ""; + empty_text_data.SetNameExplicitlyEmpty(); + ComputeWordBoundariesOffsets(text_content, word_start_offsets, + word_end_offsets); + empty_text_data.AddIntListAttribute(ax::mojom::IntListAttribute::kWordStarts, + word_start_offsets); + empty_text_data.AddIntListAttribute(ax::mojom::IntListAttribute::kWordEnds, + word_end_offsets); + + root_data.child_ids = {group1_data.id, text_input_data.id, group2_data.id}; + group1_data.child_ids = {text_data.id}; + text_input_data.child_ids = {empty_text_data.id}; + group2_data.child_ids = {more_text_data.id}; + + 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 = {root_data, group1_data, text_data, text_input_data, + group2_data, more_text_data, empty_text_data}; + + Init(update); + + // Set up variables from the tree for testing. + AXNode* root_node = GetRootAsAXNode(); + AXNode* text_node = root_node->children()[0]->children()[0]; + + ComPtr text_range_provider; + GetTextRangeProviderFromTextNode(text_range_provider, text_node); + + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"some text"); + + int count; + // Tests for TextUnit_Character. + ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_End, TextUnit_Character, /*count*/ 2, &count)); + ASSERT_EQ(2, count); + // Note that by design, empty objects such as empty text fields, are placed in + // their own paragraph for easier screen reader navigation. + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"some text\n\xFFFc"); + + ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_End, TextUnit_Character, /*count*/ 2, &count)); + ASSERT_EQ(2, count); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"some text\n\xFFFc\nm"); + + ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_End, TextUnit_Character, /*count*/ -1, &count)); + ASSERT_EQ(-1, count); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"some text\n\xFFFC\n"); + + ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_End, TextUnit_Character, /*count*/ -2, &count)); + ASSERT_EQ(-2, count); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"some text\n"); + + // Tests for TextUnit_Word. + ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_End, TextUnit_Word, /*count*/ 1, &count)); + ASSERT_EQ(1, count); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"some text\n\xFFFC\n"); + + ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_End, TextUnit_Word, /*count*/ 1, &count)); + ASSERT_EQ(1, count); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"some text\n\xFFFC\nmore "); + + ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_End, TextUnit_Word, /*count*/ -1, &count)); + ASSERT_EQ(-1, count); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"some text\n\xFFFC\n"); + + ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_End, TextUnit_Word, /*count*/ -1, &count)); + ASSERT_EQ(-1, count); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"some text\n"); + + // Tests for TextUnit_Line. + ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_End, TextUnit_Line, /*count*/ 1, &count)); + ASSERT_EQ(1, count); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"some text\n\xFFFC"); + + ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_End, TextUnit_Line, /*count*/ 1, &count)); + ASSERT_EQ(1, count); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"some text\n\xFFFC\nmore text"); + + ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_End, TextUnit_Line, /*count*/ -1, &count)); + ASSERT_EQ(-1, count); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"some text\n\xFFFC"); + + ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_End, TextUnit_Line, /*count*/ -1, &count)); + ASSERT_EQ(-1, count); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"some text"); +} + +// TODO(schectman) https://github.com/flutter/flutter/issues/117012 +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderMoveEndpointByFormat) { + Init(BuildAXTreeForMoveByFormat()); + AXNode* root_node = GetRootAsAXNode(); + + ComPtr text_range_provider; + GetTextRangeProviderFromTextNode(text_range_provider, root_node); + + EXPECT_UIA_TEXTRANGE_EQ( + text_range_provider, + L"Text with formatting\nStandalone line with no formatting\nbold " + L"text\nParagraph 1\nParagraph 2\nParagraph 3\nParagraph 4"); + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_End, TextUnit_Format, + /*count*/ -2, + /*expected_text*/ + L"Text with formatting\nStandalone line with no formatting\nbold " + L"text\nParagraph 1", + /*expected_count*/ -2); + + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_End, TextUnit_Format, + /*count*/ -1, + /*expected_text*/ + L"Text with formatting\nStandalone line with no formatting\nbold text", + /*expected_count*/ -1); + + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_End, TextUnit_Format, + /*count*/ -1, + /*expected_text*/ + L"Text with formatting\nStandalone line with no formatting\n", + /*expected_count*/ -1); + + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_End, TextUnit_Format, + /*count*/ -1, + /*expected_text*/ L"Text with formatting", + /*expected_count*/ -1); + + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_End, TextUnit_Format, + /*count*/ -1, + /*expected_text*/ L"", + /*expected_count*/ -1); + + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_End, TextUnit_Format, + /*count*/ 7, + /*expected_text*/ + L"Text with formatting\nStandalone line with no formatting\nbold " + L"text\nParagraph 1\nParagraph 2\nParagraph 3\nParagraph 4", + /*expected_count*/ 6); + + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_End, TextUnit_Format, + /*count*/ -8, + /*expected_text*/ L"", + /*expected_count*/ -6); +} + +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderCompare) { + Init(BuildTextDocument({"some text", "some text"})); + AXNode* root_node = GetRootAsAXNode(); + + // Get the textRangeProvider for the document, + // which contains text "some textsome text". + ComPtr document_text_range_provider; + GetTextRangeProviderFromTextNode(document_text_range_provider, root_node); + + // Get the textRangeProvider for the first text node. + ComPtr text_range_provider; + GetTextRangeProviderFromTextNode(text_range_provider, + root_node->children()[0]); + + // Get the textRangeProvider for the second text node. + ComPtr more_text_range_provider; + GetTextRangeProviderFromTextNode(more_text_range_provider, + root_node->children()[1]); + + // Compare text range of the entire document with itself, which should return + // that they are equal. + BOOL result; + EXPECT_HRESULT_SUCCEEDED(document_text_range_provider->Compare( + document_text_range_provider.Get(), &result)); + EXPECT_TRUE(result); + + // Compare the text range of the entire document with one of its child, which + // should return that they are not equal. + EXPECT_HRESULT_SUCCEEDED(document_text_range_provider->Compare( + text_range_provider.Get(), &result)); + EXPECT_FALSE(result); + + // Compare the text range of text_node which contains "some text" with + // text range of more_text_node which also contains "some text". Those two + // text ranges should not equal, because their endpoints are different, even + // though their contents are the same. + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->Compare(more_text_range_provider.Get(), &result)); + EXPECT_FALSE(result); +} + +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderSelection) { + Init(BuildTextDocument({"some text"})); + + ComPtr text_range_provider; + GetTextRangeProviderFromTextNode(text_range_provider, GetRootAsAXNode()); + + ASSERT_UIA_INVALIDOPERATION(text_range_provider->AddToSelection()); + ASSERT_UIA_INVALIDOPERATION(text_range_provider->RemoveFromSelection()); +} + +// TODO(schectman) Rectangles not implemented as in Chromium. +// https://github.com/flutter/flutter/issues/117012 +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderGetBoundingRectangles) { + ui::AXTreeUpdate update = BuildAXTreeForBoundingRectangles(); + Init(update); + ComPtr text_range_provider; + base::win::ScopedSafearray rectangles; + int units_moved; + + // Expected bounding rects: + // Line 1
Line 2 + // |---------------------||---------------------||----| |------| + GetTextRangeProviderFromTextNode(text_range_provider, GetRootAsAXNode()); + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetBoundingRectangles(rectangles.Receive())); + std::vector expected_values = {20, 20, 200, 30, /* button */ + 20, 50, 200, 30, /* check box */ + 220, 20, 30, 30, /* line 1 */ + 220, 50, 42, 30 /* line 2 */}; + EXPECT_UIA_SAFEARRAY_EQ(rectangles.Get(), expected_values); + rectangles.Reset(); + + // Move the text range end back by one character. + // Expected bounding rects: + // Line 1
Line 2 + // |---------------------||---------------------||----| |----| + ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_End, TextUnit_Character, /*count*/ -1, + &units_moved)); + ASSERT_EQ(-1, units_moved); + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetBoundingRectangles(rectangles.Receive())); + expected_values = {20, 20, 200, 30, /* button */ + 20, 50, 200, 30, /* check box */ + 220, 20, 30, 30, /* line 1 */ + 220, 50, 35, 30 /* line 2 */}; + EXPECT_UIA_SAFEARRAY_EQ(rectangles.Get(), expected_values); + rectangles.Reset(); + + // Move the text range end back by one line. + // Expected bounding rects: + // Line 1
Line 2 + // |---------------------||---------------------||--------| + ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_End, TextUnit_Line, /*count*/ -1, &units_moved)); + ASSERT_EQ(-1, units_moved); + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetBoundingRectangles(rectangles.Receive())); + expected_values = {20, 20, 200, 30, /* button */ + 20, 50, 200, 30, /* check box */ + 220, 20, 30, 30 /* line 1 */}; + EXPECT_UIA_SAFEARRAY_EQ(rectangles.Get(), expected_values); + rectangles.Reset(); + + // Move the text range end back by one line. + // Expected bounding rects: + // Line 1
Line 2 + // |---------------------||---------------------| + ASSERT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByUnit( + TextPatternRangeEndpoint_End, TextUnit_Word, /*count*/ -3, &units_moved)); + ASSERT_EQ(-3, units_moved); + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetBoundingRectangles(rectangles.Receive())); + expected_values = {20, 20, 200, 30, /* button */ + 20, 50, 200, 30 /* check box */}; + EXPECT_UIA_SAFEARRAY_EQ(rectangles.Get(), expected_values); +} + +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderGetEnclosingElement) { + // Set up ax tree with the following structure: + // + // root + // | + // paragraph______________________________________________ + // | | | | | + // static_text link link search input pdf_highlight + // | | | | | + // text_node static_text ul text_node static_text + // | | | + // text_node li text_node + // | + // static_text + // | + // text_node + + ui::AXNodeData root_data; + root_data.id = 1; + root_data.role = ax::mojom::Role::kRootWebArea; + + ui::AXNodeData paragraph_data; + paragraph_data.id = 2; + paragraph_data.role = ax::mojom::Role::kParagraph; + root_data.child_ids.push_back(paragraph_data.id); + + ui::AXNodeData static_text_data1; + static_text_data1.id = 3; + static_text_data1.role = ax::mojom::Role::kStaticText; + paragraph_data.child_ids.push_back(static_text_data1.id); + + ui::AXNodeData inline_text_data1; + inline_text_data1.id = 4; + inline_text_data1.role = ax::mojom::Role::kInlineTextBox; + static_text_data1.child_ids.push_back(inline_text_data1.id); + + ui::AXNodeData link_data; + link_data.id = 5; + link_data.role = ax::mojom::Role::kLink; + paragraph_data.child_ids.push_back(link_data.id); + + ui::AXNodeData static_text_data2; + static_text_data2.id = 6; + static_text_data2.role = ax::mojom::Role::kStaticText; + link_data.child_ids.push_back(static_text_data2.id); + + ui::AXNodeData inline_text_data2; + inline_text_data2.id = 7; + inline_text_data2.role = ax::mojom::Role::kInlineTextBox; + static_text_data2.child_ids.push_back(inline_text_data2.id); + + ui::AXNodeData link_data2; + link_data2.id = 8; + link_data2.role = ax::mojom::Role::kLink; + paragraph_data.child_ids.push_back(link_data2.id); + + ui::AXNodeData list_data; + list_data.id = 9; + list_data.role = ax::mojom::Role::kList; + link_data2.child_ids.push_back(list_data.id); + + ui::AXNodeData list_item_data; + list_item_data.id = 10; + list_item_data.role = ax::mojom::Role::kListItem; + list_data.child_ids.push_back(list_item_data.id); + + ui::AXNodeData static_text_data3; + static_text_data3.id = 11; + static_text_data3.role = ax::mojom::Role::kStaticText; + list_item_data.child_ids.push_back(static_text_data3.id); + + ui::AXNodeData inline_text_data3; + inline_text_data3.id = 12; + inline_text_data3.role = ax::mojom::Role::kInlineTextBox; + static_text_data3.child_ids.push_back(inline_text_data3.id); + + ui::AXNodeData search_box; + search_box.id = 13; + search_box.role = ax::mojom::Role::kSearchBox; + search_box.AddState(ax::mojom::State::kEditable); + search_box.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, "input"); + search_box.AddStringAttribute(ax::mojom::StringAttribute::kInputType, + "search"); + paragraph_data.child_ids.push_back(search_box.id); + + ui::AXNodeData search_text; + search_text.id = 14; + search_text.role = ax::mojom::Role::kStaticText; + search_text.AddState(ax::mojom::State::kEditable); + search_text.SetName("placeholder"); + search_box.child_ids.push_back(search_text.id); + + ui::AXNodeData pdf_highlight_data; + pdf_highlight_data.id = 15; + pdf_highlight_data.role = ax::mojom::Role::kPdfActionableHighlight; + paragraph_data.child_ids.push_back(pdf_highlight_data.id); + + ui::AXNodeData static_text_data4; + static_text_data4.id = 16; + static_text_data4.role = ax::mojom::Role::kStaticText; + pdf_highlight_data.child_ids.push_back(static_text_data4.id); + + ui::AXNodeData inline_text_data4; + inline_text_data4.id = 17; + inline_text_data4.role = ax::mojom::Role::kInlineTextBox; + static_text_data4.child_ids.push_back(inline_text_data4.id); + + 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 = {root_data, paragraph_data, static_text_data1, + inline_text_data1, link_data, static_text_data2, + inline_text_data2, link_data2, list_data, + list_item_data, static_text_data3, inline_text_data3, + search_box, search_text, pdf_highlight_data, + static_text_data4, inline_text_data4}; + Init(update); + + // Set up variables from the tree for testing. + AXNode* paragraph_node = GetRootAsAXNode()->children()[0]; + AXNode* static_text_node1 = paragraph_node->children()[0]; + AXNode* link_node = paragraph_node->children()[1]; + AXNode* inline_text_node1 = static_text_node1->children()[0]; + AXNode* static_text_node2 = link_node->children()[0]; + AXNode* inline_text_node2 = static_text_node2->children()[0]; + AXNode* link_node2 = paragraph_node->children()[2]; + AXNode* list_node = link_node2->children()[0]; + AXNode* list_item_node = list_node->children()[0]; + AXNode* static_text_node3 = list_item_node->children()[0]; + AXNode* inline_text_node3 = static_text_node3->children()[0]; + AXNode* search_box_node = paragraph_node->children()[3]; + AXNode* search_text_node = search_box_node->children()[0]; + AXNode* pdf_highlight_node = paragraph_node->children()[4]; + AXNode* static_text_node4 = pdf_highlight_node->children()[0]; + AXNode* inline_text_node4 = static_text_node4->children()[0]; + AXPlatformNodeWin* owner = + static_cast(AXPlatformNodeFromNode(paragraph_node)); + ASSERT_NE(owner, nullptr); + + ComPtr link_node_raw = + QueryInterfaceFromNode(link_node); + ComPtr static_text_node_raw1 = + QueryInterfaceFromNode(static_text_node1); + ComPtr static_text_node_raw2 = + QueryInterfaceFromNode(static_text_node2); + ComPtr static_text_node_raw3 = + QueryInterfaceFromNode(static_text_node3); + ComPtr inline_text_node_raw1 = + QueryInterfaceFromNode(inline_text_node1); + ComPtr inline_text_node_raw2 = + QueryInterfaceFromNode(inline_text_node2); + ComPtr inline_text_node_raw3 = + QueryInterfaceFromNode(inline_text_node3); + ComPtr search_box_node_raw = + QueryInterfaceFromNode(search_box_node); + ComPtr search_text_node_raw = + QueryInterfaceFromNode(search_text_node); + ComPtr pdf_highlight_node_raw = + QueryInterfaceFromNode(pdf_highlight_node); + ComPtr inline_text_node_raw4 = + QueryInterfaceFromNode(inline_text_node4); + + // Test GetEnclosingElement for the two leaves text nodes. The enclosing + // element of the first one should be its static text parent (because inline + // text boxes shouldn't be exposed) and the enclosing element for the text + // node that is grandchild of the link node should return the link node. + // The text node in the link node with a complex subtree should behave + // normally and return the static text parent. + ComPtr text_provider; + EXPECT_HRESULT_SUCCEEDED(inline_text_node_raw1->GetPatternProvider( + UIA_TextPatternId, &text_provider)); + + ComPtr text_range_provider; + EXPECT_HRESULT_SUCCEEDED( + text_provider->get_DocumentRange(&text_range_provider)); + SetOwner(owner, text_range_provider.Get()); + + ComPtr enclosing_element; + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetEnclosingElement(&enclosing_element)); + EXPECT_EQ(inline_text_node_raw1.Get(), enclosing_element.Get()); + + EXPECT_HRESULT_SUCCEEDED(inline_text_node_raw2->GetPatternProvider( + UIA_TextPatternId, &text_provider)); + + EXPECT_HRESULT_SUCCEEDED( + text_provider->get_DocumentRange(&text_range_provider)); + SetOwner(owner, text_range_provider.Get()); + + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetEnclosingElement(&enclosing_element)); + EXPECT_EQ(link_node_raw.Get(), enclosing_element.Get()); + + EXPECT_HRESULT_SUCCEEDED(inline_text_node_raw3->GetPatternProvider( + UIA_TextPatternId, &text_provider)); + + EXPECT_HRESULT_SUCCEEDED( + text_provider->get_DocumentRange(&text_range_provider)); + SetOwner(owner, text_range_provider.Get()); + + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetEnclosingElement(&enclosing_element)); + EXPECT_EQ(inline_text_node_raw3.Get(), enclosing_element.Get()); + + // The enclosing element of a text range in the search text should give the + // search box + EXPECT_HRESULT_SUCCEEDED(search_text_node_raw->GetPatternProvider( + UIA_TextPatternId, &text_provider)); + + EXPECT_HRESULT_SUCCEEDED( + text_provider->get_DocumentRange(&text_range_provider)); + SetOwner(owner, text_range_provider.Get()); + + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->ExpandToEnclosingUnit(TextUnit_Character)); + + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetEnclosingElement(&enclosing_element)); + EXPECT_EQ(search_box_node_raw.Get(), enclosing_element.Get()); + + // The enclosing element for the text node that is grandchild of the + // pdf_highlight node should return the pdf_highlight node. + EXPECT_HRESULT_SUCCEEDED(inline_text_node_raw4->GetPatternProvider( + UIA_TextPatternId, &text_provider)); + + EXPECT_HRESULT_SUCCEEDED( + text_provider->get_DocumentRange(&text_range_provider)); + SetOwner(owner, text_range_provider.Get()); + + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetEnclosingElement(&enclosing_element)); + EXPECT_EQ(pdf_highlight_node_raw.Get(), enclosing_element.Get()); +} + +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderGetEnclosingElementRichButton) { + // Set up ax tree with the following structure: + // + // root + // ++button_1 + // ++++static_text_1 + // ++++++inline_text_1 + // ++button_2 + // ++++heading + // ++++++statix_text_2 + // ++++++++inline_text_2 + + ui::AXNodeData root; + ui::AXNodeData button_1; + ui::AXNodeData static_text_1; + ui::AXNodeData inline_text_1; + ui::AXNodeData button_2; + ui::AXNodeData heading; + ui::AXNodeData static_text_2; + ui::AXNodeData inline_text_2; + + root.id = 1; + button_1.id = 2; + static_text_1.id = 3; + inline_text_1.id = 4; + button_2.id = 5; + heading.id = 6; + static_text_2.id = 7; + inline_text_2.id = 8; + + root.role = ax::mojom::Role::kRootWebArea; + root.child_ids = {button_1.id, button_2.id}; + + button_1.role = ax::mojom::Role::kButton; + button_1.child_ids.push_back(static_text_1.id); + + static_text_1.role = ax::mojom::Role::kStaticText; + static_text_1.child_ids.push_back(inline_text_1.id); + + inline_text_1.role = ax::mojom::Role::kInlineTextBox; + + button_2.role = ax::mojom::Role::kButton; + button_2.child_ids.push_back(heading.id); + + heading.role = ax::mojom::Role::kHeading; + heading.child_ids.push_back(static_text_2.id); + + static_text_2.role = ax::mojom::Role::kStaticText; + static_text_2.child_ids.push_back(inline_text_2.id); + + inline_text_2.role = ax::mojom::Role::kInlineTextBox; + + 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.id; + update.nodes = {root, button_1, static_text_1, inline_text_1, + button_2, heading, static_text_2, inline_text_2}; + Init(update); + + // Set up variables from the tree for testing. + AXNode* button_1_node = GetRootAsAXNode()->children()[0]; + AXNode* static_text_1_node = button_1_node->children()[0]; + AXNode* inline_text_1_node = static_text_1_node->children()[0]; + AXNode* button_2_node = GetRootAsAXNode()->children()[1]; + AXNode* heading_node = button_2_node->children()[0]; + AXNode* static_text_2_node = heading_node->children()[0]; + AXNode* inline_text_2_node = static_text_2_node->children()[0]; + AXPlatformNodeWin* owner = + static_cast(AXPlatformNodeFromNode(button_1_node)); + ASSERT_NE(owner, nullptr); + + ComPtr button_1_node_raw = + QueryInterfaceFromNode(button_1_node); + ComPtr inline_text_1_node_raw = + QueryInterfaceFromNode(inline_text_1_node); + + ComPtr button_2_node_raw = + QueryInterfaceFromNode(button_2_node); + ComPtr static_text_2_node_raw = + QueryInterfaceFromNode(static_text_2_node); + ComPtr inline_text_2_node_raw = + QueryInterfaceFromNode(inline_text_2_node); + + // 1. The first button should hide its children since it contains a single + // text node. Thus, calling GetEnclosingElement on a descendant inline text + // box should return the button itself. + ComPtr text_provider; + EXPECT_HRESULT_SUCCEEDED(inline_text_1_node_raw->GetPatternProvider( + UIA_TextPatternId, &text_provider)); + + ComPtr text_range_provider; + EXPECT_HRESULT_SUCCEEDED( + text_provider->get_DocumentRange(&text_range_provider)); + SetOwner(owner, text_range_provider.Get()); + + ComPtr enclosing_element; + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetEnclosingElement(&enclosing_element)); + EXPECT_EQ(button_1_node_raw.Get(), enclosing_element.Get()); + + // 2. The second button shouldn't hide its children since it doesn't contain a + // single text node (it contains a heading node). Thus, calling + // GetEnclosingElement on a descendant inline text box should return the + // parent node. + EXPECT_HRESULT_SUCCEEDED(inline_text_2_node_raw->GetPatternProvider( + UIA_TextPatternId, &text_provider)); + + EXPECT_HRESULT_SUCCEEDED( + text_provider->get_DocumentRange(&text_range_provider)); + SetOwner(owner, text_range_provider.Get()); + + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetEnclosingElement(&enclosing_element)); + EXPECT_EQ(button_2_node_raw.Get(), enclosing_element.Get()); +} + +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderMoveEndpointByRange) { + Init(BuildTextDocument({"some text", "more text"})); + + AXNode* root_node = GetRootAsAXNode(); + AXNode* text_node = root_node->children()[0]; + AXNode* more_text_node = root_node->children()[1]; + AXPlatformNodeWin* owner = + static_cast(AXPlatformNodeFromNode(root_node)); + ASSERT_NE(owner, nullptr); + + // Text range for the document, which contains text "some textmore text". + ComPtr root_node_raw = + QueryInterfaceFromNode(root_node); + ComPtr document_provider; + EXPECT_HRESULT_SUCCEEDED( + root_node_raw->GetPatternProvider(UIA_TextPatternId, &document_provider)); + ComPtr document_text_range_provider; + ComPtr document_text_range; + + // Text range related to "some text". + ComPtr text_node_raw = + QueryInterfaceFromNode(text_node); + ComPtr text_provider; + EXPECT_HRESULT_SUCCEEDED( + text_node_raw->GetPatternProvider(UIA_TextPatternId, &text_provider)); + ComPtr text_range_provider; + ComPtr text_range; + + // Text range related to "more text". + ComPtr more_text_node_raw = + QueryInterfaceFromNode(more_text_node); + ComPtr more_text_provider; + EXPECT_HRESULT_SUCCEEDED(more_text_node_raw->GetPatternProvider( + UIA_TextPatternId, &more_text_provider)); + ComPtr more_text_range_provider; + ComPtr more_text_range; + + // Move the start of document text range "some textmore text" to the end of + // itself. + // The start of document text range "some textmore text" is at the end of + // itself. + // + // Before: + // |s e| + // "some textmore text" + // After: + // |s + // e| + // "some textmore text" + + // Get the textRangeProvider for the document, which contains text + // "some textmore text". + EXPECT_HRESULT_SUCCEEDED( + document_provider->get_DocumentRange(&document_text_range_provider)); + SetOwner(owner, document_text_range_provider.Get()); + + EXPECT_HRESULT_SUCCEEDED(document_text_range_provider->MoveEndpointByRange( + TextPatternRangeEndpoint_Start, document_text_range_provider.Get(), + TextPatternRangeEndpoint_End)); + + document_text_range_provider->QueryInterface( + IID_PPV_ARGS(&document_text_range)); + EXPECT_EQ(*GetStart(document_text_range.Get()), + *GetEnd(document_text_range.Get())); + + // Move the end of document text range "some textmore text" to the start of + // itself. + // The end of document text range "some textmore text" is at the start of + // itself. + // + // Before: + // |s e| + // "some textmore text" + // After: + // |s + // e| + // "some textmore text" + + // Get the textRangeProvider for the document, which contains text + // "some textmore text". + EXPECT_HRESULT_SUCCEEDED( + document_provider->get_DocumentRange(&document_text_range_provider)); + SetOwner(owner, document_text_range_provider.Get()); + + EXPECT_HRESULT_SUCCEEDED(document_text_range_provider->MoveEndpointByRange( + TextPatternRangeEndpoint_Start, document_text_range_provider.Get(), + TextPatternRangeEndpoint_End)); + + document_text_range_provider->QueryInterface( + IID_PPV_ARGS(&document_text_range)); + EXPECT_EQ(*GetStart(document_text_range.Get()), + *GetEnd(document_text_range.Get())); + + // Move the start of document text range "some textmore text" to the start + // of text range "more text". The start of document text range "some + // textmore text" is at the start of text range "more text". The end of + // document range does not change. + // + // Before: + // |s e| + // "some textmore text" + // After: + // |s e| + // "some textmore text" + + // Get the textRangeProvider for the document, which contains text + // "some textmore text". + EXPECT_HRESULT_SUCCEEDED( + document_provider->get_DocumentRange(&document_text_range_provider)); + SetOwner(owner, document_text_range_provider.Get()); + // Get the textRangeProvider for more_text_node which contains "more text". + EXPECT_HRESULT_SUCCEEDED( + more_text_provider->get_DocumentRange(&more_text_range_provider)); + SetOwner(owner, more_text_range_provider.Get()); + + EXPECT_HRESULT_SUCCEEDED(document_text_range_provider->MoveEndpointByRange( + TextPatternRangeEndpoint_Start, more_text_range_provider.Get(), + TextPatternRangeEndpoint_Start)); + + document_text_range_provider->QueryInterface( + IID_PPV_ARGS(&document_text_range)); + more_text_range_provider->QueryInterface(IID_PPV_ARGS(&more_text_range)); + EXPECT_EQ(*GetStart(document_text_range.Get()), + *GetStart(more_text_range.Get())); + + // Move the end of document text range "some textmore text" to the end of + // text range "some text". + // The end of document text range "some textmore text" is at the end of text + // range "some text". The start of document range does not change. + // + // Before: + // |s e| + // "some textmore text" + // After: + // |s e| + // "some textmore text" + + // Get the textRangeProvider for the document, which contains text + // "some textmore text". + EXPECT_HRESULT_SUCCEEDED( + document_provider->get_DocumentRange(&document_text_range_provider)); + SetOwner(owner, document_text_range_provider.Get()); + // Get the textRangeProvider for text_node which contains "some text". + EXPECT_HRESULT_SUCCEEDED( + text_provider->get_DocumentRange(&text_range_provider)); + SetOwner(owner, text_range_provider.Get()); + + EXPECT_HRESULT_SUCCEEDED(document_text_range_provider->MoveEndpointByRange( + TextPatternRangeEndpoint_End, text_range_provider.Get(), + TextPatternRangeEndpoint_End)); + + document_text_range_provider->QueryInterface( + IID_PPV_ARGS(&document_text_range)); + text_range_provider->QueryInterface(IID_PPV_ARGS(&text_range)); + EXPECT_EQ(*GetEnd(document_text_range.Get()), *GetEnd(text_range.Get())); + + // Move the end of text range "more text" to the start of + // text range "some text". Since the order of the endpoints being moved + // (those of "more text") have to be ensured, both endpoints of "more text" + // is at the start of "some text". + // + // Before: + // |s e| + // "some textmore text" + // After: + // e| + // |s + // "some textmore text" + + // Get the textRangeProvider for text_node which contains "some text". + EXPECT_HRESULT_SUCCEEDED( + text_provider->get_DocumentRange(&text_range_provider)); + SetOwner(owner, document_text_range_provider.Get()); + // Get the textRangeProvider for more_text_node which contains "more text". + EXPECT_HRESULT_SUCCEEDED( + more_text_provider->get_DocumentRange(&more_text_range_provider)); + SetOwner(owner, more_text_range_provider.Get()); + + EXPECT_HRESULT_SUCCEEDED(more_text_range_provider->MoveEndpointByRange( + TextPatternRangeEndpoint_End, text_range_provider.Get(), + TextPatternRangeEndpoint_Start)); + + text_range_provider->QueryInterface(IID_PPV_ARGS(&text_range)); + more_text_range_provider->QueryInterface(IID_PPV_ARGS(&more_text_range)); + EXPECT_EQ(*GetEnd(more_text_range.Get()), *GetStart(text_range.Get())); + EXPECT_EQ(*GetStart(more_text_range.Get()), *GetStart(text_range.Get())); + + // Move the start of text range "some text" to the end of text range + // "more text". Since the order of the endpoints being moved (those + // of "some text") have to be ensured, both endpoints of "some text" is at + // the end of "more text". + // + // Before: + // |s e| + // "some textmore text" + // After: + // |s + // e| + // "some textmore text" + + // Get the textRangeProvider for text_node which contains "some text". + EXPECT_HRESULT_SUCCEEDED( + text_provider->get_DocumentRange(&text_range_provider)); + SetOwner(owner, text_range_provider.Get()); + // Get the textRangeProvider for more_text_node which contains "more text". + EXPECT_HRESULT_SUCCEEDED( + more_text_provider->get_DocumentRange(&more_text_range_provider)); + SetOwner(owner, more_text_range_provider.Get()); + + EXPECT_HRESULT_SUCCEEDED(text_range_provider->MoveEndpointByRange( + TextPatternRangeEndpoint_Start, more_text_range_provider.Get(), + TextPatternRangeEndpoint_End)); + + text_range_provider->QueryInterface(IID_PPV_ARGS(&text_range)); + more_text_range_provider->QueryInterface(IID_PPV_ARGS(&more_text_range)); + EXPECT_EQ(*GetStart(text_range.Get()), *GetEnd(more_text_range.Get())); + EXPECT_EQ(*GetEnd(text_range.Get()), *GetEnd(more_text_range.Get())); +} + +// TODO(schectman) https://github.com/flutter/flutter/issues/117012 +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderGetAttributeValue) { + ui::AXNodeData text_data; + text_data.id = 2; + text_data.role = ax::mojom::Role::kStaticText; + text_data.AddStringAttribute(ax::mojom::StringAttribute::kFontFamily, "sans"); + text_data.AddFloatAttribute(ax::mojom::FloatAttribute::kFontSize, 16); + text_data.AddFloatAttribute(ax::mojom::FloatAttribute::kFontWeight, 300); + text_data.AddIntAttribute(ax::mojom::IntAttribute::kTextOverlineStyle, 1); + text_data.AddIntAttribute(ax::mojom::IntAttribute::kTextStrikethroughStyle, + 2); + text_data.AddIntAttribute(ax::mojom::IntAttribute::kTextUnderlineStyle, 3); + text_data.AddIntAttribute(ax::mojom::IntAttribute::kBackgroundColor, + 0xFFADBEEFU); + text_data.AddIntAttribute(ax::mojom::IntAttribute::kColor, 0xFFADC0DEU); + text_data.AddStringAttribute(ax::mojom::StringAttribute::kLanguage, "fr-CA"); + text_data.SetTextDirection(ax::mojom::WritingDirection::kRtl); + text_data.AddTextStyle(ax::mojom::TextStyle::kItalic); + text_data.SetTextPosition(ax::mojom::TextPosition::kSubscript); + text_data.SetRestriction(ax::mojom::Restriction::kReadOnly); + text_data.SetTextAlign(ax::mojom::TextAlign::kCenter); + text_data.AddIntListAttribute(ax::mojom::IntListAttribute::kMarkerTypes, + {(int)ax::mojom::MarkerType::kGrammar, + (int)ax::mojom::MarkerType::kSpelling}); + text_data.AddIntListAttribute(ax::mojom::IntListAttribute::kMarkerStarts, + {0, 5, 0, 14, 19}); + text_data.AddIntListAttribute(ax::mojom::IntListAttribute::kMarkerEnds, + {9, 9, 4, 18, 24}); + text_data.SetName("some text and some other text"); + + ui::AXNodeData heading_data; + heading_data.id = 3; + heading_data.role = ax::mojom::Role::kHeading; + heading_data.AddIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel, 6); + heading_data.AddIntAttribute(ax::mojom::IntAttribute::kBackgroundColor, + 0xFFADBEEFU); + heading_data.AddIntAttribute(ax::mojom::IntAttribute::kColor, 0xFFADC0DEU); + heading_data.SetTextDirection(ax::mojom::WritingDirection::kRtl); + heading_data.SetTextPosition(ax::mojom::TextPosition::kSuperscript); + heading_data.AddState(ax::mojom::State::kEditable); + heading_data.child_ids = {4}; + + ui::AXNodeData heading_text_data; + heading_text_data.id = 4; + heading_text_data.role = ax::mojom::Role::kStaticText; + heading_text_data.AddState(ax::mojom::State::kInvisible); + heading_text_data.AddIntAttribute(ax::mojom::IntAttribute::kBackgroundColor, + 0xFFADBEEFU); + heading_text_data.AddIntAttribute(ax::mojom::IntAttribute::kColor, + 0xFFADC0DEU); + heading_text_data.SetTextDirection(ax::mojom::WritingDirection::kRtl); + heading_text_data.SetTextPosition(ax::mojom::TextPosition::kSuperscript); + heading_text_data.AddState(ax::mojom::State::kEditable); + heading_text_data.SetTextAlign(ax::mojom::TextAlign::kJustify); + heading_text_data.AddIntListAttribute( + ax::mojom::IntListAttribute::kMarkerTypes, + {(int)ax::mojom::MarkerType::kSpelling}); + heading_text_data.AddIntListAttribute( + ax::mojom::IntListAttribute::kMarkerStarts, {5}); + heading_text_data.AddIntListAttribute( + ax::mojom::IntListAttribute::kMarkerEnds, {9}); + heading_text_data.SetName("more text"); + + ui::AXNodeData mark_data; + mark_data.id = 5; + mark_data.role = ax::mojom::Role::kMark; + mark_data.AddIntAttribute(ax::mojom::IntAttribute::kBackgroundColor, + 0xFFADBEEFU); + mark_data.AddIntAttribute(ax::mojom::IntAttribute::kColor, 0xFFADC0DEU); + mark_data.SetTextDirection(ax::mojom::WritingDirection::kRtl); + mark_data.child_ids = {6}; + + ui::AXNodeData mark_text_data; + mark_text_data.id = 6; + mark_text_data.role = ax::mojom::Role::kStaticText; + mark_text_data.AddIntAttribute(ax::mojom::IntAttribute::kBackgroundColor, + 0xFFADBEEFU); + mark_text_data.AddIntAttribute(ax::mojom::IntAttribute::kColor, 0xFFADC0DEU); + mark_text_data.SetTextDirection(ax::mojom::WritingDirection::kRtl); + mark_text_data.SetTextAlign(ax::mojom::TextAlign::kNone); + mark_text_data.SetName("marked text"); + + ui::AXNodeData list_data; + list_data.id = 7; + list_data.role = ax::mojom::Role::kList; + list_data.child_ids = {8, 10}; + list_data.AddIntAttribute(ax::mojom::IntAttribute::kBackgroundColor, + 0xFFADBEEFU); + list_data.AddIntAttribute(ax::mojom::IntAttribute::kColor, 0xFFADC0DEU); + + ui::AXNodeData list_item_data; + list_item_data.id = 8; + list_item_data.role = ax::mojom::Role::kListItem; + list_item_data.child_ids = {9}; + list_item_data.AddIntAttribute( + ax::mojom::IntAttribute::kListStyle, + static_cast(ax::mojom::ListStyle::kOther)); + list_item_data.AddIntAttribute(ax::mojom::IntAttribute::kBackgroundColor, + 0xFFADBEEFU); + list_item_data.AddIntAttribute(ax::mojom::IntAttribute::kColor, 0xFFADC0DEU); + + ui::AXNodeData list_item_text_data; + list_item_text_data.id = 9; + list_item_text_data.role = ax::mojom::Role::kStaticText; + list_item_text_data.AddIntAttribute(ax::mojom::IntAttribute::kBackgroundColor, + 0xFFADBEEFU); + list_item_text_data.AddIntAttribute(ax::mojom::IntAttribute::kColor, + 0xFFADC0DEU); + list_item_text_data.SetName("list item"); + + ui::AXNodeData list_item2_data; + list_item2_data.id = 10; + list_item2_data.role = ax::mojom::Role::kListItem; + list_item2_data.child_ids = {11}; + list_item2_data.AddIntAttribute( + ax::mojom::IntAttribute::kListStyle, + static_cast(ax::mojom::ListStyle::kDisc)); + list_item2_data.AddIntAttribute(ax::mojom::IntAttribute::kBackgroundColor, + 0xFFADBEEFU); + list_item2_data.AddIntAttribute(ax::mojom::IntAttribute::kColor, 0xFFADC0DEU); + + ui::AXNodeData list_item2_text_data; + list_item2_text_data.id = 11; + list_item2_text_data.role = ax::mojom::Role::kStaticText; + list_item2_text_data.AddIntAttribute( + ax::mojom::IntAttribute::kBackgroundColor, 0xFFADBEEFU); + list_item2_text_data.AddIntAttribute(ax::mojom::IntAttribute::kColor, + 0xFFADC0DEU); + list_item2_text_data.SetName("list item 2"); + + ui::AXNodeData input_text_data; + input_text_data.id = 12; + input_text_data.role = ax::mojom::Role::kTextField; + input_text_data.AddState(ax::mojom::State::kEditable); + input_text_data.AddIntAttribute( + ax::mojom::IntAttribute::kNameFrom, + static_cast(ax::mojom::NameFrom::kPlaceholder)); + input_text_data.AddStringAttribute(ax::mojom::StringAttribute::kPlaceholder, + "placeholder2"); + input_text_data.AddIntAttribute(ax::mojom::IntAttribute::kBackgroundColor, + 0xFFADBEEFU); + input_text_data.AddIntAttribute(ax::mojom::IntAttribute::kColor, 0xFFADC0DEU); + input_text_data.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, + "input"); + input_text_data.AddStringAttribute(ax::mojom::StringAttribute::kInputType, + "text"); + input_text_data.SetName("placeholder"); + input_text_data.child_ids = {13}; + + ui::AXNodeData placeholder_text_data; + placeholder_text_data.id = 13; + placeholder_text_data.role = ax::mojom::Role::kStaticText; + placeholder_text_data.AddIntAttribute( + ax::mojom::IntAttribute::kBackgroundColor, 0xFFADBEEFU); + placeholder_text_data.AddIntAttribute(ax::mojom::IntAttribute::kColor, + 0xFFADC0DEU); + placeholder_text_data.SetName("placeholder"); + + ui::AXNodeData input_text_data2; + input_text_data2.id = 14; + input_text_data2.role = ax::mojom::Role::kTextField; + input_text_data2.AddState(ax::mojom::State::kEditable); + input_text_data2.SetRestriction(ax::mojom::Restriction::kDisabled); + input_text_data2.AddStringAttribute(ax::mojom::StringAttribute::kPlaceholder, + "placeholder2"); + input_text_data2.AddIntAttribute(ax::mojom::IntAttribute::kBackgroundColor, + 0xFFADBEEFU); + input_text_data2.AddIntAttribute(ax::mojom::IntAttribute::kColor, + 0xFFADC0DEU); + input_text_data2.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, + "input"); + input_text_data2.AddStringAttribute(ax::mojom::StringAttribute::kInputType, + "text"); + input_text_data2.SetName("foo"); + input_text_data2.child_ids = {15}; + + ui::AXNodeData placeholder_text_data2; + placeholder_text_data2.id = 15; + placeholder_text_data2.role = ax::mojom::Role::kStaticText; + placeholder_text_data2.AddIntAttribute( + ax::mojom::IntAttribute::kBackgroundColor, 0xFFADBEEFU); + placeholder_text_data2.AddIntAttribute(ax::mojom::IntAttribute::kColor, + 0xFFADC0DEU); + placeholder_text_data2.SetName("placeholder2"); + + ui::AXNodeData link_data; + link_data.id = 16; + link_data.role = ax::mojom::Role::kLink; + link_data.AddIntAttribute(ax::mojom::IntAttribute::kBackgroundColor, + 0xFFADBEEFU); + link_data.AddIntAttribute(ax::mojom::IntAttribute::kColor, 0xFFADC0DEU); + + ui::AXNodeData link_text_data; + link_text_data.id = 17; + link_text_data.role = ax::mojom::Role::kStaticText; + link_text_data.AddIntAttribute(ax::mojom::IntAttribute::kBackgroundColor, + 0xFFADBEEFU); + link_text_data.AddIntAttribute(ax::mojom::IntAttribute::kColor, 0xFFADC0DEU); + link_data.child_ids = {17}; + + ui::AXNodeData root_data; + root_data.id = 1; + root_data.role = ax::mojom::Role::kRootWebArea; + root_data.child_ids = {2, 3, 5, 7, 12, 14, 16}; + + 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(heading_data); + update.nodes.push_back(heading_text_data); + update.nodes.push_back(mark_data); + update.nodes.push_back(mark_text_data); + update.nodes.push_back(list_data); + update.nodes.push_back(list_item_data); + update.nodes.push_back(list_item_text_data); + update.nodes.push_back(list_item2_data); + update.nodes.push_back(list_item2_text_data); + update.nodes.push_back(input_text_data); + update.nodes.push_back(placeholder_text_data); + update.nodes.push_back(input_text_data2); + update.nodes.push_back(placeholder_text_data2); + update.nodes.push_back(link_data); + update.nodes.push_back(link_text_data); + + Init(update); + + AXNode* root_node = GetRootAsAXNode(); + AXNode* text_node = root_node->children()[0]; + AXNode* heading_node = root_node->children()[1]; + AXNode* heading_text_node = heading_node->children()[0]; + AXNode* mark_node = root_node->children()[2]; + AXNode* mark_text_node = mark_node->children()[0]; + AXNode* list_node = root_node->children()[3]; + AXNode* list_item_node = list_node->children()[0]; + AXNode* list_item_text_node = list_item_node->children()[0]; + AXNode* list_item2_node = list_node->children()[1]; + AXNode* list_item2_text_node = list_item2_node->children()[0]; + AXNode* input_text_node = root_node->children()[4]; + AXNode* placeholder_text_node = input_text_node->children()[0]; + AXNode* input_text_node2 = root_node->children()[5]; + AXNode* placeholder_text_node2 = input_text_node2->children()[0]; + AXNode* link_node = root_node->children()[6]; + AXNode* link_text_node = link_node->children()[0]; + + ComPtr document_range_provider; + GetTextRangeProviderFromTextNode(document_range_provider, root_node); + ComPtr text_range_provider; + GetTextRangeProviderFromTextNode(text_range_provider, text_node); + ComPtr heading_text_range_provider; + GetTextRangeProviderFromTextNode(heading_text_range_provider, + heading_text_node); + ComPtr mark_text_range_provider; + GetTextRangeProviderFromTextNode(mark_text_range_provider, mark_text_node); + ComPtr list_item_text_range_provider; + GetTextRangeProviderFromTextNode(list_item_text_range_provider, + list_item_text_node); + ComPtr list_item2_text_range_provider; + GetTextRangeProviderFromTextNode(list_item2_text_range_provider, + list_item2_text_node); + + ComPtr placeholder_text_range_provider; + GetTextRangeProviderFromTextNode(placeholder_text_range_provider, + placeholder_text_node); + + ComPtr placeholder_text_range_provider2; + GetTextRangeProviderFromTextNode(placeholder_text_range_provider2, + placeholder_text_node2); + + ComPtr link_text_range_provider; + GetTextRangeProviderFromTextNode(link_text_range_provider, link_text_node); + + base::win::ScopedVariant expected_variant; + + // SkColor is ARGB, COLORREF is 0BGR + expected_variant.Set(static_cast(0x00EFBEADU)); + EXPECT_UIA_TEXTATTRIBUTE_EQ(text_range_provider, + UIA_BackgroundColorAttributeId, expected_variant); + // Important: all nodes need to have the kColor and kBackgroundColor attribute + // set for this test, otherwise the following assert will fail. + EXPECT_UIA_TEXTATTRIBUTE_EQ(document_range_provider, + UIA_BackgroundColorAttributeId, expected_variant); + expected_variant.Reset(); + + expected_variant.Set(static_cast(BulletStyle::BulletStyle_None)); + EXPECT_UIA_TEXTATTRIBUTE_EQ(list_item_text_range_provider, + UIA_BulletStyleAttributeId, expected_variant); + expected_variant.Reset(); + + expected_variant.Set( + static_cast(BulletStyle::BulletStyle_FilledRoundBullet)); + EXPECT_UIA_TEXTATTRIBUTE_EQ(list_item2_text_range_provider, + UIA_BulletStyleAttributeId, expected_variant); + expected_variant.Reset(); + + { + base::win::ScopedVariant lang_variant; + EXPECT_HRESULT_SUCCEEDED(text_range_provider->GetAttributeValue( + UIA_CultureAttributeId, lang_variant.Receive())); + + EXPECT_EQ(lang_variant.type(), VT_I4); + const LCID lcid = V_I4(lang_variant.ptr()); + EXPECT_EQ(LANG_FRENCH, PRIMARYLANGID(lcid)); + EXPECT_EQ(SUBLANG_FRENCH_CANADIAN, SUBLANGID(lcid)); + EXPECT_EQ(SORT_DEFAULT, SORTIDFROMLCID(lcid)); + } + + std::wstring font_name = L"sans"; + expected_variant.Set(SysAllocString(font_name.c_str())); + EXPECT_UIA_TEXTATTRIBUTE_EQ(text_range_provider, UIA_FontNameAttributeId, + expected_variant); + expected_variant.Reset(); + + expected_variant.Set(12.0); + EXPECT_UIA_TEXTATTRIBUTE_EQ(text_range_provider, UIA_FontSizeAttributeId, + expected_variant); + expected_variant.Reset(); + + expected_variant.Set(300); + EXPECT_UIA_TEXTATTRIBUTE_EQ(text_range_provider, UIA_FontWeightAttributeId, + expected_variant); + expected_variant.Reset(); + + // SkColor is ARGB, COLORREF is 0BGR + expected_variant.Set(static_cast(0x00DEC0ADU)); + EXPECT_UIA_TEXTATTRIBUTE_EQ(text_range_provider, + UIA_ForegroundColorAttributeId, expected_variant); + EXPECT_UIA_TEXTATTRIBUTE_EQ(document_range_provider, + UIA_ForegroundColorAttributeId, expected_variant); + expected_variant.Reset(); + + expected_variant.Set(false); + EXPECT_UIA_TEXTATTRIBUTE_EQ(text_range_provider, UIA_IsHiddenAttributeId, + expected_variant); + expected_variant.Reset(); + + EXPECT_UIA_TEXTATTRIBUTE_MIXED(document_range_provider, + UIA_IsHiddenAttributeId); + + expected_variant.Set(true); + EXPECT_UIA_TEXTATTRIBUTE_EQ(text_range_provider, UIA_IsItalicAttributeId, + expected_variant); + expected_variant.Reset(); + + expected_variant.Set(false); + EXPECT_UIA_TEXTATTRIBUTE_EQ(heading_text_range_provider, + UIA_IsItalicAttributeId, expected_variant); + expected_variant.Reset(); + + expected_variant.Set(true); + EXPECT_UIA_TEXTATTRIBUTE_EQ(text_range_provider, UIA_IsReadOnlyAttributeId, + expected_variant); + expected_variant.Reset(); + + expected_variant.Set(false); + EXPECT_UIA_TEXTATTRIBUTE_EQ(heading_text_range_provider, + UIA_IsReadOnlyAttributeId, expected_variant); + expected_variant.Reset(); + + expected_variant.Set(false); + EXPECT_UIA_TEXTATTRIBUTE_EQ(placeholder_text_range_provider, + UIA_IsReadOnlyAttributeId, expected_variant); + expected_variant.Reset(); + + expected_variant.Set(true); + EXPECT_UIA_TEXTATTRIBUTE_EQ(placeholder_text_range_provider2, + UIA_IsReadOnlyAttributeId, expected_variant); + expected_variant.Reset(); + + expected_variant.Set(true); + EXPECT_UIA_TEXTATTRIBUTE_EQ(link_text_range_provider, + UIA_IsReadOnlyAttributeId, expected_variant); + expected_variant.Reset(); + + expected_variant.Set(HorizontalTextAlignment_Centered); + EXPECT_UIA_TEXTATTRIBUTE_EQ(text_range_provider, + UIA_HorizontalTextAlignmentAttributeId, + expected_variant); + expected_variant.Reset(); + + expected_variant.Set(HorizontalTextAlignment_Justified); + EXPECT_UIA_TEXTATTRIBUTE_EQ(heading_text_range_provider, + UIA_HorizontalTextAlignmentAttributeId, + expected_variant); + expected_variant.Reset(); + + expected_variant.Set(true); + EXPECT_UIA_TEXTATTRIBUTE_EQ(text_range_provider, UIA_IsSubscriptAttributeId, + expected_variant); + expected_variant.Reset(); + + expected_variant.Set(false); + EXPECT_UIA_TEXTATTRIBUTE_EQ(heading_text_range_provider, + UIA_IsSubscriptAttributeId, expected_variant); + expected_variant.Reset(); + + expected_variant.Set(false); + EXPECT_UIA_TEXTATTRIBUTE_EQ(text_range_provider, UIA_IsSuperscriptAttributeId, + expected_variant); + expected_variant.Reset(); + + expected_variant.Set(true); + EXPECT_UIA_TEXTATTRIBUTE_EQ(heading_text_range_provider, + UIA_IsSuperscriptAttributeId, expected_variant); + expected_variant.Reset(); + + expected_variant.Set(TextDecorationLineStyle::TextDecorationLineStyle_Dot); + EXPECT_UIA_TEXTATTRIBUTE_EQ(text_range_provider, UIA_OverlineStyleAttributeId, + expected_variant); + expected_variant.Reset(); + + expected_variant.Set(TextDecorationLineStyle::TextDecorationLineStyle_Dash); + EXPECT_UIA_TEXTATTRIBUTE_EQ( + text_range_provider, UIA_StrikethroughStyleAttributeId, expected_variant); + expected_variant.Reset(); + + expected_variant.Set(TextDecorationLineStyle::TextDecorationLineStyle_Single); + EXPECT_UIA_TEXTATTRIBUTE_EQ(text_range_provider, + UIA_UnderlineStyleAttributeId, expected_variant); + expected_variant.Reset(); + + std::wstring style_name; + expected_variant.Set(SysAllocString(style_name.c_str())); + EXPECT_UIA_TEXTATTRIBUTE_EQ(text_range_provider, UIA_StyleNameAttributeId, + expected_variant); + expected_variant.Reset(); + + expected_variant.Set(static_cast(StyleId_Heading6)); + EXPECT_UIA_TEXTATTRIBUTE_EQ(heading_text_range_provider, + UIA_StyleIdAttributeId, expected_variant); + expected_variant.Reset(); + + style_name = L"mark"; + expected_variant.Set(SysAllocString(style_name.c_str())); + EXPECT_UIA_TEXTATTRIBUTE_EQ(mark_text_range_provider, + UIA_StyleNameAttributeId, expected_variant); + expected_variant.Reset(); + + expected_variant.Set(static_cast(StyleId_NumberedList)); + EXPECT_UIA_TEXTATTRIBUTE_EQ(list_item_text_range_provider, + UIA_StyleIdAttributeId, expected_variant); + expected_variant.Reset(); + + expected_variant.Set(static_cast(StyleId_BulletedList)); + EXPECT_UIA_TEXTATTRIBUTE_EQ(list_item2_text_range_provider, + UIA_StyleIdAttributeId, expected_variant); + expected_variant.Reset(); + + expected_variant.Set( + static_cast(FlowDirections::FlowDirections_RightToLeft)); + EXPECT_UIA_TEXTATTRIBUTE_EQ( + text_range_provider, UIA_TextFlowDirectionsAttributeId, expected_variant); + EXPECT_UIA_TEXTATTRIBUTE_MIXED(document_range_provider, + UIA_TextFlowDirectionsAttributeId); + expected_variant.Reset(); + + // Move the start endpoint back and forth one character to force such endpoint + // to be located at the end of the previous anchor, this shouldn't cause + // GetAttributeValue to include the previous anchor's attributes. + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT(mark_text_range_provider, + TextPatternRangeEndpoint_Start, + TextUnit_Character, + /*count*/ -1, + /*expected_text*/ L"tmarked text", + /*expected_count*/ -1); + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT(mark_text_range_provider, + TextPatternRangeEndpoint_Start, + TextUnit_Character, + /*count*/ 1, + /*expected_text*/ L"marked text", + /*expected_count*/ 1); + expected_variant.Set(false); + EXPECT_UIA_TEXTATTRIBUTE_EQ(mark_text_range_provider, + UIA_IsSuperscriptAttributeId, expected_variant); + expected_variant.Reset(); + + // Same idea as above, but moving forth and back the end endpoint to force it + // to be located at the start of the next anchor. + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT(mark_text_range_provider, + TextPatternRangeEndpoint_End, + TextUnit_Character, + /*count*/ 1, + /*expected_text*/ L"marked textl", + /*expected_count*/ 1); + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT(mark_text_range_provider, + TextPatternRangeEndpoint_End, + TextUnit_Character, + /*count*/ -1, + /*expected_text*/ L"marked text", + /*expected_count*/ -1); + expected_variant.Set( + static_cast(FlowDirections::FlowDirections_RightToLeft)); + EXPECT_UIA_TEXTATTRIBUTE_EQ(mark_text_range_provider, + UIA_TextFlowDirectionsAttributeId, + expected_variant); + expected_variant.Reset(); + + { + // |text_node| has a grammar error on "some text", a highlight for the + // first word, a spelling error for the second word, a "spelling-error" + // highlight for the fourth word, and a "grammar-error" highlight for the + // fifth word. So the range has mixed annotations. + EXPECT_UIA_TEXTATTRIBUTE_MIXED(text_range_provider, + UIA_AnnotationTypesAttributeId); + + // Testing annotations in range [5,9) + // start: TextPosition, anchor_id=2, text_offset=5, + // annotated_text=some ext and some other text + // end : TextPosition, anchor_id=2, text_offset=9, + // annotated_text=some text<> and some other text + AXPlatformNodeWin* owner = + static_cast(AXPlatformNodeFromNode(text_node)); + ComPtr range_with_annotations; + CreateTextRangeProviderWin( + range_with_annotations, owner, + /*start_anchor=*/text_node, /*start_offset=*/5, + /*start_affinity*/ ax::mojom::TextAffinity::kDownstream, + /*end_anchor=*/text_node, /*end_offset=*/9, + /*end_affinity*/ ax::mojom::TextAffinity::kDownstream); + + base::win::ScopedVariant annotation_types_variant; + EXPECT_HRESULT_SUCCEEDED(range_with_annotations->GetAttributeValue( + UIA_AnnotationTypesAttributeId, annotation_types_variant.Receive())); + + EXPECT_EQ(annotation_types_variant.type(), VT_ARRAY | VT_I4); + std::vector expected_annotations = {AnnotationType_SpellingError, + AnnotationType_GrammarError}; + EXPECT_UIA_SAFEARRAY_EQ(V_ARRAY(annotation_types_variant.ptr()), + expected_annotations); + } + + { + // Testing annotations in range [0,4) + // start: TextPosition, anchor_id=2, text_offset=0, + // annotated_text=ome text and some other text + // end : TextPosition, anchor_id=2, text_offset=4, + // annotated_text=some<> text and some other text + AXPlatformNodeWin* owner = + static_cast(AXPlatformNodeFromNode(text_node)); + ComPtr range_with_annotations; + CreateTextRangeProviderWin( + range_with_annotations, owner, + /*start_anchor=*/text_node, /*start_offset=*/0, + /*start_affinity*/ ax::mojom::TextAffinity::kDownstream, + /*end_anchor=*/text_node, /*end_offset=*/4, + /*end_affinity*/ ax::mojom::TextAffinity::kDownstream); + + base::win::ScopedVariant annotation_types_variant; + EXPECT_HRESULT_SUCCEEDED(range_with_annotations->GetAttributeValue( + UIA_AnnotationTypesAttributeId, annotation_types_variant.Receive())); + + EXPECT_EQ(annotation_types_variant.type(), VT_ARRAY | VT_I4); + std::vector expected_annotations = {AnnotationType_GrammarError, + AnnotationType_Highlighted}; + EXPECT_UIA_SAFEARRAY_EQ(V_ARRAY(annotation_types_variant.ptr()), + expected_annotations); + } + + { + // Testing annotations in range [14,18) + // start: TextPosition, anchor_id=2, text_offset=14, + // annotated_text=some text and ome other text + // end : TextPosition, anchor_id=2, text_offset=18, + // annotated_text=some text and some<> other text + AXPlatformNodeWin* owner = + static_cast(AXPlatformNodeFromNode(text_node)); + ComPtr range_with_annotations; + CreateTextRangeProviderWin( + range_with_annotations, owner, + /*start_anchor=*/text_node, /*start_offset=*/14, + /*start_affinity*/ ax::mojom::TextAffinity::kDownstream, + /*end_anchor=*/text_node, /*end_offset=*/18, + /*end_affinity*/ ax::mojom::TextAffinity::kDownstream); + + base::win::ScopedVariant annotation_types_variant; + EXPECT_HRESULT_SUCCEEDED(range_with_annotations->GetAttributeValue( + UIA_AnnotationTypesAttributeId, annotation_types_variant.Receive())); + + EXPECT_EQ(annotation_types_variant.type(), VT_ARRAY | VT_I4); + std::vector expected_annotations = {AnnotationType_SpellingError}; + EXPECT_UIA_SAFEARRAY_EQ(V_ARRAY(annotation_types_variant.ptr()), + expected_annotations); + } + + { + // Testing annotations in range [19,24) + // start: TextPosition, anchor_id=2, text_offset=19, + // annotated_text=some text and some ther text + // end : TextPosition, anchor_id=2, text_offset=24, + // annotated_text=some text and some other<> text + AXPlatformNodeWin* owner = + static_cast(AXPlatformNodeFromNode(text_node)); + ComPtr range_with_annotations; + CreateTextRangeProviderWin( + range_with_annotations, owner, + /*start_anchor=*/text_node, /*start_offset=*/19, + /*start_affinity*/ ax::mojom::TextAffinity::kDownstream, + /*end_anchor=*/text_node, /*end_offset=*/24, + /*end_affinity*/ ax::mojom::TextAffinity::kDownstream); + + base::win::ScopedVariant annotation_types_variant; + EXPECT_HRESULT_SUCCEEDED(range_with_annotations->GetAttributeValue( + UIA_AnnotationTypesAttributeId, annotation_types_variant.Receive())); + + EXPECT_EQ(annotation_types_variant.type(), VT_ARRAY | VT_I4); + std::vector expected_annotations = {AnnotationType_GrammarError}; + EXPECT_UIA_SAFEARRAY_EQ(V_ARRAY(annotation_types_variant.ptr()), + expected_annotations); + } + + { + // |heading_text_node| has a a spelling error for one word, and no + // annotations for the remaining text, so the range has mixed annotations. + EXPECT_UIA_TEXTATTRIBUTE_MIXED(heading_text_range_provider, + UIA_AnnotationTypesAttributeId); + + // start: TextPosition, anchor_id=4, text_offset=5, + // annotated_text=more ext + // end : TextPosition, anchor_id=4, text_offset=9, + // annotated_text=more text<> + AXPlatformNodeWin* owner = static_cast( + AXPlatformNodeFromNode(heading_text_node)); + ComPtr range_with_annotations; + CreateTextRangeProviderWin( + range_with_annotations, owner, + /*start_anchor=*/heading_text_node, /*start_offset=*/5, + /*start_affinity*/ ax::mojom::TextAffinity::kDownstream, + /*end_anchor=*/heading_text_node, /*end_offset=*/9, + /*end_affinity*/ ax::mojom::TextAffinity::kDownstream); + + base::win::ScopedVariant annotation_types_variant; + EXPECT_HRESULT_SUCCEEDED(range_with_annotations->GetAttributeValue( + UIA_AnnotationTypesAttributeId, annotation_types_variant.Receive())); + + std::vector expected_annotations = {AnnotationType_SpellingError}; + EXPECT_UIA_SAFEARRAY_EQ(V_ARRAY(annotation_types_variant.ptr()), + expected_annotations); + } + + { + base::win::ScopedVariant empty_variant; + EXPECT_UIA_TEXTATTRIBUTE_EQ(mark_text_range_provider, + UIA_AnnotationTypesAttributeId, empty_variant); + } +} + +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderGetAttributeValueAnnotationObjects) { + // rootWebArea id=1 + // ++mark id=2 detailsIds=comment1 comment2 highlighted + // ++++staticText id=3 name="some text" + // ++comment id=4 name="comment 1" + // ++++staticText id=5 name="comment 1" + // ++comment id=6 name="comment 2" + // ++++staticText id=7 name="comment 2" + // ++mark id=8 name="highlighted" + // ++++staticText id=9 name="highlighted" + + AXNodeData root; + AXNodeData annotation_target; + AXNodeData some_text; + AXNodeData comment1; + AXNodeData comment1_text; + AXNodeData comment2; + AXNodeData comment2_text; + AXNodeData highlighted; + AXNodeData highlighted_text; + + root.id = 1; + annotation_target.id = 2; + some_text.id = 3; + comment1.id = 4; + comment1_text.id = 5; + comment2.id = 6; + comment2_text.id = 7; + highlighted.id = 8; + highlighted_text.id = 9; + + root.role = ax::mojom::Role::kRootWebArea; + root.SetName("root"); + root.child_ids = {annotation_target.id, comment1.id, comment2.id, + highlighted.id}; + + annotation_target.role = ax::mojom::Role::kMark; + annotation_target.child_ids = {some_text.id}; + annotation_target.AddIntListAttribute( + ax::mojom::IntListAttribute::kDetailsIds, + {comment1.id, comment2.id, highlighted.id}); + + some_text.role = ax::mojom::Role::kStaticText; + some_text.SetName("some text"); + + comment1.role = ax::mojom::Role::kComment; + comment1.SetName("comment 1"); + comment1.child_ids = {comment1_text.id}; + + comment1_text.role = ax::mojom::Role::kStaticText; + comment1_text.SetName("comment 1"); + + comment2.role = ax::mojom::Role::kComment; + comment2.SetName("comment 2"); + comment2.child_ids = {comment2_text.id}; + + comment2_text.role = ax::mojom::Role::kStaticText; + comment2_text.SetName("comment 2"); + + highlighted.role = ax::mojom::Role::kMark; + highlighted.SetName("highlighted"); + highlighted.child_ids = {highlighted_text.id}; + + highlighted_text.role = ax::mojom::Role::kStaticText; + highlighted_text.SetName("highlighted"); + + ui::AXTreeUpdate update; + update.has_tree_data = true; + update.root_id = root.id; + update.nodes = {root, annotation_target, some_text, + comment1, comment1_text, comment2, + comment2_text, highlighted, highlighted_text}; + update.tree_data.tree_id = ui::AXTreeID::CreateNewAXTreeID(); + + Init(update); + + AXNode* root_node = GetRootAsAXNode(); + AXNode* annotation_target_node = root_node->children()[0]; + AXNode* comment1_node = root_node->children()[1]; + AXNode* comment2_node = root_node->children()[2]; + AXNode* highlighted_node = root_node->children()[3]; + + ComPtr some_text_range_provider; + + // Create a text range encapsulates |annotation_target_node| with content + // "some text". + // start: TextPosition, anchor_id=2, text_offset=0, annotated_text=ome text + // end : TextPosition, anchor_id=2, text_offset=9, annotated_text=some text<> + AXPlatformNodeWin* owner = static_cast( + AXPlatformNodeFromNode(annotation_target_node)); + CreateTextRangeProviderWin( + some_text_range_provider, owner, + /*start_anchor=*/annotation_target_node, /*start_offset=*/0, + /*start_affinity*/ ax::mojom::TextAffinity::kDownstream, + /*end_anchor=*/annotation_target_node, /*end_offset=*/9, + /*end_affinity*/ ax::mojom::TextAffinity::kDownstream); + ASSERT_NE(nullptr, some_text_range_provider.Get()); + EXPECT_UIA_TEXTRANGE_EQ(some_text_range_provider, L"some text"); + + ComPtr comment1_provider = + QueryInterfaceFromNode(comment1_node); + ASSERT_NE(nullptr, comment1_provider.Get()); + ComPtr comment2_provider = + QueryInterfaceFromNode(comment2_node); + ASSERT_NE(nullptr, comment2_provider.Get()); + ComPtr highlighted_provider = + QueryInterfaceFromNode(highlighted_node); + ASSERT_NE(nullptr, highlighted_provider.Get()); + + ComPtr annotation_provider; + int annotation_type; + + // Validate |comment1_node| with Role::kComment supports IAnnotationProvider. + EXPECT_HRESULT_SUCCEEDED(comment1_provider->GetPatternProvider( + UIA_AnnotationPatternId, &annotation_provider)); + ASSERT_NE(nullptr, annotation_provider.Get()); + EXPECT_HRESULT_SUCCEEDED( + annotation_provider->get_AnnotationTypeId(&annotation_type)); + EXPECT_EQ(AnnotationType_Comment, annotation_type); + annotation_provider.Reset(); + + // Validate |comment2_node| with Role::kComment supports IAnnotationProvider. + EXPECT_HRESULT_SUCCEEDED(comment2_provider->GetPatternProvider( + UIA_AnnotationPatternId, &annotation_provider)); + ASSERT_NE(nullptr, annotation_provider.Get()); + EXPECT_HRESULT_SUCCEEDED( + annotation_provider->get_AnnotationTypeId(&annotation_type)); + EXPECT_EQ(AnnotationType_Comment, annotation_type); + annotation_provider.Reset(); + + // Validate |highlighted_node| with Role::kMark supports + // IAnnotationProvider. + EXPECT_HRESULT_SUCCEEDED(highlighted_provider->GetPatternProvider( + UIA_AnnotationPatternId, &annotation_provider)); + ASSERT_NE(nullptr, annotation_provider.Get()); + EXPECT_HRESULT_SUCCEEDED( + annotation_provider->get_AnnotationTypeId(&annotation_type)); + EXPECT_EQ(AnnotationType_Highlighted, annotation_type); + annotation_provider.Reset(); + + base::win::ScopedVariant annotation_objects_variant; + EXPECT_HRESULT_SUCCEEDED(some_text_range_provider->GetAttributeValue( + UIA_AnnotationObjectsAttributeId, annotation_objects_variant.Receive())); + EXPECT_EQ(VT_UNKNOWN | VT_ARRAY, annotation_objects_variant.type()); + + std::vector expected_names = {L"comment 1", L"comment 2", + L"highlighted"}; + EXPECT_UIA_ELEMENT_ARRAY_BSTR_EQ(V_ARRAY(annotation_objects_variant.ptr()), + UIA_NamePropertyId, expected_names); +} + +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderGetAttributeValueAnnotationObjectsMixed) { + // rootWebArea id=1 + // ++mark id=2 detailsIds=comment + // ++++staticText id=3 name="some text" + // ++staticText id=4 name="read only" restriction=readOnly + // ++comment id=5 name="comment 1" + // ++++staticText id=6 name="comment 1" + + AXNodeData root; + AXNodeData highlighted; + AXNodeData some_text; + AXNodeData readonly_text; + AXNodeData comment1; + AXNodeData comment1_text; + + root.id = 1; + highlighted.id = 2; + some_text.id = 3; + readonly_text.id = 4; + comment1.id = 5; + comment1_text.id = 6; + + root.role = ax::mojom::Role::kRootWebArea; + root.SetName("root"); + root.child_ids = {highlighted.id, readonly_text.id, comment1.id}; + + highlighted.role = ax::mojom::Role::kMark; + highlighted.child_ids = {some_text.id}; + highlighted.AddIntListAttribute(ax::mojom::IntListAttribute::kDetailsIds, + {comment1.id}); + + some_text.role = ax::mojom::Role::kStaticText; + some_text.SetName("some text"); + + readonly_text.role = ax::mojom::Role::kStaticText; + readonly_text.SetRestriction(ax::mojom::Restriction::kReadOnly); + readonly_text.SetName("read only"); + + comment1.role = ax::mojom::Role::kComment; + comment1.SetName("comment 1"); + comment1.child_ids = {comment1_text.id}; + + comment1_text.role = ax::mojom::Role::kStaticText; + comment1_text.SetName("comment 1"); + + ui::AXTreeUpdate update; + update.has_tree_data = true; + update.root_id = root.id; + update.nodes = {root, highlighted, some_text, + readonly_text, comment1, comment1_text}; + update.tree_data.tree_id = ui::AXTreeID::CreateNewAXTreeID(); + + Init(update); + + AXNode* root_node = GetRootAsAXNode(); + AXNode* highlighted_node = root_node->children()[0]; + AXNode* some_text_node = highlighted_node->children()[0]; + AXNode* readonly_text_node = root_node->children()[1]; + AXNode* comment1_node = root_node->children()[2]; + + // Create a text range encapsulates |highlighted_node| with content + // "some text". + // start: TextPosition, anchor_id=2, text_offset=0, annotated_text=ome text + // end : TextPosition, anchor_id=2, text_offset=9, annotated_text=some text<> + ComPtr some_text_range_provider; + AXPlatformNodeWin* owner = + static_cast(AXPlatformNodeFromNode(highlighted_node)); + CreateTextRangeProviderWin( + some_text_range_provider, owner, + /*start_anchor=*/highlighted_node, /*start_offset=*/0, + /*start_affinity*/ ax::mojom::TextAffinity::kDownstream, + /*end_anchor=*/highlighted_node, /*end_offset=*/9, + /*end_affinity*/ ax::mojom::TextAffinity::kDownstream); + ASSERT_NE(nullptr, some_text_range_provider.Get()); + EXPECT_UIA_TEXTRANGE_EQ(some_text_range_provider, L"some text"); + + ComPtr readonly_text_range_provider; + GetTextRangeProviderFromTextNode(readonly_text_range_provider, + readonly_text_node); + ASSERT_NE(nullptr, readonly_text_range_provider.Get()); + + ComPtr comment1_provider = + QueryInterfaceFromNode(comment1_node); + ASSERT_NE(nullptr, comment1_provider.Get()); + + ComPtr annotation_provider; + int annotation_type; + base::win::ScopedVariant expected_variant; + + // Validate |comment1_node| with Role::kComment supports IAnnotationProvider. + EXPECT_HRESULT_SUCCEEDED(comment1_provider->GetPatternProvider( + UIA_AnnotationPatternId, &annotation_provider)); + ASSERT_NE(nullptr, annotation_provider.Get()); + EXPECT_HRESULT_SUCCEEDED( + annotation_provider->get_AnnotationTypeId(&annotation_type)); + EXPECT_EQ(AnnotationType_Comment, annotation_type); + annotation_provider.Reset(); + + // Validate text range "some text" supports AnnotationObjectsAttribute. + EXPECT_HRESULT_SUCCEEDED(some_text_range_provider->GetAttributeValue( + UIA_AnnotationObjectsAttributeId, expected_variant.Receive())); + EXPECT_EQ(VT_UNKNOWN | VT_ARRAY, expected_variant.type()); + + std::vector expected_names = {L"comment 1"}; + EXPECT_UIA_ELEMENT_ARRAY_BSTR_EQ(V_ARRAY(expected_variant.ptr()), + UIA_NamePropertyId, expected_names); + expected_variant.Reset(); + + // Validate text range "read only" supports IsReadOnlyAttribute. + // Use IsReadOnly on text range "read only" as a second property in order to + // test the "mixed" property in the following section. + expected_variant.Set(true); + EXPECT_UIA_TEXTATTRIBUTE_EQ(readonly_text_range_provider, + UIA_IsReadOnlyAttributeId, expected_variant); + + // Validate text range "some textread only" returns mixed attribute. + // start: TextPosition, anchor_id=2, text_offset=0, annotated_text=ome text + // end : TextPosition, anchor_id=3, text_offset=9, annotated_text=read only<> + ComPtr mixed_text_range_provider; + CreateTextRangeProviderWin( + mixed_text_range_provider, owner, + /*start_anchor=*/some_text_node, /*start_offset=*/0, + /*start_affinity*/ ax::mojom::TextAffinity::kDownstream, + /*end_anchor=*/readonly_text_node, /*end_offset=*/9, + /*end_affinity*/ ax::mojom::TextAffinity::kDownstream); + + EXPECT_UIA_TEXTRANGE_EQ(mixed_text_range_provider, L"some textread only"); + EXPECT_UIA_TEXTATTRIBUTE_MIXED(mixed_text_range_provider, + UIA_AnnotationObjectsAttributeId); +} + +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderGetAttributeValueNotSupported) { + ui::AXNodeData root_data; + root_data.id = 1; + root_data.role = ax::mojom::Role::kRootWebArea; + + ui::AXNodeData text_data_first; + text_data_first.id = 2; + text_data_first.role = ax::mojom::Role::kStaticText; + text_data_first.SetName("first"); + root_data.child_ids.push_back(text_data_first.id); + + ui::AXNodeData text_data_second; + text_data_second.id = 3; + text_data_second.role = ax::mojom::Role::kStaticText; + text_data_second.SetName("second"); + root_data.child_ids.push_back(text_data_second.id); + + 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_first); + update.nodes.push_back(text_data_second); + + Init(update); + + ComPtr document_range_provider; + GetTextRangeProviderFromTextNode(document_range_provider, GetRootAsAXNode()); + + EXPECT_UIA_TEXTATTRIBUTE_NOTSUPPORTED(document_range_provider, + UIA_AfterParagraphSpacingAttributeId); + EXPECT_UIA_TEXTATTRIBUTE_NOTSUPPORTED(document_range_provider, + UIA_AnimationStyleAttributeId); + EXPECT_UIA_TEXTATTRIBUTE_NOTSUPPORTED(document_range_provider, + UIA_BeforeParagraphSpacingAttributeId); + EXPECT_UIA_TEXTATTRIBUTE_NOTSUPPORTED(document_range_provider, + UIA_CapStyleAttributeId); + EXPECT_UIA_TEXTATTRIBUTE_NOTSUPPORTED(document_range_provider, + UIA_CaretBidiModeAttributeId); + EXPECT_UIA_TEXTATTRIBUTE_NOTSUPPORTED(document_range_provider, + UIA_CaretPositionAttributeId); + EXPECT_UIA_TEXTATTRIBUTE_NOTSUPPORTED(document_range_provider, + UIA_IndentationFirstLineAttributeId); + EXPECT_UIA_TEXTATTRIBUTE_NOTSUPPORTED(document_range_provider, + UIA_IndentationLeadingAttributeId); + EXPECT_UIA_TEXTATTRIBUTE_NOTSUPPORTED(document_range_provider, + UIA_IndentationTrailingAttributeId); + EXPECT_UIA_TEXTATTRIBUTE_NOTSUPPORTED(document_range_provider, + UIA_IsActiveAttributeId); + EXPECT_UIA_TEXTATTRIBUTE_NOTSUPPORTED(document_range_provider, + UIA_LineSpacingAttributeId); + EXPECT_UIA_TEXTATTRIBUTE_NOTSUPPORTED(document_range_provider, + UIA_LinkAttributeId); + EXPECT_UIA_TEXTATTRIBUTE_NOTSUPPORTED(document_range_provider, + UIA_MarginBottomAttributeId); + EXPECT_UIA_TEXTATTRIBUTE_NOTSUPPORTED(document_range_provider, + UIA_MarginLeadingAttributeId); + EXPECT_UIA_TEXTATTRIBUTE_NOTSUPPORTED(document_range_provider, + UIA_MarginTopAttributeId); + EXPECT_UIA_TEXTATTRIBUTE_NOTSUPPORTED(document_range_provider, + UIA_MarginTrailingAttributeId); + EXPECT_UIA_TEXTATTRIBUTE_NOTSUPPORTED(document_range_provider, + UIA_OutlineStylesAttributeId); + EXPECT_UIA_TEXTATTRIBUTE_NOTSUPPORTED(document_range_provider, + UIA_OverlineColorAttributeId); + EXPECT_UIA_TEXTATTRIBUTE_NOTSUPPORTED(document_range_provider, + UIA_SelectionActiveEndAttributeId); + EXPECT_UIA_TEXTATTRIBUTE_NOTSUPPORTED(document_range_provider, + UIA_StrikethroughColorAttributeId); + EXPECT_UIA_TEXTATTRIBUTE_NOTSUPPORTED(document_range_provider, + UIA_TabsAttributeId); + EXPECT_UIA_TEXTATTRIBUTE_NOTSUPPORTED(document_range_provider, + UIA_UnderlineColorAttributeId); +} + +TEST_F( + AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderGetAttributeValueWithAncestorTextPosition) { + ui::AXTreeUpdate initial_state; + ui::AXTreeID tree_id = ui::AXTreeID::CreateNewAXTreeID(); + initial_state.tree_data.tree_id = tree_id; + initial_state.has_tree_data = true; + initial_state.root_id = 1; + initial_state.nodes.resize(5); + initial_state.nodes[0].id = 1; + initial_state.nodes[0].child_ids = {2}; + initial_state.nodes[0].role = ax::mojom::Role::kRootWebArea; + initial_state.nodes[1].id = 2; + initial_state.nodes[1].child_ids = {3}; + initial_state.nodes[1].role = ax::mojom::Role::kGenericContainer; + initial_state.nodes[2].id = 3; + initial_state.nodes[2].child_ids = {4, 5}; + initial_state.nodes[2].role = ax::mojom::Role::kGenericContainer; + initial_state.nodes[3].id = 4; + initial_state.nodes[3].role = ax::mojom::Role::kStaticText; + initial_state.nodes[3].SetName("some text"); + initial_state.nodes[3].AddIntAttribute( + ax::mojom::IntAttribute::kBackgroundColor, 0xFFADBEEFU); + initial_state.nodes[4].id = 5; + initial_state.nodes[4].role = ax::mojom::Role::kStaticText; + initial_state.nodes[4].SetName("more text"); + initial_state.nodes[4].AddIntAttribute( + ax::mojom::IntAttribute::kBackgroundColor, 0xFFADBEEFU); + + Init(initial_state); + const AXTree* tree = GetTree(); + const AXNode* some_text_node = tree->GetFromId(4); + const AXNode* more_text_node = tree->GetFromId(5); + + // Making |owner| AXID:2 so that |TestAXNodeWrapper::BuildAllWrappers| + // will build the entire subtree, and not only AXID:3 for example. + AXPlatformNodeWin* owner = static_cast( + AXPlatformNodeFromNode(GetNodeFromTree(tree_id, 2))); + + // start: TextPosition, anchor_id=4, text_offset=0, annotated_text=ome text + // end : TextPosition, anchor_id=5, text_offset=8, + // annotated_text=more tex + ComPtr text_range_provider_win; + CreateTextRangeProviderWin( + text_range_provider_win, owner, + /*start_anchor=*/some_text_node, /*start_offset=*/0, + /*start_affinity*/ ax::mojom::TextAffinity::kDownstream, + /*end_anchor=*/more_text_node, /*end_offset=*/8, + /*end_affinity*/ ax::mojom::TextAffinity::kDownstream); + + ASSERT_TRUE(GetStart(text_range_provider_win.Get())->IsTextPosition()); + ASSERT_EQ(4, GetStart(text_range_provider_win.Get())->anchor_id()); + ASSERT_EQ(0, GetStart(text_range_provider_win.Get())->text_offset()); + ASSERT_TRUE(GetEnd(text_range_provider_win.Get())->IsTextPosition()); + ASSERT_EQ(5, GetEnd(text_range_provider_win.Get())->anchor_id()); + ASSERT_EQ(8, GetEnd(text_range_provider_win.Get())->text_offset()); + + base::win::ScopedVariant expected_variant; + // SkColor is ARGB, COLORREF is 0BGR + expected_variant.Set(static_cast(0x00EFBEADU)); + EXPECT_UIA_TEXTATTRIBUTE_EQ(text_range_provider_win, + UIA_BackgroundColorAttributeId, expected_variant); +} + +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderSelect) { + Init(BuildTextDocument({"some text", "more text2"})); + AXNode* root_node = GetRootAsAXNode(); + + // Text range for the document, which contains text "some textmore text2". + ComPtr root_node_raw = + QueryInterfaceFromNode(root_node); + ComPtr document_provider; + ComPtr document_text_range_provider; + ComPtr document_text_range; + EXPECT_HRESULT_SUCCEEDED( + root_node_raw->GetPatternProvider(UIA_TextPatternId, &document_provider)); + EXPECT_HRESULT_SUCCEEDED( + document_provider->get_DocumentRange(&document_text_range_provider)); + document_text_range_provider->QueryInterface( + IID_PPV_ARGS(&document_text_range)); + AXPlatformNodeWin* owner_platform = + static_cast(AXPlatformNodeFromNode(root_node)); + ASSERT_NE(owner_platform, nullptr); + SetOwner(owner_platform, document_text_range_provider.Get()); + + // Text range related to "some text". + ComPtr text_range_provider; + GetTextRangeProviderFromTextNode(text_range_provider, + root_node->children()[0]); + ComPtr text_range; + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->QueryInterface(IID_PPV_ARGS(&text_range))); + + // Text range related to "more text2". + ComPtr more_text_range_provider; + GetTextRangeProviderFromTextNode(more_text_range_provider, + root_node->children()[1]); + SetOwner(owner_platform, more_text_range_provider.Get()); + ComPtr more_text_range; + more_text_range_provider->QueryInterface(IID_PPV_ARGS(&more_text_range)); + + AXPlatformNodeDelegate* delegate = + GetOwner(document_text_range.Get())->GetDelegate(); + + ComPtr selected_text_range_provider; + base::win::ScopedSafearray selection; + LONG index = 0; + LONG ubound; + LONG lbound; + + // Text range "some text" performs select. + { + text_range_provider->Select(); + + // Verify selection. + AXTree::Selection unignored_selection = delegate->GetUnignoredSelection(); + EXPECT_EQ(3, unignored_selection.anchor_object_id); + EXPECT_EQ(3, unignored_selection.focus_object_id); + EXPECT_EQ(0, unignored_selection.anchor_offset); + EXPECT_EQ(9, unignored_selection.focus_offset); + + // Verify the content of the selection. + document_provider->GetSelection(selection.Receive()); + ASSERT_NE(nullptr, selection.Get()); + + EXPECT_HRESULT_SUCCEEDED(SafeArrayGetUBound(selection.Get(), 1, &ubound)); + EXPECT_EQ(0, ubound); + EXPECT_HRESULT_SUCCEEDED(SafeArrayGetLBound(selection.Get(), 1, &lbound)); + EXPECT_EQ(0, lbound); + EXPECT_HRESULT_SUCCEEDED(SafeArrayGetElement( + selection.Get(), &index, + static_cast(&selected_text_range_provider))); + SetOwner(owner_platform, selected_text_range_provider.Get()); + EXPECT_UIA_TEXTRANGE_EQ(selected_text_range_provider, L"some text"); + + selected_text_range_provider.Reset(); + selection.Reset(); + } + + // Text range "more text2" performs select. + { + more_text_range_provider->Select(); + + // Verify selection + AXTree::Selection unignored_selection = delegate->GetUnignoredSelection(); + EXPECT_EQ(5, unignored_selection.anchor_object_id); + EXPECT_EQ(5, unignored_selection.focus_object_id); + EXPECT_EQ(0, unignored_selection.anchor_offset); + EXPECT_EQ(10, unignored_selection.focus_offset); + + // Verify the content of the selection. + document_provider->GetSelection(selection.Receive()); + ASSERT_NE(nullptr, selection.Get()); + + EXPECT_HRESULT_SUCCEEDED(SafeArrayGetUBound(selection.Get(), 1, &ubound)); + EXPECT_EQ(0, ubound); + EXPECT_HRESULT_SUCCEEDED(SafeArrayGetLBound(selection.Get(), 1, &lbound)); + EXPECT_EQ(0, lbound); + EXPECT_HRESULT_SUCCEEDED(SafeArrayGetElement( + selection.Get(), &index, + static_cast(&selected_text_range_provider))); + SetOwner(owner_platform, selected_text_range_provider.Get()); + EXPECT_UIA_TEXTRANGE_EQ(selected_text_range_provider, L"more text2"); + + selected_text_range_provider.Reset(); + selection.Reset(); + } + + // Document text range "some textmore text2" performs select. + { + document_text_range_provider->Select(); + + // Verify selection. + AXTree::Selection unignored_selection = delegate->GetUnignoredSelection(); + EXPECT_EQ(3, unignored_selection.anchor_object_id); + EXPECT_EQ(5, unignored_selection.focus_object_id); + EXPECT_EQ(0, unignored_selection.anchor_offset); + EXPECT_EQ(10, unignored_selection.focus_offset); + + // Verify the content of the selection. + document_provider->GetSelection(selection.Receive()); + ASSERT_NE(nullptr, selection.Get()); + + document_provider->GetSelection(selection.Receive()); + EXPECT_HRESULT_SUCCEEDED(SafeArrayGetUBound(selection.Get(), 1, &ubound)); + EXPECT_EQ(0, ubound); + EXPECT_HRESULT_SUCCEEDED(SafeArrayGetLBound(selection.Get(), 1, &lbound)); + EXPECT_EQ(0, lbound); + EXPECT_HRESULT_SUCCEEDED(SafeArrayGetElement( + selection.Get(), &index, + static_cast(&selected_text_range_provider))); + SetOwner(owner_platform, selected_text_range_provider.Get()); + EXPECT_UIA_TEXTRANGE_EQ(selected_text_range_provider, + L"some textmore text2"); + } + + // A degenerate text range performs select. + { + // Move the endpoint of text range so it becomes degenerate, then select. + text_range_provider->MoveEndpointByRange(TextPatternRangeEndpoint_Start, + text_range_provider.Get(), + TextPatternRangeEndpoint_End); + text_range_provider->Select(); + + // Verify selection. + AXTree::Selection unignored_selection = delegate->GetUnignoredSelection(); + EXPECT_EQ(3, unignored_selection.anchor_object_id); + EXPECT_EQ(3, unignored_selection.focus_object_id); + EXPECT_EQ(9, unignored_selection.anchor_offset); + EXPECT_EQ(9, unignored_selection.focus_offset); + + // Verify selection on degenerate range. + document_provider->GetSelection(selection.Receive()); + ASSERT_NE(nullptr, selection.Get()); + + EXPECT_HRESULT_SUCCEEDED(SafeArrayGetUBound(selection.Get(), 1, &ubound)); + EXPECT_EQ(0, ubound); + EXPECT_HRESULT_SUCCEEDED(SafeArrayGetLBound(selection.Get(), 1, &lbound)); + EXPECT_EQ(0, lbound); + EXPECT_HRESULT_SUCCEEDED(SafeArrayGetElement( + selection.Get(), &index, + static_cast(&selected_text_range_provider))); + SetOwner(owner_platform, selected_text_range_provider.Get()); + EXPECT_UIA_TEXTRANGE_EQ(selected_text_range_provider, L""); + + selected_text_range_provider.Reset(); + selection.Reset(); + } +} + +// TODO(crbug.com/1124051): Remove this test once this crbug is fixed. +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderSelectListMarker) { + ui::AXNodeData root_data; + root_data.id = 1; + root_data.role = ax::mojom::Role::kRootWebArea; + + ui::AXNodeData list_data; + list_data.id = 2; + list_data.role = ax::mojom::Role::kList; + root_data.child_ids.push_back(list_data.id); + + ui::AXNodeData list_item_data; + list_item_data.id = 3; + list_item_data.role = ax::mojom::Role::kListItem; + list_data.child_ids.push_back(list_item_data.id); + + ui::AXNodeData list_marker; + list_marker.id = 4; + list_marker.role = ax::mojom::Role::kListMarker; + list_item_data.child_ids.push_back(list_marker.id); + + ui::AXNodeData static_text_data; + static_text_data.id = 5; + static_text_data.role = ax::mojom::Role::kStaticText; + static_text_data.SetName("1. "); + list_marker.child_ids.push_back(static_text_data.id); + + ui::AXNodeData list_item_text_data; + list_item_text_data.id = 6; + list_item_text_data.role = ax::mojom::Role::kStaticText; + list_item_text_data.SetName("First Item"); + list_item_data.child_ids.push_back(list_item_text_data.id); + + 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 = {root_data, list_data, list_item_data, + list_marker, static_text_data, list_item_text_data}; + Init(update); + AXNode* root_node = GetRootAsAXNode(); + + // Text range related to "1. ". + AXNode* list_node = root_node->children()[0]; + AXNode* list_item_node = list_node->children()[0]; + AXNode* list_marker_node = list_item_node->children()[0]; + ComPtr list_marker_text_range_provider; + GetTextRangeProviderFromTextNode(list_marker_text_range_provider, + list_marker_node->children()[0]); + + // A list marker text range performs select. + EXPECT_HRESULT_SUCCEEDED(list_marker_text_range_provider->Select()); + + // Verify selection was not performed on list marker range. + base::win::ScopedSafearray selection; + ComPtr root_node_raw = + QueryInterfaceFromNode(root_node); + ComPtr document_provider; + EXPECT_HRESULT_SUCCEEDED( + root_node_raw->GetPatternProvider(UIA_TextPatternId, &document_provider)); + EXPECT_HRESULT_SUCCEEDED( + document_provider->GetSelection(selection.Receive())); + ASSERT_EQ(nullptr, selection.Get()); + selection.Reset(); +} + +// TODO(schectman) Find text cannot ignore case yet. +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderFindText) { + Init(BuildTextDocument({"some text", "more text"}, + false /* build_word_boundaries_offsets */, + true /* place_text_on_one_line */)); + + AXNode* root_node = GetRootAsAXNode(); + AXPlatformNodeWin* owner = + static_cast(AXPlatformNodeFromNode(root_node)); + ASSERT_NE(owner, nullptr); + ComPtr range; + + // Test Leaf kStaticText search. + GetTextRangeProviderFromTextNode(range, root_node->children()[0]); + EXPECT_UIA_FIND_TEXT(range, L"some text", false, owner); + // Some expectations like the one below are currently skipped until we can + // implement ignoreCase in FindText. + // EXPECT_UIA_FIND_TEXT(range, L"SoMe TeXt", false, owner); + GetTextRangeProviderFromTextNode(range, root_node->children()[1]); + EXPECT_UIA_FIND_TEXT(range, L"more", false, owner); + // EXPECT_UIA_FIND_TEXT(range, L"MoRe", true, owner); + + // Test searching for leaf content from ancestor. + GetTextRangeProviderFromTextNode(range, root_node); + EXPECT_UIA_FIND_TEXT(range, L"some text", false, owner); + // EXPECT_UIA_FIND_TEXT(range, L"SoMe TeXt", true, owner); + EXPECT_UIA_FIND_TEXT(range, L"more text", false, owner); + // EXPECT_UIA_FIND_TEXT(range, L"MoRe TeXt", true, owner); + EXPECT_UIA_FIND_TEXT(range, L"more", false, owner); + // Test finding text that crosses a node boundary. + EXPECT_UIA_FIND_TEXT(range, L"textmore", false, owner); + // Test no match. + EXPECT_UIA_FIND_TEXT_NO_MATCH(range, L"no match", false, owner); + + // Test if range returned is in expected anchor node. + GetTextRangeProviderFromTextNode(range, root_node->children()[1]); + base::win::ScopedBstr find_string(L"more text"); + Microsoft::WRL::ComPtr text_range_provider_found; + EXPECT_HRESULT_SUCCEEDED(range->FindText(find_string.Get(), false, false, + &text_range_provider_found)); + Microsoft::WRL::ComPtr + text_range_provider_win; + text_range_provider_found->QueryInterface( + IID_PPV_ARGS(&text_range_provider_win)); + ASSERT_TRUE(GetStart(text_range_provider_win.Get())->IsTextPosition()); + EXPECT_EQ(5, GetStart(text_range_provider_win.Get())->anchor_id()); + EXPECT_EQ(0, GetStart(text_range_provider_win.Get())->text_offset()); + ASSERT_TRUE(GetEnd(text_range_provider_win.Get())->IsTextPosition()); + EXPECT_EQ(5, GetEnd(text_range_provider_win.Get())->anchor_id()); + EXPECT_EQ(9, GetEnd(text_range_provider_win.Get())->text_offset()); +} + +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_FindTextWithEmbeddedObjectCharacter) { + // ++1 kRootWebArea + // ++++2 kList + // ++++++3 kListItem + // ++++++++4 kStaticText + // ++++++++++5 kInlineTextBox + // ++++++6 kListItem + // ++++++++7 kStaticText + // ++++++++++8 kInlineTextBox + ui::AXNodeData root_1; + ui::AXNodeData list_2; + ui::AXNodeData list_item_3; + ui::AXNodeData static_text_4; + ui::AXNodeData inline_box_5; + ui::AXNodeData list_item_6; + ui::AXNodeData static_text_7; + ui::AXNodeData inline_box_8; + + root_1.id = 1; + list_2.id = 2; + list_item_3.id = 3; + static_text_4.id = 4; + inline_box_5.id = 5; + list_item_6.id = 6; + static_text_7.id = 7; + inline_box_8.id = 8; + + root_1.role = ax::mojom::Role::kRootWebArea; + root_1.child_ids = {list_2.id}; + + list_2.role = ax::mojom::Role::kList; + list_2.child_ids = {list_item_3.id, list_item_6.id}; + + list_item_3.role = ax::mojom::Role::kListItem; + list_item_3.child_ids = {static_text_4.id}; + + static_text_4.role = ax::mojom::Role::kStaticText; + static_text_4.SetName("foo"); + static_text_4.child_ids = {inline_box_5.id}; + + inline_box_5.role = ax::mojom::Role::kInlineTextBox; + inline_box_5.SetName("foo"); + + list_item_6.role = ax::mojom::Role::kListItem; + list_item_6.child_ids = {static_text_7.id}; + + static_text_7.role = ax::mojom::Role::kStaticText; + static_text_7.child_ids = {inline_box_8.id}; + static_text_7.SetName("bar"); + + inline_box_8.role = ax::mojom::Role::kInlineTextBox; + inline_box_8.SetName("bar"); + + 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_1.id; + update.nodes = {root_1, list_2, list_item_3, static_text_4, + inline_box_5, list_item_6, static_text_7, inline_box_8}; + + Init(update); + + AXNode* root_node = GetRootAsAXNode(); + ComPtr text_range_provider; + GetTextRangeProviderFromTextNode(text_range_provider, root_node); + + base::win::ScopedBstr find_string(L"oobar"); + Microsoft::WRL::ComPtr text_range_provider_found; + EXPECT_HRESULT_SUCCEEDED(text_range_provider->FindText( + find_string.Get(), false, false, &text_range_provider_found)); + ASSERT_TRUE(text_range_provider_found.Get()); + Microsoft::WRL::ComPtr + text_range_provider_win; + text_range_provider_found->QueryInterface( + IID_PPV_ARGS(&text_range_provider_win)); + ASSERT_TRUE(GetStart(text_range_provider_win.Get())->IsTextPosition()); + EXPECT_EQ(5, GetStart(text_range_provider_win.Get())->anchor_id()); + EXPECT_EQ(1, GetStart(text_range_provider_win.Get())->text_offset()); + ASSERT_TRUE(GetEnd(text_range_provider_win.Get())->IsTextPosition()); + EXPECT_EQ(8, GetEnd(text_range_provider_win.Get())->anchor_id()); + EXPECT_EQ(3, GetEnd(text_range_provider_win.Get())->text_offset()); +} + +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderFindTextBackwards) { + Init(BuildTextDocument({"text", "some", "text"}, + false /* build_word_boundaries_offsets */, + true /* place_text_on_one_line */)); + AXNode* root_node = GetRootAsAXNode(); + + ComPtr root_range_provider; + GetTextRangeProviderFromTextNode(root_range_provider, root_node); + ComPtr text_node1_range; + GetTextRangeProviderFromTextNode(text_node1_range, root_node->children()[0]); + ComPtr text_node3_range; + GetTextRangeProviderFromTextNode(text_node3_range, root_node->children()[2]); + + ComPtr text_range_provider_found; + base::win::ScopedBstr find_string(L"text"); + BOOL range_equal; + + // Forward search finds the text_node1. + EXPECT_HRESULT_SUCCEEDED(root_range_provider->FindText( + find_string.Get(), false, false, &text_range_provider_found)); + CopyOwnerToClone(root_range_provider.Get(), text_range_provider_found.Get()); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider_found, find_string.Get()); + + range_equal = false; + EXPECT_HRESULT_SUCCEEDED( + text_range_provider_found->Compare(text_node1_range.Get(), &range_equal)); + EXPECT_TRUE(range_equal); + + // Backwards search finds the text_node3. + EXPECT_HRESULT_SUCCEEDED(root_range_provider->FindText( + find_string.Get(), true, false, &text_range_provider_found)); + CopyOwnerToClone(root_range_provider.Get(), text_range_provider_found.Get()); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider_found, find_string.Get()); + + range_equal = false; + EXPECT_HRESULT_SUCCEEDED( + text_range_provider_found->Compare(text_node3_range.Get(), &range_equal)); + EXPECT_TRUE(range_equal); +} + +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderFindAttribute) { + // document - visible + // [empty] + // + // Search forward, look for IsHidden=true. + // Expected: nullptr + // Search forward, look for IsHidden=false. + // Expected: "" + // Note: returns "" rather than nullptr here because document root web area by + // default set to visible. So the text range represents document matches + // our searching criteria. And we return a degenerate range. + // + // Search backward, look for IsHidden=true. + // Expected: nullptr + // Search backward, look for IsHidden=false. + // Expected: "" + // Note: returns "" rather than nullptr here because document root web area by + // default set to visible. So the text range represents document matches + // our searching criteria. And we return a degenerate range. + { + ui::AXNodeData root_data; + root_data.id = 1; + root_data.role = ax::mojom::Role::kRootWebArea; + + ui::AXTreeUpdate update; + update.tree_data.tree_id = ui::AXTreeID::CreateNewAXTreeID(); + update.has_tree_data = true; + update.root_id = root_data.id; + update.nodes = {root_data}; + + Init(update); + + bool is_search_backward; + VARIANT is_hidden_attr_val; + V_VT(&is_hidden_attr_val) = VT_BOOL; + ComPtr matched_range_provider; + ComPtr document_range_provider; + GetTextRangeProviderFromTextNode(document_range_provider, + GetRootAsAXNode()); + + // Search forward, look for IsHidden=true. + // Expected: nullptr + V_BOOL(&is_hidden_attr_val) = VARIANT_TRUE; + is_search_backward = false; + document_range_provider->FindAttribute( + UIA_IsHiddenAttributeId, is_hidden_attr_val, is_search_backward, + &matched_range_provider); + ASSERT_EQ(nullptr, matched_range_provider.Get()); + + // Search forward, look for IsHidden=false. + // Expected: "" + // Note: returns "" rather than nullptr here because document root web area + // by default set to visible. So the text range represents document + // matches our searching criteria. And we return a degenerate range. + V_BOOL(&is_hidden_attr_val) = VARIANT_FALSE; + is_search_backward = false; + document_range_provider->FindAttribute( + UIA_IsHiddenAttributeId, is_hidden_attr_val, is_search_backward, + &matched_range_provider); + ASSERT_NE(nullptr, matched_range_provider.Get()); + CopyOwnerToClone(document_range_provider.Get(), + matched_range_provider.Get()); + EXPECT_UIA_TEXTRANGE_EQ(matched_range_provider, L""); + matched_range_provider.Reset(); + + // Search backward, look for IsHidden=true. + // Expected: nullptr + V_BOOL(&is_hidden_attr_val) = VARIANT_TRUE; + is_search_backward = true; + document_range_provider->FindAttribute( + UIA_IsHiddenAttributeId, is_hidden_attr_val, is_search_backward, + &matched_range_provider); + ASSERT_EQ(nullptr, matched_range_provider.Get()); + + // Search backward, look for IsHidden=false. + // Expected: "" + // Note: returns "" rather than nullptr here because document root web area + // by default set to visible. So the text range represents document + // matches our searching criteria. And we return a degenerate range. + V_BOOL(&is_hidden_attr_val) = VARIANT_FALSE; + is_search_backward = true; + document_range_provider->FindAttribute( + UIA_IsHiddenAttributeId, is_hidden_attr_val, is_search_backward, + &matched_range_provider); + ASSERT_NE(nullptr, matched_range_provider.Get()); + CopyOwnerToClone(document_range_provider.Get(), + matched_range_provider.Get()); + EXPECT_UIA_TEXTRANGE_EQ(matched_range_provider, L""); + } + + // document - visible + // text1 - invisible + // + // Search forward, look for IsHidden=true. + // Expected: "text1" + // Search forward, look for IsHidden=false. + // Expected: nullptr + // Search backward, look for IsHidden=true. + // Expected: "text1" + // Search backward, look for IsHidden=false. + // Expected: nullptr + { + ui::AXNodeData text_data1; + text_data1.id = 2; + text_data1.role = ax::mojom::Role::kStaticText; + text_data1.AddState(ax::mojom::State::kInvisible); + text_data1.SetName("text1"); + + ui::AXNodeData root_data; + root_data.id = 1; + root_data.role = ax::mojom::Role::kRootWebArea; + root_data.child_ids = {2}; + + ui::AXTreeUpdate update; + update.tree_data.tree_id = ui::AXTreeID::CreateNewAXTreeID(); + update.has_tree_data = true; + update.root_id = root_data.id; + update.nodes = {root_data, text_data1}; + + Init(update); + + bool is_search_backward; + VARIANT is_hidden_attr_val; + V_VT(&is_hidden_attr_val) = VT_BOOL; + ComPtr matched_range_provider; + ComPtr document_range_provider; + GetTextRangeProviderFromTextNode(document_range_provider, + GetRootAsAXNode()); + + // Search forward, look for IsHidden=true. + // Expected: "text1" + V_BOOL(&is_hidden_attr_val) = VARIANT_TRUE; + is_search_backward = false; + document_range_provider->FindAttribute( + UIA_IsHiddenAttributeId, is_hidden_attr_val, is_search_backward, + &matched_range_provider); + ASSERT_NE(nullptr, matched_range_provider.Get()); + CopyOwnerToClone(document_range_provider.Get(), + matched_range_provider.Get()); + EXPECT_UIA_TEXTRANGE_EQ(matched_range_provider, L"text1"); + matched_range_provider.Reset(); + + // Search forward, look for IsHidden=false. + // Expected: nullptr + V_BOOL(&is_hidden_attr_val) = VARIANT_FALSE; + is_search_backward = false; + document_range_provider->FindAttribute( + UIA_IsHiddenAttributeId, is_hidden_attr_val, is_search_backward, + &matched_range_provider); + ASSERT_EQ(nullptr, matched_range_provider.Get()); + + // Search backward, look for IsHidden=true. + // Expected: "text1" + V_BOOL(&is_hidden_attr_val) = VARIANT_TRUE; + is_search_backward = true; + document_range_provider->FindAttribute( + UIA_IsHiddenAttributeId, is_hidden_attr_val, is_search_backward, + &matched_range_provider); + ASSERT_NE(nullptr, matched_range_provider.Get()); + CopyOwnerToClone(document_range_provider.Get(), + matched_range_provider.Get()); + EXPECT_UIA_TEXTRANGE_EQ(matched_range_provider, L"text1"); + matched_range_provider.Reset(); + + // Search backward, look for IsHidden=false. + // Expected: nullptr + V_BOOL(&is_hidden_attr_val) = VARIANT_FALSE; + is_search_backward = true; + document_range_provider->FindAttribute( + UIA_IsHiddenAttributeId, is_hidden_attr_val, is_search_backward, + &matched_range_provider); + ASSERT_EQ(nullptr, matched_range_provider.Get()); + } + + // document - visible + // text1 - visible + // text2 - visible + // + // Search forward, look for IsHidden=true. + // Expected: nullptr + // Search forward, look for IsHidden=false. + // Expected: "text1text2" + // Search backward, look for IsHidden=true. + // Expected: nullptr + // Search backward, look for IsHidden=false. + // Expected: "text1text2" + { + ui::AXNodeData text_data1; + text_data1.id = 2; + text_data1.role = ax::mojom::Role::kStaticText; + text_data1.SetName("text1"); + + ui::AXNodeData text_data2; + text_data2.id = 3; + text_data2.role = ax::mojom::Role::kStaticText; + text_data2.SetName("text2"); + + ui::AXNodeData root_data; + root_data.id = 1; + root_data.role = ax::mojom::Role::kRootWebArea; + root_data.child_ids = {2, 3}; + + ui::AXTreeUpdate update; + update.tree_data.tree_id = ui::AXTreeID::CreateNewAXTreeID(); + update.has_tree_data = true; + update.root_id = root_data.id; + update.nodes = {root_data, text_data1, text_data2}; + + Init(update); + + bool is_search_backward; + VARIANT is_hidden_attr_val; + V_VT(&is_hidden_attr_val) = VT_BOOL; + ComPtr matched_range_provider; + ComPtr document_range_provider; + GetTextRangeProviderFromTextNode(document_range_provider, + GetRootAsAXNode()); + + // Search forward, look for IsHidden=true. + // Expected: nullptr + V_BOOL(&is_hidden_attr_val) = VARIANT_TRUE; + is_search_backward = false; + document_range_provider->FindAttribute( + UIA_IsHiddenAttributeId, is_hidden_attr_val, is_search_backward, + &matched_range_provider); + ASSERT_EQ(nullptr, matched_range_provider.Get()); + + // Search forward, look for IsHidden=false. + // Expected: "text1text2" + V_BOOL(&is_hidden_attr_val) = VARIANT_FALSE; + is_search_backward = false; + document_range_provider->FindAttribute( + UIA_IsHiddenAttributeId, is_hidden_attr_val, is_search_backward, + &matched_range_provider); + ASSERT_NE(nullptr, matched_range_provider.Get()); + CopyOwnerToClone(document_range_provider.Get(), + matched_range_provider.Get()); + EXPECT_UIA_TEXTRANGE_EQ(matched_range_provider, L"text1text2"); + matched_range_provider.Reset(); + + // Search backward, look for IsHidden=true. + // Expected: nullptr + V_BOOL(&is_hidden_attr_val) = VARIANT_TRUE; + is_search_backward = true; + document_range_provider->FindAttribute( + UIA_IsHiddenAttributeId, is_hidden_attr_val, is_search_backward, + &matched_range_provider); + ASSERT_EQ(nullptr, matched_range_provider.Get()); + + // Search backward, look for IsHidden=false. + // Expected: "text1text2" + V_BOOL(&is_hidden_attr_val) = VARIANT_FALSE; + is_search_backward = true; + document_range_provider->FindAttribute( + UIA_IsHiddenAttributeId, is_hidden_attr_val, is_search_backward, + &matched_range_provider); + ASSERT_NE(nullptr, matched_range_provider.Get()); + CopyOwnerToClone(document_range_provider.Get(), + matched_range_provider.Get()); + EXPECT_UIA_TEXTRANGE_EQ(matched_range_provider, L"text1text2"); + } + + // document - visible + // text1 - visible + // text2 - invisible + // text3 - invisible + // text4 - visible + // text5 - invisible + // + // Search forward, look for IsHidden=true. + // Expected: "text2text3" + // Search forward, look for IsHidden=false. + // Expected: "text1" + // Search backward, look for IsHidden=true. + // Expected: "text5" + // Search backward, look for IsHidden=false. + // Expected: "text4" + { + ui::AXNodeData text_data1; + text_data1.id = 2; + text_data1.role = ax::mojom::Role::kStaticText; + text_data1.SetName("text1"); + + ui::AXNodeData text_data2; + text_data2.id = 3; + text_data2.role = ax::mojom::Role::kStaticText; + text_data2.AddState(ax::mojom::State::kInvisible); + text_data2.SetName("text2"); + + ui::AXNodeData text_data3; + text_data3.id = 4; + text_data3.role = ax::mojom::Role::kStaticText; + text_data3.AddState(ax::mojom::State::kInvisible); + text_data3.SetName("text3"); + + ui::AXNodeData text_data4; + text_data4.id = 5; + text_data4.role = ax::mojom::Role::kStaticText; + text_data4.SetName("text4"); + + ui::AXNodeData text_data5; + text_data5.id = 6; + text_data5.role = ax::mojom::Role::kStaticText; + text_data5.AddState(ax::mojom::State::kInvisible); + text_data5.SetName("text5"); + + ui::AXNodeData root_data; + root_data.id = 1; + root_data.role = ax::mojom::Role::kRootWebArea; + root_data.child_ids = {2, 3, 4, 5, 6}; + + ui::AXTreeUpdate update; + update.tree_data.tree_id = ui::AXTreeID::CreateNewAXTreeID(); + update.has_tree_data = true; + update.root_id = root_data.id; + update.nodes = {root_data, text_data1, text_data2, + text_data3, text_data4, text_data5}; + + Init(update); + + bool is_search_backward; + VARIANT is_hidden_attr_val; + V_VT(&is_hidden_attr_val) = VT_BOOL; + ComPtr matched_range_provider; + ComPtr document_range_provider; + GetTextRangeProviderFromTextNode(document_range_provider, + GetRootAsAXNode()); + + // Search forward, look for IsHidden=true. + // Expected: "text2text3" + V_BOOL(&is_hidden_attr_val) = VARIANT_TRUE; + is_search_backward = false; + document_range_provider->FindAttribute( + UIA_IsHiddenAttributeId, is_hidden_attr_val, is_search_backward, + &matched_range_provider); + ASSERT_NE(nullptr, matched_range_provider.Get()); + CopyOwnerToClone(document_range_provider.Get(), + matched_range_provider.Get()); + EXPECT_UIA_TEXTRANGE_EQ(matched_range_provider, L"text2text3"); + matched_range_provider.Reset(); + + // Search forward, look for IsHidden=false. + // Expected: "text1" + V_BOOL(&is_hidden_attr_val) = VARIANT_FALSE; + is_search_backward = false; + document_range_provider->FindAttribute( + UIA_IsHiddenAttributeId, is_hidden_attr_val, is_search_backward, + &matched_range_provider); + ASSERT_NE(nullptr, matched_range_provider.Get()); + CopyOwnerToClone(document_range_provider.Get(), + matched_range_provider.Get()); + EXPECT_UIA_TEXTRANGE_EQ(matched_range_provider, L"text1"); + matched_range_provider.Reset(); + + // Search backward, look for IsHidden=true. + // Expected: "text5" + V_BOOL(&is_hidden_attr_val) = VARIANT_TRUE; + is_search_backward = true; + document_range_provider->FindAttribute( + UIA_IsHiddenAttributeId, is_hidden_attr_val, is_search_backward, + &matched_range_provider); + ASSERT_NE(nullptr, matched_range_provider.Get()); + CopyOwnerToClone(document_range_provider.Get(), + matched_range_provider.Get()); + EXPECT_UIA_TEXTRANGE_EQ(matched_range_provider, L"text5"); + matched_range_provider.Reset(); + + // Search backward, look for IsHidden=false. + // Expected: "text4" + V_BOOL(&is_hidden_attr_val) = VARIANT_FALSE; + is_search_backward = true; + document_range_provider->FindAttribute( + UIA_IsHiddenAttributeId, is_hidden_attr_val, is_search_backward, + &matched_range_provider); + ASSERT_NE(nullptr, matched_range_provider.Get()); + CopyOwnerToClone(document_range_provider.Get(), + matched_range_provider.Get()); + EXPECT_UIA_TEXTRANGE_EQ(matched_range_provider, L"text4"); + } + + // document - visible + // text1 - visible + // text2 - invisible + // text3 - invisible + // text4 - invisible + // text5 - visible + // + // Search forward, look for IsHidden=true. + // Expected: "text2text3text4" + // Search forward, look for IsHidden=false. + // Expected: "text1" + // Search backward, look for IsHidden=true. + // Expected: "text2text3text4" + // Search backward, look for IsHidden=false. + // Expected: "text5" + { + ui::AXNodeData text_data1; + text_data1.id = 2; + text_data1.role = ax::mojom::Role::kStaticText; + text_data1.SetName("text1"); + + ui::AXNodeData text_data2; + text_data2.id = 3; + text_data2.role = ax::mojom::Role::kStaticText; + text_data2.AddState(ax::mojom::State::kInvisible); + text_data2.SetName("text2"); + + ui::AXNodeData text_data3; + text_data3.id = 4; + text_data3.role = ax::mojom::Role::kStaticText; + text_data3.AddState(ax::mojom::State::kInvisible); + text_data3.SetName("text3"); + + ui::AXNodeData text_data4; + text_data4.id = 5; + text_data4.role = ax::mojom::Role::kStaticText; + text_data4.AddState(ax::mojom::State::kInvisible); + text_data4.SetName("text4"); + + ui::AXNodeData text_data5; + text_data5.id = 6; + text_data5.role = ax::mojom::Role::kStaticText; + text_data5.SetName("text5"); + + ui::AXNodeData root_data; + root_data.id = 1; + root_data.role = ax::mojom::Role::kRootWebArea; + root_data.child_ids = {2, 3, 4, 5, 6}; + + ui::AXTreeUpdate update; + update.tree_data.tree_id = ui::AXTreeID::CreateNewAXTreeID(); + update.has_tree_data = true; + update.root_id = root_data.id; + update.nodes = {root_data, text_data1, text_data2, + text_data3, text_data4, text_data5}; + + Init(update); + + bool is_search_backward; + VARIANT is_hidden_attr_val; + V_VT(&is_hidden_attr_val) = VT_BOOL; + ComPtr matched_range_provider; + ComPtr document_range_provider; + GetTextRangeProviderFromTextNode(document_range_provider, + GetRootAsAXNode()); + + // Search forward, look for IsHidden=true. + // Expected: "text2text3text4" + V_BOOL(&is_hidden_attr_val) = VARIANT_TRUE; + is_search_backward = false; + document_range_provider->FindAttribute( + UIA_IsHiddenAttributeId, is_hidden_attr_val, is_search_backward, + &matched_range_provider); + ASSERT_NE(nullptr, matched_range_provider.Get()); + CopyOwnerToClone(document_range_provider.Get(), + matched_range_provider.Get()); + EXPECT_UIA_TEXTRANGE_EQ(matched_range_provider, L"text2text3text4"); + matched_range_provider.Reset(); + + // Search forward, look for IsHidden=false. + // Expected: "text1" + V_BOOL(&is_hidden_attr_val) = VARIANT_FALSE; + is_search_backward = false; + document_range_provider->FindAttribute( + UIA_IsHiddenAttributeId, is_hidden_attr_val, is_search_backward, + &matched_range_provider); + ASSERT_NE(nullptr, matched_range_provider.Get()); + CopyOwnerToClone(document_range_provider.Get(), + matched_range_provider.Get()); + EXPECT_UIA_TEXTRANGE_EQ(matched_range_provider, L"text1"); + matched_range_provider.Reset(); + + // Search backward, look for IsHidden=true. + // Expected: "text2text3text4" + V_BOOL(&is_hidden_attr_val) = VARIANT_TRUE; + is_search_backward = true; + document_range_provider->FindAttribute( + UIA_IsHiddenAttributeId, is_hidden_attr_val, is_search_backward, + &matched_range_provider); + ASSERT_NE(nullptr, matched_range_provider.Get()); + CopyOwnerToClone(document_range_provider.Get(), + matched_range_provider.Get()); + EXPECT_UIA_TEXTRANGE_EQ(matched_range_provider, L"text2text3text4"); + matched_range_provider.Reset(); + + // Search backward, look for IsHidden=false. + // Expected: "text5" + V_BOOL(&is_hidden_attr_val) = VARIANT_FALSE; + is_search_backward = true; + document_range_provider->FindAttribute( + UIA_IsHiddenAttributeId, is_hidden_attr_val, is_search_backward, + &matched_range_provider); + ASSERT_NE(nullptr, matched_range_provider.Get()); + CopyOwnerToClone(document_range_provider.Get(), + matched_range_provider.Get()); + EXPECT_UIA_TEXTRANGE_EQ(matched_range_provider, L"text5"); + } +} + +TEST_F(AXPlatformNodeTextRangeProviderTest, DISABLED_ElementNotAvailable) { + AXNodeData root_ax_node_data; + root_ax_node_data.id = 1; + root_ax_node_data.role = ax::mojom::Role::kRootWebArea; + + Init(root_ax_node_data); + + ComPtr raw_element_provider_simple = + QueryInterfaceFromNode(GetRootAsAXNode()); + ASSERT_NE(nullptr, raw_element_provider_simple.Get()); + + ComPtr text_provider; + ASSERT_HRESULT_SUCCEEDED(raw_element_provider_simple->GetPatternProvider( + UIA_TextPatternId, &text_provider)); + ASSERT_NE(nullptr, text_provider.Get()); + + ComPtr text_range_provider; + ASSERT_HRESULT_SUCCEEDED( + text_provider->get_DocumentRange(&text_range_provider)); + ASSERT_NE(nullptr, text_range_provider.Get()); + + // An empty tree. + SetTree(std::make_unique()); + + BOOL bool_arg = FALSE; + ASSERT_EQ(static_cast(UIA_E_ELEMENTNOTAVAILABLE), + text_range_provider->ScrollIntoView(bool_arg)); +} + +// TODO(schectman) Non-empty ignored nodes are not used by Flutter. +// https://github.com/flutter/flutter/issues/117012 +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestITextRangeProviderIgnoredNodes) { + // Parent Tree + // 1 + // | + // 2(i) + // |________________________________ + // | | | | | | + // 3 4 5 6 7(i) 8(i) + // | |________ + // | | | + // 9(i) 10(i) 11 + // | |____ + // | | | + // 12 13 14 + + ui::AXTreeUpdate tree_update; + ui::AXTreeID tree_id = ui::AXTreeID::CreateNewAXTreeID(); + tree_update.tree_data.tree_id = tree_id; + tree_update.has_tree_data = true; + tree_update.root_id = 1; + tree_update.nodes.resize(14); + tree_update.nodes[0].id = 1; + tree_update.nodes[0].child_ids = {2}; + tree_update.nodes[0].role = ax::mojom::Role::kRootWebArea; + + tree_update.nodes[1].id = 2; + tree_update.nodes[1].child_ids = {3, 4, 5, 6, 7, 8}; + // According to the existing Blink code, editable roots are never ignored. + // However, we can still create this tree structure only for test purposes. + tree_update.nodes[1].AddState(ax::mojom::State::kIgnored); + tree_update.nodes[1].AddState(ax::mojom::State::kEditable); + tree_update.nodes[1].AddState(ax::mojom::State::kRichlyEditable); + // tree_update.nodes[1].AddBoolAttribute( + // ax::mojom::BoolAttribute::kNonAtomicTextFieldRoot, true); + tree_update.nodes[1].role = ax::mojom::Role::kGenericContainer; + + tree_update.nodes[2].id = 3; + tree_update.nodes[2].role = ax::mojom::Role::kStaticText; + tree_update.nodes[2].SetName(".3."); + + tree_update.nodes[3].id = 4; + tree_update.nodes[3].role = ax::mojom::Role::kStaticText; + tree_update.nodes[3].SetName(".4."); + + tree_update.nodes[4].id = 5; + tree_update.nodes[4].role = ax::mojom::Role::kStaticText; + tree_update.nodes[4].SetName(".5."); + + tree_update.nodes[5].id = 6; + tree_update.nodes[5].role = ax::mojom::Role::kButton; + tree_update.nodes[5].child_ids = {9}; + + tree_update.nodes[6].id = 7; + tree_update.nodes[6].child_ids = {10, 11}; + tree_update.nodes[6].AddState(ax::mojom::State::kIgnored); + tree_update.nodes[6].role = ax::mojom::Role::kGenericContainer; + + tree_update.nodes[7].id = 8; + tree_update.nodes[7].AddState(ax::mojom::State::kIgnored); + tree_update.nodes[7].role = ax::mojom::Role::kStaticText; + tree_update.nodes[7].SetName(".8."); + + tree_update.nodes[8].id = 9; + tree_update.nodes[8].child_ids = {12}; + tree_update.nodes[8].AddState(ax::mojom::State::kIgnored); + tree_update.nodes[8].role = ax::mojom::Role::kGenericContainer; + + tree_update.nodes[9].id = 10; + tree_update.nodes[9].child_ids = {13, 14}; + tree_update.nodes[9].AddState(ax::mojom::State::kIgnored); + tree_update.nodes[8].role = ax::mojom::Role::kGenericContainer; + + tree_update.nodes[10].id = 11; + tree_update.nodes[10].role = ax::mojom::Role::kStaticText; + tree_update.nodes[10].SetName(".11."); + + tree_update.nodes[11].id = 12; + tree_update.nodes[11].role = ax::mojom::Role::kStaticText; + tree_update.nodes[11].AddState(ax::mojom::State::kIgnored); + tree_update.nodes[11].SetName(".12."); + + tree_update.nodes[12].id = 13; + tree_update.nodes[12].role = ax::mojom::Role::kStaticText; + tree_update.nodes[12].SetName(".13."); + + tree_update.nodes[13].id = 14; + tree_update.nodes[13].role = ax::mojom::Role::kStaticText; + tree_update.nodes[13].SetName(".14."); + + Init(tree_update); + EXPECT_ENCLOSING_ELEMENT(GetNodeFromTree(tree_id, 1), + GetNodeFromTree(tree_id, 1)); + EXPECT_ENCLOSING_ELEMENT(GetNodeFromTree(tree_id, 2), + GetNodeFromTree(tree_id, 1)); + EXPECT_ENCLOSING_ELEMENT(GetNodeFromTree(tree_id, 3), + GetNodeFromTree(tree_id, 3)); + EXPECT_ENCLOSING_ELEMENT(GetNodeFromTree(tree_id, 4), + GetNodeFromTree(tree_id, 4)); + EXPECT_ENCLOSING_ELEMENT(GetNodeFromTree(tree_id, 5), + GetNodeFromTree(tree_id, 5)); + EXPECT_ENCLOSING_ELEMENT(GetNodeFromTree(tree_id, 8), + GetNodeFromTree(tree_id, 1)); + EXPECT_ENCLOSING_ELEMENT(GetNodeFromTree(tree_id, 11), + GetNodeFromTree(tree_id, 11)); + EXPECT_ENCLOSING_ELEMENT(GetNodeFromTree(tree_id, 13), + GetNodeFromTree(tree_id, 13)); + EXPECT_ENCLOSING_ELEMENT(GetNodeFromTree(tree_id, 14), + GetNodeFromTree(tree_id, 14)); + + // Test movement and GetText() + ComPtr text_range_provider; + GetTextRangeProviderFromTextNode(text_range_provider, + GetNodeFromTree(tree_id, 1)); + + ASSERT_HRESULT_SUCCEEDED( + text_range_provider->ExpandToEnclosingUnit(TextUnit_Character)); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"."); + + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_End, TextUnit_Character, + /*count*/ 2, + /*expected_text*/ L".3.", + /*expected_count*/ 2); + + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_End, TextUnit_Character, + /*count*/ 6, + /*expected_text*/ L".3..4..5.", + /*expected_count*/ 6); + + // By design, empty objects, such as the unlabelled button in this case, are + // placed in their own paragraph for easier screen reader navigation. + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_End, TextUnit_Character, + /*count*/ 15, + /*expected_text*/ L".3..4..5.\n\xFFFC\n.13..14..11.", + /*expected_count*/ 15); +} + +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestNormalizeTextRangePastEndOfDocument) { + ui::AXTreeUpdate initial_state; + ui::AXTreeID tree_id = ui::AXTreeID::CreateNewAXTreeID(); + initial_state.tree_data.tree_id = tree_id; + initial_state.has_tree_data = true; + initial_state.root_id = 1; + initial_state.nodes.resize(3); + initial_state.nodes[0].id = 1; + initial_state.nodes[0].child_ids = {2}; + initial_state.nodes[0].role = ax::mojom::Role::kRootWebArea; + initial_state.nodes[1].id = 2; + initial_state.nodes[1].child_ids = {3}; + initial_state.nodes[1].role = ax::mojom::Role::kStaticText; + initial_state.nodes[1].SetName("aaa"); + initial_state.nodes[2].id = 3; + initial_state.nodes[2].role = ax::mojom::Role::kInlineTextBox; + initial_state.nodes[2].SetName("aaa"); + + Init(initial_state); + + ComPtr text_range_provider; + GetTextRangeProviderFromTextNode(text_range_provider, + GetNodeFromTree(tree_id, 3)); + + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"aaa"); + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_Start, TextUnit_Character, + /*count*/ 2, + /*expected_text*/ L"a", + /*expected_count*/ 2); + + ComPtr text_range_provider_win; + text_range_provider->QueryInterface(IID_PPV_ARGS(&text_range_provider_win)); + + const AXNodePosition::AXPositionInstance start_after_move = + GetStart(text_range_provider_win.Get())->Clone(); + const AXNodePosition::AXPositionInstance end_after_move = + GetEnd(text_range_provider_win.Get())->Clone(); + EXPECT_LT(*start_after_move, *end_after_move); + + AXTreeUpdate update; + update.nodes.resize(2); + update.nodes[0] = initial_state.nodes[1]; + update.nodes[0].SetName("aa"); + update.nodes[1] = initial_state.nodes[2]; + update.nodes[1].SetName("aa"); + ASSERT_TRUE(GetTree()->Unserialize(update)); + + auto* text_range = text_range_provider_win.Get(); + + auto original_start = GetStart(text_range)->Clone(); + auto original_end = GetEnd(text_range)->Clone(); + + auto normalized_start = GetStart(text_range)->Clone(); + auto normalized_end = GetEnd(text_range)->Clone(); + NormalizeTextRange(text_range, normalized_start, normalized_end); + // Verify that the original range was not changed by normalization. + ExpectPositionsEqual(original_start, GetStart(text_range)); + ExpectPositionsEqual(original_end, GetEnd(text_range)); + + EXPECT_EQ(*start_after_move, *normalized_start); + EXPECT_EQ(*end_after_move, *normalized_end); +} + +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestNormalizeTextRangePastEndOfDocumentWithIgnoredNodes) { + ui::AXTreeUpdate initial_state; + ui::AXTreeID tree_id = ui::AXTreeID::CreateNewAXTreeID(); + initial_state.tree_data.tree_id = tree_id; + initial_state.has_tree_data = true; + initial_state.root_id = 1; + initial_state.nodes.resize(4); + initial_state.nodes[0].id = 1; + initial_state.nodes[0].child_ids = {2}; + initial_state.nodes[0].role = ax::mojom::Role::kRootWebArea; + initial_state.nodes[1].id = 2; + initial_state.nodes[1].child_ids = {3, 4}; + initial_state.nodes[1].role = ax::mojom::Role::kStaticText; + initial_state.nodes[1].SetName("aaa"); + initial_state.nodes[2].id = 3; + initial_state.nodes[2].role = ax::mojom::Role::kInlineTextBox; + initial_state.nodes[2].SetName("aaa"); + initial_state.nodes[3].id = 4; + initial_state.nodes[3].role = ax::mojom::Role::kInlineTextBox; + initial_state.nodes[3].AddState(ax::mojom::State::kIgnored); + initial_state.nodes[3].SetName("ignored"); + + Init(initial_state); + + ComPtr text_range_provider; + GetTextRangeProviderFromTextNode(text_range_provider, + GetNodeFromTree(tree_id, 3)); + + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"aaa"); + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_Start, TextUnit_Character, + /*count*/ 2, + /*expected_text*/ L"a", + /*expected_count*/ 2); + + ComPtr text_range_provider_win; + text_range_provider->QueryInterface(IID_PPV_ARGS(&text_range_provider_win)); + + const AXNodePosition::AXPositionInstance start_after_move = + GetStart(text_range_provider_win.Get())->Clone(); + const AXNodePosition::AXPositionInstance end_after_move = + GetEnd(text_range_provider_win.Get())->Clone(); + EXPECT_LT(*start_after_move, *end_after_move); + + AXTreeUpdate update; + update.nodes.resize(2); + update.nodes[0] = initial_state.nodes[1]; + update.nodes[0].SetName("aa"); + update.nodes[1] = initial_state.nodes[2]; + update.nodes[1].SetName("aa"); + ASSERT_TRUE(GetTree()->Unserialize(update)); + + auto* text_range = text_range_provider_win.Get(); + + auto original_start = GetStart(text_range)->Clone(); + auto original_end = GetEnd(text_range)->Clone(); + + auto normalized_start = GetStart(text_range)->Clone(); + auto normalized_end = GetEnd(text_range)->Clone(); + NormalizeTextRange(text_range, normalized_start, normalized_end); + // Verify that the original range was not changed by normalization. + ExpectPositionsEqual(original_start, GetStart(text_range)); + ExpectPositionsEqual(original_end, GetEnd(text_range)); + + EXPECT_EQ(*start_after_move, *normalized_start); + EXPECT_EQ(*end_after_move, *normalized_end); +} + +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestNormalizeTextRangeInsideIgnoredNodes) { + ui::AXTreeUpdate initial_state; + ui::AXTreeID tree_id = ui::AXTreeID::CreateNewAXTreeID(); + initial_state.tree_data.tree_id = tree_id; + initial_state.has_tree_data = true; + initial_state.root_id = 1; + initial_state.nodes.resize(4); + initial_state.nodes[0].id = 1; + initial_state.nodes[0].child_ids = {2, 3, 4}; + initial_state.nodes[0].role = ax::mojom::Role::kRootWebArea; + initial_state.nodes[1].id = 2; + initial_state.nodes[1].role = ax::mojom::Role::kStaticText; + initial_state.nodes[1].SetName("before"); + initial_state.nodes[2].id = 3; + initial_state.nodes[2].role = ax::mojom::Role::kStaticText; + initial_state.nodes[2].AddState(ax::mojom::State::kIgnored); + initial_state.nodes[2].SetName("ignored"); + initial_state.nodes[3].id = 4; + initial_state.nodes[3].role = ax::mojom::Role::kStaticText; + initial_state.nodes[3].SetName("after"); + + Init(initial_state); + const AXTree* tree = GetTree(); + const AXNode* ignored_node = tree->GetFromId(3); + + // Making |owner| AXID:1 so that |TestAXNodeWrapper::BuildAllWrappers| + // will build the entire tree. + AXPlatformNodeWin* owner = static_cast( + AXPlatformNodeFromNode(GetNodeFromTree(tree_id, 1))); + + // start: TextPosition, anchor_id=3, text_offset=1, annotated_text=inored + // end : TextPosition, anchor_id=3, text_offset=6, annotated_text=ignore + ComPtr ignored_range_win; + CreateTextRangeProviderWin( + ignored_range_win, owner, + /*start_anchor=*/ignored_node, /*start_offset=*/0, + /*start_affinity*/ ax::mojom::TextAffinity::kDownstream, + /*end_anchor=*/ignored_node, /*end_offset=*/0, + /*end_affinity*/ ax::mojom::TextAffinity::kDownstream); + + EXPECT_TRUE(GetStart(ignored_range_win.Get())->IsIgnored()); + EXPECT_TRUE(GetEnd(ignored_range_win.Get())->IsIgnored()); + + auto original_start = GetStart(ignored_range_win.Get())->Clone(); + auto original_end = GetEnd(ignored_range_win.Get())->Clone(); + + auto normalized_start = GetStart(ignored_range_win.Get())->Clone(); + auto normalized_end = GetEnd(ignored_range_win.Get())->Clone(); + NormalizeTextRange(ignored_range_win.Get(), normalized_start, normalized_end); + // Verify that the original range was not changed by normalization. + ExpectPositionsEqual(original_start, GetStart(ignored_range_win.Get())); + ExpectPositionsEqual(original_end, GetEnd(ignored_range_win.Get())); + + EXPECT_FALSE(normalized_start->IsIgnored()); + EXPECT_FALSE(normalized_end->IsIgnored()); + EXPECT_LE(*GetStart(ignored_range_win.Get()), *normalized_start); + EXPECT_LE(*GetEnd(ignored_range_win.Get()), *normalized_end); + EXPECT_LE(*normalized_start, *normalized_end); + + // Remove the last node, forcing |NormalizeTextRange| to normalize + // using the opposite AdjustmentBehavior. + AXTreeUpdate update; + update.nodes.resize(1); + update.nodes[0] = initial_state.nodes[0]; + update.nodes[0].child_ids = {2, 3}; + ASSERT_TRUE(GetTree()->Unserialize(update)); + + original_start = GetStart(ignored_range_win.Get())->Clone(); + original_end = GetEnd(ignored_range_win.Get())->Clone(); + + normalized_start = GetStart(ignored_range_win.Get())->Clone(); + normalized_end = GetEnd(ignored_range_win.Get())->Clone(); + NormalizeTextRange(ignored_range_win.Get(), normalized_start, normalized_end); + // Verify that the original range was not changed by normalization. + ExpectPositionsEqual(original_start, GetStart(ignored_range_win.Get())); + ExpectPositionsEqual(original_end, GetEnd(ignored_range_win.Get())); + + EXPECT_FALSE(normalized_start->IsIgnored()); + EXPECT_FALSE(normalized_end->IsIgnored()); + EXPECT_GE(*GetStart(ignored_range_win.Get()), *normalized_start); + EXPECT_GE(*GetEnd(ignored_range_win.Get()), *normalized_end); + EXPECT_LE(*normalized_start, *normalized_end); +} + +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestNormalizeTextRangeSpanIgnoredNodes) { + ui::AXNodeData root_data; + root_data.id = 1; + root_data.role = ax::mojom::Role::kRootWebArea; + + ui::AXNodeData before_text; + before_text.id = 2; + before_text.role = ax::mojom::Role::kStaticText; + before_text.SetName("before"); + root_data.child_ids.push_back(before_text.id); + + ui::AXNodeData ignored_text1; + ignored_text1.id = 3; + ignored_text1.role = ax::mojom::Role::kStaticText; + ignored_text1.AddState(ax::mojom::State::kIgnored); + ignored_text1.SetName("ignored1"); + root_data.child_ids.push_back(ignored_text1.id); + + ui::AXNodeData ignored_text2; + ignored_text2.id = 4; + ignored_text2.role = ax::mojom::Role::kStaticText; + ignored_text2.AddState(ax::mojom::State::kIgnored); + ignored_text2.SetName("ignored2"); + root_data.child_ids.push_back(ignored_text2.id); + + ui::AXNodeData after_text; + after_text.id = 5; + after_text.role = ax::mojom::Role::kStaticText; + after_text.SetName("after"); + root_data.child_ids.push_back(after_text.id); + + ui::AXTreeUpdate update; + ui::AXTreeID tree_id = ui::AXTreeID::CreateNewAXTreeID(); + update.root_id = root_data.id; + update.tree_data.tree_id = tree_id; + update.has_tree_data = true; + update.nodes = {root_data, before_text, ignored_text1, ignored_text2, + after_text}; + + Init(update); + const AXTree* tree = GetTree(); + + const AXNode* before_text_node = tree->GetFromId(before_text.id); + const AXNode* after_text_node = tree->GetFromId(after_text.id); + + // Making |owner| AXID:1 so that |TestAXNodeWrapper::BuildAllWrappers| + // will build the entire tree. + AXPlatformNodeWin* owner = static_cast( + AXPlatformNodeFromNode(GetNodeFromTree(tree_id, 1))); + + // Original range before NormalizeTextRange() + // |before<>||ignored1||ignored2||fter| + // |-----------------------| + // start: TextPosition, anchor_id=2, text_offset=6, annotated_text=before<> + // end : TextPosition, anchor_id=5, text_offset=0, annotated_text=fter + ComPtr range_span_ignored_nodes; + CreateTextRangeProviderWin( + range_span_ignored_nodes, owner, + /*start_anchor=*/before_text_node, /*start_offset=*/6, + /*start_affinity*/ ax::mojom::TextAffinity::kDownstream, + /*end_anchor=*/after_text_node, /*end_offset=*/0, + /*end_affinity*/ ax::mojom::TextAffinity::kDownstream); + + auto original_start = GetStart(range_span_ignored_nodes.Get())->Clone(); + auto original_end = GetEnd(range_span_ignored_nodes.Get())->Clone(); + + // Normalized range after NormalizeTextRange() + // |before||ignored1||ignored2||fter| + // |-| + AXNodePosition::AXPositionInstance normalized_start = + GetStart(range_span_ignored_nodes.Get())->Clone(); + AXNodePosition::AXPositionInstance normalized_end = + GetEnd(range_span_ignored_nodes.Get())->Clone(); + NormalizeTextRange(range_span_ignored_nodes.Get(), normalized_start, + normalized_end); + // Verify that the original range was not changed by normalization. + ExpectPositionsEqual(original_start, + GetStart(range_span_ignored_nodes.Get())); + ExpectPositionsEqual(original_end, GetEnd(range_span_ignored_nodes.Get())); + + EXPECT_EQ(*normalized_start, *normalized_end); + + EXPECT_TRUE(normalized_start->IsTextPosition()); + EXPECT_TRUE(normalized_start->AtStartOfAnchor()); + EXPECT_EQ(5, normalized_start->anchor_id()); + EXPECT_EQ(0, normalized_start->text_offset()); + + EXPECT_TRUE(normalized_end->IsTextPosition()); + EXPECT_TRUE(normalized_end->AtStartOfAnchor()); + EXPECT_EQ(5, normalized_end->anchor_id()); + EXPECT_EQ(0, normalized_end->text_offset()); +} + +// TODO(schectman) Non-zero text offset in position into an empty node. +// Why? https://github.com/flutter/flutter/issues/117012 +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestNormalizeTextRangeForceSameAnchorOnDegenerateRange) { + // ++1 kRootWebArea + // ++++2 kGenericContainer + // ++++++3 kImage + // ++++4 kTextField + // ++++++5 kGenericContainer + // ++++++++6 kStaticText + // ++++++++++7 kInlineTextBox + ui::AXNodeData root_1; + ui::AXNodeData generic_container_2; + ui::AXNodeData line_break_3; + ui::AXNodeData text_field_4; + ui::AXNodeData generic_container_5; + ui::AXNodeData static_text_6; + ui::AXNodeData inline_box_7; + + root_1.id = 1; + generic_container_2.id = 2; + line_break_3.id = 3; + text_field_4.id = 4; + generic_container_5.id = 5; + static_text_6.id = 6; + inline_box_7.id = 7; + + root_1.role = ax::mojom::Role::kRootWebArea; + root_1.child_ids = {generic_container_2.id, text_field_4.id}; + + generic_container_2.role = ax::mojom::Role::kGenericContainer; + generic_container_2.AddBoolAttribute( + ax::mojom::BoolAttribute::kIsLineBreakingObject, true); + generic_container_2.child_ids = {line_break_3.id}; + + line_break_3.role = ax::mojom::Role::kLineBreak; + + text_field_4.role = ax::mojom::Role::kTextField; + text_field_4.AddState(ax::mojom::State::kEditable); + text_field_4.child_ids = {generic_container_5.id}; + text_field_4.SetValue("3.14"); + + generic_container_5.role = ax::mojom::Role::kGenericContainer; + generic_container_5.child_ids = {static_text_6.id}; + + static_text_6.role = ax::mojom::Role::kStaticText; + static_text_6.child_ids = {inline_box_7.id}; + static_text_6.SetName("3.14"); + + inline_box_7.role = ax::mojom::Role::kInlineTextBox; + inline_box_7.SetName("3.14"); + + 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_1.id; + update.nodes.push_back(root_1); + update.nodes.push_back(generic_container_2); + update.nodes.push_back(line_break_3); + update.nodes.push_back(text_field_4); + update.nodes.push_back(generic_container_5); + update.nodes.push_back(static_text_6); + update.nodes.push_back(inline_box_7); + + Init(update); + const AXTree* tree = GetTree(); + + const AXNode* line_break_3_node = tree->GetFromId(line_break_3.id); + const AXNode* inline_box_7_node = tree->GetFromId(inline_box_7.id); + + AXPlatformNodeWin* owner = static_cast( + AXPlatformNodeFromNode(GetNodeFromTree(tree_data.tree_id, 1))); + + // start: TextPosition, anchor_id=3, text_offset=1, annotated_text=/xFFFC<> + // end : TextPosition, anchor_id=7, text_offset=0, annotated_text=

i + ComPtr range; + CreateTextRangeProviderWin( + range, owner, + /*start_anchor=*/line_break_3_node, /*start_offset=*/1, + /*start_affinity*/ ax::mojom::TextAffinity::kDownstream, + /*end_anchor=*/inline_box_7_node, /*end_offset=*/0, + /*end_affinity*/ ax::mojom::TextAffinity::kDownstream); + + auto original_start = GetStart(range.Get())->Clone(); + auto original_end = GetEnd(range.Get())->Clone(); + + AXNodePosition::AXPositionInstance normalized_start = + GetStart(range.Get())->Clone(); + AXNodePosition::AXPositionInstance normalized_end = + GetEnd(range.Get())->Clone(); + NormalizeTextRange(range.Get(), normalized_start, normalized_end); + // Verify that the original range was not changed by normalization. + ExpectPositionsEqual(original_start, GetStart(range.Get())); + ExpectPositionsEqual(original_end, GetEnd(range.Get())); + + EXPECT_EQ(*normalized_start, *normalized_start); + + EXPECT_TRUE(normalized_start->AtStartOfAnchor()); + EXPECT_TRUE(normalized_end->AtStartOfAnchor()); + EXPECT_EQ(7, normalized_start->anchor_id()); + EXPECT_EQ(7, normalized_end->anchor_id()); +} + +// TODO(schectman) https://github.com/flutter/flutter/issues/117012 +TEST_F(AXPlatformNodeTextRangeProviderTest, DISABLED_TestValidateStartAndEnd) { + // This test updates the tree structure to test a specific edge case - + // CreatePositionAtFormatBoundary when text lies at the beginning and end + // of the AX tree. + AXNodeData root_data; + root_data.id = 1; + root_data.role = ax::mojom::Role::kRootWebArea; + + AXNodeData text_data; + text_data.id = 2; + text_data.role = ax::mojom::Role::kStaticText; + text_data.SetName("some text"); + + AXNodeData more_text_data; + more_text_data.id = 3; + more_text_data.role = ax::mojom::Role::kStaticText; + more_text_data.SetName("more text"); + + root_data.child_ids = {text_data.id, more_text_data.id}; + + ui::AXTreeUpdate update; + ui::AXTreeID tree_id = ui::AXTreeID::CreateNewAXTreeID(); + update.root_id = root_data.id; + update.tree_data.tree_id = tree_id; + update.has_tree_data = true; + update.nodes = {root_data, text_data, more_text_data}; + + Init(update); + const AXTree* tree = GetTree(); + + const AXNode* root_node = tree->GetFromId(root_data.id); + const AXNode* more_text_node = tree->GetFromId(more_text_data.id); + + // Create a position at MaxTextOffset + // Making |owner| AXID:1 so that |TestAXNodeWrapper::BuildAllWrappers| + // will build the entire tree. + AXPlatformNodeWin* owner = static_cast( + AXPlatformNodeFromNode(GetNodeFromTree(tree_id, 1))); + + // start: TextPosition, anchor_id=1, text_offset=0, annotated_text=ome text + // end : TextPosition, anchor_id=3, text_offset=9, annotated_text=more text<> + ComPtr text_range_provider; + CreateTextRangeProviderWin( + text_range_provider, owner, + /*start_anchor=*/root_node, /*start_offset=*/0, + /*start_affinity*/ ax::mojom::TextAffinity::kDownstream, + /*end_anchor=*/more_text_node, /*end_offset=*/9, + /*end_affinity*/ ax::mojom::TextAffinity::kDownstream); + + // Since the end of the range is at MaxTextOffset, moving it by 1 character + // should have an expected_count of 0. + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_End, TextUnit_Character, + /*count*/ 1, + /*expected_text*/ L"some textmore text", + /*expected_count*/ 0); + + // Now make a change to shorten MaxTextOffset. Ensure that this position is + // invalid, then call SnapToMaxTextOffsetIfBeyond and ensure that it is now + // valid. + more_text_data.SetName("ore tex"); + AXTreeUpdate test_update; + test_update.nodes = {more_text_data}; + ASSERT_TRUE(GetTree()->Unserialize(test_update)); + + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_End, TextUnit_Character, + /*count*/ 1, + /*expected_text*/ L"some textore tex", + /*expected_count*/ 0); + + // Now modify the tree so that start_ is pointing to a node that has been + // removed from the tree. + text_data.SetNameExplicitlyEmpty(); + AXTreeUpdate test_update2; + test_update2.nodes = {text_data}; + ASSERT_TRUE(GetTree()->Unserialize(test_update2)); + + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_Start, TextUnit_Character, + /*count*/ 1, + /*expected_text*/ L"re tex", + /*expected_count*/ 1); + + // Now adjust a node that's not the final node in the tree to point past + // MaxTextOffset. First move the range endpoints so that they're pointing to + // MaxTextOffset on the first node. + text_data.SetName("some text"); + AXTreeUpdate test_update3; + test_update3.nodes = {text_data}; + ASSERT_TRUE(GetTree()->Unserialize(test_update3)); + + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_Start, TextUnit_Character, + /*count*/ -10, + /*expected_text*/ L"some textore tex", + /*expected_count*/ -10); + + // Ensure that we're at MaxTextOffset on the first node by first + // overshooting a negative move... + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_End, TextUnit_Character, + /*count*/ -8, + /*expected_text*/ L"some tex", + /*expected_count*/ -8); + + // ...followed by a positive move + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_End, TextUnit_Character, + /*count*/ 1, + /*expected_text*/ L"some text", + /*expected_count*/ 1); + + // Now our range's start_ is pointing to offset 0 on the first node and end_ + // is pointing to MaxTextOffset on the first node. Now modify the tree so + // that MaxTextOffset is invalid on the first node and ensure that we can + // still move + text_data.SetName("some tex"); + AXTreeUpdate test_update4; + test_update4.nodes = {text_data}; + ASSERT_TRUE(GetTree()->Unserialize(test_update4)); + + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_Start, TextUnit_Character, + /*count*/ 1, + /*expected_text*/ L"ome tex", + /*expected_count*/ 1); +} + +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestReplaceStartAndEndEndpointNode) { + // This test updates the tree structure to ensure that the text range is still + // valid after a text node gets replaced by another one. This case occurs + // every time an AT's focus moves to a node whose style is affected by focus, + // thus generating a tree update. + // + // ++1 kRootWebArea + // ++++2 kGroup (ignored) + // ++++++3 kStaticText/++++4 kStaticText (replacement node) + // ++++5 kStaticText/++++6 kStaticText (replacement node) + AXNodeData root_1; + AXNodeData group_2; + AXNodeData text_3; + AXNodeData text_4; + AXNodeData text_5; + AXNodeData text_6; + + root_1.id = 1; + group_2.id = 2; + text_3.id = 3; + text_4.id = 4; + text_5.id = 5; + text_6.id = 6; + + root_1.role = ax::mojom::Role::kRootWebArea; + root_1.child_ids = {text_3.id, text_5.id}; + + group_2.role = ax::mojom::Role::kGroup; + group_2.AddState(ax::mojom::State::kIgnored); + group_2.child_ids = {text_3.id}; + + text_3.role = ax::mojom::Role::kStaticText; + text_3.SetName("some text"); + + // Replacement node of |text_3|. + text_4.role = ax::mojom::Role::kStaticText; + text_4.SetName("some text"); + + text_5.role = ax::mojom::Role::kStaticText; + text_5.SetName("more text"); + + // Replacement node of |text_5|. + text_6.role = ax::mojom::Role::kStaticText; + text_6.SetName("more text"); + + ui::AXTreeUpdate update; + ui::AXTreeID tree_id = ui::AXTreeID::CreateNewAXTreeID(); + update.root_id = root_1.id; + update.tree_data.tree_id = tree_id; + update.has_tree_data = true; + update.nodes = {root_1, text_3, text_5}; + + Init(update); + const AXTree* tree = GetTree(); + + const AXNode* text_3_node = tree->GetFromId(text_3.id); + const AXNode* text_5_node = tree->GetFromId(text_5.id); + + // Create a position at MaxTextOffset. + // Making |owner| AXID:1 so that |TestAXNodeWrapper::BuildAllWrappers| + // will build the entire tree. + AXPlatformNodeWin* owner = static_cast( + AXPlatformNodeFromNode(GetNodeFromTree(tree_id, 1))); + + // start: TextPosition, anchor_id=3, text_offset=0, annotated_text=ome text + // end : TextPosition, anchor_id=5, text_offset=9, annotated_text=more text<> + ComPtr range; + CreateTextRangeProviderWin( + range, owner, + /*start_anchor*/ text_3_node, /*start_offset*/ 0, + /*start_affinity*/ ax::mojom::TextAffinity::kDownstream, + /*end_anchor*/ text_5_node, /*end_offset*/ 9, + /*end_affinity*/ ax::mojom::TextAffinity::kDownstream); + + EXPECT_UIA_TEXTRANGE_EQ(range, /*expected_text*/ L"some textmore text"); + + // 1. Replace the node on which |start_| is. + { + // Replace node |text_3| with |text_4|. + root_1.child_ids = {text_4.id, text_5.id}; + AXTreeUpdate test_update; + test_update.nodes = {root_1, text_4}; + ASSERT_TRUE(GetTree()->Unserialize(test_update)); + + // Replacing that node shouldn't impact the range. + base::win::ScopedSafearray children; + range->GetChildren(children.Receive()); + EXPECT_UIA_TEXTRANGE_EQ(range, /*expected_text*/ L"some textmore text"); + + // The |start_| endpoint should have moved to the root, skipping its ignored + // parent. + EXPECT_EQ(root_1.id, GetStart(range.Get())->anchor_id()); + EXPECT_EQ(0, GetStart(range.Get())->text_offset()); + + // The |end_| endpoint should not have moved. + EXPECT_EQ(text_5.id, GetEnd(range.Get())->anchor_id()); + EXPECT_EQ(9, GetEnd(range.Get())->text_offset()); + } + + // 2. Replace the node on which |end_| is. + { + // Replace node |text_4| with |text_5|. + root_1.child_ids = {text_4.id, text_6.id}; + AXTreeUpdate test_update; + test_update.nodes = {root_1, text_6}; + ASSERT_TRUE(GetTree()->Unserialize(test_update)); + + // Replacing that node shouldn't impact the range. + base::win::ScopedSafearray children; + range->GetChildren(children.Receive()); + EXPECT_UIA_TEXTRANGE_EQ(range, /*expected_text*/ L"some textmore text"); + + // The |start_| endpoint should still be on its parent. + EXPECT_EQ(root_1.id, GetStart(range.Get())->anchor_id()); + EXPECT_EQ(0, GetStart(range.Get())->text_offset()); + + // The |end_| endpoint should have moved to its parent. + EXPECT_EQ(root_1.id, GetEnd(range.Get())->anchor_id()); + EXPECT_EQ(18, GetEnd(range.Get())->text_offset()); + } + + // 3. Replace the node on which |start_| and |end_| is. + { + // start: TextPosition, anchor_id=4, text_offset=0, annotated_text=ome + // end : TextPosition, anchor_id=4, text_offset=4, annotated_text=some<> + const AXNode* text_4_node = tree->GetFromId(text_4.id); + ComPtr range_2; + CreateTextRangeProviderWin( + range_2, owner, + /*start_anchor*/ text_4_node, /*start_offset*/ 0, + /*start_affinity*/ ax::mojom::TextAffinity::kDownstream, + /*end_anchor*/ text_4_node, /*end_offset*/ 4, + /*end_affinity*/ ax::mojom::TextAffinity::kDownstream); + + EXPECT_UIA_TEXTRANGE_EQ(range_2, /*expected_text*/ L"some"); + + // Replace node |text_4| with |text_3|. + root_1.child_ids = {text_3.id, text_6.id}; + AXTreeUpdate test_update; + test_update.nodes = {root_1, text_3}; + ASSERT_TRUE(GetTree()->Unserialize(test_update)); + + // Replacing that node shouldn't impact the range. + base::win::ScopedSafearray children; + range_2->GetChildren(children.Receive()); + EXPECT_UIA_TEXTRANGE_EQ(range_2, /*expected_text*/ L"some"); + + // The |start_| endpoint should have moved to its parent. + EXPECT_EQ(root_1.id, GetStart(range_2.Get())->anchor_id()); + EXPECT_EQ(0, GetStart(range_2.Get())->text_offset()); + + // The |end_| endpoint should have moved to its parent. + EXPECT_EQ(root_1.id, GetEnd(range_2.Get())->anchor_id()); + EXPECT_EQ(4, GetEnd(range_2.Get())->text_offset()); + } +} + +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestDeleteSubtreeThatIncludesEndpoints) { + // This test updates the tree structure to ensure that the text range is still + // valid after a subtree that includes the text range is deleted, resulting in + // a change to the range. + // + // ++1 kRootWebArea + // ++++2 kStaticText "one" + // ++++3 kGenericContainer + // ++++++4 kGenericContainer + // ++++++++5 kStaticText " two" + // ++++++6 kGenericContainer + // ++++++++7 kStaticText " three" + AXNodeData root_1; + AXNodeData text_2; + AXNodeData gc_3; + AXNodeData gc_4; + AXNodeData text_5; + AXNodeData gc_6; + AXNodeData text_7; + + root_1.id = 1; + text_2.id = 2; + gc_3.id = 3; + gc_4.id = 4; + text_5.id = 5; + gc_6.id = 6; + text_7.id = 7; + + root_1.role = ax::mojom::Role::kRootWebArea; + root_1.child_ids = {text_2.id, gc_3.id}; + + text_2.role = ax::mojom::Role::kStaticText; + text_2.SetName("one"); + + gc_3.role = ax::mojom::Role::kGenericContainer; + gc_3.child_ids = {gc_4.id, gc_6.id}; + + gc_4.role = ax::mojom::Role::kGenericContainer; + gc_4.child_ids = {text_5.id}; + + text_5.role = ax::mojom::Role::kStaticText; + text_5.SetName(" two"); + + gc_6.role = ax::mojom::Role::kGenericContainer; + gc_6.child_ids = {text_7.id}; + + text_7.role = ax::mojom::Role::kStaticText; + text_7.SetName(" three"); + + ui::AXTreeUpdate update; + ui::AXTreeID tree_id = ui::AXTreeID::CreateNewAXTreeID(); + update.root_id = root_1.id; + update.tree_data.tree_id = tree_id; + update.has_tree_data = true; + update.nodes = {root_1, text_2, gc_3, gc_4, text_5, gc_6, text_7}; + + Init(update); + const AXTree* tree = GetTree(); + + const AXNode* text_5_node = tree->GetFromId(text_5.id); + const AXNode* text_7_node = tree->GetFromId(text_7.id); + + // Making |owner| AXID:1 so that |TestAXNodeWrapper::BuildAllWrappers| + // will build the entire tree. + AXPlatformNodeWin* owner = static_cast( + AXPlatformNodeFromNode(GetNodeFromTree(tree_id, 1))); + + // Create a range that spans " two three" located on the leaf nodes. + + // start: TextPosition, anchor_id=5, text_offset=0 + // end : TextPosition, anchor_id=7, text_offset=6 + ComPtr range; + CreateTextRangeProviderWin( + range, owner, + /*start_anchor*/ text_5_node, /*start_offset*/ 0, + /*start_affinity*/ ax::mojom::TextAffinity::kDownstream, + /*end_anchor*/ text_7_node, /*end_offset*/ 6, + /*end_affinity*/ ax::mojom::TextAffinity::kDownstream); + + EXPECT_UIA_TEXTRANGE_EQ(range, /*expected_text*/ L" two three"); + + // Delete |gc_3|, which will delete the entire subtree where both of our + // endpoints are. + AXTreeUpdate test_update; + root_1.child_ids = {text_2.id}; + test_update.nodes = {root_1}; + ASSERT_TRUE(GetTree()->Unserialize(test_update)); + + // The text range should now be a degenerate range positioned at the end of + // root, the parent of |gc_3|, since |gc_3| has been deleted. + EXPECT_EQ(root_1.id, GetStart(range.Get())->anchor_id()); + EXPECT_EQ(3, GetStart(range.Get())->text_offset()); + + EXPECT_EQ(root_1.id, GetEnd(range.Get())->anchor_id()); + EXPECT_EQ(3, GetEnd(range.Get())->text_offset()); +} + +// TODO(schectman) https://github.com/flutter/flutter/issues/117012 +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestDeleteSubtreeWithIgnoredAncestors) { + // This test updates the tree structure to ensure that the text range doesn't + // crash and points to null positions after a subtree that includes the text + // range is deleted and all ancestors are ignored. + // + // ++1 kRootWebArea ignored + // ++++2 kStaticText "one" + // ++++3 kGenericContainer ignored + // ++++++4 kGenericContainer + // ++++++++5 kGenericContainer + // ++++++++++6 kStaticText " two" + // ++++++++7 kGenericContainer ignored + // ++++++++++8 kStaticText " ignored" ignored + // ++++++++9 kGenericContainer + // ++++++++++10 kStaticText " three" + // ++++11 kGenericContainer + // ++++++12 kStaticText "four" + AXNodeData root_1; + AXNodeData text_2; + AXNodeData gc_3; + AXNodeData gc_4; + AXNodeData gc_5; + AXNodeData text_6; + AXNodeData gc_7; + AXNodeData text_8; + AXNodeData gc_9; + AXNodeData text_10; + AXNodeData gc_11; + AXNodeData text_12; + + root_1.id = 1; + text_2.id = 2; + gc_3.id = 3; + gc_4.id = 4; + gc_5.id = 5; + text_6.id = 6; + gc_7.id = 7; + text_8.id = 8; + gc_9.id = 9; + text_10.id = 10; + gc_11.id = 11; + text_12.id = 12; + + root_1.role = ax::mojom::Role::kRootWebArea; + root_1.child_ids = {text_2.id, gc_3.id, gc_11.id}; + root_1.AddState(ax::mojom::State::kIgnored); + + text_2.role = ax::mojom::Role::kStaticText; + text_2.SetName("one"); + + gc_3.role = ax::mojom::Role::kGenericContainer; + gc_3.AddState(ax::mojom::State::kIgnored); + gc_3.child_ids = {gc_4.id}; + + gc_4.role = ax::mojom::Role::kGenericContainer; + gc_4.child_ids = {gc_5.id, gc_7.id, gc_9.id}; + + gc_5.role = ax::mojom::Role::kGenericContainer; + gc_5.child_ids = {text_6.id}; + + text_6.role = ax::mojom::Role::kStaticText; + text_6.SetName(" two"); + + gc_7.role = ax::mojom::Role::kGenericContainer; + gc_7.AddState(ax::mojom::State::kIgnored); + gc_7.child_ids = {text_8.id}; + + text_8.role = ax::mojom::Role::kStaticText; + text_8.AddState(ax::mojom::State::kIgnored); + text_8.SetName(" ignored"); + + gc_9.role = ax::mojom::Role::kGenericContainer; + gc_9.child_ids = {text_10.id}; + + text_10.role = ax::mojom::Role::kStaticText; + text_10.SetName(" three"); + + gc_11.role = ax::mojom::Role::kGenericContainer; + gc_11.child_ids = {text_12.id}; + + text_12.role = ax::mojom::Role::kStaticText; + text_12.SetName("four"); + + ui::AXTreeUpdate update; + ui::AXTreeID tree_id = ui::AXTreeID::CreateNewAXTreeID(); + update.root_id = root_1.id; + update.tree_data.tree_id = tree_id; + update.has_tree_data = true; + update.nodes = {root_1, text_2, gc_3, gc_4, gc_5, text_6, + gc_7, text_8, gc_9, text_10, gc_11, text_12}; + + Init(update); + const AXTree* tree = GetTree(); + + const AXNode* text_6_node = tree->GetFromId(text_6.id); + const AXNode* text_10_node = tree->GetFromId(text_10.id); + + // Making |owner| AXID:1 so that |TestAXNodeWrapper::BuildAllWrappers| + // will build the entire tree. + AXPlatformNodeWin* owner = static_cast( + AXPlatformNodeFromNode(GetNodeFromTree(tree_id, 1))); + + // Create a range that spans " two three" located on the leaf nodes. + + // start: TextPosition, anchor_id=5, text_offset=0 + // end : TextPosition, anchor_id=7, text_offset=6 + ComPtr range; + CreateTextRangeProviderWin( + range, owner, + /*start_anchor*/ text_6_node, /*start_offset*/ 2, + /*start_affinity*/ ax::mojom::TextAffinity::kDownstream, + /*end_anchor*/ text_10_node, /*end_offset*/ 6, + /*end_affinity*/ ax::mojom::TextAffinity::kDownstream); + + EXPECT_UIA_TEXTRANGE_EQ(range, /*expected_text*/ L"wo three"); + + // Delete |gc_3|, which will delete the entire subtree where both of our + // endpoints are. + AXTreeUpdate test_update; + gc_3.child_ids = {}; + test_update.nodes = {gc_3}; + ASSERT_TRUE(GetTree()->Unserialize(test_update)); + + // There was no unignored position in which to place the start and end - they + // should now be null positions. + EXPECT_TRUE(GetStart(range.Get())->IsNullPosition()); + EXPECT_TRUE(GetEnd(range.Get())->IsNullPosition()); +} + +// TODO(schectman) https://github.com/flutter/flutter/issues/117012 +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestDeleteSubtreeThatIncludesEndpointsNormalizeMoves) { + // This test updates the tree structure to ensure that the text range is still + // valid after a subtree that includes the text range is deleted, resulting in + // a change to the range that is adjusted forwards due to an ignored node. + // + // ++1 kRootWebArea + // ++++2 kStaticText "one" + // ++++3 kGenericContainer ignored + // ++++++4 kGenericContainer + // ++++++++5 kGenericContainer + // ++++++++++6 kStaticText " two" + // ++++++++7 kGenericContainer + // ++++++++++8 kStaticText " three" + // ++++++++9 kGenericContainer ignored + // ++++++++++10 kStaticText " ignored" ignored + // ++++11 kGenericContainer + // ++++++12 kStaticText "four" + AXNodeData root_1; + AXNodeData text_2; + AXNodeData gc_3; + AXNodeData gc_4; + AXNodeData gc_5; + AXNodeData text_6; + AXNodeData gc_7; + AXNodeData text_8; + AXNodeData gc_9; + AXNodeData text_10; + AXNodeData gc_11; + AXNodeData text_12; + + root_1.id = 1; + text_2.id = 2; + gc_3.id = 3; + gc_4.id = 4; + gc_5.id = 5; + text_6.id = 6; + gc_7.id = 7; + text_8.id = 8; + gc_9.id = 9; + text_10.id = 10; + gc_11.id = 11; + text_12.id = 12; + + root_1.role = ax::mojom::Role::kRootWebArea; + root_1.child_ids = {text_2.id, gc_3.id, gc_11.id}; + + text_2.role = ax::mojom::Role::kStaticText; + text_2.SetName("one"); + + gc_3.role = ax::mojom::Role::kGenericContainer; + gc_3.AddState(ax::mojom::State::kIgnored); + gc_3.child_ids = {gc_4.id}; + + gc_4.role = ax::mojom::Role::kGenericContainer; + gc_4.child_ids = {gc_5.id, gc_7.id, gc_9.id}; + + gc_5.role = ax::mojom::Role::kGenericContainer; + gc_5.child_ids = {text_6.id}; + + text_6.role = ax::mojom::Role::kStaticText; + text_6.SetName(" two"); + + gc_7.role = ax::mojom::Role::kGenericContainer; + gc_7.child_ids = {text_8.id}; + + text_8.role = ax::mojom::Role::kStaticText; + text_8.SetName(" three"); + + gc_9.role = ax::mojom::Role::kGenericContainer; + gc_9.AddState(ax::mojom::State::kIgnored); + gc_9.child_ids = {text_10.id}; + + text_10.role = ax::mojom::Role::kStaticText; + text_10.AddState(ax::mojom::State::kIgnored); + text_10.SetName(" ignored"); + + gc_11.role = ax::mojom::Role::kGenericContainer; + gc_11.child_ids = {text_12.id}; + + text_12.role = ax::mojom::Role::kStaticText; + text_12.SetName("four"); + + ui::AXTreeUpdate update; + ui::AXTreeID tree_id = ui::AXTreeID::CreateNewAXTreeID(); + update.root_id = root_1.id; + update.tree_data.tree_id = tree_id; + update.has_tree_data = true; + update.nodes = {root_1, text_2, gc_3, gc_4, gc_5, text_6, + gc_7, text_8, gc_9, text_10, gc_11, text_12}; + + Init(update); + const AXTree* tree = GetTree(); + + const AXNode* text_6_node = tree->GetFromId(text_6.id); + const AXNode* text_8_node = tree->GetFromId(text_8.id); + + // Making |owner| AXID:1 so that |TestAXNodeWrapper::BuildAllWrappers| + // will build the entire tree. + AXPlatformNodeWin* owner = static_cast( + AXPlatformNodeFromNode(GetNodeFromTree(tree_id, 1))); + + // Create a range that spans " two three" located on the leaf nodes. + + // start: TextPosition, anchor_id=5, text_offset=0 + // end : TextPosition, anchor_id=7, text_offset=6 + ComPtr range; + CreateTextRangeProviderWin( + range, owner, + /*start_anchor*/ text_6_node, /*start_offset*/ 2, + /*start_affinity*/ ax::mojom::TextAffinity::kDownstream, + /*end_anchor*/ text_8_node, /*end_offset*/ 6, + /*end_affinity*/ ax::mojom::TextAffinity::kDownstream); + + EXPECT_UIA_TEXTRANGE_EQ(range, /*expected_text*/ L"wo three"); + + // Delete |gc_3|, which will delete the entire subtree where both of our + // endpoints are. + AXTreeUpdate test_update; + gc_3.child_ids = {}; + test_update.nodes = {gc_3}; + ASSERT_TRUE(GetTree()->Unserialize(test_update)); + + // The text range should now be a degenerate range positioned at the end of + // root, the parent of |gc_3|, since |gc_3| has been deleted. + EXPECT_EQ(text_12.id, GetStart(range.Get())->anchor_id()); + EXPECT_EQ(0, GetStart(range.Get())->text_offset()); + + EXPECT_EQ(text_12.id, GetEnd(range.Get())->anchor_id()); + EXPECT_EQ(0, GetEnd(range.Get())->text_offset()); +} + +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestDeleteTreePositionPreviousSibling) { + // This test creates a degenerate range with endpoints pointing after the last + // child of the 2 generic container. It then deletes a previous sibling and + // ensures that we don't crash with an out of bounds index that causes null + // child positions to be created. + // + // ++1 kRootWebArea + // ++++2 kGenericContainer + // ++++++3 kHeading + // ++++++++4 kStaticText + // ++++++++++5 kInlineTextBox + // ++++++6 kGenericContainer + // ++++++7 kButton + ui::AXNodeData root_1; + ui::AXNodeData generic_container_2; + ui::AXNodeData heading_3; + ui::AXNodeData static_text_4; + ui::AXNodeData inline_box_5; + ui::AXNodeData generic_container_6; + ui::AXNodeData button_7; + + root_1.id = 1; + generic_container_2.id = 2; + heading_3.id = 3; + static_text_4.id = 4; + inline_box_5.id = 5; + generic_container_6.id = 6; + button_7.id = 7; + + root_1.role = ax::mojom::Role::kRootWebArea; + root_1.child_ids = {generic_container_2.id}; + + generic_container_2.role = ax::mojom::Role::kGenericContainer; + generic_container_2.child_ids = {heading_3.id, generic_container_6.id, + button_7.id}; + + heading_3.role = ax::mojom::Role::kHeading; + heading_3.child_ids = {static_text_4.id}; + + static_text_4.role = ax::mojom::Role::kStaticText; + static_text_4.child_ids = {inline_box_5.id}; + static_text_4.SetName("3.14"); + + inline_box_5.role = ax::mojom::Role::kInlineTextBox; + inline_box_5.SetName("3.14"); + + generic_container_6.role = ax::mojom::Role::kGenericContainer; + generic_container_6.AddBoolAttribute( + ax::mojom::BoolAttribute::kIsLineBreakingObject, true); + + button_7.role = ax::mojom::Role::kButton; + + 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_1.id; + update.nodes = {root_1, generic_container_2, heading_3, static_text_4, + inline_box_5, generic_container_6, button_7}; + + Init(update); + AXTree* tree = GetTree(); + + AXNode* root_node = GetRootAsAXNode(); + AXNodePosition::AXPositionInstance range_start = + AXNodePosition::CreateTreePosition(tree->GetAXTreeID(), + generic_container_2.id, + /*child_index*/ 3); + AXNodePosition::AXPositionInstance range_end = range_start->Clone(); + + AXPlatformNodeWin* owner = + static_cast(AXPlatformNodeFromNode(root_node)); + ComPtr text_range_provider = + AXPlatformNodeTextRangeProviderWin::CreateTextRangeProviderForTesting( + owner, std::move(range_start), std::move(range_end)); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L""); + + generic_container_2.child_ids = {heading_3.id, button_7.id}; + AXTreeUpdate test_update; + test_update.nodes = {generic_container_2}; + ASSERT_TRUE(tree->Unserialize(test_update)); + + root_1.child_ids = {}; + test_update.nodes = {root_1}; + ASSERT_TRUE(tree->Unserialize(test_update)); +} + +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_TestReplaceStartAndEndEndpointRepeatRemoval) { + // This test updates the tree structure to ensure that the text range is still + // valid after text nodes get removed repeatedly. + // + // ++1 kRootWebArea + // ++++2 kStaticText + // ++++3 kGroup (ignored) + // ++++++4 kStaticText + // ++++5 kStaticText + AXNodeData root_1; + AXNodeData text_2; + AXNodeData group_3; + AXNodeData text_4; + AXNodeData text_5; + + root_1.id = 1; + text_2.id = 2; + group_3.id = 3; + text_4.id = 4; + text_5.id = 5; + + root_1.role = ax::mojom::Role::kRootWebArea; + root_1.child_ids = {text_2.id, group_3.id, text_5.id}; + + text_2.role = ax::mojom::Role::kStaticText; + text_2.SetName("text 2"); + + group_3.role = ax::mojom::Role::kGroup; + group_3.AddState(ax::mojom::State::kIgnored); + group_3.child_ids = {text_4.id}; + + text_4.role = ax::mojom::Role::kStaticText; + text_4.SetName("text 4"); + + text_5.role = ax::mojom::Role::kStaticText; + text_5.SetName("text 5"); + + ui::AXTreeUpdate update; + ui::AXTreeID tree_id = ui::AXTreeID::CreateNewAXTreeID(); + update.root_id = root_1.id; + update.tree_data.tree_id = tree_id; + update.has_tree_data = true; + update.nodes = {root_1, text_2, group_3, text_4, text_5}; + + Init(update); + const AXTree* tree = GetTree(); + + const AXNode* text_2_node = tree->GetFromId(text_2.id); + const AXNode* text_4_node = tree->GetFromId(text_4.id); + + // Making |owner| AXID:1 so that |TestAXNodeWrapper::BuildAllWrappers| + // will build the entire tree. + AXPlatformNodeWin* owner = static_cast( + AXPlatformNodeFromNode(GetNodeFromTree(tree_id, 1))); + + ComPtr range; + CreateTextRangeProviderWin( + range, owner, + /*start_anchor*/ text_2_node, /*start_offset*/ 0, + /*start_affinity*/ ax::mojom::TextAffinity::kDownstream, + /*end_anchor*/ text_4_node, /*end_offset*/ 0, + /*end_affinity*/ ax::mojom::TextAffinity::kDownstream); + + EXPECT_UIA_TEXTRANGE_EQ(range, /*expected_text*/ L"text 2"); + + // start: TextPosition, anchor_id=2, text_offset=0, annotated_text=ext2 + // end : TextPosition, anchor_id=4, text_offset=0, annotated_text=<>text4 + // 1. Remove |text_4| which |end_| is anchored on. + { + // Remove node |text_4|. + group_3.child_ids = {}; + AXTreeUpdate test_update; + test_update.nodes = {root_1, group_3}; + ASSERT_TRUE(GetTree()->Unserialize(test_update)); + + // Replacing that node should not impact the range. + EXPECT_UIA_TEXTRANGE_EQ(range, /*expected_text*/ L"text 2"); + } + + // start: TextPosition, anchor_id=2, text_offset=0, annotated_text=<>text2 + // end : TextPosition, anchor_id=2, text_offset=5, annotated_text=text2<> + // 2. Remove |text_2|, which both |start_| and |end_| are anchored to and + // replace with |text_5|. + { + root_1.child_ids = {group_3.id, text_5.id}; + AXTreeUpdate test_update; + test_update.nodes = {root_1, group_3}; + ASSERT_TRUE(GetTree()->Unserialize(test_update)); + + // Removing that node should adjust the range to the |text_5|, as it took + // |text_2|'s position. + EXPECT_UIA_TEXTRANGE_EQ(range, /*expected_text*/ L"text 5"); + } + + // start: TextPosition, anchor_id=5, text_offset=0, annotated_text=<>text5 + // end : TextPosition, anchor_id=5, text_offset=5, annotated_text=text5<> + // 3. Remove |text_5|, which both |start_| and |end_| are pointing to. + { + root_1.child_ids = {group_3.id}; + AXTreeUpdate test_update; + test_update.nodes = {root_1, group_3}; + ASSERT_TRUE(GetTree()->Unserialize(test_update)); + + // Removing the last text node should leave a degenerate range. + EXPECT_UIA_TEXTRANGE_EQ(range, /*expected_text*/ L""); + } +} + +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_CaretAtEndOfTextFieldReadOnly) { + // This test places a degenerate range at end of text field, and it should not + // normalize to other positions, so we should expect the + // 'UIA_IsReadOnlyAttributeId' attribute queried at this position to return + // false. + // ++1 kRootWebArea + // ++++2 kTextField editable value="hello" + // ++++++3 kGenericContainer editable isLineBreakingObject=true + // ++++++++4 kStaticText editable name="hello" + // ++++++++++5 kInlineTextBox editable name="hello" + // ++++6 kStaticText name="abc" + // ++++++7 kInlineTextBox name="abc" + AXNodeData root_1; + AXNodeData text_field_2; + AXNodeData generic_container_3; + AXNodeData static_text_4; + AXNodeData inline_text_5; + AXNodeData static_text_6; + AXNodeData inline_text_7; + + root_1.id = 1; + text_field_2.id = 2; + generic_container_3.id = 3; + static_text_4.id = 4; + inline_text_5.id = 5; + static_text_6.id = 6; + inline_text_7.id = 7; + + root_1.role = ax::mojom::Role::kRootWebArea; + root_1.child_ids = {text_field_2.id, static_text_6.id}; + + text_field_2.role = ax::mojom::Role::kTextField; + text_field_2.AddState(ax::mojom::State::kEditable); + text_field_2.SetValue("hello"); + text_field_2.child_ids = {generic_container_3.id}; + + generic_container_3.role = ax::mojom::Role::kGenericContainer; + generic_container_3.AddState(ax::mojom::State::kEditable); + generic_container_3.AddBoolAttribute( + ax::mojom::BoolAttribute::kIsLineBreakingObject, true); + generic_container_3.child_ids = {static_text_4.id}; + + static_text_4.role = ax::mojom::Role::kStaticText; + static_text_4.SetName("hello"); + static_text_4.AddState(ax::mojom::State::kEditable); + static_text_4.child_ids = {inline_text_5.id}; + + inline_text_5.role = ax::mojom::Role::kInlineTextBox; + inline_text_5.SetName("hello"); + inline_text_5.AddState(ax::mojom::State::kEditable); + + static_text_6.role = ax::mojom::Role::kStaticText; + static_text_6.SetName("abc"); + static_text_6.child_ids = {inline_text_7.id}; + + inline_text_7.role = ax::mojom::Role::kInlineTextBox; + inline_text_7.SetName("abc"); + + ui::AXTreeUpdate update; + ui::AXTreeID tree_id = ui::AXTreeID::CreateNewAXTreeID(); + update.root_id = root_1.id; + update.tree_data.tree_id = tree_id; + update.has_tree_data = true; + update.nodes = {root_1, text_field_2, generic_container_3, + static_text_4, inline_text_5, static_text_6, + inline_text_7}; + + Init(update); + const AXTree* tree = GetTree(); + const AXNode* inline_text_5_node = tree->GetFromId(inline_text_5.id); + + // Making |owner| AXID:1 so that |TestAXNodeWrapper::BuildAllWrappers| + // will build the entire tree. + AXPlatformNodeWin* owner = static_cast( + AXPlatformNodeFromNode(GetNodeFromTree(tree_id, 1))); + + ComPtr range; + base::win::ScopedVariant expected_variant; + + CreateTextRangeProviderWin( + range, owner, + /*start_anchor*/ inline_text_5_node, /*start_offset*/ 3, + /*start_affinity*/ ax::mojom::TextAffinity::kDownstream, + /*end_anchor*/ inline_text_5_node, /*end_offset*/ 4, + /*end_affinity*/ ax::mojom::TextAffinity::kDownstream); + + EXPECT_UIA_TEXTRANGE_EQ(range, /*expected_text*/ L"l"); + + expected_variant.Set(false); + EXPECT_UIA_TEXTATTRIBUTE_EQ(range, UIA_IsReadOnlyAttributeId, + expected_variant); + + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT(range, TextPatternRangeEndpoint_Start, + TextUnit_Character, + /*count*/ 1, + /*expected_text*/ L"", + /*expected_count*/ 1); + expected_variant.Set(false); + EXPECT_UIA_TEXTATTRIBUTE_EQ(range, UIA_IsReadOnlyAttributeId, + expected_variant); + + EXPECT_UIA_MOVE(range, TextUnit_Character, + /*count*/ 1, + /*expected_text*/ + L"", + /*expected_count*/ 1); + expected_variant.Set(false); + EXPECT_UIA_TEXTATTRIBUTE_EQ(range, UIA_IsReadOnlyAttributeId, + expected_variant); + const AXNodePosition::AXPositionInstance& start = GetStart(range.Get()); + const AXNodePosition::AXPositionInstance& end = GetEnd(range.Get()); + EXPECT_TRUE(start->AtEndOfAnchor()); + EXPECT_EQ(5, start->anchor_id()); + EXPECT_EQ(5, start->text_offset()); + + EXPECT_TRUE(end->AtEndOfAnchor()); + EXPECT_EQ(5, end->anchor_id()); + EXPECT_EQ(5, end->text_offset()); +} + +// TODO(schectman) Not all attributes treated as in Chromium. +// https://github.com/flutter/flutter/issues/117012 +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_GeneratedNewlineReturnsCommonAnchorReadonly) { + // This test places a range that starts at the end of a paragraph and + // ends at the beginning of the next paragraph. The range only contains the + // generated newline character. The readonly attribute value returned should + // be the one of the common anchor of the start and end endpoint. + + // ++1 kRootWebArea + // ++++2 kGenericContainer + // ++++++3 kImage + // ++++++4 kTextField editable + // ++++5 kGenericContainer editable + // ++++++6 kImage + // ++++++7 kTextField editable + // ++++8 kGenericContainer + // ++++++9 kTextField editable + // ++++++10 kTextField editable + AXNodeData root_1; + AXNodeData generic_container_2; + AXNodeData image_3; + AXNodeData text_field_4; + AXNodeData generic_container_5; + AXNodeData image_6; + AXNodeData text_field_7; + AXNodeData generic_container_8; + AXNodeData text_field_9; + AXNodeData text_field_10; + + root_1.id = 1; + generic_container_2.id = 2; + image_3.id = 3; + text_field_4.id = 4; + generic_container_5.id = 5; + image_6.id = 6; + text_field_7.id = 7; + generic_container_8.id = 8; + text_field_9.id = 9; + text_field_10.id = 10; + + root_1.role = ax::mojom::Role::kRootWebArea; + root_1.child_ids = {generic_container_2.id, generic_container_5.id, + generic_container_8.id}; + + generic_container_2.role = ax::mojom::Role::kGenericContainer; + generic_container_2.child_ids = {image_3.id, text_field_4.id}; + + image_3.role = ax::mojom::Role::kImage; + image_3.AddBoolAttribute(ax::mojom::BoolAttribute::kIsLineBreakingObject, + true); + + text_field_4.role = ax::mojom::Role::kTextField; + text_field_4.AddState(ax::mojom::State::kEditable); + + generic_container_5.role = ax::mojom::Role::kGenericContainer; + generic_container_5.AddState(ax::mojom::State::kEditable); + generic_container_5.child_ids = {image_6.id, text_field_7.id}; + + image_6.role = ax::mojom::Role::kImage; + image_6.AddBoolAttribute(ax::mojom::BoolAttribute::kIsLineBreakingObject, + true); + + text_field_7.role = ax::mojom::Role::kTextField; + text_field_7.AddState(ax::mojom::State::kEditable); + + generic_container_8.role = ax::mojom::Role::kGenericContainer; + generic_container_8.child_ids = {text_field_9.id, text_field_10.id}; + + text_field_9.role = ax::mojom::Role::kTextField; + text_field_9.AddState(ax::mojom::State::kEditable); + text_field_9.AddBoolAttribute(ax::mojom::BoolAttribute::kIsLineBreakingObject, + true); + + text_field_10.role = ax::mojom::Role::kTextField; + text_field_10.AddState(ax::mojom::State::kEditable); + + ui::AXTreeUpdate update; + ui::AXTreeID tree_id = ui::AXTreeID::CreateNewAXTreeID(); + update.root_id = root_1.id; + update.tree_data.tree_id = tree_id; + update.has_tree_data = true; + update.nodes = {root_1, generic_container_2, image_3, + text_field_4, generic_container_5, image_6, + text_field_7, generic_container_8, text_field_9, + text_field_10}; + + Init(update); + const AXTree* tree = GetTree(); + + const AXNode* image_3_node = tree->GetFromId(image_3.id); + const AXNode* image_6_node = tree->GetFromId(image_6.id); + const AXNode* text_field_4_node = tree->GetFromId(text_field_4.id); + const AXNode* text_field_7_node = tree->GetFromId(text_field_7.id); + const AXNode* text_field_9_node = tree->GetFromId(text_field_9.id); + const AXNode* text_field_10_node = tree->GetFromId(text_field_10.id); + + // Making |owner| AXID:1 so that |TestAXNodeWrapper::BuildAllWrappers| + // will build the entire tree. + AXPlatformNodeWin* owner = static_cast( + AXPlatformNodeFromNode(GetNodeFromTree(tree_id, 1))); + + base::win::ScopedVariant expected_variant; + + ComPtr range_1; + CreateTextRangeProviderWin( + range_1, owner, + /*start_anchor*/ image_3_node, /*start_offset*/ 1, + /*start_affinity*/ ax::mojom::TextAffinity::kDownstream, + /*end_anchor*/ text_field_4_node, /*end_offset*/ 0, + /*end_affinity*/ ax::mojom::TextAffinity::kDownstream); + + EXPECT_UIA_TEXTRANGE_EQ(range_1, /*expected_text*/ L""); + + expected_variant.Set(true); + EXPECT_UIA_TEXTATTRIBUTE_EQ(range_1, UIA_IsReadOnlyAttributeId, + expected_variant); + expected_variant.Reset(); + + ComPtr range_2; + CreateTextRangeProviderWin( + range_2, owner, + /*start_anchor*/ image_6_node, /*start_offset*/ 1, + /*start_affinity*/ ax::mojom::TextAffinity::kDownstream, + /*end_anchor*/ text_field_7_node, /*end_offset*/ 0, + /*end_affinity*/ ax::mojom::TextAffinity::kDownstream); + + EXPECT_UIA_TEXTRANGE_EQ(range_2, /*expected_text*/ L""); + + expected_variant.Set(false); + EXPECT_UIA_TEXTATTRIBUTE_EQ(range_2, UIA_IsReadOnlyAttributeId, + expected_variant); + expected_variant.Reset(); + + // This is testing a corner case when the range spans two text fields + // separated by a paragraph boundary. This case used to not work because we + // were relying on NormalizeTextRange to handle generated newlines and + // normalization doesn't work when the range spans text fields. + ComPtr range_3; + CreateTextRangeProviderWin( + range_3, owner, + /*start_anchor*/ text_field_9_node, /*start_offset*/ 1, + /*start_affinity*/ ax::mojom::TextAffinity::kDownstream, + /*end_anchor*/ text_field_10_node, /*end_offset*/ 0, + /*end_affinity*/ ax::mojom::TextAffinity::kDownstream); + + EXPECT_UIA_TEXTRANGE_EQ(range_3, /*expected_text*/ L""); + + expected_variant.Set(true); + EXPECT_UIA_TEXTATTRIBUTE_EQ(range_3, UIA_IsReadOnlyAttributeId, + expected_variant); + expected_variant.Reset(); +} + +// TODO(schectman) https://github.com/flutter/flutter/issues/117012 +TEST_F(AXPlatformNodeTextRangeProviderTest, + DISABLED_MoveEndpointToLastIgnoredForTextNavigationNode) { + // This test moves the end endpoint of a range by one paragraph unit forward + // to the last node of the tree. That last node happens to be a node that is + // ignored for text navigation, but since it's the last node in the tree, it + // should successfully move the endpoint to that node and keep the units_moved + // value in sync. + // ++1 kRootWebArea + // ++++2 kStaticText name="abc" + // ++++++3 kInlineTextBox name="abc" + // ++++4 kGenericContainer + AXNodeData root_1; + AXNodeData static_text_2; + AXNodeData inline_text_3; + AXNodeData generic_container_4; + + root_1.id = 1; + static_text_2.id = 2; + inline_text_3.id = 3; + generic_container_4.id = 4; + + root_1.role = ax::mojom::Role::kRootWebArea; + root_1.child_ids = {static_text_2.id, generic_container_4.id}; + + static_text_2.role = ax::mojom::Role::kStaticText; + static_text_2.SetName("abc"); + static_text_2.child_ids = {inline_text_3.id}; + + inline_text_3.role = ax::mojom::Role::kInlineTextBox; + inline_text_3.SetName("abc"); + + generic_container_4.role = ax::mojom::Role::kGenericContainer; + + ui::AXTreeUpdate update; + ui::AXTreeID tree_id = ui::AXTreeID::CreateNewAXTreeID(); + update.root_id = root_1.id; + update.tree_data.tree_id = tree_id; + update.has_tree_data = true; + update.nodes = {root_1, static_text_2, inline_text_3, generic_container_4}; + + Init(update); + const AXTree* tree = GetTree(); + const AXNode* inline_text_3_node = tree->GetFromId(inline_text_3.id); + + // Making |owner| AXID:1 so that |TestAXNodeWrapper::BuildAllWrappers| + // will build the entire tree. + AXPlatformNodeWin* owner = static_cast( + AXPlatformNodeFromNode(GetNodeFromTree(tree_id, 1))); + + ComPtr range; + base::win::ScopedVariant expected_variant; + + CreateTextRangeProviderWin( + range, owner, + /*start_anchor*/ inline_text_3_node, /*start_offset*/ 0, + /*start_affinity*/ ax::mojom::TextAffinity::kDownstream, + /*end_anchor*/ inline_text_3_node, /*end_offset*/ 3, + /*end_affinity*/ ax::mojom::TextAffinity::kDownstream); + + EXPECT_UIA_TEXTRANGE_EQ(range, /*expected_text*/ L"abc"); + + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT(range, TextPatternRangeEndpoint_End, + TextUnit_Paragraph, + /*count*/ 1, + /*expected_text*/ L"abc\xFFFC", + /*expected_count*/ 1); +} + +} // namespace ui diff --git a/third_party/accessibility/ax/platform/ax_platform_tree_manager.h b/third_party/accessibility/ax/platform/ax_platform_tree_manager.h new file mode 100644 index 0000000000000..e9ee973ce26cf --- /dev/null +++ b/third_party/accessibility/ax/platform/ax_platform_tree_manager.h @@ -0,0 +1,39 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UI_ACCESSIBILITY_PLATFORM_AX_PLATFORM_TREE_MANAGER_H_ +#define UI_ACCESSIBILITY_PLATFORM_AX_PLATFORM_TREE_MANAGER_H_ + +#include "ax/ax_export.h" +#include "ax/ax_node.h" +#include "ax/ax_tree_id.h" +#include "ax/ax_tree_manager.h" + +namespace ui { + +class AXPlatformNode; +class AXPlatformNodeDelegate; + +// Abstract interface for a class that owns an AXTree and manages its +// connections to other AXTrees in the same page or desktop (parent and child +// trees). +class AX_EXPORT AXPlatformTreeManager : public AXTreeManager { + public: + virtual ~AXPlatformTreeManager() = default; + + // Returns an AXPlatformNode with the specified and |node_id|. + virtual AXPlatformNode* GetPlatformNodeFromTree( + const AXNode::AXID node_id) const = 0; + + // Returns an AXPlatformNode that corresponds to the given |node|. + virtual AXPlatformNode* GetPlatformNodeFromTree(const AXNode& node) const = 0; + + // Returns an AXPlatformNodeDelegate that corresponds to a root node + // of the accessibility tree. + virtual AXPlatformNodeDelegate* RootDelegate() const = 0; +}; + +} // namespace ui + +#endif // UI_ACCESSIBILITY_PLATFORM_AX_PLATFORM_TREE_MANAGER_H_