| // Copyright 2012 The Chromium Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "ui/accessibility/platform/browser_accessibility_win.h" | |
| #include <objbase.h> | |
| #include <stdint.h> | |
| #include <wrl/client.h> | |
| #include <memory> | |
| #include <string> | |
| #include <utility> | |
| #include <vector> | |
| #include "base/command_line.h" | |
| #include "base/strings/utf_string_conversions.h" | |
| #include "base/test/task_environment.h" | |
| #include "base/win/scoped_bstr.h" | |
| #include "base/win/scoped_variant.h" | |
| #include "testing/gtest/include/gtest/gtest.h" | |
| #include "ui/accessibility/ax_node_data.h" | |
| #include "ui/accessibility/platform/ax_platform.h" | |
| #include "ui/accessibility/platform/ax_platform_node_win.h" | |
| #include "ui/accessibility/platform/browser_accessibility_manager.h" | |
| #include "ui/accessibility/platform/browser_accessibility_manager_win.h" | |
| #include "ui/accessibility/platform/test_ax_node_id_delegate.h" | |
| #include "ui/accessibility/platform/test_ax_platform_tree_manager_delegate.h" | |
| #include "ui/base/win/atl_module.h" | |
| namespace ui { | |
| #define EXPECT_IA2_TEXT_AT_OFFSET(provider, index, text_boundary, expected_hr, \ | |
| start, end, text) \ | |
| { \ | |
| LONG actual_start; \ | |
| LONG actual_end; \ | |
| base::win::ScopedBstr actual_text; \ | |
| EXPECT_EQ(expected_hr, \ | |
| provider->get_textAtOffset(index, text_boundary, &actual_start, \ | |
| &actual_end, actual_text.Receive())); \ | |
| EXPECT_EQ(start, actual_start); \ | |
| EXPECT_EQ(end, actual_end); \ | |
| EXPECT_STREQ(text, actual_text.Get()); \ | |
| } | |
| #define EXPECT_IA2_TEXT_BEFORE_OFFSET(provider, index, text_boundary, \ | |
| expected_hr, start, end, text) \ | |
| { \ | |
| LONG actual_start; \ | |
| LONG actual_end; \ | |
| base::win::ScopedBstr actual_text; \ | |
| EXPECT_EQ(expected_hr, provider->get_textBeforeOffset( \ | |
| index, text_boundary, &actual_start, \ | |
| &actual_end, actual_text.Receive())); \ | |
| EXPECT_EQ(start, actual_start); \ | |
| EXPECT_EQ(end, actual_end); \ | |
| EXPECT_STREQ(text, actual_text.Get()); \ | |
| } | |
| #define EXPECT_IA2_TEXT_AFTER_OFFSET(provider, index, text_boundary, \ | |
| expected_hr, start, end, text) \ | |
| { \ | |
| LONG actual_start; \ | |
| LONG actual_end; \ | |
| base::win::ScopedBstr actual_text; \ | |
| EXPECT_EQ(expected_hr, provider->get_textAfterOffset( \ | |
| index, text_boundary, &actual_start, \ | |
| &actual_end, actual_text.Receive())); \ | |
| EXPECT_EQ(start, actual_start); \ | |
| EXPECT_EQ(end, actual_end); \ | |
| EXPECT_STREQ(text, actual_text.Get()); \ | |
| } | |
| #define EXPECT_NATIVE_VIEW_ACCESSIBLE_VECTOR_EQ(descendants, \ | |
| expected_descendants) \ | |
| { \ | |
| size_t count = descendants.size(); \ | |
| EXPECT_EQ(count, expected_descendants.size()); \ | |
| for (size_t i = 0; i < count; ++i) { \ | |
| EXPECT_EQ( \ | |
| AXPlatformNode::FromNativeViewAccessible(descendants[i]) \ | |
| ->GetDelegate() \ | |
| ->GetData() \ | |
| .ToString(), \ | |
| AXPlatformNode::FromNativeViewAccessible(expected_descendants[i]) \ | |
| ->GetDelegate() \ | |
| ->GetData() \ | |
| .ToString()); \ | |
| } \ | |
| } | |
| // BrowserAccessibilityWinTest ------------------------------------------------ | |
| class BrowserAccessibilityWinTest : public ::testing::Test { | |
| public: | |
| BrowserAccessibilityWinTest(); | |
| BrowserAccessibilityWinTest(const BrowserAccessibilityWinTest&) = delete; | |
| BrowserAccessibilityWinTest& operator=(const BrowserAccessibilityWinTest&) = | |
| delete; | |
| ~BrowserAccessibilityWinTest() override; | |
| protected: | |
| std::unique_ptr<TestAXPlatformTreeManagerDelegate> | |
| test_browser_accessibility_delegate_; | |
| TestAXNodeIdDelegate node_id_delegate_; | |
| private: | |
| void SetUp() override; | |
| base::test::SingleThreadTaskEnvironment task_environment_; | |
| }; | |
| BrowserAccessibilityWinTest::BrowserAccessibilityWinTest() {} | |
| BrowserAccessibilityWinTest::~BrowserAccessibilityWinTest() {} | |
| void BrowserAccessibilityWinTest::SetUp() { | |
| win::CreateATLModuleIfNeeded(); | |
| test_browser_accessibility_delegate_ = | |
| std::make_unique<TestAXPlatformTreeManagerDelegate>(); | |
| } | |
| // Actual tests --------------------------------------------------------------- | |
| // Test that BrowserAccessibilityManager correctly releases the tree of | |
| // BrowserAccessibility instances upon delete. | |
| TEST_F(BrowserAccessibilityWinTest, TestNoLeaks) { | |
| // Create AXNodeData objects for a simple document tree, | |
| // representing the accessibility information used to initialize | |
| // BrowserAccessibilityManager. | |
| AXNodeData button; | |
| button.id = 2; | |
| button.role = ax::mojom::Role::kButton; | |
| button.SetName("Button"); | |
| AXNodeData checkbox; | |
| checkbox.id = 3; | |
| checkbox.role = ax::mojom::Role::kCheckBox; | |
| checkbox.SetName("Checkbox"); | |
| AXNodeData root; | |
| root.id = 1; | |
| root.role = ax::mojom::Role::kRootWebArea; | |
| root.SetName("Document"); | |
| root.child_ids.push_back(2); | |
| root.child_ids.push_back(3); | |
| // Construct a BrowserAccessibilityManager with this | |
| // AXNodeData tree and a factory for an instance-counting | |
| // BrowserAccessibility, and ensure that exactly 3 instances were | |
| // created. Note that the manager takes ownership of the factory. | |
| std::unique_ptr<BrowserAccessibilityManager> manager( | |
| BrowserAccessibilityManager::Create( | |
| MakeAXTreeUpdateForTesting(root, button, checkbox), node_id_delegate_, | |
| test_browser_accessibility_delegate_.get())); | |
| // Delete the manager and test that all 3 instances are deleted. | |
| manager.reset(); | |
| // Construct a manager again, and this time use the IAccessible interface | |
| // to get new references to two of the three nodes in the tree. | |
| manager.reset(BrowserAccessibilityManager::Create( | |
| MakeAXTreeUpdateForTesting(root, button, checkbox), node_id_delegate_, | |
| test_browser_accessibility_delegate_.get())); | |
| IAccessible* root_accessible = | |
| ToBrowserAccessibilityWin(manager->GetBrowserAccessibilityRoot()) | |
| ->GetCOM(); | |
| IDispatch* root_iaccessible = NULL; | |
| IDispatch* child1_iaccessible = NULL; | |
| base::win::ScopedVariant childid_self(CHILDID_SELF); | |
| HRESULT hr = root_accessible->get_accChild(childid_self, &root_iaccessible); | |
| ASSERT_EQ(S_OK, hr); | |
| base::win::ScopedVariant one(1); | |
| hr = root_accessible->get_accChild(one, &child1_iaccessible); | |
| ASSERT_EQ(S_OK, hr); | |
| // Now delete the manager, and only one of the three nodes in the tree | |
| // should be released. | |
| manager.reset(); | |
| // Release each of our references and make sure that each one results in | |
| // the instance being deleted as its reference count hits zero. | |
| root_iaccessible->Release(); | |
| child1_iaccessible->Release(); | |
| } | |
| TEST_F(BrowserAccessibilityWinTest, TestChildrenChange) { | |
| // Create AXNodeData objects for a simple document tree, | |
| // representing the accessibility information used to initialize | |
| // BrowserAccessibilityManager. | |
| AXNodeData text; | |
| text.id = 2; | |
| text.role = ax::mojom::Role::kStaticText; | |
| text.SetName("old text"); | |
| AXNodeData root; | |
| root.id = 1; | |
| root.role = ax::mojom::Role::kRootWebArea; | |
| root.SetName("Document"); | |
| root.child_ids.push_back(2); | |
| // Construct a BrowserAccessibilityManager with this | |
| // AXNodeData tree and a factory for an instance-counting | |
| // BrowserAccessibility. | |
| std::unique_ptr<BrowserAccessibilityManager> manager( | |
| BrowserAccessibilityManager::Create( | |
| MakeAXTreeUpdateForTesting(root, text), node_id_delegate_, | |
| test_browser_accessibility_delegate_.get())); | |
| // Query for the text IAccessible and verify that it returns "old text" as its | |
| // value. | |
| base::win::ScopedVariant one(1); | |
| Microsoft::WRL::ComPtr<IDispatch> text_dispatch; | |
| HRESULT hr = ToBrowserAccessibilityWin(manager->GetBrowserAccessibilityRoot()) | |
| ->GetCOM() | |
| ->get_accChild(one, &text_dispatch); | |
| ASSERT_EQ(S_OK, hr); | |
| Microsoft::WRL::ComPtr<IAccessible> text_accessible; | |
| hr = text_dispatch.As(&text_accessible); | |
| ASSERT_EQ(S_OK, hr); | |
| base::win::ScopedVariant childid_self(CHILDID_SELF); | |
| base::win::ScopedBstr name; | |
| hr = text_accessible->get_accName(childid_self, name.Receive()); | |
| ASSERT_EQ(S_OK, hr); | |
| EXPECT_EQ(L"old text", std::wstring(name.Get())); | |
| name.Reset(); | |
| text_dispatch.Reset(); | |
| text_accessible.Reset(); | |
| // Notify the BrowserAccessibilityManager that the text child has changed. | |
| AXNodeData text2; | |
| text2.id = 2; | |
| text2.role = ax::mojom::Role::kStaticText; | |
| text2.SetName("new text"); | |
| AXUpdatesAndEvents event_bundle; | |
| event_bundle.updates.resize(1); | |
| event_bundle.updates[0].nodes.push_back(text2); | |
| ASSERT_TRUE(manager->OnAccessibilityEvents(event_bundle)); | |
| // Query for the text IAccessible and verify that it now returns "new text" | |
| // as its value. | |
| hr = ToBrowserAccessibilityWin(manager->GetBrowserAccessibilityRoot()) | |
| ->GetCOM() | |
| ->get_accChild(one, &text_dispatch); | |
| ASSERT_EQ(S_OK, hr); | |
| hr = text_dispatch.As(&text_accessible); | |
| ASSERT_EQ(S_OK, hr); | |
| hr = text_accessible->get_accName(childid_self, name.Receive()); | |
| ASSERT_EQ(S_OK, hr); | |
| EXPECT_EQ(L"new text", std::wstring(name.Get())); | |
| text_dispatch.Reset(); | |
| text_accessible.Reset(); | |
| // Delete the manager and test that all BrowserAccessibility instances are | |
| // deleted. | |
| manager.reset(); | |
| } | |
| TEST_F(BrowserAccessibilityWinTest, TestChildrenChangeNoLeaks) { | |
| // Create AXNodeData objects for a simple document tree, | |
| // representing the accessibility information used to initialize | |
| // BrowserAccessibilityManager. | |
| AXNodeData div; | |
| div.id = 2; | |
| div.role = ax::mojom::Role::kGroup; | |
| AXNodeData text3; | |
| text3.id = 3; | |
| text3.role = ax::mojom::Role::kStaticText; | |
| AXNodeData text4; | |
| text4.id = 4; | |
| text4.role = ax::mojom::Role::kStaticText; | |
| div.child_ids.push_back(3); | |
| div.child_ids.push_back(4); | |
| AXNodeData root; | |
| root.id = 1; | |
| root.role = ax::mojom::Role::kRootWebArea; | |
| root.child_ids.push_back(2); | |
| // Construct a BrowserAccessibilityManager with this | |
| // AXNodeData tree and a factory for an instance-counting | |
| // BrowserAccessibility and ensure that exactly 4 instances were | |
| // created. Note that the manager takes ownership of the factory. | |
| std::unique_ptr<BrowserAccessibilityManager> manager( | |
| BrowserAccessibilityManager::Create( | |
| MakeAXTreeUpdateForTesting(root, div, text3, text4), | |
| node_id_delegate_, test_browser_accessibility_delegate_.get())); | |
| // Notify the BrowserAccessibilityManager that the div node and its children | |
| // were removed and ensure that only one BrowserAccessibility instance exists. | |
| root.child_ids.clear(); | |
| AXUpdatesAndEvents event_bundle; | |
| event_bundle.updates.resize(1); | |
| event_bundle.updates[0].nodes.push_back(root); | |
| ASSERT_TRUE(manager->OnAccessibilityEvents(event_bundle)); | |
| // Delete the manager and test that all BrowserAccessibility instances are | |
| // deleted. | |
| manager.reset(); | |
| } | |
| TEST_F(BrowserAccessibilityWinTest, TestTextBoundaries) { | |
| // | |
| // +-1 root | |
| // +-2 text_field | |
| // +-3 text_container | |
| // +-4 static_text1 "One two three." | |
| // | +-5 inline_box1 "One two three." | |
| // +-6 line_break1 "\n" | |
| // +-7 static_text2 "Four five six." | |
| // | +-8 inline_box2 "Four five six." | |
| // +-9 line_break2 "\n" kIsLineBreakingObject | |
| // +-10 static_text3 "Seven eight nine." | |
| // +-11 inline_box3 "Seven eight nine." | |
| // | |
| std::string line1 = "One two three."; | |
| std::string line2 = "Four five six."; | |
| std::string line3 = "Seven eight nine."; | |
| std::string text_value = line1 + '\n' + line2 + '\n' + line3; | |
| AXNodeData root; | |
| root.id = 1; | |
| AXNodeData text_field; | |
| text_field.id = 2; | |
| AXNodeData text_container; | |
| text_container.id = 3; | |
| AXNodeData static_text1; | |
| static_text1.id = 4; | |
| AXNodeData inline_box1; | |
| inline_box1.id = 5; | |
| AXNodeData line_break1; | |
| line_break1.id = 6; | |
| AXNodeData static_text2; | |
| static_text2.id = 7; | |
| AXNodeData inline_box2; | |
| inline_box2.id = 8; | |
| AXNodeData line_break2; | |
| line_break2.id = 9; | |
| AXNodeData static_text3; | |
| static_text3.id = 10; | |
| AXNodeData inline_box3; | |
| inline_box3.id = 11; | |
| root.role = ax::mojom::Role::kRootWebArea; | |
| root.child_ids = {text_field.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(text_value); | |
| text_field.AddIntListAttribute(ax::mojom::IntListAttribute::kLineStarts, | |
| {15}); | |
| text_field.child_ids = {text_container.id}; | |
| text_container.role = ax::mojom::Role::kGenericContainer; | |
| text_container.child_ids = {static_text1.id, line_break1.id, static_text2.id, | |
| line_break2.id, static_text3.id}; | |
| static_text1.role = ax::mojom::Role::kStaticText; | |
| static_text1.AddState(ax::mojom::State::kEditable); | |
| static_text1.SetName(line1); | |
| static_text1.child_ids = {inline_box1.id}; | |
| inline_box1.role = ax::mojom::Role::kInlineTextBox; | |
| inline_box1.AddState(ax::mojom::State::kEditable); | |
| inline_box1.SetName(line1); | |
| inline_box1.AddIntListAttribute(ax::mojom::IntListAttribute::kWordStarts, | |
| {0, 4, 8}); | |
| line_break1.role = ax::mojom::Role::kLineBreak; | |
| line_break1.AddState(ax::mojom::State::kEditable); | |
| line_break1.SetName("\n"); | |
| inline_box1.AddIntAttribute(ax::mojom::IntAttribute::kNextOnLineId, | |
| line_break1.id); | |
| line_break1.AddIntAttribute(ax::mojom::IntAttribute::kPreviousOnLineId, | |
| inline_box1.id); | |
| static_text2.role = ax::mojom::Role::kStaticText; | |
| static_text2.AddState(ax::mojom::State::kEditable); | |
| static_text2.SetName(line2); | |
| static_text2.child_ids = {inline_box2.id}; | |
| inline_box2.role = ax::mojom::Role::kInlineTextBox; | |
| inline_box2.AddState(ax::mojom::State::kEditable); | |
| inline_box2.SetName(line2); | |
| inline_box2.AddIntListAttribute(ax::mojom::IntListAttribute::kWordStarts, | |
| {0, 5, 10}); | |
| line_break2.role = ax::mojom::Role::kLineBreak; | |
| line_break2.AddState(ax::mojom::State::kEditable); | |
| line_break2.SetName("\n"); | |
| line_break2.AddBoolAttribute(ax::mojom::BoolAttribute::kIsLineBreakingObject, | |
| true); | |
| inline_box2.AddIntAttribute(ax::mojom::IntAttribute::kNextOnLineId, | |
| line_break2.id); | |
| line_break2.AddIntAttribute(ax::mojom::IntAttribute::kPreviousOnLineId, | |
| inline_box2.id); | |
| static_text3.role = ax::mojom::Role::kStaticText; | |
| static_text3.AddState(ax::mojom::State::kEditable); | |
| static_text3.SetName(line3); | |
| static_text3.child_ids = {inline_box3.id}; | |
| inline_box3.role = ax::mojom::Role::kInlineTextBox; | |
| inline_box3.AddState(ax::mojom::State::kEditable); | |
| inline_box3.SetName(line3); | |
| inline_box3.AddIntListAttribute(ax::mojom::IntListAttribute::kWordStarts, | |
| {0, 6, 12}); | |
| std::unique_ptr<BrowserAccessibilityManager> manager( | |
| BrowserAccessibilityManager::Create( | |
| MakeAXTreeUpdateForTesting(root, text_field, text_container, | |
| static_text1, inline_box1, line_break1, | |
| static_text2, inline_box2, line_break2, | |
| static_text3, inline_box3), | |
| node_id_delegate_, test_browser_accessibility_delegate_.get())); | |
| BrowserAccessibilityWin* root_obj = | |
| ToBrowserAccessibilityWin(manager->GetBrowserAccessibilityRoot()); | |
| ASSERT_NE(nullptr, root_obj); | |
| ASSERT_EQ(1U, root_obj->PlatformChildCount()); | |
| BrowserAccessibilityComWin* text_field_obj = | |
| ToBrowserAccessibilityComWin(root_obj->PlatformGetChild(0)); | |
| ASSERT_NE(nullptr, text_field_obj); | |
| LONG text_len; | |
| EXPECT_EQ(S_OK, text_field_obj->get_nCharacters(&text_len)); | |
| base::win::ScopedBstr text; | |
| EXPECT_EQ(S_OK, text_field_obj->get_text(0, text_len, text.Receive())); | |
| EXPECT_EQ(text_value, base::WideToUTF8(std::wstring(text.Get()))); | |
| text.Reset(); | |
| EXPECT_EQ(S_OK, text_field_obj->get_text(0, 4, text.Receive())); | |
| EXPECT_STREQ(L"One ", text.Get()); | |
| text.Reset(); | |
| EXPECT_IA2_TEXT_AT_OFFSET(text_field_obj, 1, IA2_TEXT_BOUNDARY_CHAR, | |
| /*expected_hr=*/S_OK, /*start=*/1, /*end=*/2, | |
| /*text=*/L"n"); | |
| EXPECT_IA2_TEXT_AT_OFFSET(text_field_obj, text_len, IA2_TEXT_BOUNDARY_CHAR, | |
| /*expected_hr=*/E_INVALIDARG, /*start=*/0, | |
| /*end=*/0, | |
| /*text=*/nullptr); | |
| EXPECT_IA2_TEXT_AT_OFFSET(text_field_obj, text_len, IA2_TEXT_BOUNDARY_WORD, | |
| /*expected_hr=*/E_INVALIDARG, /*start=*/0, | |
| /*end=*/0, | |
| /*text=*/nullptr); | |
| EXPECT_IA2_TEXT_AT_OFFSET(text_field_obj, 1, IA2_TEXT_BOUNDARY_WORD, | |
| /*expected_hr=*/S_OK, /*start=*/0, /*end=*/4, | |
| /*text=*/L"One "); | |
| EXPECT_IA2_TEXT_AT_OFFSET(text_field_obj, 6, IA2_TEXT_BOUNDARY_WORD, | |
| /*expected_hr=*/S_OK, /*start=*/4, /*end=*/8, | |
| /*text=*/L"two "); | |
| EXPECT_IA2_TEXT_AT_OFFSET(text_field_obj, text_len - 1, | |
| IA2_TEXT_BOUNDARY_WORD, | |
| /*expected_hr=*/S_OK, /*start=*/42, /*end=*/47, | |
| /*text=*/L"nine."); | |
| EXPECT_IA2_TEXT_AT_OFFSET(text_field_obj, 1, IA2_TEXT_BOUNDARY_LINE, | |
| /*expected_hr=*/S_OK, /*start=*/0, /*end=*/15, | |
| /*text=*/L"One two three.\n"); | |
| EXPECT_IA2_TEXT_AT_OFFSET(text_field_obj, text_len, IA2_TEXT_BOUNDARY_LINE, | |
| /*expected_hr=*/S_OK, /*start=*/30, /*end=*/47, | |
| /*text=*/L"Seven eight nine."); | |
| EXPECT_IA2_TEXT_AT_OFFSET(text_field_obj, 1, IA2_TEXT_BOUNDARY_PARAGRAPH, | |
| /*expected_hr=*/S_OK, /*start=*/0, /*end=*/15, | |
| /*text=*/L"One two three.\n"); | |
| EXPECT_IA2_TEXT_AT_OFFSET(text_field_obj, 29, IA2_TEXT_BOUNDARY_PARAGRAPH, | |
| /*expected_hr=*/S_OK, /*start=*/15, /*end=*/30, | |
| /*text=*/L"Four five six.\n"); | |
| EXPECT_IA2_TEXT_AT_OFFSET(text_field_obj, 30, IA2_TEXT_BOUNDARY_PARAGRAPH, | |
| /*expected_hr=*/S_OK, /*start=*/30, /*end=*/47, | |
| /*text=*/L"Seven eight nine."); | |
| EXPECT_IA2_TEXT_AT_OFFSET(text_field_obj, text_len - 1, | |
| IA2_TEXT_BOUNDARY_PARAGRAPH, | |
| /*expected_hr=*/S_OK, /*start=*/30, /*end=*/47, | |
| /*text=*/L"Seven eight nine."); | |
| EXPECT_IA2_TEXT_AT_OFFSET(text_field_obj, text_len, | |
| IA2_TEXT_BOUNDARY_PARAGRAPH, | |
| /*expected_hr=*/E_INVALIDARG, /*start=*/0, | |
| /*end=*/0, | |
| /*text=*/nullptr); | |
| EXPECT_IA2_TEXT_BEFORE_OFFSET(text_field_obj, 0, IA2_TEXT_BOUNDARY_CHAR, | |
| /*expected_hr=*/S_FALSE, /*start=*/0, /*end=*/0, | |
| /*text=*/nullptr); | |
| EXPECT_IA2_TEXT_BEFORE_OFFSET(text_field_obj, 1, IA2_TEXT_BOUNDARY_CHAR, | |
| /*expected_hr=*/S_OK, /*start=*/0, /*end=*/1, | |
| /*text=*/L"O"); | |
| EXPECT_IA2_TEXT_BEFORE_OFFSET( | |
| text_field_obj, text_len, IA2_TEXT_BOUNDARY_CHAR, | |
| /*expected_hr=*/E_INVALIDARG, /*start=*/0, /*end=*/0, | |
| /*text=*/nullptr); | |
| EXPECT_IA2_TEXT_BEFORE_OFFSET( | |
| text_field_obj, text_len, IA2_TEXT_BOUNDARY_WORD, | |
| /*expected_hr=*/E_INVALIDARG, /*start=*/0, /*end=*/0, | |
| /*text=*/nullptr); | |
| EXPECT_IA2_TEXT_BEFORE_OFFSET(text_field_obj, 1, IA2_TEXT_BOUNDARY_WORD, | |
| /*expected_hr=*/S_FALSE, /*start=*/0, /*end=*/0, | |
| /*text=*/nullptr); | |
| EXPECT_IA2_TEXT_BEFORE_OFFSET(text_field_obj, 4, IA2_TEXT_BOUNDARY_WORD, | |
| /*expected_hr=*/S_OK, /*start=*/0, /*end=*/4, | |
| /*text=*/L"One "); | |
| EXPECT_IA2_TEXT_BEFORE_OFFSET(text_field_obj, 6, IA2_TEXT_BOUNDARY_WORD, | |
| /*expected_hr=*/S_OK, /*start=*/0, /*end=*/4, | |
| /*text=*/L"One "); | |
| EXPECT_IA2_TEXT_BEFORE_OFFSET(text_field_obj, text_len - 1, | |
| IA2_TEXT_BOUNDARY_WORD, | |
| /*expected_hr=*/S_OK, /*start=*/36, /*end=*/42, | |
| /*text=*/L"eight "); | |
| EXPECT_IA2_TEXT_BEFORE_OFFSET(text_field_obj, 0, IA2_TEXT_BOUNDARY_LINE, | |
| /*expected_hr=*/S_FALSE, /*start=*/0, /*end=*/0, | |
| /*text=*/nullptr); | |
| EXPECT_IA2_TEXT_BEFORE_OFFSET(text_field_obj, text_len - 1, | |
| IA2_TEXT_BOUNDARY_LINE, | |
| /*expected_hr=*/S_OK, /*start=*/15, /*end=*/30, | |
| /*text=*/L"Four five six.\n"); | |
| EXPECT_IA2_TEXT_BEFORE_OFFSET(text_field_obj, 18, IA2_TEXT_BOUNDARY_PARAGRAPH, | |
| /*expected_hr=*/S_OK, /*start=*/0, /*end=*/15, | |
| /*text=*/L"One two three.\n"); | |
| EXPECT_IA2_TEXT_BEFORE_OFFSET(text_field_obj, 29, IA2_TEXT_BOUNDARY_PARAGRAPH, | |
| /*expected_hr=*/S_OK, /*start=*/0, /*end=*/15, | |
| /*text=*/L"One two three.\n"); | |
| EXPECT_IA2_TEXT_BEFORE_OFFSET(text_field_obj, 30, IA2_TEXT_BOUNDARY_PARAGRAPH, | |
| /*expected_hr=*/S_OK, /*start=*/15, /*end=*/30, | |
| /*text=*/L"Four five six.\n"); | |
| EXPECT_IA2_TEXT_BEFORE_OFFSET(text_field_obj, text_len - 1, | |
| IA2_TEXT_BOUNDARY_PARAGRAPH, | |
| /*expected_hr=*/S_OK, /*start=*/15, /*end=*/30, | |
| /*text=*/L"Four five six.\n"); | |
| EXPECT_IA2_TEXT_AFTER_OFFSET(text_field_obj, 0, IA2_TEXT_BOUNDARY_CHAR, | |
| /*expected_hr=*/S_OK, /*start=*/1, /*end=*/2, | |
| /*text=*/L"n"); | |
| EXPECT_IA2_TEXT_AFTER_OFFSET(text_field_obj, 1, IA2_TEXT_BOUNDARY_CHAR, | |
| /*expected_hr=*/S_OK, /*start=*/2, /*end=*/3, | |
| /*text=*/L"e"); | |
| EXPECT_IA2_TEXT_AFTER_OFFSET(text_field_obj, text_len, IA2_TEXT_BOUNDARY_CHAR, | |
| /*expected_hr=*/E_INVALIDARG, /*start=*/0, | |
| /*end=*/0, | |
| /*text=*/nullptr); | |
| EXPECT_IA2_TEXT_AFTER_OFFSET(text_field_obj, 1, IA2_TEXT_BOUNDARY_WORD, | |
| /*expected_hr=*/S_OK, /*start=*/4, /*end=*/8, | |
| /*text=*/L"two "); | |
| EXPECT_IA2_TEXT_AFTER_OFFSET(text_field_obj, 6, IA2_TEXT_BOUNDARY_WORD, | |
| /*expected_hr=*/S_OK, /*start=*/8, /*end=*/15, | |
| /*text=*/L"three.\n"); | |
| EXPECT_IA2_TEXT_AFTER_OFFSET(text_field_obj, text_len, IA2_TEXT_BOUNDARY_WORD, | |
| /*expected_hr=*/E_INVALIDARG, /*start=*/0, | |
| /*end=*/0, | |
| /*text=*/nullptr); | |
| EXPECT_IA2_TEXT_AFTER_OFFSET(text_field_obj, 0, IA2_TEXT_BOUNDARY_LINE, | |
| /*expected_hr=*/S_OK, /*start=*/15, /*end=*/30, | |
| /*text=*/L"Four five six.\n"); | |
| EXPECT_IA2_TEXT_AFTER_OFFSET(text_field_obj, text_len - 1, | |
| IA2_TEXT_BOUNDARY_LINE, | |
| /*expected_hr=*/S_FALSE, /*start=*/0, /*end=*/0, | |
| /*text=*/nullptr); | |
| EXPECT_IA2_TEXT_AFTER_OFFSET(text_field_obj, 18, IA2_TEXT_BOUNDARY_PARAGRAPH, | |
| /*expected_hr=*/S_OK, /*start=*/30, /*end=*/47, | |
| /*text=*/L"Seven eight nine."); | |
| EXPECT_IA2_TEXT_AFTER_OFFSET(text_field_obj, 29, IA2_TEXT_BOUNDARY_PARAGRAPH, | |
| /*expected_hr=*/S_OK, /*start=*/30, /*end=*/47, | |
| /*text=*/L"Seven eight nine."); | |
| EXPECT_IA2_TEXT_AFTER_OFFSET(text_field_obj, text_len - 1, | |
| IA2_TEXT_BOUNDARY_PARAGRAPH, | |
| /*expected_hr=*/S_FALSE, /*start=*/0, /*end=*/0, | |
| /*text=*/nullptr); | |
| EXPECT_EQ(S_OK, text_field_obj->get_text(0, IA2_TEXT_OFFSET_LENGTH, | |
| text.Receive())); | |
| EXPECT_EQ(text_value, base::WideToUTF8(std::wstring(text.Get()))); | |
| EXPECT_IA2_TEXT_AT_OFFSET(text_field_obj, 14, IA2_TEXT_BOUNDARY_WORD, | |
| /*expected_hr=*/S_OK, /*start=*/14, /*end=*/15, | |
| /*text=*/L"\n"); | |
| EXPECT_IA2_TEXT_AT_OFFSET(text_field_obj, 29, IA2_TEXT_BOUNDARY_WORD, | |
| /*expected_hr=*/S_OK, /*start=*/29, /*end=*/30, | |
| /*text=*/L"\n"); | |
| // Delete the manager and test that all BrowserAccessibility instances are | |
| // deleted. | |
| manager.reset(); | |
| } | |
| TEST_F(BrowserAccessibilityWinTest, TestWordBoundariesWithNewline) { | |
| // This test verifies that newline characters are treated as word boundaries | |
| // in IA2_TEXT_BOUNDARY_WORD. It's based on the following accessibility tree | |
| // dump from a textarea containing "Hi, how are you?\nHow's life?": | |
| // | |
| // Key nodes and attributes relevant to word boundaries: | |
| // | |
| // textField: | |
| // - value: 'Hi, how are you?<newline>How's life?' | |
| // - wordStarts: [0,4,8,12,17,23] | |
| // - wordEnds: [2,7,11,15,22,27] | |
| // - lineStarts: [0,17] | |
| // - lineEnds: [17,28] | |
| // | |
| // First line staticText: | |
| // - name: 'Hi, how are you?' | |
| // - wordStarts: [0,4,8,12] | |
| // - wordEnds: [2,7,11,15] | |
| // | |
| // First line inlineTextBox: | |
| // - name: 'Hi, how are you?' | |
| // - wordStarts: [0,2,4,8,12,15] | |
| // - wordEnds: [2,3,7,11,15,16] | |
| // | |
| // lineBreak: | |
| // - name: '<newline>' | |
| // - previousOnLineId: inlineTextBox:"Hi, how are you?" | |
| // | |
| // Second line staticText: | |
| // - name: 'How's life?' | |
| // - wordStarts: [0,6] | |
| // - wordEnds: [5,10] | |
| // | |
| // Second line inlineTextBox: | |
| // - name: 'How's life?' | |
| // - wordStarts: [0,6,10] | |
| // - wordEnds: [5,10,11] | |
| std::string line1 = "Hi, how are you?"; | |
| std::string line2 = "How's life?"; | |
| std::string text_value = line1 + '\n' + line2; | |
| AXNodeData root; | |
| root.id = 1; | |
| AXNodeData text_field; | |
| text_field.id = 2; | |
| AXNodeData generic_container; | |
| generic_container.id = 3; | |
| AXNodeData static_text1; | |
| static_text1.id = 4; | |
| AXNodeData inline_box1; | |
| inline_box1.id = 5; | |
| AXNodeData line_break; | |
| line_break.id = 6; | |
| AXNodeData static_text2; | |
| static_text2.id = 7; | |
| AXNodeData inline_box2; | |
| inline_box2.id = 8; | |
| root.role = ax::mojom::Role::kRootWebArea; | |
| root.child_ids = {text_field.id}; | |
| text_field.role = ax::mojom::Role::kTextField; | |
| text_field.AddState(ax::mojom::State::kEditable); | |
| text_field.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, | |
| "textarea"); | |
| text_field.SetValue(text_value); | |
| text_field.AddState(ax::mojom::State::kMultiline); | |
| text_field.AddIntListAttribute(ax::mojom::IntListAttribute::kLineStarts, | |
| {0, 17}); | |
| text_field.AddIntListAttribute(ax::mojom::IntListAttribute::kWordStarts, | |
| {0, 4, 8, 12, 17, 23}); | |
| text_field.AddIntListAttribute(ax::mojom::IntListAttribute::kWordEnds, | |
| {2, 7, 11, 15, 22, 27}); | |
| text_field.AddIntListAttribute(ax::mojom::IntListAttribute::kLineEnds, | |
| {17, 28}); | |
| text_field.child_ids = {generic_container.id}; | |
| generic_container.role = ax::mojom::Role::kGenericContainer; | |
| generic_container.AddState(ax::mojom::State::kIgnored); | |
| generic_container.child_ids = {static_text1.id, line_break.id, | |
| static_text2.id}; | |
| static_text1.role = ax::mojom::Role::kStaticText; | |
| static_text1.AddState(ax::mojom::State::kEditable); | |
| static_text1.SetName(line1); | |
| static_text1.child_ids = {inline_box1.id}; | |
| static_text1.AddIntListAttribute(ax::mojom::IntListAttribute::kWordStarts, | |
| {0, 4, 8, 12}); | |
| static_text1.AddIntListAttribute(ax::mojom::IntListAttribute::kWordEnds, | |
| {2, 7, 11, 15}); | |
| inline_box1.role = ax::mojom::Role::kInlineTextBox; | |
| inline_box1.AddState(ax::mojom::State::kEditable); | |
| inline_box1.SetName(line1); | |
| inline_box1.AddIntListAttribute(ax::mojom::IntListAttribute::kWordStarts, | |
| {0, 2, 4, 8, 12, 15}); | |
| inline_box1.AddIntListAttribute(ax::mojom::IntListAttribute::kWordEnds, | |
| {2, 3, 7, 11, 15, 16}); | |
| 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("\n"); | |
| line_break.AddBoolAttribute(ax::mojom::BoolAttribute::kIsLineBreakingObject, | |
| true); | |
| line_break.AddIntAttribute(ax::mojom::IntAttribute::kPreviousOnLineId, | |
| inline_box1.id); | |
| static_text2.role = ax::mojom::Role::kStaticText; | |
| static_text2.AddState(ax::mojom::State::kEditable); | |
| static_text2.SetName(line2); | |
| static_text2.child_ids = {inline_box2.id}; | |
| static_text2.AddIntListAttribute(ax::mojom::IntListAttribute::kWordStarts, | |
| {0, 6}); | |
| static_text2.AddIntListAttribute(ax::mojom::IntListAttribute::kWordEnds, | |
| {5, 10}); | |
| inline_box2.role = ax::mojom::Role::kInlineTextBox; | |
| inline_box2.AddState(ax::mojom::State::kEditable); | |
| inline_box2.SetName(line2); | |
| inline_box2.AddIntListAttribute(ax::mojom::IntListAttribute::kWordStarts, | |
| {0, 6, 10}); | |
| inline_box2.AddIntListAttribute(ax::mojom::IntListAttribute::kWordEnds, | |
| {5, 10, 11}); | |
| std::unique_ptr<BrowserAccessibilityManager> manager( | |
| BrowserAccessibilityManager::Create( | |
| MakeAXTreeUpdateForTesting(root, text_field, generic_container, | |
| static_text1, inline_box1, line_break, | |
| static_text2, inline_box2), | |
| node_id_delegate_, test_browser_accessibility_delegate_.get())); | |
| BrowserAccessibilityWin* root_obj = | |
| ToBrowserAccessibilityWin(manager->GetBrowserAccessibilityRoot()); | |
| ASSERT_NE(nullptr, root_obj); | |
| ASSERT_EQ(1U, root_obj->PlatformChildCount()); | |
| BrowserAccessibilityComWin* text_field_obj = | |
| ToBrowserAccessibilityComWin(root_obj->PlatformGetChild(0)); | |
| ASSERT_NE(nullptr, text_field_obj); | |
| LONG text_len; | |
| EXPECT_EQ(S_OK, text_field_obj->get_nCharacters(&text_len)); | |
| EXPECT_EQ(28, text_len); | |
| base::win::ScopedBstr text; | |
| EXPECT_EQ(S_OK, text_field_obj->get_text(0, text_len, text.Receive())); | |
| EXPECT_EQ(text_value, base::WideToUTF8(std::wstring(text.Get()))); | |
| text.Reset(); | |
| EXPECT_IA2_TEXT_AT_OFFSET(text_field_obj, 0, IA2_TEXT_BOUNDARY_WORD, S_OK, 0, | |
| 2, L"Hi"); | |
| EXPECT_IA2_TEXT_AT_OFFSET(text_field_obj, 2, IA2_TEXT_BOUNDARY_WORD, S_OK, 2, | |
| 4, L", "); | |
| EXPECT_IA2_TEXT_AT_OFFSET(text_field_obj, 4, IA2_TEXT_BOUNDARY_WORD, S_OK, 4, | |
| 8, L"how "); | |
| EXPECT_IA2_TEXT_AT_OFFSET(text_field_obj, 8, IA2_TEXT_BOUNDARY_WORD, S_OK, 8, | |
| 12, L"are "); | |
| EXPECT_IA2_TEXT_AT_OFFSET(text_field_obj, 12, IA2_TEXT_BOUNDARY_WORD, S_OK, | |
| 12, 15, L"you"); | |
| EXPECT_IA2_TEXT_AT_OFFSET(text_field_obj, 15, IA2_TEXT_BOUNDARY_WORD, S_OK, | |
| 15, 17, L"?\n"); | |
| EXPECT_IA2_TEXT_BEFORE_OFFSET(text_field_obj, 16, IA2_TEXT_BOUNDARY_WORD, | |
| S_OK, 15, 16, L"?"); | |
| EXPECT_IA2_TEXT_AT_OFFSET(text_field_obj, 16, IA2_TEXT_BOUNDARY_WORD, S_OK, | |
| 16, 17, L"\n"); | |
| EXPECT_IA2_TEXT_BEFORE_OFFSET(text_field_obj, 17, IA2_TEXT_BOUNDARY_WORD, | |
| S_OK, 16, 17, L"\n"); | |
| EXPECT_IA2_TEXT_AFTER_OFFSET(text_field_obj, 16, IA2_TEXT_BOUNDARY_WORD, S_OK, | |
| 17, 23, L"How's "); | |
| EXPECT_IA2_TEXT_AT_OFFSET(text_field_obj, 17, IA2_TEXT_BOUNDARY_WORD, S_OK, | |
| 17, 23, L"How's "); | |
| EXPECT_IA2_TEXT_AT_OFFSET(text_field_obj, 23, IA2_TEXT_BOUNDARY_WORD, S_OK, | |
| 23, 27, L"life"); | |
| EXPECT_IA2_TEXT_AT_OFFSET(text_field_obj, 27, IA2_TEXT_BOUNDARY_WORD, S_OK, | |
| 27, 28, L"?"); | |
| EXPECT_IA2_TEXT_AT_OFFSET(text_field_obj, -1, IA2_TEXT_BOUNDARY_WORD, | |
| E_INVALIDARG, 0, 0, nullptr); | |
| EXPECT_IA2_TEXT_AT_OFFSET(text_field_obj, 28, IA2_TEXT_BOUNDARY_WORD, | |
| E_INVALIDARG, 0, 0, nullptr); | |
| EXPECT_IA2_TEXT_AT_OFFSET(text_field_obj, 17, IA2_TEXT_BOUNDARY_WORD, S_OK, | |
| 17, 23, L"How's "); | |
| manager.reset(); | |
| } | |
| TEST_F(BrowserAccessibilityWinTest, TestSimpleHypertext) { | |
| const std::string text1_name = "One two three."; | |
| const std::string text2_name = " Four five six."; | |
| const LONG text_name_len = text1_name.length() + text2_name.length(); | |
| AXNodeData text1; | |
| text1.id = 11; | |
| text1.role = ax::mojom::Role::kStaticText; | |
| text1.SetName(text1_name); | |
| AXNodeData text2; | |
| text2.id = 12; | |
| text2.role = ax::mojom::Role::kStaticText; | |
| text2.SetName(text2_name); | |
| AXNodeData root; | |
| root.id = 1; | |
| root.role = ax::mojom::Role::kRootWebArea; | |
| root.child_ids.push_back(text1.id); | |
| root.child_ids.push_back(text2.id); | |
| std::unique_ptr<BrowserAccessibilityManager> manager( | |
| BrowserAccessibilityManager::Create( | |
| MakeAXTreeUpdateForTesting(root, text1, text2), node_id_delegate_, | |
| test_browser_accessibility_delegate_.get())); | |
| BrowserAccessibilityComWin* root_obj = | |
| ToBrowserAccessibilityWin(manager->GetBrowserAccessibilityRoot()) | |
| ->GetCOM(); | |
| LONG text_len; | |
| EXPECT_EQ(S_OK, root_obj->get_nCharacters(&text_len)); | |
| EXPECT_EQ(text_name_len, text_len); | |
| base::win::ScopedBstr text; | |
| EXPECT_EQ(S_OK, root_obj->get_text(0, text_name_len, text.Receive())); | |
| EXPECT_EQ(text1_name + text2_name, | |
| base::WideToUTF8(std::wstring(text.Get()))); | |
| LONG hyperlink_count; | |
| EXPECT_EQ(S_OK, root_obj->get_nHyperlinks(&hyperlink_count)); | |
| EXPECT_EQ(0, hyperlink_count); | |
| Microsoft::WRL::ComPtr<IAccessibleHyperlink> hyperlink; | |
| EXPECT_EQ(E_INVALIDARG, root_obj->get_hyperlink(-1, &hyperlink)); | |
| EXPECT_EQ(E_INVALIDARG, root_obj->get_hyperlink(0, &hyperlink)); | |
| EXPECT_EQ(E_INVALIDARG, root_obj->get_hyperlink(text_name_len, &hyperlink)); | |
| EXPECT_EQ(E_INVALIDARG, | |
| root_obj->get_hyperlink(text_name_len + 1, &hyperlink)); | |
| LONG hyperlink_index; | |
| EXPECT_EQ(S_FALSE, root_obj->get_hyperlinkIndex(0, &hyperlink_index)); | |
| EXPECT_EQ(-1, hyperlink_index); | |
| // Invalid arguments should not be modified. | |
| hyperlink_index = -2; | |
| EXPECT_EQ(E_INVALIDARG, | |
| root_obj->get_hyperlinkIndex(text_name_len, &hyperlink_index)); | |
| EXPECT_EQ(-2, hyperlink_index); | |
| EXPECT_EQ(E_INVALIDARG, root_obj->get_hyperlinkIndex(-1, &hyperlink_index)); | |
| EXPECT_EQ(-2, hyperlink_index); | |
| EXPECT_EQ(E_INVALIDARG, | |
| root_obj->get_hyperlinkIndex(text_name_len + 1, &hyperlink_index)); | |
| EXPECT_EQ(-2, hyperlink_index); | |
| manager.reset(); | |
| } | |
| TEST_F(BrowserAccessibilityWinTest, TestComplexHypertext) { | |
| const std::u16string text1_name = u"One two three."; | |
| const std::u16string combo_box_name = u"City:"; | |
| const std::u16string combo_box_value = u"Happyland"; | |
| const std::u16string text2_name = u" Four five six."; | |
| const std::u16string check_box_name = u"I agree"; | |
| const std::u16string check_box_value = u"Checked"; | |
| const std::u16string button_text_name = u"Red"; | |
| const std::u16string link_text_name = u"Blue"; | |
| // Each control (combo / check box, button and link) will be represented by an | |
| // embedded object character. | |
| const std::u16string embed(1, AXPlatformNodeBase::kEmbeddedCharacter); | |
| const std::u16string root_hypertext = | |
| text1_name + embed + text2_name + embed + embed + embed; | |
| const LONG root_hypertext_len = root_hypertext.length(); | |
| AXNodeData text1; | |
| text1.id = 11; | |
| text1.role = ax::mojom::Role::kStaticText; | |
| text1.SetName(base::UTF16ToUTF8(text1_name)); | |
| AXNodeData combo_box; | |
| combo_box.id = 12; | |
| combo_box.role = ax::mojom::Role::kTextFieldWithComboBox; | |
| combo_box.AddState(ax::mojom::State::kEditable); | |
| combo_box.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, "input"); | |
| combo_box.AddStringAttribute(ax::mojom::StringAttribute::kInputType, "text"); | |
| combo_box.SetName(base::UTF16ToUTF8(combo_box_name)); | |
| combo_box.SetValue(base::UTF16ToUTF8(combo_box_value)); | |
| AXNodeData text2; | |
| text2.id = 13; | |
| text2.role = ax::mojom::Role::kStaticText; | |
| text2.SetName(base::UTF16ToUTF8(text2_name)); | |
| AXNodeData check_box; | |
| check_box.id = 14; | |
| check_box.role = ax::mojom::Role::kCheckBox; | |
| check_box.SetCheckedState(ax::mojom::CheckedState::kTrue); | |
| check_box.SetName(base::UTF16ToUTF8(check_box_name)); | |
| // ARIA checkbox where the name is derived from its inner text. | |
| check_box.SetNameFrom(ax::mojom::NameFrom::kContents); | |
| check_box.SetValue(base::UTF16ToUTF8(check_box_value)); | |
| AXNodeData button, button_text; | |
| button.id = 15; | |
| button_text.id = 17; | |
| button.role = ax::mojom::Role::kButton; | |
| button.SetName(base::UTF16ToUTF8(button_text_name)); | |
| button.SetNameFrom(ax::mojom::NameFrom::kContents); | |
| // A single text child with the same name should be hidden from accessibility | |
| // to prevent double speaking. | |
| button_text.role = ax::mojom::Role::kStaticText; | |
| button_text.SetName(base::UTF16ToUTF8(button_text_name)); | |
| button.child_ids.push_back(button_text.id); | |
| AXNodeData link, link_text; | |
| link.id = 16; | |
| link_text.id = 18; | |
| link.role = ax::mojom::Role::kLink; | |
| link_text.role = ax::mojom::Role::kStaticText; | |
| link_text.SetName(base::UTF16ToUTF8(link_text_name)); | |
| link.child_ids.push_back(link_text.id); | |
| AXNodeData root; | |
| root.id = 1; | |
| root.role = ax::mojom::Role::kRootWebArea; | |
| root.child_ids.push_back(text1.id); | |
| root.child_ids.push_back(combo_box.id); | |
| root.child_ids.push_back(text2.id); | |
| root.child_ids.push_back(check_box.id); | |
| root.child_ids.push_back(button.id); | |
| root.child_ids.push_back(link.id); | |
| std::unique_ptr<BrowserAccessibilityManager> manager( | |
| BrowserAccessibilityManager::Create( | |
| MakeAXTreeUpdateForTesting(root, text1, combo_box, text2, check_box, | |
| button, button_text, link, link_text), | |
| node_id_delegate_, test_browser_accessibility_delegate_.get())); | |
| BrowserAccessibilityComWin* root_obj = | |
| ToBrowserAccessibilityWin(manager->GetBrowserAccessibilityRoot()) | |
| ->GetCOM(); | |
| LONG text_len; | |
| EXPECT_EQ(S_OK, root_obj->get_nCharacters(&text_len)); | |
| EXPECT_EQ(root_hypertext_len, text_len); | |
| base::win::ScopedBstr text; | |
| EXPECT_EQ(S_OK, root_obj->get_text(0, root_hypertext_len, text.Receive())); | |
| EXPECT_EQ(root_hypertext, base::WideToUTF16(text.Get())); | |
| text.Reset(); | |
| LONG hyperlink_count; | |
| EXPECT_EQ(S_OK, root_obj->get_nHyperlinks(&hyperlink_count)); | |
| EXPECT_EQ(4, hyperlink_count); | |
| Microsoft::WRL::ComPtr<IAccessibleHyperlink> hyperlink; | |
| Microsoft::WRL::ComPtr<IAccessibleText> hypertext; | |
| EXPECT_EQ(E_INVALIDARG, root_obj->get_hyperlink(-1, &hyperlink)); | |
| EXPECT_EQ(E_INVALIDARG, root_obj->get_hyperlink(4, &hyperlink)); | |
| // Get the text of the combo box. | |
| // It should be its value. | |
| EXPECT_EQ(S_OK, root_obj->get_hyperlink(0, &hyperlink)); | |
| EXPECT_EQ(S_OK, hyperlink.As(&hypertext)); | |
| EXPECT_EQ(S_OK, | |
| hypertext->get_text(0, IA2_TEXT_OFFSET_LENGTH, text.Receive())); | |
| EXPECT_EQ(combo_box_value, base::WideToUTF16(text.Get())); | |
| text.Reset(); | |
| hyperlink.Reset(); | |
| hypertext.Reset(); | |
| // Get the text of the check box. | |
| // It should be its name. | |
| EXPECT_EQ(S_OK, root_obj->get_hyperlink(1, &hyperlink)); | |
| EXPECT_EQ(S_OK, hyperlink.As(&hypertext)); | |
| EXPECT_EQ(S_OK, | |
| hypertext->get_text(0, IA2_TEXT_OFFSET_LENGTH, text.Receive())); | |
| EXPECT_EQ(check_box_name, base::WideToUTF16(text.Get())); | |
| text.Reset(); | |
| hyperlink.Reset(); | |
| hypertext.Reset(); | |
| // Get the text of the button. | |
| EXPECT_EQ(S_OK, root_obj->get_hyperlink(2, &hyperlink)); | |
| EXPECT_EQ(S_OK, hyperlink.As(&hypertext)); | |
| EXPECT_EQ(S_OK, | |
| hypertext->get_text(0, IA2_TEXT_OFFSET_LENGTH, text.Receive())); | |
| EXPECT_EQ(button_text_name, base::WideToUTF16(text.Get())); | |
| text.Reset(); | |
| hyperlink.Reset(); | |
| hypertext.Reset(); | |
| // Get the text of the link. | |
| EXPECT_EQ(S_OK, root_obj->get_hyperlink(3, &hyperlink)); | |
| EXPECT_EQ(S_OK, hyperlink.As(&hypertext)); | |
| EXPECT_EQ(S_OK, hypertext->get_text(0, 4, text.Receive())); | |
| EXPECT_EQ(link_text_name, base::WideToUTF16(text.Get())); | |
| text.Reset(); | |
| hyperlink.Reset(); | |
| hypertext.Reset(); | |
| LONG hyperlink_index; | |
| EXPECT_EQ(S_FALSE, root_obj->get_hyperlinkIndex(0, &hyperlink_index)); | |
| EXPECT_EQ(-1, hyperlink_index); | |
| // Invalid arguments should not be modified. | |
| hyperlink_index = -2; | |
| EXPECT_EQ(E_INVALIDARG, | |
| root_obj->get_hyperlinkIndex(root_hypertext_len, &hyperlink_index)); | |
| EXPECT_EQ(-2, hyperlink_index); | |
| EXPECT_EQ(S_OK, root_obj->get_hyperlinkIndex(14, &hyperlink_index)); | |
| EXPECT_EQ(0, hyperlink_index); | |
| EXPECT_EQ(S_OK, root_obj->get_hyperlinkIndex(30, &hyperlink_index)); | |
| EXPECT_EQ(1, hyperlink_index); | |
| EXPECT_EQ(S_OK, root_obj->get_hyperlinkIndex(31, &hyperlink_index)); | |
| EXPECT_EQ(2, hyperlink_index); | |
| EXPECT_EQ(S_OK, root_obj->get_hyperlinkIndex(32, &hyperlink_index)); | |
| EXPECT_EQ(3, hyperlink_index); | |
| manager.reset(); | |
| } | |
| TEST_F(BrowserAccessibilityWinTest, TestGetUIADirectChildrenInRange) { | |
| // Set up ax tree with the following structure: | |
| // | |
| // root___________________________________________________ | |
| // | | | | | | |
| // para1____ link1 (ignored) link2 button para2___________ | |
| // | | | | | | | | |
| // text1 text2 text3 text4 text_5 image text6 | |
| AXNodeData text1; | |
| text1.id = 111; | |
| text1.role = ax::mojom::Role::kStaticText; | |
| text1.SetName("One two three."); | |
| AXNodeData text2; | |
| text2.id = 112; | |
| text2.role = ax::mojom::Role::kStaticText; | |
| text2.SetName("Two three four."); | |
| AXNodeData text3; | |
| text3.id = 113; | |
| text3.role = ax::mojom::Role::kStaticText; | |
| text3.SetName("Three four five."); | |
| text3.AddState(ax::mojom::State::kIgnored); | |
| AXNodeData text4; | |
| text4.id = 114; | |
| text4.role = ax::mojom::Role::kStaticText; | |
| text4.SetName("four five six."); | |
| AXNodeData text5; | |
| text5.id = 115; | |
| text5.role = ax::mojom::Role::kStaticText; | |
| text5.SetName("five six seven."); | |
| AXNodeData image; | |
| image.id = 116; | |
| image.role = ax::mojom::Role::kImage; | |
| AXNodeData text6; | |
| text6.id = 117; | |
| text6.role = ax::mojom::Role::kStaticText; | |
| text6.SetName("six seven eight."); | |
| AXNodeData para1; | |
| para1.id = 11; | |
| para1.role = ax::mojom::Role::kParagraph; | |
| para1.child_ids = {text1.id, text2.id}; | |
| AXNodeData link1; | |
| link1.id = 12; | |
| link1.role = ax::mojom::Role::kLink; | |
| link1.child_ids = {text3.id}; | |
| link1.AddState(ax::mojom::State::kIgnored); | |
| AXNodeData link2; | |
| link2.id = 13; | |
| link2.role = ax::mojom::Role::kLink; | |
| link2.child_ids = {text4.id}; | |
| AXNodeData button; | |
| button.id = 14; | |
| button.role = ax::mojom::Role::kButton; | |
| AXNodeData para2; | |
| para2.id = 15; | |
| para2.role = ax::mojom::Role::kParagraph; | |
| para2.child_ids = {text5.id, image.id, text6.id}; | |
| AXNodeData root; | |
| root.id = 1; | |
| root.role = ax::mojom::Role::kRootWebArea; | |
| root.child_ids = {para1.id, link1.id, link2.id, button.id, para2.id}; | |
| std::unique_ptr<BrowserAccessibilityManager> manager( | |
| BrowserAccessibilityManager::Create( | |
| MakeAXTreeUpdateForTesting(root, para1, text1, text2, link1, text3, | |
| link2, text4, button, para2, text5, image, | |
| text6), | |
| node_id_delegate_, test_browser_accessibility_delegate_.get())); | |
| BrowserAccessibility* root_obj = manager->GetBrowserAccessibilityRoot(); | |
| BrowserAccessibility* para1_obj = manager->GetFromID(11); | |
| BrowserAccessibility* link2_obj = manager->GetFromID(13); | |
| BrowserAccessibility* button_obj = manager->GetFromID(14); | |
| BrowserAccessibility* para2_obj = manager->GetFromID(15); | |
| BrowserAccessibility* text1_obj = manager->GetFromID(111); | |
| BrowserAccessibility* text2_obj = manager->GetFromID(112); | |
| BrowserAccessibility* text3_obj = manager->GetFromID(113); | |
| BrowserAccessibility* text4_obj = manager->GetFromID(114); | |
| BrowserAccessibility* text5_obj = manager->GetFromID(115); | |
| BrowserAccessibility* image_obj = manager->GetFromID(116); | |
| BrowserAccessibility* text6_obj = manager->GetFromID(117); | |
| // When a range starts and end on a leaf node, no nodes should be returned. | |
| std::vector<gfx::NativeViewAccessible> descendants; | |
| std::vector<gfx::NativeViewAccessible> expected_descendants = {}; | |
| descendants = text1_obj->GetUIADirectChildrenInRange(text1_obj, text1_obj); | |
| EXPECT_NATIVE_VIEW_ACCESSIBLE_VECTOR_EQ(descendants, expected_descendants); | |
| descendants = text2_obj->GetUIADirectChildrenInRange(text2_obj, text2_obj); | |
| EXPECT_NATIVE_VIEW_ACCESSIBLE_VECTOR_EQ(descendants, expected_descendants); | |
| descendants = text3_obj->GetUIADirectChildrenInRange(text3_obj, text3_obj); | |
| EXPECT_NATIVE_VIEW_ACCESSIBLE_VECTOR_EQ(descendants, expected_descendants); | |
| descendants = text4_obj->GetUIADirectChildrenInRange(text4_obj, text4_obj); | |
| EXPECT_NATIVE_VIEW_ACCESSIBLE_VECTOR_EQ(descendants, expected_descendants); | |
| descendants = text5_obj->GetUIADirectChildrenInRange(text5_obj, text5_obj); | |
| EXPECT_NATIVE_VIEW_ACCESSIBLE_VECTOR_EQ(descendants, expected_descendants); | |
| descendants = image_obj->GetUIADirectChildrenInRange(image_obj, image_obj); | |
| EXPECT_NATIVE_VIEW_ACCESSIBLE_VECTOR_EQ(descendants, expected_descendants); | |
| descendants = text5_obj->GetUIADirectChildrenInRange(text6_obj, text6_obj); | |
| EXPECT_NATIVE_VIEW_ACCESSIBLE_VECTOR_EQ(descendants, expected_descendants); | |
| descendants = button_obj->GetUIADirectChildrenInRange(button_obj, button_obj); | |
| EXPECT_NATIVE_VIEW_ACCESSIBLE_VECTOR_EQ(descendants, expected_descendants); | |
| // When called on a range that spans the entire document, the unignored link | |
| // object and the button should be returned by the function as they are the | |
| // two UIA embedded objects that are direct children of the root node. | |
| descendants = root_obj->GetUIADirectChildrenInRange(root_obj, root_obj); | |
| expected_descendants = {link2_obj->GetNativeViewAccessible(), | |
| button_obj->GetNativeViewAccessible()}; | |
| EXPECT_NATIVE_VIEW_ACCESSIBLE_VECTOR_EQ(descendants, expected_descendants); | |
| // When called on a range that doesn't contain any UIA embedded object, | |
| // nothing should be returned from the function. | |
| descendants = para1_obj->GetUIADirectChildrenInRange(text2_obj, button_obj); | |
| expected_descendants = {}; | |
| EXPECT_NATIVE_VIEW_ACCESSIBLE_VECTOR_EQ(descendants, expected_descendants); | |
| // Validate that the function doesn't include objects that are outside of the | |
| // the range. In this example, the button object shouldn't be included. | |
| descendants = root_obj->GetUIADirectChildrenInRange(text2_obj, text4_obj); | |
| expected_descendants = {link2_obj->GetNativeViewAccessible()}; | |
| EXPECT_NATIVE_VIEW_ACCESSIBLE_VECTOR_EQ(descendants, expected_descendants); | |
| // Validates that it works on other nodes than the root one. | |
| descendants = para2_obj->GetUIADirectChildrenInRange(text5_obj, text6_obj); | |
| expected_descendants = {image_obj->GetNativeViewAccessible()}; | |
| EXPECT_NATIVE_VIEW_ACCESSIBLE_VECTOR_EQ(descendants, expected_descendants); | |
| manager.reset(); | |
| } | |
| TEST_F(BrowserAccessibilityWinTest, TestCreateEmptyDocument) { | |
| std::unique_ptr<BrowserAccessibilityManager> manager( | |
| new BrowserAccessibilityManagerWin( | |
| BrowserAccessibilityManagerWin::GetEmptyDocument(), node_id_delegate_, | |
| test_browser_accessibility_delegate_.get())); | |
| // Verify the root is as we expect by default. | |
| BrowserAccessibility* root = manager->GetBrowserAccessibilityRoot(); | |
| EXPECT_EQ(kInitialEmptyDocumentRootNodeID, root->GetId()); | |
| EXPECT_EQ(ax::mojom::Role::kRootWebArea, root->GetRole()); | |
| EXPECT_EQ(AXStates(0U), root->GetStates()); | |
| // Tree with a child textfield. | |
| AXNodeData tree1_1; | |
| tree1_1.id = 1; | |
| tree1_1.role = ax::mojom::Role::kRootWebArea; | |
| tree1_1.child_ids.push_back(2); | |
| AXNodeData tree1_2; | |
| tree1_2.id = 2; | |
| tree1_2.role = ax::mojom::Role::kTextField; | |
| tree1_2.AddState(ax::mojom::State::kEditable); | |
| tree1_2.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, "input"); | |
| tree1_2.AddStringAttribute(ax::mojom::StringAttribute::kInputType, "text"); | |
| // Process a load complete. | |
| AXUpdatesAndEvents event_bundle; | |
| event_bundle.updates.resize(1); | |
| event_bundle.updates[0].node_id_to_clear = root->GetId(); | |
| event_bundle.updates[0].root_id = tree1_1.id; | |
| event_bundle.updates[0].nodes.push_back(tree1_1); | |
| event_bundle.updates[0].nodes.push_back(tree1_2); | |
| ASSERT_TRUE(manager->OnAccessibilityEvents(event_bundle)); | |
| // The root for the initial empty document is replaced. | |
| root = manager->GetBrowserAccessibilityRoot(); | |
| BrowserAccessibility* acc1_2 = manager->GetFromID(2); | |
| EXPECT_EQ(ax::mojom::Role::kTextField, acc1_2->GetRole()); | |
| EXPECT_EQ(2, acc1_2->GetId()); | |
| // Tree with a child button. | |
| AXNodeData tree2_1; | |
| tree2_1.id = 1; | |
| tree2_1.role = ax::mojom::Role::kRootWebArea; | |
| tree2_1.child_ids.push_back(3); | |
| AXNodeData tree2_2; | |
| tree2_2.id = 3; | |
| tree2_2.role = ax::mojom::Role::kButton; | |
| event_bundle.updates[0].nodes.clear(); | |
| event_bundle.updates[0].node_id_to_clear = tree1_1.id; | |
| event_bundle.updates[0].root_id = tree2_1.id; | |
| event_bundle.updates[0].nodes.push_back(tree2_1); | |
| event_bundle.updates[0].nodes.push_back(tree2_2); | |
| // Unserialize the new tree update. | |
| ASSERT_TRUE(manager->OnAccessibilityEvents(event_bundle)); | |
| // Verify that the root has been cleared, not replaced. | |
| EXPECT_EQ(root, manager->GetBrowserAccessibilityRoot()); | |
| BrowserAccessibility* acc2_2 = manager->GetFromID(3); | |
| EXPECT_EQ(ax::mojom::Role::kButton, acc2_2->GetRole()); | |
| EXPECT_EQ(3, acc2_2->GetId()); | |
| // Ensure we properly cleaned up. | |
| manager.reset(); | |
| } | |
| int32_t GetUniqueId(BrowserAccessibility* accessibility) { | |
| BrowserAccessibilityWin* win_root = ToBrowserAccessibilityWin(accessibility); | |
| return win_root->GetCOM()->GetUniqueId(); | |
| } | |
| // This is a regression test for a bug where the initial empty document | |
| // loaded by a BrowserAccessibilityManagerWin couldn't be looked up by | |
| // its UniqueIDWin, because the AX Tree was loaded in | |
| // BrowserAccessibilityManager code before BrowserAccessibilityManagerWin | |
| // was initialized. | |
| TEST_F(BrowserAccessibilityWinTest, EmptyDocHasUniqueIdWin) { | |
| std::unique_ptr<BrowserAccessibilityManagerWin> manager( | |
| new BrowserAccessibilityManagerWin( | |
| BrowserAccessibilityManagerWin::GetEmptyDocument(), node_id_delegate_, | |
| test_browser_accessibility_delegate_.get())); | |
| // Verify the root is as we expect by default. | |
| BrowserAccessibility* root = manager->GetBrowserAccessibilityRoot(); | |
| EXPECT_EQ(kInitialEmptyDocumentRootNodeID, root->GetId()); | |
| EXPECT_EQ(ax::mojom::Role::kRootWebArea, root->GetRole()); | |
| EXPECT_EQ(AXStates(0U), root->GetStates()); | |
| BrowserAccessibilityWin* win_root = ToBrowserAccessibilityWin(root); | |
| AXPlatformNode* node = static_cast<AXPlatformNode*>( | |
| AXPlatformNodeWin::GetFromUniqueId(GetUniqueId(win_root))); | |
| AXPlatformNode* other_node = static_cast<AXPlatformNode*>(win_root->GetCOM()); | |
| ASSERT_EQ(node, other_node); | |
| } | |
| TEST_F(BrowserAccessibilityWinTest, TestIA2Attributes) { | |
| AXNodeData pseudo_before; | |
| pseudo_before.id = 2; | |
| pseudo_before.role = ax::mojom::Role::kGenericContainer; | |
| pseudo_before.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, | |
| "<pseudo:before>"); | |
| pseudo_before.AddStringAttribute(ax::mojom::StringAttribute::kDisplay, | |
| "none"); | |
| AXNodeData checkbox; | |
| checkbox.id = 3; | |
| checkbox.role = ax::mojom::Role::kCheckBox; | |
| checkbox.SetCheckedState(ax::mojom::CheckedState::kTrue); | |
| checkbox.SetName("Checkbox"); | |
| AXNodeData root; | |
| root.id = 1; | |
| root.role = ax::mojom::Role::kRootWebArea; | |
| root.AddState(ax::mojom::State::kFocusable); | |
| root.SetName("Document"); | |
| root.child_ids.push_back(2); | |
| root.child_ids.push_back(3); | |
| std::unique_ptr<BrowserAccessibilityManager> manager( | |
| BrowserAccessibilityManager::Create( | |
| MakeAXTreeUpdateForTesting(root, pseudo_before, checkbox), | |
| node_id_delegate_, test_browser_accessibility_delegate_.get())); | |
| ASSERT_NE(nullptr, manager->GetBrowserAccessibilityRoot()); | |
| BrowserAccessibilityWin* root_accessible = | |
| ToBrowserAccessibilityWin(manager->GetBrowserAccessibilityRoot()); | |
| ASSERT_NE(nullptr, root_accessible); | |
| ASSERT_EQ(2U, root_accessible->PlatformChildCount()); | |
| BrowserAccessibilityWin* pseudo_accessible = | |
| ToBrowserAccessibilityWin(root_accessible->PlatformGetChild(0)); | |
| ASSERT_NE(nullptr, pseudo_accessible); | |
| base::win::ScopedBstr attributes; | |
| HRESULT hr = | |
| pseudo_accessible->GetCOM()->get_attributes(attributes.Receive()); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_NE(nullptr, attributes.Get()); | |
| std::wstring attributes_str(attributes.Get(), attributes.Length()); | |
| EXPECT_EQ(L"display:none;tag:<pseudo\\:before>;", attributes_str); | |
| BrowserAccessibilityWin* checkbox_accessible = | |
| ToBrowserAccessibilityWin(root_accessible->PlatformGetChild(1)); | |
| ASSERT_NE(nullptr, checkbox_accessible); | |
| attributes.Reset(); | |
| hr = checkbox_accessible->GetCOM()->get_attributes(attributes.Receive()); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_NE(nullptr, attributes.Get()); | |
| attributes_str = std::wstring(attributes.Get(), attributes.Length()); | |
| EXPECT_EQ(L"checkable:true;name-from:attribute;explicit-name:true;", | |
| attributes_str); | |
| manager.reset(); | |
| } | |
| TEST_F(BrowserAccessibilityWinTest, TestValueAttributeInTextControls) { | |
| AXNodeData root; | |
| root.id = 1; | |
| root.role = ax::mojom::Role::kRootWebArea; | |
| AXNodeData combo_box, combo_box_text_container, combo_box_text; | |
| combo_box.id = 2; | |
| combo_box_text_container.id = 3; | |
| combo_box_text.id = 4; | |
| combo_box.role = ax::mojom::Role::kTextFieldWithComboBox; | |
| combo_box_text_container.role = ax::mojom::Role::kGenericContainer; | |
| combo_box_text.role = ax::mojom::Role::kStaticText; | |
| combo_box.SetName("Combo box:"); | |
| combo_box.SetValue("Combo box text"); | |
| combo_box_text.SetName("Combo box text"); | |
| combo_box.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, "input"); | |
| combo_box.AddStringAttribute(ax::mojom::StringAttribute::kInputType, "text"); | |
| combo_box.AddState(ax::mojom::State::kEditable); | |
| combo_box.AddState(ax::mojom::State::kFocusable); | |
| combo_box_text.AddState(ax::mojom::State::kEditable); | |
| combo_box.child_ids = {combo_box_text_container.id}; | |
| combo_box_text_container.child_ids = {combo_box_text.id}; | |
| AXNodeData search_box, search_box_text_container, search_box_text, new_line; | |
| search_box.id = 5; | |
| search_box_text_container.id = 6; | |
| search_box_text.id = 7; | |
| new_line.id = 8; | |
| search_box.role = ax::mojom::Role::kSearchBox; | |
| search_box_text_container.role = ax::mojom::Role::kGenericContainer; | |
| search_box_text.role = ax::mojom::Role::kStaticText; | |
| new_line.role = ax::mojom::Role::kLineBreak; | |
| search_box.SetName("Search for:"); | |
| search_box.SetValue("Search box text\n"); | |
| search_box_text.SetName("Search box text"); | |
| new_line.SetName("\n"); | |
| search_box.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, "input"); | |
| search_box.AddStringAttribute(ax::mojom::StringAttribute::kInputType, "text"); | |
| search_box.AddState(ax::mojom::State::kEditable); | |
| search_box.AddState(ax::mojom::State::kFocusable); | |
| search_box_text.AddState(ax::mojom::State::kEditable); | |
| new_line.AddState(ax::mojom::State::kEditable); | |
| search_box.child_ids = {search_box_text_container.id}; | |
| search_box_text_container.child_ids = {search_box_text.id, new_line.id}; | |
| AXNodeData text_field, text_field_text_container; | |
| text_field.id = 9; | |
| text_field.role = ax::mojom::Role::kTextField; | |
| text_field.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, "input"); | |
| text_field.AddStringAttribute(ax::mojom::StringAttribute::kInputType, "text"); | |
| text_field.AddState(ax::mojom::State::kEditable); | |
| text_field.AddState(ax::mojom::State::kFocusable); | |
| // Exposes a placeholder. The text container is otherwise empty. | |
| text_field.SetValue("Text field text"); | |
| text_field_text_container.id = 10; | |
| text_field_text_container.role = ax::mojom::Role::kGenericContainer; | |
| text_field.child_ids.push_back(text_field_text_container.id); | |
| AXNodeData link, link_text; | |
| link.id = 11; | |
| link_text.id = 12; | |
| link.role = ax::mojom::Role::kLink; | |
| link_text.role = ax::mojom::Role::kStaticText; | |
| link_text.SetName("Link text"); | |
| link.child_ids.push_back(link_text.id); | |
| AXNodeData slider, slider_text; | |
| slider.id = 13; | |
| slider_text.id = 14; | |
| slider.role = ax::mojom::Role::kSlider; | |
| slider_text.role = ax::mojom::Role::kStaticText; | |
| slider.AddFloatAttribute(ax::mojom::FloatAttribute::kValueForRange, 5.0F); | |
| slider_text.SetName("Slider text"); | |
| slider.child_ids.push_back(slider_text.id); | |
| root.child_ids = {combo_box.id, search_box.id, text_field.id, link.id, | |
| slider.id}; | |
| std::unique_ptr<BrowserAccessibilityManager> manager( | |
| BrowserAccessibilityManager::Create( | |
| MakeAXTreeUpdateForTesting( | |
| root, combo_box, combo_box_text_container, combo_box_text, | |
| search_box, search_box_text_container, search_box_text, new_line, | |
| text_field, text_field_text_container, link, link_text, slider, | |
| slider_text), | |
| node_id_delegate_, test_browser_accessibility_delegate_.get())); | |
| ASSERT_NE(nullptr, manager->GetBrowserAccessibilityRoot()); | |
| BrowserAccessibilityWin* root_accessible = | |
| ToBrowserAccessibilityWin(manager->GetBrowserAccessibilityRoot()); | |
| ASSERT_NE(nullptr, root_accessible); | |
| ASSERT_EQ(5U, root_accessible->PlatformChildCount()); | |
| BrowserAccessibilityWin* combo_box_accessible = | |
| ToBrowserAccessibilityWin(root_accessible->PlatformGetChild(0)); | |
| ASSERT_NE(nullptr, combo_box_accessible); | |
| AXTreeData data = manager->GetTreeData(); | |
| data.focus_id = combo_box_accessible->GetId(); | |
| manager->ax_tree()->UpdateDataForTesting(data); | |
| ASSERT_EQ(combo_box_accessible, | |
| ToBrowserAccessibilityWin(manager->GetFocus())); | |
| BrowserAccessibilityWin* search_box_accessible = | |
| ToBrowserAccessibilityWin(root_accessible->PlatformGetChild(1)); | |
| ASSERT_NE(nullptr, search_box_accessible); | |
| BrowserAccessibilityWin* text_field_accessible = | |
| ToBrowserAccessibilityWin(root_accessible->PlatformGetChild(2)); | |
| ASSERT_NE(nullptr, text_field_accessible); | |
| BrowserAccessibilityWin* link_accessible = | |
| ToBrowserAccessibilityWin(root_accessible->PlatformGetChild(3)); | |
| ASSERT_NE(nullptr, link_accessible); | |
| BrowserAccessibilityWin* slider_accessible = | |
| ToBrowserAccessibilityWin(root_accessible->PlatformGetChild(4)); | |
| ASSERT_NE(nullptr, slider_accessible); | |
| base::win::ScopedVariant childid_self(CHILDID_SELF); | |
| base::win::ScopedVariant childid_slider(5); | |
| base::win::ScopedBstr value; | |
| HRESULT hr = combo_box_accessible->GetCOM()->get_accValue(childid_self, | |
| value.Receive()); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_STREQ(L"Combo box text", value.Get()); | |
| value.Reset(); | |
| hr = search_box_accessible->GetCOM()->get_accValue(childid_self, | |
| value.Receive()); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_STREQ(L"Search box text\n", value.Get()); | |
| value.Reset(); | |
| hr = text_field_accessible->GetCOM()->get_accValue(childid_self, | |
| value.Receive()); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_STREQ(L"Text field text", value.Get()); | |
| value.Reset(); | |
| // Other controls, such as links, should not use their inner text as their | |
| // value. Only text entry controls. | |
| hr = link_accessible->GetCOM()->get_accValue(childid_self, value.Receive()); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(0u, value.Length()); | |
| value.Reset(); | |
| // Sliders and other range controls should expose their current value and not | |
| // their inner text. | |
| // Also, try accessing the slider via its child number instead of directly. | |
| hr = root_accessible->GetCOM()->get_accValue(childid_slider, value.Receive()); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_STREQ(L"5", value.Get()); | |
| value.Reset(); | |
| manager.reset(); | |
| } | |
| TEST_F(BrowserAccessibilityWinTest, TestWordBoundariesInTextControls) { | |
| const std::string line1("This is a very LONG line of text that "); | |
| const std::string line2("should wrap on more than one lines "); | |
| const std::string text(line1 + line2); | |
| std::vector<int32_t> line1_word_starts; | |
| line1_word_starts.push_back(0); | |
| line1_word_starts.push_back(5); | |
| line1_word_starts.push_back(8); | |
| line1_word_starts.push_back(10); | |
| line1_word_starts.push_back(15); | |
| line1_word_starts.push_back(20); | |
| line1_word_starts.push_back(25); | |
| line1_word_starts.push_back(28); | |
| line1_word_starts.push_back(33); | |
| std::vector<int32_t> line2_word_starts; | |
| line2_word_starts.push_back(0); | |
| line2_word_starts.push_back(7); | |
| line2_word_starts.push_back(12); | |
| line2_word_starts.push_back(15); | |
| line2_word_starts.push_back(20); | |
| line2_word_starts.push_back(25); | |
| line2_word_starts.push_back(29); | |
| AXNodeData root; | |
| root.id = 1; | |
| root.role = ax::mojom::Role::kRootWebArea; | |
| root.AddState(ax::mojom::State::kFocusable); | |
| AXNodeData textarea, textarea_div, textarea_text; | |
| textarea.id = 2; | |
| textarea_div.id = 3; | |
| textarea_text.id = 4; | |
| textarea.role = ax::mojom::Role::kTextField; | |
| textarea_div.role = ax::mojom::Role::kGenericContainer; | |
| textarea_text.role = ax::mojom::Role::kStaticText; | |
| textarea.AddState(ax::mojom::State::kEditable); | |
| textarea.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, "textarea"); | |
| textarea.AddState(ax::mojom::State::kFocusable); | |
| textarea.AddState(ax::mojom::State::kMultiline); | |
| textarea_div.AddState(ax::mojom::State::kEditable); | |
| textarea_text.AddState(ax::mojom::State::kEditable); | |
| textarea.SetValue(text); | |
| textarea_text.SetName(text); | |
| textarea.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, "textarea"); | |
| textarea.child_ids.push_back(textarea_div.id); | |
| textarea_div.child_ids.push_back(textarea_text.id); | |
| AXNodeData textarea_line1, textarea_line2; | |
| textarea_line1.id = 5; | |
| textarea_line2.id = 6; | |
| textarea_line1.role = ax::mojom::Role::kInlineTextBox; | |
| textarea_line2.role = ax::mojom::Role::kInlineTextBox; | |
| textarea_line1.AddState(ax::mojom::State::kEditable); | |
| textarea_line2.AddState(ax::mojom::State::kEditable); | |
| textarea_line1.SetName(line1); | |
| textarea_line2.SetName(line2); | |
| textarea_line1.AddIntListAttribute(ax::mojom::IntListAttribute::kWordStarts, | |
| line1_word_starts); | |
| textarea_line2.AddIntListAttribute(ax::mojom::IntListAttribute::kWordStarts, | |
| line2_word_starts); | |
| textarea_text.child_ids.push_back(textarea_line1.id); | |
| textarea_text.child_ids.push_back(textarea_line2.id); | |
| AXNodeData text_field, text_field_div, text_field_text; | |
| text_field.id = 7; | |
| text_field_div.id = 8; | |
| text_field_text.id = 9; | |
| text_field.role = ax::mojom::Role::kTextField; | |
| text_field_div.role = ax::mojom::Role::kGenericContainer; | |
| text_field_text.role = ax::mojom::Role::kStaticText; | |
| 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.AddState(ax::mojom::State::kFocusable); | |
| text_field_div.AddState(ax::mojom::State::kEditable); | |
| text_field_text.AddState(ax::mojom::State::kEditable); | |
| text_field.SetValue(line1); | |
| text_field_text.SetName(line1); | |
| text_field.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, "input"); | |
| text_field.AddStringAttribute(ax::mojom::StringAttribute::kInputType, "text"); | |
| text_field.child_ids.push_back(text_field_div.id); | |
| text_field_div.child_ids.push_back(text_field_text.id); | |
| AXNodeData text_field_line; | |
| text_field_line.id = 10; | |
| text_field_line.role = ax::mojom::Role::kInlineTextBox; | |
| text_field_line.AddState(ax::mojom::State::kEditable); | |
| text_field_line.SetName(line1); | |
| text_field_line.AddIntListAttribute(ax::mojom::IntListAttribute::kWordStarts, | |
| line1_word_starts); | |
| text_field_text.child_ids.push_back(text_field_line.id); | |
| root.child_ids.push_back(2); // Textarea. | |
| root.child_ids.push_back(7); // Text field. | |
| std::unique_ptr<BrowserAccessibilityManager> manager( | |
| BrowserAccessibilityManager::Create( | |
| MakeAXTreeUpdateForTesting(root, textarea, textarea_div, | |
| textarea_text, textarea_line1, | |
| textarea_line2, text_field, text_field_div, | |
| text_field_text, text_field_line), | |
| node_id_delegate_, test_browser_accessibility_delegate_.get())); | |
| ASSERT_NE(nullptr, manager->GetBrowserAccessibilityRoot()); | |
| BrowserAccessibilityWin* root_accessible = | |
| ToBrowserAccessibilityWin(manager->GetBrowserAccessibilityRoot()); | |
| ASSERT_NE(nullptr, root_accessible); | |
| ASSERT_EQ(2U, root_accessible->PlatformChildCount()); | |
| BrowserAccessibilityWin* textarea_accessible = | |
| ToBrowserAccessibilityWin(root_accessible->PlatformGetChild(0)); | |
| ASSERT_NE(nullptr, textarea_accessible); | |
| BrowserAccessibilityWin* text_field_accessible = | |
| ToBrowserAccessibilityWin(root_accessible->PlatformGetChild(1)); | |
| ASSERT_NE(nullptr, text_field_accessible); | |
| Microsoft::WRL::ComPtr<IAccessibleText> textarea_object; | |
| EXPECT_HRESULT_SUCCEEDED(textarea_accessible->GetCOM()->QueryInterface( | |
| IID_PPV_ARGS(&textarea_object))); | |
| Microsoft::WRL::ComPtr<IAccessibleText> text_field_object; | |
| EXPECT_HRESULT_SUCCEEDED(text_field_accessible->GetCOM()->QueryInterface( | |
| IID_PPV_ARGS(&text_field_object))); | |
| LONG offset = 0; | |
| while (offset < static_cast<LONG>(text.length())) { | |
| LONG start, end; | |
| base::win::ScopedBstr word; | |
| EXPECT_EQ(S_OK, | |
| textarea_object->get_textAtOffset(offset, IA2_TEXT_BOUNDARY_WORD, | |
| &start, &end, word.Receive())); | |
| EXPECT_EQ(offset, start); | |
| EXPECT_LT(offset, end); | |
| LONG space_offset = static_cast<LONG>(text.find(' ', offset)); | |
| EXPECT_EQ(space_offset + 1, end); | |
| LONG length = end - start; | |
| EXPECT_EQ(base::ASCIIToWide(text.substr(start, length)), word.Get()); | |
| word.Reset(); | |
| offset = end; | |
| } | |
| offset = 0; | |
| while (offset < static_cast<LONG>(line1.length())) { | |
| LONG start, end; | |
| base::win::ScopedBstr word; | |
| EXPECT_EQ(S_OK, text_field_object->get_textAtOffset( | |
| offset, IA2_TEXT_BOUNDARY_WORD, &start, &end, | |
| word.Receive())); | |
| EXPECT_EQ(offset, start); | |
| EXPECT_LT(offset, end); | |
| LONG space_offset = static_cast<LONG>(line1.find(' ', offset)); | |
| EXPECT_EQ(space_offset + 1, end); | |
| LONG length = end - start; | |
| EXPECT_EQ(base::ASCIIToWide(text.substr(start, length)), word.Get()); | |
| word.Reset(); | |
| offset = end; | |
| } | |
| textarea_object.Reset(); | |
| text_field_object.Reset(); | |
| manager.reset(); | |
| } | |
| TEST_F(BrowserAccessibilityWinTest, TextBoundariesOnlyEmbeddedObjectsNoCrash) { | |
| // Update the tree structure to test get_textAtOffset from an | |
| // embedded object that has no text, only an embedded object child. | |
| // | |
| // +-1 root_data | |
| // +-2 menu_data | |
| // | +-3 button_1_data | |
| // | +-4 button_2_data | |
| // +-5 static_text_data "after" | |
| // | |
| AXNodeData root_data; | |
| root_data.id = 1; | |
| root_data.role = ax::mojom::Role::kRootWebArea; | |
| AXNodeData menu_data; | |
| menu_data.id = 2; | |
| menu_data.role = ax::mojom::Role::kMenu; | |
| AXNodeData button_1_data; | |
| button_1_data.id = 3; | |
| button_1_data.role = ax::mojom::Role::kButton; | |
| AXNodeData button_2_data; | |
| button_2_data.id = 4; | |
| button_2_data.role = ax::mojom::Role::kButton; | |
| AXNodeData static_text_data; | |
| static_text_data.id = 5; | |
| static_text_data.role = ax::mojom::Role::kStaticText; | |
| static_text_data.SetName("after"); | |
| root_data.child_ids = {menu_data.id, static_text_data.id}; | |
| menu_data.child_ids = {button_1_data.id, button_2_data.id}; | |
| std::unique_ptr<BrowserAccessibilityManager> manager( | |
| BrowserAccessibilityManager::Create( | |
| MakeAXTreeUpdateForTesting(root_data, menu_data, button_1_data, | |
| button_2_data, static_text_data), | |
| node_id_delegate_, test_browser_accessibility_delegate_.get())); | |
| ASSERT_NE(nullptr, manager->GetBrowserAccessibilityRoot()); | |
| BrowserAccessibilityWin* root_accessible = | |
| ToBrowserAccessibilityWin(manager->GetBrowserAccessibilityRoot()); | |
| ASSERT_NE(nullptr, root_accessible); | |
| ASSERT_EQ(2U, root_accessible->PlatformChildCount()); | |
| BrowserAccessibilityComWin* menu_accessible_com = | |
| ToBrowserAccessibilityComWin(root_accessible->PlatformGetChild(0)); | |
| ASSERT_NE(nullptr, menu_accessible_com); | |
| ASSERT_EQ(ax::mojom::Role::kMenu, menu_accessible_com->GetRole()); | |
| EXPECT_IA2_TEXT_AT_OFFSET( | |
| menu_accessible_com, 0, IA2_TEXT_BOUNDARY_CHAR, | |
| /*expected_hr=*/S_OK, /*start=*/0, /*end=*/1, | |
| /*text=*/ | |
| base::as_wcstr(std::u16string{AXPlatformNodeBase::kEmbeddedCharacter})); | |
| } | |
| TEST_F(BrowserAccessibilityWinTest, | |
| DISABLED_TestTextBoundariesEmbeddedCharacterText) { | |
| // Update the tree structure to test empty leaf text positions. | |
| // | |
| // +-1 root_data | |
| // +-2 body_data | |
| // +-3 static_text_1_data "before" | |
| // | +-4 inline_text_1_data "before" | |
| // +-5 menu_data_1 | |
| // | +-6 button_data | |
| // | +-7 button_leaf_container_data | |
| // | +-8 button_leaf_svg_data | |
| // +-9 menu_data_2 | |
| // +-10 static_text_2_data "after" | |
| // | +-11 inline_text_2_data "after" | |
| // +-12 static_text_3_data "tail" | |
| // | +-13 inline_text_3_data "tail" | |
| // | |
| AXNodeData root_data; | |
| root_data.id = 1; | |
| root_data.role = ax::mojom::Role::kRootWebArea; | |
| AXNodeData body_data; | |
| body_data.id = 2; | |
| body_data.role = ax::mojom::Role::kGenericContainer; | |
| AXNodeData static_text_1_data; | |
| static_text_1_data.id = 3; | |
| static_text_1_data.role = ax::mojom::Role::kStaticText; | |
| static_text_1_data.SetName("before"); | |
| AXNodeData inline_text_1_data; | |
| inline_text_1_data.id = 4; | |
| inline_text_1_data.role = ax::mojom::Role::kInlineTextBox; | |
| inline_text_1_data.SetName("before"); | |
| AXNodeData menu_data_1; | |
| menu_data_1.id = 5; | |
| menu_data_1.role = ax::mojom::Role::kMenu; | |
| AXNodeData button_data; | |
| button_data.id = 6; | |
| button_data.role = ax::mojom::Role::kButton; | |
| AXNodeData button_leaf_container_data; | |
| button_leaf_container_data.id = 7; | |
| button_leaf_container_data.role = ax::mojom::Role::kGenericContainer; | |
| AXNodeData button_leaf_svg_data; | |
| button_leaf_svg_data.id = 8; | |
| button_leaf_svg_data.role = ax::mojom::Role::kSvgRoot; | |
| AXNodeData menu_data_2; | |
| menu_data_2.id = 9; | |
| menu_data_2.role = ax::mojom::Role::kMenu; | |
| AXNodeData static_text_2_data; | |
| static_text_2_data.id = 10; | |
| static_text_2_data.role = ax::mojom::Role::kStaticText; | |
| static_text_2_data.SetName("after"); | |
| AXNodeData inline_text_2_data; | |
| inline_text_2_data.id = 11; | |
| inline_text_2_data.role = ax::mojom::Role::kInlineTextBox; | |
| inline_text_2_data.SetName("after"); | |
| AXNodeData static_text_3_data; | |
| static_text_3_data.id = 12; | |
| static_text_3_data.role = ax::mojom::Role::kStaticText; | |
| static_text_3_data.SetName("tail"); | |
| AXNodeData inline_text_3_data; | |
| inline_text_3_data.id = 13; | |
| inline_text_3_data.role = ax::mojom::Role::kInlineTextBox; | |
| inline_text_3_data.SetName("tail"); | |
| root_data.child_ids = {body_data.id}; | |
| body_data.child_ids = {static_text_1_data.id, menu_data_1.id, menu_data_2.id, | |
| static_text_2_data.id, static_text_3_data.id}; | |
| menu_data_1.child_ids = {button_data.id}; | |
| button_data.child_ids = {button_leaf_container_data.id, | |
| button_leaf_svg_data.id}; | |
| static_text_1_data.child_ids = {inline_text_1_data.id}; | |
| static_text_2_data.child_ids = {inline_text_2_data.id}; | |
| static_text_3_data.child_ids = {inline_text_3_data.id}; | |
| AXTreeUpdate update; | |
| AXTreeData tree_data; | |
| tree_data.tree_id = AXTreeID::CreateNewAXTreeID(); | |
| tree_data.focused_tree_id = tree_data.tree_id; | |
| update.tree_data = tree_data; | |
| update.has_tree_data = true; | |
| update.root_id = root_data.id; | |
| update.nodes = {root_data, | |
| body_data, | |
| static_text_1_data, | |
| inline_text_1_data, | |
| menu_data_1, | |
| button_data, | |
| button_leaf_container_data, | |
| button_leaf_svg_data, | |
| menu_data_2, | |
| static_text_2_data, | |
| inline_text_2_data, | |
| static_text_3_data, | |
| inline_text_3_data}; | |
| std::unique_ptr<BrowserAccessibilityManager> manager( | |
| BrowserAccessibilityManager::Create( | |
| update, node_id_delegate_, | |
| test_browser_accessibility_delegate_.get())); | |
| ASSERT_NE(nullptr, manager->GetBrowserAccessibilityRoot()); | |
| BrowserAccessibilityWin* root_accessible = | |
| ToBrowserAccessibilityWin(manager->GetBrowserAccessibilityRoot()); | |
| ASSERT_NE(nullptr, root_accessible); | |
| ASSERT_EQ(1U, root_accessible->PlatformChildCount()); | |
| BrowserAccessibilityWin* body_accessible = | |
| ToBrowserAccessibilityWin(root_accessible->PlatformGetChild(0)); | |
| ASSERT_NE(nullptr, body_accessible); | |
| ASSERT_EQ(5U, body_accessible->PlatformChildCount()); | |
| BrowserAccessibilityComWin* body_accessible_com = body_accessible->GetCOM(); | |
| ASSERT_NE(nullptr, body_accessible_com); | |
| BrowserAccessibilityComWin* static_text_1_com = | |
| ToBrowserAccessibilityWin(body_accessible->PlatformGetChild(0))->GetCOM(); | |
| ASSERT_NE(nullptr, static_text_1_com); | |
| ASSERT_EQ(ax::mojom::Role::kStaticText, static_text_1_com->GetRole()); | |
| BrowserAccessibilityComWin* menu_1_accessible_com = | |
| ToBrowserAccessibilityWin(body_accessible->PlatformGetChild(1))->GetCOM(); | |
| ASSERT_NE(nullptr, menu_1_accessible_com); | |
| ASSERT_EQ(ax::mojom::Role::kMenu, menu_1_accessible_com->GetRole()); | |
| BrowserAccessibilityComWin* menu_2_accessible_com = | |
| ToBrowserAccessibilityWin(body_accessible->PlatformGetChild(2))->GetCOM(); | |
| ASSERT_NE(nullptr, menu_2_accessible_com); | |
| ASSERT_EQ(ax::mojom::Role::kMenu, menu_2_accessible_com->GetRole()); | |
| BrowserAccessibilityComWin* static_text_2_com = | |
| ToBrowserAccessibilityWin(body_accessible->PlatformGetChild(3))->GetCOM(); | |
| ASSERT_NE(nullptr, static_text_2_com); | |
| ASSERT_EQ(ax::mojom::Role::kStaticText, static_text_2_com->GetRole()); | |
| BrowserAccessibilityComWin* static_text_3_com = | |
| ToBrowserAccessibilityWin(body_accessible->PlatformGetChild(4))->GetCOM(); | |
| ASSERT_NE(nullptr, static_text_3_com); | |
| ASSERT_EQ(ax::mojom::Role::kStaticText, static_text_3_com->GetRole()); | |
| // [obj] stands for the embedded object replacement character \xFFFC. | |
| // L"<b>efore" [obj] [obj] L"after" L"tail" | |
| EXPECT_IA2_TEXT_AT_OFFSET(body_accessible_com, 0, IA2_TEXT_BOUNDARY_CHAR, | |
| /*expected_hr=*/S_OK, /*start=*/0, /*end=*/1, | |
| /*text=*/L"b"); | |
| // L"bef<o>re" [obj] [obj] L"after" L"tail" | |
| EXPECT_IA2_TEXT_AT_OFFSET(body_accessible_com, 3, IA2_TEXT_BOUNDARY_CHAR, | |
| /*expected_hr=*/S_OK, /*start=*/3, /*end=*/4, | |
| /*text=*/L"o"); | |
| // L"befor<e>" [obj] [obj] L"after" L"tail" | |
| EXPECT_IA2_TEXT_AT_OFFSET(body_accessible_com, 5, IA2_TEXT_BOUNDARY_CHAR, | |
| /*expected_hr=*/S_OK, /*start=*/5, /*end=*/6, | |
| /*text=*/L"e"); | |
| // L"before" <[obj]> [obj] L"after" L"tail" | |
| EXPECT_IA2_TEXT_AT_OFFSET( | |
| body_accessible_com, 6, IA2_TEXT_BOUNDARY_CHAR, | |
| /*expected_hr=*/S_OK, /*start=*/6, /*end=*/7, | |
| /*text=*/ | |
| base::as_wcstr(std::u16string{AXPlatformNodeBase::kEmbeddedCharacter})); | |
| // L"before" [obj] <[obj]> L"after" L"tail" | |
| EXPECT_IA2_TEXT_AT_OFFSET( | |
| body_accessible_com, 7, IA2_TEXT_BOUNDARY_CHAR, | |
| /*expected_hr=*/S_OK, /*start=*/7, /*end=*/8, | |
| /*text=*/ | |
| base::as_wcstr(std::u16string{AXPlatformNodeBase::kEmbeddedCharacter})); | |
| // L"before" [obj] [obj] L"<a>fter" L"tail" | |
| EXPECT_IA2_TEXT_AT_OFFSET(body_accessible_com, 8, IA2_TEXT_BOUNDARY_CHAR, | |
| /*expected_hr=*/S_OK, /*start=*/8, /*end=*/9, | |
| /*text=*/L"a"); | |
| // L"before" [obj] [obj] L"a<f>ter" L"tail" | |
| EXPECT_IA2_TEXT_AT_OFFSET(body_accessible_com, 9, IA2_TEXT_BOUNDARY_CHAR, | |
| /*expected_hr=*/S_OK, /*start=*/9, /*end=*/10, | |
| /*text=*/L"f"); | |
| // L"before" [obj] [obj] L"afte<r>" L"tail" | |
| EXPECT_IA2_TEXT_AT_OFFSET(body_accessible_com, 12, IA2_TEXT_BOUNDARY_CHAR, | |
| /*expected_hr=*/S_OK, /*start=*/12, /*end=*/13, | |
| /*text=*/L"r"); | |
| // L"before" [obj] [obj] L"after" L"<t>ail" | |
| EXPECT_IA2_TEXT_AT_OFFSET(body_accessible_com, 13, IA2_TEXT_BOUNDARY_CHAR, | |
| /*expected_hr=*/S_OK, /*start=*/13, /*end=*/14, | |
| /*text=*/L"t"); | |
| // L"before" [obj] [obj] L"after" L"ta<i>l" | |
| EXPECT_IA2_TEXT_AT_OFFSET(body_accessible_com, 15, IA2_TEXT_BOUNDARY_CHAR, | |
| /*expected_hr=*/S_OK, /*start=*/15, /*end=*/16, | |
| /*text=*/L"i"); | |
| // L"before" [obj] [obj] L"after" L"tai<l>" | |
| EXPECT_IA2_TEXT_AT_OFFSET(body_accessible_com, 16, IA2_TEXT_BOUNDARY_CHAR, | |
| /*expected_hr=*/S_OK, /*start=*/16, /*end=*/17, | |
| /*text=*/L"l"); | |
| // L"before" [obj] [obj] L"after" L"tail<>" | |
| EXPECT_IA2_TEXT_AT_OFFSET(body_accessible_com, 17, IA2_TEXT_BOUNDARY_CHAR, | |
| /*expected_hr=*/E_INVALIDARG, /*start=*/0, | |
| /*end=*/0, | |
| /*text=*/nullptr); | |
| // L"<b>efore" | |
| EXPECT_IA2_TEXT_AT_OFFSET(static_text_1_com, 0, IA2_TEXT_BOUNDARY_CHAR, | |
| /*expected_hr=*/S_OK, /*start=*/0, /*end=*/1, | |
| /*text=*/L"b"); | |
| // L"be<f>ore" | |
| EXPECT_IA2_TEXT_AT_OFFSET(static_text_1_com, 2, IA2_TEXT_BOUNDARY_CHAR, | |
| /*expected_hr=*/S_OK, /*start=*/2, /*end=*/3, | |
| /*text=*/L"f"); | |
| // L"before<>" | |
| EXPECT_IA2_TEXT_AT_OFFSET(static_text_1_com, 6, IA2_TEXT_BOUNDARY_CHAR, | |
| /*expected_hr=*/E_INVALIDARG, /*start=*/0, | |
| /*end=*/0, | |
| /*text=*/nullptr); | |
| // <[obj]> | |
| EXPECT_IA2_TEXT_AT_OFFSET( | |
| menu_1_accessible_com, 0, IA2_TEXT_BOUNDARY_CHAR, | |
| /*expected_hr=*/S_OK, /*start=*/0, /*end=*/1, | |
| /*text=*/ | |
| base::as_wcstr(std::u16string{AXPlatformNodeBase::kEmbeddedCharacter})); | |
| // [obj]<> | |
| EXPECT_IA2_TEXT_AT_OFFSET(menu_1_accessible_com, 1, IA2_TEXT_BOUNDARY_CHAR, | |
| /*expected_hr=*/E_INVALIDARG, /*start=*/0, | |
| /*end=*/0, | |
| /*text=*/nullptr); | |
| // L"<>" | |
| EXPECT_IA2_TEXT_AT_OFFSET(menu_2_accessible_com, 0, IA2_TEXT_BOUNDARY_CHAR, | |
| /*expected_hr=*/E_INVALIDARG, /*start=*/0, | |
| /*end=*/0, | |
| /*text=*/nullptr); | |
| // L"<a>fter" | |
| EXPECT_IA2_TEXT_AT_OFFSET(static_text_2_com, 0, IA2_TEXT_BOUNDARY_CHAR, | |
| /*expected_hr=*/S_OK, /*start=*/0, /*end=*/1, | |
| /*text=*/L"a"); | |
| // L"af<t>er" | |
| EXPECT_IA2_TEXT_AT_OFFSET(static_text_2_com, 2, IA2_TEXT_BOUNDARY_CHAR, | |
| /*expected_hr=*/S_OK, /*start=*/2, /*end=*/3, | |
| /*text=*/L"t"); | |
| // L"after<>" | |
| EXPECT_IA2_TEXT_AT_OFFSET(static_text_2_com, 5, IA2_TEXT_BOUNDARY_CHAR, | |
| /*expected_hr=*/E_INVALIDARG, /*start=*/0, | |
| /*end=*/0, | |
| /*text=*/nullptr); | |
| // L"<t>ail" | |
| EXPECT_IA2_TEXT_AT_OFFSET(static_text_3_com, 0, IA2_TEXT_BOUNDARY_CHAR, | |
| /*expected_hr=*/S_OK, /*start=*/0, /*end=*/1, | |
| /*text=*/L"t"); | |
| // L"ta<i>l" | |
| EXPECT_IA2_TEXT_AT_OFFSET(static_text_3_com, 2, IA2_TEXT_BOUNDARY_CHAR, | |
| /*expected_hr=*/S_OK, /*start=*/2, /*end=*/3, | |
| /*text=*/L"i"); | |
| // L"tail<>" | |
| EXPECT_IA2_TEXT_AT_OFFSET(static_text_3_com, 4, IA2_TEXT_BOUNDARY_CHAR, | |
| /*expected_hr=*/E_INVALIDARG, /*start=*/0, | |
| /*end=*/0, | |
| /*text=*/nullptr); | |
| // L"before" [obj] <[obj]> L"<a>fter" L"tail" | |
| EXPECT_IA2_TEXT_BEFORE_OFFSET( | |
| body_accessible_com, 7, IA2_TEXT_BOUNDARY_CHAR, | |
| /*expected_hr=*/S_OK, /*start=*/6, /*end=*/7, | |
| /*text=*/ | |
| base::as_wcstr(std::u16string{AXPlatformNodeBase::kEmbeddedCharacter})); | |
| // L"before" <[obj]> [obj] L"after" L"tail" | |
| EXPECT_IA2_TEXT_BEFORE_OFFSET(body_accessible_com, 6, IA2_TEXT_BOUNDARY_CHAR, | |
| /*expected_hr=*/S_OK, /*start=*/5, /*end=*/6, | |
| /*text=*/L"e"); | |
| // <[obj]> | |
| EXPECT_IA2_TEXT_BEFORE_OFFSET(menu_1_accessible_com, 0, | |
| IA2_TEXT_BOUNDARY_CHAR, | |
| /*expected_hr=*/S_FALSE, /*start=*/0, /*end=*/0, | |
| /*text=*/nullptr); | |
| // L"<>" | |
| EXPECT_IA2_TEXT_BEFORE_OFFSET(menu_2_accessible_com, 0, | |
| IA2_TEXT_BOUNDARY_CHAR, | |
| /*expected_hr=*/E_INVALIDARG, /*start=*/0, | |
| /*end=*/0, | |
| /*text=*/nullptr); | |
| // L"befor<e>" [obj] <[obj]> L"after" L"tail" | |
| EXPECT_IA2_TEXT_AFTER_OFFSET( | |
| body_accessible_com, 5, IA2_TEXT_BOUNDARY_CHAR, | |
| /*expected_hr=*/S_OK, /*start=*/6, /*end=*/7, | |
| /*text=*/ | |
| base::as_wcstr(std::u16string{AXPlatformNodeBase::kEmbeddedCharacter})); | |
| // L"before" <[obj]> [obj] L"after" L"tail" | |
| EXPECT_IA2_TEXT_AFTER_OFFSET( | |
| body_accessible_com, 6, IA2_TEXT_BOUNDARY_CHAR, | |
| /*expected_hr=*/S_OK, /*start=*/7, /*end=*/8, | |
| /*text=*/ | |
| base::as_wcstr(std::u16string{AXPlatformNodeBase::kEmbeddedCharacter})); | |
| // <[obj]> | |
| EXPECT_IA2_TEXT_AFTER_OFFSET(menu_1_accessible_com, 0, IA2_TEXT_BOUNDARY_CHAR, | |
| /*expected_hr=*/S_FALSE, /*start=*/0, /*end=*/0, | |
| /*text=*/nullptr); | |
| // L"<>" | |
| EXPECT_IA2_TEXT_AFTER_OFFSET(menu_2_accessible_com, 0, IA2_TEXT_BOUNDARY_CHAR, | |
| /*expected_hr=*/E_INVALIDARG, /*start=*/0, | |
| /*end=*/0, | |
| /*text=*/nullptr); | |
| } | |
| TEST_F(BrowserAccessibilityWinTest, TestCaretAndSelectionInSimpleFields) { | |
| AXNodeData root; | |
| root.id = 1; | |
| root.role = ax::mojom::Role::kRootWebArea; | |
| root.AddState(ax::mojom::State::kFocusable); | |
| AXNodeData combo_box; | |
| combo_box.id = 2; | |
| combo_box.role = ax::mojom::Role::kTextFieldWithComboBox; | |
| combo_box.AddState(ax::mojom::State::kEditable); | |
| combo_box.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, "input"); | |
| combo_box.AddStringAttribute(ax::mojom::StringAttribute::kInputType, "text"); | |
| combo_box.AddState(ax::mojom::State::kFocusable); | |
| combo_box.SetValue("Test1"); | |
| // Place the caret between 't' and 'e'. | |
| combo_box.AddIntAttribute(ax::mojom::IntAttribute::kTextSelStart, 1); | |
| combo_box.AddIntAttribute(ax::mojom::IntAttribute::kTextSelEnd, 1); | |
| AXNodeData text_field; | |
| text_field.id = 3; | |
| 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.AddState(ax::mojom::State::kFocusable); | |
| text_field.SetValue("Test2"); | |
| // Select the letter 'e'. | |
| text_field.AddIntAttribute(ax::mojom::IntAttribute::kTextSelStart, 1); | |
| text_field.AddIntAttribute(ax::mojom::IntAttribute::kTextSelEnd, 2); | |
| root.child_ids.push_back(2); | |
| root.child_ids.push_back(3); | |
| std::unique_ptr<BrowserAccessibilityManager> manager( | |
| BrowserAccessibilityManager::Create( | |
| MakeAXTreeUpdateForTesting(root, combo_box, text_field), | |
| node_id_delegate_, test_browser_accessibility_delegate_.get())); | |
| ASSERT_NE(nullptr, manager->GetBrowserAccessibilityRoot()); | |
| BrowserAccessibilityWin* root_accessible = | |
| ToBrowserAccessibilityWin(manager->GetBrowserAccessibilityRoot()); | |
| ASSERT_NE(nullptr, root_accessible); | |
| ASSERT_EQ(2U, root_accessible->PlatformChildCount()); | |
| BrowserAccessibilityWin* combo_box_accessible = | |
| ToBrowserAccessibilityWin(root_accessible->PlatformGetChild(0)); | |
| ASSERT_NE(nullptr, combo_box_accessible); | |
| AXTreeData data = manager->GetTreeData(); | |
| data.focus_id = combo_box_accessible->GetId(); | |
| data.sel_anchor_object_id = combo_box_accessible->GetId(); | |
| data.sel_focus_object_id = combo_box_accessible->GetId(); | |
| manager->ax_tree()->UpdateDataForTesting(data); | |
| ASSERT_EQ(combo_box_accessible, | |
| ToBrowserAccessibilityWin(manager->GetFocus())); | |
| BrowserAccessibilityWin* text_field_accessible = | |
| ToBrowserAccessibilityWin(root_accessible->PlatformGetChild(1)); | |
| ASSERT_NE(nullptr, text_field_accessible); | |
| // -2 is never a valid offset. | |
| LONG caret_offset = -2; | |
| LONG n_selections = -2; | |
| LONG selection_start = -2; | |
| LONG selection_end = -2; | |
| // Test get_caretOffset. | |
| HRESULT hr = combo_box_accessible->GetCOM()->get_caretOffset(&caret_offset); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(1, caret_offset); | |
| // The caret should not be visible because the text field is not focused. | |
| hr = text_field_accessible->GetCOM()->get_caretOffset(&caret_offset); | |
| EXPECT_EQ(S_FALSE, hr); | |
| EXPECT_EQ(0, caret_offset); | |
| // Move the focus to the text field. | |
| data = manager->GetTreeData(); | |
| data.focus_id = text_field_accessible->GetId(); | |
| data.sel_anchor_object_id = text_field_accessible->GetId(); | |
| data.sel_focus_object_id = text_field_accessible->GetId(); | |
| manager->ax_tree()->UpdateDataForTesting(data); | |
| ASSERT_EQ(text_field_accessible, | |
| ToBrowserAccessibilityWin(manager->GetFocus())); | |
| // The caret should now appear at the end of the text field. | |
| hr = text_field_accessible->GetCOM()->get_caretOffset(&caret_offset); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(2, caret_offset); | |
| // Test get_nSelections. | |
| hr = combo_box_accessible->GetCOM()->get_nSelections(&n_selections); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(0, n_selections); | |
| hr = text_field_accessible->GetCOM()->get_nSelections(&n_selections); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(1, n_selections); | |
| // Test get_selection. | |
| hr = combo_box_accessible->GetCOM()->get_selection( | |
| 0L /* selection_index */, &selection_start, &selection_end); | |
| EXPECT_EQ(E_INVALIDARG, hr); // No selections available. | |
| hr = text_field_accessible->GetCOM()->get_selection( | |
| 0L /* selection_index */, &selection_start, &selection_end); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(1, selection_start); | |
| EXPECT_EQ(2, selection_end); | |
| manager.reset(); | |
| } | |
| TEST_F(BrowserAccessibilityWinTest, TestCaretInContentEditables) { | |
| AXNodeData root; | |
| root.id = 1; | |
| root.role = ax::mojom::Role::kRootWebArea; | |
| root.AddState(ax::mojom::State::kFocusable); | |
| AXNodeData div_editable; | |
| div_editable.id = 2; | |
| div_editable.role = ax::mojom::Role::kGenericContainer; | |
| div_editable.AddState(ax::mojom::State::kEditable); | |
| div_editable.AddState(ax::mojom::State::kRichlyEditable); | |
| div_editable.AddState(ax::mojom::State::kFocusable); | |
| div_editable.AddBoolAttribute( | |
| ax::mojom::BoolAttribute::kNonAtomicTextFieldRoot, true); | |
| AXNodeData text; | |
| text.id = 3; | |
| text.role = ax::mojom::Role::kStaticText; | |
| text.AddState(ax::mojom::State::kEditable); | |
| text.AddState(ax::mojom::State::kRichlyEditable); | |
| text.SetName("Click "); | |
| AXNodeData link; | |
| link.id = 4; | |
| link.role = ax::mojom::Role::kLink; | |
| link.AddState(ax::mojom::State::kEditable); | |
| link.AddState(ax::mojom::State::kRichlyEditable); | |
| link.AddState(ax::mojom::State::kFocusable); | |
| link.AddState(ax::mojom::State::kLinked); | |
| link.SetName("here"); | |
| AXNodeData link_text; | |
| link_text.id = 5; | |
| link_text.role = ax::mojom::Role::kStaticText; | |
| link_text.AddState(ax::mojom::State::kEditable); | |
| link_text.AddState(ax::mojom::State::kRichlyEditable); | |
| link_text.AddState(ax::mojom::State::kFocusable); | |
| link_text.AddState(ax::mojom::State::kLinked); | |
| link_text.SetName("here"); | |
| root.child_ids.push_back(2); | |
| div_editable.child_ids.push_back(3); | |
| div_editable.child_ids.push_back(4); | |
| link.child_ids.push_back(5); | |
| AXTreeUpdate update = | |
| MakeAXTreeUpdateForTesting(root, div_editable, link, link_text, text); | |
| // Place the caret between 'h' and 'e'. | |
| update.has_tree_data = true; | |
| update.tree_data.sel_anchor_object_id = 5; | |
| update.tree_data.sel_anchor_offset = 1; | |
| update.tree_data.sel_focus_object_id = 5; | |
| update.tree_data.sel_focus_offset = 1; | |
| std::unique_ptr<BrowserAccessibilityManager> manager( | |
| BrowserAccessibilityManager::Create( | |
| update, node_id_delegate_, | |
| test_browser_accessibility_delegate_.get())); | |
| ASSERT_NE(nullptr, manager->GetBrowserAccessibilityRoot()); | |
| BrowserAccessibilityWin* root_accessible = | |
| ToBrowserAccessibilityWin(manager->GetBrowserAccessibilityRoot()); | |
| ASSERT_NE(nullptr, root_accessible); | |
| ASSERT_EQ(1U, root_accessible->PlatformChildCount()); | |
| BrowserAccessibilityWin* div_editable_accessible = | |
| ToBrowserAccessibilityWin(root_accessible->PlatformGetChild(0)); | |
| ASSERT_NE(nullptr, div_editable_accessible); | |
| ASSERT_EQ(2U, div_editable_accessible->PlatformChildCount()); | |
| // -2 is never a valid offset. | |
| LONG caret_offset = -2; | |
| LONG n_selections = -2; | |
| // No selection should be present. | |
| HRESULT hr = | |
| div_editable_accessible->GetCOM()->get_nSelections(&n_selections); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(0, n_selections); | |
| // The caret should be on the embedded object character. | |
| hr = div_editable_accessible->GetCOM()->get_caretOffset(&caret_offset); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(6, caret_offset); | |
| // Move the focus to the content editable. | |
| AXTreeData data = manager->GetTreeData(); | |
| data.focus_id = div_editable_accessible->GetId(); | |
| manager->ax_tree()->UpdateDataForTesting(data); | |
| ASSERT_EQ(div_editable_accessible, | |
| ToBrowserAccessibilityWin(manager->GetFocus())); | |
| BrowserAccessibilityWin* text_accessible = | |
| ToBrowserAccessibilityWin(div_editable_accessible->PlatformGetChild(0)); | |
| ASSERT_NE(nullptr, text_accessible); | |
| BrowserAccessibilityWin* link_accessible = | |
| ToBrowserAccessibilityWin(div_editable_accessible->PlatformGetChild(1)); | |
| ASSERT_NE(nullptr, link_accessible); | |
| ASSERT_EQ(1U, link_accessible->PlatformChildCount()); | |
| BrowserAccessibilityWin* link_text_accessible = | |
| ToBrowserAccessibilityWin(link_accessible->PlatformGetChild(0)); | |
| ASSERT_NE(nullptr, link_text_accessible); | |
| // The caret should not have moved. | |
| hr = div_editable_accessible->GetCOM()->get_nSelections(&n_selections); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(0, n_selections); | |
| hr = div_editable_accessible->GetCOM()->get_caretOffset(&caret_offset); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(6, caret_offset); | |
| hr = link_accessible->GetCOM()->get_nSelections(&n_selections); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(0, n_selections); | |
| hr = link_text_accessible->GetCOM()->get_nSelections(&n_selections); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(0, n_selections); | |
| hr = link_accessible->GetCOM()->get_caretOffset(&caret_offset); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(1, caret_offset); | |
| hr = link_text_accessible->GetCOM()->get_caretOffset(&caret_offset); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(1, caret_offset); | |
| manager.reset(); | |
| } | |
| TEST_F(BrowserAccessibilityWinTest, TestSelectionInContentEditables) { | |
| AXNodeData root; | |
| root.id = 1; | |
| root.role = ax::mojom::Role::kRootWebArea; | |
| root.AddState(ax::mojom::State::kFocusable); | |
| AXNodeData div_editable; | |
| div_editable.id = 2; | |
| div_editable.role = ax::mojom::Role::kGenericContainer; | |
| div_editable.AddState(ax::mojom::State::kFocusable); | |
| div_editable.AddState(ax::mojom::State::kEditable); | |
| AXNodeData text; | |
| text.id = 3; | |
| text.role = ax::mojom::Role::kStaticText; | |
| text.AddState(ax::mojom::State::kFocusable); | |
| text.AddState(ax::mojom::State::kEditable); | |
| text.SetName("Click "); | |
| AXNodeData link; | |
| link.id = 4; | |
| link.role = ax::mojom::Role::kLink; | |
| link.AddState(ax::mojom::State::kFocusable); | |
| link.AddState(ax::mojom::State::kEditable); | |
| link.AddState(ax::mojom::State::kLinked); | |
| link.SetName("here"); | |
| AXNodeData link_text; | |
| link_text.id = 5; | |
| link_text.role = ax::mojom::Role::kStaticText; | |
| link_text.AddState(ax::mojom::State::kFocusable); | |
| link_text.AddState(ax::mojom::State::kEditable); | |
| link_text.AddState(ax::mojom::State::kLinked); | |
| link_text.SetName("here"); | |
| root.child_ids.push_back(2); | |
| div_editable.child_ids.push_back(3); | |
| div_editable.child_ids.push_back(4); | |
| link.child_ids.push_back(5); | |
| AXTreeUpdate update = | |
| MakeAXTreeUpdateForTesting(root, div_editable, link, link_text, text); | |
| // Select the following part of the text: "lick here". | |
| update.has_tree_data = true; | |
| update.tree_data.sel_anchor_object_id = 3; | |
| update.tree_data.sel_anchor_offset = 1; | |
| update.tree_data.sel_focus_object_id = 5; | |
| update.tree_data.sel_focus_offset = 4; | |
| std::unique_ptr<BrowserAccessibilityManager> manager( | |
| BrowserAccessibilityManager::Create( | |
| update, node_id_delegate_, | |
| test_browser_accessibility_delegate_.get())); | |
| ASSERT_NE(nullptr, manager->GetBrowserAccessibilityRoot()); | |
| BrowserAccessibilityWin* root_accessible = | |
| ToBrowserAccessibilityWin(manager->GetBrowserAccessibilityRoot()); | |
| ASSERT_NE(nullptr, root_accessible); | |
| ASSERT_EQ(1U, root_accessible->PlatformChildCount()); | |
| BrowserAccessibilityWin* div_editable_accessible = | |
| ToBrowserAccessibilityWin(root_accessible->PlatformGetChild(0)); | |
| ASSERT_NE(nullptr, div_editable_accessible); | |
| ASSERT_EQ(2U, div_editable_accessible->PlatformChildCount()); | |
| // -2 is never a valid offset. | |
| LONG caret_offset = -2; | |
| LONG n_selections = -2; | |
| LONG selection_start = -2; | |
| LONG selection_end = -2; | |
| BrowserAccessibilityWin* text_accessible = | |
| ToBrowserAccessibilityWin(div_editable_accessible->PlatformGetChild(0)); | |
| ASSERT_NE(nullptr, text_accessible); | |
| BrowserAccessibilityWin* link_accessible = | |
| ToBrowserAccessibilityWin(div_editable_accessible->PlatformGetChild(1)); | |
| ASSERT_NE(nullptr, link_accessible); | |
| ASSERT_EQ(1U, link_accessible->PlatformChildCount()); | |
| BrowserAccessibilityWin* link_text_accessible = | |
| ToBrowserAccessibilityWin(link_accessible->PlatformGetChild(0)); | |
| ASSERT_NE(nullptr, link_text_accessible); | |
| // get_nSelections should work on all objects. | |
| HRESULT hr = | |
| div_editable_accessible->GetCOM()->get_nSelections(&n_selections); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(1, n_selections); | |
| hr = text_accessible->GetCOM()->get_nSelections(&n_selections); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(1, n_selections); | |
| hr = link_accessible->GetCOM()->get_nSelections(&n_selections); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(1, n_selections); | |
| hr = link_text_accessible->GetCOM()->get_nSelections(&n_selections); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(1, n_selections); | |
| // get_selection should be unaffected by focus placement. | |
| hr = div_editable_accessible->GetCOM()->get_selection( | |
| 0L /* selection_index */, &selection_start, &selection_end); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(1, selection_start); | |
| // selection_end should be after embedded object character. | |
| EXPECT_EQ(7, selection_end); | |
| hr = text_accessible->GetCOM()->get_selection( | |
| 0L /* selection_index */, &selection_start, &selection_end); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(1, selection_start); | |
| // No embedded character on this object, only the first part of the text. | |
| EXPECT_EQ(6, selection_end); | |
| hr = link_accessible->GetCOM()->get_selection( | |
| 0L /* selection_index */, &selection_start, &selection_end); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(0, selection_start); | |
| EXPECT_EQ(4, selection_end); | |
| hr = link_text_accessible->GetCOM()->get_selection( | |
| 0L /* selection_index */, &selection_start, &selection_end); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(0, selection_start); | |
| EXPECT_EQ(4, selection_end); | |
| // The caret should be at the focus (the end) of the selection. | |
| hr = div_editable_accessible->GetCOM()->get_caretOffset(&caret_offset); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(6, caret_offset); | |
| // Move the focus to the content editable. | |
| AXTreeData data = manager->GetTreeData(); | |
| data.focus_id = div_editable_accessible->GetId(); | |
| manager->ax_tree()->UpdateDataForTesting(data); | |
| ASSERT_EQ(div_editable_accessible, | |
| ToBrowserAccessibilityWin(manager->GetFocus())); | |
| // The caret should not have moved. | |
| hr = div_editable_accessible->GetCOM()->get_caretOffset(&caret_offset); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(6, caret_offset); | |
| // The caret offset should reflect the position of the selection's focus in | |
| // any given object. | |
| hr = link_accessible->GetCOM()->get_caretOffset(&caret_offset); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(4, caret_offset); | |
| hr = link_text_accessible->GetCOM()->get_caretOffset(&caret_offset); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(4, caret_offset); | |
| hr = div_editable_accessible->GetCOM()->get_selection( | |
| 0L /* selection_index */, &selection_start, &selection_end); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(1, selection_start); | |
| EXPECT_EQ(7, selection_end); | |
| manager.reset(); | |
| } | |
| TEST_F(BrowserAccessibilityWinTest, TestIAccessibleHyperlink) { | |
| AXNodeData root; | |
| root.id = 1; | |
| root.role = ax::mojom::Role::kRootWebArea; | |
| root.AddState(ax::mojom::State::kFocusable); | |
| AXNodeData div; | |
| div.id = 2; | |
| div.role = ax::mojom::Role::kGenericContainer; | |
| div.AddState(ax::mojom::State::kFocusable); | |
| AXNodeData text; | |
| text.id = 3; | |
| text.role = ax::mojom::Role::kStaticText; | |
| text.SetName("Click "); | |
| AXNodeData link; | |
| link.id = 4; | |
| link.role = ax::mojom::Role::kLink; | |
| link.AddState(ax::mojom::State::kFocusable); | |
| link.AddState(ax::mojom::State::kLinked); | |
| link.SetName("here"); | |
| link.SetNameFrom(ax::mojom::NameFrom::kContents); | |
| link.AddStringAttribute(ax::mojom::StringAttribute::kUrl, "example.com"); | |
| root.child_ids.push_back(div.id); | |
| div.child_ids = {text.id, link.id}; | |
| std::unique_ptr<BrowserAccessibilityManager> manager( | |
| BrowserAccessibilityManager::Create( | |
| MakeAXTreeUpdateForTesting(root, div, link, text), node_id_delegate_, | |
| test_browser_accessibility_delegate_.get())); | |
| ASSERT_NE(nullptr, manager->GetBrowserAccessibilityRoot()); | |
| BrowserAccessibilityWin* root_accessible = | |
| ToBrowserAccessibilityWin(manager->GetBrowserAccessibilityRoot()); | |
| ASSERT_NE(nullptr, root_accessible); | |
| ASSERT_EQ(1U, root_accessible->PlatformChildCount()); | |
| BrowserAccessibilityWin* div_accessible = | |
| ToBrowserAccessibilityWin(root_accessible->PlatformGetChild(0)); | |
| ASSERT_NE(nullptr, div_accessible); | |
| ASSERT_EQ(2U, div_accessible->PlatformChildCount()); | |
| BrowserAccessibilityWin* text_accessible = | |
| ToBrowserAccessibilityWin(div_accessible->PlatformGetChild(0)); | |
| ASSERT_NE(nullptr, text_accessible); | |
| BrowserAccessibilityWin* link_accessible = | |
| ToBrowserAccessibilityWin(div_accessible->PlatformGetChild(1)); | |
| ASSERT_NE(nullptr, link_accessible); | |
| // -1 is never a valid value. | |
| LONG n_actions = -1; | |
| LONG start_index = -1; | |
| LONG end_index = -1; | |
| Microsoft::WRL::ComPtr<IAccessibleHyperlink> hyperlink; | |
| base::win::ScopedVariant anchor; | |
| base::win::ScopedVariant anchor_target; | |
| base::win::ScopedBstr bstr; | |
| std::u16string div_hypertext(u"Click "); | |
| div_hypertext.push_back(AXPlatformNodeBase::kEmbeddedCharacter); | |
| // div_accessible and link_accessible are the only IA2 hyperlinks. | |
| EXPECT_HRESULT_FAILED( | |
| root_accessible->GetCOM()->QueryInterface(IID_PPV_ARGS(&hyperlink))); | |
| hyperlink.Reset(); | |
| EXPECT_HRESULT_SUCCEEDED( | |
| div_accessible->GetCOM()->QueryInterface(IID_PPV_ARGS(&hyperlink))); | |
| hyperlink.Reset(); | |
| EXPECT_HRESULT_FAILED( | |
| text_accessible->GetCOM()->QueryInterface(IID_PPV_ARGS(&hyperlink))); | |
| hyperlink.Reset(); | |
| EXPECT_HRESULT_SUCCEEDED( | |
| link_accessible->GetCOM()->QueryInterface(IID_PPV_ARGS(&hyperlink))); | |
| hyperlink.Reset(); | |
| EXPECT_HRESULT_SUCCEEDED(root_accessible->GetCOM()->nActions(&n_actions)); | |
| EXPECT_EQ(2, n_actions); | |
| EXPECT_HRESULT_SUCCEEDED(div_accessible->GetCOM()->nActions(&n_actions)); | |
| EXPECT_EQ(2, n_actions); | |
| EXPECT_HRESULT_SUCCEEDED(text_accessible->GetCOM()->nActions(&n_actions)); | |
| EXPECT_EQ(2, n_actions); | |
| EXPECT_HRESULT_SUCCEEDED(link_accessible->GetCOM()->nActions(&n_actions)); | |
| EXPECT_EQ(2, n_actions); | |
| EXPECT_HRESULT_FAILED( | |
| root_accessible->GetCOM()->get_anchor(0, anchor.Receive())); | |
| anchor.Reset(); | |
| HRESULT hr = div_accessible->GetCOM()->get_anchor(0, anchor.Receive()); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(VT_BSTR, anchor.type()); | |
| bstr.Reset(V_BSTR(anchor.ptr())); | |
| EXPECT_EQ(div_hypertext, base::WideToUTF16(bstr.Get())); | |
| bstr.Reset(); | |
| anchor.Reset(); | |
| EXPECT_HRESULT_FAILED( | |
| text_accessible->GetCOM()->get_anchor(0, anchor.Receive())); | |
| anchor.Reset(); | |
| hr = link_accessible->GetCOM()->get_anchor(0, anchor.Receive()); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(VT_BSTR, anchor.type()); | |
| bstr.Reset(V_BSTR(anchor.ptr())); | |
| EXPECT_STREQ(L"here", bstr.Get()); | |
| bstr.Reset(); | |
| anchor.Reset(); | |
| EXPECT_HRESULT_FAILED( | |
| div_accessible->GetCOM()->get_anchor(1, anchor.Receive())); | |
| anchor.Reset(); | |
| EXPECT_HRESULT_FAILED( | |
| link_accessible->GetCOM()->get_anchor(1, anchor.Receive())); | |
| anchor.Reset(); | |
| EXPECT_HRESULT_FAILED( | |
| root_accessible->GetCOM()->get_anchorTarget(0, anchor_target.Receive())); | |
| anchor_target.Reset(); | |
| hr = div_accessible->GetCOM()->get_anchorTarget(0, anchor_target.Receive()); | |
| EXPECT_EQ(S_FALSE, hr); | |
| EXPECT_EQ(VT_BSTR, anchor_target.type()); | |
| bstr.Reset(V_BSTR(anchor_target.ptr())); | |
| // Target should be empty. | |
| EXPECT_STREQ(L"", bstr.Get()); | |
| bstr.Reset(); | |
| anchor_target.Reset(); | |
| EXPECT_HRESULT_FAILED( | |
| text_accessible->GetCOM()->get_anchorTarget(0, anchor_target.Receive())); | |
| anchor_target.Reset(); | |
| hr = link_accessible->GetCOM()->get_anchorTarget(0, anchor_target.Receive()); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(VT_BSTR, anchor_target.type()); | |
| bstr.Reset(V_BSTR(anchor_target.ptr())); | |
| EXPECT_STREQ(L"example.com", bstr.Get()); | |
| bstr.Reset(); | |
| anchor_target.Reset(); | |
| EXPECT_HRESULT_FAILED( | |
| div_accessible->GetCOM()->get_anchorTarget(1, anchor_target.Receive())); | |
| anchor_target.Reset(); | |
| EXPECT_HRESULT_FAILED( | |
| link_accessible->GetCOM()->get_anchorTarget(1, anchor_target.Receive())); | |
| anchor_target.Reset(); | |
| EXPECT_HRESULT_FAILED( | |
| root_accessible->GetCOM()->get_startIndex(&start_index)); | |
| EXPECT_HRESULT_SUCCEEDED( | |
| div_accessible->GetCOM()->get_startIndex(&start_index)); | |
| EXPECT_EQ(0, start_index); | |
| EXPECT_HRESULT_FAILED( | |
| text_accessible->GetCOM()->get_startIndex(&start_index)); | |
| EXPECT_HRESULT_SUCCEEDED( | |
| link_accessible->GetCOM()->get_startIndex(&start_index)); | |
| EXPECT_EQ(6, start_index); | |
| EXPECT_HRESULT_FAILED(root_accessible->GetCOM()->get_endIndex(&end_index)); | |
| EXPECT_HRESULT_SUCCEEDED(div_accessible->GetCOM()->get_endIndex(&end_index)); | |
| EXPECT_EQ(1, end_index); | |
| EXPECT_HRESULT_FAILED(text_accessible->GetCOM()->get_endIndex(&end_index)); | |
| EXPECT_HRESULT_SUCCEEDED(link_accessible->GetCOM()->get_endIndex(&end_index)); | |
| EXPECT_EQ(7, end_index); | |
| } | |
| TEST_F(BrowserAccessibilityWinTest, TestTextAttributesInContentEditables) { | |
| AXNodeData root; | |
| root.id = 1; | |
| root.role = ax::mojom::Role::kRootWebArea; | |
| root.AddState(ax::mojom::State::kFocusable); | |
| AXNodeData div_editable; | |
| div_editable.id = 2; | |
| div_editable.role = ax::mojom::Role::kGenericContainer; | |
| div_editable.AddState(ax::mojom::State::kEditable); | |
| div_editable.AddState(ax::mojom::State::kRichlyEditable); | |
| div_editable.AddBoolAttribute( | |
| ax::mojom::BoolAttribute::kNonAtomicTextFieldRoot, true); | |
| div_editable.AddState(ax::mojom::State::kFocusable); | |
| div_editable.AddStringAttribute(ax::mojom::StringAttribute::kFontFamily, | |
| "Helvetica"); | |
| AXNodeData text_before; | |
| text_before.id = 3; | |
| text_before.role = ax::mojom::Role::kStaticText; | |
| text_before.AddState(ax::mojom::State::kEditable); | |
| text_before.AddState(ax::mojom::State::kRichlyEditable); | |
| text_before.SetName("Before "); | |
| text_before.AddTextStyle(ax::mojom::TextStyle::kBold); | |
| text_before.AddTextStyle(ax::mojom::TextStyle::kItalic); | |
| AXNodeData link; | |
| link.id = 4; | |
| link.role = ax::mojom::Role::kLink; | |
| link.AddState(ax::mojom::State::kEditable); | |
| link.AddState(ax::mojom::State::kRichlyEditable); | |
| link.AddState(ax::mojom::State::kFocusable); | |
| link.AddState(ax::mojom::State::kLinked); | |
| link.SetName("lnk"); | |
| link.AddTextStyle(ax::mojom::TextStyle::kUnderline); | |
| AXNodeData link_text; | |
| link_text.id = 5; | |
| link_text.role = ax::mojom::Role::kStaticText; | |
| link_text.AddState(ax::mojom::State::kEditable); | |
| link_text.AddState(ax::mojom::State::kRichlyEditable); | |
| link_text.AddState(ax::mojom::State::kFocusable); | |
| link_text.AddState(ax::mojom::State::kLinked); | |
| link_text.SetName("lnk"); | |
| link_text.AddTextStyle(ax::mojom::TextStyle::kUnderline); | |
| // The name "lnk" is misspelled. | |
| std::vector<int32_t> marker_types{ | |
| static_cast<int32_t>(ax::mojom::MarkerType::kSpelling)}; | |
| std::vector<int32_t> highlight_types{ | |
| static_cast<int32_t>(ax::mojom::HighlightType::kNone)}; | |
| std::vector<int32_t> marker_starts{0}; | |
| std::vector<int32_t> marker_ends{3}; | |
| link_text.AddIntListAttribute(ax::mojom::IntListAttribute::kMarkerTypes, | |
| marker_types); | |
| link_text.AddIntListAttribute(ax::mojom::IntListAttribute::kHighlightTypes, | |
| highlight_types); | |
| link_text.AddIntListAttribute(ax::mojom::IntListAttribute::kMarkerStarts, | |
| marker_starts); | |
| link_text.AddIntListAttribute(ax::mojom::IntListAttribute::kMarkerEnds, | |
| marker_ends); | |
| AXNodeData text_after; | |
| text_after.id = 6; | |
| text_after.role = ax::mojom::Role::kStaticText; | |
| text_after.AddState(ax::mojom::State::kEditable); | |
| text_after.AddState(ax::mojom::State::kRichlyEditable); | |
| text_after.SetName(" after."); | |
| // Leave text style as normal. | |
| root.child_ids.push_back(div_editable.id); | |
| div_editable.child_ids.push_back(text_before.id); | |
| div_editable.child_ids.push_back(link.id); | |
| div_editable.child_ids.push_back(text_after.id); | |
| link.child_ids.push_back(link_text.id); | |
| AXTreeUpdate update = MakeAXTreeUpdateForTesting( | |
| root, div_editable, text_before, link, link_text, text_after); | |
| std::unique_ptr<BrowserAccessibilityManager> manager( | |
| BrowserAccessibilityManager::Create( | |
| update, node_id_delegate_, | |
| test_browser_accessibility_delegate_.get())); | |
| ASSERT_NE(nullptr, manager->GetBrowserAccessibilityRoot()); | |
| BrowserAccessibilityWin* ax_root = | |
| ToBrowserAccessibilityWin(manager->GetBrowserAccessibilityRoot()); | |
| ASSERT_NE(nullptr, ax_root); | |
| ASSERT_EQ(1U, ax_root->PlatformChildCount()); | |
| BrowserAccessibilityWin* ax_div = | |
| ToBrowserAccessibilityWin(ax_root->PlatformGetChild(0)); | |
| ASSERT_NE(nullptr, ax_div); | |
| ASSERT_EQ(3U, ax_div->PlatformChildCount()); | |
| BrowserAccessibilityWin* ax_before = | |
| ToBrowserAccessibilityWin(ax_div->PlatformGetChild(0)); | |
| ASSERT_NE(nullptr, ax_before); | |
| BrowserAccessibilityWin* ax_link = | |
| ToBrowserAccessibilityWin(ax_div->PlatformGetChild(1)); | |
| ASSERT_NE(nullptr, ax_link); | |
| ASSERT_EQ(1U, ax_link->PlatformChildCount()); | |
| BrowserAccessibilityWin* ax_after = | |
| ToBrowserAccessibilityWin(ax_div->PlatformGetChild(2)); | |
| ASSERT_NE(nullptr, ax_after); | |
| BrowserAccessibilityWin* ax_link_text = | |
| ToBrowserAccessibilityWin(ax_link->PlatformGetChild(0)); | |
| ASSERT_NE(nullptr, ax_link_text); | |
| HRESULT hr; | |
| LONG n_characters, start_offset, end_offset; | |
| base::win::ScopedBstr text_attributes; | |
| ASSERT_HRESULT_SUCCEEDED(ax_root->GetCOM()->get_nCharacters(&n_characters)); | |
| ASSERT_EQ(1, n_characters); | |
| ASSERT_HRESULT_SUCCEEDED(ax_div->GetCOM()->get_nCharacters(&n_characters)); | |
| ASSERT_EQ(15, n_characters); | |
| // Test the style of the root. | |
| hr = ax_root->GetCOM()->get_attributes(0, &start_offset, &end_offset, | |
| text_attributes.Receive()); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(0, start_offset); | |
| EXPECT_EQ(1, end_offset); | |
| EXPECT_NE(std::wstring::npos, | |
| std::wstring(text_attributes.Get()).find(L"font-family:Helvetica")); | |
| text_attributes.Reset(); | |
| // Test the style of text_before. | |
| for (LONG offset = 0; offset < 7; ++offset) { | |
| hr = ax_div->GetCOM()->get_attributes(0, &start_offset, &end_offset, | |
| text_attributes.Receive()); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(0, start_offset); | |
| EXPECT_EQ(7, end_offset); | |
| std::wstring attributes(text_attributes.Get()); | |
| EXPECT_NE(std::wstring::npos, attributes.find(L"font-family:Helvetica")); | |
| EXPECT_NE(std::wstring::npos, attributes.find(L"font-weight:bold")); | |
| EXPECT_NE(std::wstring::npos, attributes.find(L"font-style:italic")); | |
| text_attributes.Reset(); | |
| } | |
| // Test the style of the link. | |
| hr = ax_link->GetCOM()->get_attributes(0, &start_offset, &end_offset, | |
| text_attributes.Receive()); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(0, start_offset); | |
| EXPECT_EQ(3, end_offset); | |
| EXPECT_NE(std::wstring::npos, | |
| std::wstring(text_attributes.Get()).find(L"font-family:Helvetica")); | |
| EXPECT_EQ(std::wstring::npos, | |
| std::wstring(text_attributes.Get()).find(L"font-weight:")); | |
| EXPECT_EQ(std::wstring::npos, | |
| std::wstring(text_attributes.Get()).find(L"font-style:")); | |
| EXPECT_NE( | |
| std::wstring::npos, | |
| std::wstring(text_attributes.Get()).find(L"text-underline-style:solid")); | |
| EXPECT_EQ(std::wstring::npos, | |
| std::wstring(text_attributes.Get()).find(L"text-underline-type:")); | |
| // For compatibility with Firefox, spelling attributes should also be | |
| // propagated to the parent of static text leaves. | |
| EXPECT_NE(std::wstring::npos, | |
| std::wstring(text_attributes.Get()).find(L"invalid:spelling")); | |
| text_attributes.Reset(); | |
| hr = ax_link_text->GetCOM()->get_attributes(2, &start_offset, &end_offset, | |
| text_attributes.Receive()); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(0, start_offset); | |
| EXPECT_EQ(3, end_offset); | |
| EXPECT_NE(std::wstring::npos, | |
| std::wstring(text_attributes.Get()).find(L"font-family:Helvetica")); | |
| EXPECT_EQ(std::wstring::npos, | |
| std::wstring(text_attributes.Get()).find(L"font-weight:")); | |
| EXPECT_EQ(std::wstring::npos, | |
| std::wstring(text_attributes.Get()).find(L"font-style:")); | |
| EXPECT_NE( | |
| std::wstring::npos, | |
| std::wstring(text_attributes.Get()).find(L"text-underline-style:solid")); | |
| EXPECT_EQ(std::wstring::npos, | |
| std::wstring(text_attributes.Get()).find(L"text-underline-type:")); | |
| EXPECT_NE(std::wstring::npos, | |
| std::wstring(text_attributes.Get()).find(L"invalid:spelling")); | |
| text_attributes.Reset(); | |
| // Test the style of text_after. | |
| for (LONG offset = 8; offset < 15; ++offset) { | |
| hr = ax_div->GetCOM()->get_attributes(offset, &start_offset, &end_offset, | |
| text_attributes.Receive()); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(8, start_offset); | |
| EXPECT_EQ(15, end_offset); | |
| std::wstring attributes(text_attributes.Get()); | |
| EXPECT_NE(std::wstring::npos, attributes.find(L"font-family:Helvetica")); | |
| EXPECT_EQ(std::wstring::npos, attributes.find(L"font-weight:")); | |
| EXPECT_EQ(std::wstring::npos, attributes.find(L"font-style:")); | |
| EXPECT_EQ(std::wstring::npos, std::wstring(text_attributes.Get()) | |
| .find(L"text-underline-style:solid")); | |
| EXPECT_EQ( | |
| std::wstring::npos, | |
| std::wstring(text_attributes.Get()).find(L"text-underline-type:")); | |
| EXPECT_EQ(std::wstring::npos, attributes.find(L"invalid:spelling")); | |
| text_attributes.Reset(); | |
| } | |
| // Test the style of the static text nodes. | |
| hr = ax_before->GetCOM()->get_attributes(6, &start_offset, &end_offset, | |
| text_attributes.Receive()); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(0, start_offset); | |
| EXPECT_EQ(7, end_offset); | |
| EXPECT_NE(std::wstring::npos, | |
| std::wstring(text_attributes.Get()).find(L"font-family:Helvetica")); | |
| EXPECT_NE(std::wstring::npos, | |
| std::wstring(text_attributes.Get()).find(L"font-weight:bold")); | |
| EXPECT_NE(std::wstring::npos, | |
| std::wstring(text_attributes.Get()).find(L"font-style:italic")); | |
| EXPECT_EQ(std::wstring::npos, | |
| std::wstring(text_attributes.Get()).find(L"invalid:spelling")); | |
| text_attributes.Reset(); | |
| hr = ax_after->GetCOM()->get_attributes(6, &start_offset, &end_offset, | |
| text_attributes.Receive()); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(0, start_offset); | |
| EXPECT_EQ(7, end_offset); | |
| EXPECT_NE(std::wstring::npos, | |
| std::wstring(text_attributes.Get()).find(L"font-family:Helvetica")); | |
| EXPECT_EQ(std::wstring::npos, | |
| std::wstring(text_attributes.Get()).find(L"font-weight:")); | |
| EXPECT_EQ(std::wstring::npos, | |
| std::wstring(text_attributes.Get()).find(L"font-style:")); | |
| EXPECT_EQ(std::wstring::npos, | |
| std::wstring(text_attributes.Get()).find(L"text-underline-style:")); | |
| EXPECT_EQ(std::wstring::npos, | |
| std::wstring(text_attributes.Get()).find(L"text-underline-type:")); | |
| EXPECT_EQ(std::wstring::npos, | |
| std::wstring(text_attributes.Get()).find(L"invalid:spelling")); | |
| text_attributes.Reset(); | |
| manager.reset(); | |
| } | |
| TEST_F(BrowserAccessibilityWinTest, | |
| TestExistingMisspellingsInSimpleTextFields) { | |
| std::string value1("Testing ."); | |
| // The word "helo" is misspelled. | |
| std::string value2("Helo there."); | |
| LONG value1_length = static_cast<LONG>(value1.length()); | |
| LONG value2_length = static_cast<LONG>(value2.length()); | |
| LONG combo_box_value_length = value1_length + value2_length; | |
| AXNodeData root; | |
| root.id = 1; | |
| root.role = ax::mojom::Role::kRootWebArea; | |
| root.AddState(ax::mojom::State::kFocusable); | |
| AXNodeData combo_box; | |
| combo_box.id = 2; | |
| combo_box.role = ax::mojom::Role::kTextFieldWithComboBox; | |
| combo_box.AddState(ax::mojom::State::kEditable); | |
| combo_box.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, "input"); | |
| combo_box.AddStringAttribute(ax::mojom::StringAttribute::kInputType, "text"); | |
| combo_box.AddState(ax::mojom::State::kFocusable); | |
| combo_box.SetValue(value1 + value2); | |
| AXNodeData combo_box_div; | |
| combo_box_div.id = 3; | |
| combo_box_div.role = ax::mojom::Role::kGenericContainer; | |
| combo_box_div.AddState(ax::mojom::State::kEditable); | |
| AXNodeData static_text1; | |
| static_text1.id = 4; | |
| static_text1.role = ax::mojom::Role::kStaticText; | |
| static_text1.AddState(ax::mojom::State::kEditable); | |
| static_text1.SetName(value1); | |
| AXNodeData static_text2; | |
| static_text2.id = 5; | |
| static_text2.role = ax::mojom::Role::kStaticText; | |
| static_text2.AddState(ax::mojom::State::kEditable); | |
| static_text2.SetName(value2); | |
| std::vector<int32_t> marker_types; | |
| marker_types.push_back( | |
| static_cast<int32_t>(ax::mojom::MarkerType::kSpelling)); | |
| std::vector<int32_t> highlight_types; | |
| highlight_types.push_back( | |
| static_cast<int32_t>(ax::mojom::HighlightType::kNone)); | |
| std::vector<int32_t> marker_starts; | |
| marker_starts.push_back(0); | |
| std::vector<int32_t> marker_ends; | |
| marker_ends.push_back(4); | |
| static_text2.AddIntListAttribute(ax::mojom::IntListAttribute::kMarkerTypes, | |
| marker_types); | |
| static_text2.AddIntListAttribute(ax::mojom::IntListAttribute::kHighlightTypes, | |
| highlight_types); | |
| static_text2.AddIntListAttribute(ax::mojom::IntListAttribute::kMarkerStarts, | |
| marker_starts); | |
| static_text2.AddIntListAttribute(ax::mojom::IntListAttribute::kMarkerEnds, | |
| marker_ends); | |
| root.child_ids.push_back(combo_box.id); | |
| combo_box.child_ids.push_back(combo_box_div.id); | |
| combo_box_div.child_ids.push_back(static_text1.id); | |
| combo_box_div.child_ids.push_back(static_text2.id); | |
| std::unique_ptr<BrowserAccessibilityManager> manager( | |
| BrowserAccessibilityManager::Create( | |
| MakeAXTreeUpdateForTesting(root, combo_box, combo_box_div, | |
| static_text1, static_text2), | |
| node_id_delegate_, test_browser_accessibility_delegate_.get())); | |
| ASSERT_NE(nullptr, manager->GetBrowserAccessibilityRoot()); | |
| BrowserAccessibilityWin* ax_root = | |
| ToBrowserAccessibilityWin(manager->GetBrowserAccessibilityRoot()); | |
| ASSERT_NE(nullptr, ax_root); | |
| ASSERT_EQ(1U, ax_root->PlatformChildCount()); | |
| BrowserAccessibilityWin* ax_combo_box = | |
| ToBrowserAccessibilityWin(ax_root->PlatformGetChild(0)); | |
| ASSERT_NE(nullptr, ax_combo_box); | |
| HRESULT hr; | |
| LONG start_offset, end_offset; | |
| base::win::ScopedBstr text_attributes; | |
| // Ensure that the first part of the value is not marked misspelled. | |
| for (LONG offset = 0; offset < value1_length; ++offset) { | |
| hr = ax_combo_box->GetCOM()->get_attributes( | |
| offset, &start_offset, &end_offset, text_attributes.Receive()); | |
| EXPECT_TRUE(std::wstring(text_attributes.Get()).empty()); | |
| EXPECT_EQ(0, start_offset); | |
| EXPECT_EQ(value1_length, end_offset); | |
| text_attributes.Reset(); | |
| } | |
| // Ensure that "helo" is marked misspelled. | |
| for (LONG offset = value1_length; offset < value1_length + 4; ++offset) { | |
| hr = ax_combo_box->GetCOM()->get_attributes( | |
| offset, &start_offset, &end_offset, text_attributes.Receive()); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(value1_length, start_offset); | |
| EXPECT_EQ(value1_length + 4, end_offset); | |
| EXPECT_NE(std::wstring::npos, | |
| std::wstring(text_attributes.Get()).find(L"invalid:spelling")); | |
| text_attributes.Reset(); | |
| } | |
| // Ensure that the last part of the value is not marked misspelled. | |
| for (LONG offset = value1_length + 4; offset < combo_box_value_length; | |
| ++offset) { | |
| hr = ax_combo_box->GetCOM()->get_attributes( | |
| offset, &start_offset, &end_offset, text_attributes.Receive()); | |
| EXPECT_TRUE(std::wstring(text_attributes.Get()).empty()); | |
| EXPECT_EQ(value1_length + 4, start_offset); | |
| EXPECT_EQ(combo_box_value_length, end_offset); | |
| text_attributes.Reset(); | |
| } | |
| manager.reset(); | |
| } | |
| TEST_F(BrowserAccessibilityWinTest, TestNewMisspellingsInSimpleTextFields) { | |
| std::string value1("Testing ."); | |
| // The word "helo" is misspelled. | |
| std::string value2("Helo there."); | |
| LONG value1_length = static_cast<LONG>(value1.length()); | |
| LONG value2_length = static_cast<LONG>(value2.length()); | |
| LONG combo_box_value_length = value1_length + value2_length; | |
| AXNodeData root; | |
| root.id = 1; | |
| root.role = ax::mojom::Role::kRootWebArea; | |
| root.AddState(ax::mojom::State::kFocusable); | |
| AXNodeData combo_box; | |
| combo_box.id = 2; | |
| combo_box.role = ax::mojom::Role::kTextFieldWithComboBox; | |
| combo_box.AddState(ax::mojom::State::kEditable); | |
| combo_box.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, "input"); | |
| combo_box.AddStringAttribute(ax::mojom::StringAttribute::kInputType, "text"); | |
| combo_box.AddState(ax::mojom::State::kFocusable); | |
| combo_box.SetValue(value1 + value2); | |
| AXNodeData combo_box_div; | |
| combo_box_div.id = 3; | |
| combo_box_div.role = ax::mojom::Role::kGenericContainer; | |
| combo_box_div.AddState(ax::mojom::State::kEditable); | |
| AXNodeData static_text1; | |
| static_text1.id = 4; | |
| static_text1.role = ax::mojom::Role::kStaticText; | |
| static_text1.AddState(ax::mojom::State::kEditable); | |
| static_text1.SetName(value1); | |
| AXNodeData static_text2; | |
| static_text2.id = 5; | |
| static_text2.role = ax::mojom::Role::kStaticText; | |
| static_text2.AddState(ax::mojom::State::kEditable); | |
| static_text2.SetName(value2); | |
| root.child_ids.push_back(combo_box.id); | |
| combo_box.child_ids.push_back(combo_box_div.id); | |
| combo_box_div.child_ids.push_back(static_text1.id); | |
| combo_box_div.child_ids.push_back(static_text2.id); | |
| std::unique_ptr<BrowserAccessibilityManager> manager( | |
| BrowserAccessibilityManager::Create( | |
| MakeAXTreeUpdateForTesting(root, combo_box, combo_box_div, | |
| static_text1, static_text2), | |
| node_id_delegate_, test_browser_accessibility_delegate_.get())); | |
| ASSERT_NE(nullptr, manager->GetBrowserAccessibilityRoot()); | |
| BrowserAccessibilityWin* ax_root = | |
| ToBrowserAccessibilityWin(manager->GetBrowserAccessibilityRoot()); | |
| ASSERT_NE(nullptr, ax_root); | |
| ASSERT_EQ(1U, ax_root->PlatformChildCount()); | |
| BrowserAccessibilityWin* ax_combo_box = | |
| ToBrowserAccessibilityWin(ax_root->PlatformGetChild(0)); | |
| ASSERT_NE(nullptr, ax_combo_box); | |
| HRESULT hr; | |
| LONG start_offset, end_offset; | |
| base::win::ScopedBstr text_attributes; | |
| // Ensure that nothing is marked misspelled. | |
| for (LONG offset = 0; offset < combo_box_value_length; ++offset) { | |
| hr = ax_combo_box->GetCOM()->get_attributes( | |
| offset, &start_offset, &end_offset, text_attributes.Receive()); | |
| EXPECT_TRUE(std::wstring(text_attributes.Get()).empty()); | |
| EXPECT_EQ(0, start_offset); | |
| EXPECT_EQ(combo_box_value_length, end_offset); | |
| text_attributes.Reset(); | |
| } | |
| // Add the spelling markers on "helo". | |
| std::vector<int32_t> marker_types{ | |
| static_cast<int32_t>(ax::mojom::MarkerType::kSpelling)}; | |
| std::vector<int32_t> highlight_types{ | |
| static_cast<int32_t>(ax::mojom::HighlightType::kNone)}; | |
| std::vector<int32_t> marker_starts{0}; | |
| std::vector<int32_t> marker_ends{4}; | |
| static_text2.AddIntListAttribute(ax::mojom::IntListAttribute::kMarkerTypes, | |
| marker_types); | |
| static_text2.AddIntListAttribute(ax::mojom::IntListAttribute::kHighlightTypes, | |
| highlight_types); | |
| static_text2.AddIntListAttribute(ax::mojom::IntListAttribute::kMarkerStarts, | |
| marker_starts); | |
| static_text2.AddIntListAttribute(ax::mojom::IntListAttribute::kMarkerEnds, | |
| marker_ends); | |
| AXTree* tree = const_cast<AXTree*>(manager->ax_tree()); | |
| ASSERT_NE(nullptr, tree); | |
| AXTreeUpdate update = MakeAXTreeUpdateForTesting(static_text2); | |
| update.tree_data.tree_id = manager->GetTreeID(); | |
| ASSERT_TRUE(tree->Unserialize(update)); | |
| // Ensure that value1 is still not marked misspelled. | |
| for (LONG offset = 0; offset < value1_length; ++offset) { | |
| hr = ax_combo_box->GetCOM()->get_attributes( | |
| offset, &start_offset, &end_offset, text_attributes.Receive()); | |
| EXPECT_TRUE(std::wstring(text_attributes.Get()).empty()); | |
| EXPECT_EQ(0, start_offset); | |
| EXPECT_EQ(value1_length, end_offset); | |
| text_attributes.Reset(); | |
| } | |
| // Ensure that "helo" is now marked misspelled. | |
| for (LONG offset = value1_length; offset < value1_length + 4; ++offset) { | |
| hr = ax_combo_box->GetCOM()->get_attributes( | |
| offset, &start_offset, &end_offset, text_attributes.Receive()); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(value1_length, start_offset); | |
| EXPECT_EQ(value1_length + 4, end_offset); | |
| EXPECT_NE(std::wstring::npos, | |
| std::wstring(text_attributes.Get()).find(L"invalid:spelling")); | |
| text_attributes.Reset(); | |
| } | |
| // Ensure that the last part of the value is not marked misspelled. | |
| for (LONG offset = value1_length + 4; offset < combo_box_value_length; | |
| ++offset) { | |
| hr = ax_combo_box->GetCOM()->get_attributes( | |
| offset, &start_offset, &end_offset, text_attributes.Receive()); | |
| EXPECT_TRUE(std::wstring(text_attributes.Get()).empty()); | |
| EXPECT_EQ(value1_length + 4, start_offset); | |
| EXPECT_EQ(combo_box_value_length, end_offset); | |
| text_attributes.Reset(); | |
| } | |
| manager.reset(); | |
| } | |
| TEST_F(BrowserAccessibilityWinTest, TestCustomHighlightsInSimpleTextFields) { | |
| AXNodeData root; | |
| root.id = 1; | |
| root.role = ax::mojom::Role::kRootWebArea; | |
| root.AddState(ax::mojom::State::kFocusable); | |
| // The text field will contain the text "Helo world..", with "Helo" marked | |
| // misspelled, "world" highlighted, and ".." marked as a grammar error. | |
| const std::string text = "Helo world.."; | |
| AXNodeData text_field_container; | |
| text_field_container.id = 2; | |
| text_field_container.role = ax::mojom::Role::kGenericContainer; | |
| text_field_container.AddState(ax::mojom::State::kEditable); | |
| text_field_container.AddState(ax::mojom::State::kFocusable); | |
| text_field_container.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, | |
| "input"); | |
| text_field_container.AddStringAttribute( | |
| ax::mojom::StringAttribute::kInputType, "text"); | |
| text_field_container.SetValue(text); | |
| AXNodeData text_field; | |
| text_field.id = 3; | |
| text_field.role = ax::mojom::Role::kStaticText; | |
| text_field.AddState(ax::mojom::State::kEditable); | |
| text_field.SetName(text); | |
| std::vector<int32_t> marker_types{ | |
| static_cast<int32_t>(ax::mojom::MarkerType::kHighlight), | |
| static_cast<int32_t>(ax::mojom::MarkerType::kHighlight), | |
| static_cast<int32_t>(ax::mojom::MarkerType::kHighlight)}; | |
| std::vector<int32_t> highlight_types{ | |
| static_cast<int32_t>(ax::mojom::HighlightType::kSpellingError), | |
| static_cast<int32_t>(ax::mojom::HighlightType::kHighlight), | |
| static_cast<int32_t>(ax::mojom::HighlightType::kGrammarError)}; | |
| std::vector<int32_t> marker_starts{0, 5, 10}; | |
| std::vector<int32_t> marker_ends{4, 10, 12}; | |
| text_field.AddIntListAttribute(ax::mojom::IntListAttribute::kMarkerTypes, | |
| marker_types); | |
| text_field.AddIntListAttribute(ax::mojom::IntListAttribute::kHighlightTypes, | |
| highlight_types); | |
| text_field.AddIntListAttribute(ax::mojom::IntListAttribute::kMarkerStarts, | |
| marker_starts); | |
| text_field.AddIntListAttribute(ax::mojom::IntListAttribute::kMarkerEnds, | |
| marker_ends); | |
| root.child_ids.push_back(2); | |
| text_field_container.child_ids.push_back(3); | |
| std::unique_ptr<BrowserAccessibilityManager> manager( | |
| BrowserAccessibilityManager::Create( | |
| MakeAXTreeUpdateForTesting(root, text_field_container, text_field), | |
| node_id_delegate_, test_browser_accessibility_delegate_.get())); | |
| ASSERT_NE(nullptr, manager->GetBrowserAccessibilityRoot()); | |
| BrowserAccessibilityWin* ax_root = | |
| ToBrowserAccessibilityWin(manager->GetBrowserAccessibilityRoot()); | |
| ASSERT_NE(nullptr, ax_root); | |
| ASSERT_EQ(1U, ax_root->PlatformChildCount()); | |
| BrowserAccessibilityWin* ax_text_field = | |
| ToBrowserAccessibilityWin(ax_root->PlatformGetChild(0)); | |
| ASSERT_NE(nullptr, ax_text_field); | |
| HRESULT hr; | |
| LONG start_offset, end_offset; | |
| base::win::ScopedBstr text_attributes; | |
| // Ensure that "Helo" is marked misspelled. | |
| for (LONG offset = 0; offset < 4; ++offset) { | |
| hr = ax_text_field->GetCOM()->get_attributes( | |
| offset, &start_offset, &end_offset, text_attributes.Receive()); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(0, start_offset); | |
| EXPECT_EQ(4, end_offset); | |
| EXPECT_NE(std::wstring::npos, | |
| std::wstring(text_attributes.Get()).find(L"invalid:spelling")); | |
| text_attributes.Reset(); | |
| } | |
| // Ensure that the next character " " is not marked. | |
| hr = ax_text_field->GetCOM()->get_attributes(4, &start_offset, &end_offset, | |
| text_attributes.Receive()); | |
| EXPECT_TRUE(std::wstring(text_attributes.Get()).empty()); | |
| EXPECT_EQ(4, start_offset); | |
| EXPECT_EQ(5, end_offset); | |
| text_attributes.Reset(); | |
| // Ensure that "world" is a highlight. | |
| for (LONG offset = 5; offset < 10; ++offset) { | |
| hr = ax_text_field->GetCOM()->get_attributes( | |
| offset, &start_offset, &end_offset, text_attributes.Receive()); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(5, start_offset); | |
| EXPECT_EQ(10, end_offset); | |
| EXPECT_NE(std::wstring::npos, | |
| std::wstring(text_attributes.Get()).find(L"mark:true")); | |
| text_attributes.Reset(); | |
| } | |
| // Ensure that ".." is marked as a grammar error. | |
| for (LONG offset = 10; offset < 12; ++offset) { | |
| hr = ax_text_field->GetCOM()->get_attributes( | |
| offset, &start_offset, &end_offset, text_attributes.Receive()); | |
| EXPECT_EQ(S_OK, hr); | |
| EXPECT_EQ(10, start_offset); | |
| EXPECT_EQ(12, end_offset); | |
| EXPECT_NE(std::wstring::npos, | |
| std::wstring(text_attributes.Get()).find(L"invalid:grammar")); | |
| text_attributes.Reset(); | |
| } | |
| manager.reset(); | |
| } | |
| TEST_F(BrowserAccessibilityWinTest, TestDeepestFirstLastChild) { | |
| AXNodeData root; | |
| root.id = 1; | |
| root.role = ax::mojom::Role::kRootWebArea; | |
| AXNodeData child1; | |
| child1.id = 2; | |
| child1.role = ax::mojom::Role::kStaticText; | |
| root.child_ids.push_back(2); | |
| AXNodeData child2; | |
| child2.id = 3; | |
| child2.role = ax::mojom::Role::kStaticText; | |
| root.child_ids.push_back(3); | |
| AXNodeData child2_child1; | |
| child2_child1.id = 4; | |
| child2_child1.role = ax::mojom::Role::kInlineTextBox; | |
| child2.child_ids.push_back(4); | |
| AXNodeData child2_child2; | |
| child2_child2.id = 5; | |
| child2_child2.role = ax::mojom::Role::kInlineTextBox; | |
| child2.child_ids.push_back(5); | |
| std::unique_ptr<BrowserAccessibilityManager> manager( | |
| BrowserAccessibilityManager::Create( | |
| MakeAXTreeUpdateForTesting(root, child1, child2, child2_child1, | |
| child2_child2), | |
| node_id_delegate_, test_browser_accessibility_delegate_.get())); | |
| BrowserAccessibility* root_accessible = | |
| manager->GetBrowserAccessibilityRoot(); | |
| ASSERT_NE(nullptr, root_accessible); | |
| ASSERT_EQ(2U, root_accessible->PlatformChildCount()); | |
| BrowserAccessibility* child1_accessible = | |
| root_accessible->PlatformGetChild(0); | |
| ASSERT_NE(nullptr, child1_accessible); | |
| BrowserAccessibility* child2_accessible = | |
| root_accessible->PlatformGetChild(1); | |
| ASSERT_NE(nullptr, child2_accessible); | |
| ASSERT_EQ(0U, child2_accessible->PlatformChildCount()); | |
| ASSERT_EQ(2U, child2_accessible->InternalChildCount()); | |
| BrowserAccessibility* child2_child1_accessible = | |
| child2_accessible->InternalGetChild(0); | |
| ASSERT_NE(nullptr, child2_child1_accessible); | |
| BrowserAccessibility* child2_child2_accessible = | |
| child2_accessible->InternalGetChild(1); | |
| ASSERT_NE(nullptr, child2_child2_accessible); | |
| EXPECT_EQ(child1_accessible, root_accessible->PlatformDeepestFirstChild()); | |
| EXPECT_EQ(child1_accessible, root_accessible->InternalDeepestFirstChild()); | |
| EXPECT_EQ(child2_accessible, root_accessible->PlatformDeepestLastChild()); | |
| EXPECT_EQ(child2_child2_accessible, | |
| root_accessible->InternalDeepestLastChild()); | |
| EXPECT_EQ(nullptr, child1_accessible->PlatformDeepestFirstChild()); | |
| EXPECT_EQ(nullptr, child1_accessible->InternalDeepestFirstChild()); | |
| EXPECT_EQ(nullptr, child1_accessible->PlatformDeepestLastChild()); | |
| EXPECT_EQ(nullptr, child1_accessible->InternalDeepestLastChild()); | |
| EXPECT_EQ(nullptr, child2_accessible->PlatformDeepestFirstChild()); | |
| EXPECT_EQ(child2_child1_accessible, | |
| child2_accessible->InternalDeepestFirstChild()); | |
| EXPECT_EQ(nullptr, child2_accessible->PlatformDeepestLastChild()); | |
| EXPECT_EQ(child2_child2_accessible, | |
| child2_accessible->InternalDeepestLastChild()); | |
| EXPECT_EQ(nullptr, child2_child1_accessible->PlatformDeepestFirstChild()); | |
| EXPECT_EQ(nullptr, child2_child1_accessible->InternalDeepestFirstChild()); | |
| EXPECT_EQ(nullptr, child2_child1_accessible->PlatformDeepestLastChild()); | |
| EXPECT_EQ(nullptr, child2_child1_accessible->InternalDeepestLastChild()); | |
| EXPECT_EQ(nullptr, child2_child2_accessible->PlatformDeepestFirstChild()); | |
| EXPECT_EQ(nullptr, child2_child2_accessible->InternalDeepestFirstChild()); | |
| EXPECT_EQ(nullptr, child2_child2_accessible->PlatformDeepestLastChild()); | |
| EXPECT_EQ(nullptr, child2_child2_accessible->InternalDeepestLastChild()); | |
| } | |
| TEST_F(BrowserAccessibilityWinTest, TestInheritedStringAttributes) { | |
| AXNodeData root; | |
| root.id = 1; | |
| root.role = ax::mojom::Role::kRootWebArea; | |
| root.AddStringAttribute(ax::mojom::StringAttribute::kLanguage, "en-US"); | |
| root.AddStringAttribute(ax::mojom::StringAttribute::kFontFamily, "Helvetica"); | |
| AXNodeData child1; | |
| child1.id = 2; | |
| child1.role = ax::mojom::Role::kStaticText; | |
| root.child_ids.push_back(2); | |
| AXNodeData child2; | |
| child2.id = 3; | |
| child2.role = ax::mojom::Role::kStaticText; | |
| child2.AddStringAttribute(ax::mojom::StringAttribute::kLanguage, "fr"); | |
| child2.AddStringAttribute(ax::mojom::StringAttribute::kFontFamily, "Arial"); | |
| root.child_ids.push_back(3); | |
| AXNodeData child2_child1; | |
| child2_child1.id = 4; | |
| child2_child1.role = ax::mojom::Role::kInlineTextBox; | |
| child2.child_ids.push_back(4); | |
| AXNodeData child2_child2; | |
| child2_child2.id = 5; | |
| child2_child2.role = ax::mojom::Role::kInlineTextBox; | |
| child2.child_ids.push_back(5); | |
| std::unique_ptr<BrowserAccessibilityManager> manager( | |
| BrowserAccessibilityManager::Create( | |
| MakeAXTreeUpdateForTesting(root, child1, child2, child2_child1, | |
| child2_child2), | |
| node_id_delegate_, test_browser_accessibility_delegate_.get())); | |
| BrowserAccessibility* root_accessible = | |
| manager->GetBrowserAccessibilityRoot(); | |
| ASSERT_NE(nullptr, root_accessible); | |
| BrowserAccessibility* child1_accessible = | |
| root_accessible->PlatformGetChild(0); | |
| ASSERT_NE(nullptr, child1_accessible); | |
| BrowserAccessibility* child2_accessible = | |
| root_accessible->PlatformGetChild(1); | |
| ASSERT_NE(nullptr, child2_accessible); | |
| BrowserAccessibility* child2_child1_accessible = | |
| child2_accessible->InternalGetChild(0); | |
| ASSERT_NE(nullptr, child2_child1_accessible); | |
| BrowserAccessibility* child2_child2_accessible = | |
| child2_accessible->InternalGetChild(1); | |
| ASSERT_NE(nullptr, child2_child2_accessible); | |
| // Test GetInheritedString16Attribute(attribute). | |
| EXPECT_EQ(u"en-US", root_accessible->GetInheritedString16Attribute( | |
| ax::mojom::StringAttribute::kLanguage)); | |
| EXPECT_EQ(u"en-US", child1_accessible->GetInheritedString16Attribute( | |
| ax::mojom::StringAttribute::kLanguage)); | |
| EXPECT_EQ(u"fr", child2_accessible->GetInheritedString16Attribute( | |
| ax::mojom::StringAttribute::kLanguage)); | |
| EXPECT_EQ(u"fr", child2_child1_accessible->GetInheritedString16Attribute( | |
| ax::mojom::StringAttribute::kLanguage)); | |
| EXPECT_EQ(u"fr", child2_child2_accessible->GetInheritedString16Attribute( | |
| ax::mojom::StringAttribute::kLanguage)); | |
| // Test GetInheritedStringAttribute(attribute). | |
| EXPECT_EQ("Helvetica", root_accessible->GetInheritedStringAttribute( | |
| ax::mojom::StringAttribute::kFontFamily)); | |
| EXPECT_EQ("Helvetica", child1_accessible->GetInheritedStringAttribute( | |
| ax::mojom::StringAttribute::kFontFamily)); | |
| EXPECT_EQ("Arial", child2_accessible->GetInheritedStringAttribute( | |
| ax::mojom::StringAttribute::kFontFamily)); | |
| EXPECT_EQ("Arial", child2_child1_accessible->GetInheritedStringAttribute( | |
| ax::mojom::StringAttribute::kFontFamily)); | |
| EXPECT_EQ("Arial", child2_child2_accessible->GetInheritedStringAttribute( | |
| ax::mojom::StringAttribute::kFontFamily)); | |
| } | |
| TEST_F(BrowserAccessibilityWinTest, AccChildOnlyReturnsDescendants) { | |
| AXNodeData root_node; | |
| root_node.id = 1; | |
| root_node.role = ax::mojom::Role::kRootWebArea; | |
| AXNodeData child_node; | |
| child_node.id = 2; | |
| root_node.child_ids.push_back(2); | |
| std::unique_ptr<BrowserAccessibilityManagerWin> manager( | |
| new BrowserAccessibilityManagerWin( | |
| MakeAXTreeUpdateForTesting(root_node, child_node), node_id_delegate_, | |
| test_browser_accessibility_delegate_.get())); | |
| BrowserAccessibility* root = manager->GetBrowserAccessibilityRoot(); | |
| BrowserAccessibility* child = root->PlatformGetChild(0); | |
| base::win::ScopedVariant root_unique_id_variant(-GetUniqueId(root)); | |
| Microsoft::WRL::ComPtr<IDispatch> result; | |
| EXPECT_EQ(E_INVALIDARG, | |
| ToBrowserAccessibilityWin(child)->GetCOM()->get_accChild( | |
| root_unique_id_variant, &result)); | |
| base::win::ScopedVariant child_unique_id_variant(-GetUniqueId(child)); | |
| EXPECT_EQ(S_OK, ToBrowserAccessibilityWin(root)->GetCOM()->get_accChild( | |
| child_unique_id_variant, &result)); | |
| } | |
| // TODO(crbug.com/41439880): Disabled due to flakiness. | |
| TEST_F(BrowserAccessibilityWinTest, DISABLED_TestIAccessible2Relations) { | |
| AXNodeData root; | |
| root.id = 1; | |
| root.role = ax::mojom::Role::kRootWebArea; | |
| // Reflexive relations should be ignored. | |
| std::vector<int32_t> describedby_ids = {1, 2, 3}; | |
| root.AddIntListAttribute(ax::mojom::IntListAttribute::kDescribedbyIds, | |
| describedby_ids); | |
| AXNodeData child1; | |
| child1.id = 2; | |
| child1.role = ax::mojom::Role::kStaticText; | |
| root.child_ids.push_back(2); | |
| AXNodeData child2; | |
| child2.id = 3; | |
| child2.role = ax::mojom::Role::kStaticText; | |
| root.child_ids.push_back(3); | |
| std::unique_ptr<BrowserAccessibilityManager> manager( | |
| BrowserAccessibilityManager::Create( | |
| MakeAXTreeUpdateForTesting(root, child1, child2), node_id_delegate_, | |
| test_browser_accessibility_delegate_.get())); | |
| BrowserAccessibilityWin* ax_root = | |
| ToBrowserAccessibilityWin(manager->GetBrowserAccessibilityRoot()); | |
| ASSERT_NE(nullptr, ax_root); | |
| BrowserAccessibilityWin* ax_child1 = | |
| ToBrowserAccessibilityWin(ax_root->PlatformGetChild(0)); | |
| ASSERT_NE(nullptr, ax_child1); | |
| BrowserAccessibilityWin* ax_child2 = | |
| ToBrowserAccessibilityWin(ax_root->PlatformGetChild(1)); | |
| ASSERT_NE(nullptr, ax_child2); | |
| LONG n_relations = 0; | |
| LONG n_targets = 0; | |
| LONG unique_id = 0; | |
| base::win::ScopedBstr relation_type; | |
| Microsoft::WRL::ComPtr<IAccessibleRelation> describedby_relation; | |
| Microsoft::WRL::ComPtr<IAccessibleRelation> description_for_relation; | |
| Microsoft::WRL::ComPtr<IUnknown> target; | |
| Microsoft::WRL::ComPtr<IAccessible2> ax_target; | |
| EXPECT_HRESULT_SUCCEEDED(ax_root->GetCOM()->get_nRelations(&n_relations)); | |
| EXPECT_EQ(1, n_relations); | |
| EXPECT_HRESULT_SUCCEEDED( | |
| ax_root->GetCOM()->get_relation(0, &describedby_relation)); | |
| EXPECT_HRESULT_SUCCEEDED( | |
| describedby_relation->get_relationType(relation_type.Receive())); | |
| EXPECT_EQ(L"describedBy", std::wstring(relation_type.Get())); | |
| relation_type.Reset(); | |
| EXPECT_HRESULT_SUCCEEDED(describedby_relation->get_nTargets(&n_targets)); | |
| EXPECT_EQ(2, n_targets); | |
| EXPECT_HRESULT_SUCCEEDED(describedby_relation->get_target(0, &target)); | |
| target.As(&ax_target); | |
| EXPECT_HRESULT_SUCCEEDED(ax_target->get_uniqueID(&unique_id)); | |
| EXPECT_EQ(-GetUniqueId(ax_child1), unique_id); | |
| ax_target.Reset(); | |
| target.Reset(); | |
| EXPECT_HRESULT_SUCCEEDED(describedby_relation->get_target(1, &target)); | |
| target.As(&ax_target); | |
| EXPECT_HRESULT_SUCCEEDED(ax_target->get_uniqueID(&unique_id)); | |
| EXPECT_EQ(-GetUniqueId(ax_child2), unique_id); | |
| ax_target.Reset(); | |
| target.Reset(); | |
| describedby_relation.Reset(); | |
| // Test the reverse relations. | |
| EXPECT_HRESULT_SUCCEEDED(ax_child1->GetCOM()->get_nRelations(&n_relations)); | |
| EXPECT_EQ(1, n_relations); | |
| EXPECT_HRESULT_SUCCEEDED( | |
| ax_child1->GetCOM()->get_relation(0, &description_for_relation)); | |
| EXPECT_HRESULT_SUCCEEDED( | |
| description_for_relation->get_relationType(relation_type.Receive())); | |
| EXPECT_EQ(L"descriptionFor", std::wstring(relation_type.Get())); | |
| relation_type.Reset(); | |
| EXPECT_HRESULT_SUCCEEDED(description_for_relation->get_nTargets(&n_targets)); | |
| EXPECT_EQ(1, n_targets); | |
| EXPECT_HRESULT_SUCCEEDED(description_for_relation->get_target(0, &target)); | |
| target.As(&ax_target); | |
| EXPECT_HRESULT_SUCCEEDED(ax_target->get_uniqueID(&unique_id)); | |
| EXPECT_EQ(-GetUniqueId(ax_root), unique_id); | |
| ax_target.Reset(); | |
| target.Reset(); | |
| description_for_relation.Reset(); | |
| EXPECT_HRESULT_SUCCEEDED(ax_child2->GetCOM()->get_nRelations(&n_relations)); | |
| EXPECT_EQ(1, n_relations); | |
| EXPECT_HRESULT_SUCCEEDED( | |
| ax_child2->GetCOM()->get_relation(0, &description_for_relation)); | |
| EXPECT_HRESULT_SUCCEEDED( | |
| description_for_relation->get_relationType(relation_type.Receive())); | |
| EXPECT_EQ(L"descriptionFor", std::wstring(relation_type.Get())); | |
| relation_type.Reset(); | |
| EXPECT_HRESULT_SUCCEEDED(description_for_relation->get_nTargets(&n_targets)); | |
| EXPECT_EQ(1, n_targets); | |
| EXPECT_HRESULT_SUCCEEDED(description_for_relation->get_target(0, &target)); | |
| target.As(&ax_target); | |
| EXPECT_HRESULT_SUCCEEDED(ax_target->get_uniqueID(&unique_id)); | |
| EXPECT_EQ(-GetUniqueId(ax_root), unique_id); | |
| ax_target.Reset(); | |
| target.Reset(); | |
| // Try adding one more relation. | |
| std::vector<int32_t> labelledby_ids = {3}; | |
| child1.AddIntListAttribute(ax::mojom::IntListAttribute::kLabelledbyIds, | |
| labelledby_ids); | |
| AXUpdatesAndEvents event_bundle; | |
| event_bundle.updates.resize(1); | |
| event_bundle.updates[0].nodes.push_back(child1); | |
| ASSERT_TRUE(manager->OnAccessibilityEvents(event_bundle)); | |
| EXPECT_HRESULT_SUCCEEDED(ax_child1->GetCOM()->get_nRelations(&n_relations)); | |
| EXPECT_EQ(2, n_relations); | |
| EXPECT_HRESULT_SUCCEEDED(ax_child2->GetCOM()->get_nRelations(&n_relations)); | |
| EXPECT_EQ(2, n_relations); | |
| } | |
| TEST_F(BrowserAccessibilityWinTest, UIACreateExtraAnnouncementNodesFails) { | |
| AXNodeData root_data; | |
| root_data.id = 1; | |
| root_data.role = ax::mojom::Role::kGenericContainer; | |
| root_data.child_ids = {2}; | |
| AXNodeData button; | |
| button.id = 2; | |
| button.role = ax::mojom::Role::kButton; | |
| std::unique_ptr<BrowserAccessibilityManager> manager( | |
| BrowserAccessibilityManager::Create( | |
| MakeAXTreeUpdateForTesting(root_data, button), node_id_delegate_, | |
| test_browser_accessibility_delegate_.get())); | |
| manager->FireAriaNotificationEvent( | |
| manager->GetFromID(2), "This is an announcement", | |
| ax::mojom::AriaNotificationPriority::kNormal, | |
| ax::mojom::AriaNotificationInterrupt::kNone, "" /*type */); | |
| AXTree* tree = const_cast<AXTree*>(manager->ax_tree()); | |
| ASSERT_FALSE(tree->extra_announcement_nodes()); | |
| BrowserAccessibilityWin* root_node = | |
| ToBrowserAccessibilityWin(manager->GetBrowserAccessibilityRoot()); | |
| EXPECT_EQ(1U, root_node->PlatformChildCount()); | |
| // Only the root node should not have the extra announcement nodes as | |
| // children. | |
| BrowserAccessibilityWin* button_node = | |
| ToBrowserAccessibilityWin(root_node->PlatformGetChild(0)); | |
| EXPECT_EQ(0U, button_node->PlatformChildCount()); | |
| } | |
| TEST_F(BrowserAccessibilityWinTest, CreateExtraAnnouncementNodes) { | |
| AXPlatform::GetInstance().DisableActiveUiaProvider(); | |
| // Create AXNodeData objects for a simple document tree. | |
| AXNodeData root_data; | |
| root_data.id = 1; | |
| root_data.role = ax::mojom::Role::kGenericContainer; | |
| root_data.child_ids = {2}; | |
| AXNodeData button; | |
| button.id = 2; | |
| button.role = ax::mojom::Role::kButton; | |
| button.SetName("Test Button"); | |
| // Construct a BrowserAccessibilityManager and fire an announcement. | |
| std::unique_ptr<BrowserAccessibilityManager> manager( | |
| BrowserAccessibilityManager::Create( | |
| MakeAXTreeUpdateForTesting(root_data, button), node_id_delegate_, | |
| test_browser_accessibility_delegate_.get())); | |
| manager->FireAriaNotificationEvent( | |
| manager->GetBrowserAccessibilityRoot(), "This is an announcement", | |
| ax::mojom::AriaNotificationPriority::kNormal, | |
| ax::mojom::AriaNotificationInterrupt::kNone, "" /*type */); | |
| AXTree* tree = const_cast<AXTree*>(manager->ax_tree()); | |
| EXPECT_EQ(2, tree->extra_announcement_nodes()->Count()); | |
| BrowserAccessibilityWin* root_node = | |
| ToBrowserAccessibilityWin(manager->GetBrowserAccessibilityRoot()); | |
| EXPECT_EQ(3U, root_node->PlatformChildCount()); | |
| // Only the root node should have the extra announcement nodes as children. | |
| BrowserAccessibilityWin* button_node = | |
| ToBrowserAccessibilityWin(root_node->PlatformGetChild(0)); | |
| EXPECT_EQ(0U, button_node->PlatformChildCount()); | |
| // Delete the manager and test that extra announcement nodes are properly | |
| // cleaned up. | |
| manager.reset(); | |
| // Construct a new manager with the same tree and test multiple updates. | |
| manager.reset(BrowserAccessibilityManager::Create( | |
| MakeAXTreeUpdateForTesting(root_data, button), node_id_delegate_, | |
| test_browser_accessibility_delegate_.get())); | |
| manager->FireAriaNotificationEvent( | |
| manager->GetBrowserAccessibilityRoot(), "another announcement", | |
| ax::mojom::AriaNotificationPriority::kNormal, | |
| ax::mojom::AriaNotificationInterrupt::kNone, "" /*type */); | |
| tree = const_cast<AXTree*>(manager->ax_tree()); | |
| EXPECT_EQ(2, tree->extra_announcement_nodes()->Count()); | |
| root_node = ToBrowserAccessibilityWin(manager->GetBrowserAccessibilityRoot()); | |
| EXPECT_EQ(3U, root_node->PlatformChildCount()); | |
| // Verify the polite announcement node contains the first announcement. | |
| BrowserAccessibility* polite_node = root_node->GetExtraAnnouncementNode( | |
| ax::mojom::AriaNotificationPriority::kNormal); | |
| ASSERT_NE(nullptr, polite_node); | |
| EXPECT_EQ(polite_node->GetNameAsString16(), u"another announcement"); | |
| // Fire a second announcement with high priority. | |
| manager->FireAriaNotificationEvent( | |
| manager->GetBrowserAccessibilityRoot(), "Urgent message!", | |
| ax::mojom::AriaNotificationPriority::kHigh, | |
| ax::mojom::AriaNotificationInterrupt::kNone, "" /*type */); | |
| // Verify the tree structure remains stable. | |
| EXPECT_EQ(2, tree->extra_announcement_nodes()->Count()); | |
| EXPECT_EQ(3U, root_node->PlatformChildCount()); | |
| BrowserAccessibility* assertive_node = root_node->GetExtraAnnouncementNode( | |
| ax::mojom::AriaNotificationPriority::kHigh); | |
| ASSERT_NE(nullptr, assertive_node); | |
| EXPECT_EQ(assertive_node->GetNameAsString16(), u"Urgent message!"); | |
| // Fire a third announcement to test that announcement nodes can be updated. | |
| manager->FireAriaNotificationEvent( | |
| manager->GetBrowserAccessibilityRoot(), "Updated announcement", | |
| ax::mojom::AriaNotificationPriority::kNormal, | |
| ax::mojom::AriaNotificationInterrupt::kNone, "" /*type */); | |
| polite_node = root_node->GetExtraAnnouncementNode( | |
| ax::mojom::AriaNotificationPriority::kNormal); | |
| ASSERT_NE(nullptr, polite_node); | |
| EXPECT_EQ(polite_node->GetNameAsString16(), u"Updated announcement"); | |
| // Test tree updates by adding a new child to the root. | |
| AXNodeData new_text; | |
| new_text.id = 3; | |
| new_text.role = ax::mojom::Role::kStaticText; | |
| new_text.SetName("New text node"); | |
| root_data.child_ids.push_back(3); // Add new child to root | |
| AXUpdatesAndEvents event_bundle; | |
| event_bundle.updates.resize(1); | |
| event_bundle.updates[0].nodes.push_back(root_data); | |
| event_bundle.updates[0].nodes.push_back(new_text); | |
| ASSERT_TRUE(manager->OnAccessibilityEvents(event_bundle)); | |
| // It is OK for announcement nodes to be cleared, but they should do so | |
| // without crashing or leaving dangling pointers. | |
| root_node = ToBrowserAccessibilityWin(manager->GetBrowserAccessibilityRoot()); | |
| EXPECT_EQ(2U, root_node->PlatformChildCount()); | |
| manager->FireAriaNotificationEvent( | |
| manager->GetBrowserAccessibilityRoot(), "Updated announcement", | |
| ax::mojom::AriaNotificationPriority::kNormal, | |
| ax::mojom::AriaNotificationInterrupt::kNone, "" /*type */); | |
| // Announcement nodes will be lazily constructed when they are next needed. | |
| EXPECT_EQ(4U, root_node->PlatformChildCount()); | |
| EXPECT_EQ(2, tree->extra_announcement_nodes()->Count()); | |
| // Ensure there is no issue re-creating the announcement nodes. | |
| polite_node = root_node->GetExtraAnnouncementNode( | |
| ax::mojom::AriaNotificationPriority::kNormal); | |
| ASSERT_NE(nullptr, polite_node); | |
| EXPECT_EQ(polite_node->GetNameAsString16(), u"Updated announcement"); | |
| // Delete the manager and test that all instances are cleaned up properly. | |
| manager.reset(); | |
| } | |
| TEST_F(BrowserAccessibilityWinTest, GetExtraAnnouncementNodes) { | |
| AXPlatform::GetInstance().DisableActiveUiaProvider(); | |
| AXNodeData root_data; | |
| root_data.id = 1; | |
| root_data.role = ax::mojom::Role::kGenericContainer; | |
| std::unique_ptr<BrowserAccessibilityManager> manager( | |
| BrowserAccessibilityManager::Create( | |
| MakeAXTreeUpdateForTesting(root_data), node_id_delegate_, | |
| test_browser_accessibility_delegate_.get())); | |
| AXTree* tree = const_cast<AXTree*>(manager->ax_tree()); | |
| tree->CreateExtraAnnouncementNodes(); | |
| ASSERT_TRUE(tree->extra_announcement_nodes()); | |
| EXPECT_EQ(2, tree->extra_announcement_nodes()->Count()); | |
| BrowserAccessibility* root_node = manager->GetBrowserAccessibilityRoot(); | |
| EXPECT_EQ(2U, root_node->PlatformChildCount()); | |
| BrowserAccessibility* assertive_node = root_node->GetExtraAnnouncementNode( | |
| ax::mojom::AriaNotificationPriority::kHigh); | |
| EXPECT_EQ(assertive_node->GetData().GetStringAttribute( | |
| ax::mojom::StringAttribute::kContainerLiveStatus), | |
| "assertive"); | |
| BrowserAccessibility* polite_node = root_node->GetExtraAnnouncementNode( | |
| ax::mojom::AriaNotificationPriority::kNormal); | |
| EXPECT_EQ(polite_node->GetData().GetStringAttribute( | |
| ax::mojom::StringAttribute::kContainerLiveStatus), | |
| "polite"); | |
| } | |
| TEST_F(BrowserAccessibilityWinTest, IA2AriaNotifyFallback) { | |
| AXPlatform::GetInstance().DisableActiveUiaProvider(); | |
| AXNodeData root_data; | |
| root_data.id = 1; | |
| root_data.role = ax::mojom::Role::kGenericContainer; | |
| root_data.child_ids = {2}; | |
| AXNodeData button; | |
| button.id = 2; | |
| button.role = ax::mojom::Role::kButton; | |
| std::unique_ptr<BrowserAccessibilityManager> manager( | |
| BrowserAccessibilityManager::Create( | |
| MakeAXTreeUpdateForTesting(root_data, button), node_id_delegate_, | |
| test_browser_accessibility_delegate_.get())); | |
| manager->FireAriaNotificationEvent( | |
| manager->GetBrowserAccessibilityRoot(), "This is an announcement", | |
| ax::mojom::AriaNotificationPriority::kNormal, | |
| ax::mojom::AriaNotificationInterrupt::kNone, "" /*type */); | |
| BrowserAccessibility* root_node = manager->GetBrowserAccessibilityRoot(); | |
| BrowserAccessibility* polite_node = root_node->GetExtraAnnouncementNode( | |
| ax::mojom::AriaNotificationPriority::kNormal); | |
| EXPECT_EQ(polite_node->GetData().GetStringAttribute( | |
| ax::mojom::StringAttribute::kContainerLiveStatus), | |
| "polite"); | |
| EXPECT_EQ(polite_node->GetNameAsString16(), u"This is an announcement"); | |
| } | |
| TEST_F(BrowserAccessibilityWinTest, PlatformGetChild) { | |
| AXPlatform::GetInstance().DisableActiveUiaProvider(); | |
| AXNodeData root_data; | |
| root_data.id = 1; | |
| root_data.role = ax::mojom::Role::kGenericContainer; | |
| root_data.child_ids = {2}; | |
| AXNodeData button; | |
| button.id = 2; | |
| button.role = ax::mojom::Role::kButton; | |
| std::unique_ptr<BrowserAccessibilityManager> manager( | |
| BrowserAccessibilityManager::Create( | |
| MakeAXTreeUpdateForTesting(root_data, button), node_id_delegate_, | |
| test_browser_accessibility_delegate_.get())); | |
| AXTree* tree = const_cast<AXTree*>(manager->ax_tree()); | |
| tree->CreateExtraAnnouncementNodes(); | |
| ASSERT_TRUE(tree->extra_announcement_nodes()); | |
| EXPECT_EQ(2, tree->extra_announcement_nodes()->Count()); | |
| BrowserAccessibilityWin* root_node = | |
| ToBrowserAccessibilityWin(manager->GetBrowserAccessibilityRoot()); | |
| EXPECT_EQ(3U, root_node->PlatformChildCount()); | |
| BrowserAccessibilityWin* button_node = | |
| ToBrowserAccessibilityWin(root_node->PlatformGetChild(0)); | |
| EXPECT_EQ(button_node->GetData().id, 2); | |
| EXPECT_EQ(button_node->GetData().role, ax::mojom::Role::kButton); | |
| BrowserAccessibilityWin* assertive_node = | |
| ToBrowserAccessibilityWin(root_node->PlatformGetChild(1)); | |
| EXPECT_EQ(assertive_node->GetData().id, -1); | |
| EXPECT_EQ(assertive_node->GetData().GetStringAttribute( | |
| ax::mojom::StringAttribute::kContainerLiveStatus), | |
| "assertive"); | |
| BrowserAccessibilityWin* polite_node = | |
| ToBrowserAccessibilityWin(root_node->PlatformGetChild(2)); | |
| EXPECT_EQ(polite_node->GetData().id, -2); | |
| EXPECT_EQ(polite_node->GetData().GetStringAttribute( | |
| ax::mojom::StringAttribute::kContainerLiveStatus), | |
| "polite"); | |
| EXPECT_EQ(nullptr, root_node->PlatformGetChild(3)); | |
| } | |
| TEST_F(BrowserAccessibilityWinTest, PlatformGetLastChild) { | |
| AXPlatform::GetInstance().DisableActiveUiaProvider(); | |
| AXNodeData root_data; | |
| root_data.id = 1; | |
| root_data.role = ax::mojom::Role::kGenericContainer; | |
| root_data.child_ids = {2}; | |
| AXNodeData button; | |
| button.id = 2; | |
| button.role = ax::mojom::Role::kButton; | |
| std::unique_ptr<BrowserAccessibilityManager> manager( | |
| BrowserAccessibilityManager::Create( | |
| MakeAXTreeUpdateForTesting(root_data, button), node_id_delegate_, | |
| test_browser_accessibility_delegate_.get())); | |
| BrowserAccessibilityWin* root_node = | |
| ToBrowserAccessibilityWin(manager->GetBrowserAccessibilityRoot()); | |
| EXPECT_EQ(1U, root_node->PlatformChildCount()); | |
| BrowserAccessibilityWin* last_child = | |
| ToBrowserAccessibilityWin(root_node->PlatformGetLastChild()); | |
| EXPECT_EQ(last_child->GetData().id, 2); | |
| EXPECT_EQ(last_child->GetData().role, ax::mojom::Role::kButton); | |
| AXTree* tree = const_cast<AXTree*>(manager->ax_tree()); | |
| tree->CreateExtraAnnouncementNodes(); | |
| ASSERT_TRUE(tree->extra_announcement_nodes()); | |
| EXPECT_EQ(2, tree->extra_announcement_nodes()->Count()); | |
| EXPECT_EQ(3U, root_node->PlatformChildCount()); | |
| last_child = ToBrowserAccessibilityWin(root_node->PlatformGetLastChild()); | |
| EXPECT_EQ(last_child->GetData().id, -2); | |
| EXPECT_EQ(last_child->GetData().GetStringAttribute( | |
| ax::mojom::StringAttribute::kContainerLiveStatus), | |
| "polite"); | |
| } | |
| TEST_F(BrowserAccessibilityWinTest, PlatformGetSiblings) { | |
| AXPlatform::GetInstance().DisableActiveUiaProvider(); | |
| AXNodeData root_data; | |
| root_data.id = 1; | |
| root_data.role = ax::mojom::Role::kGenericContainer; | |
| root_data.child_ids = {2}; | |
| AXNodeData button; | |
| button.id = 2; | |
| button.role = ax::mojom::Role::kButton; | |
| std::unique_ptr<BrowserAccessibilityManager> manager( | |
| BrowserAccessibilityManager::Create( | |
| MakeAXTreeUpdateForTesting(root_data, button), node_id_delegate_, | |
| test_browser_accessibility_delegate_.get())); | |
| BrowserAccessibilityWin* root_node = | |
| ToBrowserAccessibilityWin(manager->GetBrowserAccessibilityRoot()); | |
| EXPECT_EQ(1U, root_node->PlatformChildCount()); | |
| BrowserAccessibilityWin* button_node = | |
| ToBrowserAccessibilityWin(root_node->PlatformGetChild(0)); | |
| EXPECT_EQ(button_node->GetData().id, 2); | |
| EXPECT_EQ(button_node->GetData().role, ax::mojom::Role::kButton); | |
| EXPECT_EQ(nullptr, button_node->PlatformGetNextSibling()); | |
| EXPECT_EQ(nullptr, button_node->PlatformGetPreviousSibling()); | |
| AXTree* tree = const_cast<AXTree*>(manager->ax_tree()); | |
| tree->CreateExtraAnnouncementNodes(); | |
| ASSERT_TRUE(tree->extra_announcement_nodes()); | |
| EXPECT_EQ(2, tree->extra_announcement_nodes()->Count()); | |
| EXPECT_EQ(3U, root_node->PlatformChildCount()); | |
| BrowserAccessibilityWin* assertive_node = | |
| ToBrowserAccessibilityWin(button_node->PlatformGetNextSibling()); | |
| ASSERT_NE(nullptr, assertive_node); | |
| EXPECT_EQ(assertive_node->GetData().id, -1); | |
| EXPECT_EQ(assertive_node->GetData().GetStringAttribute( | |
| ax::mojom::StringAttribute::kContainerLiveStatus), | |
| "assertive"); | |
| EXPECT_EQ(button_node, assertive_node->PlatformGetPreviousSibling()); | |
| BrowserAccessibilityWin* polite_node = | |
| ToBrowserAccessibilityWin(assertive_node->PlatformGetNextSibling()); | |
| ASSERT_NE(nullptr, polite_node); | |
| EXPECT_EQ(polite_node->GetData().id, -2); | |
| EXPECT_EQ(polite_node->GetData().GetStringAttribute( | |
| ax::mojom::StringAttribute::kContainerLiveStatus), | |
| "polite"); | |
| EXPECT_EQ(assertive_node, polite_node->PlatformGetPreviousSibling()); | |
| EXPECT_EQ(nullptr, polite_node->PlatformGetNextSibling()); | |
| } | |
| TEST_F(BrowserAccessibilityWinTest, OnExtendedPropertiesUsedAfterDestruction) { | |
| // Create a simple tree with a text field that has extended properties. | |
| AXNodeData text_field; | |
| text_field.id = 2; | |
| text_field.role = ax::mojom::Role::kTextField; | |
| text_field.AddStringAttribute(ax::mojom::StringAttribute::kDescription, | |
| "Test description"); | |
| text_field.AddIntAttribute(ax::mojom::IntAttribute::kTextSelStart, 0); | |
| text_field.AddIntAttribute(ax::mojom::IntAttribute::kTextSelEnd, 5); | |
| AXNodeData root; | |
| root.id = 1; | |
| root.role = ax::mojom::Role::kRootWebArea; | |
| root.child_ids.push_back(2); | |
| // Create the manager and get the text field node. | |
| std::unique_ptr<BrowserAccessibilityManager> manager( | |
| BrowserAccessibilityManager::Create( | |
| MakeAXTreeUpdateForTesting(root, text_field), node_id_delegate_, | |
| test_browser_accessibility_delegate_.get())); | |
| BrowserAccessibilityComWin* text_field_node = | |
| ToBrowserAccessibilityComWin(manager->GetFromID(2)); | |
| ASSERT_NE(nullptr, text_field_node); | |
| // Test that methods work normally before destruction | |
| // get_endIndex method calls OnExtendedPropertiesUsed with IsDestroyed() | |
| // check. | |
| LONG end_index = -1; | |
| EXPECT_HRESULT_SUCCEEDED(text_field_node->get_endIndex(&end_index)); | |
| // get_description method calls OnExtendedPropertiesUsed with IsDestroyed() | |
| // check. | |
| base::win::ScopedBstr description; | |
| EXPECT_HRESULT_SUCCEEDED( | |
| text_field_node->get_description(description.Receive())); | |
| // For testing only. This method clears the delegate pointer to simulate | |
| // destruction. | |
| auto* pre_delegate = text_field_node->SetDelegateForTesting(nullptr); | |
| // These calls should not crash even though the object is destroyed | |
| // and OnExtendedPropertiesUsed is called with IsDestroyed() check | |
| // in get_endIndex and get_description methods. | |
| // Test get_endIndex after destruction (calls OnExtendedPropertiesUsed) | |
| // This should fail gracefully without crashing. | |
| end_index = -1; | |
| EXPECT_HRESULT_FAILED(text_field_node->get_endIndex(&end_index)); | |
| // Test get_description after destruction (calls OnExtendedPropertiesUsed) | |
| // This should also fail gracefully without crashing. | |
| description.Reset(); | |
| EXPECT_HRESULT_FAILED( | |
| text_field_node->get_description(description.Receive())); | |
| // Additional test: Try other methods that call OnExtendedPropertiesUsed | |
| // with proper IsDestroyed() checks to ensure they handle destruction | |
| // correctly. | |
| // nActions method has IsDestroyed() check before OnExtendedPropertiesUsed. | |
| LONG n_actions = -1; | |
| EXPECT_HRESULT_FAILED(text_field_node->nActions(&n_actions)); | |
| text_field_node->SetDelegateForTesting(pre_delegate); | |
| } | |
| } // namespace ui |